@cydm/happy-elves 0.1.0-beta.279 → 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 +20 -3
- package/apps/daemon/dist/session/lifecycle.js +39 -17
- package/apps/daemon/package.json +1 -1
- package/apps/relay/dist/connections.d.ts +1 -0
- package/apps/relay/dist/machine-handlers.js +6 -2
- package/apps/relay/dist/relay-context.js +47 -11
- package/package.json +1 -1
- package/packages/client/dist/client.d.ts +5 -0
- package/packages/client/dist/client.js +44 -0
- package/packages/client/dist/types.d.ts +1 -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,7 +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
|
-
type HistoricalBackfillDirection = "latest" | "older";
|
|
4
|
+
export type HistoricalBackfillDirection = "latest" | "older";
|
|
5
|
+
type HistoricalBackfillMode = HistoricalBackfillDirection | "auto";
|
|
5
6
|
export type HistoricalBackfillResult = {
|
|
6
7
|
availableEvents: number;
|
|
7
8
|
cursor: number;
|
|
@@ -17,7 +18,7 @@ export type HistoricalBackfillResult = {
|
|
|
17
18
|
export declare function handleCreateSession(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
|
|
18
19
|
export declare function handleImportHistoricalSession(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
|
|
19
20
|
export declare function handleListHistoricalSessions(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
|
|
20
|
-
export declare function resumeSession(ws: WebSocket, config: DaemonConfig, sessionId: string, requestId: string, fallback?: SessionFallback, announceSuccess?: boolean, syncDirection?:
|
|
21
|
+
export declare function resumeSession(ws: WebSocket, config: DaemonConfig, sessionId: string, requestId: string, fallback?: SessionFallback, announceSuccess?: boolean, syncDirection?: HistoricalBackfillMode): Promise<SessionState | null>;
|
|
21
22
|
export declare function usesAuthoritativeTranscript(session: SessionState): boolean;
|
|
22
23
|
export declare function replaceAuthoritativeTranscript(ws: WebSocket, config: DaemonConfig, session: SessionState, runtimeSessionId?: string | undefined, input?: {
|
|
23
24
|
currentHead?: string;
|
|
@@ -27,6 +28,22 @@ export declare function replaceAuthoritativeTranscript(ws: WebSocket, config: Da
|
|
|
27
28
|
status?: "completed" | "cancelled" | "failed";
|
|
28
29
|
strict?: boolean;
|
|
29
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
|
+
};
|
|
30
47
|
export declare function handleResume(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
|
|
31
48
|
export declare function handleClose(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
|
|
32
49
|
export {};
|
|
@@ -256,7 +256,7 @@ 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, syncDirection = "
|
|
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);
|
|
@@ -584,33 +584,37 @@ async function backfillHistoricalEvents(ws, config, session, runtimeSessionId =
|
|
|
584
584
|
throw error;
|
|
585
585
|
historicalEvents = [];
|
|
586
586
|
}
|
|
587
|
-
const
|
|
587
|
+
const direction = options.direction ?? "auto";
|
|
588
|
+
const plan = planAppendOnlyHistoricalBackfill(historicalEvents, previousBackfill, direction);
|
|
589
|
+
const tailWindow = plan.tailWindow;
|
|
588
590
|
if (tailWindow.events.length > 0) {
|
|
589
591
|
await emitRuntimeTailEvents(ws, config, session.sessionId, tailWindow.events, tailWindow.startIndex);
|
|
590
592
|
}
|
|
591
|
-
const
|
|
592
|
-
const olderWindow = historicalEventsWindowByTurn(historicalEvents, cursor, historicalBackfillEventLimit);
|
|
593
|
+
const olderWindow = plan.olderWindow;
|
|
593
594
|
if (olderWindow.events.length > 0) {
|
|
594
595
|
await emitHistoricalEvents(ws, config, session.sessionId, olderWindow.events, olderWindow.startIndex);
|
|
595
596
|
}
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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);
|
|
605
609
|
const result = {
|
|
606
610
|
emittedEvents: tailWindow.events.length + olderWindow.events.length,
|
|
607
611
|
availableEvents: historicalEvents.length,
|
|
608
|
-
cursor:
|
|
612
|
+
cursor: plan.cursor,
|
|
609
613
|
olderBackfilledEvents: olderWindow.events.length,
|
|
610
|
-
status,
|
|
614
|
+
status: plan.status,
|
|
611
615
|
tailRepairedEvents: tailWindow.events.length,
|
|
612
616
|
totalEvents: historicalEvents.length,
|
|
613
|
-
truncated:
|
|
617
|
+
truncated: plan.truncated,
|
|
614
618
|
...(head ? { head } : {}),
|
|
615
619
|
};
|
|
616
620
|
if (head) {
|
|
@@ -652,6 +656,23 @@ function normalizedBackfillCursor(previousBackfill, totalEvents) {
|
|
|
652
656
|
return 0;
|
|
653
657
|
return Math.max(0, Math.min(previousBackfill.cursor, totalEvents));
|
|
654
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
|
+
}
|
|
655
676
|
function historicalTailWindowByTurn(events, previousTotalEvents) {
|
|
656
677
|
if (previousTotalEvents === undefined || events.length <= previousTotalEvents) {
|
|
657
678
|
return { events: [], startIndex: events.length };
|
|
@@ -726,6 +747,7 @@ function historicalBackfillMetadata(backfill) {
|
|
|
726
747
|
historicalOlderBackfilledEvents: backfill.olderBackfilledEvents,
|
|
727
748
|
historicalTailRepairedEvents: backfill.tailRepairedEvents,
|
|
728
749
|
historicalHead: backfill.head,
|
|
750
|
+
runtimeCurrentEventCount: backfill.totalEvents,
|
|
729
751
|
availableHistoricalEvents: backfill.availableEvents,
|
|
730
752
|
truncatedHistoricalBackfill: backfill.truncated,
|
|
731
753
|
};
|
|
@@ -735,7 +757,7 @@ export async function handleResume(ws, config, command) {
|
|
|
735
757
|
return;
|
|
736
758
|
if (!claimCommandRequest(ws, command.requestId))
|
|
737
759
|
return;
|
|
738
|
-
await resumeSession(ws, config, command.sessionId, command.requestId, await sessionFallbackFromCommand(config, command), true, command.syncDirection ?? "
|
|
760
|
+
await resumeSession(ws, config, command.sessionId, command.requestId, await sessionFallbackFromCommand(config, command), true, command.syncDirection ?? "auto");
|
|
739
761
|
}
|
|
740
762
|
export async function handleClose(ws, config, command) {
|
|
741
763
|
if (command.type !== "machine:close")
|
package/apps/daemon/package.json
CHANGED
|
@@ -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();
|
|
@@ -687,6 +687,10 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
687
687
|
.get(message.sessionId, connection.accountId, connection.machineId);
|
|
688
688
|
if (!sessionRow)
|
|
689
689
|
return;
|
|
690
|
+
if (sessionRow.status === "closed") {
|
|
691
|
+
acknowledgeMachineMessage();
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
690
694
|
maybeAliasHistoricalRuntimeEventToActiveClaim({
|
|
691
695
|
db,
|
|
692
696
|
accountId: connection.accountId,
|
|
@@ -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;
|
|
@@ -522,10 +556,10 @@ export function createRelayContext(options) {
|
|
|
522
556
|
head_basis_logical_time = ?,
|
|
523
557
|
head_basis_turn_seq = ?,
|
|
524
558
|
updated_at = ?
|
|
525
|
-
WHERE id =
|
|
559
|
+
WHERE id = ? AND status != 'closed'`).run(`event:${eventId}`, params.turnId, eventId, params.messageId ?? null, order.time, order.seq, createdAt, params.sessionId);
|
|
526
560
|
}
|
|
527
561
|
else {
|
|
528
|
-
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);
|
|
529
563
|
}
|
|
530
564
|
return {
|
|
531
565
|
id: eventId,
|
|
@@ -548,6 +582,8 @@ export function createRelayContext(options) {
|
|
|
548
582
|
WHERE account_id = ? AND id = ? AND machine_id = ?`)
|
|
549
583
|
.get(params.accountId, params.sessionId, params.machineId);
|
|
550
584
|
const currentHead = session?.current_head;
|
|
585
|
+
if (session?.status === "closed")
|
|
586
|
+
return false;
|
|
551
587
|
if (!currentHead)
|
|
552
588
|
return true;
|
|
553
589
|
if (typeof session?.head_basis_message_id === "string" &&
|
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>;
|
|
@@ -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);
|
|
@@ -493,6 +534,9 @@ export class ControllerClient {
|
|
|
493
534
|
const agent = requireNonEmpty(input.agent, "agent");
|
|
494
535
|
const runtimeSessionId = requireNonEmpty(input.runtimeSessionId, "runtimeSessionId");
|
|
495
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: [] };
|
|
496
540
|
const requestId = randomId("req");
|
|
497
541
|
const result = await this.sendCommand({
|
|
498
542
|
type: "controller:importHistoricalSession",
|