@dougbots/avenor-core 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -145,6 +145,7 @@ interface WorkflowControllerCreateParams {
145
145
  max_inflight: number;
146
146
  }
147
147
  interface MessageSendParams {
148
+ expected_sentinel_file?: string;
148
149
  to_run_id: string;
149
150
  message_id: string;
150
151
  generation: string;
@@ -317,7 +318,6 @@ declare class Client {
317
318
  events(options?: SubscribeOptions): AsyncIterableIterator<Event>;
318
319
  history(options: HistoryOptions): Promise<HistoryResult>;
319
320
  status(runtimeId?: string): Promise<Record<string, unknown>>;
320
- result(runtimeId?: string): Promise<Record<string, unknown>>;
321
321
  list(): Promise<Array<Record<string, unknown>>>;
322
322
  spawn(params: SpawnParams): Promise<SpawnResult>;
323
323
  shutdown(mode: string): Promise<void>;
@@ -356,7 +356,9 @@ declare class Client {
356
356
  messagePreflight(): Promise<MessagePreflightResult>;
357
357
  resume(params: ResumeParams): Promise<ResumeResult>;
358
358
  sessionRename(sessionId: string, name: string): Promise<SessionRenameResult>;
359
- sessionLast(runtimeId: string): Promise<SessionLastResult>;
359
+ sessionLast(runtimeId: string, expectedSentinelFile?: string): Promise<SessionLastResult>;
360
+ /** The complete terminal reply for a runtime, with the sentinel that turn currently owns. */
361
+ result(runtimeId: string): Promise<Record<string, unknown>>;
360
362
  sessionLastSet(runtimeId: string, finalOutput: string, stopReason: string, truncated: boolean): Promise<void>;
361
363
  workflowCancel(workflowId: string, actor: string, reason: string): Promise<Record<string, unknown>>;
362
364
  isClosed(): boolean;
@@ -523,7 +525,12 @@ interface RunSnapshot {
523
525
  }
524
526
  declare function createRunSnapshot(limits?: Partial<RunReducerLimits>): RunSnapshot;
525
527
  declare function reduceRunSnapshot(current: RunSnapshot, input: unknown, limits?: Partial<RunReducerLimits>): RunSnapshot;
526
- type RunSnapshotSeed = Partial<Omit<RunSnapshot, '_seen_seq_by_scope' | '_recent_legacy_keys' | '_last_semantic_fingerprint' | '_last_source_event'>>;
528
+ /**
529
+ * A snapshot as the inspect surface and an observer publish it: the reducer's
530
+ * dedupe bookkeeping stays internal to `RunReducer`.
531
+ */
532
+ type RunSnapshotView = Omit<RunSnapshot, '_seen_seq_by_scope' | '_recent_legacy_keys' | '_last_semantic_fingerprint' | '_last_source_event'>;
533
+ type RunSnapshotSeed = Partial<RunSnapshotView>;
527
534
  declare class RunReducer {
528
535
  private state;
529
536
  constructor(limits?: Partial<RunReducerLimits>, initial?: RunSnapshotSeed);
@@ -625,20 +632,46 @@ declare class Supervisor {
625
632
  registerResumed(run: RunInfo): void;
626
633
  /** The one place a run's ID and label are registered, for spawn and for resume. */
627
634
  private register;
628
- /** Match a stored run by its broker runtime ID. */
635
+ /** The stored runs that carry a broker runtime ID. */
636
+ getRunsByRuntimeId(runtimeId: string): RunInfo[];
637
+ /**
638
+ * Match a stored run by its broker runtime ID, returning the entry only
639
+ * when exactly one run carries the id. Several registered turns can share
640
+ * a runtime (a warm resume, or a restart that re-minted one while the
641
+ * pre-restart entry stays registered); the current turn is identified by
642
+ * the sentinel the runtime holds, so a shared id is disambiguated with
643
+ * getRunByRuntimeTurn rather than by whichever entry was inserted last.
644
+ */
629
645
  getRunByRuntimeId(runtimeId: string): RunInfo | undefined;
646
+ /**
647
+ * Return the entry for (runtimeId, sentinelPath) only when exactly one
648
+ * stored run carries both. A sentinel no entry carries, or one carried by
649
+ * several entries, is claimed by no turn.
650
+ */
651
+ getRunByRuntimeTurn(runtimeId: string, sentinelPath: string): RunInfo | undefined;
630
652
  /**
631
653
  * The live singleton when it owns supervisorId, null otherwise. Single
632
654
  * source of truth for the ownership check, so callers cannot observe
633
655
  * isCurrentInstance true while currentInstance is null.
634
656
  */
657
+ /**
658
+ * Build an instance that owns socketPath without spawning a supervisor
659
+ * process, and adopt it as the live singleton. `start` is the production
660
+ * writer of both; this exists so a test can drive the in-process plane
661
+ * without forging private state, and hands back the previous singleton so
662
+ * the caller can restore it.
663
+ */
664
+ static adoptTestInstance(binaryPath: string, socketPath: string): {
665
+ supervisor: Supervisor;
666
+ restore: () => void;
667
+ };
635
668
  static getInstance(supervisorId: string): Supervisor | null;
636
669
  static isCurrentInstance(supervisorId: string): boolean;
637
670
  static currentInstance(): Supervisor | null;
638
671
  /** Iterate stored runs without exposing the internal map. */
639
672
  forEachRun(callback: (info: RunInfo) => void): void;
640
673
  getClient(): Client;
641
- spawn(params: SpawnParams, runId?: `${string}-${string}-${string}-${string}-${string}`, metadata?: SpawnMetadata): Promise<RunInfo>;
674
+ spawn(params: SpawnParams, runId?: string, metadata?: SpawnMetadata): Promise<RunInfo>;
642
675
  close(options?: {
643
676
  skipShutdown?: boolean;
644
677
  }): Promise<void>;
@@ -749,6 +782,7 @@ interface StatusResult {
749
782
  latest_seq?: number;
750
783
  final_output?: string;
751
784
  final_output_truncated?: boolean;
785
+ runtime_output_available?: boolean;
752
786
  }
753
787
  declare const statusTool: (args: StatusToolArgs) => Promise<StatusResult | StatusListResult>;
754
788
  //#endregion
@@ -768,6 +802,11 @@ interface EventsToolResult {
768
802
  [key: string]: unknown;
769
803
  }>;
770
804
  }
805
+ /**
806
+ * Stream the parsed records of one NDJSON event log. Malformed lines are
807
+ * dropped; an unreadable log rejects, so a caller that cannot read the log
808
+ * never sees a silent empty tail.
809
+ */
771
810
  declare const eventsTool: (args: EventsToolArgs) => Promise<EventsToolResult>;
772
811
  //#endregion
773
812
  //#region src/tools/inspect.d.ts
@@ -781,7 +820,7 @@ interface InspectResult {
781
820
  run_id: string;
782
821
  label: string;
783
822
  status: StatusResult;
784
- snapshot: Omit<RunSnapshot, '_seen_seq_by_scope' | '_recent_legacy_keys' | '_last_semantic_fingerprint' | '_last_source_event'>;
823
+ snapshot: RunSnapshotView;
785
824
  transcript: RunSnapshot['transcript'];
786
825
  tools: RunSnapshot['tools'];
787
826
  live_tools: RunSnapshot['live_tools'];
@@ -967,9 +1006,9 @@ interface MessageProvider {
967
1006
  surface(): MessageSurface;
968
1007
  role(): string;
969
1008
  resolve(ref: string): Promise<Resolution>;
970
- state(runId: string): Promise<RunFacts>;
971
- tell(targetRunId: string, message: string): Promise<ProviderResult>;
972
- ask(targetRunId: string, message: string, timeoutMs?: number, signal?: AbortSignal): Promise<ProviderResult>;
1009
+ state(runId: string, publicRunId?: string): Promise<RunFacts>;
1010
+ tell(targetRunId: string, message: string, publicRunId?: string): Promise<ProviderResult>;
1011
+ ask(targetRunId: string, message: string, timeoutMs?: number, signal?: AbortSignal, publicRunId?: string): Promise<ProviderResult>;
973
1012
  reply(inboundAskId: string, message: string, askerRunId: string): Promise<ProviderResult>;
974
1013
  withdraw(messageId: string): Promise<ProviderResult>;
975
1014
  controlReply?(target: string, payload: unknown): Promise<ProviderResult>;
@@ -984,7 +1023,7 @@ interface MessageProvider {
984
1023
  sessionId?: string;
985
1024
  name: string;
986
1025
  }): Promise<ProviderResult>;
987
- last(runId: string): Promise<ProviderResult>;
1026
+ last(runId: string, publicRunId?: string): Promise<ProviderResult>;
988
1027
  peers(): Promise<PeersProviderResult>;
989
1028
  /** Host surfaces that own a turn loop publish their own finished turn. */
990
1029
  recordLastTurn?(text: string, stopReason: string, truncated: boolean): Promise<void>;
@@ -1098,6 +1137,7 @@ interface MessageClient {
1098
1137
  generation: string;
1099
1138
  body: string;
1100
1139
  no_reply?: boolean;
1140
+ expected_sentinel_file?: string;
1101
1141
  }): Promise<MessageSendResult>;
1102
1142
  messageWait(params: {
1103
1143
  message_id: string;
@@ -1127,7 +1167,7 @@ interface MessageClient {
1127
1167
  on_event?: string;
1128
1168
  }): Promise<ResumeResult>;
1129
1169
  sessionRename(sessionId: string, name: string): Promise<SessionRenameResult>;
1130
- sessionLast(runtimeId: string): Promise<SessionLastResult>;
1170
+ sessionLast(runtimeId: string, expectedSentinelFile?: string): Promise<SessionLastResult>;
1131
1171
  sessionLastSet(runtimeId: string, finalOutput: string, stopReason: string, truncated: boolean): Promise<void>;
1132
1172
  status(runtimeId?: string): Promise<Record<string, unknown>>;
1133
1173
  }
@@ -1200,10 +1240,14 @@ declare function createChildMessageProvider(options: ChildProviderOptions): Mess
1200
1240
  //#region src/tools/peers.d.ts
1201
1241
  interface PeersToolArgs extends PeersInput {}
1202
1242
  type PeersResult = PeersRouterResult;
1203
- /** The one avenor_peers handler; it resolves and reads through the bound provider. */
1204
- declare function peersTool(args: PeersToolArgs, provider: MessageProvider): Promise<PeersResult>;
1243
+ /** The one avenor_peers handler; it resolves and reads through the bound provider.
1244
+ *
1245
+ * Admission belongs to the shared router for every surface, so the argument is
1246
+ * the untyped JSON a tool call carries and `executePeers` validates it first —
1247
+ * the same shape `messageTool` takes. */
1248
+ declare function peersTool(args: unknown, provider: MessageProvider): Promise<PeersResult>;
1205
1249
  /** Bind the peers handler to one provider. */
1206
- declare function createPeersTool(provider: MessageProvider): (args: PeersToolArgs) => Promise<PeersResult>;
1250
+ declare function createPeersTool(provider: MessageProvider): (args: unknown) => Promise<PeersResult>;
1207
1251
  //#endregion
1208
1252
  //#region src/tools/kill.d.ts
1209
1253
  interface KillToolArgs {
@@ -1330,4 +1374,4 @@ type WorkflowCancelResult = Record<string, unknown>;
1330
1374
  declare function createWorkflowCancelTool(getSupervisorClient: GetSupervisorClient): (args: WorkflowCancelArgs) => Promise<WorkflowCancelResult>;
1331
1375
  declare const workflowCancelTool: (args: WorkflowCancelArgs) => Promise<WorkflowCancelResult>;
1332
1376
  //#endregion
1333
- export { type AskClaims, type BrokerReceiveArgs, type BrokerReceiveResult, type ChildBroker, type Client, ClosedSocketError, DEFAULT_RUN_REDUCER_LIMITS, type Event, type ExecutionIdentity, ForeignPeerError, type HistoryOptions, type HistoryResult, type InboundAsk, type InspectResult, type KillResult, type KillToolArgs, MAX_FINAL_OUTPUT_RUNES, MAX_WAIT_MS, MESSAGE_OUTCOMES, type MessageCallerKind, type MessageClient, type MessageInput, type MessageOutcome, type MessageProvider, type MessageSurface, type NormalizedRunEvent, type ObserveRunOptions, type OwnerAttachOptions, OwnerAuthError, OwnerRecordMissingError, type PeerRecord, type PeersInput, type PeersProviderResult, type PeersResult, type PeersRouterResult, type PeersToolArgs, type PendingAsk, type ProviderResult, type Resolution, type ResultResult, type ResultToolArgs, RpcError, type RunChunkRole, type RunFacts, type RunIdentity, type RunInfo, type RunObserver, type RunObserverClient, type RunPermissionOption, type RunPermissionSummary, RunReducer, type RunReducerLimits, type RunSnapshot, type RunSnapshotSeed, type RunToolState, type RunToolSummary, type RunTranscriptEntry, SUPERVISOR_RUN_ID, type SpawnMetadata, type SpawnParams, type SpawnResult, type SpawnSelectionInput, type SpawnToolArgs, type SpawnToolResult, StaleOwnerRecordError, type StatusListResult, type StatusResult, type StatusToolArgs, type StatusView, type SubscribeOptions, Supervisor, type SupervisorClientOptions, type SupervisorOptions, THINKING_LEVELS, type ThinkingLevel, type ThinkingOutcome, ToolError, type WorkflowCancelArgs, type WorkflowCancelResult, type WorkflowCompleteParams, type WorkflowCompleteResult, type WorkflowCompleteToolArgs, type WorkflowControllerCreateParams, type WorkflowControllerStatusResult, type WorkflowControllerStatusToolArgs, type WorkflowEventsResult, type WorkflowEventsToolArgs, type WorkflowGateOperation, type WorkflowGateParams, type WorkflowGateResult, type WorkflowGateToolArgs, type WorkflowHeartbeatParams, type WorkflowInspectResult, type WorkflowInspectToolArgs, type WorkflowReadyResult, type WorkflowReadyToolArgs, type WorkflowStatusResult, type WorkflowStatusToolArgs, type WorkflowWaitResult, type WorkflowWaitToolArgs, answerPermissionTool, assertMessageInput, assertPeersInput, attachOwner, boundFinalOutput, brokerReceiveTool, createChildMessageProvider, createHostMessageProvider, createMessageTool, createPeersTool, createRunSnapshot, createWorkflowCancelTool, createWorkflowCompleteTool, createWorkflowControllerStatusTool, createWorkflowEventsTool, createWorkflowGateTool, createWorkflowInspectTool, createWorkflowReadyTool, createWorkflowStatusTool, createWorkflowWaitTool, dial, evaluateThinkingPolicy, eventsTool, executeMessage, executePeers, extractEventText, extractReplyMessage, extractTurnText, findAvenorBinary, getInstallDir, getSupervisorClient, getVersion, inspectTool, installAskClaims, installerBinaryPath, isClosedSocketError, isGenerationMismatchError, isThinkingLevel, killTool, messageTool, newMsgID, normalizeAskTimeoutMs, normalizeRunEvent, normalizeTurnStopReason, observeRun, peersDetails, peersTool, quote, quoteList, reduceRunSnapshot, renderAskFailure, renderLast, renderOutcome, renderPeerList, repair, resultOutput, resultTool, sanitize, shutdownTool, socketsRoot, spawnTool, statusTool, validateSpawnSelection, validateThinking, validateThinkingForBackend, validateThinkingForBackendResume, workflowCancelTool, workflowCompleteTool, workflowControllerStatusTool, workflowEventsTool, workflowGateTool, workflowInspectTool, workflowReadyTool, workflowStatusTool, workflowWaitTool };
1377
+ export { type AskClaims, type BrokerReceiveArgs, type BrokerReceiveResult, type ChildBroker, type Client, ClosedSocketError, DEFAULT_RUN_REDUCER_LIMITS, type Event, type ExecutionIdentity, ForeignPeerError, type HistoryOptions, type HistoryResult, type InboundAsk, type InspectResult, type KillResult, type KillToolArgs, MAX_FINAL_OUTPUT_RUNES, MAX_WAIT_MS, MESSAGE_OUTCOMES, type MessageCallerKind, type MessageClient, type MessageInput, type MessageOutcome, type MessageProvider, type MessageSurface, type NormalizedRunEvent, type ObserveRunOptions, type OwnerAttachOptions, OwnerAuthError, OwnerRecordMissingError, type PeerRecord, type PeersInput, type PeersProviderResult, type PeersResult, type PeersRouterResult, type PeersToolArgs, type PendingAsk, type ProviderResult, type Resolution, type ResultResult, type ResultToolArgs, RpcError, type RunChunkRole, type RunFacts, type RunIdentity, type RunInfo, type RunObserver, type RunObserverClient, type RunPermissionOption, type RunPermissionSummary, RunReducer, type RunReducerLimits, type RunSnapshot, type RunSnapshotSeed, type RunSnapshotView, type RunToolState, type RunToolSummary, type RunTranscriptEntry, SUPERVISOR_RUN_ID, type SpawnMetadata, type SpawnParams, type SpawnResult, type SpawnSelectionInput, type SpawnToolArgs, type SpawnToolResult, StaleOwnerRecordError, type StatusListResult, type StatusResult, type StatusToolArgs, type StatusView, type SubscribeOptions, Supervisor, type SupervisorClientOptions, type SupervisorOptions, THINKING_LEVELS, type ThinkingLevel, type ThinkingOutcome, ToolError, type WorkflowCancelArgs, type WorkflowCancelResult, type WorkflowCompleteParams, type WorkflowCompleteResult, type WorkflowCompleteToolArgs, type WorkflowControllerCreateParams, type WorkflowControllerStatusResult, type WorkflowControllerStatusToolArgs, type WorkflowEventsResult, type WorkflowEventsToolArgs, type WorkflowGateOperation, type WorkflowGateParams, type WorkflowGateResult, type WorkflowGateToolArgs, type WorkflowHeartbeatParams, type WorkflowInspectResult, type WorkflowInspectToolArgs, type WorkflowReadyResult, type WorkflowReadyToolArgs, type WorkflowStatusResult, type WorkflowStatusToolArgs, type WorkflowWaitResult, type WorkflowWaitToolArgs, answerPermissionTool, assertMessageInput, assertPeersInput, attachOwner, boundFinalOutput, brokerReceiveTool, createChildMessageProvider, createHostMessageProvider, createMessageTool, createPeersTool, createRunSnapshot, createWorkflowCancelTool, createWorkflowCompleteTool, createWorkflowControllerStatusTool, createWorkflowEventsTool, createWorkflowGateTool, createWorkflowInspectTool, createWorkflowReadyTool, createWorkflowStatusTool, createWorkflowWaitTool, dial, evaluateThinkingPolicy, eventsTool, executeMessage, executePeers, extractEventText, extractReplyMessage, extractTurnText, findAvenorBinary, getInstallDir, getSupervisorClient, getVersion, inspectTool, installAskClaims, installerBinaryPath, isClosedSocketError, isGenerationMismatchError, isThinkingLevel, killTool, messageTool, newMsgID, normalizeAskTimeoutMs, normalizeRunEvent, normalizeTurnStopReason, observeRun, peersDetails, peersTool, quote, quoteList, reduceRunSnapshot, renderAskFailure, renderLast, renderOutcome, renderPeerList, repair, resultOutput, resultTool, sanitize, shutdownTool, socketsRoot, spawnTool, statusTool, validateSpawnSelection, validateThinking, validateThinkingForBackend, validateThinkingForBackendResume, workflowCancelTool, workflowCompleteTool, workflowControllerStatusTool, workflowEventsTool, workflowGateTool, workflowInspectTool, workflowReadyTool, workflowStatusTool, workflowWaitTool };
package/dist/index.js CHANGED
@@ -368,10 +368,6 @@ var Client = class {
368
368
  const params = runtimeId ? { runtime_id: runtimeId } : void 0;
369
369
  return this.call("status", params);
370
370
  }
371
- async result(runtimeId) {
372
- const params = runtimeId ? { runtime_id: runtimeId } : void 0;
373
- return this.call("result", params);
374
- }
375
371
  async list() {
376
372
  return this.call("list");
377
373
  }
@@ -504,7 +500,7 @@ var Client = class {
504
500
  });
505
501
  }
506
502
  async messageSend(params) {
507
- return this.call("message_send", params);
503
+ return this.call(params.expected_sentinel_file ? "message_send_turn" : "message_send", params);
508
504
  }
509
505
  async messageWait(params) {
510
506
  const requested = params.timeout_ms !== void 0 && params.timeout_ms > 0 ? params.timeout_ms : MESSAGE_MAX_WAIT_MS;
@@ -535,8 +531,15 @@ var Client = class {
535
531
  name
536
532
  });
537
533
  }
538
- async sessionLast(runtimeId) {
539
- return this.call("session_last", { runtime_id: runtimeId });
534
+ async sessionLast(runtimeId, expectedSentinelFile) {
535
+ return this.call(expectedSentinelFile ? "session_last_turn" : "session_last", {
536
+ runtime_id: runtimeId,
537
+ ...expectedSentinelFile ? { expected_sentinel_file: expectedSentinelFile } : {}
538
+ });
539
+ }
540
+ /** The complete terminal reply for a runtime, with the sentinel that turn currently owns. */
541
+ async result(runtimeId) {
542
+ return this.call("result", { runtime_id: runtimeId });
540
543
  }
541
544
  async sessionLastSet(runtimeId, finalOutput, stopReason, truncated) {
542
545
  await this.call("session_last_set", {
@@ -2149,16 +2152,56 @@ var Supervisor = class Supervisor {
2149
2152
  this.runs.set(run.runId, run);
2150
2153
  this.aliases.set(run.label, run);
2151
2154
  }
2152
- /** Match a stored run by its broker runtime ID. */
2155
+ /** The stored runs that carry a broker runtime ID. */
2156
+ getRunsByRuntimeId(runtimeId) {
2157
+ if (!runtimeId) return [];
2158
+ return [...this.runs.values()].filter((info) => info.runtimeId === runtimeId);
2159
+ }
2160
+ /**
2161
+ * Match a stored run by its broker runtime ID, returning the entry only
2162
+ * when exactly one run carries the id. Several registered turns can share
2163
+ * a runtime (a warm resume, or a restart that re-minted one while the
2164
+ * pre-restart entry stays registered); the current turn is identified by
2165
+ * the sentinel the runtime holds, so a shared id is disambiguated with
2166
+ * getRunByRuntimeTurn rather than by whichever entry was inserted last.
2167
+ */
2153
2168
  getRunByRuntimeId(runtimeId) {
2154
- if (!runtimeId) return void 0;
2155
- for (const runInfo of this.runs.values()) if (runInfo.runtimeId === runtimeId) return runInfo;
2169
+ const matches = this.getRunsByRuntimeId(runtimeId);
2170
+ return matches.length === 1 ? matches[0] : void 0;
2171
+ }
2172
+ /**
2173
+ * Return the entry for (runtimeId, sentinelPath) only when exactly one
2174
+ * stored run carries both. A sentinel no entry carries, or one carried by
2175
+ * several entries, is claimed by no turn.
2176
+ */
2177
+ getRunByRuntimeTurn(runtimeId, sentinelPath) {
2178
+ if (!runtimeId || !sentinelPath) return void 0;
2179
+ const matches = this.getRunsByRuntimeId(runtimeId).filter((info) => info.sentinelPath === sentinelPath);
2180
+ return matches.length === 1 ? matches[0] : void 0;
2156
2181
  }
2157
2182
  /**
2158
2183
  * The live singleton when it owns supervisorId, null otherwise. Single
2159
2184
  * source of truth for the ownership check, so callers cannot observe
2160
2185
  * isCurrentInstance true while currentInstance is null.
2161
2186
  */
2187
+ /**
2188
+ * Build an instance that owns socketPath without spawning a supervisor
2189
+ * process, and adopt it as the live singleton. `start` is the production
2190
+ * writer of both; this exists so a test can drive the in-process plane
2191
+ * without forging private state, and hands back the previous singleton so
2192
+ * the caller can restore it.
2193
+ */
2194
+ static adoptTestInstance(binaryPath, socketPath) {
2195
+ const previous = Supervisor.instance;
2196
+ const supervisor = new Supervisor(binaryPath, socketPath);
2197
+ Supervisor.instance = supervisor;
2198
+ return {
2199
+ supervisor,
2200
+ restore: () => {
2201
+ Supervisor.instance = previous;
2202
+ }
2203
+ };
2204
+ }
2162
2205
  static getInstance(supervisorId) {
2163
2206
  if (Supervisor.instance === null) return null;
2164
2207
  if (Supervisor.instance.supervisorId === supervisorId) return Supervisor.instance;
@@ -2637,7 +2680,7 @@ var thinking_policy_umpire_default = {
2637
2680
  {
2638
2681
  "op": "condIn",
2639
2682
  "condition": "backend",
2640
- "values": ["claude", "claude-channel"]
2683
+ "values": ["claude"]
2641
2684
  },
2642
2685
  {
2643
2686
  "op": "condEq",
@@ -2657,7 +2700,7 @@ var thinking_policy_umpire_default = {
2657
2700
  }
2658
2701
  ]
2659
2702
  },
2660
- "reason": "claude and claude-channel support low through max only on a new session"
2703
+ "reason": "claude supports low through max only on a new session"
2661
2704
  }]
2662
2705
  }
2663
2706
  }]
@@ -2781,6 +2824,55 @@ function validateThinkingForBackendResume(backend, value) {
2781
2824
  throw new Error(`backend ${JSON.stringify(backend)} does not support parameter "thinking"`);
2782
2825
  }
2783
2826
  //#endregion
2827
+ //#region src/tools/sentinel-status.ts
2828
+ /**
2829
+ * Sentinel statuses that settle the provider session itself, so the recorded
2830
+ * session id is a loadable transcript. TIMEOUT and KILLED are supervisor-imposed
2831
+ * terminations (time budget, cancellation) where an automatic continuation would
2832
+ * surprise. Matches the Go `readSentinelSession`.
2833
+ */
2834
+ const RESUMABLE_SENTINEL_STATUSES = new Set([
2835
+ "DONE",
2836
+ "FAILED",
2837
+ "BLOCKED"
2838
+ ]);
2839
+ /**
2840
+ * The terminal sentinel statuses the provider's state fallback answers from,
2841
+ * matching the Go `terminalStatusFromRunInfo` set: a run that settled before
2842
+ * a supervisor restart keeps its outcome discoverable by run id.
2843
+ */
2844
+ const TERMINAL_SENTINEL_STATUSES = new Set([
2845
+ "DONE",
2846
+ "FAILED",
2847
+ "BLOCKED",
2848
+ "TIMEOUT",
2849
+ "KILLED"
2850
+ ]);
2851
+ /**
2852
+ * The result word a sentinel status settles on. Both the status surface and the
2853
+ * message provider's state fallback answer from this one mapping, so the same
2854
+ * sentinel file cannot classify differently between them. BLOCKED reads as
2855
+ * failed: it names why the run stopped, not a result of its own.
2856
+ */
2857
+ function sentinelStatusToResult(sentinelStatus) {
2858
+ switch (sentinelStatus) {
2859
+ case "DONE": return "done";
2860
+ case "FAILED": return "failed";
2861
+ case "TIMEOUT": return "timeout";
2862
+ case "KILLED": return "killed";
2863
+ case "BLOCKED": return "failed";
2864
+ default: return "running";
2865
+ }
2866
+ }
2867
+ /**
2868
+ * Whether a terminal sentinel leaves a session worth resuming. Mirrors the only
2869
+ * hit Go's `runFactsFromStatus` reaches for a registry entry: a non-empty
2870
+ * session id. The sentinel word never decides this.
2871
+ */
2872
+ function resumableFromSession(sessionId) {
2873
+ return typeof sessionId === "string" && sessionId !== "";
2874
+ }
2875
+ //#endregion
2784
2876
  //#region src/tools/run-resolution.ts
2785
2877
  /** Resolve a local public run ID before considering a label alias. */
2786
2878
  function findLocalRunByReference(sup, reference) {
@@ -2821,16 +2913,6 @@ async function sentinelExists(filePath) {
2821
2913
  function rawStatusPhase(source) {
2822
2914
  return stringField(source ?? {}, "phase") || stringField(source ?? {}, "status") || "running";
2823
2915
  }
2824
- function sentinelStatusToResult(sentinelStatus) {
2825
- switch (sentinelStatus) {
2826
- case "DONE": return "done";
2827
- case "FAILED": return "failed";
2828
- case "TIMEOUT": return "timeout";
2829
- case "KILLED": return "killed";
2830
- case "BLOCKED": return "failed";
2831
- default: return "running";
2832
- }
2833
- }
2834
2916
  function translateStatus(rawPhase, sentinelData) {
2835
2917
  const sentinelStatus = sentinelData?._status;
2836
2918
  const phase = rawPhase.toLowerCase();
@@ -2933,16 +3015,19 @@ function shapeStatusResult(result, view = "full") {
2933
3015
  };
2934
3016
  }
2935
3017
  async function buildRunStatus(runInfo, liveStatus) {
2936
- if (liveStatus) retainLiveIdentity(runInfo, liveStatus);
3018
+ const liveSentinel = stringField(liveStatus ?? {}, "sentinel_file");
3019
+ const belongsToRun = !liveSentinel || liveSentinel === runInfo.sentinelPath;
3020
+ if (liveStatus && belongsToRun) retainLiveIdentity(runInfo, liveStatus);
2937
3021
  let sentinel = null;
2938
3022
  if (await sentinelExists(runInfo.sentinelPath)) sentinel = await parseSentinel(runInfo.sentinelPath);
2939
- const translated = translateStatus(rawStatusPhase(liveStatus), sentinel);
2940
- return buildBaseStatus(liveStatus ?? {}, {
3023
+ const source = belongsToRun ? liveStatus : null;
3024
+ const translated = belongsToRun && liveStatus ? translateStatus(rawStatusPhase(source), sentinel) : sentinel ? sentinelStatusToResult(sentinel._status) : translateStatus(rawStatusPhase(source), null);
3025
+ const result = buildBaseStatus(source ?? {}, {
2941
3026
  run_id: runInfo.runId,
2942
3027
  label: runInfo.label,
2943
3028
  prefer_fallback_run_id: true,
2944
3029
  runtime_id: runInfo.runtimeId,
2945
- session_id: sentinel?.SESSION ?? liveStatus?.session_id ?? runInfo.sessionId,
3030
+ session_id: sentinel?.SESSION ?? source?.session_id ?? runInfo.sessionId,
2946
3031
  stop_reason: sentinel?.STOP_REASON,
2947
3032
  identity: {
2948
3033
  roster_file: runInfo.rosterFile,
@@ -2956,6 +3041,10 @@ async function buildRunStatus(runInfo, liveStatus) {
2956
3041
  effective_backend: runInfo.effectiveBackend
2957
3042
  }
2958
3043
  }, translated);
3044
+ result.label = runInfo.label;
3045
+ if (!belongsToRun || !result.event_path) result.event_path = runInfo.eventLogPath;
3046
+ if (!belongsToRun || !liveStatus) result.runtime_output_available = false;
3047
+ return result;
2959
3048
  }
2960
3049
  async function queryLiveStatus(client, runtimeId) {
2961
3050
  if (!runtimeId) return null;
@@ -3112,27 +3201,41 @@ function matchesFilters(record, args) {
3112
3201
  if (args.after_seq !== void 0 && seq !== void 0 && seq <= args.after_seq) return false;
3113
3202
  return true;
3114
3203
  }
3115
- async function readNdjsonEvents(filePath, args) {
3204
+ /**
3205
+ * Stream the parsed records of one NDJSON event log. Malformed lines are
3206
+ * dropped; an unreadable log rejects, so a caller that cannot read the log
3207
+ * never sees a silent empty tail.
3208
+ */
3209
+ async function* readNdjsonRecords(filePath) {
3116
3210
  const stream = fs.createReadStream(filePath, { encoding: "utf-8" });
3117
3211
  const rl = readline.createInterface({
3118
3212
  input: stream,
3119
3213
  crlfDelay: Infinity
3120
3214
  });
3121
- const events = [];
3122
3215
  try {
3123
3216
  for await (const line of rl) {
3124
3217
  if (!line.trim()) continue;
3218
+ let parsed;
3125
3219
  try {
3126
- const record = asRecord(JSON.parse(line));
3127
- if (!record || !matchesFilters(record, args)) continue;
3128
- events.push(clipEvent(record));
3129
- if (events.length > args.limit) events.splice(0, events.length - args.limit);
3130
- } catch {}
3220
+ parsed = JSON.parse(line);
3221
+ } catch {
3222
+ continue;
3223
+ }
3224
+ const record = asRecord(parsed);
3225
+ if (record) yield record;
3131
3226
  }
3132
3227
  } finally {
3133
3228
  rl.close();
3134
3229
  stream.close();
3135
3230
  }
3231
+ }
3232
+ async function readNdjsonEvents(filePath, args) {
3233
+ const events = [];
3234
+ for await (const record of readNdjsonRecords(filePath)) {
3235
+ if (!matchesFilters(record, args)) continue;
3236
+ events.push(clipEvent(record));
3237
+ if (events.length > args.limit) events.splice(0, events.length - args.limit);
3238
+ }
3136
3239
  return events;
3137
3240
  }
3138
3241
  async function resolveRun(args, getSupervisorClient) {
@@ -3221,7 +3324,7 @@ async function executeEventsTool(args, getSupervisorClient) {
3221
3324
  } catch (error) {
3222
3325
  failures.push(contextualError(`read event log ${resolved.eventLogPath}`, error));
3223
3326
  }
3224
- if (resolved.runtimeId) try {
3327
+ if (resolved.runtimeId && !resolved.runInfo) try {
3225
3328
  const history = await resolved.client.history({
3226
3329
  runtime_id: resolved.runtimeId,
3227
3330
  limit,
@@ -3333,23 +3436,31 @@ function sleep(ms, signal) {
3333
3436
  signal?.addEventListener("abort", onAbort, { once: true });
3334
3437
  });
3335
3438
  }
3336
- async function fullOutput(runtimeId, supervisorId) {
3337
- if (supervisorId) {
3338
- const { client, isSingleton } = await getSupervisorClient(supervisorId);
3339
- try {
3340
- const result = await client.result(runtimeId);
3341
- return typeof result.final_output === "string" ? result.final_output : void 0;
3342
- } finally {
3343
- if (!isSingleton) client.close();
3344
- }
3439
+ async function outputFromEvents(filePath) {
3440
+ let output;
3441
+ for await (const record of readNdjsonRecords(filePath)) try {
3442
+ const event = normalizeRunEvent(record);
3443
+ if (event?.event === "session.end" && event.final_output !== void 0) output = event.final_output;
3444
+ } catch {}
3445
+ return output;
3446
+ }
3447
+ async function realResult(supervisorId, runtimeId) {
3448
+ const { client, isSingleton } = await getSupervisorClient(supervisorId);
3449
+ try {
3450
+ return await client.result(runtimeId);
3451
+ } finally {
3452
+ if (!isSingleton) client.close();
3345
3453
  }
3346
- const result = await (await Supervisor.get()).getClient().result(runtimeId);
3347
- return typeof result.final_output === "string" ? result.final_output : void 0;
3454
+ }
3455
+ async function realSentinelPath(supervisorId, runId) {
3456
+ const { sup, supervisorId: resolvedId } = await getSupervisorClient(supervisorId);
3457
+ return (sup ? sup.getRunByReference(runId) : void 0)?.sentinelPath ?? findExternalRun(resolvedId, runId)?.sentinelPath;
3348
3458
  }
3349
3459
  const defaultDeps = {
3350
3460
  status: statusTool,
3351
3461
  inspect: inspectTool,
3352
- fullOutput,
3462
+ result: realResult,
3463
+ sentinelPath: realSentinelPath,
3353
3464
  sleep,
3354
3465
  now: Date.now,
3355
3466
  pollIntervalMs: DEFAULT_POLL_INTERVAL_MS
@@ -3388,14 +3499,25 @@ async function executeResultTool(args, deps) {
3388
3499
  if (TERMINAL_STATUSES$1.has(status.status)) {
3389
3500
  let exactOutputRetrieved = false;
3390
3501
  try {
3391
- const output = await deps.fullOutput?.(status.runtime_id, args.supervisorId);
3502
+ let output = status.event_path ? await outputFromEvents(status.event_path).catch(() => void 0) : void 0;
3392
3503
  if (output !== void 0) {
3393
3504
  status.final_output = output;
3394
3505
  status.final_output_truncated = false;
3395
3506
  exactOutputRetrieved = true;
3396
3507
  }
3397
3508
  } catch {}
3398
- if (status.final_output === void 0) try {
3509
+ if (!exactOutputRetrieved && status.runtime_output_available !== false && status.runtime_id) try {
3510
+ const result = await deps.result(args.supervisorId, status.runtime_id);
3511
+ const output = typeof result.final_output === "string" ? result.final_output : void 0;
3512
+ const sentinel = await deps.sentinelPath(args.supervisorId, status.run_id);
3513
+ const matchesTurn = sentinel === void 0 ? true : sentinel !== "" && typeof result.sentinel_file === "string" && result.sentinel_file === sentinel;
3514
+ if (output !== void 0 && matchesTurn) {
3515
+ status.final_output = output;
3516
+ status.final_output_truncated = false;
3517
+ exactOutputRetrieved = true;
3518
+ }
3519
+ } catch {}
3520
+ if (!exactOutputRetrieved && status.final_output === void 0 && status.runtime_output_available !== false) try {
3399
3521
  const inspected = await deps.inspect({
3400
3522
  runId: args.runId,
3401
3523
  supervisorId: args.supervisorId
@@ -5223,7 +5345,7 @@ var message_route_conformance_default = {
5223
5345
  "expect": {
5224
5346
  "code": "unsupported_action",
5225
5347
  "facts": {},
5226
- "message": "action \"control_reply\" is only available on the claude-channel backend"
5348
+ "message": "action \"control_reply\" is retained in the contract but no surface can deliver one"
5227
5349
  }
5228
5350
  },
5229
5351
  {
@@ -5242,7 +5364,7 @@ var message_route_conformance_default = {
5242
5364
  "expect": {
5243
5365
  "code": "unsupported_action",
5244
5366
  "facts": {},
5245
- "message": "action \"control_reply\" is only available on the claude-channel backend"
5367
+ "message": "action \"control_reply\" is retained in the contract but no surface can deliver one"
5246
5368
  }
5247
5369
  },
5248
5370
  {
@@ -5261,7 +5383,7 @@ var message_route_conformance_default = {
5261
5383
  "expect": {
5262
5384
  "code": "unsupported_action",
5263
5385
  "facts": {},
5264
- "message": "action \"control_reply\" is only available on the claude-channel backend"
5386
+ "message": "action \"control_reply\" is retained in the contract but no surface can deliver one"
5265
5387
  }
5266
5388
  },
5267
5389
  {
@@ -5280,7 +5402,7 @@ var message_route_conformance_default = {
5280
5402
  "expect": {
5281
5403
  "code": "unsupported_action",
5282
5404
  "facts": {},
5283
- "message": "action \"control_reply\" is only available on the claude-channel backend"
5405
+ "message": "action \"control_reply\" is retained in the contract but no surface can deliver one"
5284
5406
  }
5285
5407
  },
5286
5408
  {
@@ -5299,7 +5421,7 @@ var message_route_conformance_default = {
5299
5421
  "expect": {
5300
5422
  "code": "unsupported_action",
5301
5423
  "facts": {},
5302
- "message": "action \"control_reply\" is only available on the claude-channel backend"
5424
+ "message": "action \"control_reply\" is retained in the contract but no surface can deliver one"
5303
5425
  }
5304
5426
  },
5305
5427
  {
@@ -5318,7 +5440,7 @@ var message_route_conformance_default = {
5318
5440
  "expect": {
5319
5441
  "code": "unsupported_action",
5320
5442
  "facts": {},
5321
- "message": "action \"control_reply\" is only available on the claude-channel backend"
5443
+ "message": "action \"control_reply\" is retained in the contract but no surface can deliver one"
5322
5444
  }
5323
5445
  },
5324
5446
  {
@@ -8274,6 +8396,41 @@ function renderAskFailure(messageId, target, outcome = "possibly-pending") {
8274
8396
  function normalizeAskTimeoutMs(timeoutMs) {
8275
8397
  return timeoutMs !== void 0 && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : void 0;
8276
8398
  }
8399
+ /** Render a value the way Go's `%q` verb does (`strconv.Quote`). */
8400
+ function goQuote(value) {
8401
+ let out = "\"";
8402
+ for (const character of value) out += goEscapedRune(character);
8403
+ return `${out}"`;
8404
+ }
8405
+ const GO_PRINTABLE = /[\p{L}\p{M}\p{N}\p{P}\p{S}]/u;
8406
+ /**
8407
+ * One rune in `strconv.Quote` form: short escapes for a quote, a backslash, a
8408
+ * bell and the whitespace controls; `\xNN` for the remaining ASCII escapes;
8409
+ * `\uNNNN` and `\UNNNNNNNN` for a non-printable rune; the rune itself when it
8410
+ * is printable. Printable is Go's `strconv.IsPrint` — ASCII space through `~`,
8411
+ * and elsewhere the categories L, M, N, P and S. V8's Unicode tables can be
8412
+ * newer than Go's, so a rune assigned since Go's tables were generated renders
8413
+ * escaped here where Go writes it verbatim; that direction only fails to match
8414
+ * a sentence, never invents one.
8415
+ */
8416
+ function goEscapedRune(character) {
8417
+ const code = character.codePointAt(0) ?? 0;
8418
+ switch (character) {
8419
+ case "\"": return "\\\"";
8420
+ case "\\": return "\\\\";
8421
+ case "\x07": return "\\a";
8422
+ case "\b": return "\\b";
8423
+ case "\f": return "\\f";
8424
+ case "\n": return "\\n";
8425
+ case "\r": return "\\r";
8426
+ case " ": return "\\t";
8427
+ case "\v": return "\\v";
8428
+ }
8429
+ if (code < 128 ? code >= 32 && code !== 127 : GO_PRINTABLE.test(character)) return character;
8430
+ if (code < 128) return `\\x${code.toString(16).padStart(2, "0")}`;
8431
+ if (code < 65536) return `\\u${code.toString(16).padStart(4, "0")}`;
8432
+ return `\\U${code.toString(16).padStart(8, "0")}`;
8433
+ }
8277
8434
  //#endregion
8278
8435
  //#region ../../schemas/message.input.json
8279
8436
  var message_input_default = {
@@ -8409,11 +8566,11 @@ var message_input_default = {
8409
8566
  "properties": {
8410
8567
  "action": {
8411
8568
  "const": "control_reply",
8412
- "description": "Answer a claude-channel control request; registered only on the claude-channel backend."
8569
+ "description": "Retained in the contract for the retired claude-channel backend's reply path; no surface can deliver one."
8413
8570
  },
8414
8571
  "target": {
8415
8572
  "type": "string",
8416
- "description": "Opaque claude-channel control message id, passed through unchanged."
8573
+ "description": "Opaque control message id, passed through unchanged."
8417
8574
  },
8418
8575
  "payload": {
8419
8576
  "type": "object",
@@ -8452,7 +8609,7 @@ var message_route_umpire_default = {
8452
8609
  },
8453
8610
  "controlReplySupported": {
8454
8611
  "type": "boolean",
8455
- "description": "The surface can deliver a claude-channel control reply (the sidecar only)."
8612
+ "description": "Retained in the contract; no surface can deliver a control reply now that the claude-channel sidecar is retired."
8456
8613
  }
8457
8614
  },
8458
8615
  fields: {
@@ -8509,7 +8666,7 @@ var message_route_umpire_default = {
8509
8666
  "condition": "controlReplySupported"
8510
8667
  }]
8511
8668
  },
8512
- "reason": "action \"control_reply\" is only available on the claude-channel backend"
8669
+ "reason": "action \"control_reply\" is retained in the contract but no surface can deliver one"
8513
8670
  },
8514
8671
  {
8515
8672
  "type": "fairWhen",
@@ -8953,7 +9110,7 @@ async function tell(provider, input, resolution) {
8953
9110
  const label = resolutionLabel(resolution, input.target ?? "");
8954
9111
  let result;
8955
9112
  try {
8956
- result = await provider.tell(resolution.runId, input.message ?? "");
9113
+ result = await provider.tell(resolution.runId, input.message ?? "", resolution.publicRunId);
8957
9114
  } catch (err) {
8958
9115
  throw asToolError(err);
8959
9116
  }
@@ -8971,7 +9128,7 @@ async function ask(provider, input, resolution, signal) {
8971
9128
  const label = resolutionLabel(resolution, input.target ?? "");
8972
9129
  let result;
8973
9130
  try {
8974
- result = await provider.ask(resolution.runId, input.message ?? "", input.timeout_ms, signal);
9131
+ result = await provider.ask(resolution.runId, input.message ?? "", input.timeout_ms, signal, resolution.publicRunId);
8975
9132
  } catch (err) {
8976
9133
  throw asToolError(err);
8977
9134
  }
@@ -9088,9 +9245,10 @@ async function executeMessage(provider, input, options) {
9088
9245
  conditions.targetMatch = resolution.match;
9089
9246
  const denial = permissionDenial(action, conditions);
9090
9247
  if (denial) throw denial;
9248
+ if (action === "resume" && typed.idempotency_key) return resume(provider, typed, resolution);
9091
9249
  let facts;
9092
9250
  try {
9093
- facts = await provider.state(resolution.runId);
9251
+ facts = await provider.state(resolution.runId, resolution.publicRunId);
9094
9252
  } catch (err) {
9095
9253
  throw refused(err);
9096
9254
  }
@@ -9168,7 +9326,7 @@ async function executePeers(provider, input) {
9168
9326
  const resolution = await resolveTarget(provider, typed.target ?? "");
9169
9327
  let last;
9170
9328
  try {
9171
- last = await provider.last(resolution.runId);
9329
+ last = await provider.last(resolution.runId, resolution.publicRunId);
9172
9330
  } catch (err) {
9173
9331
  throw asToolError(err);
9174
9332
  }
@@ -9302,19 +9460,6 @@ function createMessageTool(provider) {
9302
9460
  return (args, options) => messageTool(args, provider, options);
9303
9461
  }
9304
9462
  //#endregion
9305
- //#region src/tools/sentinel-status.ts
9306
- /**
9307
- * Sentinel statuses that settle the provider session itself, so the recorded
9308
- * session id is a loadable transcript. TIMEOUT and KILLED are supervisor-imposed
9309
- * terminations (time budget, cancellation) where an automatic continuation would
9310
- * surprise. Matches the Go `readSentinelSession`.
9311
- */
9312
- const RESUMABLE_SENTINEL_STATUSES = new Set([
9313
- "DONE",
9314
- "FAILED",
9315
- "BLOCKED"
9316
- ]);
9317
- //#endregion
9318
9463
  //#region src/tools/message-provider.ts
9319
9464
  const SUPERVISOR_RUN_ID = "supervisor";
9320
9465
  const TERMINAL_STATUSES = new Set([
@@ -9354,6 +9499,16 @@ function isGenerationMismatchError(error) {
9354
9499
  }
9355
9500
  return error instanceof Error && /generation mismatch/i.test(error.message);
9356
9501
  }
9502
+ /**
9503
+ * Matches Go's `statusReportsNoRuntime`: true only for the supervisor's absence
9504
+ * answer for this exact runtime, so a refusal or a transport failure stays the
9505
+ * caller's error. The sentence is rendered with `goQuote` because the
9506
+ * supervisor formats the id with Go's `%q`.
9507
+ */
9508
+ function statusReportsNoRuntime(error, runId) {
9509
+ if (!(error instanceof RpcError) || error.code !== -32e3) return false;
9510
+ return rpcText(error) === `runtime ${goQuote(runId)} not found`;
9511
+ }
9357
9512
  /** Reject an abort exactly the way fetch does, so the ask cleanup path runs. */
9358
9513
  function raceAbort(promise, signal) {
9359
9514
  if (!signal) return promise;
@@ -9425,15 +9580,42 @@ function translateRunFacts(rawStatus, rawPhase) {
9425
9580
  live: !TERMINAL_STATUSES.has(status)
9426
9581
  };
9427
9582
  }
9428
- async function sentinelResumable(filePath) {
9429
- if (!filePath) return false;
9583
+ /**
9584
+ * Classify one broker peer row into the facts the router gates on. The Go
9585
+ * providers share this translation through `messaging.PeerRunFacts`: a peer row
9586
+ * carries no session and no sentinel, so the run is never resumable from here —
9587
+ * the supervisor that spawned it owns a resume.
9588
+ */
9589
+ function peerFacts(peer) {
9590
+ const translation = translateRunFacts(peer?.status ?? "", "");
9591
+ return {
9592
+ live: translation.live,
9593
+ resumable: false,
9594
+ status: translation.status
9595
+ };
9596
+ }
9597
+ /** Read one sentinel file's status word, or undefined when the file is unreadable. */
9598
+ async function readSentinelStatus(filePath) {
9430
9599
  try {
9431
- const status = ((await fs.promises.readFile(filePath, "utf-8")).trim().split("\n")[0] ?? "").trim().toUpperCase();
9432
- return RESUMABLE_SENTINEL_STATUSES.has(status);
9600
+ return ((await fs.promises.readFile(filePath, "utf-8")).trim().split("\n")[0] ?? "").trim().toUpperCase();
9433
9601
  } catch {
9434
- return false;
9602
+ return;
9435
9603
  }
9436
9604
  }
9605
+ /**
9606
+ * Read one sentinel file's terminal result word, or undefined when the file is
9607
+ * unreadable or holds a status that is not terminal. The mapping is the status
9608
+ * surface's, so both classify a sentinel the same way.
9609
+ */
9610
+ async function terminalSentinelStatus(filePath) {
9611
+ const status = await readSentinelStatus(filePath);
9612
+ if (!status || !TERMINAL_SENTINEL_STATUSES.has(status)) return void 0;
9613
+ return sentinelStatusToResult(status);
9614
+ }
9615
+ async function sentinelResumable(filePath) {
9616
+ const status = await readSentinelStatus(filePath);
9617
+ return status !== void 0 && RESUMABLE_SENTINEL_STATUSES.has(status);
9618
+ }
9437
9619
  function peerRecords(peers) {
9438
9620
  return (peers.peers ?? []).map((peer) => {
9439
9621
  const session = peer.session;
@@ -9503,10 +9685,33 @@ function createHostMessageProvider(options) {
9503
9685
  });
9504
9686
  } catch {}
9505
9687
  }
9506
- function publicRunIdFor(runId) {
9507
- const local = Supervisor.getInstance(supervisorId)?.getRunByRuntimeId(runId);
9508
- if (local?.runId) return local.runId;
9509
- return listExternalRuns(supervisorId).find((info) => info.runtimeId === runId)?.runId;
9688
+ async function publicRunIdFor(runId) {
9689
+ const instance = Supervisor.getInstance(supervisorId);
9690
+ if (instance && instance.getRunsByRuntimeId(runId).length > 0) {
9691
+ const unique = instance.getRunByRuntimeId(runId);
9692
+ if (unique) return unique.runId;
9693
+ const status = await client.status(runId);
9694
+ const match = typeof status.sentinel_file === "string" ? instance.getRunByRuntimeTurn(runId, status.sentinel_file) : void 0;
9695
+ if (!match) throw new Error(`turn identity unavailable for ${runId}`);
9696
+ return match.runId;
9697
+ }
9698
+ const matches = listExternalRuns(supervisorId).filter((info) => info.runtimeId === runId);
9699
+ if (matches.length <= 1) return matches[0]?.runId;
9700
+ const status = await client.status(runId);
9701
+ const match = matches.find((info) => info.sentinelPath === status.sentinel_file);
9702
+ if (!match) throw new Error(`turn identity unavailable for ${runId}`);
9703
+ return match.runId;
9704
+ }
9705
+ function publicRunInfo(publicRunId) {
9706
+ if (!publicRunId) return void 0;
9707
+ const instance = Supervisor.getInstance(supervisorId);
9708
+ return instance ? instance.getRunById(publicRunId) : findExternalRunById(supervisorId, publicRunId);
9709
+ }
9710
+ function expectedSentinel(publicRunId) {
9711
+ if (!publicRunId) return void 0;
9712
+ const info = publicRunInfo(publicRunId);
9713
+ if (!info?.sentinelPath) throw new Error(`turn identity unavailable for ${publicRunId}`);
9714
+ return info.sentinelPath;
9510
9715
  }
9511
9716
  function unique(info) {
9512
9717
  return {
@@ -9516,6 +9721,18 @@ function createHostMessageProvider(options) {
9516
9721
  match: "unique"
9517
9722
  };
9518
9723
  }
9724
+ async function peerRunState(runId) {
9725
+ let listed;
9726
+ try {
9727
+ listed = await listedPeers();
9728
+ } catch (error) {
9729
+ if (isGenerationMismatchError(error)) throw error;
9730
+ return;
9731
+ }
9732
+ if (listed.detail) return void 0;
9733
+ const peer = listed.peers.find((entry) => entry.run_id === runId);
9734
+ return peer ? peerFacts(peer) : void 0;
9735
+ }
9519
9736
  const provider = {
9520
9737
  caller: () => ({
9521
9738
  kind: "owner",
@@ -9545,14 +9762,14 @@ function createHostMessageProvider(options) {
9545
9762
  const exact = peers.find((peer) => peer.run_id === ref);
9546
9763
  if (exact) return {
9547
9764
  runId: exact.run_id,
9548
- publicRunId: publicRunIdFor(exact.run_id),
9765
+ publicRunId: await publicRunIdFor(exact.run_id),
9549
9766
  label: firstNonEmpty(exact.label, exact.run_id),
9550
9767
  match: "unique"
9551
9768
  };
9552
9769
  const candidates = peers.filter((peer) => peer.label === ref).map((peer) => peer.run_id);
9553
9770
  if (candidates.length === 1) return {
9554
9771
  runId: candidates[0],
9555
- publicRunId: publicRunIdFor(candidates[0]),
9772
+ publicRunId: await publicRunIdFor(candidates[0]),
9556
9773
  label: ref,
9557
9774
  match: "unique"
9558
9775
  };
@@ -9566,20 +9783,38 @@ function createHostMessageProvider(options) {
9566
9783
  match: "none"
9567
9784
  };
9568
9785
  },
9569
- async state(runId) {
9786
+ async state(runId, publicRunId) {
9570
9787
  if (runId === "supervisor") {
9571
9788
  const { peers, detail } = await listedPeers();
9572
9789
  if (detail) throw new Error(`the peer listing is incomplete: ${detail}`);
9573
- const translation = translateRunFacts(peers.find((entry) => entry.run_id === "supervisor")?.status ?? "", "");
9574
- return {
9575
- live: translation.live,
9576
- resumable: false,
9577
- status: translation.status
9578
- };
9790
+ return peerFacts(peers.find((entry) => entry.run_id === SUPERVISOR_RUN_ID));
9579
9791
  }
9580
- const status = await client.status(runId);
9792
+ let status;
9793
+ try {
9794
+ status = await client.status(runId);
9795
+ } catch (error) {
9796
+ if (!statusReportsNoRuntime(error, runId)) throw error;
9797
+ const registered = Supervisor.getInstance(supervisorId)?.getRunById(runId) ?? findExternalRunById(supervisorId, runId);
9798
+ if (registered) {
9799
+ const terminal = await terminalSentinelStatus(registered.sentinelPath);
9800
+ if (terminal) return {
9801
+ live: false,
9802
+ resumable: resumableFromSession(registered.sessionId),
9803
+ status: terminal
9804
+ };
9805
+ }
9806
+ const listed = await peerRunState(runId);
9807
+ if (listed) return listed;
9808
+ throw error;
9809
+ }
9810
+ const info = publicRunInfo(publicRunId);
9811
+ if (info && typeof status.sentinel_file === "string" && status.sentinel_file !== info.sentinelPath) return {
9812
+ live: false,
9813
+ resumable: false,
9814
+ status: await terminalSentinelStatus(info.sentinelPath) ?? "unknown"
9815
+ };
9581
9816
  const translation = translateRunFacts(typeof status.status === "string" ? status.status : "", typeof status.phase === "string" ? status.phase : "");
9582
- let resumable = typeof status.session_id === "string" && status.session_id !== "";
9817
+ let resumable = resumableFromSession(status.session_id);
9583
9818
  if (!resumable && typeof status.sentinel_file === "string") resumable = await sentinelResumable(status.sentinel_file);
9584
9819
  return {
9585
9820
  live: translation.live,
@@ -9587,7 +9822,8 @@ function createHostMessageProvider(options) {
9587
9822
  status: translation.status
9588
9823
  };
9589
9824
  },
9590
- async tell(targetRunId, message) {
9825
+ async tell(targetRunId, message, publicRunId) {
9826
+ const expected = expectedSentinel(publicRunId);
9591
9827
  const messageId = newMsgID();
9592
9828
  const body = JSON.stringify({
9593
9829
  from: SUPERVISOR_RUN_ID,
@@ -9603,7 +9839,8 @@ function createHostMessageProvider(options) {
9603
9839
  message_id: messageId,
9604
9840
  generation,
9605
9841
  body,
9606
- no_reply: true
9842
+ no_reply: true,
9843
+ ...expected ? { expected_sentinel_file: expected } : {}
9607
9844
  });
9608
9845
  } catch (error) {
9609
9846
  const outcome = controlOutcome(error);
@@ -9618,7 +9855,8 @@ function createHostMessageProvider(options) {
9618
9855
  messageId: firstNonEmpty(result.message_id, messageId)
9619
9856
  };
9620
9857
  },
9621
- async ask(targetRunId, message, timeoutMs, signal) {
9858
+ async ask(targetRunId, message, timeoutMs, signal, publicRunId) {
9859
+ const expected = expectedSentinel(publicRunId);
9622
9860
  const messageId = newMsgID();
9623
9861
  const body = JSON.stringify({
9624
9862
  id: messageId,
@@ -9635,7 +9873,8 @@ function createHostMessageProvider(options) {
9635
9873
  to_run_id: targetRunId,
9636
9874
  message_id: messageId,
9637
9875
  generation,
9638
- body
9876
+ body,
9877
+ ...expected ? { expected_sentinel_file: expected } : {}
9639
9878
  });
9640
9879
  } catch (error) {
9641
9880
  const outcome = controlOutcome(error);
@@ -9764,10 +10003,20 @@ function createHostMessageProvider(options) {
9764
10003
  previousLabel: renamed.previous_label
9765
10004
  };
9766
10005
  },
9767
- async last(runId) {
10006
+ async last(runId, publicRunId) {
10007
+ const expected = expectedSentinel(publicRunId);
10008
+ if (expected) {
10009
+ let status;
10010
+ try {
10011
+ status = await client.status(runId);
10012
+ } catch (error) {
10013
+ throw authorityError(error);
10014
+ }
10015
+ if (typeof status.sentinel_file === "string" && status.sentinel_file !== expected) throw authorityError(new RpcError(-32e3, `turn no longer current for runtime ${runId}`, { code: "invalid-request" }));
10016
+ }
9768
10017
  let last;
9769
10018
  try {
9770
- last = await client.sessionLast(runId);
10019
+ last = await client.sessionLast(runId, expected);
9771
10020
  } catch (error) {
9772
10021
  throw authorityError(error);
9773
10022
  }
@@ -9917,12 +10166,7 @@ function createChildMessageProvider(options) {
9917
10166
  if (detail) throw new Error(`the peer listing is incomplete: ${detail}`);
9918
10167
  const peer = peers.find((entry) => entry.run_id === targetRunId);
9919
10168
  if (!peer) throw new Error(`run ${targetRunId} is not visible`);
9920
- const translation = translateRunFacts(peer.status ?? "", "");
9921
- return {
9922
- live: translation.live,
9923
- resumable: false,
9924
- status: translation.status
9925
- };
10169
+ return peerFacts(peer);
9926
10170
  },
9927
10171
  tell: (targetRunId, message) => broker.tell(targetRunId, message, newMsgID()),
9928
10172
  ask: (targetRunId, message, timeoutMs, signal) => broker.ask(targetRunId, message, newMsgID(), timeoutMs, signal),
@@ -9944,7 +10188,11 @@ function createChildMessageProvider(options) {
9944
10188
  }
9945
10189
  //#endregion
9946
10190
  //#region src/tools/peers.ts
9947
- /** The one avenor_peers handler; it resolves and reads through the bound provider. */
10191
+ /** The one avenor_peers handler; it resolves and reads through the bound provider.
10192
+ *
10193
+ * Admission belongs to the shared router for every surface, so the argument is
10194
+ * the untyped JSON a tool call carries and `executePeers` validates it first —
10195
+ * the same shape `messageTool` takes. */
9948
10196
  async function peersTool(args, provider) {
9949
10197
  return executePeers(provider, args);
9950
10198
  }
@@ -4,6 +4,6 @@ import { n as getVersion, r as installerBinaryPath, t as getInstallDir } from ".
4
4
  declare function platformMapping(platform: string, arch: string): string | null;
5
5
  declare function verifyChecksum(buffer: Buffer, expectedHash: string): boolean;
6
6
  declare function parseChecksums(content: string, assetName: string): string | null;
7
- declare function postinstall(fetchFn?: typeof globalThis.fetch, platform?: string, arch?: string): Promise<void>;
7
+ declare function postinstall(fetchFn?: (url: string) => Promise<Response>, platform?: string, arch?: string): Promise<void>;
8
8
  //#endregion
9
9
  export { getInstallDir, getVersion, installerBinaryPath, parseChecksums, platformMapping, postinstall, verifyChecksum };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dougbots/avenor-core",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -23,6 +23,7 @@
23
23
  },
24
24
  "scripts": {
25
25
  "build": "tsdown",
26
+ "typecheck": "tsc --noEmit",
26
27
  "test": "bun test",
27
28
  "test:e2e": "AVENOR_REAL_INTEGRATION=1 bun test",
28
29
  "dev": "tsdown --watch",