@scotthuang/agent-knock-knock 0.12.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/dist/src/cli-core.js +332 -29
- package/dist/src/cli-core.js.map +1 -1
- package/dist/src/herdr-terminal-control-provider.js +8 -3
- package/dist/src/herdr-terminal-control-provider.js.map +1 -1
- package/dist/src/terminal-control-provider.js +60 -16
- package/dist/src/terminal-control-provider.js.map +1 -1
- package/dist/src/terminal-submission-acceptance.d.ts +31 -6
- package/dist/src/terminal-submission-acceptance.js +93 -16
- package/dist/src/terminal-submission-acceptance.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.1 - 2026-08-11
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Allow a genuinely virgin Codex TUI to accept its first AKK send by pinning the exact process incarnation before input, then atomically refining the Session and Turn to the newly materialized native UUID, rollout, and request evidence after the single Enter.
|
|
8
|
+
- Recover both post-Enter virgin-binding crash windows without replaying terminal text or Enter, while preserving the existing ownership, binding-generation, process, pane, cwd, rollout, and request-hash fences.
|
|
9
|
+
- Preserve tmux `pane_current_path` when extended eight-field pane output is collapsed to whitespace or underscores, rejecting ambiguous or truncated records instead of appending the server socket and pane ID to the workspace.
|
|
10
|
+
- Preserve Codex composer ANSI styling through Herdr's visible screen buffer so an empty dim placeholder is not mistaken for authored draft text, while leaving ordinary monitor and status reads on Herdr's agent-aware detection buffer.
|
|
11
|
+
|
|
3
12
|
## 0.12.0 - 2026-08-10
|
|
4
13
|
|
|
5
14
|
### Added
|
package/dist/src/cli-core.js
CHANGED
|
@@ -9907,21 +9907,22 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
9907
9907
|
runtime: preSendRuntime
|
|
9908
9908
|
});
|
|
9909
9909
|
assertSafeTerminalSend(executor.kind, status);
|
|
9910
|
-
if (executor.kind === "codex" &&
|
|
9911
|
-
(pendingManagedNativeBinding || allowedPreMaterializationIdentity)
|
|
9912
|
-
|
|
9913
|
-
|
|
9914
|
-
|
|
9915
|
-
|
|
9916
|
-
|
|
9917
|
-
|
|
9918
|
-
|
|
9919
|
-
|
|
9920
|
-
|
|
9921
|
-
|
|
9922
|
-
|
|
9923
|
-
|
|
9924
|
-
|
|
9910
|
+
if (executor.kind === "codex" && needsPostSendNativeBinding) {
|
|
9911
|
+
if (pendingManagedNativeBinding || allowedPreMaterializationIdentity) {
|
|
9912
|
+
const expectedForegroundId = stringValue(sendTakeover?.terminal_agent_expected_session_id);
|
|
9913
|
+
const foreground = createRuntimeTerminalAgentRegistry(options)
|
|
9914
|
+
.require("codex")
|
|
9915
|
+
.observeThreadLifecycle?.({
|
|
9916
|
+
operation: { kind: "new_thread" },
|
|
9917
|
+
phase: "before",
|
|
9918
|
+
screen: status.screen.excerpt ?? ""
|
|
9919
|
+
});
|
|
9920
|
+
if (!expectedForegroundId ||
|
|
9921
|
+
foreground?.status !== "observed" ||
|
|
9922
|
+
foreground.nativeThreadId !== expectedForegroundId) {
|
|
9923
|
+
throw new Error("Codex foreground thread changed after the managed /status proof; " +
|
|
9924
|
+
"refresh list before sending");
|
|
9925
|
+
}
|
|
9925
9926
|
}
|
|
9926
9927
|
await assertCodexComposerReadyForAutomatedInput({
|
|
9927
9928
|
options,
|
|
@@ -9936,6 +9937,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
9936
9937
|
options,
|
|
9937
9938
|
currentIdentity: currentNativeIdentity,
|
|
9938
9939
|
expectedNativeThreadId: expectedManagedNativeThreadId,
|
|
9940
|
+
boundProcessUuid: stringValue(sendTakeover?.terminal_agent_process_uuid),
|
|
9941
|
+
boundProcessBirth: stringValue(sendTakeover?.terminal_agent_process_birth),
|
|
9939
9942
|
allowedPreMaterializationIdentity,
|
|
9940
9943
|
needsPostSendNativeBinding
|
|
9941
9944
|
});
|
|
@@ -10332,6 +10335,10 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
10332
10335
|
if (!enterDispatchedAt) {
|
|
10333
10336
|
throw new Error("terminal bridge returned without an enter_dispatched receipt");
|
|
10334
10337
|
}
|
|
10338
|
+
if (codexRolloutAcceptanceAnchor?.version === 2 &&
|
|
10339
|
+
cliEnv().AKK_TEST_EXIT_AFTER_VIRGIN_ENTER_DISPATCHED === "1") {
|
|
10340
|
+
cliExit(86);
|
|
10341
|
+
}
|
|
10335
10342
|
let submittedBase = stagedConversation;
|
|
10336
10343
|
if (needsPostSendNativeBinding) {
|
|
10337
10344
|
let boundIdentity;
|
|
@@ -10353,6 +10360,14 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
10353
10360
|
}
|
|
10354
10361
|
if (boundIdentity) {
|
|
10355
10362
|
try {
|
|
10363
|
+
if (codexRolloutAcceptanceAnchor &&
|
|
10364
|
+
(boundIdentity.processUuid !==
|
|
10365
|
+
codexRolloutAcceptanceAnchor.process_uuid ||
|
|
10366
|
+
boundIdentity.processBirth !==
|
|
10367
|
+
codexRolloutAcceptanceAnchor.process_birth)) {
|
|
10368
|
+
throw new Error("Codex process incarnation changed while its native " +
|
|
10369
|
+
"thread materialized");
|
|
10370
|
+
}
|
|
10356
10371
|
const expectedNativeThreadId = stringValue(sendTakeover?.terminal_agent_expected_session_id);
|
|
10357
10372
|
if (expectedNativeThreadId &&
|
|
10358
10373
|
boundIdentity.sessionId !== expectedNativeThreadId) {
|
|
@@ -10378,6 +10393,10 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
10378
10393
|
terminalControl,
|
|
10379
10394
|
identity: boundIdentity
|
|
10380
10395
|
});
|
|
10396
|
+
if (codexRolloutAcceptanceAnchor?.version === 2 &&
|
|
10397
|
+
cliEnv().AKK_TEST_EXIT_AFTER_VIRGIN_SESSION_BINDING === "1") {
|
|
10398
|
+
cliExit(86);
|
|
10399
|
+
}
|
|
10381
10400
|
// A virgin terminal Turn and its Session start with the same
|
|
10382
10401
|
// provisional binding id/generation before Codex has materialized a
|
|
10383
10402
|
// native thread. Persist the observed identity as a monotonic
|
|
@@ -11050,7 +11069,7 @@ function syntheticTerminalAcceptanceEvidence({ options, executor, conversation,
|
|
|
11050
11069
|
.digest("hex")
|
|
11051
11070
|
};
|
|
11052
11071
|
}
|
|
11053
|
-
function captureCodexAcceptanceAnchorForSend({ options, currentIdentity, expectedNativeThreadId, allowedPreMaterializationIdentity, needsPostSendNativeBinding }) {
|
|
11072
|
+
function captureCodexAcceptanceAnchorForSend({ options, currentIdentity, expectedNativeThreadId, boundProcessUuid, boundProcessBirth, allowedPreMaterializationIdentity, needsPostSendNativeBinding }) {
|
|
11054
11073
|
if (allowsSyntheticTerminalAcceptance(options)) {
|
|
11055
11074
|
return undefined;
|
|
11056
11075
|
}
|
|
@@ -11058,25 +11077,258 @@ function captureCodexAcceptanceAnchorForSend({ options, currentIdentity, expecte
|
|
|
11058
11077
|
? expectedNativeThreadId
|
|
11059
11078
|
: currentIdentity?.sessionId;
|
|
11060
11079
|
const processUuid = currentIdentity?.processUuid ??
|
|
11061
|
-
allowedPreMaterializationIdentity?.processUuid
|
|
11080
|
+
allowedPreMaterializationIdentity?.processUuid ??
|
|
11081
|
+
boundProcessUuid;
|
|
11062
11082
|
const processBirth = currentIdentity?.processBirth ??
|
|
11063
|
-
allowedPreMaterializationIdentity?.processBirth
|
|
11064
|
-
|
|
11065
|
-
|
|
11083
|
+
allowedPreMaterializationIdentity?.processBirth ??
|
|
11084
|
+
boundProcessBirth;
|
|
11085
|
+
if (!processUuid || !processBirth) {
|
|
11086
|
+
throw new Error("Codex native acceptance anchor requires an exact process incarnation");
|
|
11087
|
+
}
|
|
11088
|
+
if (!nativeThreadId && !needsPostSendNativeBinding) {
|
|
11089
|
+
throw new Error("Codex native acceptance anchor requires an exact thread");
|
|
11066
11090
|
}
|
|
11067
|
-
const base = { nativeThreadId, processUuid, processBirth };
|
|
11068
11091
|
if (!needsPostSendNativeBinding && currentIdentity?.rollout) {
|
|
11069
11092
|
return captureCodexRolloutAcceptanceAnchor({
|
|
11070
|
-
|
|
11093
|
+
nativeThreadId: nativeThreadId,
|
|
11094
|
+
processUuid,
|
|
11095
|
+
processBirth,
|
|
11096
|
+
now: cliNow(),
|
|
11071
11097
|
mode: "existing",
|
|
11072
11098
|
rollout: currentIdentity.rollout
|
|
11073
11099
|
});
|
|
11074
11100
|
}
|
|
11075
|
-
return
|
|
11076
|
-
|
|
11077
|
-
|
|
11078
|
-
|
|
11079
|
-
|
|
11101
|
+
return nativeThreadId
|
|
11102
|
+
? captureCodexRolloutAcceptanceAnchor({
|
|
11103
|
+
nativeThreadId,
|
|
11104
|
+
processUuid,
|
|
11105
|
+
processBirth,
|
|
11106
|
+
now: cliNow(),
|
|
11107
|
+
mode: "pre_materialization",
|
|
11108
|
+
expectedEmptyNativeSession: true
|
|
11109
|
+
})
|
|
11110
|
+
: captureCodexRolloutAcceptanceAnchor({
|
|
11111
|
+
processUuid,
|
|
11112
|
+
processBirth,
|
|
11113
|
+
now: cliNow(),
|
|
11114
|
+
mode: "pre_materialization",
|
|
11115
|
+
expectedEmptyNativeSession: true
|
|
11116
|
+
});
|
|
11117
|
+
}
|
|
11118
|
+
function exactNativeRolloutMatches(left, right) {
|
|
11119
|
+
return Boolean(isCompleteNativeRollout(left) &&
|
|
11120
|
+
right &&
|
|
11121
|
+
left.fd === right.fd &&
|
|
11122
|
+
left.device === right.device &&
|
|
11123
|
+
left.inode === right.inode &&
|
|
11124
|
+
left.path === right.path);
|
|
11125
|
+
}
|
|
11126
|
+
/**
|
|
11127
|
+
* Finish the one monotonic binding transaction that a process crash may split
|
|
11128
|
+
* after a virgin Codex Turn has durably dispatched Enter. This is deliberately
|
|
11129
|
+
* scoped to the v2 post-submission anchor: legacy/v1 Turns retain their normal
|
|
11130
|
+
* binding fences and never gain a new recovery path.
|
|
11131
|
+
*/
|
|
11132
|
+
async function recoverVirginCodexPostSubmissionBinding({ options, conversation, statePath, logPath, terminalLockHeld = false }) {
|
|
11133
|
+
const initialTakeover = isRecord(conversation.native_session_takeover)
|
|
11134
|
+
? conversation.native_session_takeover
|
|
11135
|
+
: undefined;
|
|
11136
|
+
const initialAnchor = isRecord(initialTakeover?.codex_rollout_acceptance_anchor)
|
|
11137
|
+
? initialTakeover.codex_rollout_acceptance_anchor
|
|
11138
|
+
: undefined;
|
|
11139
|
+
const initialSubmission = terminalBridgeSubmission(conversation);
|
|
11140
|
+
const initialTerminalControl = terminalControlFromTakeover(initialTakeover);
|
|
11141
|
+
if (executorForConversation(conversation).kind !== "codex" ||
|
|
11142
|
+
initialAnchor?.version !== 2 ||
|
|
11143
|
+
initialAnchor?.native_thread_binding !== "post_submission" ||
|
|
11144
|
+
!initialTerminalControl ||
|
|
11145
|
+
!["enter_dispatched", "agent_accepted"].includes(String(initialSubmission?.status ?? ""))) {
|
|
11146
|
+
return { conversation, state: "not_applicable" };
|
|
11147
|
+
}
|
|
11148
|
+
const storeDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
11149
|
+
const releaseTerminalLock = terminalLockHeld
|
|
11150
|
+
? () => { }
|
|
11151
|
+
: acquireTerminalBridgeSendLock(storeDir, initialTerminalControl, { timeoutMs: 30000 });
|
|
11152
|
+
try {
|
|
11153
|
+
return await withStoreWriterLeaseAsync(storeDir, async () => {
|
|
11154
|
+
const releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
11155
|
+
try {
|
|
11156
|
+
const current = loadState(statePath);
|
|
11157
|
+
const takeover = isRecord(current.native_session_takeover)
|
|
11158
|
+
? current.native_session_takeover
|
|
11159
|
+
: undefined;
|
|
11160
|
+
const anchor = isRecord(takeover?.codex_rollout_acceptance_anchor)
|
|
11161
|
+
? takeover.codex_rollout_acceptance_anchor
|
|
11162
|
+
: undefined;
|
|
11163
|
+
const submission = terminalBridgeSubmission(current);
|
|
11164
|
+
const terminalControl = terminalControlFromTakeover(takeover);
|
|
11165
|
+
const messageId = stringValue(submission?.message_id);
|
|
11166
|
+
const requestHash = stringValue(takeover?.terminal_bridge_request_hash);
|
|
11167
|
+
const requestText = String(takeover?.terminal_bridge_request_text ?? current.user_request ?? "");
|
|
11168
|
+
if (executorForConversation(current).kind !== "codex" ||
|
|
11169
|
+
anchor?.version !== 2 ||
|
|
11170
|
+
anchor?.native_thread_binding !== "post_submission" ||
|
|
11171
|
+
!terminalControl ||
|
|
11172
|
+
!terminalControlsShareIncarnation(terminalControl, initialTerminalControl) ||
|
|
11173
|
+
!messageId ||
|
|
11174
|
+
messageId !== stringValue(takeover?.terminal_bridge_message_id) ||
|
|
11175
|
+
!["enter_dispatched", "agent_accepted"].includes(String(submission?.status ?? "")) ||
|
|
11176
|
+
!requestHash ||
|
|
11177
|
+
requestHash !== terminalBridgeRequestFingerprint(requestText)) {
|
|
11178
|
+
return { conversation: current, state: "not_applicable" };
|
|
11179
|
+
}
|
|
11180
|
+
const bindingId = stringValue(current.terminal_binding_id);
|
|
11181
|
+
const bindingGeneration = Number(current.terminal_binding_generation);
|
|
11182
|
+
const session = loadManagedSession(storeDir, sessionIdForConversation(current));
|
|
11183
|
+
const binding = session.binding;
|
|
11184
|
+
const pid = Number(takeover?.terminal_agent_pid);
|
|
11185
|
+
if (!bindingId ||
|
|
11186
|
+
!Number.isSafeInteger(bindingGeneration) ||
|
|
11187
|
+
!Number.isSafeInteger(pid) ||
|
|
11188
|
+
pid <= 1 ||
|
|
11189
|
+
session.agent !== "codex" ||
|
|
11190
|
+
session.status !== "bound" ||
|
|
11191
|
+
!binding ||
|
|
11192
|
+
binding.binding_id !== bindingId ||
|
|
11193
|
+
binding.generation !== bindingGeneration ||
|
|
11194
|
+
binding.native_process.pid !== pid ||
|
|
11195
|
+
!terminalControlsShareIncarnation(binding.terminal_control, terminalControl)) {
|
|
11196
|
+
throw new Error("virgin Codex post-submission binding changed before recovery");
|
|
11197
|
+
}
|
|
11198
|
+
const anchorProcessUuid = stringValue(anchor.process_uuid);
|
|
11199
|
+
const anchorProcessBirth = stringValue(anchor.process_birth);
|
|
11200
|
+
const turnProcessUuid = stringValue(takeover?.terminal_agent_process_uuid);
|
|
11201
|
+
const turnProcessBirth = stringValue(takeover?.terminal_agent_process_birth);
|
|
11202
|
+
if (!anchorProcessUuid ||
|
|
11203
|
+
!anchorProcessBirth ||
|
|
11204
|
+
turnProcessUuid !== anchorProcessUuid ||
|
|
11205
|
+
turnProcessBirth !== anchorProcessBirth ||
|
|
11206
|
+
binding.native_process.process_uuid !== anchorProcessUuid ||
|
|
11207
|
+
binding.native_process.process_birth !== anchorProcessBirth) {
|
|
11208
|
+
throw new Error("virgin Codex process incarnation changed before binding recovery");
|
|
11209
|
+
}
|
|
11210
|
+
const turnNativeThreadId = stringValue(current.native_thread_id) ??
|
|
11211
|
+
stringValue(takeover?.terminal_agent_session_id);
|
|
11212
|
+
const sessionNativeThreadId = binding.native_thread_id;
|
|
11213
|
+
if (turnNativeThreadId &&
|
|
11214
|
+
sessionNativeThreadId &&
|
|
11215
|
+
turnNativeThreadId !== sessionNativeThreadId) {
|
|
11216
|
+
throw new Error("virgin Codex Session and Turn disagree before binding recovery");
|
|
11217
|
+
}
|
|
11218
|
+
if (turnNativeThreadId && !isCompleteNativeRollout(takeover?.terminal_agent_rollout)) {
|
|
11219
|
+
throw new Error("virgin Codex Turn has a partial recovered native identity");
|
|
11220
|
+
}
|
|
11221
|
+
if (sessionNativeThreadId && !binding.native_process.rollout) {
|
|
11222
|
+
throw new Error("virgin Codex Session has a partial recovered native identity");
|
|
11223
|
+
}
|
|
11224
|
+
if (turnNativeThreadId && sessionNativeThreadId) {
|
|
11225
|
+
assertTurnBindingCurrent(current, "recover virgin Codex binding for");
|
|
11226
|
+
return { conversation: current, state: "already_bound" };
|
|
11227
|
+
}
|
|
11228
|
+
const preferredSessionId = turnNativeThreadId ?? sessionNativeThreadId;
|
|
11229
|
+
const identity = await resolveCurrentNativeAgentSessionIdentity({
|
|
11230
|
+
options,
|
|
11231
|
+
agent: "codex",
|
|
11232
|
+
pid,
|
|
11233
|
+
cwd: terminalControl.currentPath,
|
|
11234
|
+
preferredSessionId
|
|
11235
|
+
});
|
|
11236
|
+
if (!identity) {
|
|
11237
|
+
return { conversation: current, state: "pending" };
|
|
11238
|
+
}
|
|
11239
|
+
if (!isExactNativeThreadId(identity.sessionId) ||
|
|
11240
|
+
identity.processUuid !== anchorProcessUuid ||
|
|
11241
|
+
identity.processBirth !== anchorProcessBirth ||
|
|
11242
|
+
!isCompleteNativeRollout(identity.rollout) ||
|
|
11243
|
+
(preferredSessionId && identity.sessionId !== preferredSessionId) ||
|
|
11244
|
+
(turnNativeThreadId &&
|
|
11245
|
+
!exactNativeRolloutMatches(takeover?.terminal_agent_rollout, identity.rollout)) ||
|
|
11246
|
+
(sessionNativeThreadId &&
|
|
11247
|
+
!exactNativeRolloutMatches(binding.native_process.rollout, identity.rollout))) {
|
|
11248
|
+
throw new Error("virgin Codex native identity changed before binding recovery");
|
|
11249
|
+
}
|
|
11250
|
+
// When neither side was committed, only the exact native acceptance
|
|
11251
|
+
// record may tell us that this newly materialized UUID belongs to the
|
|
11252
|
+
// dispatched request. If either side was already committed before the
|
|
11253
|
+
// crash, that durable CAS plus a fresh exact identity is sufficient to
|
|
11254
|
+
// finish the other side while acceptance is still being written.
|
|
11255
|
+
if (!turnNativeThreadId && !sessionNativeThreadId) {
|
|
11256
|
+
const acceptance = detectCodexRolloutAcceptance({
|
|
11257
|
+
anchor: anchor,
|
|
11258
|
+
currentIdentity: identity,
|
|
11259
|
+
requestHash
|
|
11260
|
+
});
|
|
11261
|
+
if (!acceptance) {
|
|
11262
|
+
return { conversation: current, state: "pending" };
|
|
11263
|
+
}
|
|
11264
|
+
}
|
|
11265
|
+
await assertNativeThreadHasExclusiveOwnership({
|
|
11266
|
+
options,
|
|
11267
|
+
agent: "codex",
|
|
11268
|
+
currentPid: pid,
|
|
11269
|
+
nativeThreadId: identity.sessionId,
|
|
11270
|
+
storeDir,
|
|
11271
|
+
terminalControl,
|
|
11272
|
+
excludedManagedSessionId: session.session_id
|
|
11273
|
+
});
|
|
11274
|
+
if (!sessionNativeThreadId) {
|
|
11275
|
+
const persistedSession = persistManagedSessionNativeIdentity({
|
|
11276
|
+
conversation: current,
|
|
11277
|
+
terminalControl,
|
|
11278
|
+
identity
|
|
11279
|
+
});
|
|
11280
|
+
if (persistedSession?.binding?.native_thread_id !==
|
|
11281
|
+
identity.sessionId ||
|
|
11282
|
+
persistedSession.binding.native_process.process_uuid !==
|
|
11283
|
+
identity.processUuid ||
|
|
11284
|
+
persistedSession.binding.native_process.process_birth !==
|
|
11285
|
+
identity.processBirth ||
|
|
11286
|
+
!exactNativeRolloutMatches(persistedSession.binding.native_process.rollout, identity.rollout)) {
|
|
11287
|
+
throw new Error("virgin Codex Session identity was not durably committed during recovery");
|
|
11288
|
+
}
|
|
11289
|
+
}
|
|
11290
|
+
const recoveredAt = cliNow().toISOString();
|
|
11291
|
+
const recoveredConversation = turnNativeThreadId
|
|
11292
|
+
? current
|
|
11293
|
+
: {
|
|
11294
|
+
...withNativeAgentSessionIdentity(current, identity),
|
|
11295
|
+
updated_at: recoveredAt
|
|
11296
|
+
};
|
|
11297
|
+
if (!turnNativeThreadId) {
|
|
11298
|
+
saveState(statePath, recoveredConversation);
|
|
11299
|
+
}
|
|
11300
|
+
assertNativeAgentIdentityForTurn({
|
|
11301
|
+
conversation: recoveredConversation,
|
|
11302
|
+
currentIdentity: identity,
|
|
11303
|
+
operation: "recover virgin Codex binding for"
|
|
11304
|
+
});
|
|
11305
|
+
try {
|
|
11306
|
+
appendEvent(logPath, {
|
|
11307
|
+
ts: recoveredAt,
|
|
11308
|
+
conversation_id: recoveredConversation.conversation_id,
|
|
11309
|
+
event: "virgin_codex_post_submission_binding_recovered",
|
|
11310
|
+
message_id: messageId,
|
|
11311
|
+
native_thread_id: identity.sessionId,
|
|
11312
|
+
terminal_control: terminalControl
|
|
11313
|
+
});
|
|
11314
|
+
}
|
|
11315
|
+
catch (error) {
|
|
11316
|
+
runtimeLog("warn", "virgin_codex_binding_recovery_event_failed", {
|
|
11317
|
+
conversation_id: recoveredConversation.conversation_id,
|
|
11318
|
+
terminal_target: terminalControl.target,
|
|
11319
|
+
error: error instanceof Error ? error.message : String(error)
|
|
11320
|
+
});
|
|
11321
|
+
}
|
|
11322
|
+
return { conversation: recoveredConversation, state: "recovered" };
|
|
11323
|
+
}
|
|
11324
|
+
finally {
|
|
11325
|
+
releaseStateLock();
|
|
11326
|
+
}
|
|
11327
|
+
});
|
|
11328
|
+
}
|
|
11329
|
+
finally {
|
|
11330
|
+
releaseTerminalLock();
|
|
11331
|
+
}
|
|
11080
11332
|
}
|
|
11081
11333
|
async function detectTerminalSubmissionAcceptance({ options, executor, conversation, terminalControl }) {
|
|
11082
11334
|
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
@@ -11187,12 +11439,57 @@ async function pollTerminalSubmissionAcceptance({ options, executor, conversatio
|
|
|
11187
11439
|
return { outcome: "pending_acceptance" };
|
|
11188
11440
|
}
|
|
11189
11441
|
async function reconcileTerminalAcceptanceInMonitor({ options, conversation, statePath, logPath, terminalControl, executor, terminalBridge }) {
|
|
11190
|
-
|
|
11442
|
+
let bindingRecovery = await recoverVirginCodexPostSubmissionBinding({
|
|
11443
|
+
options,
|
|
11444
|
+
conversation,
|
|
11445
|
+
statePath,
|
|
11446
|
+
logPath,
|
|
11447
|
+
terminalLockHeld: true
|
|
11448
|
+
});
|
|
11449
|
+
conversation = bindingRecovery.conversation;
|
|
11450
|
+
let evidence = await detectTerminalSubmissionAcceptance({
|
|
11191
11451
|
options,
|
|
11192
11452
|
executor: executor.kind,
|
|
11193
11453
|
conversation,
|
|
11194
11454
|
terminalControl
|
|
11195
11455
|
});
|
|
11456
|
+
const recoveredAnchor = isRecord(conversation.native_session_takeover) &&
|
|
11457
|
+
isRecord(conversation.native_session_takeover.codex_rollout_acceptance_anchor)
|
|
11458
|
+
? conversation.native_session_takeover.codex_rollout_acceptance_anchor
|
|
11459
|
+
: undefined;
|
|
11460
|
+
if (evidence &&
|
|
11461
|
+
recoveredAnchor?.version === 2 &&
|
|
11462
|
+
bindingRecovery.state === "not_applicable") {
|
|
11463
|
+
throw new Error("virgin Codex acceptance appeared without a recoverable Session/Turn binding");
|
|
11464
|
+
}
|
|
11465
|
+
if (evidence &&
|
|
11466
|
+
recoveredAnchor?.version === 2 &&
|
|
11467
|
+
bindingRecovery.state === "pending") {
|
|
11468
|
+
// The session_meta and exact user record can land between the first
|
|
11469
|
+
// recovery probe and this acceptance probe. Never persist agent_accepted
|
|
11470
|
+
// until the same evidence has also closed the Session/Turn binding CAS.
|
|
11471
|
+
bindingRecovery = await recoverVirginCodexPostSubmissionBinding({
|
|
11472
|
+
options,
|
|
11473
|
+
conversation,
|
|
11474
|
+
statePath,
|
|
11475
|
+
logPath,
|
|
11476
|
+
terminalLockHeld: true
|
|
11477
|
+
});
|
|
11478
|
+
if (bindingRecovery.state !== "recovered" &&
|
|
11479
|
+
bindingRecovery.state !== "already_bound") {
|
|
11480
|
+
return { outcome: "pending" };
|
|
11481
|
+
}
|
|
11482
|
+
conversation = bindingRecovery.conversation;
|
|
11483
|
+
evidence = await detectTerminalSubmissionAcceptance({
|
|
11484
|
+
options,
|
|
11485
|
+
executor: executor.kind,
|
|
11486
|
+
conversation,
|
|
11487
|
+
terminalControl
|
|
11488
|
+
});
|
|
11489
|
+
if (!evidence) {
|
|
11490
|
+
return { outcome: "pending" };
|
|
11491
|
+
}
|
|
11492
|
+
}
|
|
11196
11493
|
const submission = terminalBridgeSubmission(conversation);
|
|
11197
11494
|
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
11198
11495
|
? conversation.native_session_takeover
|
|
@@ -12331,12 +12628,18 @@ async function reconcileMonitors(options, { includeCallbackRecovery, reason, con
|
|
|
12331
12628
|
ignored += 1;
|
|
12332
12629
|
continue;
|
|
12333
12630
|
}
|
|
12334
|
-
|
|
12631
|
+
let initialConversation = await migrateLegacyTerminalAgentIdentity({
|
|
12335
12632
|
conversation: loadState(statePath),
|
|
12336
12633
|
statePath,
|
|
12337
12634
|
logPath,
|
|
12338
12635
|
options
|
|
12339
12636
|
});
|
|
12637
|
+
initialConversation = (await recoverVirginCodexPostSubmissionBinding({
|
|
12638
|
+
options,
|
|
12639
|
+
conversation: initialConversation,
|
|
12640
|
+
statePath,
|
|
12641
|
+
logPath
|
|
12642
|
+
})).conversation;
|
|
12340
12643
|
assertTurnBindingCurrent(initialConversation, "reconcile monitor for");
|
|
12341
12644
|
const initialEligibility = terminalBridgeReconciliationEligibility(initialConversation);
|
|
12342
12645
|
if (!initialEligibility.eligible) {
|