@cydm/happy-elves 0.1.0-beta.274 → 0.1.0-beta.279
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/session-output.js +56 -3
- package/apps/cli/package.json +1 -1
- package/apps/daemon/dist/relay/release.js +14 -10
- package/apps/daemon/dist/session/lifecycle.d.ts +3 -1
- package/apps/daemon/dist/session/lifecycle.js +9 -5
- package/apps/daemon/package.json +1 -1
- package/apps/relay/dist/controller-handlers.js +11 -11
- package/apps/relay/dist/machine-handlers.js +204 -6
- package/apps/relay/dist/relay-context.js +26 -1
- package/package.json +1 -1
- package/packages/client/dist/client.d.ts +3 -1
- package/packages/client/dist/client.js +7 -2
- package/packages/shared/dist/protocol.d.ts +2 -0
- package/packages/shared/dist/protocol.js +2 -0
|
@@ -315,7 +315,7 @@ function foldAliasedRuntimeEvents(events) {
|
|
|
315
315
|
const profiles = new Map();
|
|
316
316
|
const runtimeToRequest = new Map();
|
|
317
317
|
for (const event of events) {
|
|
318
|
-
const profile = profiles.get(event.turnId) ?? { output: "" };
|
|
318
|
+
const profile = profiles.get(event.turnId) ?? { output: "", status: "running" };
|
|
319
319
|
const payload = event.decoded;
|
|
320
320
|
if (payload?.type === "user_prompt" && profile.prompt === undefined) {
|
|
321
321
|
profile.prompt = payload.text.trim();
|
|
@@ -328,6 +328,7 @@ function foldAliasedRuntimeEvents(events) {
|
|
|
328
328
|
if (runtimeTurnId && runtimeTurnId !== event.turnId && !runtimeTurnId.startsWith("event:")) {
|
|
329
329
|
runtimeToRequest.set(runtimeTurnId, event.turnId);
|
|
330
330
|
}
|
|
331
|
+
profile.status = payload.status;
|
|
331
332
|
}
|
|
332
333
|
profiles.set(event.turnId, profile);
|
|
333
334
|
}
|
|
@@ -345,11 +346,46 @@ function foldAliasedRuntimeEvents(events) {
|
|
|
345
346
|
duplicateRuntimeToRequest.set(runtimeTurnId, requestTurnId);
|
|
346
347
|
}
|
|
347
348
|
}
|
|
349
|
+
const inferredRuntimeToRequest = inferRuntimeCompletionAliases(profiles);
|
|
350
|
+
const inferredRequestTurns = new Set(inferredRuntimeToRequest.values());
|
|
351
|
+
const foldedRuntimeToRequest = new Map([...duplicateRuntimeToRequest, ...inferredRuntimeToRequest]);
|
|
352
|
+
const foldedEvents = [];
|
|
353
|
+
for (const event of events) {
|
|
354
|
+
if (duplicateRuntimeTurns.has(event.turnId))
|
|
355
|
+
continue;
|
|
356
|
+
const inferredRequestTurnId = inferredRuntimeToRequest.get(event.turnId);
|
|
357
|
+
if (inferredRequestTurnId) {
|
|
358
|
+
if (event.decoded?.type === "user_prompt" && profiles.get(inferredRequestTurnId)?.prompt)
|
|
359
|
+
continue;
|
|
360
|
+
foldedEvents.push({ ...event, turnId: inferredRequestTurnId });
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (inferredRequestTurns.has(event.turnId) &&
|
|
364
|
+
event.decoded?.type === "text_delta" &&
|
|
365
|
+
event.decoded.stream !== "thought") {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
foldedEvents.push(event);
|
|
369
|
+
}
|
|
348
370
|
return {
|
|
349
|
-
events:
|
|
350
|
-
runtimeToRequest:
|
|
371
|
+
events: foldedEvents,
|
|
372
|
+
runtimeToRequest: foldedRuntimeToRequest,
|
|
351
373
|
};
|
|
352
374
|
}
|
|
375
|
+
function inferRuntimeCompletionAliases(profiles) {
|
|
376
|
+
const requestTurns = [...profiles.entries()].filter(([turnId, profile]) => turnId.startsWith("turn_") && profile.status === "running" && Boolean(profile.prompt?.trim()));
|
|
377
|
+
const runtimeTurns = [...profiles.entries()].filter(([turnId, profile]) => !turnId.startsWith("turn_") && profile.status !== "running" && Boolean(profile.prompt?.trim()));
|
|
378
|
+
const result = new Map();
|
|
379
|
+
for (const [runtimeTurnId, runtime] of runtimeTurns) {
|
|
380
|
+
const candidates = requestTurns.filter(([, request]) => promptCompatible(request.prompt ?? "", runtime.prompt ?? "") &&
|
|
381
|
+
outputCompatibleForInferredAlias(request.output, runtime.output, request.prompt ?? "", runtime.prompt ?? ""));
|
|
382
|
+
if (candidates.length !== 1)
|
|
383
|
+
continue;
|
|
384
|
+
const [requestTurnId] = candidates[0];
|
|
385
|
+
result.set(runtimeTurnId, requestTurnId);
|
|
386
|
+
}
|
|
387
|
+
return result;
|
|
388
|
+
}
|
|
353
389
|
function dedupeSyntheticHistoricalEvents(events) {
|
|
354
390
|
const groups = new Map();
|
|
355
391
|
for (const event of events) {
|
|
@@ -408,6 +444,23 @@ function isDuplicateRuntimeOutput(requestOutput, runtimeOutput) {
|
|
|
408
444
|
const runtime = comparableOutput(runtimeOutput);
|
|
409
445
|
return !runtime || request === runtime || request.includes(runtime);
|
|
410
446
|
}
|
|
447
|
+
function promptCompatible(requestPrompt, runtimePrompt) {
|
|
448
|
+
const request = comparableOutput(requestPrompt);
|
|
449
|
+
const runtime = comparableOutput(runtimePrompt);
|
|
450
|
+
if (!request || !runtime)
|
|
451
|
+
return false;
|
|
452
|
+
if (request === runtime)
|
|
453
|
+
return true;
|
|
454
|
+
return (request.length >= 40 && runtime.includes(request)) ||
|
|
455
|
+
(runtime.length >= 40 && request.includes(runtime));
|
|
456
|
+
}
|
|
457
|
+
function outputCompatibleForInferredAlias(requestOutput, runtimeOutput, requestPrompt, runtimePrompt) {
|
|
458
|
+
const request = comparableOutput(requestOutput);
|
|
459
|
+
const runtime = comparableOutput(runtimeOutput);
|
|
460
|
+
if (request && runtime)
|
|
461
|
+
return request.includes(runtime) || runtime.includes(request);
|
|
462
|
+
return comparableOutput(requestPrompt) === comparableOutput(runtimePrompt) && Boolean(request || runtime);
|
|
463
|
+
}
|
|
411
464
|
function comparableOutput(value) {
|
|
412
465
|
return value.replace(/\s+/g, " ").trim();
|
|
413
466
|
}
|
package/apps/cli/package.json
CHANGED
|
@@ -248,7 +248,7 @@ async function runStage(stage) {
|
|
|
248
248
|
await log("$ " + stage.command + " " + args.map(shellQuote).join(" "));
|
|
249
249
|
const result = await run(stage.command, args, {
|
|
250
250
|
cwd: input.repoCwd,
|
|
251
|
-
env:
|
|
251
|
+
env: envForStage(stage),
|
|
252
252
|
});
|
|
253
253
|
await log(result.stdout);
|
|
254
254
|
if (result.stderr) await log(result.stderr);
|
|
@@ -300,16 +300,20 @@ async function run(command, args, options) {
|
|
|
300
300
|
});
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
-
function
|
|
303
|
+
function envForStage(stage) {
|
|
304
|
+
const env = { ...process.env };
|
|
305
|
+
if (stage.phase !== "update" && stage.phase !== "notify") {
|
|
306
|
+
delete env.HAPPY_ELVES_RELAY_URL;
|
|
307
|
+
delete env.HAPPY_ELVES_CONTROLLER_TOKEN;
|
|
308
|
+
delete env.HAPPY_ELVES_ACCOUNT_SECRET;
|
|
309
|
+
return env;
|
|
310
|
+
}
|
|
304
311
|
const config = input.controllerConfig;
|
|
305
|
-
return
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
HAPPY_ELVES_ACCOUNT_SECRET: config.accountSecret,
|
|
311
|
-
} : {}),
|
|
312
|
-
};
|
|
312
|
+
if (!config) return env;
|
|
313
|
+
env.HAPPY_ELVES_RELAY_URL = config.relayUrl;
|
|
314
|
+
env.HAPPY_ELVES_CONTROLLER_TOKEN = config.controllerToken;
|
|
315
|
+
env.HAPPY_ELVES_ACCOUNT_SECRET = config.accountSecret;
|
|
316
|
+
return env;
|
|
313
317
|
}
|
|
314
318
|
|
|
315
319
|
async function setStage(name, patch) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type MachineCommand } from "../../../../packages/shared/dist/index.js";
|
|
2
2
|
import type { DaemonConfig, HistoricalBackfillStatus, SessionFallback, SessionState } from "../types.js";
|
|
3
3
|
import { type HistoricalSessionHead } from "./events.js";
|
|
4
|
+
type HistoricalBackfillDirection = "latest" | "older";
|
|
4
5
|
export type HistoricalBackfillResult = {
|
|
5
6
|
availableEvents: number;
|
|
6
7
|
cursor: number;
|
|
@@ -16,7 +17,7 @@ export type HistoricalBackfillResult = {
|
|
|
16
17
|
export declare function handleCreateSession(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
|
|
17
18
|
export declare function handleImportHistoricalSession(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
|
|
18
19
|
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>;
|
|
20
|
+
export declare function resumeSession(ws: WebSocket, config: DaemonConfig, sessionId: string, requestId: string, fallback?: SessionFallback, announceSuccess?: boolean, syncDirection?: HistoricalBackfillDirection): Promise<SessionState | null>;
|
|
20
21
|
export declare function usesAuthoritativeTranscript(session: SessionState): boolean;
|
|
21
22
|
export declare function replaceAuthoritativeTranscript(ws: WebSocket, config: DaemonConfig, session: SessionState, runtimeSessionId?: string | undefined, input?: {
|
|
22
23
|
currentHead?: string;
|
|
@@ -28,3 +29,4 @@ export declare function replaceAuthoritativeTranscript(ws: WebSocket, config: Da
|
|
|
28
29
|
}): Promise<HistoricalBackfillResult>;
|
|
29
30
|
export declare function handleResume(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
|
|
30
31
|
export declare function handleClose(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
|
|
32
|
+
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 = "latest") {
|
|
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 });
|
|
@@ -731,7 +735,7 @@ export async function handleResume(ws, config, command) {
|
|
|
731
735
|
return;
|
|
732
736
|
if (!claimCommandRequest(ws, command.requestId))
|
|
733
737
|
return;
|
|
734
|
-
await resumeSession(ws, config, command.sessionId, command.requestId, await sessionFallbackFromCommand(config, command));
|
|
738
|
+
await resumeSession(ws, config, command.sessionId, command.requestId, await sessionFallbackFromCommand(config, command), true, command.syncDirection ?? "latest");
|
|
735
739
|
}
|
|
736
740
|
export async function handleClose(ws, config, command) {
|
|
737
741
|
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,
|
|
@@ -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 &&
|
|
@@ -396,13 +424,30 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
396
424
|
return;
|
|
397
425
|
}
|
|
398
426
|
const ts = now();
|
|
427
|
+
const promptTurnForRequest = message.requestId
|
|
428
|
+
? promptTurnForRequestId(db, connection.accountId, message.sessionId, connection.machineId, message.requestId)
|
|
429
|
+
: undefined;
|
|
430
|
+
const activeClaim = activePromptClaim(db, connection.accountId, message.sessionId, connection.machineId);
|
|
431
|
+
const replacementRequestTurn = promptTurnForRequest?.turn_id ??
|
|
432
|
+
(sessionRow.status === "running" && message.status ? activeClaim?.turn_id : undefined);
|
|
433
|
+
const replacementRequestId = promptTurnForRequest?.request_id ??
|
|
434
|
+
(replacementRequestTurn === activeClaim?.turn_id ? activeClaim?.request_id : undefined);
|
|
399
435
|
const inserted = [];
|
|
400
436
|
db.exec("BEGIN");
|
|
401
437
|
try {
|
|
402
438
|
db.prepare(`DELETE FROM session_events
|
|
403
439
|
WHERE account_id = ? AND session_id = ? AND machine_id = ?`).run(connection.accountId, message.sessionId, connection.machineId);
|
|
404
|
-
|
|
405
|
-
|
|
440
|
+
if (replacementRequestTurn) {
|
|
441
|
+
recordAliasesForRuntimeTurnIds({
|
|
442
|
+
accountId: connection.accountId,
|
|
443
|
+
sessionId: message.sessionId,
|
|
444
|
+
machineId: connection.machineId,
|
|
445
|
+
requestTurnId: replacementRequestTurn,
|
|
446
|
+
requestId: replacementRequestId,
|
|
447
|
+
runtimeTurnIds: [message.lastTurnId, message.currentHead, replacementBasis?.turnId],
|
|
448
|
+
recordTurnAlias,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
406
451
|
clearPendingSessionHeadAdvances(db, connection.accountId, message.sessionId, connection.machineId);
|
|
407
452
|
for (const event of message.events) {
|
|
408
453
|
const result = db
|
|
@@ -449,6 +494,16 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
449
494
|
encrypted_metadata = COALESCE(?, encrypted_metadata),
|
|
450
495
|
updated_at = ?
|
|
451
496
|
WHERE id = ? AND account_id = ? AND machine_id = ?`).run(message.status ?? null, message.currentHead ?? null, lastEvent ? `event:${lastEvent.id}` : sessionRow.current_head, message.lastTurnId ?? null, lastEvent?.turnId ?? sessionRow.last_turn_id, headBasisEvent?.id ?? null, headBasisEvent?.messageId ?? null, headBasisOrder?.time ?? null, headBasisOrder?.seq ?? null, message.encryptedMetadata ? JSON.stringify(message.encryptedMetadata) : null, ts, message.sessionId, connection.accountId, connection.machineId);
|
|
497
|
+
if (message.status && replacementRequestTurn) {
|
|
498
|
+
finishSessionClaim({
|
|
499
|
+
accountId: connection.accountId,
|
|
500
|
+
sessionId: message.sessionId,
|
|
501
|
+
...(replacementRequestId ? { requestId: replacementRequestId } : {}),
|
|
502
|
+
turnId: replacementRequestTurn,
|
|
503
|
+
terminalStatus: message.status,
|
|
504
|
+
finishedAt: ts,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
452
507
|
db.exec("COMMIT");
|
|
453
508
|
}
|
|
454
509
|
catch (error) {
|
|
@@ -464,7 +519,7 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
464
519
|
broadcastControllers(connection.accountId, {
|
|
465
520
|
type: "server:sessionTranscriptReplaced",
|
|
466
521
|
session,
|
|
467
|
-
events: inserted,
|
|
522
|
+
events: inserted.map((event) => canonicalEventForControllers(db, connection.accountId, event)),
|
|
468
523
|
hasMoreBefore: message.hasMoreBefore,
|
|
469
524
|
nextBefore: message.nextBefore,
|
|
470
525
|
});
|
|
@@ -481,6 +536,10 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
481
536
|
acknowledgeMachineMessage();
|
|
482
537
|
return;
|
|
483
538
|
}
|
|
539
|
+
if (sessionRow.status === "closed") {
|
|
540
|
+
acknowledgeMachineMessage();
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
484
543
|
const ts = now();
|
|
485
544
|
const inserted = [];
|
|
486
545
|
db.exec("BEGIN");
|
|
@@ -540,6 +599,27 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
540
599
|
}
|
|
541
600
|
if (message.type === "machine:sessionHeadAdvanced") {
|
|
542
601
|
const ts = now();
|
|
602
|
+
const sessionBefore = db
|
|
603
|
+
.prepare("SELECT * FROM sessions WHERE id = ? AND account_id = ? AND machine_id = ?")
|
|
604
|
+
.get(message.sessionId, connection.accountId, connection.machineId);
|
|
605
|
+
if (sessionBefore?.status === "closed") {
|
|
606
|
+
acknowledgeMachineMessage();
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const activeClaim = sessionBefore
|
|
610
|
+
? activePromptClaim(db, connection.accountId, message.sessionId, connection.machineId)
|
|
611
|
+
: undefined;
|
|
612
|
+
if (sessionBefore?.status === "running" && activeClaim && message.status) {
|
|
613
|
+
recordAliasesForRuntimeTurnIds({
|
|
614
|
+
accountId: connection.accountId,
|
|
615
|
+
sessionId: message.sessionId,
|
|
616
|
+
machineId: connection.machineId,
|
|
617
|
+
requestTurnId: activeClaim.turn_id,
|
|
618
|
+
requestId: activeClaim.request_id,
|
|
619
|
+
runtimeTurnIds: [message.lastTurnId, message.currentHead, message.basis.turnId],
|
|
620
|
+
recordTurnAlias,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
543
623
|
const input = {
|
|
544
624
|
accountId: connection.accountId,
|
|
545
625
|
sessionId: message.sessionId,
|
|
@@ -580,7 +660,22 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
580
660
|
acknowledgeMachineMessage();
|
|
581
661
|
return;
|
|
582
662
|
}
|
|
583
|
-
|
|
663
|
+
if (message.status && activeClaim && sessionBefore?.status === "running") {
|
|
664
|
+
db.prepare(`UPDATE sessions
|
|
665
|
+
SET status = ?,
|
|
666
|
+
last_turn_id = COALESCE(?, last_turn_id),
|
|
667
|
+
updated_at = ?
|
|
668
|
+
WHERE id = ? AND account_id = ? AND machine_id = ? AND status = 'running'`).run(message.status, message.lastTurnId ?? message.currentHead ?? message.basis.turnId, ts, message.sessionId, connection.accountId, connection.machineId);
|
|
669
|
+
finishSessionClaim({
|
|
670
|
+
accountId: connection.accountId,
|
|
671
|
+
sessionId: message.sessionId,
|
|
672
|
+
requestId: activeClaim.request_id,
|
|
673
|
+
turnId: activeClaim.turn_id,
|
|
674
|
+
terminalStatus: message.status,
|
|
675
|
+
finishedAt: ts,
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
const row = db.prepare("SELECT * FROM sessions WHERE id = ?").get(message.sessionId);
|
|
584
679
|
if (row)
|
|
585
680
|
broadcastControllers(connection.accountId, { type: "server:session", session: sessionSnapshot(row) });
|
|
586
681
|
acknowledgeMachineMessage();
|
|
@@ -592,6 +687,17 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
592
687
|
.get(message.sessionId, connection.accountId, connection.machineId);
|
|
593
688
|
if (!sessionRow)
|
|
594
689
|
return;
|
|
690
|
+
maybeAliasHistoricalRuntimeEventToActiveClaim({
|
|
691
|
+
db,
|
|
692
|
+
accountId: connection.accountId,
|
|
693
|
+
sessionId: message.sessionId,
|
|
694
|
+
machineId: connection.machineId,
|
|
695
|
+
session: sessionRow,
|
|
696
|
+
turnId: message.turnId,
|
|
697
|
+
messageId: message.messageId,
|
|
698
|
+
occurredAt: message.occurredAt,
|
|
699
|
+
recordTurnAlias,
|
|
700
|
+
});
|
|
595
701
|
const event = insertEvent({
|
|
596
702
|
accountId: connection.accountId,
|
|
597
703
|
sessionId: message.sessionId,
|
|
@@ -604,7 +710,7 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
604
710
|
payload: message.payload,
|
|
605
711
|
});
|
|
606
712
|
const pendingSessionRow = applyPendingSessionHeadAdvances(db, connection.accountId, message.sessionId, connection.machineId, now());
|
|
607
|
-
broadcastControllers(connection.accountId, { type: "server:event", event });
|
|
713
|
+
broadcastControllers(connection.accountId, { type: "server:event", event: canonicalEventForControllers(db, connection.accountId, event) });
|
|
608
714
|
if (pendingSessionRow)
|
|
609
715
|
broadcastControllers(connection.accountId, { type: "server:session", session: sessionSnapshot(pendingSessionRow) });
|
|
610
716
|
acknowledgeMachineMessage();
|
|
@@ -639,6 +745,18 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
639
745
|
acknowledgeMachineMessage();
|
|
640
746
|
return;
|
|
641
747
|
}
|
|
748
|
+
if (sessionRow.status === "closed") {
|
|
749
|
+
finishSessionClaim({
|
|
750
|
+
accountId: connection.accountId,
|
|
751
|
+
sessionId: message.sessionId,
|
|
752
|
+
turnId: message.turnId,
|
|
753
|
+
terminalStatus: status,
|
|
754
|
+
...(message.error ? { errorCode: "TURN_ERROR" } : {}),
|
|
755
|
+
finishedAt: ts,
|
|
756
|
+
});
|
|
757
|
+
acknowledgeMachineMessage();
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
642
760
|
const currentOrder = sessionHeadBasisOrder(db, sessionRow);
|
|
643
761
|
const basisComparison = basisOrder ? compareHeadBasisOrder(basisOrder, currentOrder) : null;
|
|
644
762
|
const isRuntimeAliasForCurrentRequest = Boolean(runtimeBasisEvent && runtimeTurnId !== message.turnId && (sessionRow.last_turn_id === message.turnId || requestBasisEvent));
|
|
@@ -790,6 +908,8 @@ function applySessionHeadAdvance(db, input, ts) {
|
|
|
790
908
|
.get(input.sessionId, input.accountId, input.machineId);
|
|
791
909
|
if (!sessionRow)
|
|
792
910
|
return { status: "session-not-found" };
|
|
911
|
+
if (sessionRow.status === "closed")
|
|
912
|
+
return { status: "noop", session: sessionRow };
|
|
793
913
|
const basisEvent = resolveHeadAdvanceBasisEvent(db, input.accountId, input.sessionId, input.machineId, input.basis);
|
|
794
914
|
if (!basisEvent)
|
|
795
915
|
return { status: "missing", session: sessionRow };
|
|
@@ -903,4 +1023,82 @@ function latestEventForTurn(db, accountId, sessionId, machineId, turnId) {
|
|
|
903
1023
|
LIMIT 1`)
|
|
904
1024
|
.get(accountId, sessionId, machineId, turnId);
|
|
905
1025
|
}
|
|
1026
|
+
function activePromptClaim(db, accountId, sessionId, machineId) {
|
|
1027
|
+
return db
|
|
1028
|
+
.prepare(`SELECT * FROM relay_session_claims
|
|
1029
|
+
WHERE account_id = ? AND session_id = ? AND machine_id = ? AND state = 'active'
|
|
1030
|
+
LIMIT 1`)
|
|
1031
|
+
.get(accountId, sessionId, machineId);
|
|
1032
|
+
}
|
|
1033
|
+
function promptTurnForRequestId(db, accountId, sessionId, machineId, requestId) {
|
|
1034
|
+
const claim = db
|
|
1035
|
+
.prepare(`SELECT request_id, turn_id FROM relay_session_claims
|
|
1036
|
+
WHERE account_id = ? AND session_id = ? AND machine_id = ? AND request_id = ? AND state = 'active'
|
|
1037
|
+
LIMIT 1`)
|
|
1038
|
+
.get(accountId, sessionId, machineId, requestId);
|
|
1039
|
+
if (claim)
|
|
1040
|
+
return claim;
|
|
1041
|
+
return db
|
|
1042
|
+
.prepare(`SELECT request_id, turn_id FROM relay_admissions
|
|
1043
|
+
WHERE account_id = ? AND session_id = ? AND machine_id = ? AND request_id = ? AND kind = 'prompt' AND turn_id IS NOT NULL
|
|
1044
|
+
LIMIT 1`)
|
|
1045
|
+
.get(accountId, sessionId, machineId, requestId);
|
|
1046
|
+
}
|
|
1047
|
+
function maybeAliasHistoricalRuntimeEventToActiveClaim(input) {
|
|
1048
|
+
if (input.session.status !== "running")
|
|
1049
|
+
return;
|
|
1050
|
+
if (!isHistoricalRuntimeBackfillMessage(input.messageId, input.occurredAt))
|
|
1051
|
+
return;
|
|
1052
|
+
const claim = activePromptClaim(input.db, input.accountId, input.sessionId, input.machineId);
|
|
1053
|
+
if (!claim)
|
|
1054
|
+
return;
|
|
1055
|
+
recordAliasesForRuntimeTurnIds({
|
|
1056
|
+
accountId: input.accountId,
|
|
1057
|
+
sessionId: input.sessionId,
|
|
1058
|
+
machineId: input.machineId,
|
|
1059
|
+
requestTurnId: claim.turn_id,
|
|
1060
|
+
requestId: claim.request_id,
|
|
1061
|
+
runtimeTurnIds: [input.turnId],
|
|
1062
|
+
recordTurnAlias: input.recordTurnAlias,
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
function isHistoricalRuntimeBackfillMessage(messageId, occurredAt) {
|
|
1066
|
+
if (messageId?.startsWith("hist_") || messageId?.startsWith("tail_"))
|
|
1067
|
+
return true;
|
|
1068
|
+
return occurredAt !== undefined;
|
|
1069
|
+
}
|
|
1070
|
+
function recordAliasesForRuntimeTurnIds(input) {
|
|
1071
|
+
for (const runtimeTurnId of input.runtimeTurnIds) {
|
|
1072
|
+
if (!isRuntimeTurnAliasCandidate(input.requestTurnId, runtimeTurnId))
|
|
1073
|
+
continue;
|
|
1074
|
+
input.recordTurnAlias({
|
|
1075
|
+
accountId: input.accountId,
|
|
1076
|
+
sessionId: input.sessionId,
|
|
1077
|
+
machineId: input.machineId,
|
|
1078
|
+
requestTurnId: input.requestTurnId,
|
|
1079
|
+
runtimeTurnId,
|
|
1080
|
+
requestId: input.requestId,
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
function isRuntimeTurnAliasCandidate(requestTurnId, runtimeTurnId) {
|
|
1085
|
+
if (!runtimeTurnId)
|
|
1086
|
+
return false;
|
|
1087
|
+
const value = runtimeTurnId.trim();
|
|
1088
|
+
if (!value || value === requestTurnId)
|
|
1089
|
+
return false;
|
|
1090
|
+
if (value.startsWith("event:"))
|
|
1091
|
+
return false;
|
|
1092
|
+
return true;
|
|
1093
|
+
}
|
|
1094
|
+
function canonicalEventForControllers(db, accountId, event) {
|
|
1095
|
+
const alias = db
|
|
1096
|
+
.prepare(`SELECT request_turn_id FROM session_turn_aliases
|
|
1097
|
+
WHERE account_id = ? AND session_id = ? AND machine_id = ? AND runtime_turn_id = ?
|
|
1098
|
+
LIMIT 1`)
|
|
1099
|
+
.get(accountId, event.sessionId, event.machineId, event.turnId);
|
|
1100
|
+
return alias && alias.request_turn_id !== event.turnId
|
|
1101
|
+
? { ...event, turnId: alias.request_turn_id }
|
|
1102
|
+
: event;
|
|
1103
|
+
}
|
|
906
1104
|
//# sourceMappingURL=machine-handlers.js.map
|
|
@@ -366,6 +366,29 @@ export function createRelayContext(options) {
|
|
|
366
366
|
if (!machine)
|
|
367
367
|
return "queued";
|
|
368
368
|
const command = JSON.parse(row.message);
|
|
369
|
+
if (command.type === "machine:prompt") {
|
|
370
|
+
const session = options.db
|
|
371
|
+
.prepare("SELECT status FROM sessions WHERE account_id = ? AND id = ? AND machine_id = ?")
|
|
372
|
+
.get(accountId, command.sessionId, row.machine_id);
|
|
373
|
+
if (session?.status === "closed") {
|
|
374
|
+
const ts = now();
|
|
375
|
+
options.db
|
|
376
|
+
.prepare(`UPDATE relay_dispatch_outbox
|
|
377
|
+
SET state = 'delivered', delivered_at = ?, updated_at = ?
|
|
378
|
+
WHERE account_id = ? AND request_id = ? AND state = 'queued'`)
|
|
379
|
+
.run(ts, ts, accountId, requestId);
|
|
380
|
+
finishSessionClaim({
|
|
381
|
+
accountId,
|
|
382
|
+
sessionId: command.sessionId,
|
|
383
|
+
requestId,
|
|
384
|
+
turnId: command.turnId,
|
|
385
|
+
terminalStatus: "cancelled",
|
|
386
|
+
errorCode: "SESSION_CLOSED",
|
|
387
|
+
finishedAt: ts,
|
|
388
|
+
});
|
|
389
|
+
return "queued";
|
|
390
|
+
}
|
|
391
|
+
}
|
|
369
392
|
send(machine.socket, command);
|
|
370
393
|
const ts = now();
|
|
371
394
|
options.db
|
|
@@ -635,7 +658,9 @@ export function createRelayContext(options) {
|
|
|
635
658
|
}
|
|
636
659
|
function setSessionStatus(sessionId, status) {
|
|
637
660
|
const ts = now();
|
|
638
|
-
options.db
|
|
661
|
+
options.db
|
|
662
|
+
.prepare("UPDATE sessions SET status = ?, updated_at = ? WHERE id = ? AND (status != 'closed' OR ? = 'closed')")
|
|
663
|
+
.run(status, ts, sessionId, status);
|
|
639
664
|
const row = options.db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
640
665
|
return row ? sessionSnapshot(row) : undefined;
|
|
641
666
|
}
|
package/package.json
CHANGED
|
@@ -70,7 +70,9 @@ export declare class ControllerClient {
|
|
|
70
70
|
collectPage(sessionId: string, input?: CollectInput): Promise<DecodedSessionEventsPage>;
|
|
71
71
|
repairSessionHead(sessionId: string, input: RepairSessionHeadInput): Promise<RepairSessionHeadResult>;
|
|
72
72
|
wait(sessionId: string, input?: WaitInput): Promise<SessionSnapshot>;
|
|
73
|
-
resume(sessionId: string
|
|
73
|
+
resume(sessionId: string, options?: {
|
|
74
|
+
syncDirection?: "latest" | "older";
|
|
75
|
+
}): Promise<CommandResult>;
|
|
74
76
|
listHistoricalSessionsPage(input: HistoricalSessionsInput): Promise<HistoricalSessionListPage>;
|
|
75
77
|
listHistoricalSessions(input: HistoricalSessionsInput): Promise<HistoricalSessionSnapshot[]>;
|
|
76
78
|
listDirectory(input: DirectoryInput): Promise<DirectoryListing>;
|
|
@@ -302,10 +302,15 @@ export class ControllerClient {
|
|
|
302
302
|
await sleep(intervalMs);
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
|
-
async resume(sessionId) {
|
|
305
|
+
async resume(sessionId, options = {}) {
|
|
306
306
|
const checkedSessionId = requireNonEmpty(sessionId, "sessionId");
|
|
307
307
|
const requestId = randomId("req");
|
|
308
|
-
return await this.sendCommand({
|
|
308
|
+
return await this.sendCommand({
|
|
309
|
+
type: "controller:resume",
|
|
310
|
+
requestId,
|
|
311
|
+
sessionId: checkedSessionId,
|
|
312
|
+
...(options.syncDirection ? { syncDirection: options.syncDirection } : {}),
|
|
313
|
+
});
|
|
309
314
|
}
|
|
310
315
|
async listHistoricalSessionsPage(input) {
|
|
311
316
|
const machineId = requireNonEmpty(input.machineId, "machineId");
|
|
@@ -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({
|