diff --git a/docs/public/llms-full.txt b/docs/public/llms-full.txt index 48b0c0d5..742ade51 100644 --- a/docs/public/llms-full.txt +++ b/docs/public/llms-full.txt @@ -4128,9 +4128,10 @@ Read the Claude Code session transcript for the project. archgate session-context claude-code [options] ``` -| Option | Description | -| ------------------- | ---------------------------------------- | -| `--max-entries ` | Maximum entries to return (default: 200) | +| Option | Description | +| ------------------- | ------------------------------------------------------------------ | +| `--max-entries ` | Maximum entries to return (default: 200) | +| `--skip ` | Skip the N most recent sessions (useful when running as sub-agent) | ### archgate session-context copilot @@ -4140,10 +4141,11 @@ Read the Copilot CLI session transcript for the project. Sessions are matched by archgate session-context copilot [options] ``` -| Option | Description | -| ------------------- | ---------------------------------------- | -| `--max-entries ` | Maximum entries to return (default: 200) | -| `--session-id ` | Specific session UUID to read | +| Option | Description | +| ------------------- | ------------------------------------------------------------------ | +| `--max-entries ` | Maximum entries to return (default: 200) | +| `--skip ` | Skip the N most recent sessions (useful when running as sub-agent) | +| `--session-id ` | Specific session UUID to read | ### archgate session-context cursor @@ -4153,10 +4155,11 @@ Read the Cursor agent session transcript for the project. archgate session-context cursor [options] ``` -| Option | Description | -| ------------------- | ---------------------------------------- | -| `--max-entries ` | Maximum entries to return (default: 200) | -| `--session-id ` | Specific session UUID to read | +| Option | Description | +| ------------------- | ------------------------------------------------------------------ | +| `--max-entries ` | Maximum entries to return (default: 200) | +| `--skip ` | Skip the N most recent sessions (useful when running as sub-agent) | +| `--session-id ` | Specific session UUID to read | ### archgate session-context opencode @@ -4166,10 +4169,11 @@ Read the opencode session transcript for the project. Sessions are matched by th archgate session-context opencode [options] ``` -| Option | Description | -| ------------------- | ---------------------------------------- | -| `--max-entries ` | Maximum entries to return (default: 200) | -| `--session-id ` | Specific session ID to read | +| Option | Description | +| ------------------- | ------------------------------------------------------------------ | +| `--max-entries ` | Maximum entries to return (default: 200) | +| `--skip ` | Skip the N most recent sessions (useful when running as sub-agent) | +| `--session-id ` | Specific session ID to read | ## Examples @@ -4197,6 +4201,12 @@ Read the latest opencode session: archgate session-context opencode ``` +Read the parent session (skip the sub-agent's own session): + +```bash +archgate session-context claude-code --skip 1 +``` + --- ## Reference: archgate telemetry diff --git a/docs/src/content/docs/pt-br/reference/cli/session-context.mdx b/docs/src/content/docs/pt-br/reference/cli/session-context.mdx index 88a14092..23afb3c0 100644 --- a/docs/src/content/docs/pt-br/reference/cli/session-context.mdx +++ b/docs/src/content/docs/pt-br/reference/cli/session-context.mdx @@ -19,9 +19,10 @@ Lê a transcrição de sessão do Claude Code para o projeto. archgate session-context claude-code [options] ``` -| Opção | Descrição | -| ------------------- | ------------------------------------------- | -| `--max-entries ` | Máximo de entradas a retornar (padrão: 200) | +| Opção | Descrição | +| ------------------- | ------------------------------------------------------------------- | +| `--max-entries ` | Máximo de entradas a retornar (padrão: 200) | +| `--skip ` | Pular as N sessões mais recentes (útil ao executar como sub-agente) | ### archgate session-context copilot @@ -31,10 +32,11 @@ Lê a transcrição de sessão do Copilot CLI para o projeto. Sessões são corr archgate session-context copilot [options] ``` -| Opção | Descrição | -| ------------------- | ------------------------------------------- | -| `--max-entries ` | Máximo de entradas a retornar (padrão: 200) | -| `--session-id ` | UUID específico da sessão a ser lida | +| Opção | Descrição | +| ------------------- | ------------------------------------------------------------------- | +| `--max-entries ` | Máximo de entradas a retornar (padrão: 200) | +| `--skip ` | Pular as N sessões mais recentes (útil ao executar como sub-agente) | +| `--session-id ` | UUID específico da sessão a ser lida | ### archgate session-context cursor @@ -44,10 +46,11 @@ Lê a transcrição de sessão do agente Cursor para o projeto. archgate session-context cursor [options] ``` -| Opção | Descrição | -| ------------------- | ------------------------------------------- | -| `--max-entries ` | Máximo de entradas a retornar (padrão: 200) | -| `--session-id ` | UUID específico da sessão a ser lida | +| Opção | Descrição | +| ------------------- | ------------------------------------------------------------------- | +| `--max-entries ` | Máximo de entradas a retornar (padrão: 200) | +| `--skip ` | Pular as N sessões mais recentes (útil ao executar como sub-agente) | +| `--session-id ` | UUID específico da sessão a ser lida | ### archgate session-context opencode @@ -57,10 +60,11 @@ Lê a transcrição de sessão do opencode para o projeto. Sessões são corresp archgate session-context opencode [options] ``` -| Opção | Descrição | -| ------------------- | ------------------------------------------- | -| `--max-entries ` | Máximo de entradas a retornar (padrão: 200) | -| `--session-id ` | ID específico da sessão a ser lida | +| Opção | Descrição | +| ------------------- | ------------------------------------------------------------------- | +| `--max-entries ` | Máximo de entradas a retornar (padrão: 200) | +| `--skip ` | Pular as N sessões mais recentes (útil ao executar como sub-agente) | +| `--session-id ` | ID específico da sessão a ser lida | ## Exemplos @@ -87,3 +91,9 @@ Ler a última sessão do opencode: ```bash archgate session-context opencode ``` + +Ler a sessão pai (pular a sessão do sub-agente): + +```bash +archgate session-context claude-code --skip 1 +``` diff --git a/docs/src/content/docs/reference/cli/session-context.mdx b/docs/src/content/docs/reference/cli/session-context.mdx index 62a31e8e..624a9d6a 100644 --- a/docs/src/content/docs/reference/cli/session-context.mdx +++ b/docs/src/content/docs/reference/cli/session-context.mdx @@ -19,9 +19,10 @@ Read the Claude Code session transcript for the project. archgate session-context claude-code [options] ``` -| Option | Description | -| ------------------- | ---------------------------------------- | -| `--max-entries ` | Maximum entries to return (default: 200) | +| Option | Description | +| ------------------- | ------------------------------------------------------------------ | +| `--max-entries ` | Maximum entries to return (default: 200) | +| `--skip ` | Skip the N most recent sessions (useful when running as sub-agent) | ### archgate session-context copilot @@ -31,10 +32,11 @@ Read the Copilot CLI session transcript for the project. Sessions are matched by archgate session-context copilot [options] ``` -| Option | Description | -| ------------------- | ---------------------------------------- | -| `--max-entries ` | Maximum entries to return (default: 200) | -| `--session-id ` | Specific session UUID to read | +| Option | Description | +| ------------------- | ------------------------------------------------------------------ | +| `--max-entries ` | Maximum entries to return (default: 200) | +| `--skip ` | Skip the N most recent sessions (useful when running as sub-agent) | +| `--session-id ` | Specific session UUID to read | ### archgate session-context cursor @@ -44,10 +46,11 @@ Read the Cursor agent session transcript for the project. archgate session-context cursor [options] ``` -| Option | Description | -| ------------------- | ---------------------------------------- | -| `--max-entries ` | Maximum entries to return (default: 200) | -| `--session-id ` | Specific session UUID to read | +| Option | Description | +| ------------------- | ------------------------------------------------------------------ | +| `--max-entries ` | Maximum entries to return (default: 200) | +| `--skip ` | Skip the N most recent sessions (useful when running as sub-agent) | +| `--session-id ` | Specific session UUID to read | ### archgate session-context opencode @@ -57,10 +60,11 @@ Read the opencode session transcript for the project. Sessions are matched by th archgate session-context opencode [options] ``` -| Option | Description | -| ------------------- | ---------------------------------------- | -| `--max-entries ` | Maximum entries to return (default: 200) | -| `--session-id ` | Specific session ID to read | +| Option | Description | +| ------------------- | ------------------------------------------------------------------ | +| `--max-entries ` | Maximum entries to return (default: 200) | +| `--skip ` | Skip the N most recent sessions (useful when running as sub-agent) | +| `--session-id ` | Specific session ID to read | ## Examples @@ -87,3 +91,9 @@ Read the latest opencode session: ```bash archgate session-context opencode ``` + +Read the parent session (skip the sub-agent's own session): + +```bash +archgate session-context claude-code --skip 1 +``` diff --git a/src/commands/session-context/claude-code.ts b/src/commands/session-context/claude-code.ts index 7e7fea01..77283596 100644 --- a/src/commands/session-context/claude-code.ts +++ b/src/commands/session-context/claude-code.ts @@ -14,16 +14,25 @@ const maxEntriesOption = new Option( "maximum entries to return (default: 200)" ).argParser((val) => parseInt(val, 10)); +const skipOption = new Option( + "--skip ", + "skip the N most recent sessions (useful when running as a sub-agent)" +) + .argParser((val) => parseInt(val, 10)) + .default(0); + export function registerClaudeCodeSessionContextCommand(parent: Command) { parent .command("claude-code") .description("Read Claude Code session transcript for the project") .addOption(maxEntriesOption) + .addOption(skipOption) .action(async (opts) => { try { const projectRoot = findProjectRoot(); const result = await readClaudeCodeSession(projectRoot, { maxEntries: opts.maxEntries, + skip: opts.skip, }); if (!result.ok) { diff --git a/src/commands/session-context/copilot.ts b/src/commands/session-context/copilot.ts index bb99881e..6e463165 100644 --- a/src/commands/session-context/copilot.ts +++ b/src/commands/session-context/copilot.ts @@ -14,17 +14,26 @@ const maxEntriesOption = new Option( "maximum entries to return (default: 200)" ).argParser((val) => parseInt(val, 10)); +const skipOption = new Option( + "--skip ", + "skip the N most recent sessions (useful when running as a sub-agent)" +) + .argParser((val) => parseInt(val, 10)) + .default(0); + export function registerCopilotSessionContextCommand(parent: Command) { parent .command("copilot") .description("Read Copilot CLI session transcript for the project") .addOption(maxEntriesOption) + .addOption(skipOption) .option("--session-id ", "Specific session UUID to read") .action(async (opts) => { try { const projectRoot = findProjectRoot(); const result = await readCopilotSession(projectRoot, { maxEntries: opts.maxEntries, + skip: opts.skip, sessionId: opts.sessionId, }); diff --git a/src/commands/session-context/cursor.ts b/src/commands/session-context/cursor.ts index 31e11383..03356b9f 100644 --- a/src/commands/session-context/cursor.ts +++ b/src/commands/session-context/cursor.ts @@ -14,17 +14,26 @@ const maxEntriesOption = new Option( "maximum entries to return (default: 200)" ).argParser((val) => parseInt(val, 10)); +const skipOption = new Option( + "--skip ", + "skip the N most recent sessions (useful when running as a sub-agent)" +) + .argParser((val) => parseInt(val, 10)) + .default(0); + export function registerCursorSessionContextCommand(parent: Command) { parent .command("cursor") .description("Read Cursor agent session transcript for the project") .addOption(maxEntriesOption) + .addOption(skipOption) .option("--session-id ", "Specific session UUID to read") .action(async (opts) => { try { const projectRoot = findProjectRoot(); const result = await readCursorSession(projectRoot, { maxEntries: opts.maxEntries, + skip: opts.skip, sessionId: opts.sessionId, }); diff --git a/src/commands/session-context/opencode.ts b/src/commands/session-context/opencode.ts index f8e98e9a..afccc54b 100644 --- a/src/commands/session-context/opencode.ts +++ b/src/commands/session-context/opencode.ts @@ -14,17 +14,26 @@ const maxEntriesOption = new Option( "maximum entries to return (default: 200)" ).argParser((val) => parseInt(val, 10)); +const skipOption = new Option( + "--skip ", + "skip the N most recent sessions (useful when running as a sub-agent)" +) + .argParser((val) => parseInt(val, 10)) + .default(0); + export function registerOpencodeSessionContextCommand(parent: Command) { parent .command("opencode") .description("Read opencode session transcript for the project") .addOption(maxEntriesOption) + .addOption(skipOption) .option("--session-id ", "Specific session ID to read") .action(async (opts) => { try { const projectRoot = findProjectRoot(); const result = await readOpencodeSession(projectRoot, { maxEntries: opts.maxEntries, + skip: opts.skip, sessionId: opts.sessionId, }); diff --git a/src/helpers/session-context-copilot.ts b/src/helpers/session-context-copilot.ts index 73e926f8..dcf5c856 100644 --- a/src/helpers/session-context-copilot.ts +++ b/src/helpers/session-context-copilot.ts @@ -119,17 +119,17 @@ export async function readCopilotSession( }; } - // 3. Select session by ID or most recent + // 3. Select session by ID or most recent (with optional skip) + const skip = options?.skip ?? 0; const target = options?.sessionId ? matching.find((s) => s.name === options.sessionId) - : matching[0]; + : matching[skip]; if (!target) { - return { - ok: false, - error: `Session not found: ${options?.sessionId}`, - available: matching.map((s) => s.name), - }; + const error = options?.sessionId + ? `Session not found: ${options.sessionId}` + : `Only ${String(matching.length)} session(s) available but --skip ${String(skip)} requested`; + return { ok: false, error, available: matching.map((s) => s.name) }; } // 4. Read events.jsonl diff --git a/src/helpers/session-context-opencode.ts b/src/helpers/session-context-opencode.ts index fbc0d9fd..f4311df5 100644 --- a/src/helpers/session-context-opencode.ts +++ b/src/helpers/session-context-opencode.ts @@ -105,17 +105,17 @@ export function readOpencodeSession( }; } - // 3. Select session by ID or most recent + // 3. Select session by ID or most recent (with optional skip) + const skip = options?.skip ?? 0; const target = options?.sessionId ? matching.find((s) => s.id === options.sessionId) - : matching[0]; + : matching[skip]; if (!target) { - return { - ok: false, - error: `Session not found: ${options?.sessionId}`, - available: matching.map((s) => s.id), - }; + const error = options?.sessionId + ? `Session not found: ${options.sessionId}` + : `Only ${String(matching.length)} session(s) available but --skip ${String(skip)} requested`; + return { ok: false, error, available: matching.map((s) => s.id) }; } // 4. Read messages for the session diff --git a/src/helpers/session-context.ts b/src/helpers/session-context.ts index 9cbe44e8..dcb97f15 100644 --- a/src/helpers/session-context.ts +++ b/src/helpers/session-context.ts @@ -109,6 +109,12 @@ export function getContentPreview(entry: TranscriptEntry): string { export interface ReadSessionOptions { maxEntries?: number; + /** + * Skip the N most recent sessions before selecting the one to read. + * Useful when running as a sub-agent: the sub-agent's own session is + * the most recent, so `skip: 1` reads the parent session instead. + */ + skip?: number; } interface ReadCursorSessionOptions extends ReadSessionOptions { @@ -146,7 +152,16 @@ export async function readClaudeCodeSession( }; } - const sessionFile = join(projectsDir, files[0]); + const skip = options?.skip ?? 0; + if (skip >= files.length) { + return { + ok: false, + error: `Only ${String(files.length)} session(s) available but --skip ${String(skip)} requested`, + path: projectsDir, + }; + } + + const sessionFile = join(projectsDir, files[skip]); let entries: TranscriptEntry[]; try { const raw = await Bun.file(sessionFile).text(); @@ -232,16 +247,16 @@ export async function readCursorSession( }; } + const skip = options?.skip ?? 0; const targetDir = options?.sessionId ? sessionDirs.find((d) => d.name === options.sessionId) - : sessionDirs[0]; + : sessionDirs[skip]; if (!targetDir) { - return { - ok: false, - error: `Session not found: ${options?.sessionId}`, - available: sessionDirs.map((d) => d.name), - }; + const error = options?.sessionId + ? `Session not found: ${options.sessionId}` + : `Only ${String(sessionDirs.length)} session(s) available but --skip ${String(skip)} requested`; + return { ok: false, error, available: sessionDirs.map((d) => d.name) }; } const sessionFile = join( diff --git a/tests/commands/session-context.test.ts b/tests/commands/session-context.test.ts index f4158f3f..f98b8afd 100644 --- a/tests/commands/session-context.test.ts +++ b/tests/commands/session-context.test.ts @@ -61,7 +61,7 @@ describe("registerSessionContextCommand", () => { expect(sub).toBeDefined(); }); - test("claude-code subcommand has --max-entries option", () => { + test("claude-code subcommand has --max-entries and --skip options", () => { const program = new Command(); registerSessionContextCommand(program); const parent = program.commands.find( @@ -70,9 +70,10 @@ describe("registerSessionContextCommand", () => { const sub = parent.commands.find((c) => c.name() === "claude-code")!; const opts = sub.options.map((o) => o.long); expect(opts).toContain("--max-entries"); + expect(opts).toContain("--skip"); }); - test("cursor subcommand has --max-entries and --session-id options", () => { + test("cursor subcommand has --max-entries, --skip, and --session-id options", () => { const program = new Command(); registerSessionContextCommand(program); const parent = program.commands.find( @@ -81,10 +82,11 @@ describe("registerSessionContextCommand", () => { const sub = parent.commands.find((c) => c.name() === "cursor")!; const opts = sub.options.map((o) => o.long); expect(opts).toContain("--max-entries"); + expect(opts).toContain("--skip"); expect(opts).toContain("--session-id"); }); - test("copilot subcommand has --max-entries and --session-id options", () => { + test("copilot subcommand has --max-entries, --skip, and --session-id options", () => { const program = new Command(); registerSessionContextCommand(program); const parent = program.commands.find( @@ -93,10 +95,11 @@ describe("registerSessionContextCommand", () => { const sub = parent.commands.find((c) => c.name() === "copilot")!; const opts = sub.options.map((o) => o.long); expect(opts).toContain("--max-entries"); + expect(opts).toContain("--skip"); expect(opts).toContain("--session-id"); }); - test("opencode subcommand has --max-entries and --session-id options", () => { + test("opencode subcommand has --max-entries, --skip, and --session-id options", () => { const program = new Command(); registerSessionContextCommand(program); const parent = program.commands.find( @@ -105,6 +108,7 @@ describe("registerSessionContextCommand", () => { const sub = parent.commands.find((c) => c.name() === "opencode")!; const opts = sub.options.map((o) => o.long); expect(opts).toContain("--max-entries"); + expect(opts).toContain("--skip"); expect(opts).toContain("--session-id"); }); }); diff --git a/tests/commands/session-context/claude-code.test.ts b/tests/commands/session-context/claude-code.test.ts index 99dc32b9..d24cca25 100644 --- a/tests/commands/session-context/claude-code.test.ts +++ b/tests/commands/session-context/claude-code.test.ts @@ -176,6 +176,7 @@ describe("claude-code action handler", () => { // findProjectRoot found our tempDir (which has .archgate/) expect(mockReadClaudeCodeSession).toHaveBeenCalledWith(tempDir, { maxEntries: undefined, + skip: 0, }); }); }); diff --git a/tests/commands/session-context/copilot.test.ts b/tests/commands/session-context/copilot.test.ts index fab1e31f..d25cc2ed 100644 --- a/tests/commands/session-context/copilot.test.ts +++ b/tests/commands/session-context/copilot.test.ts @@ -176,6 +176,7 @@ describe("copilot action handler", () => { expect(mockReadCopilotSession).toHaveBeenCalledWith(tempDir, { maxEntries: undefined, + skip: 0, sessionId: undefined, }); }); diff --git a/tests/commands/session-context/cursor.test.ts b/tests/commands/session-context/cursor.test.ts index 88ad74c3..0ff55ff9 100644 --- a/tests/commands/session-context/cursor.test.ts +++ b/tests/commands/session-context/cursor.test.ts @@ -176,6 +176,7 @@ describe("cursor action handler", () => { expect(mockReadCursorSession).toHaveBeenCalledWith(tempDir, { maxEntries: undefined, + skip: 0, sessionId: undefined, }); }); diff --git a/tests/commands/session-context/opencode.test.ts b/tests/commands/session-context/opencode.test.ts index fc424b31..d59b4930 100644 --- a/tests/commands/session-context/opencode.test.ts +++ b/tests/commands/session-context/opencode.test.ts @@ -178,6 +178,7 @@ describe("opencode action handler", () => { expect(mockReadOpencodeSession).toHaveBeenCalledWith(tempDir, { maxEntries: undefined, + skip: 0, sessionId: undefined, }); }); diff --git a/tests/helpers/session-context-copilot.test.ts b/tests/helpers/session-context-copilot.test.ts index d35abd99..4cefa479 100644 --- a/tests/helpers/session-context-copilot.test.ts +++ b/tests/helpers/session-context-copilot.test.ts @@ -233,6 +233,66 @@ describe("readCopilotSession", () => { expect(result.ok).toBe(false); }); + test("skip option reads the second-most-recent matching session", async () => { + // Create parent session (make it older by creating first) + const parentId = `copilot-${uniqueId}-parent`; + makeSession(parentId, projectRoot, [ + JSON.stringify({ + type: "user.message", + data: { content: "parent question" }, + }), + JSON.stringify({ + type: "assistant.message", + data: { content: "parent answer" }, + }), + ]); + + // Make parent dir older so sub-agent dir is most recent + const { utimesSync } = await import("node:fs"); + const past = new Date(Date.now() - 60_000); + utimesSync(join(stateDir, parentId), past, past); + + // Create sub-agent session (newer) + const subagentId = `copilot-${uniqueId}-subagent`; + makeSession(subagentId, projectRoot, [ + JSON.stringify({ + type: "user.message", + data: { content: "sub-agent init" }, + }), + ]); + + // Without skip → reads sub-agent session (most recent) + const resultNoSkip = await readCopilotSession(projectRoot); + expect(resultNoSkip.ok).toBe(true); + if (!resultNoSkip.ok) throw new Error("expected ok"); + expect(resultNoSkip.data.sessionId).toBe(subagentId); + + // With skip=1 → reads parent session + const resultSkip = await readCopilotSession(projectRoot, { skip: 1 }); + expect(resultSkip.ok).toBe(true); + if (!resultSkip.ok) throw new Error("expected ok"); + expect(resultSkip.data.sessionId).toBe(parentId); + expect(resultSkip.data.transcript[0]?.contentPreview).toBe( + "parent question" + ); + }); + + test("skip beyond available matching sessions returns error", async () => { + const sessionId = `copilot-${uniqueId}-onlysession`; + makeSession(sessionId, projectRoot, [ + JSON.stringify({ + type: "user.message", + data: { content: "only session" }, + }), + ]); + + const result = await readCopilotSession(projectRoot, { skip: 2 }); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toContain("--skip 2 requested"); + } + }); + test("truncates string content preview to 500 chars", async () => { const sessionId = `copilot-${uniqueId}-truncate`; makeSession(sessionId, projectRoot, [ diff --git a/tests/helpers/session-context-cursor.test.ts b/tests/helpers/session-context-cursor.test.ts index 5493d194..eefd1fed 100644 --- a/tests/helpers/session-context-cursor.test.ts +++ b/tests/helpers/session-context-cursor.test.ts @@ -198,6 +198,63 @@ describe("readCursorSession", () => { expect(result.data.transcript[1]?.contentPreview).toBe("msg 7"); }); + test("skip option reads the second-most-recent session directory", async () => { + // Create parent session (make it older) + makeSession("session-parent", [ + JSON.stringify({ + role: "user", + message: { role: "user", content: "parent question" }, + }), + JSON.stringify({ + role: "assistant", + message: { role: "assistant", content: "parent answer" }, + }), + ]); + + // Make parent dir older + const { utimesSync } = await import("node:fs"); + const past = new Date(Date.now() - 60_000); + utimesSync(join(transcriptsDir, "session-parent"), past, past); + + // Create sub-agent session (newer) + makeSession("session-subagent", [ + JSON.stringify({ + role: "user", + message: { role: "user", content: "sub-agent init" }, + }), + ]); + + // Without skip → reads sub-agent session (most recent) + const resultNoSkip = await readCursorSession(projectRoot); + expect(resultNoSkip.ok).toBe(true); + if (!resultNoSkip.ok) throw new Error("expected ok"); + expect(resultNoSkip.data.sessionId).toBe("session-subagent"); + + // With skip=1 → reads parent session + const resultSkip = await readCursorSession(projectRoot, { skip: 1 }); + expect(resultSkip.ok).toBe(true); + if (!resultSkip.ok) throw new Error("expected ok"); + expect(resultSkip.data.sessionId).toBe("session-parent"); + expect(resultSkip.data.transcript[0]?.contentPreview).toBe( + "parent question" + ); + }); + + test("skip beyond available sessions returns error", async () => { + makeSession("session-only", [ + JSON.stringify({ + role: "user", + message: { role: "user", content: "only session" }, + }), + ]); + + const result = await readCursorSession(projectRoot, { skip: 2 }); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toContain("--skip 2 requested"); + } + }); + test("ignores non-directory entries in transcripts dir", async () => { // Put a plain file in the transcripts dir — it should be skipped writeFileSync(join(transcriptsDir, "stray-file.txt"), "noise"); diff --git a/tests/helpers/session-context-opencode.test.ts b/tests/helpers/session-context-opencode.test.ts index 54c4e37c..705fcb65 100644 --- a/tests/helpers/session-context-opencode.test.ts +++ b/tests/helpers/session-context-opencode.test.ts @@ -393,6 +393,49 @@ describe("readOpencodeSession", () => { ); }); + test("skip=1 reads the parent session instead of the sub-agent session", async () => { + const db = createDb(); + makeSession( + db, + "ses_parent", + projectRoot, + [{ id: "msg_p1", role: "user", content: "parent question" }], + 1000 + ); + makeSession( + db, + "ses_subagent", + projectRoot, + [{ id: "msg_s1", role: "user", content: "sub-agent init" }], + 2000 + ); + db.close(); + + const noSkip = await readOpencodeSession(projectRoot); + expect(noSkip.ok).toBe(true); + if (noSkip.ok) expect(noSkip.data.sessionId).toBe("ses_subagent"); + + const skipped = await readOpencodeSession(projectRoot, { skip: 1 }); + expect(skipped.ok).toBe(true); + if (skipped.ok) expect(skipped.data.sessionId).toBe("ses_parent"); + }); + + test("skip beyond available matching sessions returns error", async () => { + const db = createDb(); + makeSession( + db, + "ses_only", + projectRoot, + [{ id: "msg_001", role: "user", content: "only" }], + 1000 + ); + db.close(); + + const result = await readOpencodeSession(projectRoot, { skip: 3 }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toContain("--skip 3 requested"); + }); + test("includes tool parts as [tool: name]", async () => { const db = createDb(); const sessionId = `ses_${uniqueId}_tools`; diff --git a/tests/helpers/session-context.test.ts b/tests/helpers/session-context.test.ts index 78fd87a0..d0099c7c 100644 --- a/tests/helpers/session-context.test.ts +++ b/tests/helpers/session-context.test.ts @@ -243,6 +243,67 @@ describe("readClaudeCodeSession", () => { expect(result.data.transcript[2]?.contentPreview).toBe("message 9"); }); + test("skip option reads the second-most-recent session file", async () => { + // Write a newer session file (the sub-agent's session) + writeFileSync( + join(projectsDir, "subagent.jsonl"), + [ + JSON.stringify({ + type: "user", + message: { role: "user", content: "sub-agent msg" }, + }), + ].join("\n") + ); + + // Write an older session file (the parent's session) + const olderFile = join(projectsDir, "parent.jsonl"); + writeFileSync( + olderFile, + [ + JSON.stringify({ + type: "user", + message: { role: "user", content: "parent msg" }, + }), + JSON.stringify({ + type: "assistant", + message: { role: "assistant", content: "parent reply" }, + }), + ].join("\n") + ); + + // Make parent older than subagent by backdating its mtime + const { utimesSync } = await import("node:fs"); + const past = new Date(Date.now() - 60_000); + utimesSync(olderFile, past, past); + + // Without skip → reads subagent (most recent) + const resultNoSkip = await readClaudeCodeSession(projectRoot); + expect(resultNoSkip.ok).toBe(true); + if (!resultNoSkip.ok) throw new Error("expected ok"); + expect(resultNoSkip.data.transcript[0]?.contentPreview).toBe( + "sub-agent msg" + ); + + // With skip=1 → reads parent session + const resultSkip = await readClaudeCodeSession(projectRoot, { skip: 1 }); + expect(resultSkip.ok).toBe(true); + if (!resultSkip.ok) throw new Error("expected ok"); + expect(resultSkip.data.transcript[0]?.contentPreview).toBe("parent msg"); + expect(resultSkip.data.relevantEntries).toBe(2); + }); + + test("skip beyond available sessions returns error", async () => { + writeSession([ + { type: "user", message: { role: "user", content: "only session" } }, + ]); + + const result = await readClaudeCodeSession(projectRoot, { skip: 5 }); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toContain("--skip 5 requested"); + } + }); + test("returns error when directory exists but has no .jsonl files", async () => { writeFileSync(join(projectsDir, "notes.txt"), "not a session");