@scotthuang/agent-knock-knock 0.3.0-beta.2 → 0.3.1

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/src/cli.js CHANGED
@@ -35,7 +35,18 @@ const DEFAULT_AGENT_HARD_TIMEOUT_MINUTES = 720;
35
35
  const DEFAULT_MONITOR_POLL_INTERVAL_MS = 5000;
36
36
  const CLAUDE_SCREEN_APPROVAL_TTL_MS = 10 * 60 * 1000;
37
37
  const CALLBACK_DELIVERY_TIMEOUT_MS = 30_000;
38
+ const CALLBACK_AGENT_WAIT_TIMEOUT_MS = 20_000;
39
+ const CALLBACK_AGENT_WAIT_CLI_TIMEOUT_MS = 25_000;
40
+ const CALLBACK_AGENT_WAIT_PROCESS_TIMEOUT_MS = 30_000;
38
41
  const CALLBACK_RETRY_DELAYS_MS = [5000, 15000, 60000, 60000];
42
+ const TERMINAL_BRIDGE_SUPERSEDE_STATUSES = new Set([
43
+ "created",
44
+ "running",
45
+ "waiting_for_agent",
46
+ "waiting_for_openclaw",
47
+ "stalled",
48
+ "cancelling"
49
+ ]);
39
50
  const TERMINAL_BRIDGE_MONITOR_LOCK_VERSION = 1;
40
51
  const MINIMUM_NODE_VERSION = "22.14.0";
41
52
  const PRIVATE_LOCK_FILE_MODE = 0o600;
@@ -1044,29 +1055,42 @@ function createRuntimeTerminalAgentRegistry(options) {
1044
1055
  if (!isRecord(conversation)) {
1045
1056
  return undefined;
1046
1057
  }
1047
- const contextMatch = await loadCodexTerminalContext({
1058
+ const contextMatches = await loadCodexTerminalContexts({
1048
1059
  conversation,
1049
1060
  nativeTakeover,
1050
1061
  options
1051
1062
  });
1052
- if (!contextMatch?.context) {
1053
- return undefined;
1054
- }
1055
- const evidence = detectCodexDurableCompletion({
1056
- ...request,
1057
- context: contextMatch.context
1058
- });
1059
- return evidence
1060
- ? {
1061
- ...evidence,
1062
- confidence: contextMatch.confidence,
1063
- metadata: {
1064
- ...evidence.metadata,
1065
- context_match: contextMatch.match,
1066
- session: contextMatch.context.source
1063
+ const matches = [];
1064
+ const detectionErrors = [];
1065
+ for (const contextMatch of contextMatches) {
1066
+ try {
1067
+ const evidence = detectCodexDurableCompletion({
1068
+ ...request,
1069
+ context: contextMatch.context
1070
+ });
1071
+ if (evidence) {
1072
+ matches.push({
1073
+ ...evidence,
1074
+ confidence: contextMatch.confidence,
1075
+ metadata: {
1076
+ ...evidence.metadata,
1077
+ context_match: contextMatch.match,
1078
+ session: contextMatch.context.source
1079
+ }
1080
+ });
1067
1081
  }
1068
1082
  }
1069
- : undefined;
1083
+ catch (error) {
1084
+ detectionErrors.push(error instanceof Error ? error.message : String(error));
1085
+ }
1086
+ }
1087
+ if (detectionErrors.length > 0) {
1088
+ throw new Error(`could not inspect every plausible Codex completion: ${detectionErrors.join("; ")}`);
1089
+ }
1090
+ if (matches.length > 1) {
1091
+ throw new Error("multiple same-cwd Codex sessions match the managed terminal request");
1092
+ }
1093
+ return matches[0];
1070
1094
  }
1071
1095
  }),
1072
1096
  createClaudeTerminalAgentAdapter({
@@ -4560,6 +4584,39 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
4560
4584
  }
4561
4585
  throw error;
4562
4586
  }
4587
+ let terminalCompletionReconciliation = {
4588
+ prepared: [],
4589
+ reconciledConversationIds: [],
4590
+ protectedConversationIds: [],
4591
+ skipSupersede: false
4592
+ };
4593
+ if (bridge) {
4594
+ try {
4595
+ terminalCompletionReconciliation =
4596
+ await reconcileTerminalBridgeCompletionsBeforeSupersede({
4597
+ options,
4598
+ storeDir: storeDirFromOptions(options),
4599
+ terminalControl,
4600
+ replacementConversationId: conversation.conversation_id
4601
+ });
4602
+ }
4603
+ catch (error) {
4604
+ terminalCompletionReconciliation.skipSupersede = true;
4605
+ terminalCompletionReconciliation.protectedConversationIds =
4606
+ fenceTerminalBridgeConversationsForReconciliation({
4607
+ storeDir: storeDirFromOptions(options),
4608
+ terminalControl,
4609
+ replacementConversationId: conversation.conversation_id
4610
+ });
4611
+ appendEvent(logPath, {
4612
+ ts: new Date().toISOString(),
4613
+ conversation_id: conversation.conversation_id,
4614
+ event: "terminal_bridge_pre_supersede_reconciliation_failed",
4615
+ terminal_control: terminalControl,
4616
+ error: error instanceof Error ? error.message : String(error)
4617
+ });
4618
+ }
4619
+ }
4563
4620
  const bridgeConversation = bridge
4564
4621
  ? withTerminalBridgeState({
4565
4622
  conversation: conversationWithHookLease,
@@ -4583,11 +4640,16 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
4583
4640
  stateLockHeld: terminalStateLockHeld
4584
4641
  });
4585
4642
  saveState(statePath, deliveredConversation);
4586
- const supersededConversationIds = bridge
4643
+ const supersededConversationIds = bridge &&
4644
+ !terminalCompletionReconciliation.skipSupersede
4587
4645
  ? supersedeTerminalBridgeConversations({
4588
4646
  storeDir: storeDirFromOptions(options),
4589
4647
  terminalControl,
4590
- replacementConversationId: conversation.conversation_id
4648
+ replacementConversationId: conversation.conversation_id,
4649
+ excludedConversationIds: [
4650
+ ...terminalCompletionReconciliation.reconciledConversationIds,
4651
+ ...terminalCompletionReconciliation.protectedConversationIds
4652
+ ]
4591
4653
  })
4592
4654
  : [];
4593
4655
  if (recordRawAttachmentAfterSend) {
@@ -4631,6 +4693,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
4631
4693
  terminal_control: terminalControl,
4632
4694
  message: textSummary(message.body),
4633
4695
  payload: textSummary(terminalPayload),
4696
+ reconciled_conversation_ids: terminalCompletionReconciliation.reconciledConversationIds,
4697
+ reconciliation_protected_conversation_ids: terminalCompletionReconciliation.protectedConversationIds,
4634
4698
  superseded_conversation_ids: supersededConversationIds
4635
4699
  });
4636
4700
  runtimeLog("info", "terminal_message_send", {
@@ -4639,6 +4703,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
4639
4703
  terminal_target: terminalControl.target,
4640
4704
  message: textSummary(message.body),
4641
4705
  payload: textSummary(terminalPayload),
4706
+ reconciled_conversation_ids: terminalCompletionReconciliation.reconciledConversationIds,
4707
+ reconciliation_protected_conversation_ids: terminalCompletionReconciliation.protectedConversationIds,
4642
4708
  superseded_conversation_ids: supersededConversationIds
4643
4709
  });
4644
4710
  const bridgeMonitor = bridge
@@ -4682,6 +4748,11 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
4682
4748
  callbackExpected: Boolean(deliveredConversation.callback_command || deliveredConversation.gateway_method)
4683
4749
  })
4684
4750
  });
4751
+ if (terminalCompletionReconciliation.prepared.length > 0) {
4752
+ setImmediate(() => {
4753
+ deliverReconciledTerminalBridgeCallbacks(terminalCompletionReconciliation.prepared);
4754
+ });
4755
+ }
4685
4756
  }
4686
4757
  function terminalSubmissionPayload(payload) {
4687
4758
  return payload.trimEnd();
@@ -4770,18 +4841,272 @@ function createManagedTerminalConversationFromRawId({ options, conversationId, a
4770
4841
  message
4771
4842
  };
4772
4843
  }
4773
- function supersedeTerminalBridgeConversations({ storeDir, terminalControl, replacementConversationId }) {
4774
- const activeStatuses = new Set([
4775
- "created",
4776
- "running",
4777
- "waiting_for_agent",
4778
- "waiting_for_openclaw",
4779
- "stalled",
4780
- "cancelling"
4781
- ]);
4844
+ async function reconcileTerminalBridgeCompletionsBeforeSupersede({ options, storeDir, terminalControl, replacementConversationId }) {
4845
+ const prepared = [];
4846
+ const reconciledConversationIds = [];
4847
+ const protectedConversationIds = [];
4848
+ const registry = createRuntimeTerminalAgentRegistry(options);
4849
+ for (const listedConversation of listConversations(storeDir)) {
4850
+ if (listedConversation.conversation_id === replacementConversationId ||
4851
+ !TERMINAL_BRIDGE_SUPERSEDE_STATUSES.has(listedConversation.status)) {
4852
+ continue;
4853
+ }
4854
+ const listedTakeover = isRecord(listedConversation.native_session_takeover)
4855
+ ? listedConversation.native_session_takeover
4856
+ : undefined;
4857
+ const listedControl = terminalControlFromTakeover(listedTakeover);
4858
+ if (listedTakeover?.terminal_bridge !== true ||
4859
+ !listedControl ||
4860
+ listedControl.target !== terminalControl.target ||
4861
+ listedControl.socketPath !== terminalControl.socketPath ||
4862
+ !listedControl.capabilities.includes("durable_completion")) {
4863
+ continue;
4864
+ }
4865
+ const candidateStatePath = stringValue(listedConversation.state_path);
4866
+ if (!candidateStatePath) {
4867
+ continue;
4868
+ }
4869
+ const candidateLogPath = logPathForStatePath(candidateStatePath);
4870
+ let candidate = loadState(candidateStatePath);
4871
+ const candidateTakeover = isRecord(candidate.native_session_takeover)
4872
+ ? candidate.native_session_takeover
4873
+ : undefined;
4874
+ const candidateControl = terminalControlFromTakeover(candidateTakeover);
4875
+ const terminalMessageId = stringValue(candidateTakeover?.terminal_bridge_message_id);
4876
+ if (!TERMINAL_BRIDGE_SUPERSEDE_STATUSES.has(candidate.status) ||
4877
+ candidateTakeover?.terminal_bridge !== true ||
4878
+ !candidateControl ||
4879
+ candidateControl.target !== terminalControl.target ||
4880
+ candidateControl.socketPath !== terminalControl.socketPath ||
4881
+ !candidateControl.capabilities.includes("durable_completion") ||
4882
+ !terminalMessageId) {
4883
+ continue;
4884
+ }
4885
+ const fenced = fenceTerminalBridgeConversationForReconciliation({
4886
+ statePath: candidateStatePath,
4887
+ logPath: candidateLogPath,
4888
+ terminalControl: candidateControl,
4889
+ terminalMessageId,
4890
+ replacementConversationId
4891
+ });
4892
+ if (!fenced.fenced) {
4893
+ continue;
4894
+ }
4895
+ candidate = fenced.conversation;
4896
+ const executor = executorForConversation(candidate);
4897
+ const adapter = registry.require(executor.kind);
4898
+ if (adapter.capabilities.durableCompletion !== true ||
4899
+ typeof adapter.detectDurableCompletion !== "function") {
4900
+ continue;
4901
+ }
4902
+ let completion;
4903
+ try {
4904
+ completion = await adapter.detectDurableCompletion(terminalDurableRequestForConversation(candidate, candidateControl));
4905
+ }
4906
+ catch (error) {
4907
+ protectedConversationIds.push(candidate.conversation_id);
4908
+ appendEvent(candidateLogPath, {
4909
+ ts: new Date().toISOString(),
4910
+ conversation_id: candidate.conversation_id,
4911
+ event: "terminal_bridge_pre_supersede_reconciliation_failed",
4912
+ terminal_control: candidateControl,
4913
+ terminal_bridge_message_id: terminalMessageId,
4914
+ error: error instanceof Error ? error.message : String(error)
4915
+ });
4916
+ continue;
4917
+ }
4918
+ if (!completion || completion.source !== "durable") {
4919
+ continue;
4920
+ }
4921
+ let preparedCompletion;
4922
+ try {
4923
+ preparedCompletion = prepareTerminalBridgeCompletionCallback({
4924
+ options,
4925
+ statePath: candidateStatePath,
4926
+ logPath: candidateLogPath,
4927
+ conversation: candidate,
4928
+ executor,
4929
+ terminalControl: candidateControl,
4930
+ terminalMessageId,
4931
+ completion,
4932
+ allowSupersedeRecovery: true
4933
+ });
4934
+ }
4935
+ catch (error) {
4936
+ protectedConversationIds.push(candidate.conversation_id);
4937
+ appendEvent(candidateLogPath, {
4938
+ ts: new Date().toISOString(),
4939
+ conversation_id: candidate.conversation_id,
4940
+ event: "terminal_bridge_pre_supersede_reconciliation_failed",
4941
+ terminal_control: candidateControl,
4942
+ terminal_bridge_message_id: terminalMessageId,
4943
+ error: error instanceof Error ? error.message : String(error)
4944
+ });
4945
+ continue;
4946
+ }
4947
+ if (!preparedCompletion.claimed) {
4948
+ const latest = preparedCompletion.conversation;
4949
+ if (TERMINAL_BRIDGE_SUPERSEDE_STATUSES.has(latest.status)) {
4950
+ protectedConversationIds.push(candidate.conversation_id);
4951
+ }
4952
+ else {
4953
+ reconciledConversationIds.push(candidate.conversation_id);
4954
+ }
4955
+ continue;
4956
+ }
4957
+ const reconciledAt = new Date().toISOString();
4958
+ appendEvent(candidateLogPath, {
4959
+ ts: reconciledAt,
4960
+ conversation_id: candidate.conversation_id,
4961
+ event: "terminal_bridge_completion_reconciled_before_supersede",
4962
+ terminal_control: candidateControl,
4963
+ terminal_bridge_message_id: terminalMessageId,
4964
+ callback_message_id: preparedCompletion.callbackMessageId,
4965
+ replacement_conversation_id: replacementConversationId
4966
+ });
4967
+ reconciledConversationIds.push(candidate.conversation_id);
4968
+ prepared.push({
4969
+ conversationId: candidate.conversation_id,
4970
+ statePath: candidateStatePath,
4971
+ logPath: candidateLogPath,
4972
+ terminalControl: candidateControl,
4973
+ prepared: preparedCompletion.prepared
4974
+ });
4975
+ }
4976
+ return {
4977
+ prepared,
4978
+ reconciledConversationIds,
4979
+ protectedConversationIds,
4980
+ skipSupersede: false
4981
+ };
4982
+ }
4983
+ function fenceTerminalBridgeConversationsForReconciliation({ storeDir, terminalControl, replacementConversationId }) {
4984
+ const fencedConversationIds = [];
4985
+ for (const listedConversation of listConversations(storeDir)) {
4986
+ if (listedConversation.conversation_id === replacementConversationId ||
4987
+ !TERMINAL_BRIDGE_SUPERSEDE_STATUSES.has(listedConversation.status)) {
4988
+ continue;
4989
+ }
4990
+ const statePath = stringValue(listedConversation.state_path);
4991
+ const listedTakeover = isRecord(listedConversation.native_session_takeover)
4992
+ ? listedConversation.native_session_takeover
4993
+ : undefined;
4994
+ const listedControl = terminalControlFromTakeover(listedTakeover);
4995
+ const terminalMessageId = stringValue(listedTakeover?.terminal_bridge_message_id);
4996
+ if (!statePath ||
4997
+ listedTakeover?.terminal_bridge !== true ||
4998
+ !listedControl ||
4999
+ listedControl.target !== terminalControl.target ||
5000
+ listedControl.socketPath !== terminalControl.socketPath ||
5001
+ !terminalMessageId) {
5002
+ continue;
5003
+ }
5004
+ const result = fenceTerminalBridgeConversationForReconciliation({
5005
+ statePath,
5006
+ logPath: logPathForStatePath(statePath),
5007
+ terminalControl: listedControl,
5008
+ terminalMessageId,
5009
+ replacementConversationId
5010
+ });
5011
+ if (result.fenced) {
5012
+ fencedConversationIds.push(listedConversation.conversation_id);
5013
+ }
5014
+ }
5015
+ return fencedConversationIds;
5016
+ }
5017
+ function fenceTerminalBridgeConversationForReconciliation({ statePath, logPath, terminalControl, terminalMessageId, replacementConversationId }) {
5018
+ const releaseLock = acquireFileLock(`${statePath}.lock`);
5019
+ try {
5020
+ const conversation = loadState(statePath);
5021
+ const nativeTakeover = isRecord(conversation.native_session_takeover)
5022
+ ? conversation.native_session_takeover
5023
+ : undefined;
5024
+ const currentControl = terminalControlFromTakeover(nativeTakeover);
5025
+ if (!TERMINAL_BRIDGE_SUPERSEDE_STATUSES.has(conversation.status) ||
5026
+ nativeTakeover?.terminal_bridge !== true ||
5027
+ currentControl?.target !== terminalControl.target ||
5028
+ currentControl?.socketPath !== terminalControl.socketPath ||
5029
+ stringValue(nativeTakeover?.terminal_bridge_message_id) !==
5030
+ terminalMessageId) {
5031
+ return {
5032
+ fenced: false,
5033
+ conversation
5034
+ };
5035
+ }
5036
+ const existingFence = isRecord(nativeTakeover.terminal_bridge_reconciliation_fence)
5037
+ ? nativeTakeover.terminal_bridge_reconciliation_fence
5038
+ : undefined;
5039
+ if (conversation.status === "stalled" &&
5040
+ existingFence?.replacement_conversation_id === replacementConversationId) {
5041
+ return {
5042
+ fenced: true,
5043
+ conversation
5044
+ };
5045
+ }
5046
+ const fencedAt = new Date().toISOString();
5047
+ const fencedConversation = {
5048
+ ...conversation,
5049
+ status: "stalled",
5050
+ stalled_reason: "terminal bridge paused because a newer task reused the same terminal before durable completion was resolved",
5051
+ native_session_takeover: {
5052
+ ...nativeTakeover,
5053
+ terminal_bridge_reconciliation_fence: {
5054
+ replacement_conversation_id: replacementConversationId,
5055
+ terminal_bridge_message_id: terminalMessageId,
5056
+ previous_status: conversation.status,
5057
+ fenced_at: fencedAt
5058
+ }
5059
+ },
5060
+ updated_at: fencedAt
5061
+ };
5062
+ saveState(statePath, fencedConversation);
5063
+ appendEvent(logPath, {
5064
+ ts: fencedAt,
5065
+ conversation_id: conversation.conversation_id,
5066
+ event: "terminal_bridge_reconciliation_fenced",
5067
+ terminal_control: terminalControl,
5068
+ terminal_bridge_message_id: terminalMessageId,
5069
+ previous_status: conversation.status,
5070
+ replacement_conversation_id: replacementConversationId
5071
+ });
5072
+ return {
5073
+ fenced: true,
5074
+ conversation: fencedConversation
5075
+ };
5076
+ }
5077
+ finally {
5078
+ releaseLock();
5079
+ }
5080
+ }
5081
+ function deliverReconciledTerminalBridgeCallbacks(reconciledCallbacks) {
5082
+ for (const callback of reconciledCallbacks) {
5083
+ try {
5084
+ runPreparedCallback(callback.prepared, { emit: false });
5085
+ }
5086
+ catch (error) {
5087
+ appendEvent(callback.logPath, {
5088
+ ts: new Date().toISOString(),
5089
+ conversation_id: callback.conversationId,
5090
+ event: "terminal_bridge_reconciled_callback_delivery_failed",
5091
+ terminal_control: callback.terminalControl,
5092
+ error: error instanceof Error ? error.message : String(error)
5093
+ });
5094
+ runtimeLog("warn", "terminal_bridge_reconciled_callback_delivery_failed", {
5095
+ conversation_id: callback.conversationId,
5096
+ terminal_target: callback.terminalControl.target,
5097
+ state_path: callback.statePath,
5098
+ error: error instanceof Error ? error.message : String(error)
5099
+ });
5100
+ }
5101
+ }
5102
+ }
5103
+ function supersedeTerminalBridgeConversations({ storeDir, terminalControl, replacementConversationId, excludedConversationIds = [] }) {
5104
+ const excluded = new Set(excludedConversationIds);
4782
5105
  const superseded = [];
4783
5106
  for (const candidate of listConversations(storeDir)) {
4784
- if (candidate.conversation_id === replacementConversationId || !activeStatuses.has(candidate.status)) {
5107
+ if (candidate.conversation_id === replacementConversationId ||
5108
+ excluded.has(candidate.conversation_id) ||
5109
+ !TERMINAL_BRIDGE_SUPERSEDE_STATUSES.has(candidate.status)) {
4785
5110
  continue;
4786
5111
  }
4787
5112
  const candidateTakeover = isRecord(candidate.native_session_takeover)
@@ -4800,7 +5125,7 @@ function supersedeTerminalBridgeConversations({ storeDir, terminalControl, repla
4800
5125
  const releaseLock = acquireFileLock(`${candidateStatePath}.lock`);
4801
5126
  try {
4802
5127
  const current = loadState(candidateStatePath);
4803
- if (!activeStatuses.has(current.status)) {
5128
+ if (!TERMINAL_BRIDGE_SUPERSEDE_STATUSES.has(current.status)) {
4804
5129
  continue;
4805
5130
  }
4806
5131
  const currentTakeover = isRecord(current.native_session_takeover)
@@ -7275,99 +7600,29 @@ async function runTerminalBridgeMonitorWithLock(options) {
7275
7600
  const completionStable = completionFingerprint !== undefined && completionFingerprint === idleCompletionFingerprint;
7276
7601
  idleCompletionFingerprint = completionFingerprint;
7277
7602
  if (completion && completionStable && completionFingerprint) {
7278
- const completionOutcome = completion.outcome === "failure" ? "failure" : "success";
7279
- const callbackMessageId = deterministicTerminalCallbackMessageId({
7280
- conversationId: conversation.conversation_id,
7281
- terminalMessageId: currentMessageId,
7282
- completionFingerprint,
7283
- outcome: completionOutcome
7284
- });
7285
- const claim = claimTerminalBridgeCompletion({
7603
+ const preparedCompletion = prepareTerminalBridgeCompletionCallback({
7604
+ options,
7286
7605
  statePath,
7287
7606
  logPath,
7607
+ conversation,
7608
+ executor,
7609
+ terminalControl,
7288
7610
  terminalMessageId: currentMessageId,
7289
- completionFingerprint,
7290
- completionId: completion.id,
7291
- callbackMessageId,
7292
- outcome: completionOutcome
7611
+ completion,
7612
+ completionFingerprint
7293
7613
  });
7294
- if (!claim.claimed) {
7614
+ if (!preparedCompletion.claimed) {
7295
7615
  printJson({
7296
- conversation: claim.conversation,
7616
+ conversation: preparedCompletion.conversation,
7297
7617
  monitored: true,
7298
7618
  terminal_bridge: true,
7299
7619
  completed: false,
7300
7620
  duplicate: true,
7301
- reason: claim.reason
7621
+ reason: preparedCompletion.reason
7302
7622
  });
7303
7623
  return;
7304
7624
  }
7305
- let preparedCallback;
7306
- try {
7307
- conversation = claim.conversation;
7308
- appendEvent(logPath, {
7309
- ts: new Date().toISOString(),
7310
- conversation_id: conversation.conversation_id,
7311
- event: "terminal_bridge_completion_detected",
7312
- terminal_control: terminalControl,
7313
- match: completionMatch,
7314
- completion_source: completion.source,
7315
- completion_outcome: completionOutcome,
7316
- completion_id: completion.id,
7317
- terminal_session: completionMetadata.session,
7318
- context_match: completionMetadata.context_match,
7319
- assistant_timestamp: completion?.timestamp,
7320
- rollout_turn_id: completion.source === "durable" ? completion.id : undefined,
7321
- terminal_bridge_message_id: currentMessageId,
7322
- callback_message_id: callbackMessageId
7323
- });
7324
- const callbackMessage = {
7325
- ...createMessage({
7326
- conversation,
7327
- from: executor.actor,
7328
- to: "openclaw",
7329
- type: completionOutcome === "failure" ? "error" : "done",
7330
- requiresResponse: false,
7331
- body: completion.text,
7332
- metadata: {
7333
- source: "terminal_bridge",
7334
- terminal_control: terminalControl,
7335
- ...completionMetadata,
7336
- completion_source: completion.source,
7337
- completion_outcome: completionOutcome,
7338
- completion_id: completion.id,
7339
- terminal_session: completionMetadata.session,
7340
- confidence: completion.confidence,
7341
- match: completionMatch,
7342
- assistant_timestamp: completion?.timestamp,
7343
- rollout_turn_id: completion.source === "durable" ? completion.id : undefined,
7344
- terminal_bridge_message_id: currentMessageId
7345
- }
7346
- }),
7347
- id: callbackMessageId
7348
- };
7349
- preparedCallback = prepareLockedCallback({
7350
- ...options,
7351
- statePath,
7352
- log: logPath,
7353
- closeTerminalBridgeOnDone: completionOutcome === "success",
7354
- trackCallbackDelivery: true,
7355
- recoverTerminalCompletion: claim.resumed === true,
7356
- preserveMessageId: true,
7357
- messageJson: JSON.stringify(callbackMessage),
7358
- gatewayMethod: conversation.gateway_method,
7359
- gatewaySession: conversation.gateway_session,
7360
- openclawSession: conversation.openclaw_session,
7361
- openclawBin: conversation.openclaw_bin,
7362
- gatewayUrl: stringValue(conversation.gateway_token) ? conversation.gateway_url : undefined,
7363
- token: stringValue(conversation.gateway_token)
7364
- });
7365
- }
7366
- finally {
7367
- releaseClaudeHookLease(conversation);
7368
- claim.release();
7369
- }
7370
- runPreparedCallback(preparedCallback);
7625
+ runPreparedCallback(preparedCompletion.prepared);
7371
7626
  return;
7372
7627
  }
7373
7628
  // A concrete approval or completion observed on this poll wins over a timeout boundary.
@@ -7531,14 +7786,132 @@ function deterministicTerminalCallbackMessageId({ conversationId, terminalMessag
7531
7786
  .slice(0, 32);
7532
7787
  return `msg-terminal-${digest}`;
7533
7788
  }
7534
- function claimTerminalBridgeCompletion({ statePath, logPath, terminalMessageId, completionFingerprint, completionId, callbackMessageId, outcome }) {
7789
+ function terminalBridgeCompletionFingerprint({ completion, terminalMessageId }) {
7790
+ const metadata = isRecord(completion.metadata) ? completion.metadata : {};
7791
+ const match = stringValue(metadata.match) ??
7792
+ (completion.source === "screen" ? "terminal_screen" : "durable_completion");
7793
+ return createHash("sha256")
7794
+ .update(JSON.stringify({
7795
+ text: completion.text,
7796
+ timestamp: completion.timestamp,
7797
+ match,
7798
+ source: completion.source,
7799
+ id: completion.id,
7800
+ message_id: terminalMessageId
7801
+ }))
7802
+ .digest("hex");
7803
+ }
7804
+ function prepareTerminalBridgeCompletionCallback({ options, statePath, logPath, conversation, executor, terminalControl, terminalMessageId, completion, allowSupersedeRecovery = false, completionFingerprint = terminalBridgeCompletionFingerprint({
7805
+ completion,
7806
+ terminalMessageId
7807
+ }) }) {
7808
+ const completionMetadata = isRecord(completion.metadata) ? completion.metadata : {};
7809
+ const completionMatch = stringValue(completionMetadata.match) ??
7810
+ (completion.source === "screen" ? "terminal_screen" : "durable_completion");
7811
+ const completionOutcome = completion.outcome === "failure" ? "failure" : "success";
7812
+ const callbackMessageId = deterministicTerminalCallbackMessageId({
7813
+ conversationId: conversation.conversation_id,
7814
+ terminalMessageId,
7815
+ completionFingerprint,
7816
+ outcome: completionOutcome
7817
+ });
7818
+ const claim = claimTerminalBridgeCompletion({
7819
+ statePath,
7820
+ logPath,
7821
+ terminalMessageId,
7822
+ completionFingerprint,
7823
+ completionId: completion.id,
7824
+ callbackMessageId,
7825
+ outcome: completionOutcome,
7826
+ allowSupersedeRecovery
7827
+ });
7828
+ if (!claim.claimed) {
7829
+ return claim;
7830
+ }
7831
+ let claimedConversation = claim.conversation;
7832
+ try {
7833
+ appendEvent(logPath, {
7834
+ ts: new Date().toISOString(),
7835
+ conversation_id: claimedConversation.conversation_id,
7836
+ event: "terminal_bridge_completion_detected",
7837
+ terminal_control: terminalControl,
7838
+ match: completionMatch,
7839
+ completion_source: completion.source,
7840
+ completion_outcome: completionOutcome,
7841
+ completion_id: completion.id,
7842
+ terminal_session: completionMetadata.session,
7843
+ context_match: completionMetadata.context_match,
7844
+ assistant_timestamp: completion.timestamp,
7845
+ rollout_turn_id: completion.source === "durable" ? completion.id : undefined,
7846
+ terminal_bridge_message_id: terminalMessageId,
7847
+ callback_message_id: callbackMessageId
7848
+ });
7849
+ const callbackMessage = {
7850
+ ...createMessage({
7851
+ conversation: claimedConversation,
7852
+ from: executor.actor,
7853
+ to: "openclaw",
7854
+ type: completionOutcome === "failure" ? "error" : "done",
7855
+ requiresResponse: false,
7856
+ body: completion.text,
7857
+ metadata: {
7858
+ source: "terminal_bridge",
7859
+ terminal_control: terminalControl,
7860
+ ...completionMetadata,
7861
+ completion_source: completion.source,
7862
+ completion_outcome: completionOutcome,
7863
+ completion_id: completion.id,
7864
+ terminal_session: completionMetadata.session,
7865
+ confidence: completion.confidence,
7866
+ match: completionMatch,
7867
+ assistant_timestamp: completion.timestamp,
7868
+ rollout_turn_id: completion.source === "durable" ? completion.id : undefined,
7869
+ terminal_bridge_message_id: terminalMessageId
7870
+ }
7871
+ }),
7872
+ id: callbackMessageId
7873
+ };
7874
+ const prepared = prepareLockedCallback({
7875
+ ...options,
7876
+ statePath,
7877
+ log: logPath,
7878
+ closeTerminalBridgeOnDone: completionOutcome === "success",
7879
+ trackCallbackDelivery: true,
7880
+ recoverTerminalCompletion: claim.resumed === true,
7881
+ allowTerminalCompletionRecoveryStatus: allowSupersedeRecovery,
7882
+ preserveMessageId: true,
7883
+ messageJson: JSON.stringify(callbackMessage),
7884
+ gatewayMethod: claimedConversation.gateway_method,
7885
+ gatewaySession: claimedConversation.gateway_session,
7886
+ openclawSession: claimedConversation.openclaw_session,
7887
+ openclawBin: claimedConversation.openclaw_bin,
7888
+ gatewayUrl: stringValue(claimedConversation.gateway_token)
7889
+ ? claimedConversation.gateway_url
7890
+ : undefined,
7891
+ token: stringValue(claimedConversation.gateway_token)
7892
+ });
7893
+ return {
7894
+ claimed: true,
7895
+ conversation: claimedConversation,
7896
+ prepared,
7897
+ callbackMessageId
7898
+ };
7899
+ }
7900
+ finally {
7901
+ releaseClaudeHookLease(claimedConversation);
7902
+ claim.release();
7903
+ }
7904
+ }
7905
+ function claimTerminalBridgeCompletion({ statePath, logPath, terminalMessageId, completionFingerprint, completionId, callbackMessageId, outcome, allowSupersedeRecovery = false }) {
7535
7906
  const release = acquireFileLock(`${statePath}.lock`);
7536
7907
  try {
7537
7908
  const conversation = loadState(statePath);
7538
7909
  const nativeTakeover = isRecord(conversation.native_session_takeover)
7539
7910
  ? conversation.native_session_takeover
7540
7911
  : {};
7541
- if (!isWaitingForAgent(conversation.status)) {
7912
+ if (!isWaitingForAgent(conversation.status) &&
7913
+ !(allowSupersedeRecovery &&
7914
+ TERMINAL_BRIDGE_SUPERSEDE_STATUSES.has(conversation.status))) {
7542
7915
  release();
7543
7916
  return {
7544
7917
  claimed: false,
@@ -7738,7 +8111,7 @@ function terminalBridgeApprovalCandidate({ executor, terminalControl, terminalSt
7738
8111
  : {})
7739
8112
  };
7740
8113
  }
7741
- async function loadCodexTerminalContext({ conversation, nativeTakeover, options }) {
8114
+ async function loadCodexTerminalContexts({ conversation, nativeTakeover, options }) {
7742
8115
  const provider = createAgentSessionProvider("codex", options);
7743
8116
  const nativeSessionId = stringValue(nativeTakeover?.["native_session_id"]);
7744
8117
  const startedAtMs = Date.parse(String(nativeTakeover?.["terminal_bridge_started_at"] ?? ""));
@@ -7753,17 +8126,17 @@ async function loadCodexTerminalContext({ conversation, nativeTakeover, options
7753
8126
  maxTextLength: Number(options.maxTextLength ?? 4000)
7754
8127
  });
7755
8128
  if (context) {
7756
- return {
7757
- context,
7758
- process: activeProcess,
7759
- match: activeProcess?.sessionId ? "process_session_id" : "native_session_id",
7760
- confidence: "high"
7761
- };
8129
+ return [{
8130
+ context,
8131
+ process: activeProcess,
8132
+ match: activeProcess?.sessionId ? "process_session_id" : "native_session_id",
8133
+ confidence: "high"
8134
+ }];
7762
8135
  }
7763
8136
  }
7764
8137
  const cwd = activeProcess?.cwd ?? stringValue(nativeTakeover?.["source_cwd"]);
7765
8138
  if (!cwd) {
7766
- return undefined;
8139
+ return [];
7767
8140
  }
7768
8141
  const sessions = (await provider.listHistoricalSessions())
7769
8142
  .filter((session) => session.cwd === cwd)
@@ -7771,28 +8144,40 @@ async function loadCodexTerminalContext({ conversation, nativeTakeover, options
7771
8144
  if (!Number.isFinite(startedAtMs)) {
7772
8145
  return true;
7773
8146
  }
7774
- return Number(session.updatedAtMs ?? 0) >= startedAtMs;
8147
+ if (session.updatedAtMs === undefined || session.updatedAtMs === null) {
8148
+ return true;
8149
+ }
8150
+ const updatedAtMs = Number(session.updatedAtMs);
8151
+ return !Number.isFinite(updatedAtMs) || updatedAtMs >= startedAtMs;
7775
8152
  })
7776
8153
  .sort((left, right) => Number(right.updatedAtMs ?? 0) - Number(left.updatedAtMs ?? 0));
7777
- const selected = sessions[0];
7778
- if (!selected) {
7779
- return undefined;
8154
+ const matches = [];
8155
+ const candidateErrors = [];
8156
+ for (const session of sessions) {
8157
+ try {
8158
+ const context = await provider.getForkContext({
8159
+ sessionId: session.id,
8160
+ maxMessages: Number(options.maxMessages ?? 16),
8161
+ maxCommands: Number(options.maxCommands ?? 10),
8162
+ maxTextLength: Number(options.maxTextLength ?? 4000)
8163
+ });
8164
+ if (context) {
8165
+ matches.push({
8166
+ context,
8167
+ process: activeProcess,
8168
+ match: sessions.length === 1 ? "cwd" : "cwd_request_hash",
8169
+ confidence: sessions.length === 1 ? "medium" : "low"
8170
+ });
8171
+ }
8172
+ }
8173
+ catch (error) {
8174
+ candidateErrors.push(`${session.id}: ${error instanceof Error ? error.message : String(error)}`);
8175
+ }
7780
8176
  }
7781
- const context = await provider.getForkContext({
7782
- sessionId: selected.id,
7783
- maxMessages: Number(options.maxMessages ?? 16),
7784
- maxCommands: Number(options.maxCommands ?? 10),
7785
- maxTextLength: Number(options.maxTextLength ?? 4000)
7786
- });
7787
- if (!context) {
7788
- return undefined;
8177
+ if (candidateErrors.length > 0) {
8178
+ throw new Error(`could not inspect every plausible same-cwd Codex session: ${candidateErrors.join("; ")}`);
7789
8179
  }
7790
- return {
7791
- context,
7792
- process: activeProcess,
7793
- match: sessions.length === 1 ? "cwd" : "cwd_latest",
7794
- confidence: sessions.length === 1 ? "medium" : "low"
7795
- };
8180
+ return matches;
7796
8181
  }
7797
8182
  function resolveExecutable(command) {
7798
8183
  if (command.includes(path.sep)) {
@@ -7986,7 +8371,9 @@ function prepareLockedCallback(options) {
7986
8371
  callbackDelivery.message.id !== message.id);
7987
8372
  const recoveringTerminalCompletion = options.recoverTerminalCompletion === true &&
7988
8373
  duplicateMessage &&
7989
- isWaitingForAgent(conversation.status);
8374
+ (isWaitingForAgent(conversation.status) ||
8375
+ (options.allowTerminalCompletionRecoveryStatus === true &&
8376
+ TERMINAL_BRIDGE_SUPERSEDE_STATUSES.has(conversation.status)));
7990
8377
  if (duplicateMessage &&
7991
8378
  !retryingPending &&
7992
8379
  !recoveringTerminalCompletion &&
@@ -8373,9 +8760,8 @@ function deliverCallbackToOpenClaw({ options, statePath, logPath, conversation,
8373
8760
  if (delivery.status !== 0) {
8374
8761
  throw new Error(delivery.stderr || delivery.stdout || `gateway method delivery failed with status ${delivery.status}`);
8375
8762
  }
8376
- const gatewayPayload = parseOptionalJson(delivery.stdout);
8377
- const chatSendParams = isRecord(gatewayPayload?.chat_send) ? gatewayPayload.chat_send : undefined;
8378
- const sessionSendParams = isRecord(gatewayPayload?.session_send) ? gatewayPayload.session_send : undefined;
8763
+ const gatewayPayload = parseRequiredGatewayDeliveryPayload(delivery.stdout);
8764
+ const { chatSendParams, sessionSendParams } = parseGatewayCallbackDeliveryPlan(gatewayPayload);
8379
8765
  if (chatSendParams) {
8380
8766
  const chatSendDelivery = deliverToChatSend({
8381
8767
  openclawBin: options.openclawBin,
@@ -8383,16 +8769,71 @@ function deliverCallbackToOpenClaw({ options, statePath, logPath, conversation,
8383
8769
  token: options.token,
8384
8770
  params: chatSendParams
8385
8771
  });
8772
+ if (chatSendDelivery.status !== 0) {
8773
+ recordCallbackProcessDelivery({
8774
+ logPath,
8775
+ conversation,
8776
+ message,
8777
+ event: "callback_chat_send_delivery",
8778
+ runtimeEvent: "callback_chat_send_delivery",
8779
+ delivery: chatSendDelivery
8780
+ });
8781
+ throw new Error(chatSendDelivery.stderr || chatSendDelivery.stdout || `chat callback delivery failed with status ${chatSendDelivery.status}`);
8782
+ }
8783
+ const chatSendAck = parseChatSendAcknowledgement(chatSendDelivery.stdout, String(chatSendParams.idempotencyKey));
8386
8784
  recordCallbackProcessDelivery({
8387
8785
  logPath,
8388
8786
  conversation,
8389
8787
  message,
8390
8788
  event: "callback_chat_send_delivery",
8391
8789
  runtimeEvent: "callback_chat_send_delivery",
8392
- delivery: chatSendDelivery
8790
+ delivery: chatSendDelivery,
8791
+ detail: {
8792
+ run_id: chatSendAck.runId,
8793
+ run_status: chatSendAck.status
8794
+ }
8393
8795
  });
8394
- if (chatSendDelivery.status !== 0) {
8395
- throw new Error(chatSendDelivery.stderr || chatSendDelivery.stdout || `chat callback delivery failed with status ${chatSendDelivery.status}`);
8796
+ if (chatSendAck.status === "ok") {
8797
+ return "gateway_method+chat_send";
8798
+ }
8799
+ const agentWaitDelivery = deliverToAgentWait({
8800
+ openclawBin: options.openclawBin,
8801
+ gatewayUrl: options.gatewayUrl,
8802
+ token: options.token,
8803
+ runId: chatSendAck.runId
8804
+ });
8805
+ if (agentWaitDelivery.status !== 0) {
8806
+ recordCallbackProcessDelivery({
8807
+ logPath,
8808
+ conversation,
8809
+ message,
8810
+ event: "callback_agent_wait_delivery",
8811
+ runtimeEvent: "callback_agent_wait_delivery",
8812
+ delivery: agentWaitDelivery,
8813
+ detail: { run_id: chatSendAck.runId }
8814
+ });
8815
+ throw new Error(agentWaitDelivery.stderr ||
8816
+ agentWaitDelivery.stdout ||
8817
+ `callback agent wait failed with status ${agentWaitDelivery.status}`);
8818
+ }
8819
+ const waitResult = parseAgentWaitResult(agentWaitDelivery.stdout, chatSendAck.runId);
8820
+ recordCallbackProcessDelivery({
8821
+ logPath,
8822
+ conversation,
8823
+ message,
8824
+ event: "callback_agent_wait_delivery",
8825
+ runtimeEvent: "callback_agent_wait_delivery",
8826
+ delivery: agentWaitDelivery,
8827
+ detail: {
8828
+ run_id: chatSendAck.runId,
8829
+ run_status: waitResult.status
8830
+ }
8831
+ });
8832
+ if (waitResult.status !== "ok") {
8833
+ const detail = stringValue(waitResult.error) ??
8834
+ stringValue(waitResult.stopReason) ??
8835
+ `agent.wait returned ${String(waitResult.status)}`;
8836
+ throw new Error(`callback Gateway run did not complete successfully: ${detail}`);
8396
8837
  }
8397
8838
  return "gateway_method+chat_send";
8398
8839
  }
@@ -8403,16 +8844,70 @@ function deliverCallbackToOpenClaw({ options, statePath, logPath, conversation,
8403
8844
  token: options.token,
8404
8845
  params: sessionSendParams
8405
8846
  });
8847
+ if (sessionSendDelivery.status !== 0) {
8848
+ recordCallbackProcessDelivery({
8849
+ logPath,
8850
+ conversation,
8851
+ message,
8852
+ event: "callback_session_send_delivery",
8853
+ runtimeEvent: "callback_session_send_delivery",
8854
+ delivery: sessionSendDelivery
8855
+ });
8856
+ throw new Error(sessionSendDelivery.stderr || sessionSendDelivery.stdout || `session callback delivery failed with status ${sessionSendDelivery.status}`);
8857
+ }
8858
+ const sessionSendAck = parseChatSendAcknowledgement(sessionSendDelivery.stdout, String(sessionSendParams.idempotencyKey));
8406
8859
  recordCallbackProcessDelivery({
8407
8860
  logPath,
8408
8861
  conversation,
8409
8862
  message,
8410
8863
  event: "callback_session_send_delivery",
8411
8864
  runtimeEvent: "callback_session_send_delivery",
8412
- delivery: sessionSendDelivery
8865
+ delivery: sessionSendDelivery,
8866
+ detail: {
8867
+ run_id: sessionSendAck.runId,
8868
+ run_status: sessionSendAck.status
8869
+ }
8413
8870
  });
8414
- if (sessionSendDelivery.status !== 0) {
8415
- throw new Error(sessionSendDelivery.stderr || sessionSendDelivery.stdout || `session callback delivery failed with status ${sessionSendDelivery.status}`);
8871
+ if (sessionSendAck.status !== "ok") {
8872
+ const agentWaitDelivery = deliverToAgentWait({
8873
+ openclawBin: options.openclawBin,
8874
+ gatewayUrl: options.gatewayUrl,
8875
+ token: options.token,
8876
+ runId: sessionSendAck.runId
8877
+ });
8878
+ if (agentWaitDelivery.status !== 0) {
8879
+ recordCallbackProcessDelivery({
8880
+ logPath,
8881
+ conversation,
8882
+ message,
8883
+ event: "callback_agent_wait_delivery",
8884
+ runtimeEvent: "callback_agent_wait_delivery",
8885
+ delivery: agentWaitDelivery,
8886
+ detail: { run_id: sessionSendAck.runId }
8887
+ });
8888
+ throw new Error(agentWaitDelivery.stderr ||
8889
+ agentWaitDelivery.stdout ||
8890
+ `callback agent wait failed with status ${agentWaitDelivery.status}`);
8891
+ }
8892
+ const waitResult = parseAgentWaitResult(agentWaitDelivery.stdout, sessionSendAck.runId);
8893
+ recordCallbackProcessDelivery({
8894
+ logPath,
8895
+ conversation,
8896
+ message,
8897
+ event: "callback_agent_wait_delivery",
8898
+ runtimeEvent: "callback_agent_wait_delivery",
8899
+ delivery: agentWaitDelivery,
8900
+ detail: {
8901
+ run_id: sessionSendAck.runId,
8902
+ run_status: waitResult.status
8903
+ }
8904
+ });
8905
+ if (waitResult.status !== "ok") {
8906
+ const detail = stringValue(waitResult.error) ??
8907
+ stringValue(waitResult.stopReason) ??
8908
+ `agent.wait returned ${String(waitResult.status)}`;
8909
+ throw new Error(`callback Gateway run did not complete successfully: ${detail}`);
8910
+ }
8416
8911
  }
8417
8912
  return "gateway_method+sessions_send";
8418
8913
  }
@@ -9704,6 +10199,43 @@ function deliverToChatSend({ openclawBin, gatewayUrl, token, params }) {
9704
10199
  stderr: result.stderr ?? ""
9705
10200
  };
9706
10201
  }
10202
+ function deliverToAgentWait({ openclawBin, gatewayUrl, token, runId }) {
10203
+ const args = [
10204
+ "gateway",
10205
+ "call",
10206
+ "agent.wait",
10207
+ "--params",
10208
+ JSON.stringify({
10209
+ runId,
10210
+ timeoutMs: CALLBACK_AGENT_WAIT_TIMEOUT_MS
10211
+ }),
10212
+ "--json",
10213
+ "--timeout",
10214
+ String(CALLBACK_AGENT_WAIT_CLI_TIMEOUT_MS)
10215
+ ];
10216
+ if (gatewayUrl) {
10217
+ args.push("--url", gatewayUrl);
10218
+ }
10219
+ const result = spawnSync(openclawBin ?? "openclaw", args, {
10220
+ encoding: "utf8",
10221
+ maxBuffer: 1024 * 1024 * 10,
10222
+ timeout: CALLBACK_AGENT_WAIT_PROCESS_TIMEOUT_MS,
10223
+ killSignal: "SIGKILL",
10224
+ env: openClawGatewayEnvironment(token)
10225
+ });
10226
+ if (result.error) {
10227
+ return {
10228
+ status: 1,
10229
+ stdout: result.stdout ?? "",
10230
+ stderr: result.error.message
10231
+ };
10232
+ }
10233
+ return {
10234
+ status: result.status ?? 1,
10235
+ stdout: result.stdout ?? "",
10236
+ stderr: result.stderr ?? ""
10237
+ };
10238
+ }
9707
10239
  function openClawGatewayEnvironment(token) {
9708
10240
  if (!token || token === "<token>") {
9709
10241
  return process.env;
@@ -9764,6 +10296,96 @@ function parseOptionalJson(text) {
9764
10296
  return undefined;
9765
10297
  }
9766
10298
  }
10299
+ function parseRequiredGatewayDeliveryPayload(text) {
10300
+ const payload = parseOptionalJson(text);
10301
+ if (!isRecord(payload)) {
10302
+ throw new Error("gateway callback returned malformed JSON");
10303
+ }
10304
+ if (payload.ok !== true) {
10305
+ throw new Error(`gateway callback was not accepted: ${stringValue(payload.error) ?? stringValue(payload.message) ?? "ok was not true"}`);
10306
+ }
10307
+ if (payload.delivery_required !== undefined &&
10308
+ typeof payload.delivery_required !== "boolean") {
10309
+ throw new Error("gateway callback returned an invalid delivery_required value");
10310
+ }
10311
+ return payload;
10312
+ }
10313
+ function parseGatewayCallbackDeliveryPlan(payload) {
10314
+ const chatSendParams = isRecord(payload.chat_send) ? payload.chat_send : undefined;
10315
+ const sessionSendParams = isRecord(payload.session_send) ? payload.session_send : undefined;
10316
+ if (chatSendParams && sessionSendParams) {
10317
+ throw new Error("gateway callback returned multiple delivery plans");
10318
+ }
10319
+ const deliveryRequired = payload.delivery_required === true;
10320
+ const deliveryExplicitlyNotRequired = payload.delivery_required === false;
10321
+ const deliveryMode = stringValue(payload.delivery_mode);
10322
+ if (deliveryRequired && !chatSendParams && !sessionSendParams) {
10323
+ throw new Error("gateway callback requires delivery but returned no supported chat_send or session_send plan");
10324
+ }
10325
+ if (deliveryExplicitlyNotRequired && (chatSendParams || sessionSendParams)) {
10326
+ throw new Error("gateway callback returned a delivery plan without delivery_required");
10327
+ }
10328
+ if (deliveryMode && deliveryMode !== "none") {
10329
+ const expectedMode = chatSendParams ? "chat.send" : sessionSendParams ? "sessions.send" : undefined;
10330
+ if (deliveryMode !== expectedMode) {
10331
+ throw new Error("gateway callback delivery_mode does not match its delivery plan");
10332
+ }
10333
+ }
10334
+ if (deliveryMode === "none" && deliveryRequired) {
10335
+ throw new Error("gateway callback delivery_mode none cannot require delivery");
10336
+ }
10337
+ if (chatSendParams) {
10338
+ if (!stringValue(chatSendParams.sessionKey) ||
10339
+ !stringValue(chatSendParams.message) ||
10340
+ !stringValue(chatSendParams.idempotencyKey) ||
10341
+ chatSendParams.deliver !== true) {
10342
+ throw new Error("gateway callback returned an invalid chat_send delivery plan");
10343
+ }
10344
+ }
10345
+ if (sessionSendParams) {
10346
+ if (!stringValue(sessionSendParams.key) ||
10347
+ !stringValue(sessionSendParams.message) ||
10348
+ !stringValue(sessionSendParams.idempotencyKey)) {
10349
+ throw new Error("gateway callback returned an invalid session_send delivery plan");
10350
+ }
10351
+ }
10352
+ return { chatSendParams, sessionSendParams };
10353
+ }
10354
+ function parseChatSendAcknowledgement(text, expectedRunId) {
10355
+ const payload = parseOptionalJson(text);
10356
+ if (!isRecord(payload)) {
10357
+ throw new Error("chat.send returned malformed JSON");
10358
+ }
10359
+ const runId = stringValue(payload.runId);
10360
+ const status = stringValue(payload.status);
10361
+ if (!runId) {
10362
+ throw new Error("chat.send acknowledgement is missing runId");
10363
+ }
10364
+ if (runId !== expectedRunId) {
10365
+ throw new Error("chat.send acknowledgement runId does not match its idempotencyKey");
10366
+ }
10367
+ if (!status || !["started", "in_flight", "ok"].includes(status)) {
10368
+ throw new Error(`chat.send returned unexpected status ${JSON.stringify(status ?? null)}`);
10369
+ }
10370
+ return {
10371
+ runId,
10372
+ status: status
10373
+ };
10374
+ }
10375
+ function parseAgentWaitResult(text, expectedRunId) {
10376
+ const payload = parseOptionalJson(text);
10377
+ if (!isRecord(payload)) {
10378
+ throw new Error("agent.wait returned malformed JSON");
10379
+ }
10380
+ if (stringValue(payload.runId) !== expectedRunId) {
10381
+ throw new Error("agent.wait returned a result for a different runId");
10382
+ }
10383
+ const status = stringValue(payload.status);
10384
+ if (!status || !["ok", "error", "timeout", "pending"].includes(status)) {
10385
+ throw new Error(`agent.wait returned unexpected status ${JSON.stringify(status ?? null)}`);
10386
+ }
10387
+ return payload;
10388
+ }
9767
10389
  function createAgentSessionProvider(agent, options) {
9768
10390
  if (agent !== "codex") {
9769
10391
  throw new Error(`unsupported agent session provider: ${agent}`);