@scotthuang/agent-knock-knock 0.10.1 → 0.10.3
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 +21 -0
- package/README.md +12 -17
- package/dist/src/claude-local-transcript-provider.d.ts +16 -0
- package/dist/src/claude-local-transcript-provider.js +111 -47
- package/dist/src/claude-local-transcript-provider.js.map +1 -1
- package/dist/src/cli.js +3135 -489
- package/dist/src/cli.js.map +1 -1
- package/dist/src/live-lifecycle-smoke.js +7 -2
- package/dist/src/live-lifecycle-smoke.js.map +1 -1
- package/dist/src/openclaw-plugin-helpers.d.ts +1 -0
- package/dist/src/openclaw-plugin-helpers.js +30 -2
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin.d.ts +3 -1
- package/dist/src/openclaw-plugin.js +251 -53
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/store.d.ts +6 -0
- package/dist/src/store.js +15 -1
- package/dist/src/store.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +31 -2
- package/dist/src/terminal-agent-bridge.js +231 -11
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-submission-acceptance.d.ts +118 -0
- package/dist/src/terminal-submission-acceptance.js +785 -0
- package/dist/src/terminal-submission-acceptance.js.map +1 -0
- package/package.json +1 -1
package/dist/src/cli.js
CHANGED
|
@@ -7,14 +7,15 @@ import process from "node:process";
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { createCodexTerminalAgentAdapter, detectCodexDurableCompletion } from "./codex-terminal-agent-adapter.js";
|
|
9
9
|
import { createClaudeTerminalAgentAdapter } from "./claude-terminal-agent-adapter.js";
|
|
10
|
-
import { captureClaudeTranscriptAnchor, defaultClaudeHome, detectClaudeTranscriptCompletion, detectClaudeTranscriptPendingApproval, listClaudeThreadLifecycleCandidates, revalidateClaudeThreadLifecycleCandidate } from "./claude-local-transcript-provider.js";
|
|
10
|
+
import { captureClaudeTranscriptAnchor, defaultClaudeHome, detectClaudeTranscriptAcceptance, detectClaudeTranscriptCompletion, detectClaudeTranscriptPendingApproval, listClaudeThreadLifecycleCandidates, revalidateClaudeThreadLifecycleCandidate } from "./claude-local-transcript-provider.js";
|
|
11
|
+
import { captureCodexRolloutAcceptanceAnchor, detectCodexBoundRolloutCompletion, detectCodexRolloutAcceptance, terminalSubmissionReplayReceipt, validateTerminalSubmissionAcceptanceEvidence } from "./terminal-submission-acceptance.js";
|
|
11
12
|
import { CodexLocalSessionProvider } from "./codex-local-session-provider.js";
|
|
12
13
|
import { CodexStoreAdapter } from "./codex-store-adapter.js";
|
|
13
14
|
import { applyMessageToConversation, budgetAction, createConversation, createMessage, effectiveTurnStatus, executorForConversation, extractStructuredMessage, isTurnPhaseStatus, normalizeLegacyCallbackStatus, parseMessageJson, resolveExecutor, sessionIdForConversation, turnIdForConversation } from "./protocol.js";
|
|
14
15
|
import { EXECUTOR_KINDS, executorDefinitionForKind, isExecutorKind } from "./executors.js";
|
|
15
16
|
import { redactString, writeRuntimeLog } from "./runtime-log.js";
|
|
16
17
|
import { formatTranscript, readNdjsonLog } from "./transcript.js";
|
|
17
|
-
import { appendEvent, assertStoreWriterCompatible, defaultStoreDir, ensureDir, ensureStoreWritable, inspectStoreCompatibility, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, statePathForConversationId, withStoreWriterLeaseAsync } from "./store.js";
|
|
18
|
+
import { appendEvent, assertStoreWriterCompatible, defaultStoreDir, ensureDir, ensureStoreWritable, inspectStoreCompatibility, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, StoreLockTimeoutError, statePathForConversationId, withStoreWriterLease, withStoreWriterLeaseAsync } from "./store.js";
|
|
18
19
|
import { createManagedSessionId, createNativeThreadTransitionId, isExactNativeThreadId, managedSessionBindingToken, nativeThreadCommandFingerprint, terminalBindingFrom, unmanagedTerminalBindingToken } from "./managed-session.js";
|
|
19
20
|
import { classifyCodexLifecyclePostcondition, evaluateResumeCandidateAvailability, hasStrongCodexLifecycleIdentity, isFreshCodexPostProbeScreen } from "./native-thread-lifecycle-policy.js";
|
|
20
21
|
import { listManagedSessions, loadManagedSession, loadNativeThreadTransition, saveManagedSession, saveNativeThreadTransition, tryLoadManagedSession } from "./session-store.js";
|
|
@@ -31,6 +32,8 @@ const DEFAULT_IDLE_TIMEOUT_MINUTES = 10080;
|
|
|
31
32
|
const DEFAULT_AGENT_TIMEOUT_MINUTES = 60;
|
|
32
33
|
const DEFAULT_AGENT_HARD_TIMEOUT_MINUTES = 720;
|
|
33
34
|
const DEFAULT_MONITOR_POLL_INTERVAL_MS = 5000;
|
|
35
|
+
const DEFAULT_TERMINAL_ACCEPTANCE_TIMEOUT_MS = 5000;
|
|
36
|
+
const DEFAULT_TERMINAL_ACCEPTANCE_POLL_INTERVAL_MS = 50;
|
|
34
37
|
const CLAUDE_SCREEN_APPROVAL_TTL_MS = 10 * 60 * 1000;
|
|
35
38
|
const CALLBACK_DELIVERY_TIMEOUT_MS = 30_000;
|
|
36
39
|
const CALLBACK_AGENT_WAIT_TIMEOUT_MS = 20_000;
|
|
@@ -38,6 +41,32 @@ const CALLBACK_AGENT_WAIT_CLI_TIMEOUT_MS = 25_000;
|
|
|
38
41
|
const CALLBACK_AGENT_WAIT_PROCESS_TIMEOUT_MS = 30_000;
|
|
39
42
|
const CALLBACK_ATTEMPT_LEASE_MS = 2 * 60 * 1000;
|
|
40
43
|
const CALLBACK_RETRY_DELAYS_MS = [5000, 15000, 60000, 60000];
|
|
44
|
+
const TERMINAL_LEDGER_RECEIPT_STATUSES = new Set([
|
|
45
|
+
"text_injected",
|
|
46
|
+
"enter_dispatched",
|
|
47
|
+
"submitted",
|
|
48
|
+
"agent_accepted",
|
|
49
|
+
"not_accepted",
|
|
50
|
+
"uncertain",
|
|
51
|
+
"aborted"
|
|
52
|
+
]);
|
|
53
|
+
const TERMINAL_LEDGER_RECEIPT_IMMUTABLE_FIELDS = [
|
|
54
|
+
"binding_id",
|
|
55
|
+
"binding_generation",
|
|
56
|
+
"native_thread_id",
|
|
57
|
+
"store_dir",
|
|
58
|
+
"conversation_id",
|
|
59
|
+
"session_id",
|
|
60
|
+
"turn_id",
|
|
61
|
+
"message_id",
|
|
62
|
+
"message_type",
|
|
63
|
+
"message_body_hash",
|
|
64
|
+
"request_hash",
|
|
65
|
+
"executor_kind",
|
|
66
|
+
"openclaw_session",
|
|
67
|
+
"state_path",
|
|
68
|
+
"event_log_path"
|
|
69
|
+
];
|
|
41
70
|
const TERMINAL_BRIDGE_SUPERSEDE_STATUSES = new Set([
|
|
42
71
|
"created",
|
|
43
72
|
"running",
|
|
@@ -96,6 +125,13 @@ const STORE_MUTATION_COMMANDS = new Set([
|
|
|
96
125
|
"clear-thread",
|
|
97
126
|
"resume-thread"
|
|
98
127
|
]);
|
|
128
|
+
class TurnBindingSupersededError extends Error {
|
|
129
|
+
code = "AKK_TURN_BINDING_SUPERSEDED";
|
|
130
|
+
constructor(message) {
|
|
131
|
+
super(message);
|
|
132
|
+
this.name = "TurnBindingSupersededError";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
99
135
|
class InlineCodexSessionAdapter {
|
|
100
136
|
threads;
|
|
101
137
|
processes;
|
|
@@ -682,6 +718,16 @@ function createRuntimeTerminalAgentRegistry(options) {
|
|
|
682
718
|
if (!isRecord(conversation)) {
|
|
683
719
|
return undefined;
|
|
684
720
|
}
|
|
721
|
+
const exactCompletion = detectExactBoundCodexCompletion({
|
|
722
|
+
conversation,
|
|
723
|
+
nativeTakeover,
|
|
724
|
+
request,
|
|
725
|
+
runtime,
|
|
726
|
+
options
|
|
727
|
+
});
|
|
728
|
+
if (exactCompletion.handled) {
|
|
729
|
+
return exactCompletion.completion;
|
|
730
|
+
}
|
|
685
731
|
const contextMatches = await loadCodexTerminalContexts({
|
|
686
732
|
nativeTakeover,
|
|
687
733
|
options
|
|
@@ -737,6 +783,89 @@ function createRuntimeTerminalAgentRegistry(options) {
|
|
|
737
783
|
]
|
|
738
784
|
});
|
|
739
785
|
}
|
|
786
|
+
function detectExactBoundCodexCompletion({ conversation, nativeTakeover, request, runtime, options }) {
|
|
787
|
+
const submission = terminalBridgeSubmission(conversation);
|
|
788
|
+
const acceptanceEvidence = isRecord(submission?.acceptance_evidence)
|
|
789
|
+
? submission.acceptance_evidence
|
|
790
|
+
: undefined;
|
|
791
|
+
const anchor = isRecord(nativeTakeover?.codex_rollout_acceptance_anchor)
|
|
792
|
+
? nativeTakeover.codex_rollout_acceptance_anchor
|
|
793
|
+
: undefined;
|
|
794
|
+
if (submission?.status !== "agent_accepted") {
|
|
795
|
+
return { handled: false };
|
|
796
|
+
}
|
|
797
|
+
const exactRequired = requiresExactBoundCodexCompletion(conversation, options);
|
|
798
|
+
if (!exactRequired) {
|
|
799
|
+
return { handled: false };
|
|
800
|
+
}
|
|
801
|
+
if (acceptanceEvidence?.source !== "codex_rollout") {
|
|
802
|
+
throw new Error("[codex_exact_bound_rollout:invalid_acceptance_evidence] " +
|
|
803
|
+
"the accepted modern Codex Turn has no exact rollout acceptance evidence");
|
|
804
|
+
}
|
|
805
|
+
if (!anchor) {
|
|
806
|
+
throw new Error("[codex_exact_bound_rollout:invalid_anchor] " +
|
|
807
|
+
"the accepted modern Codex Turn has no exact rollout byte anchor");
|
|
808
|
+
}
|
|
809
|
+
const nativeRollout = isRecord(runtime?.nativeRollout)
|
|
810
|
+
? runtime.nativeRollout
|
|
811
|
+
: undefined;
|
|
812
|
+
const result = detectCodexBoundRolloutCompletion({
|
|
813
|
+
anchor: anchor,
|
|
814
|
+
acceptanceEvidence: acceptanceEvidence,
|
|
815
|
+
currentIdentity: {
|
|
816
|
+
sessionId: stringValue(runtime?.nativeSessionId) ??
|
|
817
|
+
stringValue(runtime?.sessionId) ??
|
|
818
|
+
"",
|
|
819
|
+
processUuid: stringValue(runtime?.nativeProcessUuid),
|
|
820
|
+
processBirth: stringValue(runtime?.nativeProcessBirth),
|
|
821
|
+
...(nativeRollout
|
|
822
|
+
? {
|
|
823
|
+
rollout: {
|
|
824
|
+
fd: String(nativeRollout.fd ?? ""),
|
|
825
|
+
device: String(nativeRollout.device ?? ""),
|
|
826
|
+
inode: String(nativeRollout.inode ?? ""),
|
|
827
|
+
path: String(nativeRollout.path ?? "")
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
: {})
|
|
831
|
+
},
|
|
832
|
+
requestHash: stringValue(request.requestHash) ??
|
|
833
|
+
stringValue(nativeTakeover?.terminal_bridge_request_hash) ??
|
|
834
|
+
""
|
|
835
|
+
});
|
|
836
|
+
if (result.status === "failure") {
|
|
837
|
+
throw new Error(`[codex_exact_bound_rollout:${result.diagnostics.code}] ${result.diagnostics.detail ?? "the exact bound rollout is not safely inspectable"}`);
|
|
838
|
+
}
|
|
839
|
+
if (result.status === "pending") {
|
|
840
|
+
return { handled: true };
|
|
841
|
+
}
|
|
842
|
+
return {
|
|
843
|
+
handled: true,
|
|
844
|
+
completion: {
|
|
845
|
+
...result.completion,
|
|
846
|
+
metadata: {
|
|
847
|
+
...result.completion.metadata,
|
|
848
|
+
context_match: "exact_bound_rollout",
|
|
849
|
+
detector_code: result.diagnostics.code
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
function requiresExactBoundCodexCompletion(conversation, options) {
|
|
855
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
856
|
+
? conversation.native_session_takeover
|
|
857
|
+
: undefined;
|
|
858
|
+
const submission = terminalBridgeSubmission(conversation);
|
|
859
|
+
if (submission?.status !== "agent_accepted") {
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
const hasExactArtifacts = isRecord(nativeTakeover?.codex_rollout_acceptance_anchor) &&
|
|
863
|
+
isRecord(submission.acceptance_evidence) &&
|
|
864
|
+
submission.acceptance_evidence.source === "codex_rollout";
|
|
865
|
+
const modernProductionTurn = Number(nativeTakeover?.terminal_agent_identity_protocol) === 1 &&
|
|
866
|
+
!allowsSyntheticTerminalAcceptance(options);
|
|
867
|
+
return hasExactArtifacts || modernProductionTurn;
|
|
868
|
+
}
|
|
740
869
|
function createTerminalAgentBridge(options, terminalProvider = createTerminalControlProvider(options), registry = createRuntimeTerminalAgentRegistry(options)) {
|
|
741
870
|
const processSource = createTerminalProcessSource(options);
|
|
742
871
|
return new TerminalAgentBridge({
|
|
@@ -1063,6 +1192,158 @@ function isTerminalControlCapability(value) {
|
|
|
1063
1192
|
"terminal_cancel"
|
|
1064
1193
|
].includes(value);
|
|
1065
1194
|
}
|
|
1195
|
+
function assertSafeAbortedTerminalRetryBinding({ owner, receipt, storeDir, terminalControl, messageId }) {
|
|
1196
|
+
if (!(receipt.status === "aborted" && receipt.safe_to_retry === true)) {
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
const sessionId = sessionIdForConversation(owner);
|
|
1200
|
+
const managedSession = tryLoadManagedSession(storeDir, sessionId);
|
|
1201
|
+
const binding = managedSession?.binding;
|
|
1202
|
+
const receiptBindingId = stringValue(receipt.binding_id);
|
|
1203
|
+
const receiptBindingGeneration = Number(receipt.binding_generation);
|
|
1204
|
+
const receiptNativeThreadId = stringValue(receipt.native_thread_id);
|
|
1205
|
+
const ownerTakeover = isRecord(owner.native_session_takeover)
|
|
1206
|
+
? owner.native_session_takeover
|
|
1207
|
+
: undefined;
|
|
1208
|
+
const ownerControl = terminalControlFromTakeover(ownerTakeover);
|
|
1209
|
+
const ownerNativeThreadId = stringValue(owner.native_thread_id) ??
|
|
1210
|
+
stringValue(ownerTakeover?.terminal_agent_session_id) ??
|
|
1211
|
+
stringValue(ownerTakeover?.terminal_agent_expected_session_id);
|
|
1212
|
+
const ownerAgentPid = Number(ownerTakeover?.terminal_agent_pid);
|
|
1213
|
+
if (!managedSession ||
|
|
1214
|
+
managedSession.status !== "bound" ||
|
|
1215
|
+
!binding ||
|
|
1216
|
+
!receiptBindingId ||
|
|
1217
|
+
!Number.isSafeInteger(receiptBindingGeneration) ||
|
|
1218
|
+
!receiptNativeThreadId ||
|
|
1219
|
+
receiptBindingId !== stringValue(owner.terminal_binding_id) ||
|
|
1220
|
+
receiptBindingGeneration !== Number(owner.terminal_binding_generation) ||
|
|
1221
|
+
receiptNativeThreadId !== ownerNativeThreadId ||
|
|
1222
|
+
binding.binding_id !== receiptBindingId ||
|
|
1223
|
+
binding.generation !== receiptBindingGeneration ||
|
|
1224
|
+
binding.native_thread_id !== receiptNativeThreadId ||
|
|
1225
|
+
!Number.isSafeInteger(ownerAgentPid) ||
|
|
1226
|
+
binding.native_process.pid !== ownerAgentPid ||
|
|
1227
|
+
!ownerControl ||
|
|
1228
|
+
terminalControlSelectorKey(ownerControl) !==
|
|
1229
|
+
terminalControlSelectorKey(terminalControl) ||
|
|
1230
|
+
terminalControlSelectorKey(binding.terminal_control) !==
|
|
1231
|
+
terminalControlSelectorKey(terminalControl)) {
|
|
1232
|
+
throw new Error(`terminal idempotency key ${messageId} belongs to a safe-aborted Turn ` +
|
|
1233
|
+
"whose Session binding is no longer current; no terminal input was sent");
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
function stableDelegateTerminalRoute({ options, request, workspace, requestedAgent }) {
|
|
1237
|
+
const messageId = stringValue(options.messageId);
|
|
1238
|
+
if (!messageId) {
|
|
1239
|
+
return undefined;
|
|
1240
|
+
}
|
|
1241
|
+
const storeDir = path.resolve(storeDirFromOptions(options));
|
|
1242
|
+
const requestHash = terminalBridgeRequestFingerprint(terminalSubmissionPayload(request));
|
|
1243
|
+
const bodyHash = createHash("sha256").update(request).digest("hex");
|
|
1244
|
+
const requestedOpenClawSession = stringValue(options.openclawSession);
|
|
1245
|
+
const matches = listConversations(storeDir).flatMap((owner) => terminalBridgeSubmissionReceipts(owner)
|
|
1246
|
+
.filter((receipt) => stringValue(receipt.message_id) === messageId)
|
|
1247
|
+
.map((receipt) => ({ owner, receipt })));
|
|
1248
|
+
if (matches.length === 0) {
|
|
1249
|
+
return undefined;
|
|
1250
|
+
}
|
|
1251
|
+
const routed = matches.map(({ owner, receipt }) => {
|
|
1252
|
+
const ownerStoreDir = managedSessionStoreDirForConversation(owner);
|
|
1253
|
+
const takeover = isRecord(owner.native_session_takeover)
|
|
1254
|
+
? owner.native_session_takeover
|
|
1255
|
+
: undefined;
|
|
1256
|
+
const terminalControl = terminalControlFromTakeover(takeover);
|
|
1257
|
+
const conversationId = stringValue(takeover?.native_session_id);
|
|
1258
|
+
let eventMessages = [];
|
|
1259
|
+
const eventLogPath = stringValue(owner.event_log_path);
|
|
1260
|
+
if (eventLogPath) {
|
|
1261
|
+
try {
|
|
1262
|
+
eventMessages = readNdjsonLog(eventLogPath)
|
|
1263
|
+
.filter((event) => isRecord(event.message) && event.message.id === messageId)
|
|
1264
|
+
.map((event) => event.message);
|
|
1265
|
+
}
|
|
1266
|
+
catch {
|
|
1267
|
+
eventMessages = [];
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
if (eventMessages.length > 1) {
|
|
1271
|
+
throw new Error(`terminal idempotency key ${messageId} has duplicate durable messages`);
|
|
1272
|
+
}
|
|
1273
|
+
const eventMessage = eventMessages[0];
|
|
1274
|
+
const messageType = stringValue(receipt.message_type) ??
|
|
1275
|
+
(isRecord(eventMessage) ? stringValue(eventMessage.type) : undefined);
|
|
1276
|
+
const storedBodyHash = stringValue(receipt.message_body_hash) ??
|
|
1277
|
+
(isRecord(eventMessage) && typeof eventMessage.body === "string"
|
|
1278
|
+
? createHash("sha256").update(eventMessage.body).digest("hex")
|
|
1279
|
+
: undefined);
|
|
1280
|
+
const ownerWorkspace = canonicalWorkspace(owner.workspace);
|
|
1281
|
+
if (!ownerStoreDir ||
|
|
1282
|
+
path.resolve(ownerStoreDir) !== storeDir ||
|
|
1283
|
+
(stringValue(receipt.store_dir) !== undefined &&
|
|
1284
|
+
path.resolve(String(receipt.store_dir)) !== storeDir) ||
|
|
1285
|
+
!terminalControl ||
|
|
1286
|
+
!conversationId ||
|
|
1287
|
+
stringValue(receipt.request_hash) !== requestHash ||
|
|
1288
|
+
messageType !== "task" ||
|
|
1289
|
+
storedBodyHash !== bodyHash ||
|
|
1290
|
+
(requestedOpenClawSession &&
|
|
1291
|
+
(stringValue(receipt.openclaw_session) ?? owner.openclaw_session) !==
|
|
1292
|
+
requestedOpenClawSession) ||
|
|
1293
|
+
(requestedAgent && executorForConversation(owner).kind !== requestedAgent) ||
|
|
1294
|
+
(workspace && ownerWorkspace !== workspace)) {
|
|
1295
|
+
throw new Error(`terminal idempotency key ${messageId} does not match its original ` +
|
|
1296
|
+
"delegate request boundary; no terminal input was sent");
|
|
1297
|
+
}
|
|
1298
|
+
return {
|
|
1299
|
+
owner,
|
|
1300
|
+
receipt,
|
|
1301
|
+
conversationId,
|
|
1302
|
+
workspace: ownerWorkspace,
|
|
1303
|
+
terminalKey: terminalControlSelectorKey(terminalControl),
|
|
1304
|
+
panePid: terminalControl.panePid
|
|
1305
|
+
};
|
|
1306
|
+
});
|
|
1307
|
+
const authoritative = routed.filter(({ receipt }) => !(receipt.status === "aborted" && receipt.safe_to_retry === true));
|
|
1308
|
+
if (authoritative.length > 1) {
|
|
1309
|
+
throw new Error(`terminal idempotency key ${messageId} has multiple durable delegate receipts`);
|
|
1310
|
+
}
|
|
1311
|
+
const terminalKeys = new Set(routed.map((entry) => `${entry.conversationId}\0${entry.terminalKey}\0${entry.panePid}`));
|
|
1312
|
+
if (terminalKeys.size !== 1) {
|
|
1313
|
+
throw new Error(`terminal idempotency key ${messageId} has conflicting terminal routes`);
|
|
1314
|
+
}
|
|
1315
|
+
const selected = authoritative[0] ?? routed.at(-1);
|
|
1316
|
+
if (!selected) {
|
|
1317
|
+
return undefined;
|
|
1318
|
+
}
|
|
1319
|
+
if (selected.receipt.status === "aborted" &&
|
|
1320
|
+
selected.receipt.safe_to_retry === true) {
|
|
1321
|
+
const sessionId = sessionIdForConversation(selected.owner);
|
|
1322
|
+
const ownerControl = terminalControlFromTakeover(isRecord(selected.owner.native_session_takeover)
|
|
1323
|
+
? selected.owner.native_session_takeover
|
|
1324
|
+
: undefined);
|
|
1325
|
+
if (!ownerControl) {
|
|
1326
|
+
throw new Error(`terminal idempotency key ${messageId} has no durable terminal route`);
|
|
1327
|
+
}
|
|
1328
|
+
assertSafeAbortedTerminalRetryBinding({
|
|
1329
|
+
owner: selected.owner,
|
|
1330
|
+
receipt: selected.receipt,
|
|
1331
|
+
storeDir,
|
|
1332
|
+
terminalControl: ownerControl,
|
|
1333
|
+
messageId
|
|
1334
|
+
});
|
|
1335
|
+
return {
|
|
1336
|
+
kind: "session",
|
|
1337
|
+
sessionId,
|
|
1338
|
+
workspace: selected.workspace
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
return {
|
|
1342
|
+
kind: "terminal",
|
|
1343
|
+
conversationId: selected.conversationId,
|
|
1344
|
+
workspace: selected.workspace
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1066
1347
|
async function runDelegate(options) {
|
|
1067
1348
|
const request = required(options.request, "--request is required");
|
|
1068
1349
|
const workspace = options.workspace === undefined
|
|
@@ -1071,6 +1352,32 @@ async function runDelegate(options) {
|
|
|
1071
1352
|
const requestedAgent = options.agent === undefined
|
|
1072
1353
|
? undefined
|
|
1073
1354
|
: resolveExecutor({ kind: options.agent }).kind;
|
|
1355
|
+
const stableRoute = stableDelegateTerminalRoute({
|
|
1356
|
+
options,
|
|
1357
|
+
request,
|
|
1358
|
+
workspace,
|
|
1359
|
+
requestedAgent
|
|
1360
|
+
});
|
|
1361
|
+
if (stableRoute) {
|
|
1362
|
+
await runSend(stableRoute.kind === "session"
|
|
1363
|
+
? {
|
|
1364
|
+
...options,
|
|
1365
|
+
session: stableRoute.sessionId,
|
|
1366
|
+
conversation: undefined,
|
|
1367
|
+
message: request,
|
|
1368
|
+
workspace: stableRoute.workspace,
|
|
1369
|
+
background: true
|
|
1370
|
+
}
|
|
1371
|
+
: {
|
|
1372
|
+
...options,
|
|
1373
|
+
conversation: stableRoute.conversationId,
|
|
1374
|
+
session: undefined,
|
|
1375
|
+
message: request,
|
|
1376
|
+
workspace: stableRoute.workspace,
|
|
1377
|
+
background: true
|
|
1378
|
+
});
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1074
1381
|
const scan = await buildTerminalListGroup({
|
|
1075
1382
|
options: {
|
|
1076
1383
|
...options,
|
|
@@ -1340,7 +1647,7 @@ function terminalBridgeEnabled(conversation) {
|
|
|
1340
1647
|
: undefined;
|
|
1341
1648
|
return nativeTakeover?.["terminal_bridge"] === true;
|
|
1342
1649
|
}
|
|
1343
|
-
function withTerminalBridgeState({ conversation, message, requestText, startedAt, agentTimeoutMinutes, agentHardTimeoutMinutes, preSendScreenFingerprint, claudeTranscriptAnchor, claudeHome }) {
|
|
1650
|
+
function withTerminalBridgeState({ conversation, message, requestText, startedAt, agentTimeoutMinutes, agentHardTimeoutMinutes, preSendScreenFingerprint, codexRolloutAcceptanceAnchor, claudeTranscriptAnchor, claudeHome }) {
|
|
1344
1651
|
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
1345
1652
|
? conversation.native_session_takeover
|
|
1346
1653
|
: {};
|
|
@@ -1354,10 +1661,12 @@ function withTerminalBridgeState({ conversation, message, requestText, startedAt
|
|
|
1354
1661
|
terminal_bridge_request_text: requestText,
|
|
1355
1662
|
terminal_bridge_request_hash: terminalBridgeRequestFingerprint(requestText),
|
|
1356
1663
|
terminal_bridge_pre_send_screen_fingerprint: preSendScreenFingerprint,
|
|
1664
|
+
codex_rollout_acceptance_anchor: codexRolloutAcceptanceAnchor,
|
|
1357
1665
|
claude_transcript_anchor: claudeTranscriptAnchor,
|
|
1358
1666
|
claude_home: claudeHome,
|
|
1359
1667
|
terminal_bridge_completion_claim: undefined,
|
|
1360
1668
|
terminal_bridge_approval_dispatch: undefined,
|
|
1669
|
+
terminal_bridge_detector_diagnostic: undefined,
|
|
1361
1670
|
terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
|
|
1362
1671
|
terminal_bridge_monitor_started_at: startedAt,
|
|
1363
1672
|
terminal_bridge_last_activity_at: startedAt,
|
|
@@ -1369,40 +1678,653 @@ function withTerminalBridgeState({ conversation, message, requestText, startedAt
|
|
|
1369
1678
|
updated_at: startedAt
|
|
1370
1679
|
};
|
|
1371
1680
|
}
|
|
1372
|
-
function withTerminalBridgeSubmission({ conversation, messageId, requestText, status, preparedAt, submittedAt, uncertainAt, abortedAt, error }) {
|
|
1681
|
+
function withTerminalBridgeSubmission({ conversation, messageId, messageType, messageBody, requestText, status, preparedAt, textInjectedAt, enterDispatchedAt, agentAcceptedAt, notAcceptedAt, submittedAt, uncertainAt, abortedAt, error, acceptanceEvidence, lastProvenStage, safeToRetry }) {
|
|
1373
1682
|
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
1374
1683
|
? conversation.native_session_takeover
|
|
1375
1684
|
: {};
|
|
1376
1685
|
const previousSubmission = isRecord(nativeTakeover.terminal_bridge_submission)
|
|
1377
1686
|
? nativeTakeover.terminal_bridge_submission
|
|
1378
1687
|
: undefined;
|
|
1379
|
-
const
|
|
1688
|
+
const receiptsValue = nativeTakeover.terminal_bridge_submission_receipts;
|
|
1689
|
+
if (receiptsValue !== undefined && !Array.isArray(receiptsValue)) {
|
|
1690
|
+
throw new Error("terminal submission receipt history is malformed");
|
|
1691
|
+
}
|
|
1692
|
+
const storedReceipts = (Array.isArray(receiptsValue) ? receiptsValue : [])
|
|
1693
|
+
.map((value) => {
|
|
1694
|
+
if (!isRecord(value) || !stringValue(value.message_id)) {
|
|
1695
|
+
throw new Error("terminal submission receipt history is malformed");
|
|
1696
|
+
}
|
|
1697
|
+
return value;
|
|
1698
|
+
});
|
|
1699
|
+
const storedReceiptIds = new Set();
|
|
1700
|
+
for (const receipt of storedReceipts) {
|
|
1701
|
+
const receiptId = String(receipt.message_id);
|
|
1702
|
+
if (storedReceiptIds.has(receiptId)) {
|
|
1703
|
+
throw new Error(`terminal submission receipt ${receiptId} is duplicated`);
|
|
1704
|
+
}
|
|
1705
|
+
storedReceiptIds.add(receiptId);
|
|
1706
|
+
}
|
|
1707
|
+
// A v0.10.1 Turn has only the latest singleton receipt. Seed it into the
|
|
1708
|
+
// append-only history before recording a later response so upgrading cannot
|
|
1709
|
+
// erase the only proof that an older message was already dispatched.
|
|
1710
|
+
const previousSubmissionId = stringValue(previousSubmission?.message_id);
|
|
1711
|
+
const previousReceipts = previousSubmissionId &&
|
|
1712
|
+
!storedReceiptIds.has(previousSubmissionId)
|
|
1713
|
+
? [...storedReceipts, previousSubmission]
|
|
1714
|
+
: storedReceipts;
|
|
1715
|
+
const matchingReceipts = previousReceipts.filter((receipt) => stringValue(receipt.message_id) === messageId);
|
|
1716
|
+
if (matchingReceipts.length > 1) {
|
|
1717
|
+
throw new Error(`terminal submission receipt ${messageId} is duplicated`);
|
|
1718
|
+
}
|
|
1719
|
+
const previousReceipt = matchingReceipts[0];
|
|
1720
|
+
const previousGenerationSubmission = stringValue(previousSubmission?.message_id) === messageId
|
|
1721
|
+
? previousSubmission
|
|
1722
|
+
: undefined;
|
|
1723
|
+
if (previousReceipt &&
|
|
1724
|
+
previousGenerationSubmission &&
|
|
1725
|
+
canonicalJson(previousReceipt) !== canonicalJson(previousGenerationSubmission)) {
|
|
1726
|
+
throw new Error(`terminal submission receipt ${messageId} conflicts with its current generation`);
|
|
1727
|
+
}
|
|
1728
|
+
const durableMessageType = messageType ??
|
|
1729
|
+
(stringValue(previousReceipt?.message_type) === "answer"
|
|
1730
|
+
? "answer"
|
|
1731
|
+
: stringValue(previousReceipt?.message_type) === "task"
|
|
1732
|
+
? "task"
|
|
1733
|
+
: stringValue(previousGenerationSubmission?.message_type) === "answer"
|
|
1734
|
+
? "answer"
|
|
1735
|
+
: stringValue(previousGenerationSubmission?.message_type) === "task"
|
|
1736
|
+
? "task"
|
|
1737
|
+
: undefined);
|
|
1738
|
+
const messageBodyHash = messageBody !== undefined
|
|
1739
|
+
? createHash("sha256").update(messageBody).digest("hex")
|
|
1740
|
+
: stringValue(previousReceipt?.message_body_hash) ??
|
|
1741
|
+
stringValue(previousGenerationSubmission?.message_body_hash);
|
|
1742
|
+
const previousDispatcherPid = Number(previousGenerationSubmission?.dispatcher_pid);
|
|
1380
1743
|
const dispatcherPid = status === "prepared" ||
|
|
1381
1744
|
!Number.isSafeInteger(previousDispatcherPid) ||
|
|
1382
1745
|
previousDispatcherPid <= 1
|
|
1383
1746
|
? process.pid
|
|
1384
1747
|
: previousDispatcherPid;
|
|
1748
|
+
const provenStage = lastProvenStage ?? terminalSubmissionLastProvenStage(status, stringValue(previousGenerationSubmission?.last_proven_stage));
|
|
1749
|
+
const validatedAcceptanceEvidence = status === "agent_accepted"
|
|
1750
|
+
? terminalAcceptanceEvidenceForConversation(conversation, requestText, acceptanceEvidence)
|
|
1751
|
+
: undefined;
|
|
1752
|
+
const storedControl = terminalControlFromTakeover(nativeTakeover);
|
|
1753
|
+
const requestHash = terminalBridgeRequestFingerprint(requestText);
|
|
1754
|
+
const candidateImmutableReceiptFields = {
|
|
1755
|
+
session_id: sessionIdForConversation(conversation),
|
|
1756
|
+
turn_id: turnIdForConversation(conversation),
|
|
1757
|
+
message_id: messageId,
|
|
1758
|
+
binding_id: stringValue(conversation.terminal_binding_id),
|
|
1759
|
+
binding_generation: Number.isSafeInteger(Number(conversation.terminal_binding_generation))
|
|
1760
|
+
? Number(conversation.terminal_binding_generation)
|
|
1761
|
+
: undefined,
|
|
1762
|
+
...(durableMessageType ? { message_type: durableMessageType } : {}),
|
|
1763
|
+
...(messageBodyHash ? { message_body_hash: messageBodyHash } : {}),
|
|
1764
|
+
request_hash: requestHash,
|
|
1765
|
+
executor_kind: executorForConversation(conversation).kind,
|
|
1766
|
+
openclaw_session: conversation.openclaw_session,
|
|
1767
|
+
store_dir: managedSessionStoreDirForConversation(conversation),
|
|
1768
|
+
native_thread_id: stringValue(conversation.native_thread_id) ??
|
|
1769
|
+
stringValue(nativeTakeover.terminal_agent_session_id) ??
|
|
1770
|
+
stringValue(nativeTakeover.terminal_agent_expected_session_id),
|
|
1771
|
+
terminal_target: storedControl?.target,
|
|
1772
|
+
terminal_socket_path: storedControl?.socketPath ?? null,
|
|
1773
|
+
terminal_pane_pid: storedControl?.panePid
|
|
1774
|
+
};
|
|
1775
|
+
const immutableReceiptFields = Object.fromEntries(Object.entries(candidateImmutableReceiptFields).map(([key, value]) => [
|
|
1776
|
+
key,
|
|
1777
|
+
value === undefined && previousReceipt?.[key] !== undefined
|
|
1778
|
+
? previousReceipt[key]
|
|
1779
|
+
: value
|
|
1780
|
+
]));
|
|
1781
|
+
if (previousReceipt) {
|
|
1782
|
+
for (const [key, value] of Object.entries(candidateImmutableReceiptFields)) {
|
|
1783
|
+
if (previousReceipt[key] !== undefined &&
|
|
1784
|
+
value !== undefined &&
|
|
1785
|
+
String(previousReceipt[key]) !== String(value)) {
|
|
1786
|
+
throw new Error(`terminal submission receipt ${messageId} changed immutable ${key}`);
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
const nextSubmission = {
|
|
1791
|
+
status,
|
|
1792
|
+
...immutableReceiptFields,
|
|
1793
|
+
prepared_at: preparedAt,
|
|
1794
|
+
dispatcher_pid: dispatcherPid,
|
|
1795
|
+
last_proven_stage: provenStage,
|
|
1796
|
+
...(textInjectedAt ? { text_injected_at: textInjectedAt } : {}),
|
|
1797
|
+
...(enterDispatchedAt ? { enter_dispatched_at: enterDispatchedAt } : {}),
|
|
1798
|
+
...(agentAcceptedAt ? { agent_accepted_at: agentAcceptedAt } : {}),
|
|
1799
|
+
...(notAcceptedAt ? { not_accepted_at: notAcceptedAt } : {}),
|
|
1800
|
+
...(submittedAt ? { submitted_at: submittedAt } : {}),
|
|
1801
|
+
...(uncertainAt ? { uncertain_at: uncertainAt } : {}),
|
|
1802
|
+
...(abortedAt ? { aborted_at: abortedAt } : {}),
|
|
1803
|
+
...(error ? { error: textSummary(error) } : {}),
|
|
1804
|
+
...(safeToRetry !== undefined ? { safe_to_retry: safeToRetry } : {}),
|
|
1805
|
+
...(validatedAcceptanceEvidence
|
|
1806
|
+
? { acceptance_evidence: validatedAcceptanceEvidence }
|
|
1807
|
+
: {})
|
|
1808
|
+
};
|
|
1809
|
+
const nextReceipts = previousReceipt
|
|
1810
|
+
? previousReceipts.map((receipt) => stringValue(receipt.message_id) === messageId
|
|
1811
|
+
? nextSubmission
|
|
1812
|
+
: receipt)
|
|
1813
|
+
: [...previousReceipts, nextSubmission];
|
|
1385
1814
|
return {
|
|
1386
1815
|
...conversation,
|
|
1387
1816
|
native_session_takeover: {
|
|
1388
1817
|
...nativeTakeover,
|
|
1389
|
-
terminal_bridge_submission:
|
|
1390
|
-
|
|
1391
|
-
session_id: sessionIdForConversation(conversation),
|
|
1392
|
-
turn_id: turnIdForConversation(conversation),
|
|
1393
|
-
message_id: messageId,
|
|
1394
|
-
request_hash: terminalBridgeRequestFingerprint(requestText),
|
|
1395
|
-
prepared_at: preparedAt,
|
|
1396
|
-
dispatcher_pid: dispatcherPid,
|
|
1397
|
-
...(submittedAt ? { submitted_at: submittedAt } : {}),
|
|
1398
|
-
...(uncertainAt ? { uncertain_at: uncertainAt } : {}),
|
|
1399
|
-
...(abortedAt ? { aborted_at: abortedAt } : {}),
|
|
1400
|
-
...(error ? { error: textSummary(error) } : {})
|
|
1401
|
-
}
|
|
1818
|
+
terminal_bridge_submission: nextSubmission,
|
|
1819
|
+
terminal_bridge_submission_receipts: nextReceipts
|
|
1402
1820
|
},
|
|
1403
|
-
updated_at:
|
|
1821
|
+
updated_at: agentAcceptedAt ?? notAcceptedAt ?? uncertainAt ?? abortedAt ??
|
|
1822
|
+
enterDispatchedAt ?? textInjectedAt ?? submittedAt ?? preparedAt
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
function terminalAcceptanceEvidenceForConversation(conversation, requestText, evidence) {
|
|
1826
|
+
return validateTerminalSubmissionAcceptanceEvidence(evidence, terminalAcceptanceEvidenceExpectation(conversation, requestText));
|
|
1827
|
+
}
|
|
1828
|
+
function terminalAcceptanceEvidenceExpectation(conversation, requestText) {
|
|
1829
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
1830
|
+
? conversation.native_session_takeover
|
|
1831
|
+
: undefined;
|
|
1832
|
+
const executor = executorForConversation(conversation).kind;
|
|
1833
|
+
const nativeThreadId = stringValue(conversation.native_thread_id) ??
|
|
1834
|
+
stringValue(nativeTakeover?.terminal_agent_session_id) ??
|
|
1835
|
+
stringValue(nativeTakeover?.terminal_agent_expected_session_id);
|
|
1836
|
+
if (!nativeThreadId) {
|
|
1837
|
+
throw new Error("native acceptance evidence cannot be bound without an exact native thread");
|
|
1838
|
+
}
|
|
1839
|
+
const requestHash = terminalBridgeRequestFingerprint(requestText);
|
|
1840
|
+
if (!requestHash) {
|
|
1841
|
+
throw new Error("native acceptance evidence has no exact request hash");
|
|
1842
|
+
}
|
|
1843
|
+
return {
|
|
1844
|
+
source: executor === "codex" ? "codex_rollout" : "claude_transcript",
|
|
1845
|
+
nativeThreadId,
|
|
1846
|
+
requestHash
|
|
1404
1847
|
};
|
|
1405
1848
|
}
|
|
1849
|
+
function replayExactActiveTerminalSubmission({ options, terminalControl, requestText, expectedStoreDir, expectedSessionId, expectedTurnId, expectedMessageType = "task", expectedStatePath }) {
|
|
1850
|
+
const messageId = stringValue(options.messageId);
|
|
1851
|
+
if (!messageId) {
|
|
1852
|
+
return false;
|
|
1853
|
+
}
|
|
1854
|
+
const terminalPayload = terminalSubmissionPayload(requestText);
|
|
1855
|
+
const requestHash = terminalBridgeRequestFingerprint(terminalPayload);
|
|
1856
|
+
if (!requestHash) {
|
|
1857
|
+
return false;
|
|
1858
|
+
}
|
|
1859
|
+
const loadedLedger = loadTerminalBridgeDispatchLedger(terminalControl);
|
|
1860
|
+
const ledgerReceiptMatches = terminalLedgerReceiptHistory(loadedLedger)
|
|
1861
|
+
.filter((receipt) => stringValue(receipt.message_id) === messageId);
|
|
1862
|
+
if (ledgerReceiptMatches.length > 1) {
|
|
1863
|
+
throw new Error(`terminal idempotency key ${messageId} has multiple terminal-ledger receipts`);
|
|
1864
|
+
}
|
|
1865
|
+
const ledgerReceipt = ledgerReceiptMatches[0];
|
|
1866
|
+
const loadedLedgerControl = isRecord(loadedLedger?.terminal_control)
|
|
1867
|
+
? loadedLedger.terminal_control
|
|
1868
|
+
: undefined;
|
|
1869
|
+
const loadedLedgerMessageId = stringValue(loadedLedger?.message_id);
|
|
1870
|
+
const loadedLedgerStoreDir = stringValue(loadedLedger?.store_dir);
|
|
1871
|
+
const loadedLedgerStatePath = stringValue(loadedLedger?.state_path);
|
|
1872
|
+
const loadedLedgerOwner = loadedLedger && loadedLedgerMessageId === messageId
|
|
1873
|
+
? loadTerminalDispatchLedgerOwner(loadedLedger)
|
|
1874
|
+
: undefined;
|
|
1875
|
+
const ledgerReceiptControl = isRecord(ledgerReceipt?.terminal_control)
|
|
1876
|
+
? ledgerReceipt.terminal_control
|
|
1877
|
+
: undefined;
|
|
1878
|
+
const expectedMessageBodyHash = createHash("sha256")
|
|
1879
|
+
.update(requestText)
|
|
1880
|
+
.digest("hex");
|
|
1881
|
+
if (ledgerReceipt &&
|
|
1882
|
+
!(ledgerReceipt.status === "aborted" && ledgerReceipt.safe_to_retry === true) &&
|
|
1883
|
+
(stringValue(ledgerReceiptControl?.target) !== terminalControl.target ||
|
|
1884
|
+
(stringValue(ledgerReceiptControl?.socket_path) ?? undefined) !==
|
|
1885
|
+
terminalControl.socketPath ||
|
|
1886
|
+
Number(ledgerReceiptControl?.pane_pid) !== Number(terminalControl.panePid) ||
|
|
1887
|
+
(stringValue(ledgerReceipt.store_dir) !== undefined &&
|
|
1888
|
+
path.resolve(String(ledgerReceipt.store_dir)) !==
|
|
1889
|
+
path.resolve(expectedStoreDir)) ||
|
|
1890
|
+
(expectedSessionId && stringValue(ledgerReceipt.session_id) !== undefined &&
|
|
1891
|
+
stringValue(ledgerReceipt.session_id) !== expectedSessionId) ||
|
|
1892
|
+
(expectedTurnId && stringValue(ledgerReceipt.turn_id) !== undefined &&
|
|
1893
|
+
stringValue(ledgerReceipt.turn_id) !== expectedTurnId) ||
|
|
1894
|
+
(expectedStatePath && stringValue(ledgerReceipt.state_path) !== undefined &&
|
|
1895
|
+
!sameCanonicalStatePath(ledgerReceipt.state_path, expectedStatePath)) ||
|
|
1896
|
+
(stringValue(ledgerReceipt.message_type) !== undefined &&
|
|
1897
|
+
stringValue(ledgerReceipt.message_type) !== expectedMessageType) ||
|
|
1898
|
+
(stringValue(ledgerReceipt.message_body_hash) !== undefined &&
|
|
1899
|
+
stringValue(ledgerReceipt.message_body_hash) !== expectedMessageBodyHash) ||
|
|
1900
|
+
(stringValue(ledgerReceipt.request_hash) !== undefined &&
|
|
1901
|
+
stringValue(ledgerReceipt.request_hash) !== requestHash) ||
|
|
1902
|
+
(stringValue(options.openclawSession) !== undefined &&
|
|
1903
|
+
stringValue(ledgerReceipt.openclaw_session) !== undefined &&
|
|
1904
|
+
stringValue(ledgerReceipt.openclaw_session) !==
|
|
1905
|
+
stringValue(options.openclawSession)))) {
|
|
1906
|
+
throw new Error(`terminal idempotency key ${messageId} does not match its durable ` +
|
|
1907
|
+
"terminal receipt; no terminal input was sent");
|
|
1908
|
+
}
|
|
1909
|
+
if (loadedLedger &&
|
|
1910
|
+
loadedLedgerMessageId === messageId &&
|
|
1911
|
+
(stringValue(loadedLedgerControl?.target) !== terminalControl.target ||
|
|
1912
|
+
(stringValue(loadedLedgerControl?.socket_path) ?? undefined) !==
|
|
1913
|
+
terminalControl.socketPath ||
|
|
1914
|
+
Number(loadedLedgerControl?.pane_pid) !== Number(terminalControl.panePid) ||
|
|
1915
|
+
(loadedLedgerStoreDir !== undefined &&
|
|
1916
|
+
path.resolve(loadedLedgerStoreDir) !== path.resolve(expectedStoreDir)) ||
|
|
1917
|
+
(expectedSessionId && stringValue(loadedLedger.session_id) !== undefined &&
|
|
1918
|
+
stringValue(loadedLedger.session_id) !== expectedSessionId) ||
|
|
1919
|
+
(expectedTurnId && stringValue(loadedLedger.turn_id) !== undefined &&
|
|
1920
|
+
stringValue(loadedLedger.turn_id) !== expectedTurnId) ||
|
|
1921
|
+
(expectedStatePath && loadedLedgerStatePath !== undefined &&
|
|
1922
|
+
!sameCanonicalStatePath(loadedLedgerStatePath, expectedStatePath)) ||
|
|
1923
|
+
(stringValue(loadedLedger.message_type) !== undefined &&
|
|
1924
|
+
stringValue(loadedLedger.message_type) !== expectedMessageType) ||
|
|
1925
|
+
(stringValue(loadedLedger.request_hash) !== undefined &&
|
|
1926
|
+
stringValue(loadedLedger.request_hash) !== requestHash) ||
|
|
1927
|
+
(stringValue(options.openclawSession) !== undefined &&
|
|
1928
|
+
loadedLedgerOwner !== undefined &&
|
|
1929
|
+
loadedLedgerOwner.openclaw_session !==
|
|
1930
|
+
stringValue(options.openclawSession)))) {
|
|
1931
|
+
throw new Error(`terminal idempotency key ${messageId} does not match its original ` +
|
|
1932
|
+
"Store, Session, Turn, OpenClaw session, message, or tmux pane binding; " +
|
|
1933
|
+
"no terminal input was sent");
|
|
1934
|
+
}
|
|
1935
|
+
if (replayExactStoredTerminalSubmission({
|
|
1936
|
+
options,
|
|
1937
|
+
terminalControl,
|
|
1938
|
+
requestText,
|
|
1939
|
+
requestHash,
|
|
1940
|
+
messageId,
|
|
1941
|
+
expectedStoreDir,
|
|
1942
|
+
expectedSessionId,
|
|
1943
|
+
expectedTurnId,
|
|
1944
|
+
expectedMessageType,
|
|
1945
|
+
expectedStatePath
|
|
1946
|
+
})) {
|
|
1947
|
+
return true;
|
|
1948
|
+
}
|
|
1949
|
+
if (ledgerReceipt &&
|
|
1950
|
+
(loadedLedgerMessageId !== messageId ||
|
|
1951
|
+
!["submitted", "enter_dispatched", "agent_accepted"].includes(String(ledgerReceipt.status)) ||
|
|
1952
|
+
!["submitted", "enter_dispatched", "agent_accepted"].includes(String(loadedLedger?.status)))) {
|
|
1953
|
+
throw new Error(`terminal idempotency key ${messageId} already has durable ` +
|
|
1954
|
+
`${String(ledgerReceipt.status)} proof and cannot be dispatched again; ` +
|
|
1955
|
+
"no terminal input was sent");
|
|
1956
|
+
}
|
|
1957
|
+
const incarnationLedger = resolveTerminalDispatchLedgerPaneIncarnation(terminalControl, loadedLedger);
|
|
1958
|
+
const ledger = reconcilePreparedTerminalDispatchLedger(terminalControl, incarnationLedger);
|
|
1959
|
+
const ledgerControl = isRecord(ledger?.terminal_control)
|
|
1960
|
+
? ledger.terminal_control
|
|
1961
|
+
: undefined;
|
|
1962
|
+
if (!ledger ||
|
|
1963
|
+
!["submitted", "enter_dispatched", "agent_accepted"].includes(String(ledger.status)) ||
|
|
1964
|
+
stringValue(ledgerControl?.target) !== terminalControl.target ||
|
|
1965
|
+
(stringValue(ledgerControl?.socket_path) ?? undefined) !==
|
|
1966
|
+
terminalControl.socketPath ||
|
|
1967
|
+
Number(ledgerControl?.pane_pid) !== Number(terminalControl.panePid) ||
|
|
1968
|
+
stringValue(ledger.message_id) !== messageId ||
|
|
1969
|
+
(stringValue(ledger.message_type) !== undefined &&
|
|
1970
|
+
stringValue(ledger.message_type) !== expectedMessageType) ||
|
|
1971
|
+
stringValue(ledger.request_hash) !== requestHash) {
|
|
1972
|
+
return false;
|
|
1973
|
+
}
|
|
1974
|
+
const owner = loadTerminalDispatchLedgerOwner(ledger);
|
|
1975
|
+
const ledgerStoreDir = stringValue(ledger.store_dir);
|
|
1976
|
+
const ownerStoreDir = owner
|
|
1977
|
+
? managedSessionStoreDirForConversation(owner)
|
|
1978
|
+
: undefined;
|
|
1979
|
+
if (!owner ||
|
|
1980
|
+
TERMINAL_DISPATCH_RELEASE_STATUSES.has(owner.status) ||
|
|
1981
|
+
!ledgerStoreDir ||
|
|
1982
|
+
!ownerStoreDir ||
|
|
1983
|
+
path.resolve(ledgerStoreDir) !== path.resolve(expectedStoreDir) ||
|
|
1984
|
+
path.resolve(ownerStoreDir) !== path.resolve(expectedStoreDir) ||
|
|
1985
|
+
(expectedSessionId && sessionIdForConversation(owner) !== expectedSessionId) ||
|
|
1986
|
+
(expectedTurnId && turnIdForConversation(owner) !== expectedTurnId) ||
|
|
1987
|
+
(expectedStatePath && !sameCanonicalStatePath(ledger.state_path, expectedStatePath)) ||
|
|
1988
|
+
(stringValue(options.openclawSession) &&
|
|
1989
|
+
owner.openclaw_session !== stringValue(options.openclawSession))) {
|
|
1990
|
+
return false;
|
|
1991
|
+
}
|
|
1992
|
+
const submission = terminalBridgeSubmission(owner);
|
|
1993
|
+
const nativeTakeover = isRecord(owner.native_session_takeover)
|
|
1994
|
+
? owner.native_session_takeover
|
|
1995
|
+
: undefined;
|
|
1996
|
+
if (stringValue(nativeTakeover?.terminal_bridge_message_id) !== messageId ||
|
|
1997
|
+
stringValue(submission?.message_id) !== messageId ||
|
|
1998
|
+
(stringValue(submission?.message_type) !== undefined &&
|
|
1999
|
+
stringValue(submission?.message_type) !== expectedMessageType) ||
|
|
2000
|
+
stringValue(submission?.request_hash) !== requestHash) {
|
|
2001
|
+
return false;
|
|
2002
|
+
}
|
|
2003
|
+
const executor = executorForConversation(owner);
|
|
2004
|
+
const nativeThreadId = stringValue(owner.native_thread_id) ??
|
|
2005
|
+
stringValue(nativeTakeover?.terminal_agent_session_id) ??
|
|
2006
|
+
stringValue(nativeTakeover?.terminal_agent_expected_session_id) ??
|
|
2007
|
+
sessionIdForConversation(owner);
|
|
2008
|
+
const proofLevel = String(ledger.status);
|
|
2009
|
+
const replayReceipt = terminalSubmissionReplayReceipt({
|
|
2010
|
+
proofLevel,
|
|
2011
|
+
evidence: ledger.acceptance_evidence,
|
|
2012
|
+
expected: {
|
|
2013
|
+
source: executor.kind === "codex"
|
|
2014
|
+
? "codex_rollout"
|
|
2015
|
+
: "claude_transcript",
|
|
2016
|
+
nativeThreadId,
|
|
2017
|
+
requestHash
|
|
2018
|
+
}
|
|
2019
|
+
});
|
|
2020
|
+
const logPath = stringValue(ledger.event_log_path) ??
|
|
2021
|
+
stringValue(owner.event_log_path);
|
|
2022
|
+
let loggedMessage;
|
|
2023
|
+
if (logPath) {
|
|
2024
|
+
try {
|
|
2025
|
+
loggedMessage = readNdjsonLog(logPath).find((event) => isRecord(event.message) && event.message.id === messageId)?.message;
|
|
2026
|
+
}
|
|
2027
|
+
catch {
|
|
2028
|
+
loggedMessage = undefined;
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
if (isRecord(loggedMessage) &&
|
|
2032
|
+
(loggedMessage.type !== expectedMessageType ||
|
|
2033
|
+
loggedMessage.body !== requestText ||
|
|
2034
|
+
loggedMessage.conversation_id !== owner.conversation_id ||
|
|
2035
|
+
loggedMessage.session_id !== sessionIdForConversation(owner) ||
|
|
2036
|
+
loggedMessage.turn_id !== turnIdForConversation(owner))) {
|
|
2037
|
+
return false;
|
|
2038
|
+
}
|
|
2039
|
+
const durableMessageType = stringValue(ledger.message_type) ??
|
|
2040
|
+
stringValue(submission?.message_type) ??
|
|
2041
|
+
(isRecord(loggedMessage) ? stringValue(loggedMessage.type) : undefined);
|
|
2042
|
+
if (durableMessageType !== expectedMessageType) {
|
|
2043
|
+
return false;
|
|
2044
|
+
}
|
|
2045
|
+
const replayedMessage = isRecord(loggedMessage)
|
|
2046
|
+
? loggedMessage
|
|
2047
|
+
: createMessage({
|
|
2048
|
+
conversation: owner,
|
|
2049
|
+
id: messageId,
|
|
2050
|
+
from: "openclaw",
|
|
2051
|
+
to: executor.actor,
|
|
2052
|
+
type: expectedMessageType,
|
|
2053
|
+
body: requestText,
|
|
2054
|
+
metadata: {
|
|
2055
|
+
executor_kind: executor.kind,
|
|
2056
|
+
executor_session: executor.session
|
|
2057
|
+
}
|
|
2058
|
+
});
|
|
2059
|
+
const acceptanceInvalid = replayReceipt.submission_outcome === "uncertain";
|
|
2060
|
+
printJson({
|
|
2061
|
+
session_id: sessionIdForConversation(owner),
|
|
2062
|
+
turn_id: turnIdForConversation(owner),
|
|
2063
|
+
conversation: owner,
|
|
2064
|
+
message: replayedMessage,
|
|
2065
|
+
delivered: replayReceipt.delivered,
|
|
2066
|
+
status: replayReceipt.status,
|
|
2067
|
+
submission_outcome: replayReceipt.submission_outcome,
|
|
2068
|
+
background: true,
|
|
2069
|
+
callback_expected: !acceptanceInvalid && Boolean(owner.gateway_method ?? ledger.callback_expected),
|
|
2070
|
+
terminal_control: terminalControl,
|
|
2071
|
+
executor,
|
|
2072
|
+
replayed: replayReceipt.replayed,
|
|
2073
|
+
delivery_receipt: replayReceipt.delivery_receipt,
|
|
2074
|
+
...(replayReceipt.do_not_retry
|
|
2075
|
+
? { do_not_retry: replayReceipt.do_not_retry }
|
|
2076
|
+
: {}),
|
|
2077
|
+
reason: replayReceipt.delivered
|
|
2078
|
+
? "AKK replayed the durable native acceptance receipt and sent no additional tmux input."
|
|
2079
|
+
: acceptanceInvalid
|
|
2080
|
+
? "AKK replayed an invalid native acceptance receipt as uncertain and sent no additional tmux input."
|
|
2081
|
+
: "AKK replayed the original transport proof without upgrading it and sent no additional tmux input.",
|
|
2082
|
+
openclaw_next_action: replayReceipt.delivered
|
|
2083
|
+
? openClawYieldNextAction({
|
|
2084
|
+
conversationId: owner.conversation_id,
|
|
2085
|
+
sessionId: sessionIdForConversation(owner),
|
|
2086
|
+
turnId: turnIdForConversation(owner),
|
|
2087
|
+
source: "terminal_control",
|
|
2088
|
+
callbackExpected: Boolean(owner.gateway_method ?? ledger.callback_expected)
|
|
2089
|
+
})
|
|
2090
|
+
: {
|
|
2091
|
+
action: "inspect",
|
|
2092
|
+
conversation_id: owner.conversation_id,
|
|
2093
|
+
session_id: sessionIdForConversation(owner),
|
|
2094
|
+
turn_id: turnIdForConversation(owner),
|
|
2095
|
+
do_not_retry: true,
|
|
2096
|
+
reason: acceptanceInvalid
|
|
2097
|
+
? "Stored native acceptance evidence is invalid."
|
|
2098
|
+
: "Only terminal transport is proven."
|
|
2099
|
+
}
|
|
2100
|
+
});
|
|
2101
|
+
return true;
|
|
2102
|
+
}
|
|
2103
|
+
function validateStoredTerminalSubmissionMatch({ owner, receipt, options, terminalControl, requestText, requestHash, expectedStoreDir, expectedSessionId, expectedTurnId, expectedMessageType, expectedStatePath }) {
|
|
2104
|
+
const messageId = required(stringValue(receipt.message_id), "stored terminal receipt message id is required");
|
|
2105
|
+
const ownerStoreDir = managedSessionStoreDirForConversation(owner);
|
|
2106
|
+
const nativeTakeover = isRecord(owner.native_session_takeover)
|
|
2107
|
+
? owner.native_session_takeover
|
|
2108
|
+
: undefined;
|
|
2109
|
+
const storedControl = terminalControlFromTakeover(nativeTakeover);
|
|
2110
|
+
const receiptStoreDir = stringValue(receipt.store_dir) ?? ownerStoreDir;
|
|
2111
|
+
const receiptSessionId = stringValue(receipt.session_id) ??
|
|
2112
|
+
sessionIdForConversation(owner);
|
|
2113
|
+
const receiptTurnId = stringValue(receipt.turn_id) ??
|
|
2114
|
+
turnIdForConversation(owner);
|
|
2115
|
+
const receiptOpenClawSession = stringValue(receipt.openclaw_session) ??
|
|
2116
|
+
owner.openclaw_session;
|
|
2117
|
+
const receiptTarget = stringValue(receipt.terminal_target) ??
|
|
2118
|
+
storedControl?.target;
|
|
2119
|
+
const receiptSocketPath = receipt.terminal_socket_path === null
|
|
2120
|
+
? undefined
|
|
2121
|
+
: stringValue(receipt.terminal_socket_path) ?? storedControl?.socketPath;
|
|
2122
|
+
const receiptPanePid = Number(receipt.terminal_pane_pid ?? storedControl?.panePid);
|
|
2123
|
+
const requestedOpenClawSession = stringValue(options.openclawSession);
|
|
2124
|
+
if (!ownerStoreDir ||
|
|
2125
|
+
!receiptStoreDir ||
|
|
2126
|
+
path.resolve(ownerStoreDir) !== path.resolve(expectedStoreDir) ||
|
|
2127
|
+
path.resolve(receiptStoreDir) !== path.resolve(expectedStoreDir) ||
|
|
2128
|
+
!storedControl ||
|
|
2129
|
+
receiptTarget !== terminalControl.target ||
|
|
2130
|
+
receiptSocketPath !== terminalControl.socketPath ||
|
|
2131
|
+
receiptPanePid !== Number(terminalControl.panePid) ||
|
|
2132
|
+
receiptSessionId !== sessionIdForConversation(owner) ||
|
|
2133
|
+
receiptTurnId !== turnIdForConversation(owner) ||
|
|
2134
|
+
(expectedSessionId && receiptSessionId !== expectedSessionId) ||
|
|
2135
|
+
(expectedTurnId && receiptTurnId !== expectedTurnId) ||
|
|
2136
|
+
(expectedStatePath && !sameCanonicalStatePath(owner.state_path, expectedStatePath)) ||
|
|
2137
|
+
(requestedOpenClawSession && receiptOpenClawSession !== requestedOpenClawSession) ||
|
|
2138
|
+
stringValue(receipt.request_hash) !== requestHash ||
|
|
2139
|
+
(stringValue(receipt.executor_kind) !== undefined &&
|
|
2140
|
+
stringValue(receipt.executor_kind) !== executorForConversation(owner).kind)) {
|
|
2141
|
+
throw new Error(`terminal idempotency key ${messageId} does not match its original ` +
|
|
2142
|
+
"Store, Session, Turn, OpenClaw session, or tmux pane binding; no terminal input was sent");
|
|
2143
|
+
}
|
|
2144
|
+
const logPath = stringValue(owner.event_log_path);
|
|
2145
|
+
let loggedMessages = [];
|
|
2146
|
+
if (logPath) {
|
|
2147
|
+
try {
|
|
2148
|
+
loggedMessages = readNdjsonLog(logPath)
|
|
2149
|
+
.filter((event) => isRecord(event.message) && event.message.id === messageId)
|
|
2150
|
+
.map((event) => event.message);
|
|
2151
|
+
}
|
|
2152
|
+
catch {
|
|
2153
|
+
loggedMessages = [];
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
if (loggedMessages.length > 1) {
|
|
2157
|
+
throw new Error(`terminal idempotency key ${messageId} has duplicate durable messages`);
|
|
2158
|
+
}
|
|
2159
|
+
const loggedMessage = loggedMessages[0];
|
|
2160
|
+
if (isRecord(loggedMessage) &&
|
|
2161
|
+
(loggedMessage.type !== expectedMessageType ||
|
|
2162
|
+
loggedMessage.body !== requestText ||
|
|
2163
|
+
loggedMessage.conversation_id !== owner.conversation_id ||
|
|
2164
|
+
loggedMessage.session_id !== sessionIdForConversation(owner) ||
|
|
2165
|
+
loggedMessage.turn_id !== turnIdForConversation(owner))) {
|
|
2166
|
+
throw new Error(`terminal idempotency key ${messageId} does not match its durable message; no terminal input was sent`);
|
|
2167
|
+
}
|
|
2168
|
+
const durableMessageType = stringValue(receipt.message_type) ??
|
|
2169
|
+
(isRecord(loggedMessage) ? stringValue(loggedMessage.type) : undefined);
|
|
2170
|
+
const durableMessageBodyHash = stringValue(receipt.message_body_hash) ??
|
|
2171
|
+
(isRecord(loggedMessage) && typeof loggedMessage.body === "string"
|
|
2172
|
+
? createHash("sha256").update(loggedMessage.body).digest("hex")
|
|
2173
|
+
: undefined);
|
|
2174
|
+
const expectedMessageBodyHash = createHash("sha256")
|
|
2175
|
+
.update(requestText)
|
|
2176
|
+
.digest("hex");
|
|
2177
|
+
if (durableMessageType !== expectedMessageType ||
|
|
2178
|
+
durableMessageBodyHash !== expectedMessageBodyHash) {
|
|
2179
|
+
throw new Error(`terminal idempotency key ${messageId} was already used for a different message; no terminal input was sent`);
|
|
2180
|
+
}
|
|
2181
|
+
return loggedMessage;
|
|
2182
|
+
}
|
|
2183
|
+
function replayExactStoredTerminalSubmission({ options, terminalControl, requestText, requestHash, messageId, expectedStoreDir, expectedSessionId, expectedTurnId, expectedMessageType, expectedStatePath }) {
|
|
2184
|
+
const allMatches = listConversations(expectedStoreDir).flatMap((owner) => terminalBridgeSubmissionReceipts(owner)
|
|
2185
|
+
.filter((receipt) => stringValue(receipt.message_id) === messageId)
|
|
2186
|
+
.map((receipt) => ({ owner, receipt })));
|
|
2187
|
+
const validatedMatches = allMatches.map((match) => ({
|
|
2188
|
+
...match,
|
|
2189
|
+
loggedMessage: validateStoredTerminalSubmissionMatch({
|
|
2190
|
+
...match,
|
|
2191
|
+
options,
|
|
2192
|
+
terminalControl,
|
|
2193
|
+
requestText,
|
|
2194
|
+
requestHash,
|
|
2195
|
+
expectedStoreDir,
|
|
2196
|
+
expectedSessionId,
|
|
2197
|
+
expectedTurnId,
|
|
2198
|
+
expectedMessageType,
|
|
2199
|
+
expectedStatePath
|
|
2200
|
+
})
|
|
2201
|
+
}));
|
|
2202
|
+
const matches = validatedMatches.filter(({ receipt }) => !(receipt.status === "aborted" && receipt.safe_to_retry === true));
|
|
2203
|
+
if (matches.length > 1) {
|
|
2204
|
+
throw new Error(`terminal idempotency key ${messageId} has multiple durable receipts in one Store`);
|
|
2205
|
+
}
|
|
2206
|
+
const match = matches[0];
|
|
2207
|
+
if (!match) {
|
|
2208
|
+
for (const { owner, receipt } of validatedMatches) {
|
|
2209
|
+
assertSafeAbortedTerminalRetryBinding({
|
|
2210
|
+
owner,
|
|
2211
|
+
receipt,
|
|
2212
|
+
storeDir: expectedStoreDir,
|
|
2213
|
+
terminalControl,
|
|
2214
|
+
messageId
|
|
2215
|
+
});
|
|
2216
|
+
}
|
|
2217
|
+
// A prepared-stage failure with a restored ledger proves that tmux was
|
|
2218
|
+
// untouched. The same exact id may therefore start a fresh attempt; all
|
|
2219
|
+
// immutable boundaries above were still validated before allowing it.
|
|
2220
|
+
return false;
|
|
2221
|
+
}
|
|
2222
|
+
const { owner, receipt, loggedMessage } = match;
|
|
2223
|
+
const nativeTakeover = isRecord(owner.native_session_takeover)
|
|
2224
|
+
? owner.native_session_takeover
|
|
2225
|
+
: undefined;
|
|
2226
|
+
const currentSubmission = terminalBridgeSubmission(owner);
|
|
2227
|
+
const isCurrentSubmission = stringValue(currentSubmission?.message_id) === messageId;
|
|
2228
|
+
if (isCurrentSubmission &&
|
|
2229
|
+
!TERMINAL_DISPATCH_RELEASE_STATUSES.has(owner.status)) {
|
|
2230
|
+
return false;
|
|
2231
|
+
}
|
|
2232
|
+
if (!["submitted", "enter_dispatched", "agent_accepted"].includes(String(receipt.status))) {
|
|
2233
|
+
throw new Error(`terminal idempotency key ${messageId} has durable ${String(receipt.status)} ` +
|
|
2234
|
+
"proof and must not be retried; no terminal input was sent");
|
|
2235
|
+
}
|
|
2236
|
+
const executor = executorForConversation(owner);
|
|
2237
|
+
const nativeThreadId = stringValue(receipt.native_thread_id) ??
|
|
2238
|
+
stringValue(owner.native_thread_id) ??
|
|
2239
|
+
stringValue(nativeTakeover?.terminal_agent_session_id) ??
|
|
2240
|
+
stringValue(nativeTakeover?.terminal_agent_expected_session_id) ??
|
|
2241
|
+
sessionIdForConversation(owner);
|
|
2242
|
+
const proofLevel = String(receipt.status);
|
|
2243
|
+
const replayReceipt = terminalSubmissionReplayReceipt({
|
|
2244
|
+
proofLevel,
|
|
2245
|
+
evidence: receipt.acceptance_evidence,
|
|
2246
|
+
expected: {
|
|
2247
|
+
source: executor.kind === "codex"
|
|
2248
|
+
? "codex_rollout"
|
|
2249
|
+
: "claude_transcript",
|
|
2250
|
+
nativeThreadId,
|
|
2251
|
+
requestHash
|
|
2252
|
+
}
|
|
2253
|
+
});
|
|
2254
|
+
const replayedMessage = isRecord(loggedMessage)
|
|
2255
|
+
? loggedMessage
|
|
2256
|
+
: createMessage({
|
|
2257
|
+
conversation: owner,
|
|
2258
|
+
id: messageId,
|
|
2259
|
+
from: "openclaw",
|
|
2260
|
+
to: executor.actor,
|
|
2261
|
+
type: expectedMessageType,
|
|
2262
|
+
body: requestText,
|
|
2263
|
+
metadata: {
|
|
2264
|
+
executor_kind: executor.kind,
|
|
2265
|
+
executor_session: executor.session
|
|
2266
|
+
}
|
|
2267
|
+
});
|
|
2268
|
+
const acceptanceInvalid = replayReceipt.submission_outcome === "uncertain";
|
|
2269
|
+
const callbackExpected = Boolean(owner.gateway_method);
|
|
2270
|
+
printJson({
|
|
2271
|
+
session_id: sessionIdForConversation(owner),
|
|
2272
|
+
turn_id: turnIdForConversation(owner),
|
|
2273
|
+
conversation: owner,
|
|
2274
|
+
message: replayedMessage,
|
|
2275
|
+
delivered: replayReceipt.delivered,
|
|
2276
|
+
status: replayReceipt.status,
|
|
2277
|
+
submission_outcome: replayReceipt.submission_outcome,
|
|
2278
|
+
background: true,
|
|
2279
|
+
callback_expected: !acceptanceInvalid && callbackExpected,
|
|
2280
|
+
terminal_control: terminalControl,
|
|
2281
|
+
executor,
|
|
2282
|
+
replayed: true,
|
|
2283
|
+
delivery_receipt: replayReceipt.delivery_receipt,
|
|
2284
|
+
...(replayReceipt.do_not_retry
|
|
2285
|
+
? { do_not_retry: replayReceipt.do_not_retry }
|
|
2286
|
+
: {}),
|
|
2287
|
+
reason: replayReceipt.delivered
|
|
2288
|
+
? "AKK replayed the stored durable native acceptance receipt and sent no additional tmux input."
|
|
2289
|
+
: acceptanceInvalid
|
|
2290
|
+
? "AKK replayed an invalid stored native acceptance receipt as uncertain and sent no additional tmux input."
|
|
2291
|
+
: "AKK replayed the stored transport proof without upgrading it and sent no additional tmux input.",
|
|
2292
|
+
openclaw_next_action: replayReceipt.delivered
|
|
2293
|
+
? openClawYieldNextAction({
|
|
2294
|
+
conversationId: owner.conversation_id,
|
|
2295
|
+
sessionId: sessionIdForConversation(owner),
|
|
2296
|
+
turnId: turnIdForConversation(owner),
|
|
2297
|
+
source: "terminal_control",
|
|
2298
|
+
callbackExpected
|
|
2299
|
+
})
|
|
2300
|
+
: {
|
|
2301
|
+
action: "inspect",
|
|
2302
|
+
conversation_id: owner.conversation_id,
|
|
2303
|
+
session_id: sessionIdForConversation(owner),
|
|
2304
|
+
turn_id: turnIdForConversation(owner),
|
|
2305
|
+
do_not_retry: true,
|
|
2306
|
+
reason: acceptanceInvalid
|
|
2307
|
+
? "Stored native acceptance evidence is invalid."
|
|
2308
|
+
: "Only terminal transport is proven."
|
|
2309
|
+
}
|
|
2310
|
+
});
|
|
2311
|
+
return true;
|
|
2312
|
+
}
|
|
2313
|
+
function terminalSubmissionLastProvenStage(status, previous) {
|
|
2314
|
+
if (status === "agent_accepted") {
|
|
2315
|
+
return "agent_accepted";
|
|
2316
|
+
}
|
|
2317
|
+
if (status === "enter_dispatched" ||
|
|
2318
|
+
status === "submitted" ||
|
|
2319
|
+
status === "not_accepted") {
|
|
2320
|
+
return "enter_dispatched";
|
|
2321
|
+
}
|
|
2322
|
+
if (status === "uncertain" &&
|
|
2323
|
+
["prepared", "text_injected", "enter_dispatched", "agent_accepted"].includes(previous ?? "")) {
|
|
2324
|
+
return previous;
|
|
2325
|
+
}
|
|
2326
|
+
return status === "text_injected" ? "text_injected" : "prepared";
|
|
2327
|
+
}
|
|
1406
2328
|
function terminalBridgeSubmission(conversation) {
|
|
1407
2329
|
const nativeTakeover = isRecord(conversation?.native_session_takeover)
|
|
1408
2330
|
? conversation.native_session_takeover
|
|
@@ -1411,10 +2333,45 @@ function terminalBridgeSubmission(conversation) {
|
|
|
1411
2333
|
? nativeTakeover.terminal_bridge_submission
|
|
1412
2334
|
: undefined;
|
|
1413
2335
|
}
|
|
2336
|
+
function terminalBridgeSubmissionReceipts(conversation) {
|
|
2337
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
2338
|
+
? conversation.native_session_takeover
|
|
2339
|
+
: undefined;
|
|
2340
|
+
const historyValue = nativeTakeover?.terminal_bridge_submission_receipts;
|
|
2341
|
+
if (historyValue !== undefined && !Array.isArray(historyValue)) {
|
|
2342
|
+
throw new Error("terminal submission receipt history is malformed");
|
|
2343
|
+
}
|
|
2344
|
+
const receipts = (Array.isArray(historyValue) ? historyValue : []).map((value) => {
|
|
2345
|
+
if (!isRecord(value) || !stringValue(value.message_id)) {
|
|
2346
|
+
throw new Error("terminal submission receipt history is malformed");
|
|
2347
|
+
}
|
|
2348
|
+
return value;
|
|
2349
|
+
});
|
|
2350
|
+
const ids = new Set();
|
|
2351
|
+
for (const receipt of receipts) {
|
|
2352
|
+
const id = String(receipt.message_id);
|
|
2353
|
+
if (ids.has(id)) {
|
|
2354
|
+
throw new Error(`terminal submission receipt ${id} is duplicated`);
|
|
2355
|
+
}
|
|
2356
|
+
ids.add(id);
|
|
2357
|
+
}
|
|
2358
|
+
const current = terminalBridgeSubmission(conversation);
|
|
2359
|
+
const currentId = stringValue(current?.message_id);
|
|
2360
|
+
if (current && currentId && ids.has(currentId)) {
|
|
2361
|
+
const historical = receipts.find((receipt) => stringValue(receipt.message_id) === currentId);
|
|
2362
|
+
if (!historical || canonicalJson(historical) !== canonicalJson(current)) {
|
|
2363
|
+
throw new Error(`terminal submission receipt ${currentId} conflicts with its current generation`);
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
return current && currentId && !ids.has(currentId)
|
|
2367
|
+
? [...receipts, current]
|
|
2368
|
+
: receipts;
|
|
2369
|
+
}
|
|
1414
2370
|
function unresolvedTerminalBridgeSubmission(conversation) {
|
|
1415
2371
|
const submission = terminalBridgeSubmission(conversation);
|
|
1416
2372
|
return submission &&
|
|
1417
|
-
|
|
2373
|
+
["prepared", "text_injected", "enter_dispatched", "not_accepted", "uncertain"]
|
|
2374
|
+
.includes(String(submission.status))
|
|
1418
2375
|
? submission
|
|
1419
2376
|
: undefined;
|
|
1420
2377
|
}
|
|
@@ -1429,10 +2386,18 @@ function assertNoUnresolvedTerminalBridgeSubmission(storeDir, terminalControl, c
|
|
|
1429
2386
|
if (candidate.conversation_id === currentConversationId ||
|
|
1430
2387
|
!isActiveStatus(candidate.status) ||
|
|
1431
2388
|
!submission ||
|
|
1432
|
-
![
|
|
2389
|
+
![
|
|
2390
|
+
"prepared",
|
|
2391
|
+
"text_injected",
|
|
2392
|
+
"enter_dispatched",
|
|
2393
|
+
"agent_accepted",
|
|
2394
|
+
"submitted",
|
|
2395
|
+
"not_accepted",
|
|
2396
|
+
"uncertain"
|
|
2397
|
+
].includes(String(submission.status))) {
|
|
1433
2398
|
continue;
|
|
1434
2399
|
}
|
|
1435
|
-
if (submission.status
|
|
2400
|
+
if (["submitted", "agent_accepted"].includes(String(submission.status)) &&
|
|
1436
2401
|
stringValue(submission.request_hash) !== requestHash) {
|
|
1437
2402
|
continue;
|
|
1438
2403
|
}
|
|
@@ -2291,8 +3256,12 @@ function terminalDispatchOwnership(terminalControl) {
|
|
|
2291
3256
|
}
|
|
2292
3257
|
if (![
|
|
2293
3258
|
"prepared",
|
|
3259
|
+
"text_injected",
|
|
3260
|
+
"enter_dispatched",
|
|
3261
|
+
"agent_accepted",
|
|
2294
3262
|
"dispatching",
|
|
2295
3263
|
"submitted",
|
|
3264
|
+
"not_accepted",
|
|
2296
3265
|
"uncertain"
|
|
2297
3266
|
].includes(String(ledger.status))) {
|
|
2298
3267
|
return { state: "none" };
|
|
@@ -2320,7 +3289,16 @@ function terminalDispatchOwnership(terminalControl) {
|
|
|
2320
3289
|
: undefined;
|
|
2321
3290
|
const ledgerMessageId = stringValue(ledger.message_id);
|
|
2322
3291
|
const ownerMessageId = stringValue(ownerTakeover?.terminal_bridge_message_id);
|
|
2323
|
-
if ([
|
|
3292
|
+
if ([
|
|
3293
|
+
"prepared",
|
|
3294
|
+
"text_injected",
|
|
3295
|
+
"enter_dispatched",
|
|
3296
|
+
"agent_accepted",
|
|
3297
|
+
"dispatching",
|
|
3298
|
+
"submitted",
|
|
3299
|
+
"not_accepted",
|
|
3300
|
+
"uncertain"
|
|
3301
|
+
].includes(String(ledger.status)) &&
|
|
2324
3302
|
ledgerMessageId &&
|
|
2325
3303
|
ownerMessageId !== ledgerMessageId) {
|
|
2326
3304
|
return {
|
|
@@ -3985,11 +4963,18 @@ function assertTerminalLifecycleReady({ options, terminal, terminalStatus }) {
|
|
|
3985
4963
|
`transition ${stringValue(ledger?.transition_id) ?? "with invalid identity"}`);
|
|
3986
4964
|
}
|
|
3987
4965
|
if (ledger &&
|
|
3988
|
-
[
|
|
4966
|
+
[
|
|
4967
|
+
"prepared",
|
|
4968
|
+
"text_injected",
|
|
4969
|
+
"enter_dispatched",
|
|
4970
|
+
"dispatching",
|
|
4971
|
+
"not_accepted",
|
|
4972
|
+
"uncertain"
|
|
4973
|
+
].includes(String(ledger.status))) {
|
|
3989
4974
|
throw new Error(`terminal ${terminal.terminalControl.target} has an unresolved ` +
|
|
3990
4975
|
`${String(ledger.kind ?? "turn")} operation (${String(ledger.status)})`);
|
|
3991
4976
|
}
|
|
3992
|
-
if (ledger
|
|
4977
|
+
if (ledger && ["submitted", "agent_accepted"].includes(String(ledger.status))) {
|
|
3993
4978
|
const owner = loadTerminalDispatchLedgerOwner(ledger);
|
|
3994
4979
|
if (!owner || !TERMINAL_DISPATCH_RELEASE_STATUSES.has(owner.status)) {
|
|
3995
4980
|
throw new Error(`terminal ${terminal.terminalControl.target} still has a submitted operation`);
|
|
@@ -6000,6 +6985,7 @@ function prepareManagedSend({ options, statePath, logPath, messageBody, stateLoc
|
|
|
6000
6985
|
}
|
|
6001
6986
|
const message = createMessage({
|
|
6002
6987
|
conversation,
|
|
6988
|
+
id: stringValue(options.messageId),
|
|
6003
6989
|
from: "openclaw",
|
|
6004
6990
|
to: executor.actor,
|
|
6005
6991
|
type,
|
|
@@ -6060,7 +7046,6 @@ async function runSend(options) {
|
|
|
6060
7046
|
}
|
|
6061
7047
|
const rawStoreDir = storeDirFromOptions(options);
|
|
6062
7048
|
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(rawStoreDir, terminalConversation.terminalControl), { timeoutMs: 30000 });
|
|
6063
|
-
let releaseStateLock;
|
|
6064
7049
|
try {
|
|
6065
7050
|
await withStoreWriterLeaseAsync(rawStoreDir, async () => {
|
|
6066
7051
|
await recoverLifecycleFenceBeforeMutation({
|
|
@@ -6071,6 +7056,15 @@ async function runSend(options) {
|
|
|
6071
7056
|
// protocol migration, rather than mutable Turn recency, is the only code
|
|
6072
7057
|
// allowed to materialize Session records from existing Turns.
|
|
6073
7058
|
ensureStoreWritable(rawStoreDir);
|
|
7059
|
+
if (replayExactActiveTerminalSubmission({
|
|
7060
|
+
options,
|
|
7061
|
+
terminalControl: terminalConversation.terminalControl,
|
|
7062
|
+
requestText: String(messageBody),
|
|
7063
|
+
expectedStoreDir: rawStoreDir,
|
|
7064
|
+
expectedMessageType: "task"
|
|
7065
|
+
})) {
|
|
7066
|
+
return;
|
|
7067
|
+
}
|
|
6074
7068
|
const claimedSession = soleBoundManagedSessionClaimForTerminal(rawStoreDir, terminalConversation);
|
|
6075
7069
|
const knownCodexCompanions = claimedSession
|
|
6076
7070
|
? codexAllowedCompanionSetForManagedSession({
|
|
@@ -6178,33 +7172,33 @@ async function runSend(options) {
|
|
|
6178
7172
|
});
|
|
6179
7173
|
ensureStoreWritable(managed.conversation.store_dir);
|
|
6180
7174
|
ensureDir(path.dirname(managed.statePath));
|
|
6181
|
-
releaseStateLock = acquireFileLock(`${managed.statePath}.lock`);
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
7175
|
+
const releaseStateLock = acquireFileLock(`${managed.statePath}.lock`);
|
|
7176
|
+
try {
|
|
7177
|
+
await runTerminalControlSend({
|
|
7178
|
+
options,
|
|
7179
|
+
conversation: managed.conversation,
|
|
7180
|
+
nextConversation: managed.nextConversation,
|
|
7181
|
+
statePath: managed.statePath,
|
|
7182
|
+
logPath: managed.logPath,
|
|
7183
|
+
executor: managed.executor,
|
|
7184
|
+
message: managed.message,
|
|
7185
|
+
terminalControl: terminalConversation.terminalControl,
|
|
7186
|
+
terminalSendLockHeld: true,
|
|
7187
|
+
terminalStateLockHeld: true,
|
|
7188
|
+
storeWriterLeaseHeld: true,
|
|
7189
|
+
recordMessageAfterSend: true,
|
|
7190
|
+
recordRawAttachmentAfterSend: reusableTurn === undefined,
|
|
7191
|
+
allowedPreMaterializationIdentity,
|
|
7192
|
+
allowedAdditionalIdentities
|
|
7193
|
+
});
|
|
7194
|
+
}
|
|
7195
|
+
finally {
|
|
7196
|
+
releaseStateLock();
|
|
7197
|
+
}
|
|
6199
7198
|
});
|
|
6200
7199
|
}
|
|
6201
7200
|
finally {
|
|
6202
|
-
|
|
6203
|
-
releaseStateLock?.();
|
|
6204
|
-
}
|
|
6205
|
-
finally {
|
|
6206
|
-
releaseTerminalLock();
|
|
6207
|
-
}
|
|
7201
|
+
releaseTerminalLock();
|
|
6208
7202
|
}
|
|
6209
7203
|
return;
|
|
6210
7204
|
}
|
|
@@ -6243,7 +7237,6 @@ async function runSend(options) {
|
|
|
6243
7237
|
throw new Error(`session ${sessionId} is not attached to a live tmux terminal`);
|
|
6244
7238
|
}
|
|
6245
7239
|
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDir, resolvedTerminal.terminalControl), { timeoutMs: 30000 });
|
|
6246
|
-
let releaseStateLock;
|
|
6247
7240
|
try {
|
|
6248
7241
|
await withStoreWriterLeaseAsync(storeDir, async () => {
|
|
6249
7242
|
await recoverLifecycleFenceBeforeMutation({
|
|
@@ -6254,6 +7247,16 @@ async function runSend(options) {
|
|
|
6254
7247
|
// atomic migration. Once protocol 3 is active, a missing Session is corrupt
|
|
6255
7248
|
// state and must not be reconstructed from whichever Turn looks newest.
|
|
6256
7249
|
ensureStoreWritable(storeDir);
|
|
7250
|
+
if (replayExactActiveTerminalSubmission({
|
|
7251
|
+
options,
|
|
7252
|
+
terminalControl: resolvedTerminal.terminalControl,
|
|
7253
|
+
requestText: String(messageBody),
|
|
7254
|
+
expectedStoreDir: storeDir,
|
|
7255
|
+
expectedSessionId: sessionId,
|
|
7256
|
+
expectedMessageType: "task"
|
|
7257
|
+
})) {
|
|
7258
|
+
return;
|
|
7259
|
+
}
|
|
6257
7260
|
let currentSession = tryLoadManagedSession(storeDir, sessionId);
|
|
6258
7261
|
const knownCodexCompanions = currentSession
|
|
6259
7262
|
? codexAllowedCompanionSetForManagedSession({
|
|
@@ -6344,32 +7347,32 @@ async function runSend(options) {
|
|
|
6344
7347
|
});
|
|
6345
7348
|
ensureStoreWritable(managed.conversation.store_dir);
|
|
6346
7349
|
ensureDir(path.dirname(managed.statePath));
|
|
6347
|
-
releaseStateLock = acquireFileLock(`${managed.statePath}.lock`);
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
7350
|
+
const releaseStateLock = acquireFileLock(`${managed.statePath}.lock`);
|
|
7351
|
+
try {
|
|
7352
|
+
await runTerminalControlSend({
|
|
7353
|
+
options,
|
|
7354
|
+
conversation: managed.conversation,
|
|
7355
|
+
nextConversation: managed.nextConversation,
|
|
7356
|
+
statePath: managed.statePath,
|
|
7357
|
+
logPath: managed.logPath,
|
|
7358
|
+
executor: managed.executor,
|
|
7359
|
+
message: managed.message,
|
|
7360
|
+
terminalControl: resolvedTerminal.terminalControl,
|
|
7361
|
+
terminalSendLockHeld: true,
|
|
7362
|
+
terminalStateLockHeld: true,
|
|
7363
|
+
storeWriterLeaseHeld: true,
|
|
7364
|
+
recordMessageAfterSend: true,
|
|
7365
|
+
allowedPreMaterializationIdentity,
|
|
7366
|
+
allowedAdditionalIdentities
|
|
7367
|
+
});
|
|
7368
|
+
}
|
|
7369
|
+
finally {
|
|
7370
|
+
releaseStateLock();
|
|
7371
|
+
}
|
|
6364
7372
|
});
|
|
6365
7373
|
}
|
|
6366
7374
|
finally {
|
|
6367
|
-
|
|
6368
|
-
releaseStateLock?.();
|
|
6369
|
-
}
|
|
6370
|
-
finally {
|
|
6371
|
-
releaseTerminalLock();
|
|
6372
|
-
}
|
|
7375
|
+
releaseTerminalLock();
|
|
6373
7376
|
}
|
|
6374
7377
|
}
|
|
6375
7378
|
async function runRespond(options) {
|
|
@@ -6386,101 +7389,151 @@ async function runRespond(options) {
|
|
|
6386
7389
|
async function runTurnResponse({ options, messageBody }) {
|
|
6387
7390
|
const loaded = loadConversationFromOptions(options);
|
|
6388
7391
|
const { statePath, logPath } = loaded;
|
|
6389
|
-
const
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
? conversation.native_session_takeover
|
|
7392
|
+
const initialConversation = loaded.conversation;
|
|
7393
|
+
const requestedOpenClawSession = stringValue(options.openclawSession);
|
|
7394
|
+
if (requestedOpenClawSession &&
|
|
7395
|
+
initialConversation.openclaw_session !== requestedOpenClawSession) {
|
|
7396
|
+
throw new Error(`turn ${turnIdForConversation(initialConversation)} belongs to a ` +
|
|
7397
|
+
"different OpenClaw session; no terminal input was sent");
|
|
7398
|
+
}
|
|
7399
|
+
const nativeTakeover = isRecord(initialConversation.native_session_takeover)
|
|
7400
|
+
? initialConversation.native_session_takeover
|
|
6399
7401
|
: undefined;
|
|
6400
|
-
const
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
7402
|
+
const storedTerminalControl = terminalControlFromTakeover(nativeTakeover);
|
|
7403
|
+
const nativeTerminalId = stringValue(nativeTakeover?.native_session_id);
|
|
7404
|
+
if (!storedTerminalControl || !nativeTerminalId) {
|
|
7405
|
+
throw new Error(`turn ${turnIdForConversation(initialConversation)} is not attached to a live tmux terminal`);
|
|
7406
|
+
}
|
|
7407
|
+
const liveTerminal = await createTerminalAgentBridge(options)
|
|
7408
|
+
.resolveConversationId(nativeTerminalId);
|
|
7409
|
+
if (!liveTerminal ||
|
|
7410
|
+
liveTerminal.agent !== executorForConversation(initialConversation).kind ||
|
|
7411
|
+
liveTerminal.terminalControl.target !== storedTerminalControl.target ||
|
|
7412
|
+
liveTerminal.terminalControl.socketPath !== storedTerminalControl.socketPath ||
|
|
7413
|
+
liveTerminal.terminalControl.panePid !== storedTerminalControl.panePid) {
|
|
7414
|
+
throw new Error(`turn ${turnIdForConversation(initialConversation)} is not attached to its expected live tmux terminal`);
|
|
7415
|
+
}
|
|
7416
|
+
const terminalControl = liveTerminal.terminalControl;
|
|
7417
|
+
const responseStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
7418
|
+
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(responseStoreDir, terminalControl), { timeoutMs: 30000 });
|
|
6406
7419
|
try {
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
7420
|
+
await withStoreWriterLeaseAsync(responseStoreDir, async () => {
|
|
7421
|
+
// Legacy identity migration may write the Turn, so keep it inside the
|
|
7422
|
+
// canonical terminal -> Store writer -> state ordering as well.
|
|
7423
|
+
await migrateLegacyTerminalAgentIdentity({ ...loaded, options });
|
|
7424
|
+
const releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
7425
|
+
try {
|
|
7426
|
+
const lockedConversation = loadState(statePath);
|
|
7427
|
+
const lockedTakeover = isRecord(lockedConversation.native_session_takeover)
|
|
7428
|
+
? lockedConversation.native_session_takeover
|
|
7429
|
+
: undefined;
|
|
7430
|
+
const lockedControl = terminalControlFromTakeover(lockedTakeover);
|
|
7431
|
+
if (stringValue(lockedTakeover?.native_session_id) !== nativeTerminalId ||
|
|
7432
|
+
executorForConversation(lockedConversation).kind !== liveTerminal.agent ||
|
|
7433
|
+
!lockedControl ||
|
|
7434
|
+
lockedControl.target !== terminalControl.target ||
|
|
7435
|
+
lockedControl.socketPath !== terminalControl.socketPath ||
|
|
7436
|
+
lockedControl.panePid !== terminalControl.panePid) {
|
|
7437
|
+
throw new Error("terminal control changed while waiting to respond; refresh status and retry");
|
|
7438
|
+
}
|
|
7439
|
+
if (requestedOpenClawSession &&
|
|
7440
|
+
lockedConversation.openclaw_session !== requestedOpenClawSession) {
|
|
7441
|
+
throw new Error(`turn ${turnIdForConversation(lockedConversation)} belongs to a ` +
|
|
7442
|
+
"different OpenClaw session; no terminal input was sent");
|
|
7443
|
+
}
|
|
7444
|
+
if (replayExactActiveTerminalSubmission({
|
|
7445
|
+
options,
|
|
7446
|
+
terminalControl,
|
|
7447
|
+
requestText: String(messageBody),
|
|
7448
|
+
expectedStoreDir: responseStoreDir,
|
|
7449
|
+
expectedSessionId: sessionIdForConversation(lockedConversation),
|
|
7450
|
+
expectedTurnId: turnIdForConversation(lockedConversation),
|
|
7451
|
+
expectedMessageType: "answer",
|
|
7452
|
+
expectedStatePath: statePath
|
|
7453
|
+
})) {
|
|
7454
|
+
return;
|
|
7455
|
+
}
|
|
7456
|
+
if (lockedConversation.status !== "waiting_for_openclaw") {
|
|
7457
|
+
throw new Error(`cannot respond to turn ${turnIdForConversation(lockedConversation)}; ` +
|
|
7458
|
+
`turn is ${lockedConversation.status}, not waiting_for_openclaw`);
|
|
7459
|
+
}
|
|
7460
|
+
const prepared = prepareManagedSend({
|
|
7461
|
+
options: { ...options, type: "answer" },
|
|
7462
|
+
statePath,
|
|
7463
|
+
logPath,
|
|
7464
|
+
messageBody,
|
|
7465
|
+
stateLockHeld: true,
|
|
7466
|
+
persist: false
|
|
7467
|
+
});
|
|
7468
|
+
const preparedTakeover = isRecord(prepared.conversation.native_session_takeover)
|
|
7469
|
+
? prepared.conversation.native_session_takeover
|
|
7470
|
+
: undefined;
|
|
7471
|
+
const terminalAgentPid = Number(preparedTakeover?.terminal_agent_pid);
|
|
7472
|
+
const responseManagedSession = tryLoadManagedSession(responseStoreDir, sessionIdForConversation(prepared.conversation));
|
|
7473
|
+
const responseCodexCompanions = prepared.executor.kind === "codex" && responseManagedSession
|
|
7474
|
+
? codexAllowedCompanionSetForManagedSession({
|
|
7475
|
+
storeDir: responseStoreDir,
|
|
7476
|
+
session: responseManagedSession
|
|
7477
|
+
})
|
|
7478
|
+
: { additional: [] };
|
|
7479
|
+
const currentNativeIdentity = await resolveCurrentNativeAgentSessionIdentity({
|
|
7480
|
+
options,
|
|
7481
|
+
agent: prepared.executor.kind,
|
|
7482
|
+
pid: terminalAgentPid,
|
|
7483
|
+
cwd: terminalControl.currentPath,
|
|
7484
|
+
preferredSessionId: responseCodexCompanions.primary
|
|
7485
|
+
? stringValue(preparedTakeover?.terminal_agent_session_id)
|
|
7486
|
+
: undefined,
|
|
7487
|
+
allowedCompanionIdentity: responseCodexCompanions.primary,
|
|
7488
|
+
allowedAdditionalIdentities: responseCodexCompanions.additional
|
|
7489
|
+
});
|
|
7490
|
+
assertNativeAgentIdentityForTurn({
|
|
7491
|
+
conversation: prepared.conversation,
|
|
7492
|
+
currentIdentity: currentNativeIdentity,
|
|
7493
|
+
operation: "respond to"
|
|
7494
|
+
});
|
|
7495
|
+
const currentTerminalControl = terminalControlFromTakeover(prepared.nativeTakeoverForSend);
|
|
7496
|
+
if (!currentTerminalControl ||
|
|
7497
|
+
currentTerminalControl.target !== terminalControl.target ||
|
|
7498
|
+
currentTerminalControl.socketPath !== terminalControl.socketPath ||
|
|
7499
|
+
currentTerminalControl.panePid !== terminalControl.panePid) {
|
|
7500
|
+
throw new Error("terminal control changed while waiting to respond; refresh status and retry");
|
|
7501
|
+
}
|
|
7502
|
+
const responseOptions = {
|
|
7503
|
+
...options,
|
|
7504
|
+
type: "answer",
|
|
7505
|
+
agentTimeoutMinutes: options.agentTimeoutMinutes ??
|
|
7506
|
+
preparedTakeover?.terminal_bridge_inactivity_timeout_minutes ??
|
|
7507
|
+
DEFAULT_AGENT_TIMEOUT_MINUTES,
|
|
7508
|
+
agentHardTimeoutMinutes: options.agentHardTimeoutMinutes ??
|
|
7509
|
+
preparedTakeover?.terminal_bridge_hard_timeout_minutes ??
|
|
7510
|
+
DEFAULT_AGENT_HARD_TIMEOUT_MINUTES
|
|
7511
|
+
};
|
|
7512
|
+
await runTerminalControlSend({
|
|
7513
|
+
options: responseOptions,
|
|
7514
|
+
conversation: prepared.conversation,
|
|
7515
|
+
nextConversation: prepared.nextConversation,
|
|
7516
|
+
statePath,
|
|
7517
|
+
logPath,
|
|
7518
|
+
executor: prepared.executor,
|
|
7519
|
+
message: prepared.message,
|
|
7520
|
+
terminalControl: currentTerminalControl,
|
|
7521
|
+
terminalSendLockHeld: true,
|
|
7522
|
+
terminalStateLockHeld: true,
|
|
7523
|
+
storeWriterLeaseHeld: true,
|
|
7524
|
+
recordMessageAfterSend: true,
|
|
7525
|
+
allowedPreMaterializationIdentity: responseCodexCompanions.primary,
|
|
7526
|
+
allowedAdditionalIdentities: responseCodexCompanions.additional,
|
|
7527
|
+
continuingTurnResponse: true
|
|
7528
|
+
});
|
|
7529
|
+
}
|
|
7530
|
+
finally {
|
|
7531
|
+
releaseStateLock();
|
|
7532
|
+
}
|
|
6475
7533
|
});
|
|
6476
7534
|
}
|
|
6477
7535
|
finally {
|
|
6478
|
-
|
|
6479
|
-
releaseStateLock?.();
|
|
6480
|
-
}
|
|
6481
|
-
finally {
|
|
6482
|
-
releaseTerminalLock();
|
|
6483
|
-
}
|
|
7536
|
+
releaseTerminalLock();
|
|
6484
7537
|
}
|
|
6485
7538
|
}
|
|
6486
7539
|
async function runApprove(options) {
|
|
@@ -7167,14 +8220,18 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
7167
8220
|
"refresh AKK list and use its exact recovery action");
|
|
7168
8221
|
}
|
|
7169
8222
|
if (previousDispatchLedger?.status === "prepared" ||
|
|
8223
|
+
previousDispatchLedger?.status === "text_injected" ||
|
|
7170
8224
|
previousDispatchLedger?.status === "dispatching" ||
|
|
7171
|
-
previousDispatchLedger?.status === "uncertain"
|
|
8225
|
+
previousDispatchLedger?.status === "uncertain" ||
|
|
8226
|
+
previousDispatchLedger?.status === "not_accepted") {
|
|
7172
8227
|
throw new Error(`terminal ${terminalControl.target} has a terminal-level ` +
|
|
7173
8228
|
`${String(previousDispatchLedger.status)} dispatch owned by ` +
|
|
7174
8229
|
`${stringValue(previousDispatchLedger.conversation_id) ?? "an unknown conversation"}; ` +
|
|
7175
8230
|
"inspect the shared tmux pane and explicitly close that AKK conversation before retrying");
|
|
7176
8231
|
}
|
|
7177
|
-
if (previousDispatchLedger?.status === "submitted"
|
|
8232
|
+
if (previousDispatchLedger?.status === "submitted" ||
|
|
8233
|
+
previousDispatchLedger?.status === "enter_dispatched" ||
|
|
8234
|
+
previousDispatchLedger?.status === "agent_accepted") {
|
|
7178
8235
|
const owner = loadTerminalDispatchLedgerOwner(previousDispatchLedger);
|
|
7179
8236
|
if (!owner) {
|
|
7180
8237
|
throw new Error(`terminal ${terminalControl.target} has a submitted dispatch whose ` +
|
|
@@ -7194,6 +8251,14 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
7194
8251
|
stringValue(previousDispatchLedger.message_id) === message.id &&
|
|
7195
8252
|
sameCanonicalStatePath(previousDispatchLedger.state_path, statePath));
|
|
7196
8253
|
if (exactDispatchReplay) {
|
|
8254
|
+
const proofLevel = String(previousDispatchLedger.status);
|
|
8255
|
+
const replayReceipt = terminalSubmissionReplayReceipt({
|
|
8256
|
+
proofLevel,
|
|
8257
|
+
evidence: previousDispatchLedger.acceptance_evidence,
|
|
8258
|
+
expected: terminalAcceptanceEvidenceExpectation(owner, terminalPayload)
|
|
8259
|
+
});
|
|
8260
|
+
const accepted = replayReceipt.delivered;
|
|
8261
|
+
const acceptanceInvalid = replayReceipt.submission_outcome === "uncertain";
|
|
7197
8262
|
const receiptConversationId = stringValue(previousDispatchLedger.conversation_id) ??
|
|
7198
8263
|
owner.conversation_id;
|
|
7199
8264
|
const receiptSessionId = sessionIdForConversation(owner);
|
|
@@ -7217,24 +8282,43 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
7217
8282
|
turn_id: receiptTurnId
|
|
7218
8283
|
}
|
|
7219
8284
|
},
|
|
7220
|
-
delivered:
|
|
7221
|
-
status:
|
|
8285
|
+
delivered: replayReceipt.delivered,
|
|
8286
|
+
status: replayReceipt.status,
|
|
8287
|
+
submission_outcome: replayReceipt.submission_outcome,
|
|
7222
8288
|
background: true,
|
|
7223
|
-
callback_expected: Boolean(owner.gateway_method ??
|
|
8289
|
+
callback_expected: !acceptanceInvalid && Boolean(owner.gateway_method ??
|
|
7224
8290
|
previousDispatchLedger.callback_expected),
|
|
7225
8291
|
terminal_control: terminalControl,
|
|
7226
8292
|
executor,
|
|
7227
|
-
replayed:
|
|
7228
|
-
delivery_receipt:
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
8293
|
+
replayed: replayReceipt.replayed,
|
|
8294
|
+
delivery_receipt: replayReceipt.delivery_receipt,
|
|
8295
|
+
...(replayReceipt.do_not_retry
|
|
8296
|
+
? { do_not_retry: replayReceipt.do_not_retry }
|
|
8297
|
+
: {}),
|
|
8298
|
+
reason: accepted
|
|
8299
|
+
? "AKK replayed the durable native acceptance receipt for an identical active terminal request and did not send tmux input again."
|
|
8300
|
+
: acceptanceInvalid
|
|
8301
|
+
? `AKK refused to replay an invalid native acceptance receipt (${replayReceipt.evidence_error ?? "evidence validation failed"}); no tmux input was sent.`
|
|
8302
|
+
: "AKK replayed the original transport-level receipt without upgrading it to native acceptance and did not send tmux input again.",
|
|
8303
|
+
openclaw_next_action: accepted
|
|
8304
|
+
? openClawYieldNextAction({
|
|
8305
|
+
conversationId: receiptConversationId,
|
|
8306
|
+
sessionId: receiptSessionId,
|
|
8307
|
+
turnId: receiptTurnId,
|
|
8308
|
+
source: "terminal_control",
|
|
8309
|
+
callbackExpected: Boolean(owner.gateway_method ??
|
|
8310
|
+
previousDispatchLedger.callback_expected)
|
|
8311
|
+
})
|
|
8312
|
+
: {
|
|
8313
|
+
action: "inspect",
|
|
8314
|
+
conversation_id: receiptConversationId,
|
|
8315
|
+
session_id: receiptSessionId,
|
|
8316
|
+
turn_id: receiptTurnId,
|
|
8317
|
+
do_not_retry: true,
|
|
8318
|
+
reason: acceptanceInvalid
|
|
8319
|
+
? "The stored native acceptance evidence is invalid; inspect and explicitly close this Turn."
|
|
8320
|
+
: "Only terminal transport is proven; wait for native acceptance or inspect the shared pane."
|
|
8321
|
+
}
|
|
7238
8322
|
});
|
|
7239
8323
|
return;
|
|
7240
8324
|
}
|
|
@@ -7288,6 +8372,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
7288
8372
|
messageId: message.id
|
|
7289
8373
|
};
|
|
7290
8374
|
let preSendScreenFingerprint;
|
|
8375
|
+
let codexRolloutAcceptanceAnchor;
|
|
7291
8376
|
let claudeTranscriptAnchor;
|
|
7292
8377
|
const claudeHome = executor.kind === "claude"
|
|
7293
8378
|
? path.resolve(expandHome(options.claudeHome) ?? defaultClaudeHome())
|
|
@@ -7322,7 +8407,16 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
7322
8407
|
if (bridge) {
|
|
7323
8408
|
preSendScreenFingerprint = stringValue(status.screen.digest) ??
|
|
7324
8409
|
terminalBridgeScreenFingerprint(status.screen.excerpt);
|
|
7325
|
-
if (executor.kind === "
|
|
8410
|
+
if (executor.kind === "codex") {
|
|
8411
|
+
codexRolloutAcceptanceAnchor = captureCodexAcceptanceAnchorForSend({
|
|
8412
|
+
options,
|
|
8413
|
+
currentIdentity: currentNativeIdentity,
|
|
8414
|
+
expectedNativeThreadId: expectedManagedNativeThreadId,
|
|
8415
|
+
allowedPreMaterializationIdentity,
|
|
8416
|
+
needsPostSendNativeBinding
|
|
8417
|
+
});
|
|
8418
|
+
}
|
|
8419
|
+
else {
|
|
7326
8420
|
claudeTranscriptAnchor = captureClaudeTranscriptAnchor({
|
|
7327
8421
|
sessionId: preSendRuntime.sessionId,
|
|
7328
8422
|
cwd: preSendRuntime.cwd,
|
|
@@ -7348,6 +8442,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
7348
8442
|
agentTimeoutMinutes,
|
|
7349
8443
|
agentHardTimeoutMinutes,
|
|
7350
8444
|
preSendScreenFingerprint,
|
|
8445
|
+
codexRolloutAcceptanceAnchor,
|
|
7351
8446
|
claudeTranscriptAnchor,
|
|
7352
8447
|
claudeHome
|
|
7353
8448
|
})
|
|
@@ -7355,6 +8450,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
7355
8450
|
const preparedConversation = withTerminalBridgeSubmission({
|
|
7356
8451
|
conversation: bridgeConversation,
|
|
7357
8452
|
messageId: message.id,
|
|
8453
|
+
messageType: message.type,
|
|
8454
|
+
messageBody: String(message.body),
|
|
7358
8455
|
requestText: terminalPayload,
|
|
7359
8456
|
status: "prepared",
|
|
7360
8457
|
preparedAt: bridgeStartedAt
|
|
@@ -7367,6 +8464,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
7367
8464
|
session_id: sessionIdForConversation(preparedConversation),
|
|
7368
8465
|
turn_id: turnIdForConversation(preparedConversation),
|
|
7369
8466
|
message_id: message.id,
|
|
8467
|
+
message_type: message.type,
|
|
7370
8468
|
request_hash: terminalRequestHash,
|
|
7371
8469
|
prepared_at: bridgeStartedAt,
|
|
7372
8470
|
dispatcher_pid: process.pid,
|
|
@@ -7435,32 +8533,6 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
7435
8533
|
request_hash: terminalBridgeRequestFingerprint(terminalPayload),
|
|
7436
8534
|
dispatcher_pid: process.pid
|
|
7437
8535
|
});
|
|
7438
|
-
bridgeMonitor = bridge
|
|
7439
|
-
? startTerminalBridgeMonitorForConversation({
|
|
7440
|
-
conversation: preparedConversation,
|
|
7441
|
-
statePath,
|
|
7442
|
-
logPath,
|
|
7443
|
-
options
|
|
7444
|
-
})
|
|
7445
|
-
: undefined;
|
|
7446
|
-
if (bridgeMonitor) {
|
|
7447
|
-
appendEvent(logPath, {
|
|
7448
|
-
ts: new Date().toISOString(),
|
|
7449
|
-
conversation_id: preparedConversation.conversation_id,
|
|
7450
|
-
event: "terminal_bridge_monitor_launch",
|
|
7451
|
-
pid: bridgeMonitor.pid ?? null,
|
|
7452
|
-
terminal_control: terminalControl,
|
|
7453
|
-
phase: "before_terminal_submit",
|
|
7454
|
-
agent_timeout_minutes: agentTimeoutMinutes,
|
|
7455
|
-
agent_hard_timeout_minutes: agentHardTimeoutMinutes
|
|
7456
|
-
});
|
|
7457
|
-
runtimeLog("info", "terminal_bridge_monitor_launch", {
|
|
7458
|
-
conversation_id: preparedConversation.conversation_id,
|
|
7459
|
-
monitor_pid: bridgeMonitor.pid ?? null,
|
|
7460
|
-
terminal_target: terminalControl.target,
|
|
7461
|
-
phase: "before_terminal_submit"
|
|
7462
|
-
});
|
|
7463
|
-
}
|
|
7464
8536
|
}
|
|
7465
8537
|
catch (error) {
|
|
7466
8538
|
const abortedAt = new Date().toISOString();
|
|
@@ -7490,18 +8562,44 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
7490
8562
|
failed_at: abortedAt,
|
|
7491
8563
|
failure_reason: "terminal submission setup failed before tmux input"
|
|
7492
8564
|
}
|
|
7493
|
-
:
|
|
8565
|
+
: {
|
|
8566
|
+
...preparedConversation,
|
|
8567
|
+
status: conversation.status,
|
|
8568
|
+
...(conversation.idle_since
|
|
8569
|
+
? { idle_since: conversation.idle_since }
|
|
8570
|
+
: {})
|
|
8571
|
+
};
|
|
7494
8572
|
const abortedConversation = withTerminalBridgeSubmission({
|
|
7495
8573
|
conversation: failureBase,
|
|
7496
8574
|
messageId: message.id,
|
|
8575
|
+
messageType: message.type,
|
|
8576
|
+
messageBody: String(message.body),
|
|
7497
8577
|
requestText: terminalPayload,
|
|
7498
8578
|
status: "aborted",
|
|
7499
8579
|
preparedAt: bridgeStartedAt,
|
|
7500
8580
|
abortedAt,
|
|
7501
|
-
error: errorMessage
|
|
8581
|
+
error: errorMessage,
|
|
8582
|
+
safeToRetry: dispatchLedgerRestored
|
|
7502
8583
|
});
|
|
8584
|
+
let abortedStatePersisted = false;
|
|
7503
8585
|
try {
|
|
8586
|
+
if (process.env.AKK_TEST_ABORTED_STATE_PERSISTENCE_FAILURE === "1") {
|
|
8587
|
+
throw new Error("injected aborted submission state persistence failure");
|
|
8588
|
+
}
|
|
7504
8589
|
saveState(statePath, abortedConversation);
|
|
8590
|
+
abortedStatePersisted = true;
|
|
8591
|
+
}
|
|
8592
|
+
catch (persistenceError) {
|
|
8593
|
+
runtimeLog("error", "terminal_message_submit_aborted_persist_failed", {
|
|
8594
|
+
conversation_id: abortedConversation.conversation_id,
|
|
8595
|
+
terminal_target: terminalControl.target,
|
|
8596
|
+
error: persistenceError instanceof Error
|
|
8597
|
+
? persistenceError.message
|
|
8598
|
+
: String(persistenceError)
|
|
8599
|
+
});
|
|
8600
|
+
}
|
|
8601
|
+
const safeToRetry = dispatchLedgerRestored && abortedStatePersisted;
|
|
8602
|
+
try {
|
|
7505
8603
|
appendEvent(logPath, {
|
|
7506
8604
|
ts: abortedAt,
|
|
7507
8605
|
conversation_id: abortedConversation.conversation_id,
|
|
@@ -7510,11 +8608,11 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
7510
8608
|
executor,
|
|
7511
8609
|
terminal_control: terminalControl,
|
|
7512
8610
|
error: textSummary(errorMessage),
|
|
7513
|
-
safe_to_retry:
|
|
8611
|
+
safe_to_retry: safeToRetry
|
|
7514
8612
|
});
|
|
7515
8613
|
}
|
|
7516
8614
|
catch (persistenceError) {
|
|
7517
|
-
runtimeLog("error", "
|
|
8615
|
+
runtimeLog("error", "terminal_message_submit_aborted_event_failed", {
|
|
7518
8616
|
conversation_id: abortedConversation.conversation_id,
|
|
7519
8617
|
terminal_target: terminalControl.target,
|
|
7520
8618
|
error: persistenceError instanceof Error
|
|
@@ -7522,16 +8620,32 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
7522
8620
|
: String(persistenceError)
|
|
7523
8621
|
});
|
|
7524
8622
|
}
|
|
8623
|
+
const reportedAbortedConversation = safeToRetry
|
|
8624
|
+
? abortedConversation
|
|
8625
|
+
: withTerminalBridgeSubmission({
|
|
8626
|
+
conversation: abortedConversation,
|
|
8627
|
+
messageId: message.id,
|
|
8628
|
+
messageType: message.type,
|
|
8629
|
+
messageBody: String(message.body),
|
|
8630
|
+
requestText: terminalPayload,
|
|
8631
|
+
status: "aborted",
|
|
8632
|
+
preparedAt: bridgeStartedAt,
|
|
8633
|
+
abortedAt,
|
|
8634
|
+
error: errorMessage,
|
|
8635
|
+
safeToRetry: false
|
|
8636
|
+
});
|
|
7525
8637
|
runtimeLog("error", "terminal_message_submit_aborted", {
|
|
7526
8638
|
conversation_id: abortedConversation.conversation_id,
|
|
7527
8639
|
terminal_target: terminalControl.target,
|
|
7528
8640
|
error: errorMessage,
|
|
7529
|
-
safe_to_retry:
|
|
8641
|
+
safe_to_retry: safeToRetry,
|
|
8642
|
+
dispatch_ledger_restored: dispatchLedgerRestored,
|
|
8643
|
+
aborted_state_persisted: abortedStatePersisted
|
|
7530
8644
|
});
|
|
7531
8645
|
printJson({
|
|
7532
8646
|
session_id: sessionIdForConversation(abortedConversation),
|
|
7533
8647
|
turn_id: turnIdForConversation(abortedConversation),
|
|
7534
|
-
conversation:
|
|
8648
|
+
conversation: reportedAbortedConversation,
|
|
7535
8649
|
message,
|
|
7536
8650
|
delivered: false,
|
|
7537
8651
|
status: "submission_aborted",
|
|
@@ -7541,32 +8655,108 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
7541
8655
|
terminal_control: terminalControl,
|
|
7542
8656
|
monitor_pid: bridgeMonitor?.pid ?? null,
|
|
7543
8657
|
executor,
|
|
7544
|
-
safe_to_retry:
|
|
7545
|
-
do_not_retry: !
|
|
7546
|
-
reason:
|
|
8658
|
+
safe_to_retry: safeToRetry,
|
|
8659
|
+
do_not_retry: !safeToRetry,
|
|
8660
|
+
reason: safeToRetry
|
|
7547
8661
|
? "AKK failed before touching tmux; this terminal submission was not sent and may be retried."
|
|
7548
|
-
:
|
|
8662
|
+
: !dispatchLedgerRestored
|
|
8663
|
+
? "AKK failed before tmux input but could not restore the terminal dispatch ledger; inspect and close the conversation before retrying."
|
|
8664
|
+
: "AKK failed before tmux input but could not persist the aborted receipt; inspect the conversation before retrying.",
|
|
7549
8665
|
openclaw_next_action: {
|
|
7550
|
-
action:
|
|
8666
|
+
action: safeToRetry ? "retry" : "inspect",
|
|
7551
8667
|
conversation_id: abortedConversation.conversation_id,
|
|
7552
|
-
session_id: sessionIdForConversation(
|
|
7553
|
-
turn_id: turnIdForConversation(
|
|
7554
|
-
safe_to_retry:
|
|
7555
|
-
do_not_retry: !
|
|
7556
|
-
reason:
|
|
8668
|
+
session_id: sessionIdForConversation(reportedAbortedConversation),
|
|
8669
|
+
turn_id: turnIdForConversation(reportedAbortedConversation),
|
|
8670
|
+
safe_to_retry: safeToRetry,
|
|
8671
|
+
do_not_retry: !safeToRetry,
|
|
8672
|
+
reason: safeToRetry
|
|
7557
8673
|
? "The failure occurred before any tmux input."
|
|
7558
|
-
:
|
|
8674
|
+
: !dispatchLedgerRestored
|
|
8675
|
+
? "The terminal ledger could not be restored automatically."
|
|
8676
|
+
: "The aborted receipt could not be made durable."
|
|
7559
8677
|
}
|
|
7560
8678
|
});
|
|
7561
8679
|
return;
|
|
7562
8680
|
}
|
|
7563
8681
|
let deliveredConversation;
|
|
8682
|
+
let stagedConversation = preparedConversation;
|
|
8683
|
+
let textInjectedAt;
|
|
8684
|
+
let enterDispatchedAt;
|
|
8685
|
+
let acceptanceResult;
|
|
8686
|
+
let bookkeepingWarning;
|
|
8687
|
+
const recordPostTransportBookkeepingFailure = (phase, error) => {
|
|
8688
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8689
|
+
bookkeepingWarning ??= message;
|
|
8690
|
+
runtimeLog("warn", "terminal_message_post_transport_bookkeeping_failed", {
|
|
8691
|
+
conversation_id: stagedConversation.conversation_id,
|
|
8692
|
+
terminal_target: terminalControl.target,
|
|
8693
|
+
phase,
|
|
8694
|
+
error: message
|
|
8695
|
+
});
|
|
8696
|
+
};
|
|
7564
8697
|
try {
|
|
7565
8698
|
await terminalBridge.send(executor.kind, terminalControl, terminalPayload, {
|
|
7566
|
-
runtime: preSendRuntime
|
|
8699
|
+
runtime: preSendRuntime,
|
|
8700
|
+
async onTransportStage(event) {
|
|
8701
|
+
const stageAt = new Date().toISOString();
|
|
8702
|
+
if (event.stage === "text_injected") {
|
|
8703
|
+
textInjectedAt = stageAt;
|
|
8704
|
+
}
|
|
8705
|
+
else {
|
|
8706
|
+
enterDispatchedAt = stageAt;
|
|
8707
|
+
}
|
|
8708
|
+
stagedConversation = withTerminalBridgeSubmission({
|
|
8709
|
+
conversation: stagedConversation,
|
|
8710
|
+
messageId: message.id,
|
|
8711
|
+
messageType: message.type,
|
|
8712
|
+
messageBody: String(message.body),
|
|
8713
|
+
requestText: terminalPayload,
|
|
8714
|
+
status: event.stage,
|
|
8715
|
+
preparedAt: bridgeStartedAt,
|
|
8716
|
+
textInjectedAt,
|
|
8717
|
+
enterDispatchedAt
|
|
8718
|
+
});
|
|
8719
|
+
saveState(statePath, stagedConversation);
|
|
8720
|
+
saveTerminalBridgeDispatchLedger(terminalControl, {
|
|
8721
|
+
...terminalBindingLedgerFields(stagedConversation),
|
|
8722
|
+
status: event.stage,
|
|
8723
|
+
generation_id: message.id,
|
|
8724
|
+
conversation_id: stagedConversation.conversation_id,
|
|
8725
|
+
session_id: sessionIdForConversation(stagedConversation),
|
|
8726
|
+
turn_id: turnIdForConversation(stagedConversation),
|
|
8727
|
+
message_id: message.id,
|
|
8728
|
+
message_type: message.type,
|
|
8729
|
+
request_hash: terminalRequestHash,
|
|
8730
|
+
prepared_at: bridgeStartedAt,
|
|
8731
|
+
...(textInjectedAt ? { text_injected_at: textInjectedAt } : {}),
|
|
8732
|
+
...(enterDispatchedAt ? { enter_dispatched_at: enterDispatchedAt } : {}),
|
|
8733
|
+
dispatcher_pid: process.pid,
|
|
8734
|
+
state_path: statePath,
|
|
8735
|
+
event_log_path: logPath,
|
|
8736
|
+
callback_expected: Boolean(stagedConversation.gateway_method),
|
|
8737
|
+
previous_generation_id: stringValue(previousDispatchLedger?.generation_id) ??
|
|
8738
|
+
stringValue(previousDispatchLedger?.message_id)
|
|
8739
|
+
});
|
|
8740
|
+
try {
|
|
8741
|
+
appendEvent(logPath, {
|
|
8742
|
+
ts: stageAt,
|
|
8743
|
+
conversation_id: stagedConversation.conversation_id,
|
|
8744
|
+
event: `terminal_message_${event.stage}`,
|
|
8745
|
+
message_id: message.id,
|
|
8746
|
+
executor,
|
|
8747
|
+
terminal_control: terminalControl,
|
|
8748
|
+
request_hash: terminalRequestHash
|
|
8749
|
+
});
|
|
8750
|
+
}
|
|
8751
|
+
catch (error) {
|
|
8752
|
+
recordPostTransportBookkeepingFailure(event.stage, error);
|
|
8753
|
+
}
|
|
8754
|
+
}
|
|
7567
8755
|
});
|
|
7568
|
-
|
|
7569
|
-
|
|
8756
|
+
if (!enterDispatchedAt) {
|
|
8757
|
+
throw new Error("terminal bridge returned without an enter_dispatched receipt");
|
|
8758
|
+
}
|
|
8759
|
+
let submittedBase = stagedConversation;
|
|
7570
8760
|
if (needsPostSendNativeBinding) {
|
|
7571
8761
|
let boundIdentity;
|
|
7572
8762
|
let boundConversation;
|
|
@@ -7593,7 +8783,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
7593
8783
|
throw new Error(`native agent created thread ${boundIdentity.sessionId}, expected ` +
|
|
7594
8784
|
`${expectedNativeThreadId}`);
|
|
7595
8785
|
}
|
|
7596
|
-
boundConversation = withNativeAgentSessionIdentity(
|
|
8786
|
+
boundConversation = withNativeAgentSessionIdentity(stagedConversation, boundIdentity);
|
|
7597
8787
|
const bindingStoreDir = managedSessionStoreDirForConversation(boundConversation);
|
|
7598
8788
|
if (!bindingStoreDir) {
|
|
7599
8789
|
throw new Error("managed Session Store is unavailable before native identity commit");
|
|
@@ -7633,16 +8823,16 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
7633
8823
|
if (!boundIdentity) {
|
|
7634
8824
|
const bindingFailedAt = new Date().toISOString();
|
|
7635
8825
|
const bindingReason = bindingError
|
|
7636
|
-
? `AKK
|
|
7637
|
-
: "AKK
|
|
8826
|
+
? `AKK dispatched terminal input but could not verify the new native agent session: ${bindingError}`
|
|
8827
|
+
: "AKK dispatched terminal input but no exact native agent session appeared within the binding window";
|
|
7638
8828
|
const unfencedBase = {
|
|
7639
|
-
...
|
|
8829
|
+
...stagedConversation,
|
|
7640
8830
|
status: "stalled",
|
|
7641
8831
|
stalled_at: bindingFailedAt,
|
|
7642
8832
|
stalled_reason: bindingReason,
|
|
7643
8833
|
native_session_takeover: {
|
|
7644
|
-
...(isRecord(
|
|
7645
|
-
?
|
|
8834
|
+
...(isRecord(stagedConversation.native_session_takeover)
|
|
8835
|
+
? stagedConversation.native_session_takeover
|
|
7646
8836
|
: {}),
|
|
7647
8837
|
terminal_agent_identity_status: "unresolved_after_submit",
|
|
7648
8838
|
terminal_agent_identity_error: textSummary(bindingReason)
|
|
@@ -7652,10 +8842,16 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
7652
8842
|
const unfencedConversation = withTerminalBridgeSubmission({
|
|
7653
8843
|
conversation: unfencedBase,
|
|
7654
8844
|
messageId: message.id,
|
|
8845
|
+
messageType: message.type,
|
|
8846
|
+
messageBody: String(message.body),
|
|
7655
8847
|
requestText: terminalPayload,
|
|
7656
|
-
status: "
|
|
8848
|
+
status: "uncertain",
|
|
7657
8849
|
preparedAt: bridgeStartedAt,
|
|
7658
|
-
|
|
8850
|
+
textInjectedAt,
|
|
8851
|
+
enterDispatchedAt,
|
|
8852
|
+
uncertainAt: bindingFailedAt,
|
|
8853
|
+
error: bindingReason,
|
|
8854
|
+
lastProvenStage: "enter_dispatched"
|
|
7659
8855
|
});
|
|
7660
8856
|
quarantineManagedSessionBinding({
|
|
7661
8857
|
conversation: unfencedConversation,
|
|
@@ -7663,15 +8859,18 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
7663
8859
|
});
|
|
7664
8860
|
saveTerminalBridgeDispatchLedger(terminalControl, {
|
|
7665
8861
|
...terminalBindingLedgerFields(unfencedConversation),
|
|
7666
|
-
status: "
|
|
8862
|
+
status: "uncertain",
|
|
7667
8863
|
generation_id: message.id,
|
|
7668
8864
|
conversation_id: unfencedConversation.conversation_id,
|
|
7669
8865
|
session_id: sessionIdForConversation(unfencedConversation),
|
|
7670
8866
|
turn_id: turnIdForConversation(unfencedConversation),
|
|
7671
8867
|
message_id: message.id,
|
|
8868
|
+
message_type: message.type,
|
|
7672
8869
|
request_hash: terminalRequestHash,
|
|
7673
8870
|
prepared_at: bridgeStartedAt,
|
|
7674
|
-
|
|
8871
|
+
text_injected_at: textInjectedAt,
|
|
8872
|
+
enter_dispatched_at: enterDispatchedAt,
|
|
8873
|
+
uncertain_at: bindingFailedAt,
|
|
7675
8874
|
dispatcher_pid: process.pid,
|
|
7676
8875
|
state_path: statePath,
|
|
7677
8876
|
event_log_path: logPath,
|
|
@@ -7690,7 +8889,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
7690
8889
|
executor,
|
|
7691
8890
|
terminal_control: terminalControl,
|
|
7692
8891
|
error: textSummary(bindingReason),
|
|
7693
|
-
delivered:
|
|
8892
|
+
delivered: false,
|
|
7694
8893
|
do_not_retry: true
|
|
7695
8894
|
});
|
|
7696
8895
|
runtimeLog("error", "terminal_agent_identity_binding_failed", {
|
|
@@ -7698,7 +8897,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
7698
8897
|
agent: executor.kind,
|
|
7699
8898
|
terminal_target: terminalControl.target,
|
|
7700
8899
|
error: bindingReason,
|
|
7701
|
-
delivered:
|
|
8900
|
+
delivered: false,
|
|
7702
8901
|
do_not_retry: true
|
|
7703
8902
|
});
|
|
7704
8903
|
printJson({
|
|
@@ -7706,15 +8905,15 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
7706
8905
|
turn_id: turnIdForConversation(unfencedConversation),
|
|
7707
8906
|
conversation: unfencedConversation,
|
|
7708
8907
|
message,
|
|
7709
|
-
delivered:
|
|
7710
|
-
status: "
|
|
7711
|
-
submission_outcome: "
|
|
8908
|
+
delivered: false,
|
|
8909
|
+
status: "submission_uncertain",
|
|
8910
|
+
submission_outcome: "uncertain",
|
|
7712
8911
|
background: true,
|
|
7713
8912
|
callback_expected: false,
|
|
7714
8913
|
terminal_control: terminalControl,
|
|
7715
8914
|
monitor_pid: bridgeMonitor?.pid ?? null,
|
|
7716
8915
|
executor,
|
|
7717
|
-
delivery_receipt: "
|
|
8916
|
+
delivery_receipt: "enter_dispatched",
|
|
7718
8917
|
do_not_retry: true,
|
|
7719
8918
|
reason: bindingReason,
|
|
7720
8919
|
openclaw_next_action: {
|
|
@@ -7730,40 +8929,152 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
7730
8929
|
}
|
|
7731
8930
|
submittedBase = boundConversation;
|
|
7732
8931
|
}
|
|
7733
|
-
|
|
8932
|
+
acceptanceResult = await pollTerminalSubmissionAcceptance({
|
|
8933
|
+
options,
|
|
8934
|
+
executor: executor.kind,
|
|
7734
8935
|
conversation: submittedBase,
|
|
8936
|
+
terminalControl,
|
|
8937
|
+
terminalBridge
|
|
8938
|
+
});
|
|
8939
|
+
const acceptanceResolvedAt = new Date().toISOString();
|
|
8940
|
+
const terminalStatus = acceptanceResult.outcome === "pending_acceptance"
|
|
8941
|
+
? "enter_dispatched"
|
|
8942
|
+
: acceptanceResult.outcome;
|
|
8943
|
+
const outcomeBase = acceptanceResult.outcome === "not_accepted" ||
|
|
8944
|
+
acceptanceResult.outcome === "uncertain"
|
|
8945
|
+
? {
|
|
8946
|
+
...submittedBase,
|
|
8947
|
+
status: "stalled",
|
|
8948
|
+
stalled_at: acceptanceResolvedAt,
|
|
8949
|
+
stalled_reason: acceptanceResult.reason,
|
|
8950
|
+
updated_at: acceptanceResolvedAt
|
|
8951
|
+
}
|
|
8952
|
+
: submittedBase;
|
|
8953
|
+
deliveredConversation = withTerminalBridgeSubmission({
|
|
8954
|
+
conversation: outcomeBase,
|
|
7735
8955
|
messageId: message.id,
|
|
8956
|
+
messageType: message.type,
|
|
8957
|
+
messageBody: String(message.body),
|
|
7736
8958
|
requestText: terminalPayload,
|
|
7737
|
-
status:
|
|
8959
|
+
status: terminalStatus,
|
|
7738
8960
|
preparedAt: bridgeStartedAt,
|
|
7739
|
-
|
|
8961
|
+
textInjectedAt,
|
|
8962
|
+
enterDispatchedAt,
|
|
8963
|
+
...(acceptanceResult.outcome === "agent_accepted"
|
|
8964
|
+
? {
|
|
8965
|
+
agentAcceptedAt: acceptanceResolvedAt,
|
|
8966
|
+
acceptanceEvidence: acceptanceResult.evidence
|
|
8967
|
+
}
|
|
8968
|
+
: {}),
|
|
8969
|
+
...(acceptanceResult.outcome === "not_accepted"
|
|
8970
|
+
? { notAcceptedAt: acceptanceResolvedAt }
|
|
8971
|
+
: {}),
|
|
8972
|
+
...(acceptanceResult.outcome === "uncertain"
|
|
8973
|
+
? {
|
|
8974
|
+
uncertainAt: acceptanceResolvedAt,
|
|
8975
|
+
error: acceptanceResult.reason,
|
|
8976
|
+
lastProvenStage: "enter_dispatched"
|
|
8977
|
+
}
|
|
8978
|
+
: {})
|
|
7740
8979
|
});
|
|
7741
8980
|
saveState(statePath, deliveredConversation);
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
|
|
7759
|
-
|
|
8981
|
+
try {
|
|
8982
|
+
if (process.env.AKK_TEST_FINAL_TERMINAL_LEDGER_FAILURE === "1") {
|
|
8983
|
+
throw new Error("injected final terminal ledger persistence failure");
|
|
8984
|
+
}
|
|
8985
|
+
saveTerminalBridgeDispatchLedger(terminalControl, {
|
|
8986
|
+
...terminalBindingLedgerFields(deliveredConversation),
|
|
8987
|
+
status: terminalStatus,
|
|
8988
|
+
generation_id: message.id,
|
|
8989
|
+
conversation_id: deliveredConversation.conversation_id,
|
|
8990
|
+
session_id: sessionIdForConversation(deliveredConversation),
|
|
8991
|
+
turn_id: turnIdForConversation(deliveredConversation),
|
|
8992
|
+
message_id: message.id,
|
|
8993
|
+
message_type: message.type,
|
|
8994
|
+
request_hash: terminalRequestHash,
|
|
8995
|
+
prepared_at: bridgeStartedAt,
|
|
8996
|
+
text_injected_at: textInjectedAt,
|
|
8997
|
+
enter_dispatched_at: enterDispatchedAt,
|
|
8998
|
+
...(acceptanceResult.outcome === "agent_accepted"
|
|
8999
|
+
? {
|
|
9000
|
+
agent_accepted_at: acceptanceResolvedAt,
|
|
9001
|
+
acceptance_evidence: acceptanceResult.evidence
|
|
9002
|
+
}
|
|
9003
|
+
: {}),
|
|
9004
|
+
...(acceptanceResult.outcome === "not_accepted"
|
|
9005
|
+
? { not_accepted_at: acceptanceResolvedAt }
|
|
9006
|
+
: {}),
|
|
9007
|
+
...(acceptanceResult.outcome === "uncertain"
|
|
9008
|
+
? {
|
|
9009
|
+
uncertain_at: acceptanceResolvedAt,
|
|
9010
|
+
error: textSummary(acceptanceResult.reason)
|
|
9011
|
+
}
|
|
9012
|
+
: {}),
|
|
9013
|
+
dispatcher_pid: null,
|
|
9014
|
+
state_path: statePath,
|
|
9015
|
+
event_log_path: logPath,
|
|
9016
|
+
callback_expected: Boolean(deliveredConversation.gateway_method),
|
|
9017
|
+
previous_generation_id: stringValue(previousDispatchLedger?.generation_id) ??
|
|
9018
|
+
stringValue(previousDispatchLedger?.message_id)
|
|
9019
|
+
});
|
|
9020
|
+
}
|
|
9021
|
+
catch (error) {
|
|
9022
|
+
// State is the durable proof authority. Once a valid native ACK has
|
|
9023
|
+
// committed there, a lagging ledger is bookkeeping debt and must never
|
|
9024
|
+
// overwrite that strongest proof with `uncertain`.
|
|
9025
|
+
recordPostTransportBookkeepingFailure("final_terminal_ledger", error);
|
|
9026
|
+
}
|
|
9027
|
+
try {
|
|
9028
|
+
appendEvent(logPath, {
|
|
9029
|
+
ts: acceptanceResolvedAt,
|
|
9030
|
+
conversation_id: deliveredConversation.conversation_id,
|
|
9031
|
+
event: acceptanceResult.outcome === "agent_accepted"
|
|
9032
|
+
? "terminal_message_agent_accepted"
|
|
9033
|
+
: acceptanceResult.outcome === "pending_acceptance"
|
|
9034
|
+
? "terminal_message_acceptance_pending"
|
|
9035
|
+
: `terminal_message_${acceptanceResult.outcome}`,
|
|
9036
|
+
message_id: message.id,
|
|
9037
|
+
executor,
|
|
9038
|
+
terminal_control: terminalControl,
|
|
9039
|
+
delivery_receipt: terminalStatus,
|
|
9040
|
+
do_not_retry: acceptanceResult.outcome !== "agent_accepted"
|
|
9041
|
+
});
|
|
9042
|
+
}
|
|
9043
|
+
catch (error) {
|
|
9044
|
+
recordPostTransportBookkeepingFailure(terminalStatus, error);
|
|
9045
|
+
}
|
|
9046
|
+
if (bridge &&
|
|
9047
|
+
(acceptanceResult.outcome === "agent_accepted" ||
|
|
9048
|
+
acceptanceResult.outcome === "pending_acceptance")) {
|
|
9049
|
+
try {
|
|
9050
|
+
bridgeMonitor = startTerminalBridgeMonitorForConversation({
|
|
9051
|
+
conversation: deliveredConversation,
|
|
9052
|
+
statePath,
|
|
9053
|
+
logPath,
|
|
9054
|
+
options
|
|
9055
|
+
});
|
|
9056
|
+
if (bridgeMonitor) {
|
|
9057
|
+
appendEvent(logPath, {
|
|
9058
|
+
ts: new Date().toISOString(),
|
|
9059
|
+
conversation_id: deliveredConversation.conversation_id,
|
|
9060
|
+
event: "terminal_bridge_monitor_launch",
|
|
9061
|
+
pid: bridgeMonitor.pid ?? null,
|
|
9062
|
+
terminal_control: terminalControl,
|
|
9063
|
+
phase: acceptanceResult.outcome,
|
|
9064
|
+
agent_timeout_minutes: agentTimeoutMinutes,
|
|
9065
|
+
agent_hard_timeout_minutes: agentHardTimeoutMinutes
|
|
9066
|
+
});
|
|
9067
|
+
}
|
|
9068
|
+
}
|
|
9069
|
+
catch (error) {
|
|
9070
|
+
recordPostTransportBookkeepingFailure("monitor_launch", error);
|
|
9071
|
+
}
|
|
9072
|
+
}
|
|
7760
9073
|
}
|
|
7761
9074
|
catch (error) {
|
|
7762
9075
|
const uncertainAt = new Date().toISOString();
|
|
7763
9076
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
7764
|
-
const failureBase =
|
|
7765
|
-
? conversation
|
|
7766
|
-
: preparedConversation;
|
|
9077
|
+
const failureBase = stagedConversation;
|
|
7767
9078
|
const stalledFailureBase = {
|
|
7768
9079
|
...failureBase,
|
|
7769
9080
|
status: "stalled",
|
|
@@ -7774,11 +9085,20 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
7774
9085
|
const uncertainConversation = withTerminalBridgeSubmission({
|
|
7775
9086
|
conversation: stalledFailureBase,
|
|
7776
9087
|
messageId: message.id,
|
|
9088
|
+
messageType: message.type,
|
|
9089
|
+
messageBody: String(message.body),
|
|
7777
9090
|
requestText: terminalPayload,
|
|
7778
9091
|
status: "uncertain",
|
|
7779
9092
|
preparedAt: bridgeStartedAt,
|
|
9093
|
+
textInjectedAt,
|
|
9094
|
+
enterDispatchedAt,
|
|
7780
9095
|
uncertainAt,
|
|
7781
|
-
error: errorMessage
|
|
9096
|
+
error: errorMessage,
|
|
9097
|
+
lastProvenStage: enterDispatchedAt
|
|
9098
|
+
? "enter_dispatched"
|
|
9099
|
+
: textInjectedAt
|
|
9100
|
+
? "text_injected"
|
|
9101
|
+
: "prepared"
|
|
7782
9102
|
});
|
|
7783
9103
|
try {
|
|
7784
9104
|
saveTerminalBridgeDispatchLedger(terminalControl, {
|
|
@@ -7789,8 +9109,11 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
7789
9109
|
session_id: sessionIdForConversation(uncertainConversation),
|
|
7790
9110
|
turn_id: turnIdForConversation(uncertainConversation),
|
|
7791
9111
|
message_id: message.id,
|
|
9112
|
+
message_type: message.type,
|
|
7792
9113
|
request_hash: terminalRequestHash,
|
|
7793
9114
|
prepared_at: bridgeStartedAt,
|
|
9115
|
+
...(textInjectedAt ? { text_injected_at: textInjectedAt } : {}),
|
|
9116
|
+
...(enterDispatchedAt ? { enter_dispatched_at: enterDispatchedAt } : {}),
|
|
7794
9117
|
uncertain_at: uncertainAt,
|
|
7795
9118
|
dispatcher_pid: process.pid,
|
|
7796
9119
|
state_path: statePath,
|
|
@@ -7850,7 +9173,11 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
7850
9173
|
executor,
|
|
7851
9174
|
do_not_retry: true,
|
|
7852
9175
|
stalled_conversation_ids: stalledConversationIds,
|
|
7853
|
-
reason:
|
|
9176
|
+
reason: enterDispatchedAt
|
|
9177
|
+
? "AKK dispatched Enter but native acceptance or its exact identity became uncertain. Do not retry automatically; inspect this conversation and pane."
|
|
9178
|
+
: textInjectedAt
|
|
9179
|
+
? "AKK injected text but could not prove that Enter was dispatched. Do not retry automatically; inspect this conversation and pane."
|
|
9180
|
+
: "AKK could not prove that terminal input remained untouched. Inspect this conversation before retrying.",
|
|
7854
9181
|
openclaw_next_action: {
|
|
7855
9182
|
action: "inspect",
|
|
7856
9183
|
conversation_id: uncertainConversation.conversation_id,
|
|
@@ -7862,80 +9189,493 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
7862
9189
|
});
|
|
7863
9190
|
return;
|
|
7864
9191
|
}
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
9192
|
+
const nativeAccepted = acceptanceResult?.outcome === "agent_accepted";
|
|
9193
|
+
const acceptancePending = acceptanceResult?.outcome === "pending_acceptance";
|
|
9194
|
+
try {
|
|
9195
|
+
appendEvent(logPath, {
|
|
9196
|
+
ts: new Date().toISOString(),
|
|
9197
|
+
conversation_id: conversation.conversation_id,
|
|
9198
|
+
event: "terminal_message_send",
|
|
9199
|
+
executor,
|
|
9200
|
+
terminal_control: terminalControl,
|
|
9201
|
+
message: textSummary(message.body),
|
|
9202
|
+
payload: textSummary(terminalPayload)
|
|
9203
|
+
});
|
|
9204
|
+
runtimeLog("info", "terminal_message_send", {
|
|
9205
|
+
conversation_id: conversation.conversation_id,
|
|
9206
|
+
agent: executor.kind,
|
|
9207
|
+
terminal_target: terminalControl.target,
|
|
9208
|
+
message: textSummary(message.body),
|
|
9209
|
+
payload: textSummary(terminalPayload)
|
|
9210
|
+
});
|
|
9211
|
+
}
|
|
9212
|
+
catch (error) {
|
|
9213
|
+
bookkeepingWarning =
|
|
9214
|
+
error instanceof Error ? error.message : String(error);
|
|
9215
|
+
runtimeLog("warn", "terminal_message_post_submit_bookkeeping_failed", {
|
|
9216
|
+
conversation_id: deliveredConversation.conversation_id,
|
|
9217
|
+
terminal_target: terminalControl.target,
|
|
9218
|
+
error: bookkeepingWarning,
|
|
9219
|
+
delivered: nativeAccepted
|
|
9220
|
+
});
|
|
9221
|
+
try {
|
|
9222
|
+
appendEvent(logPath, {
|
|
9223
|
+
ts: new Date().toISOString(),
|
|
9224
|
+
conversation_id: deliveredConversation.conversation_id,
|
|
9225
|
+
event: "terminal_message_post_submit_bookkeeping_failed",
|
|
9226
|
+
terminal_control: terminalControl,
|
|
9227
|
+
error: textSummary(bookkeepingWarning),
|
|
9228
|
+
delivered: nativeAccepted
|
|
9229
|
+
});
|
|
9230
|
+
}
|
|
9231
|
+
catch {
|
|
9232
|
+
// The durable submitted receipt remains authoritative even if the event log is unavailable.
|
|
9233
|
+
}
|
|
9234
|
+
}
|
|
9235
|
+
printJson({
|
|
9236
|
+
session_id: sessionIdForConversation(deliveredConversation),
|
|
9237
|
+
turn_id: turnIdForConversation(deliveredConversation),
|
|
9238
|
+
conversation: deliveredConversation,
|
|
9239
|
+
message,
|
|
9240
|
+
delivered: nativeAccepted,
|
|
9241
|
+
status: nativeAccepted
|
|
9242
|
+
? "async_pending"
|
|
9243
|
+
: acceptancePending
|
|
9244
|
+
? "submission_pending_acceptance"
|
|
9245
|
+
: acceptanceResult?.outcome === "not_accepted"
|
|
9246
|
+
? "submission_not_accepted"
|
|
9247
|
+
: "submission_uncertain",
|
|
9248
|
+
submission_outcome: acceptanceResult?.outcome,
|
|
9249
|
+
background: true,
|
|
9250
|
+
callback_expected: Boolean(deliveredConversation.gateway_method &&
|
|
9251
|
+
(nativeAccepted || acceptancePending)),
|
|
9252
|
+
terminal_control: terminalControl,
|
|
9253
|
+
monitor_pid: bridgeMonitor?.pid ?? null,
|
|
9254
|
+
executor,
|
|
9255
|
+
budget: budgetAction(deliveredConversation),
|
|
9256
|
+
delivery_receipt: nativeAccepted
|
|
9257
|
+
? "agent_accepted"
|
|
9258
|
+
: acceptancePending
|
|
9259
|
+
? "enter_dispatched"
|
|
9260
|
+
: acceptanceResult?.outcome,
|
|
9261
|
+
...(!nativeAccepted ? { do_not_retry: true } : {}),
|
|
9262
|
+
...(bookkeepingWarning
|
|
9263
|
+
? {
|
|
9264
|
+
bookkeeping_warning: textSummary(bookkeepingWarning)
|
|
9265
|
+
}
|
|
9266
|
+
: {}),
|
|
9267
|
+
openclaw_next_action: nativeAccepted
|
|
9268
|
+
? openClawYieldNextAction({
|
|
9269
|
+
conversationId: deliveredConversation.conversation_id,
|
|
9270
|
+
sessionId: sessionIdForConversation(deliveredConversation),
|
|
9271
|
+
turnId: turnIdForConversation(deliveredConversation),
|
|
9272
|
+
source: "terminal_control",
|
|
9273
|
+
callbackExpected: Boolean(deliveredConversation.gateway_method)
|
|
9274
|
+
})
|
|
9275
|
+
: {
|
|
9276
|
+
action: acceptancePending ? "wait_for_acceptance" : "inspect",
|
|
9277
|
+
conversation_id: deliveredConversation.conversation_id,
|
|
9278
|
+
session_id: sessionIdForConversation(deliveredConversation),
|
|
9279
|
+
turn_id: turnIdForConversation(deliveredConversation),
|
|
9280
|
+
do_not_retry: true,
|
|
9281
|
+
reason: acceptancePending
|
|
9282
|
+
? "Terminal transport is proven and the background monitor is still waiting for native acceptance."
|
|
9283
|
+
: acceptanceResult?.outcome === "not_accepted"
|
|
9284
|
+
? "The exact draft is still present; inspect the composer and do not send a duplicate."
|
|
9285
|
+
: "Native acceptance evidence became uncertain; inspect the shared pane."
|
|
9286
|
+
}
|
|
9287
|
+
});
|
|
9288
|
+
}
|
|
9289
|
+
function terminalSubmissionPayload(payload) {
|
|
9290
|
+
assertOrdinaryTerminalPayloadDoesNotInvokeNativeLifecycle(payload);
|
|
9291
|
+
return payload.trimEnd();
|
|
9292
|
+
}
|
|
9293
|
+
function allowsSyntheticTerminalAcceptance(_options) {
|
|
9294
|
+
return process.env.AKK_TEST_ALLOW_SYNTHETIC_TERMINAL_ACCEPTANCE === "1";
|
|
9295
|
+
}
|
|
9296
|
+
function syntheticTerminalAcceptanceEvidence({ options, executor, conversation, requestHash }) {
|
|
9297
|
+
if (!allowsSyntheticTerminalAcceptance(options)) {
|
|
9298
|
+
return undefined;
|
|
9299
|
+
}
|
|
9300
|
+
const outcome = process.env.AKK_TEST_TERMINAL_ACCEPTANCE_OUTCOME ?? "accepted";
|
|
9301
|
+
if (outcome === "pending" || outcome === "not_accepted") {
|
|
9302
|
+
return undefined;
|
|
9303
|
+
}
|
|
9304
|
+
if (outcome === "identity_drift" || outcome === "uncertain") {
|
|
9305
|
+
throw new Error("injected terminal acceptance identity drift");
|
|
9306
|
+
}
|
|
9307
|
+
if (outcome !== "accepted") {
|
|
9308
|
+
throw new Error(`unsupported injected terminal acceptance outcome ${outcome}`);
|
|
9309
|
+
}
|
|
9310
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
9311
|
+
? conversation.native_session_takeover
|
|
9312
|
+
: undefined;
|
|
9313
|
+
const nativeThreadId = stringValue(nativeTakeover?.terminal_agent_session_id) ?? stringValue(nativeTakeover?.terminal_agent_expected_session_id) ??
|
|
9314
|
+
sessionIdForConversation(conversation);
|
|
9315
|
+
const evidenceBase = {
|
|
9316
|
+
source: executor === "codex"
|
|
9317
|
+
? "codex_rollout"
|
|
9318
|
+
: "claude_transcript",
|
|
9319
|
+
kind: "native_user_turn",
|
|
9320
|
+
nativeThreadId,
|
|
9321
|
+
requestHash,
|
|
9322
|
+
acceptanceId: `test-${turnIdForConversation(conversation)}`,
|
|
9323
|
+
anchorFingerprint: createHash("sha256")
|
|
9324
|
+
.update(`test-anchor:${nativeThreadId}:${requestHash}`)
|
|
9325
|
+
.digest("hex")
|
|
9326
|
+
};
|
|
9327
|
+
return {
|
|
9328
|
+
...evidenceBase,
|
|
9329
|
+
evidenceFingerprint: createHash("sha256")
|
|
9330
|
+
.update(JSON.stringify(evidenceBase))
|
|
9331
|
+
.digest("hex")
|
|
9332
|
+
};
|
|
9333
|
+
}
|
|
9334
|
+
function captureCodexAcceptanceAnchorForSend({ options, currentIdentity, expectedNativeThreadId, allowedPreMaterializationIdentity, needsPostSendNativeBinding }) {
|
|
9335
|
+
if (allowsSyntheticTerminalAcceptance(options)) {
|
|
9336
|
+
return undefined;
|
|
9337
|
+
}
|
|
9338
|
+
const nativeThreadId = needsPostSendNativeBinding
|
|
9339
|
+
? expectedNativeThreadId
|
|
9340
|
+
: currentIdentity?.sessionId;
|
|
9341
|
+
const processUuid = currentIdentity?.processUuid ??
|
|
9342
|
+
allowedPreMaterializationIdentity?.processUuid;
|
|
9343
|
+
const processBirth = currentIdentity?.processBirth ??
|
|
9344
|
+
allowedPreMaterializationIdentity?.processBirth;
|
|
9345
|
+
if (!nativeThreadId || !processUuid || !processBirth) {
|
|
9346
|
+
throw new Error("Codex native acceptance anchor requires an exact thread and process incarnation");
|
|
9347
|
+
}
|
|
9348
|
+
const base = { nativeThreadId, processUuid, processBirth };
|
|
9349
|
+
if (!needsPostSendNativeBinding && currentIdentity?.rollout) {
|
|
9350
|
+
return captureCodexRolloutAcceptanceAnchor({
|
|
9351
|
+
...base,
|
|
9352
|
+
mode: "existing",
|
|
9353
|
+
rollout: currentIdentity.rollout
|
|
9354
|
+
});
|
|
9355
|
+
}
|
|
9356
|
+
return captureCodexRolloutAcceptanceAnchor({
|
|
9357
|
+
...base,
|
|
9358
|
+
mode: "pre_materialization",
|
|
9359
|
+
expectedEmptyNativeSession: true
|
|
9360
|
+
});
|
|
9361
|
+
}
|
|
9362
|
+
async function detectTerminalSubmissionAcceptance({ options, executor, conversation, terminalControl }) {
|
|
9363
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
9364
|
+
? conversation.native_session_takeover
|
|
9365
|
+
: undefined;
|
|
9366
|
+
const requestHash = stringValue(nativeTakeover?.terminal_bridge_request_hash);
|
|
9367
|
+
if (!requestHash) {
|
|
9368
|
+
throw new Error("terminal acceptance request hash is unavailable");
|
|
9369
|
+
}
|
|
9370
|
+
assertTurnBindingCurrent(conversation, "monitor");
|
|
9371
|
+
const synthetic = syntheticTerminalAcceptanceEvidence({
|
|
9372
|
+
options,
|
|
9373
|
+
executor,
|
|
9374
|
+
conversation,
|
|
9375
|
+
requestHash
|
|
9376
|
+
});
|
|
9377
|
+
if (synthetic) {
|
|
9378
|
+
return synthetic;
|
|
9379
|
+
}
|
|
9380
|
+
if (allowsSyntheticTerminalAcceptance(options)) {
|
|
9381
|
+
return undefined;
|
|
9382
|
+
}
|
|
9383
|
+
if (executor === "codex") {
|
|
9384
|
+
const anchor = nativeTakeover?.codex_rollout_acceptance_anchor;
|
|
9385
|
+
if (!isRecord(anchor)) {
|
|
9386
|
+
throw new Error("Codex rollout acceptance anchor is unavailable");
|
|
9387
|
+
}
|
|
9388
|
+
const pid = Number(nativeTakeover?.terminal_agent_pid);
|
|
9389
|
+
const currentIdentity = await resolveCurrentNativeAgentSessionIdentity({
|
|
9390
|
+
options,
|
|
9391
|
+
agent: "codex",
|
|
9392
|
+
pid,
|
|
9393
|
+
cwd: terminalControl.currentPath,
|
|
9394
|
+
preferredSessionId: stringValue(anchor.native_thread_id)
|
|
9395
|
+
});
|
|
9396
|
+
if (!currentIdentity) {
|
|
9397
|
+
return undefined;
|
|
9398
|
+
}
|
|
9399
|
+
return detectCodexRolloutAcceptance({
|
|
9400
|
+
anchor: anchor,
|
|
9401
|
+
currentIdentity,
|
|
9402
|
+
requestHash
|
|
9403
|
+
});
|
|
9404
|
+
}
|
|
9405
|
+
const claudeEvidence = detectClaudeTranscriptAcceptance(terminalDurableRequestForConversation(conversation, terminalControl), {
|
|
9406
|
+
claudeHome: expandHome(options.claudeHome),
|
|
9407
|
+
agentRows: loadClaudeAgentRows(options)
|
|
9408
|
+
});
|
|
9409
|
+
if (!claudeEvidence) {
|
|
9410
|
+
return undefined;
|
|
9411
|
+
}
|
|
9412
|
+
return claudeEvidence;
|
|
9413
|
+
}
|
|
9414
|
+
async function pollTerminalSubmissionAcceptance({ options, executor, conversation, terminalControl, terminalBridge }) {
|
|
9415
|
+
const timeoutMs = positiveMilliseconds(options.terminalAcceptanceTimeoutMs ??
|
|
9416
|
+
DEFAULT_TERMINAL_ACCEPTANCE_TIMEOUT_MS, "--terminal-acceptance-timeout-ms");
|
|
9417
|
+
const pollIntervalMs = Math.max(10, Math.min(timeoutMs, Number(options.terminalAcceptancePollIntervalMs ??
|
|
9418
|
+
DEFAULT_TERMINAL_ACCEPTANCE_POLL_INTERVAL_MS)));
|
|
9419
|
+
const deadline = Date.now() + timeoutMs;
|
|
9420
|
+
while (true) {
|
|
9421
|
+
try {
|
|
9422
|
+
const evidence = await detectTerminalSubmissionAcceptance({
|
|
9423
|
+
options,
|
|
9424
|
+
executor,
|
|
9425
|
+
conversation,
|
|
9426
|
+
terminalControl
|
|
9427
|
+
});
|
|
9428
|
+
if (evidence) {
|
|
9429
|
+
return { outcome: "agent_accepted", evidence };
|
|
9430
|
+
}
|
|
9431
|
+
}
|
|
9432
|
+
catch (error) {
|
|
9433
|
+
return {
|
|
9434
|
+
outcome: "uncertain",
|
|
9435
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
9436
|
+
};
|
|
9437
|
+
}
|
|
9438
|
+
if (Date.now() >= deadline) {
|
|
9439
|
+
break;
|
|
9440
|
+
}
|
|
9441
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
9442
|
+
}
|
|
9443
|
+
if (allowsSyntheticTerminalAcceptance(options) &&
|
|
9444
|
+
process.env.AKK_TEST_TERMINAL_ACCEPTANCE_OUTCOME === "not_accepted") {
|
|
9445
|
+
return {
|
|
9446
|
+
outcome: "not_accepted",
|
|
9447
|
+
reason: "the exact managed draft remains in the terminal composer"
|
|
9448
|
+
};
|
|
9449
|
+
}
|
|
9450
|
+
try {
|
|
9451
|
+
const requestText = String(isRecord(conversation.native_session_takeover)
|
|
9452
|
+
? conversation.native_session_takeover.terminal_bridge_request_text ?? ""
|
|
9453
|
+
: "");
|
|
9454
|
+
if (await terminalBridge.proveExactDraftStillPresent(executor, terminalControl, requestText, {
|
|
9455
|
+
scrollbackLines: Number(options.scrollbackLines ?? 240),
|
|
9456
|
+
runtime: terminalRuntimeIdentityForConversation(conversation, terminalControl)
|
|
9457
|
+
})) {
|
|
9458
|
+
return {
|
|
9459
|
+
outcome: "not_accepted",
|
|
9460
|
+
reason: "the exact managed draft remains in the terminal composer"
|
|
9461
|
+
};
|
|
9462
|
+
}
|
|
9463
|
+
}
|
|
9464
|
+
catch {
|
|
9465
|
+
// Native acceptance evidence remains authoritative. A best-effort draft
|
|
9466
|
+
// inspection failure does not turn a bounded no-ACK into an identity claim.
|
|
9467
|
+
}
|
|
9468
|
+
return { outcome: "pending_acceptance" };
|
|
9469
|
+
}
|
|
9470
|
+
async function reconcileTerminalAcceptanceInMonitor({ options, conversation, statePath, logPath, terminalControl, executor, terminalBridge }) {
|
|
9471
|
+
const evidence = await detectTerminalSubmissionAcceptance({
|
|
9472
|
+
options,
|
|
9473
|
+
executor: executor.kind,
|
|
9474
|
+
conversation,
|
|
9475
|
+
terminalControl
|
|
9476
|
+
});
|
|
9477
|
+
const submission = terminalBridgeSubmission(conversation);
|
|
9478
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
9479
|
+
? conversation.native_session_takeover
|
|
9480
|
+
: undefined;
|
|
9481
|
+
const messageId = stringValue(submission?.message_id);
|
|
9482
|
+
if (!messageId) {
|
|
9483
|
+
throw new Error("terminal acceptance monitor lost its exact message identity");
|
|
9484
|
+
}
|
|
9485
|
+
let notAcceptedReason;
|
|
9486
|
+
if (!evidence) {
|
|
9487
|
+
const enterAt = validTimestampMs(submission?.enter_dispatched_at);
|
|
9488
|
+
if (enterAt !== undefined && Date.now() - enterAt >= 250) {
|
|
9489
|
+
const requestText = String(nativeTakeover?.terminal_bridge_request_text ?? "");
|
|
9490
|
+
if (await terminalBridge.proveExactDraftStillPresent(executor.kind, terminalControl, requestText, {
|
|
9491
|
+
scrollbackLines: Number(options.scrollbackLines ?? 240),
|
|
9492
|
+
runtime: terminalRuntimeIdentityForConversation(conversation, terminalControl)
|
|
9493
|
+
})) {
|
|
9494
|
+
notAcceptedReason =
|
|
9495
|
+
"the exact managed draft remains in the terminal composer";
|
|
9496
|
+
}
|
|
9497
|
+
}
|
|
9498
|
+
if (!notAcceptedReason) {
|
|
9499
|
+
return { outcome: "pending" };
|
|
9500
|
+
}
|
|
7883
9501
|
}
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
|
|
9502
|
+
const resolvedAt = new Date().toISOString();
|
|
9503
|
+
const writerStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
9504
|
+
return await withStoreWriterLeaseAsync(writerStoreDir, async () => {
|
|
9505
|
+
const releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
9506
|
+
try {
|
|
9507
|
+
const current = loadState(statePath);
|
|
9508
|
+
const currentSubmission = terminalBridgeSubmission(current);
|
|
9509
|
+
const currentTakeover = isRecord(current.native_session_takeover)
|
|
9510
|
+
? current.native_session_takeover
|
|
9511
|
+
: undefined;
|
|
9512
|
+
if (current.status !== conversation.status ||
|
|
9513
|
+
stringValue(currentTakeover?.terminal_bridge_message_id) !== messageId ||
|
|
9514
|
+
stringValue(currentSubmission?.message_id) !== messageId ||
|
|
9515
|
+
currentSubmission?.status !== "enter_dispatched") {
|
|
9516
|
+
throw new Error("terminal acceptance generation changed before monitor reconciliation");
|
|
9517
|
+
}
|
|
9518
|
+
const requestText = String(currentTakeover?.terminal_bridge_request_text ?? current.user_request ?? "");
|
|
9519
|
+
const nextBase = notAcceptedReason
|
|
9520
|
+
? {
|
|
9521
|
+
...current,
|
|
9522
|
+
status: "stalled",
|
|
9523
|
+
stalled_at: resolvedAt,
|
|
9524
|
+
stalled_reason: notAcceptedReason,
|
|
9525
|
+
updated_at: resolvedAt
|
|
9526
|
+
}
|
|
9527
|
+
: current;
|
|
9528
|
+
const acceptedConversation = withTerminalBridgeSubmission({
|
|
9529
|
+
conversation: nextBase,
|
|
9530
|
+
messageId,
|
|
9531
|
+
requestText,
|
|
9532
|
+
status: notAcceptedReason ? "not_accepted" : "agent_accepted",
|
|
9533
|
+
preparedAt: stringValue(currentSubmission.prepared_at) ?? resolvedAt,
|
|
9534
|
+
textInjectedAt: stringValue(currentSubmission.text_injected_at),
|
|
9535
|
+
enterDispatchedAt: stringValue(currentSubmission.enter_dispatched_at),
|
|
9536
|
+
...(notAcceptedReason
|
|
9537
|
+
? { notAcceptedAt: resolvedAt }
|
|
9538
|
+
: {
|
|
9539
|
+
agentAcceptedAt: resolvedAt,
|
|
9540
|
+
acceptanceEvidence: evidence
|
|
9541
|
+
})
|
|
9542
|
+
});
|
|
9543
|
+
const ledger = reconcilePreparedTerminalDispatchLedger(terminalControl, loadTerminalBridgeDispatchLedger(terminalControl));
|
|
9544
|
+
if (stringValue(ledger?.message_id) !== messageId ||
|
|
9545
|
+
ledger?.status !== "enter_dispatched") {
|
|
9546
|
+
throw new Error("terminal dispatch ledger changed before acceptance reconciliation");
|
|
9547
|
+
}
|
|
9548
|
+
saveState(statePath, acceptedConversation);
|
|
9549
|
+
try {
|
|
9550
|
+
if (process.env.AKK_TEST_MONITOR_FINAL_TERMINAL_LEDGER_FAILURE === "1") {
|
|
9551
|
+
throw new Error("injected monitor final terminal ledger persistence failure");
|
|
9552
|
+
}
|
|
9553
|
+
saveTerminalBridgeDispatchLedger(terminalControl, {
|
|
9554
|
+
...ledger,
|
|
9555
|
+
...terminalBindingLedgerFields(acceptedConversation),
|
|
9556
|
+
status: notAcceptedReason ? "not_accepted" : "agent_accepted",
|
|
9557
|
+
...(notAcceptedReason
|
|
9558
|
+
? { not_accepted_at: resolvedAt }
|
|
9559
|
+
: {
|
|
9560
|
+
agent_accepted_at: resolvedAt,
|
|
9561
|
+
acceptance_evidence: evidence
|
|
9562
|
+
}),
|
|
9563
|
+
dispatcher_pid: null
|
|
9564
|
+
});
|
|
9565
|
+
}
|
|
9566
|
+
catch (error) {
|
|
9567
|
+
runtimeLog("warn", "terminal_acceptance_monitor_ledger_lagging", {
|
|
9568
|
+
conversation_id: acceptedConversation.conversation_id,
|
|
9569
|
+
message_id: messageId,
|
|
9570
|
+
terminal_target: terminalControl.target,
|
|
9571
|
+
durable_submission_status: notAcceptedReason
|
|
9572
|
+
? "not_accepted"
|
|
9573
|
+
: "agent_accepted",
|
|
9574
|
+
error: error instanceof Error ? error.message : String(error)
|
|
9575
|
+
});
|
|
9576
|
+
}
|
|
9577
|
+
try {
|
|
9578
|
+
if (process.env.AKK_TEST_MONITOR_FINAL_EVENT_FAILURE === "1") {
|
|
9579
|
+
throw new Error("injected monitor final acceptance event persistence failure");
|
|
9580
|
+
}
|
|
9581
|
+
appendEvent(logPath, {
|
|
9582
|
+
ts: resolvedAt,
|
|
9583
|
+
conversation_id: acceptedConversation.conversation_id,
|
|
9584
|
+
event: notAcceptedReason
|
|
9585
|
+
? "terminal_message_not_accepted"
|
|
9586
|
+
: "terminal_message_agent_accepted",
|
|
9587
|
+
message_id: messageId,
|
|
9588
|
+
terminal_control: terminalControl,
|
|
9589
|
+
do_not_retry: Boolean(notAcceptedReason)
|
|
9590
|
+
});
|
|
9591
|
+
}
|
|
9592
|
+
catch (error) {
|
|
9593
|
+
runtimeLog("warn", "terminal_acceptance_monitor_event_lagging", {
|
|
9594
|
+
conversation_id: acceptedConversation.conversation_id,
|
|
9595
|
+
message_id: messageId,
|
|
9596
|
+
terminal_target: terminalControl.target,
|
|
9597
|
+
durable_submission_status: notAcceptedReason
|
|
9598
|
+
? "not_accepted"
|
|
9599
|
+
: "agent_accepted",
|
|
9600
|
+
error: error instanceof Error ? error.message : String(error)
|
|
9601
|
+
});
|
|
9602
|
+
}
|
|
9603
|
+
return notAcceptedReason
|
|
9604
|
+
? { outcome: "not_accepted", conversation: acceptedConversation }
|
|
9605
|
+
: { outcome: "accepted", conversation: acceptedConversation };
|
|
9606
|
+
}
|
|
9607
|
+
finally {
|
|
9608
|
+
releaseStateLock();
|
|
9609
|
+
}
|
|
9610
|
+
});
|
|
9611
|
+
}
|
|
9612
|
+
function markTerminalAcceptanceUncertain({ conversation, statePath, logPath, terminalControl, reason }) {
|
|
9613
|
+
const messageId = stringValue(terminalBridgeSubmission(conversation)?.message_id);
|
|
9614
|
+
const uncertainAt = new Date().toISOString();
|
|
9615
|
+
const writerStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
9616
|
+
return withStoreWriterLease(writerStoreDir, () => {
|
|
9617
|
+
const releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
7893
9618
|
try {
|
|
9619
|
+
const current = loadState(statePath);
|
|
9620
|
+
const submission = terminalBridgeSubmission(current);
|
|
9621
|
+
const currentMessageId = stringValue(submission?.message_id);
|
|
9622
|
+
if (!messageId ||
|
|
9623
|
+
currentMessageId !== messageId ||
|
|
9624
|
+
!["text_injected", "enter_dispatched"].includes(String(submission?.status))) {
|
|
9625
|
+
return current;
|
|
9626
|
+
}
|
|
9627
|
+
const takeover = isRecord(current.native_session_takeover)
|
|
9628
|
+
? current.native_session_takeover
|
|
9629
|
+
: undefined;
|
|
9630
|
+
const requestText = String(takeover?.terminal_bridge_request_text ?? current.user_request ?? "");
|
|
9631
|
+
const uncertain = withTerminalBridgeSubmission({
|
|
9632
|
+
conversation: {
|
|
9633
|
+
...current,
|
|
9634
|
+
status: "stalled",
|
|
9635
|
+
stalled_at: uncertainAt,
|
|
9636
|
+
stalled_reason: reason,
|
|
9637
|
+
updated_at: uncertainAt
|
|
9638
|
+
},
|
|
9639
|
+
messageId,
|
|
9640
|
+
requestText,
|
|
9641
|
+
status: "uncertain",
|
|
9642
|
+
preparedAt: stringValue(submission?.prepared_at) ?? uncertainAt,
|
|
9643
|
+
textInjectedAt: stringValue(submission?.text_injected_at),
|
|
9644
|
+
enterDispatchedAt: stringValue(submission?.enter_dispatched_at),
|
|
9645
|
+
uncertainAt,
|
|
9646
|
+
error: reason,
|
|
9647
|
+
lastProvenStage: submission?.status === "enter_dispatched"
|
|
9648
|
+
? "enter_dispatched"
|
|
9649
|
+
: "text_injected"
|
|
9650
|
+
});
|
|
9651
|
+
const ledger = loadTerminalBridgeDispatchLedger(terminalControl);
|
|
9652
|
+
if (stringValue(ledger?.message_id) === messageId &&
|
|
9653
|
+
["text_injected", "enter_dispatched"].includes(String(ledger?.status))) {
|
|
9654
|
+
saveTerminalBridgeDispatchLedger(terminalControl, {
|
|
9655
|
+
...ledger,
|
|
9656
|
+
status: "uncertain",
|
|
9657
|
+
uncertain_at: uncertainAt,
|
|
9658
|
+
error: textSummary(reason),
|
|
9659
|
+
dispatcher_pid: null
|
|
9660
|
+
});
|
|
9661
|
+
}
|
|
9662
|
+
saveState(statePath, uncertain);
|
|
7894
9663
|
appendEvent(logPath, {
|
|
7895
|
-
ts:
|
|
7896
|
-
conversation_id:
|
|
7897
|
-
event: "
|
|
9664
|
+
ts: uncertainAt,
|
|
9665
|
+
conversation_id: uncertain.conversation_id,
|
|
9666
|
+
event: "terminal_message_acceptance_uncertain",
|
|
9667
|
+
message_id: messageId,
|
|
7898
9668
|
terminal_control: terminalControl,
|
|
7899
|
-
error: textSummary(
|
|
7900
|
-
|
|
9669
|
+
error: textSummary(reason),
|
|
9670
|
+
do_not_retry: true
|
|
7901
9671
|
});
|
|
9672
|
+
return uncertain;
|
|
7902
9673
|
}
|
|
7903
|
-
|
|
7904
|
-
|
|
9674
|
+
finally {
|
|
9675
|
+
releaseStateLock();
|
|
7905
9676
|
}
|
|
7906
|
-
}
|
|
7907
|
-
printJson({
|
|
7908
|
-
session_id: sessionIdForConversation(deliveredConversation),
|
|
7909
|
-
turn_id: turnIdForConversation(deliveredConversation),
|
|
7910
|
-
conversation: deliveredConversation,
|
|
7911
|
-
message,
|
|
7912
|
-
delivered: true,
|
|
7913
|
-
status: "async_pending",
|
|
7914
|
-
background: true,
|
|
7915
|
-
callback_expected: Boolean(deliveredConversation.gateway_method),
|
|
7916
|
-
terminal_control: terminalControl,
|
|
7917
|
-
monitor_pid: bridgeMonitor?.pid ?? null,
|
|
7918
|
-
executor,
|
|
7919
|
-
budget: budgetAction(deliveredConversation),
|
|
7920
|
-
delivery_receipt: "submitted",
|
|
7921
|
-
...(bookkeepingWarning
|
|
7922
|
-
? {
|
|
7923
|
-
bookkeeping_warning: textSummary(bookkeepingWarning)
|
|
7924
|
-
}
|
|
7925
|
-
: {}),
|
|
7926
|
-
openclaw_next_action: openClawYieldNextAction({
|
|
7927
|
-
conversationId: deliveredConversation.conversation_id,
|
|
7928
|
-
sessionId: sessionIdForConversation(deliveredConversation),
|
|
7929
|
-
turnId: turnIdForConversation(deliveredConversation),
|
|
7930
|
-
source: "terminal_control",
|
|
7931
|
-
callbackExpected: Boolean(deliveredConversation.gateway_method)
|
|
7932
|
-
})
|
|
7933
9677
|
});
|
|
7934
9678
|
}
|
|
7935
|
-
function terminalSubmissionPayload(payload) {
|
|
7936
|
-
assertOrdinaryTerminalPayloadDoesNotInvokeNativeLifecycle(payload);
|
|
7937
|
-
return payload.trimEnd();
|
|
7938
|
-
}
|
|
7939
9679
|
function assertOrdinaryTerminalPayloadDoesNotInvokeNativeLifecycle(payload) {
|
|
7940
9680
|
const firstEffectiveLine = String(payload)
|
|
7941
9681
|
.split(/\r\n?|\n/u)
|
|
@@ -8221,7 +9961,7 @@ function assertTurnBindingCurrent(conversation, operation) {
|
|
|
8221
9961
|
turnNativeThreadId
|
|
8222
9962
|
}));
|
|
8223
9963
|
if (!exactModernBinding && !compatibleMigratedBinding) {
|
|
8224
|
-
throw new
|
|
9964
|
+
throw new TurnBindingSupersededError(`cannot ${operation} Turn ${turnIdForConversation(conversation)}: its ` +
|
|
8225
9965
|
`Session binding generation is no longer current`);
|
|
8226
9966
|
}
|
|
8227
9967
|
}
|
|
@@ -8608,6 +10348,7 @@ function createManagedTerminalTurn({ options, conversationId, agent, pid, messag
|
|
|
8608
10348
|
}, paths);
|
|
8609
10349
|
const message = createMessage({
|
|
8610
10350
|
conversation: attachedConversation,
|
|
10351
|
+
id: stringValue(options.messageId),
|
|
8611
10352
|
from: "openclaw",
|
|
8612
10353
|
to: executor.actor,
|
|
8613
10354
|
type: options.type ?? "task",
|
|
@@ -8789,9 +10530,11 @@ async function runRenew(options) {
|
|
|
8789
10530
|
});
|
|
8790
10531
|
}
|
|
8791
10532
|
async function runReconcileMonitors(options) {
|
|
10533
|
+
const reason = stringValue(options.reason) ?? "startup_reconciliation";
|
|
8792
10534
|
printJson(await reconcileMonitors(options, {
|
|
8793
|
-
includeCallbackRecovery: true
|
|
8794
|
-
|
|
10535
|
+
includeCallbackRecovery: options.terminalMonitorsOnly !== true &&
|
|
10536
|
+
reason !== "monitor_supervision",
|
|
10537
|
+
reason,
|
|
8795
10538
|
conversationId: undefined
|
|
8796
10539
|
}));
|
|
8797
10540
|
}
|
|
@@ -8877,6 +10620,9 @@ async function reconcileMonitors(options, { includeCallbackRecovery, reason, con
|
|
|
8877
10620
|
});
|
|
8878
10621
|
continue;
|
|
8879
10622
|
}
|
|
10623
|
+
const previousMonitorPid = latestTerminalBridgeMonitorLaunchPid(logPath);
|
|
10624
|
+
const unexpectedMonitorExit = previousMonitorPid !== undefined &&
|
|
10625
|
+
!isProcessAlive(previousMonitorPid);
|
|
8880
10626
|
const activeOwner = activeTerminalBridgeMonitorOwner(statePath, initialEligibility.terminalMessageId);
|
|
8881
10627
|
if (activeOwner) {
|
|
8882
10628
|
alreadyRunning += 1;
|
|
@@ -8961,13 +10707,33 @@ async function reconcileMonitors(options, { includeCallbackRecovery, reason, con
|
|
|
8961
10707
|
continue;
|
|
8962
10708
|
}
|
|
8963
10709
|
const launchedAt = new Date().toISOString();
|
|
10710
|
+
const launchReason = unexpectedMonitorExit
|
|
10711
|
+
? "unexpected_exit_recovery"
|
|
10712
|
+
: reason;
|
|
10713
|
+
if (unexpectedMonitorExit) {
|
|
10714
|
+
appendEvent(logPath, {
|
|
10715
|
+
ts: launchedAt,
|
|
10716
|
+
conversation_id: prepared.conversation.conversation_id,
|
|
10717
|
+
event: "terminal_bridge_monitor_exit_observed",
|
|
10718
|
+
previous_monitor_pid: previousMonitorPid,
|
|
10719
|
+
terminal_control: prepared.terminalControl,
|
|
10720
|
+
reason: "monitor_owner_process_missing",
|
|
10721
|
+
observed_by: reason
|
|
10722
|
+
});
|
|
10723
|
+
runtimeLog("warn", "terminal_bridge_monitor_exit_observed", {
|
|
10724
|
+
conversation_id: prepared.conversation.conversation_id,
|
|
10725
|
+
previous_monitor_pid: previousMonitorPid,
|
|
10726
|
+
terminal_target: prepared.terminalControl.target,
|
|
10727
|
+
observed_by: reason
|
|
10728
|
+
});
|
|
10729
|
+
}
|
|
8964
10730
|
appendEvent(logPath, {
|
|
8965
10731
|
ts: launchedAt,
|
|
8966
10732
|
conversation_id: prepared.conversation.conversation_id,
|
|
8967
10733
|
event: "terminal_bridge_monitor_launch",
|
|
8968
10734
|
pid: monitor.pid ?? null,
|
|
8969
10735
|
terminal_control: prepared.terminalControl,
|
|
8970
|
-
reason,
|
|
10736
|
+
reason: launchReason,
|
|
8971
10737
|
agent_timeout_minutes: prepared.inactivityTimeoutMinutes,
|
|
8972
10738
|
agent_hard_timeout_minutes: prepared.hardTimeoutMinutes
|
|
8973
10739
|
});
|
|
@@ -8980,11 +10746,23 @@ async function reconcileMonitors(options, { includeCallbackRecovery, reason, con
|
|
|
8980
10746
|
items.push({
|
|
8981
10747
|
conversation_id: prepared.conversation.conversation_id,
|
|
8982
10748
|
status: "launched",
|
|
8983
|
-
reason,
|
|
8984
|
-
monitor_pid: monitor.pid ?? null
|
|
10749
|
+
reason: launchReason,
|
|
10750
|
+
monitor_pid: monitor.pid ?? null,
|
|
10751
|
+
...(unexpectedMonitorExit
|
|
10752
|
+
? { previous_monitor_pid: previousMonitorPid }
|
|
10753
|
+
: {})
|
|
8985
10754
|
});
|
|
8986
10755
|
}
|
|
8987
10756
|
catch (error) {
|
|
10757
|
+
if (error instanceof TurnBindingSupersededError) {
|
|
10758
|
+
skipped += 1;
|
|
10759
|
+
items.push({
|
|
10760
|
+
conversation_id: listedConversation.conversation_id,
|
|
10761
|
+
status: "skipped",
|
|
10762
|
+
reason: "session_binding_superseded"
|
|
10763
|
+
});
|
|
10764
|
+
continue;
|
|
10765
|
+
}
|
|
8988
10766
|
errors += 1;
|
|
8989
10767
|
items.push({
|
|
8990
10768
|
conversation_id: listedConversation.conversation_id,
|
|
@@ -9153,15 +10931,22 @@ function terminalBridgeReconciliationEligibility(conversation) {
|
|
|
9153
10931
|
const dispatchLedger = loadTerminalBridgeDispatchLedger(terminalControl);
|
|
9154
10932
|
if (dispatchLedger &&
|
|
9155
10933
|
(stringValue(dispatchLedger.message_id) !== terminalMessageId ||
|
|
9156
|
-
![
|
|
10934
|
+
![
|
|
10935
|
+
"prepared",
|
|
10936
|
+
"text_injected",
|
|
10937
|
+
"enter_dispatched",
|
|
10938
|
+
"agent_accepted",
|
|
10939
|
+
"submitted"
|
|
10940
|
+
].includes(String(dispatchLedger.status)))) {
|
|
9157
10941
|
return {
|
|
9158
10942
|
eligible: false,
|
|
9159
10943
|
reason: `terminal_dispatch_${String(dispatchLedger.status ?? "generation_replaced")}`
|
|
9160
10944
|
};
|
|
9161
10945
|
}
|
|
9162
10946
|
const submission = terminalBridgeSubmission(conversation);
|
|
9163
|
-
if (
|
|
9164
|
-
(submission
|
|
10947
|
+
if (submission &&
|
|
10948
|
+
stringValue(submission.message_id) === terminalMessageId &&
|
|
10949
|
+
["not_accepted", "uncertain", "aborted"].includes(String(submission.status))) {
|
|
9165
10950
|
return {
|
|
9166
10951
|
eligible: false,
|
|
9167
10952
|
reason: `terminal_submission_${submission.status}`
|
|
@@ -9207,8 +10992,11 @@ function latestTerminalBridgeMonitorLaunchPid(logPath) {
|
|
|
9207
10992
|
catch {
|
|
9208
10993
|
return undefined;
|
|
9209
10994
|
}
|
|
9210
|
-
const
|
|
9211
|
-
|
|
10995
|
+
const ownership = [...events].reverse().find((event) => event.event === "terminal_bridge_monitor_launch" ||
|
|
10996
|
+
event.event === "terminal_bridge_monitor_started");
|
|
10997
|
+
const pid = Number(ownership?.event === "terminal_bridge_monitor_started"
|
|
10998
|
+
? ownership.monitor_pid
|
|
10999
|
+
: ownership?.pid);
|
|
9212
11000
|
return Number.isSafeInteger(pid) && pid > 1 ? pid : undefined;
|
|
9213
11001
|
}
|
|
9214
11002
|
function prepareTerminalBridgeMonitorReconciliation({ statePath, expectedMessageId, requireWaitingForAgentStatus = false }) {
|
|
@@ -9544,8 +11332,12 @@ async function runTerminalDispatchClose({ options, terminalConversation }) {
|
|
|
9544
11332
|
}
|
|
9545
11333
|
if (![
|
|
9546
11334
|
"prepared",
|
|
11335
|
+
"text_injected",
|
|
11336
|
+
"enter_dispatched",
|
|
9547
11337
|
"dispatching",
|
|
11338
|
+
"agent_accepted",
|
|
9548
11339
|
"submitted",
|
|
11340
|
+
"not_accepted",
|
|
9549
11341
|
"uncertain",
|
|
9550
11342
|
"verified"
|
|
9551
11343
|
].includes(ledger.status)) {
|
|
@@ -9927,6 +11719,7 @@ function runTerminalBridgeMonitorHandoff(options) {
|
|
|
9927
11719
|
}
|
|
9928
11720
|
async function runTerminalBridgeMonitor(options) {
|
|
9929
11721
|
const statePath = expandHome(required(options.state, "--state is required"));
|
|
11722
|
+
const logPath = expandHome(options.log ?? logPathForStatePath(statePath));
|
|
9930
11723
|
const conversation = loadState(statePath);
|
|
9931
11724
|
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
9932
11725
|
? conversation.native_session_takeover
|
|
@@ -9949,14 +11742,81 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
9949
11742
|
});
|
|
9950
11743
|
return;
|
|
9951
11744
|
}
|
|
11745
|
+
const lifecycle = { startedRecorded: false };
|
|
11746
|
+
let storeDeferredAttempts = 0;
|
|
11747
|
+
let storeFirstDeferredAt;
|
|
9952
11748
|
try {
|
|
9953
|
-
|
|
11749
|
+
while (true) {
|
|
11750
|
+
try {
|
|
11751
|
+
if (storeDeferredAttempts > 0) {
|
|
11752
|
+
const resumedAt = new Date().toISOString();
|
|
11753
|
+
const resumedConversation = loadState(statePath);
|
|
11754
|
+
const resumedTakeover = isRecord(resumedConversation.native_session_takeover)
|
|
11755
|
+
? resumedConversation.native_session_takeover
|
|
11756
|
+
: undefined;
|
|
11757
|
+
if (stringValue(resumedTakeover?.terminal_bridge_message_id) !==
|
|
11758
|
+
terminalMessageId) {
|
|
11759
|
+
runtimeLog("info", "terminal_bridge_monitor_finished", {
|
|
11760
|
+
conversation_id: resumedConversation.conversation_id,
|
|
11761
|
+
terminal_bridge_message_id: terminalMessageId,
|
|
11762
|
+
reason: "terminal_bridge_generation_replaced_during_store_deferral"
|
|
11763
|
+
});
|
|
11764
|
+
printJson({
|
|
11765
|
+
conversation: resumedConversation,
|
|
11766
|
+
monitored: true,
|
|
11767
|
+
terminal_bridge: true,
|
|
11768
|
+
completed: false,
|
|
11769
|
+
reason: "terminal_bridge_generation_replaced"
|
|
11770
|
+
});
|
|
11771
|
+
return;
|
|
11772
|
+
}
|
|
11773
|
+
appendEvent(logPath, {
|
|
11774
|
+
ts: resumedAt,
|
|
11775
|
+
conversation_id: resumedConversation.conversation_id,
|
|
11776
|
+
event: "terminal_bridge_monitor_store_operation_deferred",
|
|
11777
|
+
terminal_bridge_message_id: terminalMessageId,
|
|
11778
|
+
error_code: "AKK_STORE_LOCK_TIMEOUT",
|
|
11779
|
+
first_deferred_at: storeFirstDeferredAt,
|
|
11780
|
+
resumed_at: resumedAt,
|
|
11781
|
+
attempts: storeDeferredAttempts,
|
|
11782
|
+
outcome: "resumed"
|
|
11783
|
+
});
|
|
11784
|
+
runtimeLog("info", "terminal_bridge_monitor_store_operation_resumed", {
|
|
11785
|
+
conversation_id: resumedConversation.conversation_id,
|
|
11786
|
+
terminal_bridge_message_id: terminalMessageId,
|
|
11787
|
+
attempts: storeDeferredAttempts,
|
|
11788
|
+
first_deferred_at: storeFirstDeferredAt
|
|
11789
|
+
});
|
|
11790
|
+
storeDeferredAttempts = 0;
|
|
11791
|
+
storeFirstDeferredAt = undefined;
|
|
11792
|
+
}
|
|
11793
|
+
await runTerminalBridgeMonitorWithLock(options, lifecycle, terminalMessageId);
|
|
11794
|
+
return;
|
|
11795
|
+
}
|
|
11796
|
+
catch (error) {
|
|
11797
|
+
if (!(error instanceof StoreLockTimeoutError)) {
|
|
11798
|
+
throw error;
|
|
11799
|
+
}
|
|
11800
|
+
storeDeferredAttempts += 1;
|
|
11801
|
+
storeFirstDeferredAt ??= new Date().toISOString();
|
|
11802
|
+
const retryInMs = Math.min(5_000, 250 * (2 ** Math.min(5, storeDeferredAttempts - 1)));
|
|
11803
|
+
runtimeLog("warn", "terminal_bridge_monitor_store_operation_deferred", {
|
|
11804
|
+
conversation_id: conversation.conversation_id,
|
|
11805
|
+
terminal_bridge_message_id: terminalMessageId,
|
|
11806
|
+
error_code: error.code,
|
|
11807
|
+
lock_kind: error.lockKind,
|
|
11808
|
+
attempt: storeDeferredAttempts,
|
|
11809
|
+
retry_in_ms: retryInMs
|
|
11810
|
+
});
|
|
11811
|
+
sleepSync(retryInMs);
|
|
11812
|
+
}
|
|
11813
|
+
}
|
|
9954
11814
|
}
|
|
9955
11815
|
finally {
|
|
9956
11816
|
monitorLock.release();
|
|
9957
11817
|
}
|
|
9958
11818
|
}
|
|
9959
|
-
async function runTerminalBridgeMonitorWithLock(options) {
|
|
11819
|
+
async function runTerminalBridgeMonitorWithLock(options, lifecycle, expectedTerminalMessageId) {
|
|
9960
11820
|
const statePath = expandHome(required(options.state, "--state is required"));
|
|
9961
11821
|
const logPath = expandHome(options.log ?? logPathForStatePath(statePath));
|
|
9962
11822
|
const pollIntervalMs = Math.max(50, Number(options.pollIntervalMs ?? DEFAULT_MONITOR_POLL_INTERVAL_MS));
|
|
@@ -9969,6 +11829,22 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
9969
11829
|
const initialNativeTakeover = isRecord(conversation.native_session_takeover)
|
|
9970
11830
|
? conversation.native_session_takeover
|
|
9971
11831
|
: undefined;
|
|
11832
|
+
if (stringValue(initialNativeTakeover?.terminal_bridge_message_id) !==
|
|
11833
|
+
expectedTerminalMessageId) {
|
|
11834
|
+
runtimeLog("info", "terminal_bridge_monitor_finished", {
|
|
11835
|
+
conversation_id: conversation.conversation_id,
|
|
11836
|
+
terminal_bridge_message_id: expectedTerminalMessageId,
|
|
11837
|
+
reason: "terminal_bridge_generation_replaced_before_monitor_restart"
|
|
11838
|
+
});
|
|
11839
|
+
printJson({
|
|
11840
|
+
conversation,
|
|
11841
|
+
monitored: true,
|
|
11842
|
+
terminal_bridge: true,
|
|
11843
|
+
completed: false,
|
|
11844
|
+
reason: "terminal_bridge_generation_replaced"
|
|
11845
|
+
});
|
|
11846
|
+
return;
|
|
11847
|
+
}
|
|
9972
11848
|
const timeoutMinutes = Number(options.agentTimeoutMinutes ??
|
|
9973
11849
|
initialNativeTakeover?.["terminal_bridge_inactivity_timeout_minutes"] ??
|
|
9974
11850
|
DEFAULT_AGENT_TIMEOUT_MINUTES);
|
|
@@ -9976,7 +11852,7 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
9976
11852
|
initialNativeTakeover?.["terminal_bridge_hard_timeout_minutes"] ??
|
|
9977
11853
|
DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
|
|
9978
11854
|
const monitorStartedAtMs = Date.now();
|
|
9979
|
-
const monitorMessageId =
|
|
11855
|
+
const monitorMessageId = expectedTerminalMessageId;
|
|
9980
11856
|
const taskStartedAtMs = validTimestampMs(initialNativeTakeover?.["terminal_bridge_started_at"]) ?? monitorStartedAtMs;
|
|
9981
11857
|
let lastActivityAtMs = validTimestampMs(initialNativeTakeover?.["terminal_bridge_last_activity_at"]) ?? taskStartedAtMs;
|
|
9982
11858
|
let lastPersistedActivityAtMs = lastActivityAtMs;
|
|
@@ -9985,33 +11861,45 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
9985
11861
|
let previousScreenFingerprint = preSendScreenFingerprint;
|
|
9986
11862
|
let previousDurableFingerprint;
|
|
9987
11863
|
let persistedActivityReason = stringValue(initialNativeTakeover?.["terminal_bridge_last_activity_reason"]);
|
|
11864
|
+
const initialDetectorDiagnostic = isRecord(initialNativeTakeover?.["terminal_bridge_detector_diagnostic"])
|
|
11865
|
+
? initialNativeTakeover.terminal_bridge_detector_diagnostic
|
|
11866
|
+
: undefined;
|
|
11867
|
+
let persistedDetectorDiagnosticFingerprint = stringValue(initialDetectorDiagnostic?.fingerprint);
|
|
11868
|
+
let persistedDetectorDiagnosticStatus = stringValue(initialDetectorDiagnostic?.status);
|
|
9988
11869
|
const executor = executorForConversation(conversation);
|
|
9989
11870
|
const terminalBridge = createTerminalAgentBridge(options);
|
|
9990
|
-
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
|
|
9995
|
-
|
|
9996
|
-
|
|
9997
|
-
|
|
9998
|
-
|
|
9999
|
-
|
|
10000
|
-
|
|
10001
|
-
|
|
10002
|
-
:
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
:
|
|
10006
|
-
|
|
10007
|
-
|
|
10008
|
-
|
|
10009
|
-
|
|
10010
|
-
|
|
10011
|
-
|
|
10012
|
-
|
|
10013
|
-
|
|
11871
|
+
if (!lifecycle.startedRecorded) {
|
|
11872
|
+
appendEvent(logPath, {
|
|
11873
|
+
ts: new Date().toISOString(),
|
|
11874
|
+
conversation_id: conversation.conversation_id,
|
|
11875
|
+
event: "terminal_bridge_monitor_started",
|
|
11876
|
+
monitor_pid: process.pid,
|
|
11877
|
+
executor,
|
|
11878
|
+
agent_timeout_minutes: timeoutMinutes,
|
|
11879
|
+
agent_hard_timeout_minutes: hardTimeoutMinutes,
|
|
11880
|
+
poll_interval_ms: pollIntervalMs,
|
|
11881
|
+
task_started_at: new Date(taskStartedAtMs).toISOString(),
|
|
11882
|
+
last_activity_at: new Date(lastActivityAtMs).toISOString(),
|
|
11883
|
+
inactivity_deadline_at: timeoutMinutes > 0
|
|
11884
|
+
? new Date(lastActivityAtMs + timeoutMinutes * 60 * 1000).toISOString()
|
|
11885
|
+
: null,
|
|
11886
|
+
hard_deadline_at: hardTimeoutMinutes > 0
|
|
11887
|
+
? new Date(taskStartedAtMs + hardTimeoutMinutes * 60 * 1000).toISOString()
|
|
11888
|
+
: null
|
|
11889
|
+
});
|
|
11890
|
+
runtimeLog("info", "terminal_bridge_monitor_started", {
|
|
11891
|
+
conversation_id: conversation.conversation_id,
|
|
11892
|
+
monitor_pid: process.pid,
|
|
11893
|
+
agent: executor.kind,
|
|
11894
|
+
executor_session: executor.session,
|
|
11895
|
+
agent_timeout_minutes: timeoutMinutes,
|
|
11896
|
+
agent_hard_timeout_minutes: hardTimeoutMinutes
|
|
11897
|
+
});
|
|
11898
|
+
lifecycle.startedRecorded = true;
|
|
11899
|
+
}
|
|
10014
11900
|
let idleCompletionFingerprint;
|
|
11901
|
+
let bindingCheckDeferredAttempts = 0;
|
|
11902
|
+
let bindingCheckFirstDeferredAt;
|
|
10015
11903
|
while (true) {
|
|
10016
11904
|
conversation = loadState(statePath);
|
|
10017
11905
|
if (!isWaitingForAgent(conversation.status)) {
|
|
@@ -10074,6 +11962,91 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
10074
11962
|
submission &&
|
|
10075
11963
|
stringValue(submission.message_id) === currentMessageId) {
|
|
10076
11964
|
const submissionStatus = stringValue(submission.status);
|
|
11965
|
+
if (submissionStatus === "text_injected" ||
|
|
11966
|
+
submissionStatus === "enter_dispatched") {
|
|
11967
|
+
const dispatcherPid = Number(submission.dispatcher_pid);
|
|
11968
|
+
if (Number.isSafeInteger(dispatcherPid) &&
|
|
11969
|
+
dispatcherPid > 1 &&
|
|
11970
|
+
isProcessAlive(dispatcherPid)) {
|
|
11971
|
+
sleepSync(pollIntervalMs);
|
|
11972
|
+
continue;
|
|
11973
|
+
}
|
|
11974
|
+
if (submissionStatus === "text_injected") {
|
|
11975
|
+
conversation = markTerminalAcceptanceUncertain({
|
|
11976
|
+
conversation,
|
|
11977
|
+
statePath,
|
|
11978
|
+
logPath,
|
|
11979
|
+
terminalControl,
|
|
11980
|
+
reason: "terminal dispatcher exited after text injection but before Enter dispatch was durably proven"
|
|
11981
|
+
});
|
|
11982
|
+
printJson({
|
|
11983
|
+
conversation,
|
|
11984
|
+
monitored: true,
|
|
11985
|
+
terminal_bridge: true,
|
|
11986
|
+
completed: false,
|
|
11987
|
+
submission_outcome: "uncertain",
|
|
11988
|
+
delivery_receipt: "text_injected",
|
|
11989
|
+
do_not_retry: true,
|
|
11990
|
+
reason: "text was injected but native submission was not proven; inspect the composer"
|
|
11991
|
+
});
|
|
11992
|
+
return;
|
|
11993
|
+
}
|
|
11994
|
+
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
|
|
11995
|
+
try {
|
|
11996
|
+
const reconciliation = await reconcileTerminalAcceptanceInMonitor({
|
|
11997
|
+
options,
|
|
11998
|
+
conversation,
|
|
11999
|
+
statePath,
|
|
12000
|
+
logPath,
|
|
12001
|
+
terminalControl,
|
|
12002
|
+
executor,
|
|
12003
|
+
terminalBridge
|
|
12004
|
+
});
|
|
12005
|
+
if (reconciliation.outcome === "accepted") {
|
|
12006
|
+
conversation = reconciliation.conversation;
|
|
12007
|
+
continue;
|
|
12008
|
+
}
|
|
12009
|
+
if (reconciliation.outcome === "not_accepted") {
|
|
12010
|
+
printJson({
|
|
12011
|
+
conversation: reconciliation.conversation,
|
|
12012
|
+
monitored: true,
|
|
12013
|
+
terminal_bridge: true,
|
|
12014
|
+
completed: false,
|
|
12015
|
+
submission_outcome: "not_accepted",
|
|
12016
|
+
delivery_receipt: "enter_dispatched",
|
|
12017
|
+
do_not_retry: true,
|
|
12018
|
+
reason: "the exact managed draft remains in the terminal composer"
|
|
12019
|
+
});
|
|
12020
|
+
return;
|
|
12021
|
+
}
|
|
12022
|
+
}
|
|
12023
|
+
catch (error) {
|
|
12024
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
12025
|
+
conversation = markTerminalAcceptanceUncertain({
|
|
12026
|
+
conversation,
|
|
12027
|
+
statePath,
|
|
12028
|
+
logPath,
|
|
12029
|
+
terminalControl,
|
|
12030
|
+
reason
|
|
12031
|
+
});
|
|
12032
|
+
printJson({
|
|
12033
|
+
conversation,
|
|
12034
|
+
monitored: true,
|
|
12035
|
+
terminal_bridge: true,
|
|
12036
|
+
completed: false,
|
|
12037
|
+
submission_outcome: "uncertain",
|
|
12038
|
+
delivery_receipt: "enter_dispatched",
|
|
12039
|
+
do_not_retry: true,
|
|
12040
|
+
reason
|
|
12041
|
+
});
|
|
12042
|
+
return;
|
|
12043
|
+
}
|
|
12044
|
+
finally {
|
|
12045
|
+
releaseTerminalLock();
|
|
12046
|
+
}
|
|
12047
|
+
sleepSync(pollIntervalMs);
|
|
12048
|
+
continue;
|
|
12049
|
+
}
|
|
10077
12050
|
if (submissionStatus === "prepared") {
|
|
10078
12051
|
const dispatcherPid = Number(submission.dispatcher_pid);
|
|
10079
12052
|
if (Number.isSafeInteger(dispatcherPid) &&
|
|
@@ -10084,115 +12057,128 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
10084
12057
|
}
|
|
10085
12058
|
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
|
|
10086
12059
|
try {
|
|
10087
|
-
const
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
const
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
stringValue(
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10103
|
-
|
|
10104
|
-
|
|
10105
|
-
|
|
10106
|
-
const submittedConversation = withTerminalBridgeSubmission({
|
|
10107
|
-
conversation: current,
|
|
10108
|
-
messageId: currentMessageId,
|
|
10109
|
-
requestText,
|
|
10110
|
-
status: "submitted",
|
|
10111
|
-
preparedAt: stringValue(currentSubmission.prepared_at) ??
|
|
10112
|
-
submittedAt,
|
|
10113
|
-
submittedAt
|
|
10114
|
-
});
|
|
10115
|
-
saveState(statePath, submittedConversation);
|
|
10116
|
-
conversation = submittedConversation;
|
|
10117
|
-
}
|
|
10118
|
-
else {
|
|
10119
|
-
const uncertainAt = new Date().toISOString();
|
|
10120
|
-
const uncertainConversation = withTerminalBridgeSubmission({
|
|
10121
|
-
conversation: {
|
|
10122
|
-
...current,
|
|
10123
|
-
status: "stalled",
|
|
10124
|
-
stalled_at: uncertainAt,
|
|
10125
|
-
stalled_reason: "terminal dispatcher exited before AKK could prove the tmux submission",
|
|
10126
|
-
updated_at: uncertainAt
|
|
10127
|
-
},
|
|
10128
|
-
messageId: currentMessageId,
|
|
10129
|
-
requestText,
|
|
10130
|
-
status: "uncertain",
|
|
10131
|
-
preparedAt: stringValue(currentSubmission.prepared_at) ??
|
|
10132
|
-
uncertainAt,
|
|
10133
|
-
uncertainAt,
|
|
10134
|
-
error: "the terminal dispatcher exited before AKK could persist a submitted receipt"
|
|
10135
|
-
});
|
|
10136
|
-
if (!dispatchLedger ||
|
|
12060
|
+
const writerStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
12061
|
+
await withStoreWriterLeaseAsync(writerStoreDir, async () => {
|
|
12062
|
+
const dispatchLedger = loadTerminalBridgeDispatchLedger(terminalControl);
|
|
12063
|
+
const releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
12064
|
+
try {
|
|
12065
|
+
const current = loadState(statePath);
|
|
12066
|
+
const currentSubmission = terminalBridgeSubmission(current);
|
|
12067
|
+
const currentTakeover = isRecord(current.native_session_takeover)
|
|
12068
|
+
? current.native_session_takeover
|
|
12069
|
+
: undefined;
|
|
12070
|
+
const expectedMessageId = stringValue(currentTakeover?.terminal_bridge_message_id);
|
|
12071
|
+
if (expectedMessageId === currentMessageId &&
|
|
12072
|
+
stringValue(currentSubmission?.message_id) === currentMessageId &&
|
|
12073
|
+
currentSubmission?.status === "prepared") {
|
|
12074
|
+
const requestText = String(currentTakeover?.terminal_bridge_request_text ??
|
|
12075
|
+
current.user_request ??
|
|
12076
|
+
"");
|
|
12077
|
+
if (dispatchLedger &&
|
|
12078
|
+
["submitted", "agent_accepted"].includes(String(dispatchLedger.status)) &&
|
|
10137
12079
|
stringValue(dispatchLedger.message_id) === currentMessageId) {
|
|
10138
|
-
|
|
10139
|
-
|
|
12080
|
+
const submittedAt = stringValue(dispatchLedger.agent_accepted_at) ??
|
|
12081
|
+
stringValue(dispatchLedger.submitted_at) ??
|
|
12082
|
+
new Date().toISOString();
|
|
12083
|
+
const recoveredStatus = dispatchLedger.status === "agent_accepted"
|
|
12084
|
+
? "agent_accepted"
|
|
12085
|
+
: "submitted";
|
|
12086
|
+
const submittedConversation = withTerminalBridgeSubmission({
|
|
12087
|
+
conversation: current,
|
|
12088
|
+
messageId: currentMessageId,
|
|
12089
|
+
requestText,
|
|
12090
|
+
status: recoveredStatus,
|
|
12091
|
+
preparedAt: stringValue(currentSubmission.prepared_at) ??
|
|
12092
|
+
submittedAt,
|
|
12093
|
+
...(recoveredStatus === "agent_accepted"
|
|
12094
|
+
? {
|
|
12095
|
+
agentAcceptedAt: submittedAt,
|
|
12096
|
+
acceptanceEvidence: dispatchLedger.acceptance_evidence
|
|
12097
|
+
}
|
|
12098
|
+
: { submittedAt })
|
|
12099
|
+
});
|
|
12100
|
+
saveState(statePath, submittedConversation);
|
|
12101
|
+
conversation = submittedConversation;
|
|
12102
|
+
}
|
|
12103
|
+
else {
|
|
12104
|
+
const uncertainAt = new Date().toISOString();
|
|
12105
|
+
const uncertainConversation = withTerminalBridgeSubmission({
|
|
12106
|
+
conversation: {
|
|
12107
|
+
...current,
|
|
12108
|
+
status: "stalled",
|
|
12109
|
+
stalled_at: uncertainAt,
|
|
12110
|
+
stalled_reason: "terminal dispatcher exited before AKK could prove the tmux submission",
|
|
12111
|
+
updated_at: uncertainAt
|
|
12112
|
+
},
|
|
12113
|
+
messageId: currentMessageId,
|
|
12114
|
+
requestText,
|
|
10140
12115
|
status: "uncertain",
|
|
10141
|
-
|
|
12116
|
+
preparedAt: stringValue(currentSubmission.prepared_at) ??
|
|
12117
|
+
uncertainAt,
|
|
12118
|
+
uncertainAt,
|
|
12119
|
+
error: "the terminal dispatcher exited before AKK could persist a submitted receipt"
|
|
12120
|
+
});
|
|
12121
|
+
if (!dispatchLedger ||
|
|
12122
|
+
stringValue(dispatchLedger.message_id) === currentMessageId) {
|
|
12123
|
+
saveTerminalBridgeDispatchLedger(terminalControl, {
|
|
12124
|
+
...(dispatchLedger ?? {}),
|
|
12125
|
+
status: "uncertain",
|
|
12126
|
+
generation_id: currentMessageId,
|
|
12127
|
+
conversation_id: uncertainConversation.conversation_id,
|
|
12128
|
+
message_id: currentMessageId,
|
|
12129
|
+
request_hash: terminalBridgeRequestFingerprint(requestText),
|
|
12130
|
+
prepared_at: stringValue(currentSubmission.prepared_at) ??
|
|
12131
|
+
uncertainAt,
|
|
12132
|
+
uncertain_at: uncertainAt,
|
|
12133
|
+
dispatcher_pid: Number.isSafeInteger(dispatcherPid) &&
|
|
12134
|
+
dispatcherPid > 1
|
|
12135
|
+
? dispatcherPid
|
|
12136
|
+
: null,
|
|
12137
|
+
state_path: statePath,
|
|
12138
|
+
event_log_path: logPath,
|
|
12139
|
+
callback_expected: Boolean(uncertainConversation.gateway_method),
|
|
12140
|
+
reason: "dispatcher_exited_before_submitted_receipt"
|
|
12141
|
+
});
|
|
12142
|
+
}
|
|
12143
|
+
saveState(statePath, uncertainConversation);
|
|
12144
|
+
appendEvent(logPath, {
|
|
12145
|
+
ts: uncertainAt,
|
|
10142
12146
|
conversation_id: uncertainConversation.conversation_id,
|
|
12147
|
+
event: "terminal_message_submit_uncertain",
|
|
10143
12148
|
message_id: currentMessageId,
|
|
10144
|
-
|
|
10145
|
-
prepared_at: stringValue(currentSubmission.prepared_at) ??
|
|
10146
|
-
uncertainAt,
|
|
10147
|
-
uncertain_at: uncertainAt,
|
|
12149
|
+
reason: "dispatcher_exited_before_submitted_receipt",
|
|
10148
12150
|
dispatcher_pid: Number.isSafeInteger(dispatcherPid) &&
|
|
10149
12151
|
dispatcherPid > 1
|
|
10150
12152
|
? dispatcherPid
|
|
10151
12153
|
: null,
|
|
10152
|
-
|
|
10153
|
-
event_log_path: logPath,
|
|
10154
|
-
callback_expected: Boolean(uncertainConversation.gateway_method),
|
|
10155
|
-
reason: "dispatcher_exited_before_submitted_receipt"
|
|
12154
|
+
do_not_retry: true
|
|
10156
12155
|
});
|
|
12156
|
+
conversation = uncertainConversation;
|
|
10157
12157
|
}
|
|
10158
|
-
|
|
10159
|
-
|
|
10160
|
-
|
|
10161
|
-
conversation_id: uncertainConversation.conversation_id,
|
|
10162
|
-
event: "terminal_message_submit_uncertain",
|
|
10163
|
-
message_id: currentMessageId,
|
|
10164
|
-
reason: "dispatcher_exited_before_submitted_receipt",
|
|
10165
|
-
dispatcher_pid: Number.isSafeInteger(dispatcherPid) &&
|
|
10166
|
-
dispatcherPid > 1
|
|
10167
|
-
? dispatcherPid
|
|
10168
|
-
: null,
|
|
10169
|
-
do_not_retry: true
|
|
10170
|
-
});
|
|
10171
|
-
conversation = uncertainConversation;
|
|
12158
|
+
}
|
|
12159
|
+
else {
|
|
12160
|
+
conversation = current;
|
|
10172
12161
|
}
|
|
10173
12162
|
}
|
|
10174
|
-
|
|
10175
|
-
|
|
12163
|
+
finally {
|
|
12164
|
+
releaseStateLock();
|
|
10176
12165
|
}
|
|
10177
|
-
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
|
|
10182
|
-
|
|
10183
|
-
|
|
10184
|
-
|
|
10185
|
-
|
|
10186
|
-
uncertainMessageId: currentMessageId
|
|
10187
|
-
});
|
|
10188
|
-
}
|
|
12166
|
+
if (terminalBridgeSubmission(conversation)?.status === "uncertain") {
|
|
12167
|
+
stallOtherTerminalBridgeConversationsForUncertainDispatch({
|
|
12168
|
+
storeDir: writerStoreDir,
|
|
12169
|
+
terminalControl,
|
|
12170
|
+
currentConversationId: conversation.conversation_id,
|
|
12171
|
+
uncertainMessageId: currentMessageId
|
|
12172
|
+
});
|
|
12173
|
+
}
|
|
12174
|
+
});
|
|
10189
12175
|
}
|
|
10190
12176
|
finally {
|
|
10191
12177
|
releaseTerminalLock();
|
|
10192
12178
|
}
|
|
10193
12179
|
const afterSubmission = terminalBridgeSubmission(conversation);
|
|
10194
12180
|
if (stringValue(afterSubmission?.message_id) === currentMessageId &&
|
|
10195
|
-
afterSubmission?.status
|
|
12181
|
+
["submitted", "agent_accepted"].includes(String(afterSubmission?.status))) {
|
|
10196
12182
|
continue;
|
|
10197
12183
|
}
|
|
10198
12184
|
printJson({
|
|
@@ -10206,22 +12192,31 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
10206
12192
|
});
|
|
10207
12193
|
return;
|
|
10208
12194
|
}
|
|
10209
|
-
if (submissionStatus === "
|
|
12195
|
+
if (submissionStatus === "not_accepted" ||
|
|
12196
|
+
submissionStatus === "uncertain" ||
|
|
12197
|
+
submissionStatus === "aborted") {
|
|
12198
|
+
const abortedSafeToRetry = submissionStatus === "aborted" &&
|
|
12199
|
+
submission.safe_to_retry === true;
|
|
10210
12200
|
printJson({
|
|
10211
12201
|
conversation,
|
|
10212
12202
|
monitored: true,
|
|
10213
12203
|
terminal_bridge: true,
|
|
10214
12204
|
completed: false,
|
|
10215
12205
|
submission_outcome: submissionStatus,
|
|
10216
|
-
do_not_retry:
|
|
10217
|
-
safe_to_retry:
|
|
10218
|
-
reason: submissionStatus === "
|
|
10219
|
-
? "terminal
|
|
10220
|
-
:
|
|
12206
|
+
do_not_retry: !abortedSafeToRetry,
|
|
12207
|
+
safe_to_retry: abortedSafeToRetry,
|
|
12208
|
+
reason: submissionStatus === "not_accepted"
|
|
12209
|
+
? "the exact terminal draft was not accepted; automatic retry is disabled"
|
|
12210
|
+
: submissionStatus === "uncertain"
|
|
12211
|
+
? "terminal submission outcome is uncertain; automatic completion and approval attribution are disabled"
|
|
12212
|
+
: abortedSafeToRetry
|
|
12213
|
+
? "terminal submission was durably aborted before tmux input"
|
|
12214
|
+
: "terminal submission was aborted but safe retry was not durably proven"
|
|
10221
12215
|
});
|
|
10222
12216
|
return;
|
|
10223
12217
|
}
|
|
10224
|
-
if (submissionStatus !== "submitted"
|
|
12218
|
+
if (submissionStatus !== "submitted" &&
|
|
12219
|
+
submissionStatus !== "agent_accepted") {
|
|
10225
12220
|
printJson({
|
|
10226
12221
|
conversation,
|
|
10227
12222
|
monitored: true,
|
|
@@ -10237,11 +12232,55 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
10237
12232
|
}
|
|
10238
12233
|
catch (error) {
|
|
10239
12234
|
const reason = error instanceof Error ? error.message : String(error);
|
|
12235
|
+
if (error instanceof StoreLockTimeoutError) {
|
|
12236
|
+
bindingCheckDeferredAttempts += 1;
|
|
12237
|
+
bindingCheckFirstDeferredAt ??= new Date().toISOString();
|
|
12238
|
+
const backoffMs = Math.min(5_000, 250 * (2 ** Math.min(5, bindingCheckDeferredAttempts - 1)));
|
|
12239
|
+
runtimeLog("warn", "terminal_bridge_monitor_binding_check_deferred", {
|
|
12240
|
+
conversation_id: conversation.conversation_id,
|
|
12241
|
+
terminal_target: terminalControl.target,
|
|
12242
|
+
error_code: error.code,
|
|
12243
|
+
lock_kind: error.lockKind,
|
|
12244
|
+
attempt: bindingCheckDeferredAttempts,
|
|
12245
|
+
retry_in_ms: backoffMs
|
|
12246
|
+
});
|
|
12247
|
+
sleepSync(backoffMs);
|
|
12248
|
+
continue;
|
|
12249
|
+
}
|
|
12250
|
+
if (!(error instanceof TurnBindingSupersededError)) {
|
|
12251
|
+
runtimeLog("error", "terminal_bridge_monitor_binding_check_failed", {
|
|
12252
|
+
conversation_id: conversation.conversation_id,
|
|
12253
|
+
terminal_target: terminalControl.target,
|
|
12254
|
+
error_code: isRecord(error) ? stringValue(error.code) : undefined,
|
|
12255
|
+
reason
|
|
12256
|
+
});
|
|
12257
|
+
throw error;
|
|
12258
|
+
}
|
|
10240
12259
|
runtimeLog("warn", "terminal_bridge_monitor_binding_superseded", {
|
|
10241
12260
|
conversation_id: conversation.conversation_id,
|
|
10242
12261
|
terminal_target: terminalControl.target,
|
|
10243
12262
|
reason
|
|
10244
12263
|
});
|
|
12264
|
+
try {
|
|
12265
|
+
appendEvent(logPath, {
|
|
12266
|
+
ts: new Date().toISOString(),
|
|
12267
|
+
conversation_id: conversation.conversation_id,
|
|
12268
|
+
event: "terminal_bridge_monitor_binding_superseded",
|
|
12269
|
+
terminal_control: terminalControl,
|
|
12270
|
+
error_code: error.code,
|
|
12271
|
+
reason
|
|
12272
|
+
});
|
|
12273
|
+
}
|
|
12274
|
+
catch (diagnosticError) {
|
|
12275
|
+
runtimeLog("warn", "terminal_bridge_monitor_diagnostic_write_failed", {
|
|
12276
|
+
conversation_id: conversation.conversation_id,
|
|
12277
|
+
terminal_target: terminalControl.target,
|
|
12278
|
+
diagnostic_event: "terminal_bridge_monitor_binding_superseded",
|
|
12279
|
+
reason: diagnosticError instanceof Error
|
|
12280
|
+
? diagnosticError.message
|
|
12281
|
+
: String(diagnosticError)
|
|
12282
|
+
});
|
|
12283
|
+
}
|
|
10245
12284
|
printJson({
|
|
10246
12285
|
conversation,
|
|
10247
12286
|
monitored: true,
|
|
@@ -10252,6 +12291,44 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
10252
12291
|
});
|
|
10253
12292
|
return;
|
|
10254
12293
|
}
|
|
12294
|
+
if (bindingCheckDeferredAttempts > 0) {
|
|
12295
|
+
const resumedAt = new Date().toISOString();
|
|
12296
|
+
try {
|
|
12297
|
+
appendEvent(logPath, {
|
|
12298
|
+
ts: resumedAt,
|
|
12299
|
+
conversation_id: conversation.conversation_id,
|
|
12300
|
+
event: "terminal_bridge_monitor_binding_check_deferred",
|
|
12301
|
+
terminal_control: terminalControl,
|
|
12302
|
+
error_code: "AKK_STORE_LOCK_TIMEOUT",
|
|
12303
|
+
lock_kind: "writer",
|
|
12304
|
+
first_deferred_at: bindingCheckFirstDeferredAt,
|
|
12305
|
+
resumed_at: resumedAt,
|
|
12306
|
+
attempts: bindingCheckDeferredAttempts,
|
|
12307
|
+
outcome: "resumed"
|
|
12308
|
+
});
|
|
12309
|
+
runtimeLog("info", "terminal_bridge_monitor_binding_check_resumed", {
|
|
12310
|
+
conversation_id: conversation.conversation_id,
|
|
12311
|
+
terminal_target: terminalControl.target,
|
|
12312
|
+
attempts: bindingCheckDeferredAttempts,
|
|
12313
|
+
first_deferred_at: bindingCheckFirstDeferredAt
|
|
12314
|
+
});
|
|
12315
|
+
bindingCheckDeferredAttempts = 0;
|
|
12316
|
+
bindingCheckFirstDeferredAt = undefined;
|
|
12317
|
+
}
|
|
12318
|
+
catch (error) {
|
|
12319
|
+
if (!(error instanceof StoreLockTimeoutError)) {
|
|
12320
|
+
throw error;
|
|
12321
|
+
}
|
|
12322
|
+
runtimeLog("warn", "terminal_bridge_monitor_diagnostic_write_deferred", {
|
|
12323
|
+
conversation_id: conversation.conversation_id,
|
|
12324
|
+
terminal_target: terminalControl.target,
|
|
12325
|
+
error_code: error.code,
|
|
12326
|
+
lock_kind: error.lockKind
|
|
12327
|
+
});
|
|
12328
|
+
sleepSync(Math.max(250, pollIntervalMs));
|
|
12329
|
+
continue;
|
|
12330
|
+
}
|
|
12331
|
+
}
|
|
10255
12332
|
const screenChangedSinceSend = preSendScreenFingerprint !== undefined &&
|
|
10256
12333
|
previousScreenFingerprint !== undefined &&
|
|
10257
12334
|
previousScreenFingerprint !== preSendScreenFingerprint;
|
|
@@ -10260,18 +12337,25 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
10260
12337
|
const expectedStatus = conversation.status;
|
|
10261
12338
|
const releaseTerminalPollLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
|
|
10262
12339
|
try {
|
|
10263
|
-
let dispatchLedger = loadTerminalBridgeDispatchLedger(terminalControl);
|
|
12340
|
+
let dispatchLedger = reconcilePreparedTerminalDispatchLedger(terminalControl, loadTerminalBridgeDispatchLedger(terminalControl));
|
|
10264
12341
|
const durableSubmission = terminalBridgeSubmission(conversation);
|
|
10265
12342
|
if (dispatchLedger?.status === "prepared" &&
|
|
10266
12343
|
stringValue(dispatchLedger.message_id) === currentMessageId &&
|
|
10267
|
-
durableSubmission
|
|
12344
|
+
durableSubmission &&
|
|
12345
|
+
["submitted", "agent_accepted"].includes(String(durableSubmission.status)) &&
|
|
10268
12346
|
stringValue(durableSubmission.message_id) === currentMessageId) {
|
|
10269
|
-
const submittedAt = stringValue(durableSubmission.
|
|
12347
|
+
const submittedAt = stringValue(durableSubmission.agent_accepted_at) ??
|
|
12348
|
+
stringValue(durableSubmission.submitted_at) ??
|
|
10270
12349
|
new Date().toISOString();
|
|
10271
12350
|
saveTerminalBridgeDispatchLedger(terminalControl, {
|
|
10272
12351
|
...dispatchLedger,
|
|
10273
|
-
status:
|
|
10274
|
-
|
|
12352
|
+
status: durableSubmission.status,
|
|
12353
|
+
...(durableSubmission.status === "agent_accepted"
|
|
12354
|
+
? {
|
|
12355
|
+
agent_accepted_at: submittedAt,
|
|
12356
|
+
acceptance_evidence: durableSubmission.acceptance_evidence
|
|
12357
|
+
}
|
|
12358
|
+
: { submitted_at: submittedAt }),
|
|
10275
12359
|
reason: "recovered from the durable conversation submission receipt"
|
|
10276
12360
|
});
|
|
10277
12361
|
dispatchLedger =
|
|
@@ -10280,7 +12364,7 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
10280
12364
|
if (dispatchLedger) {
|
|
10281
12365
|
const ledgerStatus = stringValue(dispatchLedger.status);
|
|
10282
12366
|
const ledgerMessageId = stringValue(dispatchLedger.message_id);
|
|
10283
|
-
if (ledgerStatus
|
|
12367
|
+
if (!["submitted", "agent_accepted"].includes(ledgerStatus ?? "") ||
|
|
10284
12368
|
ledgerMessageId !== currentMessageId) {
|
|
10285
12369
|
appendEvent(logPath, {
|
|
10286
12370
|
ts: new Date().toISOString(),
|
|
@@ -10342,6 +12426,47 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
10342
12426
|
releaseTerminalPollLock();
|
|
10343
12427
|
}
|
|
10344
12428
|
const terminalStatus = poll.status;
|
|
12429
|
+
const detectorLimitation = stringValue(terminalStatus.capability_limitation);
|
|
12430
|
+
const detectorDiagnosticFingerprint = detectorLimitation
|
|
12431
|
+
? terminalBridgeActivityFingerprint(detectorLimitation)
|
|
12432
|
+
: undefined;
|
|
12433
|
+
const detectorDiagnosticChanged = detectorLimitation
|
|
12434
|
+
? detectorDiagnosticFingerprint !== persistedDetectorDiagnosticFingerprint ||
|
|
12435
|
+
persistedDetectorDiagnosticStatus !== "limited"
|
|
12436
|
+
: persistedDetectorDiagnosticStatus === "limited";
|
|
12437
|
+
if (detectorDiagnosticChanged) {
|
|
12438
|
+
try {
|
|
12439
|
+
const diagnostic = persistTerminalBridgeDetectorDiagnostic({
|
|
12440
|
+
statePath,
|
|
12441
|
+
logPath,
|
|
12442
|
+
expectedConversationId: conversation.conversation_id,
|
|
12443
|
+
expectedMessageId: currentMessageId,
|
|
12444
|
+
limitation: detectorLimitation,
|
|
12445
|
+
fingerprint: detectorDiagnosticFingerprint
|
|
12446
|
+
});
|
|
12447
|
+
if (diagnostic.diagnostic) {
|
|
12448
|
+
conversation = diagnostic.conversation;
|
|
12449
|
+
nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
12450
|
+
? conversation.native_session_takeover
|
|
12451
|
+
: undefined;
|
|
12452
|
+
persistedDetectorDiagnosticFingerprint = stringValue(diagnostic.diagnostic?.fingerprint);
|
|
12453
|
+
persistedDetectorDiagnosticStatus = stringValue(diagnostic.diagnostic?.status);
|
|
12454
|
+
}
|
|
12455
|
+
}
|
|
12456
|
+
catch (error) {
|
|
12457
|
+
if (!(error instanceof StoreLockTimeoutError)) {
|
|
12458
|
+
throw error;
|
|
12459
|
+
}
|
|
12460
|
+
runtimeLog("warn", "terminal_bridge_detector_diagnostic_deferred", {
|
|
12461
|
+
conversation_id: conversation.conversation_id,
|
|
12462
|
+
terminal_target: terminalControl.target,
|
|
12463
|
+
error_code: error.code,
|
|
12464
|
+
lock_kind: error.lockKind
|
|
12465
|
+
});
|
|
12466
|
+
sleepSync(Math.max(250, pollIntervalMs));
|
|
12467
|
+
continue;
|
|
12468
|
+
}
|
|
12469
|
+
}
|
|
10345
12470
|
const approval = terminalStatus.approval_state;
|
|
10346
12471
|
const currentScreenFingerprint = stringValue(terminalStatus?.screen?.digest) ??
|
|
10347
12472
|
terminalBridgeScreenFingerprint(terminalStatus?.screen?.excerpt);
|
|
@@ -11060,7 +13185,17 @@ function orphanedTerminalDispatchForRecovery(terminalControl) {
|
|
|
11060
13185
|
const ledgerPanePid = Number(ledgerControl?.pane_pid);
|
|
11061
13186
|
const currentPanePid = Number(terminalControl.panePid);
|
|
11062
13187
|
if (!ledger ||
|
|
11063
|
-
![
|
|
13188
|
+
![
|
|
13189
|
+
"prepared",
|
|
13190
|
+
"text_injected",
|
|
13191
|
+
"enter_dispatched",
|
|
13192
|
+
"dispatching",
|
|
13193
|
+
"agent_accepted",
|
|
13194
|
+
"submitted",
|
|
13195
|
+
"not_accepted",
|
|
13196
|
+
"uncertain",
|
|
13197
|
+
"verified"
|
|
13198
|
+
]
|
|
11064
13199
|
.includes(String(ledger.status)) ||
|
|
11065
13200
|
!recoveryIdentity ||
|
|
11066
13201
|
(!lifecycle &&
|
|
@@ -11155,14 +13290,147 @@ function saveLifecycleTerminalDispatchLedger(terminalControl, ledger, options) {
|
|
|
11155
13290
|
}
|
|
11156
13291
|
saveTerminalBridgeDispatchLedger(terminalControl, ledger);
|
|
11157
13292
|
}
|
|
13293
|
+
function terminalLedgerReceiptHistory(ledger) {
|
|
13294
|
+
if (!ledger) {
|
|
13295
|
+
return [];
|
|
13296
|
+
}
|
|
13297
|
+
const value = ledger.terminal_submission_receipts;
|
|
13298
|
+
if (value !== undefined && !Array.isArray(value)) {
|
|
13299
|
+
throw new Error("terminal dispatch receipt history is malformed");
|
|
13300
|
+
}
|
|
13301
|
+
const receipts = (Array.isArray(value) ? value : []).map((receipt) => {
|
|
13302
|
+
if (!isRecord(receipt) || !stringValue(receipt.message_id)) {
|
|
13303
|
+
throw new Error("terminal dispatch receipt history is malformed");
|
|
13304
|
+
}
|
|
13305
|
+
return receipt;
|
|
13306
|
+
});
|
|
13307
|
+
const ids = new Set();
|
|
13308
|
+
for (const receipt of receipts) {
|
|
13309
|
+
const id = String(receipt.message_id);
|
|
13310
|
+
if (ids.has(id)) {
|
|
13311
|
+
throw new Error(`terminal dispatch receipt ${id} is duplicated`);
|
|
13312
|
+
}
|
|
13313
|
+
ids.add(id);
|
|
13314
|
+
}
|
|
13315
|
+
const current = terminalLedgerReceiptCandidate(ledger);
|
|
13316
|
+
const currentId = stringValue(current?.message_id);
|
|
13317
|
+
if (!current || !currentId) {
|
|
13318
|
+
return receipts;
|
|
13319
|
+
}
|
|
13320
|
+
const previous = receipts.find((receipt) => stringValue(receipt.message_id) === currentId);
|
|
13321
|
+
if (!previous) {
|
|
13322
|
+
return [...receipts, current];
|
|
13323
|
+
}
|
|
13324
|
+
// A resolved top-level ledger may already point at a replacement tmux pane.
|
|
13325
|
+
// Resolution cannot strengthen or rebind an existing submission receipt, so
|
|
13326
|
+
// its append-only historical copy remains authoritative.
|
|
13327
|
+
if (ledger.status === "resolved") {
|
|
13328
|
+
return receipts;
|
|
13329
|
+
}
|
|
13330
|
+
const merged = mergeTerminalLedgerReceipt(previous, current);
|
|
13331
|
+
return receipts.map((receipt) => stringValue(receipt.message_id) === currentId ? merged : receipt);
|
|
13332
|
+
}
|
|
13333
|
+
function terminalLedgerReceiptCandidate(ledger) {
|
|
13334
|
+
const storedStatus = String(ledger.status);
|
|
13335
|
+
const receiptStatus = TERMINAL_LEDGER_RECEIPT_STATUSES.has(storedStatus)
|
|
13336
|
+
? storedStatus
|
|
13337
|
+
: storedStatus === "resolved" && ledger.agent_accepted_at
|
|
13338
|
+
? "agent_accepted"
|
|
13339
|
+
: storedStatus === "resolved" && ledger.uncertain_at
|
|
13340
|
+
? "uncertain"
|
|
13341
|
+
: storedStatus === "resolved" && ledger.not_accepted_at
|
|
13342
|
+
? "not_accepted"
|
|
13343
|
+
: storedStatus === "resolved" && ledger.aborted_at
|
|
13344
|
+
? "aborted"
|
|
13345
|
+
: storedStatus === "resolved" && ledger.enter_dispatched_at
|
|
13346
|
+
? "enter_dispatched"
|
|
13347
|
+
: storedStatus === "resolved" && ledger.submitted_at
|
|
13348
|
+
? "submitted"
|
|
13349
|
+
: undefined;
|
|
13350
|
+
if (terminalDispatchLedgerLooksLifecycle(ledger) ||
|
|
13351
|
+
!receiptStatus ||
|
|
13352
|
+
!stringValue(ledger.message_id)) {
|
|
13353
|
+
return undefined;
|
|
13354
|
+
}
|
|
13355
|
+
const { terminal_submission_receipts: _history, terminal_key: _terminalKey, version: _version, ...receipt } = ledger;
|
|
13356
|
+
return { ...receipt, status: receiptStatus };
|
|
13357
|
+
}
|
|
13358
|
+
function mergeTerminalLedgerReceipt(previous, next) {
|
|
13359
|
+
const messageId = required(stringValue(previous.message_id), "terminal dispatch receipt message id is required");
|
|
13360
|
+
for (const field of TERMINAL_LEDGER_RECEIPT_IMMUTABLE_FIELDS) {
|
|
13361
|
+
const previousValue = previous[field];
|
|
13362
|
+
const nextValue = next[field];
|
|
13363
|
+
if (previousValue !== undefined &&
|
|
13364
|
+
nextValue !== undefined &&
|
|
13365
|
+
canonicalJson(previousValue) !== canonicalJson(nextValue)) {
|
|
13366
|
+
throw new Error(`terminal dispatch receipt ${messageId} changed immutable ${field}`);
|
|
13367
|
+
}
|
|
13368
|
+
}
|
|
13369
|
+
const previousControl = isRecord(previous.terminal_control)
|
|
13370
|
+
? previous.terminal_control
|
|
13371
|
+
: undefined;
|
|
13372
|
+
const nextControl = isRecord(next.terminal_control)
|
|
13373
|
+
? next.terminal_control
|
|
13374
|
+
: undefined;
|
|
13375
|
+
if (previousControl &&
|
|
13376
|
+
nextControl &&
|
|
13377
|
+
(stringValue(previousControl.target) !== stringValue(nextControl.target) ||
|
|
13378
|
+
(stringValue(previousControl.socket_path) ?? undefined) !==
|
|
13379
|
+
(stringValue(nextControl.socket_path) ?? undefined) ||
|
|
13380
|
+
Number(previousControl.pane_pid) !== Number(nextControl.pane_pid))) {
|
|
13381
|
+
throw new Error(`terminal dispatch receipt ${messageId} changed immutable terminal_control`);
|
|
13382
|
+
}
|
|
13383
|
+
if (previous.status === "agent_accepted" &&
|
|
13384
|
+
next.status !== "agent_accepted") {
|
|
13385
|
+
return previous;
|
|
13386
|
+
}
|
|
13387
|
+
const previousIsTerminalFailure = [
|
|
13388
|
+
"not_accepted",
|
|
13389
|
+
"uncertain",
|
|
13390
|
+
"aborted"
|
|
13391
|
+
].includes(String(previous.status)) && previous.safe_to_retry !== true;
|
|
13392
|
+
const nextIsTransportOnly = [
|
|
13393
|
+
"text_injected",
|
|
13394
|
+
"enter_dispatched",
|
|
13395
|
+
"submitted"
|
|
13396
|
+
].includes(String(next.status));
|
|
13397
|
+
if (previousIsTerminalFailure && nextIsTransportOnly) {
|
|
13398
|
+
return previous;
|
|
13399
|
+
}
|
|
13400
|
+
const merged = { ...next };
|
|
13401
|
+
for (const field of TERMINAL_LEDGER_RECEIPT_IMMUTABLE_FIELDS) {
|
|
13402
|
+
if (merged[field] === undefined && previous[field] !== undefined) {
|
|
13403
|
+
merged[field] = previous[field];
|
|
13404
|
+
}
|
|
13405
|
+
}
|
|
13406
|
+
if (!merged.terminal_control && previousControl) {
|
|
13407
|
+
merged.terminal_control = previousControl;
|
|
13408
|
+
}
|
|
13409
|
+
return merged;
|
|
13410
|
+
}
|
|
11158
13411
|
function saveTerminalBridgeDispatchLedger(terminalControl, ledger) {
|
|
11159
13412
|
const ledgerPath = terminalBridgeDispatchLedgerPath(terminalControl);
|
|
11160
13413
|
ensureDir(path.dirname(ledgerPath));
|
|
11161
13414
|
if (fs.existsSync(ledgerPath) && fs.lstatSync(ledgerPath).isSymbolicLink()) {
|
|
11162
13415
|
throw new Error(`terminal dispatch ledger is a symlink: ${ledgerPath}`);
|
|
11163
13416
|
}
|
|
11164
|
-
const
|
|
11165
|
-
|
|
13417
|
+
const previousLedger = fs.existsSync(ledgerPath)
|
|
13418
|
+
? loadTerminalBridgeDispatchLedger(terminalControl)
|
|
13419
|
+
: undefined;
|
|
13420
|
+
let baseReceiptHistory = terminalLedgerReceiptHistory(previousLedger);
|
|
13421
|
+
for (const incomingReceipt of terminalLedgerReceiptHistory(ledger)) {
|
|
13422
|
+
const incomingId = String(incomingReceipt.message_id);
|
|
13423
|
+
const previousReceipt = baseReceiptHistory.find((receipt) => stringValue(receipt.message_id) === incomingId);
|
|
13424
|
+
const merged = previousReceipt
|
|
13425
|
+
? mergeTerminalLedgerReceipt(previousReceipt, incomingReceipt)
|
|
13426
|
+
: incomingReceipt;
|
|
13427
|
+
baseReceiptHistory = previousReceipt
|
|
13428
|
+
? baseReceiptHistory.map((receipt) => stringValue(receipt.message_id) === incomingId ? merged : receipt)
|
|
13429
|
+
: [...baseReceiptHistory, merged];
|
|
13430
|
+
}
|
|
13431
|
+
const { terminal_submission_receipts: _incomingReceiptHistory, ...ledgerWithoutReceiptHistory } = ledger;
|
|
13432
|
+
const nextWithoutHistory = {
|
|
13433
|
+
...ledgerWithoutReceiptHistory,
|
|
11166
13434
|
version: 1,
|
|
11167
13435
|
terminal_key: terminalBridgeRuntimeKey(terminalControl),
|
|
11168
13436
|
terminal_control: {
|
|
@@ -11173,6 +13441,37 @@ function saveTerminalBridgeDispatchLedger(terminalControl, ledger) {
|
|
|
11173
13441
|
current_path: terminalControl.currentPath ?? null
|
|
11174
13442
|
}
|
|
11175
13443
|
};
|
|
13444
|
+
// The top-level ledger follows the current tmux pane incarnation, while an
|
|
13445
|
+
// append-only receipt must retain the pane that actually accepted its
|
|
13446
|
+
// input. Reconciliation may save an old ledger through a newly-created
|
|
13447
|
+
// pane with the same selector, so derive that receipt from the caller's
|
|
13448
|
+
// original control record instead of silently rebinding its proof.
|
|
13449
|
+
const receiptCandidateLedger = isRecord(ledgerWithoutReceiptHistory.terminal_control)
|
|
13450
|
+
? {
|
|
13451
|
+
...nextWithoutHistory,
|
|
13452
|
+
terminal_control: ledgerWithoutReceiptHistory.terminal_control
|
|
13453
|
+
}
|
|
13454
|
+
: nextWithoutHistory;
|
|
13455
|
+
const nextCandidate = terminalLedgerReceiptCandidate(receiptCandidateLedger);
|
|
13456
|
+
let nextReceiptHistory = baseReceiptHistory;
|
|
13457
|
+
if (nextCandidate) {
|
|
13458
|
+
const messageId = String(nextCandidate.message_id);
|
|
13459
|
+
const previousReceipt = baseReceiptHistory.find((receipt) => stringValue(receipt.message_id) === messageId);
|
|
13460
|
+
const nextReceipt = previousReceipt
|
|
13461
|
+
? mergeTerminalLedgerReceipt(previousReceipt, nextCandidate)
|
|
13462
|
+
: nextCandidate;
|
|
13463
|
+
nextReceiptHistory = previousReceipt
|
|
13464
|
+
? baseReceiptHistory.map((receipt) => stringValue(receipt.message_id) === messageId
|
|
13465
|
+
? nextReceipt
|
|
13466
|
+
: receipt)
|
|
13467
|
+
: [...baseReceiptHistory, nextReceipt];
|
|
13468
|
+
}
|
|
13469
|
+
const nextLedger = {
|
|
13470
|
+
...nextWithoutHistory,
|
|
13471
|
+
...(nextReceiptHistory.length > 0
|
|
13472
|
+
? { terminal_submission_receipts: nextReceiptHistory }
|
|
13473
|
+
: {})
|
|
13474
|
+
};
|
|
11176
13475
|
const tempPath = `${ledgerPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
11177
13476
|
let tempFd;
|
|
11178
13477
|
try {
|
|
@@ -11271,7 +13570,8 @@ function assertManagedTerminalDispatchOwner({ conversation, terminalControl, act
|
|
|
11271
13570
|
const messageId = stringValue(nativeTakeover?.terminal_bridge_message_id);
|
|
11272
13571
|
const ledger = loadTerminalBridgeDispatchLedger(terminalControl);
|
|
11273
13572
|
if (!messageId ||
|
|
11274
|
-
ledger
|
|
13573
|
+
!ledger ||
|
|
13574
|
+
!["submitted", "agent_accepted"].includes(String(ledger.status)) ||
|
|
11275
13575
|
stringValue(ledger.conversation_id) !==
|
|
11276
13576
|
conversation.conversation_id ||
|
|
11277
13577
|
stringValue(ledger.message_id) !== messageId ||
|
|
@@ -11297,13 +13597,22 @@ function terminalBindingLedgerFields(conversation) {
|
|
|
11297
13597
|
const nativeThreadId = stringValue(conversation.native_thread_id) ??
|
|
11298
13598
|
stringValue(takeover?.terminal_agent_session_id);
|
|
11299
13599
|
const storeDir = managedSessionStoreDirForConversation(conversation);
|
|
13600
|
+
const submission = terminalBridgeSubmission(conversation);
|
|
13601
|
+
const messageType = stringValue(submission?.message_type);
|
|
13602
|
+
const messageBodyHash = stringValue(submission?.message_body_hash);
|
|
11300
13603
|
return {
|
|
11301
13604
|
...(bindingId ? { binding_id: bindingId } : {}),
|
|
11302
13605
|
...(Number.isSafeInteger(generation)
|
|
11303
13606
|
? { binding_generation: generation }
|
|
11304
13607
|
: {}),
|
|
11305
13608
|
...(nativeThreadId ? { native_thread_id: nativeThreadId } : {}),
|
|
11306
|
-
...(storeDir ? { store_dir: path.resolve(storeDir) } : {})
|
|
13609
|
+
...(storeDir ? { store_dir: path.resolve(storeDir) } : {}),
|
|
13610
|
+
...(messageType ? { message_type: messageType } : {}),
|
|
13611
|
+
...(messageBodyHash ? { message_body_hash: messageBodyHash } : {}),
|
|
13612
|
+
executor_kind: executorForConversation(conversation).kind,
|
|
13613
|
+
...(conversation.openclaw_session
|
|
13614
|
+
? { openclaw_session: conversation.openclaw_session }
|
|
13615
|
+
: {})
|
|
11307
13616
|
};
|
|
11308
13617
|
}
|
|
11309
13618
|
function reconcilePreparedTerminalDispatchLedger(terminalControl, ledger) {
|
|
@@ -11311,7 +13620,7 @@ function reconcilePreparedTerminalDispatchLedger(terminalControl, ledger) {
|
|
|
11311
13620
|
return ledger;
|
|
11312
13621
|
}
|
|
11313
13622
|
if (ledger?.status !== "prepared") {
|
|
11314
|
-
return ledger;
|
|
13623
|
+
return reconcileLaggingTerminalDispatchLedger(terminalControl, ledger);
|
|
11315
13624
|
}
|
|
11316
13625
|
const dispatcherPid = Number(ledger.dispatcher_pid);
|
|
11317
13626
|
if (Number.isSafeInteger(dispatcherPid) &&
|
|
@@ -11358,14 +13667,32 @@ function reconcilePreparedTerminalDispatchLedger(terminalControl, ledger) {
|
|
|
11358
13667
|
}
|
|
11359
13668
|
if (storedMessageId === messageId &&
|
|
11360
13669
|
stringValue(submission?.message_id) === messageId) {
|
|
11361
|
-
if (submission
|
|
11362
|
-
|
|
13670
|
+
if (submission &&
|
|
13671
|
+
["submitted", "text_injected", "enter_dispatched", "agent_accepted"]
|
|
13672
|
+
.includes(String(submission.status))) {
|
|
13673
|
+
const submittedAt = stringValue(submission.agent_accepted_at) ??
|
|
13674
|
+
stringValue(submission.enter_dispatched_at) ??
|
|
13675
|
+
stringValue(submission.text_injected_at) ??
|
|
13676
|
+
stringValue(submission.submitted_at) ??
|
|
11363
13677
|
stringValue(conversation.updated_at) ??
|
|
11364
13678
|
new Date().toISOString();
|
|
11365
13679
|
saveTerminalBridgeDispatchLedger(terminalControl, {
|
|
11366
13680
|
...ledger,
|
|
11367
|
-
status:
|
|
11368
|
-
|
|
13681
|
+
status: submission.status,
|
|
13682
|
+
...(submission.text_injected_at
|
|
13683
|
+
? { text_injected_at: submission.text_injected_at }
|
|
13684
|
+
: {}),
|
|
13685
|
+
...(submission.enter_dispatched_at
|
|
13686
|
+
? { enter_dispatched_at: submission.enter_dispatched_at }
|
|
13687
|
+
: {}),
|
|
13688
|
+
...(submission.status === "agent_accepted"
|
|
13689
|
+
? {
|
|
13690
|
+
agent_accepted_at: submittedAt,
|
|
13691
|
+
acceptance_evidence: submission.acceptance_evidence
|
|
13692
|
+
}
|
|
13693
|
+
: submission.status === "submitted"
|
|
13694
|
+
? { submitted_at: submittedAt }
|
|
13695
|
+
: {}),
|
|
11369
13696
|
reason: "recovered from the durable conversation submission receipt"
|
|
11370
13697
|
});
|
|
11371
13698
|
}
|
|
@@ -11380,22 +13707,34 @@ function reconcilePreparedTerminalDispatchLedger(terminalControl, ledger) {
|
|
|
11380
13707
|
return loadTerminalBridgeDispatchLedger(terminalControl);
|
|
11381
13708
|
}
|
|
11382
13709
|
if (storedMessageId &&
|
|
11383
|
-
submission
|
|
13710
|
+
submission &&
|
|
13711
|
+
["submitted", "agent_accepted"].includes(String(submission.status)) &&
|
|
11384
13712
|
stringValue(submission.message_id) === storedMessageId) {
|
|
11385
13713
|
const requestText = String(nativeTakeover?.terminal_bridge_request_text ??
|
|
11386
13714
|
conversation.user_request ??
|
|
11387
13715
|
"");
|
|
11388
13716
|
saveTerminalBridgeDispatchLedger(terminalControl, {
|
|
11389
13717
|
...terminalBindingLedgerFields(conversation),
|
|
11390
|
-
status:
|
|
13718
|
+
status: submission.status,
|
|
11391
13719
|
generation_id: storedMessageId,
|
|
11392
13720
|
conversation_id: conversation.conversation_id,
|
|
11393
13721
|
message_id: storedMessageId,
|
|
13722
|
+
...(stringValue(submission.message_type)
|
|
13723
|
+
? { message_type: stringValue(submission.message_type) }
|
|
13724
|
+
: {}),
|
|
11394
13725
|
request_hash: terminalBridgeRequestFingerprint(requestText),
|
|
11395
13726
|
prepared_at: stringValue(submission.prepared_at) ??
|
|
11396
13727
|
stringValue(conversation.updated_at),
|
|
11397
|
-
|
|
11398
|
-
|
|
13728
|
+
...(submission.status === "agent_accepted"
|
|
13729
|
+
? {
|
|
13730
|
+
agent_accepted_at: stringValue(submission.agent_accepted_at) ??
|
|
13731
|
+
stringValue(conversation.updated_at),
|
|
13732
|
+
acceptance_evidence: submission.acceptance_evidence
|
|
13733
|
+
}
|
|
13734
|
+
: {
|
|
13735
|
+
submitted_at: stringValue(submission.submitted_at) ??
|
|
13736
|
+
stringValue(conversation.updated_at)
|
|
13737
|
+
}),
|
|
11399
13738
|
dispatcher_pid: null,
|
|
11400
13739
|
state_path: statePath,
|
|
11401
13740
|
event_log_path: stringValue(ledger.event_log_path) ??
|
|
@@ -11414,6 +13753,216 @@ function reconcilePreparedTerminalDispatchLedger(terminalControl, ledger) {
|
|
|
11414
13753
|
}
|
|
11415
13754
|
return loadTerminalBridgeDispatchLedger(terminalControl);
|
|
11416
13755
|
}
|
|
13756
|
+
function reconcileLaggingTerminalDispatchLedger(terminalControl, ledger) {
|
|
13757
|
+
if (!ledger ||
|
|
13758
|
+
terminalDispatchLedgerLooksLifecycle(ledger) ||
|
|
13759
|
+
![
|
|
13760
|
+
"text_injected",
|
|
13761
|
+
"enter_dispatched",
|
|
13762
|
+
"submitted",
|
|
13763
|
+
"agent_accepted",
|
|
13764
|
+
"not_accepted",
|
|
13765
|
+
"uncertain"
|
|
13766
|
+
].includes(String(ledger.status))) {
|
|
13767
|
+
return ledger;
|
|
13768
|
+
}
|
|
13769
|
+
const statePath = stringValue(ledger.state_path);
|
|
13770
|
+
const messageId = stringValue(ledger.message_id);
|
|
13771
|
+
if (!statePath || !messageId) {
|
|
13772
|
+
return ledger;
|
|
13773
|
+
}
|
|
13774
|
+
let conversation;
|
|
13775
|
+
try {
|
|
13776
|
+
conversation = loadState(statePath);
|
|
13777
|
+
}
|
|
13778
|
+
catch {
|
|
13779
|
+
return ledger;
|
|
13780
|
+
}
|
|
13781
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
13782
|
+
? conversation.native_session_takeover
|
|
13783
|
+
: undefined;
|
|
13784
|
+
const submission = terminalBridgeSubmission(conversation);
|
|
13785
|
+
const storedControl = terminalControlFromTakeover(nativeTakeover);
|
|
13786
|
+
if (conversation.conversation_id !== stringValue(ledger.conversation_id) ||
|
|
13787
|
+
stringValue(nativeTakeover?.terminal_bridge_message_id) !== messageId ||
|
|
13788
|
+
stringValue(submission?.message_id) !== messageId ||
|
|
13789
|
+
!storedControl ||
|
|
13790
|
+
terminalBridgeRuntimeKey(storedControl) !==
|
|
13791
|
+
terminalBridgeRuntimeKey(terminalControl)) {
|
|
13792
|
+
return ledger;
|
|
13793
|
+
}
|
|
13794
|
+
const requestText = String(nativeTakeover?.terminal_bridge_request_text ?? conversation.user_request ?? "");
|
|
13795
|
+
const requestHash = terminalBridgeRequestFingerprint(requestText);
|
|
13796
|
+
if (stringValue(ledger.request_hash) !== requestHash ||
|
|
13797
|
+
stringValue(submission?.request_hash) !== requestHash) {
|
|
13798
|
+
return ledger;
|
|
13799
|
+
}
|
|
13800
|
+
const bindingFields = terminalBindingLedgerFields(conversation);
|
|
13801
|
+
for (const key of [
|
|
13802
|
+
"binding_id",
|
|
13803
|
+
"binding_generation",
|
|
13804
|
+
"native_thread_id",
|
|
13805
|
+
"store_dir"
|
|
13806
|
+
]) {
|
|
13807
|
+
if (ledger[key] !== undefined &&
|
|
13808
|
+
bindingFields[key] !== undefined &&
|
|
13809
|
+
String(ledger[key]) !== String(bindingFields[key])) {
|
|
13810
|
+
return ledger;
|
|
13811
|
+
}
|
|
13812
|
+
}
|
|
13813
|
+
const stateStatus = String(submission?.status ?? "");
|
|
13814
|
+
let stateAcceptanceEvidence;
|
|
13815
|
+
if (stateStatus === "agent_accepted") {
|
|
13816
|
+
try {
|
|
13817
|
+
stateAcceptanceEvidence = terminalAcceptanceEvidenceForConversation(conversation, requestText, submission?.acceptance_evidence);
|
|
13818
|
+
}
|
|
13819
|
+
catch {
|
|
13820
|
+
stateAcceptanceEvidence = undefined;
|
|
13821
|
+
}
|
|
13822
|
+
}
|
|
13823
|
+
let ledgerAcceptanceEvidence;
|
|
13824
|
+
let ledgerAcceptanceError;
|
|
13825
|
+
if (ledger.status === "agent_accepted") {
|
|
13826
|
+
try {
|
|
13827
|
+
ledgerAcceptanceEvidence = terminalAcceptanceEvidenceForConversation(conversation, requestText, ledger.acceptance_evidence);
|
|
13828
|
+
}
|
|
13829
|
+
catch (error) {
|
|
13830
|
+
ledgerAcceptanceError = error instanceof Error
|
|
13831
|
+
? error.message
|
|
13832
|
+
: String(error);
|
|
13833
|
+
}
|
|
13834
|
+
}
|
|
13835
|
+
if (ledgerAcceptanceEvidence) {
|
|
13836
|
+
if (!stateAcceptanceEvidence) {
|
|
13837
|
+
const acceptedAt = stringValue(ledger.agent_accepted_at) ?? new Date().toISOString();
|
|
13838
|
+
const acceptedConversation = withTerminalBridgeSubmission({
|
|
13839
|
+
conversation,
|
|
13840
|
+
messageId,
|
|
13841
|
+
requestText,
|
|
13842
|
+
status: "agent_accepted",
|
|
13843
|
+
preparedAt: stringValue(submission?.prepared_at) ??
|
|
13844
|
+
stringValue(ledger.prepared_at) ??
|
|
13845
|
+
acceptedAt,
|
|
13846
|
+
textInjectedAt: stringValue(submission?.text_injected_at) ??
|
|
13847
|
+
stringValue(ledger.text_injected_at),
|
|
13848
|
+
enterDispatchedAt: stringValue(submission?.enter_dispatched_at) ??
|
|
13849
|
+
stringValue(ledger.enter_dispatched_at),
|
|
13850
|
+
agentAcceptedAt: acceptedAt,
|
|
13851
|
+
acceptanceEvidence: ledgerAcceptanceEvidence
|
|
13852
|
+
});
|
|
13853
|
+
saveState(statePath, acceptedConversation);
|
|
13854
|
+
}
|
|
13855
|
+
return ledger;
|
|
13856
|
+
}
|
|
13857
|
+
if (stateAcceptanceEvidence) {
|
|
13858
|
+
const acceptedAt = stringValue(submission?.agent_accepted_at) ?? new Date().toISOString();
|
|
13859
|
+
saveTerminalBridgeDispatchLedger(terminalControl, {
|
|
13860
|
+
...ledger,
|
|
13861
|
+
...bindingFields,
|
|
13862
|
+
status: "agent_accepted",
|
|
13863
|
+
text_injected_at: submission?.text_injected_at,
|
|
13864
|
+
enter_dispatched_at: submission?.enter_dispatched_at,
|
|
13865
|
+
agent_accepted_at: acceptedAt,
|
|
13866
|
+
acceptance_evidence: stateAcceptanceEvidence,
|
|
13867
|
+
dispatcher_pid: null,
|
|
13868
|
+
reason: "recovered the strongest durable native acceptance receipt"
|
|
13869
|
+
});
|
|
13870
|
+
return loadTerminalBridgeDispatchLedger(terminalControl);
|
|
13871
|
+
}
|
|
13872
|
+
if (ledger.status === "agent_accepted") {
|
|
13873
|
+
const uncertainAt = new Date().toISOString();
|
|
13874
|
+
const uncertainConversation = withTerminalBridgeSubmission({
|
|
13875
|
+
conversation: {
|
|
13876
|
+
...conversation,
|
|
13877
|
+
status: "stalled",
|
|
13878
|
+
stalled_at: uncertainAt,
|
|
13879
|
+
stalled_reason: "stored native acceptance evidence is invalid",
|
|
13880
|
+
updated_at: uncertainAt
|
|
13881
|
+
},
|
|
13882
|
+
messageId,
|
|
13883
|
+
requestText,
|
|
13884
|
+
status: "uncertain",
|
|
13885
|
+
preparedAt: stringValue(submission?.prepared_at) ??
|
|
13886
|
+
stringValue(ledger.prepared_at) ??
|
|
13887
|
+
uncertainAt,
|
|
13888
|
+
textInjectedAt: stringValue(submission?.text_injected_at) ??
|
|
13889
|
+
stringValue(ledger.text_injected_at),
|
|
13890
|
+
enterDispatchedAt: stringValue(submission?.enter_dispatched_at) ??
|
|
13891
|
+
stringValue(ledger.enter_dispatched_at),
|
|
13892
|
+
uncertainAt,
|
|
13893
|
+
error: ledgerAcceptanceError ??
|
|
13894
|
+
"stored native acceptance evidence is invalid",
|
|
13895
|
+
lastProvenStage: "enter_dispatched"
|
|
13896
|
+
});
|
|
13897
|
+
saveState(statePath, uncertainConversation);
|
|
13898
|
+
saveTerminalBridgeDispatchLedger(terminalControl, {
|
|
13899
|
+
...ledger,
|
|
13900
|
+
...bindingFields,
|
|
13901
|
+
status: "uncertain",
|
|
13902
|
+
uncertain_at: uncertainAt,
|
|
13903
|
+
dispatcher_pid: null,
|
|
13904
|
+
reason: "stored native acceptance evidence is invalid"
|
|
13905
|
+
});
|
|
13906
|
+
return loadTerminalBridgeDispatchLedger(terminalControl);
|
|
13907
|
+
}
|
|
13908
|
+
const stateRank = terminalSubmissionProofRank(stateStatus, stringValue(submission?.last_proven_stage));
|
|
13909
|
+
const ledgerRank = terminalSubmissionProofRank(String(ledger.status), stringValue(ledger.last_proven_stage));
|
|
13910
|
+
const stateIsTerminal = ["not_accepted", "uncertain", "aborted"]
|
|
13911
|
+
.includes(stateStatus);
|
|
13912
|
+
if (!stateIsTerminal && stateRank <= ledgerRank) {
|
|
13913
|
+
return ledger;
|
|
13914
|
+
}
|
|
13915
|
+
if (![
|
|
13916
|
+
"text_injected",
|
|
13917
|
+
"enter_dispatched",
|
|
13918
|
+
"submitted",
|
|
13919
|
+
"not_accepted",
|
|
13920
|
+
"uncertain",
|
|
13921
|
+
"aborted"
|
|
13922
|
+
].includes(stateStatus)) {
|
|
13923
|
+
return ledger;
|
|
13924
|
+
}
|
|
13925
|
+
saveTerminalBridgeDispatchLedger(terminalControl, {
|
|
13926
|
+
...ledger,
|
|
13927
|
+
...bindingFields,
|
|
13928
|
+
status: stateStatus,
|
|
13929
|
+
...(submission?.text_injected_at
|
|
13930
|
+
? { text_injected_at: submission.text_injected_at }
|
|
13931
|
+
: {}),
|
|
13932
|
+
...(submission?.enter_dispatched_at
|
|
13933
|
+
? { enter_dispatched_at: submission.enter_dispatched_at }
|
|
13934
|
+
: {}),
|
|
13935
|
+
...(submission?.submitted_at
|
|
13936
|
+
? { submitted_at: submission.submitted_at }
|
|
13937
|
+
: {}),
|
|
13938
|
+
...(submission?.not_accepted_at
|
|
13939
|
+
? { not_accepted_at: submission.not_accepted_at }
|
|
13940
|
+
: {}),
|
|
13941
|
+
...(submission?.uncertain_at
|
|
13942
|
+
? { uncertain_at: submission.uncertain_at }
|
|
13943
|
+
: {}),
|
|
13944
|
+
...(submission?.aborted_at
|
|
13945
|
+
? { aborted_at: submission.aborted_at }
|
|
13946
|
+
: {}),
|
|
13947
|
+
last_proven_stage: submission?.last_proven_stage,
|
|
13948
|
+
...(stateIsTerminal ? { dispatcher_pid: null } : {}),
|
|
13949
|
+
reason: "recovered the strongest durable conversation proof level"
|
|
13950
|
+
});
|
|
13951
|
+
return loadTerminalBridgeDispatchLedger(terminalControl);
|
|
13952
|
+
}
|
|
13953
|
+
function terminalSubmissionProofRank(status, lastProven) {
|
|
13954
|
+
if (status === "agent_accepted" || lastProven === "agent_accepted") {
|
|
13955
|
+
return 3;
|
|
13956
|
+
}
|
|
13957
|
+
if (["enter_dispatched", "submitted", "not_accepted"].includes(status) ||
|
|
13958
|
+
lastProven === "enter_dispatched") {
|
|
13959
|
+
return 2;
|
|
13960
|
+
}
|
|
13961
|
+
if (status === "text_injected" || lastProven === "text_injected") {
|
|
13962
|
+
return 1;
|
|
13963
|
+
}
|
|
13964
|
+
return 0;
|
|
13965
|
+
}
|
|
11417
13966
|
async function recoverLifecycleFenceBeforeMutation({ options, terminal }) {
|
|
11418
13967
|
const ledger = loadTerminalBridgeDispatchLedger(terminal.terminalControl);
|
|
11419
13968
|
if (!ledger || ledger.status === "resolved") {
|
|
@@ -12797,6 +15346,101 @@ function persistTerminalBridgeActivity({ conversation, statePath, logPath, obser
|
|
|
12797
15346
|
releaseLock();
|
|
12798
15347
|
}
|
|
12799
15348
|
}
|
|
15349
|
+
function persistTerminalBridgeDetectorDiagnostic({ statePath, logPath, expectedConversationId, expectedMessageId, limitation, fingerprint }) {
|
|
15350
|
+
const storeDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
15351
|
+
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
15352
|
+
try {
|
|
15353
|
+
return withStoreWriterLease(storeDir, () => {
|
|
15354
|
+
const conversation = loadState(statePath);
|
|
15355
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
15356
|
+
? conversation.native_session_takeover
|
|
15357
|
+
: {};
|
|
15358
|
+
if (conversation.conversation_id !== expectedConversationId ||
|
|
15359
|
+
stringValue(nativeTakeover.terminal_bridge_message_id) !==
|
|
15360
|
+
expectedMessageId) {
|
|
15361
|
+
return {
|
|
15362
|
+
persisted: false,
|
|
15363
|
+
conversation,
|
|
15364
|
+
reason: "terminal_bridge_task_replaced"
|
|
15365
|
+
};
|
|
15366
|
+
}
|
|
15367
|
+
const existing = isRecord(nativeTakeover.terminal_bridge_detector_diagnostic)
|
|
15368
|
+
? nativeTakeover.terminal_bridge_detector_diagnostic
|
|
15369
|
+
: undefined;
|
|
15370
|
+
const now = new Date().toISOString();
|
|
15371
|
+
const nextDiagnostic = limitation && fingerprint
|
|
15372
|
+
? {
|
|
15373
|
+
status: "limited",
|
|
15374
|
+
source: "terminal_completion_detector",
|
|
15375
|
+
fingerprint,
|
|
15376
|
+
detail: truncateText(redactString(limitation), 1000),
|
|
15377
|
+
observed_at: now
|
|
15378
|
+
}
|
|
15379
|
+
: existing && stringValue(existing.status) === "limited"
|
|
15380
|
+
? {
|
|
15381
|
+
...existing,
|
|
15382
|
+
status: "recovered",
|
|
15383
|
+
recovered_at: now
|
|
15384
|
+
}
|
|
15385
|
+
: undefined;
|
|
15386
|
+
if (!nextDiagnostic) {
|
|
15387
|
+
return {
|
|
15388
|
+
persisted: false,
|
|
15389
|
+
conversation,
|
|
15390
|
+
diagnostic: existing,
|
|
15391
|
+
reason: "detector_diagnostic_unchanged"
|
|
15392
|
+
};
|
|
15393
|
+
}
|
|
15394
|
+
if (stringValue(existing?.status) === stringValue(nextDiagnostic.status) &&
|
|
15395
|
+
stringValue(existing?.fingerprint) ===
|
|
15396
|
+
stringValue(nextDiagnostic.fingerprint)) {
|
|
15397
|
+
return {
|
|
15398
|
+
persisted: false,
|
|
15399
|
+
conversation,
|
|
15400
|
+
diagnostic: existing,
|
|
15401
|
+
reason: "detector_diagnostic_unchanged"
|
|
15402
|
+
};
|
|
15403
|
+
}
|
|
15404
|
+
const nextConversation = {
|
|
15405
|
+
...conversation,
|
|
15406
|
+
native_session_takeover: {
|
|
15407
|
+
...nativeTakeover,
|
|
15408
|
+
terminal_bridge_detector_diagnostic: nextDiagnostic
|
|
15409
|
+
},
|
|
15410
|
+
updated_at: now
|
|
15411
|
+
};
|
|
15412
|
+
saveState(statePath, nextConversation);
|
|
15413
|
+
const event = nextDiagnostic.status === "limited"
|
|
15414
|
+
? "terminal_bridge_completion_detector_limited"
|
|
15415
|
+
: "terminal_bridge_completion_detector_recovered";
|
|
15416
|
+
appendEvent(logPath, {
|
|
15417
|
+
ts: now,
|
|
15418
|
+
conversation_id: conversation.conversation_id,
|
|
15419
|
+
event,
|
|
15420
|
+
terminal_bridge_message_id: expectedMessageId,
|
|
15421
|
+
detector_source: nextDiagnostic.source,
|
|
15422
|
+
diagnostic_fingerprint: nextDiagnostic.fingerprint,
|
|
15423
|
+
detail: nextDiagnostic.status === "limited"
|
|
15424
|
+
? nextDiagnostic.detail
|
|
15425
|
+
: undefined
|
|
15426
|
+
});
|
|
15427
|
+
runtimeLog(nextDiagnostic.status === "limited" ? "warn" : "info", event, {
|
|
15428
|
+
conversation_id: conversation.conversation_id,
|
|
15429
|
+
terminal_bridge_message_id: expectedMessageId,
|
|
15430
|
+
detector_source: nextDiagnostic.source,
|
|
15431
|
+
diagnostic_fingerprint: nextDiagnostic.fingerprint
|
|
15432
|
+
});
|
|
15433
|
+
return {
|
|
15434
|
+
persisted: true,
|
|
15435
|
+
conversation: nextConversation,
|
|
15436
|
+
diagnostic: nextDiagnostic
|
|
15437
|
+
};
|
|
15438
|
+
});
|
|
15439
|
+
}
|
|
15440
|
+
finally {
|
|
15441
|
+
releaseLock();
|
|
15442
|
+
}
|
|
15443
|
+
}
|
|
12800
15444
|
function terminalBridgeApprovalCandidate({ executor, terminalControl, terminalStatus, fingerprint }) {
|
|
12801
15445
|
const approval = isRecord(terminalStatus?.approval_state) ? terminalStatus.approval_state : {};
|
|
12802
15446
|
if (approval.approvable !== true) {
|
|
@@ -15428,6 +18072,8 @@ function redactCliOutput(value) {
|
|
|
15428
18072
|
}
|
|
15429
18073
|
if (key === "claude_transcript_anchor" ||
|
|
15430
18074
|
key === "claudeTranscriptAnchor" ||
|
|
18075
|
+
key === "codex_rollout_acceptance_anchor" ||
|
|
18076
|
+
key === "codexRolloutAcceptanceAnchor" ||
|
|
15431
18077
|
key === "claude_home" ||
|
|
15432
18078
|
key === "claudeHome") {
|
|
15433
18079
|
return [];
|