@scotthuang/agent-knock-knock 0.12.13 → 0.12.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +5 -5
  3. package/dist/src/callback-cli-adapter.d.ts +9 -6
  4. package/dist/src/callback-cli-adapter.js +119 -30
  5. package/dist/src/callback-cli-adapter.js.map +1 -1
  6. package/dist/src/callback-outbox-policy.d.ts +39 -0
  7. package/dist/src/callback-outbox-policy.js +163 -6
  8. package/dist/src/callback-outbox-policy.js.map +1 -1
  9. package/dist/src/callback-outbox-service.d.ts +28 -1
  10. package/dist/src/callback-outbox-service.js +413 -82
  11. package/dist/src/callback-outbox-service.js.map +1 -1
  12. package/dist/src/callback-outbox-settlement.d.ts +14 -1
  13. package/dist/src/callback-outbox-settlement.js +151 -30
  14. package/dist/src/callback-outbox-settlement.js.map +1 -1
  15. package/dist/src/callback-route-authority.d.ts +59 -0
  16. package/dist/src/callback-route-authority.js +123 -0
  17. package/dist/src/callback-route-authority.js.map +1 -0
  18. package/dist/src/callback-transport.d.ts +134 -0
  19. package/dist/src/callback-transport.js +256 -0
  20. package/dist/src/callback-transport.js.map +1 -0
  21. package/dist/src/cli-core.js +17 -4
  22. package/dist/src/cli-core.js.map +1 -1
  23. package/dist/src/codex-lifecycle-compatibility.js +2 -1
  24. package/dist/src/codex-lifecycle-compatibility.js.map +1 -1
  25. package/dist/src/deferred-foreground-application-service.d.ts +18 -0
  26. package/dist/src/deferred-foreground-application-service.js +72 -0
  27. package/dist/src/deferred-foreground-application-service.js.map +1 -1
  28. package/dist/src/deferred-foreground-authority-cli-adapter.js +2 -1
  29. package/dist/src/deferred-foreground-authority-cli-adapter.js.map +1 -1
  30. package/dist/src/deferred-foreground-recovery-cli-adapter.d.ts +1 -1
  31. package/dist/src/deferred-foreground-recovery-cli-adapter.js +108 -37
  32. package/dist/src/deferred-foreground-recovery-cli-adapter.js.map +1 -1
  33. package/dist/src/deferred-foreground-recovery-service.d.ts +1 -1
  34. package/dist/src/deferred-foreground-recovery-service.js +10 -3
  35. package/dist/src/deferred-foreground-recovery-service.js.map +1 -1
  36. package/dist/src/deferred-foreground-transfer-policy.d.ts +1 -1
  37. package/dist/src/deferred-foreground-transfer-policy.js +3 -2
  38. package/dist/src/deferred-foreground-transfer-policy.js.map +1 -1
  39. package/dist/src/deferred-foreground-transfer.d.ts +17 -1
  40. package/dist/src/deferred-foreground-transfer.js +244 -35
  41. package/dist/src/deferred-foreground-transfer.js.map +1 -1
  42. package/dist/src/deferred-foreground-user-close.d.ts +18 -0
  43. package/dist/src/deferred-foreground-user-close.js +180 -0
  44. package/dist/src/deferred-foreground-user-close.js.map +1 -0
  45. package/dist/src/openclaw-callback-transport.d.ts +4 -1
  46. package/dist/src/openclaw-callback-transport.js +403 -10
  47. package/dist/src/openclaw-callback-transport.js.map +1 -1
  48. package/dist/src/openclaw-managed-callback-cli-adapter.d.ts +23 -0
  49. package/dist/src/openclaw-managed-callback-cli-adapter.js +77 -0
  50. package/dist/src/openclaw-managed-callback-cli-adapter.js.map +1 -0
  51. package/dist/src/openclaw-plugin-command-adapter.js +28 -31
  52. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  53. package/dist/src/openclaw-plugin-helpers.js +8 -0
  54. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  55. package/dist/src/openclaw-plugin-schemas.d.ts +20 -1
  56. package/dist/src/openclaw-plugin-schemas.js +26 -2
  57. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  58. package/dist/src/protocol.d.ts +3 -0
  59. package/dist/src/protocol.js +4 -0
  60. package/dist/src/protocol.js.map +1 -1
  61. package/dist/src/store.d.ts +9 -0
  62. package/dist/src/store.js +79 -1
  63. package/dist/src/store.js.map +1 -1
  64. package/dist/src/terminal-acceptance-cli-adapter.d.ts +2 -0
  65. package/dist/src/terminal-acceptance-cli-adapter.js +62 -12
  66. package/dist/src/terminal-acceptance-cli-adapter.js.map +1 -1
  67. package/dist/src/terminal-action-projection.d.ts +1 -2
  68. package/dist/src/terminal-action-projection.js +4 -5
  69. package/dist/src/terminal-action-projection.js.map +1 -1
  70. package/dist/src/terminal-agent-bridge.d.ts +82 -0
  71. package/dist/src/terminal-agent-bridge.js +539 -111
  72. package/dist/src/terminal-agent-bridge.js.map +1 -1
  73. package/dist/src/terminal-command-cli-adapter.d.ts +9 -0
  74. package/dist/src/terminal-command-cli-adapter.js +2130 -5
  75. package/dist/src/terminal-command-cli-adapter.js.map +1 -1
  76. package/dist/src/terminal-dispatch-application.d.ts +4 -1
  77. package/dist/src/terminal-dispatch-application.js +11 -2
  78. package/dist/src/terminal-dispatch-application.js.map +1 -1
  79. package/dist/src/terminal-dispatch-execution.js +3 -1
  80. package/dist/src/terminal-dispatch-execution.js.map +1 -1
  81. package/dist/src/terminal-dispatch-ledger-codec.d.ts +3 -0
  82. package/dist/src/terminal-dispatch-ledger-codec.js +46 -19
  83. package/dist/src/terminal-dispatch-ledger-codec.js.map +1 -1
  84. package/dist/src/terminal-dispatch-presenter.js +5 -3
  85. package/dist/src/terminal-dispatch-presenter.js.map +1 -1
  86. package/dist/src/terminal-dispatch-receipt.d.ts +3 -0
  87. package/dist/src/terminal-dispatch-receipt.js +15 -1
  88. package/dist/src/terminal-dispatch-receipt.js.map +1 -1
  89. package/dist/src/terminal-dispatch-recovery-cli-adapter.js +68 -7
  90. package/dist/src/terminal-dispatch-recovery-cli-adapter.js.map +1 -1
  91. package/dist/src/terminal-dispatch-recovery-service.d.ts +2 -0
  92. package/dist/src/terminal-dispatch-recovery-service.js +9 -0
  93. package/dist/src/terminal-dispatch-recovery-service.js.map +1 -1
  94. package/dist/src/terminal-identity-authority-cli-adapter.d.ts +2 -0
  95. package/dist/src/terminal-identity-authority-cli-adapter.js +30 -27
  96. package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -1
  97. package/dist/src/terminal-list-cli-adapter.d.ts +1 -0
  98. package/dist/src/terminal-list-cli-adapter.js +50 -31
  99. package/dist/src/terminal-list-cli-adapter.js.map +1 -1
  100. package/dist/src/terminal-list-renderer.d.ts +9 -1
  101. package/dist/src/terminal-list-renderer.js +219 -98
  102. package/dist/src/terminal-list-renderer.js.map +1 -1
  103. package/dist/src/terminal-maintenance-cli-adapter.js +253 -326
  104. package/dist/src/terminal-maintenance-cli-adapter.js.map +1 -1
  105. package/dist/src/terminal-monitor-application-service.d.ts +5 -0
  106. package/dist/src/terminal-monitor-application-service.js +5 -0
  107. package/dist/src/terminal-monitor-application-service.js.map +1 -1
  108. package/dist/src/terminal-monitor-cli-adapter.d.ts +25 -1
  109. package/dist/src/terminal-monitor-cli-adapter.js +170 -27
  110. package/dist/src/terminal-monitor-cli-adapter.js.map +1 -1
  111. package/dist/src/terminal-monitor-reconciliation-eligibility.d.ts +1 -1
  112. package/dist/src/terminal-monitor-reconciliation-eligibility.js +220 -32
  113. package/dist/src/terminal-monitor-reconciliation-eligibility.js.map +1 -1
  114. package/dist/src/terminal-monitor-state-cli-adapter.d.ts +1 -1
  115. package/dist/src/terminal-monitor-state-cli-adapter.js +808 -147
  116. package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
  117. package/dist/src/terminal-monitor-state-reconciliation-service.d.ts +2 -1
  118. package/dist/src/terminal-monitor-state-reconciliation-service.js +12 -0
  119. package/dist/src/terminal-monitor-state-reconciliation-service.js.map +1 -1
  120. package/dist/src/terminal-monitor-supervision-cli-adapter.d.ts +2 -0
  121. package/dist/src/terminal-monitor-supervision-cli-adapter.js +31 -11
  122. package/dist/src/terminal-monitor-supervision-cli-adapter.js.map +1 -1
  123. package/dist/src/terminal-status-cli-adapter.d.ts +1 -0
  124. package/dist/src/terminal-status-cli-adapter.js +30 -28
  125. package/dist/src/terminal-status-cli-adapter.js.map +1 -1
  126. package/dist/src/terminal-submission-acceptance.js +78 -25
  127. package/dist/src/terminal-submission-acceptance.js.map +1 -1
  128. package/dist/src/terminal-submission-retry-service.d.ts +114 -0
  129. package/dist/src/terminal-submission-retry-service.js +589 -0
  130. package/dist/src/terminal-submission-retry-service.js.map +1 -0
  131. package/dist/src/terminal-watch-callback-cli-adapter.d.ts +10 -2
  132. package/dist/src/terminal-watch-callback-cli-adapter.js +76 -37
  133. package/dist/src/terminal-watch-callback-cli-adapter.js.map +1 -1
  134. package/dist/src/terminal-watch-cli-adapter.js +41 -17
  135. package/dist/src/terminal-watch-cli-adapter.js.map +1 -1
  136. package/dist/src/terminal-watch-service.d.ts +11 -6
  137. package/dist/src/terminal-watch-service.js +210 -41
  138. package/dist/src/terminal-watch-service.js.map +1 -1
  139. package/dist/src/terminal-watch-store.d.ts +23 -0
  140. package/dist/src/terminal-watch-store.js +162 -4
  141. package/dist/src/terminal-watch-store.js.map +1 -1
  142. package/docs/quickstart-tmux.md +1 -1
  143. package/package.json +1 -1
  144. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +8 -5
@@ -1,37 +1,180 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { applyMessageToConversation, createMessage, effectiveTurnStatus, executorForConversation, extractStructuredMessage, isTerminalBridgeCallbackSupersedeStatus, isTerminalDispatchOwnerReleasedStatus, isTurnPhaseStatus, isWaitingForAgentStatus, normalizeLegacyCallbackStatus, parseMessageJson, turnIdForConversation } from "./protocol.js";
3
- import { beginCallbackRetryPolicy, reduceCallbackRetryPolicy } from "./callback-outbox-policy.js";
4
- import { createCallbackOutboxSettlement } from "./callback-outbox-settlement.js";
2
+ import { applyMessageToConversation, createMessage, effectiveTurnStatus, executorForConversation, extractStructuredMessage, isTerminalBridgeCallbackSupersedeStatus, isTerminalDispatchOwnerReleasedStatus, isTurnPhaseStatus, isWaitingForAgentStatus, normalizeLegacyCallbackStatus, parseMessageJson, sessionIdForConversation, turnIdForConversation } from "./protocol.js";
3
+ import { beginCallbackRetryPolicy, reduceCallbackRetryPolicy, supersedeCallbackNotificationDelivery } from "./callback-outbox-policy.js";
4
+ import { callbackOutboxEvent, callbackOutboxField, createCallbackOutboxSettlement } from "./callback-outbox-settlement.js";
5
5
  import { canonicalJson } from "./canonical-json.js";
6
+ import { callbackRouteFingerprint } from "./callback-route-authority.js";
7
+ import { createCallbackEnvelope, parseCallbackAttemptOutcome, resolveCallbackRoute as resolveHostCallbackRoute } from "./callback-transport.js";
6
8
  import { isRecord, nonBlankString as stringValue } from "./value-guards.js";
9
+ function supersedeNotificationForLifecycleMessage(conversation, message, now) {
10
+ return message.type === "done" || message.type === "error"
11
+ ? supersedeCallbackNotificationDelivery(conversation, {
12
+ at: now.toISOString(),
13
+ reason: `superseded_by_${message.type}_callback`
14
+ })
15
+ : conversation;
16
+ }
7
17
  export function resolveCallbackGatewayRoute(...candidates) {
18
+ let gatewayUrl;
8
19
  for (const candidate of candidates) {
20
+ gatewayUrl ??= stringValue(candidate.gatewayUrl);
9
21
  const token = stringValue(candidate.token);
10
22
  if (!token || token === "<token>") {
11
23
  continue;
12
24
  }
13
25
  return {
14
- gatewayUrl: stringValue(candidate.gatewayUrl),
26
+ gatewayUrl: stringValue(candidate.gatewayUrl) ?? gatewayUrl,
15
27
  token
16
28
  };
17
29
  }
18
30
  return {
19
- gatewayUrl: undefined,
31
+ gatewayUrl,
20
32
  token: undefined
21
33
  };
22
34
  }
35
+ function resolveManagedCallbackRoute(input) {
36
+ if (input.inheritedDelivery) {
37
+ return resolveHostCallbackRoute(callbackRouteFieldCandidate(input.inheritedDelivery, "callback_route"), legacyCallbackRouteCandidate({
38
+ gatewayMethod: input.inheritedDelivery.gateway_method,
39
+ gatewaySession: input.inheritedDelivery.gateway_session ??
40
+ input.conversation.gateway_session ??
41
+ input.conversation.openclaw_session,
42
+ openclawBin: input.inheritedDelivery.openclaw_bin ??
43
+ input.conversation.openclaw_bin,
44
+ gatewayUrl: input.inheritedDelivery.gateway_url ??
45
+ input.conversation.gateway_url
46
+ }));
47
+ }
48
+ return resolveHostCallbackRoute(callbackRouteFieldCandidate(input.options, "callbackRoute"), callbackRouteFieldCandidate(input.conversation, "callback_route"), legacyCallbackRouteCandidate({
49
+ gatewayMethod: input.options.gatewayMethod ??
50
+ input.conversation.gateway_method,
51
+ gatewaySession: input.options.gatewaySession ??
52
+ input.options.openclawSession ?? input.conversation.gateway_session ??
53
+ input.conversation.openclaw_session,
54
+ openclawBin: input.options.openclawBin ?? input.conversation.openclaw_bin,
55
+ gatewayUrl: input.options.gatewayUrl ?? input.conversation.gateway_url
56
+ }), legacyCallbackRouteCandidate({
57
+ gatewayMethod: input.conversation.gateway_method,
58
+ gatewaySession: input.conversation.gateway_session ??
59
+ input.conversation.openclaw_session,
60
+ openclawBin: input.conversation.openclaw_bin,
61
+ gatewayUrl: input.conversation.gateway_url
62
+ }));
63
+ }
64
+ function callbackRouteFieldCandidate(container, field) {
65
+ return Object.hasOwn(container, field)
66
+ ? { callbackRoute: container[field] }
67
+ : {};
68
+ }
69
+ function resolvedDeliveryOptions(input) {
70
+ if (!input.inheritedDelivery) {
71
+ return {
72
+ ...input.options,
73
+ ...(input.callbackRoute ? { callbackRoute: input.callbackRoute } : {}),
74
+ gatewayMethod: input.options.gatewayMethod ??
75
+ input.conversation.gateway_method,
76
+ gatewaySession: input.options.gatewaySession ??
77
+ input.options.openclawSession ?? input.conversation.gateway_session ??
78
+ input.conversation.openclaw_session,
79
+ openclawSession: input.options.openclawSession ??
80
+ input.conversation.openclaw_session,
81
+ openclawBin: input.options.openclawBin ?? input.conversation.openclaw_bin,
82
+ gatewayUrl: input.options.gatewayUrl ?? input.conversation.gateway_url
83
+ };
84
+ }
85
+ const { callbackRoute: _ignoredCurrentRoute, ...currentOptions } = input.options;
86
+ return {
87
+ ...currentOptions,
88
+ ...(input.callbackRoute ? { callbackRoute: input.callbackRoute } : {}),
89
+ gatewayMethod: stringValue(input.inheritedDelivery.gateway_method),
90
+ gatewaySession: stringValue(input.inheritedDelivery.gateway_session) ??
91
+ input.conversation.gateway_session ??
92
+ input.conversation.openclaw_session,
93
+ openclawSession: input.conversation.openclaw_session,
94
+ openclawBin: stringValue(input.inheritedDelivery.openclaw_bin) ??
95
+ input.conversation.openclaw_bin,
96
+ gatewayUrl: stringValue(input.inheritedDelivery.gateway_url) ??
97
+ input.conversation.gateway_url
98
+ };
99
+ }
100
+ function legacyCallbackRouteCandidate(input) {
101
+ return stringValue(input.gatewayMethod)
102
+ ? {
103
+ legacyOpenClaw: {
104
+ controllerSessionId: input.gatewaySession,
105
+ gatewayMethod: input.gatewayMethod,
106
+ openclawBin: input.openclawBin,
107
+ gatewayUrl: input.gatewayUrl
108
+ }
109
+ }
110
+ : {};
111
+ }
112
+ function createManagedCallbackEnvelope(route, conversation, message) {
113
+ return createCallbackEnvelope({
114
+ route,
115
+ source: {
116
+ kind: "managed_turn",
117
+ session_id: sessionIdForConversation(conversation),
118
+ turn_id: turnIdForConversation(conversation),
119
+ conversation_id: conversation.conversation_id
120
+ },
121
+ event: {
122
+ id: message.id,
123
+ type: message.type,
124
+ body: message.body,
125
+ requires_response: message.requires_response,
126
+ metadata: message.metadata
127
+ }
128
+ });
129
+ }
130
+ function assertImmutableCallbackTransport(inheritedDelivery, route, envelope, messageId) {
131
+ if (!inheritedDelivery)
132
+ return;
133
+ if (inheritedDelivery.callback_route !== undefined &&
134
+ canonicalJson(inheritedDelivery.callback_route) !== canonicalJson(route)) {
135
+ throw new Error(`callback message ${messageId} conflicts with its persisted immutable route`);
136
+ }
137
+ if (inheritedDelivery.callback_envelope !== undefined &&
138
+ canonicalJson(inheritedDelivery.callback_envelope) !== canonicalJson(envelope)) {
139
+ throw new Error(`callback message ${messageId} conflicts with its persisted immutable envelope`);
140
+ }
141
+ }
142
+ function assertDispatchCallbackRouteAuthority(conversation, route) {
143
+ const takeover = isRecord(conversation.native_session_takeover)
144
+ ? conversation.native_session_takeover
145
+ : undefined;
146
+ const submission = isRecord(takeover?.terminal_bridge_submission)
147
+ ? takeover.terminal_bridge_submission
148
+ : undefined;
149
+ if (!submission ||
150
+ !Object.hasOwn(submission, "callback_route_fingerprint")) {
151
+ // Records from before the route-fingerprint field remain readable.
152
+ return;
153
+ }
154
+ const authority = submission.callback_route_fingerprint;
155
+ const matches = authority === null
156
+ ? route === undefined
157
+ : stringValue(authority) !== undefined && route !== undefined &&
158
+ callbackRouteFingerprint(route) === authority;
159
+ if (!matches) {
160
+ throw new Error("callback route conflicts with immutable terminal dispatch authority");
161
+ }
162
+ }
23
163
  export function createCallbackOutboxService(ports) {
24
164
  const settlement = createCallbackOutboxSettlement({
25
165
  state: {
166
+ withWriter: ports.state.withWriter,
26
167
  withStateTransaction: ports.state.withTransaction,
168
+ storeDirForStatePath: ports.state.storeDirForStatePath,
27
169
  load: ports.state.load,
28
170
  save: ports.state.save,
29
171
  append: ports.state.append
30
172
  },
31
173
  retryMonitor: {
32
- start: ({ statePath }) => ports.retry.startMonitor({
174
+ start: ({ statePath, callbackOutboxLane }) => ports.retry.startMonitor({
33
175
  statePath,
34
- delayMs: ports.retry.delaysMs[0]
176
+ delayMs: ports.retry.delaysMs[0],
177
+ callbackOutboxLane
35
178
  })
36
179
  },
37
180
  clock: { now: ports.runtime.now, nowMs: ports.runtime.nowMs },
@@ -44,6 +187,8 @@ export function createCallbackOutboxService(ports) {
44
187
  ? options.conversationOverride
45
188
  : ports.state.load(options.statePath);
46
189
  const conversation = normalizeLegacyCallbackStatus(loadedConversation);
190
+ const callbackOutboxLane = options.callbackOutboxLane ?? "lifecycle";
191
+ const callbackOutbox = callbackOutboxField(callbackOutboxLane);
47
192
  const storeDir = ports.state.storeDirForStatePath(options.statePath);
48
193
  ports.authority.assertNoDeferredTransfer({
49
194
  storeDir,
@@ -51,10 +196,6 @@ export function createCallbackOutboxService(ports) {
51
196
  action: "prepare callback for"
52
197
  });
53
198
  const executor = executorForConversation(conversation);
54
- const persistedGatewayRoute = resolveCallbackGatewayRoute({
55
- gatewayUrl: options.gatewayUrl,
56
- token: options.token
57
- });
58
199
  const message = options.retryPending === true ||
59
200
  options.preserveMessageId === true
60
201
  ? parseMessageJson(messageInput)
@@ -68,8 +209,8 @@ export function createCallbackOutboxService(ports) {
68
209
  throw new Error(`message.conversation_id ${message.conversation_id} does not match conversation ${conversation.conversation_id}`);
69
210
  }
70
211
  const existingEvents = ports.state.readEvents(logPath);
71
- const callbackDelivery = isRecord(conversation.callback_delivery)
72
- ? conversation.callback_delivery
212
+ const callbackDelivery = isRecord(conversation[callbackOutbox])
213
+ ? conversation[callbackOutbox]
73
214
  : undefined;
74
215
  const persistedDeliveryMessage = isRecord(callbackDelivery?.message)
75
216
  ? callbackDelivery.message
@@ -83,6 +224,26 @@ export function createCallbackOutboxService(ports) {
83
224
  const inheritedDelivery = sameDeliveryMessage
84
225
  ? callbackDelivery
85
226
  : undefined;
227
+ const callbackRoute = resolveManagedCallbackRoute({
228
+ options,
229
+ conversation,
230
+ inheritedDelivery
231
+ });
232
+ assertDispatchCallbackRouteAuthority(conversation, callbackRoute);
233
+ const deliveryOptions = resolvedDeliveryOptions({
234
+ options,
235
+ conversation,
236
+ inheritedDelivery,
237
+ callbackRoute
238
+ });
239
+ const persistedGatewayRoute = resolveCallbackGatewayRoute({
240
+ gatewayUrl: deliveryOptions.gatewayUrl,
241
+ token: deliveryOptions.token
242
+ });
243
+ const callbackEnvelope = callbackRoute
244
+ ? createManagedCallbackEnvelope(callbackRoute, conversation, message)
245
+ : undefined;
246
+ assertImmutableCallbackTransport(inheritedDelivery, callbackRoute, callbackEnvelope, message.id);
86
247
  const retryingPending = options.retryPending === true &&
87
248
  sameDeliveryMessage &&
88
249
  callbackRetryDisposition(ports, inheritedDelivery).state === "retryable";
@@ -141,7 +302,9 @@ export function createCallbackOutboxService(ports) {
141
302
  const deliveryAttemptId = randomUUID();
142
303
  let nextConversation = retryingPending
143
304
  ? conversation
144
- : applyMessageToConversation(conversation, message);
305
+ : callbackOutboxLane === "notification"
306
+ ? conversation
307
+ : applyMessageToConversation(supersedeNotificationForLifecycleMessage(conversation, message, ports.runtime.now()), message);
145
308
  if (closeTerminalBridgeOnDone) {
146
309
  const closedAt = ports.runtime.now().toISOString();
147
310
  nextConversation = {
@@ -166,7 +329,8 @@ export function createCallbackOutboxService(ports) {
166
329
  deliveryAttempt <= ports.retry.delaysMs.length
167
330
  ? ports.retry.startMonitor({
168
331
  statePath: options.statePath,
169
- delayMs: retryDelayMs
332
+ delayMs: retryDelayMs,
333
+ callbackOutboxLane
170
334
  })
171
335
  : undefined;
172
336
  if (!retryingPending &&
@@ -178,7 +342,7 @@ export function createCallbackOutboxService(ports) {
178
342
  const now = ports.runtime.now().toISOString();
179
343
  nextConversation = {
180
344
  ...nextConversation,
181
- callback_delivery: {
345
+ [callbackOutbox]: {
182
346
  status: "pending",
183
347
  message,
184
348
  attempts: deliveryAttempt,
@@ -188,17 +352,20 @@ export function createCallbackOutboxService(ports) {
188
352
  created_at: stringValue(inheritedDelivery?.created_at) ?? now,
189
353
  last_attempt_at: now,
190
354
  updated_at: now,
191
- gateway_method: options.gatewayMethod,
192
- gateway_session: options.gatewaySession ??
193
- options.openclawSession ?? conversation.openclaw_session,
355
+ gateway_method: deliveryOptions.gatewayMethod,
356
+ gateway_session: deliveryOptions.gatewaySession ??
357
+ deliveryOptions.openclawSession ?? conversation.openclaw_session,
194
358
  gateway_url: persistedGatewayRoute.gatewayUrl,
195
- openclaw_bin: options.openclawBin ?? conversation.openclaw_bin,
359
+ openclaw_bin: deliveryOptions.openclawBin ??
360
+ conversation.openclaw_bin,
196
361
  close_terminal_bridge_on_done: closeTerminalBridgeOnDone,
197
362
  track_delivery: true,
198
363
  final_status: finalStatus,
199
364
  preserve_conversation_status: true,
200
365
  kind: stringValue(options.callbackDeliveryKind) ??
201
366
  stringValue(inheritedDelivery?.kind),
367
+ ...(callbackRoute ? { callback_route: callbackRoute } : {}),
368
+ ...(callbackEnvelope ? { callback_envelope: callbackEnvelope } : {}),
202
369
  ...(callbackWatchdog
203
370
  ? {
204
371
  retry_monitor_pid: callbackWatchdog.pid ?? null,
@@ -210,9 +377,7 @@ export function createCallbackOutboxService(ports) {
210
377
  ports.state.append(logPath, {
211
378
  ts: now,
212
379
  conversation_id: conversation.conversation_id,
213
- event: retryingPending
214
- ? "callback_delivery_retry_started"
215
- : "callback_delivery_pending",
380
+ event: callbackOutboxEvent(callbackOutboxLane, retryingPending ? "retry_started" : "pending"),
216
381
  message_id: message.id,
217
382
  attempt: deliveryAttempt
218
383
  });
@@ -220,11 +385,13 @@ export function createCallbackOutboxService(ports) {
220
385
  ports.state.append(logPath, {
221
386
  ts: ports.runtime.now().toISOString(),
222
387
  conversation_id: conversation.conversation_id,
223
- event: "callback_retry_monitor_launched",
388
+ event: callbackOutboxLane === "notification"
389
+ ? "callback_notification_retry_monitor_launched"
390
+ : "callback_retry_monitor_launched",
224
391
  message_id: message.id,
225
392
  pid: callbackWatchdog.pid ?? null,
226
- next_attempt_at: isRecord(nextConversation.callback_delivery)
227
- ? nextConversation.callback_delivery.next_attempt_at
393
+ next_attempt_at: isRecord(nextConversation[callbackOutbox])
394
+ ? nextConversation[callbackOutbox].next_attempt_at
228
395
  : undefined
229
396
  });
230
397
  }
@@ -256,13 +423,16 @@ export function createCallbackOutboxService(ports) {
256
423
  }
257
424
  return {
258
425
  outcome: "deliver",
259
- options,
426
+ options: deliveryOptions,
260
427
  statePath: options.statePath,
261
428
  logPath,
262
429
  conversation: nextConversation,
263
430
  message,
264
431
  deliveryAttempt,
265
- deliveryAttemptId
432
+ deliveryAttemptId,
433
+ callbackOutboxLane,
434
+ ...(callbackRoute ? { callbackRoute } : {}),
435
+ callbackEnvelope
266
436
  };
267
437
  }
268
438
  return persistPreparation();
@@ -276,15 +446,30 @@ export function createCallbackOutboxService(ports) {
276
446
  message: prepared.message
277
447
  };
278
448
  }
279
- ports.state.assertWriterCompatible(ports.state.storeDirForStatePath(prepared.statePath));
449
+ const transportConversation = beginPreparedCallbackTransport(ports, prepared);
450
+ if (!transportConversation) {
451
+ return {
452
+ delivered: false,
453
+ duplicate: true,
454
+ conversation: ports.state.load(prepared.statePath),
455
+ message: prepared.message
456
+ };
457
+ }
280
458
  let acceptedDelivery;
459
+ let delivery;
281
460
  try {
282
- const delivery = ports.delivery.deliver({
461
+ delivery = ports.delivery.deliver({
283
462
  options: prepared.options,
284
463
  statePath: prepared.statePath,
285
464
  logPath: prepared.logPath,
286
465
  conversation: prepared.conversation,
287
466
  message: prepared.message,
467
+ attempt: {
468
+ number: prepared.deliveryAttempt,
469
+ id: prepared.deliveryAttemptId
470
+ },
471
+ route: prepared.callbackRoute,
472
+ envelope: prepared.callbackEnvelope,
288
473
  onProgress: (progress) => {
289
474
  settlement.persistDeliveryProgress(prepared, progress);
290
475
  },
@@ -292,16 +477,6 @@ export function createCallbackOutboxService(ports) {
292
477
  acceptedDelivery = accepted;
293
478
  }
294
479
  });
295
- return {
296
- delivered: true,
297
- duplicate: false,
298
- conversation: settlement.settleDelivery(prepared, {
299
- delivered: true,
300
- delivery
301
- }),
302
- message: prepared.message,
303
- delivery: delivery.kind
304
- };
305
480
  }
306
481
  catch (error) {
307
482
  if (acceptedDelivery) {
@@ -329,8 +504,9 @@ export function createCallbackOutboxService(ports) {
329
504
  delivered: false,
330
505
  error
331
506
  });
332
- const settledDelivery = isRecord(settled.callback_delivery)
333
- ? settled.callback_delivery
507
+ const settledOutbox = callbackOutboxField(prepared.callbackOutboxLane);
508
+ const settledDelivery = isRecord(settled[settledOutbox])
509
+ ? settled[settledOutbox]
334
510
  : undefined;
335
511
  if (settledDelivery?.status === "delivered") {
336
512
  return {
@@ -343,6 +519,45 @@ export function createCallbackOutboxService(ports) {
343
519
  }
344
520
  throw error;
345
521
  }
522
+ const attemptOutcome = delivery.attempt_outcome === undefined
523
+ ? undefined
524
+ : parseCallbackAttemptOutcome(delivery.attempt_outcome);
525
+ if (attemptOutcome && attemptOutcome.disposition !== "accepted") {
526
+ const error = new Error(stringValue(attemptOutcome.evidence?.error_message) ??
527
+ `callback transport ${attemptOutcome.disposition}: ` +
528
+ attemptOutcome.error_code);
529
+ const settled = settlement.settleDelivery(prepared, {
530
+ delivered: false,
531
+ delivery,
532
+ outcome: attemptOutcome,
533
+ error
534
+ });
535
+ const settledOutbox = callbackOutboxField(prepared.callbackOutboxLane);
536
+ const settledDelivery = isRecord(settled[settledOutbox])
537
+ ? settled[settledOutbox]
538
+ : undefined;
539
+ if (settledDelivery?.status === "delivered") {
540
+ return {
541
+ delivered: true,
542
+ duplicate: false,
543
+ conversation: settled,
544
+ message: prepared.message,
545
+ delivery: delivery.kind
546
+ };
547
+ }
548
+ throw error;
549
+ }
550
+ return {
551
+ delivered: true,
552
+ duplicate: false,
553
+ conversation: settlement.settleDelivery(prepared, {
554
+ delivered: true,
555
+ delivery,
556
+ outcome: attemptOutcome
557
+ }),
558
+ message: prepared.message,
559
+ delivery: delivery.kind
560
+ };
346
561
  }
347
562
  return {
348
563
  prepare,
@@ -352,10 +567,42 @@ export function createCallbackOutboxService(ports) {
352
567
  retry: (input) => retryCallback(ports, settlement, input),
353
568
  retryDisposition: (delivery) => callbackRetryDisposition(ports, delivery),
354
569
  prepareApprovalNotification: (input) => prepareApprovalNotification(prepare, input),
570
+ prepareStallNotification: (input) => prepareStallNotification(prepare, input),
355
571
  prepareTerminalCompletion: (input) => prepareTerminalCompletion(ports, prepare, input)
356
572
  };
357
573
  }
574
+ function beginPreparedCallbackTransport(ports, prepared) {
575
+ const storeDir = ports.state.storeDirForStatePath(prepared.statePath);
576
+ return ports.state.withWriter(storeDir, () => ports.state.withTransaction(prepared.statePath, () => {
577
+ const current = ports.state.load(prepared.statePath);
578
+ const field = callbackOutboxField(prepared.callbackOutboxLane);
579
+ const delivery = isRecord(current[field]) ? current[field] : undefined;
580
+ const message = isRecord(delivery?.message) ? delivery.message : undefined;
581
+ if (delivery?.status !== "pending" ||
582
+ message?.id !== prepared.message.id ||
583
+ Number(delivery.attempts) !== prepared.deliveryAttempt ||
584
+ stringValue(delivery.attempt_id) !== prepared.deliveryAttemptId ||
585
+ delivery.transport_started_at !== undefined) {
586
+ return undefined;
587
+ }
588
+ const now = ports.runtime.now().toISOString();
589
+ const started = {
590
+ ...current,
591
+ [field]: {
592
+ ...delivery,
593
+ transport_started_at: now,
594
+ updated_at: now
595
+ }
596
+ };
597
+ ports.state.save(prepared.statePath, started);
598
+ return started;
599
+ }));
600
+ }
358
601
  function prepareApprovalNotification(prepare, input) {
602
+ const callbackRoute = resolveManagedCallbackRoute({
603
+ options: input.options,
604
+ conversation: input.conversation
605
+ });
359
606
  const identity = terminalApprovalCallbackIdentity(input.conversation);
360
607
  const callbackMessage = createMessage({
361
608
  conversation: input.conversation,
@@ -368,7 +615,7 @@ function prepareApprovalNotification(prepare, input) {
368
615
  metadata: input.metadata,
369
616
  now: identity.now
370
617
  });
371
- if (!input.conversation.gateway_method) {
618
+ if (!callbackRoute) {
372
619
  return {
373
620
  callbackMessage,
374
621
  delivered: false
@@ -379,6 +626,7 @@ function prepareApprovalNotification(prepare, input) {
379
626
  prepared: prepare({
380
627
  options: {
381
628
  ...input.options,
629
+ ...(callbackRoute ? { callbackRoute } : {}),
382
630
  statePath: input.statePath,
383
631
  log: input.logPath,
384
632
  messageJson: JSON.stringify(callbackMessage),
@@ -386,9 +634,7 @@ function prepareApprovalNotification(prepare, input) {
386
634
  gatewaySession: input.conversation.gateway_session,
387
635
  openclawSession: input.conversation.openclaw_session,
388
636
  openclawBin: input.conversation.openclaw_bin,
389
- gatewayUrl: stringValue(input.conversation.gateway_token)
390
- ? input.conversation.gateway_url
391
- : undefined,
637
+ gatewayUrl: input.conversation.gateway_url,
392
638
  token: stringValue(input.conversation.gateway_token),
393
639
  preserveMessageId: true,
394
640
  callbackDeliveryKind: "approval_notification",
@@ -399,6 +645,41 @@ function prepareApprovalNotification(prepare, input) {
399
645
  })
400
646
  };
401
647
  }
648
+ function prepareStallNotification(prepare, input) {
649
+ const callbackRoute = resolveManagedCallbackRoute({
650
+ options: input.options,
651
+ conversation: input.conversation
652
+ });
653
+ if (!callbackRoute) {
654
+ return {
655
+ callbackMessage: input.message,
656
+ delivered: false
657
+ };
658
+ }
659
+ return {
660
+ callbackMessage: input.message,
661
+ prepared: prepare({
662
+ options: {
663
+ ...input.options,
664
+ callbackRoute,
665
+ callbackOutboxLane: "notification",
666
+ statePath: input.statePath,
667
+ log: input.logPath,
668
+ messageJson: JSON.stringify(input.message),
669
+ gatewayMethod: input.conversation.gateway_method,
670
+ gatewaySession: input.conversation.gateway_session,
671
+ openclawSession: input.conversation.openclaw_session,
672
+ openclawBin: input.conversation.openclaw_bin,
673
+ gatewayUrl: input.conversation.gateway_url,
674
+ token: stringValue(input.conversation.gateway_token),
675
+ preserveMessageId: true,
676
+ callbackDeliveryKind: "stall_notification",
677
+ conversationOverride: input.conversation
678
+ },
679
+ logPath: input.logPath
680
+ })
681
+ };
682
+ }
402
683
  function terminalApprovalCallbackIdentity(conversation) {
403
684
  const nativeTakeover = isRecord(conversation.native_session_takeover)
404
685
  ? conversation.native_session_takeover
@@ -464,7 +745,8 @@ function prepareTerminalCompletion(ports, prepare, input) {
464
745
  completionFingerprint,
465
746
  outcome: completionOutcome
466
747
  });
467
- const transaction = ports.state.withTransaction(input.statePath, () => {
748
+ const storeDir = ports.state.storeDirForStatePath(input.statePath);
749
+ const transaction = ports.state.withWriter(storeDir, () => ports.state.withTransaction(input.statePath, () => {
468
750
  const claim = claimTerminalCompletion(ports, {
469
751
  statePath: input.statePath,
470
752
  logPath: input.logPath,
@@ -523,9 +805,14 @@ function prepareTerminalCompletion(ports, prepare, input) {
523
805
  }),
524
806
  id: callbackMessageId
525
807
  };
808
+ const callbackRoute = resolveManagedCallbackRoute({
809
+ options: input.options,
810
+ conversation: claim.conversation
811
+ });
526
812
  const prepared = prepare({
527
813
  options: {
528
814
  ...input.options,
815
+ ...(callbackRoute ? { callbackRoute } : {}),
529
816
  statePath: input.statePath,
530
817
  log: input.logPath,
531
818
  closeTerminalBridgeOnDone: false,
@@ -537,11 +824,9 @@ function prepareTerminalCompletion(ports, prepare, input) {
537
824
  gatewaySession: claim.conversation.gateway_session,
538
825
  openclawSession: claim.conversation.openclaw_session,
539
826
  openclawBin: claim.conversation.openclaw_bin,
540
- gatewayUrl: stringValue(claim.conversation.gateway_token)
541
- ? claim.conversation.gateway_url
542
- : undefined,
827
+ gatewayUrl: claim.conversation.gateway_url,
543
828
  token: stringValue(claim.conversation.gateway_token),
544
- recordOnly: !stringValue(claim.conversation.gateway_method)
829
+ recordOnly: !callbackRoute
545
830
  },
546
831
  logPath: input.logPath
547
832
  });
@@ -554,7 +839,7 @@ function prepareTerminalCompletion(ports, prepare, input) {
554
839
  prepared,
555
840
  callbackMessageId
556
841
  };
557
- });
842
+ }));
558
843
  if (!transaction.claimed) {
559
844
  return transaction;
560
845
  }
@@ -654,8 +939,10 @@ function claimTerminalCompletion(ports, input) {
654
939
  };
655
940
  }
656
941
  function retryCallback(ports, settlement, { options, conversation, statePath, logPath }) {
657
- const callbackDelivery = isRecord(conversation.callback_delivery)
658
- ? conversation.callback_delivery
942
+ const callbackOutboxLane = options.callbackOutboxLane ?? "lifecycle";
943
+ const callbackOutbox = callbackOutboxField(callbackOutboxLane);
944
+ const callbackDelivery = isRecord(conversation[callbackOutbox])
945
+ ? conversation[callbackOutbox]
659
946
  : undefined;
660
947
  const legacyStatusError = stringValue(conversation.legacy_callback_status_error);
661
948
  if (legacyStatusError) {
@@ -670,7 +957,8 @@ function retryCallback(ports, settlement, { options, conversation, statePath, lo
670
957
  expectedMessageId: isRecord(callbackDelivery?.message)
671
958
  ? stringValue(callbackDelivery.message.id)
672
959
  : undefined,
673
- reason: "manual_retry_observed_accepted_transport"
960
+ reason: "manual_retry_observed_accepted_transport",
961
+ callbackOutboxLane
674
962
  });
675
963
  if (!recovered) {
676
964
  throw new Error(`cannot retry callback for ${conversation.conversation_id}; ` +
@@ -698,10 +986,16 @@ function retryCallback(ports, settlement, { options, conversation, statePath, lo
698
986
  gatewayUrl: conversation.gateway_url,
699
987
  token: conversation.gateway_token
700
988
  });
989
+ const callbackRoute = resolveManagedCallbackRoute({
990
+ options,
991
+ conversation,
992
+ inheritedDelivery: callbackDelivery
993
+ });
701
994
  return {
702
995
  kind: "retried",
703
996
  result: ports.delivery.runTransaction({
704
997
  ...options,
998
+ ...(callbackRoute ? { callbackRoute } : {}),
705
999
  statePath,
706
1000
  messageJson: JSON.stringify(callbackDelivery.message),
707
1001
  gatewayMethod: stringValue(callbackDelivery.gateway_method) ??
@@ -714,6 +1008,7 @@ function retryCallback(ports, settlement, { options, conversation, statePath, lo
714
1008
  gatewayUrl: gatewayRoute.gatewayUrl,
715
1009
  token: gatewayRoute.token,
716
1010
  closeTerminalBridgeOnDone: callbackDelivery.close_terminal_bridge_on_done === true,
1011
+ callbackOutboxLane,
717
1012
  retryPending: true
718
1013
  })
719
1014
  };
@@ -760,13 +1055,24 @@ function rejectManualRetryDisposition(conversation, disposition) {
760
1055
  throw new Error(`cannot retry callback for ${conversation.conversation_id}; callback ` +
761
1056
  `delivery retries are exhausted after attempt ${disposition.attempt}`);
762
1057
  }
1058
+ if (disposition.state === "uncertain") {
1059
+ throw new Error(`cannot retry callback for ${conversation.conversation_id}; callback ` +
1060
+ `attempt ${disposition.attempt} has an uncertain transport outcome ` +
1061
+ `(${disposition.reason}) and must not be blindly repeated`);
1062
+ }
1063
+ if (disposition.state === "permanent_failure") {
1064
+ throw new Error(`cannot retry callback for ${conversation.conversation_id}; callback ` +
1065
+ `attempt ${disposition.attempt} failed permanently ` +
1066
+ `(${disposition.reason})`);
1067
+ }
763
1068
  if (disposition.state !== "retryable") {
764
1069
  throw new Error(`cannot retry callback for ${conversation.conversation_id}; ` +
765
1070
  disposition.reason);
766
1071
  }
767
1072
  }
768
- function reconcileCallbackDelivery(ports, settlement, { statePath, logPath, delayMs }) {
769
- return ports.state.withTransaction(statePath, () => {
1073
+ function reconcileCallbackDelivery(ports, settlement, { statePath, logPath, delayMs, callbackOutboxLane = "lifecycle" }) {
1074
+ const storeDir = ports.state.storeDirForStatePath(statePath);
1075
+ return ports.state.withWriter(storeDir, () => ports.state.withTransaction(statePath, () => {
770
1076
  const conversation = ports.state.load(statePath);
771
1077
  const legacyStatusError = stringValue(conversation.legacy_callback_status_error);
772
1078
  if (legacyStatusError) {
@@ -778,8 +1084,9 @@ function reconcileCallbackDelivery(ports, settlement, { statePath, logPath, dela
778
1084
  diagnostic: legacyStatusError
779
1085
  };
780
1086
  }
781
- const callbackDelivery = isRecord(conversation.callback_delivery)
782
- ? conversation.callback_delivery
1087
+ const callbackOutbox = callbackOutboxField(callbackOutboxLane);
1088
+ const callbackDelivery = isRecord(conversation[callbackOutbox])
1089
+ ? conversation[callbackOutbox]
783
1090
  : undefined;
784
1091
  if (!["pending", "failed"].includes(String(callbackDelivery?.status ?? ""))) {
785
1092
  return { handled: false };
@@ -798,7 +1105,7 @@ function reconcileCallbackDelivery(ports, settlement, { statePath, logPath, dela
798
1105
  handled: true,
799
1106
  conversationId,
800
1107
  status: "skipped",
801
- reason: "callback_delivery_metadata_missing"
1108
+ reason: callbackOutboxEvent(callbackOutboxLane, "metadata_missing")
802
1109
  };
803
1110
  }
804
1111
  const disposition = callbackRetryDisposition(ports, callbackDelivery);
@@ -808,15 +1115,16 @@ function reconcileCallbackDelivery(ports, settlement, { statePath, logPath, dela
808
1115
  statePath,
809
1116
  logPath,
810
1117
  expectedMessageId: stringValue(callbackDelivery.message.id),
811
- reason: "startup_reconciliation_observed_accepted_transport"
1118
+ reason: "startup_reconciliation_observed_accepted_transport",
1119
+ callbackOutboxLane
812
1120
  });
813
1121
  return {
814
1122
  handled: true,
815
1123
  conversationId,
816
1124
  status: settled ? "recovered" : "skipped",
817
1125
  reason: settled
818
- ? "callback_delivery_accepted_recovered"
819
- : "callback_delivery_changed_before_recovery"
1126
+ ? callbackOutboxEvent(callbackOutboxLane, "accepted_recovered")
1127
+ : callbackOutboxEvent(callbackOutboxLane, "changed_before_recovery")
820
1128
  };
821
1129
  }
822
1130
  if (disposition.state === "in_flight") {
@@ -824,7 +1132,7 @@ function reconcileCallbackDelivery(ports, settlement, { statePath, logPath, dela
824
1132
  handled: true,
825
1133
  conversationId,
826
1134
  status: "already_running",
827
- reason: "callback_delivery_attempt_in_flight",
1135
+ reason: callbackOutboxEvent(callbackOutboxLane, "attempt_in_flight"),
828
1136
  attempt: disposition.attempt,
829
1137
  attemptPid: disposition.attempt_pid,
830
1138
  leaseExpiresAt: disposition.lease_expires_at,
@@ -836,7 +1144,7 @@ function reconcileCallbackDelivery(ports, settlement, { statePath, logPath, dela
836
1144
  handled: true,
837
1145
  conversationId,
838
1146
  status: "skipped",
839
- reason: "callback_delivery_retries_exhausted"
1147
+ reason: callbackOutboxEvent(callbackOutboxLane, "retries_exhausted")
840
1148
  };
841
1149
  }
842
1150
  if (disposition.state !== "retryable") {
@@ -852,12 +1160,16 @@ function reconcileCallbackDelivery(ports, settlement, { statePath, logPath, dela
852
1160
  configuredDelayMs >= 0
853
1161
  ? configuredDelayMs
854
1162
  : ports.retry.delaysMs[Math.max(0, attempts - 1)];
855
- const retryMonitor = ports.retry.startMonitor({ statePath, delayMs: retryDelayMs });
1163
+ const retryMonitor = ports.retry.startMonitor({
1164
+ statePath,
1165
+ delayMs: retryDelayMs,
1166
+ callbackOutboxLane
1167
+ });
856
1168
  const launchedAt = ports.runtime.now().toISOString();
857
1169
  const nextAttemptAt = new Date(ports.runtime.nowMs() + retryDelayMs).toISOString();
858
1170
  ports.state.save(statePath, {
859
1171
  ...conversation,
860
- callback_delivery: {
1172
+ [callbackOutbox]: {
861
1173
  ...callbackDelivery,
862
1174
  retry_monitor_pid: retryMonitor.pid ?? null,
863
1175
  next_attempt_at: nextAttemptAt,
@@ -867,7 +1179,9 @@ function reconcileCallbackDelivery(ports, settlement, { statePath, logPath, dela
867
1179
  ports.state.append(logPath, {
868
1180
  ts: launchedAt,
869
1181
  conversation_id: conversationId,
870
- event: "callback_retry_monitor_launched",
1182
+ event: callbackOutboxLane === "notification"
1183
+ ? "callback_notification_retry_monitor_launched"
1184
+ : "callback_retry_monitor_launched",
871
1185
  message_id: callbackDelivery.message.id,
872
1186
  pid: retryMonitor.pid ?? null,
873
1187
  next_attempt_at: nextAttemptAt,
@@ -877,23 +1191,23 @@ function reconcileCallbackDelivery(ports, settlement, { statePath, logPath, dela
877
1191
  handled: true,
878
1192
  conversationId,
879
1193
  status: "launched",
880
- reason: "callback_delivery_reconciliation",
1194
+ reason: callbackOutboxEvent(callbackOutboxLane, "reconciliation"),
881
1195
  monitorPid: retryMonitor.pid
882
1196
  };
883
- });
1197
+ }));
884
1198
  }
885
- function runCallbackRetryMonitor(ports, settlement, { statePath, initialDelayMs }) {
1199
+ function runCallbackRetryMonitor(ports, settlement, { statePath, initialDelayMs, callbackOutboxLane = "lifecycle" }) {
886
1200
  const configuredDelayMs = Number(initialDelayMs);
887
1201
  ports.runtime.sleepSync(Math.max(0, Number.isFinite(configuredDelayMs)
888
1202
  ? configuredDelayMs
889
1203
  : ports.retry.delaysMs[0]));
890
1204
  while (true) {
891
- const observed = observeCallbackRetryMonitor(ports, statePath);
1205
+ const observed = observeCallbackRetryMonitor(ports, statePath, callbackOutboxLane);
892
1206
  if (observed.kind === "stop") {
893
1207
  return;
894
1208
  }
895
1209
  if (observed.kind === "accepted") {
896
- settleRetryMonitorAcceptance(ports, settlement, statePath, observed.messageId);
1210
+ settleRetryMonitorAcceptance(ports, settlement, statePath, observed.messageId, callbackOutboxLane);
897
1211
  return;
898
1212
  }
899
1213
  if (observed.kind === "in_flight") {
@@ -901,15 +1215,15 @@ function runCallbackRetryMonitor(ports, settlement, { statePath, initialDelayMs
901
1215
  continue;
902
1216
  }
903
1217
  try {
904
- ports.delivery.runTransaction(retryMonitorDeliveryOptions(statePath, observed));
1218
+ ports.delivery.runTransaction(retryMonitorDeliveryOptions(statePath, observed, callbackOutboxLane));
905
1219
  return;
906
1220
  }
907
1221
  catch {
908
1222
  // The failed attempt is persisted before the next bounded retry.
909
1223
  }
910
- const latest = observeCallbackRetryMonitor(ports, statePath);
1224
+ const latest = observeCallbackRetryMonitor(ports, statePath, callbackOutboxLane);
911
1225
  if (latest.kind === "accepted") {
912
- settleRetryMonitorAcceptance(ports, settlement, statePath, latest.messageId);
1226
+ settleRetryMonitorAcceptance(ports, settlement, statePath, latest.messageId, callbackOutboxLane);
913
1227
  return;
914
1228
  }
915
1229
  if (latest.kind !== "retryable" ||
@@ -920,13 +1234,14 @@ function runCallbackRetryMonitor(ports, settlement, { statePath, initialDelayMs
920
1234
  ports.runtime.sleepSync(ports.retry.delaysMs[Math.max(0, latest.attempts - 1)]);
921
1235
  }
922
1236
  }
923
- function observeCallbackRetryMonitor(ports, statePath) {
1237
+ function observeCallbackRetryMonitor(ports, statePath, callbackOutboxLane) {
924
1238
  const conversation = ports.state.load(statePath);
925
1239
  if (stringValue(conversation.legacy_callback_status_error)) {
926
1240
  return { kind: "stop" };
927
1241
  }
928
- const delivery = isRecord(conversation.callback_delivery)
929
- ? conversation.callback_delivery
1242
+ const callbackOutbox = callbackOutboxField(callbackOutboxLane);
1243
+ const delivery = isRecord(conversation[callbackOutbox])
1244
+ ? conversation[callbackOutbox]
930
1245
  : undefined;
931
1246
  if (!delivery ||
932
1247
  !isRecord(delivery.message) ||
@@ -953,7 +1268,7 @@ function observeCallbackRetryMonitor(ports, statePath) {
953
1268
  attempts: Number(delivery.attempts ?? 0)
954
1269
  };
955
1270
  }
956
- function retryMonitorDeliveryOptions(statePath, observed) {
1271
+ function retryMonitorDeliveryOptions(statePath, observed, callbackOutboxLane) {
957
1272
  const { conversation, delivery } = observed;
958
1273
  const gatewayRoute = resolveCallbackGatewayRoute({
959
1274
  gatewayUrl: delivery.gateway_url,
@@ -962,8 +1277,14 @@ function retryMonitorDeliveryOptions(statePath, observed) {
962
1277
  gatewayUrl: conversation.gateway_url,
963
1278
  token: conversation.gateway_token
964
1279
  });
1280
+ const callbackRoute = resolveManagedCallbackRoute({
1281
+ options: { statePath },
1282
+ conversation,
1283
+ inheritedDelivery: delivery
1284
+ });
965
1285
  return {
966
1286
  statePath,
1287
+ ...(callbackRoute ? { callbackRoute } : {}),
967
1288
  messageJson: JSON.stringify(delivery.message),
968
1289
  gatewayMethod: stringValue(delivery.gateway_method) ??
969
1290
  conversation.gateway_method,
@@ -974,16 +1295,18 @@ function retryMonitorDeliveryOptions(statePath, observed) {
974
1295
  gatewayUrl: gatewayRoute.gatewayUrl,
975
1296
  token: gatewayRoute.token,
976
1297
  closeTerminalBridgeOnDone: delivery.close_terminal_bridge_on_done === true,
1298
+ callbackOutboxLane,
977
1299
  retryPending: true,
978
1300
  disableCallbackRetry: true
979
1301
  };
980
1302
  }
981
- function settleRetryMonitorAcceptance(ports, settlement, statePath, expectedMessageId) {
1303
+ function settleRetryMonitorAcceptance(ports, settlement, statePath, expectedMessageId, callbackOutboxLane) {
982
1304
  settlement.settleAccepted({
983
1305
  statePath,
984
1306
  logPath: ports.state.logPathForStatePath(statePath),
985
1307
  expectedMessageId,
986
- reason: "retry_monitor_observed_accepted_transport"
1308
+ reason: "retry_monitor_observed_accepted_transport",
1309
+ callbackOutboxLane
987
1310
  });
988
1311
  }
989
1312
  function requiredMessageJson(value) {
@@ -1008,6 +1331,14 @@ function rejectNonRetryableDelivery(disposition) {
1008
1331
  if (disposition.state === "exhausted") {
1009
1332
  throw new Error(`callback retries are exhausted after attempt ${disposition.attempt}`);
1010
1333
  }
1334
+ if (disposition.state === "uncertain") {
1335
+ throw new Error(`callback attempt ${disposition.attempt} has an uncertain transport ` +
1336
+ `outcome (${disposition.reason}) and must not be blindly repeated`);
1337
+ }
1338
+ if (disposition.state === "permanent_failure") {
1339
+ throw new Error(`callback attempt ${disposition.attempt} failed permanently ` +
1340
+ `(${disposition.reason})`);
1341
+ }
1011
1342
  throw new Error(disposition.state === "unavailable"
1012
1343
  ? disposition.reason
1013
1344
  : "callback delivery is not retryable");