@cydm/happy-elves 0.1.0-beta.486 → 0.1.0-beta.487

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.
Files changed (48) hide show
  1. package/apps/cli/dist/commands/lib/args.js +4 -0
  2. package/apps/cli/dist/commands/lib/usage.js +3 -3
  3. package/apps/cli/dist/commands/lib/workspace.d.ts +6 -1
  4. package/apps/cli/dist/commands/lib/workspace.js +5 -0
  5. package/apps/cli/dist/commands/session.js +54 -5
  6. package/apps/cli/package.json +1 -1
  7. package/apps/daemon/dist/orchestration/crew-queue.js +33 -2
  8. package/apps/daemon/dist/orchestration/handler.js +12 -2
  9. package/apps/daemon/dist/orchestration/store.d.ts +1 -0
  10. package/apps/daemon/dist/orchestration/store.js +12 -0
  11. package/apps/daemon/dist/relay/connection.js +61 -2
  12. package/apps/daemon/dist/runtime/headless-manager.js +39 -4
  13. package/apps/daemon/dist/runtime/headless-session-history.js +20 -1
  14. package/apps/daemon/dist/session/he-official/artifact-commands.js +9 -7
  15. package/apps/daemon/dist/session/he-official/common.d.ts +29 -2
  16. package/apps/daemon/dist/session/he-official/common.js +26 -11
  17. package/apps/daemon/dist/session/he-official/orchestration-commands.js +2 -2
  18. package/apps/daemon/dist/session/lifecycle.js +66 -3
  19. package/apps/daemon/dist/session/native-identity.js +17 -1
  20. package/apps/daemon/dist/session/prompt.d.ts +1 -0
  21. package/apps/daemon/dist/session/prompt.js +20 -5
  22. package/apps/daemon/dist/state.d.ts +5 -1
  23. package/apps/daemon/dist/state.js +28 -0
  24. package/apps/daemon/package.json +1 -1
  25. package/apps/relay/dist/http-routes.js +74 -78
  26. package/apps/relay/dist/index.js +12 -4
  27. package/apps/relay/dist/packet-store.js +9 -2
  28. package/apps/relay/dist/relay-context.d.ts +213 -3
  29. package/apps/relay/dist/relay-context.js +336 -6
  30. package/apps/relay/dist/relay-migration.d.ts +25 -2
  31. package/apps/relay/dist/relay-migration.js +27 -112
  32. package/apps/relay/dist/retention.d.ts +10 -0
  33. package/apps/relay/dist/retention.js +20 -0
  34. package/apps/relay/dist/storage-worker.d.ts +137 -0
  35. package/apps/relay/dist/storage-worker.js +490 -0
  36. package/apps/relay/dist/websocket.d.ts +4 -0
  37. package/apps/relay/dist/websocket.js +223 -106
  38. package/package.json +1 -1
  39. package/packages/client/dist/client.js +6 -1
  40. package/packages/client/dist/types.d.ts +1 -0
  41. package/packages/client/dist/workspace-shared-config.js +8 -3
  42. package/packages/headless-runtime/dist/codex-provider.js +76 -18
  43. package/packages/headless-runtime/dist/core.d.ts +1 -0
  44. package/packages/headless-runtime/dist/core.js +134 -6
  45. package/packages/shared/dist/packet-relay.d.ts +2 -0
  46. package/packages/shared/dist/packet-relay.js +1 -0
  47. package/packages/shared/dist/protocol.d.ts +1 -0
  48. package/packages/shared/dist/protocol.js +1 -0
@@ -33,12 +33,14 @@ const valueFlags = new Set([
33
33
  "limit",
34
34
  "machine",
35
35
  "machine-id",
36
+ "native-identity",
36
37
  "memory",
37
38
  "memory-mode",
38
39
  "message-id",
39
40
  "memory-project",
40
41
  "memory-top-n",
41
42
  "name",
43
+ "observed-at",
42
44
  "model",
43
45
  "thinking-level",
44
46
  "mode",
@@ -69,6 +71,7 @@ const valueFlags = new Set([
69
71
  "allow-sender",
70
72
  "expires-in",
71
73
  "format",
74
+ "history-version",
72
75
  "loops",
73
76
  "machines",
74
77
  "sessions",
@@ -112,6 +115,7 @@ const booleanFlags = new Set([
112
115
  "allow-any-sender",
113
116
  "app-secret-stdin",
114
117
  "confirm-risk",
118
+ "confirm-native-handoff",
115
119
  "copy-tags",
116
120
  "detach",
117
121
  "disable-reaction",
@@ -53,7 +53,7 @@ Check local controller config, relay reachability, and daemon state.
53
53
  happy-elves session tag remove <sessionId> --tag <tag> --json
54
54
  happy-elves session tag list <sessionId> --json
55
55
  happy-elves session history --machine <machineId> [--cwd <path>] [--agent <agent>] [--include-archived] [--limit 20] --json
56
- happy-elves session continue <runtimeSessionId> --machine <machineId> [--name <name>] --json
56
+ happy-elves session continue <runtimeSessionId> --machine <machineId> [--name <name>] [--confirm-native-handoff] [--observed-at <ms>] [--history-version <version>] --json
57
57
  happy-elves session status <sessionId> [--verbose] [--json]
58
58
  happy-elves session diagnose <sessionId> [--limit 200] [--repair-head] [--json]
59
59
  happy-elves session close <sessionId> [--reason <reason>] --json
@@ -271,7 +271,7 @@ Core commands:
271
271
  session list [--machine <machineId>] [--cwd <path>] [--limit 20] [--json]
272
272
  session create --machine <machineId> [--agent codex] [--cwd <path>] [--name <name>] --json
273
273
  session history --machine <machineId> [--cwd <path>] [--limit 20] --json
274
- session continue <runtimeSessionId> --machine <machineId> --json
274
+ session continue <runtimeSessionId> --machine <machineId> [--confirm-native-handoff] [--observed-at <ms>] [--history-version <version>] --json
275
275
  session status <sessionId> [--verbose] [--json]
276
276
  session diagnose <sessionId> [--limit 200] [--repair-head] [--json]
277
277
  session close <sessionId> [--reason <reason>] --json
@@ -326,7 +326,7 @@ Usage:
326
326
 
327
327
  Common flows:
328
328
  Discover: machine list --json; session list --machine <machineId> --json
329
- Continue: session history --machine <machineId> --json; session continue <runtimeSessionId> --machine <machineId> --json
329
+ Continue: session history --machine <machineId> --json; session continue <runtimeSessionId> --machine <machineId> --confirm-native-handoff --json
330
330
  Execute: turn run <sessionId> --text <prompt> --json; turn result <sessionId> <turnId> --json
331
331
  Branch: turn rewind <sessionId> <turnId> --json; turn fork <sessionId> <turnId> --name <name> --json
332
332
 
@@ -1,5 +1,5 @@
1
1
  import type { ControllerClient } from "../../../../../packages/client/dist/index.js";
2
- import type { HistoricalSessionSnapshot } from "../../../../../packages/shared/dist/index.js";
2
+ import type { HistoricalSessionSnapshot, HistoricalSessionWarning } from "../../../../../packages/shared/dist/index.js";
3
3
  export type WorkspaceHistoricalSession = HistoricalSessionSnapshot & {
4
4
  machineId: string;
5
5
  machineName: string;
@@ -18,6 +18,11 @@ export declare function listWorkspaceHistoricalSessions(client: ControllerClient
18
18
  code?: string;
19
19
  message: string;
20
20
  }>;
21
+ warnings: Array<{
22
+ machineId: string;
23
+ machineName: string;
24
+ warning: HistoricalSessionWarning;
25
+ }>;
21
26
  machines: Array<{
22
27
  machineId: string;
23
28
  machineName: string;
@@ -18,6 +18,7 @@ export async function listWorkspaceHistoricalSessions(client, input) {
18
18
  const machineName = projected.metadata.name ?? projected.metadata.hostname ?? machine.name;
19
19
  try {
20
20
  const sessions = [];
21
+ const warnings = [];
21
22
  let cursor;
22
23
  const seenCursors = new Set();
23
24
  while (sessions.length < input.limit) {
@@ -30,6 +31,7 @@ export async function listWorkspaceHistoricalSessions(client, input) {
30
31
  cursor,
31
32
  });
32
33
  sessions.push(...page.sessions);
34
+ warnings.push(...(page.warnings ?? []));
33
35
  if (!page.hasMore || !page.nextCursor)
34
36
  break;
35
37
  if (seenCursors.has(page.nextCursor))
@@ -42,6 +44,7 @@ export async function listWorkspaceHistoricalSessions(client, input) {
42
44
  .filter((session) => input.includeArchived === true || session.runtimeState !== "archived")
43
45
  .map((session) => ({ ...session, machineId: machine.id, machineName })),
44
46
  error: undefined,
47
+ warnings,
45
48
  machine: { machineId: machine.id, machineName },
46
49
  };
47
50
  }
@@ -55,6 +58,7 @@ export async function listWorkspaceHistoricalSessions(client, input) {
55
58
  code: enriched.code,
56
59
  message: error instanceof Error ? error.message : String(error),
57
60
  },
61
+ warnings: [],
58
62
  machine: { machineId: machine.id, machineName },
59
63
  };
60
64
  }
@@ -65,6 +69,7 @@ export async function listWorkspaceHistoricalSessions(client, input) {
65
69
  .sort((left, right) => right.updatedAt - left.updatedAt || left.runtimeSessionId.localeCompare(right.runtimeSessionId))
66
70
  .slice(0, input.limit)),
67
71
  errors: rows.flatMap((row) => row.error ? [row.error] : []),
72
+ warnings: rows.flatMap((row) => row.warnings.map((warning) => ({ machineId: row.machine.machineId, machineName: row.machine.machineName, warning }))),
68
73
  machines: rows.map((row) => row.machine),
69
74
  };
70
75
  }
@@ -546,13 +546,23 @@ export async function handleSession({ domain, action, positional, flags }) {
546
546
  returned: result.sessions.length,
547
547
  hasMore: result.sessions.length >= limit,
548
548
  },
549
+ warnings: result.warnings,
549
550
  ...(wantsVerbose(flags) ? { errors: result.errors, machines: result.machines } : {}),
550
551
  }, { machineId });
551
552
  }
552
553
  else if (sessions.length === 0) {
554
+ for (const { machineName, warning } of result.warnings) {
555
+ console.log(`Warning: ${machineName} · ${warning.provider}: ${warning.message}`);
556
+ }
557
+ for (const error of result.errors) {
558
+ console.log(`Warning: ${error.machineName}: ${error.message}`);
559
+ }
553
560
  console.log("No runtime historical sessions found.");
554
561
  }
555
562
  else {
563
+ for (const { machineName, warning } of result.warnings) {
564
+ console.log(`Warning: ${machineName} · ${warning.provider}: ${warning.message}`);
565
+ }
556
566
  for (const session of sessions) {
557
567
  const loaded = session.sessionId ?? "-";
558
568
  const state = session.runtimeState === "archived" ? " archived" : "";
@@ -576,10 +586,25 @@ export async function handleSession({ domain, action, positional, flags }) {
576
586
  includeArchived: true,
577
587
  limit: 200,
578
588
  });
579
- const historical = result.sessions.find((session) => session.runtimeSessionId === runtimeSessionId);
580
- if (!historical)
589
+ const historical = result.sessions.find((session) => session.runtimeSessionId === runtimeSessionId)
590
+ ?? result.sessions.find((session) => session.providerSessionId === runtimeSessionId && session.nativeIdentity);
591
+ if (!historical) {
592
+ const warning = result.warnings[0]?.warning;
593
+ if (warning) {
594
+ const code = warning.code === "permission-denied" ? "PROVIDER_PERMISSION_DENIED"
595
+ : warning.code === "unsupported" ? "PROVIDER_CAPABILITY_UNAVAILABLE"
596
+ : "PROVIDER_UNAVAILABLE";
597
+ throw new CliError(`${warning.provider}: ${warning.message}`, code);
598
+ }
581
599
  throw new CliError(`Runtime session not found: ${runtimeSessionId}`, "RUNTIME_SESSION_NOT_FOUND");
582
- if (historical.loadedSessionId) {
600
+ }
601
+ // Imported native sessions are represented by a stable HE runtime id in
602
+ // the canonical headless row, while the provider UUID remains the
603
+ // durable native provenance. Accept either id, but retain the native
604
+ // handoff path whenever that provenance is present.
605
+ const isNative = historical.source === "native" || Boolean(historical.nativeIdentity && historical.providerSessionId);
606
+ const resolvedRuntimeSessionId = historical.runtimeSessionId;
607
+ if (historical.loadedSessionId && !isNative) {
583
608
  ok("session.continue", {
584
609
  sessionId: historical.loadedSessionId,
585
610
  runtimeSessionId,
@@ -591,19 +616,43 @@ export async function handleSession({ domain, action, positional, flags }) {
591
616
  const name = typeof flags.name === "string" && flags.name.trim()
592
617
  ? flags.name.trim()
593
618
  : historical.name?.trim() || historical.summary?.trim() || historical.runtimeSessionName?.trim() || runtimeSessionId;
619
+ if (isNative && flags["confirm-native-handoff"] !== true && flags.manual !== true) {
620
+ throw new CliError("Native session takeover requires explicit confirmation. Re-run with --confirm-native-handoff (and optionally --observed-at/--history-version from this listing).", "NATIVE_HANDOFF_CONFIRMATION_REQUIRED");
621
+ }
622
+ const observedAtValue = flags["observed-at"];
623
+ const observedAt = observedAtValue === undefined || observedAtValue === true ? Date.now() : Number(observedAtValue);
624
+ if (!Number.isSafeInteger(observedAt) || observedAt <= 0)
625
+ throw new CliError("Invalid --observed-at", "INVALID_ARGUMENT");
626
+ const suppliedIdentity = typeof flags["native-identity"] === "string" ? flags["native-identity"].trim() : historical.nativeIdentity;
627
+ const suppliedHistoryVersion = typeof flags["history-version"] === "string" ? flags["history-version"].trim() : historical.nativeHistoryVersion;
628
+ const nativeIdentity = suppliedIdentity;
629
+ if (isNative && !nativeIdentity)
630
+ throw new CliError("Native session listing did not include a handoff identity; refresh the history listing.", "NATIVE_HANDOFF_PROOF_MISSING");
631
+ if (isNative && historical.nativeIdentity && nativeIdentity !== historical.nativeIdentity)
632
+ throw new CliError("--native-identity does not match the freshly listed native session.", "NATIVE_HANDOFF_RECONFIRM_REQUIRED");
633
+ if (isNative && historical.nativeHistoryVersion && suppliedHistoryVersion !== historical.nativeHistoryVersion)
634
+ throw new CliError("--history-version does not match the freshly listed native session.", "NATIVE_HANDOFF_RECONFIRM_REQUIRED");
635
+ const manualHandoffProof = isNative && nativeIdentity
636
+ ? {
637
+ identity: nativeIdentity,
638
+ observedAt,
639
+ ...(suppliedHistoryVersion ? { historyVersion: suppliedHistoryVersion } : {}),
640
+ }
641
+ : undefined;
594
642
  const continued = await client.importHistoricalSession({
595
643
  machineId,
596
644
  agent: historical.agent,
597
645
  cwd: historical.cwd,
598
- runtimeSessionId,
646
+ runtimeSessionId: resolvedRuntimeSessionId,
599
647
  name,
600
648
  ...(historical.name ? { providerName: historical.name } : {}),
601
649
  ...(historical.provider ? { provider: historical.provider } : {}),
602
650
  ...(historical.providerSessionId ? { providerSessionId: historical.providerSessionId } : {}),
603
- ...(historical.source ? { source: historical.source } : {}),
651
+ ...(isNative ? { source: "native" } : historical.source ? { source: historical.source } : {}),
604
652
  summary: historical.summary,
605
653
  runtimeState: historical.runtimeState,
606
654
  ...(historical.nativeIdentity ? { nativeIdentity: historical.nativeIdentity } : {}),
655
+ ...(manualHandoffProof ? { manualHandoff: true, manualHandoffProof } : {}),
607
656
  });
608
657
  ok("session.continue", {
609
658
  sessionId: continued.sessionId,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-cli",
3
- "version": "0.1.0-beta.486",
3
+ "version": "0.1.0-beta.487",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -5,7 +5,7 @@ import { publishRemoteControllerCommand } from "../relay/remote-command.js";
5
5
  import { claimCommandRequest, sendCommandResponse } from "../relay/send.js";
6
6
  import { runtimeTurnSummaryForTask } from "../session/runtime-truth.js";
7
7
  import { crewSessionRuntimeState, deliverQueuedCrewAdmission, publishRemoteCrewQueueEnqueue } from "./handler.js";
8
- import { applyConsumerQueueUpdateToStore, claimCrewQueueHead, completeMaterializedQueueAdmission, failQueuedCrewBacklog, findAdmissionByTaskAndRequest, listPendingCrewQueueNotifications, listPendingConsumerQueueEnqueues, listCrewQueueHeads, markCrewQueueNotificationAttempt, markCrewQueueNotificationApplied, releaseCrewQueueBackoff, recordAdmission, retryClaimedQueueAdmission, runtimeTurnIdForCrewTurn, updatePendingConsumerQueueEnqueue, upsertCrewQueueNotification, upsertSubscription, } from "./store.js";
8
+ import { applyConsumerQueueUpdateToStore, claimCrewQueueHead, completeMaterializedQueueAdmission, failQueuedCrewBacklog, findAdmissionByTaskAndRequest, listPendingCrewQueueNotifications, listPendingConsumerQueueEnqueues, listCrewQueueHeads, markCrewQueueNotificationAttempt, markCrewQueueNotificationApplied, markCrewQueueNotificationDeadLetter, releaseCrewQueueBackoff, recordAdmission, retryClaimedQueueAdmission, runtimeTurnIdForCrewTurn, updatePendingConsumerQueueEnqueue, upsertCrewQueueNotification, upsertSubscription, } from "./store.js";
9
9
  import { catchUpSubscriptionEvents } from "./watcher.js";
10
10
  const activeDrains = new Map();
11
11
  const pendingDrains = new Set();
@@ -16,6 +16,8 @@ const notificationRetryTimers = new Map();
16
16
  const consumerEnqueueRetryTimers = new Map();
17
17
  let notificationFlushRun;
18
18
  let notificationFlushRequested = false;
19
+ let consumerEnqueueRetryRun;
20
+ let consumerEnqueueRetryRequested = false;
19
21
  let recoveryRun;
20
22
  export async function signalCrewQueue(config, signal) {
21
23
  const key = crewQueueKey(signal);
@@ -242,6 +244,7 @@ export async function handleCrewQueueUpdateCommand(ws, config, command) {
242
244
  ownerRevision: command.ownerRevision,
243
245
  currentRevision: applied.currentRevision,
244
246
  applied: applied.applied,
247
+ status: applied.applied ? "applied" : "permanent-rejected",
245
248
  });
246
249
  if (applied.subscription) {
247
250
  void catchUpSubscriptionEvents({ config, subscription: applied.subscription }).catch((error) => {
@@ -341,6 +344,21 @@ async function completeMaterializedCrewQueueHead(config, queue) {
341
344
  return terminalRuntimeStatus(summary.status) ? "terminal" : "active";
342
345
  }
343
346
  async function retryPendingConsumerQueueEnqueues(config) {
347
+ if (consumerEnqueueRetryRun) {
348
+ consumerEnqueueRetryRequested = true;
349
+ return await consumerEnqueueRetryRun;
350
+ }
351
+ consumerEnqueueRetryRun = (async () => {
352
+ do {
353
+ consumerEnqueueRetryRequested = false;
354
+ await retryPendingConsumerQueueEnqueuesOnce(config);
355
+ } while (consumerEnqueueRetryRequested);
356
+ })().finally(() => {
357
+ consumerEnqueueRetryRun = undefined;
358
+ });
359
+ await consumerEnqueueRetryRun;
360
+ }
361
+ async function retryPendingConsumerQueueEnqueuesOnce(config) {
344
362
  const pending = await listPendingConsumerQueueEnqueues({ consumerMachineId: config.machineId });
345
363
  for (const admission of pending) {
346
364
  const key = consumerEnqueueRetryKey(admission);
@@ -611,6 +629,18 @@ async function flushCrewQueueNotificationsOnce(config) {
611
629
  expectedType: "machine:crewQueueUpdateApplied",
612
630
  timeoutMs: 15_000,
613
631
  });
632
+ if (response?.type === "machine:crewQueueUpdateApplied" && response.status === "permanent-rejected") {
633
+ await markCrewQueueNotificationDeadLetter(notification.notificationId);
634
+ await appendAudit({
635
+ machineId: config.machineId,
636
+ sessionId: notification.crewSessionId,
637
+ actor: "system",
638
+ action: "orchestration.queue.consumer_update_dead_letter",
639
+ summary: `Consumer permanently rejected queue update for ${notification.taskId}`,
640
+ evidence: { taskId: notification.taskId, dispatchRequestId: notification.dispatchRequestId, ownerRevision: notification.ownerRevision },
641
+ }).catch(() => undefined);
642
+ return;
643
+ }
614
644
  if (!crewQueueUpdateAppliedCovers(notification, response)) {
615
645
  throw new Error(`Consumer did not apply queue update revision ${notification.ownerRevision} for task ${notification.taskId}`);
616
646
  }
@@ -684,7 +714,8 @@ function crewQueueUpdateAppliedCovers(notification, response) {
684
714
  response.crewSessionId === notification.crewSessionId &&
685
715
  response.ownerRevision === notification.ownerRevision &&
686
716
  response.currentRevision >= notification.ownerRevision &&
687
- response.applied;
717
+ response.applied &&
718
+ (response.status === undefined || response.status === "applied" || response.status === "covered");
688
719
  }
689
720
  function consumerProjectionCoversUpdate(admission, command, currentRevision) {
690
721
  if (!admission)
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { decryptJson, dispatchRouteCandidates, encryptJson, filterDispatchRouteCandidates, parseMachineClientMessage, randomId } from "../../../../packages/shared/dist/index.js";
3
3
  import { appendAudit } from "../audit.js";
4
- import { sessions, sessionTurns } from "../state.js";
4
+ import { completedCommandResponses, sessions, sessionTurns } from "../state.js";
5
5
  import { admitMachineCreateSession, admitMachineFork, admitMachinePrompt } from "../relay/admission.js";
6
6
  import { publishRemoteControllerCommand, readRetainedMachineMessage, RemoteCommandError, requestRemoteMachineResult } from "../relay/remote-command.js";
7
7
  import { claimCommandRequest, sendCommandResponse } from "../relay/send.js";
@@ -11,6 +11,11 @@ import { listArtifacts } from "../artifact/store.js";
11
11
  import { ackAttention, completeClaimedQueueAdmission, crewQueueHasBacklog, deadAttention, failClaimedQueueAdmission, findAdmissionByTask, findAdmissionByTaskAndRequest, latestAttentionForTask, listAdmissions, listAttentions, recordAdmission, recordTurnRuntimeBinding, reopenUnresolvedArtifactFeedbackAttentions, retryAttention, retryClaimedQueueAdmission, runtimeTurnIdForCrewTurn, updateAdmission, updatePendingConsumerQueueEnqueue, upsertSubscription, } from "./store.js";
12
12
  import { catchUpSubscriptionEvents } from "./watcher.js";
13
13
  export async function handleDispatchCommand(ws, config, command) {
14
+ // Status/list are read projections, not mutation retries. A caller may use
15
+ // the same correlation id while the durable admission changes; never replay
16
+ // the 24h mutation response cache for these reads.
17
+ if (command.action === "status" || command.action === "list")
18
+ completedCommandResponses.delete(command.requestId);
14
19
  if (!claimCommandRequest(ws, command.requestId))
15
20
  return;
16
21
  if (command.machineId !== config.machineId) {
@@ -597,7 +602,12 @@ function validateRouteDecisionCandidate(decision, candidates) {
597
602
  };
598
603
  }
599
604
  if (candidate.availability !== "idle") {
600
- if (decision.kind === "reuse" && decision.ifBusy === "queue" && candidate.availability === "busy")
605
+ // An explicit Queue Reuse decision is a durable admission strategy. A
606
+ // remote runtime snapshot may be temporarily unknown while the owner is
607
+ // reconnecting; do not turn that observation into a route rejection or
608
+ // pretend it is idle. The owner-side enqueue will re-check runtime truth
609
+ // and either retain the inbox item or fail a permanently unavailable Crew.
610
+ if (decision.kind === "reuse" && decision.ifBusy === "queue" && (candidate.availability === "busy" || candidate.availability === "unknown"))
601
611
  return undefined;
602
612
  if (decision.kind === "fork" && candidate.availability === "unknown") {
603
613
  return {
@@ -153,6 +153,7 @@ export declare function markCrewQueueNotificationApplied(input: {
153
153
  ownerRevision: number;
154
154
  currentRevision: number;
155
155
  }): Promise<boolean>;
156
+ export declare function markCrewQueueNotificationDeadLetter(notificationId: string): Promise<boolean>;
156
157
  export declare function markCrewQueueNotificationAttempt(input: {
157
158
  notificationId: string;
158
159
  error: string;
@@ -166,6 +166,18 @@ export async function markCrewQueueNotificationApplied(input) {
166
166
  });
167
167
  return removed;
168
168
  }
169
+ export async function markCrewQueueNotificationDeadLetter(notificationId) {
170
+ let removed = false;
171
+ await updateOrchestrationStore((current) => {
172
+ const index = current.queueNotifications.findIndex((record) => record.notificationId === notificationId);
173
+ if (index < 0)
174
+ return current;
175
+ current.queueNotifications.splice(index, 1);
176
+ removed = true;
177
+ return current;
178
+ });
179
+ return removed;
180
+ }
169
181
  export async function markCrewQueueNotificationAttempt(input) {
170
182
  const now = new Date();
171
183
  const updatedAt = now.toISOString();
@@ -1,4 +1,4 @@
1
- import { decryptJson, parseControllerClientMessage, parseInternalControllerMessage, parseRelayServerWireMessage, randomId, } from "../../../../packages/shared/dist/index.js";
1
+ import { decryptJson, parseControllerClientMessage, parseInternalControllerMessage, parseMachineClientMessage, parseRelayServerWireMessage, randomId, } from "../../../../packages/shared/dist/index.js";
2
2
  import { appendAudit } from "../audit.js";
3
3
  import { drainPendingArtifactFeedbackAttentions, handleArtifactCommand } from "../artifact/handler.js";
4
4
  import { handleGatewayCommand } from "../gateway/handler.js";
@@ -21,11 +21,42 @@ import { nativeImportSessionId } from "../session/native-identity.js";
21
21
  import { encryptedSessionMetadata } from "../session/metadata.js";
22
22
  import { loadSessionProjections } from "../session/store.js";
23
23
  import { relayWsUrl } from "../relay-http.js";
24
- import { sessions, sessionTurns } from "../state.js";
24
+ import { applyRelayMachinePresence, applyRelayMachineSnapshot, reconcileRelayMachinePresence, relayMachineRegistry, sessions, sessionTurns } from "../state.js";
25
25
  import { handleDevExec, handleDiagnose } from "./devtools.js";
26
26
  import { handleInboundPromptCommand, recoverInboundPromptAdmissions } from "./inbound-prompt.js";
27
27
  import { registerMachine } from "./register.js";
28
28
  import { clearActiveRelaySocket, configureRelaySender, flushRelayOutbox, handleMachineMessageAck, handleRetainedPacketAck, handleRetainedPacketError, sendCommandResponse, setActiveRelaySocket, } from "./send.js";
29
+ async function applyMachineManifest(config, retained, presence) {
30
+ try {
31
+ const message = parseMachineClientMessage(await decryptJson(config.accountSecret, retained.encryptedPayload));
32
+ if (message.type !== "machine:register")
33
+ return;
34
+ let name = retained.updatedByNodeId;
35
+ if (message.encryptedMetadata) {
36
+ const metadata = await decryptJson(config.accountSecret, message.encryptedMetadata);
37
+ if (typeof metadata.name === "string" && metadata.name.trim())
38
+ name = metadata.name.trim();
39
+ else if (typeof metadata.hostname === "string" && metadata.hostname.trim())
40
+ name = metadata.hostname.trim();
41
+ }
42
+ const previous = relayMachineRegistry.get(retained.updatedByNodeId);
43
+ applyRelayMachineSnapshot({
44
+ id: retained.updatedByNodeId,
45
+ name,
46
+ // A retained manifest describes identity/capabilities only. Preserve
47
+ // the authoritative presence projection when no live presence record
48
+ // accompanied it; never infer online from a static retained packet.
49
+ online: presence?.online ?? previous?.online ?? false,
50
+ lastSeen: presence?.lastSeen ?? previous?.lastSeen ?? retained.updatedAt,
51
+ capabilities: message.capabilities,
52
+ encryptedMetadata: message.encryptedMetadata,
53
+ });
54
+ }
55
+ catch {
56
+ // Retained manifests are encrypted account state; malformed/unrelated
57
+ // packets must not prevent discovery of other machines.
58
+ }
59
+ }
29
60
  export async function handleServerMessage(ws, config, message) {
30
61
  if (message.type === "server:hello") {
31
62
  await registerMachine(ws, config);
@@ -186,6 +217,34 @@ async function handleRelayWireMessage(ws, config, message) {
186
217
  });
187
218
  return;
188
219
  }
220
+ if (message.type === "server:snapshot") {
221
+ for (const presence of message.presence)
222
+ applyRelayMachinePresence(presence);
223
+ reconcileRelayMachinePresence(message.presence);
224
+ for (const presence of message.presence) {
225
+ if (presence.manifest)
226
+ await applyMachineManifest(config, presence.manifest, presence);
227
+ }
228
+ for (const retained of message.retainedPackets) {
229
+ try {
230
+ const retainedMessage = parseMachineClientMessage(await decryptJson(config.accountSecret, retained.encryptedPayload));
231
+ if (retainedMessage.type === "machine:register") {
232
+ await applyMachineManifest(config, retained);
233
+ }
234
+ }
235
+ catch {
236
+ // Retained packets are encrypted per account; an unrelated/invalid
237
+ // retained entry must not prevent presence discovery for other nodes.
238
+ }
239
+ }
240
+ return;
241
+ }
242
+ if (message.type === "server:presence") {
243
+ applyRelayMachinePresence(message.presence);
244
+ if (message.presence.manifest)
245
+ await applyMachineManifest(config, message.presence.manifest, message.presence);
246
+ return;
247
+ }
189
248
  if (message.type === "server:ack") {
190
249
  if (message.packetId)
191
250
  await handleMachineMessageAck(message.packetId);
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { realpathSync } from "node:fs";
2
3
  import path from "node:path";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import { createHeadlessRuntimeClient, createHostedHeadlessRuntimeClient, ensureHeadlessRuntimeHost, resolveHeadlessRuntimeHostPaths, } from "../../../../packages/headless-runtime/dist/index.js";
@@ -44,7 +45,7 @@ export class HeadlessRuntimeManager {
44
45
  async invalidateClientForBinding(binding, cwd = this.options.defaultCwd) {
45
46
  if (!isDaemonHeadlessProvider(binding.provider))
46
47
  return;
47
- const resolvedCwd = path.resolve(cwd);
48
+ const resolvedCwd = canonicalWorkspacePath(cwd);
48
49
  const hostOptions = this.hostOptions(binding.provider, resolvedCwd, binding.profile);
49
50
  const hostPaths = resolveHeadlessRuntimeHostPaths(hostOptions);
50
51
  const key = `${binding.provider}\0${hostPaths.profile}\0${resolvedCwd}`;
@@ -66,7 +67,7 @@ export class HeadlessRuntimeManager {
66
67
  if (!this.enabledProviders().includes(normalizedProvider)) {
67
68
  throw providerConfigError(`Headless Runtime provider ${normalizedProvider} is not enabled.`, "PROVIDER_NOT_ENABLED");
68
69
  }
69
- const resolvedCwd = path.resolve(cwd);
70
+ const resolvedCwd = canonicalWorkspacePath(cwd);
70
71
  const hostOptions = this.hostOptions(normalizedProvider, resolvedCwd, profileOverride);
71
72
  const hostPaths = resolveHeadlessRuntimeHostPaths(hostOptions);
72
73
  const key = `${normalizedProvider}\0${hostPaths.profile}\0${resolvedCwd}`;
@@ -168,7 +169,7 @@ export class HeadlessRuntimeManager {
168
169
  const entry = await this.clientForProvider(provider, cwd);
169
170
  const catalog = await entry.client.listModels({ provider: entry.runtimeProvider, cwd }).catch(() => undefined);
170
171
  const base = baseProviderProfile(provider);
171
- const models = catalog?.models
172
+ const catalogModels = catalog?.models
172
173
  ?.map((model) => ({
173
174
  id: model.id,
174
175
  ...(model.label ? { label: model.label } : {}),
@@ -186,6 +187,13 @@ export class HeadlessRuntimeManager {
186
187
  : {}),
187
188
  ...(model.defaultThinkingLevel ? { defaultThinkingLevel: model.defaultThinkingLevel } : {}),
188
189
  })) ?? [];
190
+ // Some provider/app-server versions expose the effective default only as a
191
+ // scalar and omit it from the selectable array. Keep the actual default
192
+ // visible to Web/CLI without inventing a model name (for example a newer
193
+ // Astra build on a machine whose catalog is older).
194
+ const models = catalog?.defaultModel && !catalogModels.some((model) => model.id === catalog.defaultModel)
195
+ ? [...catalogModels, { id: catalog.defaultModel }]
196
+ : catalogModels;
189
197
  const thinkingLevels = catalog?.thinkingLevels
190
198
  ?.map((level) => ({
191
199
  id: level.id,
@@ -236,7 +244,34 @@ export class HeadlessRuntimeManager {
236
244
  }
237
245
  }
238
246
  function hashPath(input) {
239
- return createHash("sha256").update(path.resolve(input)).digest("hex").slice(0, 16);
247
+ return createHash("sha256").update(canonicalWorkspacePath(input)).digest("hex").slice(0, 16);
248
+ }
249
+ /**
250
+ * Workspace identity must be stable across filesystem aliases. On macOS
251
+ * `/private/tmp` and `/tmp` are the same directory; on Windows realpath also
252
+ * normalizes drive and reparse-point aliases. Keep the absolute fallback for
253
+ * a not-yet-created leaf, while canonicalizing the existing parent.
254
+ */
255
+ function canonicalWorkspacePath(input) {
256
+ const resolved = path.resolve(input);
257
+ try {
258
+ return realpathSync.native(resolved);
259
+ }
260
+ catch {
261
+ let current = resolved;
262
+ const suffix = [];
263
+ while (current !== path.dirname(current)) {
264
+ try {
265
+ const canonical = realpathSync.native(current);
266
+ return path.join(canonical, ...suffix.reverse());
267
+ }
268
+ catch {
269
+ suffix.push(path.basename(current));
270
+ current = path.dirname(current);
271
+ }
272
+ }
273
+ return resolved;
274
+ }
240
275
  }
241
276
  function headlessRuntimeProviderForDaemon(provider) {
242
277
  return provider === "pie" ? "command" : provider;
@@ -37,8 +37,19 @@ export async function listHeadlessHistoricalSessions(input) {
37
37
  const cursorBuffer = effectiveCursor?.buffer
38
38
  ? await boundedCursorBuffer(effectiveCursor.buffer, cwd, input.agent, input.includeArchived, providers)
39
39
  : [];
40
- if (providers.length === 0)
40
+ if (providers.length === 0) {
41
+ if (input.agent) {
42
+ return {
43
+ sessions: [],
44
+ warnings: [{
45
+ provider: input.agent,
46
+ code: "unconfigured",
47
+ message: `Provider ${input.agent} is not enabled on this daemon; no native history was queried.`,
48
+ }],
49
+ };
50
+ }
41
51
  return { sessions: [] };
52
+ }
42
53
  const states = new Map();
43
54
  const warnings = [];
44
55
  for (const provider of providers) {
@@ -230,6 +241,12 @@ function historicalSessionFromThread(thread, provider) {
230
241
  updatedAt: thread.updatedAt,
231
242
  runtimeState: thread.archived ? "archived" : "available",
232
243
  ...(thread.name ? { name: thread.name, runtimeSessionName: thread.name } : { runtimeSessionName: thread.id }),
244
+ ...(typeof thread.providerMetadata?.nativeIdentity === "string" && thread.providerMetadata.nativeIdentity.trim()
245
+ ? { nativeIdentity: thread.providerMetadata.nativeIdentity.trim() }
246
+ : {}),
247
+ ...(typeof thread.providerMetadata?.nativeHistoryVersion === "string" && thread.providerMetadata.nativeHistoryVersion.trim()
248
+ ? { nativeHistoryVersion: thread.providerMetadata.nativeHistoryVersion.trim() }
249
+ : {}),
233
250
  };
234
251
  }
235
252
  function nativeHistoricalSessionFromSummary(session, provider, profile, nativeDataRootFingerprint, machineId) {
@@ -446,6 +463,8 @@ function bufferedSessionFromUnknown(value) {
446
463
  ...(typeof record.runtimeSessionName === "string" ? { runtimeSessionName: record.runtimeSessionName } : {}),
447
464
  ...(record.historyStatus === "available" || record.historyStatus === "unsupported" || record.historyStatus === "unconfigured" || record.historyStatus === "permission-denied" ? { historyStatus: record.historyStatus } : {}),
448
465
  ...(typeof record.capabilityMessage === "string" ? { capabilityMessage: record.capabilityMessage } : {}),
466
+ ...(typeof record.nativeIdentity === "string" ? { nativeIdentity: record.nativeIdentity } : {}),
467
+ ...(typeof record.nativeHistoryVersion === "string" ? { nativeHistoryVersion: record.nativeHistoryVersion } : {}),
449
468
  };
450
469
  }
451
470
  async function boundedCursorBuffer(buffer, cwd, requestedAgent, includeArchived, providers) {
@@ -195,7 +195,7 @@ export async function artifactList(input) {
195
195
  ...(stringArg(input.args, "taskId") ? { taskId: stringArg(input.args, "taskId") } : {}),
196
196
  };
197
197
  const message = machineId === input.config.machineId
198
- ? await runMachineHandler(input.config, requestId, async (ws) => await handleArtifactCommand(ws, input.config, { ...command, type: "machine:artifact" }))
198
+ ? await runMachineHandler(input.config, requestId, async (ws) => await handleArtifactCommand(ws, input.config, { ...command, type: "machine:artifact" }), { cacheResponse: false })
199
199
  : await requestRemoteMachineResult(input.config, { targetMachineId: machineId, expectedType: "machine:artifactResult", command, timeoutMs: 20_000 }).catch((error) => ({
200
200
  type: "machine:error",
201
201
  requestId,
@@ -211,8 +211,9 @@ export async function artifactStatus(input) {
211
211
  const artifactId = stringArg(input.args, "artifactId");
212
212
  if (!artifactId)
213
213
  return missingArg("artifact.status", "artifactId");
214
- const localProjection = await localArtifactStatus({ artifactId });
215
- const producerMachineId = stringArg(input.args, "producerMachineId") ?? stringArg(input.args, "machineId") ?? localProjection?.producerMachineId ?? input.config.machineId;
214
+ const requestedProducerMachineId = stringArg(input.args, "producerMachineId") ?? stringArg(input.args, "machineId");
215
+ const localProjection = await localArtifactStatus({ artifactId, ...(requestedProducerMachineId ? { producerMachineId: requestedProducerMachineId } : {}) });
216
+ const producerMachineId = requestedProducerMachineId ?? localProjection?.producerMachineId ?? input.config.machineId;
216
217
  const requestId = readArtifactRequestId("artifact.status", input.args);
217
218
  const command = {
218
219
  type: "controller:artifact",
@@ -223,7 +224,7 @@ export async function artifactStatus(input) {
223
224
  artifactId,
224
225
  };
225
226
  const message = producerMachineId === input.config.machineId
226
- ? await runMachineHandler(input.config, requestId, async (ws) => await handleArtifactCommand(ws, input.config, { ...command, type: "machine:artifact" }))
227
+ ? await runMachineHandler(input.config, requestId, async (ws) => await handleArtifactCommand(ws, input.config, { ...command, type: "machine:artifact" }), { cacheResponse: false })
227
228
  : await requestRemoteMachineResult(input.config, { targetMachineId: producerMachineId, expectedType: "machine:artifactResult", command, timeoutMs: 20_000 }).catch((error) => ({
228
229
  type: "machine:error",
229
230
  requestId,
@@ -239,8 +240,9 @@ export async function artifactFeedbackList(input) {
239
240
  const artifactId = stringArg(input.args, "artifactId");
240
241
  if (!artifactId)
241
242
  return missingArg("artifact.feedback.list", "artifactId");
242
- const localProjection = await localArtifactStatus({ artifactId });
243
- const producerMachineId = stringArg(input.args, "producerMachineId") ?? stringArg(input.args, "machineId") ?? localProjection?.producerMachineId ?? input.config.machineId;
243
+ const requestedProducerMachineId = stringArg(input.args, "producerMachineId") ?? stringArg(input.args, "machineId");
244
+ const localProjection = await localArtifactStatus({ artifactId, ...(requestedProducerMachineId ? { producerMachineId: requestedProducerMachineId } : {}) });
245
+ const producerMachineId = requestedProducerMachineId ?? localProjection?.producerMachineId ?? input.config.machineId;
244
246
  const requestId = readArtifactRequestId("artifact.feedback.list", input.args);
245
247
  const command = {
246
248
  type: "controller:artifact",
@@ -252,7 +254,7 @@ export async function artifactFeedbackList(input) {
252
254
  ...(stringArg(input.args, "after") ? { after: stringArg(input.args, "after") } : {}),
253
255
  };
254
256
  const message = producerMachineId === input.config.machineId
255
- ? await runMachineHandler(input.config, requestId, async (ws) => await handleArtifactCommand(ws, input.config, { ...command, type: "machine:artifact" }))
257
+ ? await runMachineHandler(input.config, requestId, async (ws) => await handleArtifactCommand(ws, input.config, { ...command, type: "machine:artifact" }), { cacheResponse: false })
256
258
  : await requestRemoteMachineResult(input.config, { targetMachineId: producerMachineId, expectedType: "machine:artifactResult", command, timeoutMs: 20_000 }).catch((error) => ({
257
259
  type: "machine:error",
258
260
  requestId,