Skip to content

Commit 29ee011

Browse files
committed
feat(server): add transcript path groundwork
1 parent 370518f commit 29ee011

12 files changed

Lines changed: 143 additions & 35 deletions

File tree

packages/core/src/domain/types.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, expectTypeOf, it } from "vitest";
2-
import type { SessionState } from "./types";
2+
import type { ProviderDefinition } from "../provider/definition";
3+
import type { Session, SessionState } from "./types";
34
import { deriveSessionTitle, SESSION_TITLE_MAX_LENGTH } from "./types";
45

56
describe("deriveSessionTitle", () => {
@@ -42,3 +43,30 @@ describe("SessionState", () => {
4243
>();
4344
});
4445
});
46+
47+
describe("notify-hook groundwork types", () => {
48+
it("allows sessions to carry an optional transcript path", () => {
49+
expectTypeOf<Session>().toMatchTypeOf<{
50+
transcriptPath?: string;
51+
}>();
52+
});
53+
54+
it("allows provider definitions to opt into future resume and transcript helpers", () => {
55+
expectTypeOf<ProviderDefinition>().toMatchTypeOf<{
56+
buildResumeCommand?: (
57+
config: unknown,
58+
ctx: { sessionId: string; workspacePath: string; bridgeScriptPath?: string },
59+
resumeId: string
60+
) => {
61+
argv: string[];
62+
env: Record<string, string>;
63+
cwd: string;
64+
};
65+
resolveTranscriptPath?: (session: {
66+
id: string;
67+
transcriptPath?: string;
68+
providerSessionId?: string;
69+
}) => Promise<string | undefined> | string | undefined;
70+
}>();
71+
});
72+
});

packages/core/src/domain/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export interface Session {
6666
endedAt?: number;
6767
completionPercent?: number;
6868
errorReason?: string;
69+
transcriptPath?: string;
6970
/**
7071
* Human-friendly title derived from the user's first submitted instruction
7172
* (trimmed/truncated to SESSION_TITLE_MAX_LENGTH). Assigned once on first

packages/core/src/provider/definition.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@ export interface ProviderDefinition {
4949
cwd: string;
5050
};
5151

52+
buildResumeCommand?(
53+
config: ProviderConfig,
54+
ctx: LaunchContext,
55+
resumeId: string
56+
): {
57+
argv: string[];
58+
env: Record<string, string>;
59+
cwd: string;
60+
};
61+
5262
buildSupervisorEvalCommand?(
5363
config: ProviderConfig,
5464
req: SupervisorEvalCommandRequest
@@ -66,11 +76,18 @@ export interface ProviderDefinition {
6676
// Runtime requirements
6777
requiredCommands: string[];
6878

79+
resolveTranscriptPath?(session: {
80+
id: string;
81+
transcriptPath?: string;
82+
providerSessionId?: string;
83+
}): Promise<string | undefined> | string | undefined;
84+
6985
/** PTY-output-based idle detection used by the session manager. */
7086
idleHeuristics?: IdleHeuristics;
7187
}
7288

7389
export interface LaunchContext {
7490
sessionId: string;
7591
workspacePath: string;
92+
bridgeScriptPath?: string;
7693
}

packages/server/src/__tests__/session-integration.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,32 @@ describe("Session Integration", () => {
934934
);
935935
});
936936

937+
it("preserves persisted transcript path when hydrating a stale session", async () => {
938+
sessionDb.listHydratable = vi.fn().mockReturnValue([
939+
{
940+
id: "sess-hydrate-transcript",
941+
workspaceId: "ws-1",
942+
terminalId: "term-stale",
943+
providerId: "claude",
944+
state: "running",
945+
capability: "full",
946+
startedAt: 100,
947+
lastActiveAt: 200,
948+
transcriptPath: "/tmp/transcripts/sess-hydrate-transcript.jsonl",
949+
},
950+
]);
951+
952+
await sessionMgr.hydrate();
953+
954+
expect(sessionMgr.get("sess-hydrate-transcript")).toEqual(
955+
expect.objectContaining({
956+
id: "sess-hydrate-transcript",
957+
state: "ended",
958+
transcriptPath: "/tmp/transcripts/sess-hydrate-transcript.jsonl",
959+
})
960+
);
961+
});
962+
937963
it("marks persisted non-resumable sessions as ended when hydrating without a live terminal", async () => {
938964
sessionDb.listHydratable = vi.fn().mockReturnValue([
939965
{

packages/server/src/__tests__/session-repo.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,24 @@ describe("SessionRepo", () => {
100100

101101
expect(result.completionPercent).toBe(50);
102102
});
103+
104+
it("should persist and read transcript path", () => {
105+
const newSession: NewSession = {
106+
id: "s-transcript",
107+
workspaceId: "ws-1",
108+
terminalId: "t-1",
109+
providerId: "claude-cli",
110+
state: "running",
111+
capability: "full",
112+
startedAt: Date.now(),
113+
lastActiveAt: Date.now(),
114+
transcriptPath: "/tmp/transcripts/s-transcript.jsonl",
115+
};
116+
117+
const result = repo.create(newSession);
118+
119+
expect(result.transcriptPath).toBe("/tmp/transcripts/s-transcript.jsonl");
120+
});
103121
});
104122

105123
describe("listByWorkspace", () => {

packages/server/src/server.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -296,8 +296,21 @@ function createSessionDatabase(db: Database) {
296296
return {
297297
insert: (session: SessionRow) => {
298298
db.prepare(`
299-
INSERT INTO sessions (id, workspace_id, terminal_id, provider_id, state, capability, started_at, last_active_at)
300-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
299+
INSERT INTO sessions (
300+
id,
301+
workspace_id,
302+
terminal_id,
303+
provider_id,
304+
state,
305+
capability,
306+
started_at,
307+
last_active_at,
308+
completion_percent,
309+
error_reason,
310+
transcript_path,
311+
title
312+
)
313+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
301314
`).run(
302315
session.id,
303316
session.workspace_id,
@@ -306,7 +319,11 @@ function createSessionDatabase(db: Database) {
306319
session.state,
307320
session.capability,
308321
session.started_at,
309-
session.last_active_at
322+
session.last_active_at,
323+
session.completion_percent,
324+
session.error_reason,
325+
session.transcript_path,
326+
session.title
310327
);
311328
},
312329
update: (id: string, patch: Record<string, unknown>) => {
@@ -320,6 +337,7 @@ function createSessionDatabase(db: Database) {
320337
"ended_at",
321338
"completion_percent",
322339
"error_reason",
340+
"transcript_path",
323341
"last_active_at",
324342
"title",
325343
]);

packages/server/src/session/manager.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ export class SessionManager {
190190
endedAt: session.endedAt,
191191
completionPercent: session.completionPercent,
192192
errorReason: session.errorReason,
193+
transcriptPath: session.transcriptPath,
193194
});
194195

195196
this.sessions.set(session.id, hydrated);
@@ -668,6 +669,7 @@ class ActiveSession {
668669
endedAt?: number;
669670
completionPercent?: number;
670671
errorReason?: string;
672+
transcriptPath?: string;
671673
exitCode?: number;
672674
draft?: string;
673675
title?: string;
@@ -689,6 +691,7 @@ class ActiveSession {
689691
endedAt?: number;
690692
completionPercent?: number;
691693
errorReason?: string;
694+
transcriptPath?: string;
692695
}) {
693696
this.id = data.id;
694697
this.workspaceId = data.workspaceId;
@@ -703,6 +706,7 @@ class ActiveSession {
703706
this.endedAt = data.endedAt;
704707
this.completionPercent = data.completionPercent;
705708
this.errorReason = data.errorReason;
709+
this.transcriptPath = data.transcriptPath;
706710
}
707711

708712
toDTO(): Session {
@@ -718,6 +722,7 @@ class ActiveSession {
718722
endedAt: this.endedAt,
719723
completionPercent: this.completionPercent,
720724
errorReason: this.errorReason,
725+
transcriptPath: this.transcriptPath,
721726
title: this.title,
722727
};
723728
}

packages/server/src/session/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface SessionUpdatePatch {
1515
endedAt?: number;
1616
completionPercent?: number;
1717
errorReason?: string;
18+
transcriptPath?: string;
1819
lastActiveAt?: number;
1920
title?: string;
2021
}

packages/server/src/storage/db.test.ts

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ describe("database schema baseline", () => {
4949
name: string;
5050
}>;
5151
expect(sessionColumns.find((column) => column.name === "resume_id")).toBeUndefined();
52-
expect(sessionColumns.find((column) => column.name === "transcript_path")).toBeUndefined();
52+
expect(sessionColumns.find((column) => column.name === "transcript_path")).toBeDefined();
5353
expect(sessionColumns.find((column) => column.name === "title")).toBeDefined();
5454

5555
const indexNames = (
@@ -120,32 +120,6 @@ describe("database schema baseline", () => {
120120
expect(() => openDatabase(dbPath)).toThrow(/Legacy database schema detected/);
121121
});
122122

123-
it("rejects a legacy database schema that still contains transcript_path", async () => {
124-
const { openDatabase } = await import("./db");
125-
126-
const db = new DatabaseSync(dbPath);
127-
db.exec(`
128-
CREATE TABLE sessions (
129-
id TEXT PRIMARY KEY,
130-
workspace_id TEXT NOT NULL,
131-
terminal_id TEXT NOT NULL,
132-
provider_id TEXT NOT NULL,
133-
capability TEXT NOT NULL,
134-
state TEXT NOT NULL,
135-
started_at INTEGER NOT NULL,
136-
ended_at INTEGER,
137-
last_active_at INTEGER NOT NULL,
138-
completion_percent INTEGER,
139-
error_reason TEXT,
140-
archived BOOLEAN DEFAULT 0,
141-
transcript_path TEXT
142-
);
143-
`);
144-
db.close();
145-
146-
expect(() => openDatabase(dbPath)).toThrow(/Legacy database schema detected/);
147-
});
148-
149123
it("rejects a legacy database schema that still contains _migrations", async () => {
150124
const { openDatabase } = await import("./db");
151125

@@ -207,6 +181,7 @@ describe("database schema baseline", () => {
207181
last_active_at INTEGER NOT NULL,
208182
completion_percent INTEGER,
209183
error_reason TEXT,
184+
transcript_path TEXT,
210185
archived BOOLEAN DEFAULT 0
211186
);
212187
@@ -289,6 +264,7 @@ describe("database schema baseline", () => {
289264
last_active_at INTEGER NOT NULL,
290265
completion_percent INTEGER,
291266
error_reason TEXT,
267+
transcript_path TEXT,
292268
archived BOOLEAN DEFAULT 0
293269
);
294270
`);

packages/server/src/storage/db.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const SCHEMA_PATH = join(import.meta.dirname, "migrations", "001_init.sql");
3333
const SCHEMA_SQL = readFileSync(SCHEMA_PATH, "utf-8");
3434

3535
const LEGACY_TABLES = ["hook_registrations", "_migrations"] as const;
36-
const LEGACY_SESSION_COLUMNS = ["resume_id", "transcript_path"] as const;
36+
const LEGACY_SESSION_COLUMNS = ["resume_id"] as const;
3737

3838
function normalizeSql(sql: string | null): string {
3939
return (sql ?? "").replace(/\s+/g, " ").trim();

0 commit comments

Comments
 (0)