@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/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, terminalPaneContainsProcess } from "./terminal-control-provider.js";
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 createTerminalControlProvider(options) {
628
- if (options.terminalsJson || options.terminalScreensJson || options.processesJson) {
629
- return new StaticTerminalControlProvider({
630
- panes: options.terminalsJson ? parseJsonOption(options.terminalsJson, "--terminals-json") : [],
631
- screens: options.terminalScreensJson ? parseJsonOption(options.terminalScreensJson, "--terminal-screens-json") : {}
632
- });
633
- }
634
- return new TmuxTerminalControlProvider();
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 expectedWorkspace = options.workspace ?? terminalControl.currentPath;
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
- const panes = await terminalProvider.listPanes();
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, pane.currentPath, `terminal access to ${terminalControl.target} by tmux pane`);
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 ?? pane.currentPath,
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 ?? pane.currentPath,
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
- return {
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
- terminalControlSelectorKey(binding.terminal_control) !==
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 panes = await createTerminalControlProvider(options).listPanes();
1147
- const matchingPanes = panes.filter((pane) => pane.kind === terminalControl.kind &&
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
- terminalPaneContainsProcess(candidate, matchingPanes[0], snapshots));
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.target !== terminalControl.target ||
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
- terminalControlSelectorKey(ownerControl) !==
1275
- terminalControlSelectorKey(terminalControl) ||
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
- terminalKey: terminalControlSelectorKey(terminalControl),
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 terminalKeys = new Set(routed.map((entry) => `${entry.conversationId}\0${entry.terminalKey}\0${entry.panePid}`));
1358
- if (terminalKeys.size !== 1) {
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
- String(previousReceipt[key]) !== String(value)) {
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
- (stringValue(ledgerReceiptControl?.target) !== terminalControl.target ||
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
- (stringValue(loadedLedgerControl?.target) !== terminalControl.target ||
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
- stringValue(ledgerControl?.target) !== terminalControl.target ||
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 receiptTarget = stringValue(receipt.terminal_target) ??
2164
- storedControl?.target;
2165
- const receiptSocketPath = receipt.terminal_socket_path === null
2166
- ? undefined
2167
- : stringValue(receipt.terminal_socket_path) ?? storedControl?.socketPath;
2168
- const receiptPanePid = Number(receipt.terminal_pane_pid ?? storedControl?.panePid);
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
- receiptTarget !== terminalControl.target ||
2176
- receiptSocketPath !== terminalControl.socketPath ||
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 (terminalControlSelectorKey(terminalControlFromTakeover(nativeTakeover)) === targetKey) {
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
- terminalControlSelectorKey(terminalControlFromTakeover(listedTakeover)) !== targetKey) {
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
- terminalControlSelectorKey(terminalControlFromTakeover(currentTakeover)) !== targetKey) {
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
- if (provider instanceof TmuxTerminalControlProvider) {
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
- terminalKeyForManagedConversation(turn) ===
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 styledScreen = await createTerminalControlProvider(options).capture(terminalControl.target, {
2860
- scrollbackLines: 40,
2861
- socketPath: terminalControl.socketPath,
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 allByTerminal = managedConversationsByTerminal(allConversations);
2950
- const displayedByTerminal = managedConversationsByTerminal(displayedConversations);
2951
- const sessionsByTerminal = managedSessionsByTerminal(managedSessions);
2952
- const discoveredTerminalKeys = new Set();
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 terminalKey = terminalControlSelectorKey(terminalControl);
2958
- if (terminalKey) {
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 = terminalKey
2965
- ? [...(displayedByTerminal.get(terminalKey) ?? [])]
2981
+ const displayedRelated = terminalControl
2982
+ ? displayedConversations.filter((conversation) => terminalControlsShareIncarnation(terminalControlForManagedConversation(conversation), terminalControl))
2966
2983
  : [];
2967
- const relatedSessions = terminalKey
2968
- ? [...(sessionsByTerminal.get(terminalKey) ?? [])]
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 terminalKey = terminalKeyForManagedConversation(conversation);
3236
- if (terminalKey && discoveredTerminalKeys.has(terminalKey)) {
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
- terminalControlSelectorKey(binding.terminal_control) !==
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
- terminalControlSelectorKey(binding.terminal_control) ===
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 !== stringValue(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 terminalKeyForManagedConversation(conversation) {
3495
- return terminalControlSelectorKey(terminalControlFromTakeover(isRecord(conversation.native_session_takeover)
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
- const ledgerControl = isRecord(ledger.terminal_control)
3546
- ? ledger.terminal_control
3547
- : undefined;
3548
- const ledgerPanePid = Number(ledgerControl?.pane_pid);
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 ownerTerminalKey = terminalKeyForManagedConversation(owner);
3580
- const currentTerminalKey = terminalControlSelectorKey(terminalControl);
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) !== stringValue(terminal.id) ||
3755
- terminalControlSelectorKey(storedControl) !==
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 seenTerminalTargets = new Set();
3880
+ const seenTerminalIncarnations = new Set();
3902
3881
  return roots.filter((process) => {
3903
- const terminalTarget = process.terminalControl?.target
3904
- ? `${process.agent}:${process.terminalControl.target}:${process.terminalControl.panePid}`
3882
+ const endpoint = process.terminalControl
3883
+ ? terminalEndpointFromControlRef(process.terminalControl)
3905
3884
  : undefined;
3906
- if (!terminalTarget) {
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 (seenTerminalTargets.has(terminalTarget)) {
3895
+ if (seenTerminalIncarnations.has(terminalIncarnation)) {
3910
3896
  return false;
3911
3897
  }
3912
- seenTerminalTargets.add(terminalTarget);
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
- const target = stringValue(value.target);
4534
- const panePid = Number(value.panePid);
4535
- if (!target || !Number.isSafeInteger(panePid) || panePid <= 1) {
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
- target,
4540
- pane_pid: panePid,
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
- terminalControlSelectorKey(candidateBinding.terminal_control) !==
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
- terminalControlSelectorKey(binding.terminal_control) !==
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 styledScreen = await createTerminalControlProvider(options).capture(terminalControl.target, {
5020
- scrollbackLines: 40,
5021
- socketPath: terminalControl.socketPath,
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
- terminalControlSelectorKey(binding.terminal_control) !==
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 === terminal.conversationId &&
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
- terminalControlSelectorKey(session.binding.terminal_control) ===
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
- terminalControlSelectorKey(binding.terminal_control) !==
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
- terminalControlSelectorKey(session.binding.terminal_control) ===
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) => terminalKeyForManagedConversation(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
- terminalControlSelectorKey(afterBinding.terminal_control) !==
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
- const session = materialize
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
- const identity = session
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: lifecycleBindingToken({ session, terminal, identity })
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.conversationId !== expected.conversationId ||
6088
- actual.agent !== expected.agent ||
6122
+ if (actual.agent !== expected.agent ||
6089
6123
  actual.pid !== expected.pid ||
6090
- terminalControlSelectorKey(actual.terminalControl) !==
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) => terminalKeyForManagedConversation(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.bindingToken !== expectedBindingToken) {
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 releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDir, initiallyResolved.terminalControl), { timeoutMs: 30000 });
6296
+ const bridge = createTerminalAgentBridge(options);
6297
+ const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDir, initiallyResolved.terminalControl, { timeoutMs: 30000 });
6265
6298
  try {
6266
- const terminal = await resolveLifecycleTerminal(options);
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.bindingToken !== expectedBindingToken) {
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 resolveLifecycleTerminal(options);
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 resolveLifecycleTerminal(options);
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 resolveLifecycleTerminal(options);
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 = acquireFileLock(terminalBridgeSendLockPath(storeDir, initiallyResolved.terminalControl), { timeoutMs: 30000 });
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
- terminalControlSelectorKey(terminal.terminalControl) !==
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
- managedSessionBindingToken(session) !== expectedBindingToken) {
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
- terminalControlSelectorKey(binding.terminal_control) !==
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 terminalToken = lifecycleBindingToken({
6964
+ const terminalTokens = lifecycleBindingTokens({
6936
6965
  terminal,
6937
6966
  identity
6938
6967
  });
6939
- if (terminalToken !== expectedTerminalToken) {
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
- terminalControlSelectorKey(finalTerminal.terminalControl) !==
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 (lifecycleBindingToken({
7003
+ if (!lifecycleBindingTokens({
6976
7004
  terminal: finalTerminal,
6977
7005
  identity: finalIdentity
6978
- }) !== expectedTerminalToken) {
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
- managedSessionBindingToken(finalSession) !== expectedBindingToken) {
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
- if (snapshot.terminal_id !== terminal.conversationId ||
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
- snapshot.terminal_control.target !== terminal.terminalControl.target ||
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 = acquireFileLock(terminalBridgeSendLockPath(storeDir, initiallyResolved.terminalControl), { timeoutMs: 30000 });
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
- terminalControlSelectorKey(terminal.terminalControl) !==
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 (expectedToken !== snapshot.bindingToken) {
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
- await assertCodexComposerReadyForAutomatedInput({
7443
- options,
7444
- terminalControl: terminal.terminalControl
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
- inputStarted = true;
7451
- await bridge.send(terminal.agent, terminal.terminalControl, transitionStep.command, { runtime: beforeRuntime });
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
- target: terminalControl.target,
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.kind !== terminalControl.kind ||
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 = acquireFileLock(terminalBridgeSendLockPath(rawStoreDir, terminalConversation.terminalControl), { timeoutMs: 30000 });
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 resolvedTerminal = await createTerminalAgentBridge(options)
8545
- .resolveConversationId(rawTerminalId);
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 = acquireFileLock(terminalBridgeSendLockPath(storeDir, resolvedTerminal.terminalControl), { timeoutMs: 30000 });
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 liveTerminal = await createTerminalAgentBridge(options)
8718
- .resolveConversationId(nativeTerminalId);
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.target !== storedTerminalControl.target ||
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 = acquireFileLock(terminalBridgeSendLockPath(responseStoreDir, terminalControl), { timeoutMs: 30000 });
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
- const lockedConversation = loadState(statePath);
8737
- const lockedTakeover = isRecord(lockedConversation.native_session_takeover)
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.target !== terminalControl.target ||
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
- const responseManagedSession = tryLoadManagedSession(responseStoreDir, sessionIdForConversation(prepared.conversation));
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.target !== terminalControl.target ||
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 = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
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?.target !== terminalControl.target ||
9047
- currentControl?.socketPath !== terminalControl.socketPath ||
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?.target !== dispatchControl.target ||
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 = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
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 = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
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?.target !== terminalControl.target ||
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 !== terminalId ||
11495
- terminalControlSelectorKey(binding.terminal_control) !==
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 (!identity || session.status !== "bound" || !binding) {
11651
+ if (session.status !== "bound" || !binding) {
11582
11652
  return session;
11583
11653
  }
11584
- if (terminalControlSelectorKey(binding.terminal_control) !==
11585
- terminalControlSelectorKey(terminalControl) ||
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.sessionId;
11591
- const refinedProcessUuid = binding.native_process.process_uuid ?? identity.processUuid;
11592
- const refinedProcessBirth = binding.native_process.process_birth ?? identity.processBirth;
11593
- const refinedRollout = binding.native_process.rollout ?? identity.rollout;
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
- terminalControlSelectorKey(current.binding.terminal_control) !==
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
- terminalControlSelectorKey(storedControl) ===
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
- terminal_control: terminalControl,
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 panes = await createTerminalControlProvider(options).listPanes();
11924
- const terminalExists = panes.some((pane) => pane.target === terminalControl.target &&
11925
- (terminalControl.socketPath === undefined || pane.socketPath === terminalControl.socketPath));
11926
- if (!terminalExists) {
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.target !== terminalControl.target ||
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 = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
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 = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
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.target !== terminalControl.target ||
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
- ? acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 })
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 = acquireFileLock(terminalBridgeSendLockPath(storeDir, terminalControl), { timeoutMs: 30000 });
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 = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
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 = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
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 = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
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
- terminalControlSelectorKey(lockedControl) !==
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-${terminalBridgeRuntimeKey(terminalControl)}.json`);
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 ledgerPath = terminalBridgeDispatchLedgerPath(terminalControl);
14628
- if (!fs.existsSync(ledgerPath)) {
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 !== 1 ||
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
- const ledgerControl = isRecord(ledger.terminal_control)
14649
- ? ledger.terminal_control
14650
- : undefined;
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
- Number.isSafeInteger(ledgerPanePid) &&
14718
- ledgerPanePid > 0 &&
14719
- Number.isSafeInteger(currentPanePid) &&
14720
- currentPanePid > 0 &&
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
- if (previousControl &&
14891
- nextControl &&
14892
- (stringValue(previousControl.target) !== stringValue(nextControl.target) ||
14893
- (stringValue(previousControl.socket_path) ?? undefined) !==
14894
- (stringValue(nextControl.socket_path) ?? undefined) ||
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 ledgerPath = terminalBridgeDispatchLedgerPath(terminalControl);
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: 1,
14950
- terminal_key: terminalBridgeRuntimeKey(terminalControl),
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
- terminal_control: ledgerWithoutReceiptHistory.terminal_control
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
- const ledgerControl = isRecord(ledger.terminal_control)
15044
- ? ledger.terminal_control
15045
- : undefined;
15046
- const ledgerPanePid = Number(ledgerControl?.pane_pid);
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: `tmux pane incarnation changed from pid ${ledgerPanePid} to ${currentPanePid}`
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
- terminalBridgeRuntimeKey(storedControl) !==
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
- terminalBridgeRuntimeKey(storedControl) !==
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
- Number(ledgerControl?.pane_pid) !== terminal.terminalControl.panePid ||
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
- terminalControlSelectorKey(terminal.terminalControl) !==
15743
- terminalControlSelectorKey(transition.before_binding?.terminal_control ?? terminal.terminalControl) ||
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
- terminalControlSelectorKey(binding.terminal_control) !==
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 screen = await createTerminalControlProvider(options).capture(terminal.terminalControl.target, {
16281
- scrollbackLines: 40,
16282
- socketPath: terminal.terminalControl.socketPath
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 = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
16879
+ const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 });
16703
16880
  try {
16704
16881
  resolveTerminalBridgeDispatchLedger({
16705
16882
  terminalControl,