@scotthuang/agent-knock-knock 0.12.8 → 0.12.11
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 +35 -0
- package/README.md +4 -2
- package/dist/src/claude-local-transcript-provider.d.ts +21 -1
- package/dist/src/claude-local-transcript-provider.js +121 -15
- package/dist/src/claude-local-transcript-provider.js.map +1 -1
- package/dist/src/claude-terminal-agent-adapter.js +20 -5
- package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
- package/dist/src/cli-core.js +1531 -327
- package/dist/src/cli-core.js.map +1 -1
- package/dist/src/codex-terminal-agent-adapter.d.ts +2 -1
- package/dist/src/codex-terminal-agent-adapter.js +134 -38
- package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-adapter.d.ts +21 -0
- package/dist/src/terminal-agent-adapter.js +20 -0
- package/dist/src/terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-bridge.js +75 -9
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-process-source.d.ts +7 -0
- package/dist/src/terminal-process-source.js +50 -1
- package/dist/src/terminal-process-source.js.map +1 -1
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +4 -2
package/dist/src/cli-core.js
CHANGED
|
@@ -8,7 +8,7 @@ import { fileURLToPath } from "node:url";
|
|
|
8
8
|
import { createCodexTerminalAgentAdapter, detectCodexDurableCompletion } from "./codex-terminal-agent-adapter.js";
|
|
9
9
|
import { codexLifecycleBehaviorProfile } from "./codex-lifecycle-compatibility.js";
|
|
10
10
|
import { createClaudeTerminalAgentAdapter } from "./claude-terminal-agent-adapter.js";
|
|
11
|
-
import { captureClaudeTranscriptAnchor, defaultClaudeHome, detectClaudeTranscriptAcceptance, detectClaudeTranscriptCompletion, detectClaudeTranscriptPendingApproval, listClaudeThreadLifecycleCandidates, revalidateClaudeThreadLifecycleCandidate } from "./claude-local-transcript-provider.js";
|
|
11
|
+
import { captureClaudeTranscriptAnchor, defaultClaudeHome, detectClaudeTranscriptAcceptance, detectClaudeTranscriptCompletion, detectClaudeTranscriptPendingApproval, listClaudeThreadLifecycleCandidates, observeClaudeDeadProcessTranscriptCompletion, revalidateClaudeThreadLifecycleCandidate } from "./claude-local-transcript-provider.js";
|
|
12
12
|
import { captureCodexCandidateSetRolloutAcceptanceAnchor, captureCodexRolloutAcceptanceAnchor, detectCodexBoundRolloutCompletion, detectCodexCandidateSetRolloutAcceptance, detectCodexRolloutAcceptance, terminalSubmissionReplayReceipt, validateCodexRolloutAcceptanceAnchor, validateTerminalSubmissionAcceptanceEvidence } from "./terminal-submission-acceptance.js";
|
|
13
13
|
import { CodexLocalSessionProvider } from "./codex-local-session-provider.js";
|
|
14
14
|
import { CodexStoreAdapter } from "./codex-store-adapter.js";
|
|
@@ -5853,7 +5853,7 @@ function managedListApprovalState(conversation) {
|
|
|
5853
5853
|
}
|
|
5854
5854
|
function listActionContracts() {
|
|
5855
5855
|
return {
|
|
5856
|
-
version:
|
|
5856
|
+
version: 16,
|
|
5857
5857
|
instructions: [
|
|
5858
5858
|
"Treat terminals[] as the primary resource and use only actions present in available_actions, except the snapshot-bound terminals[].handoff_decision.choices.take_over_current.action and an exact terminals[].blocking_turns[].recovery_action. Either nested action requires explicit user confirmation; after it succeeds, refresh list before any follow-current send.",
|
|
5859
5859
|
"The session_exact scope uses session_id only when it is prefilled by the listed send action. A rollout-backed managed Codex pane instead uses the terminal_follow_current scope with its exact selector plus expected_terminal_token because even one materialized rollout does not prove the current TUI foreground thread. A turn id is never an ordinary send target.",
|
|
@@ -5864,6 +5864,7 @@ function listActionContracts() {
|
|
|
5864
5864
|
"List resumable threads before resume; use only a complete native_thread_id and the action returned for that candidate.",
|
|
5865
5865
|
"Use a terminal selector only when explicitly named by the user or prefilled by that terminal row's send action. A handoff action also carries expected_terminal_token; never infer, guess, or reuse either value.",
|
|
5866
5866
|
"Use respond only for an in-flight turn that is explicitly waiting for OpenClaw.",
|
|
5867
|
+
"Approval fingerprint v2 binds stable terminal, process, and action fields plus an unredacted exact prompt-region digest. Whole-screen digests and scrollback excerpts are diagnostic only and never authorize approval. A keys-mode prompt without exact prompt-region evidence is not approvable and exposes no approve action.",
|
|
5867
5868
|
"Managed controls target turn_id. A raw terminal may be controlled only through its own list-prefilled conversation_id action. When a Codex managed prompt has no usable AKK Turn owner, list may expose one manual terminal-scoped approve carrying expected_terminal_token; copy the complete action after explicit human confirmation. It does not mutate the Turn or Session binding, has no durable dispatch receipt, and is never eligible for automatic approval. If its result is interrupted, refresh status and inspect the live prompt instead of retrying blindly. Never construct, guess, or reuse either selector or token.",
|
|
5868
5869
|
"Start with the action's prefilled arguments, supply every missing_required field, and consult the top-level action's optional fields only when needed.",
|
|
5869
5870
|
"Authoritative full IDs are prefilled; short_ref is for display and human input.",
|
|
@@ -6023,6 +6024,9 @@ function listActionContracts() {
|
|
|
6023
6024
|
required: ["turn_id", "expected_approval_fingerprint"],
|
|
6024
6025
|
optional: ["expected_terminal_token"],
|
|
6025
6026
|
terminal_scoped_scope: "Only the latest list-prefilled Codex conversation_id/token action may approve a managed current prompt without a usable Turn owner. It remains human-confirmed, revalidates exact terminal/process/Session/dispatch/prompt state before keys, never mutates managed state, has no durable dispatch receipt, cannot be auto-approved, and must not be blindly retried after an interrupted result.",
|
|
6027
|
+
fingerprint_contract: "v2_prompt_region",
|
|
6028
|
+
fingerprint_authority: "Stable terminal, process, and action fields plus the unredacted exact prompt-region digest; whole-screen digests and scrollback excerpts are diagnostic only.",
|
|
6029
|
+
missing_prompt_region_evidence: "not_approvable",
|
|
6026
6030
|
requires_explicit_user_confirmation: true,
|
|
6027
6031
|
requires_fresh_status: true
|
|
6028
6032
|
},
|
|
@@ -6195,7 +6199,8 @@ function availableListActions(entry, { conversation } = {}) {
|
|
|
6195
6199
|
}
|
|
6196
6200
|
if (terminalBridgeReady &&
|
|
6197
6201
|
entry.status === "stalled" &&
|
|
6198
|
-
terminalBridgeSubmission(conversation)?.status !== "uncertain"
|
|
6202
|
+
terminalBridgeSubmission(conversation)?.status !== "uncertain" &&
|
|
6203
|
+
!isVerifiedDeadTerminalAgentProcess(conversation ?? {})) {
|
|
6199
6204
|
actions.renew = {
|
|
6200
6205
|
tool: "agent_knock_knock_renew",
|
|
6201
6206
|
arguments: targetArguments
|
|
@@ -6592,6 +6597,245 @@ function codexProcessIncarnationForPid(pid) {
|
|
|
6592
6597
|
evidence: "codex_process_birth"
|
|
6593
6598
|
};
|
|
6594
6599
|
}
|
|
6600
|
+
function observeExactCodexDeadProcessRolloutCompletion({ options, conversation, terminalControl }) {
|
|
6601
|
+
const takeover = isRecord(conversation.native_session_takeover)
|
|
6602
|
+
? conversation.native_session_takeover
|
|
6603
|
+
: undefined;
|
|
6604
|
+
const submission = terminalBridgeSubmission(conversation);
|
|
6605
|
+
const acceptanceEvidence = isRecord(submission?.acceptance_evidence)
|
|
6606
|
+
? submission.acceptance_evidence
|
|
6607
|
+
: undefined;
|
|
6608
|
+
const anchor = isRecord(takeover?.codex_rollout_acceptance_anchor)
|
|
6609
|
+
? takeover.codex_rollout_acceptance_anchor
|
|
6610
|
+
: undefined;
|
|
6611
|
+
if (submission?.status !== "agent_accepted" ||
|
|
6612
|
+
!requiresExactBoundCodexCompletion(conversation, options) ||
|
|
6613
|
+
acceptanceEvidence?.source !== "codex_rollout" ||
|
|
6614
|
+
!anchor) {
|
|
6615
|
+
return {
|
|
6616
|
+
status: "unverifiable",
|
|
6617
|
+
reason: "Codex has no exact accepted rollout authority for the dead process"
|
|
6618
|
+
};
|
|
6619
|
+
}
|
|
6620
|
+
const runtime = terminalRuntimeIdentityForConversation(conversation, terminalControl);
|
|
6621
|
+
const nativeRollout = isRecord(runtime.nativeRollout)
|
|
6622
|
+
? runtime.nativeRollout
|
|
6623
|
+
: undefined;
|
|
6624
|
+
const request = terminalDurableRequestForConversation(conversation, terminalControl);
|
|
6625
|
+
const result = detectCodexBoundRolloutCompletion({
|
|
6626
|
+
anchor: anchor,
|
|
6627
|
+
acceptanceEvidence: acceptanceEvidence,
|
|
6628
|
+
currentIdentity: {
|
|
6629
|
+
sessionId: runtime.nativeSessionId ?? runtime.sessionId ?? "",
|
|
6630
|
+
processUuid: runtime.nativeProcessUuid,
|
|
6631
|
+
processBirth: runtime.nativeProcessBirth,
|
|
6632
|
+
...(nativeRollout
|
|
6633
|
+
? {
|
|
6634
|
+
rollout: {
|
|
6635
|
+
fd: String(nativeRollout.fd ?? ""),
|
|
6636
|
+
device: String(nativeRollout.device ?? ""),
|
|
6637
|
+
inode: String(nativeRollout.inode ?? ""),
|
|
6638
|
+
path: String(nativeRollout.path ?? "")
|
|
6639
|
+
}
|
|
6640
|
+
}
|
|
6641
|
+
: {})
|
|
6642
|
+
},
|
|
6643
|
+
requestHash: stringValue(request.requestHash) ??
|
|
6644
|
+
stringValue(takeover?.terminal_bridge_request_hash) ??
|
|
6645
|
+
""
|
|
6646
|
+
});
|
|
6647
|
+
if (result.status === "completed") {
|
|
6648
|
+
return {
|
|
6649
|
+
status: "present",
|
|
6650
|
+
completion: {
|
|
6651
|
+
...result.completion,
|
|
6652
|
+
metadata: {
|
|
6653
|
+
...result.completion.metadata,
|
|
6654
|
+
context_match: "exact_bound_rollout",
|
|
6655
|
+
detector_code: result.diagnostics.code
|
|
6656
|
+
}
|
|
6657
|
+
}
|
|
6658
|
+
};
|
|
6659
|
+
}
|
|
6660
|
+
if (result.status === "pending" &&
|
|
6661
|
+
result.diagnostics.code === "exact_turn_not_complete") {
|
|
6662
|
+
return { status: "absent" };
|
|
6663
|
+
}
|
|
6664
|
+
return {
|
|
6665
|
+
status: "unverifiable",
|
|
6666
|
+
reason: `[codex_exact_bound_rollout:${result.diagnostics.code}] ` +
|
|
6667
|
+
(result.diagnostics.detail ??
|
|
6668
|
+
"the exact bound rollout is not safely inspectable")
|
|
6669
|
+
};
|
|
6670
|
+
}
|
|
6671
|
+
async function observeDurableCompletionBeforeDeadStall({ options, conversation, terminalControl }) {
|
|
6672
|
+
try {
|
|
6673
|
+
const executor = executorForConversation(conversation);
|
|
6674
|
+
const takeover = isRecord(conversation.native_session_takeover)
|
|
6675
|
+
? conversation.native_session_takeover
|
|
6676
|
+
: undefined;
|
|
6677
|
+
if (executor.kind === "claude") {
|
|
6678
|
+
const anchor = isRecord(takeover?.claude_transcript_anchor)
|
|
6679
|
+
? takeover.claude_transcript_anchor
|
|
6680
|
+
: undefined;
|
|
6681
|
+
const pid = Number(anchor?.pid);
|
|
6682
|
+
const startedAt = Number(anchor?.agent_started_at_ms);
|
|
6683
|
+
const sessionId = stringValue(anchor?.session_id);
|
|
6684
|
+
const cwd = stringValue(anchor?.cwd);
|
|
6685
|
+
if (!Number.isSafeInteger(pid) ||
|
|
6686
|
+
pid <= 1 ||
|
|
6687
|
+
!Number.isSafeInteger(startedAt) ||
|
|
6688
|
+
startedAt <= 0 ||
|
|
6689
|
+
!sessionId ||
|
|
6690
|
+
!cwd ||
|
|
6691
|
+
pid !== Number(takeover?.terminal_agent_pid) ||
|
|
6692
|
+
sessionId !== stringValue(takeover?.terminal_agent_session_id) ||
|
|
6693
|
+
path.resolve(cwd) !== path.resolve(conversation.workspace)) {
|
|
6694
|
+
return {
|
|
6695
|
+
status: "unverifiable",
|
|
6696
|
+
reason: "Claude has no exact immutable transcript anchor for the dead process"
|
|
6697
|
+
};
|
|
6698
|
+
}
|
|
6699
|
+
const processUuid = stringValue(takeover?.terminal_agent_process_uuid);
|
|
6700
|
+
if (processUuid !== `claude-pid:${pid}:started:${startedAt}`) {
|
|
6701
|
+
return {
|
|
6702
|
+
status: "unverifiable",
|
|
6703
|
+
reason: "Claude transcript anchor does not match the exact process incarnation"
|
|
6704
|
+
};
|
|
6705
|
+
}
|
|
6706
|
+
const submission = terminalBridgeSubmission(conversation);
|
|
6707
|
+
const observation = observeClaudeDeadProcessTranscriptCompletion(terminalDurableRequestForConversation(conversation, terminalControl), {
|
|
6708
|
+
claudeHome: expandHome(options.claudeHome),
|
|
6709
|
+
// Process death freezes this exact historical incarnation. The
|
|
6710
|
+
// transcript detector still revalidates the immutable PID/session/
|
|
6711
|
+
// cwd/start anchor and the no-follow file boundary before reading.
|
|
6712
|
+
agentRows: [{
|
|
6713
|
+
pid,
|
|
6714
|
+
startedAt,
|
|
6715
|
+
sessionId,
|
|
6716
|
+
cwd,
|
|
6717
|
+
kind: "interactive",
|
|
6718
|
+
status: "idle"
|
|
6719
|
+
}],
|
|
6720
|
+
acceptanceEvidence: submission?.acceptance_evidence
|
|
6721
|
+
});
|
|
6722
|
+
return observation;
|
|
6723
|
+
}
|
|
6724
|
+
return observeExactCodexDeadProcessRolloutCompletion({
|
|
6725
|
+
options,
|
|
6726
|
+
conversation,
|
|
6727
|
+
terminalControl
|
|
6728
|
+
});
|
|
6729
|
+
}
|
|
6730
|
+
catch (error) {
|
|
6731
|
+
return {
|
|
6732
|
+
status: "unverifiable",
|
|
6733
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
6734
|
+
};
|
|
6735
|
+
}
|
|
6736
|
+
}
|
|
6737
|
+
async function observeBoundTerminalAgentProcess({ options, conversation, terminalControl }) {
|
|
6738
|
+
try {
|
|
6739
|
+
assertTurnBindingCurrent(conversation, "verify the bound agent process for");
|
|
6740
|
+
const storeDir = managedSessionStoreDirForConversation(conversation);
|
|
6741
|
+
if (!storeDir) {
|
|
6742
|
+
return {
|
|
6743
|
+
status: "unverifiable",
|
|
6744
|
+
reason: "the managed Store is unavailable"
|
|
6745
|
+
};
|
|
6746
|
+
}
|
|
6747
|
+
const session = loadManagedSession(storeDir, sessionIdForConversation(conversation));
|
|
6748
|
+
const binding = session.binding;
|
|
6749
|
+
const takeover = isRecord(conversation.native_session_takeover)
|
|
6750
|
+
? conversation.native_session_takeover
|
|
6751
|
+
: undefined;
|
|
6752
|
+
const pid = Number(takeover?.terminal_agent_pid);
|
|
6753
|
+
const bindingId = stringValue(conversation.terminal_binding_id);
|
|
6754
|
+
const bindingGeneration = Number(conversation.terminal_binding_generation);
|
|
6755
|
+
const processUuid = stringValue(takeover?.terminal_agent_process_uuid);
|
|
6756
|
+
const processBirth = stringValue(takeover?.terminal_agent_process_birth);
|
|
6757
|
+
const messageId = stringValue(takeover?.terminal_bridge_message_id);
|
|
6758
|
+
const submission = terminalBridgeSubmission(conversation);
|
|
6759
|
+
if (session.status !== "bound" ||
|
|
6760
|
+
!binding ||
|
|
6761
|
+
!Number.isSafeInteger(pid) ||
|
|
6762
|
+
pid <= 1 ||
|
|
6763
|
+
!bindingId ||
|
|
6764
|
+
!Number.isSafeInteger(bindingGeneration) ||
|
|
6765
|
+
bindingGeneration < 1 ||
|
|
6766
|
+
!processUuid ||
|
|
6767
|
+
!messageId ||
|
|
6768
|
+
!hasCanonicalTerminalEndpoint(terminalControl) ||
|
|
6769
|
+
binding.native_process.pid !== pid ||
|
|
6770
|
+
!terminalControlsShareIncarnation(binding.terminal_control, terminalControl) ||
|
|
6771
|
+
binding.binding_id !== bindingId ||
|
|
6772
|
+
binding.generation !== bindingGeneration ||
|
|
6773
|
+
session.agent !== executorForConversation(conversation).kind ||
|
|
6774
|
+
path.resolve(session.workspace) !== path.resolve(conversation.workspace) ||
|
|
6775
|
+
binding.native_thread_id !==
|
|
6776
|
+
(stringValue(conversation.native_thread_id) ??
|
|
6777
|
+
stringValue(takeover?.terminal_agent_session_id)) ||
|
|
6778
|
+
binding.native_process.process_uuid !== processUuid ||
|
|
6779
|
+
stringValue(takeover?.terminal_binding_id) !== bindingId ||
|
|
6780
|
+
Number(takeover?.terminal_binding_generation) !== bindingGeneration ||
|
|
6781
|
+
submission?.status !== "agent_accepted" ||
|
|
6782
|
+
stringValue(submission.session_id) !==
|
|
6783
|
+
sessionIdForConversation(conversation) ||
|
|
6784
|
+
stringValue(submission.turn_id) !== turnIdForConversation(conversation) ||
|
|
6785
|
+
stringValue(submission.message_id) !== messageId ||
|
|
6786
|
+
stringValue(submission.binding_id) !== bindingId ||
|
|
6787
|
+
Number(submission.binding_generation) !== bindingGeneration ||
|
|
6788
|
+
(binding.native_process.process_birth ?? undefined) !== processBirth) {
|
|
6789
|
+
return {
|
|
6790
|
+
status: "unverifiable",
|
|
6791
|
+
reason: "the Turn and Session no longer share one exact process binding"
|
|
6792
|
+
};
|
|
6793
|
+
}
|
|
6794
|
+
const processSource = createTerminalProcessSource(options);
|
|
6795
|
+
if (processSource.completeInventoryAuthority !== true) {
|
|
6796
|
+
return {
|
|
6797
|
+
status: "unverifiable",
|
|
6798
|
+
reason: "the configured process source is not complete process-death authority"
|
|
6799
|
+
};
|
|
6800
|
+
}
|
|
6801
|
+
const snapshots = await processSource.listProcessSnapshots((snapshot) => snapshot.pid === pid, { includeCwd: false, includeAncestors: false });
|
|
6802
|
+
const exact = snapshots.filter((snapshot) => snapshot.pid === pid);
|
|
6803
|
+
if (exact.length === 1) {
|
|
6804
|
+
return { status: "alive", pid };
|
|
6805
|
+
}
|
|
6806
|
+
if (exact.length !== 0) {
|
|
6807
|
+
return {
|
|
6808
|
+
status: "unverifiable",
|
|
6809
|
+
reason: `the process inventory returned ${exact.length} rows for pid ${pid}`
|
|
6810
|
+
};
|
|
6811
|
+
}
|
|
6812
|
+
return {
|
|
6813
|
+
status: "verified_dead",
|
|
6814
|
+
proof: {
|
|
6815
|
+
kind: "exact_pid_absent_from_complete_process_inventory",
|
|
6816
|
+
agent: session.agent,
|
|
6817
|
+
pid,
|
|
6818
|
+
process_uuid: processUuid,
|
|
6819
|
+
process_birth: binding.native_process.process_birth,
|
|
6820
|
+
conversation_id: conversation.conversation_id,
|
|
6821
|
+
session_id: sessionIdForConversation(conversation),
|
|
6822
|
+
turn_id: turnIdForConversation(conversation),
|
|
6823
|
+
terminal_control: terminalControl,
|
|
6824
|
+
terminal_endpoint: terminalControlEvidence(terminalControl),
|
|
6825
|
+
binding_id: binding.binding_id,
|
|
6826
|
+
binding_generation: binding.generation,
|
|
6827
|
+
message_id: messageId,
|
|
6828
|
+
observed_at: cliNow().toISOString()
|
|
6829
|
+
}
|
|
6830
|
+
};
|
|
6831
|
+
}
|
|
6832
|
+
catch (error) {
|
|
6833
|
+
return {
|
|
6834
|
+
status: "unverifiable",
|
|
6835
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
6836
|
+
};
|
|
6837
|
+
}
|
|
6838
|
+
}
|
|
6595
6839
|
function processIncarnationRelationship({ binding, livePid, liveProcessUuid, liveProcessBirth }) {
|
|
6596
6840
|
if (binding.native_process.pid !== livePid) {
|
|
6597
6841
|
return "different";
|
|
@@ -12427,6 +12671,10 @@ function terminalBridgeApprovalFingerprint({ terminalControl, terminalStatus })
|
|
|
12427
12671
|
if (adapterFingerprint) {
|
|
12428
12672
|
return adapterFingerprint;
|
|
12429
12673
|
}
|
|
12674
|
+
if (approval.approvable === true &&
|
|
12675
|
+
(stringValue(approval.decision_mode) ?? "keys") === "keys") {
|
|
12676
|
+
return undefined;
|
|
12677
|
+
}
|
|
12430
12678
|
const endpoint = terminalEndpointFromControlRef(terminalControl);
|
|
12431
12679
|
const processAnchorPid = Number(endpoint.processAnchorPid);
|
|
12432
12680
|
if (!Number.isSafeInteger(processAnchorPid) || processAnchorPid <= 1) {
|
|
@@ -13808,331 +14056,337 @@ async function runApprove(options) {
|
|
|
13808
14056
|
release();
|
|
13809
14057
|
}
|
|
13810
14058
|
};
|
|
13811
|
-
releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
13812
14059
|
const writerStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
13813
|
-
|
|
13814
|
-
|
|
13815
|
-
|
|
13816
|
-
|
|
13817
|
-
|
|
13818
|
-
|
|
13819
|
-
|
|
13820
|
-
|
|
13821
|
-
|
|
13822
|
-
|
|
13823
|
-
|
|
13824
|
-
|
|
13825
|
-
|
|
13826
|
-
|
|
13827
|
-
|
|
13828
|
-
|
|
13829
|
-
|
|
13830
|
-
|
|
13831
|
-
|
|
13832
|
-
|
|
13833
|
-
|
|
13834
|
-
|
|
13835
|
-
|
|
13836
|
-
|
|
13837
|
-
|
|
13838
|
-
|
|
13839
|
-
|
|
13840
|
-
|
|
13841
|
-
|
|
13842
|
-
|
|
13843
|
-
|
|
13844
|
-
|
|
13845
|
-
|
|
13846
|
-
|
|
13847
|
-
|
|
13848
|
-
|
|
14060
|
+
const runApprovalWithStateLock = async () => {
|
|
14061
|
+
let approval;
|
|
14062
|
+
let lockedConversation = conversation;
|
|
14063
|
+
const currentConversation = loadState(statePath);
|
|
14064
|
+
const currentTakeover = isRecord(currentConversation.native_session_takeover)
|
|
14065
|
+
? currentConversation.native_session_takeover
|
|
14066
|
+
: undefined;
|
|
14067
|
+
const currentControl = terminalControlFromTakeover(currentTakeover);
|
|
14068
|
+
const currentApproval = isRecord(currentTakeover?.terminal_bridge_approval)
|
|
14069
|
+
? currentTakeover.terminal_bridge_approval
|
|
14070
|
+
: undefined;
|
|
14071
|
+
if (currentConversation.status !== conversation.status ||
|
|
14072
|
+
currentTakeover?.terminal_bridge_message_id !== nativeTakeover?.terminal_bridge_message_id ||
|
|
14073
|
+
!currentControl ||
|
|
14074
|
+
!terminalControlsShareIncarnation(currentControl, terminalControl) ||
|
|
14075
|
+
(claudeScreenApproval &&
|
|
14076
|
+
currentApproval?.fingerprint !== monitoredApproval?.fingerprint)) {
|
|
14077
|
+
throw new Error("approval state changed while waiting for terminal control; refresh status and retry");
|
|
14078
|
+
}
|
|
14079
|
+
assertManagedTerminalDispatchOwner({
|
|
14080
|
+
storeDir: writerStoreDir,
|
|
14081
|
+
conversation: currentConversation,
|
|
14082
|
+
terminalControl: currentControl,
|
|
14083
|
+
action: "approve"
|
|
14084
|
+
});
|
|
14085
|
+
lockedConversation = currentConversation;
|
|
14086
|
+
const currentRuntimeIdentity = terminalRuntimeIdentityForConversation(currentConversation, currentControl);
|
|
14087
|
+
approval = await createTerminalAgentBridge(options).approve(executor.kind, currentControl, {
|
|
14088
|
+
expectedFingerprint,
|
|
14089
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
14090
|
+
runtime: currentRuntimeIdentity,
|
|
14091
|
+
managedRequest: terminalDurableRequestForConversation(currentConversation, currentControl),
|
|
14092
|
+
requiredDecisionMode: autoApproved && executor.kind === "claude" ? "keys" : undefined,
|
|
14093
|
+
authorize: autoApproved
|
|
14094
|
+
? ({ agent, terminalControl: currentTerminalControl, inspection, fingerprint }) => {
|
|
14095
|
+
if (!autoApprovalPolicy) {
|
|
14096
|
+
return {
|
|
14097
|
+
approved: false,
|
|
14098
|
+
reason: "automatic approval requires an executor-side policy"
|
|
14099
|
+
};
|
|
14100
|
+
}
|
|
14101
|
+
const candidate = policyCandidateForInspection({
|
|
14102
|
+
agent,
|
|
14103
|
+
currentTerminalControl,
|
|
14104
|
+
inspection,
|
|
14105
|
+
fingerprint
|
|
14106
|
+
});
|
|
14107
|
+
executorPolicyDecision = evaluateApprovalPolicy({
|
|
14108
|
+
policy: autoApprovalPolicy,
|
|
14109
|
+
candidate
|
|
14110
|
+
});
|
|
14111
|
+
if (executorPolicyDecision.action !== "approve") {
|
|
14112
|
+
return {
|
|
14113
|
+
approved: false,
|
|
14114
|
+
reason: `executor-side auto-approval policy rejected the current request: ${executorPolicyDecision.reason}`
|
|
14115
|
+
};
|
|
14116
|
+
}
|
|
14117
|
+
if (policyRuleId && executorPolicyDecision.ruleId !== policyRuleId) {
|
|
14118
|
+
return {
|
|
14119
|
+
approved: false,
|
|
14120
|
+
reason: "executor-side auto-approval rule changed before execution"
|
|
14121
|
+
};
|
|
14122
|
+
}
|
|
14123
|
+
if (policyFingerprint &&
|
|
14124
|
+
executorPolicyDecision.policyFingerprint !== policyFingerprint) {
|
|
14125
|
+
return {
|
|
14126
|
+
approved: false,
|
|
14127
|
+
reason: "executor-side auto-approval policy changed before execution"
|
|
14128
|
+
};
|
|
14129
|
+
}
|
|
14130
|
+
return { approved: true };
|
|
14131
|
+
}
|
|
14132
|
+
: undefined,
|
|
14133
|
+
beforeKeyDispatch: claudeScreenApproval
|
|
14134
|
+
? ({ fingerprint, terminalControl: dispatchControl, inspection, keys }) => {
|
|
14135
|
+
if (autoApproved) {
|
|
13849
14136
|
if (!autoApprovalPolicy) {
|
|
13850
|
-
|
|
13851
|
-
approved: false,
|
|
13852
|
-
reason: "automatic approval requires an executor-side policy"
|
|
13853
|
-
};
|
|
14137
|
+
throw new Error("automatic approval requires an executor-side policy before dispatch");
|
|
13854
14138
|
}
|
|
13855
|
-
const
|
|
13856
|
-
agent,
|
|
13857
|
-
currentTerminalControl,
|
|
13858
|
-
inspection,
|
|
13859
|
-
fingerprint
|
|
13860
|
-
});
|
|
13861
|
-
executorPolicyDecision = evaluateApprovalPolicy({
|
|
14139
|
+
const freshPolicyDecision = evaluateApprovalPolicy({
|
|
13862
14140
|
policy: autoApprovalPolicy,
|
|
13863
|
-
candidate
|
|
14141
|
+
candidate: policyCandidateForInspection({
|
|
14142
|
+
agent: executor.kind,
|
|
14143
|
+
currentTerminalControl: dispatchControl,
|
|
14144
|
+
inspection,
|
|
14145
|
+
fingerprint
|
|
14146
|
+
})
|
|
13864
14147
|
});
|
|
13865
|
-
if (
|
|
13866
|
-
|
|
13867
|
-
approved: false,
|
|
13868
|
-
reason: `executor-side auto-approval policy rejected the current request: ${executorPolicyDecision.reason}`
|
|
13869
|
-
};
|
|
14148
|
+
if (freshPolicyDecision.action !== "approve") {
|
|
14149
|
+
throw new Error(`executor-side auto-approval policy rejected the recaptured request: ${freshPolicyDecision.reason}`);
|
|
13870
14150
|
}
|
|
13871
|
-
if (
|
|
13872
|
-
|
|
13873
|
-
|
|
13874
|
-
|
|
13875
|
-
|
|
14151
|
+
if (executorPolicyDecision?.ruleId &&
|
|
14152
|
+
freshPolicyDecision.ruleId !== executorPolicyDecision.ruleId) {
|
|
14153
|
+
throw new Error("executor-side auto-approval rule changed after recapture");
|
|
14154
|
+
}
|
|
14155
|
+
if (policyRuleId && freshPolicyDecision.ruleId !== policyRuleId) {
|
|
14156
|
+
throw new Error("executor-side auto-approval rule changed before dispatch");
|
|
13876
14157
|
}
|
|
13877
14158
|
if (policyFingerprint &&
|
|
13878
|
-
|
|
13879
|
-
|
|
13880
|
-
approved: false,
|
|
13881
|
-
reason: "executor-side auto-approval policy changed before execution"
|
|
13882
|
-
};
|
|
14159
|
+
freshPolicyDecision.policyFingerprint !== policyFingerprint) {
|
|
14160
|
+
throw new Error("executor-side auto-approval policy changed before dispatch");
|
|
13883
14161
|
}
|
|
13884
|
-
|
|
14162
|
+
executorPolicyDecision = freshPolicyDecision;
|
|
13885
14163
|
}
|
|
13886
|
-
|
|
13887
|
-
|
|
13888
|
-
? ({ fingerprint, terminalControl: dispatchControl, inspection, keys }) => {
|
|
13889
|
-
if (autoApproved) {
|
|
13890
|
-
if (!autoApprovalPolicy) {
|
|
13891
|
-
throw new Error("automatic approval requires an executor-side policy before dispatch");
|
|
13892
|
-
}
|
|
13893
|
-
const freshPolicyDecision = evaluateApprovalPolicy({
|
|
13894
|
-
policy: autoApprovalPolicy,
|
|
13895
|
-
candidate: policyCandidateForInspection({
|
|
13896
|
-
agent: executor.kind,
|
|
13897
|
-
currentTerminalControl: dispatchControl,
|
|
13898
|
-
inspection,
|
|
13899
|
-
fingerprint
|
|
13900
|
-
})
|
|
13901
|
-
});
|
|
13902
|
-
if (freshPolicyDecision.action !== "approve") {
|
|
13903
|
-
throw new Error(`executor-side auto-approval policy rejected the recaptured request: ${freshPolicyDecision.reason}`);
|
|
13904
|
-
}
|
|
13905
|
-
if (executorPolicyDecision?.ruleId &&
|
|
13906
|
-
freshPolicyDecision.ruleId !== executorPolicyDecision.ruleId) {
|
|
13907
|
-
throw new Error("executor-side auto-approval rule changed after recapture");
|
|
13908
|
-
}
|
|
13909
|
-
if (policyRuleId && freshPolicyDecision.ruleId !== policyRuleId) {
|
|
13910
|
-
throw new Error("executor-side auto-approval rule changed before dispatch");
|
|
13911
|
-
}
|
|
13912
|
-
if (policyFingerprint &&
|
|
13913
|
-
freshPolicyDecision.policyFingerprint !== policyFingerprint) {
|
|
13914
|
-
throw new Error("executor-side auto-approval policy changed before dispatch");
|
|
13915
|
-
}
|
|
13916
|
-
executorPolicyDecision = freshPolicyDecision;
|
|
13917
|
-
}
|
|
13918
|
-
if (approvalDispatchReserved) {
|
|
13919
|
-
throw new Error("Claude approval dispatch was already reserved");
|
|
13920
|
-
}
|
|
13921
|
-
approvalDispatchReserved = true;
|
|
13922
|
-
if (!releaseStateLock) {
|
|
13923
|
-
throw new Error("approval state lock was released before terminal dispatch");
|
|
13924
|
-
}
|
|
13925
|
-
const latestConversation = loadState(statePath);
|
|
13926
|
-
const latestTakeover = isRecord(latestConversation.native_session_takeover)
|
|
13927
|
-
? latestConversation.native_session_takeover
|
|
13928
|
-
: undefined;
|
|
13929
|
-
const latestControl = terminalControlFromTakeover(latestTakeover);
|
|
13930
|
-
const latestApproval = isRecord(latestTakeover?.terminal_bridge_approval)
|
|
13931
|
-
? latestTakeover.terminal_bridge_approval
|
|
13932
|
-
: undefined;
|
|
13933
|
-
const latestNotifiedAt = validTimestampMs(latestApproval?.notified_at);
|
|
13934
|
-
const latestApprovalState = isRecord(latestApproval?.approval_state)
|
|
13935
|
-
? latestApproval.approval_state
|
|
13936
|
-
: undefined;
|
|
13937
|
-
const latestPolicyEvidence = isRecord(latestApprovalState?.policy_evidence)
|
|
13938
|
-
? latestApprovalState.policy_evidence
|
|
13939
|
-
: undefined;
|
|
13940
|
-
const recapturedPolicyEvidence = inspection.approval.approvable
|
|
13941
|
-
? inspection.approval.policyEvidence
|
|
13942
|
-
: undefined;
|
|
13943
|
-
const latestDispatch = isRecord(latestTakeover?.terminal_bridge_approval_dispatch)
|
|
13944
|
-
? latestTakeover.terminal_bridge_approval_dispatch
|
|
13945
|
-
: undefined;
|
|
13946
|
-
if (!latestTakeover ||
|
|
13947
|
-
latestConversation.status !== "waiting_for_openclaw" ||
|
|
13948
|
-
latestTakeover.terminal_bridge_message_id !==
|
|
13949
|
-
nativeTakeover?.terminal_bridge_message_id ||
|
|
13950
|
-
latestApproval?.fingerprint !== fingerprint ||
|
|
13951
|
-
latestNotifiedAt === undefined ||
|
|
13952
|
-
cliNowMs() - latestNotifiedAt > CLAUDE_SCREEN_APPROVAL_TTL_MS ||
|
|
13953
|
-
expectedFingerprint !== fingerprint ||
|
|
13954
|
-
!terminalControlsShareIncarnation(latestControl, dispatchControl) ||
|
|
13955
|
-
(autoApproved &&
|
|
13956
|
-
(latestPolicyEvidence?.source !== "claude_transcript" ||
|
|
13957
|
-
latestPolicyEvidence.evidence_fingerprint !==
|
|
13958
|
-
recapturedPolicyEvidence?.evidenceFingerprint))) {
|
|
13959
|
-
throw new Error("approval state changed before terminal dispatch; refresh status and retry");
|
|
13960
|
-
}
|
|
13961
|
-
if (latestDispatch?.state === "reserved" &&
|
|
13962
|
-
latestDispatch.terminal_bridge_message_id ===
|
|
13963
|
-
latestTakeover.terminal_bridge_message_id) {
|
|
13964
|
-
throw new Error("a previous Claude approval dispatch has an uncertain outcome; inspect and resolve the terminal manually");
|
|
13965
|
-
}
|
|
13966
|
-
const reservedAt = cliNow().toISOString();
|
|
13967
|
-
const reservedConversation = {
|
|
13968
|
-
...latestConversation,
|
|
13969
|
-
native_session_takeover: {
|
|
13970
|
-
...latestTakeover,
|
|
13971
|
-
terminal_bridge_approval_dispatch: {
|
|
13972
|
-
state: "reserved",
|
|
13973
|
-
attempt_id: randomUUID(),
|
|
13974
|
-
fingerprint,
|
|
13975
|
-
keys,
|
|
13976
|
-
terminal_target: dispatchControl.target,
|
|
13977
|
-
terminal_bridge_message_id: latestTakeover.terminal_bridge_message_id,
|
|
13978
|
-
reserved_at: reservedAt
|
|
13979
|
-
}
|
|
13980
|
-
},
|
|
13981
|
-
updated_at: reservedAt
|
|
13982
|
-
};
|
|
13983
|
-
saveState(statePath, reservedConversation);
|
|
13984
|
-
lockedConversation = reservedConversation;
|
|
14164
|
+
if (approvalDispatchReserved) {
|
|
14165
|
+
throw new Error("Claude approval dispatch was already reserved");
|
|
13985
14166
|
}
|
|
13986
|
-
|
|
13987
|
-
|
|
13988
|
-
|
|
13989
|
-
|
|
13990
|
-
|
|
13991
|
-
|
|
13992
|
-
|
|
13993
|
-
|
|
13994
|
-
|
|
13995
|
-
|
|
13996
|
-
|
|
13997
|
-
|
|
13998
|
-
|
|
13999
|
-
|
|
14000
|
-
|
|
14001
|
-
|
|
14002
|
-
|
|
14003
|
-
|
|
14004
|
-
|
|
14005
|
-
|
|
14006
|
-
|
|
14167
|
+
approvalDispatchReserved = true;
|
|
14168
|
+
if (!releaseStateLock) {
|
|
14169
|
+
throw new Error("approval state lock was released before terminal dispatch");
|
|
14170
|
+
}
|
|
14171
|
+
const latestConversation = loadState(statePath);
|
|
14172
|
+
const latestTakeover = isRecord(latestConversation.native_session_takeover)
|
|
14173
|
+
? latestConversation.native_session_takeover
|
|
14174
|
+
: undefined;
|
|
14175
|
+
const latestControl = terminalControlFromTakeover(latestTakeover);
|
|
14176
|
+
const latestApproval = isRecord(latestTakeover?.terminal_bridge_approval)
|
|
14177
|
+
? latestTakeover.terminal_bridge_approval
|
|
14178
|
+
: undefined;
|
|
14179
|
+
const latestNotifiedAt = validTimestampMs(latestApproval?.notified_at);
|
|
14180
|
+
const latestApprovalState = isRecord(latestApproval?.approval_state)
|
|
14181
|
+
? latestApproval.approval_state
|
|
14182
|
+
: undefined;
|
|
14183
|
+
const latestPolicyEvidence = isRecord(latestApprovalState?.policy_evidence)
|
|
14184
|
+
? latestApprovalState.policy_evidence
|
|
14185
|
+
: undefined;
|
|
14186
|
+
const recapturedPolicyEvidence = inspection.approval.approvable
|
|
14187
|
+
? inspection.approval.policyEvidence
|
|
14188
|
+
: undefined;
|
|
14189
|
+
const latestDispatch = isRecord(latestTakeover?.terminal_bridge_approval_dispatch)
|
|
14190
|
+
? latestTakeover.terminal_bridge_approval_dispatch
|
|
14191
|
+
: undefined;
|
|
14192
|
+
if (!latestTakeover ||
|
|
14193
|
+
latestConversation.status !== "waiting_for_openclaw" ||
|
|
14194
|
+
latestTakeover.terminal_bridge_message_id !==
|
|
14195
|
+
nativeTakeover?.terminal_bridge_message_id ||
|
|
14196
|
+
latestApproval?.fingerprint !== fingerprint ||
|
|
14197
|
+
latestNotifiedAt === undefined ||
|
|
14198
|
+
cliNowMs() - latestNotifiedAt > CLAUDE_SCREEN_APPROVAL_TTL_MS ||
|
|
14199
|
+
expectedFingerprint !== fingerprint ||
|
|
14200
|
+
!terminalControlsShareIncarnation(latestControl, dispatchControl) ||
|
|
14201
|
+
(autoApproved &&
|
|
14202
|
+
(latestPolicyEvidence?.source !== "claude_transcript" ||
|
|
14203
|
+
latestPolicyEvidence.evidence_fingerprint !==
|
|
14204
|
+
recapturedPolicyEvidence?.evidenceFingerprint))) {
|
|
14205
|
+
throw new Error("approval state changed before terminal dispatch; refresh status and retry");
|
|
14206
|
+
}
|
|
14207
|
+
if (latestDispatch?.state === "reserved" &&
|
|
14208
|
+
latestDispatch.terminal_bridge_message_id ===
|
|
14209
|
+
latestTakeover.terminal_bridge_message_id) {
|
|
14210
|
+
throw new Error("a previous Claude approval dispatch has an uncertain outcome; inspect and resolve the terminal manually");
|
|
14211
|
+
}
|
|
14212
|
+
const reservedAt = cliNow().toISOString();
|
|
14213
|
+
const reservedConversation = {
|
|
14214
|
+
...latestConversation,
|
|
14215
|
+
native_session_takeover: {
|
|
14216
|
+
...latestTakeover,
|
|
14217
|
+
terminal_bridge_approval_dispatch: {
|
|
14218
|
+
state: "reserved",
|
|
14219
|
+
attempt_id: randomUUID(),
|
|
14220
|
+
fingerprint,
|
|
14221
|
+
keys,
|
|
14222
|
+
terminal_target: dispatchControl.target,
|
|
14223
|
+
terminal_bridge_message_id: latestTakeover.terminal_bridge_message_id,
|
|
14224
|
+
reserved_at: reservedAt
|
|
14225
|
+
}
|
|
14226
|
+
},
|
|
14227
|
+
updated_at: reservedAt
|
|
14228
|
+
};
|
|
14229
|
+
saveState(statePath, reservedConversation);
|
|
14230
|
+
lockedConversation = reservedConversation;
|
|
14007
14231
|
}
|
|
14008
|
-
|
|
14009
|
-
|
|
14010
|
-
|
|
14011
|
-
|
|
14012
|
-
|
|
14013
|
-
|
|
14014
|
-
|
|
14015
|
-
actual_approval_fingerprint: actualFingerprint,
|
|
14016
|
-
screen_excerpt: approval.screenExcerpt
|
|
14017
|
-
});
|
|
14018
|
-
return;
|
|
14019
|
-
}
|
|
14020
|
-
appendEvent(logPath, {
|
|
14021
|
-
ts: cliNow().toISOString(),
|
|
14022
|
-
conversation_id: conversation.conversation_id,
|
|
14023
|
-
event: "terminal_approval_send",
|
|
14024
|
-
terminal_control: terminalControl,
|
|
14025
|
-
key: approval.key,
|
|
14026
|
-
keys: approval.keys,
|
|
14027
|
-
label: approval.label,
|
|
14028
|
-
decision_mode: approval.decisionMode,
|
|
14029
|
-
request_id: approval.requestId,
|
|
14030
|
-
approval_fingerprint: actualFingerprint,
|
|
14031
|
-
auto_approved: autoApproved,
|
|
14032
|
-
policy_rule_id: effectivePolicyRuleId,
|
|
14033
|
-
policy_fingerprint: effectivePolicyFingerprint
|
|
14034
|
-
});
|
|
14232
|
+
: undefined
|
|
14233
|
+
});
|
|
14234
|
+
const actualFingerprint = approval.fingerprint;
|
|
14235
|
+
const effectivePolicyRuleId = executorPolicyDecision?.ruleId ?? policyRuleId;
|
|
14236
|
+
const effectivePolicyFingerprint = executorPolicyDecision?.policyFingerprint ?? policyFingerprint;
|
|
14237
|
+
if (!approval.approved) {
|
|
14238
|
+
releaseApprovalStateLock();
|
|
14035
14239
|
if (autoApproved) {
|
|
14036
14240
|
appendEvent(logPath, {
|
|
14037
14241
|
ts: cliNow().toISOString(),
|
|
14038
14242
|
conversation_id: conversation.conversation_id,
|
|
14039
14243
|
event: "terminal_auto_approval_decision",
|
|
14040
|
-
action: "
|
|
14244
|
+
action: "rejected",
|
|
14245
|
+
reason: approval.reason,
|
|
14041
14246
|
terminal_control: terminalControl,
|
|
14042
|
-
|
|
14247
|
+
expected_fingerprint: expectedFingerprint,
|
|
14248
|
+
actual_fingerprint: actualFingerprint,
|
|
14043
14249
|
policy_rule_id: effectivePolicyRuleId,
|
|
14044
14250
|
policy_fingerprint: effectivePolicyFingerprint
|
|
14045
14251
|
});
|
|
14046
14252
|
}
|
|
14047
|
-
|
|
14253
|
+
printJson({
|
|
14254
|
+
conversation,
|
|
14255
|
+
approved: false,
|
|
14256
|
+
blocked: approval.blocked,
|
|
14257
|
+
reason: approval.reason,
|
|
14258
|
+
terminal_control: terminalControl,
|
|
14259
|
+
expected_approval_fingerprint: expectedFingerprint,
|
|
14260
|
+
actual_approval_fingerprint: actualFingerprint,
|
|
14261
|
+
screen_excerpt: approval.screenExcerpt
|
|
14262
|
+
});
|
|
14263
|
+
return;
|
|
14264
|
+
}
|
|
14265
|
+
appendEvent(logPath, {
|
|
14266
|
+
ts: cliNow().toISOString(),
|
|
14267
|
+
conversation_id: conversation.conversation_id,
|
|
14268
|
+
event: "terminal_approval_send",
|
|
14269
|
+
terminal_control: terminalControl,
|
|
14270
|
+
key: approval.key,
|
|
14271
|
+
keys: approval.keys,
|
|
14272
|
+
label: approval.label,
|
|
14273
|
+
decision_mode: approval.decisionMode,
|
|
14274
|
+
request_id: approval.requestId,
|
|
14275
|
+
approval_fingerprint: actualFingerprint,
|
|
14276
|
+
auto_approved: autoApproved,
|
|
14277
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
14278
|
+
policy_fingerprint: effectivePolicyFingerprint
|
|
14279
|
+
});
|
|
14280
|
+
if (autoApproved) {
|
|
14281
|
+
appendEvent(logPath, {
|
|
14282
|
+
ts: cliNow().toISOString(),
|
|
14048
14283
|
conversation_id: conversation.conversation_id,
|
|
14049
|
-
|
|
14050
|
-
|
|
14051
|
-
|
|
14052
|
-
label: approval.label,
|
|
14053
|
-
decision_mode: approval.decisionMode,
|
|
14054
|
-
request_id: approval.requestId,
|
|
14284
|
+
event: "terminal_auto_approval_decision",
|
|
14285
|
+
action: "approved",
|
|
14286
|
+
terminal_control: terminalControl,
|
|
14055
14287
|
approval_fingerprint: actualFingerprint,
|
|
14056
|
-
auto_approved: autoApproved,
|
|
14057
14288
|
policy_rule_id: effectivePolicyRuleId,
|
|
14058
14289
|
policy_fingerprint: effectivePolicyFingerprint
|
|
14059
14290
|
});
|
|
14060
|
-
|
|
14061
|
-
|
|
14062
|
-
|
|
14063
|
-
|
|
14064
|
-
|
|
14065
|
-
|
|
14066
|
-
|
|
14067
|
-
|
|
14068
|
-
|
|
14069
|
-
|
|
14070
|
-
|
|
14071
|
-
|
|
14072
|
-
|
|
14073
|
-
|
|
14074
|
-
|
|
14075
|
-
|
|
14076
|
-
|
|
14077
|
-
|
|
14078
|
-
|
|
14079
|
-
|
|
14080
|
-
|
|
14081
|
-
|
|
14082
|
-
|
|
14083
|
-
|
|
14084
|
-
|
|
14085
|
-
|
|
14086
|
-
|
|
14087
|
-
|
|
14088
|
-
|
|
14089
|
-
|
|
14090
|
-
|
|
14091
|
-
|
|
14092
|
-
|
|
14093
|
-
|
|
14094
|
-
|
|
14095
|
-
|
|
14096
|
-
|
|
14097
|
-
|
|
14098
|
-
|
|
14099
|
-
|
|
14100
|
-
|
|
14101
|
-
|
|
14102
|
-
|
|
14103
|
-
|
|
14104
|
-
|
|
14105
|
-
|
|
14106
|
-
|
|
14107
|
-
|
|
14108
|
-
|
|
14109
|
-
|
|
14110
|
-
|
|
14111
|
-
|
|
14112
|
-
|
|
14113
|
-
|
|
14114
|
-
|
|
14115
|
-
|
|
14116
|
-
|
|
14117
|
-
|
|
14118
|
-
|
|
14119
|
-
|
|
14120
|
-
|
|
14121
|
-
|
|
14122
|
-
|
|
14123
|
-
|
|
14124
|
-
|
|
14125
|
-
|
|
14126
|
-
|
|
14127
|
-
|
|
14128
|
-
|
|
14129
|
-
|
|
14130
|
-
|
|
14131
|
-
|
|
14132
|
-
|
|
14133
|
-
|
|
14134
|
-
|
|
14135
|
-
|
|
14291
|
+
}
|
|
14292
|
+
runtimeLog("info", "terminal_approval_send", {
|
|
14293
|
+
conversation_id: conversation.conversation_id,
|
|
14294
|
+
terminal_target: terminalControl.target,
|
|
14295
|
+
key: approval.key,
|
|
14296
|
+
keys: approval.keys,
|
|
14297
|
+
label: approval.label,
|
|
14298
|
+
decision_mode: approval.decisionMode,
|
|
14299
|
+
request_id: approval.requestId,
|
|
14300
|
+
approval_fingerprint: actualFingerprint,
|
|
14301
|
+
auto_approved: autoApproved,
|
|
14302
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
14303
|
+
policy_fingerprint: effectivePolicyFingerprint
|
|
14304
|
+
});
|
|
14305
|
+
const nativeTakeoverForUpdate = isRecord(lockedConversation.native_session_takeover)
|
|
14306
|
+
? { ...lockedConversation.native_session_takeover }
|
|
14307
|
+
: {};
|
|
14308
|
+
const resolvedApproval = isRecord(nativeTakeoverForUpdate.terminal_bridge_approval)
|
|
14309
|
+
? nativeTakeoverForUpdate.terminal_bridge_approval
|
|
14310
|
+
: undefined;
|
|
14311
|
+
const resolvedApprovalScreenDigest = stringValue(resolvedApproval?.screen_digest);
|
|
14312
|
+
const resolvedApprovalState = isRecord(resolvedApproval?.approval_state)
|
|
14313
|
+
? resolvedApproval.approval_state
|
|
14314
|
+
: undefined;
|
|
14315
|
+
const resolvedTranscriptIdentity = claudeTranscriptApprovalIdentity(resolvedApprovalState);
|
|
14316
|
+
const approvalResolvedAt = cliNow().toISOString();
|
|
14317
|
+
const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ??
|
|
14318
|
+
nativeTakeoverForUpdate.terminal_bridge_inactivity_timeout_minutes ??
|
|
14319
|
+
DEFAULT_AGENT_TIMEOUT_MINUTES);
|
|
14320
|
+
const agentHardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ??
|
|
14321
|
+
nativeTakeoverForUpdate.terminal_bridge_hard_timeout_minutes ??
|
|
14322
|
+
DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
|
|
14323
|
+
const nextNativeTakeover = {
|
|
14324
|
+
...nativeTakeoverForUpdate,
|
|
14325
|
+
terminal_bridge_approval: undefined,
|
|
14326
|
+
terminal_bridge_approval_dispatch: undefined,
|
|
14327
|
+
terminal_bridge_approval_resolved_at: approvalResolvedAt,
|
|
14328
|
+
terminal_bridge_last_approval_fingerprint: actualFingerprint,
|
|
14329
|
+
terminal_bridge_last_approval_screen_digest: resolvedApprovalScreenDigest,
|
|
14330
|
+
terminal_bridge_last_approval_request_id: resolvedTranscriptIdentity?.requestId,
|
|
14331
|
+
terminal_bridge_last_approval_evidence_fingerprint: resolvedTranscriptIdentity?.evidenceFingerprint,
|
|
14332
|
+
terminal_bridge_last_approval_prompt_cleared_at: undefined,
|
|
14333
|
+
terminal_bridge_last_approval_at: approvalResolvedAt,
|
|
14334
|
+
terminal_bridge_last_approval_message_id: nativeTakeoverForUpdate.terminal_bridge_message_id,
|
|
14335
|
+
terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
|
|
14336
|
+
terminal_bridge_monitor_started_at: approvalResolvedAt,
|
|
14337
|
+
terminal_bridge_last_activity_at: approvalResolvedAt,
|
|
14338
|
+
terminal_bridge_last_activity_reason: "approval resolved",
|
|
14339
|
+
terminal_bridge_inactivity_timeout_minutes: agentTimeoutMinutes,
|
|
14340
|
+
terminal_bridge_hard_timeout_minutes: agentHardTimeoutMinutes,
|
|
14341
|
+
terminal_bridge_inactivity_deadline_at: deadlineAt(approvalResolvedAt, agentTimeoutMinutes),
|
|
14342
|
+
terminal_bridge_hard_deadline_at: deadlineAt(stringValue(nativeTakeoverForUpdate.terminal_bridge_started_at) ?? approvalResolvedAt, agentHardTimeoutMinutes)
|
|
14343
|
+
};
|
|
14344
|
+
delete nextNativeTakeover.terminal_bridge_approval;
|
|
14345
|
+
delete nextNativeTakeover.terminal_bridge_approval_dispatch;
|
|
14346
|
+
delete nextNativeTakeover.terminal_bridge_last_approval_prompt_cleared_at;
|
|
14347
|
+
const nextConversation = {
|
|
14348
|
+
...lockedConversation,
|
|
14349
|
+
status: terminalBridgeEnabled(lockedConversation)
|
|
14350
|
+
? "waiting_for_agent"
|
|
14351
|
+
: lockedConversation.status,
|
|
14352
|
+
native_session_takeover: nextNativeTakeover,
|
|
14353
|
+
updated_at: approvalResolvedAt
|
|
14354
|
+
};
|
|
14355
|
+
saveState(statePath, nextConversation);
|
|
14356
|
+
releaseApprovalStateLock();
|
|
14357
|
+
const bridgeMonitor = ensureTerminalBridgeMonitorAfterApproval({
|
|
14358
|
+
conversation: nextConversation,
|
|
14359
|
+
statePath,
|
|
14360
|
+
logPath,
|
|
14361
|
+
terminalControl,
|
|
14362
|
+
options
|
|
14363
|
+
});
|
|
14364
|
+
printJson({
|
|
14365
|
+
conversation: nextConversation,
|
|
14366
|
+
approved: true,
|
|
14367
|
+
terminal_control: terminalControl,
|
|
14368
|
+
key: approval.key,
|
|
14369
|
+
keys: approval.keys,
|
|
14370
|
+
label: approval.label,
|
|
14371
|
+
decision_mode: approval.decisionMode,
|
|
14372
|
+
request_id: approval.requestId,
|
|
14373
|
+
approval_fingerprint: actualFingerprint,
|
|
14374
|
+
auto_approved: autoApproved,
|
|
14375
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
14376
|
+
policy_fingerprint: effectivePolicyFingerprint,
|
|
14377
|
+
monitor_pid: bridgeMonitor.monitorPid ?? null,
|
|
14378
|
+
monitor_handoff_pid: bridgeMonitor.handoffWatchdog?.pid ?? null
|
|
14379
|
+
});
|
|
14380
|
+
};
|
|
14381
|
+
try {
|
|
14382
|
+
return await withStoreWriterLeaseAsync(writerStoreDir, async () => {
|
|
14383
|
+
releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
14384
|
+
try {
|
|
14385
|
+
return await runApprovalWithStateLock();
|
|
14386
|
+
}
|
|
14387
|
+
finally {
|
|
14388
|
+
releaseApprovalStateLock();
|
|
14389
|
+
}
|
|
14136
14390
|
});
|
|
14137
14391
|
}
|
|
14138
14392
|
finally {
|
|
@@ -17525,6 +17779,10 @@ async function runRenew(options) {
|
|
|
17525
17779
|
if (conversation.status !== "stalled") {
|
|
17526
17780
|
throw new Error(`cannot renew ${conversation.conversation_id}; conversation is ${conversation.status}, not stalled`);
|
|
17527
17781
|
}
|
|
17782
|
+
if (isVerifiedDeadTerminalAgentProcess(conversation)) {
|
|
17783
|
+
throw new Error(`cannot renew ${conversation.conversation_id}; its bound terminal agent ` +
|
|
17784
|
+
"process is verified dead. Close this orphaned Turn instead.");
|
|
17785
|
+
}
|
|
17528
17786
|
if (terminalBridgeSubmission(conversation)?.status === "uncertain") {
|
|
17529
17787
|
throw new Error(`cannot renew ${conversation.conversation_id}; its terminal submission ` +
|
|
17530
17788
|
"is uncertain and cannot be attributed by monitoring. Inspect the pane " +
|
|
@@ -17556,6 +17814,10 @@ async function runRenew(options) {
|
|
|
17556
17814
|
if (current.status !== "stalled") {
|
|
17557
17815
|
throw new Error(`cannot renew ${current.conversation_id}; conversation is ${current.status}, not stalled`);
|
|
17558
17816
|
}
|
|
17817
|
+
if (isVerifiedDeadTerminalAgentProcess(current)) {
|
|
17818
|
+
throw new Error(`cannot renew ${current.conversation_id}; its bound terminal agent ` +
|
|
17819
|
+
"process is verified dead");
|
|
17820
|
+
}
|
|
17559
17821
|
if (terminalBridgeSubmission(current)?.status === "uncertain") {
|
|
17560
17822
|
throw new Error(`cannot renew ${current.conversation_id}; its terminal submission is uncertain`);
|
|
17561
17823
|
}
|
|
@@ -17766,6 +18028,47 @@ async function reconcileMonitors(options, { includeCallbackRecovery, reason, con
|
|
|
17766
18028
|
logPath,
|
|
17767
18029
|
options
|
|
17768
18030
|
});
|
|
18031
|
+
const deadProcessStall = await stallAcceptedTurnForVerifiedDeadAgent({
|
|
18032
|
+
options,
|
|
18033
|
+
storeDir,
|
|
18034
|
+
statePath,
|
|
18035
|
+
logPath,
|
|
18036
|
+
expectedConversationId: initialConversation.conversation_id,
|
|
18037
|
+
expectedMessageId: stringValue(isRecord(initialConversation.native_session_takeover)
|
|
18038
|
+
? initialConversation.native_session_takeover
|
|
18039
|
+
.terminal_bridge_message_id
|
|
18040
|
+
: undefined)
|
|
18041
|
+
});
|
|
18042
|
+
if (deadProcessStall.completionPreparation) {
|
|
18043
|
+
const completionPreparation = deadProcessStall.completionPreparation;
|
|
18044
|
+
skipped += 1;
|
|
18045
|
+
if (!completionPreparation.claimed) {
|
|
18046
|
+
items.push({
|
|
18047
|
+
conversation_id: initialConversation.conversation_id,
|
|
18048
|
+
status: "skipped",
|
|
18049
|
+
reason: completionPreparation.reason
|
|
18050
|
+
});
|
|
18051
|
+
}
|
|
18052
|
+
else {
|
|
18053
|
+
const completionResult = runPreparedCallback(completionPreparation.prepared, { emit: false });
|
|
18054
|
+
items.push({
|
|
18055
|
+
conversation_id: initialConversation.conversation_id,
|
|
18056
|
+
status: "recovered",
|
|
18057
|
+
reason: "bound_agent_process_dead_completion_recovered",
|
|
18058
|
+
delivered: completionResult.delivered
|
|
18059
|
+
});
|
|
18060
|
+
}
|
|
18061
|
+
continue;
|
|
18062
|
+
}
|
|
18063
|
+
if (deadProcessStall.stalled) {
|
|
18064
|
+
skipped += 1;
|
|
18065
|
+
items.push({
|
|
18066
|
+
conversation_id: initialConversation.conversation_id,
|
|
18067
|
+
status: "stalled",
|
|
18068
|
+
reason: deadProcessStall.reason
|
|
18069
|
+
});
|
|
18070
|
+
continue;
|
|
18071
|
+
}
|
|
17769
18072
|
const initialTakeover = isRecord(initialConversation.native_session_takeover)
|
|
17770
18073
|
? initialConversation.native_session_takeover
|
|
17771
18074
|
: undefined;
|
|
@@ -18256,6 +18559,600 @@ function terminalBridgeReconciliationEligibility(conversation) {
|
|
|
18256
18559
|
hardDeadlineAtMs
|
|
18257
18560
|
};
|
|
18258
18561
|
}
|
|
18562
|
+
function terminalAgentProcessDisposition(conversation) {
|
|
18563
|
+
return isRecord(conversation.terminal_agent_process_disposition)
|
|
18564
|
+
? conversation.terminal_agent_process_disposition
|
|
18565
|
+
: undefined;
|
|
18566
|
+
}
|
|
18567
|
+
function isVerifiedDeadTerminalAgentProcess(conversation) {
|
|
18568
|
+
return terminalAgentProcessDisposition(conversation)?.status ===
|
|
18569
|
+
"verified_dead";
|
|
18570
|
+
}
|
|
18571
|
+
function verifiedDeadTerminalAgentProcessEvidenceId(proof) {
|
|
18572
|
+
return createHash("sha256")
|
|
18573
|
+
.update(canonicalJson({
|
|
18574
|
+
kind: proof.kind,
|
|
18575
|
+
agent: proof.agent,
|
|
18576
|
+
pid: proof.pid,
|
|
18577
|
+
process_uuid: proof.process_uuid,
|
|
18578
|
+
process_birth: proof.process_birth ?? null,
|
|
18579
|
+
conversation_id: proof.conversation_id,
|
|
18580
|
+
session_id: proof.session_id,
|
|
18581
|
+
turn_id: proof.turn_id,
|
|
18582
|
+
terminal_endpoint: proof.terminal_endpoint,
|
|
18583
|
+
binding_id: proof.binding_id,
|
|
18584
|
+
binding_generation: proof.binding_generation,
|
|
18585
|
+
message_id: proof.message_id
|
|
18586
|
+
}))
|
|
18587
|
+
.digest("hex");
|
|
18588
|
+
}
|
|
18589
|
+
function storedVerifiedDeadTerminalAgentProcessProof({ conversation, storeDir, terminalControl }) {
|
|
18590
|
+
const disposition = terminalAgentProcessDisposition(conversation);
|
|
18591
|
+
if (disposition?.status !== "verified_dead") {
|
|
18592
|
+
return { status: "absent" };
|
|
18593
|
+
}
|
|
18594
|
+
const proof = isRecord(disposition.proof)
|
|
18595
|
+
? disposition.proof
|
|
18596
|
+
: undefined;
|
|
18597
|
+
const evidenceId = stringValue(disposition.evidence_id);
|
|
18598
|
+
const recordedAt = stringValue(disposition.recorded_at);
|
|
18599
|
+
const takeover = isRecord(conversation.native_session_takeover)
|
|
18600
|
+
? conversation.native_session_takeover
|
|
18601
|
+
: undefined;
|
|
18602
|
+
const submission = terminalBridgeSubmission(conversation);
|
|
18603
|
+
const session = tryLoadManagedSession(storeDir, sessionIdForConversation(conversation));
|
|
18604
|
+
const binding = session?.binding;
|
|
18605
|
+
const candidate = proof;
|
|
18606
|
+
if (!candidate ||
|
|
18607
|
+
candidate.kind !==
|
|
18608
|
+
"exact_pid_absent_from_complete_process_inventory" ||
|
|
18609
|
+
!session ||
|
|
18610
|
+
session.status !== "bound" ||
|
|
18611
|
+
!binding ||
|
|
18612
|
+
candidate.agent !== session.agent ||
|
|
18613
|
+
session.agent !== executorForConversation(conversation).kind ||
|
|
18614
|
+
path.resolve(session.workspace) !== path.resolve(conversation.workspace) ||
|
|
18615
|
+
candidate.pid !== Number(takeover?.terminal_agent_pid) ||
|
|
18616
|
+
candidate.pid !== binding.native_process.pid ||
|
|
18617
|
+
candidate.process_uuid !==
|
|
18618
|
+
stringValue(takeover?.terminal_agent_process_uuid) ||
|
|
18619
|
+
candidate.process_uuid !== binding.native_process.process_uuid ||
|
|
18620
|
+
(candidate.process_birth ?? undefined) !==
|
|
18621
|
+
(stringValue(takeover?.terminal_agent_process_birth) ?? undefined) ||
|
|
18622
|
+
(candidate.process_birth ?? undefined) !==
|
|
18623
|
+
(binding.native_process.process_birth ?? undefined) ||
|
|
18624
|
+
candidate.conversation_id !== conversation.conversation_id ||
|
|
18625
|
+
candidate.session_id !== sessionIdForConversation(conversation) ||
|
|
18626
|
+
candidate.turn_id !== turnIdForConversation(conversation) ||
|
|
18627
|
+
candidate.binding_id !== stringValue(conversation.terminal_binding_id) ||
|
|
18628
|
+
candidate.binding_id !== binding.binding_id ||
|
|
18629
|
+
candidate.binding_generation !==
|
|
18630
|
+
Number(conversation.terminal_binding_generation) ||
|
|
18631
|
+
candidate.binding_generation !== binding.generation ||
|
|
18632
|
+
!terminalControlsShareIncarnation(binding.terminal_control, terminalControl) ||
|
|
18633
|
+
candidate.binding_id !== stringValue(takeover?.terminal_binding_id) ||
|
|
18634
|
+
candidate.binding_generation !==
|
|
18635
|
+
Number(takeover?.terminal_binding_generation) ||
|
|
18636
|
+
candidate.message_id !==
|
|
18637
|
+
stringValue(takeover?.terminal_bridge_message_id) ||
|
|
18638
|
+
candidate.message_id !== stringValue(submission?.message_id) ||
|
|
18639
|
+
submission?.status !== "agent_accepted" ||
|
|
18640
|
+
stringValue(submission?.session_id) !== candidate.session_id ||
|
|
18641
|
+
stringValue(submission?.turn_id) !== candidate.turn_id ||
|
|
18642
|
+
stringValue(submission?.binding_id) !== candidate.binding_id ||
|
|
18643
|
+
Number(submission?.binding_generation) !==
|
|
18644
|
+
candidate.binding_generation ||
|
|
18645
|
+
!isRecord(candidate.terminal_control) ||
|
|
18646
|
+
!terminalControlsShareIncarnation(candidate.terminal_control, terminalControl) ||
|
|
18647
|
+
!terminalControlEvidenceMatches(candidate.terminal_endpoint, terminalControl, { requireProcessAnchor: true }) ||
|
|
18648
|
+
Number(candidate.terminal_endpoint.process_anchor_pid) !==
|
|
18649
|
+
terminalEndpointFromControlRef(terminalControl).processAnchorPid ||
|
|
18650
|
+
validTimestampMs(candidate.observed_at) === undefined ||
|
|
18651
|
+
!recordedAt ||
|
|
18652
|
+
validTimestampMs(recordedAt) === undefined ||
|
|
18653
|
+
recordedAt !== candidate.observed_at ||
|
|
18654
|
+
!evidenceId ||
|
|
18655
|
+
evidenceId !== verifiedDeadTerminalAgentProcessEvidenceId(candidate)) {
|
|
18656
|
+
return {
|
|
18657
|
+
status: "invalid",
|
|
18658
|
+
reason: "the persisted process-death proof no longer matches the exact Turn, Session, terminal, or submission binding"
|
|
18659
|
+
};
|
|
18660
|
+
}
|
|
18661
|
+
return {
|
|
18662
|
+
status: "valid",
|
|
18663
|
+
proof: candidate,
|
|
18664
|
+
evidenceId,
|
|
18665
|
+
recordedAt
|
|
18666
|
+
};
|
|
18667
|
+
}
|
|
18668
|
+
function eventVerifiedDeadTerminalAgentProcessProof({ conversation, storeDir, terminalControl, logPath }) {
|
|
18669
|
+
const candidates = readExistingEvents(logPath).filter((event) => event.event === "terminal_agent_process_verified_dead" &&
|
|
18670
|
+
event.conversation_id === conversation.conversation_id);
|
|
18671
|
+
if (candidates.length === 0) {
|
|
18672
|
+
return { status: "absent" };
|
|
18673
|
+
}
|
|
18674
|
+
if (candidates.length !== 1) {
|
|
18675
|
+
return {
|
|
18676
|
+
status: "invalid",
|
|
18677
|
+
reason: "the process-death event history is ambiguous"
|
|
18678
|
+
};
|
|
18679
|
+
}
|
|
18680
|
+
const event = candidates[0];
|
|
18681
|
+
const proof = isRecord(event.proof)
|
|
18682
|
+
? event.proof
|
|
18683
|
+
: undefined;
|
|
18684
|
+
const evidenceId = stringValue(event.evidence_id);
|
|
18685
|
+
const recordedAt = stringValue(event.ts);
|
|
18686
|
+
if (!proof ||
|
|
18687
|
+
!evidenceId ||
|
|
18688
|
+
!recordedAt ||
|
|
18689
|
+
proof.observed_at !== recordedAt ||
|
|
18690
|
+
verifiedDeadTerminalAgentProcessEvidenceId(proof) !== evidenceId) {
|
|
18691
|
+
return {
|
|
18692
|
+
status: "invalid",
|
|
18693
|
+
reason: "the process-death event proof is malformed"
|
|
18694
|
+
};
|
|
18695
|
+
}
|
|
18696
|
+
const synthetic = {
|
|
18697
|
+
...conversation,
|
|
18698
|
+
terminal_agent_process_disposition: {
|
|
18699
|
+
status: "verified_dead",
|
|
18700
|
+
proof,
|
|
18701
|
+
evidence_id: evidenceId,
|
|
18702
|
+
recorded_at: recordedAt
|
|
18703
|
+
}
|
|
18704
|
+
};
|
|
18705
|
+
return storedVerifiedDeadTerminalAgentProcessProof({
|
|
18706
|
+
conversation: synthetic,
|
|
18707
|
+
storeDir,
|
|
18708
|
+
terminalControl
|
|
18709
|
+
});
|
|
18710
|
+
}
|
|
18711
|
+
function exactVerifiedDeadTerminalAgentProcessAuthority({ conversation, storeDir, terminalControl, logPath }) {
|
|
18712
|
+
const stored = storedVerifiedDeadTerminalAgentProcessProof({
|
|
18713
|
+
conversation,
|
|
18714
|
+
storeDir,
|
|
18715
|
+
terminalControl
|
|
18716
|
+
});
|
|
18717
|
+
if (stored.status === "invalid") {
|
|
18718
|
+
return stored;
|
|
18719
|
+
}
|
|
18720
|
+
const event = eventVerifiedDeadTerminalAgentProcessProof({
|
|
18721
|
+
conversation,
|
|
18722
|
+
storeDir,
|
|
18723
|
+
terminalControl,
|
|
18724
|
+
logPath
|
|
18725
|
+
});
|
|
18726
|
+
if (stored.status === "absent") {
|
|
18727
|
+
return event;
|
|
18728
|
+
}
|
|
18729
|
+
if (event.status !== "valid" ||
|
|
18730
|
+
event.evidenceId !== stored.evidenceId ||
|
|
18731
|
+
canonicalJson(event.proof) !== canonicalJson(stored.proof)) {
|
|
18732
|
+
return {
|
|
18733
|
+
status: "invalid",
|
|
18734
|
+
reason: "the persisted process-death disposition has no exact append-only event"
|
|
18735
|
+
};
|
|
18736
|
+
}
|
|
18737
|
+
return stored;
|
|
18738
|
+
}
|
|
18739
|
+
function ensureVerifiedDeadTerminalAgentProcessEvent({ logPath, proof, action }) {
|
|
18740
|
+
const evidenceId = verifiedDeadTerminalAgentProcessEvidenceId(proof);
|
|
18741
|
+
const existing = readExistingEvents(logPath).find((event) => event.event === "terminal_agent_process_verified_dead" &&
|
|
18742
|
+
event.conversation_id === proof.conversation_id &&
|
|
18743
|
+
event.evidence_id === evidenceId);
|
|
18744
|
+
if (existing) {
|
|
18745
|
+
const existingProof = isRecord(existing.proof)
|
|
18746
|
+
? existing.proof
|
|
18747
|
+
: undefined;
|
|
18748
|
+
const existingAt = stringValue(existing.ts);
|
|
18749
|
+
if (!existingProof ||
|
|
18750
|
+
!existingAt ||
|
|
18751
|
+
validTimestampMs(existingAt) === undefined ||
|
|
18752
|
+
existingProof.observed_at !== existingAt ||
|
|
18753
|
+
verifiedDeadTerminalAgentProcessEvidenceId(existingProof) !== evidenceId) {
|
|
18754
|
+
throw new Error(`verified-dead process event ${evidenceId} is inconsistent`);
|
|
18755
|
+
}
|
|
18756
|
+
return {
|
|
18757
|
+
proof: existingProof,
|
|
18758
|
+
evidenceId,
|
|
18759
|
+
recordedAt: existingAt
|
|
18760
|
+
};
|
|
18761
|
+
}
|
|
18762
|
+
appendEvent(logPath, {
|
|
18763
|
+
ts: proof.observed_at,
|
|
18764
|
+
conversation_id: proof.conversation_id,
|
|
18765
|
+
event: "terminal_agent_process_verified_dead",
|
|
18766
|
+
evidence_id: evidenceId,
|
|
18767
|
+
status: "verified_dead",
|
|
18768
|
+
proof,
|
|
18769
|
+
action
|
|
18770
|
+
});
|
|
18771
|
+
return {
|
|
18772
|
+
proof,
|
|
18773
|
+
evidenceId,
|
|
18774
|
+
recordedAt: proof.observed_at
|
|
18775
|
+
};
|
|
18776
|
+
}
|
|
18777
|
+
function ensureVerifiedDeadConversationStalledEvent({ logPath, proof, evidenceId, reason, terminalControl, completionObservation }) {
|
|
18778
|
+
const existing = readExistingEvents(logPath).find((event) => event.event === "conversation_stalled" &&
|
|
18779
|
+
event.conversation_id === proof.conversation_id &&
|
|
18780
|
+
event.evidence_id === evidenceId);
|
|
18781
|
+
if (existing) {
|
|
18782
|
+
if (existing.ts !== proof.observed_at ||
|
|
18783
|
+
existing.reason !== reason ||
|
|
18784
|
+
existing.disposition !== "verified_dead_agent_process" ||
|
|
18785
|
+
existing.completion_observation !== completionObservation) {
|
|
18786
|
+
throw new Error(`verified-dead stalled event ${evidenceId} is inconsistent`);
|
|
18787
|
+
}
|
|
18788
|
+
return;
|
|
18789
|
+
}
|
|
18790
|
+
appendEvent(logPath, {
|
|
18791
|
+
ts: proof.observed_at,
|
|
18792
|
+
conversation_id: proof.conversation_id,
|
|
18793
|
+
event: "conversation_stalled",
|
|
18794
|
+
evidence_id: evidenceId,
|
|
18795
|
+
status: "stalled",
|
|
18796
|
+
reason,
|
|
18797
|
+
terminal_bridge: true,
|
|
18798
|
+
terminal_control: terminalControl,
|
|
18799
|
+
disposition: "verified_dead_agent_process",
|
|
18800
|
+
completion_observation: completionObservation
|
|
18801
|
+
});
|
|
18802
|
+
}
|
|
18803
|
+
function verifiedDeadConversationStalledEventDecision({ logPath, proof, evidenceId, reason }) {
|
|
18804
|
+
const candidates = readExistingEvents(logPath).filter((event) => event.event === "conversation_stalled" &&
|
|
18805
|
+
event.conversation_id === proof.conversation_id &&
|
|
18806
|
+
event.evidence_id === evidenceId);
|
|
18807
|
+
if (candidates.length === 0) {
|
|
18808
|
+
return { status: "absent" };
|
|
18809
|
+
}
|
|
18810
|
+
if (candidates.length !== 1) {
|
|
18811
|
+
return {
|
|
18812
|
+
status: "invalid",
|
|
18813
|
+
reason: "the verified-dead stalled event history is ambiguous"
|
|
18814
|
+
};
|
|
18815
|
+
}
|
|
18816
|
+
const event = candidates[0];
|
|
18817
|
+
const completionObservation = stringValue(event.completion_observation);
|
|
18818
|
+
if (event.ts !== proof.observed_at ||
|
|
18819
|
+
event.status !== "stalled" ||
|
|
18820
|
+
event.reason !== reason ||
|
|
18821
|
+
event.disposition !== "verified_dead_agent_process" ||
|
|
18822
|
+
!["absent", "unverifiable"].includes(completionObservation ?? "")) {
|
|
18823
|
+
return {
|
|
18824
|
+
status: "invalid",
|
|
18825
|
+
reason: "the verified-dead stalled event decision is inconsistent"
|
|
18826
|
+
};
|
|
18827
|
+
}
|
|
18828
|
+
return {
|
|
18829
|
+
status: "valid",
|
|
18830
|
+
completionObservation: completionObservation
|
|
18831
|
+
};
|
|
18832
|
+
}
|
|
18833
|
+
function ensureVerifiedDeadConversationClosedEvent({ logPath, conversation, evidenceId }) {
|
|
18834
|
+
const closedAt = required(stringValue(conversation.closed_at), "verified-dead closed Turn has no closed_at timestamp");
|
|
18835
|
+
const reason = required(stringValue(conversation.close_reason), "verified-dead closed Turn has no close reason");
|
|
18836
|
+
if (conversation.status !== "closed" ||
|
|
18837
|
+
validTimestampMs(closedAt) === undefined) {
|
|
18838
|
+
throw new Error("verified-dead closed Turn state is inconsistent");
|
|
18839
|
+
}
|
|
18840
|
+
const existing = readExistingEvents(logPath).find((event) => event.event === "conversation_closed" &&
|
|
18841
|
+
event.conversation_id === conversation.conversation_id &&
|
|
18842
|
+
event.evidence_id === evidenceId);
|
|
18843
|
+
if (existing) {
|
|
18844
|
+
if (existing.ts !== closedAt ||
|
|
18845
|
+
existing.status !== "closed" ||
|
|
18846
|
+
existing.reason !== reason ||
|
|
18847
|
+
existing.disposition !== "verified_dead_agent_process") {
|
|
18848
|
+
throw new Error(`verified-dead close event ${evidenceId} is inconsistent`);
|
|
18849
|
+
}
|
|
18850
|
+
return;
|
|
18851
|
+
}
|
|
18852
|
+
appendEvent(logPath, {
|
|
18853
|
+
ts: closedAt,
|
|
18854
|
+
conversation_id: conversation.conversation_id,
|
|
18855
|
+
event: "conversation_closed",
|
|
18856
|
+
evidence_id: evidenceId,
|
|
18857
|
+
status: "closed",
|
|
18858
|
+
reason,
|
|
18859
|
+
disposition: "verified_dead_agent_process"
|
|
18860
|
+
});
|
|
18861
|
+
}
|
|
18862
|
+
function acceptedTurnCanBeStalledForDeadAgent({ storeDir, conversation }) {
|
|
18863
|
+
if (conversation.status !== "waiting_for_agent") {
|
|
18864
|
+
return false;
|
|
18865
|
+
}
|
|
18866
|
+
const takeover = isRecord(conversation.native_session_takeover)
|
|
18867
|
+
? conversation.native_session_takeover
|
|
18868
|
+
: undefined;
|
|
18869
|
+
const messageId = stringValue(takeover?.terminal_bridge_message_id);
|
|
18870
|
+
const submission = terminalBridgeSubmission(conversation);
|
|
18871
|
+
if (takeover?.terminal_bridge !== true ||
|
|
18872
|
+
!messageId ||
|
|
18873
|
+
submission?.status !== "agent_accepted" ||
|
|
18874
|
+
stringValue(submission.message_id) !== messageId) {
|
|
18875
|
+
return false;
|
|
18876
|
+
}
|
|
18877
|
+
const transferId = stringValue(takeover.deferred_foreground_transfer_id);
|
|
18878
|
+
if (!transferId) {
|
|
18879
|
+
return true;
|
|
18880
|
+
}
|
|
18881
|
+
const transfer = loadDeferredForegroundTransfer(storeDir, transferId);
|
|
18882
|
+
return transfer.status === "resolved";
|
|
18883
|
+
}
|
|
18884
|
+
async function stallAcceptedTurnForVerifiedDeadAgent({ options, storeDir, statePath, logPath, expectedConversationId, expectedMessageId }) {
|
|
18885
|
+
const canonicalStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
18886
|
+
if (path.resolve(storeDir) !== path.resolve(canonicalStoreDir)) {
|
|
18887
|
+
const conversation = loadState(statePath);
|
|
18888
|
+
return {
|
|
18889
|
+
stalled: false,
|
|
18890
|
+
conversation,
|
|
18891
|
+
reason: "dead_process_stall_store_mismatch"
|
|
18892
|
+
};
|
|
18893
|
+
}
|
|
18894
|
+
const initial = loadState(statePath);
|
|
18895
|
+
const initialTakeover = isRecord(initial.native_session_takeover)
|
|
18896
|
+
? initial.native_session_takeover
|
|
18897
|
+
: undefined;
|
|
18898
|
+
const terminalControl = terminalControlFromTakeover(initialTakeover);
|
|
18899
|
+
if (initial.conversation_id !== expectedConversationId ||
|
|
18900
|
+
!terminalControl ||
|
|
18901
|
+
!acceptedTurnCanBeStalledForDeadAgent({ storeDir, conversation: initial })) {
|
|
18902
|
+
return {
|
|
18903
|
+
stalled: false,
|
|
18904
|
+
conversation: initial,
|
|
18905
|
+
reason: "dead_process_stall_not_applicable"
|
|
18906
|
+
};
|
|
18907
|
+
}
|
|
18908
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDir, terminalControl, { timeoutMs: 30000 });
|
|
18909
|
+
try {
|
|
18910
|
+
return await withStoreWriterLeaseAsync(storeDir, async () => {
|
|
18911
|
+
const releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
18912
|
+
let stateLockReleased = false;
|
|
18913
|
+
try {
|
|
18914
|
+
const current = loadState(statePath);
|
|
18915
|
+
const takeover = isRecord(current.native_session_takeover)
|
|
18916
|
+
? current.native_session_takeover
|
|
18917
|
+
: undefined;
|
|
18918
|
+
const currentControl = terminalControlFromTakeover(takeover);
|
|
18919
|
+
const messageId = stringValue(takeover?.terminal_bridge_message_id);
|
|
18920
|
+
if (current.conversation_id !== expectedConversationId ||
|
|
18921
|
+
(expectedMessageId !== undefined && messageId !== expectedMessageId) ||
|
|
18922
|
+
!currentControl ||
|
|
18923
|
+
!terminalControlsShareIncarnation(currentControl, terminalControl) ||
|
|
18924
|
+
!acceptedTurnCanBeStalledForDeadAgent({
|
|
18925
|
+
storeDir,
|
|
18926
|
+
conversation: current
|
|
18927
|
+
})) {
|
|
18928
|
+
return {
|
|
18929
|
+
stalled: false,
|
|
18930
|
+
conversation: current,
|
|
18931
|
+
reason: "dead_process_stall_generation_changed"
|
|
18932
|
+
};
|
|
18933
|
+
}
|
|
18934
|
+
assertTurnBindingCurrent(current, "stall a verified-dead agent Turn for");
|
|
18935
|
+
const ledger = loadTerminalBridgeDispatchLedger(currentControl);
|
|
18936
|
+
if (!ledger ||
|
|
18937
|
+
stringValue(ledger.status) !== "agent_accepted" ||
|
|
18938
|
+
!sameCanonicalStatePath(ledger.state_path, statePath) ||
|
|
18939
|
+
path.resolve(stringValue(ledger.store_dir) ?? "") !==
|
|
18940
|
+
path.resolve(storeDir) ||
|
|
18941
|
+
path.resolve(stringValue(ledger.event_log_path) ?? "") !==
|
|
18942
|
+
path.resolve(logPath) ||
|
|
18943
|
+
stringValue(ledger.conversation_id) !== current.conversation_id ||
|
|
18944
|
+
stringValue(ledger.session_id) !== sessionIdForConversation(current) ||
|
|
18945
|
+
stringValue(ledger.turn_id) !== turnIdForConversation(current) ||
|
|
18946
|
+
stringValue(ledger.message_id) !== messageId ||
|
|
18947
|
+
!terminalDispatchRecordMatchesControl(ledger, currentControl, {
|
|
18948
|
+
requireProcessAnchor: true
|
|
18949
|
+
}) ||
|
|
18950
|
+
terminalDispatchRecordProcessAnchor(ledger) !==
|
|
18951
|
+
terminalEndpointFromControlRef(currentControl).processAnchorPid ||
|
|
18952
|
+
stringValue(ledger.binding_id) !==
|
|
18953
|
+
stringValue(current.terminal_binding_id) ||
|
|
18954
|
+
Number(ledger.binding_generation) !==
|
|
18955
|
+
Number(current.terminal_binding_generation)) {
|
|
18956
|
+
return {
|
|
18957
|
+
stalled: false,
|
|
18958
|
+
conversation: current,
|
|
18959
|
+
reason: "dead_process_stall_dispatch_changed"
|
|
18960
|
+
};
|
|
18961
|
+
}
|
|
18962
|
+
try {
|
|
18963
|
+
assertVerifiedDeadTerminalBridgeDispatchAuthority({
|
|
18964
|
+
terminalControl: currentControl,
|
|
18965
|
+
conversation: current,
|
|
18966
|
+
storeDir,
|
|
18967
|
+
statePath,
|
|
18968
|
+
logPath,
|
|
18969
|
+
expectedMessageId: required(messageId, "verified-dead stall has no terminal message id")
|
|
18970
|
+
});
|
|
18971
|
+
}
|
|
18972
|
+
catch (error) {
|
|
18973
|
+
return {
|
|
18974
|
+
stalled: false,
|
|
18975
|
+
conversation: current,
|
|
18976
|
+
reason: "dead_process_stall_dispatch_changed: " +
|
|
18977
|
+
(error instanceof Error ? error.message : String(error))
|
|
18978
|
+
};
|
|
18979
|
+
}
|
|
18980
|
+
const persistedAuthority = exactVerifiedDeadTerminalAgentProcessAuthority({
|
|
18981
|
+
conversation: current,
|
|
18982
|
+
storeDir,
|
|
18983
|
+
terminalControl: currentControl,
|
|
18984
|
+
logPath
|
|
18985
|
+
});
|
|
18986
|
+
if (persistedAuthority.status === "invalid") {
|
|
18987
|
+
return {
|
|
18988
|
+
stalled: false,
|
|
18989
|
+
conversation: current,
|
|
18990
|
+
reason: `bound_agent_process_evidence_invalid: ${persistedAuthority.reason}`
|
|
18991
|
+
};
|
|
18992
|
+
}
|
|
18993
|
+
let proof;
|
|
18994
|
+
if (persistedAuthority.status === "valid") {
|
|
18995
|
+
proof = persistedAuthority.proof;
|
|
18996
|
+
}
|
|
18997
|
+
else {
|
|
18998
|
+
const observation = await observeBoundTerminalAgentProcess({
|
|
18999
|
+
options,
|
|
19000
|
+
conversation: current,
|
|
19001
|
+
terminalControl: currentControl
|
|
19002
|
+
});
|
|
19003
|
+
if (observation.status !== "verified_dead") {
|
|
19004
|
+
return {
|
|
19005
|
+
stalled: false,
|
|
19006
|
+
conversation: current,
|
|
19007
|
+
reason: observation.status === "alive"
|
|
19008
|
+
? "bound_agent_process_alive"
|
|
19009
|
+
: `bound_agent_process_unverifiable: ${observation.reason}`
|
|
19010
|
+
};
|
|
19011
|
+
}
|
|
19012
|
+
proof = observation.proof;
|
|
19013
|
+
}
|
|
19014
|
+
const stalledReason = "bound terminal agent process is verified dead";
|
|
19015
|
+
const priorStalledDecision = verifiedDeadConversationStalledEventDecision({
|
|
19016
|
+
logPath,
|
|
19017
|
+
proof,
|
|
19018
|
+
evidenceId: verifiedDeadTerminalAgentProcessEvidenceId(proof),
|
|
19019
|
+
reason: stalledReason
|
|
19020
|
+
});
|
|
19021
|
+
if (priorStalledDecision.status === "invalid") {
|
|
19022
|
+
return {
|
|
19023
|
+
stalled: false,
|
|
19024
|
+
conversation: current,
|
|
19025
|
+
reason: `bound_agent_process_evidence_invalid: ${priorStalledDecision.reason}`
|
|
19026
|
+
};
|
|
19027
|
+
}
|
|
19028
|
+
if (priorStalledDecision.status === "valid") {
|
|
19029
|
+
if (persistedAuthority.status !== "valid") {
|
|
19030
|
+
return {
|
|
19031
|
+
stalled: false,
|
|
19032
|
+
conversation: current,
|
|
19033
|
+
reason: "bound_agent_process_evidence_invalid: the stalled decision has no exact death event"
|
|
19034
|
+
};
|
|
19035
|
+
}
|
|
19036
|
+
const stalled = {
|
|
19037
|
+
...current,
|
|
19038
|
+
status: "stalled",
|
|
19039
|
+
stalled_at: persistedAuthority.recordedAt,
|
|
19040
|
+
stalled_reason: stalledReason,
|
|
19041
|
+
terminal_agent_process_disposition: {
|
|
19042
|
+
status: "verified_dead",
|
|
19043
|
+
proof: persistedAuthority.proof,
|
|
19044
|
+
evidence_id: persistedAuthority.evidenceId,
|
|
19045
|
+
recorded_at: persistedAuthority.recordedAt,
|
|
19046
|
+
completion_observation: {
|
|
19047
|
+
status: priorStalledDecision.completionObservation
|
|
19048
|
+
}
|
|
19049
|
+
},
|
|
19050
|
+
updated_at: persistedAuthority.recordedAt
|
|
19051
|
+
};
|
|
19052
|
+
saveState(statePath, stalled);
|
|
19053
|
+
return {
|
|
19054
|
+
stalled: true,
|
|
19055
|
+
conversation: stalled,
|
|
19056
|
+
reason: priorStalledDecision.completionObservation === "unverifiable"
|
|
19057
|
+
? "bound_agent_process_verified_dead_completion_unverifiable"
|
|
19058
|
+
: "bound_agent_process_verified_dead"
|
|
19059
|
+
};
|
|
19060
|
+
}
|
|
19061
|
+
const durableCompletion = await observeDurableCompletionBeforeDeadStall({
|
|
19062
|
+
options,
|
|
19063
|
+
conversation: current,
|
|
19064
|
+
terminalControl: currentControl
|
|
19065
|
+
});
|
|
19066
|
+
if (durableCompletion.status === "present") {
|
|
19067
|
+
// Completion claiming re-enters the Turn state lock. Keep the
|
|
19068
|
+
// canonical terminal and Store-writer locks, but release the state
|
|
19069
|
+
// lock before preparing the callback/local completion atomically.
|
|
19070
|
+
releaseStateLock();
|
|
19071
|
+
stateLockReleased = true;
|
|
19072
|
+
const completionPreparation = prepareTerminalBridgeCompletionCallbackWithLocksHeld({
|
|
19073
|
+
options,
|
|
19074
|
+
statePath,
|
|
19075
|
+
logPath,
|
|
19076
|
+
conversation: current,
|
|
19077
|
+
executor: executorForConversation(current),
|
|
19078
|
+
terminalControl: currentControl,
|
|
19079
|
+
terminalMessageId: required(messageId, "verified-dead completion has no terminal message id"),
|
|
19080
|
+
completion: durableCompletion.completion
|
|
19081
|
+
});
|
|
19082
|
+
return {
|
|
19083
|
+
stalled: false,
|
|
19084
|
+
conversation: completionPreparation.conversation,
|
|
19085
|
+
reason: completionPreparation.claimed
|
|
19086
|
+
? "bound_agent_process_dead_completion_prepared"
|
|
19087
|
+
: `bound_agent_process_dead_completion_${completionPreparation.reason}`,
|
|
19088
|
+
completionPreparation
|
|
19089
|
+
};
|
|
19090
|
+
}
|
|
19091
|
+
const completionUnverifiable = durableCompletion.status === "unverifiable";
|
|
19092
|
+
// Keep the durable lifecycle reason independent of a transient
|
|
19093
|
+
// completion-read outcome. If the process crashes between the
|
|
19094
|
+
// append-only events and state save, a later reconciliation must be
|
|
19095
|
+
// able to reuse the same death evidence even when the transcript or
|
|
19096
|
+
// rollout has become readable in the meantime.
|
|
19097
|
+
const reason = stalledReason;
|
|
19098
|
+
const audit = ensureVerifiedDeadTerminalAgentProcessEvent({
|
|
19099
|
+
logPath,
|
|
19100
|
+
proof,
|
|
19101
|
+
action: "monitor_reconciliation"
|
|
19102
|
+
});
|
|
19103
|
+
ensureVerifiedDeadConversationStalledEvent({
|
|
19104
|
+
logPath,
|
|
19105
|
+
proof: audit.proof,
|
|
19106
|
+
evidenceId: audit.evidenceId,
|
|
19107
|
+
reason,
|
|
19108
|
+
terminalControl: currentControl,
|
|
19109
|
+
completionObservation: completionUnverifiable ? "unverifiable" : "absent"
|
|
19110
|
+
});
|
|
19111
|
+
if (cliEnv().AKK_TEST_EXIT_AFTER_VERIFIED_DEAD_STALL_EVENTS === "1") {
|
|
19112
|
+
cliExit(86);
|
|
19113
|
+
}
|
|
19114
|
+
const stalled = {
|
|
19115
|
+
...current,
|
|
19116
|
+
status: "stalled",
|
|
19117
|
+
stalled_at: audit.recordedAt,
|
|
19118
|
+
stalled_reason: reason,
|
|
19119
|
+
terminal_agent_process_disposition: {
|
|
19120
|
+
status: "verified_dead",
|
|
19121
|
+
proof: audit.proof,
|
|
19122
|
+
evidence_id: audit.evidenceId,
|
|
19123
|
+
recorded_at: audit.recordedAt,
|
|
19124
|
+
completion_observation: {
|
|
19125
|
+
status: completionUnverifiable ? "unverifiable" : "absent"
|
|
19126
|
+
}
|
|
19127
|
+
},
|
|
19128
|
+
updated_at: audit.recordedAt
|
|
19129
|
+
};
|
|
19130
|
+
saveState(statePath, stalled);
|
|
19131
|
+
runtimeLog("warn", "terminal_agent_process_verified_dead", {
|
|
19132
|
+
conversation_id: current.conversation_id,
|
|
19133
|
+
terminal_target: currentControl.target,
|
|
19134
|
+
pid: audit.proof.pid,
|
|
19135
|
+
reason
|
|
19136
|
+
});
|
|
19137
|
+
return {
|
|
19138
|
+
stalled: true,
|
|
19139
|
+
conversation: stalled,
|
|
19140
|
+
reason: completionUnverifiable
|
|
19141
|
+
? "bound_agent_process_verified_dead_completion_unverifiable"
|
|
19142
|
+
: "bound_agent_process_verified_dead"
|
|
19143
|
+
};
|
|
19144
|
+
}
|
|
19145
|
+
finally {
|
|
19146
|
+
if (!stateLockReleased) {
|
|
19147
|
+
releaseStateLock();
|
|
19148
|
+
}
|
|
19149
|
+
}
|
|
19150
|
+
});
|
|
19151
|
+
}
|
|
19152
|
+
finally {
|
|
19153
|
+
releaseTerminalLock();
|
|
19154
|
+
}
|
|
19155
|
+
}
|
|
18259
19156
|
function settleLocalTerminalBridgeCompletionClaim({ storeDir, statePath, logPath }) {
|
|
18260
19157
|
const initial = loadState(statePath);
|
|
18261
19158
|
const initialTakeover = isRecord(initial.native_session_takeover)
|
|
@@ -18899,6 +19796,7 @@ async function runClose(options) {
|
|
|
18899
19796
|
const releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
18900
19797
|
try {
|
|
18901
19798
|
const conversation = loadState(statePath);
|
|
19799
|
+
let verifiedDeadProcess;
|
|
18902
19800
|
assertConversationHasNoNonterminalDeferredForegroundTransfer({
|
|
18903
19801
|
storeDir: closeStoreDir,
|
|
18904
19802
|
conversation,
|
|
@@ -18912,34 +19810,120 @@ async function runClose(options) {
|
|
|
18912
19810
|
!terminalControlsShareIncarnation(currentTerminalControl, terminalControl)) {
|
|
18913
19811
|
throw new Error("terminal control changed after the close action was listed; refresh AKK list");
|
|
18914
19812
|
}
|
|
18915
|
-
if (
|
|
18916
|
-
|
|
18917
|
-
options,
|
|
18918
|
-
storeDir: closeStoreDir,
|
|
19813
|
+
if (conversation.status === "closed" && currentTerminalControl) {
|
|
19814
|
+
const recoveredAuthority = exactVerifiedDeadTerminalAgentProcessAuthority({
|
|
18919
19815
|
conversation,
|
|
18920
|
-
|
|
19816
|
+
storeDir: closeStoreDir,
|
|
19817
|
+
terminalControl: currentTerminalControl,
|
|
19818
|
+
logPath
|
|
18921
19819
|
});
|
|
19820
|
+
if (recoveredAuthority.status === "invalid") {
|
|
19821
|
+
throw new Error(`cannot finish close recovery for ${conversation.conversation_id}; ` +
|
|
19822
|
+
recoveredAuthority.reason);
|
|
19823
|
+
}
|
|
19824
|
+
if (recoveredAuthority.status === "valid") {
|
|
19825
|
+
const audit = ensureVerifiedDeadTerminalAgentProcessEvent({
|
|
19826
|
+
logPath,
|
|
19827
|
+
proof: recoveredAuthority.proof,
|
|
19828
|
+
action: "managed_close"
|
|
19829
|
+
});
|
|
19830
|
+
const expectedMessageId = required(stringValue(currentTakeover?.terminal_bridge_message_id), "verified-dead close recovery has no terminal message id");
|
|
19831
|
+
const dispatchLedgerResolved = resolveVerifiedDeadTerminalBridgeDispatchLedger({
|
|
19832
|
+
terminalControl: currentTerminalControl,
|
|
19833
|
+
conversation,
|
|
19834
|
+
storeDir: closeStoreDir,
|
|
19835
|
+
statePath,
|
|
19836
|
+
logPath,
|
|
19837
|
+
expectedMessageId,
|
|
19838
|
+
reason: "conversation explicitly closed by request"
|
|
19839
|
+
});
|
|
19840
|
+
ensureVerifiedDeadConversationClosedEvent({
|
|
19841
|
+
logPath,
|
|
19842
|
+
conversation,
|
|
19843
|
+
evidenceId: audit.evidenceId
|
|
19844
|
+
});
|
|
19845
|
+
printJson({
|
|
19846
|
+
conversation,
|
|
19847
|
+
closed: true,
|
|
19848
|
+
recovered: true,
|
|
19849
|
+
terminal_dispatch_resolved: dispatchLedgerResolved
|
|
19850
|
+
});
|
|
19851
|
+
return;
|
|
19852
|
+
}
|
|
19853
|
+
}
|
|
19854
|
+
if (terminalControl && currentTerminalControl) {
|
|
19855
|
+
verifiedDeadProcess =
|
|
19856
|
+
await assertGenericCloseDoesNotBypassObservedHandoff({
|
|
19857
|
+
options,
|
|
19858
|
+
storeDir: closeStoreDir,
|
|
19859
|
+
conversation,
|
|
19860
|
+
terminalControl: currentTerminalControl
|
|
19861
|
+
});
|
|
18922
19862
|
}
|
|
18923
19863
|
const now = cliNow().toISOString();
|
|
18924
19864
|
const closeReason = options.reason ?? "closed by request";
|
|
19865
|
+
const verifiedDeadAudit = verifiedDeadProcess
|
|
19866
|
+
? ensureVerifiedDeadTerminalAgentProcessEvent({
|
|
19867
|
+
logPath,
|
|
19868
|
+
proof: verifiedDeadProcess,
|
|
19869
|
+
action: "managed_close"
|
|
19870
|
+
})
|
|
19871
|
+
: undefined;
|
|
19872
|
+
const verifiedDeadMessageId = verifiedDeadAudit
|
|
19873
|
+
? required(stringValue(currentTakeover?.terminal_bridge_message_id), "verified-dead close has no terminal message id")
|
|
19874
|
+
: undefined;
|
|
19875
|
+
if (verifiedDeadAudit && currentTerminalControl) {
|
|
19876
|
+
assertVerifiedDeadTerminalBridgeDispatchAuthority({
|
|
19877
|
+
terminalControl: currentTerminalControl,
|
|
19878
|
+
conversation,
|
|
19879
|
+
storeDir: closeStoreDir,
|
|
19880
|
+
statePath,
|
|
19881
|
+
logPath,
|
|
19882
|
+
expectedMessageId: verifiedDeadMessageId
|
|
19883
|
+
});
|
|
19884
|
+
}
|
|
18925
19885
|
const closed = {
|
|
18926
19886
|
...conversation,
|
|
18927
19887
|
status: "closed",
|
|
18928
19888
|
closed_at: now,
|
|
18929
19889
|
close_reason: closeReason,
|
|
19890
|
+
...(verifiedDeadAudit
|
|
19891
|
+
? {
|
|
19892
|
+
terminal_agent_process_disposition: {
|
|
19893
|
+
status: "verified_dead",
|
|
19894
|
+
proof: verifiedDeadAudit.proof,
|
|
19895
|
+
evidence_id: verifiedDeadAudit.evidenceId,
|
|
19896
|
+
recorded_at: verifiedDeadAudit.recordedAt
|
|
19897
|
+
}
|
|
19898
|
+
}
|
|
19899
|
+
: {}),
|
|
18930
19900
|
updated_at: now
|
|
18931
19901
|
};
|
|
18932
19902
|
saveState(statePath, closed);
|
|
19903
|
+
if (verifiedDeadAudit &&
|
|
19904
|
+
cliEnv().AKK_TEST_EXIT_AFTER_VERIFIED_DEAD_CLOSE_STATE === "1") {
|
|
19905
|
+
cliExit(86);
|
|
19906
|
+
}
|
|
18933
19907
|
let dispatchLedgerResolved = false;
|
|
18934
19908
|
let dispatchLedgerWarning;
|
|
18935
19909
|
if (currentTerminalControl) {
|
|
18936
19910
|
try {
|
|
18937
|
-
dispatchLedgerResolved =
|
|
18938
|
-
|
|
18939
|
-
|
|
18940
|
-
|
|
18941
|
-
|
|
18942
|
-
|
|
19911
|
+
dispatchLedgerResolved = verifiedDeadAudit
|
|
19912
|
+
? resolveVerifiedDeadTerminalBridgeDispatchLedger({
|
|
19913
|
+
terminalControl: currentTerminalControl,
|
|
19914
|
+
conversation: closed,
|
|
19915
|
+
storeDir: closeStoreDir,
|
|
19916
|
+
statePath,
|
|
19917
|
+
logPath,
|
|
19918
|
+
expectedMessageId: verifiedDeadMessageId,
|
|
19919
|
+
reason: "conversation explicitly closed by request"
|
|
19920
|
+
})
|
|
19921
|
+
: resolveTerminalBridgeDispatchLedger({
|
|
19922
|
+
terminalControl: currentTerminalControl,
|
|
19923
|
+
conversation: closed,
|
|
19924
|
+
expectedMessageId: stringValue(currentTakeover?.terminal_bridge_message_id),
|
|
19925
|
+
reason: "conversation explicitly closed by request"
|
|
19926
|
+
});
|
|
18943
19927
|
}
|
|
18944
19928
|
catch (error) {
|
|
18945
19929
|
dispatchLedgerWarning =
|
|
@@ -18951,13 +19935,22 @@ async function runClose(options) {
|
|
|
18951
19935
|
});
|
|
18952
19936
|
}
|
|
18953
19937
|
}
|
|
18954
|
-
|
|
18955
|
-
|
|
18956
|
-
|
|
18957
|
-
|
|
18958
|
-
|
|
18959
|
-
|
|
18960
|
-
}
|
|
19938
|
+
if (verifiedDeadAudit) {
|
|
19939
|
+
ensureVerifiedDeadConversationClosedEvent({
|
|
19940
|
+
logPath,
|
|
19941
|
+
conversation: closed,
|
|
19942
|
+
evidenceId: verifiedDeadAudit.evidenceId
|
|
19943
|
+
});
|
|
19944
|
+
}
|
|
19945
|
+
else {
|
|
19946
|
+
appendEvent(logPath, {
|
|
19947
|
+
ts: now,
|
|
19948
|
+
conversation_id: conversation.conversation_id,
|
|
19949
|
+
event: "conversation_closed",
|
|
19950
|
+
status: "closed",
|
|
19951
|
+
reason: closed.close_reason
|
|
19952
|
+
});
|
|
19953
|
+
}
|
|
18961
19954
|
runtimeLog("info", "conversation_closed", {
|
|
18962
19955
|
conversation_id: conversation.conversation_id,
|
|
18963
19956
|
status: "closed",
|
|
@@ -19003,6 +19996,19 @@ async function assertGenericCloseDoesNotBypassObservedHandoff({ options, storeDi
|
|
|
19003
19996
|
!terminalControlsShareIncarnation(sourceSession.binding.terminal_control, terminalControl)) {
|
|
19004
19997
|
return;
|
|
19005
19998
|
}
|
|
19999
|
+
const storedProof = exactVerifiedDeadTerminalAgentProcessAuthority({
|
|
20000
|
+
conversation,
|
|
20001
|
+
storeDir,
|
|
20002
|
+
terminalControl,
|
|
20003
|
+
logPath: stringValue(conversation.event_log_path) ??
|
|
20004
|
+
logPathForStatePath(required(stringValue(conversation.state_path), "managed Turn state path is unavailable"))
|
|
20005
|
+
});
|
|
20006
|
+
if (storedProof.status === "valid") {
|
|
20007
|
+
return storedProof.proof;
|
|
20008
|
+
}
|
|
20009
|
+
if (storedProof.status === "invalid") {
|
|
20010
|
+
throw new Error(`cannot close ${conversation.conversation_id}; ${storedProof.reason}`);
|
|
20011
|
+
}
|
|
19006
20012
|
const takeover = isRecord(conversation.native_session_takeover)
|
|
19007
20013
|
? conversation.native_session_takeover
|
|
19008
20014
|
: undefined;
|
|
@@ -19016,12 +20022,24 @@ async function assertGenericCloseDoesNotBypassObservedHandoff({ options, storeDi
|
|
|
19016
20022
|
terminal = await bridge.resolveStoredTerminal(sourceSession.agent, pid, terminalControl, { pid });
|
|
19017
20023
|
}
|
|
19018
20024
|
catch (error) {
|
|
19019
|
-
if (
|
|
19020
|
-
|
|
20025
|
+
if (error instanceof TerminalControlUnavailableError) {
|
|
20026
|
+
// An unavailable terminal cannot prove a live A -> B handoff. Preserve the
|
|
20027
|
+
// explicit Store-only close path used to retire unavailable Turn history.
|
|
20028
|
+
return;
|
|
19021
20029
|
}
|
|
19022
|
-
|
|
19023
|
-
|
|
19024
|
-
|
|
20030
|
+
const processObservation = await observeBoundTerminalAgentProcess({
|
|
20031
|
+
options,
|
|
20032
|
+
conversation,
|
|
20033
|
+
terminalControl
|
|
20034
|
+
});
|
|
20035
|
+
if (processObservation.status === "verified_dead") {
|
|
20036
|
+
return processObservation.proof;
|
|
20037
|
+
}
|
|
20038
|
+
if (processObservation.status === "unverifiable") {
|
|
20039
|
+
throw new Error(`cannot close ${conversation.conversation_id}; the bound agent process ` +
|
|
20040
|
+
`death is unverifiable: ${processObservation.reason}`);
|
|
20041
|
+
}
|
|
20042
|
+
throw error;
|
|
19025
20043
|
}
|
|
19026
20044
|
const companions = sourceSession.agent === "codex"
|
|
19027
20045
|
? codexAllowedCompanionSetForManagedSession({
|
|
@@ -20341,12 +21359,20 @@ async function runTerminalBridgeMonitorWithLock(options, lifecycle, expectedTerm
|
|
|
20341
21359
|
});
|
|
20342
21360
|
}
|
|
20343
21361
|
}
|
|
21362
|
+
const approvalPromptEvidenceUnavailable = isRecord(approval) &&
|
|
21363
|
+
approval.blocked === true &&
|
|
21364
|
+
approval.approvable === true &&
|
|
21365
|
+
(stringValue(approval.decision_mode) ?? "keys") === "keys" &&
|
|
21366
|
+
!stringValue(approval.fingerprint);
|
|
20344
21367
|
if (!suppressApprovalNotification &&
|
|
20345
21368
|
isRecord(approval) &&
|
|
20346
21369
|
approval.blocked === true &&
|
|
20347
|
-
approval.approvable !== true
|
|
20348
|
-
|
|
20349
|
-
|
|
21370
|
+
(approval.approvable !== true ||
|
|
21371
|
+
approvalPromptEvidenceUnavailable)) {
|
|
21372
|
+
const approvalReason = approvalPromptEvidenceUnavailable
|
|
21373
|
+
? `${executor.display_name} approval prompt lacks exact prompt-region evidence`
|
|
21374
|
+
: stringValue(approval.reason) ??
|
|
21375
|
+
"Claude Code permission state cannot be safely resolved through AKK";
|
|
20350
21376
|
appendEvent(logPath, {
|
|
20351
21377
|
ts: cliNow().toISOString(),
|
|
20352
21378
|
conversation_id: conversation.conversation_id,
|
|
@@ -20717,6 +21743,46 @@ async function runTerminalBridgeMonitorWithLock(options, lifecycle, expectedTerm
|
|
|
20717
21743
|
runPreparedCallback(preparedCompletion.prepared);
|
|
20718
21744
|
return;
|
|
20719
21745
|
}
|
|
21746
|
+
// Durable completion wins over process-death cleanup. A coding-agent
|
|
21747
|
+
// process may exit immediately after committing its final transcript, so
|
|
21748
|
+
// only classify the Turn as orphaned after this poll (and an independent
|
|
21749
|
+
// durable probe under the mutation locks) both find no completion.
|
|
21750
|
+
if (!completion) {
|
|
21751
|
+
const deadProcessStall = await stallAcceptedTurnForVerifiedDeadAgent({
|
|
21752
|
+
options,
|
|
21753
|
+
storeDir: pathsForConversationDir(path.dirname(statePath)).storeDir,
|
|
21754
|
+
statePath,
|
|
21755
|
+
logPath,
|
|
21756
|
+
expectedConversationId: conversation.conversation_id,
|
|
21757
|
+
expectedMessageId: currentMessageId
|
|
21758
|
+
});
|
|
21759
|
+
if (deadProcessStall.completionPreparation) {
|
|
21760
|
+
const completionPreparation = deadProcessStall.completionPreparation;
|
|
21761
|
+
if (!completionPreparation.claimed) {
|
|
21762
|
+
printJson({
|
|
21763
|
+
conversation: completionPreparation.conversation,
|
|
21764
|
+
monitored: true,
|
|
21765
|
+
terminal_bridge: true,
|
|
21766
|
+
completed: false,
|
|
21767
|
+
duplicate: true,
|
|
21768
|
+
reason: completionPreparation.reason
|
|
21769
|
+
});
|
|
21770
|
+
return;
|
|
21771
|
+
}
|
|
21772
|
+
runPreparedCallback(completionPreparation.prepared);
|
|
21773
|
+
return;
|
|
21774
|
+
}
|
|
21775
|
+
if (deadProcessStall.stalled) {
|
|
21776
|
+
printJson({
|
|
21777
|
+
conversation: deadProcessStall.conversation,
|
|
21778
|
+
monitored: true,
|
|
21779
|
+
terminal_bridge: true,
|
|
21780
|
+
stalled: true,
|
|
21781
|
+
reason: deadProcessStall.reason
|
|
21782
|
+
});
|
|
21783
|
+
return;
|
|
21784
|
+
}
|
|
21785
|
+
}
|
|
20720
21786
|
// A concrete approval or completion observed on this poll wins over a timeout boundary.
|
|
20721
21787
|
const nowMs = cliNowMs();
|
|
20722
21788
|
if (Number.isFinite(hardTimeoutMinutes) &&
|
|
@@ -24439,6 +25505,144 @@ function resolveTerminalBridgeDispatchLedger({ terminalControl, conversation, ex
|
|
|
24439
25505
|
});
|
|
24440
25506
|
return true;
|
|
24441
25507
|
}
|
|
25508
|
+
function assertVerifiedDeadTerminalBridgeDispatchAuthority({ terminalControl, conversation, storeDir, statePath, logPath, expectedMessageId }) {
|
|
25509
|
+
assertTurnBindingCurrent(conversation, "resolve a verified-dead agent dispatch for");
|
|
25510
|
+
const takeover = isRecord(conversation.native_session_takeover)
|
|
25511
|
+
? conversation.native_session_takeover
|
|
25512
|
+
: undefined;
|
|
25513
|
+
const submission = terminalBridgeSubmission(conversation);
|
|
25514
|
+
const rawStateReceiptHistory = takeover?.terminal_bridge_submission_receipts;
|
|
25515
|
+
const rawStateReceipts = Array.isArray(rawStateReceiptHistory)
|
|
25516
|
+
? rawStateReceiptHistory.filter((receipt) => isRecord(receipt) &&
|
|
25517
|
+
stringValue(receipt.message_id) === expectedMessageId)
|
|
25518
|
+
: [];
|
|
25519
|
+
const validatedStateReceipts = terminalBridgeSubmissionReceipts(conversation).filter((receipt) => stringValue(receipt.message_id) === expectedMessageId);
|
|
25520
|
+
const requestText = String(takeover?.terminal_bridge_request_text ?? conversation.user_request ?? "");
|
|
25521
|
+
const requestHash = terminalBridgeRequestFingerprint(requestText);
|
|
25522
|
+
const sessionId = sessionIdForConversation(conversation);
|
|
25523
|
+
const turnId = turnIdForConversation(conversation);
|
|
25524
|
+
const bindingId = stringValue(conversation.terminal_binding_id);
|
|
25525
|
+
const bindingGeneration = Number(conversation.terminal_binding_generation);
|
|
25526
|
+
const nativeThreadId = stringValue(conversation.native_thread_id) ??
|
|
25527
|
+
stringValue(takeover?.terminal_agent_session_id);
|
|
25528
|
+
const endpointAnchor = terminalEndpointFromControlRef(terminalControl).processAnchorPid;
|
|
25529
|
+
if (!expectedMessageId ||
|
|
25530
|
+
!Array.isArray(rawStateReceiptHistory) ||
|
|
25531
|
+
rawStateReceipts.length !== 1 ||
|
|
25532
|
+
validatedStateReceipts.length !== 1 ||
|
|
25533
|
+
canonicalJson(rawStateReceipts[0]) !== canonicalJson(submission) ||
|
|
25534
|
+
canonicalJson(validatedStateReceipts[0]) !== canonicalJson(submission) ||
|
|
25535
|
+
stringValue(takeover?.terminal_bridge_message_id) !== expectedMessageId ||
|
|
25536
|
+
submission?.status !== "agent_accepted" ||
|
|
25537
|
+
stringValue(submission.message_id) !== expectedMessageId ||
|
|
25538
|
+
stringValue(submission.session_id) !== sessionId ||
|
|
25539
|
+
stringValue(submission.turn_id) !== turnId ||
|
|
25540
|
+
stringValue(submission.binding_id) !== bindingId ||
|
|
25541
|
+
Number(submission.binding_generation) !== bindingGeneration ||
|
|
25542
|
+
!requestHash ||
|
|
25543
|
+
stringValue(submission.request_hash) !== requestHash ||
|
|
25544
|
+
!bindingId ||
|
|
25545
|
+
!Number.isSafeInteger(bindingGeneration) ||
|
|
25546
|
+
bindingGeneration < 1 ||
|
|
25547
|
+
!nativeThreadId ||
|
|
25548
|
+
!Number.isSafeInteger(endpointAnchor) ||
|
|
25549
|
+
Number(endpointAnchor) < 1 ||
|
|
25550
|
+
!sameCanonicalStatePath(conversation.state_path, statePath) ||
|
|
25551
|
+
path.resolve(stringValue(conversation.event_log_path) ?? "") !==
|
|
25552
|
+
path.resolve(logPath) ||
|
|
25553
|
+
path.resolve(managedSessionStoreDirForConversation(conversation) ?? "") !==
|
|
25554
|
+
path.resolve(storeDir)) {
|
|
25555
|
+
throw new Error(`verified-dead Turn ${conversation.conversation_id} has no exact accepted submission authority`);
|
|
25556
|
+
}
|
|
25557
|
+
const ledger = loadTerminalBridgeDispatchLedger(terminalControl);
|
|
25558
|
+
const rawReceiptHistory = ledger?.terminal_submission_receipts;
|
|
25559
|
+
const receipts = Array.isArray(rawReceiptHistory)
|
|
25560
|
+
? rawReceiptHistory.filter((receipt) => isRecord(receipt) &&
|
|
25561
|
+
stringValue(receipt.message_id) === expectedMessageId)
|
|
25562
|
+
: [];
|
|
25563
|
+
// Parse the entire append-only history as well as selecting the one exact
|
|
25564
|
+
// receipt. A synthesized top-level candidate is not sufficient cleanup
|
|
25565
|
+
// authority for a verified-dead process.
|
|
25566
|
+
const validatedReceiptHistory = terminalLedgerReceiptHistory(ledger);
|
|
25567
|
+
const validatedReceipts = validatedReceiptHistory.filter((receipt) => stringValue(receipt.message_id) === expectedMessageId);
|
|
25568
|
+
const receipt = receipts[0];
|
|
25569
|
+
const recordMatches = (record, expectedStatus) => Boolean(record &&
|
|
25570
|
+
record.status === expectedStatus &&
|
|
25571
|
+
stringValue(record.generation_id) === expectedMessageId &&
|
|
25572
|
+
stringValue(record.conversation_id) === conversation.conversation_id &&
|
|
25573
|
+
stringValue(record.session_id) === sessionId &&
|
|
25574
|
+
stringValue(record.turn_id) === turnId &&
|
|
25575
|
+
stringValue(record.message_id) === expectedMessageId &&
|
|
25576
|
+
stringValue(record.request_hash) === requestHash &&
|
|
25577
|
+
sameCanonicalStatePath(record.state_path, statePath) &&
|
|
25578
|
+
path.resolve(stringValue(record.store_dir) ?? "") ===
|
|
25579
|
+
path.resolve(storeDir) &&
|
|
25580
|
+
path.resolve(stringValue(record.event_log_path) ?? "") ===
|
|
25581
|
+
path.resolve(logPath) &&
|
|
25582
|
+
stringValue(record.binding_id) === bindingId &&
|
|
25583
|
+
Number(record.binding_generation) === bindingGeneration &&
|
|
25584
|
+
stringValue(record.native_thread_id) === nativeThreadId &&
|
|
25585
|
+
stringValue(record.executor_kind) ===
|
|
25586
|
+
executorForConversation(conversation).kind &&
|
|
25587
|
+
(stringValue(record.openclaw_session) ?? undefined) ===
|
|
25588
|
+
(stringValue(conversation.openclaw_session) ?? undefined) &&
|
|
25589
|
+
Boolean(record.callback_expected) === Boolean(conversation.gateway_method) &&
|
|
25590
|
+
(stringValue(record.message_type) ?? undefined) ===
|
|
25591
|
+
(stringValue(submission.message_type) ?? undefined) &&
|
|
25592
|
+
(stringValue(record.message_body_hash) ?? undefined) ===
|
|
25593
|
+
(stringValue(submission.message_body_hash) ?? undefined) &&
|
|
25594
|
+
isRecord(record.terminal_endpoint) &&
|
|
25595
|
+
terminalDispatchRecordMatchesControl(record, terminalControl, {
|
|
25596
|
+
requireProcessAnchor: true
|
|
25597
|
+
}) &&
|
|
25598
|
+
terminalDispatchRecordProcessAnchor(record) === endpointAnchor);
|
|
25599
|
+
if (!ledger ||
|
|
25600
|
+
terminalDispatchLedgerLooksLifecycle(ledger) ||
|
|
25601
|
+
!["agent_accepted", "resolved"].includes(String(ledger.status)) ||
|
|
25602
|
+
!recordMatches(ledger, ledger.status === "resolved" ? "resolved" : "agent_accepted") ||
|
|
25603
|
+
receipts.length !== 1 ||
|
|
25604
|
+
validatedReceipts.length !== 1 ||
|
|
25605
|
+
canonicalJson(receipts[0]) !== canonicalJson(validatedReceipts[0]) ||
|
|
25606
|
+
!recordMatches(receipt, "agent_accepted") ||
|
|
25607
|
+
(ledger.status === "resolved" &&
|
|
25608
|
+
validTimestampMs(ledger.resolved_at) === undefined)) {
|
|
25609
|
+
throw new Error(`verified-dead Turn ${conversation.conversation_id} no longer owns one exact terminal dispatch receipt`);
|
|
25610
|
+
}
|
|
25611
|
+
let stateAcceptance;
|
|
25612
|
+
let ledgerAcceptance;
|
|
25613
|
+
let receiptAcceptance;
|
|
25614
|
+
try {
|
|
25615
|
+
stateAcceptance = terminalAcceptanceEvidenceForConversation(conversation, requestText, submission.acceptance_evidence);
|
|
25616
|
+
ledgerAcceptance = terminalAcceptanceEvidenceForConversation(conversation, requestText, ledger.acceptance_evidence);
|
|
25617
|
+
receiptAcceptance = terminalAcceptanceEvidenceForConversation(conversation, requestText, receipt.acceptance_evidence);
|
|
25618
|
+
}
|
|
25619
|
+
catch (error) {
|
|
25620
|
+
throw new Error(`verified-dead Turn ${conversation.conversation_id} has invalid native acceptance evidence: ` +
|
|
25621
|
+
(error instanceof Error ? error.message : String(error)));
|
|
25622
|
+
}
|
|
25623
|
+
if (canonicalJson(stateAcceptance) !== canonicalJson(ledgerAcceptance) ||
|
|
25624
|
+
canonicalJson(stateAcceptance) !== canonicalJson(receiptAcceptance)) {
|
|
25625
|
+
throw new Error(`verified-dead Turn ${conversation.conversation_id} has conflicting native acceptance receipts`);
|
|
25626
|
+
}
|
|
25627
|
+
return { ledger, resolved: ledger.status === "resolved" };
|
|
25628
|
+
}
|
|
25629
|
+
function resolveVerifiedDeadTerminalBridgeDispatchLedger(options) {
|
|
25630
|
+
const authority = assertVerifiedDeadTerminalBridgeDispatchAuthority(options);
|
|
25631
|
+
if (authority.resolved) {
|
|
25632
|
+
return true;
|
|
25633
|
+
}
|
|
25634
|
+
saveTerminalBridgeDispatchLedger(options.terminalControl, {
|
|
25635
|
+
...authority.ledger,
|
|
25636
|
+
status: "resolved",
|
|
25637
|
+
resolved_at: cliNow().toISOString(),
|
|
25638
|
+
reason: options.reason
|
|
25639
|
+
});
|
|
25640
|
+
const resolved = assertVerifiedDeadTerminalBridgeDispatchAuthority(options);
|
|
25641
|
+
if (!resolved.resolved) {
|
|
25642
|
+
throw new Error(`verified-dead Turn ${options.conversation.conversation_id} dispatch did not resolve`);
|
|
25643
|
+
}
|
|
25644
|
+
return true;
|
|
25645
|
+
}
|
|
24442
25646
|
function terminalBridgeRequestFingerprint(value) {
|
|
24443
25647
|
const text = String(value ?? "");
|
|
24444
25648
|
return text ? createHash("sha256").update(text).digest("hex") : undefined;
|