@cydm/happy-elves 0.1.0-beta.40 → 0.1.0-beta.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/cli/dist/commands/lib/args.js +3 -0
- package/apps/cli/dist/commands/lib/usage.js +9 -2
- package/apps/cli/dist/commands/lib/workspace.js +21 -8
- package/apps/cli/dist/commands/session.js +191 -3
- package/apps/daemon/dist/session/events.d.ts +1 -1
- package/apps/daemon/dist/session/events.js +2 -1
- package/apps/daemon/dist/session/lifecycle.js +1 -1
- package/apps/daemon/dist/session/prompt.js +1 -1
- package/apps/daemon/package.json +1 -1
- package/apps/relay/dist/http-routes.js +21 -10
- package/apps/relay/dist/http-schemas.d.ts +5 -0
- package/apps/relay/dist/http-schemas.js +1 -0
- package/apps/relay/dist/machine-handlers.js +19 -8
- package/package.json +1 -1
- package/packages/client/dist/client.js +2 -0
- package/packages/client/dist/parsers.js +1 -0
- package/packages/client/dist/types.d.ts +2 -0
- package/packages/shared/dist/protocol.d.ts +1 -0
- package/packages/shared/dist/protocol.js +1 -0
|
@@ -27,6 +27,8 @@ const valueFlags = new Set([
|
|
|
27
27
|
"reason",
|
|
28
28
|
"relay",
|
|
29
29
|
"root",
|
|
30
|
+
"runtime-limit",
|
|
31
|
+
"runtime-session",
|
|
30
32
|
"shell",
|
|
31
33
|
"actions",
|
|
32
34
|
"expires-in",
|
|
@@ -58,6 +60,7 @@ const booleanFlags = new Set([
|
|
|
58
60
|
"h",
|
|
59
61
|
"include-archived",
|
|
60
62
|
"json",
|
|
63
|
+
"keep-source",
|
|
61
64
|
"local",
|
|
62
65
|
"no-open",
|
|
63
66
|
"no-wait",
|
|
@@ -50,7 +50,8 @@ Check local controller config, relay reachability, and daemon state.
|
|
|
50
50
|
happy-elves session history --machine <machineId> [--cwd <path>] [--agent <agent>] [--include-archived] [--limit 20] --json
|
|
51
51
|
happy-elves session continue <runtimeSessionId> --machine <machineId> [--name <name>] --json
|
|
52
52
|
happy-elves session status <sessionId> [--verbose] [--json]
|
|
53
|
-
happy-elves session diagnose <sessionId> [--limit 200] [--repair-head] [--json]
|
|
53
|
+
happy-elves session diagnose <sessionId> [--limit 200] [--runtime-limit 500] [--repair-head] [--json]
|
|
54
|
+
happy-elves session repair-binding <sessionId> [--runtime-session <runtimeSessionId>] [--runtime-limit 500] [--keep-source] --json
|
|
54
55
|
happy-elves session close <sessionId> [--reason <reason>] --json
|
|
55
56
|
|
|
56
57
|
Session management:
|
|
@@ -64,6 +65,8 @@ Session management:
|
|
|
64
65
|
counters, and latest materialized relay page counts for debugging empty or
|
|
65
66
|
stale transcripts. --repair-head advances a stale canonical head to the
|
|
66
67
|
latest completed relay event when the relay already has the event basis.
|
|
68
|
+
repair-binding imports the matching runtime history row as a new loaded
|
|
69
|
+
session and closes the empty/stale source session unless --keep-source is set.
|
|
67
70
|
close archives a loaded session. There is no separate archive command.
|
|
68
71
|
`,
|
|
69
72
|
turn: `Usage:
|
|
@@ -141,8 +144,9 @@ Core commands:
|
|
|
141
144
|
session create --machine <machineId> [--agent codex] [--cwd <path>] [--name <name>] --json
|
|
142
145
|
session history --machine <machineId> [--cwd <path>] [--limit 20] --json
|
|
143
146
|
session continue <runtimeSessionId> --machine <machineId> --json
|
|
147
|
+
session repair-binding <sessionId> [--runtime-session <runtimeSessionId>] --json
|
|
144
148
|
session status <sessionId> [--verbose] [--json]
|
|
145
|
-
session diagnose <sessionId> [--limit 200] [--repair-head] [--json]
|
|
149
|
+
session diagnose <sessionId> [--limit 200] [--runtime-limit 500] [--repair-head] [--json]
|
|
146
150
|
session close <sessionId> [--reason <reason>] --json
|
|
147
151
|
|
|
148
152
|
turn list <sessionId> [--limit 10] [--json]
|
|
@@ -217,6 +221,9 @@ Session management:
|
|
|
217
221
|
provider sessions unless --include-archived is set.
|
|
218
222
|
session continue <runtimeSessionId> --machine <machineId> --json continues a
|
|
219
223
|
runtime history row as a loaded session, or returns an existing sessionId.
|
|
224
|
+
session repair-binding <sessionId> --json recovers a loaded session that was
|
|
225
|
+
bound to the wrong/empty provider history by importing the matching runtime
|
|
226
|
+
history row as a new loaded session.
|
|
220
227
|
session close <sessionId> --reason "archived" --json archives a loaded
|
|
221
228
|
session. There is no separate archive command.
|
|
222
229
|
|
|
@@ -2,7 +2,7 @@ import { CliError } from "../../errors.js";
|
|
|
2
2
|
import { showMachine } from "./session-view.js";
|
|
3
3
|
export async function listWorkspaceHistoricalSessions(client, input) {
|
|
4
4
|
const snapshot = await client.snapshot();
|
|
5
|
-
const
|
|
5
|
+
const providerPageSize = Math.min(200, Math.max(Math.min(input.limit, 200), 50));
|
|
6
6
|
const selectedMachines = snapshot.machines.filter((machine) => {
|
|
7
7
|
if (input.machineId)
|
|
8
8
|
return machine.id === input.machineId;
|
|
@@ -17,13 +17,26 @@ export async function listWorkspaceHistoricalSessions(client, input) {
|
|
|
17
17
|
const rows = await Promise.all(machines.map(async ({ machine, projected }) => {
|
|
18
18
|
const machineName = projected.metadata.name ?? projected.metadata.hostname ?? machine.name;
|
|
19
19
|
try {
|
|
20
|
-
const sessions =
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
const sessions = [];
|
|
21
|
+
let cursor;
|
|
22
|
+
const seenCursors = new Set();
|
|
23
|
+
while (sessions.length < input.limit) {
|
|
24
|
+
const page = await client.listHistoricalSessionsPage({
|
|
25
|
+
machineId: machine.id,
|
|
26
|
+
cwd: input.cwd,
|
|
27
|
+
agent: input.agent,
|
|
28
|
+
includeArchived: input.includeArchived,
|
|
29
|
+
limit: providerPageSize,
|
|
30
|
+
cursor,
|
|
31
|
+
});
|
|
32
|
+
sessions.push(...page.sessions);
|
|
33
|
+
if (!page.hasMore || !page.nextCursor)
|
|
34
|
+
break;
|
|
35
|
+
if (seenCursors.has(page.nextCursor))
|
|
36
|
+
break;
|
|
37
|
+
seenCursors.add(page.nextCursor);
|
|
38
|
+
cursor = page.nextCursor;
|
|
39
|
+
}
|
|
27
40
|
return {
|
|
28
41
|
sessions: sessions
|
|
29
42
|
.filter((session) => input.includeArchived === true || session.runtimeState !== "archived")
|
|
@@ -125,6 +125,7 @@ function repairHeadInputForLatestCompletedEvent(events) {
|
|
|
125
125
|
return {
|
|
126
126
|
currentHead,
|
|
127
127
|
lastTurnId,
|
|
128
|
+
status: "completed",
|
|
128
129
|
basis: {
|
|
129
130
|
turnId: doneEvent.turnId,
|
|
130
131
|
eventMessageId: doneEvent.messageId,
|
|
@@ -217,6 +218,130 @@ function diagnosticSessionMetadata(metadata) {
|
|
|
217
218
|
emptyRuntimeSession: metadata.emptyRuntimeSession === true,
|
|
218
219
|
};
|
|
219
220
|
}
|
|
221
|
+
function addRuntimeSessionId(ids, value) {
|
|
222
|
+
if (typeof value === "string" && value.trim())
|
|
223
|
+
ids.add(value.trim());
|
|
224
|
+
}
|
|
225
|
+
function metadataRuntimeSessionIds(metadata) {
|
|
226
|
+
const ids = new Set();
|
|
227
|
+
addRuntimeSessionId(ids, stringMetadata(metadata, "importedFromRuntimeSessionId"));
|
|
228
|
+
const runtime = isRecord(metadata.runtime) ? metadata.runtime : {};
|
|
229
|
+
addRuntimeSessionId(ids, runtime.backendSessionId);
|
|
230
|
+
addRuntimeSessionId(ids, runtime.agentSessionId);
|
|
231
|
+
addRuntimeSessionId(ids, runtime.acpxRecordId);
|
|
232
|
+
if (isRecord(runtime.externalHandle))
|
|
233
|
+
addRuntimeSessionId(ids, runtime.externalHandle.sessionId);
|
|
234
|
+
return ids;
|
|
235
|
+
}
|
|
236
|
+
function normalizedMatchValue(value) {
|
|
237
|
+
return typeof value === "string" && value.trim() ? value.trim().toLocaleLowerCase() : undefined;
|
|
238
|
+
}
|
|
239
|
+
function runtimeBindingCandidates(session, metadata, historicalSessions) {
|
|
240
|
+
const knownRuntimeIds = metadataRuntimeSessionIds(metadata);
|
|
241
|
+
const expectedAgent = stringMetadata(metadata, "agent") ?? session.agent;
|
|
242
|
+
const expectedCwd = stringMetadata(metadata, "cwd") ?? session.cwd;
|
|
243
|
+
const names = new Set([
|
|
244
|
+
normalizedMatchValue(stringMetadata(metadata, "name")),
|
|
245
|
+
normalizedMatchValue(stringMetadata(metadata, "providerName")),
|
|
246
|
+
normalizedMatchValue(session.name),
|
|
247
|
+
].filter((value) => Boolean(value)));
|
|
248
|
+
const summaries = new Set([
|
|
249
|
+
normalizedMatchValue(stringMetadata(metadata, "importedSummary")),
|
|
250
|
+
normalizedMatchValue(session.name),
|
|
251
|
+
].filter((value) => Boolean(value)));
|
|
252
|
+
return historicalSessions
|
|
253
|
+
.filter((historical) => historical.agent === expectedAgent &&
|
|
254
|
+
historical.cwd === expectedCwd &&
|
|
255
|
+
!knownRuntimeIds.has(historical.runtimeSessionId))
|
|
256
|
+
.map((historical) => {
|
|
257
|
+
const reasons = [];
|
|
258
|
+
let score = 0;
|
|
259
|
+
const historicalName = normalizedMatchValue(historical.name ?? historical.runtimeSessionName);
|
|
260
|
+
const historicalSummary = normalizedMatchValue(historical.summary);
|
|
261
|
+
if (historicalName && names.has(historicalName)) {
|
|
262
|
+
score += 100;
|
|
263
|
+
reasons.push("name");
|
|
264
|
+
}
|
|
265
|
+
if (historicalSummary && summaries.has(historicalSummary)) {
|
|
266
|
+
score += 30;
|
|
267
|
+
reasons.push("summary");
|
|
268
|
+
}
|
|
269
|
+
if (historical.loadedSessionId) {
|
|
270
|
+
score -= 20;
|
|
271
|
+
reasons.push("already-loaded");
|
|
272
|
+
}
|
|
273
|
+
if (score <= 0)
|
|
274
|
+
return undefined;
|
|
275
|
+
return {
|
|
276
|
+
runtimeSessionId: historical.runtimeSessionId,
|
|
277
|
+
agent: historical.agent,
|
|
278
|
+
cwd: historical.cwd,
|
|
279
|
+
name: historical.name ?? historical.runtimeSessionName,
|
|
280
|
+
summary: historical.summary,
|
|
281
|
+
updatedAt: historical.updatedAt,
|
|
282
|
+
score,
|
|
283
|
+
reasons,
|
|
284
|
+
...(historical.runtimeState === "archived" ? { runtimeState: "archived" } : {}),
|
|
285
|
+
};
|
|
286
|
+
})
|
|
287
|
+
.filter((candidate) => Boolean(candidate))
|
|
288
|
+
.sort((left, right) => right.score - left.score || right.updatedAt - left.updatedAt || left.runtimeSessionId.localeCompare(right.runtimeSessionId));
|
|
289
|
+
}
|
|
290
|
+
function shouldSearchRuntimeBindingCandidates(latestEvents, metadata) {
|
|
291
|
+
const available = numberMetadata(metadata, "availableHistoricalEvents");
|
|
292
|
+
const emitted = numberMetadata(metadata, "backfilledEvents");
|
|
293
|
+
return latestEvents.length === 0 &&
|
|
294
|
+
(available === undefined || available === 0) &&
|
|
295
|
+
(emitted === undefined || emitted === 0);
|
|
296
|
+
}
|
|
297
|
+
function runtimeBindingSuspected(candidates) {
|
|
298
|
+
return candidates.some((candidate) => candidate.reasons.includes("name") && !candidate.reasons.includes("already-loaded"));
|
|
299
|
+
}
|
|
300
|
+
async function findRuntimeBindingCandidates(client, session, metadata, flags) {
|
|
301
|
+
const limit = parsePositiveIntFlag(flags, "runtime-limit") ?? 500;
|
|
302
|
+
const result = await listWorkspaceHistoricalSessions(client, {
|
|
303
|
+
machineId: session.machineId,
|
|
304
|
+
cwd: stringMetadata(metadata, "cwd") ?? session.cwd,
|
|
305
|
+
agent: stringMetadata(metadata, "agent") ?? session.agent,
|
|
306
|
+
includeArchived: true,
|
|
307
|
+
limit,
|
|
308
|
+
});
|
|
309
|
+
return {
|
|
310
|
+
candidates: runtimeBindingCandidates(session, metadata, result.sessions).slice(0, 8),
|
|
311
|
+
searched: true,
|
|
312
|
+
limit,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
async function resolveRuntimeBindingRepairTarget(client, session, metadata, flags) {
|
|
316
|
+
const runtimeSessionId = typeof flags["runtime-session"] === "string" ? flags["runtime-session"].trim() : undefined;
|
|
317
|
+
const limit = parsePositiveIntFlag(flags, "runtime-limit") ?? 500;
|
|
318
|
+
const result = await listWorkspaceHistoricalSessions(client, {
|
|
319
|
+
machineId: session.machineId,
|
|
320
|
+
cwd: stringMetadata(metadata, "cwd") ?? session.cwd,
|
|
321
|
+
agent: stringMetadata(metadata, "agent") ?? session.agent,
|
|
322
|
+
includeArchived: true,
|
|
323
|
+
limit,
|
|
324
|
+
});
|
|
325
|
+
const candidateDiagnostics = runtimeBindingCandidates(session, metadata, result.sessions);
|
|
326
|
+
if (runtimeSessionId) {
|
|
327
|
+
const explicit = result.sessions.find((item) => item.runtimeSessionId === runtimeSessionId);
|
|
328
|
+
if (!explicit)
|
|
329
|
+
throw new CliError(`Runtime session not found in the first ${limit} historical rows: ${runtimeSessionId}`, "RUNTIME_SESSION_NOT_FOUND");
|
|
330
|
+
return { candidate: explicit, candidates: candidateDiagnostics.slice(0, 8) };
|
|
331
|
+
}
|
|
332
|
+
const usableCandidates = candidateDiagnostics.filter((candidate) => !candidate.reasons.includes("already-loaded"));
|
|
333
|
+
if (usableCandidates.length === 0) {
|
|
334
|
+
throw new CliError("No unambiguous runtime binding repair candidate found. Re-run with --runtime-session <id>.", "SESSION_BINDING_REPAIR_UNAVAILABLE");
|
|
335
|
+
}
|
|
336
|
+
const [first, second] = usableCandidates;
|
|
337
|
+
if (second && second.score === first.score) {
|
|
338
|
+
throw new CliError("Multiple runtime binding repair candidates found. Re-run with --runtime-session <id>.", "SESSION_BINDING_REPAIR_AMBIGUOUS");
|
|
339
|
+
}
|
|
340
|
+
const selected = result.sessions.find((item) => item.runtimeSessionId === first.runtimeSessionId);
|
|
341
|
+
if (!selected)
|
|
342
|
+
throw new CliError("Selected repair candidate disappeared from runtime history", "SESSION_BINDING_REPAIR_UNAVAILABLE");
|
|
343
|
+
return { candidate: selected, candidates: candidateDiagnostics.slice(0, 8) };
|
|
344
|
+
}
|
|
220
345
|
export async function handleSession({ domain, action, positional, flags }) {
|
|
221
346
|
if (domain !== "session")
|
|
222
347
|
return false;
|
|
@@ -363,15 +488,15 @@ export async function handleSession({ domain, action, positional, flags }) {
|
|
|
363
488
|
if (!initialSession)
|
|
364
489
|
throw new CliError("Session not found", "SESSION_NOT_FOUND");
|
|
365
490
|
let session = initialSession;
|
|
366
|
-
if (flags["repair-head"] === true && session.status === "running") {
|
|
367
|
-
throw new CliError("Cannot repair a running session head", "SESSION_HEAD_REPAIR_RUNNING");
|
|
368
|
-
}
|
|
369
491
|
const machine = snapshot.machines.find((item) => item.id === session.machineId);
|
|
370
492
|
const metadata = await client.decodeSessionMetadata(session);
|
|
371
493
|
const latestPage = await client.collectPage(session.id, { limit });
|
|
372
494
|
const turns = summarizeTurns(latestPage.events);
|
|
373
495
|
const latestCompletedTurn = turns.find((turn) => turn.status === "completed");
|
|
374
496
|
const latestTerminalTurn = turns.find((turn) => turn.status !== "running");
|
|
497
|
+
const runtimeBinding = shouldSearchRuntimeBindingCandidates(latestPage.events, metadata)
|
|
498
|
+
? await findRuntimeBindingCandidates(client, session, metadata, flags)
|
|
499
|
+
: { candidates: [], searched: false, limit: parsePositiveIntFlag(flags, "runtime-limit") ?? 500 };
|
|
375
500
|
const repair = flags["repair-head"] === true
|
|
376
501
|
? await client.repairSessionHead(session.id, repairHeadInputForLatestCompletedEvent(latestPage.events))
|
|
377
502
|
: undefined;
|
|
@@ -430,12 +555,25 @@ export async function handleSession({ domain, action, positional, flags }) {
|
|
|
430
555
|
backfillAvailableEvents: numberMetadata(metadata, "availableHistoricalEvents"),
|
|
431
556
|
backfillEmittedEvents: numberMetadata(metadata, "backfilledEvents"),
|
|
432
557
|
backfillStatus: stringMetadata(metadata, "historicalBackfillStatus") ?? stringMetadata(metadata, "historicalBackfill"),
|
|
558
|
+
wrongRuntimeBindingSuspected: runtimeBindingSuspected(runtimeBinding.candidates),
|
|
559
|
+
runtimeBindingCandidateCount: runtimeBinding.candidates.length,
|
|
433
560
|
staleHeadSuspected: staleHeadSuspected(session, latestCompletedTurn),
|
|
434
561
|
headMatchesLatestCompletedTurn: headMatchesTurn(session, latestCompletedTurn),
|
|
435
562
|
},
|
|
563
|
+
runtimeBinding: {
|
|
564
|
+
searched: runtimeBinding.searched,
|
|
565
|
+
searchLimit: runtimeBinding.limit,
|
|
566
|
+
suspected: runtimeBindingSuspected(runtimeBinding.candidates),
|
|
567
|
+
candidates: runtimeBinding.candidates,
|
|
568
|
+
repairCommand: runtimeBinding.candidates[0]
|
|
569
|
+
? `happy-elves session repair-binding ${session.id} --runtime-session ${runtimeBinding.candidates[0].runtimeSessionId} --json`
|
|
570
|
+
: undefined,
|
|
571
|
+
},
|
|
436
572
|
repair: repair ? {
|
|
437
573
|
advanced: repair.advanced,
|
|
574
|
+
settled: repair.settled,
|
|
438
575
|
basis: repair.basis,
|
|
576
|
+
status: repair.session.status,
|
|
439
577
|
currentHead: repair.session.currentHead,
|
|
440
578
|
lastTurnId: repair.session.lastTurnId,
|
|
441
579
|
reason: repair.reason,
|
|
@@ -452,9 +590,59 @@ export async function handleSession({ domain, action, positional, flags }) {
|
|
|
452
590
|
console.log(`Backfill: ${data.diagnosis.backfillStatus ?? "-"} available=${data.diagnosis.backfillAvailableEvents ?? "-"} emitted=${data.diagnosis.backfillEmittedEvents ?? "-"}`);
|
|
453
591
|
console.log(`Head: ${session.currentHead ?? "-"} lastTurn=${session.lastTurnId ?? "-"} stale=${data.diagnosis.staleHeadSuspected ? "yes" : "no"}`);
|
|
454
592
|
console.log(`Latest page: ${latestPage.events.length} events, visible=${latestPage.events.filter(isVisibleTranscriptPayloadEvent).length}, hasMore=${latestPage.hasMoreBefore}`);
|
|
593
|
+
if (data.runtimeBinding.suspected && data.runtimeBinding.candidates[0]) {
|
|
594
|
+
console.log(`Runtime binding: suspected wrong binding; candidate=${data.runtimeBinding.candidates[0].runtimeSessionId}`);
|
|
595
|
+
console.log(`Repair: ${data.runtimeBinding.repairCommand}`);
|
|
596
|
+
}
|
|
455
597
|
}
|
|
456
598
|
return true;
|
|
457
599
|
}
|
|
600
|
+
if (action === "repair-binding") {
|
|
601
|
+
const sessionId = requirePositional(positional[0], "sessionId");
|
|
602
|
+
const client = await makeClient();
|
|
603
|
+
const session = await client.getSession(sessionId);
|
|
604
|
+
if (!session)
|
|
605
|
+
throw new CliError("Session not found", "SESSION_NOT_FOUND");
|
|
606
|
+
if (session.status === "running")
|
|
607
|
+
throw new CliError("Cannot repair binding for a running session", "SESSION_BINDING_REPAIR_RUNNING");
|
|
608
|
+
const metadata = await client.decodeSessionMetadata(session);
|
|
609
|
+
const { candidate, candidates } = await resolveRuntimeBindingRepairTarget(client, session, metadata, flags);
|
|
610
|
+
const name = typeof flags.name === "string" && flags.name.trim()
|
|
611
|
+
? flags.name.trim()
|
|
612
|
+
: stringMetadata(metadata, "name") ?? session.name ?? candidate.name ?? candidate.summary ?? candidate.runtimeSessionId;
|
|
613
|
+
const imported = await client.importHistoricalSession({
|
|
614
|
+
machineId: session.machineId,
|
|
615
|
+
agent: candidate.agent,
|
|
616
|
+
cwd: candidate.cwd,
|
|
617
|
+
runtimeSessionId: candidate.runtimeSessionId,
|
|
618
|
+
name,
|
|
619
|
+
summary: candidate.summary,
|
|
620
|
+
runtimeState: candidate.runtimeState,
|
|
621
|
+
...(candidate.name ? { providerName: candidate.name } : {}),
|
|
622
|
+
});
|
|
623
|
+
const closeSource = flags["keep-source"] !== true;
|
|
624
|
+
let sourceClosed = false;
|
|
625
|
+
if (closeSource) {
|
|
626
|
+
await client.close(session.id, `replaced by runtime session ${candidate.runtimeSessionId}`);
|
|
627
|
+
sourceClosed = true;
|
|
628
|
+
}
|
|
629
|
+
ok("session.repair-binding", {
|
|
630
|
+
sourceSessionId: session.id,
|
|
631
|
+
sessionId: imported.sessionId,
|
|
632
|
+
runtimeSessionId: candidate.runtimeSessionId,
|
|
633
|
+
mode: "created",
|
|
634
|
+
sourceClosed,
|
|
635
|
+
candidate: {
|
|
636
|
+
runtimeSessionId: candidate.runtimeSessionId,
|
|
637
|
+
name: candidate.name ?? candidate.runtimeSessionName,
|
|
638
|
+
summary: candidate.summary,
|
|
639
|
+
updatedAt: candidate.updatedAt,
|
|
640
|
+
...(candidate.runtimeState === "archived" ? { runtimeState: "archived" } : {}),
|
|
641
|
+
},
|
|
642
|
+
candidates,
|
|
643
|
+
}, { requestId: imported.requestId, machineId: session.machineId, sessionId: imported.sessionId });
|
|
644
|
+
return true;
|
|
645
|
+
}
|
|
458
646
|
if (action === "history") {
|
|
459
647
|
const machineId = requireString(flags, "machine");
|
|
460
648
|
const limit = parsePositiveIntFlag(flags, "limit") ?? 20;
|
|
@@ -4,7 +4,7 @@ import type { DaemonConfig } from "../types.js";
|
|
|
4
4
|
export declare function emitEvent(ws: WebSocket, config: DaemonConfig, sessionId: string, turnId: string, payload: SessionEventPayload): Promise<void>;
|
|
5
5
|
export declare function emitHistoricalEvents(ws: WebSocket, config: DaemonConfig, sessionId: string, events: RuntimeHistoricalEvent[], startIndex?: number): Promise<void>;
|
|
6
6
|
export declare function emitRuntimeTailEvents(ws: WebSocket, config: DaemonConfig, sessionId: string, events: RuntimeHistoricalEvent[], startIndex?: number): Promise<void>;
|
|
7
|
-
export declare function emitSessionHeadAdvanced(ws: WebSocket, sessionId: string, head: HistoricalSessionHead, encryptedMetadata?: EncryptedEnvelope): Promise<void>;
|
|
7
|
+
export declare function emitSessionHeadAdvanced(ws: WebSocket, sessionId: string, head: HistoricalSessionHead, encryptedMetadata?: EncryptedEnvelope, status?: "completed" | "cancelled" | "failed"): Promise<void>;
|
|
8
8
|
export declare function emitTranscriptReplacement(ws: WebSocket, config: DaemonConfig, sessionId: string, events: RuntimeHistoricalEvent[], input?: {
|
|
9
9
|
requestId?: string;
|
|
10
10
|
currentHead?: string;
|
|
@@ -32,13 +32,14 @@ export async function emitRuntimeTailEvents(ws, config, sessionId, events, start
|
|
|
32
32
|
}, ws);
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
export async function emitSessionHeadAdvanced(ws, sessionId, head, encryptedMetadata) {
|
|
35
|
+
export async function emitSessionHeadAdvanced(ws, sessionId, head, encryptedMetadata, status) {
|
|
36
36
|
await sendReliable({
|
|
37
37
|
type: "machine:sessionHeadAdvanced",
|
|
38
38
|
sessionId,
|
|
39
39
|
currentHead: head.currentHead,
|
|
40
40
|
lastTurnId: head.lastTurnId,
|
|
41
41
|
basis: head.basis,
|
|
42
|
+
status,
|
|
42
43
|
encryptedMetadata,
|
|
43
44
|
}, ws);
|
|
44
45
|
}
|
|
@@ -605,7 +605,7 @@ async function backfillHistoricalEvents(ws, config, session, runtimeSessionId =
|
|
|
605
605
|
await emitSessionHeadAdvanced(ws, session.sessionId, head, await encryptedSessionMetadata(config, session.sessionId, backfilledSession, {
|
|
606
606
|
...historicalBackfillMetadata(result),
|
|
607
607
|
historicalBackfilledAt: new Date().toISOString(),
|
|
608
|
-
}));
|
|
608
|
+
}), "completed");
|
|
609
609
|
}
|
|
610
610
|
return result;
|
|
611
611
|
}
|
|
@@ -121,7 +121,7 @@ export async function handlePrompt(ws, config, command) {
|
|
|
121
121
|
currentHead: result.currentHead,
|
|
122
122
|
lastTurnId: result.lastTurnId,
|
|
123
123
|
});
|
|
124
|
-
await refreshAuthoritativeTranscriptAfterTurn(ws, config, session, command.requestId, result.currentHead, result.lastTurnId);
|
|
124
|
+
await refreshAuthoritativeTranscriptAfterTurn(ws, config, session, command.requestId, result.currentHead, result.lastTurnId, result.status);
|
|
125
125
|
releasePromptTurn();
|
|
126
126
|
await sendReliable({
|
|
127
127
|
type: "machine:turnDone",
|
package/apps/daemon/package.json
CHANGED
|
@@ -249,7 +249,7 @@ export function registerHttpRoutes(app, context, controllerInviteTtlMs) {
|
|
|
249
249
|
.get(token.account_id, params.sessionId);
|
|
250
250
|
if (!session)
|
|
251
251
|
throw new HttpError(404, "SESSION_NOT_FOUND", "Session not found");
|
|
252
|
-
if (session.status === "running") {
|
|
252
|
+
if (session.status === "running" && !body.status) {
|
|
253
253
|
throw new HttpError(409, "SESSION_HEAD_REPAIR_RUNNING", "Cannot repair a running session head");
|
|
254
254
|
}
|
|
255
255
|
const basisEvent = context.db
|
|
@@ -265,18 +265,28 @@ export function registerHttpRoutes(app, context, controllerInviteTtlMs) {
|
|
|
265
265
|
const currentOrder = sessionHeadBasisOrderForRow(context.db, session);
|
|
266
266
|
const rewriteFenced = typeof session.head_basis_message_id === "string" &&
|
|
267
267
|
session.head_basis_message_id.startsWith("rewrite:");
|
|
268
|
-
const
|
|
269
|
-
|
|
268
|
+
const comparison = compareHeadOrders(nextOrder, currentOrder);
|
|
269
|
+
const currentHeadAlreadySettled = session.current_head === `event:${basisEvent.id}` ||
|
|
270
|
+
session.current_head === body.currentHead ||
|
|
271
|
+
session.last_turn_id === body.lastTurnId;
|
|
272
|
+
const advanced = !rewriteFenced && comparison > 0;
|
|
273
|
+
const statusChanged = !rewriteFenced &&
|
|
274
|
+
body.status !== undefined &&
|
|
275
|
+
session.status === "running" &&
|
|
276
|
+
comparison === 0 &&
|
|
277
|
+
currentHeadAlreadySettled;
|
|
278
|
+
if (advanced || statusChanged) {
|
|
270
279
|
const ts = context.now();
|
|
271
280
|
context.db.prepare(`UPDATE sessions
|
|
272
|
-
SET
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
281
|
+
SET status = COALESCE(?, status),
|
|
282
|
+
current_head = COALESCE(?, current_head),
|
|
283
|
+
last_turn_id = COALESCE(?, last_turn_id),
|
|
284
|
+
head_basis_event_id = COALESCE(?, head_basis_event_id),
|
|
285
|
+
head_basis_message_id = COALESCE(?, head_basis_message_id),
|
|
286
|
+
head_basis_logical_time = COALESCE(?, head_basis_logical_time),
|
|
287
|
+
head_basis_turn_seq = COALESCE(?, head_basis_turn_seq),
|
|
278
288
|
updated_at = ?
|
|
279
|
-
WHERE id = ? AND account_id = ?`).run(body.currentHead, body.lastTurnId, basisEvent.id, basisEvent.message_id, nextOrder.time, nextOrder.seq, ts, params.sessionId, token.account_id);
|
|
289
|
+
WHERE id = ? AND account_id = ?`).run(body.status && session.status === "running" ? body.status : null, advanced ? body.currentHead : null, advanced ? body.lastTurnId : null, advanced ? basisEvent.id : null, advanced ? basisEvent.message_id : null, advanced ? nextOrder.time : null, advanced ? nextOrder.seq : null, ts, params.sessionId, token.account_id);
|
|
280
290
|
}
|
|
281
291
|
const row = context.db
|
|
282
292
|
.prepare("SELECT * FROM sessions WHERE account_id = ? AND id = ?")
|
|
@@ -294,6 +304,7 @@ export function registerHttpRoutes(app, context, controllerInviteTtlMs) {
|
|
|
294
304
|
logicalTime: nextOrder.time,
|
|
295
305
|
turnSeq: nextOrder.seq,
|
|
296
306
|
},
|
|
307
|
+
...(statusChanged ? { settled: true } : {}),
|
|
297
308
|
...(rewriteFenced ? { reason: "rewrite-fenced" } : {}),
|
|
298
309
|
};
|
|
299
310
|
});
|
|
@@ -49,6 +49,11 @@ export declare const sessionEventsQuerySchema: z.ZodObject<{
|
|
|
49
49
|
export declare const sessionHeadAdvanceSchema: z.ZodObject<{
|
|
50
50
|
currentHead: z.ZodString;
|
|
51
51
|
lastTurnId: z.ZodString;
|
|
52
|
+
status: z.ZodOptional<z.ZodEnum<{
|
|
53
|
+
completed: "completed";
|
|
54
|
+
failed: "failed";
|
|
55
|
+
cancelled: "cancelled";
|
|
56
|
+
}>>;
|
|
52
57
|
basis: z.ZodObject<{
|
|
53
58
|
turnId: z.ZodString;
|
|
54
59
|
eventMessageId: z.ZodString;
|
|
@@ -56,6 +56,7 @@ export const sessionEventsQuerySchema = z.object({
|
|
|
56
56
|
export const sessionHeadAdvanceSchema = z.object({
|
|
57
57
|
currentHead: z.string().min(1),
|
|
58
58
|
lastTurnId: z.string().min(1),
|
|
59
|
+
status: z.enum(["completed", "cancelled", "failed"]).optional(),
|
|
59
60
|
basis: z.object({
|
|
60
61
|
turnId: z.string().min(1),
|
|
61
62
|
eventMessageId: z.string().min(1),
|
|
@@ -418,6 +418,7 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
418
418
|
currentHead: message.currentHead,
|
|
419
419
|
lastTurnId: message.lastTurnId,
|
|
420
420
|
basis: message.basis,
|
|
421
|
+
status: message.status,
|
|
421
422
|
...(message.encryptedMetadata ? { encryptedMetadata: message.encryptedMetadata } : {}),
|
|
422
423
|
};
|
|
423
424
|
const result = applySessionHeadAdvance(db, input, ts);
|
|
@@ -634,6 +635,7 @@ function pendingRowToHeadAdvanceInput(row) {
|
|
|
634
635
|
turnSeq: row.basis_turn_seq,
|
|
635
636
|
...(row.basis_occurred_at !== null ? { occurredAt: row.basis_occurred_at } : {}),
|
|
636
637
|
},
|
|
638
|
+
status: "completed",
|
|
637
639
|
...(row.encrypted_metadata ? { encryptedMetadata: JSON.parse(row.encrypted_metadata) } : {}),
|
|
638
640
|
};
|
|
639
641
|
}
|
|
@@ -655,19 +657,28 @@ function applySessionHeadAdvance(db, input, ts) {
|
|
|
655
657
|
sessionRow.head_basis_message_id.startsWith(REWRITE_HEAD_BASIS_PREFIX);
|
|
656
658
|
const comparison = compareHeadBasisOrder(nextOrder, currentOrder);
|
|
657
659
|
const currentHeadIsBasisEvent = sessionRow.current_head === `event:${basisEvent.id}`;
|
|
660
|
+
const currentHeadAlreadySettled = currentHeadIsBasisEvent ||
|
|
661
|
+
sessionRow.current_head === input.currentHead ||
|
|
662
|
+
sessionRow.last_turn_id === input.lastTurnId;
|
|
658
663
|
const shouldAdvance = !rewriteFenced && (comparison > 0 || (comparison === 0 && currentHeadIsBasisEvent));
|
|
659
|
-
|
|
664
|
+
const shouldSettleStatus = !rewriteFenced &&
|
|
665
|
+
input.status !== undefined &&
|
|
666
|
+
sessionRow.status === "running" &&
|
|
667
|
+
comparison === 0 &&
|
|
668
|
+
currentHeadAlreadySettled;
|
|
669
|
+
if (!shouldAdvance && !shouldSettleStatus)
|
|
660
670
|
return { status: "noop", session: sessionRow };
|
|
661
671
|
db.prepare(`UPDATE sessions
|
|
662
|
-
SET
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
672
|
+
SET status = COALESCE(?, status),
|
|
673
|
+
current_head = COALESCE(?, current_head),
|
|
674
|
+
last_turn_id = COALESCE(?, last_turn_id),
|
|
675
|
+
head_basis_event_id = COALESCE(?, head_basis_event_id),
|
|
676
|
+
head_basis_message_id = COALESCE(?, head_basis_message_id),
|
|
677
|
+
head_basis_logical_time = COALESCE(?, head_basis_logical_time),
|
|
678
|
+
head_basis_turn_seq = COALESCE(?, head_basis_turn_seq),
|
|
668
679
|
encrypted_metadata = COALESCE(?, encrypted_metadata),
|
|
669
680
|
updated_at = ?
|
|
670
|
-
WHERE id = ? AND account_id = ? AND machine_id = ?`).run(input.currentHead, input.lastTurnId, basisEvent.id, basisEvent.message_id, nextOrder.time, nextOrder.seq, input.encryptedMetadata ? JSON.stringify(input.encryptedMetadata) : null, ts, input.sessionId, input.accountId, input.machineId);
|
|
681
|
+
WHERE id = ? AND account_id = ? AND machine_id = ?`).run(input.status && sessionRow.status === "running" ? input.status : null, shouldAdvance ? input.currentHead : null, shouldAdvance ? input.lastTurnId : null, shouldAdvance ? basisEvent.id : null, shouldAdvance ? basisEvent.message_id : null, shouldAdvance ? nextOrder.time : null, shouldAdvance ? nextOrder.seq : null, input.encryptedMetadata ? JSON.stringify(input.encryptedMetadata) : null, ts, input.sessionId, input.accountId, input.machineId);
|
|
671
682
|
const session = db.prepare("SELECT * FROM sessions WHERE id = ?").get(input.sessionId);
|
|
672
683
|
return { status: "applied", session };
|
|
673
684
|
}
|
package/package.json
CHANGED
|
@@ -165,6 +165,7 @@ export class ControllerClient {
|
|
|
165
165
|
const checkedSessionId = requireNonEmpty(sessionId, "sessionId");
|
|
166
166
|
const currentHead = requireNonEmpty(input.currentHead, "currentHead");
|
|
167
167
|
const lastTurnId = requireNonEmpty(input.lastTurnId, "lastTurnId");
|
|
168
|
+
const status = input.status;
|
|
168
169
|
const turnId = requireNonEmpty(input.basis.turnId, "basis.turnId");
|
|
169
170
|
const eventMessageId = requireNonEmpty(input.basis.eventMessageId, "basis.eventMessageId");
|
|
170
171
|
return await authenticatedJson(this.config, `/api/sessions/${encodeURIComponent(checkedSessionId)}/head`, {
|
|
@@ -173,6 +174,7 @@ export class ControllerClient {
|
|
|
173
174
|
body: JSON.stringify({
|
|
174
175
|
currentHead,
|
|
175
176
|
lastTurnId,
|
|
177
|
+
...(status ? { status } : {}),
|
|
176
178
|
basis: {
|
|
177
179
|
turnId,
|
|
178
180
|
eventMessageId,
|
|
@@ -183,6 +183,7 @@ export function parseRepairSessionHeadResult(value) {
|
|
|
183
183
|
const reason = record.reason;
|
|
184
184
|
return {
|
|
185
185
|
advanced: readBooleanField(record, "advanced", "Session head repair"),
|
|
186
|
+
...(typeof record.settled === "boolean" ? { settled: record.settled } : {}),
|
|
186
187
|
session: session,
|
|
187
188
|
basis: {
|
|
188
189
|
eventId: readNumberField(basis, "eventId", "Session head repair basis"),
|
|
@@ -205,6 +205,7 @@ export type CollectInput = {
|
|
|
205
205
|
export type RepairSessionHeadInput = {
|
|
206
206
|
currentHead: string;
|
|
207
207
|
lastTurnId: string;
|
|
208
|
+
status?: "completed" | "cancelled" | "failed";
|
|
208
209
|
basis: {
|
|
209
210
|
turnId: string;
|
|
210
211
|
eventMessageId: string;
|
|
@@ -214,6 +215,7 @@ export type RepairSessionHeadInput = {
|
|
|
214
215
|
};
|
|
215
216
|
export type RepairSessionHeadResult = {
|
|
216
217
|
advanced: boolean;
|
|
218
|
+
settled?: boolean;
|
|
217
219
|
session: SessionSnapshot;
|
|
218
220
|
basis: {
|
|
219
221
|
eventId: number;
|
|
@@ -144,6 +144,7 @@ export type MachineClientMessage = {
|
|
|
144
144
|
currentHead: string;
|
|
145
145
|
lastTurnId: string;
|
|
146
146
|
basis: SessionHeadAdvanceBasis;
|
|
147
|
+
status?: "completed" | "cancelled" | "failed";
|
|
147
148
|
encryptedMetadata?: EncryptedEnvelope;
|
|
148
149
|
} | {
|
|
149
150
|
type: "machine:turnDone";
|
|
@@ -169,6 +169,7 @@ const machineMessageSchema = z.discriminatedUnion("type", [
|
|
|
169
169
|
messageId: z.string().min(1).optional(),
|
|
170
170
|
currentHead: z.string().min(1),
|
|
171
171
|
lastTurnId: z.string().min(1),
|
|
172
|
+
status: z.enum(["completed", "cancelled", "failed"]).optional(),
|
|
172
173
|
basis: z.object({
|
|
173
174
|
turnId: z.string().min(1),
|
|
174
175
|
eventMessageId: z.string().min(1),
|