@cydm/happy-elves 0.1.0-beta.60 → 0.1.0-beta.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/cli/dist/commands/lib/exit.js +2 -0
- package/apps/cli/dist/commands/lib/types.d.ts +1 -0
- package/apps/cli/dist/commands/orchestrator.js +87 -3
- package/apps/daemon/dist/gateway/fake.js +2 -0
- package/apps/daemon/dist/gateway/runtime.js +18 -8
- package/apps/daemon/dist/session/events.d.ts +3 -1
- package/apps/daemon/dist/session/events.js +6 -1
- package/apps/daemon/dist/session/prompt.js +13 -1
- package/apps/daemon/package.json +1 -1
- package/apps/relay/dist/http-routes.js +22 -11
- package/apps/relay/dist/machine-handler-context.d.ts +1 -0
- package/apps/relay/dist/relay-context.d.ts +1 -0
- package/apps/relay/dist/relay-context.js +1 -1
- package/apps/relay/dist/session-projection-reducer.js +14 -8
- package/npm-shrinkwrap.json +12 -24
- package/package.json +1 -1
- package/packages/shared/dist/protocol.d.ts +4 -0
|
@@ -127,6 +127,8 @@ export function handleCliError(error) {
|
|
|
127
127
|
latestCompletedTurnId: enriched.latestCompletedTurnId,
|
|
128
128
|
commandAcked: enriched.commandAcked,
|
|
129
129
|
rawStatus: enriched.rawStatus,
|
|
130
|
+
sessionStatus: enriched.sessionStatus,
|
|
131
|
+
staleRunningTurnId: enriched.staleRunningTurnId,
|
|
130
132
|
lastTransientError: enriched.lastTransientError,
|
|
131
133
|
retryCount: enriched.retryCount,
|
|
132
134
|
}, enriched.capability, enriched.retryable);
|
|
@@ -62,6 +62,7 @@ async function threadSummary(client, session, machines, options = {}) {
|
|
|
62
62
|
const events = options.events ?? [];
|
|
63
63
|
const turns = events.length > 0 ? summarizeOrchestratorTurns(events, metadata) : [];
|
|
64
64
|
const latestTurn = turns[0];
|
|
65
|
+
const runningClassification = classifyRunningTurn(session, turns, options.focusTurn);
|
|
65
66
|
const orchestration = options.historyFailure
|
|
66
67
|
? {
|
|
67
68
|
state: "blocked",
|
|
@@ -71,8 +72,10 @@ async function threadSummary(client, session, machines, options = {}) {
|
|
|
71
72
|
completed: false,
|
|
72
73
|
reason: `history unavailable: ${options.historyFailure.code}`,
|
|
73
74
|
}
|
|
74
|
-
:
|
|
75
|
-
|
|
75
|
+
: runningClassification.state === "stale"
|
|
76
|
+
? staleRunningOrchestration(session, runningClassification.turn.turnId)
|
|
77
|
+
: deriveOrchestration(session, events, options.focusTurn);
|
|
78
|
+
const status = normalizeOrchestratorStatus(session, orchestration, runningClassification.state === "stale" ? undefined : latestTurn);
|
|
76
79
|
const machine = machineForSession(machines, session);
|
|
77
80
|
return {
|
|
78
81
|
threadId: session.id,
|
|
@@ -90,6 +93,7 @@ async function threadSummary(client, session, machines, options = {}) {
|
|
|
90
93
|
online: machine?.online === true,
|
|
91
94
|
},
|
|
92
95
|
orchestration,
|
|
96
|
+
...(runningClassification.state === "stale" ? { staleRunningTurnId: runningClassification.turn.turnId } : {}),
|
|
93
97
|
...(options.historyFailure ? {
|
|
94
98
|
historyStatus: "unavailable",
|
|
95
99
|
historyError: options.historyFailure,
|
|
@@ -200,6 +204,43 @@ function threadHistoryFailure(error) {
|
|
|
200
204
|
retryable: retryableError(error),
|
|
201
205
|
};
|
|
202
206
|
}
|
|
207
|
+
function isActiveRunningSessionStatus(status) {
|
|
208
|
+
return status === "running" || status === "new" || status === "failed";
|
|
209
|
+
}
|
|
210
|
+
function stateFromNonRunningSessionStatus(status) {
|
|
211
|
+
if (status === "closed")
|
|
212
|
+
return "closed";
|
|
213
|
+
if (status === "offline" || status === "failed")
|
|
214
|
+
return "blocked";
|
|
215
|
+
if (status === "completed")
|
|
216
|
+
return "completed";
|
|
217
|
+
return "idle";
|
|
218
|
+
}
|
|
219
|
+
function classifyRunningTurn(session, turns, focusTurn) {
|
|
220
|
+
const latestTurn = turns[0];
|
|
221
|
+
const runningTurn = latestTurn?.status === "running"
|
|
222
|
+
? latestTurn
|
|
223
|
+
: focusTurn?.status === "running"
|
|
224
|
+
? focusTurn
|
|
225
|
+
: undefined;
|
|
226
|
+
if (!runningTurn)
|
|
227
|
+
return { state: "none" };
|
|
228
|
+
return isActiveRunningSessionStatus(session.status)
|
|
229
|
+
? { state: "active", turn: runningTurn }
|
|
230
|
+
: { state: "stale", turn: runningTurn };
|
|
231
|
+
}
|
|
232
|
+
function staleRunningOrchestration(session, turnId) {
|
|
233
|
+
const state = stateFromNonRunningSessionStatus(session.status);
|
|
234
|
+
return {
|
|
235
|
+
state,
|
|
236
|
+
rawStatus: session.status,
|
|
237
|
+
needsUser: false,
|
|
238
|
+
blocked: state === "blocked",
|
|
239
|
+
completed: state === "completed",
|
|
240
|
+
reason: `stale running turn from a previous daemon execution: ${turnId}`,
|
|
241
|
+
staleRunningTurnId: turnId,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
203
244
|
function summarizeOrchestratorTurns(events, metadata) {
|
|
204
245
|
const turns = summarizeTurns(events);
|
|
205
246
|
if (!metadata)
|
|
@@ -456,6 +497,16 @@ function threadBusy(threadId, active, latestCompleted) {
|
|
|
456
497
|
rawStatus: "running",
|
|
457
498
|
});
|
|
458
499
|
}
|
|
500
|
+
function turnOrphaned(threadId, turnId, turnStatus, sessionStatus) {
|
|
501
|
+
return new OrchestratorError(`Turn ${turnId} is orphaned: session is ${sessionStatus}, but the turn has no terminal event.`, "TURN_ORPHANED", {
|
|
502
|
+
retryable: false,
|
|
503
|
+
sessionId: threadId,
|
|
504
|
+
turnId,
|
|
505
|
+
rawStatus: turnStatus,
|
|
506
|
+
sessionStatus,
|
|
507
|
+
staleRunningTurnId: turnId,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
459
510
|
async function waitForTurn(client, threadId, turnId, timeoutMs, metadata) {
|
|
460
511
|
const startedAt = Date.now();
|
|
461
512
|
let retryCount = 0;
|
|
@@ -474,6 +525,12 @@ async function waitForTurn(client, threadId, turnId, timeoutMs, metadata) {
|
|
|
474
525
|
}
|
|
475
526
|
if (turn && turn.status !== "running")
|
|
476
527
|
return turn;
|
|
528
|
+
if (turn?.status === "running") {
|
|
529
|
+
const session = await client.getSession(threadId).catch(() => undefined);
|
|
530
|
+
if (session && classifyRunningTurn(session, [], turn).state === "stale" && session.status !== "offline") {
|
|
531
|
+
throw turnOrphaned(threadId, turnId, turn.status, session.status);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
477
534
|
if (Date.now() - startedAt >= timeoutMs) {
|
|
478
535
|
throw new OrchestratorError(lastTransientError
|
|
479
536
|
? `Turn ${turnId} did not become readable before timeout; last transient error: ${lastTransientError.message}`
|
|
@@ -541,6 +598,9 @@ async function sendOrchestratorMessage(client, flags, input) {
|
|
|
541
598
|
if (!sameBaseRequestShape(existingRequest, { turnId, inputSha256, ...selection, permissionMode: activePermissionMode }))
|
|
542
599
|
throw requestConflict(input.threadId, requestId, turnId);
|
|
543
600
|
const existingTurn = await readExactTurn(client, input.threadId, turnId, metadata);
|
|
601
|
+
if (existingTurn?.status === "running" && classifyRunningTurn(session, [], existingTurn).state === "stale" && session.status !== "offline") {
|
|
602
|
+
throw turnOrphaned(input.threadId, turnId, existingTurn.status, session.status);
|
|
603
|
+
}
|
|
544
604
|
let context;
|
|
545
605
|
let resolvedEffectivePrompt = !input.contextTurn;
|
|
546
606
|
if (input.contextTurn) {
|
|
@@ -552,6 +612,26 @@ async function sendOrchestratorMessage(client, flags, input) {
|
|
|
552
612
|
resolvedEffectivePrompt = false;
|
|
553
613
|
}
|
|
554
614
|
}
|
|
615
|
+
if (!resolvedEffectivePrompt && existingTurn && existingTurn.status !== "running") {
|
|
616
|
+
const output = await rebuildOutputForTurn(client, input.threadId, existingTurn);
|
|
617
|
+
ok(input.responseType, {
|
|
618
|
+
threadId: input.threadId,
|
|
619
|
+
sessionId: input.threadId,
|
|
620
|
+
turnId,
|
|
621
|
+
requestId,
|
|
622
|
+
status: terminalStatusForTurn(existingTurn),
|
|
623
|
+
rawStatus: existingTurn.status,
|
|
624
|
+
finalOutput: output.output || undefined,
|
|
625
|
+
artifactPaths: {
|
|
626
|
+
output: output.outputPath,
|
|
627
|
+
latest: output.latestPath,
|
|
628
|
+
metadata: output.metadataPath,
|
|
629
|
+
},
|
|
630
|
+
...permissionEvidence(requestedPermissionMode, existingRequest),
|
|
631
|
+
...earlyContextData,
|
|
632
|
+
}, { requestId, sessionId: input.threadId, turnId });
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
555
635
|
if (!resolvedEffectivePrompt) {
|
|
556
636
|
ok(input.responseType, {
|
|
557
637
|
threadId: input.threadId,
|
|
@@ -648,7 +728,8 @@ async function sendOrchestratorMessage(client, flags, input) {
|
|
|
648
728
|
const existingTurn = await readExactTurn(client, input.threadId, turnId, metadata);
|
|
649
729
|
if (existingTurn?.prompt && promptSha256(existingTurn.prompt) !== hash)
|
|
650
730
|
throw requestConflict(input.threadId, requestId, turnId);
|
|
651
|
-
const
|
|
731
|
+
const runningClassification = classifyRunningTurn(session, turns);
|
|
732
|
+
const active = runningClassification.state === "active" && turns[0]?.status === "running" ? turns[0] : undefined;
|
|
652
733
|
const latestCompleted = turns.find((turn) => turn.status === "completed");
|
|
653
734
|
if (active && active.turnId !== turnId)
|
|
654
735
|
throw threadBusy(input.threadId, active, latestCompleted);
|
|
@@ -682,6 +763,9 @@ async function sendOrchestratorMessage(client, flags, input) {
|
|
|
682
763
|
return;
|
|
683
764
|
}
|
|
684
765
|
if (existingTurn?.status === "running") {
|
|
766
|
+
if (classifyRunningTurn(session, [], existingTurn).state === "stale" && session.status !== "offline") {
|
|
767
|
+
throw turnOrphaned(input.threadId, turnId, existingTurn.status, session.status);
|
|
768
|
+
}
|
|
685
769
|
const runningTurnRecord = requestRecordForExistingTurn(existingTurn, hash, selection, undefined, inputSha256, context?.sourceOutputSha256, Date.now());
|
|
686
770
|
ok(input.responseType, {
|
|
687
771
|
threadId: input.threadId,
|
|
@@ -4,6 +4,8 @@ export function createFakeGatewayAdapter(_context) {
|
|
|
4
4
|
return {
|
|
5
5
|
async start() {
|
|
6
6
|
// Fake adapter is intentionally passive; tests can call the daemon gateway sender directly.
|
|
7
|
+
if (process.env.HAPPY_ELVES_GATEWAY_FAKE_START_FAIL === "1")
|
|
8
|
+
throw new Error("fake gateway start failed");
|
|
7
9
|
if (process.env.HAPPY_ELVES_GATEWAY_FAKE_LIFECYCLE_LOG === "1")
|
|
8
10
|
console.log("[gateway:fake:lifecycle] start");
|
|
9
11
|
},
|
|
@@ -376,8 +376,8 @@ async function runGatewayPrompt(ws, config, gateway, binding, input, session, ac
|
|
|
376
376
|
turnId,
|
|
377
377
|
}, ws);
|
|
378
378
|
if (gatewayProjectionSupported()) {
|
|
379
|
-
await
|
|
380
|
-
await
|
|
379
|
+
await emitGatewayEventBestEffort(ws, config, binding.sessionId, turnId, requestId, "prompt", { type: "user_prompt", text: input.text });
|
|
380
|
+
await emitGatewayEventBestEffort(ws, config, binding.sessionId, turnId, requestId, "submitted", {
|
|
381
381
|
type: "status",
|
|
382
382
|
text: "Gateway prompt submitted to runtime",
|
|
383
383
|
tag: "gateway",
|
|
@@ -409,7 +409,7 @@ async function runGatewayPrompt(ws, config, gateway, binding, input, session, ac
|
|
|
409
409
|
promptChars: input.text.length,
|
|
410
410
|
},
|
|
411
411
|
});
|
|
412
|
-
pumpRuntimeEvents(turn, ws, config, binding.sessionId, turnId, outputPreview);
|
|
412
|
+
pumpRuntimeEvents(turn, ws, config, binding.sessionId, turnId, requestId, outputPreview);
|
|
413
413
|
result = await turn.result.catch((error) => runtimeTurnFailureResult(error));
|
|
414
414
|
}
|
|
415
415
|
catch (error) {
|
|
@@ -437,7 +437,7 @@ async function runGatewayPrompt(ws, config, gateway, binding, input, session, ac
|
|
|
437
437
|
evidence: { gatewayId: gateway.id, platformEventId: input.platformEventId, error: errorMessage(error) },
|
|
438
438
|
});
|
|
439
439
|
}
|
|
440
|
-
await emitDoneEvent(ws, config, binding.sessionId, turnId, result);
|
|
440
|
+
await emitDoneEvent(ws, config, binding.sessionId, turnId, requestId, result);
|
|
441
441
|
release();
|
|
442
442
|
if (gatewayProjectionSupported()) {
|
|
443
443
|
sendGatewayProjection({
|
|
@@ -485,16 +485,18 @@ async function runGatewayPrompt(ws, config, gateway, binding, input, session, ac
|
|
|
485
485
|
});
|
|
486
486
|
return true;
|
|
487
487
|
}
|
|
488
|
-
function pumpRuntimeEvents(turn, ws, config, sessionId, turnId, outputPreview) {
|
|
488
|
+
function pumpRuntimeEvents(turn, ws, config, sessionId, turnId, requestId, outputPreview) {
|
|
489
489
|
void (async () => {
|
|
490
|
+
let runtimeEventIndex = 0;
|
|
490
491
|
try {
|
|
491
492
|
for await (const event of turn.events) {
|
|
493
|
+
runtimeEventIndex += 1;
|
|
492
494
|
const payload = runtimeEventToPayload(event);
|
|
493
495
|
if (payload.type === "text_delta" && payload.stream !== "thought") {
|
|
494
496
|
outputPreview.text = `${outputPreview.text}${payload.text}`.slice(-4000);
|
|
495
497
|
}
|
|
496
498
|
if (gatewayProjectionSupported()) {
|
|
497
|
-
await
|
|
499
|
+
await emitGatewayEventBestEffort(ws, config, sessionId, turnId, requestId, `runtime:${runtimeEventIndex}`, payload);
|
|
498
500
|
}
|
|
499
501
|
}
|
|
500
502
|
}
|
|
@@ -510,11 +512,11 @@ function pumpRuntimeEvents(turn, ws, config, sessionId, turnId, outputPreview) {
|
|
|
510
512
|
}
|
|
511
513
|
})();
|
|
512
514
|
}
|
|
513
|
-
async function emitDoneEvent(ws, config, sessionId, turnId, result) {
|
|
515
|
+
async function emitDoneEvent(ws, config, sessionId, turnId, requestId, result) {
|
|
514
516
|
if (!gatewayProjectionSupported())
|
|
515
517
|
return;
|
|
516
518
|
try {
|
|
517
|
-
await
|
|
519
|
+
await emitGatewayEventBestEffort(ws, config, sessionId, turnId, requestId, "done", result.status === "failed"
|
|
518
520
|
? {
|
|
519
521
|
type: "done",
|
|
520
522
|
status: "failed",
|
|
@@ -541,7 +543,13 @@ async function emitDoneEvent(ws, config, sessionId, turnId, result) {
|
|
|
541
543
|
});
|
|
542
544
|
}
|
|
543
545
|
}
|
|
546
|
+
async function emitGatewayEventBestEffort(ws, config, sessionId, turnId, requestId, eventKey, payload) {
|
|
547
|
+
await emitEventBestEffort(ws, config, sessionId, turnId, payload, {
|
|
548
|
+
messageIdParts: ["gateway", requestId, sessionId, turnId, eventKey],
|
|
549
|
+
});
|
|
550
|
+
}
|
|
544
551
|
async function deliverGatewayStopNotifications(config, sessionId, turnId, payload) {
|
|
552
|
+
await refreshGatewayStoreSnapshot();
|
|
545
553
|
const state = await readGatewayState();
|
|
546
554
|
const tracked = gatewayTurnDestinations.get(gatewayTurnKey(sessionId, turnId));
|
|
547
555
|
for (const gateway of enabledGateways.values()) {
|
|
@@ -667,6 +675,8 @@ async function selectNextQueuedInbound() {
|
|
|
667
675
|
return removeQueuedInboundFromState(state, next, { kind: "dropped", input: next, reason: "missing_gateway" });
|
|
668
676
|
if (!gateway)
|
|
669
677
|
continue;
|
|
678
|
+
if (!gatewayAdapters.has(next.gatewayId))
|
|
679
|
+
continue;
|
|
670
680
|
const binding = state.bindings.find((item) => item.gatewayId === next.gatewayId && conversationKey(item) === conversationKey(next.destination));
|
|
671
681
|
if (!binding)
|
|
672
682
|
return removeQueuedInboundFromState(state, next, { kind: "dropped", input: next, reason: "missing_binding" });
|
|
@@ -2,7 +2,9 @@ import { type EncryptedEnvelope, type SessionEventPayload, type SessionHeadAdvan
|
|
|
2
2
|
import type { RuntimeEvent, RuntimeHistoricalEvent } from "../../../../packages/runtime/dist/index.js";
|
|
3
3
|
import type { DaemonConfig } from "../types.js";
|
|
4
4
|
export declare function emitEvent(ws: WebSocket, config: DaemonConfig, sessionId: string, turnId: string, payload: SessionEventPayload): Promise<void>;
|
|
5
|
-
export declare function emitEventBestEffort(ws: WebSocket, config: DaemonConfig, sessionId: string, turnId: string, payload: SessionEventPayload
|
|
5
|
+
export declare function emitEventBestEffort(ws: WebSocket, config: DaemonConfig, sessionId: string, turnId: string, payload: SessionEventPayload, options?: {
|
|
6
|
+
messageIdParts?: readonly unknown[];
|
|
7
|
+
}): Promise<void>;
|
|
6
8
|
export declare function emitHistoricalEvents(ws: WebSocket, config: DaemonConfig, sessionId: string, events: RuntimeHistoricalEvent[], startIndex?: number): Promise<void>;
|
|
7
9
|
export declare function emitRuntimeTailEvents(ws: WebSocket, config: DaemonConfig, sessionId: string, events: RuntimeHistoricalEvent[], startIndex?: number): Promise<void>;
|
|
8
10
|
export declare function emitSessionHeadAdvanced(ws: WebSocket, sessionId: string, head: HistoricalSessionHead, encryptedMetadata?: EncryptedEnvelope, status?: "completed" | "cancelled" | "failed"): Promise<void>;
|
|
@@ -12,13 +12,14 @@ export async function emitEvent(ws, config, sessionId, turnId, payload) {
|
|
|
12
12
|
payload: await encryptJson(config.accountSecret, `event:${sessionId}:${turnId}:${eventSeq}`, payload),
|
|
13
13
|
}, ws);
|
|
14
14
|
}
|
|
15
|
-
export async function emitEventBestEffort(ws, config, sessionId, turnId, payload) {
|
|
15
|
+
export async function emitEventBestEffort(ws, config, sessionId, turnId, payload, options = {}) {
|
|
16
16
|
eventSeq++;
|
|
17
17
|
const message = {
|
|
18
18
|
type: "machine:event",
|
|
19
19
|
sessionId,
|
|
20
20
|
turnId,
|
|
21
21
|
seq: eventSeq,
|
|
22
|
+
...(options.messageIdParts ? { messageId: bestEffortEventMessageId(options.messageIdParts) } : {}),
|
|
22
23
|
payload: await encryptJson(config.accountSecret, `event:${sessionId}:${turnId}:${eventSeq}`, payload),
|
|
23
24
|
};
|
|
24
25
|
try {
|
|
@@ -28,6 +29,10 @@ export async function emitEventBestEffort(ws, config, sessionId, turnId, payload
|
|
|
28
29
|
// Gateway relay projection is best-effort; local runtime progress must not depend on it.
|
|
29
30
|
}
|
|
30
31
|
}
|
|
32
|
+
function bestEffortEventMessageId(parts) {
|
|
33
|
+
const digest = createHash("sha256").update(JSON.stringify(parts)).digest("base64url").slice(0, 32);
|
|
34
|
+
return `msg_gateway_event_${digest}`;
|
|
35
|
+
}
|
|
31
36
|
export async function emitHistoricalEvents(ws, config, sessionId, events, startIndex = 0) {
|
|
32
37
|
const encryptedEvents = await encryptedHistoricalEvents(config, sessionId, events, startIndex);
|
|
33
38
|
for (const event of encryptedEvents) {
|
|
@@ -293,13 +293,25 @@ export async function handleCancel(ws, config, command) {
|
|
|
293
293
|
const activeTurnId = sessionTurns.activeTurnId(command.sessionId);
|
|
294
294
|
const turn = sessionTurns.activeTurn(command.sessionId);
|
|
295
295
|
if (!turn) {
|
|
296
|
+
if (command.turnId) {
|
|
297
|
+
const stopReason = command.reason
|
|
298
|
+
? `${command.reason} (no active runtime turn after daemon restart)`
|
|
299
|
+
: "cancel requested after daemon restart; runtime turn is no longer active";
|
|
300
|
+
await emitEvent(ws, config, command.sessionId, command.turnId, {
|
|
301
|
+
type: "done",
|
|
302
|
+
status: "cancelled",
|
|
303
|
+
stopReason,
|
|
304
|
+
currentHead: command.turnId,
|
|
305
|
+
lastTurnId: command.turnId,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
296
308
|
await appendAudit({
|
|
297
309
|
sessionId: command.sessionId,
|
|
298
310
|
machineId: config.machineId,
|
|
299
311
|
actor: "user",
|
|
300
312
|
action: "turn.cancel.noop",
|
|
301
313
|
summary: `No active turn to cancel for session ${command.sessionId}`,
|
|
302
|
-
evidence: { requestId: command.requestId, turnId: command.turnId, reason: command.reason },
|
|
314
|
+
evidence: { requestId: command.requestId, turnId: command.turnId, reason: command.reason, emittedTerminal: Boolean(command.turnId) },
|
|
303
315
|
});
|
|
304
316
|
return;
|
|
305
317
|
}
|
package/apps/daemon/package.json
CHANGED
|
@@ -371,20 +371,31 @@ export function registerHttpRoutes(app, context, controllerInviteTtlMs) {
|
|
|
371
371
|
if (!session)
|
|
372
372
|
throw new HttpError(404, "SESSION_NOT_FOUND", "Session not found");
|
|
373
373
|
const ts = context.now();
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
374
|
+
const hasExpectedEncryptedMetadata = body.expectedEncryptedMetadata !== undefined;
|
|
375
|
+
const expectedEncryptedMetadata = hasExpectedEncryptedMetadata
|
|
376
|
+
? body.expectedEncryptedMetadata === null
|
|
377
377
|
? null
|
|
378
|
-
: JSON.stringify(body.expectedEncryptedMetadata)
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
378
|
+
: JSON.stringify(body.expectedEncryptedMetadata)
|
|
379
|
+
: session.encrypted_metadata;
|
|
380
|
+
const encryptedMetadata = JSON.stringify(body.encryptedMetadata);
|
|
381
|
+
const result = (() => {
|
|
382
|
+
if (body.expectedUpdatedAt !== undefined) {
|
|
383
|
+
return context.db.prepare(`UPDATE sessions
|
|
384
|
+
SET encrypted_metadata = ?,
|
|
385
|
+
updated_at = ?
|
|
386
|
+
WHERE account_id = ? AND id = ? AND updated_at = ? AND encrypted_metadata IS ?`).run(encryptedMetadata, ts, token.account_id, params.sessionId, body.expectedUpdatedAt, expectedEncryptedMetadata);
|
|
387
|
+
}
|
|
388
|
+
if (hasExpectedEncryptedMetadata) {
|
|
389
|
+
return context.db.prepare(`UPDATE sessions
|
|
390
|
+
SET encrypted_metadata = ?,
|
|
391
|
+
updated_at = ?
|
|
392
|
+
WHERE account_id = ? AND id = ? AND encrypted_metadata IS ?`).run(encryptedMetadata, ts, token.account_id, params.sessionId, expectedEncryptedMetadata);
|
|
393
|
+
}
|
|
394
|
+
return context.db.prepare(`UPDATE sessions
|
|
385
395
|
SET encrypted_metadata = ?,
|
|
386
396
|
updated_at = ?
|
|
387
|
-
WHERE account_id = ? AND id =
|
|
397
|
+
WHERE account_id = ? AND id = ?`).run(encryptedMetadata, ts, token.account_id, params.sessionId);
|
|
398
|
+
})();
|
|
388
399
|
if (result.changes === 0) {
|
|
389
400
|
throw new HttpError(409, "SESSION_METADATA_CONFLICT", "Session metadata changed; retry with the latest session");
|
|
390
401
|
}
|
|
@@ -68,6 +68,7 @@ export declare function createRelayContext(options: CreateRelayContextOptions):
|
|
|
68
68
|
turnSeq?: number;
|
|
69
69
|
occurredAt?: number;
|
|
70
70
|
payload: EncryptedEnvelope;
|
|
71
|
+
advanceSessionHead?: boolean;
|
|
71
72
|
}) => EncryptedSessionEvent;
|
|
72
73
|
aliasedRuntimeEventHasRequestEvents: (params: {
|
|
73
74
|
accountId: string;
|
|
@@ -587,7 +587,7 @@ export function createRelayContext(options) {
|
|
|
587
587
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
588
588
|
.run(params.accountId, params.sessionId, params.machineId, params.messageId ?? null, params.turnId, seqRow.nextSeq, params.turnSeq ?? null, JSON.stringify(params.payload), createdAt, params.occurredAt ?? null);
|
|
589
589
|
const eventId = Number(result.lastInsertRowid);
|
|
590
|
-
if (shouldAdvanceSessionHead(params, eventId, createdAt)) {
|
|
590
|
+
if (params.advanceSessionHead !== false && shouldAdvanceSessionHead(params, eventId, createdAt)) {
|
|
591
591
|
const order = logicalEventOrder({
|
|
592
592
|
id: eventId,
|
|
593
593
|
createdAt,
|
|
@@ -340,15 +340,12 @@ function handleMachineEvent(context, connection, message, acknowledgeMachineMess
|
|
|
340
340
|
acknowledgeMachineMessage();
|
|
341
341
|
return;
|
|
342
342
|
}
|
|
343
|
-
|
|
343
|
+
const suppressAliasedRuntimeProjection = context.aliasedRuntimeEventHasRequestEvents({
|
|
344
344
|
accountId: connection.accountId,
|
|
345
345
|
sessionId: message.sessionId,
|
|
346
346
|
machineId: connection.machineId,
|
|
347
347
|
runtimeTurnId: message.turnId,
|
|
348
|
-
})
|
|
349
|
-
acknowledgeMachineMessage();
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
348
|
+
});
|
|
352
349
|
const event = context.insertEvent({
|
|
353
350
|
accountId: connection.accountId,
|
|
354
351
|
sessionId: message.sessionId,
|
|
@@ -359,9 +356,11 @@ function handleMachineEvent(context, connection, message, acknowledgeMachineMess
|
|
|
359
356
|
turnSeq: message.seq,
|
|
360
357
|
occurredAt: message.occurredAt,
|
|
361
358
|
payload: message.payload,
|
|
359
|
+
advanceSessionHead: !suppressAliasedRuntimeProjection,
|
|
362
360
|
});
|
|
363
361
|
const pendingSessionRow = applyPendingSessionHeadAdvances(context.db, connection.accountId, message.sessionId, connection.machineId, context.now());
|
|
364
|
-
|
|
362
|
+
if (!suppressAliasedRuntimeProjection)
|
|
363
|
+
context.broadcastControllers(connection.accountId, { type: "server:event", event });
|
|
365
364
|
if (pendingSessionRow)
|
|
366
365
|
context.broadcastControllers(connection.accountId, { type: "server:session", session: context.sessionSnapshot(pendingSessionRow) });
|
|
367
366
|
acknowledgeMachineMessage();
|
|
@@ -411,14 +410,21 @@ function handleTurnDone(context, connection, message, acknowledgeMachineMessage)
|
|
|
411
410
|
const basisComparison = basisOrder ? compareHeadBasisOrder(basisOrder, currentOrder) : null;
|
|
412
411
|
const isRuntimeAliasForCurrentRequest = Boolean(runtimeBasisEvent && runtimeTurnId !== message.turnId && (sessionRow.last_turn_id === message.turnId || requestBasisEvent));
|
|
413
412
|
const isInitialTerminal = !sessionRow.last_turn_id && !currentOrder;
|
|
413
|
+
const requestTurnCompletesActiveRun = sessionRow.status === "running" &&
|
|
414
|
+
requestBasisEvent !== undefined &&
|
|
415
|
+
requestOrder !== undefined &&
|
|
416
|
+
compareHeadBasisOrder(requestOrder, currentOrder) >= 0;
|
|
414
417
|
const isTerminalForCurrentTurn = isRuntimeAliasForCurrentRequest ||
|
|
415
418
|
sessionRow.last_turn_id === message.turnId ||
|
|
416
419
|
isInitialTerminal ||
|
|
417
|
-
(sessionRow.status === "running" && !sessionRow.last_turn_id)
|
|
420
|
+
(sessionRow.status === "running" && !sessionRow.last_turn_id) ||
|
|
421
|
+
requestTurnCompletesActiveRun;
|
|
418
422
|
const aliasCompletesCurrentRequest = isRuntimeAliasForCurrentRequest &&
|
|
419
423
|
requestOrder !== undefined &&
|
|
420
424
|
compareHeadBasisOrder(requestOrder, currentOrder) >= 0;
|
|
421
|
-
const shouldApplyTurnDone = isTerminalForCurrentTurn && (aliasCompletesCurrentRequest ||
|
|
425
|
+
const shouldApplyTurnDone = isTerminalForCurrentTurn && (aliasCompletesCurrentRequest ||
|
|
426
|
+
requestTurnCompletesActiveRun ||
|
|
427
|
+
(basisComparison !== null ? basisComparison >= 0 : !currentOrder));
|
|
422
428
|
const shouldApplyHead = shouldApplyTurnDone;
|
|
423
429
|
const pendingRun = context.pendingRunCommand(connection.accountId, {
|
|
424
430
|
machineId: connection.machineId,
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cydm/happy-elves",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.62",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@cydm/happy-elves",
|
|
9
|
-
"version": "0.1.0-beta.
|
|
9
|
+
"version": "0.1.0-beta.62",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@fastify/cors": "11.2.0",
|
|
@@ -757,9 +757,9 @@
|
|
|
757
757
|
"license": "BSD-3-Clause"
|
|
758
758
|
},
|
|
759
759
|
"node_modules/@types/node": {
|
|
760
|
-
"version": "26.1.
|
|
761
|
-
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.
|
|
762
|
-
"integrity": "sha512-
|
|
760
|
+
"version": "26.1.1",
|
|
761
|
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz",
|
|
762
|
+
"integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==",
|
|
763
763
|
"license": "MIT",
|
|
764
764
|
"dependencies": {
|
|
765
765
|
"undici-types": "~8.3.0"
|
|
@@ -898,9 +898,9 @@
|
|
|
898
898
|
}
|
|
899
899
|
},
|
|
900
900
|
"node_modules/bare-fs": {
|
|
901
|
-
"version": "4.7.
|
|
902
|
-
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.
|
|
903
|
-
"integrity": "sha512-
|
|
901
|
+
"version": "4.7.4",
|
|
902
|
+
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.4.tgz",
|
|
903
|
+
"integrity": "sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ==",
|
|
904
904
|
"license": "Apache-2.0",
|
|
905
905
|
"dependencies": {
|
|
906
906
|
"bare-events": "^2.5.4",
|
|
@@ -921,23 +921,11 @@
|
|
|
921
921
|
}
|
|
922
922
|
}
|
|
923
923
|
},
|
|
924
|
-
"node_modules/bare-os": {
|
|
925
|
-
"version": "3.9.3",
|
|
926
|
-
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.3.tgz",
|
|
927
|
-
"integrity": "sha512-fF4Q7QsyKVF5Rj0qvI8BgUNjqzC2JvQlpTaPLjVJVxYVUX5Zr9un+y3w1HmA4nNKdFmRBT8z/WmrjvXzXVerKQ==",
|
|
928
|
-
"license": "Apache-2.0",
|
|
929
|
-
"engines": {
|
|
930
|
-
"bare": ">=1.14.0"
|
|
931
|
-
}
|
|
932
|
-
},
|
|
933
924
|
"node_modules/bare-path": {
|
|
934
|
-
"version": "3.
|
|
935
|
-
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.
|
|
936
|
-
"integrity": "sha512-
|
|
937
|
-
"license": "Apache-2.0"
|
|
938
|
-
"dependencies": {
|
|
939
|
-
"bare-os": "^3.0.1"
|
|
940
|
-
}
|
|
925
|
+
"version": "3.1.1",
|
|
926
|
+
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz",
|
|
927
|
+
"integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==",
|
|
928
|
+
"license": "Apache-2.0"
|
|
941
929
|
},
|
|
942
930
|
"node_modules/bare-stream": {
|
|
943
931
|
"version": "2.13.3",
|
package/package.json
CHANGED
|
@@ -467,6 +467,8 @@ export type JsonEnvelope<T = unknown> = {
|
|
|
467
467
|
activeTurnId?: string;
|
|
468
468
|
latestCompletedTurnId?: string;
|
|
469
469
|
commandAcked?: boolean;
|
|
470
|
+
sessionStatus?: string;
|
|
471
|
+
staleRunningTurnId?: string;
|
|
470
472
|
lastTransientError?: {
|
|
471
473
|
code: string;
|
|
472
474
|
message: string;
|
|
@@ -490,6 +492,8 @@ export type JsonEnvelope<T = unknown> = {
|
|
|
490
492
|
latestCompletedTurnId?: string;
|
|
491
493
|
commandAcked?: boolean;
|
|
492
494
|
rawStatus?: string;
|
|
495
|
+
sessionStatus?: string;
|
|
496
|
+
staleRunningTurnId?: string;
|
|
493
497
|
lastTransientError?: {
|
|
494
498
|
code: string;
|
|
495
499
|
message: string;
|