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