Documentation
¶
Overview ¶
Package transcript is the cross-repo SSOT for the agent-session transcript domain: one domain model (Transcript/Turn/Block), one Source port with an ACL adapter per runtime (claude jsonl / codex jsonl / deepwork-native jsonl), one aggregator, and the touched-files domain service (ExtractTouched).
Terminal abstraction (SSOT-SESSION-LOADER §6): the agent runtime OWNS its transcript; this package is a read-only viewer/normalizer. Adding a runtime = adding a Source adapter; the domain model, aggregator, touched service and every downstream consumer are unchanged. Soft-delete/rename live in the host's own HiddenStore overlay and never touch the runtime transcript (SSOT protection).
Consumers: a web UI host (read + native-write sides) and deepwork-terminal both import this package instead of re-declaring the model + parsers — collapsing three hand-synced copies into one. Pure stdlib; the DB coupling is inverted via the DeepworkSessionProvider / HiddenStore interfaces.
Package transcript — roots.go: the SINGLE answer to "where does this runtime keep its files on disk".
Every layer that reads a CLI runtime's on-disk state (session lists, agent trees, token usage, quota snapshots, presence probes) needs the same two directories. Resolving them independently is how a host ends up reading one ~/.claude for quota and a different one for spend: kit/usage once hardcoded ~/.claude/projects while this package honoured DW_CLAUDE_PROJECTS, so a CLAUDE_CONFIG_DIR user got two different truths. One resolver, no drift.
Env precedence (per runtime, most specific first):
claude: DW_CLAUDE_PROJECTS (exact projects dir) → CLAUDE_CONFIG_DIR/projects → ~/.claude/projects codex: DW_CODEX_HOME (exact home) → ~/.codex
CLAUDE_CONFIG_DIR is the claude CLI's own knob, so honouring it means we look where the user's CLI actually lives. DW_* are ours, and win because they are the explicit override.
Package transcript — scan.go: the shared primitives for FINDING transcript files and READING the part of them you actually need.
Both runtimes store JSONL that grows without bound (a single codex rollout reaches tens of MB). Most consumers do not need the whole file:
- "is there any transcript here at all?" → HasAnyFile (stops at the first hit)
- "what are the most recent sessions?" → NewestFiles (bounded, newest-first)
- "what is the latest X the runtime recorded?" → ScanTail (reads only the end)
Before these existed, each consumer wrote its own walker and its own full-file read; the quota probe re-read a 12 MB rollout every 60 seconds to find one object near its end.
Package sessionsource is the CHG-014 Runtime-SSOT session aggregation layer.
Core principle (SSOT-SESSION-LOADER.md §0): the agent runtime owns the session transcript; deepwork is a viewer/orchestrator. A SessionSource reads a runtime's own storage (claude jsonl, codex history, deepwork DB) and never writes or deletes it. Soft-delete (hidden) lives only in deepwork's own table and merely filters the aggregated list — the runtime transcript is untouched.
Terminal abstraction (§6): one interface + many implementations + one aggregator. Adding a new runtime = adding a Source; the aggregator is unchanged.
Index ¶
- Constants
- Variables
- func ClaudeHome() string
- func ClaudeProjectsRoot() string
- func CodexHome() string
- func CodexSessionsRoot() string
- func EncodeProjectDir(projectDir string) string
- func HasAnyFile(root, suffix string) bool
- func NewestFiles(root, prefix, suffix string, n int) []string
- func ScanTail(path string, tailBytes int64, fn func(line []byte) bool) error
- func TouchedPath(toolName string, toolInput map[string]interface{}) (string, bool)
- type AgentRun
- type Aggregator
- func (a *Aggregator) CountByDir(ctx context.Context) map[string]int
- func (a *Aggregator) List(ctx context.Context, projectDir string, includeHidden bool) (*ListResult, error)
- func (a *Aggregator) LoadTranscript(ctx context.Context, source string, ref SessionRef) (*Transcript, error)
- func (a *Aggregator) LoadTranscriptWindow(ctx context.Context, source string, ref SessionRef, req WindowRequest) (*WindowResult, error)
- func (a *Aggregator) SourceByKind(kind string) SessionSource
- type Block
- type ClaudeSource
- func (s *ClaudeSource) CountSessionsForDir(projectDir string) int
- func (s *ClaudeSource) Kind() string
- func (s *ClaudeSource) ListSessions(ctx context.Context, projectDir string) ([]SessionMeta, error)
- func (s *ClaudeSource) LoadTranscript(ctx context.Context, ref SessionRef) (*Transcript, error)
- func (s *ClaudeSource) LoadTranscriptFile(ctx context.Context, path, refID string) (*Transcript, error)
- func (s *ClaudeSource) LoadTranscriptFileWindow(ctx context.Context, path, refID string, req WindowRequest) (*WindowResult, error)
- func (s *ClaudeSource) LoadTranscriptWindow(ctx context.Context, ref SessionRef, req WindowRequest) (*WindowResult, error)
- func (s *ClaudeSource) TranscriptPathFor(projectDir, id string) string
- type CodexModelUsage
- type CodexSource
- func (s *CodexSource) CountSessionsByDir(ctx context.Context) (map[string]int, error)
- func (s *CodexSource) Kind() string
- func (s *CodexSource) ListSessions(ctx context.Context, projectDir string) ([]SessionMeta, error)
- func (s *CodexSource) LoadTranscript(ctx context.Context, ref SessionRef) (*Transcript, error)
- func (s *CodexSource) LoadTranscriptFile(ctx context.Context, path, refID string) (*Transcript, error)
- func (s *CodexSource) LoadTranscriptFileWindow(ctx context.Context, path, refID string, req WindowRequest) (*WindowResult, error)
- func (s *CodexSource) LoadTranscriptWindow(ctx context.Context, ref SessionRef, req WindowRequest) (*WindowResult, error)
- func (s *CodexSource) NewestRollouts(n int) []string
- func (s *CodexSource) RolloutPathFor(id string) string
- func (s *CodexSource) TranscriptBelongsToProject(id, projectDir string) bool
- func (s *CodexSource) TranscriptPathFor(id string) string
- func (s *CodexSource) TranscriptProjectDir(id string) string
- type DeepworkSessionMeta
- type DeepworkSessionProvider
- type DeepworkSource
- type DeepworkTurn
- type DirCounter
- type HiddenStore
- type ListResult
- type NativeContentBlock
- type NativeEntry
- type NativeMessage
- type NativeMetrics
- type NativeUsage
- type RunDiagnostic
- type RunIntent
- type RunUsage
- type SQLiteHiddenStore
- func (s *SQLiteHiddenStore) HiddenSet(ctx context.Context, source string) (map[string]bool, error)
- func (s *SQLiteHiddenStore) IsHidden(ctx context.Context, source, ssotKey string) (bool, error)
- func (s *SQLiteHiddenStore) Overlays(ctx context.Context, source string) (map[string]SessionOverlay, error)
- func (s *SQLiteHiddenStore) SetFriendlyName(ctx context.Context, source, ssotKey, name string) error
- func (s *SQLiteHiddenStore) SetHidden(ctx context.Context, source, ssotKey string, hidden bool) error
- type SessionMeta
- type SessionOverlay
- type SessionRef
- type SessionRuntimeResolver
- type SessionSource
- type TouchedFile
- type Transcript
- type Turn
- type WindowRequest
- type WindowResult
- type WindowSource
Constants ¶
const ( RunCompleted = "completed" RunInterrupted = "interrupted" RunError = "error" RunOpen = "open" )
AgentRun.Status — what the TRANSCRIPT says about how the run ended. Nothing more.
RunOpen deliberately does NOT mean "abandoned": a transcript with no terminal event is either (a) still being written right now by a live CLI, or (b) a session that died mid-loop — and this package cannot tell those apart. It has no clock, no process table, no file watcher; asserting "未完成/会话中断" on a session that is at this moment working would be a lie. Liveness is a different fact, owned by a different layer (sessionactivity: owned REPL + transcript mtime), and the wire layer joins the two:
open + LIVE/OBSERVED → running (the CLI is writing to it as we speak) open + QUIET → abandoned (nobody is writing; the run never finished)
That split is why the projector stays a pure function of the transcript.
const ( RolloutPrefix = "rollout-" JSONLSuffix = ".jsonl" )
File naming shared by the runtimes: codex names each session rollout-<ts>-<uuid>.jsonl, and both runtimes write JSONL.
const ( KindClaude = "claude" KindCodex = "codex" KindDeepwork = "deepwork" )
Source kind constants — the SSOT origin of a session.
const ( TerminalEndTurn = "end_turn" // yielded to human → run completed TerminalAborted = "aborted" // interrupted (user ESC / turn_aborted) TerminalError = "error" // runtime error ended the run )
Turn.Terminal values — how a run ended (runtime-agnostic).
const ( InputIntent = "intent" // an independent human request → opens a run (a round) InputAmendment = "amendment" // a steer INTO the running run → never a new round InputSystem = "system" // runtime notification → never a round, never a bubble )
Turn.InputKind values — what a human line MEANS (adapter-decided, never guessed).
const ( BlockText = "text" BlockThinking = "thinking" BlockTool = "tool" // a tool_use (+ its tool_result, attached) BlockAgent = "agent" // an `Agent` tool_use → subagent subflow (nested) BlockUser = "user_bubble" BlockUsage = "usage" BlockError = "error" // BlockTaskNotification is a runtime system event emitted when a background // task / dispatched agent finishes (claude jsonl user line carrying a // `<task-notification>` payload — verified schema, claude.go). It is a // first-class block kind (extension point for future system-event blocks) // rendered as a compact one-line card, NOT a full-width text bubble. BlockTaskNotification = "task-notification" // BlockCompaction is the runtime's automatic context compaction (codex // `event_msg/context_compacted` — verified: 5 occurrences in a real 16 MB // rollout). It is part of the run's causal story ("上下文已自动压缩") and must // survive an expanded ProcessTrace; it is NOT a conversation turn. BlockCompaction = "compaction" )
Block type constants — aligned with the @ce stream block registry (thinking / text / tool-group / agent subflow / usage / error / user-bubble).
const DefaultTailBytes int64 = 1 << 20 // 1 MiB
DefaultTailBytes is a sane tail window for "the newest record in this transcript": large enough to span the last few minutes of events, small enough to stay cheap at poll rates.
Variables ¶
var ErrUnknownSource = errors.New("sessionsource: unknown source")
ErrUnknownSource is returned when a transcript load names a source kind that is not wired into the aggregator.
Functions ¶
func ClaudeHome ¶ added in v0.13.0
func ClaudeHome() string
ClaudeHome returns the claude CLI's config dir (~/.claude, or CLAUDE_CONFIG_DIR).
func ClaudeProjectsRoot ¶ added in v0.13.0
func ClaudeProjectsRoot() string
ClaudeProjectsRoot returns the dir holding claude's per-project transcript shards.
func CodexHome ¶ added in v0.13.0
func CodexHome() string
CodexHome returns the codex CLI's home dir (~/.codex, or DW_CODEX_HOME).
func CodexSessionsRoot ¶ added in v0.13.0
func CodexSessionsRoot() string
CodexSessionsRoot returns the dir holding codex's rollout transcripts.
func EncodeProjectDir ¶
EncodeProjectDir maps an absolute project dir to claude's directory name. claude collapses BOTH '/' and '.' to '-', so `/home/u/.deepwork/ws` becomes `-home-u--deepwork-ws` (the '/.' → '--'). Encoding only '/' was a latent bug: it worked for dot-free paths (…/my-project) but pointed at a non-existent shard for any dotted path (…/.deepwork/…) — which broke session reads AND the collaborate jail's RW bind of that shard (jailed agent could not persist its turn).
func HasAnyFile ¶ added in v0.13.0
HasAnyFile reports whether root holds at least one file with the given suffix, anywhere in its tree. It stops at the first hit.
The bar is deliberately a FILE, not a directory: both CLIs leave dated directory skeletons (sessions/2026/07/12/) behind even when they never wrote a transcript there, so "the directory exists" proves nothing about whether this account was ever used.
func NewestFiles ¶ added in v0.13.0
NewestFiles returns up to n files under root matching prefix+suffix, newest first by mtime. n <= 0 returns every match. A missing root is an honest empty list, never an error — an absent runtime must not degrade the caller.
func ScanTail ¶ added in v0.13.0
ScanTail calls fn for each COMPLETE line in the last tailBytes of path (the whole file when it is smaller). A leading fragment produced by seeking into the middle of a line is dropped rather than handed to fn as if it were a record. fn returning false stops the scan.
Lines arrive in file order, so a caller looking for "the latest record" should keep the last match rather than the first.
func TouchedPath ¶
TouchedPath is the edit-tool allowlist PREDICATE — the irreducible domain rule of "which tool_use named a work-product file": only the edit tools count via file_path, Read counts only for images, everything else (Bash, path-free tools) does not.
This is the cross-repo SSOT for the rule (not the traversal): kit's ExtractTouched applies it over a whole parsed Transcript (pro share/owner per-session view), while deepwork-terminal's incremental tail-window project scanner applies it per raw jsonl tool line (its perf-critical cross-project "recent files" view keeps its own reader, but delegates the allowlist decision here so the rule can never diverge).
Types ¶
type AgentRun ¶ added in v0.13.0
type AgentRun struct {
ID string `json:"id"`
Index int `json:"index"` // 1-based ordinal of the human intent (RoundNav's #N SSOT)
// UserIntent is the human's own input that opened this run. Nil only for a system
// run (runtime activity/notification with no human line — Diagnostic.NoIntent).
UserIntent *RunIntent `json:"user_intent,omitempty"`
// Amendments are human inputs that steered THIS run while it was still working.
// They are not rounds (the adapter, not the projector, decides this — Turn.InputKind).
Amendments []RunIntent `json:"amendments,omitempty"`
// Segments is the agent's PROCESS, in strict transcript order: narration, thinking,
// tools, subagents, compaction, errors. Collapsing it in the UI is a visibility state
// only — segment ids/count/order/content never change.
Segments []Block `json:"segments"`
// FinalAnswer is the run's RESULT — the text of the terminal (yielding) iteration.
// It is a separate field, not a segment, precisely so that collapsing the process can
// never hide the answer. Empty = this run produced no answer (interrupted, tool-only,
// crashed): the UI then says so honestly instead of rendering an empty bubble.
FinalAnswer []Block `json:"final_answer,omitempty"`
// Usage is the run's single aggregate (the footer consumes only this one).
Usage *RunUsage `json:"usage,omitempty"`
Status string `json:"status"`
StartedAt *time.Time `json:"started_at,omitempty"`
EndedAt *time.Time `json:"ended_at,omitempty"`
Diagnostic RunDiagnostic `json:"diagnostic"`
}
AgentRun is the PRODUCT domain unit of a conversation: one independent human intent and everything the agent did in response to it.
It exists because a runtime "turn" is not a UI turn. A single human intent routinely drives dozens of model iterations and tool calls (real data: a claude transcript with 17 real user bubbles carries 552 assistant turns — 32.5× — and a codex rollout emits a separate event for every reasoning chunk, tool call and token_count). Rendering each of those as its own chat turn produces the bug this type kills: repeated avatars, empty answers, duplicated usage footers.
Identity comes from the human intent, never from a jsonl line number, a tool id or a provider message id.
func ProjectAgentRuns ¶ added in v0.13.0
func ProjectAgentRuns(tr *Transcript) []AgentRun
ProjectAgentRuns is the SINGLE, runtime-agnostic projector: adapter turns → human- facing runs. It has ZERO provider branches by construction: every fact it needs (is this human line an intent or a steer? did the runtime yield? which text is the answer?) was already stated by the adapter on Turn.InputKind / Turn.Terminal / Block.Final. It decides nothing by position and guesses nothing.
Deterministic, idempotent, linear in turns: the same transcript always yields the same run ids, counts and order.
type Aggregator ¶
type Aggregator struct {
// contains filtered or unexported fields
}
Aggregator fans a project across every wired SessionSource, merges the per-source session lists, applies the deepwork soft-delete filter, and sorts by recency. Adding a runtime = registering one more Source; this layer is unchanged (SSOT-SESSION-LOADER.md §6).
func NewAggregator ¶
func NewAggregator(hidden HiddenStore, sources ...SessionSource) *Aggregator
NewAggregator wires the sources + the soft-delete store (hidden may be nil to disable filtering, e.g. in tests).
func (*Aggregator) CountByDir ¶
func (a *Aggregator) CountByDir(ctx context.Context) map[string]int
CountByDir returns, per project dir, the merged session count across every wired source — computed with a single sweep per source. Sources implementing DirCounter use their cheap fast-path; the rest fall back to ListSessions("") (enumerate-all) grouped by SsotPath dir is not derivable generically, so a non-DirCounter source is simply skipped from the by-dir map and must be counted via the per-workspace path. In practice claude/codex implement the fast-path and deepwork is workspace-scoped (counted separately by the caller).
The returned map keys are exact project dirs (cwd / root_dir); a workspace resolves its count with one lookup. Missing key → 0 (honest empty), never an error: a degraded source must not break the whole list.
func (*Aggregator) List ¶
func (a *Aggregator) List(ctx context.Context, projectDir string, includeHidden bool) (*ListResult, error)
List aggregates sessions for projectDir. includeHidden controls whether soft-deleted rows are returned (default list hides them). Hidden state is always stamped onto every row so the UI can show the badge.
func (*Aggregator) LoadTranscript ¶
func (a *Aggregator) LoadTranscript(ctx context.Context, source string, ref SessionRef) (*Transcript, error)
LoadTranscript dispatches a transcript load to the named source.
func (*Aggregator) LoadTranscriptWindow ¶ added in v0.14.0
func (a *Aggregator) LoadTranscriptWindow(ctx context.Context, source string, ref SessionRef, req WindowRequest) (*WindowResult, error)
LoadTranscriptWindow dispatches the bounded-tail contract to capable sources. Small/native sources retain a correctness-first full-load fallback.
func (*Aggregator) SourceByKind ¶
func (a *Aggregator) SourceByKind(kind string) SessionSource
SourceByKind returns the wired source for a kind, or nil.
type Block ¶
type Block struct {
Type string `json:"type"`
// EventID is a STABLE identity for this content unit, derived from the source event
// (claude message.id + ordinal / codex call_id / line ordinal) — never a render-time
// counter. The UI keys expansion state and list diffing on it, so a reload or a
// re-fetch must produce the same ids or the user's open/closed choices re-bind to the
// wrong segments.
EventID string `json:"event_id,omitempty"`
// text / thinking
Text string `json:"text,omitempty"`
// Final marks the text that belongs to the run's TERMINAL (yielding) iteration — the
// answer, as opposed to mid-run narration. The projector lifts these out of the
// process trace into AgentRun.FinalAnswer, which is why it can never be swallowed by
// a collapse. It is a DOMAIN fact (claude stop_reason=end_turn / codex the message
// that precedes task_complete), not a position: "the last text block" would let an
// aborted run's narration impersonate an answer, and would push a real answer back
// into the trace whenever a notification happened to follow it.
Final bool `json:"-"`
// tool / agent (a tool_use)
ToolName string `json:"tool_name,omitempty"`
ToolUseID string `json:"tool_use_id,omitempty"`
ToolInput map[string]interface{} `json:"tool_input,omitempty"`
ToolResult string `json:"tool_result,omitempty"`
IsError bool `json:"is_error,omitempty"`
// ResultSeen: a tool_result actually arrived for this call. false = the tool never
// completed (the run was interrupted / the session died mid-call). The UI must render
// it as stopped, NOT as done — stamping "done" on every replayed tool (the old
// behavior) fabricates success for work that never finished.
ResultSeen bool `json:"result_seen,omitempty"`
// Orphan: a tool_result with no matching call in this transcript. Kept (never dropped)
// + counted in RunDiagnostic, so a lossy source degrades visibly.
Orphan bool `json:"orphan,omitempty"`
// agent (Agent tool_use → subagent). SubagentType is the dispatched agent
// kind; Description is the human-readable task; the inner subflow blocks are
// in Children (P1: empty — sidechain transcript stitching deferred to P2).
SubagentType string `json:"subagent_type,omitempty"`
Description string `json:"description,omitempty"`
Children []Block `json:"children,omitempty"`
// agent usage/timing (CHG-014 P3b — Gap-4). DurationMs is the wall-clock
// delta between the Agent tool_use line and its tool_result line — the only
// honest end-to-end subagent timing available from the parent jsonl (claude
// does not inline the subagent's own usage; sidechain transcripts are absent
// in practice). InTokens/OutTokens stay 0 (→ omitted → frontend "—") unless a
// future source supplies the subagent's own usage. Honest-degradation rule:
// never fabricate token counts (SPEC-UX-ROUND3 §5).
DurationMs int `json:"duration_ms,omitempty"`
InTokens int `json:"in_tokens,omitempty"`
OutTokens int `json:"out_tokens,omitempty"`
// task-notification (CHG-014 P3b — Gap-4). NotifyStatus = completed|failed|
// killed; Text carries the human-readable <summary>; TaskID is the runtime
// task id (debug/round-trip). Parsed from a claude `<task-notification>` user
// line — see claude.go.
NotifyStatus string `json:"notify_status,omitempty"`
TaskID string `json:"task_id,omitempty"`
// usage
Usage map[string]interface{} `json:"usage,omitempty"`
}
Block is one typed content unit inside a turn. Fields are populated per Type; JSON omitempty keeps the wire payload aligned to what the @ce block expects.
type ClaudeSource ¶
type ClaudeSource struct {
// Root is the projects base dir; defaults to ~/.claude/projects.
Root string
}
ClaudeSource reads the claude CLI's session storage:
~/.claude/projects/{encode(projectDir)}/*.jsonl
where encode replaces every '/' in the absolute project dir with '-' (e.g. /home/user/my-project →
-home-user-my-project).
Each *.jsonl is one session transcript (the SSOT). This source is strictly read-only: it never writes or deletes a jsonl.
func NewClaudeSource ¶
func NewClaudeSource() *ClaudeSource
NewClaudeSource builds a ClaudeSource rooted at ~/.claude/projects (or the override in DW_CLAUDE_PROJECTS, useful for tests/sandboxes).
func (*ClaudeSource) CountSessionsForDir ¶
func (s *ClaudeSource) CountSessionsForDir(projectDir string) int
CountSessionsForDir returns the number of *.jsonl sessions claude stored for projectDir — a cheap directory listing (no file parse). claude shards by the encoded project dir, so this touches only that one dir's entries, never the whole projects tree. Used by the GET /api/workspaces session_count fast-path (the codex equivalent CountSessionsByDir sweeps the whole tree once).
A missing dir is an honest 0 (no claude history), never an error.
func (*ClaudeSource) Kind ¶
func (s *ClaudeSource) Kind() string
func (*ClaudeSource) ListSessions ¶
func (s *ClaudeSource) ListSessions(ctx context.Context, projectDir string) ([]SessionMeta, error)
ListSessions scans the encoded project dir for *.jsonl files and extracts a lightweight SessionMeta per file (no full parse — only the cheap header scan).
func (*ClaudeSource) LoadTranscript ¶
func (s *ClaudeSource) LoadTranscript(ctx context.Context, ref SessionRef) (*Transcript, error)
LoadTranscript fully parses one jsonl into ordered turns/blocks.
func (*ClaudeSource) LoadTranscriptFile ¶ added in v0.14.0
func (s *ClaudeSource) LoadTranscriptFile(ctx context.Context, path, refID string) (*Transcript, error)
LoadTranscriptFile parses a Claude transcript already resolved by a trusted caller. It exists for child AgentExecution streams, which live under <root-session>/subagents/agent-<id>.jsonl rather than the project shard's top level. HTTP handlers must capability-check the agent against its root tree before calling this method; accepting arbitrary user paths is intentionally not this layer's job.
func (*ClaudeSource) LoadTranscriptFileWindow ¶ added in v0.14.0
func (s *ClaudeSource) LoadTranscriptFileWindow(ctx context.Context, path, refID string, req WindowRequest) (*WindowResult, error)
LoadTranscriptFileWindow is the trusted-path variant used by child executions.
func (*ClaudeSource) LoadTranscriptWindow ¶ added in v0.14.0
func (s *ClaudeSource) LoadTranscriptWindow(ctx context.Context, ref SessionRef, req WindowRequest) (*WindowResult, error)
LoadTranscriptWindow finds runtime yield boundaries from the tail and parses only that byte range. Large immutable prefixes never enter the JSON/block parser.
func (*ClaudeSource) TranscriptPathFor ¶
func (s *ClaudeSource) TranscriptPathFor(projectDir, id string) string
TranscriptPathFor returns the absolute jsonl path claude stores a session under: <Root>/<EncodeProjectDir(projectDir)>/<id>.jsonl. It only builds the path (no existence check) — the caller (sessionactivity) stats it for mtime. Empty when either input is empty (an unresolvable transcript, not a guess).
type CodexModelUsage ¶
type CodexModelUsage struct {
At time.Time
Model string
InputTokens int
OutputTokens int
CacheReadTokens int
}
CodexModelUsage is one per-turn token-usage delta tagged with the model that was active for that turn (a session may switch models mid-conversation).
func ScanCodexModelUsage ¶
func ScanCodexModelUsage(path string) ([]CodexModelUsage, error)
ScanCodexModelUsage streams one rollout-*.jsonl file and returns its per-turn, model-tagged token deltas — the SSOT primitive for any cost/report aggregation over codex usage (e.g. a usage/cost report). It reuses EXACTLY the field extraction LoadTranscript uses to build its usage footer (event_msg/token_count → info.last_token_usage → usageMap()), so there is a single parser for the codex rollout wire format; callers outside this package must not re-parse rollout jsonl themselves.
DEDUP: only last_token_usage (the per-turn delta) is read. total_token_usage is a cumulative running counter that — on sessions interleaving multiple conversations/subagents — is NOT safe to sum directly (it double- or under-counts depending on how it's read); last_token_usage has no such hazard because codex resets it fresh on every turn.
model is taken from the most recent preceding turn_context line (empty string if none seen yet — an honest "unknown model" bucket, never guessed).
type CodexSource ¶
type CodexSource struct {
Root string // defaults to ~/.codex
// contains filtered or unexported fields
}
CodexSource reads the codex CLI's session storage (read-only). Real layout (verified on ~/.codex):
<Root>/sessions/YYYY/MM/DD/rollout-<ISO>-<uuid>.jsonl — one per session <Root>/session_index.jsonl — session index <Root>/history.jsonl — prompt history
Each rollout-*.jsonl is one session transcript (the SSOT). Its first line is a session_meta payload carrying the session id (uuid), the originating cwd, and the wall clock — so ListSessions scopes by cwd == projectDir, mirroring how ClaudeSource shards by the encoded project dir. This source never writes or deletes a rollout file.
func NewCodexSource ¶
func NewCodexSource() *CodexSource
NewCodexSource roots at ~/.codex (override via DW_CODEX_HOME for tests).
func (*CodexSource) CountSessionsByDir ¶
CountSessionsByDir sweeps every rollout-*.jsonl once and returns cwd→count, reading ONLY the first line of each file (the session_meta payload carries the originating cwd) instead of full-parsing the transcript. This is the perf fast-path behind GET /api/workspaces (DirCounter): O(files) total with ~few-KB reads per file, replacing the old O(workspaces × files) full-parse.
Result is memoized under a fingerprint (file count + newest mtime) + short TTL, so repeated calls within one request (and bursts) reuse the sweep.
func (*CodexSource) Kind ¶
func (s *CodexSource) Kind() string
func (*CodexSource) ListSessions ¶
func (s *CodexSource) ListSessions(ctx context.Context, projectDir string) ([]SessionMeta, error)
ListSessions scans every rollout-*.jsonl, keeps those whose session_meta.cwd matches projectDir (when projectDir is non-empty), and extracts a lightweight SessionMeta per file via a single streaming pass.
func (*CodexSource) LoadTranscript ¶
func (s *CodexSource) LoadTranscript(ctx context.Context, ref SessionRef) (*Transcript, error)
LoadTranscript fully parses one rollout jsonl into ordered turns/blocks. The ref carries the session id (uuid); the file is resolved by walking the rollout tree and matching the id (the per-date path is not encoded in the ref).
func (*CodexSource) LoadTranscriptFile ¶ added in v0.14.0
func (s *CodexSource) LoadTranscriptFile(ctx context.Context, path, refID string) (*Transcript, error)
LoadTranscriptFile parses a Codex rollout already resolved by a trusted caller. Child AgentExecutions carry their own rollout path in the runtime-neutral tree; the web layer first proves that node belongs to the requested root session, then calls this parser. The parser remains read-only and does not infer parentage from paths.
func (*CodexSource) LoadTranscriptFileWindow ¶ added in v0.14.0
func (s *CodexSource) LoadTranscriptFileWindow(ctx context.Context, path, refID string, req WindowRequest) (*WindowResult, error)
LoadTranscriptFileWindow is the trusted-path variant used by child executions.
func (*CodexSource) LoadTranscriptWindow ¶ added in v0.14.0
func (s *CodexSource) LoadTranscriptWindow(ctx context.Context, ref SessionRef, req WindowRequest) (*WindowResult, error)
func (*CodexSource) NewestRollouts ¶ added in v0.13.0
func (s *CodexSource) NewestRollouts(n int) []string
NewestRollouts returns the n most recently written rollout transcripts, newest first. Callers that only want "what happened lately" (quota readings, live state) should bound themselves this way rather than walking the whole tree.
func (*CodexSource) RolloutPathFor ¶
func (s *CodexSource) RolloutPathFor(id string) string
RolloutPathFor returns the absolute rollout-*.jsonl path for a codex session id by walking the sessions tree (the same suffix match the transcript loader uses). Empty when the id is empty, unknown, or the tree is unreadable — an unresolvable transcript (the caller treats "" as non-advancing), never an error to the caller.
func (*CodexSource) TranscriptBelongsToProject ¶ added in v0.14.0
func (s *CodexSource) TranscriptBelongsToProject(id, projectDir string) bool
TranscriptBelongsToProject validates a runtime session capability against the cwd recorded by Codex in session_meta. A source-scoped id alone is insufficient: HTTP routes are workspace-scoped and must not read a valid thread from another cwd.
func (*CodexSource) TranscriptPathFor ¶ added in v0.14.0
func (s *CodexSource) TranscriptPathFor(id string) string
TranscriptPathFor resolves a source-scoped Codex thread id to its rollout file. It exposes the same resolver LoadTranscript uses so HTTP version/ETag checks do not duplicate the rollout filename convention or parse the transcript body.
func (*CodexSource) TranscriptProjectDir ¶ added in v0.14.0
func (s *CodexSource) TranscriptProjectDir(id string) string
TranscriptProjectDir returns the cwd capability recorded by Codex for a thread.
type DeepworkSessionMeta ¶
type DeepworkSessionMeta struct {
ID int64
Title string
CreatedAt time.Time
UpdatedAt time.Time
TurnCount int
}
DeepworkSessionMeta is the projected metadata for a deepwork session.
type DeepworkSessionProvider ¶
type DeepworkSessionProvider interface {
// ListWorkspaceSessions returns the deepwork-owned sessions for a workspace.
ListWorkspaceSessions(ctx context.Context, workspaceID int64) ([]DeepworkSessionMeta, error)
// KnownSessionIDs returns the set of ALL deepwork session ids the DB knows about
// (any workspace, any source) as decimal strings. Directory-as-index recovery uses
// it to recover ONLY genuine orphans: a transcript file whose id is globally known
// but absent from THIS workspace's rows belongs to another workspace/source and must
// not be re-surfaced (CHG-016 R3 cross-workspace session leak). A nil map (provider
// error / DB gone) degrades to recover-all, preserving the "delete DB still lists".
KnownSessionIDs(ctx context.Context) (map[string]struct{}, error)
// LoadSessionTurns returns the typed turns for one deepwork session (DB
// fallback only — the jsonl file is the primary read source).
LoadSessionTurns(ctx context.Context, sessionID int64) ([]DeepworkTurn, error)
}
DeepworkSessionProvider is the narrow read API DeepworkSource needs from the conversation processor. Declared here (not importing internal/conversation) to keep this package free of a heavy dependency and trivially testable with a fake. The webui wiring adapts *conversation.Processor onto this interface.
CHG-015 P2/P3 (路 A): the deepwork transcript file (dw-<id>.jsonl) is the content SSOT — LoadTranscript reads it, not the DB; ListSessions treats the transcript directory as the index (目录即索引). The provider survives only as a cache/overlay: (a) workspace-scoped ordering + title rename for ListSessions and (b) a graceful content fallback when a session has no jsonl yet (pre-P1 / file missing). Delete the DB and both list + open still work from the files.
type DeepworkSource ¶
type DeepworkSource struct {
// contains filtered or unexported fields
}
DeepworkSource exposes the deepwork self-hosted agent sessions. CHG-015 路 A: the transcript file (transcriptDir/dw-<id>.jsonl) is the content SSOT — three sources (claude/codex/deepwork) now all read their own jsonl and project into the same Transcript model. The DB is a projection cache / list index, not the fact source: delete the DB and the conversation still rebuilds from the file.
func NewDeepworkSource ¶
func NewDeepworkSource(p DeepworkSessionProvider, workspaceID int64) *DeepworkSource
NewDeepworkSource binds a provider + the workspace id this project maps to. transcriptDir is the directory holding the dw-<id>.jsonl files; when empty the source degrades to the DB provider (so old wiring / tests without a dir still work). Prefer NewDeepworkSourceWithDir to enable file-SSOT reads.
func NewDeepworkSourceWithDir ¶
func NewDeepworkSourceWithDir(p DeepworkSessionProvider, workspaceID int64, transcriptDir string) *DeepworkSource
NewDeepworkSourceWithDir is the CHG-015 P2 constructor: it additionally binds the transcript directory so LoadTranscript reads dw-<id>.jsonl (file SSOT), falling back to the DB provider only when the file is absent/empty.
func (*DeepworkSource) Kind ¶
func (s *DeepworkSource) Kind() string
func (*DeepworkSource) ListSessions ¶
func (s *DeepworkSource) ListSessions(ctx context.Context, projectDir string) ([]SessionMeta, error)
ListSessions enumerates the deepwork sessions for this workspace. CHG-015 P3 (目录即索引): the transcript directory is the session index — ListSessions scans transcriptDir for dw-<id>.jsonl (the SSOT) and the DB provider is a cache/overlay (title rename, workspace-scoped ordering). The merge rule:
DB row present → use it (authoritative title/ordering, workspace-scoped),
stamp the on-disk path so the row round-trips to the file.
file-only (orphan, e.g. DB deleted/empty) → recover the row from the file
via the cheap header scan (claude.scanMeta parity).
So deleting the DB does NOT empty the list: every dw-<id>.jsonl still surfaces (lazy rebuild — no DB write, the file is both index and content source).
func (*DeepworkSource) LoadTranscript ¶
func (s *DeepworkSource) LoadTranscript(ctx context.Context, ref SessionRef) (*Transcript, error)
type DeepworkTurn ¶
DeepworkTurn is the projected user/assistant exchange of a deepwork session.
type DirCounter ¶
type DirCounter interface {
// CountSessionsByDir returns a map of project dir (cwd) → session count for
// every session this source stored, in one sweep. Hidden/soft-delete is NOT
// applied here (the panel hint counts SSOT sessions; the aggregator applies
// the overlay filter only on the detailed List path).
CountSessionsByDir(ctx context.Context) (map[string]int, error)
}
DirCounter is an optional, cheap fast-path a SessionSource may implement to count its sessions grouped by originating project dir (cwd) in a SINGLE sweep of its storage — without the per-file full parse ListSessions does and without being re-invoked once per workspace.
CHG-014 P5 perf fix: GET /api/workspaces enriches every workspace with a session_count. The naive path called ListSessions(projectDir) once per workspace, and CodexSource.ListSessions full-parses every rollout-*.jsonl (162 MB / 475 files here) on each call → O(workspaces × allCodexFiles) ≈ 8 s. CountByDir scans each source's storage ONCE, returns cwd→count, so the handler resolves N workspaces with N O(1) map lookups → O(allFiles) total. For codex the sweep reads only the first line (session_meta carries cwd) per file, not the whole transcript, cutting the bytes read by ~100×.
type HiddenStore ¶
type HiddenStore interface {
// IsHidden reports whether (source, ssotKey) is soft-deleted.
IsHidden(ctx context.Context, source, ssotKey string) (bool, error)
// HiddenSet returns the set of hidden ssot keys for a source (batch filter).
HiddenSet(ctx context.Context, source string) (map[string]bool, error)
// Overlays returns the full overlay state (hidden + friendly_name) per ssot
// key for a source, in one batch read (used by the aggregator).
Overlays(ctx context.Context, source string) (map[string]SessionOverlay, error)
// SetHidden marks/unmarks a session as hidden (idempotent). Preserves any
// existing friendly_name.
SetHidden(ctx context.Context, source, ssotKey string, hidden bool) error
// SetFriendlyName sets/clears the soft-rename (idempotent). Preserves the
// existing hidden flag. Empty name clears the rename.
SetFriendlyName(ctx context.Context, source, ssotKey, name string) error
}
HiddenStore is the deepwork-owned soft-delete / soft-rename layer. It only marks/filters/renames in deepwork's own table; it MUST NOT touch any runtime transcript (SSOT protection, §0).
type ListResult ¶
type ListResult struct {
Sessions []SessionMeta
Degraded []string // source kinds that failed
Wired int // number of wired sources
Failed int // number that errored
}
ListResult carries the merged sessions plus the set of sources that errored (degraded), mirroring the fleet handler's honesty contract.
type NativeContentBlock ¶
type NativeContentBlock struct {
Type string `json:"type"` // text | thinking | tool_use | tool_result
Text string `json:"text,omitempty"`
Thinking string `json:"thinking,omitempty"`
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Input json.RawMessage `json:"input,omitempty"`
ToolUseID string `json:"tool_use_id,omitempty"`
Content json.RawMessage `json:"content,omitempty"`
IsError bool `json:"is_error,omitempty"`
}
NativeContentBlock is one typed content unit. text/thinking carry their string; tool_use carries name/id/input; tool_result carries tool_use_id + content (a JSON array of {type:text,text} blocks, or a bare string).
type NativeEntry ¶
type NativeEntry struct {
Format string `json:"format,omitempty"`
Runtime string `json:"runtime,omitempty"`
Type string `json:"type"` // user | assistant | result | progress
UUID string `json:"uuid,omitempty"`
ParentUUID *string `json:"parentUuid"`
SessionID string `json:"sessionId"`
Timestamp string `json:"timestamp"`
Message *NativeMessage `json:"message,omitempty"`
Subtype string `json:"subtype,omitempty"`
DurationMs int `json:"duration_ms,omitempty"`
NumTurns int `json:"num_turns,omitempty"`
DeepworkSessionID int64 `json:"deepworkSessionId,omitempty"`
DeepworkTurnID int64 `json:"deepworkTurnId,omitempty"`
Workstream json.RawMessage `json:"workstream,omitempty"`
// Metrics (v1.1) inlines per-turn ttft/usage on the turn-closing `result`
// entry so the transcript carries the round's metrics without the DB. nil on
// non-result entries and on legacy (v1) files.
Metrics *NativeMetrics `json:"metrics,omitempty"`
// CWD / GitBranch / Version make a line SELF-DESCRIBING — the same three claude
// stamps on every entry. Without them a transcript cannot say which checkout it
// describes, and replay/attribution has to reach outside the file for context.
CWD string `json:"cwd,omitempty"`
GitBranch string `json:"gitBranch,omitempty"`
Version string `json:"version,omitempty"`
// RequestID pairs with NativeMessage.ID as the DEDUP KEY for token accounting —
// the pair kit/usage keys claude usage on. Deepwork turns carried neither, which
// is precisely why deepwork-native sessions contribute nothing to the cost report
// today: there is no stable identity to dedup a re-read line by.
RequestID string `json:"requestId,omitempty"`
// IsSidechain / AgentID model a SUBAGENT branch, exactly as claude does: a
// sidechain entry belongs to a spawned agent's own conversation rather than the
// main thread, and AgentID names which one. The agent-tree projection is built
// from these two fields.
//
// RESERVED — the deepwork agent runtime does not spawn subagents yet, so nothing
// sets these. They exist now so the wire format does not need a breaking bump the
// day it does, and so the reader/tree code can be written against a stable shape.
IsSidechain bool `json:"isSidechain,omitempty"`
AgentID string `json:"agentId,omitempty"`
// UserType mirrors claude ("external" for a human turn, "internal" for one the
// runtime synthesised, e.g. a tool_result fed back in). Empty = unspecified.
UserType string `json:"userType,omitempty"`
}
native_schema.go is the SINGLE SSOT for the Deepwork Native Transcript JSONL wire schema (deepwork.native_transcript.v1 / v1.1). Before this file the shape was declared TWICE — once as an unexported READ view here (deepwork_raw.go's former dwLine/dwMessage/dwContentBlock/dwUsage/dwMetrics) and once as the WRITE model in a host's worktranscript package (Entry/Message/ContentBlock/TurnUsage/TurnMetrics) — with no compiler check that the two stayed byte-compatible. Both sides now import these exported Native* types instead.
Workstream is deliberately json.RawMessage (opaque): this package must not depend on deepwork's workstream event types (kit stays dependency-light and import-cycle free). worktranscript marshals its StreamPayload into this field on write; the reader in this package never decodes `progress` lines into blocks, so it never needs to look inside.
json tags below are byte-identical to the historical write model (pkg/worktranscript.Entry et al.) — do not change them without a wire migration; anything that previously round-tripped through the two hand-synced schemas must keep marshaling/unmarshaling identically.
type NativeMessage ¶
type NativeMessage struct {
// ID (v1.2) is the provider's message id (claude: "msg_…"). With NativeEntry.RequestID
// it forms the dedup key token accounting uses — see RequestID's note.
ID string `json:"id,omitempty"`
Role string `json:"role"`
// Model (v1.1) is the real model id that produced this assistant turn,
// mirroring claude's `message.model`. Empty = path did not report.
Model string `json:"model,omitempty"`
Content []NativeContentBlock `json:"content"`
// StopReason (v1.2) is why the assistant stopped: "end_turn" | "tool_use" |
// "max_tokens" | "stop_sequence". This is the signal agent-status detection reads
// to tell "the agent finished its turn and is waiting for you" apart from "it
// paused mid-turn to run a tool" — without it, a deepwork session cannot express
// the running→waiting transition that drives the turn-end notification.
// Empty = the path did not report one (honest unknown, never guessed).
StopReason string `json:"stop_reason,omitempty"`
// Usage (v1.1) inlines the round's token accounting on the assistant message,
// mirroring claude's `message.usage`. nil = path reported no usage (honest
// unknown, never fabricated). Self-sufficient: deleting the DB does not lose it.
Usage *NativeUsage `json:"usage,omitempty"`
}
NativeMessage is the inner message object for user/assistant lines.
type NativeMetrics ¶
type NativeMetrics struct {
TTFTMs *int `json:"ttft_ms,omitempty"`
DurationMs int `json:"duration_ms,omitempty"`
}
NativeMetrics is the inlined per-turn timing carried on the `result` entry (v1.1).
type NativeUsage ¶
type NativeUsage struct {
InputTokens *int `json:"input_tokens,omitempty"`
OutputTokens *int `json:"output_tokens,omitempty"`
// ThinkingTokens — reasoning token 单列 (CHG-016). Inlined on the assistant
// line so the deepwork replay footer shows the SAME thinking token the live
// stream did (nil = unknown → 「—」, distinct from the thinking duration).
ThinkingTokens *int `json:"thinking_tokens,omitempty"`
CacheReadTokens *int `json:"cache_read_tokens,omitempty"`
CacheCreateTokens *int `json:"cache_creation_tokens,omitempty"`
// TTFTMs — first-content latency, inlined on the assistant line's usage so the
// per-turn replay footer renders TTFT without chasing the separate `result`
// line (which the unified reader skips). nil = unknown → 「—」.
TTFTMs *int `json:"ttft_ms,omitempty"`
}
NativeUsage is the inlined per-turn token accounting (v1.1). Pointer fields keep the "nil = unknown vs 0 = observed" distinction the writer honors.
type RunDiagnostic ¶ added in v0.13.0
type RunDiagnostic struct {
RawTurns int `json:"raw_turns"` // adapter turns folded into this run
SegmentCount int `json:"segment_count"` //
UsageBlocks int `json:"usage_blocks,omitempty"` // usage deltas summed into Usage
OrphanResults int `json:"orphan_results,omitempty"` // tool results with no matching call
PendingTools int `json:"pending_tools,omitempty"` // tool calls whose result never arrived
NoIntent bool `json:"no_intent,omitempty"` // runtime activity with no human line
Unterminated bool `json:"unterminated,omitempty"` // the runtime never yielded
}
RunDiagnostic exposes what the projection had to cope with, so a wrong-looking round is debuggable without re-reading the jsonl — and so a lossy source degrades visibly instead of silently.
type RunIntent ¶ added in v0.13.0
RunIntent is one human input (the opening intent, or a mid-run amendment).
type RunUsage ¶ added in v0.13.0
type RunUsage struct {
InputTokens int `json:"input_tokens,omitempty"`
OutputTokens int `json:"output_tokens,omitempty"`
ThinkingTokens int `json:"thinking_tokens,omitempty"`
CacheReadTokens int `json:"cache_read_tokens,omitempty"`
CacheCreateTokens int `json:"cache_create_tokens,omitempty"`
TTFTMs *int `json:"ttft_ms,omitempty"`
DurationMs *int `json:"duration_ms,omitempty"`
CostUSD *float64 `json:"cost_usd,omitempty"`
Model string `json:"model,omitempty"`
Models []string `json:"models,omitempty"`
}
RunUsage is the run's aggregate — TYPED, because "sum every number in the map" is wrong for half of these fields: summing ttft across a run's 30 model iterations, or summing a per-iteration duration into a wall clock, invents numbers nobody measured; and a float cost truncated into an int silently loses money.
Per-field policy is stated once, here:
- tokens/cost → SUM across iterations (they are per-iteration deltas)
- ttft → FIRST iteration's (time to first token of the run)
- duration → SUM of the runtime's own measured per-iteration durations, when it reports any; otherwise absent → the UI derives the honest wall clock from EndedAt-StartedAt (never fabricated)
- model → the model in force when the run ended, plus Models[] when it switched
type SQLiteHiddenStore ¶
type SQLiteHiddenStore struct {
// contains filtered or unexported fields
}
SQLiteHiddenStore implements HiddenStore over the conversationDB. It is the ONLY mutable state in this package — and it only ever flips a soft-delete flag or stores a soft-rename. The runtime transcript (jsonl / deepwork session) is never touched (SSOT protection, SSOT-SESSION-LOADER.md §0).
Table runtime_session_hidden(source, ssot_key UNIQUE, hidden, hidden_at, friendly_name) is created by storage.migrateConversationDB(); the (source, ssot_key) pair is the stable key (ssot_key = the SessionMeta.ID for that source). A row may exist with hidden=0 when it carries only a rename.
func NewSQLiteHiddenStore ¶
func NewSQLiteHiddenStore(db *sql.DB) *SQLiteHiddenStore
NewSQLiteHiddenStore binds a HiddenStore to the conversationDB handle.
func (*SQLiteHiddenStore) Overlays ¶
func (s *SQLiteHiddenStore) Overlays(ctx context.Context, source string) (map[string]SessionOverlay, error)
Overlays returns hidden + friendly_name per ssot key for a source (one read).
func (*SQLiteHiddenStore) SetFriendlyName ¶
func (s *SQLiteHiddenStore) SetFriendlyName(ctx context.Context, source, ssotKey, name string) error
SetFriendlyName sets or clears the soft-rename. Idempotent. Preserves hidden. An empty name clears the rename; the row is removed if it then carries no state. The runtime transcript title is never modified (SSOT protection).
func (*SQLiteHiddenStore) SetHidden ¶
func (s *SQLiteHiddenStore) SetHidden(ctx context.Context, source, ssotKey string, hidden bool) error
SetHidden marks (hidden=true) or unmarks (hidden=false) a session. Idempotent. It only writes the deepwork-owned flag table; it does NOT delete any SSOT. A row carrying a friendly_name is preserved on un-hide (hidden set to 0); a row with neither hidden nor a rename is removed to keep the table lean.
type SessionMeta ¶
type SessionMeta struct {
// ID is a stable, source-scoped identifier used to round-trip back to the
// SSOT for a transcript load. For claude it is the jsonl basename (uuid);
// for deepwork it is the numeric session id as a string.
ID string `json:"id"`
Source string `json:"source"` // claude | codex | deepwork
// SsotPath is the on-disk transcript path (claude jsonl / codex jsonl) or
// the deepwork session id. Surfaced to the UI for the "源自 …" tooltip.
SsotPath string `json:"ssot_path"`
Title string `json:"title"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
TurnCount int `json:"turn_count"`
// Hidden is the deepwork soft-delete flag (virtual delete). The SSOT is
// never removed; hidden rows are filtered out of the default list.
Hidden bool `json:"hidden"`
// FriendlyName is the deepwork-owned soft-rename. When non-empty it has
// already been applied onto Title by the aggregator; it is surfaced here so
// the UI can distinguish a renamed session from its SSOT title. The runtime
// transcript title is never modified (SSOT protection).
FriendlyName string `json:"friendly_name,omitempty"`
// Activity / Attention / Owned are the WLS-8b live activity state, derived at
// list time (NOT stored): Activity = LIVE|OBSERVED|QUIET, Attention =
// working|waiting|error|idle, Owned = this MuxHost holds a live REPL for it.
// Empty Activity means the list route did not derive it (no deriver wired) —
// the frontend treats absent state as QUIET/idle. These drive the two-axis tab
// + tree badge and the OBSERVED「在另一会话中活跃·观测中」banner/composer-lock.
Activity string `json:"activity,omitempty"`
Attention string `json:"attention,omitempty"`
Owned bool `json:"owned,omitempty"`
// SessionExecution is orthogonal to the root liveness/write-authority axes above.
// A quiet root may still have running child AgentExecutions.
ExecutionActivity string `json:"execution_activity,omitempty"` // running | idle
RootAgentState string `json:"root_agent_state,omitempty"` // running | waiting | idle | error
InteractionMode string `json:"interaction_mode,omitempty"` // owned | observed | history
RunningAgents int `json:"running_agents,omitempty"`
ExecutionObservedAt int64 `json:"execution_observed_at,omitempty"`
AgentSourceStatus string `json:"agent_source_status,omitempty"` // complete | partial | unknown
}
SessionMeta is one row in the aggregated session list (list view, no body).
type SessionOverlay ¶
SessionOverlay is the deepwork-owned overlay state for one runtime session: soft-delete (Hidden) and soft-rename (FriendlyName). Both are independent and neither touches the runtime transcript (SSOT protection, §0).
type SessionRef ¶
type SessionRef struct {
ProjectDir string // the project root_dir the session belongs to
ID string // source-scoped id (claude jsonl basename / deepwork session id)
}
SessionRef addresses one session inside a source for transcript loading.
type SessionRuntimeResolver ¶
type SessionRuntimeResolver interface {
// SessionRuntimeInfo returns the session's runtime ("claude"/"codex"/…) and the
// upstream CLI-owned conversation id (empty for a native deepwork_api session).
SessionRuntimeInfo(ctx context.Context, sessionID int64) (runtime, runtimeSessionID string, err error)
}
LoadTranscript reads dw-<id>.jsonl (the content SSOT) into turns/blocks. When the file is missing/empty (pre-P1 session or never-written) it degrades to the DB provider so old sessions still display (零回归). The file path carries thinking/tool/usage blocks (P1 写补) so the workArea panel rebuilds from the file with no DB read. SessionRuntimeResolver is an OPTIONAL provider capability (type-asserted): it maps a deepwork session id to the external CLI runtime that OWNS its transcript. A provider that does not implement it (test fakes) simply keeps the deepwork-native read path.
type SessionSource ¶
type SessionSource interface {
// Kind returns the SSOT origin tag (claude | codex | deepwork).
Kind() string
// ListSessions enumerates the sessions this runtime stored for projectDir.
ListSessions(ctx context.Context, projectDir string) ([]SessionMeta, error)
// LoadTranscript parses one session's body from the SSOT into turns/blocks.
LoadTranscript(ctx context.Context, ref SessionRef) (*Transcript, error)
}
SessionSource is the terminal abstraction: one runtime's session storage, read-only. Implementations: ClaudeSource, CodexSource, DeepworkSource.
type TouchedFile ¶
type TouchedFile struct {
Name string `json:"name"`
Path string `json:"path"` // relative to rootDir
IsDir bool `json:"is_dir"`
Size int64 `json:"size"`
TouchedAt int64 `json:"touched_at,omitempty"` // unix ms of the tool_use that touched it
Tool string `json:"tool,omitempty"` // the edit tool (Write/Edit/…)
}
TouchedFile is one "涉及文件" row: a work-product file the session's edit tools touched, addressed by a source-root-relative path (the absolute host path never leaves this layer). It is the domain result of ExtractTouched, consumed by every file-view host (share visitor / ws owner / terminal) through one endpoint shape.
func ExtractTouched ¶
func ExtractTouched(tr *Transcript, rootDir string) []TouchedFile
ExtractTouched walks the transcript (incl. agent subflows) and returns the files the session touched, root-clamped, existence-checked, newest-first. Dedup keeps the newest tool_use per path. Every path is clamped to rootDir so a tool that touched outside the shared project never leaks into the list.
type Transcript ¶
type Transcript struct {
Source string `json:"source"`
Ref string `json:"ref"`
Title string `json:"title"`
Turns []Turn `json:"-"` // adapter fact, server-side only
Runs []AgentRun `json:"runs"` // ← the wire contract (ProjectAgentRuns)
Meta map[string]interface{} `json:"meta,omitempty"` // usage totals etc.
}
Transcript is the parsed body of one session.
Turns is the ADAPTER FACT: the raw, ordered parse of the runtime's own jsonl (one Turn per runtime message/event group). It is deliberately NOT on the wire — a runtime event is not a UI conversation turn (a single human intent routinely spans dozens of model/tool iterations). The UI consumes Runs, the AgentRun projection (agentrun.go), which is a pure function of Turns.
Server-side consumers of the raw parse (redaction, touched-file extraction, usage metrics) keep reading Turns; they run BEFORE ProjectAgentRuns.
type Turn ¶
type Turn struct {
Index int `json:"index"`
Role string `json:"role"` // user | assistant
Blocks []Block `json:"blocks"`
// Timestamp of the underlying jsonl line, when available.
At *time.Time `json:"at,omitempty"`
// Terminal is the ADAPTER-supplied, runtime-agnostic "this assistant turn
// yielded control back to the human" fact. Empty = the runtime is still working
// (a tool loop continues).
//
// Derivation per runtime (verified against real transcripts):
// claude message.stop_reason: end_turn → TerminalEndTurn; tool_use → "" ;
// a `[Request interrupted by user]` line → TerminalAborted.
// codex event_msg: task_complete → TerminalEndTurn; turn_aborted → TerminalAborted.
// deepwork native: every assistant entry closes its exchange → TerminalEndTurn.
Terminal string `json:"terminal,omitempty"`
// InputKind classifies a USER turn. The projector must never GUESS whether a human
// line opens a new round or steers the running one ("is a run currently open?" is
// not a decidable contract — a runtime that dies mid-loop would silently swallow the
// next independent request into the previous round). So each adapter states it, from
// the runtime's own facts:
//
// codex `event_msg/task_started` opens a task → the message that drives it is an
// intent; a user_message arriving inside an already-started task is a steer.
// (Real rollout: task_started × 10, user_message × 16 → 10 intents, 6 steers.)
// claude a user line while the model is still in a tool loop (no end_turn yet) is
// queued — the runtime records it as `queue-operation/enqueue` (72 in the
// real transcript). Yielded → the next human line is a new intent.
// system runtime notifications (task-notification, interrupt markers) — never a round.
InputKind string `json:"input_kind,omitempty"`
}
Turn is one runtime message/event group as the adapter parsed it — NOT a UI conversation turn. Role is "user" or "assistant". Blocks are the typed content units rendered by the @ce stream.
type WindowRequest ¶ added in v0.14.0
WindowRequest addresses a bounded suffix/range of an append-only transcript. Before is an opaque source cursor (currently a byte boundary), not a run index.
type WindowResult ¶ added in v0.14.0
type WindowResult struct {
Transcript *Transcript
Before int64
HasMore bool
Version string
Generation string
Reset bool
BytesParsed int64
}
WindowResult carries a projected AgentRun window. Generation remains stable across appends and changes on replacement; Reset tells clients to discard an invalid cursor.
type WindowSource ¶ added in v0.14.0
type WindowSource interface {
LoadTranscriptWindow(ctx context.Context, ref SessionRef, req WindowRequest) (*WindowResult, error)
}
WindowSource is the performance boundary for large append-only transcripts. Implementations find and parse only the requested tail/range.