@scotthuang/agent-knock-knock 0.8.0 → 0.9.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 +27 -0
- package/README.md +33 -15
- package/dist/src/agent-session-provider.d.ts +13 -0
- package/dist/src/cli.js +1178 -177
- package/dist/src/cli.js.map +1 -1
- package/dist/src/codex-local-session-provider.d.ts +3 -1
- package/dist/src/codex-local-session-provider.js +17 -0
- package/dist/src/codex-local-session-provider.js.map +1 -1
- package/dist/src/codex-store-adapter.d.ts +17 -0
- package/dist/src/codex-store-adapter.js +206 -0
- package/dist/src/codex-store-adapter.js.map +1 -1
- package/dist/src/openclaw-plugin-helpers.d.ts +15 -7
- package/dist/src/openclaw-plugin-helpers.js +162 -40
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin.js +591 -136
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/protocol.d.ts +22 -1
- package/dist/src/protocol.js +93 -4
- package/dist/src/protocol.js.map +1 -1
- package/dist/src/store.d.ts +4 -3
- package/dist/src/store.js +203 -10
- package/dist/src/store.js.map +1 -1
- package/dist/src/terminal-agent-adapter.d.ts +22 -0
- package/dist/src/terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +1 -0
- package/dist/src/terminal-agent-bridge.js +2 -1
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/docs/quickstart-tmux.md +12 -2
- package/openclaw.plugin.json +5 -1
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +23 -16
package/dist/src/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ import { createClaudeTerminalAgentAdapter } from "./claude-terminal-agent-adapte
|
|
|
10
10
|
import { captureClaudeTranscriptAnchor, defaultClaudeHome, detectClaudeTranscriptCompletion, detectClaudeTranscriptPendingApproval } from "./claude-local-transcript-provider.js";
|
|
11
11
|
import { CodexLocalSessionProvider } from "./codex-local-session-provider.js";
|
|
12
12
|
import { CodexStoreAdapter } from "./codex-store-adapter.js";
|
|
13
|
-
import { applyMessageToConversation, budgetAction, createConversation, createMessage, executorForConversation, extractStructuredMessage, parseMessageJson, resolveExecutor } from "./protocol.js";
|
|
13
|
+
import { applyMessageToConversation, budgetAction, createConversation, createMessage, executorForConversation, extractStructuredMessage, parseMessageJson, resolveExecutor, sessionIdForConversation, turnIdForConversation } from "./protocol.js";
|
|
14
14
|
import { EXECUTOR_KINDS, executorDefinitionForKind, isExecutorKind } from "./executors.js";
|
|
15
15
|
import { redactString, writeRuntimeLog } from "./runtime-log.js";
|
|
16
16
|
import { formatTranscript, readNdjsonLog } from "./transcript.js";
|
|
@@ -48,6 +48,16 @@ const TERMINAL_DISPATCH_RELEASE_STATUSES = new Set([
|
|
|
48
48
|
"closed",
|
|
49
49
|
"cancelled"
|
|
50
50
|
]);
|
|
51
|
+
const SESSION_SEND_BLOCKING_STATUSES = new Set([
|
|
52
|
+
"created",
|
|
53
|
+
"running",
|
|
54
|
+
"waiting_for_agent",
|
|
55
|
+
"waiting_for_openclaw",
|
|
56
|
+
"stalled",
|
|
57
|
+
"callback_pending",
|
|
58
|
+
"callback_failed",
|
|
59
|
+
"cancelling"
|
|
60
|
+
]);
|
|
51
61
|
const TERMINAL_BRIDGE_MONITOR_LOCK_VERSION = 1;
|
|
52
62
|
const MINIMUM_NODE_VERSION = "22.19.0";
|
|
53
63
|
const PRIVATE_LOCK_FILE_MODE = 0o600;
|
|
@@ -71,6 +81,7 @@ const CONVERSATION_STATUSES = new Set([
|
|
|
71
81
|
const SESSION_SELECTOR_COMMANDS = new Set([
|
|
72
82
|
"status",
|
|
73
83
|
"send",
|
|
84
|
+
"respond",
|
|
74
85
|
"approve",
|
|
75
86
|
"cancel",
|
|
76
87
|
"renew",
|
|
@@ -80,6 +91,7 @@ const SESSION_SELECTOR_COMMANDS = new Set([
|
|
|
80
91
|
const STORE_MUTATION_COMMANDS = new Set([
|
|
81
92
|
"delegate",
|
|
82
93
|
"send",
|
|
94
|
+
"respond",
|
|
83
95
|
"approve",
|
|
84
96
|
"cancel",
|
|
85
97
|
"renew",
|
|
@@ -95,13 +107,52 @@ class InlineCodexSessionAdapter {
|
|
|
95
107
|
processBatches;
|
|
96
108
|
processBatchIndex = 0;
|
|
97
109
|
rollouts;
|
|
98
|
-
|
|
110
|
+
activeSessionIdentities;
|
|
111
|
+
constructor({ threads, processes, rollouts, activeSessionIdentities }) {
|
|
99
112
|
this.threads = Array.isArray(threads) ? threads : [];
|
|
100
113
|
this.processBatches = Array.isArray(processes?.[0])
|
|
101
114
|
? processes
|
|
102
115
|
: [];
|
|
103
116
|
this.processes = Array.isArray(processes) && !Array.isArray(processes[0]) ? processes : [];
|
|
104
117
|
this.rollouts = new Map(Object.entries(rollouts ?? {}));
|
|
118
|
+
this.activeSessionIdentities = new Map(Object.entries(activeSessionIdentities ?? {}).flatMap(([pidValue, value]) => {
|
|
119
|
+
const pid = Number(pidValue);
|
|
120
|
+
if (!Number.isSafeInteger(pid) || pid <= 1 || !isRecord(value)) {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
const sessionId = stringValue(value.sessionId ?? value.session_id);
|
|
124
|
+
if (!sessionId) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
return [[pid, {
|
|
128
|
+
sessionId,
|
|
129
|
+
...(stringValue(value.processUuid ?? value.process_uuid)
|
|
130
|
+
? {
|
|
131
|
+
processUuid: stringValue(value.processUuid ?? value.process_uuid)
|
|
132
|
+
}
|
|
133
|
+
: {}),
|
|
134
|
+
...(stringValue(value.processBirth ?? value.process_birth)
|
|
135
|
+
? {
|
|
136
|
+
processBirth: stringValue(value.processBirth ?? value.process_birth)
|
|
137
|
+
}
|
|
138
|
+
: {}),
|
|
139
|
+
...(isRecord(value.rollout) &&
|
|
140
|
+
stringValue(value.rollout.fd) &&
|
|
141
|
+
stringValue(value.rollout.device) &&
|
|
142
|
+
stringValue(value.rollout.inode) &&
|
|
143
|
+
stringValue(value.rollout.path)
|
|
144
|
+
? {
|
|
145
|
+
rollout: {
|
|
146
|
+
fd: stringValue(value.rollout.fd),
|
|
147
|
+
device: stringValue(value.rollout.device),
|
|
148
|
+
inode: stringValue(value.rollout.inode),
|
|
149
|
+
path: stringValue(value.rollout.path)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
: {}),
|
|
153
|
+
evidence: stringValue(value.evidence) ?? "static_exact_fixture"
|
|
154
|
+
}]];
|
|
155
|
+
}));
|
|
105
156
|
}
|
|
106
157
|
async listThreadRows() {
|
|
107
158
|
return this.threads;
|
|
@@ -117,6 +168,9 @@ class InlineCodexSessionAdapter {
|
|
|
117
168
|
}
|
|
118
169
|
return this.processes;
|
|
119
170
|
}
|
|
171
|
+
async resolveActiveSessionIdentityForPid(pid) {
|
|
172
|
+
return this.activeSessionIdentities.get(pid);
|
|
173
|
+
}
|
|
120
174
|
}
|
|
121
175
|
const command = process.argv[2];
|
|
122
176
|
const rawArgs = process.argv.slice(3);
|
|
@@ -163,6 +217,9 @@ async function runCommand(commandName, options) {
|
|
|
163
217
|
else if (commandName === "send") {
|
|
164
218
|
await runSend(options);
|
|
165
219
|
}
|
|
220
|
+
else if (commandName === "respond") {
|
|
221
|
+
await runRespond(options);
|
|
222
|
+
}
|
|
166
223
|
else if (commandName === "approve") {
|
|
167
224
|
await runApprove(options);
|
|
168
225
|
}
|
|
@@ -534,7 +591,7 @@ function createTerminalProcessSource(options) {
|
|
|
534
591
|
}
|
|
535
592
|
return new SystemTerminalProcessSource();
|
|
536
593
|
}
|
|
537
|
-
function loadClaudeAgentRows(options = {}) {
|
|
594
|
+
function loadClaudeAgentRows(options = {}, observation = {}) {
|
|
538
595
|
let value;
|
|
539
596
|
if (options.claudeAgentsJson !== undefined) {
|
|
540
597
|
value = typeof options.claudeAgentsJson === "string"
|
|
@@ -547,6 +604,9 @@ function loadClaudeAgentRows(options = {}) {
|
|
|
547
604
|
else {
|
|
548
605
|
const claudeExecutable = resolveOptionalExecutable("claude");
|
|
549
606
|
if (!claudeExecutable) {
|
|
607
|
+
if (observation.required) {
|
|
608
|
+
throw new Error("Claude agent session observation is unavailable because the Claude CLI could not be resolved");
|
|
609
|
+
}
|
|
550
610
|
return [];
|
|
551
611
|
}
|
|
552
612
|
const result = spawnSync(claudeExecutable, ["agents", "--json", "--all"], {
|
|
@@ -560,6 +620,9 @@ function loadClaudeAgentRows(options = {}) {
|
|
|
560
620
|
error: result.error?.message,
|
|
561
621
|
stderr: textSummary(cleanProcessText(result.stderr))
|
|
562
622
|
});
|
|
623
|
+
if (observation.required) {
|
|
624
|
+
throw new Error("Claude agent session observation failed; refusing to treat the process as a virgin session");
|
|
625
|
+
}
|
|
563
626
|
return [];
|
|
564
627
|
}
|
|
565
628
|
try {
|
|
@@ -569,6 +632,9 @@ function loadClaudeAgentRows(options = {}) {
|
|
|
569
632
|
runtimeLog("warn", "claude_agents_list_invalid_json", {
|
|
570
633
|
stdout: textSummary(result.stdout)
|
|
571
634
|
});
|
|
635
|
+
if (observation.required) {
|
|
636
|
+
throw new Error("Claude agent session observation returned invalid JSON; refusing to treat the process as a virgin session");
|
|
637
|
+
}
|
|
572
638
|
return [];
|
|
573
639
|
}
|
|
574
640
|
}
|
|
@@ -576,7 +642,13 @@ function loadClaudeAgentRows(options = {}) {
|
|
|
576
642
|
? value
|
|
577
643
|
: isRecord(value) && Array.isArray(value.agents)
|
|
578
644
|
? value.agents
|
|
579
|
-
:
|
|
645
|
+
: undefined;
|
|
646
|
+
if (!rows) {
|
|
647
|
+
if (observation.required) {
|
|
648
|
+
throw new Error("Claude agent session observation returned an unsupported result shape; refusing to treat the process as a virgin session");
|
|
649
|
+
}
|
|
650
|
+
return [];
|
|
651
|
+
}
|
|
580
652
|
return rows.flatMap((row) => {
|
|
581
653
|
if (!isRecord(row) || !Number.isInteger(Number(row.pid))) {
|
|
582
654
|
return [];
|
|
@@ -666,7 +738,7 @@ function createTerminalAgentBridge(options, terminalProvider = createTerminalCon
|
|
|
666
738
|
return new TerminalAgentBridge({
|
|
667
739
|
registry,
|
|
668
740
|
terminalProvider,
|
|
669
|
-
async verifyIdentity({ agent, pid, terminalControl }) {
|
|
741
|
+
async verifyIdentity({ agent, pid, terminalControl, runtime }) {
|
|
670
742
|
const adapter = registry.require(agent);
|
|
671
743
|
const expectedWorkspace = options.workspace ?? terminalControl.currentPath;
|
|
672
744
|
if (!expectedWorkspace) {
|
|
@@ -689,6 +761,18 @@ function createTerminalAgentBridge(options, terminalProvider = createTerminalCon
|
|
|
689
761
|
}
|
|
690
762
|
assertConfiguredWorkspace(expectedWorkspace, snapshot.cwd, `terminal access to ${terminalControl.target} by agent process ${pid}`);
|
|
691
763
|
assertConfiguredWorkspace(expectedWorkspace, pane.currentPath, `terminal access to ${terminalControl.target} by tmux pane`);
|
|
764
|
+
const currentNativeIdentity = await resolveCurrentNativeAgentSessionIdentity({
|
|
765
|
+
options,
|
|
766
|
+
agent,
|
|
767
|
+
pid,
|
|
768
|
+
cwd: snapshot.cwd ?? pane.currentPath
|
|
769
|
+
});
|
|
770
|
+
assertNativeAgentIdentityForRuntime({
|
|
771
|
+
runtime,
|
|
772
|
+
currentIdentity: currentNativeIdentity,
|
|
773
|
+
agent,
|
|
774
|
+
pid
|
|
775
|
+
});
|
|
692
776
|
return {
|
|
693
777
|
terminalControl: {
|
|
694
778
|
...terminalControl,
|
|
@@ -751,11 +835,34 @@ function terminalRuntimeIdentityForConversation(conversation, terminalControl) {
|
|
|
751
835
|
const terminalIdentity = parseTerminalConversationId(nativeSessionId);
|
|
752
836
|
const explicitSessionId = stringValue(nativeTakeover?.terminal_agent_session_id) ??
|
|
753
837
|
(terminalIdentity ? undefined : nativeSessionId);
|
|
838
|
+
const nativeRollout = isRecord(nativeTakeover?.terminal_agent_rollout)
|
|
839
|
+
? nativeTakeover.terminal_agent_rollout
|
|
840
|
+
: undefined;
|
|
841
|
+
const strictNativeIdentity = Number(nativeTakeover?.terminal_agent_identity_protocol) === 1;
|
|
842
|
+
const requireNativeProcessUuid = strictNativeIdentity && executorForConversation(conversation).kind === "claude";
|
|
843
|
+
const requireNativeRolloutIdentity = strictNativeIdentity && executorForConversation(conversation).kind === "codex";
|
|
754
844
|
return {
|
|
755
845
|
pid: Number.isInteger(Number(nativeTakeover?.terminal_agent_pid))
|
|
756
846
|
? Number(nativeTakeover?.terminal_agent_pid)
|
|
757
847
|
: terminalIdentity?.pid,
|
|
758
848
|
sessionId: explicitSessionId,
|
|
849
|
+
nativeSessionId: stringValue(nativeTakeover?.terminal_agent_session_id),
|
|
850
|
+
nativeProcessUuid: stringValue(nativeTakeover?.terminal_agent_process_uuid),
|
|
851
|
+
nativeProcessBirth: stringValue(nativeTakeover?.terminal_agent_process_birth),
|
|
852
|
+
requireNativeProcessUuid,
|
|
853
|
+
requireNativeRolloutIdentity,
|
|
854
|
+
...(nativeRollout
|
|
855
|
+
? {
|
|
856
|
+
nativeRollout: {
|
|
857
|
+
fd: String(nativeRollout.fd ?? ""),
|
|
858
|
+
device: String(nativeRollout.device ?? ""),
|
|
859
|
+
inode: String(nativeRollout.inode ?? ""),
|
|
860
|
+
path: String(nativeRollout.path ?? "")
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
: {}),
|
|
864
|
+
expectedEmptyNativeSession: strictNativeIdentity &&
|
|
865
|
+
!stringValue(nativeTakeover?.terminal_agent_session_id),
|
|
759
866
|
cwd: stringValue(nativeTakeover?.source_cwd) ?? terminalControl.currentPath,
|
|
760
867
|
conversationId: stringValue(conversation?.conversation_id),
|
|
761
868
|
messageId: stringValue(nativeTakeover?.terminal_bridge_message_id),
|
|
@@ -1225,6 +1332,8 @@ function withTerminalBridgeSubmission({ conversation, messageId, requestText, st
|
|
|
1225
1332
|
...nativeTakeover,
|
|
1226
1333
|
terminal_bridge_submission: {
|
|
1227
1334
|
status,
|
|
1335
|
+
session_id: sessionIdForConversation(conversation),
|
|
1336
|
+
turn_id: turnIdForConversation(conversation),
|
|
1228
1337
|
message_id: messageId,
|
|
1229
1338
|
request_hash: terminalBridgeRequestFingerprint(requestText),
|
|
1230
1339
|
prepared_at: preparedAt,
|
|
@@ -1513,14 +1622,21 @@ async function terminalControlDiagnostics(provider) {
|
|
|
1513
1622
|
};
|
|
1514
1623
|
}
|
|
1515
1624
|
function managedTurnListEntry(task, { terminalBridge = false, approvalState, conversation } = {}) {
|
|
1625
|
+
const sessionId = stringValue(task.session_id) ??
|
|
1626
|
+
stringValue(task.conversation_id);
|
|
1627
|
+
const turnId = stringValue(task.turn_id) ??
|
|
1628
|
+
stringValue(task.conversation_id) ??
|
|
1629
|
+
String(task.id ?? "");
|
|
1516
1630
|
const entry = {
|
|
1517
1631
|
...task,
|
|
1518
|
-
|
|
1519
|
-
|
|
1632
|
+
session_id: sessionId,
|
|
1633
|
+
turn_id: turnId,
|
|
1634
|
+
id: turnId,
|
|
1635
|
+
short_ref: sessionShortRef(turnId),
|
|
1520
1636
|
source: "managed_turn",
|
|
1521
1637
|
...(approvalState ? { approval_state: approvalState } : {}),
|
|
1522
1638
|
commands: {
|
|
1523
|
-
|
|
1639
|
+
respond: task.status === "waiting_for_openclaw",
|
|
1524
1640
|
cancel: isWaitingForAgent(task.status),
|
|
1525
1641
|
close: task.status !== "closed",
|
|
1526
1642
|
status: true,
|
|
@@ -1546,6 +1662,23 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
1546
1662
|
terminalTarget: terminalControl.target
|
|
1547
1663
|
});
|
|
1548
1664
|
const orphanedDispatch = orphanedTerminalDispatchForRecovery(terminalControl);
|
|
1665
|
+
let nativeAgentIdentity;
|
|
1666
|
+
try {
|
|
1667
|
+
nativeAgentIdentity = await resolveCurrentNativeAgentSessionIdentity({
|
|
1668
|
+
options,
|
|
1669
|
+
agent: session.agent,
|
|
1670
|
+
pid: session.pid,
|
|
1671
|
+
cwd: session.cwd ?? terminalControl.currentPath
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
catch (error) {
|
|
1675
|
+
runtimeLog("warn", "terminal_native_session_identity_unavailable", {
|
|
1676
|
+
agent: session.agent,
|
|
1677
|
+
terminal_target: terminalControl.target,
|
|
1678
|
+
pid: session.pid,
|
|
1679
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1549
1682
|
const entry = {
|
|
1550
1683
|
id: bridge.terminalConversationId(session),
|
|
1551
1684
|
short_ref: sessionShortRef(bridge.terminalConversationId(session)),
|
|
@@ -1558,7 +1691,11 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
1558
1691
|
cwd: session.cwd,
|
|
1559
1692
|
workspace: session.cwd,
|
|
1560
1693
|
elapsed: session.elapsed,
|
|
1561
|
-
|
|
1694
|
+
native_agent_session_id: nativeAgentIdentity?.sessionId,
|
|
1695
|
+
native_agent_process_uuid: nativeAgentIdentity?.processUuid,
|
|
1696
|
+
native_agent_process_birth: nativeAgentIdentity?.processBirth,
|
|
1697
|
+
native_agent_rollout: nativeAgentIdentity?.rollout,
|
|
1698
|
+
native_agent_identity_evidence: nativeAgentIdentity?.evidence,
|
|
1562
1699
|
confidence: session.confidence,
|
|
1563
1700
|
reason: session.reason,
|
|
1564
1701
|
terminal_control: terminalControl,
|
|
@@ -1627,13 +1764,32 @@ function terminalFirstListProjection({ terminals, allConversations, displayedCon
|
|
|
1627
1764
|
!allRelated.some((conversation) => conversation.conversation_id === ownership.conversation.conversation_id)) {
|
|
1628
1765
|
allRelated.push(ownership.conversation);
|
|
1629
1766
|
}
|
|
1767
|
+
const sessionIds = new Set(allRelated.map((conversation) => sessionIdForConversation(conversation)));
|
|
1768
|
+
const managedSessionId = ownership.state === "current"
|
|
1769
|
+
? sessionIdForConversation(ownership.conversation)
|
|
1770
|
+
: [...displayedRelated]
|
|
1771
|
+
.sort(compareManagedConversationRecency)
|
|
1772
|
+
.map((conversation) => sessionIdForConversation(conversation))[0] ??
|
|
1773
|
+
[...allRelated]
|
|
1774
|
+
.sort(compareManagedConversationRecency)
|
|
1775
|
+
.map((conversation) => sessionIdForConversation(conversation))[0];
|
|
1776
|
+
const sessionAllRelated = managedSessionId
|
|
1777
|
+
? allRelated.filter((conversation) => sessionIdForConversation(conversation) === managedSessionId)
|
|
1778
|
+
: [];
|
|
1779
|
+
const sessionDisplayedRelated = managedSessionId
|
|
1780
|
+
? displayedRelated.filter((conversation) => sessionIdForConversation(conversation) === managedSessionId)
|
|
1781
|
+
: [];
|
|
1782
|
+
const sessionBindingTurn = [...sessionAllRelated]
|
|
1783
|
+
.sort(compareManagedConversationRecency)[0];
|
|
1784
|
+
const sessionBindingMatchesLiveTerminal = Boolean(sessionBindingTurn &&
|
|
1785
|
+
managedTurnMatchesLiveTerminal(sessionBindingTurn, terminal));
|
|
1630
1786
|
const currentTurnValue = ownership.state === "current"
|
|
1631
1787
|
? currentManagedTurnForTerminal(ownership.conversation, terminal, rawActions)
|
|
1632
1788
|
: undefined;
|
|
1633
1789
|
const currentTurn = currentTurnValue && !mutationsAllowed
|
|
1634
1790
|
? readOnlyManagedTurn(currentTurnValue)
|
|
1635
1791
|
: currentTurnValue;
|
|
1636
|
-
const sortedDisplayed = [...
|
|
1792
|
+
const sortedDisplayed = [...sessionDisplayedRelated]
|
|
1637
1793
|
.filter((conversation) => conversation.conversation_id !== currentTurn?.conversation_id)
|
|
1638
1794
|
.sort(compareManagedConversationRecency);
|
|
1639
1795
|
const recentConversation = currentTurn ? undefined : sortedDisplayed[0];
|
|
@@ -1654,17 +1810,29 @@ function terminalFirstListProjection({ terminals, allConversations, displayedCon
|
|
|
1654
1810
|
.map((turn) => stringValue(turn?.conversation_id))
|
|
1655
1811
|
.filter((id) => id !== undefined));
|
|
1656
1812
|
const management = {
|
|
1813
|
+
session_id: managedSessionId ?? null,
|
|
1814
|
+
session_short_ref: managedSessionId
|
|
1815
|
+
? sessionShortRef(managedSessionId)
|
|
1816
|
+
: null,
|
|
1657
1817
|
current_turn: currentTurn ?? null,
|
|
1658
1818
|
recent_turn: recentTurn ?? null,
|
|
1659
|
-
turn_count:
|
|
1660
|
-
hidden_turn_count:
|
|
1819
|
+
turn_count: sessionAllRelated.length,
|
|
1820
|
+
hidden_turn_count: sessionAllRelated.filter((conversation) => !visibleTurnIds.has(conversation.conversation_id)).length,
|
|
1821
|
+
session_count: sessionIds.size,
|
|
1661
1822
|
...(includeAll ? { history } : {})
|
|
1662
1823
|
};
|
|
1663
1824
|
const availableActions = ownership.state === "current"
|
|
1664
1825
|
? currentTerminalActions(currentTurn)
|
|
1665
1826
|
: ownership.state === "conflict"
|
|
1666
1827
|
? safeTerminalActionsDuringConflict(rawActions)
|
|
1667
|
-
:
|
|
1828
|
+
: managedSessionId &&
|
|
1829
|
+
sessionBindingMatchesLiveTerminal &&
|
|
1830
|
+
isRecord(rawActions.send)
|
|
1831
|
+
? {
|
|
1832
|
+
...rawActions,
|
|
1833
|
+
send: sendActionForManagedSession(rawActions.send, managedSessionId)
|
|
1834
|
+
}
|
|
1835
|
+
: rawActions;
|
|
1668
1836
|
return {
|
|
1669
1837
|
...terminal,
|
|
1670
1838
|
management_state: ownership.state === "current"
|
|
@@ -1871,7 +2039,7 @@ function currentTerminalActions(currentTurn) {
|
|
|
1871
2039
|
return {};
|
|
1872
2040
|
}
|
|
1873
2041
|
const actions = {};
|
|
1874
|
-
for (const action of ["status", "approve", "cancel", "renew", "retry_callback"]) {
|
|
2042
|
+
for (const action of ["status", "respond", "approve", "cancel", "renew", "retry_callback"]) {
|
|
1875
2043
|
if (isRecord(currentTurn.available_actions[action])) {
|
|
1876
2044
|
actions[action] = currentTurn.available_actions[action];
|
|
1877
2045
|
}
|
|
@@ -1887,6 +2055,18 @@ function safeTerminalActionsDuringConflict(rawActions) {
|
|
|
1887
2055
|
}
|
|
1888
2056
|
return actions;
|
|
1889
2057
|
}
|
|
2058
|
+
function sendActionForManagedSession(action, sessionId) {
|
|
2059
|
+
const { selector: _selector, ...existingArguments } = isRecord(action.arguments)
|
|
2060
|
+
? action.arguments
|
|
2061
|
+
: {};
|
|
2062
|
+
return {
|
|
2063
|
+
...action,
|
|
2064
|
+
arguments: {
|
|
2065
|
+
...existingArguments,
|
|
2066
|
+
session_id: sessionId
|
|
2067
|
+
}
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
1890
2070
|
function safeUnavailableManagedTurnActions(actionsValue) {
|
|
1891
2071
|
const actions = {};
|
|
1892
2072
|
for (const action of ["status", "retry_callback", "close"]) {
|
|
@@ -1919,11 +2099,6 @@ function historicalManagedTurnForTerminal(conversation, terminalCanAcceptSend, t
|
|
|
1919
2099
|
? managedTurn.available_actions
|
|
1920
2100
|
: {};
|
|
1921
2101
|
const safeActions = safeUnavailableManagedTurnActions(availableActions);
|
|
1922
|
-
if (terminalCanAcceptSend &&
|
|
1923
|
-
managedTurnMatchesLiveTerminal(conversation, terminal) &&
|
|
1924
|
-
isRecord(availableActions.follow_up)) {
|
|
1925
|
-
safeActions.follow_up = availableActions.follow_up;
|
|
1926
|
-
}
|
|
1927
2102
|
return {
|
|
1928
2103
|
...managedTurn,
|
|
1929
2104
|
available_actions: safeActions
|
|
@@ -1948,9 +2123,31 @@ function managedTurnMatchesLiveTerminal(conversation, terminal) {
|
|
|
1948
2123
|
terminalControlSelectorKey(liveControl)) {
|
|
1949
2124
|
return false;
|
|
1950
2125
|
}
|
|
1951
|
-
const
|
|
1952
|
-
const
|
|
1953
|
-
|
|
2126
|
+
const liveSessionId = stringValue(terminal.native_agent_session_id);
|
|
2127
|
+
const liveProcessUuid = stringValue(terminal.native_agent_process_uuid);
|
|
2128
|
+
const liveProcessBirth = stringValue(terminal.native_agent_process_birth);
|
|
2129
|
+
const liveRollout = isRecord(terminal.native_agent_rollout)
|
|
2130
|
+
? terminal.native_agent_rollout
|
|
2131
|
+
: undefined;
|
|
2132
|
+
const liveNativeIdentity = liveSessionId
|
|
2133
|
+
? {
|
|
2134
|
+
sessionId: liveSessionId,
|
|
2135
|
+
...(liveProcessUuid ? { processUuid: liveProcessUuid } : {}),
|
|
2136
|
+
...(liveProcessBirth ? { processBirth: liveProcessBirth } : {}),
|
|
2137
|
+
...(liveRollout
|
|
2138
|
+
? {
|
|
2139
|
+
rollout: {
|
|
2140
|
+
fd: String(liveRollout.fd ?? ""),
|
|
2141
|
+
device: String(liveRollout.device ?? ""),
|
|
2142
|
+
inode: String(liveRollout.inode ?? ""),
|
|
2143
|
+
path: String(liveRollout.path ?? "")
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
: {}),
|
|
2147
|
+
evidence: "live_terminal"
|
|
2148
|
+
}
|
|
2149
|
+
: undefined;
|
|
2150
|
+
if (!nativeAgentIdentityMatchesTurn(conversation, liveNativeIdentity)) {
|
|
1954
2151
|
return false;
|
|
1955
2152
|
}
|
|
1956
2153
|
const liveWorkspace = terminal.workspace ?? terminal.cwd;
|
|
@@ -2002,7 +2199,8 @@ function retargetConversationAction(action, conversationId) {
|
|
|
2002
2199
|
...action,
|
|
2003
2200
|
arguments: {
|
|
2004
2201
|
...(isRecord(action.arguments) ? action.arguments : {}),
|
|
2005
|
-
|
|
2202
|
+
turn_id: conversationId,
|
|
2203
|
+
conversation_id: undefined
|
|
2006
2204
|
},
|
|
2007
2205
|
...(beforeCall
|
|
2008
2206
|
? {
|
|
@@ -2012,7 +2210,8 @@ function retargetConversationAction(action, conversationId) {
|
|
|
2012
2210
|
...(isRecord(beforeCall.arguments)
|
|
2013
2211
|
? beforeCall.arguments
|
|
2014
2212
|
: {}),
|
|
2015
|
-
|
|
2213
|
+
turn_id: conversationId,
|
|
2214
|
+
conversation_id: undefined
|
|
2016
2215
|
}
|
|
2017
2216
|
}
|
|
2018
2217
|
}
|
|
@@ -2083,9 +2282,6 @@ function childPidsForRoot(root, processes) {
|
|
|
2083
2282
|
.filter((process) => process.agent === root.agent && process.ppid === root.pid)
|
|
2084
2283
|
.map((process) => process.pid);
|
|
2085
2284
|
}
|
|
2086
|
-
function canFollowUpManagedTurn(status) {
|
|
2087
|
-
return !["done", "failed", "closed", "cancelled"].includes(status);
|
|
2088
|
-
}
|
|
2089
2285
|
function managedListApprovalState(conversation) {
|
|
2090
2286
|
if (!terminalBridgeEnabled(conversation) ||
|
|
2091
2287
|
!["waiting_for_agent", "waiting_for_openclaw"].includes(String(conversation.status))) {
|
|
@@ -2121,10 +2317,13 @@ function managedListApprovalState(conversation) {
|
|
|
2121
2317
|
}
|
|
2122
2318
|
function listActionContracts() {
|
|
2123
2319
|
return {
|
|
2124
|
-
version:
|
|
2320
|
+
version: 4,
|
|
2125
2321
|
instructions: [
|
|
2126
2322
|
"Treat terminals[] as the primary resource and use only actions present in available_actions.",
|
|
2127
|
-
"
|
|
2323
|
+
"An existing managed session's ordinary send targets session_id and creates a new turn. A turn id is never an ordinary send target.",
|
|
2324
|
+
"For first attach only, use the selector prefilled by that unmanaged raw-terminal row's available send action; never construct, guess, or reuse it.",
|
|
2325
|
+
"Use respond only for an in-flight turn that is explicitly waiting for OpenClaw.",
|
|
2326
|
+
"Managed controls target turn_id. A raw terminal may be controlled only through its own list-prefilled conversation_id action; never construct, guess, or reuse that compatibility selector.",
|
|
2128
2327
|
"Start with the action's prefilled arguments, supply every missing_required field, and consult the top-level action's optional fields only when needed.",
|
|
2129
2328
|
"Authoritative full IDs are prefilled; short_ref is for display and human input.",
|
|
2130
2329
|
"Availability is a snapshot. AKK revalidates process, tmux pane, workspace, activity, approval, and recovery state before side effects."
|
|
@@ -2144,7 +2343,8 @@ function listActionContracts() {
|
|
|
2144
2343
|
},
|
|
2145
2344
|
managed: {
|
|
2146
2345
|
current_turn: "the authoritative dispatch-ledger owner, never inferred from history",
|
|
2147
|
-
recent_turn: "the latest visible non-owning turn
|
|
2346
|
+
recent_turn: "the latest visible non-owning turn in the current managed session",
|
|
2347
|
+
session_id: "the continuing agent context and authoritative ordinary-send target",
|
|
2148
2348
|
history: "older turns, present only with --all"
|
|
2149
2349
|
},
|
|
2150
2350
|
available_actions: {
|
|
@@ -2155,7 +2355,9 @@ function listActionContracts() {
|
|
|
2155
2355
|
actions: {
|
|
2156
2356
|
send: {
|
|
2157
2357
|
tool: "agent_knock_knock_send",
|
|
2158
|
-
target_argument: "
|
|
2358
|
+
target_argument: "session_id",
|
|
2359
|
+
initial_attach_target_argument: "selector",
|
|
2360
|
+
initial_attach_scope: "Only the selector prefilled by an unmanaged raw-terminal row's available send action; never construct, guess, or reuse it.",
|
|
2159
2361
|
required: ["request"],
|
|
2160
2362
|
optional: [
|
|
2161
2363
|
"selector",
|
|
@@ -2165,55 +2367,61 @@ function listActionContracts() {
|
|
|
2165
2367
|
"agentHardTimeoutMinutes"
|
|
2166
2368
|
],
|
|
2167
2369
|
unsupported: ["timeoutSeconds"],
|
|
2168
|
-
ordinary_use: "
|
|
2370
|
+
ordinary_use: "Create a new managed turn in the selected session. A live terminal selector is accepted only for initial attach/discovery compatibility."
|
|
2169
2371
|
},
|
|
2170
|
-
|
|
2171
|
-
tool: "
|
|
2172
|
-
target_argument: "
|
|
2173
|
-
required: ["request"],
|
|
2174
|
-
|
|
2175
|
-
"selector",
|
|
2176
|
-
"idleTimeoutMinutes",
|
|
2177
|
-
"agentTimeoutMinutes",
|
|
2178
|
-
"agentHardTimeoutMinutes"
|
|
2179
|
-
],
|
|
2180
|
-
ordinary_use: "Continue the explicitly selected managed turn. Start from its prefilled selector and add request."
|
|
2372
|
+
respond: {
|
|
2373
|
+
tool: "agent_knock_knock_respond",
|
|
2374
|
+
target_argument: "turn_id",
|
|
2375
|
+
required: ["turn_id", "request"],
|
|
2376
|
+
ordinary_use: "Answer an agent question inside the explicitly selected in-flight turn without creating another turn."
|
|
2181
2377
|
},
|
|
2182
2378
|
status: {
|
|
2183
2379
|
tool: "agent_knock_knock_status",
|
|
2184
|
-
target_argument: "
|
|
2185
|
-
|
|
2380
|
+
target_argument: "turn_id",
|
|
2381
|
+
compatibility_target_argument: "conversation_id",
|
|
2382
|
+
compatibility_scope: "A deprecated legacy Turn alias, or only the exact selector prefilled by an unmanaged raw-terminal row's available status action; never construct, guess, or reuse it.",
|
|
2383
|
+
required: ["turn_id"],
|
|
2186
2384
|
optional: ["idleTimeoutMinutes", "trace"]
|
|
2187
2385
|
},
|
|
2188
2386
|
approve: {
|
|
2189
2387
|
tool: "agent_knock_knock_approve",
|
|
2190
|
-
target_argument: "
|
|
2191
|
-
|
|
2388
|
+
target_argument: "turn_id",
|
|
2389
|
+
compatibility_target_argument: "conversation_id",
|
|
2390
|
+
compatibility_scope: "A deprecated legacy Turn alias, or only the exact selector prefilled by an unmanaged raw-terminal row's available approval action; never construct, guess, or reuse it.",
|
|
2391
|
+
required: ["turn_id", "expected_approval_fingerprint"],
|
|
2192
2392
|
requires_explicit_user_confirmation: true,
|
|
2193
2393
|
requires_fresh_status: true
|
|
2194
2394
|
},
|
|
2195
2395
|
cancel: {
|
|
2196
2396
|
tool: "agent_knock_knock_cancel",
|
|
2197
|
-
target_argument: "
|
|
2198
|
-
|
|
2397
|
+
target_argument: "turn_id",
|
|
2398
|
+
compatibility_target_argument: "conversation_id",
|
|
2399
|
+
compatibility_scope: "A deprecated legacy Turn alias, or only the exact selector prefilled by an unmanaged raw-terminal row's available cancellation action; never construct, guess, or reuse it.",
|
|
2400
|
+
required: ["turn_id"],
|
|
2199
2401
|
optional: ["idleTimeoutMinutes"],
|
|
2200
2402
|
requires_user_intent: true
|
|
2201
2403
|
},
|
|
2202
2404
|
renew: {
|
|
2203
2405
|
tool: "agent_knock_knock_renew",
|
|
2204
|
-
target_argument: "
|
|
2205
|
-
|
|
2406
|
+
target_argument: "turn_id",
|
|
2407
|
+
compatibility_target_argument: "conversation_id",
|
|
2408
|
+
compatibility_scope: "Deprecated legacy Turn alias only; unmanaged raw-terminal rows never advertise renew.",
|
|
2409
|
+
required: ["turn_id"],
|
|
2206
2410
|
optional: ["minutes"]
|
|
2207
2411
|
},
|
|
2208
2412
|
retry_callback: {
|
|
2209
2413
|
tool: "agent_knock_knock_retry_callback",
|
|
2210
|
-
target_argument: "
|
|
2211
|
-
|
|
2414
|
+
target_argument: "turn_id",
|
|
2415
|
+
compatibility_target_argument: "conversation_id",
|
|
2416
|
+
compatibility_scope: "Deprecated legacy Turn alias only; unmanaged raw-terminal rows never advertise callback retry.",
|
|
2417
|
+
required: ["turn_id"]
|
|
2212
2418
|
},
|
|
2213
2419
|
close: {
|
|
2214
2420
|
tool: "agent_knock_knock_close",
|
|
2215
|
-
target_argument: "
|
|
2216
|
-
|
|
2421
|
+
target_argument: "turn_id",
|
|
2422
|
+
compatibility_target_argument: "conversation_id",
|
|
2423
|
+
compatibility_scope: "A deprecated legacy Turn alias, or only the exact selector prefilled by an unmanaged raw-terminal row's orphan-close action; never construct, guess, or reuse it.",
|
|
2424
|
+
required: ["turn_id"],
|
|
2217
2425
|
optional: ["reason", "expected_message_id"],
|
|
2218
2426
|
requires_explicit_user_confirmation: true
|
|
2219
2427
|
}
|
|
@@ -2226,14 +2434,17 @@ function availableListActions(entry, { conversation } = {}) {
|
|
|
2226
2434
|
return {};
|
|
2227
2435
|
}
|
|
2228
2436
|
const commands = isRecord(entry.commands) ? entry.commands : {};
|
|
2437
|
+
const managed = entry.source === "managed_turn";
|
|
2438
|
+
const targetArguments = managed
|
|
2439
|
+
? { turn_id: id }
|
|
2440
|
+
: { conversation_id: id };
|
|
2229
2441
|
const actions = {
|
|
2230
2442
|
status: {
|
|
2231
2443
|
tool: "agent_knock_knock_status",
|
|
2232
|
-
arguments:
|
|
2444
|
+
arguments: targetArguments
|
|
2233
2445
|
}
|
|
2234
2446
|
};
|
|
2235
2447
|
const terminalControlled = entry.source === "terminal";
|
|
2236
|
-
const managed = entry.source === "managed_turn";
|
|
2237
2448
|
const approvalState = isRecord(entry.approval_state)
|
|
2238
2449
|
? entry.approval_state
|
|
2239
2450
|
: {};
|
|
@@ -2244,20 +2455,28 @@ function availableListActions(entry, { conversation } = {}) {
|
|
|
2244
2455
|
const terminalBridgeReady = managed &&
|
|
2245
2456
|
terminalBridgeEnabled(conversation) &&
|
|
2246
2457
|
terminalControlFromTakeover(nativeTakeover) !== undefined;
|
|
2247
|
-
if (
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
(terminalControlled &&
|
|
2253
|
-
entry.activity_state === "idle" &&
|
|
2254
|
-
approvalState.blocked !== true))) {
|
|
2255
|
-
actions[managed ? "follow_up" : "send"] = {
|
|
2458
|
+
if (terminalControlled &&
|
|
2459
|
+
commands.send === true &&
|
|
2460
|
+
entry.activity_state === "idle" &&
|
|
2461
|
+
approvalState.blocked !== true) {
|
|
2462
|
+
actions.send = {
|
|
2256
2463
|
tool: "agent_knock_knock_send",
|
|
2257
2464
|
arguments: { selector: id },
|
|
2258
2465
|
missing_required: ["request"]
|
|
2259
2466
|
};
|
|
2260
2467
|
}
|
|
2468
|
+
if (managed &&
|
|
2469
|
+
commands.respond === true &&
|
|
2470
|
+
terminalBridgeReady &&
|
|
2471
|
+
entry.status === "waiting_for_openclaw" &&
|
|
2472
|
+
!managedApprovalPending &&
|
|
2473
|
+
approvalState.blocked !== true) {
|
|
2474
|
+
actions.respond = {
|
|
2475
|
+
tool: "agent_knock_knock_respond",
|
|
2476
|
+
arguments: { turn_id: id },
|
|
2477
|
+
missing_required: ["request"]
|
|
2478
|
+
};
|
|
2479
|
+
}
|
|
2261
2480
|
const approvalFingerprint = stringValue(approvalState.fingerprint);
|
|
2262
2481
|
const managedApprovalEligible = terminalBridgeReady &&
|
|
2263
2482
|
entry.status === "waiting_for_openclaw" &&
|
|
@@ -2271,11 +2490,11 @@ function availableListActions(entry, { conversation } = {}) {
|
|
|
2271
2490
|
managedApprovalEligible)) {
|
|
2272
2491
|
actions.approve = {
|
|
2273
2492
|
tool: "agent_knock_knock_approve",
|
|
2274
|
-
arguments:
|
|
2493
|
+
arguments: targetArguments,
|
|
2275
2494
|
missing_required: ["expected_approval_fingerprint"],
|
|
2276
2495
|
before_call: {
|
|
2277
2496
|
tool: "agent_knock_knock_status",
|
|
2278
|
-
arguments:
|
|
2497
|
+
arguments: targetArguments,
|
|
2279
2498
|
use: "After explicit user confirmation, copy the latest terminal_status.approval_state.fingerprint into expected_approval_fingerprint."
|
|
2280
2499
|
},
|
|
2281
2500
|
requires_explicit_user_confirmation: true,
|
|
@@ -2294,14 +2513,14 @@ function availableListActions(entry, { conversation } = {}) {
|
|
|
2294
2513
|
if (rawCancellable || managedCancellable) {
|
|
2295
2514
|
actions.cancel = {
|
|
2296
2515
|
tool: "agent_knock_knock_cancel",
|
|
2297
|
-
arguments:
|
|
2516
|
+
arguments: targetArguments,
|
|
2298
2517
|
requires_user_intent: true
|
|
2299
2518
|
};
|
|
2300
2519
|
}
|
|
2301
2520
|
if (terminalBridgeReady && entry.status === "stalled") {
|
|
2302
2521
|
actions.renew = {
|
|
2303
2522
|
tool: "agent_knock_knock_renew",
|
|
2304
|
-
arguments:
|
|
2523
|
+
arguments: targetArguments
|
|
2305
2524
|
};
|
|
2306
2525
|
}
|
|
2307
2526
|
const callbackDelivery = isRecord(conversation?.callback_delivery)
|
|
@@ -2312,7 +2531,7 @@ function availableListActions(entry, { conversation } = {}) {
|
|
|
2312
2531
|
isRetryableCallbackDelivery(conversation, callbackDelivery)) {
|
|
2313
2532
|
actions.retry_callback = {
|
|
2314
2533
|
tool: "agent_knock_knock_retry_callback",
|
|
2315
|
-
arguments:
|
|
2534
|
+
arguments: targetArguments
|
|
2316
2535
|
};
|
|
2317
2536
|
}
|
|
2318
2537
|
if (commands.close === true) {
|
|
@@ -2323,7 +2542,7 @@ function availableListActions(entry, { conversation } = {}) {
|
|
|
2323
2542
|
actions.close = {
|
|
2324
2543
|
tool: "agent_knock_knock_close",
|
|
2325
2544
|
arguments: {
|
|
2326
|
-
conversation_id: id,
|
|
2545
|
+
...(managed ? { turn_id: id } : { conversation_id: id }),
|
|
2327
2546
|
...(expectedMessageId
|
|
2328
2547
|
? { expected_message_id: expectedMessageId }
|
|
2329
2548
|
: {})
|
|
@@ -2338,17 +2557,32 @@ async function resolveConversationSelectorOption(commandName, options) {
|
|
|
2338
2557
|
options.state) {
|
|
2339
2558
|
return;
|
|
2340
2559
|
}
|
|
2341
|
-
const
|
|
2560
|
+
const sendOperation = commandName === "send";
|
|
2561
|
+
const supplied = stringValue(sendOperation
|
|
2562
|
+
? options.session ?? options.conversation ?? options.conversationId
|
|
2563
|
+
: options.turn ?? options.conversation ?? options.conversationId)?.trim();
|
|
2342
2564
|
if (supplied && !isSessionSelectorSyntax(supplied)) {
|
|
2343
2565
|
// Full authoritative IDs keep their existing command-specific validation
|
|
2344
2566
|
// path. This avoids a discovery scan before option validation and preserves
|
|
2345
2567
|
// precise downstream errors for closed or currently non-actionable state.
|
|
2568
|
+
if (sendOperation) {
|
|
2569
|
+
options.session = supplied;
|
|
2570
|
+
}
|
|
2571
|
+
else {
|
|
2572
|
+
options.turn = supplied;
|
|
2573
|
+
}
|
|
2346
2574
|
return;
|
|
2347
2575
|
}
|
|
2348
2576
|
const candidates = await sessionSelectorCandidates(commandName, options);
|
|
2349
2577
|
const resolution = resolveSessionSelector(supplied, candidates, {
|
|
2350
2578
|
operation: commandName
|
|
2351
2579
|
});
|
|
2580
|
+
if (sendOperation) {
|
|
2581
|
+
options.session = resolution.id;
|
|
2582
|
+
}
|
|
2583
|
+
else {
|
|
2584
|
+
options.turn = resolution.id;
|
|
2585
|
+
}
|
|
2352
2586
|
options.conversation = resolution.id;
|
|
2353
2587
|
delete options.conversationId;
|
|
2354
2588
|
}
|
|
@@ -2389,6 +2623,86 @@ async function sessionSelectorCandidates(commandName, options) {
|
|
|
2389
2623
|
mutationsAllowed
|
|
2390
2624
|
});
|
|
2391
2625
|
const observedAtMs = Date.now();
|
|
2626
|
+
if (commandName === "send") {
|
|
2627
|
+
const sessionEntries = terminalProjection.terminals.flatMap((entry) => {
|
|
2628
|
+
const managedState = isRecord(entry.managed) ? entry.managed : undefined;
|
|
2629
|
+
const recentTurn = isRecord(managedState?.recent_turn)
|
|
2630
|
+
? managedState.recent_turn
|
|
2631
|
+
: undefined;
|
|
2632
|
+
const currentTurn = isRecord(managedState?.current_turn)
|
|
2633
|
+
? managedState.current_turn
|
|
2634
|
+
: undefined;
|
|
2635
|
+
const sessionId = stringValue(managedState?.session_id);
|
|
2636
|
+
const actions = isRecord(entry.available_actions)
|
|
2637
|
+
? entry.available_actions
|
|
2638
|
+
: undefined;
|
|
2639
|
+
const sendAction = isRecord(actions?.send)
|
|
2640
|
+
? actions.send
|
|
2641
|
+
: undefined;
|
|
2642
|
+
const sendArguments = isRecord(sendAction?.arguments)
|
|
2643
|
+
? sendAction.arguments
|
|
2644
|
+
: undefined;
|
|
2645
|
+
if (!sessionId ||
|
|
2646
|
+
!sendAction ||
|
|
2647
|
+
stringValue(sendArguments?.session_id) !== sessionId) {
|
|
2648
|
+
return [];
|
|
2649
|
+
}
|
|
2650
|
+
const commonEntry = {
|
|
2651
|
+
agent: entry.agent,
|
|
2652
|
+
status: "idle",
|
|
2653
|
+
workspace: entry.workspace ?? entry.cwd,
|
|
2654
|
+
updated_at: recentTurn?.updated_at ??
|
|
2655
|
+
currentTurn?.updated_at,
|
|
2656
|
+
available_actions: {
|
|
2657
|
+
send: sendAction
|
|
2658
|
+
}
|
|
2659
|
+
};
|
|
2660
|
+
return [
|
|
2661
|
+
{
|
|
2662
|
+
...commonEntry,
|
|
2663
|
+
id: sessionId,
|
|
2664
|
+
short_ref: sessionShortRef(sessionId),
|
|
2665
|
+
source: "managed_session"
|
|
2666
|
+
},
|
|
2667
|
+
{
|
|
2668
|
+
...commonEntry,
|
|
2669
|
+
id: String(entry.id),
|
|
2670
|
+
short_ref: stringValue(entry.short_ref) ??
|
|
2671
|
+
sessionShortRef(String(entry.id)),
|
|
2672
|
+
source: "managed_session_terminal_alias"
|
|
2673
|
+
}
|
|
2674
|
+
];
|
|
2675
|
+
});
|
|
2676
|
+
const rawTerminalEntries = terminalProjection.terminals.filter((entry) => {
|
|
2677
|
+
const managedState = isRecord(entry.managed) ? entry.managed : undefined;
|
|
2678
|
+
const actions = isRecord(entry.available_actions)
|
|
2679
|
+
? entry.available_actions
|
|
2680
|
+
: undefined;
|
|
2681
|
+
const sendAction = isRecord(actions?.send) ? actions.send : undefined;
|
|
2682
|
+
const sendArguments = isRecord(sendAction?.arguments)
|
|
2683
|
+
? sendAction.arguments
|
|
2684
|
+
: undefined;
|
|
2685
|
+
return (!stringValue(managedState?.session_id) ||
|
|
2686
|
+
!stringValue(sendArguments?.session_id));
|
|
2687
|
+
});
|
|
2688
|
+
return [
|
|
2689
|
+
...managed.map((entry) => sessionSelectorCandidateForEntry(entry, commandName, observedAtMs, {
|
|
2690
|
+
defaultActionable: false,
|
|
2691
|
+
mutationsAllowed
|
|
2692
|
+
})),
|
|
2693
|
+
...sessionEntries.map((entry) => sessionSelectorCandidateForEntry(entry, commandName, observedAtMs, {
|
|
2694
|
+
// A managed terminal's full id/@short-ref is an explicit alias for
|
|
2695
|
+
// its Session send target. It must not duplicate the Session in
|
|
2696
|
+
// omitted, only, latest, or agent-name selection.
|
|
2697
|
+
defaultActionable: entry.source !== "managed_session_terminal_alias",
|
|
2698
|
+
mutationsAllowed
|
|
2699
|
+
})),
|
|
2700
|
+
...rawTerminalEntries.map((entry) => sessionSelectorCandidateForEntry(entry, commandName, observedAtMs, {
|
|
2701
|
+
defaultActionable: true,
|
|
2702
|
+
mutationsAllowed
|
|
2703
|
+
}))
|
|
2704
|
+
];
|
|
2705
|
+
}
|
|
2392
2706
|
return [
|
|
2393
2707
|
...managed.map((entry) => sessionSelectorCandidateForEntry(entry, commandName, observedAtMs, {
|
|
2394
2708
|
defaultActionable: options.managedOnly === true,
|
|
@@ -2424,9 +2738,7 @@ function listActionForCommand(entry, commandName) {
|
|
|
2424
2738
|
: {};
|
|
2425
2739
|
const actionName = commandName === "retry-callback"
|
|
2426
2740
|
? "retry_callback"
|
|
2427
|
-
: commandName
|
|
2428
|
-
? "follow_up"
|
|
2429
|
-
: commandName;
|
|
2741
|
+
: commandName;
|
|
2430
2742
|
if (isRecord(actions[actionName])) {
|
|
2431
2743
|
return actions[actionName];
|
|
2432
2744
|
}
|
|
@@ -2463,7 +2775,10 @@ function listActionTargetId(action) {
|
|
|
2463
2775
|
const actionArguments = isRecord(action?.arguments)
|
|
2464
2776
|
? action.arguments
|
|
2465
2777
|
: undefined;
|
|
2466
|
-
return stringValue(actionArguments?.
|
|
2778
|
+
return stringValue(actionArguments?.session_id ??
|
|
2779
|
+
actionArguments?.turn_id ??
|
|
2780
|
+
actionArguments?.selector ??
|
|
2781
|
+
actionArguments?.conversation_id);
|
|
2467
2782
|
}
|
|
2468
2783
|
function terminalControlSelectorKey(value) {
|
|
2469
2784
|
if (!isRecord(value)) {
|
|
@@ -2492,7 +2807,10 @@ function sessionEntryRecency(entry, observedAtMs) {
|
|
|
2492
2807
|
return {};
|
|
2493
2808
|
}
|
|
2494
2809
|
async function resolveTerminalConversationFromOptions(options) {
|
|
2495
|
-
return createTerminalAgentBridge(options).resolveConversationId(stringValue(options.
|
|
2810
|
+
return createTerminalAgentBridge(options).resolveConversationId(stringValue(options.session ??
|
|
2811
|
+
options.turn ??
|
|
2812
|
+
options.conversation ??
|
|
2813
|
+
options.conversationId));
|
|
2496
2814
|
}
|
|
2497
2815
|
async function runStatus(options) {
|
|
2498
2816
|
const explicitStatePath = options.state
|
|
@@ -2501,7 +2819,7 @@ async function runStatus(options) {
|
|
|
2501
2819
|
const storeDir = explicitStatePath
|
|
2502
2820
|
? pathsForConversationDir(path.dirname(explicitStatePath)).storeDir
|
|
2503
2821
|
: storeDirFromOptions(options);
|
|
2504
|
-
const reconciliationConversationId = stringValue(options.conversation ?? options.conversationId) ??
|
|
2822
|
+
const reconciliationConversationId = stringValue(options.turn ?? options.conversation ?? options.conversationId) ??
|
|
2505
2823
|
(explicitStatePath
|
|
2506
2824
|
? path.basename(pathsForConversationDir(path.dirname(explicitStatePath))
|
|
2507
2825
|
.conversationDir)
|
|
@@ -3022,12 +3340,13 @@ function prepareManagedSend({ options, statePath, logPath, messageBody, stateLoc
|
|
|
3022
3340
|
}
|
|
3023
3341
|
}
|
|
3024
3342
|
const conversation = loadState(statePath);
|
|
3025
|
-
if (
|
|
3026
|
-
|
|
3343
|
+
if (conversation.status !== "waiting_for_openclaw" ||
|
|
3344
|
+
options.type !== "answer") {
|
|
3345
|
+
throw new Error(`cannot respond to turn ${turnIdForConversation(conversation)}; ` +
|
|
3346
|
+
`turn is ${conversation.status}`);
|
|
3027
3347
|
}
|
|
3028
3348
|
const executor = executorForConversation(conversation);
|
|
3029
|
-
const type =
|
|
3030
|
-
(conversation.status === "waiting_for_openclaw" ? "answer" : "task");
|
|
3349
|
+
const type = "answer";
|
|
3031
3350
|
const nativeTakeoverForSend = isRecord(conversation.native_session_takeover)
|
|
3032
3351
|
? conversation.native_session_takeover
|
|
3033
3352
|
: undefined;
|
|
@@ -3085,6 +3404,12 @@ async function runSend(options) {
|
|
|
3085
3404
|
if (options.agentHardTimeoutMinutes !== undefined) {
|
|
3086
3405
|
positiveMinutes(options.agentHardTimeoutMinutes, "--agent-hard-timeout-minutes");
|
|
3087
3406
|
}
|
|
3407
|
+
if (options.respond === true) {
|
|
3408
|
+
return runTurnResponse({ options, messageBody });
|
|
3409
|
+
}
|
|
3410
|
+
if ((options.type ?? "task") !== "task") {
|
|
3411
|
+
throw new Error("ordinary send only accepts message type task; use respond --turn to answer an in-flight Turn");
|
|
3412
|
+
}
|
|
3088
3413
|
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
3089
3414
|
if (terminalConversation) {
|
|
3090
3415
|
if (!options.background) {
|
|
@@ -3093,13 +3418,29 @@ async function runSend(options) {
|
|
|
3093
3418
|
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalConversation.terminalControl), { timeoutMs: 30000 });
|
|
3094
3419
|
let releaseStateLock;
|
|
3095
3420
|
try {
|
|
3096
|
-
const
|
|
3421
|
+
const currentNativeIdentity = await resolveCurrentNativeAgentSessionIdentity({
|
|
3422
|
+
options,
|
|
3423
|
+
agent: terminalConversation.agent,
|
|
3424
|
+
pid: terminalConversation.pid,
|
|
3425
|
+
cwd: terminalConversation.terminalControl.currentPath
|
|
3426
|
+
});
|
|
3427
|
+
const reusableTurn = reusableManagedSessionTurnForTerminal({
|
|
3428
|
+
options,
|
|
3429
|
+
terminalConversation,
|
|
3430
|
+
currentNativeIdentity
|
|
3431
|
+
});
|
|
3432
|
+
assertManagedSessionCanStartTurn(reusableTurn
|
|
3433
|
+
? managedTurnsForSession(storeDirFromOptions(options), sessionIdForConversation(reusableTurn))
|
|
3434
|
+
: []);
|
|
3435
|
+
const managed = createManagedTerminalTurn({
|
|
3097
3436
|
options,
|
|
3098
3437
|
conversationId: terminalConversation.conversationId,
|
|
3099
3438
|
agent: terminalConversation.agent,
|
|
3100
3439
|
pid: terminalConversation.pid,
|
|
3101
3440
|
messageBody,
|
|
3102
|
-
terminalControl: terminalConversation.terminalControl
|
|
3441
|
+
terminalControl: terminalConversation.terminalControl,
|
|
3442
|
+
previousTurn: reusableTurn,
|
|
3443
|
+
nativeAgentIdentity: currentNativeIdentity
|
|
3103
3444
|
});
|
|
3104
3445
|
ensureStoreWritable(managed.conversation.store_dir);
|
|
3105
3446
|
ensureDir(path.dirname(managed.statePath));
|
|
@@ -3116,7 +3457,7 @@ async function runSend(options) {
|
|
|
3116
3457
|
terminalSendLockHeld: true,
|
|
3117
3458
|
terminalStateLockHeld: true,
|
|
3118
3459
|
recordMessageAfterSend: true,
|
|
3119
|
-
recordRawAttachmentAfterSend:
|
|
3460
|
+
recordRawAttachmentAfterSend: reusableTurn === undefined
|
|
3120
3461
|
});
|
|
3121
3462
|
}
|
|
3122
3463
|
finally {
|
|
@@ -3129,62 +3470,185 @@ async function runSend(options) {
|
|
|
3129
3470
|
}
|
|
3130
3471
|
return;
|
|
3131
3472
|
}
|
|
3473
|
+
const sessionId = required(stringValue(options.session ?? options.conversation ?? options.conversationId), "--session is required for an ordinary managed send");
|
|
3474
|
+
const initialTurns = managedTurnsForSessionTarget(storeDirFromOptions(options), sessionId);
|
|
3475
|
+
assertManagedSessionCanStartTurn(initialTurns);
|
|
3476
|
+
const bindingTurn = initialTurns[0];
|
|
3477
|
+
const bindingTakeover = isRecord(bindingTurn.native_session_takeover)
|
|
3478
|
+
? bindingTurn.native_session_takeover
|
|
3479
|
+
: undefined;
|
|
3480
|
+
const rawTerminalId = stringValue(bindingTakeover?.native_session_id);
|
|
3481
|
+
const resolvedTerminal = await createTerminalAgentBridge(options)
|
|
3482
|
+
.resolveConversationId(rawTerminalId);
|
|
3483
|
+
if (!resolvedTerminal) {
|
|
3484
|
+
throw new Error(`session ${sessionId} is not attached to a live tmux terminal`);
|
|
3485
|
+
}
|
|
3486
|
+
const currentNativeIdentity = await resolveCurrentNativeAgentSessionIdentity({
|
|
3487
|
+
options,
|
|
3488
|
+
agent: resolvedTerminal.agent,
|
|
3489
|
+
pid: resolvedTerminal.pid,
|
|
3490
|
+
cwd: resolvedTerminal.terminalControl.currentPath
|
|
3491
|
+
});
|
|
3492
|
+
assertNativeAgentIdentityForTurn({
|
|
3493
|
+
conversation: bindingTurn,
|
|
3494
|
+
currentIdentity: currentNativeIdentity,
|
|
3495
|
+
operation: "send to"
|
|
3496
|
+
});
|
|
3497
|
+
if (!managedTurnMatchesResolvedTerminal(bindingTurn, resolvedTerminal, currentNativeIdentity)) {
|
|
3498
|
+
throw new Error(`session ${sessionId} no longer matches its terminal or agent process incarnation`);
|
|
3499
|
+
}
|
|
3500
|
+
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), resolvedTerminal.terminalControl), { timeoutMs: 30000 });
|
|
3501
|
+
let releaseStateLock;
|
|
3502
|
+
try {
|
|
3503
|
+
const currentTurns = managedTurnsForSessionTarget(storeDirFromOptions(options), sessionId);
|
|
3504
|
+
assertManagedSessionCanStartTurn(currentTurns);
|
|
3505
|
+
const currentBinding = currentTurns[0];
|
|
3506
|
+
const lockedNativeIdentity = await resolveCurrentNativeAgentSessionIdentity({
|
|
3507
|
+
options,
|
|
3508
|
+
agent: resolvedTerminal.agent,
|
|
3509
|
+
pid: resolvedTerminal.pid,
|
|
3510
|
+
cwd: resolvedTerminal.terminalControl.currentPath
|
|
3511
|
+
});
|
|
3512
|
+
assertNativeAgentIdentityForTurn({
|
|
3513
|
+
conversation: currentBinding,
|
|
3514
|
+
currentIdentity: lockedNativeIdentity,
|
|
3515
|
+
operation: "send to"
|
|
3516
|
+
});
|
|
3517
|
+
if (!managedTurnMatchesResolvedTerminal(currentBinding, resolvedTerminal, lockedNativeIdentity)) {
|
|
3518
|
+
throw new Error("managed session identity changed while waiting to send; refresh list and retry");
|
|
3519
|
+
}
|
|
3520
|
+
const managed = createManagedTerminalTurn({
|
|
3521
|
+
options,
|
|
3522
|
+
conversationId: resolvedTerminal.conversationId,
|
|
3523
|
+
agent: resolvedTerminal.agent,
|
|
3524
|
+
pid: resolvedTerminal.pid,
|
|
3525
|
+
messageBody,
|
|
3526
|
+
terminalControl: resolvedTerminal.terminalControl,
|
|
3527
|
+
previousTurn: currentBinding,
|
|
3528
|
+
nativeAgentIdentity: lockedNativeIdentity
|
|
3529
|
+
});
|
|
3530
|
+
ensureStoreWritable(managed.conversation.store_dir);
|
|
3531
|
+
ensureDir(path.dirname(managed.statePath));
|
|
3532
|
+
releaseStateLock = acquireFileLock(`${managed.statePath}.lock`);
|
|
3533
|
+
await runTerminalControlSend({
|
|
3534
|
+
options,
|
|
3535
|
+
conversation: managed.conversation,
|
|
3536
|
+
nextConversation: managed.nextConversation,
|
|
3537
|
+
statePath: managed.statePath,
|
|
3538
|
+
logPath: managed.logPath,
|
|
3539
|
+
executor: managed.executor,
|
|
3540
|
+
message: managed.message,
|
|
3541
|
+
terminalControl: resolvedTerminal.terminalControl,
|
|
3542
|
+
terminalSendLockHeld: true,
|
|
3543
|
+
terminalStateLockHeld: true,
|
|
3544
|
+
recordMessageAfterSend: true
|
|
3545
|
+
});
|
|
3546
|
+
}
|
|
3547
|
+
finally {
|
|
3548
|
+
try {
|
|
3549
|
+
releaseStateLock?.();
|
|
3550
|
+
}
|
|
3551
|
+
finally {
|
|
3552
|
+
releaseTerminalLock();
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
async function runRespond(options) {
|
|
3557
|
+
const turnId = required(stringValue(options.turn ?? options.conversation ?? options.conversationId), "--turn is required");
|
|
3558
|
+
return runSend({
|
|
3559
|
+
...options,
|
|
3560
|
+
turn: turnId,
|
|
3561
|
+
conversation: turnId,
|
|
3562
|
+
session: undefined,
|
|
3563
|
+
type: "answer",
|
|
3564
|
+
respond: true
|
|
3565
|
+
});
|
|
3566
|
+
}
|
|
3567
|
+
async function runTurnResponse({ options, messageBody }) {
|
|
3132
3568
|
const loaded = loadConversationFromOptions(options);
|
|
3133
3569
|
const { statePath, logPath } = loaded;
|
|
3134
|
-
const
|
|
3570
|
+
const conversation = await migrateLegacyTerminalAgentIdentity({
|
|
3135
3571
|
...loaded,
|
|
3136
3572
|
options
|
|
3137
3573
|
});
|
|
3138
|
-
|
|
3139
|
-
|
|
3574
|
+
if (conversation.status !== "waiting_for_openclaw") {
|
|
3575
|
+
throw new Error(`cannot respond to turn ${turnIdForConversation(conversation)}; ` +
|
|
3576
|
+
`turn is ${conversation.status}, not waiting_for_openclaw`);
|
|
3577
|
+
}
|
|
3578
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
3579
|
+
? conversation.native_session_takeover
|
|
3140
3580
|
: undefined;
|
|
3141
|
-
const
|
|
3142
|
-
if (
|
|
3143
|
-
|
|
3144
|
-
|
|
3581
|
+
const terminalControl = terminalControlFromTakeover(nativeTakeover);
|
|
3582
|
+
if (!terminalControl) {
|
|
3583
|
+
throw new Error(`turn ${turnIdForConversation(conversation)} is not attached to a live tmux terminal`);
|
|
3584
|
+
}
|
|
3585
|
+
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
|
|
3586
|
+
let releaseStateLock;
|
|
3587
|
+
try {
|
|
3588
|
+
releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
3589
|
+
const prepared = prepareManagedSend({
|
|
3590
|
+
options: { ...options, type: "answer" },
|
|
3591
|
+
statePath,
|
|
3592
|
+
logPath,
|
|
3593
|
+
messageBody,
|
|
3594
|
+
stateLockHeld: true,
|
|
3595
|
+
persist: false
|
|
3596
|
+
});
|
|
3597
|
+
const preparedTakeover = isRecord(prepared.conversation.native_session_takeover)
|
|
3598
|
+
? prepared.conversation.native_session_takeover
|
|
3599
|
+
: undefined;
|
|
3600
|
+
const terminalAgentPid = Number(preparedTakeover?.terminal_agent_pid);
|
|
3601
|
+
const currentNativeIdentity = await resolveCurrentNativeAgentSessionIdentity({
|
|
3602
|
+
options,
|
|
3603
|
+
agent: prepared.executor.kind,
|
|
3604
|
+
pid: terminalAgentPid,
|
|
3605
|
+
cwd: terminalControl.currentPath
|
|
3606
|
+
});
|
|
3607
|
+
assertNativeAgentIdentityForTurn({
|
|
3608
|
+
conversation: prepared.conversation,
|
|
3609
|
+
currentIdentity: currentNativeIdentity,
|
|
3610
|
+
operation: "respond to"
|
|
3611
|
+
});
|
|
3612
|
+
const currentTerminalControl = terminalControlFromTakeover(prepared.nativeTakeoverForSend);
|
|
3613
|
+
if (!currentTerminalControl ||
|
|
3614
|
+
currentTerminalControl.target !== terminalControl.target ||
|
|
3615
|
+
currentTerminalControl.socketPath !== terminalControl.socketPath ||
|
|
3616
|
+
currentTerminalControl.panePid !== terminalControl.panePid) {
|
|
3617
|
+
throw new Error("terminal control changed while waiting to respond; refresh status and retry");
|
|
3618
|
+
}
|
|
3619
|
+
const responseOptions = {
|
|
3620
|
+
...options,
|
|
3621
|
+
type: "answer",
|
|
3622
|
+
agentTimeoutMinutes: options.agentTimeoutMinutes ??
|
|
3623
|
+
preparedTakeover?.terminal_bridge_inactivity_timeout_minutes ??
|
|
3624
|
+
DEFAULT_AGENT_TIMEOUT_MINUTES,
|
|
3625
|
+
agentHardTimeoutMinutes: options.agentHardTimeoutMinutes ??
|
|
3626
|
+
preparedTakeover?.terminal_bridge_hard_timeout_minutes ??
|
|
3627
|
+
DEFAULT_AGENT_HARD_TIMEOUT_MINUTES
|
|
3628
|
+
};
|
|
3629
|
+
await runTerminalControlSend({
|
|
3630
|
+
options: responseOptions,
|
|
3631
|
+
conversation: prepared.conversation,
|
|
3632
|
+
nextConversation: prepared.nextConversation,
|
|
3633
|
+
statePath,
|
|
3634
|
+
logPath,
|
|
3635
|
+
executor: prepared.executor,
|
|
3636
|
+
message: prepared.message,
|
|
3637
|
+
terminalControl: currentTerminalControl,
|
|
3638
|
+
terminalSendLockHeld: true,
|
|
3639
|
+
terminalStateLockHeld: true,
|
|
3640
|
+
recordMessageAfterSend: true,
|
|
3641
|
+
continuingTurnResponse: true
|
|
3642
|
+
});
|
|
3643
|
+
}
|
|
3644
|
+
finally {
|
|
3145
3645
|
try {
|
|
3146
|
-
releaseStateLock
|
|
3147
|
-
const prepared = prepareManagedSend({
|
|
3148
|
-
options,
|
|
3149
|
-
statePath,
|
|
3150
|
-
logPath,
|
|
3151
|
-
messageBody,
|
|
3152
|
-
stateLockHeld: true,
|
|
3153
|
-
persist: false
|
|
3154
|
-
});
|
|
3155
|
-
const currentTerminalControl = terminalControlFromTakeover(prepared.nativeTakeoverForSend);
|
|
3156
|
-
if (!currentTerminalControl ||
|
|
3157
|
-
currentTerminalControl.kind !== migratedTerminalControl.kind ||
|
|
3158
|
-
currentTerminalControl.target !== migratedTerminalControl.target ||
|
|
3159
|
-
currentTerminalControl.socketPath !== migratedTerminalControl.socketPath ||
|
|
3160
|
-
currentTerminalControl.panePid !== migratedTerminalControl.panePid) {
|
|
3161
|
-
throw new Error("terminal control changed while waiting to send; refresh status and retry");
|
|
3162
|
-
}
|
|
3163
|
-
await runTerminalControlSend({
|
|
3164
|
-
options,
|
|
3165
|
-
conversation: prepared.conversation,
|
|
3166
|
-
nextConversation: prepared.nextConversation,
|
|
3167
|
-
statePath,
|
|
3168
|
-
logPath,
|
|
3169
|
-
executor: prepared.executor,
|
|
3170
|
-
message: prepared.message,
|
|
3171
|
-
terminalControl: currentTerminalControl,
|
|
3172
|
-
terminalSendLockHeld: true,
|
|
3173
|
-
terminalStateLockHeld: true,
|
|
3174
|
-
recordMessageAfterSend: true
|
|
3175
|
-
});
|
|
3646
|
+
releaseStateLock?.();
|
|
3176
3647
|
}
|
|
3177
3648
|
finally {
|
|
3178
|
-
|
|
3179
|
-
releaseStateLock?.();
|
|
3180
|
-
}
|
|
3181
|
-
finally {
|
|
3182
|
-
releaseTerminalLock();
|
|
3183
|
-
}
|
|
3649
|
+
releaseTerminalLock();
|
|
3184
3650
|
}
|
|
3185
|
-
return;
|
|
3186
3651
|
}
|
|
3187
|
-
throw new Error(`conversation ${migratedConversation.conversation_id} is not attached to a live tmux terminal`);
|
|
3188
3652
|
}
|
|
3189
3653
|
async function runApprove(options) {
|
|
3190
3654
|
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
@@ -3327,7 +3791,6 @@ async function runApprove(options) {
|
|
|
3327
3791
|
const autoApprovalPolicy = autoApproved
|
|
3328
3792
|
? parseJsonOption(options.autoApprovalPolicyJson, "--auto-approval-policy-json")
|
|
3329
3793
|
: undefined;
|
|
3330
|
-
const runtimeIdentity = terminalRuntimeIdentityForConversation(conversation, terminalControl);
|
|
3331
3794
|
let executorPolicyDecision;
|
|
3332
3795
|
const policyCandidateForInspection = ({ agent, currentTerminalControl, inspection, fingerprint }) => {
|
|
3333
3796
|
const evidence = inspection.approval.approvable
|
|
@@ -3396,11 +3859,12 @@ async function runApprove(options) {
|
|
|
3396
3859
|
action: "approve"
|
|
3397
3860
|
});
|
|
3398
3861
|
lockedConversation = currentConversation;
|
|
3399
|
-
|
|
3862
|
+
const currentRuntimeIdentity = terminalRuntimeIdentityForConversation(currentConversation, currentControl);
|
|
3863
|
+
approval = await createTerminalAgentBridge(options).approve(executor.kind, currentControl, {
|
|
3400
3864
|
expectedFingerprint,
|
|
3401
3865
|
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
3402
|
-
runtime:
|
|
3403
|
-
managedRequest: terminalDurableRequestForConversation(currentConversation,
|
|
3866
|
+
runtime: currentRuntimeIdentity,
|
|
3867
|
+
managedRequest: terminalDurableRequestForConversation(currentConversation, currentControl),
|
|
3404
3868
|
requiredDecisionMode: autoApproved && executor.kind === "claude" ? "keys" : undefined,
|
|
3405
3869
|
authorize: autoApproved
|
|
3406
3870
|
? ({ agent, terminalControl: currentTerminalControl, inspection, fingerprint }) => {
|
|
@@ -3766,7 +4230,7 @@ async function runTerminalConversationApprove({ options, conversationId, agent,
|
|
|
3766
4230
|
releaseTerminalLock();
|
|
3767
4231
|
}
|
|
3768
4232
|
}
|
|
3769
|
-
async function runTerminalControlSend({ options, conversation, nextConversation, statePath, logPath, executor, message, terminalControl, terminalSendLockHeld = false, terminalStateLockHeld = false, storeWriterLeaseHeld = false, recordMessageAfterSend = false, recordRawAttachmentAfterSend = false }) {
|
|
4233
|
+
async function runTerminalControlSend({ options, conversation, nextConversation, statePath, logPath, executor, message, terminalControl, terminalSendLockHeld = false, terminalStateLockHeld = false, storeWriterLeaseHeld = false, recordMessageAfterSend = false, recordRawAttachmentAfterSend = false, continuingTurnResponse = false }) {
|
|
3770
4234
|
const bridge = terminalBridgeEnabled(conversation);
|
|
3771
4235
|
if (!terminalSendLockHeld) {
|
|
3772
4236
|
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
|
|
@@ -3784,7 +4248,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3784
4248
|
terminalStateLockHeld,
|
|
3785
4249
|
storeWriterLeaseHeld,
|
|
3786
4250
|
recordMessageAfterSend,
|
|
3787
|
-
recordRawAttachmentAfterSend
|
|
4251
|
+
recordRawAttachmentAfterSend,
|
|
4252
|
+
continuingTurnResponse
|
|
3788
4253
|
});
|
|
3789
4254
|
}
|
|
3790
4255
|
finally {
|
|
@@ -3820,7 +4285,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3820
4285
|
terminalStateLockHeld: true,
|
|
3821
4286
|
storeWriterLeaseHeld,
|
|
3822
4287
|
recordMessageAfterSend,
|
|
3823
|
-
recordRawAttachmentAfterSend
|
|
4288
|
+
recordRawAttachmentAfterSend,
|
|
4289
|
+
continuingTurnResponse
|
|
3824
4290
|
});
|
|
3825
4291
|
}
|
|
3826
4292
|
finally {
|
|
@@ -3842,7 +4308,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3842
4308
|
terminalStateLockHeld,
|
|
3843
4309
|
storeWriterLeaseHeld: true,
|
|
3844
4310
|
recordMessageAfterSend,
|
|
3845
|
-
recordRawAttachmentAfterSend
|
|
4311
|
+
recordRawAttachmentAfterSend,
|
|
4312
|
+
continuingTurnResponse
|
|
3846
4313
|
}));
|
|
3847
4314
|
}
|
|
3848
4315
|
const terminalBridge = createTerminalAgentBridge(options);
|
|
@@ -3868,19 +4335,41 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3868
4335
|
"owner state is unavailable; inspect the shared tmux pane and repair " +
|
|
3869
4336
|
"or explicitly resolve that conversation before sending another task");
|
|
3870
4337
|
}
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
4338
|
+
const continuingSameTurn = Boolean(continuingTurnResponse &&
|
|
4339
|
+
sessionIdForConversation(owner) === sessionIdForConversation(conversation) &&
|
|
4340
|
+
turnIdForConversation(owner) === turnIdForConversation(conversation) &&
|
|
4341
|
+
sameCanonicalStatePath(previousDispatchLedger.state_path, statePath));
|
|
4342
|
+
if (!TERMINAL_DISPATCH_RELEASE_STATUSES.has(owner.status) &&
|
|
4343
|
+
!continuingSameTurn) {
|
|
4344
|
+
const exactDispatchReplay = Boolean(stringValue(previousDispatchLedger.request_hash) ===
|
|
4345
|
+
terminalRequestHash &&
|
|
4346
|
+
stringValue(previousDispatchLedger.conversation_id) ===
|
|
4347
|
+
conversation.conversation_id &&
|
|
4348
|
+
stringValue(previousDispatchLedger.message_id) === message.id &&
|
|
4349
|
+
sameCanonicalStatePath(previousDispatchLedger.state_path, statePath));
|
|
4350
|
+
if (exactDispatchReplay) {
|
|
3874
4351
|
const receiptConversationId = stringValue(previousDispatchLedger.conversation_id) ??
|
|
3875
4352
|
owner.conversation_id;
|
|
4353
|
+
const receiptSessionId = sessionIdForConversation(owner);
|
|
4354
|
+
const receiptTurnId = turnIdForConversation(owner);
|
|
3876
4355
|
const receiptMessageId = stringValue(previousDispatchLedger.message_id) ??
|
|
3877
4356
|
message.id;
|
|
3878
4357
|
printJson({
|
|
4358
|
+
session_id: receiptSessionId,
|
|
4359
|
+
turn_id: receiptTurnId,
|
|
3879
4360
|
conversation: owner,
|
|
3880
4361
|
message: {
|
|
3881
4362
|
...message,
|
|
3882
4363
|
id: receiptMessageId,
|
|
3883
|
-
conversation_id: receiptConversationId
|
|
4364
|
+
conversation_id: receiptConversationId,
|
|
4365
|
+
session_id: receiptSessionId,
|
|
4366
|
+
turn_id: receiptTurnId,
|
|
4367
|
+
metadata: {
|
|
4368
|
+
...(isRecord(message.metadata) ? message.metadata : {}),
|
|
4369
|
+
task_id: receiptConversationId,
|
|
4370
|
+
session_id: receiptSessionId,
|
|
4371
|
+
turn_id: receiptTurnId
|
|
4372
|
+
}
|
|
3884
4373
|
},
|
|
3885
4374
|
delivered: true,
|
|
3886
4375
|
status: "async_pending",
|
|
@@ -3894,6 +4383,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3894
4383
|
reason: "AKK replayed the durable receipt for an identical active terminal request and did not send tmux input again.",
|
|
3895
4384
|
openclaw_next_action: openClawYieldNextAction({
|
|
3896
4385
|
conversationId: receiptConversationId,
|
|
4386
|
+
sessionId: receiptSessionId,
|
|
4387
|
+
turnId: receiptTurnId,
|
|
3897
4388
|
source: "terminal_control",
|
|
3898
4389
|
callbackExpected: Boolean(owner.gateway_method ??
|
|
3899
4390
|
previousDispatchLedger.callback_expected)
|
|
@@ -3910,6 +4401,30 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3910
4401
|
if (bridge) {
|
|
3911
4402
|
assertNoUnresolvedTerminalBridgeSubmission(storeDirFromOptions(options), terminalControl, conversation.conversation_id, terminalPayload);
|
|
3912
4403
|
}
|
|
4404
|
+
const sendTakeover = isRecord(conversation.native_session_takeover)
|
|
4405
|
+
? conversation.native_session_takeover
|
|
4406
|
+
: undefined;
|
|
4407
|
+
const terminalAgentPid = Number(sendTakeover?.terminal_agent_pid);
|
|
4408
|
+
const currentNativeIdentity = await resolveCurrentNativeAgentSessionIdentity({
|
|
4409
|
+
options,
|
|
4410
|
+
agent: executor.kind,
|
|
4411
|
+
pid: terminalAgentPid,
|
|
4412
|
+
cwd: terminalControl.currentPath
|
|
4413
|
+
});
|
|
4414
|
+
const virginRawAttach = Boolean(recordRawAttachmentAfterSend &&
|
|
4415
|
+
!stringValue(sendTakeover?.terminal_agent_session_id));
|
|
4416
|
+
if (virginRawAttach) {
|
|
4417
|
+
if (currentNativeIdentity) {
|
|
4418
|
+
throw new Error("native agent session appeared while preparing a virgin terminal attach; refresh list and retry");
|
|
4419
|
+
}
|
|
4420
|
+
}
|
|
4421
|
+
else {
|
|
4422
|
+
assertNativeAgentIdentityForTurn({
|
|
4423
|
+
conversation,
|
|
4424
|
+
currentIdentity: currentNativeIdentity,
|
|
4425
|
+
operation: "send to"
|
|
4426
|
+
});
|
|
4427
|
+
}
|
|
3913
4428
|
const preSendRuntime = {
|
|
3914
4429
|
...terminalRuntimeIdentityForConversation(nextConversation, terminalControl),
|
|
3915
4430
|
messageId: message.id
|
|
@@ -3969,6 +4484,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3969
4484
|
status: "prepared",
|
|
3970
4485
|
generation_id: message.id,
|
|
3971
4486
|
conversation_id: preparedConversation.conversation_id,
|
|
4487
|
+
session_id: sessionIdForConversation(preparedConversation),
|
|
4488
|
+
turn_id: turnIdForConversation(preparedConversation),
|
|
3972
4489
|
message_id: message.id,
|
|
3973
4490
|
request_hash: terminalRequestHash,
|
|
3974
4491
|
prepared_at: bridgeStartedAt,
|
|
@@ -4132,6 +4649,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
4132
4649
|
safe_to_retry: dispatchLedgerRestored
|
|
4133
4650
|
});
|
|
4134
4651
|
printJson({
|
|
4652
|
+
session_id: sessionIdForConversation(abortedConversation),
|
|
4653
|
+
turn_id: turnIdForConversation(abortedConversation),
|
|
4135
4654
|
conversation: abortedConversation,
|
|
4136
4655
|
message,
|
|
4137
4656
|
delivered: false,
|
|
@@ -4150,6 +4669,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
4150
4669
|
openclaw_next_action: {
|
|
4151
4670
|
action: dispatchLedgerRestored ? "retry" : "inspect",
|
|
4152
4671
|
conversation_id: abortedConversation.conversation_id,
|
|
4672
|
+
session_id: sessionIdForConversation(abortedConversation),
|
|
4673
|
+
turn_id: turnIdForConversation(abortedConversation),
|
|
4153
4674
|
safe_to_retry: dispatchLedgerRestored,
|
|
4154
4675
|
do_not_retry: !dispatchLedgerRestored,
|
|
4155
4676
|
reason: dispatchLedgerRestored
|
|
@@ -4165,8 +4686,134 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
4165
4686
|
runtime: preSendRuntime
|
|
4166
4687
|
});
|
|
4167
4688
|
const submittedAt = new Date().toISOString();
|
|
4689
|
+
let submittedBase = preparedConversation;
|
|
4690
|
+
if (virginRawAttach) {
|
|
4691
|
+
let boundIdentity;
|
|
4692
|
+
let boundConversation;
|
|
4693
|
+
let bindingError;
|
|
4694
|
+
try {
|
|
4695
|
+
boundIdentity = await pollNativeAgentSessionIdentity({
|
|
4696
|
+
options,
|
|
4697
|
+
executor,
|
|
4698
|
+
terminalControl,
|
|
4699
|
+
pid: terminalAgentPid
|
|
4700
|
+
});
|
|
4701
|
+
}
|
|
4702
|
+
catch (error) {
|
|
4703
|
+
bindingError = error instanceof Error ? error.message : String(error);
|
|
4704
|
+
}
|
|
4705
|
+
if (boundIdentity) {
|
|
4706
|
+
try {
|
|
4707
|
+
boundConversation = withNativeAgentSessionIdentity(preparedConversation, boundIdentity);
|
|
4708
|
+
assertNativeAgentIdentityForTurn({
|
|
4709
|
+
conversation: boundConversation,
|
|
4710
|
+
currentIdentity: boundIdentity,
|
|
4711
|
+
operation: "bind"
|
|
4712
|
+
});
|
|
4713
|
+
}
|
|
4714
|
+
catch (error) {
|
|
4715
|
+
bindingError = error instanceof Error ? error.message : String(error);
|
|
4716
|
+
boundIdentity = undefined;
|
|
4717
|
+
boundConversation = undefined;
|
|
4718
|
+
}
|
|
4719
|
+
}
|
|
4720
|
+
if (!boundIdentity) {
|
|
4721
|
+
const bindingFailedAt = new Date().toISOString();
|
|
4722
|
+
const bindingReason = bindingError
|
|
4723
|
+
? `AKK delivered the terminal input but could not verify the new native agent session: ${bindingError}`
|
|
4724
|
+
: "AKK delivered the terminal input but no exact native agent session appeared within the binding window";
|
|
4725
|
+
const unfencedBase = {
|
|
4726
|
+
...preparedConversation,
|
|
4727
|
+
status: "stalled",
|
|
4728
|
+
stalled_at: bindingFailedAt,
|
|
4729
|
+
stalled_reason: bindingReason,
|
|
4730
|
+
native_session_takeover: {
|
|
4731
|
+
...(isRecord(preparedConversation.native_session_takeover)
|
|
4732
|
+
? preparedConversation.native_session_takeover
|
|
4733
|
+
: {}),
|
|
4734
|
+
terminal_agent_identity_status: "unresolved_after_submit",
|
|
4735
|
+
terminal_agent_identity_error: textSummary(bindingReason)
|
|
4736
|
+
},
|
|
4737
|
+
updated_at: bindingFailedAt
|
|
4738
|
+
};
|
|
4739
|
+
const unfencedConversation = withTerminalBridgeSubmission({
|
|
4740
|
+
conversation: unfencedBase,
|
|
4741
|
+
messageId: message.id,
|
|
4742
|
+
requestText: terminalPayload,
|
|
4743
|
+
status: "submitted",
|
|
4744
|
+
preparedAt: bridgeStartedAt,
|
|
4745
|
+
submittedAt
|
|
4746
|
+
});
|
|
4747
|
+
saveTerminalBridgeDispatchLedger(terminalControl, {
|
|
4748
|
+
status: "submitted",
|
|
4749
|
+
generation_id: message.id,
|
|
4750
|
+
conversation_id: unfencedConversation.conversation_id,
|
|
4751
|
+
session_id: sessionIdForConversation(unfencedConversation),
|
|
4752
|
+
turn_id: turnIdForConversation(unfencedConversation),
|
|
4753
|
+
message_id: message.id,
|
|
4754
|
+
request_hash: terminalRequestHash,
|
|
4755
|
+
prepared_at: bridgeStartedAt,
|
|
4756
|
+
submitted_at: submittedAt,
|
|
4757
|
+
dispatcher_pid: process.pid,
|
|
4758
|
+
state_path: statePath,
|
|
4759
|
+
event_log_path: logPath,
|
|
4760
|
+
callback_expected: false,
|
|
4761
|
+
native_identity_status: "unresolved_after_submit",
|
|
4762
|
+
error: textSummary(bindingReason),
|
|
4763
|
+
previous_generation_id: stringValue(previousDispatchLedger?.generation_id) ??
|
|
4764
|
+
stringValue(previousDispatchLedger?.message_id)
|
|
4765
|
+
});
|
|
4766
|
+
saveState(statePath, unfencedConversation);
|
|
4767
|
+
appendEvent(logPath, {
|
|
4768
|
+
ts: bindingFailedAt,
|
|
4769
|
+
conversation_id: unfencedConversation.conversation_id,
|
|
4770
|
+
event: "terminal_agent_identity_binding_failed",
|
|
4771
|
+
message_id: message.id,
|
|
4772
|
+
executor,
|
|
4773
|
+
terminal_control: terminalControl,
|
|
4774
|
+
error: textSummary(bindingReason),
|
|
4775
|
+
delivered: true,
|
|
4776
|
+
do_not_retry: true
|
|
4777
|
+
});
|
|
4778
|
+
runtimeLog("error", "terminal_agent_identity_binding_failed", {
|
|
4779
|
+
conversation_id: unfencedConversation.conversation_id,
|
|
4780
|
+
agent: executor.kind,
|
|
4781
|
+
terminal_target: terminalControl.target,
|
|
4782
|
+
error: bindingReason,
|
|
4783
|
+
delivered: true,
|
|
4784
|
+
do_not_retry: true
|
|
4785
|
+
});
|
|
4786
|
+
printJson({
|
|
4787
|
+
session_id: sessionIdForConversation(unfencedConversation),
|
|
4788
|
+
turn_id: turnIdForConversation(unfencedConversation),
|
|
4789
|
+
conversation: unfencedConversation,
|
|
4790
|
+
message,
|
|
4791
|
+
delivered: true,
|
|
4792
|
+
status: "delivered_unfenced",
|
|
4793
|
+
submission_outcome: "submitted",
|
|
4794
|
+
background: true,
|
|
4795
|
+
callback_expected: false,
|
|
4796
|
+
terminal_control: terminalControl,
|
|
4797
|
+
monitor_pid: bridgeMonitor?.pid ?? null,
|
|
4798
|
+
executor,
|
|
4799
|
+
delivery_receipt: "submitted",
|
|
4800
|
+
do_not_retry: true,
|
|
4801
|
+
reason: bindingReason,
|
|
4802
|
+
openclaw_next_action: {
|
|
4803
|
+
action: "inspect",
|
|
4804
|
+
conversation_id: unfencedConversation.conversation_id,
|
|
4805
|
+
session_id: sessionIdForConversation(unfencedConversation),
|
|
4806
|
+
turn_id: turnIdForConversation(unfencedConversation),
|
|
4807
|
+
do_not_retry: true,
|
|
4808
|
+
reason: "The input was submitted, but AKK could not fence later side effects to an exact native session. Inspect the pane and close this Turn before continuing."
|
|
4809
|
+
}
|
|
4810
|
+
});
|
|
4811
|
+
return;
|
|
4812
|
+
}
|
|
4813
|
+
submittedBase = boundConversation;
|
|
4814
|
+
}
|
|
4168
4815
|
deliveredConversation = withTerminalBridgeSubmission({
|
|
4169
|
-
conversation:
|
|
4816
|
+
conversation: submittedBase,
|
|
4170
4817
|
messageId: message.id,
|
|
4171
4818
|
requestText: terminalPayload,
|
|
4172
4819
|
status: "submitted",
|
|
@@ -4178,6 +4825,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
4178
4825
|
status: "submitted",
|
|
4179
4826
|
generation_id: message.id,
|
|
4180
4827
|
conversation_id: deliveredConversation.conversation_id,
|
|
4828
|
+
session_id: sessionIdForConversation(deliveredConversation),
|
|
4829
|
+
turn_id: turnIdForConversation(deliveredConversation),
|
|
4181
4830
|
message_id: message.id,
|
|
4182
4831
|
request_hash: terminalRequestHash,
|
|
4183
4832
|
prepared_at: bridgeStartedAt,
|
|
@@ -4217,6 +4866,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
4217
4866
|
status: "uncertain",
|
|
4218
4867
|
generation_id: message.id,
|
|
4219
4868
|
conversation_id: uncertainConversation.conversation_id,
|
|
4869
|
+
session_id: sessionIdForConversation(uncertainConversation),
|
|
4870
|
+
turn_id: turnIdForConversation(uncertainConversation),
|
|
4220
4871
|
message_id: message.id,
|
|
4221
4872
|
request_hash: terminalRequestHash,
|
|
4222
4873
|
prepared_at: bridgeStartedAt,
|
|
@@ -4265,6 +4916,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
4265
4916
|
stalled_conversation_ids: stalledConversationIds
|
|
4266
4917
|
});
|
|
4267
4918
|
printJson({
|
|
4919
|
+
session_id: sessionIdForConversation(uncertainConversation),
|
|
4920
|
+
turn_id: turnIdForConversation(uncertainConversation),
|
|
4268
4921
|
conversation: uncertainConversation,
|
|
4269
4922
|
message,
|
|
4270
4923
|
delivered: false,
|
|
@@ -4281,6 +4934,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
4281
4934
|
openclaw_next_action: {
|
|
4282
4935
|
action: "inspect",
|
|
4283
4936
|
conversation_id: uncertainConversation.conversation_id,
|
|
4937
|
+
session_id: sessionIdForConversation(uncertainConversation),
|
|
4938
|
+
turn_id: turnIdForConversation(uncertainConversation),
|
|
4284
4939
|
do_not_retry: true,
|
|
4285
4940
|
reason: "The terminal submission outcome is uncertain. Inspect AKK status and the shared tmux pane before deciding whether to close or continue."
|
|
4286
4941
|
}
|
|
@@ -4330,6 +4985,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
4330
4985
|
}
|
|
4331
4986
|
}
|
|
4332
4987
|
printJson({
|
|
4988
|
+
session_id: sessionIdForConversation(deliveredConversation),
|
|
4989
|
+
turn_id: turnIdForConversation(deliveredConversation),
|
|
4333
4990
|
conversation: deliveredConversation,
|
|
4334
4991
|
message,
|
|
4335
4992
|
delivered: true,
|
|
@@ -4348,6 +5005,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
4348
5005
|
: {}),
|
|
4349
5006
|
openclaw_next_action: openClawYieldNextAction({
|
|
4350
5007
|
conversationId: deliveredConversation.conversation_id,
|
|
5008
|
+
sessionId: sessionIdForConversation(deliveredConversation),
|
|
5009
|
+
turnId: turnIdForConversation(deliveredConversation),
|
|
4351
5010
|
source: "terminal_control",
|
|
4352
5011
|
callbackExpected: Boolean(deliveredConversation.gateway_method)
|
|
4353
5012
|
})
|
|
@@ -4356,18 +5015,290 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
4356
5015
|
function terminalSubmissionPayload(payload) {
|
|
4357
5016
|
return payload.trimEnd();
|
|
4358
5017
|
}
|
|
4359
|
-
function
|
|
5018
|
+
function isCompleteNativeRollout(value) {
|
|
5019
|
+
return isRecord(value) &&
|
|
5020
|
+
Boolean(stringValue(value.fd)) &&
|
|
5021
|
+
Boolean(stringValue(value.device)) &&
|
|
5022
|
+
Boolean(stringValue(value.inode)) &&
|
|
5023
|
+
Boolean(stringValue(value.path));
|
|
5024
|
+
}
|
|
5025
|
+
async function resolveCurrentNativeAgentSessionIdentity({ options, agent, pid, cwd }) {
|
|
5026
|
+
if (agent === "codex") {
|
|
5027
|
+
return createAgentSessionProvider("codex", options)
|
|
5028
|
+
.resolveActiveSessionIdentityForPid(pid, cwd);
|
|
5029
|
+
}
|
|
5030
|
+
const rows = loadClaudeAgentRows(options, { required: true })
|
|
5031
|
+
.filter((row) => row.pid === pid);
|
|
5032
|
+
if (rows.length === 0) {
|
|
5033
|
+
return undefined;
|
|
5034
|
+
}
|
|
5035
|
+
const sessionIds = [...new Set(rows.map((row) => stringValue(row.sessionId)).filter((value) => value !== undefined))];
|
|
5036
|
+
if (sessionIds.length > 1) {
|
|
5037
|
+
throw new Error(`Claude process ${pid} has conflicting exact session identities`);
|
|
5038
|
+
}
|
|
5039
|
+
const sessionId = sessionIds[0];
|
|
5040
|
+
if (!sessionId) {
|
|
5041
|
+
throw new Error(`Claude process ${pid} is visible but its exact sessionId is unavailable`);
|
|
5042
|
+
}
|
|
5043
|
+
const startedAtValues = [...new Set(rows
|
|
5044
|
+
.map((row) => Number(row.startedAt))
|
|
5045
|
+
.filter((value) => Number.isSafeInteger(value) && value > 0))];
|
|
5046
|
+
if (startedAtValues.length > 1) {
|
|
5047
|
+
throw new Error(`Claude process ${pid} has conflicting process-incarnation timestamps`);
|
|
5048
|
+
}
|
|
5049
|
+
const startedAt = startedAtValues[0];
|
|
5050
|
+
if (!startedAt) {
|
|
5051
|
+
throw new Error(`Claude process ${pid} is visible but its process-incarnation startedAt is unavailable`);
|
|
5052
|
+
}
|
|
5053
|
+
return {
|
|
5054
|
+
sessionId,
|
|
5055
|
+
processUuid: `claude-pid:${pid}:started:${startedAt}`,
|
|
5056
|
+
evidence: "claude_agents_exact_pid"
|
|
5057
|
+
};
|
|
5058
|
+
}
|
|
5059
|
+
function nativeAgentIdentityMatchesTurn(conversation, currentIdentity) {
|
|
5060
|
+
const takeover = isRecord(conversation.native_session_takeover)
|
|
5061
|
+
? conversation.native_session_takeover
|
|
5062
|
+
: undefined;
|
|
5063
|
+
const storedSessionId = stringValue(takeover?.terminal_agent_session_id);
|
|
5064
|
+
const storedProcessUuid = stringValue(takeover?.terminal_agent_process_uuid);
|
|
5065
|
+
const storedProcessBirth = stringValue(takeover?.terminal_agent_process_birth);
|
|
5066
|
+
const storedRollout = isRecord(takeover?.terminal_agent_rollout)
|
|
5067
|
+
? takeover.terminal_agent_rollout
|
|
5068
|
+
: undefined;
|
|
5069
|
+
const strictNativeIdentity = Number(takeover?.terminal_agent_identity_protocol) === 1;
|
|
5070
|
+
const strictClaudeTurn = strictNativeIdentity && executorForConversation(conversation).kind === "claude";
|
|
5071
|
+
const strictCodexTurn = strictNativeIdentity && executorForConversation(conversation).kind === "codex";
|
|
5072
|
+
if (strictNativeIdentity &&
|
|
5073
|
+
(!storedSessionId || !currentIdentity?.sessionId)) {
|
|
5074
|
+
return false;
|
|
5075
|
+
}
|
|
5076
|
+
if (strictClaudeTurn &&
|
|
5077
|
+
(!storedProcessUuid ||
|
|
5078
|
+
!currentIdentity?.processUuid ||
|
|
5079
|
+
storedProcessUuid !== currentIdentity.processUuid)) {
|
|
5080
|
+
return false;
|
|
5081
|
+
}
|
|
5082
|
+
if (strictCodexTurn &&
|
|
5083
|
+
(!storedProcessUuid ||
|
|
5084
|
+
!storedProcessBirth ||
|
|
5085
|
+
!isCompleteNativeRollout(storedRollout) ||
|
|
5086
|
+
!currentIdentity?.processUuid ||
|
|
5087
|
+
!currentIdentity.processBirth ||
|
|
5088
|
+
!isCompleteNativeRollout(currentIdentity.rollout))) {
|
|
5089
|
+
return false;
|
|
5090
|
+
}
|
|
5091
|
+
if (storedSessionId && storedSessionId !== currentIdentity?.sessionId) {
|
|
5092
|
+
return false;
|
|
5093
|
+
}
|
|
5094
|
+
if (storedProcessUuid &&
|
|
5095
|
+
storedProcessUuid !== currentIdentity?.processUuid) {
|
|
5096
|
+
return false;
|
|
5097
|
+
}
|
|
5098
|
+
if (storedProcessBirth &&
|
|
5099
|
+
storedProcessBirth !== currentIdentity?.processBirth) {
|
|
5100
|
+
return false;
|
|
5101
|
+
}
|
|
5102
|
+
if (storedRollout &&
|
|
5103
|
+
(stringValue(storedRollout.fd) !== currentIdentity?.rollout?.fd ||
|
|
5104
|
+
stringValue(storedRollout.device) !== currentIdentity?.rollout?.device ||
|
|
5105
|
+
stringValue(storedRollout.inode) !== currentIdentity?.rollout?.inode ||
|
|
5106
|
+
stringValue(storedRollout.path) !== currentIdentity?.rollout?.path)) {
|
|
5107
|
+
return false;
|
|
5108
|
+
}
|
|
5109
|
+
return true;
|
|
5110
|
+
}
|
|
5111
|
+
function assertNativeAgentIdentityForRuntime({ runtime, currentIdentity, agent, pid }) {
|
|
5112
|
+
if (runtime?.expectedEmptyNativeSession === true) {
|
|
5113
|
+
if (!currentIdentity) {
|
|
5114
|
+
return;
|
|
5115
|
+
}
|
|
5116
|
+
throw new Error(`native ${agent} session appeared for process ${pid} during terminal control`);
|
|
5117
|
+
}
|
|
5118
|
+
if (runtime?.requireNativeRolloutIdentity === true &&
|
|
5119
|
+
(!runtime.nativeSessionId ||
|
|
5120
|
+
!runtime.nativeProcessUuid ||
|
|
5121
|
+
!runtime.nativeProcessBirth ||
|
|
5122
|
+
!isCompleteNativeRollout(runtime.nativeRollout) ||
|
|
5123
|
+
!currentIdentity?.sessionId ||
|
|
5124
|
+
!currentIdentity.processUuid ||
|
|
5125
|
+
!currentIdentity.processBirth ||
|
|
5126
|
+
!isCompleteNativeRollout(currentIdentity.rollout))) {
|
|
5127
|
+
throw new Error(`native ${agent} rollout incarnation cannot be verified for process ${pid}; ` +
|
|
5128
|
+
"refresh list before controlling the terminal");
|
|
5129
|
+
}
|
|
5130
|
+
if (runtime?.requireNativeProcessUuid === true &&
|
|
5131
|
+
(!runtime.nativeProcessUuid ||
|
|
5132
|
+
!currentIdentity?.processUuid ||
|
|
5133
|
+
currentIdentity.processUuid !== runtime.nativeProcessUuid)) {
|
|
5134
|
+
throw new Error(`native ${agent} process incarnation cannot be verified for process ${pid}; ` +
|
|
5135
|
+
"refresh list before controlling the terminal");
|
|
5136
|
+
}
|
|
5137
|
+
if (!runtime?.nativeSessionId) {
|
|
5138
|
+
return;
|
|
5139
|
+
}
|
|
5140
|
+
const expectedRollout = runtime.nativeRollout;
|
|
5141
|
+
if (currentIdentity?.sessionId !== runtime.nativeSessionId ||
|
|
5142
|
+
(runtime.nativeProcessUuid &&
|
|
5143
|
+
currentIdentity?.processUuid !== runtime.nativeProcessUuid) ||
|
|
5144
|
+
(runtime.nativeProcessBirth &&
|
|
5145
|
+
currentIdentity?.processBirth !== runtime.nativeProcessBirth) ||
|
|
5146
|
+
(expectedRollout &&
|
|
5147
|
+
(currentIdentity?.rollout?.fd !== expectedRollout.fd ||
|
|
5148
|
+
currentIdentity?.rollout?.device !== expectedRollout.device ||
|
|
5149
|
+
currentIdentity?.rollout?.inode !== expectedRollout.inode ||
|
|
5150
|
+
currentIdentity?.rollout?.path !== expectedRollout.path))) {
|
|
5151
|
+
throw new Error(`native ${agent} session identity changed for process ${pid}; ` +
|
|
5152
|
+
"refresh list before controlling the terminal");
|
|
5153
|
+
}
|
|
5154
|
+
}
|
|
5155
|
+
function assertNativeAgentIdentityForTurn({ conversation, currentIdentity, operation }) {
|
|
5156
|
+
if (nativeAgentIdentityMatchesTurn(conversation, currentIdentity)) {
|
|
5157
|
+
return;
|
|
5158
|
+
}
|
|
5159
|
+
const takeover = isRecord(conversation.native_session_takeover)
|
|
5160
|
+
? conversation.native_session_takeover
|
|
5161
|
+
: undefined;
|
|
5162
|
+
const strictClaudeTurn = Boolean(Number(takeover?.terminal_agent_identity_protocol) === 1 &&
|
|
5163
|
+
executorForConversation(conversation).kind === "claude");
|
|
5164
|
+
const storedProcessUuid = stringValue(takeover?.terminal_agent_process_uuid);
|
|
5165
|
+
if (strictClaudeTurn &&
|
|
5166
|
+
(!storedProcessUuid || !currentIdentity?.processUuid)) {
|
|
5167
|
+
throw new Error(`cannot ${operation} Turn ${turnIdForConversation(conversation)}: ` +
|
|
5168
|
+
"native Claude process incarnation cannot be verified; this Claude CLI " +
|
|
5169
|
+
"must report both sessionId and startedAt before AKK can control it");
|
|
5170
|
+
}
|
|
5171
|
+
const expected = stringValue(takeover?.terminal_agent_session_id) ??
|
|
5172
|
+
"unavailable";
|
|
5173
|
+
const observed = currentIdentity?.sessionId ?? "unverifiable";
|
|
5174
|
+
throw new Error(`cannot ${operation} Turn ${turnIdForConversation(conversation)}: native ` +
|
|
5175
|
+
`agent session identity changed or cannot be verified ` +
|
|
5176
|
+
`(expected ${expected}, observed ${observed})`);
|
|
5177
|
+
}
|
|
5178
|
+
function withNativeAgentSessionIdentity(conversation, identity) {
|
|
5179
|
+
const takeover = isRecord(conversation.native_session_takeover)
|
|
5180
|
+
? conversation.native_session_takeover
|
|
5181
|
+
: {};
|
|
5182
|
+
return {
|
|
5183
|
+
...conversation,
|
|
5184
|
+
native_session_takeover: {
|
|
5185
|
+
...takeover,
|
|
5186
|
+
terminal_agent_identity_protocol: 1,
|
|
5187
|
+
terminal_agent_session_id: identity.sessionId,
|
|
5188
|
+
terminal_agent_process_uuid: identity.processUuid,
|
|
5189
|
+
terminal_agent_process_birth: identity.processBirth,
|
|
5190
|
+
terminal_agent_rollout: identity.rollout,
|
|
5191
|
+
terminal_agent_identity_evidence: identity.evidence,
|
|
5192
|
+
terminal_agent_identity_bound_at: new Date().toISOString()
|
|
5193
|
+
}
|
|
5194
|
+
};
|
|
5195
|
+
}
|
|
5196
|
+
async function pollNativeAgentSessionIdentity({ options, executor, terminalControl, pid, attempts = 40, delayMs = 50 }) {
|
|
5197
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
5198
|
+
const identity = await resolveCurrentNativeAgentSessionIdentity({
|
|
5199
|
+
options,
|
|
5200
|
+
agent: executor.kind,
|
|
5201
|
+
pid,
|
|
5202
|
+
cwd: terminalControl.currentPath
|
|
5203
|
+
});
|
|
5204
|
+
if (identity) {
|
|
5205
|
+
return identity;
|
|
5206
|
+
}
|
|
5207
|
+
if (attempt + 1 < attempts) {
|
|
5208
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
5209
|
+
}
|
|
5210
|
+
}
|
|
5211
|
+
return undefined;
|
|
5212
|
+
}
|
|
5213
|
+
function managedTurnsForSession(storeDir, sessionId) {
|
|
5214
|
+
return listConversations(storeDir)
|
|
5215
|
+
.filter(isDiscoverableTmuxConversation)
|
|
5216
|
+
.filter((conversation) => sessionIdForConversation(conversation) === sessionId)
|
|
5217
|
+
.sort(compareManagedConversationRecency);
|
|
5218
|
+
}
|
|
5219
|
+
function managedTurnsForSessionTarget(storeDir, targetId) {
|
|
5220
|
+
const allTurns = listConversations(storeDir)
|
|
5221
|
+
.filter(isDiscoverableTmuxConversation);
|
|
5222
|
+
const exactTurn = allTurns.find((conversation) => turnIdForConversation(conversation) === targetId ||
|
|
5223
|
+
conversation.conversation_id === targetId);
|
|
5224
|
+
if (exactTurn &&
|
|
5225
|
+
sessionIdForConversation(exactTurn) !== targetId) {
|
|
5226
|
+
throw new Error(`turn ${targetId} is an execution identity, not an ordinary send target; ` +
|
|
5227
|
+
`send to session ${sessionIdForConversation(exactTurn)} instead`);
|
|
5228
|
+
}
|
|
5229
|
+
const turns = allTurns
|
|
5230
|
+
.filter((conversation) => sessionIdForConversation(conversation) === targetId)
|
|
5231
|
+
.sort(compareManagedConversationRecency);
|
|
5232
|
+
if (turns.length === 0) {
|
|
5233
|
+
throw new Error(`managed session ${targetId} was not found`);
|
|
5234
|
+
}
|
|
5235
|
+
return turns;
|
|
5236
|
+
}
|
|
5237
|
+
function assertManagedSessionCanStartTurn(turns) {
|
|
5238
|
+
const blocking = turns.filter((conversation) => SESSION_SEND_BLOCKING_STATUSES.has(conversation.status));
|
|
5239
|
+
if (blocking.length > 0) {
|
|
5240
|
+
const owner = blocking[0];
|
|
5241
|
+
throw new Error(`session ${sessionIdForConversation(owner)} already has active turn ` +
|
|
5242
|
+
`${turnIdForConversation(owner)} (${owner.status}); wait for its callback, ` +
|
|
5243
|
+
"respond to it if it is waiting for OpenClaw, cancel it, or close it before sending another turn");
|
|
5244
|
+
}
|
|
5245
|
+
}
|
|
5246
|
+
function reusableManagedSessionTurnForTerminal({ options, terminalConversation, currentNativeIdentity }) {
|
|
5247
|
+
const matches = listConversations(storeDirFromOptions(options))
|
|
5248
|
+
.filter(isDiscoverableTmuxConversation)
|
|
5249
|
+
.filter((conversation) => managedTurnMatchesResolvedTerminal(conversation, terminalConversation, currentNativeIdentity));
|
|
5250
|
+
const sessions = new Map();
|
|
5251
|
+
for (const turn of matches) {
|
|
5252
|
+
const sessionId = sessionIdForConversation(turn);
|
|
5253
|
+
const group = sessions.get(sessionId) ?? [];
|
|
5254
|
+
group.push(turn);
|
|
5255
|
+
sessions.set(sessionId, group);
|
|
5256
|
+
}
|
|
5257
|
+
if (sessions.size > 1) {
|
|
5258
|
+
throw new Error(`terminal ${terminalConversation.terminalControl.target} matches multiple ` +
|
|
5259
|
+
"managed sessions; send to an explicit session_id from AKK list");
|
|
5260
|
+
}
|
|
5261
|
+
const group = [...sessions.values()][0];
|
|
5262
|
+
return group?.sort(compareManagedConversationRecency)[0];
|
|
5263
|
+
}
|
|
5264
|
+
function managedTurnMatchesResolvedTerminal(conversation, terminalConversation, currentNativeIdentity) {
|
|
5265
|
+
const takeover = isRecord(conversation.native_session_takeover)
|
|
5266
|
+
? conversation.native_session_takeover
|
|
5267
|
+
: undefined;
|
|
5268
|
+
const storedControl = terminalControlFromTakeover(takeover);
|
|
5269
|
+
const storedTerminalIdentity = parseTerminalConversationId(stringValue(takeover?.native_session_id));
|
|
5270
|
+
const currentTerminalIdentity = parseTerminalConversationId(terminalConversation.conversationId);
|
|
5271
|
+
return Boolean(storedControl &&
|
|
5272
|
+
storedTerminalIdentity &&
|
|
5273
|
+
currentTerminalIdentity &&
|
|
5274
|
+
executorForConversation(conversation).kind === terminalConversation.agent &&
|
|
5275
|
+
storedTerminalIdentity.agent === currentTerminalIdentity.agent &&
|
|
5276
|
+
storedTerminalIdentity.target === currentTerminalIdentity.target &&
|
|
5277
|
+
storedTerminalIdentity.pid === currentTerminalIdentity.pid &&
|
|
5278
|
+
Number(takeover?.terminal_agent_pid) === terminalConversation.pid &&
|
|
5279
|
+
nativeAgentIdentityMatchesTurn(conversation, currentNativeIdentity) &&
|
|
5280
|
+
terminalControlSelectorKey(storedControl) ===
|
|
5281
|
+
terminalControlSelectorKey(terminalConversation.terminalControl) &&
|
|
5282
|
+
matchesConfiguredWorkspace(conversation.workspace, terminalConversation.terminalControl.currentPath));
|
|
5283
|
+
}
|
|
5284
|
+
function createManagedTerminalTurn({ options, conversationId, agent, pid, messageBody, terminalControl, previousTurn, nativeAgentIdentity }) {
|
|
4360
5285
|
const workspace = terminalControl.currentPath ?? process.cwd();
|
|
4361
5286
|
const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(workspace));
|
|
4362
|
-
const executor =
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
5287
|
+
const executor = previousTurn
|
|
5288
|
+
? executorForConversation(previousTurn)
|
|
5289
|
+
: resolveExecutor({
|
|
5290
|
+
kind: agent,
|
|
5291
|
+
session: conversationId
|
|
5292
|
+
});
|
|
4366
5293
|
const now = new Date();
|
|
4367
5294
|
const conversation = createConversation({
|
|
4368
5295
|
userRequest: String(messageBody),
|
|
5296
|
+
sessionId: previousTurn
|
|
5297
|
+
? sessionIdForConversation(previousTurn)
|
|
5298
|
+
: undefined,
|
|
4369
5299
|
workspace,
|
|
4370
|
-
openclawSession: options.openclawSession ??
|
|
5300
|
+
openclawSession: options.openclawSession ?? previousTurn?.openclaw_session ??
|
|
5301
|
+
"agent:main:main",
|
|
4371
5302
|
executorKind: executor.kind,
|
|
4372
5303
|
executorSession: executor.session,
|
|
4373
5304
|
softLimit: Number(options.softLimit ?? 50),
|
|
@@ -4375,8 +5306,8 @@ function createManagedTerminalConversationFromRawId({ options, conversationId, a
|
|
|
4375
5306
|
now
|
|
4376
5307
|
});
|
|
4377
5308
|
const paths = pathsForConversation(conversation.conversation_id, storeDir);
|
|
4378
|
-
const
|
|
4379
|
-
?
|
|
5309
|
+
const previousTakeover = previousTurn && isRecord(previousTurn.native_session_takeover)
|
|
5310
|
+
? previousTurn.native_session_takeover
|
|
4380
5311
|
: undefined;
|
|
4381
5312
|
const attachedConversation = withStoragePaths({
|
|
4382
5313
|
...conversation,
|
|
@@ -4384,20 +5315,31 @@ function createManagedTerminalConversationFromRawId({ options, conversationId, a
|
|
|
4384
5315
|
status: "idle",
|
|
4385
5316
|
idle_since: now.toISOString(),
|
|
4386
5317
|
updated_at: now.toISOString(),
|
|
4387
|
-
gateway_url: options.gatewayUrl ??
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
5318
|
+
gateway_url: options.gatewayUrl ?? previousTurn?.gateway_url ??
|
|
5319
|
+
"ws://127.0.0.1:18789",
|
|
5320
|
+
gateway_method: options.gatewayMethod ?? previousTurn?.gateway_method,
|
|
5321
|
+
gateway_session: options.gatewaySession ?? options.openclawSession ??
|
|
5322
|
+
previousTurn?.gateway_session ?? previousTurn?.openclaw_session ??
|
|
5323
|
+
"agent:main:main",
|
|
5324
|
+
openclaw_bin: options.openclawBin ?? previousTurn?.openclaw_bin ??
|
|
5325
|
+
resolveOptionalExecutable("openclaw"),
|
|
4391
5326
|
native_session_takeover: {
|
|
4392
5327
|
agent,
|
|
5328
|
+
terminal_agent_identity_protocol: 1,
|
|
4393
5329
|
native_session_id: conversationId,
|
|
4394
5330
|
terminal_agent_pid: pid,
|
|
4395
|
-
terminal_agent_session_id:
|
|
5331
|
+
terminal_agent_session_id: nativeAgentIdentity?.sessionId,
|
|
5332
|
+
terminal_agent_process_uuid: nativeAgentIdentity?.processUuid,
|
|
5333
|
+
terminal_agent_process_birth: nativeAgentIdentity?.processBirth,
|
|
5334
|
+
terminal_agent_rollout: nativeAgentIdentity?.rollout,
|
|
5335
|
+
terminal_agent_identity_evidence: nativeAgentIdentity?.evidence,
|
|
4396
5336
|
source_cwd: workspace,
|
|
4397
5337
|
source_title: `Terminal-controlled ${executor.display_name} ${terminalControl.target}`,
|
|
4398
5338
|
strategy: "terminal_control",
|
|
4399
|
-
attached_at: now.toISOString(),
|
|
4400
|
-
takeover_match_kind:
|
|
5339
|
+
attached_at: stringValue(previousTakeover?.attached_at) ?? now.toISOString(),
|
|
5340
|
+
takeover_match_kind: previousTurn
|
|
5341
|
+
? "managed_session_send"
|
|
5342
|
+
: "raw_terminal_send",
|
|
4401
5343
|
terminal_control: terminalControl,
|
|
4402
5344
|
needs_bootstrap: false,
|
|
4403
5345
|
terminal_bridge: true
|
|
@@ -4425,15 +5367,17 @@ function createManagedTerminalConversationFromRawId({ options, conversationId, a
|
|
|
4425
5367
|
message
|
|
4426
5368
|
};
|
|
4427
5369
|
}
|
|
4428
|
-
function openClawYieldNextAction({ conversationId, source, callbackExpected }) {
|
|
5370
|
+
function openClawYieldNextAction({ conversationId, sessionId, turnId, source, callbackExpected }) {
|
|
4429
5371
|
const callbackText = callbackExpected
|
|
4430
5372
|
? "The coding agent should report completion, questions, or errors through the existing Agent Knock Knock callback for this conversation."
|
|
4431
5373
|
: "No AKK-managed callback is registered for this raw terminal-controlled id; do not wait synchronously. Use AKK status/list later or attach/create an AKK conversation when callback delivery is required.";
|
|
4432
5374
|
return {
|
|
4433
5375
|
action: "yield",
|
|
4434
|
-
reason: "The
|
|
5376
|
+
reason: "The requested agent work was handed off asynchronously. End this OpenClaw turn now instead of waiting, polling, or treating the send as a synchronous agent result.",
|
|
4435
5377
|
source,
|
|
4436
5378
|
conversation_id: conversationId,
|
|
5379
|
+
session_id: sessionId,
|
|
5380
|
+
turn_id: turnId,
|
|
4437
5381
|
callback_expected: callbackExpected,
|
|
4438
5382
|
do_not: "Do not inspect event logs, process lists, terminal screens, files, stdout, or stderr while waiting unless the user explicitly asks for status.",
|
|
4439
5383
|
expected_callback: callbackText
|
|
@@ -5400,6 +6344,13 @@ function runCallbackRetryMonitor(options) {
|
|
|
5400
6344
|
}
|
|
5401
6345
|
return;
|
|
5402
6346
|
}
|
|
6347
|
+
const gatewayRoute = resolveCallbackGatewayRoute({
|
|
6348
|
+
gatewayUrl: callbackDelivery.gateway_url,
|
|
6349
|
+
token: callbackDelivery.gateway_token
|
|
6350
|
+
}, {
|
|
6351
|
+
gatewayUrl: conversation.gateway_url,
|
|
6352
|
+
token: conversation.gateway_token
|
|
6353
|
+
});
|
|
5403
6354
|
try {
|
|
5404
6355
|
runCallbackTransaction({
|
|
5405
6356
|
statePath,
|
|
@@ -5408,8 +6359,8 @@ function runCallbackRetryMonitor(options) {
|
|
|
5408
6359
|
gatewaySession: stringValue(callbackDelivery.gateway_session) ?? conversation.gateway_session,
|
|
5409
6360
|
openclawSession: conversation.openclaw_session,
|
|
5410
6361
|
openclawBin: stringValue(callbackDelivery.openclaw_bin) ?? conversation.openclaw_bin,
|
|
5411
|
-
gatewayUrl:
|
|
5412
|
-
token:
|
|
6362
|
+
gatewayUrl: gatewayRoute.gatewayUrl,
|
|
6363
|
+
token: gatewayRoute.token,
|
|
5413
6364
|
closeTerminalBridgeOnDone: callbackDelivery.close_terminal_bridge_on_done === true,
|
|
5414
6365
|
retryPending: true,
|
|
5415
6366
|
disableCallbackRetry: true
|
|
@@ -7414,6 +8365,16 @@ function runRetryCallback(options) {
|
|
|
7414
8365
|
if (!callbackDelivery || !isRecord(callbackDelivery.message)) {
|
|
7415
8366
|
throw new Error(`cannot retry callback for ${conversation.conversation_id}; pending callback is missing`);
|
|
7416
8367
|
}
|
|
8368
|
+
const gatewayRoute = resolveCallbackGatewayRoute({
|
|
8369
|
+
gatewayUrl: options.gatewayUrl,
|
|
8370
|
+
token: options.token
|
|
8371
|
+
}, {
|
|
8372
|
+
gatewayUrl: callbackDelivery.gateway_url,
|
|
8373
|
+
token: callbackDelivery.gateway_token
|
|
8374
|
+
}, {
|
|
8375
|
+
gatewayUrl: conversation.gateway_url,
|
|
8376
|
+
token: conversation.gateway_token
|
|
8377
|
+
});
|
|
7417
8378
|
runCallbackTransaction({
|
|
7418
8379
|
...options,
|
|
7419
8380
|
statePath,
|
|
@@ -7422,12 +8383,28 @@ function runRetryCallback(options) {
|
|
|
7422
8383
|
gatewaySession: stringValue(callbackDelivery.gateway_session) ?? conversation.gateway_session,
|
|
7423
8384
|
openclawSession: conversation.openclaw_session,
|
|
7424
8385
|
openclawBin: stringValue(callbackDelivery.openclaw_bin) ?? conversation.openclaw_bin,
|
|
7425
|
-
gatewayUrl:
|
|
7426
|
-
token:
|
|
8386
|
+
gatewayUrl: gatewayRoute.gatewayUrl,
|
|
8387
|
+
token: gatewayRoute.token,
|
|
7427
8388
|
closeTerminalBridgeOnDone: callbackDelivery.close_terminal_bridge_on_done === true,
|
|
7428
8389
|
retryPending: true
|
|
7429
8390
|
});
|
|
7430
8391
|
}
|
|
8392
|
+
function resolveCallbackGatewayRoute(...candidates) {
|
|
8393
|
+
for (const candidate of candidates) {
|
|
8394
|
+
const token = stringValue(candidate?.token);
|
|
8395
|
+
if (!token || token === "<token>") {
|
|
8396
|
+
continue;
|
|
8397
|
+
}
|
|
8398
|
+
return {
|
|
8399
|
+
gatewayUrl: stringValue(candidate?.gatewayUrl),
|
|
8400
|
+
token
|
|
8401
|
+
};
|
|
8402
|
+
}
|
|
8403
|
+
return {
|
|
8404
|
+
gatewayUrl: undefined,
|
|
8405
|
+
token: undefined
|
|
8406
|
+
};
|
|
8407
|
+
}
|
|
7431
8408
|
function runCallbackTransaction(options) {
|
|
7432
8409
|
const releaseLock = acquireFileLock(`${options.statePath}.lock`);
|
|
7433
8410
|
let prepared;
|
|
@@ -7446,6 +8423,10 @@ function prepareLockedCallback(options) {
|
|
|
7446
8423
|
? options.conversationOverride
|
|
7447
8424
|
: loadState(options.statePath);
|
|
7448
8425
|
const executor = executorForConversation(conversation);
|
|
8426
|
+
const persistedGatewayRoute = resolveCallbackGatewayRoute({
|
|
8427
|
+
gatewayUrl: options.gatewayUrl,
|
|
8428
|
+
token: options.token
|
|
8429
|
+
});
|
|
7449
8430
|
const message = options.retryPending === true || options.preserveMessageId === true
|
|
7450
8431
|
? parseMessageJson(messageInput)
|
|
7451
8432
|
: extractStructuredMessage({
|
|
@@ -7470,6 +8451,11 @@ function prepareLockedCallback(options) {
|
|
|
7470
8451
|
sameDeliveryMessage &&
|
|
7471
8452
|
isRetryableCallbackDelivery(conversation, inheritedDelivery);
|
|
7472
8453
|
const duplicateMessage = isDuplicateMessage(existingEvents, message);
|
|
8454
|
+
if (TERMINAL_DISPATCH_RELEASE_STATUSES.has(conversation.status) &&
|
|
8455
|
+
!duplicateMessage) {
|
|
8456
|
+
throw new Error(`refusing late callback ${message.id} for released Turn ` +
|
|
8457
|
+
`${turnIdForConversation(conversation)} (${conversation.status})`);
|
|
8458
|
+
}
|
|
7473
8459
|
const recoveryMessageAlreadyLogged = options.recoverMissingOutbox === true
|
|
7474
8460
|
? exactLoggedMessageForRecovery(existingEvents, message)
|
|
7475
8461
|
: false;
|
|
@@ -7555,7 +8541,7 @@ function prepareLockedCallback(options) {
|
|
|
7555
8541
|
last_attempt_at: now,
|
|
7556
8542
|
gateway_method: options.gatewayMethod,
|
|
7557
8543
|
gateway_session: options.gatewaySession ?? options.openclawSession ?? conversation.openclaw_session,
|
|
7558
|
-
gateway_url:
|
|
8544
|
+
gateway_url: persistedGatewayRoute.gatewayUrl,
|
|
7559
8545
|
openclaw_bin: options.openclawBin ?? conversation.openclaw_bin,
|
|
7560
8546
|
close_terminal_bridge_on_done: closeTerminalBridgeOnDone,
|
|
7561
8547
|
track_delivery: true,
|
|
@@ -8211,7 +9197,7 @@ function readExistingEvents(logPath) {
|
|
|
8211
9197
|
}
|
|
8212
9198
|
function loadConversationFromOptions(options) {
|
|
8213
9199
|
const storeDir = storeDirFromOptions(options);
|
|
8214
|
-
const conversationId = options.conversation ?? options.conversationId;
|
|
9200
|
+
const conversationId = options.turn ?? options.conversation ?? options.conversationId;
|
|
8215
9201
|
const statePath = expandHome(options.state ?? (conversationId ? statePathForConversationId(conversationId, storeDir) : undefined));
|
|
8216
9202
|
if (!statePath) {
|
|
8217
9203
|
throw new Error("--conversation or --state is required");
|
|
@@ -8238,6 +9224,8 @@ function storeDirFromOptions(options) {
|
|
|
8238
9224
|
function summarizeConversation(conversation) {
|
|
8239
9225
|
const executor = executorForConversation(conversation);
|
|
8240
9226
|
return {
|
|
9227
|
+
session_id: sessionIdForConversation(conversation),
|
|
9228
|
+
turn_id: turnIdForConversation(conversation),
|
|
8241
9229
|
conversation_id: conversation.conversation_id,
|
|
8242
9230
|
agent: executor.kind,
|
|
8243
9231
|
executor,
|
|
@@ -8719,11 +9707,12 @@ function markConversationStalled({ statePath, logPath, reason, detail = {} }) {
|
|
|
8719
9707
|
body: [
|
|
8720
9708
|
`AKK marked this ${executor.display_name} task as stalled: ${reason}.`,
|
|
8721
9709
|
"",
|
|
8722
|
-
`
|
|
8723
|
-
`
|
|
9710
|
+
`Turn: ${turnIdForConversation(conversation)}`,
|
|
9711
|
+
`AKK session: ${sessionIdForConversation(conversation)}`,
|
|
9712
|
+
`Agent session: ${executor.session}`,
|
|
8724
9713
|
terminalBridge
|
|
8725
|
-
? `Use \`AKK status ${conversation
|
|
8726
|
-
:
|
|
9714
|
+
? `Use \`AKK status --turn ${turnIdForConversation(conversation)}\` for details, \`AKK renew --turn ${turnIdForConversation(conversation)}\` to resume monitoring in this Turn, or \`AKK close --turn ${turnIdForConversation(conversation)}\` to close it. Start any independent retry with \`AKK send --session ${sessionIdForConversation(conversation)}\`.`
|
|
9715
|
+
: `Use \`AKK status --turn ${turnIdForConversation(conversation)}\` for details or \`AKK close --turn ${turnIdForConversation(conversation)}\` to close this Turn.`
|
|
8727
9716
|
].join("\n")
|
|
8728
9717
|
})
|
|
8729
9718
|
: undefined;
|
|
@@ -8973,6 +9962,8 @@ function canonicalJson(value) {
|
|
|
8973
9962
|
}
|
|
8974
9963
|
function messageFingerprint(message) {
|
|
8975
9964
|
return JSON.stringify({
|
|
9965
|
+
session_id: message.session_id,
|
|
9966
|
+
turn_id: message.turn_id,
|
|
8976
9967
|
conversation_id: message.conversation_id,
|
|
8977
9968
|
from: message.from,
|
|
8978
9969
|
to: message.to,
|
|
@@ -8989,6 +9980,8 @@ function deliverToGatewayMethod({ method, openclawBin, gatewayUrl, token, sessio
|
|
|
8989
9980
|
"--params",
|
|
8990
9981
|
JSON.stringify({
|
|
8991
9982
|
sessionKey,
|
|
9983
|
+
session_id: sessionIdForConversation(conversation),
|
|
9984
|
+
turn_id: turnIdForConversation(conversation),
|
|
8992
9985
|
statePath,
|
|
8993
9986
|
logPath,
|
|
8994
9987
|
conversation: redactCliOutput(conversation),
|
|
@@ -9265,11 +10258,15 @@ function createAgentSessionProvider(agent, options) {
|
|
|
9265
10258
|
if (agent !== "codex") {
|
|
9266
10259
|
throw new Error(`unsupported agent session provider: ${agent}`);
|
|
9267
10260
|
}
|
|
9268
|
-
if (options.threadsJson ||
|
|
10261
|
+
if (options.threadsJson ||
|
|
10262
|
+
options.processesJson ||
|
|
10263
|
+
options.rolloutsJson ||
|
|
10264
|
+
options.codexActiveSessionIdentitiesJson) {
|
|
9269
10265
|
return new CodexLocalSessionProvider(new InlineCodexSessionAdapter({
|
|
9270
10266
|
threads: parseJsonOption(options.threadsJson, "--threads-json"),
|
|
9271
10267
|
processes: parseJsonOption(options.processesJson, "--processes-json"),
|
|
9272
|
-
rollouts: parseJsonOption(options.rolloutsJson, "--rollouts-json")
|
|
10268
|
+
rollouts: parseJsonOption(options.rolloutsJson, "--rollouts-json"),
|
|
10269
|
+
activeSessionIdentities: parseJsonOption(options.codexActiveSessionIdentitiesJson, "--codex-active-session-identities-json")
|
|
9273
10270
|
}));
|
|
9274
10271
|
}
|
|
9275
10272
|
return new CodexLocalSessionProvider(new CodexStoreAdapter({
|
|
@@ -9397,10 +10394,14 @@ function usage() {
|
|
|
9397
10394
|
agent-knock-knock --version
|
|
9398
10395
|
agent-knock-knock delegate --request <text> [--agent ${agentList}] [--workspace <path>] [--store-dir <dir>]
|
|
9399
10396
|
agent-knock-knock list [--store-dir <dir>] [--agent ${agentList}] [--status <status>] [--all] [--reconcile] [--no-approval-scan] [--terminal-debug]
|
|
9400
|
-
agent-knock-knock status [--
|
|
9401
|
-
agent-knock-knock send [--
|
|
9402
|
-
agent-knock-knock
|
|
9403
|
-
agent-knock-knock
|
|
10397
|
+
agent-knock-knock status [--turn <turn-id|selector>] [--conversation <selector>] [--store-dir <dir>] [--reconcile] [--trace]
|
|
10398
|
+
agent-knock-knock send [--session <session-id|selector>] [--conversation <selector>] --message <text> [--type task] [--agent-timeout-minutes <minutes>] [--agent-hard-timeout-minutes <minutes>]
|
|
10399
|
+
agent-knock-knock respond --turn <turn-id|selector> --message <text> [--conversation <selector>]
|
|
10400
|
+
agent-knock-knock approve [--turn <turn-id|selector>] [--conversation <selector>] --expected-approval-fingerprint <fingerprint>
|
|
10401
|
+
agent-knock-knock cancel [--turn <turn-id|selector>] [--conversation <selector>]
|
|
10402
|
+
agent-knock-knock renew [--turn <turn-id|selector>] [--conversation <selector>]
|
|
10403
|
+
agent-knock-knock retry-callback [--turn <turn-id|selector>] [--conversation <selector>]
|
|
10404
|
+
agent-knock-knock close [--turn <turn-id|selector>] [--conversation <selector>]
|
|
9404
10405
|
agent-knock-knock install-openclaw [--verify] [--openclaw-bin <path>] [--skill-path <path>] [--skill-only] [--no-restart]
|
|
9405
10406
|
agent-knock-knock doctor [--openclaw-bin <path>]
|
|
9406
10407
|
agent-knock-knock callback --state <file> --message-json <json> [--record-only]
|