Skip to content

Commit 789ccf0

Browse files
committed
feat: persist supervisor target ids
1 parent 33ac73b commit 789ccf0

8 files changed

Lines changed: 345 additions & 9 deletions

File tree

packages/core/src/domain/supervisor.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,37 @@ export type CycleStatus =
1919

2020
export type CycleTrigger = "turn_completed" | "manual" | "scheduled";
2121

22-
export type SupervisorStopReason = "objective_complete" | "max_supervision_count_reached";
22+
export type SupervisorStopReason =
23+
| "objective_complete"
24+
| "max_supervision_count_reached"
25+
| "supervisor_uncertain"
26+
| "needs_user_input";
27+
28+
export type SupervisorPlanStepStatus = "pending" | "in_progress" | "completed";
29+
30+
export interface SupervisorPlanStep {
31+
step: string;
32+
status: SupervisorPlanStepStatus;
33+
}
34+
35+
export interface SupervisorTargetMemory {
36+
summary: string;
37+
plan: SupervisorPlanStep[];
38+
updatedAt: number;
39+
}
40+
41+
export interface SupervisorCycleStepUpdate {
42+
step: string;
43+
status: SupervisorPlanStepStatus;
44+
}
45+
46+
export interface SupervisorCycleTargetRecord {
47+
cycleId: string;
48+
targetId: string;
49+
summary?: string;
50+
stepUpdates: SupervisorCycleStepUpdate[];
51+
createdAt: number;
52+
}
2353

2454
export type SupervisorCycleAttemptStatus = "evaluating" | "completed" | "failed" | "cancelled";
2555

@@ -65,6 +95,7 @@ export interface Supervisor {
6595
id: string;
6696
sessionId: string;
6797
workspaceId: string;
98+
targetId: string;
6899
state: SupervisorState;
69100
objective: string;
70101
evaluatorProviderId: string;
@@ -73,6 +104,8 @@ export interface Supervisor {
73104
completedSupervisionCount: number;
74105
scheduledAt?: number;
75106
stopReason?: SupervisorStopReason;
107+
currentTargetMemory?: SupervisorTargetMemory;
108+
recentTargetCycles: SupervisorCycleTargetRecord[];
76109
cycles: SupervisorCycle[];
77110
lastCycleAt?: number;
78111
lastEvaluatedTurnId?: string;

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

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
CURRENT_SCHEMA_VERSION,
99
IncompatibleSchemaError,
1010
V1_SCHEMA_SQL,
11+
V2_SCHEMA_SQL,
1112
} from "../storage/schema-version.js";
1213

1314
describe("Database", () => {
@@ -98,7 +99,7 @@ describe("Database", () => {
9899
expect(tables.map((table) => table.name)).not.toContain("_migrations");
99100
});
100101

101-
it("should upgrade a known v1 supervisor schema to v2 when user_version is unset", () => {
102+
it("should upgrade a known v1 supervisor schema to v3 when user_version is unset", () => {
102103
const dbPath = join(tempDir, "v1.db");
103104
const rawDb = new DatabaseSync(dbPath);
104105
rawDb.exec("PRAGMA user_version = 0");
@@ -108,13 +109,14 @@ describe("Database", () => {
108109
db = openDatabase(dbPath);
109110

110111
const userVersion = db.prepare("PRAGMA user_version").get() as { user_version: number };
111-
expect(userVersion.user_version).toBe(2);
112+
expect(userVersion.user_version).toBe(CURRENT_SCHEMA_VERSION);
112113

113114
const supervisorColumns = db.prepare("PRAGMA table_info(supervisors)").all() as Array<{
114115
name: string;
115116
}>;
116117
expect(supervisorColumns.map((column) => column.name)).toEqual(
117118
expect.arrayContaining([
119+
"target_id",
118120
"evaluator_model",
119121
"max_supervision_count",
120122
"completed_supervision_count",
@@ -138,6 +140,79 @@ describe("Database", () => {
138140
expect(upgradedIndex?.name).toBe("idx_supervisor_cycle_attempts_cycle");
139141
});
140142

143+
it("should upgrade a known v2 supervisor schema to v3 and backfill target ids", () => {
144+
const dbPath = join(tempDir, "v2.db");
145+
const rawDb = new DatabaseSync(dbPath);
146+
rawDb.exec("PRAGMA user_version = 2");
147+
rawDb.exec(V2_SCHEMA_SQL);
148+
rawDb.exec(`
149+
INSERT INTO workspaces (id, path, target_runtime, opened_at, last_active_at, ui_state)
150+
VALUES ('ws-1', '/workspace', 'native', 1, 1, '{}');
151+
`);
152+
rawDb.exec(`
153+
INSERT INTO terminals (id, workspace_id, kind, cwd, argv, cols, rows, created_at)
154+
VALUES ('term-1', 'ws-1', 'agent', '/workspace', '[]', 120, 30, 1);
155+
`);
156+
rawDb.exec(`
157+
INSERT INTO sessions (
158+
id, workspace_id, terminal_id, provider_id, capability, state, started_at, last_active_at
159+
) VALUES ('sess-1', 'ws-1', 'term-1', 'codex', 'full', 'idle', 1, 1);
160+
`);
161+
rawDb.exec(`
162+
INSERT INTO supervisors (
163+
id,
164+
session_id,
165+
workspace_id,
166+
state,
167+
objective,
168+
evaluator_provider_id,
169+
evaluator_model,
170+
max_supervision_count,
171+
completed_supervision_count,
172+
scheduled_at,
173+
stop_reason,
174+
last_cycle_at,
175+
last_evaluated_turn_id,
176+
error_reason,
177+
created_at,
178+
updated_at
179+
) VALUES (
180+
'sup-legacy',
181+
'sess-1',
182+
'ws-1',
183+
'idle',
184+
'Legacy supervisor',
185+
'codex',
186+
NULL,
187+
0,
188+
0,
189+
NULL,
190+
NULL,
191+
NULL,
192+
NULL,
193+
NULL,
194+
1,
195+
1
196+
);
197+
`);
198+
rawDb.close();
199+
200+
db = openDatabase(dbPath);
201+
202+
const userVersion = db.prepare("PRAGMA user_version").get() as { user_version: number };
203+
expect(userVersion.user_version).toBe(CURRENT_SCHEMA_VERSION);
204+
205+
const supervisorColumns = db.prepare("PRAGMA table_info(supervisors)").all() as Array<{
206+
name: string;
207+
}>;
208+
expect(supervisorColumns.map((column) => column.name)).toContain("target_id");
209+
210+
const upgradedRow = db
211+
.prepare("SELECT target_id FROM supervisors WHERE id = ?")
212+
.get("sup-legacy") as { target_id: string };
213+
expect(upgradedRow.target_id).toBe("legacy_sup-legacy");
214+
});
215+
141216
it("should restamp user_version for an already-current schema when it is unset", () => {
142217
const dbPath = join(tempDir, "current-unstamped.db");
143218
db = openDatabase(dbPath);

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ describe("SupervisorRepo", () => {
4646
id: "sup-1",
4747
sessionId: "sess-1",
4848
workspaceId: "ws-1",
49+
targetId: "target-1",
4950
state: "idle",
5051
objective: "Finish supervisor persistence",
5152
evaluatorProviderId: "codex",
@@ -64,6 +65,7 @@ describe("SupervisorRepo", () => {
6465
id: "sup-1",
6566
sessionId: "sess-1",
6667
workspaceId: "ws-1",
68+
targetId: "target-initial",
6769
state: "stopped",
6870
objective: "Stop when objective is complete",
6971
evaluatorProviderId: "codex",
@@ -87,6 +89,31 @@ describe("SupervisorRepo", () => {
8789
});
8890
});
8991

92+
it("persists targetId on create and update", () => {
93+
supervisorRepo.create({
94+
id: "sup-1",
95+
sessionId: "sess-1",
96+
workspaceId: "ws-1",
97+
targetId: "target-alpha",
98+
state: "idle",
99+
objective: "Track target scope",
100+
evaluatorProviderId: "codex",
101+
createdAt: 10,
102+
updatedAt: 10,
103+
});
104+
105+
const created = supervisorRepo.findById("sup-1");
106+
expect(created?.targetId).toBe("target-alpha");
107+
108+
const updated = supervisorRepo.update("sup-1", {
109+
targetId: "target-beta",
110+
updatedAt: 11,
111+
});
112+
113+
expect(updated.targetId).toBe("target-beta");
114+
expect(supervisorRepo.findById("sup-1")?.targetId).toBe("target-beta");
115+
});
116+
90117
it("rejects a supervisor whose workspace does not match its session workspace", () => {
91118
db.prepare(
92119
"INSERT INTO workspaces (id, path, target_runtime, opened_at, last_active_at, ui_state) VALUES (?, ?, ?, ?, ?, ?)"
@@ -103,6 +130,7 @@ describe("SupervisorRepo", () => {
103130
id: "sup-bad",
104131
sessionId: "sess-2",
105132
workspaceId: "ws-1",
133+
targetId: "target-bad",
106134
state: "idle",
107135
objective: "This insert must fail",
108136
evaluatorProviderId: "claude",
@@ -124,6 +152,7 @@ describe("SupervisorRepo", () => {
124152
id: "sup-1",
125153
sessionId: "sess-1",
126154
workspaceId: "ws-1",
155+
targetId: "target-1",
127156
state: "idle",
128157
objective: "Enforce supervisor/session integrity",
129158
evaluatorProviderId: "claude",
@@ -152,6 +181,7 @@ describe("SupervisorRepo", () => {
152181
id: "sup-1",
153182
sessionId: "sess-1",
154183
workspaceId: "ws-1",
184+
targetId: "target-1",
155185
state: "idle",
156186
objective: "Keep nullable fields intact",
157187
evaluatorProviderId: "claude",
@@ -178,6 +208,7 @@ describe("SupervisorRepo", () => {
178208
id: "sup-1",
179209
sessionId: "sess-1",
180210
workspaceId: "ws-1",
211+
targetId: "target-1",
181212
state: "idle",
182213
objective: "Clear nullable fields",
183214
evaluatorProviderId: "claude",
@@ -205,6 +236,7 @@ describe("SupervisorRepo", () => {
205236
id: "sup-1",
206237
sessionId: "sess-1",
207238
workspaceId: "ws-1",
239+
targetId: "target-1",
208240
state: "idle",
209241
objective: "Clear execution policy fields",
210242
evaluatorProviderId: "claude",
@@ -236,6 +268,7 @@ describe("SupervisorRepo", () => {
236268
id: "sup-1",
237269
sessionId: "sess-1",
238270
workspaceId: "ws-1",
271+
targetId: "target-1",
239272
state: "idle",
240273
objective: "Keep cycle fields intact",
241274
evaluatorProviderId: "claude",
@@ -276,6 +309,7 @@ describe("SupervisorRepo", () => {
276309
id: "sup-1",
277310
sessionId: "sess-1",
278311
workspaceId: "ws-1",
312+
targetId: "target-1",
279313
state: "idle",
280314
objective: "Clear cycle fields",
281315
evaluatorProviderId: "claude",
@@ -336,6 +370,7 @@ describe("SupervisorRepo", () => {
336370
id: "sup-1",
337371
sessionId: "sess-1",
338372
workspaceId: "ws-1",
373+
targetId: "target-1",
339374
state: "idle",
340375
objective: "Keep the newest 100 cycles",
341376
evaluatorProviderId: "claude",
@@ -371,6 +406,7 @@ describe("SupervisorRepo", () => {
371406
id: "sup-1",
372407
sessionId: "sess-1",
373408
workspaceId: "ws-1",
409+
targetId: "target-1",
374410
state: "idle",
375411
objective: "Allow scheduled cancelled cycle",
376412
evaluatorProviderId: "claude",
@@ -399,6 +435,7 @@ describe("SupervisorRepo", () => {
399435
id: "sup-1",
400436
sessionId: "sess-1",
401437
workspaceId: "ws-1",
438+
targetId: "target-1",
402439
state: "idle",
403440
objective: "Track attempts",
404441
evaluatorProviderId: "claude",
@@ -456,6 +493,7 @@ describe("SupervisorRepo", () => {
456493
id: "sup-1",
457494
sessionId: "sess-1",
458495
workspaceId: "ws-1",
496+
targetId: "target-1",
459497
state: "idle",
460498
objective: "Update attempts",
461499
evaluatorProviderId: "claude",

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ describe("database schema baseline", () => {
5353
expect(sessionColumns.find((column) => column.name === "transcript_path")).toBeUndefined();
5454
expect(sessionColumns.find((column) => column.name === "title")).toBeDefined();
5555

56+
const supervisorColumns = db.prepare("PRAGMA table_info(supervisors)").all() as Array<{
57+
name: string;
58+
}>;
59+
expect(supervisorColumns.find((column) => column.name === "target_id")).toBeDefined();
60+
5661
const indexNames = (
5762
db.prepare("SELECT name FROM sqlite_master WHERE type='index' ORDER BY name").all() as Array<{
5863
name: string;

packages/server/src/storage/db.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
detectSchema,
77
IncompatibleSchemaError,
88
stampCurrentSchemaVersion,
9+
stampSchemaVersion,
910
} from "./schema-version.js";
1011

1112
interface IntegrityCheckRow {
@@ -97,6 +98,14 @@ function upgradeSchemaV1ToV2(db: Database): void {
9798
db.exec(
9899
"CREATE INDEX idx_supervisor_cycle_attempts_cycle ON supervisor_cycle_attempts(cycle_id, attempt_index)"
99100
);
101+
stampSchemaVersion(db, 2);
102+
});
103+
}
104+
105+
function upgradeSchemaV2ToV3(db: Database): void {
106+
withTransaction(db, () => {
107+
db.exec("ALTER TABLE supervisors ADD COLUMN target_id TEXT NOT NULL DEFAULT ''");
108+
db.exec("UPDATE supervisors SET target_id = 'legacy_' || id WHERE target_id = ''");
100109
stampCurrentSchemaVersion(db);
101110
});
102111
}
@@ -128,6 +137,12 @@ function initializeOrUpgradeSchema(db: Database, dbPath: string): void {
128137

129138
case "v1":
130139
upgradeSchemaV1ToV2(db);
140+
upgradeSchemaV2ToV3(db);
141+
assertCurrentSchema(db, dbPath);
142+
return;
143+
144+
case "v2":
145+
upgradeSchemaV2ToV3(db);
131146
assertCurrentSchema(db, dbPath);
132147
return;
133148

packages/server/src/storage/migrations/001_init.sql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
-- Current database schema baseline
2-
PRAGMA user_version = 2;
2+
PRAGMA user_version = 3;
33

44
CREATE TABLE IF NOT EXISTS workspaces (
55
id TEXT PRIMARY KEY,
@@ -78,7 +78,7 @@ CREATE TABLE IF NOT EXISTS supervisors (
7878
last_evaluated_turn_id TEXT,
7979
error_reason TEXT,
8080
created_at INTEGER NOT NULL,
81-
updated_at INTEGER NOT NULL, evaluator_model TEXT, max_supervision_count INTEGER NOT NULL DEFAULT 0, completed_supervision_count INTEGER NOT NULL DEFAULT 0, scheduled_at INTEGER, stop_reason TEXT,
81+
updated_at INTEGER NOT NULL, evaluator_model TEXT, max_supervision_count INTEGER NOT NULL DEFAULT 0, completed_supervision_count INTEGER NOT NULL DEFAULT 0, scheduled_at INTEGER, stop_reason TEXT, target_id TEXT NOT NULL DEFAULT '',
8282
FOREIGN KEY (session_id, workspace_id) REFERENCES sessions(id, workspace_id) ON DELETE CASCADE,
8383
FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE
8484
);

0 commit comments

Comments
 (0)