@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.
@@ -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);
@@ -47,6 +47,7 @@ export type OrchestrationSummary = {
47
47
  evidence: "running-turn";
48
48
  turnId: string;
49
49
  };
50
+ staleRunningTurnId?: string;
50
51
  };
51
52
  export type LoopRecord = {
52
53
  id: string;
@@ -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
- : deriveOrchestration(session, events, options.focusTurn);
75
- const status = normalizeOrchestratorStatus(session, orchestration, latestTurn);
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 active = turns[0]?.status === "running" ? turns[0] : undefined;
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 emitEventBestEffort(ws, config, binding.sessionId, turnId, { type: "user_prompt", text: input.text });
380
- await emitEventBestEffort(ws, config, binding.sessionId, turnId, {
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 emitEventBestEffort(ws, config, sessionId, turnId, payload);
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 emitEventBestEffort(ws, config, sessionId, turnId, result.status === "failed"
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): Promise<void>;
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
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-daemon",
3
- "version": "0.1.0-beta.60",
3
+ "version": "0.1.0-beta.62",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -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 expectedEncryptedMetadata = body.expectedEncryptedMetadata === undefined
375
- ? session.encrypted_metadata
376
- : body.expectedEncryptedMetadata === null
374
+ const hasExpectedEncryptedMetadata = body.expectedEncryptedMetadata !== undefined;
375
+ const expectedEncryptedMetadata = hasExpectedEncryptedMetadata
376
+ ? body.expectedEncryptedMetadata === null
377
377
  ? null
378
- : JSON.stringify(body.expectedEncryptedMetadata);
379
- const result = body.expectedUpdatedAt === undefined
380
- ? context.db.prepare(`UPDATE sessions
381
- SET encrypted_metadata = ?,
382
- updated_at = ?
383
- WHERE account_id = ? AND id = ?`).run(JSON.stringify(body.encryptedMetadata), ts, token.account_id, params.sessionId)
384
- : context.db.prepare(`UPDATE sessions
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 = ? AND updated_at = ? AND encrypted_metadata IS ?`).run(JSON.stringify(body.encryptedMetadata), ts, token.account_id, params.sessionId, body.expectedUpdatedAt, expectedEncryptedMetadata);
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
  }
@@ -46,6 +46,7 @@ export type MachineHandlerContext = {
46
46
  turnSeq?: number;
47
47
  occurredAt?: number;
48
48
  payload: EncryptedEnvelope;
49
+ advanceSessionHead?: boolean;
49
50
  }): Extract<ServerMessage, {
50
51
  type: "server:event";
51
52
  }>["event"];
@@ -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
- if (context.aliasedRuntimeEventHasRequestEvents({
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
- context.broadcastControllers(connection.accountId, { type: "server:event", event });
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 || (basisComparison !== null ? basisComparison >= 0 : !currentOrder));
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,
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves",
3
- "version": "0.1.0-beta.60",
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.60",
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.0",
761
- "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz",
762
- "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==",
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.3",
902
- "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.3.tgz",
903
- "integrity": "sha512-xRgplks8SvcKkdlv2M6Z2LZmRsmqd+x0nXXGXeMEjwdibj1HSDrlnqBRLeYdMvsgCox7Bq0e+DHwfczOfsn6IA==",
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.0.1",
935
- "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.1.tgz",
936
- "integrity": "sha512-ghj2DSK/2e99a1anTVPCV4m4YIYtrbXhfM7V3D7XZLOTsybnYyaJloymGqssQc8l/or0UoDyRtNQkmkEF/ysgQ==",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves",
3
- "version": "0.1.0-beta.60",
3
+ "version": "0.1.0-beta.62",
4
4
  "description": "Remote controller for local coding agents with hosted or self-hosted relay support.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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;