@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.
@@ -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: duplicateRuntimeTurns.size === 0 ? events : events.filter((event) => !duplicateRuntimeTurns.has(event.turnId)),
350
- runtimeToRequest: duplicateRuntimeToRequest,
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
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-cli",
3
- "version": "0.1.0-beta.274",
3
+ "version": "0.1.0-beta.279",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -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: controllerEnv(),
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 controllerEnv() {
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
- ...process.env,
307
- ...(config ? {
308
- HAPPY_ELVES_RELAY_URL: config.relayUrl,
309
- HAPPY_ELVES_CONTROLLER_TOKEN: config.controllerToken,
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")
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-daemon",
3
- "version": "0.1.0-beta.274",
3
+ "version": "0.1.0-beta.279",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -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 = ?`).run(message.result.terminalStatus, message.result.currentHead ?? null, message.result.lastTurnId ?? message.result.turnId ?? null, ts, message.result.sessionId, connection.accountId, connection.machineId);
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
- db.prepare(`DELETE FROM session_turn_aliases
405
- WHERE account_id = ? AND session_id = ? AND machine_id = ?`).run(connection.accountId, message.sessionId, connection.machineId);
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
- const row = result.session ?? db.prepare("SELECT * FROM sessions WHERE id = ?").get(message.sessionId);
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.prepare("UPDATE sessions SET status = ?, updated_at = ? WHERE id = ?").run(status, ts, sessionId);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves",
3
- "version": "0.1.0-beta.274",
3
+ "version": "0.1.0-beta.279",
4
4
  "description": "Remote controller for local coding agents with hosted or self-hosted relay support.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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): Promise<CommandResult>;
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({ type: "controller:resume", requestId, sessionId: checkedSessionId });
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({