@scotthuang/agent-knock-knock 0.11.5 → 0.11.7
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 +25 -0
- package/README.md +3 -3
- package/dist/src/claude-lifecycle-compatibility.d.ts +16 -0
- package/dist/src/claude-lifecycle-compatibility.js +77 -0
- package/dist/src/claude-lifecycle-compatibility.js.map +1 -0
- package/dist/src/claude-local-transcript-provider.js +37 -17
- package/dist/src/claude-local-transcript-provider.js.map +1 -1
- package/dist/src/claude-terminal-agent-adapter.d.ts +4 -4
- package/dist/src/claude-terminal-agent-adapter.js +42 -35
- package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
- package/dist/src/cli.js +604 -405
- package/dist/src/cli.js.map +1 -1
- package/dist/src/codex-lifecycle-compatibility.js +2 -1
- package/dist/src/codex-lifecycle-compatibility.js.map +1 -1
- package/dist/src/codex-terminal-agent-adapter.d.ts +1 -0
- package/dist/src/codex-terminal-agent-adapter.js +3 -1
- package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
- package/dist/src/managed-session.d.ts +15 -0
- package/dist/src/managed-session.js +105 -4
- package/dist/src/managed-session.js.map +1 -1
- package/dist/src/native-thread-resume-snapshot.d.ts +5 -2
- package/dist/src/native-thread-resume-snapshot.js +65 -15
- package/dist/src/native-thread-resume-snapshot.js.map +1 -1
- package/dist/src/openclaw-plugin.js +2 -2
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/session-store.js +5 -0
- package/dist/src/session-store.js.map +1 -1
- package/dist/src/terminal-agent-adapter.d.ts +4 -14
- package/dist/src/terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +8 -2
- package/dist/src/terminal-agent-bridge.js +250 -95
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-control-provider.d.ts +49 -21
- package/dist/src/terminal-control-provider.js +328 -30
- package/dist/src/terminal-control-provider.js.map +1 -1
- package/dist/src/terminal-control-ref.d.ts +119 -0
- package/dist/src/terminal-control-ref.js +410 -0
- package/dist/src/terminal-control-ref.js.map +1 -0
- package/docs/quickstart-tmux.md +1 -1
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +1 -1
package/dist/src/cli.js
CHANGED
|
@@ -16,12 +16,13 @@ import { EXECUTOR_KINDS, executorDefinitionForKind, isExecutorKind } from "./exe
|
|
|
16
16
|
import { redactString, writeRuntimeLog } from "./runtime-log.js";
|
|
17
17
|
import { formatTranscript, readNdjsonLog } from "./transcript.js";
|
|
18
18
|
import { appendEvent, assertStoreWriterCompatible, defaultStoreDir, ensureDir, ensureStoreWritable, inspectStoreCompatibility, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, StoreLockTimeoutError, statePathForConversationId, withStoreWriterLease, withStoreWriterLeaseAsync } from "./store.js";
|
|
19
|
-
import { createManagedSessionId, createNativeThreadTransitionId, isExactNativeThreadId, managedSessionBindingToken, nativeThreadCommandFingerprint, terminalBindingFrom, unmanagedTerminalBindingToken } from "./managed-session.js";
|
|
19
|
+
import { createManagedSessionId, createNativeThreadTransitionId, isExactNativeThreadId, legacyManagedSessionBindingToken, legacyUnmanagedTerminalBindingToken, managedSessionBindingToken, nativeThreadCommandFingerprint, terminalBindingFrom, unmanagedTerminalBindingToken } from "./managed-session.js";
|
|
20
20
|
import { classifyCodexLifecyclePostcondition, evaluateResumeCandidateAvailability, hasStrongCodexLifecycleIdentity, isFreshCodexPostProbeScreen } from "./native-thread-lifecycle-policy.js";
|
|
21
21
|
import { createNativeThreadResumeSnapshot, nativeThreadCandidateSnapshotFingerprint, nativeThreadResumeSnapshotRowsMatchCandidates, resolveNativeThreadResumeSelection, saveNativeThreadResumeSnapshot, sortNativeThreadCandidates, terminalActionFingerprint, verifiedPreviousResumeCandidate } from "./native-thread-resume-snapshot.js";
|
|
22
22
|
import { listManagedSessions, loadManagedSession, loadNativeThreadTransition, nativeThreadTransitionsDir, saveManagedSession, saveNativeThreadTransition, tryLoadManagedSession } from "./session-store.js";
|
|
23
|
-
import { StaticTerminalControlProvider, TmuxTerminalControlProvider
|
|
23
|
+
import { createTerminalControlProviderRegistry as createProviderRegistry, StaticTerminalControlProvider, TmuxTerminalControlProvider } from "./terminal-control-provider.js";
|
|
24
24
|
import { parseTerminalConversationId } from "./terminal-agent-adapter.js";
|
|
25
|
+
import { associateTerminalEndpointEvidence, hasCanonicalTerminalEndpoint, sameTerminalControlEvidenceIncarnation, sameTerminalControlIncarnation, terminalControlEvidence, terminalControlEvidenceMatches, terminalEndpointFromControlRef, terminalEndpointIdentityFromEvidence, terminalEndpointIdentityKey, terminalLegacyRuntimeRoute, sameTerminalEndpointIdentity } from "./terminal-control-ref.js";
|
|
25
26
|
import { createProductionTerminalAgentRegistry } from "./terminal-agent-registry.js";
|
|
26
27
|
import { parseProcessElapsedSeconds, StaticTerminalProcessSource, SystemTerminalProcessSource } from "./terminal-process-source.js";
|
|
27
28
|
import { isExactClaudeNativeInspectionIdleComposer, NativeInspectionSubmissionError, TerminalAgentBridge, TerminalInputNotStartedError } from "./terminal-agent-bridge.js";
|
|
@@ -624,14 +625,22 @@ async function listActiveSessionsWithTerminalControl(provider, options, terminal
|
|
|
624
625
|
: [];
|
|
625
626
|
return createTerminalAgentBridge(options, terminalProvider).attachProcesses(provider.agent, activeSessions, { processTree });
|
|
626
627
|
}
|
|
627
|
-
function
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
628
|
+
function createRuntimeTerminalControlProviderRegistry(options) {
|
|
629
|
+
return createProviderRegistry([
|
|
630
|
+
options.terminalsJson || options.terminalScreensJson || options.processesJson
|
|
631
|
+
? new StaticTerminalControlProvider({
|
|
632
|
+
panes: options.terminalsJson
|
|
633
|
+
? parseJsonOption(options.terminalsJson, "--terminals-json")
|
|
634
|
+
: [],
|
|
635
|
+
screens: options.terminalScreensJson
|
|
636
|
+
? parseJsonOption(options.terminalScreensJson, "--terminal-screens-json")
|
|
637
|
+
: {}
|
|
638
|
+
})
|
|
639
|
+
: new TmuxTerminalControlProvider()
|
|
640
|
+
]);
|
|
641
|
+
}
|
|
642
|
+
function createTerminalControlProvider(options, registry = createRuntimeTerminalControlProviderRegistry(options)) {
|
|
643
|
+
return registry.require("tmux");
|
|
635
644
|
}
|
|
636
645
|
function createTerminalProcessSource(options) {
|
|
637
646
|
if (options.processesJson) {
|
|
@@ -884,7 +893,10 @@ function createTerminalAgentBridge(options, terminalProvider = createTerminalCon
|
|
|
884
893
|
terminalProvider,
|
|
885
894
|
async verifyIdentity({ agent, pid, terminalControl, runtime }) {
|
|
886
895
|
const adapter = registry.require(agent);
|
|
887
|
-
const
|
|
896
|
+
const requestedTerminal = terminalProvider.endpoint(terminalControl);
|
|
897
|
+
const resolvedTerminal = await terminalProvider.resolve(requestedTerminal);
|
|
898
|
+
const expectedWorkspace = options.workspace ?? resolvedTerminal.route.currentPath ??
|
|
899
|
+
terminalControl.currentPath;
|
|
888
900
|
if (!expectedWorkspace) {
|
|
889
901
|
throw new Error(`refusing terminal access to ${terminalControl.target}; its workspace is unavailable`);
|
|
890
902
|
}
|
|
@@ -896,15 +908,11 @@ function createTerminalAgentBridge(options, terminalProvider = createTerminalCon
|
|
|
896
908
|
if (!snapshot || !adapter.classifyProcess(snapshot)) {
|
|
897
909
|
throw new Error(`terminal conversation agent ${agent} with pid ${pid} is no longer active`);
|
|
898
910
|
}
|
|
899
|
-
|
|
900
|
-
const pane = panes.find((candidate) => candidate.kind === terminalControl.kind &&
|
|
901
|
-
candidate.target === terminalControl.target &&
|
|
902
|
-
candidate.panePid === terminalControl.panePid);
|
|
903
|
-
if (!pane || !terminalPaneContainsProcess(snapshot, pane, snapshots)) {
|
|
911
|
+
if (!terminalProvider.containsProcess(resolvedTerminal, snapshot, snapshots)) {
|
|
904
912
|
throw new Error(`terminal conversation agent ${agent} with pid ${pid} no longer belongs to pane ${terminalControl.target}`);
|
|
905
913
|
}
|
|
906
914
|
assertConfiguredWorkspace(expectedWorkspace, snapshot.cwd, `terminal access to ${terminalControl.target} by agent process ${pid}`);
|
|
907
|
-
assertConfiguredWorkspace(expectedWorkspace,
|
|
915
|
+
assertConfiguredWorkspace(expectedWorkspace, resolvedTerminal.route.currentPath, `terminal access to ${terminalControl.target} by terminal endpoint`);
|
|
908
916
|
const requiresNativeIdentity = Boolean(runtime?.nativeSessionId ||
|
|
909
917
|
runtime?.nativeProcessUuid ||
|
|
910
918
|
runtime?.nativeProcessBirth ||
|
|
@@ -923,7 +931,7 @@ function createTerminalAgentBridge(options, terminalProvider = createTerminalCon
|
|
|
923
931
|
options,
|
|
924
932
|
agent,
|
|
925
933
|
pid,
|
|
926
|
-
cwd: snapshot.cwd ??
|
|
934
|
+
cwd: snapshot.cwd ?? resolvedTerminal.route.currentPath,
|
|
927
935
|
preferredSessionId: runtime?.allowedPreMaterializationNativeIdentity &&
|
|
928
936
|
(runtime.expectedNativeSessionId ?? runtime.nativeSessionId) &&
|
|
929
937
|
runtime.allowedPreMaterializationNativeIdentity.sessionId !==
|
|
@@ -954,7 +962,7 @@ function createTerminalAgentBridge(options, terminalProvider = createTerminalCon
|
|
|
954
962
|
phase: "before",
|
|
955
963
|
pid,
|
|
956
964
|
processStartedAt: runtime.nativeProcessStartedAt,
|
|
957
|
-
cwd: snapshot.cwd ??
|
|
965
|
+
cwd: snapshot.cwd ?? resolvedTerminal.route.currentPath,
|
|
958
966
|
agentRows
|
|
959
967
|
});
|
|
960
968
|
const exactRows = agentRows.filter((row) => row.pid === pid);
|
|
@@ -973,13 +981,7 @@ function createTerminalAgentBridge(options, terminalProvider = createTerminalCon
|
|
|
973
981
|
}
|
|
974
982
|
}
|
|
975
983
|
return {
|
|
976
|
-
terminalControl:
|
|
977
|
-
...terminalControl,
|
|
978
|
-
socketPath: pane.socketPath,
|
|
979
|
-
panePid: pane.panePid,
|
|
980
|
-
currentCommand: pane.currentCommand,
|
|
981
|
-
currentPath: pane.currentPath
|
|
982
|
-
}
|
|
984
|
+
terminalControl: terminalProvider.toControlRef(resolvedTerminal, terminalControl.capabilities)
|
|
983
985
|
};
|
|
984
986
|
}
|
|
985
987
|
});
|
|
@@ -1003,7 +1005,7 @@ function terminalControlFromTakeover(nativeTakeover) {
|
|
|
1003
1005
|
const storedCapabilities = Array.isArray(terminalControl.capabilities)
|
|
1004
1006
|
? terminalControl.capabilities.filter(isTerminalControlCapability)
|
|
1005
1007
|
: [];
|
|
1006
|
-
|
|
1008
|
+
const control = {
|
|
1007
1009
|
kind: "tmux",
|
|
1008
1010
|
target,
|
|
1009
1011
|
session,
|
|
@@ -1025,6 +1027,59 @@ function terminalControlFromTakeover(nativeTakeover) {
|
|
|
1025
1027
|
"terminal_cancel"
|
|
1026
1028
|
]
|
|
1027
1029
|
};
|
|
1030
|
+
const endpointEvidence = nativeTakeover["terminal_endpoint"];
|
|
1031
|
+
if (endpointEvidence !== undefined) {
|
|
1032
|
+
try {
|
|
1033
|
+
associateTerminalEndpointEvidence(control, endpointEvidence);
|
|
1034
|
+
}
|
|
1035
|
+
catch {
|
|
1036
|
+
return undefined;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return control;
|
|
1040
|
+
}
|
|
1041
|
+
function terminalEndpointTakeoverFields(terminalControl) {
|
|
1042
|
+
return {
|
|
1043
|
+
terminal_control: terminalControl,
|
|
1044
|
+
...(hasCanonicalTerminalEndpoint(terminalControl)
|
|
1045
|
+
? { terminal_endpoint: terminalControlEvidence(terminalControl) }
|
|
1046
|
+
: {})
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Add canonical endpoint evidence to a verified v0.11.x Turn without changing
|
|
1051
|
+
* its public route-shaped terminal id or provider-owned control payload.
|
|
1052
|
+
* Callers must already hold the terminal, Store-writer, and Turn state locks.
|
|
1053
|
+
*/
|
|
1054
|
+
function refineTerminalTurnEndpoint({ conversation, statePath, terminalControl }) {
|
|
1055
|
+
const takeover = isRecord(conversation.native_session_takeover)
|
|
1056
|
+
? conversation.native_session_takeover
|
|
1057
|
+
: undefined;
|
|
1058
|
+
const storedControl = terminalControlFromTakeover(takeover);
|
|
1059
|
+
if (!takeover ||
|
|
1060
|
+
!storedControl ||
|
|
1061
|
+
takeover.terminal_endpoint !== undefined ||
|
|
1062
|
+
!hasCanonicalTerminalEndpoint(terminalControl)) {
|
|
1063
|
+
return conversation;
|
|
1064
|
+
}
|
|
1065
|
+
if (!terminalControlsShareIncarnation(storedControl, terminalControl)) {
|
|
1066
|
+
throw new Error(`cannot refine Turn ${turnIdForConversation(conversation)} terminal ` +
|
|
1067
|
+
"endpoint after its terminal incarnation changed");
|
|
1068
|
+
}
|
|
1069
|
+
const terminalEndpoint = terminalControlEvidence(terminalControl);
|
|
1070
|
+
// Validate that the additive evidence agrees with the exact legacy route
|
|
1071
|
+
// before making it durable. This also restores the in-memory association on
|
|
1072
|
+
// the old provider-owned control object.
|
|
1073
|
+
associateTerminalEndpointEvidence(storedControl, terminalEndpoint);
|
|
1074
|
+
const refined = {
|
|
1075
|
+
...conversation,
|
|
1076
|
+
native_session_takeover: {
|
|
1077
|
+
...takeover,
|
|
1078
|
+
terminal_endpoint: terminalEndpoint
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
saveState(statePath, refined);
|
|
1082
|
+
return refined;
|
|
1028
1083
|
}
|
|
1029
1084
|
function terminalRuntimeIdentityForConversation(conversation, terminalControl) {
|
|
1030
1085
|
const nativeTakeover = isRecord(conversation?.native_session_takeover)
|
|
@@ -1091,8 +1146,7 @@ function terminalRuntimeIdentityForConversation(conversation, terminalControl) {
|
|
|
1091
1146
|
binding.generation !== Number(conversation.terminal_binding_generation) ||
|
|
1092
1147
|
binding.native_thread_id !== expectedThreadId ||
|
|
1093
1148
|
binding.native_process.pid !== runtime.pid ||
|
|
1094
|
-
|
|
1095
|
-
terminalControlSelectorKey(terminalControl)) {
|
|
1149
|
+
!terminalControlsShareIncarnation(binding.terminal_control, terminalControl)) {
|
|
1096
1150
|
return runtime;
|
|
1097
1151
|
}
|
|
1098
1152
|
return withCodexCompanionFences(runtime, codexAllowedCompanionSetForManagedSession({
|
|
@@ -1143,19 +1197,14 @@ async function migrateLegacyTerminalAgentIdentity({ conversation, statePath, log
|
|
|
1143
1197
|
const registry = createRuntimeTerminalAgentRegistry(options);
|
|
1144
1198
|
const adapter = registry.require("codex");
|
|
1145
1199
|
const snapshots = await createTerminalProcessSource(options).listProcessSnapshots((snapshot) => adapter.classifyProcess(snapshot) !== undefined, { includeAncestors: true });
|
|
1146
|
-
const
|
|
1147
|
-
const
|
|
1148
|
-
pane.target === terminalControl.target &&
|
|
1149
|
-
pane.panePid === terminalControl.panePid);
|
|
1150
|
-
if (matchingPanes.length !== 1) {
|
|
1151
|
-
return conversation;
|
|
1152
|
-
}
|
|
1200
|
+
const terminalProvider = createTerminalControlProvider(options);
|
|
1201
|
+
const resolvedTerminal = await terminalProvider.resolve(terminalProvider.endpoint(terminalControl));
|
|
1153
1202
|
const candidates = snapshots.flatMap((snapshot) => {
|
|
1154
1203
|
const classified = adapter.classifyProcess(snapshot);
|
|
1155
1204
|
return classified ? [{ ...classified, agent: "codex" }] : [];
|
|
1156
1205
|
});
|
|
1157
1206
|
const matches = candidates.filter((candidate) => candidate.sessionId === nativeSessionId &&
|
|
1158
|
-
|
|
1207
|
+
terminalProvider.containsProcess(resolvedTerminal, candidate, snapshots));
|
|
1159
1208
|
if (matches.length !== 1) {
|
|
1160
1209
|
return conversation;
|
|
1161
1210
|
}
|
|
@@ -1189,9 +1238,7 @@ async function migrateLegacyTerminalAgentIdentity({ conversation, statePath, log
|
|
|
1189
1238
|
return current;
|
|
1190
1239
|
}
|
|
1191
1240
|
if (currentTakeover.native_session_id !== nativeSessionId ||
|
|
1192
|
-
currentControl
|
|
1193
|
-
currentControl.socketPath !== terminalControl.socketPath ||
|
|
1194
|
-
currentControl.panePid !== terminalControl.panePid) {
|
|
1241
|
+
!terminalControlsShareIncarnation(currentControl, terminalControl)) {
|
|
1195
1242
|
return current;
|
|
1196
1243
|
}
|
|
1197
1244
|
const migratedAt = new Date().toISOString();
|
|
@@ -1271,10 +1318,8 @@ function assertSafeAbortedTerminalRetryBinding({ owner, receipt, storeDir, termi
|
|
|
1271
1318
|
!Number.isSafeInteger(ownerAgentPid) ||
|
|
1272
1319
|
binding.native_process.pid !== ownerAgentPid ||
|
|
1273
1320
|
!ownerControl ||
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
terminalControlSelectorKey(binding.terminal_control) !==
|
|
1277
|
-
terminalControlSelectorKey(terminalControl)) {
|
|
1321
|
+
!terminalControlsShareIncarnation(ownerControl, terminalControl) ||
|
|
1322
|
+
!terminalControlsShareIncarnation(binding.terminal_control, terminalControl)) {
|
|
1278
1323
|
throw new Error(`terminal idempotency key ${messageId} belongs to a safe-aborted Turn ` +
|
|
1279
1324
|
"whose Session binding is no longer current; no terminal input was sent");
|
|
1280
1325
|
}
|
|
@@ -1346,16 +1391,17 @@ function stableDelegateTerminalRoute({ options, request, workspace, requestedAge
|
|
|
1346
1391
|
receipt,
|
|
1347
1392
|
conversationId,
|
|
1348
1393
|
workspace: ownerWorkspace,
|
|
1349
|
-
|
|
1350
|
-
panePid: terminalControl.panePid
|
|
1394
|
+
terminalControl
|
|
1351
1395
|
};
|
|
1352
1396
|
});
|
|
1353
1397
|
const authoritative = routed.filter(({ receipt }) => !(receipt.status === "aborted" && receipt.safe_to_retry === true));
|
|
1354
1398
|
if (authoritative.length > 1) {
|
|
1355
1399
|
throw new Error(`terminal idempotency key ${messageId} has multiple durable delegate receipts`);
|
|
1356
1400
|
}
|
|
1357
|
-
const
|
|
1358
|
-
if (
|
|
1401
|
+
const firstRoute = routed[0];
|
|
1402
|
+
if (!firstRoute ||
|
|
1403
|
+
routed.some((entry) => entry.conversationId !== firstRoute.conversationId ||
|
|
1404
|
+
!terminalControlsShareIncarnation(entry.terminalControl, firstRoute.terminalControl))) {
|
|
1359
1405
|
throw new Error(`terminal idempotency key ${messageId} has conflicting terminal routes`);
|
|
1360
1406
|
}
|
|
1361
1407
|
const selected = authoritative[0] ?? routed.at(-1);
|
|
@@ -1816,7 +1862,10 @@ function withTerminalBridgeSubmission({ conversation, messageId, messageType, me
|
|
|
1816
1862
|
stringValue(nativeTakeover.terminal_agent_expected_session_id),
|
|
1817
1863
|
terminal_target: storedControl?.target,
|
|
1818
1864
|
terminal_socket_path: storedControl?.socketPath ?? null,
|
|
1819
|
-
terminal_pane_pid: storedControl?.panePid
|
|
1865
|
+
terminal_pane_pid: storedControl?.panePid,
|
|
1866
|
+
terminal_endpoint: storedControl && hasCanonicalTerminalEndpoint(storedControl)
|
|
1867
|
+
? terminalControlEvidence(storedControl)
|
|
1868
|
+
: previousReceipt?.terminal_endpoint
|
|
1820
1869
|
};
|
|
1821
1870
|
const immutableReceiptFields = Object.fromEntries(Object.entries(candidateImmutableReceiptFields).map(([key, value]) => [
|
|
1822
1871
|
key,
|
|
@@ -1828,7 +1877,7 @@ function withTerminalBridgeSubmission({ conversation, messageId, messageType, me
|
|
|
1828
1877
|
for (const [key, value] of Object.entries(candidateImmutableReceiptFields)) {
|
|
1829
1878
|
if (previousReceipt[key] !== undefined &&
|
|
1830
1879
|
value !== undefined &&
|
|
1831
|
-
|
|
1880
|
+
canonicalJson(previousReceipt[key]) !== canonicalJson(value)) {
|
|
1832
1881
|
throw new Error(`terminal submission receipt ${messageId} changed immutable ${key}`);
|
|
1833
1882
|
}
|
|
1834
1883
|
}
|
|
@@ -1909,27 +1958,18 @@ function replayExactActiveTerminalSubmission({ options, terminalControl, request
|
|
|
1909
1958
|
throw new Error(`terminal idempotency key ${messageId} has multiple terminal-ledger receipts`);
|
|
1910
1959
|
}
|
|
1911
1960
|
const ledgerReceipt = ledgerReceiptMatches[0];
|
|
1912
|
-
const loadedLedgerControl = isRecord(loadedLedger?.terminal_control)
|
|
1913
|
-
? loadedLedger.terminal_control
|
|
1914
|
-
: undefined;
|
|
1915
1961
|
const loadedLedgerMessageId = stringValue(loadedLedger?.message_id);
|
|
1916
1962
|
const loadedLedgerStoreDir = stringValue(loadedLedger?.store_dir);
|
|
1917
1963
|
const loadedLedgerStatePath = stringValue(loadedLedger?.state_path);
|
|
1918
1964
|
const loadedLedgerOwner = loadedLedger && loadedLedgerMessageId === messageId
|
|
1919
1965
|
? loadTerminalDispatchLedgerOwner(loadedLedger)
|
|
1920
1966
|
: undefined;
|
|
1921
|
-
const ledgerReceiptControl = isRecord(ledgerReceipt?.terminal_control)
|
|
1922
|
-
? ledgerReceipt.terminal_control
|
|
1923
|
-
: undefined;
|
|
1924
1967
|
const expectedMessageBodyHash = createHash("sha256")
|
|
1925
1968
|
.update(requestText)
|
|
1926
1969
|
.digest("hex");
|
|
1927
1970
|
if (ledgerReceipt &&
|
|
1928
1971
|
!(ledgerReceipt.status === "aborted" && ledgerReceipt.safe_to_retry === true) &&
|
|
1929
|
-
(
|
|
1930
|
-
(stringValue(ledgerReceiptControl?.socket_path) ?? undefined) !==
|
|
1931
|
-
terminalControl.socketPath ||
|
|
1932
|
-
Number(ledgerReceiptControl?.pane_pid) !== Number(terminalControl.panePid) ||
|
|
1972
|
+
(!terminalDispatchRecordMatchesControl(ledgerReceipt, terminalControl) ||
|
|
1933
1973
|
(stringValue(ledgerReceipt.store_dir) !== undefined &&
|
|
1934
1974
|
path.resolve(String(ledgerReceipt.store_dir)) !==
|
|
1935
1975
|
path.resolve(expectedStoreDir)) ||
|
|
@@ -1954,10 +1994,7 @@ function replayExactActiveTerminalSubmission({ options, terminalControl, request
|
|
|
1954
1994
|
}
|
|
1955
1995
|
if (loadedLedger &&
|
|
1956
1996
|
loadedLedgerMessageId === messageId &&
|
|
1957
|
-
(
|
|
1958
|
-
(stringValue(loadedLedgerControl?.socket_path) ?? undefined) !==
|
|
1959
|
-
terminalControl.socketPath ||
|
|
1960
|
-
Number(loadedLedgerControl?.pane_pid) !== Number(terminalControl.panePid) ||
|
|
1997
|
+
(!terminalDispatchRecordMatchesControl(loadedLedger, terminalControl) ||
|
|
1961
1998
|
(loadedLedgerStoreDir !== undefined &&
|
|
1962
1999
|
path.resolve(loadedLedgerStoreDir) !== path.resolve(expectedStoreDir)) ||
|
|
1963
2000
|
(expectedSessionId && stringValue(loadedLedger.session_id) !== undefined &&
|
|
@@ -2002,15 +2039,9 @@ function replayExactActiveTerminalSubmission({ options, terminalControl, request
|
|
|
2002
2039
|
}
|
|
2003
2040
|
const incarnationLedger = resolveTerminalDispatchLedgerPaneIncarnation(terminalControl, loadedLedger);
|
|
2004
2041
|
const ledger = reconcilePreparedTerminalDispatchLedger(terminalControl, incarnationLedger);
|
|
2005
|
-
const ledgerControl = isRecord(ledger?.terminal_control)
|
|
2006
|
-
? ledger.terminal_control
|
|
2007
|
-
: undefined;
|
|
2008
2042
|
if (!ledger ||
|
|
2009
2043
|
!["submitted", "enter_dispatched", "agent_accepted"].includes(String(ledger.status)) ||
|
|
2010
|
-
|
|
2011
|
-
(stringValue(ledgerControl?.socket_path) ?? undefined) !==
|
|
2012
|
-
terminalControl.socketPath ||
|
|
2013
|
-
Number(ledgerControl?.pane_pid) !== Number(terminalControl.panePid) ||
|
|
2044
|
+
!terminalDispatchRecordMatchesControl(ledger, terminalControl) ||
|
|
2014
2045
|
stringValue(ledger.message_id) !== messageId ||
|
|
2015
2046
|
(stringValue(ledger.message_type) !== undefined &&
|
|
2016
2047
|
stringValue(ledger.message_type) !== expectedMessageType) ||
|
|
@@ -2160,21 +2191,26 @@ function validateStoredTerminalSubmissionMatch({ owner, receipt, options, termin
|
|
|
2160
2191
|
turnIdForConversation(owner);
|
|
2161
2192
|
const receiptOpenClawSession = stringValue(receipt.openclaw_session) ??
|
|
2162
2193
|
owner.openclaw_session;
|
|
2163
|
-
const
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2194
|
+
const receiptTerminalEvidence = receipt.terminal_endpoint !== undefined
|
|
2195
|
+
? receipt.terminal_endpoint
|
|
2196
|
+
: {
|
|
2197
|
+
kind: "tmux",
|
|
2198
|
+
target: stringValue(receipt.terminal_target) ?? storedControl?.target,
|
|
2199
|
+
socket_path: receipt.terminal_socket_path === null
|
|
2200
|
+
? null
|
|
2201
|
+
: stringValue(receipt.terminal_socket_path) ??
|
|
2202
|
+
storedControl?.socketPath ??
|
|
2203
|
+
null,
|
|
2204
|
+
pane_pid: Number(receipt.terminal_pane_pid ?? storedControl?.panePid)
|
|
2205
|
+
};
|
|
2169
2206
|
const requestedOpenClawSession = stringValue(options.openclawSession);
|
|
2170
2207
|
if (!ownerStoreDir ||
|
|
2171
2208
|
!receiptStoreDir ||
|
|
2172
2209
|
path.resolve(ownerStoreDir) !== path.resolve(expectedStoreDir) ||
|
|
2173
2210
|
path.resolve(receiptStoreDir) !== path.resolve(expectedStoreDir) ||
|
|
2174
2211
|
!storedControl ||
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
receiptPanePid !== Number(terminalControl.panePid) ||
|
|
2212
|
+
!terminalControlsShareIncarnation(storedControl, terminalControl) ||
|
|
2213
|
+
!terminalControlEvidenceMatches(receiptTerminalEvidence, terminalControl) ||
|
|
2178
2214
|
receiptSessionId !== sessionIdForConversation(owner) ||
|
|
2179
2215
|
receiptTurnId !== turnIdForConversation(owner) ||
|
|
2180
2216
|
(expectedSessionId && receiptSessionId !== expectedSessionId) ||
|
|
@@ -2422,11 +2458,7 @@ function unresolvedTerminalBridgeSubmission(conversation) {
|
|
|
2422
2458
|
: undefined;
|
|
2423
2459
|
}
|
|
2424
2460
|
function assertNoUnresolvedTerminalBridgeSubmission(storeDir, terminalControl, currentConversationId, requestText) {
|
|
2425
|
-
const targetKey = terminalControlSelectorKey(terminalControl);
|
|
2426
2461
|
const requestHash = terminalBridgeRequestFingerprint(requestText);
|
|
2427
|
-
if (!targetKey) {
|
|
2428
|
-
return;
|
|
2429
|
-
}
|
|
2430
2462
|
for (const candidate of listConversations(storeDir)) {
|
|
2431
2463
|
const submission = terminalBridgeSubmission(candidate);
|
|
2432
2464
|
if (candidate.conversation_id === currentConversationId ||
|
|
@@ -2450,7 +2482,7 @@ function assertNoUnresolvedTerminalBridgeSubmission(storeDir, terminalControl, c
|
|
|
2450
2482
|
const nativeTakeover = isRecord(candidate.native_session_takeover)
|
|
2451
2483
|
? candidate.native_session_takeover
|
|
2452
2484
|
: undefined;
|
|
2453
|
-
if (
|
|
2485
|
+
if (terminalControlsShareIncarnation(terminalControlFromTakeover(nativeTakeover), terminalControl)) {
|
|
2454
2486
|
throw new Error(`terminal ${terminalControl.target} has a conflicting ${String(submission.status)} ` +
|
|
2455
2487
|
`AKK submission in ${candidate.conversation_id}; inspect that conversation and pane, ` +
|
|
2456
2488
|
"then close it before retrying");
|
|
@@ -2458,10 +2490,6 @@ function assertNoUnresolvedTerminalBridgeSubmission(storeDir, terminalControl, c
|
|
|
2458
2490
|
}
|
|
2459
2491
|
}
|
|
2460
2492
|
function stallOtherTerminalBridgeConversationsForUncertainDispatch({ storeDir, terminalControl, currentConversationId, uncertainMessageId }) {
|
|
2461
|
-
const targetKey = terminalControlSelectorKey(terminalControl);
|
|
2462
|
-
if (!targetKey) {
|
|
2463
|
-
return [];
|
|
2464
|
-
}
|
|
2465
2493
|
const stalledConversationIds = [];
|
|
2466
2494
|
for (const listed of listConversations(storeDir)) {
|
|
2467
2495
|
if (listed.conversation_id === currentConversationId ||
|
|
@@ -2472,7 +2500,7 @@ function stallOtherTerminalBridgeConversationsForUncertainDispatch({ storeDir, t
|
|
|
2472
2500
|
? listed.native_session_takeover
|
|
2473
2501
|
: undefined;
|
|
2474
2502
|
if (listedTakeover?.terminal_bridge !== true ||
|
|
2475
|
-
|
|
2503
|
+
!terminalControlsShareIncarnation(terminalControlFromTakeover(listedTakeover), terminalControl)) {
|
|
2476
2504
|
continue;
|
|
2477
2505
|
}
|
|
2478
2506
|
const listedStatePath = stringValue(listed.state_path);
|
|
@@ -2487,7 +2515,7 @@ function stallOtherTerminalBridgeConversationsForUncertainDispatch({ storeDir, t
|
|
|
2487
2515
|
: undefined;
|
|
2488
2516
|
if (!isActiveStatus(current.status) ||
|
|
2489
2517
|
currentTakeover?.terminal_bridge !== true ||
|
|
2490
|
-
|
|
2518
|
+
!terminalControlsShareIncarnation(terminalControlFromTakeover(currentTakeover), terminalControl)) {
|
|
2491
2519
|
continue;
|
|
2492
2520
|
}
|
|
2493
2521
|
const stalledAt = new Date().toISOString();
|
|
@@ -2686,13 +2714,7 @@ async function buildTerminalListGroup({ options, agentFilter, statusFilter }) {
|
|
|
2686
2714
|
};
|
|
2687
2715
|
}
|
|
2688
2716
|
async function terminalControlDiagnostics(provider) {
|
|
2689
|
-
|
|
2690
|
-
return provider.diagnose();
|
|
2691
|
-
}
|
|
2692
|
-
return {
|
|
2693
|
-
provider: "static",
|
|
2694
|
-
paneCount: (await provider.listPanes()).length
|
|
2695
|
-
};
|
|
2717
|
+
return provider.diagnostics();
|
|
2696
2718
|
}
|
|
2697
2719
|
function managedTurnListEntry(task, { terminalBridge = false, approvalState, conversation } = {}) {
|
|
2698
2720
|
const sessionId = stringValue(task.session_id) ??
|
|
@@ -2844,9 +2866,28 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
2844
2866
|
const codexLifecycleIncarnationAvailable = session.agent !== "codex" ||
|
|
2845
2867
|
Boolean(nativeProcessUuid && nativeProcessBirth);
|
|
2846
2868
|
const nativeInspectionHasBlockingTurn = listConversations(storeDirFromOptions(options)).some((turn) => isDiscoverableTmuxConversation(turn) &&
|
|
2847
|
-
|
|
2848
|
-
terminalControlSelectorKey(terminalControl) &&
|
|
2869
|
+
terminalControlsShareIncarnation(terminalControlForManagedConversation(turn), terminalControl) &&
|
|
2849
2870
|
SESSION_SEND_BLOCKING_STATUSES.has(turn.status));
|
|
2871
|
+
let nativeInspectionComposerReady = nativeInspectionComposerEmpty(session.agent, terminalState.screen_excerpt);
|
|
2872
|
+
if (session.agent === "codex" &&
|
|
2873
|
+
terminalState.activity_state === "idle" &&
|
|
2874
|
+
terminalState.approval_state.blocked !== true &&
|
|
2875
|
+
nativeInspectionCapability.status === "supported" &&
|
|
2876
|
+
nativeInspectionCapability.statusInspection === true &&
|
|
2877
|
+
terminalControl.capabilities.includes("send_keys") &&
|
|
2878
|
+
terminalControl.capabilities.includes("screen_status")) {
|
|
2879
|
+
try {
|
|
2880
|
+
const provider = createTerminalControlProvider(options);
|
|
2881
|
+
const resolvedTerminal = await provider.resolve(provider.endpoint(terminalControl));
|
|
2882
|
+
const styledScreen = await provider.capture(resolvedTerminal, { scrollbackLines: 40, preserveEscapes: true });
|
|
2883
|
+
nativeInspectionComposerReady = codexStyledComposerEmpty(styledScreen);
|
|
2884
|
+
}
|
|
2885
|
+
catch {
|
|
2886
|
+
// Advertising an input action is optional. The action itself repeats the
|
|
2887
|
+
// same styled composer proof under the terminal lock before any input.
|
|
2888
|
+
nativeInspectionComposerReady = false;
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2850
2891
|
const entry = {
|
|
2851
2892
|
id: bridge.terminalConversationId(session),
|
|
2852
2893
|
short_ref: sessionShortRef(bridge.terminalConversationId(session)),
|
|
@@ -2905,7 +2946,7 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
2905
2946
|
codexLifecycleIncarnationAvailable,
|
|
2906
2947
|
native_inspect: nativeInspectionCapability.status === "supported" &&
|
|
2907
2948
|
nativeInspectionCapability.statusInspection === true &&
|
|
2908
|
-
|
|
2949
|
+
nativeInspectionComposerReady &&
|
|
2909
2950
|
terminalControl.capabilities.includes("send_keys") &&
|
|
2910
2951
|
terminalControl.capabilities.includes("screen_status") &&
|
|
2911
2952
|
(session.agent === "codex"
|
|
@@ -2924,26 +2965,24 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
2924
2965
|
};
|
|
2925
2966
|
}
|
|
2926
2967
|
function terminalFirstListProjection({ storeDir, terminals, managedSessions, sessionAuthorityRequired, allConversations, displayedConversations, includeAll, managedOnly, statusFilter, mutationsAllowed }) {
|
|
2927
|
-
const
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2968
|
+
const discoveredTerminalControls = terminals.flatMap((terminal) => {
|
|
2969
|
+
const control = isRecord(terminal.terminal_control)
|
|
2970
|
+
? terminal.terminal_control
|
|
2971
|
+
: undefined;
|
|
2972
|
+
return control ? [control] : [];
|
|
2973
|
+
});
|
|
2931
2974
|
const projectedTerminals = terminals.map((terminal) => {
|
|
2932
2975
|
const terminalControl = isRecord(terminal.terminal_control)
|
|
2933
2976
|
? terminal.terminal_control
|
|
2934
2977
|
: undefined;
|
|
2935
|
-
const
|
|
2936
|
-
|
|
2937
|
-
discoveredTerminalKeys.add(terminalKey);
|
|
2938
|
-
}
|
|
2939
|
-
const allRelated = terminalKey
|
|
2940
|
-
? [...(allByTerminal.get(terminalKey) ?? [])]
|
|
2978
|
+
const allRelated = terminalControl
|
|
2979
|
+
? allConversations.filter((conversation) => terminalControlsShareIncarnation(terminalControlForManagedConversation(conversation), terminalControl))
|
|
2941
2980
|
: [];
|
|
2942
|
-
const displayedRelated =
|
|
2943
|
-
?
|
|
2981
|
+
const displayedRelated = terminalControl
|
|
2982
|
+
? displayedConversations.filter((conversation) => terminalControlsShareIncarnation(terminalControlForManagedConversation(conversation), terminalControl))
|
|
2944
2983
|
: [];
|
|
2945
|
-
const relatedSessions =
|
|
2946
|
-
?
|
|
2984
|
+
const relatedSessions = terminalControl
|
|
2985
|
+
? managedSessions.filter((session) => terminalControlsShareIncarnation(session.binding?.terminal_control, terminalControl))
|
|
2947
2986
|
: [];
|
|
2948
2987
|
const matchingSessions = relatedSessions.filter((session) => managedSessionMatchesLiveTerminalEntry(session, terminal, storeDir));
|
|
2949
2988
|
const conflictingBoundSessionClaims = relatedSessions.flatMap((session) => {
|
|
@@ -3210,8 +3249,8 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3210
3249
|
});
|
|
3211
3250
|
const unavailableManagedTurns = displayedConversations
|
|
3212
3251
|
.filter((conversation) => {
|
|
3213
|
-
const
|
|
3214
|
-
if (
|
|
3252
|
+
const managedControl = terminalControlForManagedConversation(conversation);
|
|
3253
|
+
if (discoveredTerminalControls.some((control) => terminalControlsShareIncarnation(managedControl, control))) {
|
|
3215
3254
|
return false;
|
|
3216
3255
|
}
|
|
3217
3256
|
return (includeAll ||
|
|
@@ -3248,35 +3287,6 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3248
3287
|
unavailableManagedTurns
|
|
3249
3288
|
};
|
|
3250
3289
|
}
|
|
3251
|
-
function managedConversationsByTerminal(conversations) {
|
|
3252
|
-
const groups = new Map();
|
|
3253
|
-
for (const conversation of conversations) {
|
|
3254
|
-
const key = terminalKeyForManagedConversation(conversation);
|
|
3255
|
-
if (!key) {
|
|
3256
|
-
continue;
|
|
3257
|
-
}
|
|
3258
|
-
const group = groups.get(key) ?? [];
|
|
3259
|
-
group.push(conversation);
|
|
3260
|
-
groups.set(key, group);
|
|
3261
|
-
}
|
|
3262
|
-
return groups;
|
|
3263
|
-
}
|
|
3264
|
-
function managedSessionsByTerminal(sessions) {
|
|
3265
|
-
const groups = new Map();
|
|
3266
|
-
for (const session of sessions) {
|
|
3267
|
-
if (!session.binding) {
|
|
3268
|
-
continue;
|
|
3269
|
-
}
|
|
3270
|
-
const key = terminalControlSelectorKey(session.binding.terminal_control);
|
|
3271
|
-
if (!key) {
|
|
3272
|
-
continue;
|
|
3273
|
-
}
|
|
3274
|
-
const group = groups.get(key) ?? [];
|
|
3275
|
-
group.push(session);
|
|
3276
|
-
groups.set(key, group);
|
|
3277
|
-
}
|
|
3278
|
-
return groups;
|
|
3279
|
-
}
|
|
3280
3290
|
function managedSessionMatchesLiveTerminalEntry(session, terminal, storeDir) {
|
|
3281
3291
|
const binding = session.binding;
|
|
3282
3292
|
const liveControl = isRecord(terminal.terminal_control)
|
|
@@ -3285,10 +3295,8 @@ function managedSessionMatchesLiveTerminalEntry(session, terminal, storeDir) {
|
|
|
3285
3295
|
if (session.status !== "bound" ||
|
|
3286
3296
|
!binding ||
|
|
3287
3297
|
session.agent !== terminal.agent ||
|
|
3288
|
-
binding.terminal_id !== stringValue(terminal.id) ||
|
|
3289
3298
|
binding.native_process.pid !== Number(terminal.pid) ||
|
|
3290
|
-
|
|
3291
|
-
terminalControlSelectorKey(liveControl) ||
|
|
3299
|
+
!terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminal.id, liveControl) ||
|
|
3292
3300
|
!matchesConfiguredWorkspace(session.workspace, terminal.workspace ?? terminal.cwd)) {
|
|
3293
3301
|
return false;
|
|
3294
3302
|
}
|
|
@@ -3359,8 +3367,7 @@ function managedSessionClaimsLiveTerminalEntry(session, terminal) {
|
|
|
3359
3367
|
return Boolean(binding &&
|
|
3360
3368
|
session.agent === terminal.agent &&
|
|
3361
3369
|
binding.native_process.pid === Number(terminal.pid) &&
|
|
3362
|
-
|
|
3363
|
-
terminalControlSelectorKey(liveControl));
|
|
3370
|
+
terminalControlsShareIncarnation(binding.terminal_control, liveControl));
|
|
3364
3371
|
}
|
|
3365
3372
|
function listedTerminalProcessIncarnation(terminal) {
|
|
3366
3373
|
const processUuid = stringValue(terminal.native_agent_process_uuid);
|
|
@@ -3403,7 +3410,9 @@ function managedBindingConflictKindForLiveTerminalEntry({ storeDir, session, ter
|
|
|
3403
3410
|
if (relationship === "different") {
|
|
3404
3411
|
return "stale_process_incarnation";
|
|
3405
3412
|
}
|
|
3406
|
-
if (binding.terminal_id
|
|
3413
|
+
if (!terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminal.id, isRecord(terminal.terminal_control)
|
|
3414
|
+
? terminal.terminal_control
|
|
3415
|
+
: undefined) ||
|
|
3407
3416
|
!matchesConfiguredWorkspace(session.workspace, terminal.workspace ?? terminal.cwd)) {
|
|
3408
3417
|
return "unverifiable";
|
|
3409
3418
|
}
|
|
@@ -3469,10 +3478,10 @@ function managedSessionHasUnresolvedNativeTransition(storeDir, session) {
|
|
|
3469
3478
|
}
|
|
3470
3479
|
return false;
|
|
3471
3480
|
}
|
|
3472
|
-
function
|
|
3473
|
-
return
|
|
3481
|
+
function terminalControlForManagedConversation(conversation) {
|
|
3482
|
+
return terminalControlFromTakeover(isRecord(conversation.native_session_takeover)
|
|
3474
3483
|
? conversation.native_session_takeover
|
|
3475
|
-
: undefined)
|
|
3484
|
+
: undefined);
|
|
3476
3485
|
}
|
|
3477
3486
|
function compareManagedConversationRecency(left, right) {
|
|
3478
3487
|
const leftTime = Date.parse(String(left.updated_at ?? left.created_at ?? ""));
|
|
@@ -3520,16 +3529,10 @@ function terminalDispatchOwnership(terminalControl) {
|
|
|
3520
3529
|
if (!ledger || ledger.status === "resolved") {
|
|
3521
3530
|
return { state: "none" };
|
|
3522
3531
|
}
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
const currentPanePid = Number(terminalControl.panePid);
|
|
3528
|
-
if (Number.isSafeInteger(ledgerPanePid) &&
|
|
3529
|
-
ledgerPanePid > 0 &&
|
|
3530
|
-
Number.isSafeInteger(currentPanePid) &&
|
|
3531
|
-
currentPanePid > 0 &&
|
|
3532
|
-
ledgerPanePid !== currentPanePid) {
|
|
3532
|
+
if (terminalDispatchRecordMatchesControl(ledger, terminalControl, {
|
|
3533
|
+
requireProcessAnchor: false
|
|
3534
|
+
}) &&
|
|
3535
|
+
!terminalDispatchRecordMatchesControl(ledger, terminalControl)) {
|
|
3533
3536
|
return { state: "none" };
|
|
3534
3537
|
}
|
|
3535
3538
|
if (![
|
|
@@ -3554,9 +3557,8 @@ function terminalDispatchOwnership(terminalControl) {
|
|
|
3554
3557
|
if (TERMINAL_DISPATCH_RELEASE_STATUSES.has(owner.status)) {
|
|
3555
3558
|
return { state: "none" };
|
|
3556
3559
|
}
|
|
3557
|
-
const
|
|
3558
|
-
|
|
3559
|
-
if (!ownerTerminalKey || ownerTerminalKey !== currentTerminalKey) {
|
|
3560
|
+
const ownerTerminalControl = terminalControlForManagedConversation(owner);
|
|
3561
|
+
if (!terminalControlsShareIncarnation(ownerTerminalControl, terminalControl)) {
|
|
3560
3562
|
return {
|
|
3561
3563
|
state: "conflict",
|
|
3562
3564
|
conflict: terminalDispatchConflict(ledger, "dispatch owner does not reference this tmux pane incarnation")
|
|
@@ -3729,9 +3731,8 @@ function managedTurnMatchesLiveTerminal(conversation, terminal) {
|
|
|
3729
3731
|
!Number.isSafeInteger(livePid) ||
|
|
3730
3732
|
livePid <= 1 ||
|
|
3731
3733
|
storedPid !== livePid ||
|
|
3732
|
-
stringValue(takeover?.native_session_id)
|
|
3733
|
-
|
|
3734
|
-
terminalControlSelectorKey(liveControl)) {
|
|
3734
|
+
!terminalControlAliasMatches(stringValue(takeover?.native_session_id), storedControl, stringValue(terminal.id), liveControl) ||
|
|
3735
|
+
!terminalControlsShareIncarnation(storedControl, liveControl)) {
|
|
3735
3736
|
return false;
|
|
3736
3737
|
}
|
|
3737
3738
|
const liveSessionId = stringValue(terminal.native_agent_session_id);
|
|
@@ -3876,18 +3877,25 @@ async function listStateForTerminal(agent, terminalControl, options, bridge = cr
|
|
|
3876
3877
|
function rootActiveProcesses(processes) {
|
|
3877
3878
|
const pids = new Set(processes.map((process) => `${process.agent}:${process.pid}`));
|
|
3878
3879
|
const roots = processes.filter((process) => !process.ppid || !pids.has(`${process.agent}:${process.ppid}`));
|
|
3879
|
-
const
|
|
3880
|
+
const seenTerminalIncarnations = new Set();
|
|
3880
3881
|
return roots.filter((process) => {
|
|
3881
|
-
const
|
|
3882
|
-
?
|
|
3882
|
+
const endpoint = process.terminalControl
|
|
3883
|
+
? terminalEndpointFromControlRef(process.terminalControl)
|
|
3884
|
+
: undefined;
|
|
3885
|
+
const terminalIncarnation = endpoint
|
|
3886
|
+
? JSON.stringify({
|
|
3887
|
+
agent: process.agent,
|
|
3888
|
+
identity: terminalEndpointIdentityKey(endpoint),
|
|
3889
|
+
process_anchor_pid: endpoint.processAnchorPid ?? null
|
|
3890
|
+
})
|
|
3883
3891
|
: undefined;
|
|
3884
|
-
if (!
|
|
3892
|
+
if (!terminalIncarnation) {
|
|
3885
3893
|
return true;
|
|
3886
3894
|
}
|
|
3887
|
-
if (
|
|
3895
|
+
if (seenTerminalIncarnations.has(terminalIncarnation)) {
|
|
3888
3896
|
return false;
|
|
3889
3897
|
}
|
|
3890
|
-
|
|
3898
|
+
seenTerminalIncarnations.add(terminalIncarnation);
|
|
3891
3899
|
return true;
|
|
3892
3900
|
});
|
|
3893
3901
|
}
|
|
@@ -4508,17 +4516,43 @@ function terminalControlSelectorKey(value) {
|
|
|
4508
4516
|
if (!isRecord(value)) {
|
|
4509
4517
|
return undefined;
|
|
4510
4518
|
}
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4519
|
+
let endpoint;
|
|
4520
|
+
try {
|
|
4521
|
+
endpoint = terminalEndpointFromControlRef(value);
|
|
4522
|
+
}
|
|
4523
|
+
catch {
|
|
4524
|
+
return undefined;
|
|
4525
|
+
}
|
|
4526
|
+
const processAnchorPid = Number(endpoint.processAnchorPid);
|
|
4527
|
+
if (!Number.isSafeInteger(processAnchorPid) || processAnchorPid <= 1) {
|
|
4514
4528
|
return undefined;
|
|
4515
4529
|
}
|
|
4516
4530
|
return JSON.stringify({
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
socket_path: stringValue(value.socketPath) ?? null
|
|
4531
|
+
identity: terminalEndpointIdentityKey(endpoint),
|
|
4532
|
+
process_anchor_pid: processAnchorPid
|
|
4520
4533
|
});
|
|
4521
4534
|
}
|
|
4535
|
+
function terminalControlsShareIncarnation(left, right) {
|
|
4536
|
+
if (!terminalControlSelectorKey(left) || !terminalControlSelectorKey(right)) {
|
|
4537
|
+
return false;
|
|
4538
|
+
}
|
|
4539
|
+
return sameTerminalControlIncarnation(left, right);
|
|
4540
|
+
}
|
|
4541
|
+
/**
|
|
4542
|
+
* `terminal:v2` is a route-shaped compatibility alias, not terminal
|
|
4543
|
+
* ownership. Once both sides carry canonical endpoint evidence, a route
|
|
4544
|
+
* rename must not detach or duplicate the stable resource. Legacy records
|
|
4545
|
+
* continue to require the exact historical alias and route tuple.
|
|
4546
|
+
*/
|
|
4547
|
+
function terminalControlAliasMatches(storedTerminalId, storedControl, currentTerminalId, currentControl) {
|
|
4548
|
+
if (!terminalControlsShareIncarnation(storedControl, currentControl)) {
|
|
4549
|
+
return false;
|
|
4550
|
+
}
|
|
4551
|
+
const stored = storedControl;
|
|
4552
|
+
const current = currentControl;
|
|
4553
|
+
return (hasCanonicalTerminalEndpoint(stored) &&
|
|
4554
|
+
hasCanonicalTerminalEndpoint(current)) || stringValue(storedTerminalId) === stringValue(currentTerminalId);
|
|
4555
|
+
}
|
|
4522
4556
|
function sessionEntryRecency(entry, observedAtMs) {
|
|
4523
4557
|
const timestamp = Date.parse(String(entry.updated_at ?? entry.created_at ?? ""));
|
|
4524
4558
|
if (Number.isFinite(timestamp)) {
|
|
@@ -4812,14 +4846,12 @@ function codexAllowedCompanionSetForManagedSession({ storeDir, session }) {
|
|
|
4812
4846
|
candidate.agent !== "codex" ||
|
|
4813
4847
|
candidate.status !== "detached" ||
|
|
4814
4848
|
!candidateBinding?.native_thread_id ||
|
|
4815
|
-
candidateBinding.terminal_id !== binding.terminal_id ||
|
|
4816
4849
|
candidateBinding.native_process.pid !== binding.native_process.pid ||
|
|
4817
4850
|
candidateBinding.native_process.process_uuid !==
|
|
4818
4851
|
binding.native_process.process_uuid ||
|
|
4819
4852
|
candidateBinding.native_process.process_birth !==
|
|
4820
4853
|
binding.native_process.process_birth ||
|
|
4821
|
-
|
|
4822
|
-
terminalControlSelectorKey(binding.terminal_control) ||
|
|
4854
|
+
!terminalControlAliasMatches(candidateBinding.terminal_id, candidateBinding.terminal_control, binding.terminal_id, binding.terminal_control) ||
|
|
4823
4855
|
path.resolve(candidate.workspace) !== path.resolve(session.workspace) ||
|
|
4824
4856
|
!candidateBinding.native_process.process_uuid ||
|
|
4825
4857
|
!candidateBinding.native_process.process_birth ||
|
|
@@ -4907,8 +4939,8 @@ function codexKnownRootSetForLifecycleTransition({ storeDir, terminal, transitio
|
|
|
4907
4939
|
if (after?.native_thread_id &&
|
|
4908
4940
|
after.native_process.process_uuid &&
|
|
4909
4941
|
after.native_process.process_birth &&
|
|
4910
|
-
after.terminal_id === terminal.conversationId &&
|
|
4911
4942
|
after.native_process.pid === terminal.pid &&
|
|
4943
|
+
terminalControlAliasMatches(after.terminal_id, after.terminal_control, terminal.conversationId, terminal.terminalControl) &&
|
|
4912
4944
|
after.native_process.process_uuid === transition.before_process_uuid &&
|
|
4913
4945
|
after.native_process.process_birth === transition.before_process_birth &&
|
|
4914
4946
|
isCompleteNativeRollout(after.native_process.rollout)) {
|
|
@@ -4926,12 +4958,10 @@ function codexKnownRootSetForLifecycleTransition({ storeDir, terminal, transitio
|
|
|
4926
4958
|
!binding?.native_thread_id ||
|
|
4927
4959
|
!binding.native_process.process_uuid ||
|
|
4928
4960
|
!binding.native_process.process_birth ||
|
|
4929
|
-
binding.terminal_id !== terminal.conversationId ||
|
|
4930
4961
|
binding.native_process.pid !== terminal.pid ||
|
|
4931
4962
|
binding.native_process.process_uuid !== transition.before_process_uuid ||
|
|
4932
4963
|
binding.native_process.process_birth !== transition.before_process_birth ||
|
|
4933
|
-
|
|
4934
|
-
terminalControlSelectorKey(terminal.terminalControl) ||
|
|
4964
|
+
!terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminal.conversationId, terminal.terminalControl) ||
|
|
4935
4965
|
!matchesConfiguredWorkspace(transition.workspace, session.workspace) ||
|
|
4936
4966
|
!isCompleteNativeRollout(binding.native_process.rollout)) {
|
|
4937
4967
|
continue;
|
|
@@ -4994,11 +5024,9 @@ function nativeIdentityMatchesCodexPreMaterialization(identity, expected) {
|
|
|
4994
5024
|
identity.rollout?.path === expected.rollout.path);
|
|
4995
5025
|
}
|
|
4996
5026
|
async function assertCodexComposerReadyForAutomatedInput({ options, terminalControl }) {
|
|
4997
|
-
const
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
preserveEscapes: true
|
|
5001
|
-
});
|
|
5027
|
+
const provider = createTerminalControlProvider(options);
|
|
5028
|
+
const resolvedTerminal = await provider.resolve(provider.endpoint(terminalControl));
|
|
5029
|
+
const styledScreen = await provider.capture(resolvedTerminal, { scrollbackLines: 40, preserveEscapes: true });
|
|
5002
5030
|
if (!codexStyledComposerEmpty(styledScreen)) {
|
|
5003
5031
|
throw new Error("Codex composer contains non-placeholder input; refusing automated terminal input");
|
|
5004
5032
|
}
|
|
@@ -5086,10 +5114,8 @@ function bindingMatchesLiveTerminal(session, terminal, identity, storeDir) {
|
|
|
5086
5114
|
return false;
|
|
5087
5115
|
}
|
|
5088
5116
|
if (session.agent !== terminal.agent ||
|
|
5089
|
-
binding.terminal_id !== terminal.conversationId ||
|
|
5090
5117
|
binding.native_process.pid !== terminal.pid ||
|
|
5091
|
-
|
|
5092
|
-
terminalControlSelectorKey(terminal.terminalControl) ||
|
|
5118
|
+
!terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminal.conversationId, terminal.terminalControl) ||
|
|
5093
5119
|
!matchesConfiguredWorkspace(session.workspace, terminal.terminalControl.currentPath)) {
|
|
5094
5120
|
return false;
|
|
5095
5121
|
}
|
|
@@ -5131,9 +5157,7 @@ function boundManagedSessionForTerminal({ storeDir, terminal, identity }) {
|
|
|
5131
5157
|
const unresolved = sessions.filter((session) => ["transitioning", "quarantined"].includes(session.status) &&
|
|
5132
5158
|
session.binding &&
|
|
5133
5159
|
session.agent === terminal.agent &&
|
|
5134
|
-
session.binding.terminal_id
|
|
5135
|
-
terminalControlSelectorKey(session.binding.terminal_control) ===
|
|
5136
|
-
terminalControlSelectorKey(terminal.terminalControl));
|
|
5160
|
+
terminalControlAliasMatches(session.binding.terminal_id, session.binding.terminal_control, terminal.conversationId, terminal.terminalControl));
|
|
5137
5161
|
if (unresolved.length > 0) {
|
|
5138
5162
|
throw new Error(`terminal ${terminal.terminalControl.target} is still claimed by ` +
|
|
5139
5163
|
`${unresolved[0].status} managed Session ${unresolved[0].session_id}` +
|
|
@@ -5149,8 +5173,7 @@ function boundManagedSessionForTerminal({ storeDir, terminal, identity }) {
|
|
|
5149
5173
|
const conflicting = sessions.filter((session) => session.status === "bound" &&
|
|
5150
5174
|
session.binding &&
|
|
5151
5175
|
session.agent === terminal.agent &&
|
|
5152
|
-
|
|
5153
|
-
terminalControlSelectorKey(terminal.terminalControl) &&
|
|
5176
|
+
terminalControlsShareIncarnation(session.binding.terminal_control, terminal.terminalControl) &&
|
|
5154
5177
|
session.binding.native_process.pid === terminal.pid &&
|
|
5155
5178
|
!managedSessionOwnerIsConclusivelyInactive({
|
|
5156
5179
|
session,
|
|
@@ -5169,10 +5192,8 @@ function managedBindingConflictKindForResolvedTerminal({ storeDir, session, term
|
|
|
5169
5192
|
if (session.status !== "bound" ||
|
|
5170
5193
|
!binding ||
|
|
5171
5194
|
session.agent !== terminal.agent ||
|
|
5172
|
-
binding.terminal_id !== terminal.conversationId ||
|
|
5173
5195
|
binding.native_process.pid !== terminal.pid ||
|
|
5174
|
-
|
|
5175
|
-
terminalControlSelectorKey(terminal.terminalControl) ||
|
|
5196
|
+
!terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminal.conversationId, terminal.terminalControl) ||
|
|
5176
5197
|
!matchesConfiguredWorkspace(session.workspace, terminal.terminalControl.currentPath) ||
|
|
5177
5198
|
bindingMatchesLiveTerminal(session, terminal, identity, storeDir)) {
|
|
5178
5199
|
return undefined;
|
|
@@ -5211,10 +5232,8 @@ function soleBoundManagedSessionClaimForTerminal(storeDir, terminal) {
|
|
|
5211
5232
|
const claims = listManagedSessions(storeDir).filter((session) => session.status === "bound" &&
|
|
5212
5233
|
session.binding &&
|
|
5213
5234
|
session.agent === terminal.agent &&
|
|
5214
|
-
session.binding.terminal_id === terminal.conversationId &&
|
|
5215
5235
|
session.binding.native_process.pid === terminal.pid &&
|
|
5216
|
-
|
|
5217
|
-
terminalControlSelectorKey(terminal.terminalControl) &&
|
|
5236
|
+
terminalControlAliasMatches(session.binding.terminal_id, session.binding.terminal_control, terminal.conversationId, terminal.terminalControl) &&
|
|
5218
5237
|
matchesConfiguredWorkspace(session.workspace, terminal.terminalControl.currentPath) &&
|
|
5219
5238
|
!managedSessionOwnerIsConclusivelyInactive({
|
|
5220
5239
|
session,
|
|
@@ -5368,6 +5387,32 @@ function lifecycleBindingToken({ session, terminal, identity }) {
|
|
|
5368
5387
|
rollout: identity?.rollout
|
|
5369
5388
|
});
|
|
5370
5389
|
}
|
|
5390
|
+
function managedSessionBindingTokens(session) {
|
|
5391
|
+
return [...new Set([
|
|
5392
|
+
managedSessionBindingToken(session),
|
|
5393
|
+
legacyManagedSessionBindingToken(session)
|
|
5394
|
+
])];
|
|
5395
|
+
}
|
|
5396
|
+
function lifecycleBindingTokens({ session, terminal, identity }) {
|
|
5397
|
+
const current = lifecycleBindingToken({ session, terminal, identity });
|
|
5398
|
+
const codexIncarnation = terminal.agent === "codex" && !identity
|
|
5399
|
+
? codexProcessIncarnationForPid(terminal.pid)
|
|
5400
|
+
: undefined;
|
|
5401
|
+
const legacy = session
|
|
5402
|
+
? legacyManagedSessionBindingToken(session)
|
|
5403
|
+
: legacyUnmanagedTerminalBindingToken({
|
|
5404
|
+
terminalId: terminal.conversationId,
|
|
5405
|
+
terminalControl: terminal.terminalControl,
|
|
5406
|
+
agent: terminal.agent,
|
|
5407
|
+
pid: terminal.pid,
|
|
5408
|
+
workspace: terminal.terminalControl.currentPath ?? process.cwd(),
|
|
5409
|
+
nativeThreadId: identity?.sessionId,
|
|
5410
|
+
processUuid: identity?.processUuid ?? codexIncarnation?.processUuid,
|
|
5411
|
+
processBirth: identity?.processBirth ?? codexIncarnation?.processBirth,
|
|
5412
|
+
rollout: identity?.rollout
|
|
5413
|
+
});
|
|
5414
|
+
return [...new Set([current, legacy])];
|
|
5415
|
+
}
|
|
5371
5416
|
function agentVersionForRunningProcess(agent, pid, options) {
|
|
5372
5417
|
const fixture = options.agentVersionsJson
|
|
5373
5418
|
? parseJsonOption(options.agentVersionsJson, "--agent-versions-json")
|
|
@@ -5408,8 +5453,7 @@ function assertTerminalLifecycleReady({ options, terminal, terminalStatus }) {
|
|
|
5408
5453
|
}
|
|
5409
5454
|
const blockers = listConversations(storeDirFromOptions(options))
|
|
5410
5455
|
.filter(isDiscoverableTmuxConversation)
|
|
5411
|
-
.filter((turn) =>
|
|
5412
|
-
terminalControlSelectorKey(terminal.terminalControl) &&
|
|
5456
|
+
.filter((turn) => terminalControlsShareIncarnation(terminalControlForManagedConversation(turn), terminal.terminalControl) &&
|
|
5413
5457
|
SESSION_SEND_BLOCKING_STATUSES.has(turn.status));
|
|
5414
5458
|
if (blockers.length > 0) {
|
|
5415
5459
|
throw new Error(`terminal ${terminal.terminalControl.target} still has unresolved Turn ` +
|
|
@@ -5611,10 +5655,8 @@ async function assertLifecycleTargetHasExclusiveOwnership({ options, terminal, t
|
|
|
5611
5655
|
if (!afterBinding ||
|
|
5612
5656
|
!nativeThreadId ||
|
|
5613
5657
|
!isExactNativeThreadId(nativeThreadId) ||
|
|
5614
|
-
afterBinding.terminal_id !== terminal.conversationId ||
|
|
5615
5658
|
afterBinding.native_process.pid !== terminal.pid ||
|
|
5616
|
-
|
|
5617
|
-
terminalControlSelectorKey(terminal.terminalControl)) {
|
|
5659
|
+
!terminalControlAliasMatches(afterBinding.terminal_id, afterBinding.terminal_control, terminal.conversationId, terminal.terminalControl)) {
|
|
5618
5660
|
throw new Error("cannot exclude the current lifecycle process without an exact after_binding");
|
|
5619
5661
|
}
|
|
5620
5662
|
await assertNativeThreadHasExclusiveOwnership({
|
|
@@ -5907,7 +5949,7 @@ async function currentLifecycleSnapshot(options, terminal, { materialize = false
|
|
|
5907
5949
|
allowedCompanionIdentity: claimedCodexCompanions.primary,
|
|
5908
5950
|
allowedAdditionalIdentities: claimedCodexCompanions.additional
|
|
5909
5951
|
});
|
|
5910
|
-
|
|
5952
|
+
let session = materialize
|
|
5911
5953
|
? materializeCurrentManagedSession({
|
|
5912
5954
|
options,
|
|
5913
5955
|
terminal,
|
|
@@ -5918,13 +5960,26 @@ async function currentLifecycleSnapshot(options, terminal, { materialize = false
|
|
|
5918
5960
|
terminal,
|
|
5919
5961
|
identity: observedIdentity
|
|
5920
5962
|
});
|
|
5921
|
-
|
|
5963
|
+
let identity = session
|
|
5922
5964
|
? logicalIdentityForManagedSession({
|
|
5923
5965
|
storeDir,
|
|
5924
5966
|
session,
|
|
5925
5967
|
observedIdentity
|
|
5926
5968
|
})
|
|
5927
5969
|
: observedIdentity;
|
|
5970
|
+
if (materialize && session) {
|
|
5971
|
+
session = refineManagedSessionNativeIdentity({
|
|
5972
|
+
storeDir,
|
|
5973
|
+
session,
|
|
5974
|
+
terminalControl: terminal.terminalControl,
|
|
5975
|
+
identity
|
|
5976
|
+
});
|
|
5977
|
+
identity = logicalIdentityForManagedSession({
|
|
5978
|
+
storeDir,
|
|
5979
|
+
session,
|
|
5980
|
+
observedIdentity
|
|
5981
|
+
});
|
|
5982
|
+
}
|
|
5928
5983
|
const codexCompanions = terminal.agent === "codex" && session
|
|
5929
5984
|
? codexAllowedCompanionSetForManagedSession({ storeDir, session })
|
|
5930
5985
|
: claimedCodexCompanions;
|
|
@@ -5949,6 +6004,7 @@ async function currentLifecycleSnapshot(options, terminal, { materialize = false
|
|
|
5949
6004
|
resumeExact: false,
|
|
5950
6005
|
reason: `${adapter.displayName} has no native-thread lifecycle adapter`
|
|
5951
6006
|
};
|
|
6007
|
+
const bindingTokens = lifecycleBindingTokens({ session, terminal, identity });
|
|
5952
6008
|
return {
|
|
5953
6009
|
identity,
|
|
5954
6010
|
runtimeIdentity: observedIdentity,
|
|
@@ -5957,7 +6013,8 @@ async function currentLifecycleSnapshot(options, terminal, { materialize = false
|
|
|
5957
6013
|
version,
|
|
5958
6014
|
adapter,
|
|
5959
6015
|
capabilities,
|
|
5960
|
-
bindingToken:
|
|
6016
|
+
bindingToken: bindingTokens[0],
|
|
6017
|
+
bindingTokens
|
|
5961
6018
|
};
|
|
5962
6019
|
}
|
|
5963
6020
|
async function runListResumableThreads(options) {
|
|
@@ -6062,11 +6119,9 @@ async function runListResumableThreads(options) {
|
|
|
6062
6119
|
function assertSameNativeInspectionTerminal(expected, actual, stage) {
|
|
6063
6120
|
const expectedPath = expected.terminalControl.currentPath;
|
|
6064
6121
|
const actualPath = actual.terminalControl.currentPath;
|
|
6065
|
-
if (actual.
|
|
6066
|
-
actual.agent !== expected.agent ||
|
|
6122
|
+
if (actual.agent !== expected.agent ||
|
|
6067
6123
|
actual.pid !== expected.pid ||
|
|
6068
|
-
|
|
6069
|
-
terminalControlSelectorKey(expected.terminalControl) ||
|
|
6124
|
+
!terminalControlAliasMatches(expected.conversationId, expected.terminalControl, actual.conversationId, actual.terminalControl) ||
|
|
6070
6125
|
!expectedPath ||
|
|
6071
6126
|
!actualPath ||
|
|
6072
6127
|
path.resolve(actualPath) !== path.resolve(expectedPath)) {
|
|
@@ -6083,8 +6138,7 @@ function assertTerminalNativeInspectionReady({ options, terminal, terminalStatus
|
|
|
6083
6138
|
}
|
|
6084
6139
|
const blocker = listConversations(storeDirFromOptions(options))
|
|
6085
6140
|
.filter(isDiscoverableTmuxConversation)
|
|
6086
|
-
.find((turn) =>
|
|
6087
|
-
terminalControlSelectorKey(terminal.terminalControl) &&
|
|
6141
|
+
.find((turn) => terminalControlsShareIncarnation(terminalControlForManagedConversation(turn), terminal.terminalControl) &&
|
|
6088
6142
|
SESSION_SEND_BLOCKING_STATUSES.has(turn.status));
|
|
6089
6143
|
if (blocker) {
|
|
6090
6144
|
throw new Error(`terminal ${terminal.terminalControl.target} still has unresolved Turn ` +
|
|
@@ -6148,7 +6202,7 @@ function nativeInspectionRuntime({ terminal, snapshot }) {
|
|
|
6148
6202
|
}
|
|
6149
6203
|
function assertNativeInspectionSnapshotUnchanged({ options, expectedTerminal, actualTerminal, expectedBindingToken, expectedVersion, actualSnapshot, stage, expectedClaudeState = "idle" }) {
|
|
6150
6204
|
assertSameNativeInspectionTerminal(expectedTerminal, actualTerminal, stage);
|
|
6151
|
-
if (actualSnapshot.
|
|
6205
|
+
if (!actualSnapshot.bindingTokens.includes(expectedBindingToken)) {
|
|
6152
6206
|
throw new Error(`terminal binding changed ${stage}; refresh AKK list`);
|
|
6153
6207
|
}
|
|
6154
6208
|
if (actualSnapshot.version !== expectedVersion) {
|
|
@@ -6203,7 +6257,7 @@ function assertNativeInspectionAgentIdentity({ options, terminal, snapshot, stag
|
|
|
6203
6257
|
}
|
|
6204
6258
|
async function assertNativeInspectionExclusiveOwnership({ options, terminal, snapshot }) {
|
|
6205
6259
|
// Claude's exact `claude agents` mapping makes global active ownership part
|
|
6206
|
-
// of the
|
|
6260
|
+
// of the supported exact Claude inspection profiles. Keep the existing Codex #112 path
|
|
6207
6261
|
// unchanged: an unmanaged Codex pane may be inspected before a rollout
|
|
6208
6262
|
// identity exists, and its status card is the bounded identity evidence.
|
|
6209
6263
|
if (terminal.agent !== "claude") {
|
|
@@ -6239,12 +6293,13 @@ async function runNativeInspect(options) {
|
|
|
6239
6293
|
throw new Error("native inspection requires a compatible AKK Store so binding authority can be verified");
|
|
6240
6294
|
}
|
|
6241
6295
|
const initiallyResolved = await resolveLifecycleTerminal(options);
|
|
6242
|
-
const
|
|
6296
|
+
const bridge = createTerminalAgentBridge(options);
|
|
6297
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDir, initiallyResolved.terminalControl, { timeoutMs: 30000 });
|
|
6243
6298
|
try {
|
|
6244
|
-
const terminal = await
|
|
6299
|
+
const terminal = await bridge.resolveStoredTerminal(initiallyResolved.agent, initiallyResolved.pid, initiallyResolved.terminalControl, { pid: initiallyResolved.pid });
|
|
6245
6300
|
assertSameNativeInspectionTerminal(initiallyResolved, terminal, "while waiting for native-inspection control");
|
|
6246
6301
|
const snapshot = await currentLifecycleSnapshot(options, terminal);
|
|
6247
|
-
if (snapshot.
|
|
6302
|
+
if (!snapshot.bindingTokens.includes(expectedBindingToken)) {
|
|
6248
6303
|
throw new Error("terminal binding changed after it was listed; refresh AKK list and retry");
|
|
6249
6304
|
}
|
|
6250
6305
|
const capability = snapshot.adapter.probeNativeInspection?.(snapshot.version);
|
|
@@ -6272,7 +6327,6 @@ async function runNativeInspect(options) {
|
|
|
6272
6327
|
snapshot
|
|
6273
6328
|
});
|
|
6274
6329
|
const runtime = nativeInspectionRuntime({ terminal, snapshot });
|
|
6275
|
-
const bridge = createTerminalAgentBridge(options);
|
|
6276
6330
|
const initialStatus = await bridge.status(terminal.agent, terminal.terminalControl, { runtime });
|
|
6277
6331
|
assertTerminalNativeInspectionReady({
|
|
6278
6332
|
options,
|
|
@@ -6289,7 +6343,7 @@ async function runNativeInspect(options) {
|
|
|
6289
6343
|
submission = await bridge.submitNativeInspection(terminal.agent, terminal.terminalControl, plan, {
|
|
6290
6344
|
runtime,
|
|
6291
6345
|
beforeEnter: async () => {
|
|
6292
|
-
const finalTerminal = await
|
|
6346
|
+
const finalTerminal = await bridge.resolveStoredTerminal(terminal.agent, terminal.pid, terminal.terminalControl, runtime);
|
|
6293
6347
|
const finalSnapshot = await currentLifecycleSnapshot(options, finalTerminal);
|
|
6294
6348
|
assertNativeInspectionSnapshotUnchanged({
|
|
6295
6349
|
options,
|
|
@@ -6388,7 +6442,7 @@ async function runNativeInspect(options) {
|
|
|
6388
6442
|
runtime: postEnterRuntime,
|
|
6389
6443
|
scrollbackLines: 240,
|
|
6390
6444
|
beforeDismiss: async () => {
|
|
6391
|
-
const dismissTerminal = await
|
|
6445
|
+
const dismissTerminal = await bridge.resolveStoredTerminal(terminal.agent, terminal.pid, terminal.terminalControl, postEnterRuntime);
|
|
6392
6446
|
const dismissSnapshot = await currentLifecycleSnapshot(options, dismissTerminal);
|
|
6393
6447
|
assertNativeInspectionSnapshotUnchanged({
|
|
6394
6448
|
options,
|
|
@@ -6434,7 +6488,7 @@ async function runNativeInspect(options) {
|
|
|
6434
6488
|
throw new Error("Claude Status panel dismissal was dispatched exactly once, but the original idle composer was not restored; do not retry automatically");
|
|
6435
6489
|
}
|
|
6436
6490
|
}
|
|
6437
|
-
const finalTerminal = await
|
|
6491
|
+
const finalTerminal = await bridge.resolveStoredTerminal(terminal.agent, terminal.pid, terminal.terminalControl, runtime);
|
|
6438
6492
|
const finalSnapshot = await currentLifecycleSnapshot(options, finalTerminal);
|
|
6439
6493
|
assertNativeInspectionSnapshotUnchanged({
|
|
6440
6494
|
options,
|
|
@@ -6873,14 +6927,13 @@ async function runReconcileBinding(options) {
|
|
|
6873
6927
|
}
|
|
6874
6928
|
const expectedBindingToken = required(stringValue(options.expectedBindingToken), "--expected-binding-token is required");
|
|
6875
6929
|
const expectedTerminalToken = required(stringValue(options.expectedTerminalToken), "--expected-terminal-token is required");
|
|
6876
|
-
const releaseTerminalLock =
|
|
6930
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDir, initiallyResolved.terminalControl, { timeoutMs: 30000 });
|
|
6877
6931
|
try {
|
|
6878
6932
|
return await withStoreWriterLeaseAsync(storeDir, async () => {
|
|
6879
6933
|
const terminal = await resolveLifecycleTerminal(options);
|
|
6880
6934
|
if (terminal.pid !== initiallyResolved.pid ||
|
|
6881
6935
|
terminal.conversationId !== initiallyResolved.conversationId ||
|
|
6882
|
-
|
|
6883
|
-
terminalControlSelectorKey(initiallyResolved.terminalControl)) {
|
|
6936
|
+
!terminalControlsShareIncarnation(terminal.terminalControl, initiallyResolved.terminalControl)) {
|
|
6884
6937
|
throw new Error("terminal identity changed while waiting to reconcile its binding; refresh AKK list");
|
|
6885
6938
|
}
|
|
6886
6939
|
await recoverLifecycleFenceBeforeMutation({ options, terminal });
|
|
@@ -6890,17 +6943,15 @@ async function runReconcileBinding(options) {
|
|
|
6890
6943
|
}
|
|
6891
6944
|
const session = loadManagedSession(storeDir, conflictingSessionId);
|
|
6892
6945
|
if (session.revision !== expectedSessionRevision ||
|
|
6893
|
-
|
|
6946
|
+
!managedSessionBindingTokens(session).includes(expectedBindingToken)) {
|
|
6894
6947
|
throw new Error("managed Session binding changed after it was listed; refresh AKK list");
|
|
6895
6948
|
}
|
|
6896
6949
|
const binding = session.binding;
|
|
6897
6950
|
if (session.status !== "bound" ||
|
|
6898
6951
|
!binding ||
|
|
6899
6952
|
session.agent !== terminal.agent ||
|
|
6900
|
-
binding.terminal_id !== terminal.conversationId ||
|
|
6901
6953
|
binding.native_process.pid !== terminal.pid ||
|
|
6902
|
-
|
|
6903
|
-
terminalControlSelectorKey(terminal.terminalControl) ||
|
|
6954
|
+
!terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminal.conversationId, terminal.terminalControl) ||
|
|
6904
6955
|
!matchesConfiguredWorkspace(session.workspace, terminal.terminalControl.currentPath)) {
|
|
6905
6956
|
throw new Error("the listed managed Session no longer claims this exact terminal");
|
|
6906
6957
|
}
|
|
@@ -6910,11 +6961,11 @@ async function runReconcileBinding(options) {
|
|
|
6910
6961
|
pid: terminal.pid,
|
|
6911
6962
|
cwd: terminal.terminalControl.currentPath
|
|
6912
6963
|
});
|
|
6913
|
-
const
|
|
6964
|
+
const terminalTokens = lifecycleBindingTokens({
|
|
6914
6965
|
terminal,
|
|
6915
6966
|
identity
|
|
6916
6967
|
});
|
|
6917
|
-
if (
|
|
6968
|
+
if (!terminalTokens.includes(expectedTerminalToken)) {
|
|
6918
6969
|
throw new Error("live terminal identity changed after the conflict was listed; refresh AKK list");
|
|
6919
6970
|
}
|
|
6920
6971
|
const bridge = createTerminalAgentBridge(options);
|
|
@@ -6929,8 +6980,7 @@ async function runReconcileBinding(options) {
|
|
|
6929
6980
|
const finalTerminal = await resolveLifecycleTerminal(options);
|
|
6930
6981
|
if (finalTerminal.pid !== terminal.pid ||
|
|
6931
6982
|
finalTerminal.conversationId !== terminal.conversationId ||
|
|
6932
|
-
|
|
6933
|
-
terminalControlSelectorKey(terminal.terminalControl)) {
|
|
6983
|
+
!terminalControlsShareIncarnation(finalTerminal.terminalControl, terminal.terminalControl)) {
|
|
6934
6984
|
throw new Error("terminal identity changed during binding reconciliation; refresh AKK list");
|
|
6935
6985
|
}
|
|
6936
6986
|
const finalStatus = await bridge.status(finalTerminal.agent, finalTerminal.terminalControl, {
|
|
@@ -6950,15 +7000,15 @@ async function runReconcileBinding(options) {
|
|
|
6950
7000
|
pid: finalTerminal.pid,
|
|
6951
7001
|
cwd: finalTerminal.terminalControl.currentPath
|
|
6952
7002
|
});
|
|
6953
|
-
if (
|
|
7003
|
+
if (!lifecycleBindingTokens({
|
|
6954
7004
|
terminal: finalTerminal,
|
|
6955
7005
|
identity: finalIdentity
|
|
6956
|
-
})
|
|
7006
|
+
}).includes(expectedTerminalToken)) {
|
|
6957
7007
|
throw new Error("live terminal identity changed during binding reconciliation; refresh AKK list");
|
|
6958
7008
|
}
|
|
6959
7009
|
const finalSession = loadManagedSession(storeDir, conflictingSessionId);
|
|
6960
7010
|
if (finalSession.revision !== expectedSessionRevision ||
|
|
6961
|
-
|
|
7011
|
+
!managedSessionBindingTokens(finalSession).includes(expectedBindingToken)) {
|
|
6962
7012
|
throw new Error("managed Session binding changed during reconciliation; refresh AKK list");
|
|
6963
7013
|
}
|
|
6964
7014
|
const conflictKind = managedBindingConflictKindForResolvedTerminal({
|
|
@@ -7016,12 +7066,14 @@ async function runReconcileBinding(options) {
|
|
|
7016
7066
|
}
|
|
7017
7067
|
function assertResumeSnapshotMatchesTerminal(snapshot, terminal) {
|
|
7018
7068
|
const workspace = path.resolve(terminal.terminalControl.currentPath ?? process.cwd());
|
|
7019
|
-
|
|
7069
|
+
const terminalMatches = snapshot.version === 2
|
|
7070
|
+
? terminalControlEvidenceMatches(snapshot.terminal_endpoint, terminal.terminalControl)
|
|
7071
|
+
: terminalControlEvidenceMatches(snapshot.terminal_control, terminal.terminalControl);
|
|
7072
|
+
if ((snapshot.version === 1 &&
|
|
7073
|
+
snapshot.terminal_id !== terminal.conversationId) ||
|
|
7020
7074
|
snapshot.agent !== terminal.agent ||
|
|
7021
7075
|
path.resolve(snapshot.workspace) !== workspace ||
|
|
7022
|
-
|
|
7023
|
-
snapshot.terminal_control.socket_path !== terminal.terminalControl.socketPath ||
|
|
7024
|
-
snapshot.terminal_control.pane_pid !== terminal.terminalControl.panePid) {
|
|
7076
|
+
!terminalMatches) {
|
|
7025
7077
|
throw new Error("resume selection terminal, process, or workspace changed; run /akk threads again");
|
|
7026
7078
|
}
|
|
7027
7079
|
}
|
|
@@ -7048,13 +7100,12 @@ function assertResumeSnapshotNotExpired(snapshot) {
|
|
|
7048
7100
|
async function runNativeThreadTransition(options, operation) {
|
|
7049
7101
|
const initiallyResolved = await resolveLifecycleTerminal(options);
|
|
7050
7102
|
const storeDir = storeDirFromOptions(options);
|
|
7051
|
-
const releaseTerminalLock =
|
|
7103
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDir, initiallyResolved.terminalControl, { timeoutMs: 30000 });
|
|
7052
7104
|
try {
|
|
7053
7105
|
return await withStoreWriterLeaseAsync(storeDir, async () => {
|
|
7054
7106
|
const terminal = await resolveLifecycleTerminal(options);
|
|
7055
7107
|
if (terminal.pid !== initiallyResolved.pid ||
|
|
7056
|
-
|
|
7057
|
-
terminalControlSelectorKey(initiallyResolved.terminalControl)) {
|
|
7108
|
+
!terminalControlsShareIncarnation(terminal.terminalControl, initiallyResolved.terminalControl)) {
|
|
7058
7109
|
throw new Error("terminal identity changed while waiting for lifecycle control; refresh list");
|
|
7059
7110
|
}
|
|
7060
7111
|
if (operation.kind === "resume_thread" && operation.selectionSnapshot) {
|
|
@@ -7071,7 +7122,7 @@ async function runNativeThreadTransition(options, operation) {
|
|
|
7071
7122
|
}
|
|
7072
7123
|
const snapshot = await currentLifecycleSnapshot(options, terminal, { materialize: true });
|
|
7073
7124
|
const expectedToken = required(stringValue(options.expectedBindingToken), "--expected-binding-token is required");
|
|
7074
|
-
if (
|
|
7125
|
+
if (!snapshot.bindingTokens.includes(expectedToken)) {
|
|
7075
7126
|
throw new Error("terminal binding changed after it was listed; refresh AKK list and retry");
|
|
7076
7127
|
}
|
|
7077
7128
|
if (snapshot.capabilities.status !== "supported") {
|
|
@@ -7417,16 +7468,27 @@ async function runNativeThreadTransition(options, operation) {
|
|
|
7417
7468
|
// Candidate discovery and ownership revalidation can take long
|
|
7418
7469
|
// enough for a human to start typing after the /status probe. Keep
|
|
7419
7470
|
// this check immediately adjacent to the terminal mutation.
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7471
|
+
try {
|
|
7472
|
+
await assertCodexComposerReadyForAutomatedInput({
|
|
7473
|
+
options,
|
|
7474
|
+
terminalControl: terminal.terminalControl
|
|
7475
|
+
});
|
|
7476
|
+
}
|
|
7477
|
+
catch (error) {
|
|
7478
|
+
throw new TerminalInputNotStartedError(error instanceof Error ? error.message : String(error), { cause: error });
|
|
7479
|
+
}
|
|
7424
7480
|
}
|
|
7425
7481
|
if (operation.kind === "resume_thread" && operation.selectionSnapshot) {
|
|
7426
7482
|
assertResumeSnapshotNotExpired(operation.selectionSnapshot);
|
|
7427
7483
|
}
|
|
7428
|
-
|
|
7429
|
-
|
|
7484
|
+
await bridge.send(terminal.agent, terminal.terminalControl, transitionStep.command, {
|
|
7485
|
+
runtime: beforeRuntime,
|
|
7486
|
+
onTransportStage(event) {
|
|
7487
|
+
if (event.stage === "text_injected") {
|
|
7488
|
+
inputStarted = true;
|
|
7489
|
+
}
|
|
7490
|
+
}
|
|
7491
|
+
});
|
|
7430
7492
|
const submittedAt = new Date().toISOString();
|
|
7431
7493
|
transition = {
|
|
7432
7494
|
...transition,
|
|
@@ -7615,7 +7677,8 @@ async function runNativeThreadTransition(options, operation) {
|
|
|
7615
7677
|
});
|
|
7616
7678
|
return;
|
|
7617
7679
|
}
|
|
7618
|
-
if (!inputStarted
|
|
7680
|
+
if (!inputStarted &&
|
|
7681
|
+
error instanceof TerminalInputNotStartedError) {
|
|
7619
7682
|
transition = {
|
|
7620
7683
|
...durableTransition,
|
|
7621
7684
|
status: "aborted",
|
|
@@ -7864,10 +7927,16 @@ function terminalBridgeApprovalFingerprint({ terminalControl, terminalStatus })
|
|
|
7864
7927
|
if (adapterFingerprint) {
|
|
7865
7928
|
return adapterFingerprint;
|
|
7866
7929
|
}
|
|
7930
|
+
const endpoint = terminalEndpointFromControlRef(terminalControl);
|
|
7931
|
+
const processAnchorPid = Number(endpoint.processAnchorPid);
|
|
7932
|
+
if (!Number.isSafeInteger(processAnchorPid) || processAnchorPid <= 1) {
|
|
7933
|
+
throw new Error("terminal approval fingerprint requires a stable process anchor");
|
|
7934
|
+
}
|
|
7867
7935
|
const screen = isRecord(terminalStatus?.screen) ? terminalStatus.screen : {};
|
|
7868
7936
|
return createHash("sha256")
|
|
7869
7937
|
.update(JSON.stringify({
|
|
7870
|
-
|
|
7938
|
+
terminal_identity: terminalEndpointIdentityKey(endpoint),
|
|
7939
|
+
process_anchor_pid: processAnchorPid,
|
|
7871
7940
|
keys: approval.keys ?? (approval.key ? [approval.key] : undefined),
|
|
7872
7941
|
label: approval.label,
|
|
7873
7942
|
prompt_kind: approval.prompt_kind,
|
|
@@ -7990,10 +8059,7 @@ function recordTerminalBridgeApprovalNotification({ statePath, logPath, terminal
|
|
|
7990
8059
|
stringValue(currentNativeTakeover.terminal_bridge_message_id) !==
|
|
7991
8060
|
expectedConversation.messageId ||
|
|
7992
8061
|
!currentTerminalControl ||
|
|
7993
|
-
currentTerminalControl
|
|
7994
|
-
currentTerminalControl.target !== terminalControl.target ||
|
|
7995
|
-
currentTerminalControl.socketPath !== terminalControl.socketPath ||
|
|
7996
|
-
currentTerminalControl.panePid !== terminalControl.panePid) {
|
|
8062
|
+
!terminalControlsShareIncarnation(currentTerminalControl, terminalControl)) {
|
|
7997
8063
|
return {
|
|
7998
8064
|
conversation,
|
|
7999
8065
|
duplicate: false,
|
|
@@ -8301,7 +8367,7 @@ async function runSend(options) {
|
|
|
8301
8367
|
throw new Error("raw tmux terminal sends require --background so AKK can persist and monitor the submission safely");
|
|
8302
8368
|
}
|
|
8303
8369
|
const rawStoreDir = storeDirFromOptions(options);
|
|
8304
|
-
const releaseTerminalLock =
|
|
8370
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(rawStoreDir, terminalConversation.terminalControl, { timeoutMs: 30000 });
|
|
8305
8371
|
try {
|
|
8306
8372
|
await withStoreWriterLeaseAsync(rawStoreDir, async () => {
|
|
8307
8373
|
await recoverLifecycleFenceBeforeMutation({
|
|
@@ -8519,12 +8585,23 @@ async function runSend(options) {
|
|
|
8519
8585
|
if (!rawTerminalId) {
|
|
8520
8586
|
throw new Error(`managed Session ${sessionId} has no authoritative terminal binding`);
|
|
8521
8587
|
}
|
|
8522
|
-
const
|
|
8523
|
-
|
|
8588
|
+
const storedTerminalControl = initialSession?.binding?.terminal_control ??
|
|
8589
|
+
terminalControlFromTakeover(legacyTakeover);
|
|
8590
|
+
const storedAgent = initialSession?.agent ??
|
|
8591
|
+
(legacyBindingTurn
|
|
8592
|
+
? executorForConversation(legacyBindingTurn).kind
|
|
8593
|
+
: undefined);
|
|
8594
|
+
const storedPid = initialSession?.binding?.native_process.pid ??
|
|
8595
|
+
Number(legacyTakeover?.terminal_agent_pid);
|
|
8596
|
+
const terminalBridge = createTerminalAgentBridge(options);
|
|
8597
|
+
const resolvedTerminal = storedTerminalControl && storedAgent &&
|
|
8598
|
+
Number.isSafeInteger(storedPid) && storedPid > 1
|
|
8599
|
+
? await terminalBridge.resolveStoredTerminal(storedAgent, storedPid, storedTerminalControl, { pid: storedPid })
|
|
8600
|
+
: await terminalBridge.resolveConversationId(rawTerminalId);
|
|
8524
8601
|
if (!resolvedTerminal) {
|
|
8525
8602
|
throw new Error(`session ${sessionId} is not attached to a live tmux terminal`);
|
|
8526
8603
|
}
|
|
8527
|
-
const releaseTerminalLock =
|
|
8604
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDir, resolvedTerminal.terminalControl, { timeoutMs: 30000 });
|
|
8528
8605
|
try {
|
|
8529
8606
|
await withStoreWriterLeaseAsync(storeDir, async () => {
|
|
8530
8607
|
await recoverLifecycleFenceBeforeMutation({
|
|
@@ -8692,18 +8769,19 @@ async function runTurnResponse({ options, messageBody }) {
|
|
|
8692
8769
|
if (!storedTerminalControl || !nativeTerminalId) {
|
|
8693
8770
|
throw new Error(`turn ${turnIdForConversation(initialConversation)} is not attached to a live tmux terminal`);
|
|
8694
8771
|
}
|
|
8695
|
-
const
|
|
8696
|
-
|
|
8772
|
+
const storedPid = Number(nativeTakeover?.terminal_agent_pid);
|
|
8773
|
+
const storedAgent = executorForConversation(initialConversation).kind;
|
|
8774
|
+
const liveTerminal = Number.isSafeInteger(storedPid) && storedPid > 1
|
|
8775
|
+
? await createTerminalAgentBridge(options).resolveStoredTerminal(storedAgent, storedPid, storedTerminalControl, terminalRuntimeIdentityForConversation(initialConversation, storedTerminalControl))
|
|
8776
|
+
: undefined;
|
|
8697
8777
|
if (!liveTerminal ||
|
|
8698
8778
|
liveTerminal.agent !== executorForConversation(initialConversation).kind ||
|
|
8699
|
-
liveTerminal.terminalControl
|
|
8700
|
-
liveTerminal.terminalControl.socketPath !== storedTerminalControl.socketPath ||
|
|
8701
|
-
liveTerminal.terminalControl.panePid !== storedTerminalControl.panePid) {
|
|
8779
|
+
!terminalControlsShareIncarnation(liveTerminal.terminalControl, storedTerminalControl)) {
|
|
8702
8780
|
throw new Error(`turn ${turnIdForConversation(initialConversation)} is not attached to its expected live tmux terminal`);
|
|
8703
8781
|
}
|
|
8704
8782
|
const terminalControl = liveTerminal.terminalControl;
|
|
8705
8783
|
const responseStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
8706
|
-
const releaseTerminalLock =
|
|
8784
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(responseStoreDir, terminalControl, { timeoutMs: 30000 });
|
|
8707
8785
|
try {
|
|
8708
8786
|
await withStoreWriterLeaseAsync(responseStoreDir, async () => {
|
|
8709
8787
|
// Legacy identity migration may write the Turn, so keep it inside the
|
|
@@ -8711,17 +8789,15 @@ async function runTurnResponse({ options, messageBody }) {
|
|
|
8711
8789
|
await migrateLegacyTerminalAgentIdentity({ ...loaded, options });
|
|
8712
8790
|
const releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
8713
8791
|
try {
|
|
8714
|
-
|
|
8715
|
-
|
|
8792
|
+
let lockedConversation = loadState(statePath);
|
|
8793
|
+
let lockedTakeover = isRecord(lockedConversation.native_session_takeover)
|
|
8716
8794
|
? lockedConversation.native_session_takeover
|
|
8717
8795
|
: undefined;
|
|
8718
8796
|
const lockedControl = terminalControlFromTakeover(lockedTakeover);
|
|
8719
8797
|
if (stringValue(lockedTakeover?.native_session_id) !== nativeTerminalId ||
|
|
8720
8798
|
executorForConversation(lockedConversation).kind !== liveTerminal.agent ||
|
|
8721
8799
|
!lockedControl ||
|
|
8722
|
-
lockedControl
|
|
8723
|
-
lockedControl.socketPath !== terminalControl.socketPath ||
|
|
8724
|
-
lockedControl.panePid !== terminalControl.panePid) {
|
|
8800
|
+
!terminalControlsShareIncarnation(lockedControl, terminalControl)) {
|
|
8725
8801
|
throw new Error("terminal control changed while waiting to respond; refresh status and retry");
|
|
8726
8802
|
}
|
|
8727
8803
|
if (requestedOpenClawSession &&
|
|
@@ -8729,6 +8805,14 @@ async function runTurnResponse({ options, messageBody }) {
|
|
|
8729
8805
|
throw new Error(`turn ${turnIdForConversation(lockedConversation)} belongs to a ` +
|
|
8730
8806
|
"different OpenClaw session; no terminal input was sent");
|
|
8731
8807
|
}
|
|
8808
|
+
lockedConversation = refineTerminalTurnEndpoint({
|
|
8809
|
+
conversation: lockedConversation,
|
|
8810
|
+
statePath,
|
|
8811
|
+
terminalControl
|
|
8812
|
+
});
|
|
8813
|
+
lockedTakeover = isRecord(lockedConversation.native_session_takeover)
|
|
8814
|
+
? lockedConversation.native_session_takeover
|
|
8815
|
+
: undefined;
|
|
8732
8816
|
if (replayExactActiveTerminalSubmission({
|
|
8733
8817
|
options,
|
|
8734
8818
|
terminalControl,
|
|
@@ -8757,7 +8841,7 @@ async function runTurnResponse({ options, messageBody }) {
|
|
|
8757
8841
|
? prepared.conversation.native_session_takeover
|
|
8758
8842
|
: undefined;
|
|
8759
8843
|
const terminalAgentPid = Number(preparedTakeover?.terminal_agent_pid);
|
|
8760
|
-
|
|
8844
|
+
let responseManagedSession = tryLoadManagedSession(responseStoreDir, sessionIdForConversation(prepared.conversation));
|
|
8761
8845
|
const responseCodexCompanions = prepared.executor.kind === "codex" && responseManagedSession
|
|
8762
8846
|
? codexAllowedCompanionSetForManagedSession({
|
|
8763
8847
|
storeDir: responseStoreDir,
|
|
@@ -8780,11 +8864,22 @@ async function runTurnResponse({ options, messageBody }) {
|
|
|
8780
8864
|
currentIdentity: currentNativeIdentity,
|
|
8781
8865
|
operation: "respond to"
|
|
8782
8866
|
});
|
|
8867
|
+
if (responseManagedSession) {
|
|
8868
|
+
const logicalResponseIdentity = logicalIdentityForManagedSession({
|
|
8869
|
+
storeDir: responseStoreDir,
|
|
8870
|
+
session: responseManagedSession,
|
|
8871
|
+
observedIdentity: currentNativeIdentity
|
|
8872
|
+
});
|
|
8873
|
+
responseManagedSession = refineManagedSessionNativeIdentity({
|
|
8874
|
+
storeDir: responseStoreDir,
|
|
8875
|
+
session: responseManagedSession,
|
|
8876
|
+
terminalControl,
|
|
8877
|
+
identity: logicalResponseIdentity
|
|
8878
|
+
});
|
|
8879
|
+
}
|
|
8783
8880
|
const currentTerminalControl = terminalControlFromTakeover(prepared.nativeTakeoverForSend);
|
|
8784
8881
|
if (!currentTerminalControl ||
|
|
8785
|
-
currentTerminalControl
|
|
8786
|
-
currentTerminalControl.socketPath !== terminalControl.socketPath ||
|
|
8787
|
-
currentTerminalControl.panePid !== terminalControl.panePid) {
|
|
8882
|
+
!terminalControlsShareIncarnation(currentTerminalControl, terminalControl)) {
|
|
8788
8883
|
throw new Error("terminal control changed while waiting to respond; refresh status and retry");
|
|
8789
8884
|
}
|
|
8790
8885
|
const responseOptions = {
|
|
@@ -8988,7 +9083,7 @@ async function runApprove(options) {
|
|
|
8988
9083
|
: {})
|
|
8989
9084
|
};
|
|
8990
9085
|
};
|
|
8991
|
-
const releaseTerminalLock =
|
|
9086
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 });
|
|
8992
9087
|
let terminalLockReleased = false;
|
|
8993
9088
|
const releaseApprovalTerminalLock = () => {
|
|
8994
9089
|
if (!terminalLockReleased) {
|
|
@@ -9021,8 +9116,8 @@ async function runApprove(options) {
|
|
|
9021
9116
|
: undefined;
|
|
9022
9117
|
if (currentConversation.status !== conversation.status ||
|
|
9023
9118
|
currentTakeover?.terminal_bridge_message_id !== nativeTakeover?.terminal_bridge_message_id ||
|
|
9024
|
-
currentControl
|
|
9025
|
-
currentControl
|
|
9119
|
+
!currentControl ||
|
|
9120
|
+
!terminalControlsShareIncarnation(currentControl, terminalControl) ||
|
|
9026
9121
|
(claudeScreenApproval &&
|
|
9027
9122
|
currentApproval?.fingerprint !== monitoredApproval?.fingerprint)) {
|
|
9028
9123
|
throw new Error("approval state changed while waiting for terminal control; refresh status and retry");
|
|
@@ -9147,8 +9242,7 @@ async function runApprove(options) {
|
|
|
9147
9242
|
latestNotifiedAt === undefined ||
|
|
9148
9243
|
Date.now() - latestNotifiedAt > CLAUDE_SCREEN_APPROVAL_TTL_MS ||
|
|
9149
9244
|
expectedFingerprint !== fingerprint ||
|
|
9150
|
-
latestControl
|
|
9151
|
-
latestControl?.socketPath !== dispatchControl.socketPath ||
|
|
9245
|
+
!terminalControlsShareIncarnation(latestControl, dispatchControl) ||
|
|
9152
9246
|
(autoApproved &&
|
|
9153
9247
|
(latestPolicyEvidence?.source !== "claude_transcript" ||
|
|
9154
9248
|
latestPolicyEvidence.evidence_fingerprint !==
|
|
@@ -9342,7 +9436,7 @@ async function runApprove(options) {
|
|
|
9342
9436
|
}
|
|
9343
9437
|
}
|
|
9344
9438
|
async function runTerminalConversationApprove({ options, conversationId, agent, terminalControl, pid }) {
|
|
9345
|
-
const releaseTerminalLock =
|
|
9439
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 });
|
|
9346
9440
|
try {
|
|
9347
9441
|
if (agent === "claude") {
|
|
9348
9442
|
printJson({
|
|
@@ -9407,7 +9501,7 @@ async function runTerminalConversationApprove({ options, conversationId, agent,
|
|
|
9407
9501
|
async function runTerminalControlSend({ options, conversation, nextConversation, statePath, logPath, executor, message, terminalControl, terminalSendLockHeld = false, terminalStateLockHeld = false, storeWriterLeaseHeld = false, recordMessageAfterSend = false, recordRawAttachmentAfterSend = false, onTerminalPreflightVerified = undefined, allowedPreMaterializationIdentity = undefined, allowedAdditionalIdentities = [], continuingTurnResponse = false }) {
|
|
9408
9502
|
const bridge = terminalBridgeEnabled(conversation);
|
|
9409
9503
|
if (!terminalSendLockHeld) {
|
|
9410
|
-
const releaseTerminalLock =
|
|
9504
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 });
|
|
9411
9505
|
try {
|
|
9412
9506
|
return await runTerminalControlSend({
|
|
9413
9507
|
options,
|
|
@@ -9445,8 +9539,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
9445
9539
|
currentConversation.updated_at !== nextConversation.updated_at ||
|
|
9446
9540
|
currentConversation.status !== nextConversation.status ||
|
|
9447
9541
|
currentConversation.response_rounds_used !== nextConversation.response_rounds_used ||
|
|
9448
|
-
currentControl
|
|
9449
|
-
currentControl?.socketPath !== terminalControl.socketPath) {
|
|
9542
|
+
!terminalControlsShareIncarnation(currentControl, terminalControl)) {
|
|
9450
9543
|
throw new Error("conversation changed while waiting to send to the terminal; refresh status and retry");
|
|
9451
9544
|
}
|
|
9452
9545
|
return await runTerminalControlSend({
|
|
@@ -11469,9 +11562,8 @@ function migratedTerminalTurnMatchesSessionBinding({ conversation, takeover, ter
|
|
|
11469
11562
|
session.agent !== agent ||
|
|
11470
11563
|
path.resolve(session.workspace) !== path.resolve(conversation.workspace) ||
|
|
11471
11564
|
!terminalId ||
|
|
11472
|
-
binding.terminal_id
|
|
11473
|
-
|
|
11474
|
-
terminalControlSelectorKey(terminalControl) ||
|
|
11565
|
+
!terminalControlAliasMatches(terminalId, terminalControl, binding.terminal_id, binding.terminal_control) ||
|
|
11566
|
+
!terminalControlsShareIncarnation(binding.terminal_control, terminalControl) ||
|
|
11475
11567
|
!Number.isSafeInteger(pid) ||
|
|
11476
11568
|
binding.native_process.pid !== pid ||
|
|
11477
11569
|
!turnNativeThreadId ||
|
|
@@ -11556,24 +11648,28 @@ function nativeThreadTransitionRevision(transition) {
|
|
|
11556
11648
|
}
|
|
11557
11649
|
function refineManagedSessionNativeIdentity({ storeDir, session, terminalControl, identity }) {
|
|
11558
11650
|
const binding = session.binding;
|
|
11559
|
-
if (
|
|
11651
|
+
if (session.status !== "bound" || !binding) {
|
|
11560
11652
|
return session;
|
|
11561
11653
|
}
|
|
11562
|
-
if (
|
|
11563
|
-
|
|
11564
|
-
(binding.native_thread_id &&
|
|
11654
|
+
if (!terminalControlsShareIncarnation(binding.terminal_control, terminalControl) ||
|
|
11655
|
+
(identity && binding.native_thread_id &&
|
|
11565
11656
|
binding.native_thread_id !== identity.sessionId)) {
|
|
11566
11657
|
throw new Error(`managed Session ${session.session_id} changed native identity before send`);
|
|
11567
11658
|
}
|
|
11568
|
-
const refinedNativeThreadId = binding.native_thread_id ?? identity
|
|
11569
|
-
const refinedProcessUuid = binding.native_process.process_uuid ?? identity
|
|
11570
|
-
const refinedProcessBirth = binding.native_process.process_birth ?? identity
|
|
11571
|
-
const refinedRollout = binding.native_process.rollout ?? identity
|
|
11659
|
+
const refinedNativeThreadId = binding.native_thread_id ?? identity?.sessionId;
|
|
11660
|
+
const refinedProcessUuid = binding.native_process.process_uuid ?? identity?.processUuid;
|
|
11661
|
+
const refinedProcessBirth = binding.native_process.process_birth ?? identity?.processBirth;
|
|
11662
|
+
const refinedRollout = binding.native_process.rollout ?? identity?.rollout;
|
|
11663
|
+
const refinedTerminalEndpoint = binding.terminal_endpoint ??
|
|
11664
|
+
(hasCanonicalTerminalEndpoint(terminalControl)
|
|
11665
|
+
? terminalControlEvidence(terminalControl)
|
|
11666
|
+
: undefined);
|
|
11572
11667
|
const changed = refinedNativeThreadId !== binding.native_thread_id ||
|
|
11573
11668
|
refinedProcessUuid !== binding.native_process.process_uuid ||
|
|
11574
11669
|
refinedProcessBirth !== binding.native_process.process_birth ||
|
|
11575
11670
|
JSON.stringify(refinedRollout) !==
|
|
11576
|
-
JSON.stringify(binding.native_process.rollout)
|
|
11671
|
+
JSON.stringify(binding.native_process.rollout) ||
|
|
11672
|
+
refinedTerminalEndpoint !== binding.terminal_endpoint;
|
|
11577
11673
|
if (!changed) {
|
|
11578
11674
|
return session;
|
|
11579
11675
|
}
|
|
@@ -11582,13 +11678,16 @@ function refineManagedSessionNativeIdentity({ storeDir, session, terminalControl
|
|
|
11582
11678
|
...session,
|
|
11583
11679
|
binding: {
|
|
11584
11680
|
...binding,
|
|
11681
|
+
...(refinedTerminalEndpoint
|
|
11682
|
+
? { terminal_endpoint: refinedTerminalEndpoint }
|
|
11683
|
+
: {}),
|
|
11585
11684
|
native_thread_id: refinedNativeThreadId,
|
|
11586
11685
|
native_process: {
|
|
11587
11686
|
...binding.native_process,
|
|
11588
11687
|
process_uuid: refinedProcessUuid,
|
|
11589
11688
|
process_birth: refinedProcessBirth,
|
|
11590
11689
|
rollout: refinedRollout,
|
|
11591
|
-
evidence: identity.evidence
|
|
11690
|
+
evidence: identity?.evidence ?? binding.native_process.evidence
|
|
11592
11691
|
},
|
|
11593
11692
|
last_verified_at: now
|
|
11594
11693
|
},
|
|
@@ -11611,8 +11710,7 @@ function persistManagedSessionNativeIdentity({ conversation, terminalControl, id
|
|
|
11611
11710
|
!current.binding ||
|
|
11612
11711
|
current.binding.binding_id !== bindingId ||
|
|
11613
11712
|
current.binding.generation !== bindingGeneration ||
|
|
11614
|
-
|
|
11615
|
-
terminalControlSelectorKey(terminalControl)) {
|
|
11713
|
+
!terminalControlsShareIncarnation(current.binding.terminal_control, terminalControl)) {
|
|
11616
11714
|
throw new Error(`managed Session ${sessionId} binding changed before native identity commit`);
|
|
11617
11715
|
}
|
|
11618
11716
|
const expectedNativeThreadId = current.binding.native_thread_id;
|
|
@@ -11760,12 +11858,10 @@ function managedTurnMatchesResolvedTerminal(conversation, terminalConversation,
|
|
|
11760
11858
|
currentTerminalIdentity &&
|
|
11761
11859
|
executorForConversation(conversation).kind === terminalConversation.agent &&
|
|
11762
11860
|
storedTerminalIdentity.agent === currentTerminalIdentity.agent &&
|
|
11763
|
-
storedTerminalIdentity.target === currentTerminalIdentity.target &&
|
|
11764
11861
|
storedTerminalIdentity.pid === currentTerminalIdentity.pid &&
|
|
11765
11862
|
Number(takeover?.terminal_agent_pid) === terminalConversation.pid &&
|
|
11766
11863
|
nativeAgentIdentityMatchesTurn(conversation, currentNativeIdentity) &&
|
|
11767
|
-
|
|
11768
|
-
terminalControlSelectorKey(terminalConversation.terminalControl) &&
|
|
11864
|
+
terminalControlsShareIncarnation(storedControl, terminalConversation.terminalControl) &&
|
|
11769
11865
|
matchesConfiguredWorkspace(conversation.workspace, terminalConversation.terminalControl.currentPath));
|
|
11770
11866
|
}
|
|
11771
11867
|
function createManagedTerminalTurn({ options, conversationId, agent, pid, messageBody, terminalControl, previousTurn, managedSession, nativeAgentIdentity }) {
|
|
@@ -11834,7 +11930,7 @@ function createManagedTerminalTurn({ options, conversationId, agent, pid, messag
|
|
|
11834
11930
|
takeover_match_kind: previousTurn
|
|
11835
11931
|
? "managed_session_send"
|
|
11836
11932
|
: "raw_terminal_send",
|
|
11837
|
-
|
|
11933
|
+
...terminalEndpointTakeoverFields(terminalControl),
|
|
11838
11934
|
needs_bootstrap: false,
|
|
11839
11935
|
terminal_bridge: true
|
|
11840
11936
|
}
|
|
@@ -11898,10 +11994,11 @@ async function runRenew(options) {
|
|
|
11898
11994
|
if (!terminalControl || nativeTakeover?.["terminal_bridge"] !== true) {
|
|
11899
11995
|
throw new Error(`cannot renew ${conversation.conversation_id}; conversation is not a terminal bridge task`);
|
|
11900
11996
|
}
|
|
11901
|
-
const
|
|
11902
|
-
|
|
11903
|
-
(
|
|
11904
|
-
|
|
11997
|
+
const terminalProvider = createTerminalControlProvider(options);
|
|
11998
|
+
try {
|
|
11999
|
+
await terminalProvider.resolve(terminalProvider.endpoint(terminalControl));
|
|
12000
|
+
}
|
|
12001
|
+
catch {
|
|
11905
12002
|
throw new Error(`cannot renew ${conversation.conversation_id}; terminal ${terminalControl.target} is no longer available`);
|
|
11906
12003
|
}
|
|
11907
12004
|
const expectedMessageId = stringValue(nativeTakeover?.terminal_bridge_message_id);
|
|
@@ -11924,9 +12021,7 @@ async function runRenew(options) {
|
|
|
11924
12021
|
throw new Error(`cannot renew ${current.conversation_id}; conversation is not a terminal bridge task`);
|
|
11925
12022
|
}
|
|
11926
12023
|
if (current.conversation_id !== conversation.conversation_id ||
|
|
11927
|
-
currentControl
|
|
11928
|
-
currentControl.socketPath !== terminalControl.socketPath ||
|
|
11929
|
-
currentControl.panePid !== terminalControl.panePid ||
|
|
12024
|
+
!terminalControlsShareIncarnation(currentControl, terminalControl) ||
|
|
11930
12025
|
stringValue(currentTakeover.terminal_bridge_message_id) !== expectedMessageId ||
|
|
11931
12026
|
stringValue(currentTakeover.terminal_bridge_started_at) !== expectedStartedAt) {
|
|
11932
12027
|
throw new Error("conversation changed while waiting to renew; refresh status and retry");
|
|
@@ -12601,7 +12696,7 @@ async function runCancel(options) {
|
|
|
12601
12696
|
throw new Error(`conversation ${conversation.conversation_id} is not attached to a live tmux terminal`);
|
|
12602
12697
|
}
|
|
12603
12698
|
async function runTerminalConversationCancel({ options, conversationId, agent, terminalControl, pid }) {
|
|
12604
|
-
const releaseTerminalLock =
|
|
12699
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 });
|
|
12605
12700
|
try {
|
|
12606
12701
|
const cancellation = await createTerminalAgentBridge(options).cancel(agent, terminalControl, {
|
|
12607
12702
|
runtime: {
|
|
@@ -12639,7 +12734,7 @@ async function runTerminalConversationCancel({ options, conversationId, agent, t
|
|
|
12639
12734
|
}
|
|
12640
12735
|
}
|
|
12641
12736
|
async function runTerminalControlCancel({ options, statePath, logPath, agent, terminalControl }) {
|
|
12642
|
-
const releaseTerminalLock =
|
|
12737
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 });
|
|
12643
12738
|
let releaseStateLock;
|
|
12644
12739
|
try {
|
|
12645
12740
|
releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
@@ -12654,8 +12749,7 @@ async function runTerminalControlCancel({ options, statePath, logPath, agent, te
|
|
|
12654
12749
|
: undefined;
|
|
12655
12750
|
const currentControl = terminalControlFromTakeover(currentTakeover);
|
|
12656
12751
|
if (!currentControl ||
|
|
12657
|
-
currentControl
|
|
12658
|
-
currentControl.socketPath !== terminalControl.socketPath) {
|
|
12752
|
+
!terminalControlsShareIncarnation(currentControl, terminalControl)) {
|
|
12659
12753
|
throw new Error("terminal control changed while waiting to cancel; refresh status and retry");
|
|
12660
12754
|
}
|
|
12661
12755
|
assertManagedTerminalDispatchOwner({
|
|
@@ -12742,7 +12836,7 @@ async function runClose(options) {
|
|
|
12742
12836
|
: undefined;
|
|
12743
12837
|
const terminalControl = terminalControlFromTakeover(nativeTakeover);
|
|
12744
12838
|
const releaseTerminalLock = terminalControl
|
|
12745
|
-
?
|
|
12839
|
+
? acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 })
|
|
12746
12840
|
: () => { };
|
|
12747
12841
|
let releaseStateLock;
|
|
12748
12842
|
try {
|
|
@@ -12816,7 +12910,7 @@ async function runClose(options) {
|
|
|
12816
12910
|
async function runTerminalDispatchClose({ options, terminalConversation }) {
|
|
12817
12911
|
const terminalControl = terminalConversation.terminalControl;
|
|
12818
12912
|
const storeDir = storeDirFromOptions(options);
|
|
12819
|
-
const releaseTerminalLock =
|
|
12913
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDir, terminalControl, { timeoutMs: 30000 });
|
|
12820
12914
|
try {
|
|
12821
12915
|
return await withStoreWriterLeaseAsync(storeDir, async () => {
|
|
12822
12916
|
let ledger = resolveTerminalDispatchLedgerPaneIncarnation(terminalControl, loadTerminalBridgeDispatchLedger(terminalControl));
|
|
@@ -13484,7 +13578,7 @@ async function runTerminalBridgeMonitorWithLock(options, lifecycle, expectedTerm
|
|
|
13484
13578
|
});
|
|
13485
13579
|
return;
|
|
13486
13580
|
}
|
|
13487
|
-
const releaseTerminalLock =
|
|
13581
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 });
|
|
13488
13582
|
try {
|
|
13489
13583
|
const reconciliation = await reconcileTerminalAcceptanceInMonitor({
|
|
13490
13584
|
options,
|
|
@@ -13548,7 +13642,7 @@ async function runTerminalBridgeMonitorWithLock(options, lifecycle, expectedTerm
|
|
|
13548
13642
|
sleepSync(pollIntervalMs);
|
|
13549
13643
|
continue;
|
|
13550
13644
|
}
|
|
13551
|
-
const releaseTerminalLock =
|
|
13645
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 });
|
|
13552
13646
|
try {
|
|
13553
13647
|
const writerStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
13554
13648
|
await withStoreWriterLeaseAsync(writerStoreDir, async () => {
|
|
@@ -13828,7 +13922,7 @@ async function runTerminalBridgeMonitorWithLock(options, lifecycle, expectedTerm
|
|
|
13828
13922
|
let poll;
|
|
13829
13923
|
const expectedUpdatedAt = conversation.updated_at;
|
|
13830
13924
|
const expectedStatus = conversation.status;
|
|
13831
|
-
const releaseTerminalPollLock =
|
|
13925
|
+
const releaseTerminalPollLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 });
|
|
13832
13926
|
try {
|
|
13833
13927
|
let dispatchLedger = reconcilePreparedTerminalDispatchLedger(terminalControl, loadTerminalBridgeDispatchLedger(terminalControl));
|
|
13834
13928
|
const durableSubmission = terminalBridgeSubmission(conversation);
|
|
@@ -13894,8 +13988,7 @@ async function runTerminalBridgeMonitorWithLock(options, lifecycle, expectedTerm
|
|
|
13894
13988
|
stringValue(lockedTakeover?.terminal_bridge_message_id) !==
|
|
13895
13989
|
currentMessageId ||
|
|
13896
13990
|
!lockedControl ||
|
|
13897
|
-
|
|
13898
|
-
terminalControlSelectorKey(terminalControl)) {
|
|
13991
|
+
!terminalControlsShareIncarnation(lockedControl, terminalControl)) {
|
|
13899
13992
|
conversation = lockedConversation;
|
|
13900
13993
|
continue;
|
|
13901
13994
|
}
|
|
@@ -14579,6 +14672,39 @@ function terminalBridgeSendLockPath(_storeDir, terminalControl) {
|
|
|
14579
14672
|
const terminalKey = terminalBridgeRuntimeKey(terminalControl);
|
|
14580
14673
|
return path.join(lockDir, `terminal-bridge-send-${terminalKey}.lock`);
|
|
14581
14674
|
}
|
|
14675
|
+
function legacyTerminalBridgeSendLockPath(_storeDir, terminalControl) {
|
|
14676
|
+
const lockDir = terminalBridgeRuntimeLockDir();
|
|
14677
|
+
ensureDir(lockDir);
|
|
14678
|
+
return path.join(lockDir, `terminal-bridge-send-${legacyTerminalBridgeRuntimeKey(terminalControl)}.lock`);
|
|
14679
|
+
}
|
|
14680
|
+
function acquireTerminalBridgeSendLock(storeDir, terminalControl, options = {}) {
|
|
14681
|
+
const lockPaths = [...new Set([
|
|
14682
|
+
terminalBridgeSendLockPath(storeDir, terminalControl),
|
|
14683
|
+
legacyTerminalBridgeSendLockPath(storeDir, terminalControl)
|
|
14684
|
+
])].sort();
|
|
14685
|
+
const releases = [];
|
|
14686
|
+
try {
|
|
14687
|
+
for (const lockPath of lockPaths) {
|
|
14688
|
+
releases.push(acquireFileLock(lockPath, options));
|
|
14689
|
+
}
|
|
14690
|
+
}
|
|
14691
|
+
catch (error) {
|
|
14692
|
+
for (const release of releases.reverse()) {
|
|
14693
|
+
release();
|
|
14694
|
+
}
|
|
14695
|
+
throw error;
|
|
14696
|
+
}
|
|
14697
|
+
let released = false;
|
|
14698
|
+
return () => {
|
|
14699
|
+
if (released) {
|
|
14700
|
+
return;
|
|
14701
|
+
}
|
|
14702
|
+
released = true;
|
|
14703
|
+
for (const release of [...releases].reverse()) {
|
|
14704
|
+
release();
|
|
14705
|
+
}
|
|
14706
|
+
};
|
|
14707
|
+
}
|
|
14582
14708
|
function terminalBridgeRuntimeLockDir() {
|
|
14583
14709
|
return path.join(terminalBridgeRuntimeDir(), "terminal-locks");
|
|
14584
14710
|
}
|
|
@@ -14589,46 +14715,101 @@ function terminalBridgeRuntimeDir() {
|
|
|
14589
14715
|
: path.join(path.dirname(defaultStoreDir()), "runtime-v2");
|
|
14590
14716
|
}
|
|
14591
14717
|
function terminalBridgeRuntimeKey(terminalControl) {
|
|
14718
|
+
if (hasCanonicalTerminalEndpoint(terminalControl)) {
|
|
14719
|
+
return createHash("sha256")
|
|
14720
|
+
.update(terminalEndpointIdentityKey(terminalControl))
|
|
14721
|
+
.digest("hex")
|
|
14722
|
+
.slice(0, 20);
|
|
14723
|
+
}
|
|
14724
|
+
return legacyTerminalBridgeRuntimeKey(terminalControl);
|
|
14725
|
+
}
|
|
14726
|
+
function legacyTerminalBridgeRuntimeKey(terminalControl) {
|
|
14592
14727
|
return createHash("sha256")
|
|
14593
|
-
.update(JSON.stringify(
|
|
14594
|
-
target: terminalControl.target,
|
|
14595
|
-
socket_path: terminalControl.socketPath ?? null
|
|
14596
|
-
}))
|
|
14728
|
+
.update(JSON.stringify(terminalLegacyRuntimeRoute(terminalControl)))
|
|
14597
14729
|
.digest("hex")
|
|
14598
14730
|
.slice(0, 20);
|
|
14599
14731
|
}
|
|
14600
|
-
function terminalBridgeDispatchLedgerPath(terminalControl) {
|
|
14732
|
+
function terminalBridgeDispatchLedgerPath(terminalControl, options = {}) {
|
|
14601
14733
|
const ledgerDir = path.join(terminalBridgeRuntimeDir(), "terminal-dispatch");
|
|
14602
|
-
return path.join(ledgerDir, `terminal-dispatch-${
|
|
14734
|
+
return path.join(ledgerDir, `terminal-dispatch-${options.legacy
|
|
14735
|
+
? legacyTerminalBridgeRuntimeKey(terminalControl)
|
|
14736
|
+
: terminalBridgeRuntimeKey(terminalControl)}.json`);
|
|
14737
|
+
}
|
|
14738
|
+
function terminalBridgeDispatchLedgerPaths(terminalControl) {
|
|
14739
|
+
return [...new Set([
|
|
14740
|
+
terminalBridgeDispatchLedgerPath(terminalControl),
|
|
14741
|
+
terminalBridgeDispatchLedgerPath(terminalControl, { legacy: true })
|
|
14742
|
+
])];
|
|
14603
14743
|
}
|
|
14604
14744
|
function loadTerminalBridgeDispatchLedger(terminalControl) {
|
|
14605
|
-
const
|
|
14606
|
-
|
|
14745
|
+
const ledgerPaths = terminalBridgeDispatchLedgerPaths(terminalControl)
|
|
14746
|
+
.filter((candidate) => fs.existsSync(candidate));
|
|
14747
|
+
if (ledgerPaths.length === 0) {
|
|
14607
14748
|
return undefined;
|
|
14608
14749
|
}
|
|
14750
|
+
if (ledgerPaths.length > 1) {
|
|
14751
|
+
throw new Error(`terminal dispatch ledger has conflicting canonical and legacy owners: ` +
|
|
14752
|
+
ledgerPaths.join(", "));
|
|
14753
|
+
}
|
|
14754
|
+
const ledgerPath = ledgerPaths[0];
|
|
14609
14755
|
const stat = fs.lstatSync(ledgerPath);
|
|
14610
14756
|
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
14611
14757
|
throw new Error(`terminal dispatch ledger is not a regular file: ${ledgerPath}`);
|
|
14612
14758
|
}
|
|
14613
14759
|
const parsed = JSON.parse(fs.readFileSync(ledgerPath, "utf8"));
|
|
14614
14760
|
if (!isRecord(parsed) ||
|
|
14615
|
-
parsed.version
|
|
14616
|
-
stringValue(parsed.terminal_key) !== terminalBridgeRuntimeKey(terminalControl)) {
|
|
14761
|
+
!(parsed.version === 1 || parsed.version === 2)) {
|
|
14617
14762
|
throw new Error(`terminal dispatch ledger is invalid: ${ledgerPath}`);
|
|
14618
14763
|
}
|
|
14764
|
+
if (parsed.version === 1) {
|
|
14765
|
+
const control = isRecord(parsed.terminal_control)
|
|
14766
|
+
? parsed.terminal_control
|
|
14767
|
+
: undefined;
|
|
14768
|
+
if (stringValue(parsed.terminal_key) !==
|
|
14769
|
+
legacyTerminalBridgeRuntimeKey(terminalControl) ||
|
|
14770
|
+
stringValue(control?.target) !== terminalControl.target ||
|
|
14771
|
+
(stringValue(control?.socket_path) ?? undefined) !==
|
|
14772
|
+
terminalControl.socketPath) {
|
|
14773
|
+
throw new Error(`terminal dispatch ledger is invalid: ${ledgerPath}`);
|
|
14774
|
+
}
|
|
14775
|
+
}
|
|
14776
|
+
else {
|
|
14777
|
+
const identity = terminalEndpointIdentityFromEvidence(parsed.terminal_endpoint);
|
|
14778
|
+
if (!identity ||
|
|
14779
|
+
stringValue(parsed.terminal_key) !==
|
|
14780
|
+
terminalBridgeRuntimeKey(terminalControl) ||
|
|
14781
|
+
!sameTerminalEndpointIdentity(identity, terminalControl)) {
|
|
14782
|
+
throw new Error(`terminal dispatch ledger is invalid: ${ledgerPath}`);
|
|
14783
|
+
}
|
|
14784
|
+
}
|
|
14619
14785
|
return parsed;
|
|
14620
14786
|
}
|
|
14787
|
+
function terminalDispatchRecordMatchesControl(record, terminalControl, options = {}) {
|
|
14788
|
+
if (!record) {
|
|
14789
|
+
return false;
|
|
14790
|
+
}
|
|
14791
|
+
const evidence = record.terminal_endpoint !== undefined
|
|
14792
|
+
? record.terminal_endpoint
|
|
14793
|
+
: record.terminal_control;
|
|
14794
|
+
return terminalControlEvidenceMatches(evidence, terminalControl, options);
|
|
14795
|
+
}
|
|
14796
|
+
function terminalDispatchRecordProcessAnchor(record) {
|
|
14797
|
+
const evidence = isRecord(record.terminal_endpoint)
|
|
14798
|
+
? record.terminal_endpoint
|
|
14799
|
+
: isRecord(record.terminal_control)
|
|
14800
|
+
? record.terminal_control
|
|
14801
|
+
: undefined;
|
|
14802
|
+
const panePid = Number(evidence?.process_anchor_pid ?? evidence?.pane_pid ?? evidence?.panePid);
|
|
14803
|
+
return Number.isSafeInteger(panePid) && panePid > 0 ? panePid : undefined;
|
|
14804
|
+
}
|
|
14621
14805
|
function assertTerminalNativeThreadStoreAuthority({ terminalControl, nativeThreadId, storeDir }) {
|
|
14622
14806
|
const ledger = resolveTerminalDispatchLedgerPaneIncarnation(terminalControl, loadTerminalBridgeDispatchLedger(terminalControl));
|
|
14623
14807
|
if (!ledger) {
|
|
14624
14808
|
return;
|
|
14625
14809
|
}
|
|
14626
|
-
|
|
14627
|
-
|
|
14628
|
-
|
|
14629
|
-
if (stringValue(ledgerControl?.target) !== terminalControl.target ||
|
|
14630
|
-
(stringValue(ledgerControl?.socket_path) ?? undefined) !==
|
|
14631
|
-
terminalControl.socketPath) {
|
|
14810
|
+
if (!terminalDispatchRecordMatchesControl(ledger, terminalControl, {
|
|
14811
|
+
requireProcessAnchor: false
|
|
14812
|
+
})) {
|
|
14632
14813
|
throw new Error(`terminal ${terminalControl.target} dispatch ledger selector is invalid`);
|
|
14633
14814
|
}
|
|
14634
14815
|
const binding = isRecord(ledger.binding) ? ledger.binding : undefined;
|
|
@@ -14672,11 +14853,6 @@ function orphanedTerminalDispatchForRecovery(terminalControl) {
|
|
|
14672
14853
|
const recoveryIdentity = lifecycle
|
|
14673
14854
|
? stringValue(ledger?.transition_id)
|
|
14674
14855
|
: stringValue(ledger?.message_id);
|
|
14675
|
-
const ledgerControl = isRecord(ledger?.terminal_control)
|
|
14676
|
-
? ledger.terminal_control
|
|
14677
|
-
: undefined;
|
|
14678
|
-
const ledgerPanePid = Number(ledgerControl?.pane_pid);
|
|
14679
|
-
const currentPanePid = Number(terminalControl.panePid);
|
|
14680
14856
|
if (!ledger ||
|
|
14681
14857
|
![
|
|
14682
14858
|
"prepared",
|
|
@@ -14692,11 +14868,10 @@ function orphanedTerminalDispatchForRecovery(terminalControl) {
|
|
|
14692
14868
|
.includes(String(ledger.status)) ||
|
|
14693
14869
|
!recoveryIdentity ||
|
|
14694
14870
|
(!lifecycle &&
|
|
14695
|
-
|
|
14696
|
-
|
|
14697
|
-
|
|
14698
|
-
|
|
14699
|
-
ledgerPanePid !== currentPanePid) ||
|
|
14871
|
+
terminalDispatchRecordMatchesControl(ledger, terminalControl, {
|
|
14872
|
+
requireProcessAnchor: false
|
|
14873
|
+
}) &&
|
|
14874
|
+
!terminalDispatchRecordMatchesControl(ledger, terminalControl)) ||
|
|
14700
14875
|
(!lifecycle && loadTerminalDispatchLedgerOwner(ledger))) {
|
|
14701
14876
|
return undefined;
|
|
14702
14877
|
}
|
|
@@ -14865,12 +15040,11 @@ function mergeTerminalLedgerReceipt(previous, next) {
|
|
|
14865
15040
|
const nextControl = isRecord(next.terminal_control)
|
|
14866
15041
|
? next.terminal_control
|
|
14867
15042
|
: undefined;
|
|
14868
|
-
|
|
14869
|
-
|
|
14870
|
-
|
|
14871
|
-
|
|
14872
|
-
|
|
14873
|
-
Number(previousControl.pane_pid) !== Number(nextControl.pane_pid))) {
|
|
15043
|
+
const previousEvidence = previous.terminal_endpoint ?? previousControl;
|
|
15044
|
+
const nextEvidence = next.terminal_endpoint ?? nextControl;
|
|
15045
|
+
if (previousEvidence !== undefined &&
|
|
15046
|
+
nextEvidence !== undefined &&
|
|
15047
|
+
!sameTerminalControlEvidenceIncarnation(previousEvidence, nextEvidence)) {
|
|
14874
15048
|
throw new Error(`terminal dispatch receipt ${messageId} changed immutable terminal_control`);
|
|
14875
15049
|
}
|
|
14876
15050
|
if (previous.status === "agent_accepted" &&
|
|
@@ -14899,17 +15073,40 @@ function mergeTerminalLedgerReceipt(previous, next) {
|
|
|
14899
15073
|
if (!merged.terminal_control && previousControl) {
|
|
14900
15074
|
merged.terminal_control = previousControl;
|
|
14901
15075
|
}
|
|
15076
|
+
if (!merged.terminal_endpoint && previous.terminal_endpoint) {
|
|
15077
|
+
merged.terminal_endpoint = previous.terminal_endpoint;
|
|
15078
|
+
}
|
|
14902
15079
|
return merged;
|
|
14903
15080
|
}
|
|
14904
15081
|
function saveTerminalBridgeDispatchLedger(terminalControl, ledger) {
|
|
14905
|
-
const
|
|
15082
|
+
const previousLedger = loadTerminalBridgeDispatchLedger(terminalControl);
|
|
15083
|
+
const existingPaths = terminalBridgeDispatchLedgerPaths(terminalControl)
|
|
15084
|
+
.filter((candidate) => fs.existsSync(candidate));
|
|
15085
|
+
let ledgerPath = existingPaths[0] ??
|
|
15086
|
+
terminalBridgeDispatchLedgerPath(terminalControl);
|
|
15087
|
+
const canonicalLedgerPath = terminalBridgeDispatchLedgerPath(terminalControl);
|
|
15088
|
+
const legacyLedgerPath = terminalBridgeDispatchLedgerPath(terminalControl, { legacy: true });
|
|
15089
|
+
ensureDir(path.dirname(canonicalLedgerPath));
|
|
15090
|
+
if (hasCanonicalTerminalEndpoint(terminalControl) &&
|
|
15091
|
+
ledgerPath === legacyLedgerPath &&
|
|
15092
|
+
legacyLedgerPath !== canonicalLedgerPath) {
|
|
15093
|
+
// Promote a validated v0.11.x route-keyed ledger before writing the next
|
|
15094
|
+
// generation. rename() removes the duplicate-owner window: after a crash,
|
|
15095
|
+
// the canonical path may still contain v1 JSON, which the compatibility
|
|
15096
|
+
// reader accepts and a later writer can finish upgrading to v2.
|
|
15097
|
+
if (fs.existsSync(canonicalLedgerPath)) {
|
|
15098
|
+
throw new Error(`terminal dispatch ledger has conflicting canonical and legacy owners: ` +
|
|
15099
|
+
`${canonicalLedgerPath}, ${legacyLedgerPath}`);
|
|
15100
|
+
}
|
|
15101
|
+
fs.renameSync(legacyLedgerPath, canonicalLedgerPath);
|
|
15102
|
+
fsyncTerminalBridgeDirectory(path.dirname(canonicalLedgerPath));
|
|
15103
|
+
ledgerPath = canonicalLedgerPath;
|
|
15104
|
+
}
|
|
15105
|
+
const preserveLegacyFormat = !hasCanonicalTerminalEndpoint(terminalControl);
|
|
14906
15106
|
ensureDir(path.dirname(ledgerPath));
|
|
14907
15107
|
if (fs.existsSync(ledgerPath) && fs.lstatSync(ledgerPath).isSymbolicLink()) {
|
|
14908
15108
|
throw new Error(`terminal dispatch ledger is a symlink: ${ledgerPath}`);
|
|
14909
15109
|
}
|
|
14910
|
-
const previousLedger = fs.existsSync(ledgerPath)
|
|
14911
|
-
? loadTerminalBridgeDispatchLedger(terminalControl)
|
|
14912
|
-
: undefined;
|
|
14913
15110
|
let baseReceiptHistory = terminalLedgerReceiptHistory(previousLedger);
|
|
14914
15111
|
for (const incomingReceipt of terminalLedgerReceiptHistory(ledger)) {
|
|
14915
15112
|
const incomingId = String(incomingReceipt.message_id);
|
|
@@ -14921,18 +15118,25 @@ function saveTerminalBridgeDispatchLedger(terminalControl, ledger) {
|
|
|
14921
15118
|
? baseReceiptHistory.map((receipt) => stringValue(receipt.message_id) === incomingId ? merged : receipt)
|
|
14922
15119
|
: [...baseReceiptHistory, merged];
|
|
14923
15120
|
}
|
|
14924
|
-
const { terminal_submission_receipts: _incomingReceiptHistory, ...ledgerWithoutReceiptHistory } = ledger;
|
|
15121
|
+
const { terminal_submission_receipts: _incomingReceiptHistory, terminal_endpoint: incomingTerminalEndpoint, ...ledgerWithoutReceiptHistory } = ledger;
|
|
14925
15122
|
const nextWithoutHistory = {
|
|
14926
15123
|
...ledgerWithoutReceiptHistory,
|
|
14927
|
-
version:
|
|
14928
|
-
|
|
15124
|
+
version: hasCanonicalTerminalEndpoint(terminalControl) && !preserveLegacyFormat
|
|
15125
|
+
? 2
|
|
15126
|
+
: 1,
|
|
15127
|
+
terminal_key: preserveLegacyFormat
|
|
15128
|
+
? legacyTerminalBridgeRuntimeKey(terminalControl)
|
|
15129
|
+
: terminalBridgeRuntimeKey(terminalControl),
|
|
14929
15130
|
terminal_control: {
|
|
14930
15131
|
kind: "tmux",
|
|
14931
15132
|
target: terminalControl.target,
|
|
14932
15133
|
socket_path: terminalControl.socketPath ?? null,
|
|
14933
15134
|
pane_pid: terminalControl.panePid ?? null,
|
|
14934
15135
|
current_path: terminalControl.currentPath ?? null
|
|
14935
|
-
}
|
|
15136
|
+
},
|
|
15137
|
+
...(hasCanonicalTerminalEndpoint(terminalControl) && !preserveLegacyFormat
|
|
15138
|
+
? { terminal_endpoint: terminalControlEvidence(terminalControl) }
|
|
15139
|
+
: {})
|
|
14936
15140
|
};
|
|
14937
15141
|
// The top-level ledger follows the current tmux pane incarnation, while an
|
|
14938
15142
|
// append-only receipt must retain the pane that actually accepted its
|
|
@@ -14940,10 +15144,16 @@ function saveTerminalBridgeDispatchLedger(terminalControl, ledger) {
|
|
|
14940
15144
|
// pane with the same selector, so derive that receipt from the caller's
|
|
14941
15145
|
// original control record instead of silently rebinding its proof.
|
|
14942
15146
|
const receiptCandidateLedger = isRecord(ledgerWithoutReceiptHistory.terminal_control)
|
|
14943
|
-
? {
|
|
14944
|
-
...nextWithoutHistory
|
|
14945
|
-
|
|
14946
|
-
|
|
15147
|
+
? (() => {
|
|
15148
|
+
const { terminal_endpoint: _derivedTerminalEndpoint, ...nextWithoutDerivedTerminalEndpoint } = nextWithoutHistory;
|
|
15149
|
+
return {
|
|
15150
|
+
...nextWithoutDerivedTerminalEndpoint,
|
|
15151
|
+
terminal_control: ledgerWithoutReceiptHistory.terminal_control,
|
|
15152
|
+
...(incomingTerminalEndpoint && !preserveLegacyFormat
|
|
15153
|
+
? { terminal_endpoint: incomingTerminalEndpoint }
|
|
15154
|
+
: {})
|
|
15155
|
+
};
|
|
15156
|
+
})()
|
|
14947
15157
|
: nextWithoutHistory;
|
|
14948
15158
|
const nextCandidate = terminalLedgerReceiptCandidate(receiptCandidateLedger);
|
|
14949
15159
|
let nextReceiptHistory = baseReceiptHistory;
|
|
@@ -15018,23 +15228,20 @@ function resolveTerminalDispatchLedgerPaneIncarnation(terminalControl, ledger) {
|
|
|
15018
15228
|
// released by the generic Turn-ledger cleanup path.
|
|
15019
15229
|
return ledger;
|
|
15020
15230
|
}
|
|
15021
|
-
|
|
15022
|
-
|
|
15023
|
-
|
|
15024
|
-
|
|
15025
|
-
const currentPanePid = Number(terminalControl.panePid);
|
|
15026
|
-
if (!Number.isSafeInteger(ledgerPanePid) ||
|
|
15027
|
-
ledgerPanePid <= 0 ||
|
|
15028
|
-
!Number.isSafeInteger(currentPanePid) ||
|
|
15029
|
-
currentPanePid <= 0 ||
|
|
15030
|
-
ledgerPanePid === currentPanePid) {
|
|
15231
|
+
if (!terminalDispatchRecordMatchesControl(ledger, terminalControl, {
|
|
15232
|
+
requireProcessAnchor: false
|
|
15233
|
+
}) ||
|
|
15234
|
+
terminalDispatchRecordMatchesControl(ledger, terminalControl)) {
|
|
15031
15235
|
return ledger;
|
|
15032
15236
|
}
|
|
15237
|
+
const ledgerProcessAnchor = terminalDispatchRecordProcessAnchor(ledger);
|
|
15238
|
+
const currentProcessAnchor = terminalEndpointFromControlRef(terminalControl).processAnchorPid;
|
|
15033
15239
|
saveTerminalBridgeDispatchLedger(terminalControl, {
|
|
15034
15240
|
...ledger,
|
|
15035
15241
|
status: "resolved",
|
|
15036
15242
|
resolved_at: new Date().toISOString(),
|
|
15037
|
-
reason:
|
|
15243
|
+
reason: "terminal process incarnation changed from anchor " +
|
|
15244
|
+
`${ledgerProcessAnchor} to ${currentProcessAnchor ?? "unknown"}`
|
|
15038
15245
|
});
|
|
15039
15246
|
return loadTerminalBridgeDispatchLedger(terminalControl);
|
|
15040
15247
|
}
|
|
@@ -15154,8 +15361,7 @@ function reconcilePreparedTerminalDispatchLedger(terminalControl, ledger) {
|
|
|
15154
15361
|
if (conversation.conversation_id !==
|
|
15155
15362
|
stringValue(ledger.conversation_id) ||
|
|
15156
15363
|
!storedControl ||
|
|
15157
|
-
|
|
15158
|
-
terminalBridgeRuntimeKey(terminalControl)) {
|
|
15364
|
+
!sameTerminalControlIncarnation(storedControl, terminalControl)) {
|
|
15159
15365
|
return ledger;
|
|
15160
15366
|
}
|
|
15161
15367
|
if (storedMessageId === messageId &&
|
|
@@ -15280,8 +15486,7 @@ function reconcileLaggingTerminalDispatchLedger(terminalControl, ledger) {
|
|
|
15280
15486
|
stringValue(nativeTakeover?.terminal_bridge_message_id) !== messageId ||
|
|
15281
15487
|
stringValue(submission?.message_id) !== messageId ||
|
|
15282
15488
|
!storedControl ||
|
|
15283
|
-
|
|
15284
|
-
terminalBridgeRuntimeKey(terminalControl)) {
|
|
15489
|
+
!sameTerminalControlIncarnation(storedControl, terminalControl)) {
|
|
15285
15490
|
return ledger;
|
|
15286
15491
|
}
|
|
15287
15492
|
const requestText = String(nativeTakeover?.terminal_bridge_request_text ?? conversation.user_request ?? "");
|
|
@@ -15709,16 +15914,13 @@ function assertLifecycleLedgerMatchesTransition({ options, terminal, ledger, tra
|
|
|
15709
15914
|
stringValue(ledger.adapter_version) !== transition.adapter_version ||
|
|
15710
15915
|
stringValue(ledger.command_fingerprint) !== transition.command_fingerprint ||
|
|
15711
15916
|
path.resolve(storeDir) !== path.resolve(storeDirFromOptions(options)) ||
|
|
15712
|
-
terminal.conversationId !== transition.terminal_id ||
|
|
15713
15917
|
terminal.agent !== transition.agent ||
|
|
15714
|
-
|
|
15715
|
-
stringValue(ledgerControl?.target) !== terminal.terminalControl.target ||
|
|
15716
|
-
(stringValue(ledgerControl?.socket_path) ?? undefined) !==
|
|
15717
|
-
terminal.terminalControl.socketPath ||
|
|
15918
|
+
!terminalDispatchRecordMatchesControl(ledger, terminal.terminalControl) ||
|
|
15718
15919
|
terminal.pid !== transition.before_binding?.native_process.pid &&
|
|
15719
15920
|
transition.before_binding !== undefined ||
|
|
15720
|
-
|
|
15721
|
-
|
|
15921
|
+
!terminalControlsShareIncarnation(terminal.terminalControl, transition.before_binding?.terminal_control ?? terminal.terminalControl) ||
|
|
15922
|
+
(transition.before_binding !== undefined &&
|
|
15923
|
+
!terminalControlAliasMatches(transition.terminal_id, transition.before_binding.terminal_control, terminal.conversationId, terminal.terminalControl)) ||
|
|
15722
15924
|
!matchesConfiguredWorkspace(transition.workspace, terminal.terminalControl.currentPath) ||
|
|
15723
15925
|
JSON.stringify(ledger.binding ?? null) !==
|
|
15724
15926
|
JSON.stringify(transition.before_binding ?? null)) {
|
|
@@ -15748,10 +15950,8 @@ async function verifyRecoveredLifecycleAfterBinding({ options, terminal, transit
|
|
|
15748
15950
|
const binding = transition.after_binding;
|
|
15749
15951
|
if (!binding ||
|
|
15750
15952
|
!binding.native_thread_id ||
|
|
15751
|
-
binding.terminal_id !== terminal.conversationId ||
|
|
15752
15953
|
binding.native_process.pid !== terminal.pid ||
|
|
15753
|
-
|
|
15754
|
-
terminalControlSelectorKey(terminal.terminalControl)) {
|
|
15954
|
+
!terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminal.conversationId, terminal.terminalControl)) {
|
|
15755
15955
|
throw new Error("verified after_binding no longer matches the terminal or pid");
|
|
15756
15956
|
}
|
|
15757
15957
|
const bridge = createTerminalAgentBridge(options);
|
|
@@ -16255,10 +16455,9 @@ async function assertNativeInspectionComposerReadyForAutomatedInput({ options, t
|
|
|
16255
16455
|
});
|
|
16256
16456
|
return;
|
|
16257
16457
|
}
|
|
16258
|
-
const
|
|
16259
|
-
|
|
16260
|
-
|
|
16261
|
-
});
|
|
16458
|
+
const provider = createTerminalControlProvider(options);
|
|
16459
|
+
const resolvedTerminal = await provider.resolve(provider.endpoint(terminal.terminalControl));
|
|
16460
|
+
const screen = await provider.capture(resolvedTerminal, { scrollbackLines: 40 });
|
|
16262
16461
|
if (!isExactClaudeNativeInspectionIdleComposer(screen)) {
|
|
16263
16462
|
throw new Error("Claude composer contains input or is not at the exact idle frame; refusing automated terminal input");
|
|
16264
16463
|
}
|
|
@@ -16677,7 +16876,7 @@ function prepareTerminalBridgeCompletionCallback({ options, statePath, logPath,
|
|
|
16677
16876
|
});
|
|
16678
16877
|
claim.release();
|
|
16679
16878
|
claimReleased = true;
|
|
16680
|
-
const releaseTerminalLock =
|
|
16879
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 });
|
|
16681
16880
|
try {
|
|
16682
16881
|
resolveTerminalBridgeDispatchLedger({
|
|
16683
16882
|
terminalControl,
|