@scotthuang/agent-knock-knock 0.11.6 → 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 +13 -0
- package/dist/src/cli.js +585 -408
- package/dist/src/cli.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/session-store.js +5 -0
- package/dist/src/session-store.js.map +1 -1
- package/dist/src/terminal-agent-adapter.d.ts +2 -14
- package/dist/src/terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +7 -1
- package/dist/src/terminal-agent-bridge.js +178 -73
- 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/package.json +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,8 +2866,7 @@ 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));
|
|
2850
2871
|
let nativeInspectionComposerReady = nativeInspectionComposerEmpty(session.agent, terminalState.screen_excerpt);
|
|
2851
2872
|
if (session.agent === "codex" &&
|
|
@@ -2856,11 +2877,9 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
2856
2877
|
terminalControl.capabilities.includes("send_keys") &&
|
|
2857
2878
|
terminalControl.capabilities.includes("screen_status")) {
|
|
2858
2879
|
try {
|
|
2859
|
-
const
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
preserveEscapes: true
|
|
2863
|
-
});
|
|
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 });
|
|
2864
2883
|
nativeInspectionComposerReady = codexStyledComposerEmpty(styledScreen);
|
|
2865
2884
|
}
|
|
2866
2885
|
catch {
|
|
@@ -2946,26 +2965,24 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
2946
2965
|
};
|
|
2947
2966
|
}
|
|
2948
2967
|
function terminalFirstListProjection({ storeDir, terminals, managedSessions, sessionAuthorityRequired, allConversations, displayedConversations, includeAll, managedOnly, statusFilter, mutationsAllowed }) {
|
|
2949
|
-
const
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2968
|
+
const discoveredTerminalControls = terminals.flatMap((terminal) => {
|
|
2969
|
+
const control = isRecord(terminal.terminal_control)
|
|
2970
|
+
? terminal.terminal_control
|
|
2971
|
+
: undefined;
|
|
2972
|
+
return control ? [control] : [];
|
|
2973
|
+
});
|
|
2953
2974
|
const projectedTerminals = terminals.map((terminal) => {
|
|
2954
2975
|
const terminalControl = isRecord(terminal.terminal_control)
|
|
2955
2976
|
? terminal.terminal_control
|
|
2956
2977
|
: undefined;
|
|
2957
|
-
const
|
|
2958
|
-
|
|
2959
|
-
discoveredTerminalKeys.add(terminalKey);
|
|
2960
|
-
}
|
|
2961
|
-
const allRelated = terminalKey
|
|
2962
|
-
? [...(allByTerminal.get(terminalKey) ?? [])]
|
|
2978
|
+
const allRelated = terminalControl
|
|
2979
|
+
? allConversations.filter((conversation) => terminalControlsShareIncarnation(terminalControlForManagedConversation(conversation), terminalControl))
|
|
2963
2980
|
: [];
|
|
2964
|
-
const displayedRelated =
|
|
2965
|
-
?
|
|
2981
|
+
const displayedRelated = terminalControl
|
|
2982
|
+
? displayedConversations.filter((conversation) => terminalControlsShareIncarnation(terminalControlForManagedConversation(conversation), terminalControl))
|
|
2966
2983
|
: [];
|
|
2967
|
-
const relatedSessions =
|
|
2968
|
-
?
|
|
2984
|
+
const relatedSessions = terminalControl
|
|
2985
|
+
? managedSessions.filter((session) => terminalControlsShareIncarnation(session.binding?.terminal_control, terminalControl))
|
|
2969
2986
|
: [];
|
|
2970
2987
|
const matchingSessions = relatedSessions.filter((session) => managedSessionMatchesLiveTerminalEntry(session, terminal, storeDir));
|
|
2971
2988
|
const conflictingBoundSessionClaims = relatedSessions.flatMap((session) => {
|
|
@@ -3232,8 +3249,8 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3232
3249
|
});
|
|
3233
3250
|
const unavailableManagedTurns = displayedConversations
|
|
3234
3251
|
.filter((conversation) => {
|
|
3235
|
-
const
|
|
3236
|
-
if (
|
|
3252
|
+
const managedControl = terminalControlForManagedConversation(conversation);
|
|
3253
|
+
if (discoveredTerminalControls.some((control) => terminalControlsShareIncarnation(managedControl, control))) {
|
|
3237
3254
|
return false;
|
|
3238
3255
|
}
|
|
3239
3256
|
return (includeAll ||
|
|
@@ -3270,35 +3287,6 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3270
3287
|
unavailableManagedTurns
|
|
3271
3288
|
};
|
|
3272
3289
|
}
|
|
3273
|
-
function managedConversationsByTerminal(conversations) {
|
|
3274
|
-
const groups = new Map();
|
|
3275
|
-
for (const conversation of conversations) {
|
|
3276
|
-
const key = terminalKeyForManagedConversation(conversation);
|
|
3277
|
-
if (!key) {
|
|
3278
|
-
continue;
|
|
3279
|
-
}
|
|
3280
|
-
const group = groups.get(key) ?? [];
|
|
3281
|
-
group.push(conversation);
|
|
3282
|
-
groups.set(key, group);
|
|
3283
|
-
}
|
|
3284
|
-
return groups;
|
|
3285
|
-
}
|
|
3286
|
-
function managedSessionsByTerminal(sessions) {
|
|
3287
|
-
const groups = new Map();
|
|
3288
|
-
for (const session of sessions) {
|
|
3289
|
-
if (!session.binding) {
|
|
3290
|
-
continue;
|
|
3291
|
-
}
|
|
3292
|
-
const key = terminalControlSelectorKey(session.binding.terminal_control);
|
|
3293
|
-
if (!key) {
|
|
3294
|
-
continue;
|
|
3295
|
-
}
|
|
3296
|
-
const group = groups.get(key) ?? [];
|
|
3297
|
-
group.push(session);
|
|
3298
|
-
groups.set(key, group);
|
|
3299
|
-
}
|
|
3300
|
-
return groups;
|
|
3301
|
-
}
|
|
3302
3290
|
function managedSessionMatchesLiveTerminalEntry(session, terminal, storeDir) {
|
|
3303
3291
|
const binding = session.binding;
|
|
3304
3292
|
const liveControl = isRecord(terminal.terminal_control)
|
|
@@ -3307,10 +3295,8 @@ function managedSessionMatchesLiveTerminalEntry(session, terminal, storeDir) {
|
|
|
3307
3295
|
if (session.status !== "bound" ||
|
|
3308
3296
|
!binding ||
|
|
3309
3297
|
session.agent !== terminal.agent ||
|
|
3310
|
-
binding.terminal_id !== stringValue(terminal.id) ||
|
|
3311
3298
|
binding.native_process.pid !== Number(terminal.pid) ||
|
|
3312
|
-
|
|
3313
|
-
terminalControlSelectorKey(liveControl) ||
|
|
3299
|
+
!terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminal.id, liveControl) ||
|
|
3314
3300
|
!matchesConfiguredWorkspace(session.workspace, terminal.workspace ?? terminal.cwd)) {
|
|
3315
3301
|
return false;
|
|
3316
3302
|
}
|
|
@@ -3381,8 +3367,7 @@ function managedSessionClaimsLiveTerminalEntry(session, terminal) {
|
|
|
3381
3367
|
return Boolean(binding &&
|
|
3382
3368
|
session.agent === terminal.agent &&
|
|
3383
3369
|
binding.native_process.pid === Number(terminal.pid) &&
|
|
3384
|
-
|
|
3385
|
-
terminalControlSelectorKey(liveControl));
|
|
3370
|
+
terminalControlsShareIncarnation(binding.terminal_control, liveControl));
|
|
3386
3371
|
}
|
|
3387
3372
|
function listedTerminalProcessIncarnation(terminal) {
|
|
3388
3373
|
const processUuid = stringValue(terminal.native_agent_process_uuid);
|
|
@@ -3425,7 +3410,9 @@ function managedBindingConflictKindForLiveTerminalEntry({ storeDir, session, ter
|
|
|
3425
3410
|
if (relationship === "different") {
|
|
3426
3411
|
return "stale_process_incarnation";
|
|
3427
3412
|
}
|
|
3428
|
-
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) ||
|
|
3429
3416
|
!matchesConfiguredWorkspace(session.workspace, terminal.workspace ?? terminal.cwd)) {
|
|
3430
3417
|
return "unverifiable";
|
|
3431
3418
|
}
|
|
@@ -3491,10 +3478,10 @@ function managedSessionHasUnresolvedNativeTransition(storeDir, session) {
|
|
|
3491
3478
|
}
|
|
3492
3479
|
return false;
|
|
3493
3480
|
}
|
|
3494
|
-
function
|
|
3495
|
-
return
|
|
3481
|
+
function terminalControlForManagedConversation(conversation) {
|
|
3482
|
+
return terminalControlFromTakeover(isRecord(conversation.native_session_takeover)
|
|
3496
3483
|
? conversation.native_session_takeover
|
|
3497
|
-
: undefined)
|
|
3484
|
+
: undefined);
|
|
3498
3485
|
}
|
|
3499
3486
|
function compareManagedConversationRecency(left, right) {
|
|
3500
3487
|
const leftTime = Date.parse(String(left.updated_at ?? left.created_at ?? ""));
|
|
@@ -3542,16 +3529,10 @@ function terminalDispatchOwnership(terminalControl) {
|
|
|
3542
3529
|
if (!ledger || ledger.status === "resolved") {
|
|
3543
3530
|
return { state: "none" };
|
|
3544
3531
|
}
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
const currentPanePid = Number(terminalControl.panePid);
|
|
3550
|
-
if (Number.isSafeInteger(ledgerPanePid) &&
|
|
3551
|
-
ledgerPanePid > 0 &&
|
|
3552
|
-
Number.isSafeInteger(currentPanePid) &&
|
|
3553
|
-
currentPanePid > 0 &&
|
|
3554
|
-
ledgerPanePid !== currentPanePid) {
|
|
3532
|
+
if (terminalDispatchRecordMatchesControl(ledger, terminalControl, {
|
|
3533
|
+
requireProcessAnchor: false
|
|
3534
|
+
}) &&
|
|
3535
|
+
!terminalDispatchRecordMatchesControl(ledger, terminalControl)) {
|
|
3555
3536
|
return { state: "none" };
|
|
3556
3537
|
}
|
|
3557
3538
|
if (![
|
|
@@ -3576,9 +3557,8 @@ function terminalDispatchOwnership(terminalControl) {
|
|
|
3576
3557
|
if (TERMINAL_DISPATCH_RELEASE_STATUSES.has(owner.status)) {
|
|
3577
3558
|
return { state: "none" };
|
|
3578
3559
|
}
|
|
3579
|
-
const
|
|
3580
|
-
|
|
3581
|
-
if (!ownerTerminalKey || ownerTerminalKey !== currentTerminalKey) {
|
|
3560
|
+
const ownerTerminalControl = terminalControlForManagedConversation(owner);
|
|
3561
|
+
if (!terminalControlsShareIncarnation(ownerTerminalControl, terminalControl)) {
|
|
3582
3562
|
return {
|
|
3583
3563
|
state: "conflict",
|
|
3584
3564
|
conflict: terminalDispatchConflict(ledger, "dispatch owner does not reference this tmux pane incarnation")
|
|
@@ -3751,9 +3731,8 @@ function managedTurnMatchesLiveTerminal(conversation, terminal) {
|
|
|
3751
3731
|
!Number.isSafeInteger(livePid) ||
|
|
3752
3732
|
livePid <= 1 ||
|
|
3753
3733
|
storedPid !== livePid ||
|
|
3754
|
-
stringValue(takeover?.native_session_id)
|
|
3755
|
-
|
|
3756
|
-
terminalControlSelectorKey(liveControl)) {
|
|
3734
|
+
!terminalControlAliasMatches(stringValue(takeover?.native_session_id), storedControl, stringValue(terminal.id), liveControl) ||
|
|
3735
|
+
!terminalControlsShareIncarnation(storedControl, liveControl)) {
|
|
3757
3736
|
return false;
|
|
3758
3737
|
}
|
|
3759
3738
|
const liveSessionId = stringValue(terminal.native_agent_session_id);
|
|
@@ -3898,18 +3877,25 @@ async function listStateForTerminal(agent, terminalControl, options, bridge = cr
|
|
|
3898
3877
|
function rootActiveProcesses(processes) {
|
|
3899
3878
|
const pids = new Set(processes.map((process) => `${process.agent}:${process.pid}`));
|
|
3900
3879
|
const roots = processes.filter((process) => !process.ppid || !pids.has(`${process.agent}:${process.ppid}`));
|
|
3901
|
-
const
|
|
3880
|
+
const seenTerminalIncarnations = new Set();
|
|
3902
3881
|
return roots.filter((process) => {
|
|
3903
|
-
const
|
|
3904
|
-
?
|
|
3882
|
+
const endpoint = process.terminalControl
|
|
3883
|
+
? terminalEndpointFromControlRef(process.terminalControl)
|
|
3905
3884
|
: undefined;
|
|
3906
|
-
|
|
3885
|
+
const terminalIncarnation = endpoint
|
|
3886
|
+
? JSON.stringify({
|
|
3887
|
+
agent: process.agent,
|
|
3888
|
+
identity: terminalEndpointIdentityKey(endpoint),
|
|
3889
|
+
process_anchor_pid: endpoint.processAnchorPid ?? null
|
|
3890
|
+
})
|
|
3891
|
+
: undefined;
|
|
3892
|
+
if (!terminalIncarnation) {
|
|
3907
3893
|
return true;
|
|
3908
3894
|
}
|
|
3909
|
-
if (
|
|
3895
|
+
if (seenTerminalIncarnations.has(terminalIncarnation)) {
|
|
3910
3896
|
return false;
|
|
3911
3897
|
}
|
|
3912
|
-
|
|
3898
|
+
seenTerminalIncarnations.add(terminalIncarnation);
|
|
3913
3899
|
return true;
|
|
3914
3900
|
});
|
|
3915
3901
|
}
|
|
@@ -4530,17 +4516,43 @@ function terminalControlSelectorKey(value) {
|
|
|
4530
4516
|
if (!isRecord(value)) {
|
|
4531
4517
|
return undefined;
|
|
4532
4518
|
}
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
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) {
|
|
4536
4528
|
return undefined;
|
|
4537
4529
|
}
|
|
4538
4530
|
return JSON.stringify({
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
socket_path: stringValue(value.socketPath) ?? null
|
|
4531
|
+
identity: terminalEndpointIdentityKey(endpoint),
|
|
4532
|
+
process_anchor_pid: processAnchorPid
|
|
4542
4533
|
});
|
|
4543
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
|
+
}
|
|
4544
4556
|
function sessionEntryRecency(entry, observedAtMs) {
|
|
4545
4557
|
const timestamp = Date.parse(String(entry.updated_at ?? entry.created_at ?? ""));
|
|
4546
4558
|
if (Number.isFinite(timestamp)) {
|
|
@@ -4834,14 +4846,12 @@ function codexAllowedCompanionSetForManagedSession({ storeDir, session }) {
|
|
|
4834
4846
|
candidate.agent !== "codex" ||
|
|
4835
4847
|
candidate.status !== "detached" ||
|
|
4836
4848
|
!candidateBinding?.native_thread_id ||
|
|
4837
|
-
candidateBinding.terminal_id !== binding.terminal_id ||
|
|
4838
4849
|
candidateBinding.native_process.pid !== binding.native_process.pid ||
|
|
4839
4850
|
candidateBinding.native_process.process_uuid !==
|
|
4840
4851
|
binding.native_process.process_uuid ||
|
|
4841
4852
|
candidateBinding.native_process.process_birth !==
|
|
4842
4853
|
binding.native_process.process_birth ||
|
|
4843
|
-
|
|
4844
|
-
terminalControlSelectorKey(binding.terminal_control) ||
|
|
4854
|
+
!terminalControlAliasMatches(candidateBinding.terminal_id, candidateBinding.terminal_control, binding.terminal_id, binding.terminal_control) ||
|
|
4845
4855
|
path.resolve(candidate.workspace) !== path.resolve(session.workspace) ||
|
|
4846
4856
|
!candidateBinding.native_process.process_uuid ||
|
|
4847
4857
|
!candidateBinding.native_process.process_birth ||
|
|
@@ -4929,8 +4939,8 @@ function codexKnownRootSetForLifecycleTransition({ storeDir, terminal, transitio
|
|
|
4929
4939
|
if (after?.native_thread_id &&
|
|
4930
4940
|
after.native_process.process_uuid &&
|
|
4931
4941
|
after.native_process.process_birth &&
|
|
4932
|
-
after.terminal_id === terminal.conversationId &&
|
|
4933
4942
|
after.native_process.pid === terminal.pid &&
|
|
4943
|
+
terminalControlAliasMatches(after.terminal_id, after.terminal_control, terminal.conversationId, terminal.terminalControl) &&
|
|
4934
4944
|
after.native_process.process_uuid === transition.before_process_uuid &&
|
|
4935
4945
|
after.native_process.process_birth === transition.before_process_birth &&
|
|
4936
4946
|
isCompleteNativeRollout(after.native_process.rollout)) {
|
|
@@ -4948,12 +4958,10 @@ function codexKnownRootSetForLifecycleTransition({ storeDir, terminal, transitio
|
|
|
4948
4958
|
!binding?.native_thread_id ||
|
|
4949
4959
|
!binding.native_process.process_uuid ||
|
|
4950
4960
|
!binding.native_process.process_birth ||
|
|
4951
|
-
binding.terminal_id !== terminal.conversationId ||
|
|
4952
4961
|
binding.native_process.pid !== terminal.pid ||
|
|
4953
4962
|
binding.native_process.process_uuid !== transition.before_process_uuid ||
|
|
4954
4963
|
binding.native_process.process_birth !== transition.before_process_birth ||
|
|
4955
|
-
|
|
4956
|
-
terminalControlSelectorKey(terminal.terminalControl) ||
|
|
4964
|
+
!terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminal.conversationId, terminal.terminalControl) ||
|
|
4957
4965
|
!matchesConfiguredWorkspace(transition.workspace, session.workspace) ||
|
|
4958
4966
|
!isCompleteNativeRollout(binding.native_process.rollout)) {
|
|
4959
4967
|
continue;
|
|
@@ -5016,11 +5024,9 @@ function nativeIdentityMatchesCodexPreMaterialization(identity, expected) {
|
|
|
5016
5024
|
identity.rollout?.path === expected.rollout.path);
|
|
5017
5025
|
}
|
|
5018
5026
|
async function assertCodexComposerReadyForAutomatedInput({ options, terminalControl }) {
|
|
5019
|
-
const
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
preserveEscapes: true
|
|
5023
|
-
});
|
|
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 });
|
|
5024
5030
|
if (!codexStyledComposerEmpty(styledScreen)) {
|
|
5025
5031
|
throw new Error("Codex composer contains non-placeholder input; refusing automated terminal input");
|
|
5026
5032
|
}
|
|
@@ -5108,10 +5114,8 @@ function bindingMatchesLiveTerminal(session, terminal, identity, storeDir) {
|
|
|
5108
5114
|
return false;
|
|
5109
5115
|
}
|
|
5110
5116
|
if (session.agent !== terminal.agent ||
|
|
5111
|
-
binding.terminal_id !== terminal.conversationId ||
|
|
5112
5117
|
binding.native_process.pid !== terminal.pid ||
|
|
5113
|
-
|
|
5114
|
-
terminalControlSelectorKey(terminal.terminalControl) ||
|
|
5118
|
+
!terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminal.conversationId, terminal.terminalControl) ||
|
|
5115
5119
|
!matchesConfiguredWorkspace(session.workspace, terminal.terminalControl.currentPath)) {
|
|
5116
5120
|
return false;
|
|
5117
5121
|
}
|
|
@@ -5153,9 +5157,7 @@ function boundManagedSessionForTerminal({ storeDir, terminal, identity }) {
|
|
|
5153
5157
|
const unresolved = sessions.filter((session) => ["transitioning", "quarantined"].includes(session.status) &&
|
|
5154
5158
|
session.binding &&
|
|
5155
5159
|
session.agent === terminal.agent &&
|
|
5156
|
-
session.binding.terminal_id
|
|
5157
|
-
terminalControlSelectorKey(session.binding.terminal_control) ===
|
|
5158
|
-
terminalControlSelectorKey(terminal.terminalControl));
|
|
5160
|
+
terminalControlAliasMatches(session.binding.terminal_id, session.binding.terminal_control, terminal.conversationId, terminal.terminalControl));
|
|
5159
5161
|
if (unresolved.length > 0) {
|
|
5160
5162
|
throw new Error(`terminal ${terminal.terminalControl.target} is still claimed by ` +
|
|
5161
5163
|
`${unresolved[0].status} managed Session ${unresolved[0].session_id}` +
|
|
@@ -5171,8 +5173,7 @@ function boundManagedSessionForTerminal({ storeDir, terminal, identity }) {
|
|
|
5171
5173
|
const conflicting = sessions.filter((session) => session.status === "bound" &&
|
|
5172
5174
|
session.binding &&
|
|
5173
5175
|
session.agent === terminal.agent &&
|
|
5174
|
-
|
|
5175
|
-
terminalControlSelectorKey(terminal.terminalControl) &&
|
|
5176
|
+
terminalControlsShareIncarnation(session.binding.terminal_control, terminal.terminalControl) &&
|
|
5176
5177
|
session.binding.native_process.pid === terminal.pid &&
|
|
5177
5178
|
!managedSessionOwnerIsConclusivelyInactive({
|
|
5178
5179
|
session,
|
|
@@ -5191,10 +5192,8 @@ function managedBindingConflictKindForResolvedTerminal({ storeDir, session, term
|
|
|
5191
5192
|
if (session.status !== "bound" ||
|
|
5192
5193
|
!binding ||
|
|
5193
5194
|
session.agent !== terminal.agent ||
|
|
5194
|
-
binding.terminal_id !== terminal.conversationId ||
|
|
5195
5195
|
binding.native_process.pid !== terminal.pid ||
|
|
5196
|
-
|
|
5197
|
-
terminalControlSelectorKey(terminal.terminalControl) ||
|
|
5196
|
+
!terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminal.conversationId, terminal.terminalControl) ||
|
|
5198
5197
|
!matchesConfiguredWorkspace(session.workspace, terminal.terminalControl.currentPath) ||
|
|
5199
5198
|
bindingMatchesLiveTerminal(session, terminal, identity, storeDir)) {
|
|
5200
5199
|
return undefined;
|
|
@@ -5233,10 +5232,8 @@ function soleBoundManagedSessionClaimForTerminal(storeDir, terminal) {
|
|
|
5233
5232
|
const claims = listManagedSessions(storeDir).filter((session) => session.status === "bound" &&
|
|
5234
5233
|
session.binding &&
|
|
5235
5234
|
session.agent === terminal.agent &&
|
|
5236
|
-
session.binding.terminal_id === terminal.conversationId &&
|
|
5237
5235
|
session.binding.native_process.pid === terminal.pid &&
|
|
5238
|
-
|
|
5239
|
-
terminalControlSelectorKey(terminal.terminalControl) &&
|
|
5236
|
+
terminalControlAliasMatches(session.binding.terminal_id, session.binding.terminal_control, terminal.conversationId, terminal.terminalControl) &&
|
|
5240
5237
|
matchesConfiguredWorkspace(session.workspace, terminal.terminalControl.currentPath) &&
|
|
5241
5238
|
!managedSessionOwnerIsConclusivelyInactive({
|
|
5242
5239
|
session,
|
|
@@ -5390,6 +5387,32 @@ function lifecycleBindingToken({ session, terminal, identity }) {
|
|
|
5390
5387
|
rollout: identity?.rollout
|
|
5391
5388
|
});
|
|
5392
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
|
+
}
|
|
5393
5416
|
function agentVersionForRunningProcess(agent, pid, options) {
|
|
5394
5417
|
const fixture = options.agentVersionsJson
|
|
5395
5418
|
? parseJsonOption(options.agentVersionsJson, "--agent-versions-json")
|
|
@@ -5430,8 +5453,7 @@ function assertTerminalLifecycleReady({ options, terminal, terminalStatus }) {
|
|
|
5430
5453
|
}
|
|
5431
5454
|
const blockers = listConversations(storeDirFromOptions(options))
|
|
5432
5455
|
.filter(isDiscoverableTmuxConversation)
|
|
5433
|
-
.filter((turn) =>
|
|
5434
|
-
terminalControlSelectorKey(terminal.terminalControl) &&
|
|
5456
|
+
.filter((turn) => terminalControlsShareIncarnation(terminalControlForManagedConversation(turn), terminal.terminalControl) &&
|
|
5435
5457
|
SESSION_SEND_BLOCKING_STATUSES.has(turn.status));
|
|
5436
5458
|
if (blockers.length > 0) {
|
|
5437
5459
|
throw new Error(`terminal ${terminal.terminalControl.target} still has unresolved Turn ` +
|
|
@@ -5633,10 +5655,8 @@ async function assertLifecycleTargetHasExclusiveOwnership({ options, terminal, t
|
|
|
5633
5655
|
if (!afterBinding ||
|
|
5634
5656
|
!nativeThreadId ||
|
|
5635
5657
|
!isExactNativeThreadId(nativeThreadId) ||
|
|
5636
|
-
afterBinding.terminal_id !== terminal.conversationId ||
|
|
5637
5658
|
afterBinding.native_process.pid !== terminal.pid ||
|
|
5638
|
-
|
|
5639
|
-
terminalControlSelectorKey(terminal.terminalControl)) {
|
|
5659
|
+
!terminalControlAliasMatches(afterBinding.terminal_id, afterBinding.terminal_control, terminal.conversationId, terminal.terminalControl)) {
|
|
5640
5660
|
throw new Error("cannot exclude the current lifecycle process without an exact after_binding");
|
|
5641
5661
|
}
|
|
5642
5662
|
await assertNativeThreadHasExclusiveOwnership({
|
|
@@ -5929,7 +5949,7 @@ async function currentLifecycleSnapshot(options, terminal, { materialize = false
|
|
|
5929
5949
|
allowedCompanionIdentity: claimedCodexCompanions.primary,
|
|
5930
5950
|
allowedAdditionalIdentities: claimedCodexCompanions.additional
|
|
5931
5951
|
});
|
|
5932
|
-
|
|
5952
|
+
let session = materialize
|
|
5933
5953
|
? materializeCurrentManagedSession({
|
|
5934
5954
|
options,
|
|
5935
5955
|
terminal,
|
|
@@ -5940,13 +5960,26 @@ async function currentLifecycleSnapshot(options, terminal, { materialize = false
|
|
|
5940
5960
|
terminal,
|
|
5941
5961
|
identity: observedIdentity
|
|
5942
5962
|
});
|
|
5943
|
-
|
|
5963
|
+
let identity = session
|
|
5944
5964
|
? logicalIdentityForManagedSession({
|
|
5945
5965
|
storeDir,
|
|
5946
5966
|
session,
|
|
5947
5967
|
observedIdentity
|
|
5948
5968
|
})
|
|
5949
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
|
+
}
|
|
5950
5983
|
const codexCompanions = terminal.agent === "codex" && session
|
|
5951
5984
|
? codexAllowedCompanionSetForManagedSession({ storeDir, session })
|
|
5952
5985
|
: claimedCodexCompanions;
|
|
@@ -5971,6 +6004,7 @@ async function currentLifecycleSnapshot(options, terminal, { materialize = false
|
|
|
5971
6004
|
resumeExact: false,
|
|
5972
6005
|
reason: `${adapter.displayName} has no native-thread lifecycle adapter`
|
|
5973
6006
|
};
|
|
6007
|
+
const bindingTokens = lifecycleBindingTokens({ session, terminal, identity });
|
|
5974
6008
|
return {
|
|
5975
6009
|
identity,
|
|
5976
6010
|
runtimeIdentity: observedIdentity,
|
|
@@ -5979,7 +6013,8 @@ async function currentLifecycleSnapshot(options, terminal, { materialize = false
|
|
|
5979
6013
|
version,
|
|
5980
6014
|
adapter,
|
|
5981
6015
|
capabilities,
|
|
5982
|
-
bindingToken:
|
|
6016
|
+
bindingToken: bindingTokens[0],
|
|
6017
|
+
bindingTokens
|
|
5983
6018
|
};
|
|
5984
6019
|
}
|
|
5985
6020
|
async function runListResumableThreads(options) {
|
|
@@ -6084,11 +6119,9 @@ async function runListResumableThreads(options) {
|
|
|
6084
6119
|
function assertSameNativeInspectionTerminal(expected, actual, stage) {
|
|
6085
6120
|
const expectedPath = expected.terminalControl.currentPath;
|
|
6086
6121
|
const actualPath = actual.terminalControl.currentPath;
|
|
6087
|
-
if (actual.
|
|
6088
|
-
actual.agent !== expected.agent ||
|
|
6122
|
+
if (actual.agent !== expected.agent ||
|
|
6089
6123
|
actual.pid !== expected.pid ||
|
|
6090
|
-
|
|
6091
|
-
terminalControlSelectorKey(expected.terminalControl) ||
|
|
6124
|
+
!terminalControlAliasMatches(expected.conversationId, expected.terminalControl, actual.conversationId, actual.terminalControl) ||
|
|
6092
6125
|
!expectedPath ||
|
|
6093
6126
|
!actualPath ||
|
|
6094
6127
|
path.resolve(actualPath) !== path.resolve(expectedPath)) {
|
|
@@ -6105,8 +6138,7 @@ function assertTerminalNativeInspectionReady({ options, terminal, terminalStatus
|
|
|
6105
6138
|
}
|
|
6106
6139
|
const blocker = listConversations(storeDirFromOptions(options))
|
|
6107
6140
|
.filter(isDiscoverableTmuxConversation)
|
|
6108
|
-
.find((turn) =>
|
|
6109
|
-
terminalControlSelectorKey(terminal.terminalControl) &&
|
|
6141
|
+
.find((turn) => terminalControlsShareIncarnation(terminalControlForManagedConversation(turn), terminal.terminalControl) &&
|
|
6110
6142
|
SESSION_SEND_BLOCKING_STATUSES.has(turn.status));
|
|
6111
6143
|
if (blocker) {
|
|
6112
6144
|
throw new Error(`terminal ${terminal.terminalControl.target} still has unresolved Turn ` +
|
|
@@ -6170,7 +6202,7 @@ function nativeInspectionRuntime({ terminal, snapshot }) {
|
|
|
6170
6202
|
}
|
|
6171
6203
|
function assertNativeInspectionSnapshotUnchanged({ options, expectedTerminal, actualTerminal, expectedBindingToken, expectedVersion, actualSnapshot, stage, expectedClaudeState = "idle" }) {
|
|
6172
6204
|
assertSameNativeInspectionTerminal(expectedTerminal, actualTerminal, stage);
|
|
6173
|
-
if (actualSnapshot.
|
|
6205
|
+
if (!actualSnapshot.bindingTokens.includes(expectedBindingToken)) {
|
|
6174
6206
|
throw new Error(`terminal binding changed ${stage}; refresh AKK list`);
|
|
6175
6207
|
}
|
|
6176
6208
|
if (actualSnapshot.version !== expectedVersion) {
|
|
@@ -6261,12 +6293,13 @@ async function runNativeInspect(options) {
|
|
|
6261
6293
|
throw new Error("native inspection requires a compatible AKK Store so binding authority can be verified");
|
|
6262
6294
|
}
|
|
6263
6295
|
const initiallyResolved = await resolveLifecycleTerminal(options);
|
|
6264
|
-
const
|
|
6296
|
+
const bridge = createTerminalAgentBridge(options);
|
|
6297
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDir, initiallyResolved.terminalControl, { timeoutMs: 30000 });
|
|
6265
6298
|
try {
|
|
6266
|
-
const terminal = await
|
|
6299
|
+
const terminal = await bridge.resolveStoredTerminal(initiallyResolved.agent, initiallyResolved.pid, initiallyResolved.terminalControl, { pid: initiallyResolved.pid });
|
|
6267
6300
|
assertSameNativeInspectionTerminal(initiallyResolved, terminal, "while waiting for native-inspection control");
|
|
6268
6301
|
const snapshot = await currentLifecycleSnapshot(options, terminal);
|
|
6269
|
-
if (snapshot.
|
|
6302
|
+
if (!snapshot.bindingTokens.includes(expectedBindingToken)) {
|
|
6270
6303
|
throw new Error("terminal binding changed after it was listed; refresh AKK list and retry");
|
|
6271
6304
|
}
|
|
6272
6305
|
const capability = snapshot.adapter.probeNativeInspection?.(snapshot.version);
|
|
@@ -6294,7 +6327,6 @@ async function runNativeInspect(options) {
|
|
|
6294
6327
|
snapshot
|
|
6295
6328
|
});
|
|
6296
6329
|
const runtime = nativeInspectionRuntime({ terminal, snapshot });
|
|
6297
|
-
const bridge = createTerminalAgentBridge(options);
|
|
6298
6330
|
const initialStatus = await bridge.status(terminal.agent, terminal.terminalControl, { runtime });
|
|
6299
6331
|
assertTerminalNativeInspectionReady({
|
|
6300
6332
|
options,
|
|
@@ -6311,7 +6343,7 @@ async function runNativeInspect(options) {
|
|
|
6311
6343
|
submission = await bridge.submitNativeInspection(terminal.agent, terminal.terminalControl, plan, {
|
|
6312
6344
|
runtime,
|
|
6313
6345
|
beforeEnter: async () => {
|
|
6314
|
-
const finalTerminal = await
|
|
6346
|
+
const finalTerminal = await bridge.resolveStoredTerminal(terminal.agent, terminal.pid, terminal.terminalControl, runtime);
|
|
6315
6347
|
const finalSnapshot = await currentLifecycleSnapshot(options, finalTerminal);
|
|
6316
6348
|
assertNativeInspectionSnapshotUnchanged({
|
|
6317
6349
|
options,
|
|
@@ -6410,7 +6442,7 @@ async function runNativeInspect(options) {
|
|
|
6410
6442
|
runtime: postEnterRuntime,
|
|
6411
6443
|
scrollbackLines: 240,
|
|
6412
6444
|
beforeDismiss: async () => {
|
|
6413
|
-
const dismissTerminal = await
|
|
6445
|
+
const dismissTerminal = await bridge.resolveStoredTerminal(terminal.agent, terminal.pid, terminal.terminalControl, postEnterRuntime);
|
|
6414
6446
|
const dismissSnapshot = await currentLifecycleSnapshot(options, dismissTerminal);
|
|
6415
6447
|
assertNativeInspectionSnapshotUnchanged({
|
|
6416
6448
|
options,
|
|
@@ -6456,7 +6488,7 @@ async function runNativeInspect(options) {
|
|
|
6456
6488
|
throw new Error("Claude Status panel dismissal was dispatched exactly once, but the original idle composer was not restored; do not retry automatically");
|
|
6457
6489
|
}
|
|
6458
6490
|
}
|
|
6459
|
-
const finalTerminal = await
|
|
6491
|
+
const finalTerminal = await bridge.resolveStoredTerminal(terminal.agent, terminal.pid, terminal.terminalControl, runtime);
|
|
6460
6492
|
const finalSnapshot = await currentLifecycleSnapshot(options, finalTerminal);
|
|
6461
6493
|
assertNativeInspectionSnapshotUnchanged({
|
|
6462
6494
|
options,
|
|
@@ -6895,14 +6927,13 @@ async function runReconcileBinding(options) {
|
|
|
6895
6927
|
}
|
|
6896
6928
|
const expectedBindingToken = required(stringValue(options.expectedBindingToken), "--expected-binding-token is required");
|
|
6897
6929
|
const expectedTerminalToken = required(stringValue(options.expectedTerminalToken), "--expected-terminal-token is required");
|
|
6898
|
-
const releaseTerminalLock =
|
|
6930
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDir, initiallyResolved.terminalControl, { timeoutMs: 30000 });
|
|
6899
6931
|
try {
|
|
6900
6932
|
return await withStoreWriterLeaseAsync(storeDir, async () => {
|
|
6901
6933
|
const terminal = await resolveLifecycleTerminal(options);
|
|
6902
6934
|
if (terminal.pid !== initiallyResolved.pid ||
|
|
6903
6935
|
terminal.conversationId !== initiallyResolved.conversationId ||
|
|
6904
|
-
|
|
6905
|
-
terminalControlSelectorKey(initiallyResolved.terminalControl)) {
|
|
6936
|
+
!terminalControlsShareIncarnation(terminal.terminalControl, initiallyResolved.terminalControl)) {
|
|
6906
6937
|
throw new Error("terminal identity changed while waiting to reconcile its binding; refresh AKK list");
|
|
6907
6938
|
}
|
|
6908
6939
|
await recoverLifecycleFenceBeforeMutation({ options, terminal });
|
|
@@ -6912,17 +6943,15 @@ async function runReconcileBinding(options) {
|
|
|
6912
6943
|
}
|
|
6913
6944
|
const session = loadManagedSession(storeDir, conflictingSessionId);
|
|
6914
6945
|
if (session.revision !== expectedSessionRevision ||
|
|
6915
|
-
|
|
6946
|
+
!managedSessionBindingTokens(session).includes(expectedBindingToken)) {
|
|
6916
6947
|
throw new Error("managed Session binding changed after it was listed; refresh AKK list");
|
|
6917
6948
|
}
|
|
6918
6949
|
const binding = session.binding;
|
|
6919
6950
|
if (session.status !== "bound" ||
|
|
6920
6951
|
!binding ||
|
|
6921
6952
|
session.agent !== terminal.agent ||
|
|
6922
|
-
binding.terminal_id !== terminal.conversationId ||
|
|
6923
6953
|
binding.native_process.pid !== terminal.pid ||
|
|
6924
|
-
|
|
6925
|
-
terminalControlSelectorKey(terminal.terminalControl) ||
|
|
6954
|
+
!terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminal.conversationId, terminal.terminalControl) ||
|
|
6926
6955
|
!matchesConfiguredWorkspace(session.workspace, terminal.terminalControl.currentPath)) {
|
|
6927
6956
|
throw new Error("the listed managed Session no longer claims this exact terminal");
|
|
6928
6957
|
}
|
|
@@ -6932,11 +6961,11 @@ async function runReconcileBinding(options) {
|
|
|
6932
6961
|
pid: terminal.pid,
|
|
6933
6962
|
cwd: terminal.terminalControl.currentPath
|
|
6934
6963
|
});
|
|
6935
|
-
const
|
|
6964
|
+
const terminalTokens = lifecycleBindingTokens({
|
|
6936
6965
|
terminal,
|
|
6937
6966
|
identity
|
|
6938
6967
|
});
|
|
6939
|
-
if (
|
|
6968
|
+
if (!terminalTokens.includes(expectedTerminalToken)) {
|
|
6940
6969
|
throw new Error("live terminal identity changed after the conflict was listed; refresh AKK list");
|
|
6941
6970
|
}
|
|
6942
6971
|
const bridge = createTerminalAgentBridge(options);
|
|
@@ -6951,8 +6980,7 @@ async function runReconcileBinding(options) {
|
|
|
6951
6980
|
const finalTerminal = await resolveLifecycleTerminal(options);
|
|
6952
6981
|
if (finalTerminal.pid !== terminal.pid ||
|
|
6953
6982
|
finalTerminal.conversationId !== terminal.conversationId ||
|
|
6954
|
-
|
|
6955
|
-
terminalControlSelectorKey(terminal.terminalControl)) {
|
|
6983
|
+
!terminalControlsShareIncarnation(finalTerminal.terminalControl, terminal.terminalControl)) {
|
|
6956
6984
|
throw new Error("terminal identity changed during binding reconciliation; refresh AKK list");
|
|
6957
6985
|
}
|
|
6958
6986
|
const finalStatus = await bridge.status(finalTerminal.agent, finalTerminal.terminalControl, {
|
|
@@ -6972,15 +7000,15 @@ async function runReconcileBinding(options) {
|
|
|
6972
7000
|
pid: finalTerminal.pid,
|
|
6973
7001
|
cwd: finalTerminal.terminalControl.currentPath
|
|
6974
7002
|
});
|
|
6975
|
-
if (
|
|
7003
|
+
if (!lifecycleBindingTokens({
|
|
6976
7004
|
terminal: finalTerminal,
|
|
6977
7005
|
identity: finalIdentity
|
|
6978
|
-
})
|
|
7006
|
+
}).includes(expectedTerminalToken)) {
|
|
6979
7007
|
throw new Error("live terminal identity changed during binding reconciliation; refresh AKK list");
|
|
6980
7008
|
}
|
|
6981
7009
|
const finalSession = loadManagedSession(storeDir, conflictingSessionId);
|
|
6982
7010
|
if (finalSession.revision !== expectedSessionRevision ||
|
|
6983
|
-
|
|
7011
|
+
!managedSessionBindingTokens(finalSession).includes(expectedBindingToken)) {
|
|
6984
7012
|
throw new Error("managed Session binding changed during reconciliation; refresh AKK list");
|
|
6985
7013
|
}
|
|
6986
7014
|
const conflictKind = managedBindingConflictKindForResolvedTerminal({
|
|
@@ -7038,12 +7066,14 @@ async function runReconcileBinding(options) {
|
|
|
7038
7066
|
}
|
|
7039
7067
|
function assertResumeSnapshotMatchesTerminal(snapshot, terminal) {
|
|
7040
7068
|
const workspace = path.resolve(terminal.terminalControl.currentPath ?? process.cwd());
|
|
7041
|
-
|
|
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) ||
|
|
7042
7074
|
snapshot.agent !== terminal.agent ||
|
|
7043
7075
|
path.resolve(snapshot.workspace) !== workspace ||
|
|
7044
|
-
|
|
7045
|
-
snapshot.terminal_control.socket_path !== terminal.terminalControl.socketPath ||
|
|
7046
|
-
snapshot.terminal_control.pane_pid !== terminal.terminalControl.panePid) {
|
|
7076
|
+
!terminalMatches) {
|
|
7047
7077
|
throw new Error("resume selection terminal, process, or workspace changed; run /akk threads again");
|
|
7048
7078
|
}
|
|
7049
7079
|
}
|
|
@@ -7070,13 +7100,12 @@ function assertResumeSnapshotNotExpired(snapshot) {
|
|
|
7070
7100
|
async function runNativeThreadTransition(options, operation) {
|
|
7071
7101
|
const initiallyResolved = await resolveLifecycleTerminal(options);
|
|
7072
7102
|
const storeDir = storeDirFromOptions(options);
|
|
7073
|
-
const releaseTerminalLock =
|
|
7103
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDir, initiallyResolved.terminalControl, { timeoutMs: 30000 });
|
|
7074
7104
|
try {
|
|
7075
7105
|
return await withStoreWriterLeaseAsync(storeDir, async () => {
|
|
7076
7106
|
const terminal = await resolveLifecycleTerminal(options);
|
|
7077
7107
|
if (terminal.pid !== initiallyResolved.pid ||
|
|
7078
|
-
|
|
7079
|
-
terminalControlSelectorKey(initiallyResolved.terminalControl)) {
|
|
7108
|
+
!terminalControlsShareIncarnation(terminal.terminalControl, initiallyResolved.terminalControl)) {
|
|
7080
7109
|
throw new Error("terminal identity changed while waiting for lifecycle control; refresh list");
|
|
7081
7110
|
}
|
|
7082
7111
|
if (operation.kind === "resume_thread" && operation.selectionSnapshot) {
|
|
@@ -7093,7 +7122,7 @@ async function runNativeThreadTransition(options, operation) {
|
|
|
7093
7122
|
}
|
|
7094
7123
|
const snapshot = await currentLifecycleSnapshot(options, terminal, { materialize: true });
|
|
7095
7124
|
const expectedToken = required(stringValue(options.expectedBindingToken), "--expected-binding-token is required");
|
|
7096
|
-
if (
|
|
7125
|
+
if (!snapshot.bindingTokens.includes(expectedToken)) {
|
|
7097
7126
|
throw new Error("terminal binding changed after it was listed; refresh AKK list and retry");
|
|
7098
7127
|
}
|
|
7099
7128
|
if (snapshot.capabilities.status !== "supported") {
|
|
@@ -7439,16 +7468,27 @@ async function runNativeThreadTransition(options, operation) {
|
|
|
7439
7468
|
// Candidate discovery and ownership revalidation can take long
|
|
7440
7469
|
// enough for a human to start typing after the /status probe. Keep
|
|
7441
7470
|
// this check immediately adjacent to the terminal mutation.
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
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
|
+
}
|
|
7446
7480
|
}
|
|
7447
7481
|
if (operation.kind === "resume_thread" && operation.selectionSnapshot) {
|
|
7448
7482
|
assertResumeSnapshotNotExpired(operation.selectionSnapshot);
|
|
7449
7483
|
}
|
|
7450
|
-
|
|
7451
|
-
|
|
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
|
+
});
|
|
7452
7492
|
const submittedAt = new Date().toISOString();
|
|
7453
7493
|
transition = {
|
|
7454
7494
|
...transition,
|
|
@@ -7637,7 +7677,8 @@ async function runNativeThreadTransition(options, operation) {
|
|
|
7637
7677
|
});
|
|
7638
7678
|
return;
|
|
7639
7679
|
}
|
|
7640
|
-
if (!inputStarted
|
|
7680
|
+
if (!inputStarted &&
|
|
7681
|
+
error instanceof TerminalInputNotStartedError) {
|
|
7641
7682
|
transition = {
|
|
7642
7683
|
...durableTransition,
|
|
7643
7684
|
status: "aborted",
|
|
@@ -7886,10 +7927,16 @@ function terminalBridgeApprovalFingerprint({ terminalControl, terminalStatus })
|
|
|
7886
7927
|
if (adapterFingerprint) {
|
|
7887
7928
|
return adapterFingerprint;
|
|
7888
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
|
+
}
|
|
7889
7935
|
const screen = isRecord(terminalStatus?.screen) ? terminalStatus.screen : {};
|
|
7890
7936
|
return createHash("sha256")
|
|
7891
7937
|
.update(JSON.stringify({
|
|
7892
|
-
|
|
7938
|
+
terminal_identity: terminalEndpointIdentityKey(endpoint),
|
|
7939
|
+
process_anchor_pid: processAnchorPid,
|
|
7893
7940
|
keys: approval.keys ?? (approval.key ? [approval.key] : undefined),
|
|
7894
7941
|
label: approval.label,
|
|
7895
7942
|
prompt_kind: approval.prompt_kind,
|
|
@@ -8012,10 +8059,7 @@ function recordTerminalBridgeApprovalNotification({ statePath, logPath, terminal
|
|
|
8012
8059
|
stringValue(currentNativeTakeover.terminal_bridge_message_id) !==
|
|
8013
8060
|
expectedConversation.messageId ||
|
|
8014
8061
|
!currentTerminalControl ||
|
|
8015
|
-
currentTerminalControl
|
|
8016
|
-
currentTerminalControl.target !== terminalControl.target ||
|
|
8017
|
-
currentTerminalControl.socketPath !== terminalControl.socketPath ||
|
|
8018
|
-
currentTerminalControl.panePid !== terminalControl.panePid) {
|
|
8062
|
+
!terminalControlsShareIncarnation(currentTerminalControl, terminalControl)) {
|
|
8019
8063
|
return {
|
|
8020
8064
|
conversation,
|
|
8021
8065
|
duplicate: false,
|
|
@@ -8323,7 +8367,7 @@ async function runSend(options) {
|
|
|
8323
8367
|
throw new Error("raw tmux terminal sends require --background so AKK can persist and monitor the submission safely");
|
|
8324
8368
|
}
|
|
8325
8369
|
const rawStoreDir = storeDirFromOptions(options);
|
|
8326
|
-
const releaseTerminalLock =
|
|
8370
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(rawStoreDir, terminalConversation.terminalControl, { timeoutMs: 30000 });
|
|
8327
8371
|
try {
|
|
8328
8372
|
await withStoreWriterLeaseAsync(rawStoreDir, async () => {
|
|
8329
8373
|
await recoverLifecycleFenceBeforeMutation({
|
|
@@ -8541,12 +8585,23 @@ async function runSend(options) {
|
|
|
8541
8585
|
if (!rawTerminalId) {
|
|
8542
8586
|
throw new Error(`managed Session ${sessionId} has no authoritative terminal binding`);
|
|
8543
8587
|
}
|
|
8544
|
-
const
|
|
8545
|
-
|
|
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);
|
|
8546
8601
|
if (!resolvedTerminal) {
|
|
8547
8602
|
throw new Error(`session ${sessionId} is not attached to a live tmux terminal`);
|
|
8548
8603
|
}
|
|
8549
|
-
const releaseTerminalLock =
|
|
8604
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDir, resolvedTerminal.terminalControl, { timeoutMs: 30000 });
|
|
8550
8605
|
try {
|
|
8551
8606
|
await withStoreWriterLeaseAsync(storeDir, async () => {
|
|
8552
8607
|
await recoverLifecycleFenceBeforeMutation({
|
|
@@ -8714,18 +8769,19 @@ async function runTurnResponse({ options, messageBody }) {
|
|
|
8714
8769
|
if (!storedTerminalControl || !nativeTerminalId) {
|
|
8715
8770
|
throw new Error(`turn ${turnIdForConversation(initialConversation)} is not attached to a live tmux terminal`);
|
|
8716
8771
|
}
|
|
8717
|
-
const
|
|
8718
|
-
|
|
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;
|
|
8719
8777
|
if (!liveTerminal ||
|
|
8720
8778
|
liveTerminal.agent !== executorForConversation(initialConversation).kind ||
|
|
8721
|
-
liveTerminal.terminalControl
|
|
8722
|
-
liveTerminal.terminalControl.socketPath !== storedTerminalControl.socketPath ||
|
|
8723
|
-
liveTerminal.terminalControl.panePid !== storedTerminalControl.panePid) {
|
|
8779
|
+
!terminalControlsShareIncarnation(liveTerminal.terminalControl, storedTerminalControl)) {
|
|
8724
8780
|
throw new Error(`turn ${turnIdForConversation(initialConversation)} is not attached to its expected live tmux terminal`);
|
|
8725
8781
|
}
|
|
8726
8782
|
const terminalControl = liveTerminal.terminalControl;
|
|
8727
8783
|
const responseStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
8728
|
-
const releaseTerminalLock =
|
|
8784
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(responseStoreDir, terminalControl, { timeoutMs: 30000 });
|
|
8729
8785
|
try {
|
|
8730
8786
|
await withStoreWriterLeaseAsync(responseStoreDir, async () => {
|
|
8731
8787
|
// Legacy identity migration may write the Turn, so keep it inside the
|
|
@@ -8733,17 +8789,15 @@ async function runTurnResponse({ options, messageBody }) {
|
|
|
8733
8789
|
await migrateLegacyTerminalAgentIdentity({ ...loaded, options });
|
|
8734
8790
|
const releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
8735
8791
|
try {
|
|
8736
|
-
|
|
8737
|
-
|
|
8792
|
+
let lockedConversation = loadState(statePath);
|
|
8793
|
+
let lockedTakeover = isRecord(lockedConversation.native_session_takeover)
|
|
8738
8794
|
? lockedConversation.native_session_takeover
|
|
8739
8795
|
: undefined;
|
|
8740
8796
|
const lockedControl = terminalControlFromTakeover(lockedTakeover);
|
|
8741
8797
|
if (stringValue(lockedTakeover?.native_session_id) !== nativeTerminalId ||
|
|
8742
8798
|
executorForConversation(lockedConversation).kind !== liveTerminal.agent ||
|
|
8743
8799
|
!lockedControl ||
|
|
8744
|
-
lockedControl
|
|
8745
|
-
lockedControl.socketPath !== terminalControl.socketPath ||
|
|
8746
|
-
lockedControl.panePid !== terminalControl.panePid) {
|
|
8800
|
+
!terminalControlsShareIncarnation(lockedControl, terminalControl)) {
|
|
8747
8801
|
throw new Error("terminal control changed while waiting to respond; refresh status and retry");
|
|
8748
8802
|
}
|
|
8749
8803
|
if (requestedOpenClawSession &&
|
|
@@ -8751,6 +8805,14 @@ async function runTurnResponse({ options, messageBody }) {
|
|
|
8751
8805
|
throw new Error(`turn ${turnIdForConversation(lockedConversation)} belongs to a ` +
|
|
8752
8806
|
"different OpenClaw session; no terminal input was sent");
|
|
8753
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;
|
|
8754
8816
|
if (replayExactActiveTerminalSubmission({
|
|
8755
8817
|
options,
|
|
8756
8818
|
terminalControl,
|
|
@@ -8779,7 +8841,7 @@ async function runTurnResponse({ options, messageBody }) {
|
|
|
8779
8841
|
? prepared.conversation.native_session_takeover
|
|
8780
8842
|
: undefined;
|
|
8781
8843
|
const terminalAgentPid = Number(preparedTakeover?.terminal_agent_pid);
|
|
8782
|
-
|
|
8844
|
+
let responseManagedSession = tryLoadManagedSession(responseStoreDir, sessionIdForConversation(prepared.conversation));
|
|
8783
8845
|
const responseCodexCompanions = prepared.executor.kind === "codex" && responseManagedSession
|
|
8784
8846
|
? codexAllowedCompanionSetForManagedSession({
|
|
8785
8847
|
storeDir: responseStoreDir,
|
|
@@ -8802,11 +8864,22 @@ async function runTurnResponse({ options, messageBody }) {
|
|
|
8802
8864
|
currentIdentity: currentNativeIdentity,
|
|
8803
8865
|
operation: "respond to"
|
|
8804
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
|
+
}
|
|
8805
8880
|
const currentTerminalControl = terminalControlFromTakeover(prepared.nativeTakeoverForSend);
|
|
8806
8881
|
if (!currentTerminalControl ||
|
|
8807
|
-
currentTerminalControl
|
|
8808
|
-
currentTerminalControl.socketPath !== terminalControl.socketPath ||
|
|
8809
|
-
currentTerminalControl.panePid !== terminalControl.panePid) {
|
|
8882
|
+
!terminalControlsShareIncarnation(currentTerminalControl, terminalControl)) {
|
|
8810
8883
|
throw new Error("terminal control changed while waiting to respond; refresh status and retry");
|
|
8811
8884
|
}
|
|
8812
8885
|
const responseOptions = {
|
|
@@ -9010,7 +9083,7 @@ async function runApprove(options) {
|
|
|
9010
9083
|
: {})
|
|
9011
9084
|
};
|
|
9012
9085
|
};
|
|
9013
|
-
const releaseTerminalLock =
|
|
9086
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 });
|
|
9014
9087
|
let terminalLockReleased = false;
|
|
9015
9088
|
const releaseApprovalTerminalLock = () => {
|
|
9016
9089
|
if (!terminalLockReleased) {
|
|
@@ -9043,8 +9116,8 @@ async function runApprove(options) {
|
|
|
9043
9116
|
: undefined;
|
|
9044
9117
|
if (currentConversation.status !== conversation.status ||
|
|
9045
9118
|
currentTakeover?.terminal_bridge_message_id !== nativeTakeover?.terminal_bridge_message_id ||
|
|
9046
|
-
currentControl
|
|
9047
|
-
currentControl
|
|
9119
|
+
!currentControl ||
|
|
9120
|
+
!terminalControlsShareIncarnation(currentControl, terminalControl) ||
|
|
9048
9121
|
(claudeScreenApproval &&
|
|
9049
9122
|
currentApproval?.fingerprint !== monitoredApproval?.fingerprint)) {
|
|
9050
9123
|
throw new Error("approval state changed while waiting for terminal control; refresh status and retry");
|
|
@@ -9169,8 +9242,7 @@ async function runApprove(options) {
|
|
|
9169
9242
|
latestNotifiedAt === undefined ||
|
|
9170
9243
|
Date.now() - latestNotifiedAt > CLAUDE_SCREEN_APPROVAL_TTL_MS ||
|
|
9171
9244
|
expectedFingerprint !== fingerprint ||
|
|
9172
|
-
latestControl
|
|
9173
|
-
latestControl?.socketPath !== dispatchControl.socketPath ||
|
|
9245
|
+
!terminalControlsShareIncarnation(latestControl, dispatchControl) ||
|
|
9174
9246
|
(autoApproved &&
|
|
9175
9247
|
(latestPolicyEvidence?.source !== "claude_transcript" ||
|
|
9176
9248
|
latestPolicyEvidence.evidence_fingerprint !==
|
|
@@ -9364,7 +9436,7 @@ async function runApprove(options) {
|
|
|
9364
9436
|
}
|
|
9365
9437
|
}
|
|
9366
9438
|
async function runTerminalConversationApprove({ options, conversationId, agent, terminalControl, pid }) {
|
|
9367
|
-
const releaseTerminalLock =
|
|
9439
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 });
|
|
9368
9440
|
try {
|
|
9369
9441
|
if (agent === "claude") {
|
|
9370
9442
|
printJson({
|
|
@@ -9429,7 +9501,7 @@ async function runTerminalConversationApprove({ options, conversationId, agent,
|
|
|
9429
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 }) {
|
|
9430
9502
|
const bridge = terminalBridgeEnabled(conversation);
|
|
9431
9503
|
if (!terminalSendLockHeld) {
|
|
9432
|
-
const releaseTerminalLock =
|
|
9504
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 });
|
|
9433
9505
|
try {
|
|
9434
9506
|
return await runTerminalControlSend({
|
|
9435
9507
|
options,
|
|
@@ -9467,8 +9539,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
9467
9539
|
currentConversation.updated_at !== nextConversation.updated_at ||
|
|
9468
9540
|
currentConversation.status !== nextConversation.status ||
|
|
9469
9541
|
currentConversation.response_rounds_used !== nextConversation.response_rounds_used ||
|
|
9470
|
-
currentControl
|
|
9471
|
-
currentControl?.socketPath !== terminalControl.socketPath) {
|
|
9542
|
+
!terminalControlsShareIncarnation(currentControl, terminalControl)) {
|
|
9472
9543
|
throw new Error("conversation changed while waiting to send to the terminal; refresh status and retry");
|
|
9473
9544
|
}
|
|
9474
9545
|
return await runTerminalControlSend({
|
|
@@ -11491,9 +11562,8 @@ function migratedTerminalTurnMatchesSessionBinding({ conversation, takeover, ter
|
|
|
11491
11562
|
session.agent !== agent ||
|
|
11492
11563
|
path.resolve(session.workspace) !== path.resolve(conversation.workspace) ||
|
|
11493
11564
|
!terminalId ||
|
|
11494
|
-
binding.terminal_id
|
|
11495
|
-
|
|
11496
|
-
terminalControlSelectorKey(terminalControl) ||
|
|
11565
|
+
!terminalControlAliasMatches(terminalId, terminalControl, binding.terminal_id, binding.terminal_control) ||
|
|
11566
|
+
!terminalControlsShareIncarnation(binding.terminal_control, terminalControl) ||
|
|
11497
11567
|
!Number.isSafeInteger(pid) ||
|
|
11498
11568
|
binding.native_process.pid !== pid ||
|
|
11499
11569
|
!turnNativeThreadId ||
|
|
@@ -11578,24 +11648,28 @@ function nativeThreadTransitionRevision(transition) {
|
|
|
11578
11648
|
}
|
|
11579
11649
|
function refineManagedSessionNativeIdentity({ storeDir, session, terminalControl, identity }) {
|
|
11580
11650
|
const binding = session.binding;
|
|
11581
|
-
if (
|
|
11651
|
+
if (session.status !== "bound" || !binding) {
|
|
11582
11652
|
return session;
|
|
11583
11653
|
}
|
|
11584
|
-
if (
|
|
11585
|
-
|
|
11586
|
-
(binding.native_thread_id &&
|
|
11654
|
+
if (!terminalControlsShareIncarnation(binding.terminal_control, terminalControl) ||
|
|
11655
|
+
(identity && binding.native_thread_id &&
|
|
11587
11656
|
binding.native_thread_id !== identity.sessionId)) {
|
|
11588
11657
|
throw new Error(`managed Session ${session.session_id} changed native identity before send`);
|
|
11589
11658
|
}
|
|
11590
|
-
const refinedNativeThreadId = binding.native_thread_id ?? identity
|
|
11591
|
-
const refinedProcessUuid = binding.native_process.process_uuid ?? identity
|
|
11592
|
-
const refinedProcessBirth = binding.native_process.process_birth ?? identity
|
|
11593
|
-
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);
|
|
11594
11667
|
const changed = refinedNativeThreadId !== binding.native_thread_id ||
|
|
11595
11668
|
refinedProcessUuid !== binding.native_process.process_uuid ||
|
|
11596
11669
|
refinedProcessBirth !== binding.native_process.process_birth ||
|
|
11597
11670
|
JSON.stringify(refinedRollout) !==
|
|
11598
|
-
JSON.stringify(binding.native_process.rollout)
|
|
11671
|
+
JSON.stringify(binding.native_process.rollout) ||
|
|
11672
|
+
refinedTerminalEndpoint !== binding.terminal_endpoint;
|
|
11599
11673
|
if (!changed) {
|
|
11600
11674
|
return session;
|
|
11601
11675
|
}
|
|
@@ -11604,13 +11678,16 @@ function refineManagedSessionNativeIdentity({ storeDir, session, terminalControl
|
|
|
11604
11678
|
...session,
|
|
11605
11679
|
binding: {
|
|
11606
11680
|
...binding,
|
|
11681
|
+
...(refinedTerminalEndpoint
|
|
11682
|
+
? { terminal_endpoint: refinedTerminalEndpoint }
|
|
11683
|
+
: {}),
|
|
11607
11684
|
native_thread_id: refinedNativeThreadId,
|
|
11608
11685
|
native_process: {
|
|
11609
11686
|
...binding.native_process,
|
|
11610
11687
|
process_uuid: refinedProcessUuid,
|
|
11611
11688
|
process_birth: refinedProcessBirth,
|
|
11612
11689
|
rollout: refinedRollout,
|
|
11613
|
-
evidence: identity.evidence
|
|
11690
|
+
evidence: identity?.evidence ?? binding.native_process.evidence
|
|
11614
11691
|
},
|
|
11615
11692
|
last_verified_at: now
|
|
11616
11693
|
},
|
|
@@ -11633,8 +11710,7 @@ function persistManagedSessionNativeIdentity({ conversation, terminalControl, id
|
|
|
11633
11710
|
!current.binding ||
|
|
11634
11711
|
current.binding.binding_id !== bindingId ||
|
|
11635
11712
|
current.binding.generation !== bindingGeneration ||
|
|
11636
|
-
|
|
11637
|
-
terminalControlSelectorKey(terminalControl)) {
|
|
11713
|
+
!terminalControlsShareIncarnation(current.binding.terminal_control, terminalControl)) {
|
|
11638
11714
|
throw new Error(`managed Session ${sessionId} binding changed before native identity commit`);
|
|
11639
11715
|
}
|
|
11640
11716
|
const expectedNativeThreadId = current.binding.native_thread_id;
|
|
@@ -11782,12 +11858,10 @@ function managedTurnMatchesResolvedTerminal(conversation, terminalConversation,
|
|
|
11782
11858
|
currentTerminalIdentity &&
|
|
11783
11859
|
executorForConversation(conversation).kind === terminalConversation.agent &&
|
|
11784
11860
|
storedTerminalIdentity.agent === currentTerminalIdentity.agent &&
|
|
11785
|
-
storedTerminalIdentity.target === currentTerminalIdentity.target &&
|
|
11786
11861
|
storedTerminalIdentity.pid === currentTerminalIdentity.pid &&
|
|
11787
11862
|
Number(takeover?.terminal_agent_pid) === terminalConversation.pid &&
|
|
11788
11863
|
nativeAgentIdentityMatchesTurn(conversation, currentNativeIdentity) &&
|
|
11789
|
-
|
|
11790
|
-
terminalControlSelectorKey(terminalConversation.terminalControl) &&
|
|
11864
|
+
terminalControlsShareIncarnation(storedControl, terminalConversation.terminalControl) &&
|
|
11791
11865
|
matchesConfiguredWorkspace(conversation.workspace, terminalConversation.terminalControl.currentPath));
|
|
11792
11866
|
}
|
|
11793
11867
|
function createManagedTerminalTurn({ options, conversationId, agent, pid, messageBody, terminalControl, previousTurn, managedSession, nativeAgentIdentity }) {
|
|
@@ -11856,7 +11930,7 @@ function createManagedTerminalTurn({ options, conversationId, agent, pid, messag
|
|
|
11856
11930
|
takeover_match_kind: previousTurn
|
|
11857
11931
|
? "managed_session_send"
|
|
11858
11932
|
: "raw_terminal_send",
|
|
11859
|
-
|
|
11933
|
+
...terminalEndpointTakeoverFields(terminalControl),
|
|
11860
11934
|
needs_bootstrap: false,
|
|
11861
11935
|
terminal_bridge: true
|
|
11862
11936
|
}
|
|
@@ -11920,10 +11994,11 @@ async function runRenew(options) {
|
|
|
11920
11994
|
if (!terminalControl || nativeTakeover?.["terminal_bridge"] !== true) {
|
|
11921
11995
|
throw new Error(`cannot renew ${conversation.conversation_id}; conversation is not a terminal bridge task`);
|
|
11922
11996
|
}
|
|
11923
|
-
const
|
|
11924
|
-
|
|
11925
|
-
(
|
|
11926
|
-
|
|
11997
|
+
const terminalProvider = createTerminalControlProvider(options);
|
|
11998
|
+
try {
|
|
11999
|
+
await terminalProvider.resolve(terminalProvider.endpoint(terminalControl));
|
|
12000
|
+
}
|
|
12001
|
+
catch {
|
|
11927
12002
|
throw new Error(`cannot renew ${conversation.conversation_id}; terminal ${terminalControl.target} is no longer available`);
|
|
11928
12003
|
}
|
|
11929
12004
|
const expectedMessageId = stringValue(nativeTakeover?.terminal_bridge_message_id);
|
|
@@ -11946,9 +12021,7 @@ async function runRenew(options) {
|
|
|
11946
12021
|
throw new Error(`cannot renew ${current.conversation_id}; conversation is not a terminal bridge task`);
|
|
11947
12022
|
}
|
|
11948
12023
|
if (current.conversation_id !== conversation.conversation_id ||
|
|
11949
|
-
currentControl
|
|
11950
|
-
currentControl.socketPath !== terminalControl.socketPath ||
|
|
11951
|
-
currentControl.panePid !== terminalControl.panePid ||
|
|
12024
|
+
!terminalControlsShareIncarnation(currentControl, terminalControl) ||
|
|
11952
12025
|
stringValue(currentTakeover.terminal_bridge_message_id) !== expectedMessageId ||
|
|
11953
12026
|
stringValue(currentTakeover.terminal_bridge_started_at) !== expectedStartedAt) {
|
|
11954
12027
|
throw new Error("conversation changed while waiting to renew; refresh status and retry");
|
|
@@ -12623,7 +12696,7 @@ async function runCancel(options) {
|
|
|
12623
12696
|
throw new Error(`conversation ${conversation.conversation_id} is not attached to a live tmux terminal`);
|
|
12624
12697
|
}
|
|
12625
12698
|
async function runTerminalConversationCancel({ options, conversationId, agent, terminalControl, pid }) {
|
|
12626
|
-
const releaseTerminalLock =
|
|
12699
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 });
|
|
12627
12700
|
try {
|
|
12628
12701
|
const cancellation = await createTerminalAgentBridge(options).cancel(agent, terminalControl, {
|
|
12629
12702
|
runtime: {
|
|
@@ -12661,7 +12734,7 @@ async function runTerminalConversationCancel({ options, conversationId, agent, t
|
|
|
12661
12734
|
}
|
|
12662
12735
|
}
|
|
12663
12736
|
async function runTerminalControlCancel({ options, statePath, logPath, agent, terminalControl }) {
|
|
12664
|
-
const releaseTerminalLock =
|
|
12737
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 });
|
|
12665
12738
|
let releaseStateLock;
|
|
12666
12739
|
try {
|
|
12667
12740
|
releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
@@ -12676,8 +12749,7 @@ async function runTerminalControlCancel({ options, statePath, logPath, agent, te
|
|
|
12676
12749
|
: undefined;
|
|
12677
12750
|
const currentControl = terminalControlFromTakeover(currentTakeover);
|
|
12678
12751
|
if (!currentControl ||
|
|
12679
|
-
currentControl
|
|
12680
|
-
currentControl.socketPath !== terminalControl.socketPath) {
|
|
12752
|
+
!terminalControlsShareIncarnation(currentControl, terminalControl)) {
|
|
12681
12753
|
throw new Error("terminal control changed while waiting to cancel; refresh status and retry");
|
|
12682
12754
|
}
|
|
12683
12755
|
assertManagedTerminalDispatchOwner({
|
|
@@ -12764,7 +12836,7 @@ async function runClose(options) {
|
|
|
12764
12836
|
: undefined;
|
|
12765
12837
|
const terminalControl = terminalControlFromTakeover(nativeTakeover);
|
|
12766
12838
|
const releaseTerminalLock = terminalControl
|
|
12767
|
-
?
|
|
12839
|
+
? acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 })
|
|
12768
12840
|
: () => { };
|
|
12769
12841
|
let releaseStateLock;
|
|
12770
12842
|
try {
|
|
@@ -12838,7 +12910,7 @@ async function runClose(options) {
|
|
|
12838
12910
|
async function runTerminalDispatchClose({ options, terminalConversation }) {
|
|
12839
12911
|
const terminalControl = terminalConversation.terminalControl;
|
|
12840
12912
|
const storeDir = storeDirFromOptions(options);
|
|
12841
|
-
const releaseTerminalLock =
|
|
12913
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDir, terminalControl, { timeoutMs: 30000 });
|
|
12842
12914
|
try {
|
|
12843
12915
|
return await withStoreWriterLeaseAsync(storeDir, async () => {
|
|
12844
12916
|
let ledger = resolveTerminalDispatchLedgerPaneIncarnation(terminalControl, loadTerminalBridgeDispatchLedger(terminalControl));
|
|
@@ -13506,7 +13578,7 @@ async function runTerminalBridgeMonitorWithLock(options, lifecycle, expectedTerm
|
|
|
13506
13578
|
});
|
|
13507
13579
|
return;
|
|
13508
13580
|
}
|
|
13509
|
-
const releaseTerminalLock =
|
|
13581
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 });
|
|
13510
13582
|
try {
|
|
13511
13583
|
const reconciliation = await reconcileTerminalAcceptanceInMonitor({
|
|
13512
13584
|
options,
|
|
@@ -13570,7 +13642,7 @@ async function runTerminalBridgeMonitorWithLock(options, lifecycle, expectedTerm
|
|
|
13570
13642
|
sleepSync(pollIntervalMs);
|
|
13571
13643
|
continue;
|
|
13572
13644
|
}
|
|
13573
|
-
const releaseTerminalLock =
|
|
13645
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 });
|
|
13574
13646
|
try {
|
|
13575
13647
|
const writerStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
13576
13648
|
await withStoreWriterLeaseAsync(writerStoreDir, async () => {
|
|
@@ -13850,7 +13922,7 @@ async function runTerminalBridgeMonitorWithLock(options, lifecycle, expectedTerm
|
|
|
13850
13922
|
let poll;
|
|
13851
13923
|
const expectedUpdatedAt = conversation.updated_at;
|
|
13852
13924
|
const expectedStatus = conversation.status;
|
|
13853
|
-
const releaseTerminalPollLock =
|
|
13925
|
+
const releaseTerminalPollLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 });
|
|
13854
13926
|
try {
|
|
13855
13927
|
let dispatchLedger = reconcilePreparedTerminalDispatchLedger(terminalControl, loadTerminalBridgeDispatchLedger(terminalControl));
|
|
13856
13928
|
const durableSubmission = terminalBridgeSubmission(conversation);
|
|
@@ -13916,8 +13988,7 @@ async function runTerminalBridgeMonitorWithLock(options, lifecycle, expectedTerm
|
|
|
13916
13988
|
stringValue(lockedTakeover?.terminal_bridge_message_id) !==
|
|
13917
13989
|
currentMessageId ||
|
|
13918
13990
|
!lockedControl ||
|
|
13919
|
-
|
|
13920
|
-
terminalControlSelectorKey(terminalControl)) {
|
|
13991
|
+
!terminalControlsShareIncarnation(lockedControl, terminalControl)) {
|
|
13921
13992
|
conversation = lockedConversation;
|
|
13922
13993
|
continue;
|
|
13923
13994
|
}
|
|
@@ -14601,6 +14672,39 @@ function terminalBridgeSendLockPath(_storeDir, terminalControl) {
|
|
|
14601
14672
|
const terminalKey = terminalBridgeRuntimeKey(terminalControl);
|
|
14602
14673
|
return path.join(lockDir, `terminal-bridge-send-${terminalKey}.lock`);
|
|
14603
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
|
+
}
|
|
14604
14708
|
function terminalBridgeRuntimeLockDir() {
|
|
14605
14709
|
return path.join(terminalBridgeRuntimeDir(), "terminal-locks");
|
|
14606
14710
|
}
|
|
@@ -14611,46 +14715,101 @@ function terminalBridgeRuntimeDir() {
|
|
|
14611
14715
|
: path.join(path.dirname(defaultStoreDir()), "runtime-v2");
|
|
14612
14716
|
}
|
|
14613
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) {
|
|
14614
14727
|
return createHash("sha256")
|
|
14615
|
-
.update(JSON.stringify(
|
|
14616
|
-
target: terminalControl.target,
|
|
14617
|
-
socket_path: terminalControl.socketPath ?? null
|
|
14618
|
-
}))
|
|
14728
|
+
.update(JSON.stringify(terminalLegacyRuntimeRoute(terminalControl)))
|
|
14619
14729
|
.digest("hex")
|
|
14620
14730
|
.slice(0, 20);
|
|
14621
14731
|
}
|
|
14622
|
-
function terminalBridgeDispatchLedgerPath(terminalControl) {
|
|
14732
|
+
function terminalBridgeDispatchLedgerPath(terminalControl, options = {}) {
|
|
14623
14733
|
const ledgerDir = path.join(terminalBridgeRuntimeDir(), "terminal-dispatch");
|
|
14624
|
-
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
|
+
])];
|
|
14625
14743
|
}
|
|
14626
14744
|
function loadTerminalBridgeDispatchLedger(terminalControl) {
|
|
14627
|
-
const
|
|
14628
|
-
|
|
14745
|
+
const ledgerPaths = terminalBridgeDispatchLedgerPaths(terminalControl)
|
|
14746
|
+
.filter((candidate) => fs.existsSync(candidate));
|
|
14747
|
+
if (ledgerPaths.length === 0) {
|
|
14629
14748
|
return undefined;
|
|
14630
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];
|
|
14631
14755
|
const stat = fs.lstatSync(ledgerPath);
|
|
14632
14756
|
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
14633
14757
|
throw new Error(`terminal dispatch ledger is not a regular file: ${ledgerPath}`);
|
|
14634
14758
|
}
|
|
14635
14759
|
const parsed = JSON.parse(fs.readFileSync(ledgerPath, "utf8"));
|
|
14636
14760
|
if (!isRecord(parsed) ||
|
|
14637
|
-
parsed.version
|
|
14638
|
-
stringValue(parsed.terminal_key) !== terminalBridgeRuntimeKey(terminalControl)) {
|
|
14761
|
+
!(parsed.version === 1 || parsed.version === 2)) {
|
|
14639
14762
|
throw new Error(`terminal dispatch ledger is invalid: ${ledgerPath}`);
|
|
14640
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
|
+
}
|
|
14641
14785
|
return parsed;
|
|
14642
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
|
+
}
|
|
14643
14805
|
function assertTerminalNativeThreadStoreAuthority({ terminalControl, nativeThreadId, storeDir }) {
|
|
14644
14806
|
const ledger = resolveTerminalDispatchLedgerPaneIncarnation(terminalControl, loadTerminalBridgeDispatchLedger(terminalControl));
|
|
14645
14807
|
if (!ledger) {
|
|
14646
14808
|
return;
|
|
14647
14809
|
}
|
|
14648
|
-
|
|
14649
|
-
|
|
14650
|
-
|
|
14651
|
-
if (stringValue(ledgerControl?.target) !== terminalControl.target ||
|
|
14652
|
-
(stringValue(ledgerControl?.socket_path) ?? undefined) !==
|
|
14653
|
-
terminalControl.socketPath) {
|
|
14810
|
+
if (!terminalDispatchRecordMatchesControl(ledger, terminalControl, {
|
|
14811
|
+
requireProcessAnchor: false
|
|
14812
|
+
})) {
|
|
14654
14813
|
throw new Error(`terminal ${terminalControl.target} dispatch ledger selector is invalid`);
|
|
14655
14814
|
}
|
|
14656
14815
|
const binding = isRecord(ledger.binding) ? ledger.binding : undefined;
|
|
@@ -14694,11 +14853,6 @@ function orphanedTerminalDispatchForRecovery(terminalControl) {
|
|
|
14694
14853
|
const recoveryIdentity = lifecycle
|
|
14695
14854
|
? stringValue(ledger?.transition_id)
|
|
14696
14855
|
: stringValue(ledger?.message_id);
|
|
14697
|
-
const ledgerControl = isRecord(ledger?.terminal_control)
|
|
14698
|
-
? ledger.terminal_control
|
|
14699
|
-
: undefined;
|
|
14700
|
-
const ledgerPanePid = Number(ledgerControl?.pane_pid);
|
|
14701
|
-
const currentPanePid = Number(terminalControl.panePid);
|
|
14702
14856
|
if (!ledger ||
|
|
14703
14857
|
![
|
|
14704
14858
|
"prepared",
|
|
@@ -14714,11 +14868,10 @@ function orphanedTerminalDispatchForRecovery(terminalControl) {
|
|
|
14714
14868
|
.includes(String(ledger.status)) ||
|
|
14715
14869
|
!recoveryIdentity ||
|
|
14716
14870
|
(!lifecycle &&
|
|
14717
|
-
|
|
14718
|
-
|
|
14719
|
-
|
|
14720
|
-
|
|
14721
|
-
ledgerPanePid !== currentPanePid) ||
|
|
14871
|
+
terminalDispatchRecordMatchesControl(ledger, terminalControl, {
|
|
14872
|
+
requireProcessAnchor: false
|
|
14873
|
+
}) &&
|
|
14874
|
+
!terminalDispatchRecordMatchesControl(ledger, terminalControl)) ||
|
|
14722
14875
|
(!lifecycle && loadTerminalDispatchLedgerOwner(ledger))) {
|
|
14723
14876
|
return undefined;
|
|
14724
14877
|
}
|
|
@@ -14887,12 +15040,11 @@ function mergeTerminalLedgerReceipt(previous, next) {
|
|
|
14887
15040
|
const nextControl = isRecord(next.terminal_control)
|
|
14888
15041
|
? next.terminal_control
|
|
14889
15042
|
: undefined;
|
|
14890
|
-
|
|
14891
|
-
|
|
14892
|
-
|
|
14893
|
-
|
|
14894
|
-
|
|
14895
|
-
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)) {
|
|
14896
15048
|
throw new Error(`terminal dispatch receipt ${messageId} changed immutable terminal_control`);
|
|
14897
15049
|
}
|
|
14898
15050
|
if (previous.status === "agent_accepted" &&
|
|
@@ -14921,17 +15073,40 @@ function mergeTerminalLedgerReceipt(previous, next) {
|
|
|
14921
15073
|
if (!merged.terminal_control && previousControl) {
|
|
14922
15074
|
merged.terminal_control = previousControl;
|
|
14923
15075
|
}
|
|
15076
|
+
if (!merged.terminal_endpoint && previous.terminal_endpoint) {
|
|
15077
|
+
merged.terminal_endpoint = previous.terminal_endpoint;
|
|
15078
|
+
}
|
|
14924
15079
|
return merged;
|
|
14925
15080
|
}
|
|
14926
15081
|
function saveTerminalBridgeDispatchLedger(terminalControl, ledger) {
|
|
14927
|
-
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);
|
|
14928
15106
|
ensureDir(path.dirname(ledgerPath));
|
|
14929
15107
|
if (fs.existsSync(ledgerPath) && fs.lstatSync(ledgerPath).isSymbolicLink()) {
|
|
14930
15108
|
throw new Error(`terminal dispatch ledger is a symlink: ${ledgerPath}`);
|
|
14931
15109
|
}
|
|
14932
|
-
const previousLedger = fs.existsSync(ledgerPath)
|
|
14933
|
-
? loadTerminalBridgeDispatchLedger(terminalControl)
|
|
14934
|
-
: undefined;
|
|
14935
15110
|
let baseReceiptHistory = terminalLedgerReceiptHistory(previousLedger);
|
|
14936
15111
|
for (const incomingReceipt of terminalLedgerReceiptHistory(ledger)) {
|
|
14937
15112
|
const incomingId = String(incomingReceipt.message_id);
|
|
@@ -14943,18 +15118,25 @@ function saveTerminalBridgeDispatchLedger(terminalControl, ledger) {
|
|
|
14943
15118
|
? baseReceiptHistory.map((receipt) => stringValue(receipt.message_id) === incomingId ? merged : receipt)
|
|
14944
15119
|
: [...baseReceiptHistory, merged];
|
|
14945
15120
|
}
|
|
14946
|
-
const { terminal_submission_receipts: _incomingReceiptHistory, ...ledgerWithoutReceiptHistory } = ledger;
|
|
15121
|
+
const { terminal_submission_receipts: _incomingReceiptHistory, terminal_endpoint: incomingTerminalEndpoint, ...ledgerWithoutReceiptHistory } = ledger;
|
|
14947
15122
|
const nextWithoutHistory = {
|
|
14948
15123
|
...ledgerWithoutReceiptHistory,
|
|
14949
|
-
version:
|
|
14950
|
-
|
|
15124
|
+
version: hasCanonicalTerminalEndpoint(terminalControl) && !preserveLegacyFormat
|
|
15125
|
+
? 2
|
|
15126
|
+
: 1,
|
|
15127
|
+
terminal_key: preserveLegacyFormat
|
|
15128
|
+
? legacyTerminalBridgeRuntimeKey(terminalControl)
|
|
15129
|
+
: terminalBridgeRuntimeKey(terminalControl),
|
|
14951
15130
|
terminal_control: {
|
|
14952
15131
|
kind: "tmux",
|
|
14953
15132
|
target: terminalControl.target,
|
|
14954
15133
|
socket_path: terminalControl.socketPath ?? null,
|
|
14955
15134
|
pane_pid: terminalControl.panePid ?? null,
|
|
14956
15135
|
current_path: terminalControl.currentPath ?? null
|
|
14957
|
-
}
|
|
15136
|
+
},
|
|
15137
|
+
...(hasCanonicalTerminalEndpoint(terminalControl) && !preserveLegacyFormat
|
|
15138
|
+
? { terminal_endpoint: terminalControlEvidence(terminalControl) }
|
|
15139
|
+
: {})
|
|
14958
15140
|
};
|
|
14959
15141
|
// The top-level ledger follows the current tmux pane incarnation, while an
|
|
14960
15142
|
// append-only receipt must retain the pane that actually accepted its
|
|
@@ -14962,10 +15144,16 @@ function saveTerminalBridgeDispatchLedger(terminalControl, ledger) {
|
|
|
14962
15144
|
// pane with the same selector, so derive that receipt from the caller's
|
|
14963
15145
|
// original control record instead of silently rebinding its proof.
|
|
14964
15146
|
const receiptCandidateLedger = isRecord(ledgerWithoutReceiptHistory.terminal_control)
|
|
14965
|
-
? {
|
|
14966
|
-
...nextWithoutHistory
|
|
14967
|
-
|
|
14968
|
-
|
|
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
|
+
})()
|
|
14969
15157
|
: nextWithoutHistory;
|
|
14970
15158
|
const nextCandidate = terminalLedgerReceiptCandidate(receiptCandidateLedger);
|
|
14971
15159
|
let nextReceiptHistory = baseReceiptHistory;
|
|
@@ -15040,23 +15228,20 @@ function resolveTerminalDispatchLedgerPaneIncarnation(terminalControl, ledger) {
|
|
|
15040
15228
|
// released by the generic Turn-ledger cleanup path.
|
|
15041
15229
|
return ledger;
|
|
15042
15230
|
}
|
|
15043
|
-
|
|
15044
|
-
|
|
15045
|
-
|
|
15046
|
-
|
|
15047
|
-
const currentPanePid = Number(terminalControl.panePid);
|
|
15048
|
-
if (!Number.isSafeInteger(ledgerPanePid) ||
|
|
15049
|
-
ledgerPanePid <= 0 ||
|
|
15050
|
-
!Number.isSafeInteger(currentPanePid) ||
|
|
15051
|
-
currentPanePid <= 0 ||
|
|
15052
|
-
ledgerPanePid === currentPanePid) {
|
|
15231
|
+
if (!terminalDispatchRecordMatchesControl(ledger, terminalControl, {
|
|
15232
|
+
requireProcessAnchor: false
|
|
15233
|
+
}) ||
|
|
15234
|
+
terminalDispatchRecordMatchesControl(ledger, terminalControl)) {
|
|
15053
15235
|
return ledger;
|
|
15054
15236
|
}
|
|
15237
|
+
const ledgerProcessAnchor = terminalDispatchRecordProcessAnchor(ledger);
|
|
15238
|
+
const currentProcessAnchor = terminalEndpointFromControlRef(terminalControl).processAnchorPid;
|
|
15055
15239
|
saveTerminalBridgeDispatchLedger(terminalControl, {
|
|
15056
15240
|
...ledger,
|
|
15057
15241
|
status: "resolved",
|
|
15058
15242
|
resolved_at: new Date().toISOString(),
|
|
15059
|
-
reason:
|
|
15243
|
+
reason: "terminal process incarnation changed from anchor " +
|
|
15244
|
+
`${ledgerProcessAnchor} to ${currentProcessAnchor ?? "unknown"}`
|
|
15060
15245
|
});
|
|
15061
15246
|
return loadTerminalBridgeDispatchLedger(terminalControl);
|
|
15062
15247
|
}
|
|
@@ -15176,8 +15361,7 @@ function reconcilePreparedTerminalDispatchLedger(terminalControl, ledger) {
|
|
|
15176
15361
|
if (conversation.conversation_id !==
|
|
15177
15362
|
stringValue(ledger.conversation_id) ||
|
|
15178
15363
|
!storedControl ||
|
|
15179
|
-
|
|
15180
|
-
terminalBridgeRuntimeKey(terminalControl)) {
|
|
15364
|
+
!sameTerminalControlIncarnation(storedControl, terminalControl)) {
|
|
15181
15365
|
return ledger;
|
|
15182
15366
|
}
|
|
15183
15367
|
if (storedMessageId === messageId &&
|
|
@@ -15302,8 +15486,7 @@ function reconcileLaggingTerminalDispatchLedger(terminalControl, ledger) {
|
|
|
15302
15486
|
stringValue(nativeTakeover?.terminal_bridge_message_id) !== messageId ||
|
|
15303
15487
|
stringValue(submission?.message_id) !== messageId ||
|
|
15304
15488
|
!storedControl ||
|
|
15305
|
-
|
|
15306
|
-
terminalBridgeRuntimeKey(terminalControl)) {
|
|
15489
|
+
!sameTerminalControlIncarnation(storedControl, terminalControl)) {
|
|
15307
15490
|
return ledger;
|
|
15308
15491
|
}
|
|
15309
15492
|
const requestText = String(nativeTakeover?.terminal_bridge_request_text ?? conversation.user_request ?? "");
|
|
@@ -15731,16 +15914,13 @@ function assertLifecycleLedgerMatchesTransition({ options, terminal, ledger, tra
|
|
|
15731
15914
|
stringValue(ledger.adapter_version) !== transition.adapter_version ||
|
|
15732
15915
|
stringValue(ledger.command_fingerprint) !== transition.command_fingerprint ||
|
|
15733
15916
|
path.resolve(storeDir) !== path.resolve(storeDirFromOptions(options)) ||
|
|
15734
|
-
terminal.conversationId !== transition.terminal_id ||
|
|
15735
15917
|
terminal.agent !== transition.agent ||
|
|
15736
|
-
|
|
15737
|
-
stringValue(ledgerControl?.target) !== terminal.terminalControl.target ||
|
|
15738
|
-
(stringValue(ledgerControl?.socket_path) ?? undefined) !==
|
|
15739
|
-
terminal.terminalControl.socketPath ||
|
|
15918
|
+
!terminalDispatchRecordMatchesControl(ledger, terminal.terminalControl) ||
|
|
15740
15919
|
terminal.pid !== transition.before_binding?.native_process.pid &&
|
|
15741
15920
|
transition.before_binding !== undefined ||
|
|
15742
|
-
|
|
15743
|
-
|
|
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)) ||
|
|
15744
15924
|
!matchesConfiguredWorkspace(transition.workspace, terminal.terminalControl.currentPath) ||
|
|
15745
15925
|
JSON.stringify(ledger.binding ?? null) !==
|
|
15746
15926
|
JSON.stringify(transition.before_binding ?? null)) {
|
|
@@ -15770,10 +15950,8 @@ async function verifyRecoveredLifecycleAfterBinding({ options, terminal, transit
|
|
|
15770
15950
|
const binding = transition.after_binding;
|
|
15771
15951
|
if (!binding ||
|
|
15772
15952
|
!binding.native_thread_id ||
|
|
15773
|
-
binding.terminal_id !== terminal.conversationId ||
|
|
15774
15953
|
binding.native_process.pid !== terminal.pid ||
|
|
15775
|
-
|
|
15776
|
-
terminalControlSelectorKey(terminal.terminalControl)) {
|
|
15954
|
+
!terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminal.conversationId, terminal.terminalControl)) {
|
|
15777
15955
|
throw new Error("verified after_binding no longer matches the terminal or pid");
|
|
15778
15956
|
}
|
|
15779
15957
|
const bridge = createTerminalAgentBridge(options);
|
|
@@ -16277,10 +16455,9 @@ async function assertNativeInspectionComposerReadyForAutomatedInput({ options, t
|
|
|
16277
16455
|
});
|
|
16278
16456
|
return;
|
|
16279
16457
|
}
|
|
16280
|
-
const
|
|
16281
|
-
|
|
16282
|
-
|
|
16283
|
-
});
|
|
16458
|
+
const provider = createTerminalControlProvider(options);
|
|
16459
|
+
const resolvedTerminal = await provider.resolve(provider.endpoint(terminal.terminalControl));
|
|
16460
|
+
const screen = await provider.capture(resolvedTerminal, { scrollbackLines: 40 });
|
|
16284
16461
|
if (!isExactClaudeNativeInspectionIdleComposer(screen)) {
|
|
16285
16462
|
throw new Error("Claude composer contains input or is not at the exact idle frame; refusing automated terminal input");
|
|
16286
16463
|
}
|
|
@@ -16699,7 +16876,7 @@ function prepareTerminalBridgeCompletionCallback({ options, statePath, logPath,
|
|
|
16699
16876
|
});
|
|
16700
16877
|
claim.release();
|
|
16701
16878
|
claimReleased = true;
|
|
16702
|
-
const releaseTerminalLock =
|
|
16879
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 });
|
|
16703
16880
|
try {
|
|
16704
16881
|
resolveTerminalBridgeDispatchLedger({
|
|
16705
16882
|
terminalControl,
|