@cydm/happy-elves 0.1.0-beta.40 → 0.1.0-beta.41
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.
|
@@ -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")
|
|
@@ -217,6 +217,130 @@ function diagnosticSessionMetadata(metadata) {
|
|
|
217
217
|
emptyRuntimeSession: metadata.emptyRuntimeSession === true,
|
|
218
218
|
};
|
|
219
219
|
}
|
|
220
|
+
function addRuntimeSessionId(ids, value) {
|
|
221
|
+
if (typeof value === "string" && value.trim())
|
|
222
|
+
ids.add(value.trim());
|
|
223
|
+
}
|
|
224
|
+
function metadataRuntimeSessionIds(metadata) {
|
|
225
|
+
const ids = new Set();
|
|
226
|
+
addRuntimeSessionId(ids, stringMetadata(metadata, "importedFromRuntimeSessionId"));
|
|
227
|
+
const runtime = isRecord(metadata.runtime) ? metadata.runtime : {};
|
|
228
|
+
addRuntimeSessionId(ids, runtime.backendSessionId);
|
|
229
|
+
addRuntimeSessionId(ids, runtime.agentSessionId);
|
|
230
|
+
addRuntimeSessionId(ids, runtime.acpxRecordId);
|
|
231
|
+
if (isRecord(runtime.externalHandle))
|
|
232
|
+
addRuntimeSessionId(ids, runtime.externalHandle.sessionId);
|
|
233
|
+
return ids;
|
|
234
|
+
}
|
|
235
|
+
function normalizedMatchValue(value) {
|
|
236
|
+
return typeof value === "string" && value.trim() ? value.trim().toLocaleLowerCase() : undefined;
|
|
237
|
+
}
|
|
238
|
+
function runtimeBindingCandidates(session, metadata, historicalSessions) {
|
|
239
|
+
const knownRuntimeIds = metadataRuntimeSessionIds(metadata);
|
|
240
|
+
const expectedAgent = stringMetadata(metadata, "agent") ?? session.agent;
|
|
241
|
+
const expectedCwd = stringMetadata(metadata, "cwd") ?? session.cwd;
|
|
242
|
+
const names = new Set([
|
|
243
|
+
normalizedMatchValue(stringMetadata(metadata, "name")),
|
|
244
|
+
normalizedMatchValue(stringMetadata(metadata, "providerName")),
|
|
245
|
+
normalizedMatchValue(session.name),
|
|
246
|
+
].filter((value) => Boolean(value)));
|
|
247
|
+
const summaries = new Set([
|
|
248
|
+
normalizedMatchValue(stringMetadata(metadata, "importedSummary")),
|
|
249
|
+
normalizedMatchValue(session.name),
|
|
250
|
+
].filter((value) => Boolean(value)));
|
|
251
|
+
return historicalSessions
|
|
252
|
+
.filter((historical) => historical.agent === expectedAgent &&
|
|
253
|
+
historical.cwd === expectedCwd &&
|
|
254
|
+
!knownRuntimeIds.has(historical.runtimeSessionId))
|
|
255
|
+
.map((historical) => {
|
|
256
|
+
const reasons = [];
|
|
257
|
+
let score = 0;
|
|
258
|
+
const historicalName = normalizedMatchValue(historical.name ?? historical.runtimeSessionName);
|
|
259
|
+
const historicalSummary = normalizedMatchValue(historical.summary);
|
|
260
|
+
if (historicalName && names.has(historicalName)) {
|
|
261
|
+
score += 100;
|
|
262
|
+
reasons.push("name");
|
|
263
|
+
}
|
|
264
|
+
if (historicalSummary && summaries.has(historicalSummary)) {
|
|
265
|
+
score += 30;
|
|
266
|
+
reasons.push("summary");
|
|
267
|
+
}
|
|
268
|
+
if (historical.loadedSessionId) {
|
|
269
|
+
score -= 20;
|
|
270
|
+
reasons.push("already-loaded");
|
|
271
|
+
}
|
|
272
|
+
if (score <= 0)
|
|
273
|
+
return undefined;
|
|
274
|
+
return {
|
|
275
|
+
runtimeSessionId: historical.runtimeSessionId,
|
|
276
|
+
agent: historical.agent,
|
|
277
|
+
cwd: historical.cwd,
|
|
278
|
+
name: historical.name ?? historical.runtimeSessionName,
|
|
279
|
+
summary: historical.summary,
|
|
280
|
+
updatedAt: historical.updatedAt,
|
|
281
|
+
score,
|
|
282
|
+
reasons,
|
|
283
|
+
...(historical.runtimeState === "archived" ? { runtimeState: "archived" } : {}),
|
|
284
|
+
};
|
|
285
|
+
})
|
|
286
|
+
.filter((candidate) => Boolean(candidate))
|
|
287
|
+
.sort((left, right) => right.score - left.score || right.updatedAt - left.updatedAt || left.runtimeSessionId.localeCompare(right.runtimeSessionId));
|
|
288
|
+
}
|
|
289
|
+
function shouldSearchRuntimeBindingCandidates(latestEvents, metadata) {
|
|
290
|
+
const available = numberMetadata(metadata, "availableHistoricalEvents");
|
|
291
|
+
const emitted = numberMetadata(metadata, "backfilledEvents");
|
|
292
|
+
return latestEvents.length === 0 &&
|
|
293
|
+
(available === undefined || available === 0) &&
|
|
294
|
+
(emitted === undefined || emitted === 0);
|
|
295
|
+
}
|
|
296
|
+
function runtimeBindingSuspected(candidates) {
|
|
297
|
+
return candidates.some((candidate) => candidate.reasons.includes("name") && !candidate.reasons.includes("already-loaded"));
|
|
298
|
+
}
|
|
299
|
+
async function findRuntimeBindingCandidates(client, session, metadata, flags) {
|
|
300
|
+
const limit = parsePositiveIntFlag(flags, "runtime-limit") ?? 500;
|
|
301
|
+
const result = await listWorkspaceHistoricalSessions(client, {
|
|
302
|
+
machineId: session.machineId,
|
|
303
|
+
cwd: stringMetadata(metadata, "cwd") ?? session.cwd,
|
|
304
|
+
agent: stringMetadata(metadata, "agent") ?? session.agent,
|
|
305
|
+
includeArchived: true,
|
|
306
|
+
limit,
|
|
307
|
+
});
|
|
308
|
+
return {
|
|
309
|
+
candidates: runtimeBindingCandidates(session, metadata, result.sessions).slice(0, 8),
|
|
310
|
+
searched: true,
|
|
311
|
+
limit,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
async function resolveRuntimeBindingRepairTarget(client, session, metadata, flags) {
|
|
315
|
+
const runtimeSessionId = typeof flags["runtime-session"] === "string" ? flags["runtime-session"].trim() : undefined;
|
|
316
|
+
const limit = parsePositiveIntFlag(flags, "runtime-limit") ?? 500;
|
|
317
|
+
const result = await listWorkspaceHistoricalSessions(client, {
|
|
318
|
+
machineId: session.machineId,
|
|
319
|
+
cwd: stringMetadata(metadata, "cwd") ?? session.cwd,
|
|
320
|
+
agent: stringMetadata(metadata, "agent") ?? session.agent,
|
|
321
|
+
includeArchived: true,
|
|
322
|
+
limit,
|
|
323
|
+
});
|
|
324
|
+
const candidateDiagnostics = runtimeBindingCandidates(session, metadata, result.sessions);
|
|
325
|
+
if (runtimeSessionId) {
|
|
326
|
+
const explicit = result.sessions.find((item) => item.runtimeSessionId === runtimeSessionId);
|
|
327
|
+
if (!explicit)
|
|
328
|
+
throw new CliError(`Runtime session not found in the first ${limit} historical rows: ${runtimeSessionId}`, "RUNTIME_SESSION_NOT_FOUND");
|
|
329
|
+
return { candidate: explicit, candidates: candidateDiagnostics.slice(0, 8) };
|
|
330
|
+
}
|
|
331
|
+
const usableCandidates = candidateDiagnostics.filter((candidate) => !candidate.reasons.includes("already-loaded"));
|
|
332
|
+
if (usableCandidates.length === 0) {
|
|
333
|
+
throw new CliError("No unambiguous runtime binding repair candidate found. Re-run with --runtime-session <id>.", "SESSION_BINDING_REPAIR_UNAVAILABLE");
|
|
334
|
+
}
|
|
335
|
+
const [first, second] = usableCandidates;
|
|
336
|
+
if (second && second.score === first.score) {
|
|
337
|
+
throw new CliError("Multiple runtime binding repair candidates found. Re-run with --runtime-session <id>.", "SESSION_BINDING_REPAIR_AMBIGUOUS");
|
|
338
|
+
}
|
|
339
|
+
const selected = result.sessions.find((item) => item.runtimeSessionId === first.runtimeSessionId);
|
|
340
|
+
if (!selected)
|
|
341
|
+
throw new CliError("Selected repair candidate disappeared from runtime history", "SESSION_BINDING_REPAIR_UNAVAILABLE");
|
|
342
|
+
return { candidate: selected, candidates: candidateDiagnostics.slice(0, 8) };
|
|
343
|
+
}
|
|
220
344
|
export async function handleSession({ domain, action, positional, flags }) {
|
|
221
345
|
if (domain !== "session")
|
|
222
346
|
return false;
|
|
@@ -372,6 +496,9 @@ export async function handleSession({ domain, action, positional, flags }) {
|
|
|
372
496
|
const turns = summarizeTurns(latestPage.events);
|
|
373
497
|
const latestCompletedTurn = turns.find((turn) => turn.status === "completed");
|
|
374
498
|
const latestTerminalTurn = turns.find((turn) => turn.status !== "running");
|
|
499
|
+
const runtimeBinding = shouldSearchRuntimeBindingCandidates(latestPage.events, metadata)
|
|
500
|
+
? await findRuntimeBindingCandidates(client, session, metadata, flags)
|
|
501
|
+
: { candidates: [], searched: false, limit: parsePositiveIntFlag(flags, "runtime-limit") ?? 500 };
|
|
375
502
|
const repair = flags["repair-head"] === true
|
|
376
503
|
? await client.repairSessionHead(session.id, repairHeadInputForLatestCompletedEvent(latestPage.events))
|
|
377
504
|
: undefined;
|
|
@@ -430,9 +557,20 @@ export async function handleSession({ domain, action, positional, flags }) {
|
|
|
430
557
|
backfillAvailableEvents: numberMetadata(metadata, "availableHistoricalEvents"),
|
|
431
558
|
backfillEmittedEvents: numberMetadata(metadata, "backfilledEvents"),
|
|
432
559
|
backfillStatus: stringMetadata(metadata, "historicalBackfillStatus") ?? stringMetadata(metadata, "historicalBackfill"),
|
|
560
|
+
wrongRuntimeBindingSuspected: runtimeBindingSuspected(runtimeBinding.candidates),
|
|
561
|
+
runtimeBindingCandidateCount: runtimeBinding.candidates.length,
|
|
433
562
|
staleHeadSuspected: staleHeadSuspected(session, latestCompletedTurn),
|
|
434
563
|
headMatchesLatestCompletedTurn: headMatchesTurn(session, latestCompletedTurn),
|
|
435
564
|
},
|
|
565
|
+
runtimeBinding: {
|
|
566
|
+
searched: runtimeBinding.searched,
|
|
567
|
+
searchLimit: runtimeBinding.limit,
|
|
568
|
+
suspected: runtimeBindingSuspected(runtimeBinding.candidates),
|
|
569
|
+
candidates: runtimeBinding.candidates,
|
|
570
|
+
repairCommand: runtimeBinding.candidates[0]
|
|
571
|
+
? `happy-elves session repair-binding ${session.id} --runtime-session ${runtimeBinding.candidates[0].runtimeSessionId} --json`
|
|
572
|
+
: undefined,
|
|
573
|
+
},
|
|
436
574
|
repair: repair ? {
|
|
437
575
|
advanced: repair.advanced,
|
|
438
576
|
basis: repair.basis,
|
|
@@ -452,7 +590,57 @@ 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
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return true;
|
|
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;
|
|
455
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 });
|
|
456
644
|
return true;
|
|
457
645
|
}
|
|
458
646
|
if (action === "history") {
|
package/apps/daemon/package.json
CHANGED