@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.
- package/CHANGELOG.md +18 -0
- package/README.md +5 -5
- package/dist/src/callback-cli-adapter.d.ts +9 -6
- package/dist/src/callback-cli-adapter.js +119 -30
- package/dist/src/callback-cli-adapter.js.map +1 -1
- package/dist/src/callback-outbox-policy.d.ts +39 -0
- package/dist/src/callback-outbox-policy.js +163 -6
- package/dist/src/callback-outbox-policy.js.map +1 -1
- package/dist/src/callback-outbox-service.d.ts +28 -1
- package/dist/src/callback-outbox-service.js +413 -82
- package/dist/src/callback-outbox-service.js.map +1 -1
- package/dist/src/callback-outbox-settlement.d.ts +14 -1
- package/dist/src/callback-outbox-settlement.js +151 -30
- package/dist/src/callback-outbox-settlement.js.map +1 -1
- package/dist/src/callback-route-authority.d.ts +59 -0
- package/dist/src/callback-route-authority.js +123 -0
- package/dist/src/callback-route-authority.js.map +1 -0
- package/dist/src/callback-transport.d.ts +134 -0
- package/dist/src/callback-transport.js +256 -0
- package/dist/src/callback-transport.js.map +1 -0
- package/dist/src/cli-core.js +17 -4
- package/dist/src/cli-core.js.map +1 -1
- package/dist/src/codex-lifecycle-compatibility.js +2 -1
- package/dist/src/codex-lifecycle-compatibility.js.map +1 -1
- package/dist/src/deferred-foreground-application-service.d.ts +18 -0
- package/dist/src/deferred-foreground-application-service.js +72 -0
- package/dist/src/deferred-foreground-application-service.js.map +1 -1
- package/dist/src/deferred-foreground-authority-cli-adapter.js +2 -1
- package/dist/src/deferred-foreground-authority-cli-adapter.js.map +1 -1
- package/dist/src/deferred-foreground-recovery-cli-adapter.d.ts +1 -1
- package/dist/src/deferred-foreground-recovery-cli-adapter.js +108 -37
- package/dist/src/deferred-foreground-recovery-cli-adapter.js.map +1 -1
- package/dist/src/deferred-foreground-recovery-service.d.ts +1 -1
- package/dist/src/deferred-foreground-recovery-service.js +10 -3
- package/dist/src/deferred-foreground-recovery-service.js.map +1 -1
- package/dist/src/deferred-foreground-transfer-policy.d.ts +1 -1
- package/dist/src/deferred-foreground-transfer-policy.js +3 -2
- package/dist/src/deferred-foreground-transfer-policy.js.map +1 -1
- package/dist/src/deferred-foreground-transfer.d.ts +17 -1
- package/dist/src/deferred-foreground-transfer.js +244 -35
- package/dist/src/deferred-foreground-transfer.js.map +1 -1
- package/dist/src/deferred-foreground-user-close.d.ts +18 -0
- package/dist/src/deferred-foreground-user-close.js +180 -0
- package/dist/src/deferred-foreground-user-close.js.map +1 -0
- package/dist/src/openclaw-callback-transport.d.ts +4 -1
- package/dist/src/openclaw-callback-transport.js +403 -10
- package/dist/src/openclaw-callback-transport.js.map +1 -1
- package/dist/src/openclaw-managed-callback-cli-adapter.d.ts +23 -0
- package/dist/src/openclaw-managed-callback-cli-adapter.js +77 -0
- package/dist/src/openclaw-managed-callback-cli-adapter.js.map +1 -0
- package/dist/src/openclaw-plugin-command-adapter.js +28 -31
- package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
- package/dist/src/openclaw-plugin-helpers.js +8 -0
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin-schemas.d.ts +20 -1
- package/dist/src/openclaw-plugin-schemas.js +26 -2
- package/dist/src/openclaw-plugin-schemas.js.map +1 -1
- package/dist/src/protocol.d.ts +3 -0
- package/dist/src/protocol.js +4 -0
- package/dist/src/protocol.js.map +1 -1
- package/dist/src/store.d.ts +9 -0
- package/dist/src/store.js +79 -1
- package/dist/src/store.js.map +1 -1
- package/dist/src/terminal-acceptance-cli-adapter.d.ts +2 -0
- package/dist/src/terminal-acceptance-cli-adapter.js +62 -12
- package/dist/src/terminal-acceptance-cli-adapter.js.map +1 -1
- package/dist/src/terminal-action-projection.d.ts +1 -2
- package/dist/src/terminal-action-projection.js +4 -5
- package/dist/src/terminal-action-projection.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +82 -0
- package/dist/src/terminal-agent-bridge.js +539 -111
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-command-cli-adapter.d.ts +9 -0
- package/dist/src/terminal-command-cli-adapter.js +2130 -5
- package/dist/src/terminal-command-cli-adapter.js.map +1 -1
- package/dist/src/terminal-dispatch-application.d.ts +4 -1
- package/dist/src/terminal-dispatch-application.js +11 -2
- package/dist/src/terminal-dispatch-application.js.map +1 -1
- package/dist/src/terminal-dispatch-execution.js +3 -1
- package/dist/src/terminal-dispatch-execution.js.map +1 -1
- package/dist/src/terminal-dispatch-ledger-codec.d.ts +3 -0
- package/dist/src/terminal-dispatch-ledger-codec.js +46 -19
- package/dist/src/terminal-dispatch-ledger-codec.js.map +1 -1
- package/dist/src/terminal-dispatch-presenter.js +5 -3
- package/dist/src/terminal-dispatch-presenter.js.map +1 -1
- package/dist/src/terminal-dispatch-receipt.d.ts +3 -0
- package/dist/src/terminal-dispatch-receipt.js +15 -1
- package/dist/src/terminal-dispatch-receipt.js.map +1 -1
- package/dist/src/terminal-dispatch-recovery-cli-adapter.js +68 -7
- package/dist/src/terminal-dispatch-recovery-cli-adapter.js.map +1 -1
- package/dist/src/terminal-dispatch-recovery-service.d.ts +2 -0
- package/dist/src/terminal-dispatch-recovery-service.js +9 -0
- package/dist/src/terminal-dispatch-recovery-service.js.map +1 -1
- package/dist/src/terminal-identity-authority-cli-adapter.d.ts +2 -0
- package/dist/src/terminal-identity-authority-cli-adapter.js +30 -27
- package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -1
- package/dist/src/terminal-list-cli-adapter.d.ts +1 -0
- package/dist/src/terminal-list-cli-adapter.js +50 -31
- package/dist/src/terminal-list-cli-adapter.js.map +1 -1
- package/dist/src/terminal-list-renderer.d.ts +9 -1
- package/dist/src/terminal-list-renderer.js +219 -98
- package/dist/src/terminal-list-renderer.js.map +1 -1
- package/dist/src/terminal-maintenance-cli-adapter.js +253 -326
- package/dist/src/terminal-maintenance-cli-adapter.js.map +1 -1
- package/dist/src/terminal-monitor-application-service.d.ts +5 -0
- package/dist/src/terminal-monitor-application-service.js +5 -0
- package/dist/src/terminal-monitor-application-service.js.map +1 -1
- package/dist/src/terminal-monitor-cli-adapter.d.ts +25 -1
- package/dist/src/terminal-monitor-cli-adapter.js +170 -27
- package/dist/src/terminal-monitor-cli-adapter.js.map +1 -1
- package/dist/src/terminal-monitor-reconciliation-eligibility.d.ts +1 -1
- package/dist/src/terminal-monitor-reconciliation-eligibility.js +220 -32
- package/dist/src/terminal-monitor-reconciliation-eligibility.js.map +1 -1
- package/dist/src/terminal-monitor-state-cli-adapter.d.ts +1 -1
- package/dist/src/terminal-monitor-state-cli-adapter.js +808 -147
- package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
- package/dist/src/terminal-monitor-state-reconciliation-service.d.ts +2 -1
- package/dist/src/terminal-monitor-state-reconciliation-service.js +12 -0
- package/dist/src/terminal-monitor-state-reconciliation-service.js.map +1 -1
- package/dist/src/terminal-monitor-supervision-cli-adapter.d.ts +2 -0
- package/dist/src/terminal-monitor-supervision-cli-adapter.js +31 -11
- package/dist/src/terminal-monitor-supervision-cli-adapter.js.map +1 -1
- package/dist/src/terminal-status-cli-adapter.d.ts +1 -0
- package/dist/src/terminal-status-cli-adapter.js +30 -28
- package/dist/src/terminal-status-cli-adapter.js.map +1 -1
- package/dist/src/terminal-submission-acceptance.js +78 -25
- package/dist/src/terminal-submission-acceptance.js.map +1 -1
- package/dist/src/terminal-submission-retry-service.d.ts +114 -0
- package/dist/src/terminal-submission-retry-service.js +589 -0
- package/dist/src/terminal-submission-retry-service.js.map +1 -0
- package/dist/src/terminal-watch-callback-cli-adapter.d.ts +10 -2
- package/dist/src/terminal-watch-callback-cli-adapter.js +76 -37
- package/dist/src/terminal-watch-callback-cli-adapter.js.map +1 -1
- package/dist/src/terminal-watch-cli-adapter.js +41 -17
- package/dist/src/terminal-watch-cli-adapter.js.map +1 -1
- package/dist/src/terminal-watch-service.d.ts +11 -6
- package/dist/src/terminal-watch-service.js +210 -41
- package/dist/src/terminal-watch-service.js.map +1 -1
- package/dist/src/terminal-watch-store.d.ts +23 -0
- package/dist/src/terminal-watch-store.js +162 -4
- package/dist/src/terminal-watch-store.js.map +1 -1
- package/docs/quickstart-tmux.md +1 -1
- package/package.json +1 -1
- 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
|
|
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
|
|
72
|
-
? conversation
|
|
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
|
-
:
|
|
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
|
-
|
|
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:
|
|
192
|
-
gateway_session:
|
|
193
|
-
|
|
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:
|
|
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: "
|
|
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
|
|
227
|
-
? nextConversation.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
333
|
-
|
|
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 (!
|
|
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:
|
|
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
|
|
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:
|
|
541
|
-
? claim.conversation.gateway_url
|
|
542
|
-
: undefined,
|
|
827
|
+
gatewayUrl: claim.conversation.gateway_url,
|
|
543
828
|
token: stringValue(claim.conversation.gateway_token),
|
|
544
|
-
recordOnly: !
|
|
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
|
|
658
|
-
|
|
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
|
-
|
|
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
|
|
782
|
-
|
|
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: "
|
|
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
|
-
? "
|
|
819
|
-
: "
|
|
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: "
|
|
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: "
|
|
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({
|
|
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
|
-
|
|
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: "
|
|
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: "
|
|
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
|
|
929
|
-
|
|
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");
|