Documentation
¶
Overview ¶
Package core holds Seamless domain types shared across packages: Project, Memory, Session, Task, Trial, Event, and their enums. It has no dependencies on store, config, or any I/O -- pure data plus small helpers.
Index ¶
- Constants
- Variables
- func FormatTime(t time.Time) string
- func NewID() (string, error)
- func ParseStageHeader(body string) (status, gate string)
- func ParseTime(s string) (time.Time, error)
- func Slugify(s string) string
- func StageStatusLive(status string) bool
- func TruncateWords(s string, maxRunes int) string
- func ValidSessionSource(s string) bool
- func WikiLinkName(ref string) string
- func WikiLinks(body string) []string
- type Event
- type EventKind
- type Memory
- type MemoryKind
- type Note
- type Project
- type Session
- type SessionStatus
- type Task
- type TaskStatus
- type TokenUsage
- type Trial
- type TrialOutcome
Constants ¶
const Ellipsis = "…" // …
Ellipsis is the single-rune horizontal ellipsis appended to truncated text.
const FindingNoSummary = "(auto) session ended, no summary harvested"
FindingNoSummary is the sentinel findings value recorded when a session ends with nothing to harvest (missing/empty/unreadable transcript). It marks the session ended-without-a-summary rather than leaving findings blank -- but it carries no knowledge, so the briefing's findings queries exclude it: a content-free line is not worth an agent's context window.
const SessionIdleTTL = 45 * time.Minute
SessionIdleTTL is the default no-activity age beyond which an active session is considered dead: heartbeats (MCP tool calls for bound sessions, the ambient hooks for cc/* or cx/* sessions) bump updated_at, and anything quiet past this is reaped to SessionExpired and shown as idle in the console. It is the canonical liveness threshold shared by the gardener reaper and the console; it must comfortably exceed a long single agent turn so live work is never reaped. gardener.session_idle_minutes overrides it; every consumer takes the configured duration and falls back to this const when given <= 0.
const SlugMaxRunes = 80
SlugMaxRunes is the cap Slugify applies to its output.
const StageStatusDone = "done"
StageStatusDone marks a completed stage: it leaves the briefing immediately and the gardener may propose archiving it once it has sat unchanged.
const TimeFormat = "2006-01-02T15:04:05.000000000Z07:00"
TimeFormat is the canonical timestamp layout for all TEXT timestamp columns. It is fixed-width in UTC (always 9 fractional digits, always "Z"), so string comparison of stored timestamps matches chronological order.
Variables ¶
var MemoryKinds = []MemoryKind{ KindConstraint, KindConvention, KindRunbook, KindProtocol, KindGotcha, KindDecision, KindRefuted, KindReference, KindStage, }
MemoryKinds lists every valid kind, in briefing-priority-ish order.
var SessionSources = []string{"startup", "resume", "clear", "compact", "explicit"}
SessionSources lists every valid Session.Source. Callers must not invent values: retrieve/briefing.go branches on "compact" and "resume", so a near-miss like "compacted" silently yields the wrong briefing shape.
var SessionStatuses = []SessionStatus{SessionActive, SessionCompleted, SessionExpired}
SessionStatuses lists every valid session status.
var TaskStatuses = []TaskStatus{TaskOpen, TaskInProgress, TaskDone, TaskDropped}
TaskStatuses lists every valid task status.
Functions ¶
func FormatTime ¶
FormatTime renders t as a canonical UTC timestamp string.
func NewID ¶
NewID returns a new lexicographically-sortable ULID string. It uses crypto/rand for entropy and returns an error rather than panicking, so it is safe on request paths (never use ulid.MustNew -- see AGENTS.md).
func ParseStageHeader ¶ added in v0.3.1
ParseStageHeader extracts the Status and Gate values from a stage memory body. The convention: the body opens with "Status: open|in_progress|blocked|done" and "Gate: human|ai" lines (case-insensitive, order-independent, leading markdown like "- ", "**", or "## " tolerated). Missing fields come back "".
func ParseTime ¶
ParseTime parses a canonical timestamp. An empty string yields the zero time. It also accepts plain RFC3339 for values written by other tools (e.g. v1 data during import).
func Slugify ¶
Slugify turns arbitrary text into a filesystem- and URL-safe lowercase slug: lowercase, letter/number runs joined by single dashes, trimmed and capped at SlugMaxRunes runes. Empty input yields "untitled".
It is the ONE slugifier: every derived slug (project slugs from a repo dir or project_create, note slugs from notes_create/capture_url, plan slugs from a captured plan title, imported memory names) flows through it, so the same title yields the same slug whatever the entry path.
Letters and numbers are kept per Unicode, not ASCII, so a non-Latin title slugs to itself rather than collapsing to "untitled" -- which matters because note slugs become filenames and notes_create does not disambiguate a collision. The cap is in runes, so it can never split a multi-byte rune.
func StageStatusLive ¶ added in v0.3.1
StageStatusLive reports whether a parsed stage status marks a live gate: open, in_progress, or blocked. Done, absent, and unrecognized statuses are all non-live -- the briefing ages such stages out of pinning and the gardener proposes archiving them, so a stage only holds its permanent pin by actually carrying a gate.
func TruncateWords ¶ added in v0.3.1
TruncateWords caps s at maxRunes runes, cutting on a word boundary and appending an ellipsis so the result never ends mid-word. The returned string, ellipsis included, is at most maxRunes runes. When s already fits it is returned unchanged. A single leading token longer than the budget has no boundary to honor, so it falls back to a hard rune cut. maxRunes <= 1 disables truncation (there is no room for content plus an ellipsis).
func ValidSessionSource ¶
ValidSessionSource reports whether s is a recognized session source.
func WikiLinkName ¶
WikiLinkName normalizes a [[...]] inner reference to a bare memory name: a "project/name" reference keeps the last segment, and a trailing "|alias" or "#anchor" is dropped. It is the shared normalization for every wiki-link consumer: WikiLinks here, and the markdown renderer's goldmark extension (internal/markdown), which does its own [[...]] parsing but defers to this for the name.
func WikiLinks ¶
WikiLinks returns the referenced memory names in body's [[...]] links, normalized to the bare name (a "project/name" reference keeps the last segment; a trailing "|alias" or "#anchor" is dropped). Duplicates are removed, order of first appearance preserved. Returns nil when there are no links.
Types ¶
type Event ¶
type Event struct {
ID string `json:"id"`
TS time.Time `json:"ts"`
Kind EventKind `json:"kind"`
SessionID string `json:"sessionId,omitempty"`
ProjectSlug string `json:"projectSlug,omitempty"`
ItemID string `json:"itemId,omitempty"`
Payload map[string]any `json:"payload,omitempty"`
}
Event is one entry in the append-only log. Payload carries kind-specific detail (e.g. which memory names were injected).
type EventKind ¶
type EventKind string
EventKind identifies a kind of logged event. The append-only event log is the source for telemetry, retrieval stats, and the console feed.
const ( EventSessionStarted EventKind = "session.started" EventSessionEnded EventKind = "session.ended" EventMemoryWritten EventKind = "memory.written" EventMemoryRead EventKind = "memory.read" EventMemorySuperseded EventKind = "memory.superseded" EventMemoryArchived EventKind = "memory.archived" EventMemoryMoved EventKind = "memory.moved" // relocated to another project (reproject/split) EventRepoMoved EventKind = "repo.moved" // a moved repo's new path adopted its existing project (payload: slug, new_path, old_paths) EventFavoriteChanged EventKind = "favorite.changed" // an item was starred/unstarred (payload: kind, id, name, favorite, by) EventNoteWritten EventKind = "note.written" EventTrialRecorded EventKind = "trial.recorded" EventTaskTransition EventKind = "task.transition" EventInjected EventKind = "retrieval.injected" EventGardenerAction EventKind = "gardener.action" EventToolCall EventKind = "tool.call" // MCP tool invocation, logged live by the middleware (also the shape used by import) EventHookPrompt EventKind = "hook.prompt" // a UserPromptSubmit that matched no memory (recall miss) EventRecallMiss EventKind = "recall.miss" // a recall tool call that returned zero hits (payload: query, scope, limit, source) EventHookError EventKind = "hook.error" // a hook-stage failure swallowed fail-open (payload: stage, error, client) EventAgentMishap EventKind = "agent.mishap" // a mishap the agent self-reported at session_end (payload: description, plus item_ids of the memories the text names) // Claude Code plan-mode capture (PostToolUse/PermissionRequest/SubagentStop hooks). EventPlanCaptured EventKind = "plan.captured" // a plan-file iteration landed as a cc-plan note EventPlanPresented EventKind = "plan.presented" // the user was prompted to review the plan EventPlanApproved EventKind = "plan.approved" // the user approved the plan (ExitPlanMode) EventSubagentCaptured EventKind = "subagent.captured" // a planning subagent's prompt+report landed as a cc-agent note )
type Memory ¶
type Memory struct {
ID string `json:"id"`
Kind MemoryKind `json:"kind"`
Name string `json:"name"`
Description string `json:"description"` // <=150 chars; the only text shown in indexes
Project string `json:"project"` // empty = global
Body string `json:"body"`
FilePath string `json:"filePath"`
Tags []string `json:"tags"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
ValidFrom time.Time `json:"validFrom"`
InvalidAt *time.Time `json:"invalidAt"` // nil = still valid
SupersededBy string `json:"supersededBy"` // ULID of the replacement, "" = none
SourceSession string `json:"sourceSession"` // provenance
Model string `json:"model"` // model that produced the content, as the provider names it (e.g. "claude-fable-5")
Favorite bool `json:"favorite,omitempty"` // owner/agent star; frontmatter is authoritative, never bumps Updated
ContentHash string `json:"contentHash"`
// Extra preserves unknown frontmatter keys (e.g. Obsidian plugin fields) so
// a parse -> render round-trip is lossless. Not mirrored to the index.
Extra map[string]any `json:"extra,omitempty"`
}
Memory is a single durable knowledge item, stored one-per-file with YAML frontmatter; this struct mirrors that frontmatter plus the body.
type MemoryKind ¶
type MemoryKind string
MemoryKind classifies a memory. It is the frontmatter `kind` field and is pinned/filtered differently per kind during briefing assembly.
const ( KindConstraint MemoryKind = "constraint" // hard rule that must hold on any task KindConvention MemoryKind = "convention" // project-local choice or layout fact KindRunbook MemoryKind = "runbook" // procedure to follow KindProtocol MemoryKind = "protocol" // interaction/coordination contract KindGotcha MemoryKind = "gotcha" // surprising pitfall KindDecision MemoryKind = "decision" // a choice and its rationale KindRefuted MemoryKind = "refuted" // a claim investigated and found false KindReference MemoryKind = "reference" // a durable pointer/fact KindStage MemoryKind = "stage" // a gated stage with status + gate lines )
func (MemoryKind) Valid ¶
func (k MemoryKind) Valid() bool
Valid reports whether k is a recognized memory kind.
type Note ¶
type Note struct {
ID string `json:"id"`
Title string `json:"title"`
Slug string `json:"slug"`
Description string `json:"description"`
Project string `json:"project"` // empty = global
Body string `json:"body"`
FilePath string `json:"filePath"`
Tags []string `json:"tags"`
SourceURL string `json:"sourceUrl"`
Model string `json:"model"` // model that produced the content, as the provider names it (e.g. "claude-fable-5")
Favorite bool `json:"favorite,omitempty"` // owner/agent star; frontmatter is authoritative, never bumps Updated
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
ContentHash string `json:"contentHash"`
// Extra preserves unknown frontmatter keys for a lossless round-trip.
Extra map[string]any `json:"extra,omitempty"`
}
Note is a work artifact (research finding, decision record, meeting summary), stored one-per-file with YAML frontmatter; this struct mirrors that frontmatter plus the body. Unlike a Memory it has no lifecycle/validity.
type Project ¶
type Project struct {
ID string `json:"id"`
Slug string `json:"slug"`
Name string `json:"name"`
Description string `json:"description"`
ParentSlug string `json:"parentSlug,omitempty"`
RetiredAt *time.Time `json:"retiredAt,omitempty"`
Favorite bool `json:"favorite,omitempty"` // owner/agent star; never bumps UpdatedAt
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
Project groups memories, notes, sessions, tasks, and trials under a slug.
ParentSlug, when set, points a child project (e.g. arctop-ios) at a shared parent (e.g. arctop-mobile-apps) whose active memories are injected into the child's briefing -- how a split keeps cross-platform knowledge shared without duplicating it. RetiredAt marks a project emptied by a split: kept for provenance and still readable, but flagged as no longer the live home.
type Session ¶
type Session struct {
ID string `json:"id"`
Name string `json:"name"`
ProjectSlug string `json:"projectSlug"`
Status SessionStatus `json:"status"`
Findings string `json:"findings"`
// ExternalSessionID is the client's own session id (Claude Code's session_id,
// Codex's session id, ...) -- the key a client's SessionEnd/Stop hook uses to
// find the ambient session it owns. The DB column, the session metadata key,
// and the event payload key all remain "claude_session_id" for historical
// continuity (it long predated Codex); only this Go field was generalized.
ExternalSessionID string `json:"externalSessionId"`
// ExternalClient disambiguates identical external session ids issued by
// different agent clients. It is the authoritative client half of an ambient
// session identity; Name remains a human-readable display handle.
ExternalClient string `json:"externalClient"`
CWD string `json:"cwd"`
Source string `json:"source"` // startup|resume|compact|clear|explicit
// Model is the LLM currently powering the session's agent, stored verbatim
// as the provider names it ("claude-fable-5", "gpt-5.5"). Sources, in
// arrival order: the session_start tool's model arg, the Codex hook
// payloads' model field, or a tail-sniff of the Claude Code transcript.
// Updated in place when the agent switches models, and stamped onto
// memories/notes at write time.
Model string `json:"model"`
Ambient bool `json:"ambient"`
Favorite bool `json:"favorite,omitempty"` // owner/agent star; never bumps UpdatedAt
// Tokens is the real model token consumption harvested from the agent's
// transcript (Claude Code at SessionEnd, Codex every Stop) -- the actual burn,
// not the injection estimate. Zero until harvested, or for a client with no
// transcript token record. See TokenUsage.
Tokens TokenUsage `json:"tokens"`
Metadata map[string]any `json:"metadata"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
Session is one agent work session. Ambient sessions are created by a hook (readable {cc|cx}/ names with a stable full-id digest); explicit ones by session_start.
type SessionStatus ¶
type SessionStatus string
SessionStatus is the lifecycle state of an agent session.
const ( SessionActive SessionStatus = "active" SessionCompleted SessionStatus = "completed" // SessionExpired is a session the reaper closed after it went idle past // SessionIdleTTL without a graceful session_end (a crashed/killed agent, or an // explicit session_start whose agent never called session_end). Distinct from // completed so the console can tell a harvested session from an abandoned one. SessionExpired SessionStatus = "expired" )
func (SessionStatus) Valid ¶
func (s SessionStatus) Valid() bool
Valid reports whether s is a recognized session status.
type Task ¶
type Task struct {
ID string `json:"id"`
ProjectSlug string `json:"projectSlug"`
Title string `json:"title"`
Body string `json:"body"`
Status TaskStatus `json:"status"`
CreatedBy string `json:"createdBy"`
PlanSlug string `json:"planSlug,omitempty"`
ClaimedBy string `json:"claimedBy,omitempty"`
LeaseExpiresAt *time.Time `json:"leaseExpiresAt,omitempty"`
Favorite bool `json:"favorite,omitempty"` // owner/agent star; never bumps UpdatedAt
DependsOn []string `json:"dependsOn,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ClosedAt *time.Time `json:"closedAt,omitempty"`
}
Task is a unit of work with optional dependency edges. It is "ready" when all of its dependencies are closed.
PlanSlug composes a task into a plan (see the plan:<slug> convention): a non-empty PlanSlug marks the task as a plan step, excluded from the default ready-queue but surfaced under a plan filter. ClaimedBy holds the session ULID that currently owns the task (empty when unclaimed); LeaseExpiresAt is when that claim lapses, after which the task is claimable again (lazy expiry, no sweeper).
type TaskStatus ¶
type TaskStatus string
TaskStatus is the state of a task in the ready-queue.
const ( TaskOpen TaskStatus = "open" TaskInProgress TaskStatus = "in_progress" TaskDone TaskStatus = "done" TaskDropped TaskStatus = "dropped" )
func (TaskStatus) Closed ¶
func (s TaskStatus) Closed() bool
Closed reports whether the status is terminal (done or dropped).
func (TaskStatus) Valid ¶
func (s TaskStatus) Valid() bool
Valid reports whether s is a recognized task status.
type TokenUsage ¶ added in v0.3.9
type TokenUsage struct {
Input int `json:"input"` // fresh (uncached) input processed
Cached int `json:"cached"` // input served from the prompt cache (read)
CacheCreation int `json:"cacheCreation"` // input written to the prompt cache (Claude Code only)
Output int `json:"output"` // tokens the model produced
Total int `json:"total"` // Input + Cached + CacheCreation + Output
}
TokenUsage is a session's cumulative model token consumption, normalized across agent clients so a per-project sum is meaningful. Every field is an absolute cumulative total for the session -- OVERWRITTEN on each harvest, never accumulated -- so re-harvesting a resumed or compacted session's grown transcript cannot double-count.
Claude Code fills all four leaf fields from the per-message usage blocks (input and cache-read/cache-creation are distinct there). Codex has no cache-creation notion, so CacheCreation is always 0 and Cached carries its cached_input_tokens. Total is computed the same way for both (Input+Cached+CacheCreation+Output), so the number is comparable regardless of which client produced it.
func (TokenUsage) Empty ¶ added in v0.3.9
func (t TokenUsage) Empty() bool
Empty reports whether no token usage has been harvested (all fields zero).
func (*TokenUsage) Normalize ¶ added in v0.3.9
func (t *TokenUsage) Normalize()
Normalize sets Total to the sum of the leaf fields, the single definition of "total tokens" shared by both clients.
type Trial ¶
type Trial struct {
ID string `json:"id"`
Lab string `json:"lab"`
Title string `json:"title"`
Changes string `json:"changes"`
Expected string `json:"expected"`
Actual string `json:"actual"`
Outcome TrialOutcome `json:"outcome"`
Metrics map[string]any `json:"metrics"`
SessionID string `json:"sessionId"`
ProjectSlug string `json:"projectSlug"`
Favorite bool `json:"favorite,omitempty"` // owner/agent star
CreatedAt time.Time `json:"createdAt"`
}
Trial records one expected-vs-actual experiment inside a lab, with optional structured metrics for native querying.
type TrialOutcome ¶
type TrialOutcome string
TrialOutcome summarizes a trial result. Free-form by design (the store does not constrain it); these are the conventional values.
const ( OutcomePass TrialOutcome = "pass" OutcomeFail TrialOutcome = "fail" OutcomePartial TrialOutcome = "partial" OutcomeInconclusive TrialOutcome = "inconclusive" )