core

package
v0.4.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 31, 2026 License: MIT Imports: 9 Imported by: 0

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

View Source
const Ellipsis = "…" // …

Ellipsis is the single-rune horizontal ellipsis appended to truncated text.

View Source
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.

View Source
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.

View Source
const SlugMaxRunes = 80

SlugMaxRunes is the cap Slugify applies to its output.

View Source
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.

View Source
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

MemoryKinds lists every valid kind, in briefing-priority-ish order.

View Source
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.

SessionStatuses lists every valid session status.

TaskStatuses lists every valid task status.

Functions

func FormatTime

func FormatTime(t time.Time) string

FormatTime renders t as a canonical UTC timestamp string.

func NewID

func NewID() (string, error)

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

func ParseStageHeader(body string) (status, gate string)

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

func ParseTime(s string) (time.Time, error)

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

func Slugify(s string) string

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

func StageStatusLive(status string) bool

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

func TruncateWords(s string, maxRunes int) string

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

func ValidSessionSource(s string) bool

ValidSessionSource reports whether s is a recognized session source.

func WikiLinkName

func WikiLinkName(ref string) string

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(body string) []string

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.

func (Memory) Active

func (m Memory) Active() bool

Active reports whether the memory is still valid (not superseded or archived). Inactive memories leave the briefing/prompt/recall indexes but remain readable.

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.

func (Project) Retired

func (p Project) Retired() bool

Retired reports whether the project has been retired (emptied by a split).

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.

func (Session) LiveAsOf

func (s Session) LiveAsOf(now time.Time, ttl time.Duration) bool

LiveAsOf reports whether an active session counts as live at now: still active and updated within ttl (<= 0 falls back to SessionIdleTTL). Completed/expired sessions are never live.

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).

func (Task) ClaimLive

func (t Task) ClaimLive(now time.Time) bool

ClaimLive reports whether the task is actively claimed as of now: in_progress, a non-empty holder, and an unexpired lease. An expired lease is not live (the task is reclaimable), matching ClaimTask's lazy expiry.

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"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL