@scotthuang/agent-knock-knock 0.10.3 → 0.11.0
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 +16 -0
- package/README.md +3 -3
- package/dist/src/cli.js +209 -22
- package/dist/src/cli.js.map +1 -1
- package/dist/src/native-thread-resume-snapshot.d.ts +87 -0
- package/dist/src/native-thread-resume-snapshot.js +451 -0
- package/dist/src/native-thread-resume-snapshot.js.map +1 -0
- package/dist/src/openclaw-plugin-helpers.d.ts +18 -1
- package/dist/src/openclaw-plugin-helpers.js +112 -13
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin.js +128 -16
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/terminal-agent-bridge.js +57 -4
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/docs/quickstart-tmux.md +2 -2
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +6 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.11.0 - 2026-08-07
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Add snapshot-bound native Resume navigation with deterministic numbers, collision-safe display-only short IDs, opaque five-minute handles, and a `previous` / `刚才那个` shortcut derived from the current Session's latest committed lifecycle transition.
|
|
8
|
+
|
|
9
|
+
### Security
|
|
10
|
+
|
|
11
|
+
- Resolve every Resume shortcut back to the complete UUID and exact evidence tuple, then fail closed if the terminal action generation, process, workspace, binding, ordered candidate snapshot, ownership, or TTL changed before native input.
|
|
12
|
+
|
|
13
|
+
## 0.10.4 - 2026-08-07
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Recognize exact Codex multiline drafts across TUI-painted visual wraps before dispatching Enter once, while preserving authored newlines, indentation, repeated spaces, stable-capture checks, and fail-closed content drift detection.
|
|
18
|
+
|
|
3
19
|
## 0.10.3 - 2026-08-07
|
|
4
20
|
|
|
5
21
|
### Added
|
package/README.md
CHANGED
|
@@ -214,7 +214,7 @@ The core command surface is intentionally small:
|
|
|
214
214
|
/akk threads <exact-terminal-id>
|
|
215
215
|
/akk new-thread <exact-terminal-id>
|
|
216
216
|
/akk clear-thread <exact-terminal-id>
|
|
217
|
-
/akk resume-thread <exact-terminal-id> [
|
|
217
|
+
/akk resume-thread <exact-terminal-id> [uuid|previous|number|@short-id|snapshot-handle]
|
|
218
218
|
/akk status [only|latest|codex|claude|@short-ref]
|
|
219
219
|
/akk respond <turn-selector>: <answer>
|
|
220
220
|
/akk cancel <turn-selector>
|
|
@@ -224,13 +224,13 @@ The core command surface is intentionally small:
|
|
|
224
224
|
|
|
225
225
|
Selectors fail closed: `only` works only with one actionable target, `latest` requires a unique newest target, and `codex` or `claude` must identify exactly one eligible pane. These names and `@short-ref` are human-facing resolution inputs; a natural-language tool call may preserve one explicitly named by the user, but must not infer one. Managed JSON actions contain the authoritative full `session_id` or `turn_id`. For first attach, an unmanaged raw-terminal row's send action may instead contain its own prefilled `selector`; its advertised raw controls may contain that row's prefilled `conversation_id`. Neither compatibility selector may be guessed, copied from another row, or passed in an authoritative ID field. Before every terminal operation, AKK revalidates the expected agent PID and tmux pane identity, then confirms that the process and pane working directories still match; every send also revalidates the idle prompt immediately before typing.
|
|
226
226
|
|
|
227
|
-
To change native context, first copy the full `terminal_id` from `/akk list`; lifecycle commands do not accept `@short-ref` or loose agent
|
|
227
|
+
To change native context, first copy the full `terminal_id` from `/akk list`; lifecycle commands do not accept an ordinary-send `@short-ref` or loose agent selector. `/akk threads <exact-terminal-id>` lists exact, same-workspace candidates with a deterministic number, a collision-safe display-only `@short-id`, an opaque snapshot handle, and the complete UUID. `/akk resume-thread <exact-terminal-id>` without a selection shows that list. A complete UUID remains compatible. A number or short ID resolves only against the latest list displayed in the same OpenClaw session, while an opaque handle names its exact snapshot; all expire after five minutes and fail after terminal, process, workspace, binding, candidate-set, or relevant action changes. None is ever passed to Codex or Claude Code as native identity: AKK resolves the saved tuple back to its full UUID and fresh evidence tokens first. `previous` (or `刚才那个`) is advertised only when the current Session's latest committed lifecycle transition identifies exactly one currently verified resumable source; it never guesses from title, recency, or static lineage. `/akk new-thread` and its human alias `/akk clear-thread` start a clean context. AKK does not poll bindings in the background: if a recorded owner process exits, the next lifecycle listing can classify that sole historical binding as resumable, and the resume mutation compare-and-swap detaches it before touching the terminal. Live, stale, expired, unsupported, busy, ambiguous, active-elsewhere, or unverifiable transitions fail closed. Do not send `/clear`, `/new`, `/resume`, `/status`, Codex `/fork`, `/side`, or `/btw`, Claude `/branch`, or any other first-line native slash command as an ordinary task or answer; use an advertised AKK action, express the request in natural language, or enter an unsupported native command manually in tmux.
|
|
228
228
|
|
|
229
229
|
For natural-language tool use, `agent_knock_knock_list` is terminal-first. Each live pane appears exactly once in `terminals[]`; `process_state` reports whether its coding-agent process is alive and `activity_state` reports the parsed screen state. `managed.session_id` identifies the continuing AKK session, `managed.current_turn` is its optional active Turn, and `managed.recent_turn` is retained history; retained Turns do not occupy the terminal. Pass `all=true` to include older entries in `managed.history`. By default, `unavailable_managed_turns[]` contains attention-needed records whose pane cannot be presented as a live terminal; `all=true` also includes retained unavailable history.
|
|
230
230
|
|
|
231
231
|
Use only an `available_actions` entry returned in that snapshot, begin with its prefilled authoritative arguments, and supply every `missing_required` field. A managed Session's `send` uses its prefilled `session_id` and creates a new Turn. For first attach only, use a discovery selector explicitly named by the user or the unmanaged raw-terminal row's prefilled `selector`; do not infer or reuse one. `respond` is available only while a Turn is `waiting_for_openclaw`; it uses `turn_id` and keeps the answer inside that Turn. Managed status, approval, cancellation, renewal, callback retry, and close also use the exact `turn_id`. A raw terminal may be controlled only through the exact status, approval, cancellation, or orphan-close action that its own row advertises with a prefilled `conversation_id`; never construct or guess one. For an ordinary send, add only `request`—`timeoutSeconds` is unsupported, and monitoring limits should be omitted unless the user explicitly asks to change them. AKK revalidates availability before every side effect.
|
|
232
232
|
|
|
233
|
-
The top-level v5 `action_contracts` adds `list_resumable_threads`, `new_thread`, and `resume_thread`. The terminal row advertises `list_resumable_threads` and, when currently safe, `new_thread`. Listing is read-only, takes only the full `terminal_id`, and returns a fresh `expected_binding_token` plus candidate rows; each `resumable=true`
|
|
233
|
+
The top-level v5 `action_contracts` adds `list_resumable_threads`, `new_thread`, and `resume_thread`. The terminal row advertises `list_resumable_threads` and, when currently safe, `new_thread`. Listing is read-only with respect to Session/Turn state, takes only the full `terminal_id`, and returns a fresh `expected_binding_token` plus candidate rows; each `resumable=true` row retains its complete UUID and exact prefilled `resume_thread` action. If `previous` is present, use only its exact prefilled action for a natural-language “刚才那个” request. Numbers, short IDs, and handles are human display/navigation aids, never tool arguments or authoritative native identity. The `new_thread` and `resume_thread` mutations require the fresh token, and resume additionally requires the candidate's complete `native_thread_id` and opaque `candidate_token`. Never construct, guess, truncate, combine across snapshots, or reuse those values after another terminal action. A lifecycle result contains Session and native-thread identities but no `turn_id` because no work was sent.
|
|
234
234
|
|
|
235
235
|
Workspace is not a routing boundary. AKK can list, inspect, and control verified panes across projects; when more than one target matches, use a selector to choose one explicitly.
|
|
236
236
|
|
package/dist/src/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ import { formatTranscript, readNdjsonLog } from "./transcript.js";
|
|
|
18
18
|
import { appendEvent, assertStoreWriterCompatible, defaultStoreDir, ensureDir, ensureStoreWritable, inspectStoreCompatibility, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, StoreLockTimeoutError, statePathForConversationId, withStoreWriterLease, withStoreWriterLeaseAsync } from "./store.js";
|
|
19
19
|
import { createManagedSessionId, createNativeThreadTransitionId, isExactNativeThreadId, managedSessionBindingToken, nativeThreadCommandFingerprint, terminalBindingFrom, unmanagedTerminalBindingToken } from "./managed-session.js";
|
|
20
20
|
import { classifyCodexLifecyclePostcondition, evaluateResumeCandidateAvailability, hasStrongCodexLifecycleIdentity, isFreshCodexPostProbeScreen } from "./native-thread-lifecycle-policy.js";
|
|
21
|
+
import { createNativeThreadResumeSnapshot, nativeThreadCandidateSnapshotFingerprint, nativeThreadResumeSnapshotRowsMatchCandidates, resolveNativeThreadResumeSelection, saveNativeThreadResumeSnapshot, sortNativeThreadCandidates, terminalActionFingerprint, verifiedPreviousResumeCandidate } from "./native-thread-resume-snapshot.js";
|
|
21
22
|
import { listManagedSessions, loadManagedSession, loadNativeThreadTransition, saveManagedSession, saveNativeThreadTransition, tryLoadManagedSession } from "./session-store.js";
|
|
22
23
|
import { StaticTerminalControlProvider, TmuxTerminalControlProvider, terminalPaneContainsProcess } from "./terminal-control-provider.js";
|
|
23
24
|
import { parseTerminalConversationId } from "./terminal-agent-adapter.js";
|
|
@@ -5219,7 +5220,7 @@ async function resumableThreadCandidates({ options, terminal, currentIdentity })
|
|
|
5219
5220
|
});
|
|
5220
5221
|
}
|
|
5221
5222
|
}
|
|
5222
|
-
return [...deduplicated.values()]
|
|
5223
|
+
return sortNativeThreadCandidates([...deduplicated.values()]
|
|
5223
5224
|
.map((candidate) => {
|
|
5224
5225
|
const nativeThreadId = candidate.native_thread_id;
|
|
5225
5226
|
const managed = managedByNativeId.get(nativeThreadId) ?? [];
|
|
@@ -5255,8 +5256,7 @@ async function resumableThreadCandidates({ options, terminal, currentIdentity })
|
|
|
5255
5256
|
? new Date(Number(candidate.updated_at_ms)).toISOString()
|
|
5256
5257
|
: undefined
|
|
5257
5258
|
};
|
|
5258
|
-
})
|
|
5259
|
-
.sort((left, right) => Number(right.updated_at_ms ?? 0) - Number(left.updated_at_ms ?? 0));
|
|
5259
|
+
}));
|
|
5260
5260
|
}
|
|
5261
5261
|
function encodeThreadCandidateToken(token) {
|
|
5262
5262
|
return Buffer.from(JSON.stringify(token), "utf8").toString("base64url");
|
|
@@ -5506,37 +5506,113 @@ async function runListResumableThreads(options) {
|
|
|
5506
5506
|
terminal,
|
|
5507
5507
|
currentIdentity: snapshot.identity
|
|
5508
5508
|
});
|
|
5509
|
+
const storeDir = storeDirFromOptions(options);
|
|
5510
|
+
const workspace = path.resolve(terminal.terminalControl.currentPath ?? process.cwd());
|
|
5511
|
+
const selectionScope = stringValue(options.selectionScope) ?? "cli:unscoped";
|
|
5512
|
+
const resumeSnapshot = createNativeThreadResumeSnapshot({
|
|
5513
|
+
storeDir,
|
|
5514
|
+
selectionScope,
|
|
5515
|
+
terminalId: terminal.conversationId,
|
|
5516
|
+
agent: terminal.agent,
|
|
5517
|
+
workspace,
|
|
5518
|
+
terminalControl: terminal.terminalControl,
|
|
5519
|
+
currentSessionId: snapshot.session?.session_id,
|
|
5520
|
+
currentNativeThreadId: snapshot.identity?.sessionId ??
|
|
5521
|
+
snapshot.session?.binding?.native_thread_id,
|
|
5522
|
+
expectedBindingToken: snapshot.bindingToken,
|
|
5523
|
+
terminalActionFingerprint: terminalActionFingerprint(loadTerminalBridgeDispatchLedger(terminal.terminalControl)),
|
|
5524
|
+
candidates
|
|
5525
|
+
});
|
|
5526
|
+
saveNativeThreadResumeSnapshot(terminalBridgeRuntimeDir(), storeDir, resumeSnapshot);
|
|
5527
|
+
const snapshotRows = new Map(resumeSnapshot.rows.map((row) => [row.native_thread_id, row]));
|
|
5528
|
+
const resumeAction = (candidate) => ({
|
|
5529
|
+
tool: "agent_knock_knock_resume_thread",
|
|
5530
|
+
arguments: {
|
|
5531
|
+
terminal_id: terminal.conversationId,
|
|
5532
|
+
native_thread_id: candidate.native_thread_id,
|
|
5533
|
+
expected_binding_token: snapshot.bindingToken,
|
|
5534
|
+
...(candidate.candidate_token
|
|
5535
|
+
? { candidate_token: candidate.candidate_token }
|
|
5536
|
+
: {})
|
|
5537
|
+
},
|
|
5538
|
+
requires_user_intent: true
|
|
5539
|
+
});
|
|
5540
|
+
const previousCandidate = previousCommittedResumeCandidate({
|
|
5541
|
+
storeDir,
|
|
5542
|
+
terminal,
|
|
5543
|
+
currentSession: snapshot.session,
|
|
5544
|
+
candidates
|
|
5545
|
+
});
|
|
5546
|
+
const previousSnapshotRow = previousCandidate
|
|
5547
|
+
? snapshotRows.get(previousCandidate.native_thread_id)
|
|
5548
|
+
: undefined;
|
|
5509
5549
|
printJson({
|
|
5510
5550
|
terminal_id: terminal.conversationId,
|
|
5511
5551
|
agent: terminal.agent,
|
|
5512
|
-
workspace
|
|
5552
|
+
workspace,
|
|
5513
5553
|
current_session_id: snapshot.session?.session_id ?? null,
|
|
5514
5554
|
current_native_thread_id: snapshot.identity?.sessionId ??
|
|
5515
5555
|
snapshot.session?.binding?.native_thread_id ??
|
|
5516
5556
|
null,
|
|
5517
5557
|
expected_binding_token: snapshot.bindingToken,
|
|
5518
5558
|
capability: snapshot.capabilities,
|
|
5559
|
+
selection_snapshot: {
|
|
5560
|
+
schema: resumeSnapshot.schema,
|
|
5561
|
+
version: resumeSnapshot.version,
|
|
5562
|
+
snapshot_id: resumeSnapshot.snapshot_id,
|
|
5563
|
+
created_at: resumeSnapshot.created_at,
|
|
5564
|
+
expires_at: resumeSnapshot.expires_at,
|
|
5565
|
+
scope: "exact selection snapshot, scope, and terminal",
|
|
5566
|
+
display_only: true
|
|
5567
|
+
},
|
|
5568
|
+
...(previousCandidate && previousSnapshotRow
|
|
5569
|
+
? {
|
|
5570
|
+
previous: {
|
|
5571
|
+
keyword: "previous",
|
|
5572
|
+
native_thread_id: previousCandidate.native_thread_id,
|
|
5573
|
+
selection_number: previousSnapshotRow.selection_number,
|
|
5574
|
+
short_id: previousSnapshotRow.short_id,
|
|
5575
|
+
selection_handle: previousSnapshotRow.selection_handle,
|
|
5576
|
+
available_actions: {
|
|
5577
|
+
resume_thread: resumeAction(previousCandidate)
|
|
5578
|
+
}
|
|
5579
|
+
}
|
|
5580
|
+
}
|
|
5581
|
+
: {}),
|
|
5519
5582
|
threads: candidates.map((candidate) => ({
|
|
5520
5583
|
...candidate,
|
|
5584
|
+
selection_number: snapshotRows.get(candidate.native_thread_id)?.selection_number,
|
|
5585
|
+
short_id: snapshotRows.get(candidate.native_thread_id)?.short_id,
|
|
5586
|
+
selection_handle: snapshotRows.get(candidate.native_thread_id)?.selection_handle,
|
|
5587
|
+
selection_scope: "current_snapshot",
|
|
5521
5588
|
available_actions: candidate.resumable
|
|
5522
5589
|
? {
|
|
5523
|
-
resume_thread:
|
|
5524
|
-
tool: "agent_knock_knock_resume_thread",
|
|
5525
|
-
arguments: {
|
|
5526
|
-
terminal_id: terminal.conversationId,
|
|
5527
|
-
native_thread_id: candidate.native_thread_id,
|
|
5528
|
-
expected_binding_token: snapshot.bindingToken,
|
|
5529
|
-
...(candidate.candidate_token
|
|
5530
|
-
? { candidate_token: candidate.candidate_token }
|
|
5531
|
-
: {})
|
|
5532
|
-
},
|
|
5533
|
-
requires_user_intent: true
|
|
5534
|
-
}
|
|
5590
|
+
resume_thread: resumeAction(candidate)
|
|
5535
5591
|
}
|
|
5536
5592
|
: {}
|
|
5537
5593
|
}))
|
|
5538
5594
|
});
|
|
5539
5595
|
}
|
|
5596
|
+
function previousCommittedResumeCandidate({ storeDir, terminal, currentSession, candidates }) {
|
|
5597
|
+
if (!currentSession?.last_transition_id) {
|
|
5598
|
+
return undefined;
|
|
5599
|
+
}
|
|
5600
|
+
let transition;
|
|
5601
|
+
try {
|
|
5602
|
+
transition = loadNativeThreadTransition(storeDir, currentSession.last_transition_id);
|
|
5603
|
+
}
|
|
5604
|
+
catch {
|
|
5605
|
+
return undefined;
|
|
5606
|
+
}
|
|
5607
|
+
return verifiedPreviousResumeCandidate({
|
|
5608
|
+
terminalId: terminal.conversationId,
|
|
5609
|
+
agent: terminal.agent,
|
|
5610
|
+
workspace: terminal.terminalControl.currentPath ?? process.cwd(),
|
|
5611
|
+
currentSession,
|
|
5612
|
+
transition,
|
|
5613
|
+
candidates
|
|
5614
|
+
});
|
|
5615
|
+
}
|
|
5540
5616
|
async function waitForVerifiedThreadTransition({ options, terminal, beforeIdentity, physicalBeforeIdentity, allowedCompanionIdentity, allowedAdditionalIdentities = [], operation, plan, initialScreenDigest }) {
|
|
5541
5617
|
const bridge = createTerminalAgentBridge(options);
|
|
5542
5618
|
let probeSent = false;
|
|
@@ -5812,15 +5888,96 @@ async function runNewThread(options) {
|
|
|
5812
5888
|
});
|
|
5813
5889
|
}
|
|
5814
5890
|
async function runResumeThread(options) {
|
|
5815
|
-
const
|
|
5816
|
-
|
|
5817
|
-
|
|
5891
|
+
const exactNativeThreadId = stringValue(options.nativeThread ?? options.nativeThreadId);
|
|
5892
|
+
const hasSnapshotSelection = Boolean(options.selectionSnapshot ||
|
|
5893
|
+
options.selectionNumber ||
|
|
5894
|
+
options.selectionShortId ||
|
|
5895
|
+
options.selectionHandle);
|
|
5896
|
+
if (exactNativeThreadId && hasSnapshotSelection) {
|
|
5897
|
+
throw new Error("--native-thread cannot be combined with a snapshot-bound selection");
|
|
5898
|
+
}
|
|
5899
|
+
if (exactNativeThreadId) {
|
|
5900
|
+
if (!isExactNativeThreadId(exactNativeThreadId)) {
|
|
5901
|
+
throw new Error("--native-thread must be a complete native thread UUID");
|
|
5902
|
+
}
|
|
5903
|
+
return runNativeThreadTransition(options, {
|
|
5904
|
+
kind: "resume_thread",
|
|
5905
|
+
nativeThreadId: exactNativeThreadId.toLowerCase()
|
|
5906
|
+
});
|
|
5818
5907
|
}
|
|
5819
|
-
|
|
5908
|
+
if (!hasSnapshotSelection) {
|
|
5909
|
+
throw new Error("--native-thread or a snapshot-bound resume selection is required");
|
|
5910
|
+
}
|
|
5911
|
+
const terminal = await resolveLifecycleTerminal(options);
|
|
5912
|
+
const storeDir = storeDirFromOptions(options);
|
|
5913
|
+
const selectionScope = required(stringValue(options.selectionScope), "--selection-scope is required for snapshot-bound resume");
|
|
5914
|
+
const rawSelectionNumber = options.selectionNumber;
|
|
5915
|
+
if (rawSelectionNumber !== undefined &&
|
|
5916
|
+
(typeof rawSelectionNumber !== "string" ||
|
|
5917
|
+
!/^[1-9][0-9]*$/u.test(rawSelectionNumber))) {
|
|
5918
|
+
throw new Error("--selection-number must be a positive integer");
|
|
5919
|
+
}
|
|
5920
|
+
const selectionNumber = rawSelectionNumber === undefined
|
|
5921
|
+
? undefined
|
|
5922
|
+
: Number(rawSelectionNumber);
|
|
5923
|
+
if (selectionNumber !== undefined &&
|
|
5924
|
+
!Number.isSafeInteger(selectionNumber)) {
|
|
5925
|
+
throw new Error("--selection-number must be a positive safe integer");
|
|
5926
|
+
}
|
|
5927
|
+
const selection = resolveNativeThreadResumeSelection({
|
|
5928
|
+
runtimeDir: terminalBridgeRuntimeDir(),
|
|
5929
|
+
storeDir,
|
|
5930
|
+
terminalId: terminal.conversationId,
|
|
5931
|
+
selectionScope,
|
|
5932
|
+
snapshotId: stringValue(options.selectionSnapshot),
|
|
5933
|
+
selectionNumber,
|
|
5934
|
+
shortId: stringValue(options.selectionShortId),
|
|
5935
|
+
selectionHandle: stringValue(options.selectionHandle)
|
|
5936
|
+
});
|
|
5937
|
+
assertResumeSnapshotMatchesTerminal(selection.snapshot, terminal);
|
|
5938
|
+
assertResumeSnapshotActionFingerprint(selection.snapshot, terminal);
|
|
5939
|
+
const selectedOptions = {
|
|
5940
|
+
...options,
|
|
5941
|
+
expectedBindingToken: selection.snapshot.expected_binding_token,
|
|
5942
|
+
candidateToken: selection.row.candidate_token
|
|
5943
|
+
};
|
|
5944
|
+
return runNativeThreadTransition(selectedOptions, {
|
|
5820
5945
|
kind: "resume_thread",
|
|
5821
|
-
nativeThreadId:
|
|
5946
|
+
nativeThreadId: selection.row.native_thread_id.toLowerCase(),
|
|
5947
|
+
selectionSnapshot: selection.snapshot
|
|
5822
5948
|
});
|
|
5823
5949
|
}
|
|
5950
|
+
function assertResumeSnapshotMatchesTerminal(snapshot, terminal) {
|
|
5951
|
+
const workspace = path.resolve(terminal.terminalControl.currentPath ?? process.cwd());
|
|
5952
|
+
if (snapshot.terminal_id !== terminal.conversationId ||
|
|
5953
|
+
snapshot.agent !== terminal.agent ||
|
|
5954
|
+
path.resolve(snapshot.workspace) !== workspace ||
|
|
5955
|
+
snapshot.terminal_control.target !== terminal.terminalControl.target ||
|
|
5956
|
+
snapshot.terminal_control.socket_path !== terminal.terminalControl.socketPath ||
|
|
5957
|
+
snapshot.terminal_control.pane_pid !== terminal.terminalControl.panePid) {
|
|
5958
|
+
throw new Error("resume selection terminal, process, or workspace changed; run /akk threads again");
|
|
5959
|
+
}
|
|
5960
|
+
}
|
|
5961
|
+
function assertResumeSnapshotActionFingerprint(snapshot, terminal) {
|
|
5962
|
+
const current = terminalActionFingerprint(loadTerminalBridgeDispatchLedger(terminal.terminalControl));
|
|
5963
|
+
if (current !== snapshot.terminal_action_fingerprint) {
|
|
5964
|
+
throw new Error("terminal action history changed after the resume snapshot; run /akk threads again");
|
|
5965
|
+
}
|
|
5966
|
+
}
|
|
5967
|
+
function assertResumeSnapshotCandidates(snapshot, candidates) {
|
|
5968
|
+
if (nativeThreadCandidateSnapshotFingerprint(candidates) !==
|
|
5969
|
+
snapshot.candidate_snapshot_fingerprint) {
|
|
5970
|
+
throw new Error("resume candidates changed or reordered after the snapshot; run /akk threads again");
|
|
5971
|
+
}
|
|
5972
|
+
if (!nativeThreadResumeSnapshotRowsMatchCandidates(snapshot, candidates)) {
|
|
5973
|
+
throw new Error("resume selection rows no longer match the exact candidate snapshot; run /akk threads again");
|
|
5974
|
+
}
|
|
5975
|
+
}
|
|
5976
|
+
function assertResumeSnapshotNotExpired(snapshot) {
|
|
5977
|
+
if (Date.parse(snapshot.expires_at) <= Date.now()) {
|
|
5978
|
+
throw new Error("resume selection snapshot expired; run /akk threads again");
|
|
5979
|
+
}
|
|
5980
|
+
}
|
|
5824
5981
|
async function runNativeThreadTransition(options, operation) {
|
|
5825
5982
|
const initiallyResolved = await resolveLifecycleTerminal(options);
|
|
5826
5983
|
const storeDir = storeDirFromOptions(options);
|
|
@@ -5833,7 +5990,18 @@ async function runNativeThreadTransition(options, operation) {
|
|
|
5833
5990
|
terminalControlSelectorKey(initiallyResolved.terminalControl)) {
|
|
5834
5991
|
throw new Error("terminal identity changed while waiting for lifecycle control; refresh list");
|
|
5835
5992
|
}
|
|
5993
|
+
if (operation.kind === "resume_thread" && operation.selectionSnapshot) {
|
|
5994
|
+
assertResumeSnapshotNotExpired(operation.selectionSnapshot);
|
|
5995
|
+
assertResumeSnapshotMatchesTerminal(operation.selectionSnapshot, terminal);
|
|
5996
|
+
assertResumeSnapshotActionFingerprint(operation.selectionSnapshot, terminal);
|
|
5997
|
+
}
|
|
5836
5998
|
await recoverLifecycleFenceBeforeMutation({ options, terminal });
|
|
5999
|
+
if (operation.kind === "resume_thread" && operation.selectionSnapshot) {
|
|
6000
|
+
// Recovery may resolve or otherwise rewrite a durable dispatch fence.
|
|
6001
|
+
// A snapshot created before that mutation is stale even when the
|
|
6002
|
+
// binding and candidate files themselves did not change.
|
|
6003
|
+
assertResumeSnapshotActionFingerprint(operation.selectionSnapshot, terminal);
|
|
6004
|
+
}
|
|
5837
6005
|
const snapshot = await currentLifecycleSnapshot(options, terminal, { materialize: true });
|
|
5838
6006
|
const expectedToken = required(stringValue(options.expectedBindingToken), "--expected-binding-token is required");
|
|
5839
6007
|
if (expectedToken !== snapshot.bindingToken) {
|
|
@@ -5850,6 +6018,14 @@ async function runNativeThreadTransition(options, operation) {
|
|
|
5850
6018
|
snapshot.capabilities.resumeExact !== true) {
|
|
5851
6019
|
throw new Error(snapshot.capabilities.reason);
|
|
5852
6020
|
}
|
|
6021
|
+
if (operation.kind === "resume_thread" && operation.selectionSnapshot) {
|
|
6022
|
+
const guardedCandidates = await resumableThreadCandidates({
|
|
6023
|
+
options,
|
|
6024
|
+
terminal,
|
|
6025
|
+
currentIdentity: snapshot.identity
|
|
6026
|
+
});
|
|
6027
|
+
assertResumeSnapshotCandidates(operation.selectionSnapshot, guardedCandidates);
|
|
6028
|
+
}
|
|
5853
6029
|
const plan = snapshot.adapter.planThreadLifecycle?.(operation, snapshot.capabilities);
|
|
5854
6030
|
if (!plan) {
|
|
5855
6031
|
throw new Error("the agent adapter did not produce a lifecycle plan");
|
|
@@ -5960,6 +6136,13 @@ async function runNativeThreadTransition(options, operation) {
|
|
|
5960
6136
|
terminal,
|
|
5961
6137
|
currentIdentity: beforeIdentity
|
|
5962
6138
|
});
|
|
6139
|
+
if (operation.selectionSnapshot) {
|
|
6140
|
+
// The Codex identity probe and other readiness checks above are
|
|
6141
|
+
// asynchronous. Rebind the whole displayed snapshot, not only its
|
|
6142
|
+
// selected UUID, after those checks and before any lifecycle state
|
|
6143
|
+
// is persisted.
|
|
6144
|
+
assertResumeSnapshotCandidates(operation.selectionSnapshot, candidates);
|
|
6145
|
+
}
|
|
5963
6146
|
const candidate = candidates.find((entry) => entry.native_thread_id === operation.nativeThreadId);
|
|
5964
6147
|
if (!candidate) {
|
|
5965
6148
|
throw new Error(`native thread ${operation.nativeThreadId} is not a verified same-workspace candidate`);
|
|
@@ -6169,6 +6352,9 @@ async function runNativeThreadTransition(options, operation) {
|
|
|
6169
6352
|
terminalControl: terminal.terminalControl
|
|
6170
6353
|
});
|
|
6171
6354
|
}
|
|
6355
|
+
if (operation.kind === "resume_thread" && operation.selectionSnapshot) {
|
|
6356
|
+
assertResumeSnapshotNotExpired(operation.selectionSnapshot);
|
|
6357
|
+
}
|
|
6172
6358
|
inputStarted = true;
|
|
6173
6359
|
await bridge.send(terminal.agent, terminal.terminalControl, transitionStep.command, { runtime: beforeRuntime });
|
|
6174
6360
|
const submittedAt = new Date().toISOString();
|
|
@@ -18165,8 +18351,9 @@ function usage() {
|
|
|
18165
18351
|
agent-knock-knock send [--session <session-id|selector>] [--conversation <selector>] --message <text> [--type task] [--agent-timeout-minutes <minutes>] [--agent-hard-timeout-minutes <minutes>]
|
|
18166
18352
|
agent-knock-knock new-thread --terminal <exact-terminal-id> --expected-binding-token <token>
|
|
18167
18353
|
agent-knock-knock clear-thread --terminal <exact-terminal-id> --expected-binding-token <token>
|
|
18168
|
-
agent-knock-knock list-resumable-threads --terminal <exact-terminal-id>
|
|
18354
|
+
agent-knock-knock list-resumable-threads --terminal <exact-terminal-id> [--selection-scope <opaque-scope>]
|
|
18169
18355
|
agent-knock-knock resume-thread --terminal <exact-terminal-id> --native-thread <uuid> --expected-binding-token <token> --candidate-token <token>
|
|
18356
|
+
agent-knock-knock resume-thread --terminal <exact-terminal-id> (--selection-handle <handle> | --selection-snapshot <id> (--selection-number <n> | --selection-short-id <@id>)) --selection-scope <opaque-scope>
|
|
18170
18357
|
agent-knock-knock respond --turn <turn-id|selector> --message <text> [--conversation <selector>]
|
|
18171
18358
|
agent-knock-knock approve [--turn <turn-id|selector>] [--conversation <selector>] --expected-approval-fingerprint <fingerprint>
|
|
18172
18359
|
agent-knock-knock cancel [--turn <turn-id|selector>] [--conversation <selector>]
|