@cydm/happy-elves 0.1.0-beta.277 → 0.1.0-beta.283
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/session.js +43 -8
- package/apps/cli/package.json +1 -1
- package/apps/daemon/dist/session/lifecycle.d.ts +21 -2
- package/apps/daemon/dist/session/lifecycle.js +46 -20
- package/apps/daemon/package.json +1 -1
- package/apps/relay/dist/connections.d.ts +1 -0
- package/apps/relay/dist/controller-handlers.js +11 -11
- package/apps/relay/dist/machine-handlers.js +57 -3
- package/apps/relay/dist/relay-context.js +73 -12
- package/package.json +1 -1
- package/packages/client/dist/client.d.ts +8 -1
- package/packages/client/dist/client.js +51 -2
- package/packages/client/dist/types.d.ts +1 -0
- package/packages/shared/dist/protocol.d.ts +2 -0
- package/packages/shared/dist/protocol.js +2 -0
|
@@ -312,6 +312,26 @@ function shouldSearchRuntimeBindingCandidates(latestEvents, metadata) {
|
|
|
312
312
|
function runtimeBindingSuspected(candidates) {
|
|
313
313
|
return candidates.some((candidate) => candidate.reasons.includes("name") && !candidate.reasons.includes("already-loaded"));
|
|
314
314
|
}
|
|
315
|
+
async function enrichHistoricalLoadedSessionIds(client, sessions) {
|
|
316
|
+
const snapshot = await client.snapshot();
|
|
317
|
+
const runtimeIdToSession = new Map();
|
|
318
|
+
for (const session of snapshot.sessions) {
|
|
319
|
+
if (session.status === "closed")
|
|
320
|
+
continue;
|
|
321
|
+
const metadata = await client.decodeSessionMetadata(session);
|
|
322
|
+
for (const runtimeSessionId of metadataRuntimeSessionIds(metadata)) {
|
|
323
|
+
const key = `${session.machineId}:${session.agent}:${runtimeSessionId}`;
|
|
324
|
+
const existing = runtimeIdToSession.get(key);
|
|
325
|
+
if (!existing || session.createdAt < existing.createdAt || (session.createdAt === existing.createdAt && session.id < existing.id)) {
|
|
326
|
+
runtimeIdToSession.set(key, session);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return sessions.map((historical) => {
|
|
331
|
+
const loaded = runtimeIdToSession.get(`${historical.machineId}:${historical.agent}:${historical.runtimeSessionId}`);
|
|
332
|
+
return loaded ? { ...historical, loadedSessionId: loaded.id } : historical;
|
|
333
|
+
});
|
|
334
|
+
}
|
|
315
335
|
async function findRuntimeBindingCandidates(client, session, metadata, flags) {
|
|
316
336
|
const limit = parsePositiveIntFlag(flags, "runtime-limit") ?? 500;
|
|
317
337
|
const result = await listWorkspaceHistoricalSessions(client, {
|
|
@@ -573,6 +593,15 @@ export async function handleSession({ domain, action, positional, flags }) {
|
|
|
573
593
|
: undefined;
|
|
574
594
|
if (repair)
|
|
575
595
|
session = repair.session;
|
|
596
|
+
const metadataAvailableEvents = numberMetadata(metadata, "availableHistoricalEvents");
|
|
597
|
+
const metadataTotalEvents = numberMetadata(metadata, "historicalBackfillTotalEvents");
|
|
598
|
+
const runtimeCurrentEventCount = numberMetadata(metadata, "runtimeCurrentEventCount");
|
|
599
|
+
const tailMissingEvents = runtimeCurrentEventCount !== undefined
|
|
600
|
+
? Math.max(0, runtimeCurrentEventCount - (metadataTotalEvents ?? metadataAvailableEvents ?? 0))
|
|
601
|
+
: undefined;
|
|
602
|
+
const latestSyncLikelyStale = tailMissingEvents !== undefined
|
|
603
|
+
? tailMissingEvents > 0
|
|
604
|
+
: metadataTotalEvents !== undefined && metadataAvailableEvents !== undefined && metadataAvailableEvents > metadataTotalEvents;
|
|
576
605
|
const data = {
|
|
577
606
|
generatedAt: new Date().toISOString(),
|
|
578
607
|
session: {
|
|
@@ -623,7 +652,11 @@ export async function handleSession({ domain, action, positional, flags }) {
|
|
|
623
652
|
latestPageEmpty: latestPage.events.length === 0,
|
|
624
653
|
latestPageHasVisibleTranscript: latestPage.events.some(isVisibleTranscriptPayloadEvent),
|
|
625
654
|
metadataReportsEmptyRuntimeSession: metadata.emptyRuntimeSession === true,
|
|
626
|
-
|
|
655
|
+
runtimeCurrentEventCount,
|
|
656
|
+
metadataAvailableEvents,
|
|
657
|
+
tailMissingEvents,
|
|
658
|
+
latestSyncLikelyStale,
|
|
659
|
+
backfillAvailableEvents: metadataAvailableEvents,
|
|
627
660
|
backfillEmittedEvents: numberMetadata(metadata, "backfilledEvents"),
|
|
628
661
|
backfillStatus: stringMetadata(metadata, "historicalBackfillStatus") ?? stringMetadata(metadata, "historicalBackfill"),
|
|
629
662
|
wrongRuntimeBindingSuspected: runtimeBindingSuspected(runtimeBinding.candidates),
|
|
@@ -701,7 +734,7 @@ export async function handleSession({ domain, action, positional, flags }) {
|
|
|
701
734
|
sourceSessionId: session.id,
|
|
702
735
|
sessionId: imported.sessionId,
|
|
703
736
|
runtimeSessionId: candidate.runtimeSessionId,
|
|
704
|
-
mode: "created",
|
|
737
|
+
mode: imported.mode === "existing" ? "existing" : "created",
|
|
705
738
|
sourceClosed,
|
|
706
739
|
candidate: {
|
|
707
740
|
runtimeSessionId: candidate.runtimeSessionId,
|
|
@@ -725,7 +758,8 @@ export async function handleSession({ domain, action, positional, flags }) {
|
|
|
725
758
|
includeArchived: flags["include-archived"] === true,
|
|
726
759
|
limit,
|
|
727
760
|
});
|
|
728
|
-
const
|
|
761
|
+
const enrichedSessions = await enrichHistoricalLoadedSessionIds(client, result.sessions);
|
|
762
|
+
const sessions = enrichedSessions.map((session) => ({
|
|
729
763
|
runtimeSessionId: session.runtimeSessionId,
|
|
730
764
|
agent: session.agent,
|
|
731
765
|
cwd: session.cwd,
|
|
@@ -738,11 +772,11 @@ export async function handleSession({ domain, action, positional, flags }) {
|
|
|
738
772
|
if (wantsJson(flags) || wantsVerbose(flags)) {
|
|
739
773
|
ok("session.history", {
|
|
740
774
|
source: "runtime-history",
|
|
741
|
-
sessions: wantsVerbose(flags) ?
|
|
775
|
+
sessions: wantsVerbose(flags) ? enrichedSessions.map((session) => ({ ...session, sessionId: session.loadedSessionId })) : sessions,
|
|
742
776
|
page: {
|
|
743
777
|
limit,
|
|
744
|
-
returned:
|
|
745
|
-
hasMore:
|
|
778
|
+
returned: enrichedSessions.length,
|
|
779
|
+
hasMore: enrichedSessions.length >= limit,
|
|
746
780
|
},
|
|
747
781
|
...(wantsVerbose(flags) ? { errors: result.errors, machines: result.machines } : {}),
|
|
748
782
|
}, { machineId });
|
|
@@ -774,7 +808,8 @@ export async function handleSession({ domain, action, positional, flags }) {
|
|
|
774
808
|
includeArchived: true,
|
|
775
809
|
limit: 200,
|
|
776
810
|
});
|
|
777
|
-
const
|
|
811
|
+
const enrichedSessions = await enrichHistoricalLoadedSessionIds(client, result.sessions);
|
|
812
|
+
const historical = enrichedSessions.find((session) => session.runtimeSessionId === runtimeSessionId);
|
|
778
813
|
if (!historical)
|
|
779
814
|
throw new CliError(`Runtime session not found: ${runtimeSessionId}`, "RUNTIME_SESSION_NOT_FOUND");
|
|
780
815
|
if (historical.loadedSessionId) {
|
|
@@ -802,7 +837,7 @@ export async function handleSession({ domain, action, positional, flags }) {
|
|
|
802
837
|
ok("session.continue", {
|
|
803
838
|
sessionId: continued.sessionId,
|
|
804
839
|
runtimeSessionId,
|
|
805
|
-
mode: "created",
|
|
840
|
+
mode: continued.mode === "existing" ? "existing" : "created",
|
|
806
841
|
...(historical.runtimeState === "archived" ? { runtimeState: "archived" } : {}),
|
|
807
842
|
}, { requestId: continued.requestId, machineId, sessionId: continued.sessionId });
|
|
808
843
|
return true;
|
package/apps/cli/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type MachineCommand } from "../../../../packages/shared/dist/index.js";
|
|
2
|
-
import type { DaemonConfig, HistoricalBackfillStatus, SessionFallback, SessionState } from "../types.js";
|
|
2
|
+
import type { DaemonConfig, HistoricalBackfillState, HistoricalBackfillStatus, SessionFallback, SessionState } from "../types.js";
|
|
3
3
|
import { type HistoricalSessionHead } from "./events.js";
|
|
4
|
+
export type HistoricalBackfillDirection = "latest" | "older";
|
|
5
|
+
type HistoricalBackfillMode = HistoricalBackfillDirection | "auto";
|
|
4
6
|
export type HistoricalBackfillResult = {
|
|
5
7
|
availableEvents: number;
|
|
6
8
|
cursor: number;
|
|
@@ -16,7 +18,7 @@ export type HistoricalBackfillResult = {
|
|
|
16
18
|
export declare function handleCreateSession(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
|
|
17
19
|
export declare function handleImportHistoricalSession(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
|
|
18
20
|
export declare function handleListHistoricalSessions(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
|
|
19
|
-
export declare function resumeSession(ws: WebSocket, config: DaemonConfig, sessionId: string, requestId: string, fallback?: SessionFallback, announceSuccess?: boolean): Promise<SessionState | null>;
|
|
21
|
+
export declare function resumeSession(ws: WebSocket, config: DaemonConfig, sessionId: string, requestId: string, fallback?: SessionFallback, announceSuccess?: boolean, syncDirection?: HistoricalBackfillMode): Promise<SessionState | null>;
|
|
20
22
|
export declare function usesAuthoritativeTranscript(session: SessionState): boolean;
|
|
21
23
|
export declare function replaceAuthoritativeTranscript(ws: WebSocket, config: DaemonConfig, session: SessionState, runtimeSessionId?: string | undefined, input?: {
|
|
22
24
|
currentHead?: string;
|
|
@@ -26,5 +28,22 @@ export declare function replaceAuthoritativeTranscript(ws: WebSocket, config: Da
|
|
|
26
28
|
status?: "completed" | "cancelled" | "failed";
|
|
27
29
|
strict?: boolean;
|
|
28
30
|
}): Promise<HistoricalBackfillResult>;
|
|
31
|
+
export declare function planAppendOnlyHistoricalBackfill<T extends {
|
|
32
|
+
turnId: string;
|
|
33
|
+
}>(historicalEvents: T[], previousBackfill: HistoricalBackfillState | undefined, direction?: HistoricalBackfillMode, maxEvents?: number): {
|
|
34
|
+
cursor: number;
|
|
35
|
+
olderWindow: {
|
|
36
|
+
events: T[];
|
|
37
|
+
nextCursor: number;
|
|
38
|
+
startIndex: number;
|
|
39
|
+
};
|
|
40
|
+
status: HistoricalBackfillStatus;
|
|
41
|
+
tailWindow: {
|
|
42
|
+
events: T[];
|
|
43
|
+
startIndex: number;
|
|
44
|
+
};
|
|
45
|
+
truncated: boolean;
|
|
46
|
+
};
|
|
29
47
|
export declare function handleResume(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
|
|
30
48
|
export declare function handleClose(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
|
|
49
|
+
export {};
|
|
@@ -256,12 +256,14 @@ function historicalSessionPage(value) {
|
|
|
256
256
|
return { sessions: value };
|
|
257
257
|
return value;
|
|
258
258
|
}
|
|
259
|
-
export async function resumeSession(ws, config, sessionId, requestId, fallback, announceSuccess = true) {
|
|
259
|
+
export async function resumeSession(ws, config, sessionId, requestId, fallback, announceSuccess = true, syncDirection = "auto") {
|
|
260
260
|
const existing = sessions.get(sessionId);
|
|
261
261
|
if (existing) {
|
|
262
262
|
const refreshed = await refreshRuntimeSessionName(existing);
|
|
263
263
|
const nameChanged = refreshed.name !== existing.name;
|
|
264
|
-
const backfill = await backfillHistoricalEvents(ws, config, refreshed, undefined, refreshed.historicalBackfill ?? fallback?.historicalBackfill
|
|
264
|
+
const backfill = await backfillHistoricalEvents(ws, config, refreshed, undefined, refreshed.historicalBackfill ?? fallback?.historicalBackfill, {
|
|
265
|
+
direction: syncDirection,
|
|
266
|
+
});
|
|
265
267
|
const resumed = withHistoricalBackfill(refreshed, backfill);
|
|
266
268
|
sessions.set(sessionId, resumed);
|
|
267
269
|
await appendAudit({
|
|
@@ -311,7 +313,9 @@ export async function resumeSession(ws, config, sessionId, requestId, fallback,
|
|
|
311
313
|
const state = await refreshRuntimeSessionName({ sessionId, agent, cwd, name, handle });
|
|
312
314
|
sessions.set(sessionId, state);
|
|
313
315
|
const nameChanged = state.name !== name;
|
|
314
|
-
const backfill = await backfillHistoricalEvents(ws, config, state, resumeSessionId, fallback?.historicalBackfill
|
|
316
|
+
const backfill = await backfillHistoricalEvents(ws, config, state, resumeSessionId, fallback?.historicalBackfill, {
|
|
317
|
+
direction: syncDirection,
|
|
318
|
+
});
|
|
315
319
|
const resumed = withHistoricalBackfill(state, backfill);
|
|
316
320
|
sessions.set(sessionId, resumed);
|
|
317
321
|
await appendAudit({
|
|
@@ -558,7 +562,7 @@ async function backfillHistoricalEvents(ws, config, session, runtimeSessionId =
|
|
|
558
562
|
if (!runtimeSessionId)
|
|
559
563
|
return completedBackfillResult(0);
|
|
560
564
|
if (usesAuthoritativeTranscript(session)) {
|
|
561
|
-
if (previousBackfill?.status === "partial" && previousBackfill.providerCursor) {
|
|
565
|
+
if ((options.direction ?? "latest") === "older" && previousBackfill?.status === "partial" && previousBackfill.providerCursor) {
|
|
562
566
|
return await prependAuthoritativeTranscript(ws, config, session, runtimeSessionId, previousBackfill, { strict: options.strict });
|
|
563
567
|
}
|
|
564
568
|
return await replaceAuthoritativeTranscript(ws, config, session, runtimeSessionId, { strict: options.strict });
|
|
@@ -580,33 +584,37 @@ async function backfillHistoricalEvents(ws, config, session, runtimeSessionId =
|
|
|
580
584
|
throw error;
|
|
581
585
|
historicalEvents = [];
|
|
582
586
|
}
|
|
583
|
-
const
|
|
587
|
+
const direction = options.direction ?? "auto";
|
|
588
|
+
const plan = planAppendOnlyHistoricalBackfill(historicalEvents, previousBackfill, direction);
|
|
589
|
+
const tailWindow = plan.tailWindow;
|
|
584
590
|
if (tailWindow.events.length > 0) {
|
|
585
591
|
await emitRuntimeTailEvents(ws, config, session.sessionId, tailWindow.events, tailWindow.startIndex);
|
|
586
592
|
}
|
|
587
|
-
const
|
|
588
|
-
const olderWindow = historicalEventsWindowByTurn(historicalEvents, cursor, historicalBackfillEventLimit);
|
|
593
|
+
const olderWindow = plan.olderWindow;
|
|
589
594
|
if (olderWindow.events.length > 0) {
|
|
590
595
|
await emitHistoricalEvents(ws, config, session.sessionId, olderWindow.events, olderWindow.startIndex);
|
|
591
596
|
}
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
597
|
+
const computedHead = direction === "older" && previousBackfill
|
|
598
|
+
? previousBackfill.head
|
|
599
|
+
: latestCompletedHistoricalHead(session.sessionId, historicalEvents, {
|
|
600
|
+
...(previousBackfill?.totalEvents !== undefined && previousBackfill.head === undefined
|
|
601
|
+
? { preferTailBasisForExistingEvents: true }
|
|
602
|
+
: {}),
|
|
603
|
+
...(previousBackfill?.totalEvents !== undefined ? { previousTotalEvents: previousBackfill.totalEvents } : {}),
|
|
604
|
+
...(tailWindow.events.length > 0 ? { tailStartIndex: tailWindow.startIndex } : {}),
|
|
605
|
+
});
|
|
606
|
+
const head = direction === "older" && previousBackfill
|
|
607
|
+
? previousBackfill.head
|
|
608
|
+
: preferPersistedHistoricalHeadBasis(computedHead, previousBackfill?.head);
|
|
601
609
|
const result = {
|
|
602
610
|
emittedEvents: tailWindow.events.length + olderWindow.events.length,
|
|
603
611
|
availableEvents: historicalEvents.length,
|
|
604
|
-
cursor:
|
|
612
|
+
cursor: plan.cursor,
|
|
605
613
|
olderBackfilledEvents: olderWindow.events.length,
|
|
606
|
-
status,
|
|
614
|
+
status: plan.status,
|
|
607
615
|
tailRepairedEvents: tailWindow.events.length,
|
|
608
616
|
totalEvents: historicalEvents.length,
|
|
609
|
-
truncated:
|
|
617
|
+
truncated: plan.truncated,
|
|
610
618
|
...(head ? { head } : {}),
|
|
611
619
|
};
|
|
612
620
|
if (head) {
|
|
@@ -648,6 +656,23 @@ function normalizedBackfillCursor(previousBackfill, totalEvents) {
|
|
|
648
656
|
return 0;
|
|
649
657
|
return Math.max(0, Math.min(previousBackfill.cursor, totalEvents));
|
|
650
658
|
}
|
|
659
|
+
export function planAppendOnlyHistoricalBackfill(historicalEvents, previousBackfill, direction = "auto", maxEvents = historicalBackfillEventLimit) {
|
|
660
|
+
const tailWindow = direction === "latest" || direction === "auto"
|
|
661
|
+
? historicalTailWindowByTurn(historicalEvents, previousBackfill?.totalEvents)
|
|
662
|
+
: { events: [], startIndex: historicalEvents.length };
|
|
663
|
+
const cursor = normalizedBackfillCursor(previousBackfill, historicalEvents.length);
|
|
664
|
+
const olderWindow = direction === "older" || direction === "auto" || previousBackfill === undefined
|
|
665
|
+
? historicalEventsWindowByTurn(historicalEvents, cursor, maxEvents)
|
|
666
|
+
: { events: [], nextCursor: cursor, startIndex: cursor };
|
|
667
|
+
const status = olderWindow.nextCursor > 0 ? "partial" : "completed";
|
|
668
|
+
return {
|
|
669
|
+
cursor: olderWindow.nextCursor,
|
|
670
|
+
olderWindow,
|
|
671
|
+
status,
|
|
672
|
+
tailWindow,
|
|
673
|
+
truncated: status === "partial",
|
|
674
|
+
};
|
|
675
|
+
}
|
|
651
676
|
function historicalTailWindowByTurn(events, previousTotalEvents) {
|
|
652
677
|
if (previousTotalEvents === undefined || events.length <= previousTotalEvents) {
|
|
653
678
|
return { events: [], startIndex: events.length };
|
|
@@ -722,6 +747,7 @@ function historicalBackfillMetadata(backfill) {
|
|
|
722
747
|
historicalOlderBackfilledEvents: backfill.olderBackfilledEvents,
|
|
723
748
|
historicalTailRepairedEvents: backfill.tailRepairedEvents,
|
|
724
749
|
historicalHead: backfill.head,
|
|
750
|
+
runtimeCurrentEventCount: backfill.totalEvents,
|
|
725
751
|
availableHistoricalEvents: backfill.availableEvents,
|
|
726
752
|
truncatedHistoricalBackfill: backfill.truncated,
|
|
727
753
|
};
|
|
@@ -731,7 +757,7 @@ export async function handleResume(ws, config, command) {
|
|
|
731
757
|
return;
|
|
732
758
|
if (!claimCommandRequest(ws, command.requestId))
|
|
733
759
|
return;
|
|
734
|
-
await resumeSession(ws, config, command.sessionId, command.requestId, await sessionFallbackFromCommand(config, command));
|
|
760
|
+
await resumeSession(ws, config, command.sessionId, command.requestId, await sessionFallbackFromCommand(config, command), true, command.syncDirection ?? "auto");
|
|
735
761
|
}
|
|
736
762
|
export async function handleClose(ws, config, command) {
|
|
737
763
|
if (command.type !== "machine:close")
|
package/apps/daemon/package.json
CHANGED
|
@@ -354,6 +354,7 @@ export function handleControllerMessage(context, connection, message) {
|
|
|
354
354
|
type: "machine:resume",
|
|
355
355
|
requestId: message.requestId,
|
|
356
356
|
sessionId: message.sessionId,
|
|
357
|
+
...(message.syncDirection ? { syncDirection: message.syncDirection } : {}),
|
|
357
358
|
encryptedMetadata: sessionRow.encrypted_metadata ? parseJson(sessionRow.encrypted_metadata, undefined) : undefined,
|
|
358
359
|
});
|
|
359
360
|
return;
|
|
@@ -784,21 +785,20 @@ export function handleControllerMessage(context, connection, message) {
|
|
|
784
785
|
sendAck(connection, { type: "server:ack", requestId: message.requestId, ok: true });
|
|
785
786
|
return;
|
|
786
787
|
}
|
|
788
|
+
const closedSession = setSessionStatus(sessionRow.id, "closed");
|
|
789
|
+
finishSessionClaim({
|
|
790
|
+
accountId: connection.accountId,
|
|
791
|
+
sessionId: sessionRow.id,
|
|
792
|
+
terminalStatus: "cancelled",
|
|
793
|
+
errorCode: "SESSION_CLOSED",
|
|
794
|
+
});
|
|
795
|
+
if (closedSession)
|
|
796
|
+
broadcastControllers(connection.accountId, { type: "server:session", session: closedSession });
|
|
797
|
+
sendAck(connection, { type: "server:ack", requestId: message.requestId, ok: true });
|
|
787
798
|
const machineConnection = connectedMachine(connection.accountId, sessionRow.machine_id);
|
|
788
799
|
if (!machineConnection) {
|
|
789
|
-
const session = setSessionStatus(sessionRow.id, "closed");
|
|
790
|
-
finishSessionClaim({
|
|
791
|
-
accountId: connection.accountId,
|
|
792
|
-
sessionId: sessionRow.id,
|
|
793
|
-
terminalStatus: "cancelled",
|
|
794
|
-
errorCode: "SESSION_CLOSED",
|
|
795
|
-
});
|
|
796
|
-
if (session)
|
|
797
|
-
broadcastControllers(connection.accountId, { type: "server:session", session });
|
|
798
|
-
sendAck(connection, { type: "server:ack", requestId: message.requestId, ok: true });
|
|
799
800
|
return;
|
|
800
801
|
}
|
|
801
|
-
markCommandPending(connection.accountId, sessionRow.machine_id, message.requestId);
|
|
802
802
|
send(machineConnection.socket, {
|
|
803
803
|
type: "machine:close",
|
|
804
804
|
requestId: message.requestId,
|
|
@@ -32,7 +32,7 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
32
32
|
updated_at = ?
|
|
33
33
|
WHERE id = ? AND account_id = ? AND machine_id = ? AND status != 'closed'`).run(message.name ?? null, message.encryptedMetadata ? JSON.stringify(message.encryptedMetadata) : null, ts, message.sessionId, connection.accountId, connection.machineId);
|
|
34
34
|
const row = db.prepare("SELECT * FROM sessions WHERE id = ?").get(message.sessionId);
|
|
35
|
-
if (row)
|
|
35
|
+
if (row && row.status !== "closed")
|
|
36
36
|
broadcastControllers(connection.accountId, { type: "server:session", session: sessionSnapshot(row) });
|
|
37
37
|
broadcastAck(connection.accountId, {
|
|
38
38
|
type: "server:ack",
|
|
@@ -52,7 +52,7 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
52
52
|
updated_at = ?
|
|
53
53
|
WHERE id = ? AND account_id = ? AND machine_id = ? AND status != 'closed'`).run(message.name ?? null, message.encryptedMetadata ? JSON.stringify(message.encryptedMetadata) : null, ts, message.sessionId, connection.accountId, connection.machineId);
|
|
54
54
|
const row = db.prepare("SELECT * FROM sessions WHERE id = ?").get(message.sessionId);
|
|
55
|
-
if (row)
|
|
55
|
+
if (row && row.status !== "closed")
|
|
56
56
|
broadcastControllers(connection.accountId, { type: "server:session", session: sessionSnapshot(row) });
|
|
57
57
|
broadcastAck(connection.accountId, { type: "server:ack", requestId: message.requestId, ok: true });
|
|
58
58
|
acknowledgeMachineMessage();
|
|
@@ -223,7 +223,7 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
223
223
|
current_head = COALESCE(?, current_head),
|
|
224
224
|
last_turn_id = COALESCE(?, last_turn_id),
|
|
225
225
|
updated_at = ?
|
|
226
|
-
WHERE id = ? AND account_id = ? AND machine_id =
|
|
226
|
+
WHERE id = ? AND account_id = ? AND machine_id = ? AND status != 'closed'`).run(message.result.terminalStatus, message.result.currentHead ?? null, message.result.lastTurnId ?? message.result.turnId ?? null, ts, message.result.sessionId, connection.accountId, connection.machineId);
|
|
227
227
|
finishSessionClaim({
|
|
228
228
|
accountId: connection.accountId,
|
|
229
229
|
sessionId: message.result.sessionId,
|
|
@@ -237,6 +237,30 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
237
237
|
if (row)
|
|
238
238
|
broadcastControllers(connection.accountId, { type: "server:session", session: sessionSnapshot(row) });
|
|
239
239
|
}
|
|
240
|
+
if (message.result.status === "unknown" || message.result.status === "not_found" || message.result.status === "uninspectable") {
|
|
241
|
+
const claim = activePromptClaim(db, connection.accountId, message.result.sessionId, connection.machineId);
|
|
242
|
+
const turnId = message.result.turnId ?? claim?.turn_id;
|
|
243
|
+
const requestId = message.result.requestId ?? claim?.request_id;
|
|
244
|
+
const claimMatches = Boolean(claim) &&
|
|
245
|
+
(!message.result.requestId || message.result.requestId === claim?.request_id) &&
|
|
246
|
+
(!message.result.turnId || message.result.turnId === claim?.turn_id);
|
|
247
|
+
if (claimMatches && turnId) {
|
|
248
|
+
db.prepare(`UPDATE sessions
|
|
249
|
+
SET status = 'failed',
|
|
250
|
+
current_head = COALESCE(current_head, ?),
|
|
251
|
+
last_turn_id = ?,
|
|
252
|
+
updated_at = ?
|
|
253
|
+
WHERE id = ? AND account_id = ? AND machine_id = ? AND status != 'closed'`).run(turnId, turnId, ts, message.result.sessionId, connection.accountId, connection.machineId);
|
|
254
|
+
if (requestId) {
|
|
255
|
+
db.prepare(`UPDATE relay_admissions
|
|
256
|
+
SET last_error_code = COALESCE(last_error_code, ?), updated_at = ?
|
|
257
|
+
WHERE account_id = ? AND session_id = ? AND request_id = ?`).run(message.result.status === "not_found" ? "EXECUTION_NOT_FOUND" : "EXECUTION_UNKNOWN", ts, connection.accountId, message.result.sessionId, requestId);
|
|
258
|
+
}
|
|
259
|
+
const row = db.prepare("SELECT * FROM sessions WHERE id = ?").get(message.result.sessionId);
|
|
260
|
+
if (row)
|
|
261
|
+
broadcastControllers(connection.accountId, { type: "server:session", session: sessionSnapshot(row) });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
240
264
|
rememberCommandResponse(connection.accountId, message.requestId, response);
|
|
241
265
|
clearCommandPending(connection.accountId, message.requestId);
|
|
242
266
|
broadcastControllers(connection.accountId, response);
|
|
@@ -371,6 +395,10 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
371
395
|
acknowledgeMachineMessage();
|
|
372
396
|
return;
|
|
373
397
|
}
|
|
398
|
+
if (sessionRow.status === "closed") {
|
|
399
|
+
acknowledgeMachineMessage();
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
374
402
|
const replacementBasis = message.basis;
|
|
375
403
|
const replacementBasisEvent = replacementBasis
|
|
376
404
|
? message.events.find((event) => event.messageId === replacementBasis.eventMessageId &&
|
|
@@ -508,6 +536,10 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
508
536
|
acknowledgeMachineMessage();
|
|
509
537
|
return;
|
|
510
538
|
}
|
|
539
|
+
if (sessionRow.status === "closed") {
|
|
540
|
+
acknowledgeMachineMessage();
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
511
543
|
const ts = now();
|
|
512
544
|
const inserted = [];
|
|
513
545
|
db.exec("BEGIN");
|
|
@@ -570,6 +602,10 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
570
602
|
const sessionBefore = db
|
|
571
603
|
.prepare("SELECT * FROM sessions WHERE id = ? AND account_id = ? AND machine_id = ?")
|
|
572
604
|
.get(message.sessionId, connection.accountId, connection.machineId);
|
|
605
|
+
if (sessionBefore?.status === "closed") {
|
|
606
|
+
acknowledgeMachineMessage();
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
573
609
|
const activeClaim = sessionBefore
|
|
574
610
|
? activePromptClaim(db, connection.accountId, message.sessionId, connection.machineId)
|
|
575
611
|
: undefined;
|
|
@@ -651,6 +687,10 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
651
687
|
.get(message.sessionId, connection.accountId, connection.machineId);
|
|
652
688
|
if (!sessionRow)
|
|
653
689
|
return;
|
|
690
|
+
if (sessionRow.status === "closed") {
|
|
691
|
+
acknowledgeMachineMessage();
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
654
694
|
maybeAliasHistoricalRuntimeEventToActiveClaim({
|
|
655
695
|
db,
|
|
656
696
|
accountId: connection.accountId,
|
|
@@ -709,6 +749,18 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
709
749
|
acknowledgeMachineMessage();
|
|
710
750
|
return;
|
|
711
751
|
}
|
|
752
|
+
if (sessionRow.status === "closed") {
|
|
753
|
+
finishSessionClaim({
|
|
754
|
+
accountId: connection.accountId,
|
|
755
|
+
sessionId: message.sessionId,
|
|
756
|
+
turnId: message.turnId,
|
|
757
|
+
terminalStatus: status,
|
|
758
|
+
...(message.error ? { errorCode: "TURN_ERROR" } : {}),
|
|
759
|
+
finishedAt: ts,
|
|
760
|
+
});
|
|
761
|
+
acknowledgeMachineMessage();
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
712
764
|
const currentOrder = sessionHeadBasisOrder(db, sessionRow);
|
|
713
765
|
const basisComparison = basisOrder ? compareHeadBasisOrder(basisOrder, currentOrder) : null;
|
|
714
766
|
const isRuntimeAliasForCurrentRequest = Boolean(runtimeBasisEvent && runtimeTurnId !== message.turnId && (sessionRow.last_turn_id === message.turnId || requestBasisEvent));
|
|
@@ -860,6 +912,8 @@ function applySessionHeadAdvance(db, input, ts) {
|
|
|
860
912
|
.get(input.sessionId, input.accountId, input.machineId);
|
|
861
913
|
if (!sessionRow)
|
|
862
914
|
return { status: "session-not-found" };
|
|
915
|
+
if (sessionRow.status === "closed")
|
|
916
|
+
return { status: "noop", session: sessionRow };
|
|
863
917
|
const basisEvent = resolveHeadAdvanceBasisEvent(db, input.accountId, input.sessionId, input.machineId, input.basis);
|
|
864
918
|
if (!basisEvent)
|
|
865
919
|
return { status: "missing", session: sessionRow };
|
|
@@ -6,6 +6,7 @@ import { createRelayRepositories } from "./repositories.js";
|
|
|
6
6
|
import { filterSnapshotForScope, isFullControllerToken, messageForScopedController, scopedListAllows, scopedTokenPublic, tokenScope, } from "./scope.js";
|
|
7
7
|
import { bearerToken, encodePairingCode, isPairingCodeMatch, tokenDigest } from "./security.js";
|
|
8
8
|
const REWRITE_HEAD_BASIS_PREFIX = "rewrite:";
|
|
9
|
+
const PENDING_COMMAND_TTL_MS = 120_000;
|
|
9
10
|
export function createRelayContext(options) {
|
|
10
11
|
const { controllersByAccount, machinesByKey, pendingCommandRequests } = createConnectionState();
|
|
11
12
|
const now = options.now ?? Date.now;
|
|
@@ -201,7 +202,19 @@ export function createRelayContext(options) {
|
|
|
201
202
|
.get(accountId, sessionId);
|
|
202
203
|
if (!claim)
|
|
203
204
|
return undefined;
|
|
204
|
-
|
|
205
|
+
const staleResolution = staleSessionClaimResolution(claim);
|
|
206
|
+
if (staleResolution) {
|
|
207
|
+
if (staleResolution.terminalStatus) {
|
|
208
|
+
const ts = staleResolution.finishedAt ?? now();
|
|
209
|
+
options.db
|
|
210
|
+
.prepare(`UPDATE relay_admissions
|
|
211
|
+
SET finished_at = COALESCE(finished_at, ?),
|
|
212
|
+
terminal_status = COALESCE(terminal_status, ?),
|
|
213
|
+
last_error_code = COALESCE(?, last_error_code),
|
|
214
|
+
updated_at = ?
|
|
215
|
+
WHERE account_id = ? AND request_id = ?`)
|
|
216
|
+
.run(ts, staleResolution.terminalStatus, staleResolution.errorCode ?? null, ts, claim.account_id, claim.request_id);
|
|
217
|
+
}
|
|
205
218
|
options.db
|
|
206
219
|
.prepare("DELETE FROM relay_session_claims WHERE account_id = ? AND session_id = ? AND request_id = ?")
|
|
207
220
|
.run(claim.account_id, claim.session_id, claim.request_id);
|
|
@@ -209,14 +222,14 @@ export function createRelayContext(options) {
|
|
|
209
222
|
}
|
|
210
223
|
return claim;
|
|
211
224
|
}
|
|
212
|
-
function
|
|
225
|
+
function staleSessionClaimResolution(claim) {
|
|
213
226
|
const admission = options.db
|
|
214
227
|
.prepare(`SELECT dispatch, finished_at, terminal_status
|
|
215
228
|
FROM relay_admissions
|
|
216
229
|
WHERE account_id = ? AND request_id = ?`)
|
|
217
230
|
.get(claim.account_id, claim.request_id);
|
|
218
231
|
if (admission && (admission.finished_at !== null || admission.terminal_status !== null))
|
|
219
|
-
return
|
|
232
|
+
return {};
|
|
220
233
|
const session = options.db
|
|
221
234
|
.prepare(`SELECT status, current_head, last_turn_id, updated_at
|
|
222
235
|
FROM sessions
|
|
@@ -224,11 +237,13 @@ export function createRelayContext(options) {
|
|
|
224
237
|
.get(claim.account_id, claim.session_id);
|
|
225
238
|
if (admission?.dispatch === "delivered" &&
|
|
226
239
|
session &&
|
|
227
|
-
|
|
240
|
+
isSettledSessionProjectionStatus(session.status) &&
|
|
228
241
|
session.updated_at > claim.created_at &&
|
|
229
|
-
session.current_head !== claim.turn_id &&
|
|
230
|
-
|
|
231
|
-
|
|
242
|
+
(session.last_turn_id === claim.turn_id || (session.current_head !== claim.turn_id && session.last_turn_id !== claim.turn_id))) {
|
|
243
|
+
return {
|
|
244
|
+
terminalStatus: terminalStatusForSettledSessionStatus(session.status),
|
|
245
|
+
finishedAt: session.updated_at,
|
|
246
|
+
};
|
|
232
247
|
}
|
|
233
248
|
const newerTerminal = options.db
|
|
234
249
|
.prepare(`SELECT 1
|
|
@@ -242,7 +257,17 @@ export function createRelayContext(options) {
|
|
|
242
257
|
AND created_at > ?
|
|
243
258
|
LIMIT 1`)
|
|
244
259
|
.get(claim.account_id, claim.session_id, claim.request_id, claim.created_at);
|
|
245
|
-
return
|
|
260
|
+
return newerTerminal ? {} : undefined;
|
|
261
|
+
}
|
|
262
|
+
function terminalStatusForSettledSessionStatus(status) {
|
|
263
|
+
if (status === "failed")
|
|
264
|
+
return "failed";
|
|
265
|
+
if (status === "cancelled" || status === "closed")
|
|
266
|
+
return "cancelled";
|
|
267
|
+
return "completed";
|
|
268
|
+
}
|
|
269
|
+
function isSettledSessionProjectionStatus(status) {
|
|
270
|
+
return status === "ready" || status === "completed" || status === "cancelled" || status === "closed";
|
|
246
271
|
}
|
|
247
272
|
function claimSessionTurn(params) {
|
|
248
273
|
const existing = activeSessionClaim(params.accountId, params.sessionId);
|
|
@@ -290,13 +315,22 @@ export function createRelayContext(options) {
|
|
|
290
315
|
function commandKey(accountId, requestId) {
|
|
291
316
|
return `${accountId}:${requestId}`;
|
|
292
317
|
}
|
|
318
|
+
function prunePendingCommands(ts = now()) {
|
|
319
|
+
for (const [key, pending] of pendingCommandRequests) {
|
|
320
|
+
if (ts - pending.createdAt > PENDING_COMMAND_TTL_MS)
|
|
321
|
+
pendingCommandRequests.delete(key);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
293
324
|
function markCommandPending(accountId, machineId, requestId, options = {}) {
|
|
294
|
-
|
|
325
|
+
prunePendingCommands();
|
|
326
|
+
pendingCommandRequests.set(commandKey(accountId, requestId), { accountId, machineId, requestId, createdAt: now(), ...options });
|
|
295
327
|
}
|
|
296
328
|
function isCommandPending(accountId, requestId) {
|
|
329
|
+
prunePendingCommands();
|
|
297
330
|
return pendingCommandRequests.has(commandKey(accountId, requestId));
|
|
298
331
|
}
|
|
299
332
|
function isSessionCommandPending(accountId, sessionId) {
|
|
333
|
+
prunePendingCommands();
|
|
300
334
|
for (const pending of pendingCommandRequests.values()) {
|
|
301
335
|
if (pending.accountId === accountId && pending.sessionId === sessionId)
|
|
302
336
|
return true;
|
|
@@ -366,6 +400,29 @@ export function createRelayContext(options) {
|
|
|
366
400
|
if (!machine)
|
|
367
401
|
return "queued";
|
|
368
402
|
const command = JSON.parse(row.message);
|
|
403
|
+
if (command.type === "machine:prompt") {
|
|
404
|
+
const session = options.db
|
|
405
|
+
.prepare("SELECT status FROM sessions WHERE account_id = ? AND id = ? AND machine_id = ?")
|
|
406
|
+
.get(accountId, command.sessionId, row.machine_id);
|
|
407
|
+
if (session?.status === "closed") {
|
|
408
|
+
const ts = now();
|
|
409
|
+
options.db
|
|
410
|
+
.prepare(`UPDATE relay_dispatch_outbox
|
|
411
|
+
SET state = 'delivered', delivered_at = ?, updated_at = ?
|
|
412
|
+
WHERE account_id = ? AND request_id = ? AND state = 'queued'`)
|
|
413
|
+
.run(ts, ts, accountId, requestId);
|
|
414
|
+
finishSessionClaim({
|
|
415
|
+
accountId,
|
|
416
|
+
sessionId: command.sessionId,
|
|
417
|
+
requestId,
|
|
418
|
+
turnId: command.turnId,
|
|
419
|
+
terminalStatus: "cancelled",
|
|
420
|
+
errorCode: "SESSION_CLOSED",
|
|
421
|
+
finishedAt: ts,
|
|
422
|
+
});
|
|
423
|
+
return "queued";
|
|
424
|
+
}
|
|
425
|
+
}
|
|
369
426
|
send(machine.socket, command);
|
|
370
427
|
const ts = now();
|
|
371
428
|
options.db
|
|
@@ -499,10 +556,10 @@ export function createRelayContext(options) {
|
|
|
499
556
|
head_basis_logical_time = ?,
|
|
500
557
|
head_basis_turn_seq = ?,
|
|
501
558
|
updated_at = ?
|
|
502
|
-
WHERE id =
|
|
559
|
+
WHERE id = ? AND status != 'closed'`).run(`event:${eventId}`, params.turnId, eventId, params.messageId ?? null, order.time, order.seq, createdAt, params.sessionId);
|
|
503
560
|
}
|
|
504
561
|
else {
|
|
505
|
-
options.db.prepare("UPDATE sessions SET updated_at = ? WHERE id = ?").run(createdAt, params.sessionId);
|
|
562
|
+
options.db.prepare("UPDATE sessions SET updated_at = ? WHERE id = ? AND status != 'closed'").run(createdAt, params.sessionId);
|
|
506
563
|
}
|
|
507
564
|
return {
|
|
508
565
|
id: eventId,
|
|
@@ -525,6 +582,8 @@ export function createRelayContext(options) {
|
|
|
525
582
|
WHERE account_id = ? AND id = ? AND machine_id = ?`)
|
|
526
583
|
.get(params.accountId, params.sessionId, params.machineId);
|
|
527
584
|
const currentHead = session?.current_head;
|
|
585
|
+
if (session?.status === "closed")
|
|
586
|
+
return false;
|
|
528
587
|
if (!currentHead)
|
|
529
588
|
return true;
|
|
530
589
|
if (typeof session?.head_basis_message_id === "string" &&
|
|
@@ -635,7 +694,9 @@ export function createRelayContext(options) {
|
|
|
635
694
|
}
|
|
636
695
|
function setSessionStatus(sessionId, status) {
|
|
637
696
|
const ts = now();
|
|
638
|
-
options.db
|
|
697
|
+
options.db
|
|
698
|
+
.prepare("UPDATE sessions SET status = ?, updated_at = ? WHERE id = ? AND (status != 'closed' OR ? = 'closed')")
|
|
699
|
+
.run(status, ts, sessionId, status);
|
|
639
700
|
const row = options.db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
640
701
|
return row ? sessionSnapshot(row) : undefined;
|
|
641
702
|
}
|
package/package.json
CHANGED
|
@@ -59,6 +59,11 @@ export declare class ControllerClient {
|
|
|
59
59
|
revokedAt: number;
|
|
60
60
|
}>;
|
|
61
61
|
listSessions(filter?: SessionFilter): Promise<SessionSnapshot[]>;
|
|
62
|
+
findLoadedRuntimeSession(input: {
|
|
63
|
+
machineId: string;
|
|
64
|
+
agent: string;
|
|
65
|
+
runtimeSessionId: string;
|
|
66
|
+
}): Promise<SessionSnapshot | undefined>;
|
|
62
67
|
getSession(sessionId: string): Promise<SessionSnapshot | undefined>;
|
|
63
68
|
history(sessionId: string, limit?: number): Promise<DecodedSessionEvent[]>;
|
|
64
69
|
createSession(input: CreateSessionInput): Promise<CommandResult>;
|
|
@@ -70,7 +75,9 @@ export declare class ControllerClient {
|
|
|
70
75
|
collectPage(sessionId: string, input?: CollectInput): Promise<DecodedSessionEventsPage>;
|
|
71
76
|
repairSessionHead(sessionId: string, input: RepairSessionHeadInput): Promise<RepairSessionHeadResult>;
|
|
72
77
|
wait(sessionId: string, input?: WaitInput): Promise<SessionSnapshot>;
|
|
73
|
-
resume(sessionId: string
|
|
78
|
+
resume(sessionId: string, options?: {
|
|
79
|
+
syncDirection?: "latest" | "older";
|
|
80
|
+
}): Promise<CommandResult>;
|
|
74
81
|
listHistoricalSessionsPage(input: HistoricalSessionsInput): Promise<HistoricalSessionListPage>;
|
|
75
82
|
listHistoricalSessions(input: HistoricalSessionsInput): Promise<HistoricalSessionSnapshot[]>;
|
|
76
83
|
listDirectory(input: DirectoryInput): Promise<DirectoryListing>;
|
|
@@ -7,6 +7,29 @@ import { sendAndWaitForDirectoryListing, sendAndWaitForDaemonLifecycleResult, se
|
|
|
7
7
|
import { DEFAULT_SESSION_CREATE_WAIT_MS, } from "./types.js";
|
|
8
8
|
import { isWaitCondition, matchesWaitCondition, parseEventCursor, parseEventLimit, parsePositiveDuration, requireNonEmpty, sleep, } from "./validation.js";
|
|
9
9
|
const DEFAULT_BLOCKING_RUN_WAIT_MS = 30 * 60 * 1000;
|
|
10
|
+
function addRuntimeSessionId(ids, value) {
|
|
11
|
+
if (typeof value === "string" && value.trim())
|
|
12
|
+
ids.add(value.trim());
|
|
13
|
+
}
|
|
14
|
+
function runtimeSessionIdsForMetadata(metadata) {
|
|
15
|
+
const ids = new Set();
|
|
16
|
+
addRuntimeSessionId(ids, metadata.importedFromRuntimeSessionId);
|
|
17
|
+
const runtime = metadata.runtime;
|
|
18
|
+
if (runtime && typeof runtime === "object" && !Array.isArray(runtime)) {
|
|
19
|
+
const record = runtime;
|
|
20
|
+
addRuntimeSessionId(ids, record.backendSessionId);
|
|
21
|
+
addRuntimeSessionId(ids, record.agentSessionId);
|
|
22
|
+
addRuntimeSessionId(ids, record.acpxRecordId);
|
|
23
|
+
const externalHandle = record.externalHandle;
|
|
24
|
+
if (externalHandle && typeof externalHandle === "object" && !Array.isArray(externalHandle)) {
|
|
25
|
+
addRuntimeSessionId(ids, externalHandle.sessionId);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return ids;
|
|
29
|
+
}
|
|
30
|
+
function isCanonicalRuntimeSessionCandidate(left, right) {
|
|
31
|
+
return left.createdAt < right.createdAt || (left.createdAt === right.createdAt && left.id < right.id);
|
|
32
|
+
}
|
|
10
33
|
function runSettlementTimeoutMs(options) {
|
|
11
34
|
if (options.waitTimeoutMs !== undefined)
|
|
12
35
|
return options.waitTimeoutMs;
|
|
@@ -169,6 +192,24 @@ export class ControllerClient {
|
|
|
169
192
|
}
|
|
170
193
|
return result;
|
|
171
194
|
}
|
|
195
|
+
async findLoadedRuntimeSession(input) {
|
|
196
|
+
const machineId = requireNonEmpty(input.machineId, "machineId");
|
|
197
|
+
const agent = requireNonEmpty(input.agent, "agent");
|
|
198
|
+
const runtimeSessionId = requireNonEmpty(input.runtimeSessionId, "runtimeSessionId");
|
|
199
|
+
let selected;
|
|
200
|
+
for (const session of (await this.snapshot()).sessions) {
|
|
201
|
+
if (session.status === "closed")
|
|
202
|
+
continue;
|
|
203
|
+
if (session.machineId !== machineId || session.agent !== agent)
|
|
204
|
+
continue;
|
|
205
|
+
const metadata = await this.decodeSessionMetadata(session);
|
|
206
|
+
if (!runtimeSessionIdsForMetadata(metadata).has(runtimeSessionId))
|
|
207
|
+
continue;
|
|
208
|
+
if (!selected || isCanonicalRuntimeSessionCandidate(session, selected))
|
|
209
|
+
selected = session;
|
|
210
|
+
}
|
|
211
|
+
return selected;
|
|
212
|
+
}
|
|
172
213
|
async getSession(sessionId) {
|
|
173
214
|
const checkedSessionId = requireNonEmpty(sessionId, "sessionId");
|
|
174
215
|
return (await this.snapshot()).sessions.find((session) => session.id === checkedSessionId);
|
|
@@ -302,10 +343,15 @@ export class ControllerClient {
|
|
|
302
343
|
await sleep(intervalMs);
|
|
303
344
|
}
|
|
304
345
|
}
|
|
305
|
-
async resume(sessionId) {
|
|
346
|
+
async resume(sessionId, options = {}) {
|
|
306
347
|
const checkedSessionId = requireNonEmpty(sessionId, "sessionId");
|
|
307
348
|
const requestId = randomId("req");
|
|
308
|
-
return await this.sendCommand({
|
|
349
|
+
return await this.sendCommand({
|
|
350
|
+
type: "controller:resume",
|
|
351
|
+
requestId,
|
|
352
|
+
sessionId: checkedSessionId,
|
|
353
|
+
...(options.syncDirection ? { syncDirection: options.syncDirection } : {}),
|
|
354
|
+
});
|
|
309
355
|
}
|
|
310
356
|
async listHistoricalSessionsPage(input) {
|
|
311
357
|
const machineId = requireNonEmpty(input.machineId, "machineId");
|
|
@@ -488,6 +534,9 @@ export class ControllerClient {
|
|
|
488
534
|
const agent = requireNonEmpty(input.agent, "agent");
|
|
489
535
|
const runtimeSessionId = requireNonEmpty(input.runtimeSessionId, "runtimeSessionId");
|
|
490
536
|
const name = requireNonEmpty(input.name, "name");
|
|
537
|
+
const existing = await this.findLoadedRuntimeSession({ machineId, agent, runtimeSessionId });
|
|
538
|
+
if (existing)
|
|
539
|
+
return { sessionId: existing.id, mode: "existing", events: [] };
|
|
491
540
|
const requestId = randomId("req");
|
|
492
541
|
const result = await this.sendCommand({
|
|
493
542
|
type: "controller:importHistoricalSession",
|
|
@@ -50,6 +50,7 @@ export type ControllerClientMessage = {
|
|
|
50
50
|
type: "controller:resume";
|
|
51
51
|
requestId: string;
|
|
52
52
|
sessionId: string;
|
|
53
|
+
syncDirection?: "latest" | "older";
|
|
53
54
|
} | {
|
|
54
55
|
type: "controller:listHistoricalSessions";
|
|
55
56
|
requestId: string;
|
|
@@ -384,6 +385,7 @@ export type MachineCommand = {
|
|
|
384
385
|
type: "machine:resume";
|
|
385
386
|
requestId: string;
|
|
386
387
|
sessionId: string;
|
|
388
|
+
syncDirection?: "latest" | "older";
|
|
387
389
|
encryptedMetadata?: EncryptedEnvelope;
|
|
388
390
|
} | {
|
|
389
391
|
type: "machine:listHistoricalSessions";
|
|
@@ -57,6 +57,7 @@ const controllerMessageSchema = z.discriminatedUnion("type", [
|
|
|
57
57
|
type: z.literal("controller:resume"),
|
|
58
58
|
requestId: z.string().min(1),
|
|
59
59
|
sessionId: z.string().min(1),
|
|
60
|
+
syncDirection: z.enum(["latest", "older"]).optional(),
|
|
60
61
|
}),
|
|
61
62
|
z.object({
|
|
62
63
|
type: z.literal("controller:listHistoricalSessions"),
|
|
@@ -458,6 +459,7 @@ const machineCommandSchema = z.discriminatedUnion("type", [
|
|
|
458
459
|
type: z.literal("machine:resume"),
|
|
459
460
|
requestId: z.string().min(1),
|
|
460
461
|
sessionId: z.string().min(1),
|
|
462
|
+
syncDirection: z.enum(["latest", "older"]).optional(),
|
|
461
463
|
encryptedMetadata: encryptedEnvelopeSchema.optional(),
|
|
462
464
|
}),
|
|
463
465
|
z.object({
|