diff --git a/apps/cli/src/main.ts b/apps/cli/src/main.ts index afef34e7..d3dc737b 100644 --- a/apps/cli/src/main.ts +++ b/apps/cli/src/main.ts @@ -7,7 +7,7 @@ // very first side effect (before the coding-agent barrel is evaluated) so // config.ts resolves the Step storage namespace regardless of launcher name. import "#bootstrap/environment"; -import { join } from "node:path"; +import { join, resolve } from "node:path"; import { ambiguousPluginServerNames, @@ -30,6 +30,7 @@ import { getStepAuthPath, getStepDefaultTheme, getStepLoginStatus, + getStepSessionDirOverride, hasConfiguredStepCodeCredential, installProcessStderrDevLogCapture, isStepConfigCommand, @@ -60,6 +61,7 @@ import { resolveStepStorageRoot, restoreStdout, runFeedbackCommand, + runSessionIndexCommand, runStepConfigCommand, runStepLogin, runStepMcpImportPrompt, @@ -353,6 +355,7 @@ const isTopLevelLogin = topLevelStepCommand === "login"; const isTopLevelLoginStatus = isTopLevelLogin && process.argv[3] === "status"; const isTopLevelLogout = topLevelStepCommand === "logout"; const isTopLevelFeedback = topLevelStepCommand === "feedback"; +const isTopLevelSessionIndex = topLevelStepCommand === "session-index"; const topLevelAuthHelp = process.argv.slice(3).some((arg) => arg === "--help" || arg === "-h"); let telemetryExitReason: "normal" | "error" = "normal"; @@ -659,6 +662,14 @@ try { } } } + } else if (isTopLevelSessionIndex) { + const sessionDirOverride = getStepSessionDirOverride(process.env); + const exitCode = await runSessionIndexCommand(process.argv.slice(3), { + agentDir: resolveStepAgentDir(), + ...(sessionDirOverride ? { extraRoots: [resolve(sessionDirOverride)] } : {}), + }); + if (exitCode !== 0) process.exitCode = exitCode; + forceOneShotExit = true; } else if (isTopLevelFeedback) { const feedbackSettingsManager = currentStepSettingsManager ?? createStepSettingsManager(process.cwd(), resolveStepAgentDir()); diff --git a/apps/cli/src/ui/view/dialogs/session-selector.ts b/apps/cli/src/ui/view/dialogs/session-selector.ts index 33874ba2..a16c301a 100644 --- a/apps/cli/src/ui/view/dialogs/session-selector.ts +++ b/apps/cli/src/ui/view/dialogs/session-selector.ts @@ -6,7 +6,7 @@ import type { SessionInfo, SessionListProgress } from "@step-harness/coding-agen import { canonicalizePath as _canonicalizePath, DynamicBorder, - isChildAgentSessionId, + isChildAgentSession, KeybindingsManager, keyHint, keyText, @@ -954,7 +954,7 @@ export class SessionSelectorComponent extends Container implements Focusable { // a respawned child still reattaches to its own transcript. const sessions = ( await (scope === "current" ? this.currentSessionsLoader(onProgress) : this.allSessionsLoader(onProgress)) - ).filter((session) => !isChildAgentSessionId(session.id)); + ).filter((session) => !isChildAgentSession(session)); if (scope === "current") { this.currentSessions = sessions; diff --git a/packages/coding-agent/docs/docs.json b/packages/coding-agent/docs/docs.json index bbc9e74b..d4bbab02 100644 --- a/packages/coding-agent/docs/docs.json +++ b/packages/coding-agent/docs/docs.json @@ -84,6 +84,10 @@ { "title": "Session Format", "path": "session-format.md" + }, + { + "title": "Session Index", + "path": "session-index.md" } ] }, diff --git a/packages/coding-agent/docs/index.md b/packages/coding-agent/docs/index.md index 1ba79715..8b1c444e 100644 --- a/packages/coding-agent/docs/index.md +++ b/packages/coding-agent/docs/index.md @@ -66,6 +66,7 @@ For the full first-run flow, see [Quickstart](quickstart.md). - [Environment variables](environment-variables.md) - Step process configuration and session metadata available to bash tools. - [Session format](session-format.md) - JSONL session file format, entry types, and SessionManager API. +- [Session index](session-index.md) - derived SQLite index for session lists and full-text search. ## Platform setup diff --git a/packages/coding-agent/docs/session-format.md b/packages/coding-agent/docs/session-format.md index 8a97a1d9..818894bb 100644 --- a/packages/coding-agent/docs/session-format.md +++ b/packages/coding-agent/docs/session-format.md @@ -202,6 +202,12 @@ For sessions with a parent (created via `/fork`, `/clone`, or `newSession({ pare {"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project","parentSession":"/path/to/original/session.jsonl"} ``` +A session created by a child agent (a subagent lane or workflow agent, spawned with `STEPCODE_SUBAGENT_CHILD=1`) carries `origin: "subagent"` and, when known, the id of the session that spawned it. Session lists, the resume picker, and session search skip these sessions (`isChildAgentSession`, which also recognizes the `subagent-` / `workflow-` id prefixes of older child sessions): + +```json +{"type":"session","version":3,"id":"subagent-uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project","origin":"subagent","parentSessionId":"uuid"} +``` + ### SessionMessageEntry A message in the conversation. The `message` field contains an `AgentMessage`. @@ -401,7 +407,7 @@ Key methods for working with sessions programmatically. - `SessionManager.listAll(onProgress?)` - List all sessions across all projects ### Instance Methods - Session Management -- `newSession(options?)` - Start a new session (options: `{ parentSession?: string }`) +- `newSession(options?)` - Start a new session (options: `{ id?, parentSession?, origin?, parentSessionId? }`) - `setSessionFile(path)` - Switch to a different session file - `createBranchedSession(leafId)` - Extract branch to new session file @@ -438,3 +444,4 @@ Key methods for working with sessions programmatically. - `getSessionId()` - Session UUID - `getSessionFile()` - Session file path (undefined for in-memory) - `isPersisted()` - Whether session is saved to disk +- `setFilePersistedListener(listener)` - Observe `create` / `append` / `rewrite` writes, e.g. for the [session index](session-index.md) diff --git a/packages/coding-agent/docs/session-index.md b/packages/coding-agent/docs/session-index.md new file mode 100644 index 00000000..cdfc6cad --- /dev/null +++ b/packages/coding-agent/docs/session-index.md @@ -0,0 +1,139 @@ +# Session index + +The session index is a derived SQLite database for session lists and full-text search. Session JSONL files stay the only authoritative data: the index only reads them, and deleting it loses nothing but the time to rebuild it. + +## Storage and lifecycle + +| Item | Value | +| --- | --- | +| Location | `/index/session-search.v1.db` (plus `-wal` / `-shm`); the schema major is part of the name | +| Engine | Built-in `node:sqlite` with FTS5 `trigram`; no extra dependency | +| Owner | One worker thread per process holds the only connection. The CLI does not open the index during interactive or print sessions; only `step session-index` and hosts that call `openSessionIndex()` do | +| Roots | `/sessions` and any directory added with `addRoot()` or found holding a persisted file | +| Ownership check | `PRAGMA application_id` (`STCX`); a file without our id is never modified and the index runs degraded | +| Damaged file | Unreadable, missing tables, or `schema_major` disagreeing with the name: moved aside as `session-search.v1.db.corrupt-