@dougbots/avenor-core 0.16.1 → 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;
@@ -207,6 +208,12 @@ interface MessagePeer {
207
208
  interface MessagePeersResult {
208
209
  peers: MessagePeer[];
209
210
  generation?: string;
211
+ /**
212
+ * Set when the listing read only part of the peer set. The peers beside it
213
+ * are the ones that were read, so a partial list is never read as a
214
+ * complete one.
215
+ */
216
+ peers_error?: string;
210
217
  }
211
218
  interface MessagePreflightResult {
212
219
  protocol: number;
@@ -311,7 +318,6 @@ declare class Client {
311
318
  events(options?: SubscribeOptions): AsyncIterableIterator<Event>;
312
319
  history(options: HistoryOptions): Promise<HistoryResult>;
313
320
  status(runtimeId?: string): Promise<Record<string, unknown>>;
314
- result(runtimeId?: string): Promise<Record<string, unknown>>;
315
321
  list(): Promise<Array<Record<string, unknown>>>;
316
322
  spawn(params: SpawnParams): Promise<SpawnResult>;
317
323
  shutdown(mode: string): Promise<void>;
@@ -350,7 +356,9 @@ declare class Client {
350
356
  messagePreflight(): Promise<MessagePreflightResult>;
351
357
  resume(params: ResumeParams): Promise<ResumeResult>;
352
358
  sessionRename(sessionId: string, name: string): Promise<SessionRenameResult>;
353
- 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>>;
354
362
  sessionLastSet(runtimeId: string, finalOutput: string, stopReason: string, truncated: boolean): Promise<void>;
355
363
  workflowCancel(workflowId: string, actor: string, reason: string): Promise<Record<string, unknown>>;
356
364
  isClosed(): boolean;
@@ -517,7 +525,12 @@ interface RunSnapshot {
517
525
  }
518
526
  declare function createRunSnapshot(limits?: Partial<RunReducerLimits>): RunSnapshot;
519
527
  declare function reduceRunSnapshot(current: RunSnapshot, input: unknown, limits?: Partial<RunReducerLimits>): RunSnapshot;
520
- 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>;
521
534
  declare class RunReducer {
522
535
  private state;
523
536
  constructor(limits?: Partial<RunReducerLimits>, initial?: RunSnapshotSeed);
@@ -619,20 +632,46 @@ declare class Supervisor {
619
632
  registerResumed(run: RunInfo): void;
620
633
  /** The one place a run's ID and label are registered, for spawn and for resume. */
621
634
  private register;
622
- /** 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
+ */
623
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;
624
652
  /**
625
653
  * The live singleton when it owns supervisorId, null otherwise. Single
626
654
  * source of truth for the ownership check, so callers cannot observe
627
655
  * isCurrentInstance true while currentInstance is null.
628
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
+ };
629
668
  static getInstance(supervisorId: string): Supervisor | null;
630
669
  static isCurrentInstance(supervisorId: string): boolean;
631
670
  static currentInstance(): Supervisor | null;
632
671
  /** Iterate stored runs without exposing the internal map. */
633
672
  forEachRun(callback: (info: RunInfo) => void): void;
634
673
  getClient(): Client;
635
- spawn(params: SpawnParams, runId?: `${string}-${string}-${string}-${string}-${string}`, metadata?: SpawnMetadata): Promise<RunInfo>;
674
+ spawn(params: SpawnParams, runId?: string, metadata?: SpawnMetadata): Promise<RunInfo>;
636
675
  close(options?: {
637
676
  skipShutdown?: boolean;
638
677
  }): Promise<void>;
@@ -743,6 +782,7 @@ interface StatusResult {
743
782
  latest_seq?: number;
744
783
  final_output?: string;
745
784
  final_output_truncated?: boolean;
785
+ runtime_output_available?: boolean;
746
786
  }
747
787
  declare const statusTool: (args: StatusToolArgs) => Promise<StatusResult | StatusListResult>;
748
788
  //#endregion
@@ -762,6 +802,11 @@ interface EventsToolResult {
762
802
  [key: string]: unknown;
763
803
  }>;
764
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
+ */
765
810
  declare const eventsTool: (args: EventsToolArgs) => Promise<EventsToolResult>;
766
811
  //#endregion
767
812
  //#region src/tools/inspect.d.ts
@@ -775,7 +820,7 @@ interface InspectResult {
775
820
  run_id: string;
776
821
  label: string;
777
822
  status: StatusResult;
778
- snapshot: Omit<RunSnapshot, '_seen_seq_by_scope' | '_recent_legacy_keys' | '_last_semantic_fingerprint' | '_last_source_event'>;
823
+ snapshot: RunSnapshotView;
779
824
  transcript: RunSnapshot['transcript'];
780
825
  tools: RunSnapshot['tools'];
781
826
  live_tools: RunSnapshot['live_tools'];
@@ -961,9 +1006,9 @@ interface MessageProvider {
961
1006
  surface(): MessageSurface;
962
1007
  role(): string;
963
1008
  resolve(ref: string): Promise<Resolution>;
964
- state(runId: string): Promise<RunFacts>;
965
- tell(targetRunId: string, message: string): Promise<ProviderResult>;
966
- 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>;
967
1012
  reply(inboundAskId: string, message: string, askerRunId: string): Promise<ProviderResult>;
968
1013
  withdraw(messageId: string): Promise<ProviderResult>;
969
1014
  controlReply?(target: string, payload: unknown): Promise<ProviderResult>;
@@ -978,7 +1023,7 @@ interface MessageProvider {
978
1023
  sessionId?: string;
979
1024
  name: string;
980
1025
  }): Promise<ProviderResult>;
981
- last(runId: string): Promise<ProviderResult>;
1026
+ last(runId: string, publicRunId?: string): Promise<ProviderResult>;
982
1027
  peers(): Promise<PeersProviderResult>;
983
1028
  /** Host surfaces that own a turn loop publish their own finished turn. */
984
1029
  recordLastTurn?(text: string, stopReason: string, truncated: boolean): Promise<void>;
@@ -1092,6 +1137,7 @@ interface MessageClient {
1092
1137
  generation: string;
1093
1138
  body: string;
1094
1139
  no_reply?: boolean;
1140
+ expected_sentinel_file?: string;
1095
1141
  }): Promise<MessageSendResult>;
1096
1142
  messageWait(params: {
1097
1143
  message_id: string;
@@ -1121,7 +1167,7 @@ interface MessageClient {
1121
1167
  on_event?: string;
1122
1168
  }): Promise<ResumeResult>;
1123
1169
  sessionRename(sessionId: string, name: string): Promise<SessionRenameResult>;
1124
- sessionLast(runtimeId: string): Promise<SessionLastResult>;
1170
+ sessionLast(runtimeId: string, expectedSentinelFile?: string): Promise<SessionLastResult>;
1125
1171
  sessionLastSet(runtimeId: string, finalOutput: string, stopReason: string, truncated: boolean): Promise<void>;
1126
1172
  status(runtimeId?: string): Promise<Record<string, unknown>>;
1127
1173
  }
@@ -1166,7 +1212,10 @@ declare function installAskClaims(provider: MessageProvider, claims: AskClaims,
1166
1212
  /**
1167
1213
  * Build the bound host provider: one supervisor, one generation, one control
1168
1214
  * client. It never reads an ambient singleton or retargets, and it never
1169
- * preflights per call.
1215
+ * preflights per call. Listing-backed lookups fail fast: an incomplete
1216
+ * listing refuses every resolution and state read, even for a peer the
1217
+ * partial set contains, so a wire `peers_error` resolves the same way the Go
1218
+ * providers do. Self and public-ID resolution bypass the listing.
1170
1219
  */
1171
1220
  declare function createHostMessageProvider(options: HostProviderOptions): MessageProvider;
1172
1221
  interface ChildProviderOptions {
@@ -1181,17 +1230,24 @@ interface ChildProviderOptions {
1181
1230
  /**
1182
1231
  * Build a child provider over the surface's inherited broker. A child never
1183
1232
  * learns a public run id, and it cannot read a sentinel, so it is never
1184
- * resumable from here: the supervisor owns a resume.
1233
+ * resumable from here: the supervisor owns a resume. Listing-backed lookups
1234
+ * fail fast: an incomplete listing refuses every resolution and state read,
1235
+ * even for a peer the partial set contains, so a wire `peers_error` resolves
1236
+ * the same way the Go providers do. Self resolution bypasses the listing.
1185
1237
  */
1186
1238
  declare function createChildMessageProvider(options: ChildProviderOptions): MessageProvider;
1187
1239
  //#endregion
1188
1240
  //#region src/tools/peers.d.ts
1189
1241
  interface PeersToolArgs extends PeersInput {}
1190
1242
  type PeersResult = PeersRouterResult;
1191
- /** The one avenor_peers handler; it resolves and reads through the bound provider. */
1192
- 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>;
1193
1249
  /** Bind the peers handler to one provider. */
1194
- declare function createPeersTool(provider: MessageProvider): (args: PeersToolArgs) => Promise<PeersResult>;
1250
+ declare function createPeersTool(provider: MessageProvider): (args: unknown) => Promise<PeersResult>;
1195
1251
  //#endregion
1196
1252
  //#region src/tools/kill.d.ts
1197
1253
  interface KillToolArgs {
@@ -1318,4 +1374,4 @@ type WorkflowCancelResult = Record<string, unknown>;
1318
1374
  declare function createWorkflowCancelTool(getSupervisorClient: GetSupervisorClient): (args: WorkflowCancelArgs) => Promise<WorkflowCancelResult>;
1319
1375
  declare const workflowCancelTool: (args: WorkflowCancelArgs) => Promise<WorkflowCancelResult>;
1320
1376
  //#endregion
1321
- 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;
@@ -9480,13 +9662,20 @@ function installAskClaims(provider, claims, pendingAsks, pollGeneration) {
9480
9662
  /**
9481
9663
  * Build the bound host provider: one supervisor, one generation, one control
9482
9664
  * client. It never reads an ambient singleton or retargets, and it never
9483
- * preflights per call.
9665
+ * preflights per call. Listing-backed lookups fail fast: an incomplete
9666
+ * listing refuses every resolution and state read, even for a peer the
9667
+ * partial set contains, so a wire `peers_error` resolves the same way the Go
9668
+ * providers do. Self and public-ID resolution bypass the listing.
9484
9669
  */
9485
9670
  function createHostMessageProvider(options) {
9486
9671
  const { surface, supervisorId, generation, client, pendingAsks, pollGeneration } = options;
9487
9672
  const claims = /* @__PURE__ */ new Map();
9488
9673
  async function listedPeers() {
9489
- return peerRecords(await client.messagePeers({ generation }));
9674
+ const result = await client.messagePeers({ generation });
9675
+ return {
9676
+ peers: peerRecords(result),
9677
+ detail: result.peers_error || void 0
9678
+ };
9490
9679
  }
9491
9680
  async function retractAsk(messageId) {
9492
9681
  try {
@@ -9496,10 +9685,33 @@ function createHostMessageProvider(options) {
9496
9685
  });
9497
9686
  } catch {}
9498
9687
  }
9499
- function publicRunIdFor(runId) {
9500
- const local = Supervisor.getInstance(supervisorId)?.getRunByRuntimeId(runId);
9501
- if (local?.runId) return local.runId;
9502
- 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;
9503
9715
  }
9504
9716
  function unique(info) {
9505
9717
  return {
@@ -9509,6 +9721,18 @@ function createHostMessageProvider(options) {
9509
9721
  match: "unique"
9510
9722
  };
9511
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
+ }
9512
9736
  const provider = {
9513
9737
  caller: () => ({
9514
9738
  kind: "owner",
@@ -9533,18 +9757,19 @@ function createHostMessageProvider(options) {
9533
9757
  const external = findExternalRunById(supervisorId, ref);
9534
9758
  if (external?.runtimeId) return unique(external);
9535
9759
  }
9536
- const peers = await listedPeers();
9760
+ const { peers, detail } = await listedPeers();
9761
+ if (detail) throw new Error(`the peer listing is incomplete: ${detail}`);
9537
9762
  const exact = peers.find((peer) => peer.run_id === ref);
9538
9763
  if (exact) return {
9539
9764
  runId: exact.run_id,
9540
- publicRunId: publicRunIdFor(exact.run_id),
9765
+ publicRunId: await publicRunIdFor(exact.run_id),
9541
9766
  label: firstNonEmpty(exact.label, exact.run_id),
9542
9767
  match: "unique"
9543
9768
  };
9544
9769
  const candidates = peers.filter((peer) => peer.label === ref).map((peer) => peer.run_id);
9545
9770
  if (candidates.length === 1) return {
9546
9771
  runId: candidates[0],
9547
- publicRunId: publicRunIdFor(candidates[0]),
9772
+ publicRunId: await publicRunIdFor(candidates[0]),
9548
9773
  label: ref,
9549
9774
  match: "unique"
9550
9775
  };
@@ -9558,18 +9783,38 @@ function createHostMessageProvider(options) {
9558
9783
  match: "none"
9559
9784
  };
9560
9785
  },
9561
- async state(runId) {
9786
+ async state(runId, publicRunId) {
9562
9787
  if (runId === "supervisor") {
9563
- const translation = translateRunFacts((await listedPeers()).find((entry) => entry.run_id === "supervisor")?.status ?? "", "");
9564
- return {
9565
- live: translation.live,
9566
- resumable: false,
9567
- status: translation.status
9568
- };
9788
+ const { peers, detail } = await listedPeers();
9789
+ if (detail) throw new Error(`the peer listing is incomplete: ${detail}`);
9790
+ return peerFacts(peers.find((entry) => entry.run_id === SUPERVISOR_RUN_ID));
9569
9791
  }
9570
- 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
+ };
9571
9816
  const translation = translateRunFacts(typeof status.status === "string" ? status.status : "", typeof status.phase === "string" ? status.phase : "");
9572
- let resumable = typeof status.session_id === "string" && status.session_id !== "";
9817
+ let resumable = resumableFromSession(status.session_id);
9573
9818
  if (!resumable && typeof status.sentinel_file === "string") resumable = await sentinelResumable(status.sentinel_file);
9574
9819
  return {
9575
9820
  live: translation.live,
@@ -9577,7 +9822,8 @@ function createHostMessageProvider(options) {
9577
9822
  status: translation.status
9578
9823
  };
9579
9824
  },
9580
- async tell(targetRunId, message) {
9825
+ async tell(targetRunId, message, publicRunId) {
9826
+ const expected = expectedSentinel(publicRunId);
9581
9827
  const messageId = newMsgID();
9582
9828
  const body = JSON.stringify({
9583
9829
  from: SUPERVISOR_RUN_ID,
@@ -9593,7 +9839,8 @@ function createHostMessageProvider(options) {
9593
9839
  message_id: messageId,
9594
9840
  generation,
9595
9841
  body,
9596
- no_reply: true
9842
+ no_reply: true,
9843
+ ...expected ? { expected_sentinel_file: expected } : {}
9597
9844
  });
9598
9845
  } catch (error) {
9599
9846
  const outcome = controlOutcome(error);
@@ -9608,7 +9855,8 @@ function createHostMessageProvider(options) {
9608
9855
  messageId: firstNonEmpty(result.message_id, messageId)
9609
9856
  };
9610
9857
  },
9611
- async ask(targetRunId, message, timeoutMs, signal) {
9858
+ async ask(targetRunId, message, timeoutMs, signal, publicRunId) {
9859
+ const expected = expectedSentinel(publicRunId);
9612
9860
  const messageId = newMsgID();
9613
9861
  const body = JSON.stringify({
9614
9862
  id: messageId,
@@ -9625,7 +9873,8 @@ function createHostMessageProvider(options) {
9625
9873
  to_run_id: targetRunId,
9626
9874
  message_id: messageId,
9627
9875
  generation,
9628
- body
9876
+ body,
9877
+ ...expected ? { expected_sentinel_file: expected } : {}
9629
9878
  });
9630
9879
  } catch (error) {
9631
9880
  const outcome = controlOutcome(error);
@@ -9754,10 +10003,20 @@ function createHostMessageProvider(options) {
9754
10003
  previousLabel: renamed.previous_label
9755
10004
  };
9756
10005
  },
9757
- 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
+ }
9758
10017
  let last;
9759
10018
  try {
9760
- last = await client.sessionLast(runId);
10019
+ last = await client.sessionLast(runId, expected);
9761
10020
  } catch (error) {
9762
10021
  throw authorityError(error);
9763
10022
  }
@@ -9771,7 +10030,11 @@ function createHostMessageProvider(options) {
9771
10030
  };
9772
10031
  },
9773
10032
  async peers() {
9774
- return { peers: await listedPeers() };
10033
+ const { peers, detail } = await listedPeers();
10034
+ return {
10035
+ peers,
10036
+ detail
10037
+ };
9775
10038
  },
9776
10039
  async recordLastTurn(text, stopReason, truncated) {
9777
10040
  await client.sessionLastSet(SUPERVISOR_RUN_ID, text, stopReason, truncated);
@@ -9842,13 +10105,20 @@ function renderReply(reply) {
9842
10105
  /**
9843
10106
  * Build a child provider over the surface's inherited broker. A child never
9844
10107
  * learns a public run id, and it cannot read a sentinel, so it is never
9845
- * resumable from here: the supervisor owns a resume.
10108
+ * resumable from here: the supervisor owns a resume. Listing-backed lookups
10109
+ * fail fast: an incomplete listing refuses every resolution and state read,
10110
+ * even for a peer the partial set contains, so a wire `peers_error` resolves
10111
+ * the same way the Go providers do. Self resolution bypasses the listing.
9846
10112
  */
9847
10113
  function createChildMessageProvider(options) {
9848
10114
  const { surface, runId, broker, pendingAsks, pollGeneration } = options;
9849
10115
  const claims = /* @__PURE__ */ new Map();
9850
10116
  async function listedPeers() {
9851
- return (await broker.peers()).peers;
10117
+ const result = await broker.peers();
10118
+ return {
10119
+ peers: result.peers,
10120
+ detail: result.detail
10121
+ };
9852
10122
  }
9853
10123
  const provider = {
9854
10124
  caller: () => ({
@@ -9867,7 +10137,8 @@ function createChildMessageProvider(options) {
9867
10137
  label: ref,
9868
10138
  match: "unique"
9869
10139
  };
9870
- const peers = await listedPeers();
10140
+ const { peers, detail } = await listedPeers();
10141
+ if (detail) throw new Error(`the peer listing is incomplete: ${detail}`);
9871
10142
  const exact = peers.find((peer) => peer.run_id === ref);
9872
10143
  if (exact) return {
9873
10144
  runId: exact.run_id,
@@ -9891,14 +10162,11 @@ function createChildMessageProvider(options) {
9891
10162
  };
9892
10163
  },
9893
10164
  async state(targetRunId) {
9894
- const peer = (await listedPeers()).find((entry) => entry.run_id === targetRunId);
10165
+ const { peers, detail } = await listedPeers();
10166
+ if (detail) throw new Error(`the peer listing is incomplete: ${detail}`);
10167
+ const peer = peers.find((entry) => entry.run_id === targetRunId);
9895
10168
  if (!peer) throw new Error(`run ${targetRunId} is not visible`);
9896
- const translation = translateRunFacts(peer.status ?? "", "");
9897
- return {
9898
- live: translation.live,
9899
- resumable: false,
9900
- status: translation.status
9901
- };
10169
+ return peerFacts(peer);
9902
10170
  },
9903
10171
  tell: (targetRunId, message) => broker.tell(targetRunId, message, newMsgID()),
9904
10172
  ask: (targetRunId, message, timeoutMs, signal) => broker.ask(targetRunId, message, newMsgID(), timeoutMs, signal),
@@ -9920,7 +10188,11 @@ function createChildMessageProvider(options) {
9920
10188
  }
9921
10189
  //#endregion
9922
10190
  //#region src/tools/peers.ts
9923
- /** 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. */
9924
10196
  async function peersTool(args, provider) {
9925
10197
  return executePeers(provider, args);
9926
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.16.1",
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",