@scotthuang/agent-knock-knock 0.12.18 → 0.12.19
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 +15 -0
- package/README.md +9 -9
- package/dist/src/claude-local-transcript-provider.d.ts +32 -0
- package/dist/src/claude-local-transcript-provider.js +106 -20
- package/dist/src/claude-local-transcript-provider.js.map +1 -1
- package/dist/src/cli-core.js +3 -0
- package/dist/src/cli-core.js.map +1 -1
- package/dist/src/openclaw-plugin-command-adapter.js +15 -5
- package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
- package/dist/src/openclaw-plugin-helpers.js +5 -1
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin-schemas.js +1 -1
- package/dist/src/openclaw-plugin-schemas.js.map +1 -1
- package/dist/src/store.d.ts +1 -1
- package/dist/src/store.js +3 -3
- package/dist/src/store.js.map +1 -1
- package/dist/src/terminal-command-cli-adapter.d.ts +16 -0
- package/dist/src/terminal-command-cli-adapter.js +67 -6
- package/dist/src/terminal-command-cli-adapter.js.map +1 -1
- package/dist/src/terminal-list-cli-adapter.js +4 -3
- package/dist/src/terminal-list-cli-adapter.js.map +1 -1
- package/dist/src/terminal-list-renderer.js +4 -4
- package/dist/src/terminal-list-renderer.js.map +1 -1
- package/dist/src/terminal-watch-cli-adapter.d.ts +44 -1
- package/dist/src/terminal-watch-cli-adapter.js +616 -21
- package/dist/src/terminal-watch-cli-adapter.js.map +1 -1
- package/dist/src/terminal-watch-service.js +2 -0
- package/dist/src/terminal-watch-service.js.map +1 -1
- package/dist/src/terminal-watch-store.d.ts +61 -4
- package/dist/src/terminal-watch-store.js +312 -11
- package/dist/src/terminal-watch-store.js.map +1 -1
- package/docs/quickstart-herdr.md +3 -3
- package/docs/quickstart-tmux.md +4 -4
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +7 -7
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { callbackRouteForConversation } from "./callback-route-authority.js";
|
|
3
4
|
import { parseCallbackRoute } from "./callback-transport.js";
|
|
4
|
-
import { captureClaudeHumanStartedActiveTaskAnchor, observeClaudeHumanStartedActiveTask } from "./claude-local-transcript-provider.js";
|
|
5
|
+
import { captureClaudeTranscriptAnchor, captureClaudeHumanStartedActiveTaskAnchor, observeClaudeUserExplicitFallbackTranscript, observeClaudeHumanStartedActiveTask } from "./claude-local-transcript-provider.js";
|
|
5
6
|
import { claudeRuntimeCompatibilityWarning } from "./claude-lifecycle-compatibility.js";
|
|
6
7
|
import { codexRuntimeCompatibilityProfile } from "./codex-lifecycle-compatibility.js";
|
|
7
8
|
import { turnIdForConversation } from "./protocol.js";
|
|
8
9
|
import { decideTerminalWatchExternalTaskAuthority } from "./terminal-action-projection.js";
|
|
9
10
|
import { exactTerminalWatchAction } from "./terminal-list-renderer.js";
|
|
10
|
-
import { captureCodexHumanStartedActiveTaskAnchor, observeCodexHumanStartedActiveTask } from "./terminal-submission-acceptance.js";
|
|
11
|
+
import { captureCodexCandidateSetRolloutAcceptanceAnchor, captureCodexRolloutAcceptanceAnchor, detectCodexBoundRolloutCompletion, detectCodexCandidateSetRolloutAcceptance, detectCodexRolloutAcceptance, captureCodexHumanStartedActiveTaskAnchor, observeCodexHumanStartedActiveTask } from "./terminal-submission-acceptance.js";
|
|
11
12
|
import { sameTerminalControlEvidenceIncarnation, terminalControlEvidence } from "./terminal-control-ref.js";
|
|
12
13
|
import { createTerminalWatchCallbackCliAdapter, resolveTerminalWatchOpenClawCallback, resolveTerminalWatchOpenClawCallbackContext } from "./terminal-watch-callback-cli-adapter.js";
|
|
13
14
|
import { createTerminalWatchService, terminalWatchObservationFence } from "./terminal-watch-service.js";
|
|
14
|
-
import { createTerminalWatchStore, terminalWatchIdentityFingerprint } from "./terminal-watch-store.js";
|
|
15
|
+
import { createClaudeUserExplicitFallbackWatchAnchor, createCodexUserExplicitFallbackWatchAnchor, createTerminalWatchStore, isUserExplicitFallbackWatch, terminalUserExplicitFallbackWatchId, terminalWatchIdentityFingerprint } from "./terminal-watch-store.js";
|
|
15
16
|
import { isRecord, nonBlankString as stringValue } from "./value-guards.js";
|
|
16
17
|
const DEFAULT_TERMINAL_WATCH_HARD_TIMEOUT_MINUTES = 720;
|
|
17
18
|
export function createTerminalWatchCliAdapter(dependencies) {
|
|
@@ -81,6 +82,162 @@ export function createTerminalWatchCliAdapter(dependencies) {
|
|
|
81
82
|
}
|
|
82
83
|
});
|
|
83
84
|
}
|
|
85
|
+
async function prepareUserExplicitFallbackWatch(input) {
|
|
86
|
+
const callbackRoute = callbackRouteForUserExplicitFallback(input.options);
|
|
87
|
+
if (!callbackRoute)
|
|
88
|
+
return undefined;
|
|
89
|
+
await bestEffortStabilizePriorFallbackWatch(input.options, input.terminal.conversationId, input.requestHash);
|
|
90
|
+
const observed = await exactTerminalForWatch(input.terminal.conversationId, input.options, dependencies);
|
|
91
|
+
const rawTerminal = observed.rawTerminal;
|
|
92
|
+
assertSameUserExplicitFallbackTerminal(input.terminal, rawTerminal);
|
|
93
|
+
const agentVersion = requiredString(rawTerminal.agent_version, "running coding-agent version");
|
|
94
|
+
let anchor;
|
|
95
|
+
if (input.terminal.agent === "codex") {
|
|
96
|
+
const inventory = rawTerminal._codex_open_root_rollout_inventory;
|
|
97
|
+
const acceptanceAnchor = isRecord(inventory)
|
|
98
|
+
? captureCodexCandidateSetRolloutAcceptanceAnchor({
|
|
99
|
+
inventory: inventory,
|
|
100
|
+
now: dependencies.now()
|
|
101
|
+
})
|
|
102
|
+
: captureCodexRolloutAcceptanceAnchor({
|
|
103
|
+
nativeThreadId: requiredString(rawTerminal.native_agent_session_id, "Codex native thread id"),
|
|
104
|
+
processUuid: requiredString(rawTerminal.native_agent_process_uuid, "Codex process UUID"),
|
|
105
|
+
processBirth: requiredString(rawTerminal.native_agent_process_birth, "Codex process birth"),
|
|
106
|
+
mode: "existing",
|
|
107
|
+
rollout: codexIdentity(rawTerminal).rollout,
|
|
108
|
+
now: dependencies.now()
|
|
109
|
+
});
|
|
110
|
+
anchor = createCodexUserExplicitFallbackWatchAnchor({
|
|
111
|
+
acceptanceAnchor,
|
|
112
|
+
requestHash: input.requestHash,
|
|
113
|
+
codexVersion: agentVersion
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
const transcriptAnchor = captureClaudeTranscriptAnchor({
|
|
118
|
+
sessionId: requiredString(rawTerminal.native_agent_session_id, "Claude native session id"),
|
|
119
|
+
cwd: terminalWorkspace(rawTerminal),
|
|
120
|
+
pid: positiveInteger(rawTerminal.pid, "Claude PID"),
|
|
121
|
+
claudeHome: stringValue(input.options.claudeHome),
|
|
122
|
+
agentRows: dependencies.loadClaudeAgentRows(input.options, { required: true }),
|
|
123
|
+
now: dependencies.now()
|
|
124
|
+
});
|
|
125
|
+
if (!transcriptAnchor) {
|
|
126
|
+
throw new Error("Claude transcript anchor is unavailable before terminal input");
|
|
127
|
+
}
|
|
128
|
+
anchor = createClaudeUserExplicitFallbackWatchAnchor({
|
|
129
|
+
transcriptAnchor,
|
|
130
|
+
requestHash: input.requestHash,
|
|
131
|
+
claudeVersion: agentVersion
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
watchId: terminalUserExplicitFallbackWatchId({
|
|
136
|
+
messageId: input.messageId,
|
|
137
|
+
physicalToken: input.physicalToken,
|
|
138
|
+
requestHash: input.requestHash
|
|
139
|
+
}),
|
|
140
|
+
terminalId: input.terminal.conversationId,
|
|
141
|
+
agent: input.terminal.agent,
|
|
142
|
+
pid: input.terminal.pid,
|
|
143
|
+
terminalEndpoint: terminalControlEvidence(input.terminal.terminalControl),
|
|
144
|
+
terminalIdentity: terminalWatchIdentity(rawTerminal, input.physicalToken),
|
|
145
|
+
physicalToken: input.physicalToken,
|
|
146
|
+
requestHash: input.requestHash,
|
|
147
|
+
callbackRoute,
|
|
148
|
+
openclawSession: callbackRoute.controller_session_id,
|
|
149
|
+
openclawBin: stringValue(input.options.openclawBin) ?? "openclaw",
|
|
150
|
+
timeoutMs: positiveMinutes(input.options.hardTimeoutMinutes ??
|
|
151
|
+
input.options.agentHardTimeoutMinutes, DEFAULT_TERMINAL_WATCH_HARD_TIMEOUT_MINUTES) * 60_000,
|
|
152
|
+
anchor
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
async function attachUserExplicitFallbackWatch(input) {
|
|
156
|
+
let observed;
|
|
157
|
+
try {
|
|
158
|
+
observed = await dependencies.observeExactTerminal({
|
|
159
|
+
options: input.options,
|
|
160
|
+
terminalId: input.prepared.terminalId
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Observation itself is best effort after the user's physical Send.
|
|
165
|
+
// Persist the exact pre-Send identity and let the durable provider
|
|
166
|
+
// artifact settle or invalidate the Watch.
|
|
167
|
+
}
|
|
168
|
+
if (observed?.state === "available") {
|
|
169
|
+
assertPreparedFallbackTerminal(input.prepared, observed.rawTerminal);
|
|
170
|
+
}
|
|
171
|
+
// An absent or temporarily unavailable terminal after Enter is not a
|
|
172
|
+
// callback veto. The immutable pre-Send provider anchor and terminal
|
|
173
|
+
// identity remain sufficient to recover a completion already on disk.
|
|
174
|
+
const service = serviceFor(input.options);
|
|
175
|
+
let watch;
|
|
176
|
+
try {
|
|
177
|
+
watch = service.create({
|
|
178
|
+
watch_id: input.prepared.watchId,
|
|
179
|
+
agent: input.prepared.agent,
|
|
180
|
+
terminal: input.prepared.terminalIdentity,
|
|
181
|
+
anchor: input.prepared.anchor,
|
|
182
|
+
callback_route: input.prepared.callbackRoute,
|
|
183
|
+
openclaw_session: input.prepared.openclawSession,
|
|
184
|
+
openclaw_bin: input.prepared.openclawBin,
|
|
185
|
+
timeout_ms: input.prepared.timeoutMs
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
const existing = service.list().find((candidate) => candidate.watch_id === input.prepared.watchId);
|
|
190
|
+
if (!existing ||
|
|
191
|
+
!isUserExplicitFallbackWatch(existing) ||
|
|
192
|
+
existing.anchor.anchor_fingerprint !==
|
|
193
|
+
input.prepared.anchor.anchor_fingerprint) {
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
watch = existing;
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
callback_expected: true,
|
|
200
|
+
callback_mode: "terminal_watch",
|
|
201
|
+
watch_id: watch.watch_id
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
async function bestEffortStabilizePriorFallbackWatch(options, terminalId, requestHash) {
|
|
205
|
+
try {
|
|
206
|
+
const service = serviceFor(options);
|
|
207
|
+
const prior = service.list().filter((watch) => watch.status === "active" &&
|
|
208
|
+
isUserExplicitFallbackWatch(watch) &&
|
|
209
|
+
watch.terminal.terminal_id === terminalId &&
|
|
210
|
+
watch.anchor.request_hash === requestHash);
|
|
211
|
+
for (const watch of prior) {
|
|
212
|
+
try {
|
|
213
|
+
await service.reconcile(watch.watch_id);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// A prior callback must never delay or veto the new user Send.
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// Store or observer failure is callback-only degradation. The caller
|
|
222
|
+
// still captures the new pre-Send anchor and physical Send proceeds.
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function userExplicitFallbackWatchReceipt(input) {
|
|
226
|
+
try {
|
|
227
|
+
const watch = serviceFor(input.options).get(input.watchId);
|
|
228
|
+
if (!isUserExplicitFallbackWatch(watch) || !watch.callback_route) {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
callback_expected: true,
|
|
233
|
+
callback_mode: "terminal_watch",
|
|
234
|
+
watch_id: watch.watch_id
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
84
241
|
async function runWatch(options) {
|
|
85
242
|
const terminalId = requiredString(options.terminal, "--terminal");
|
|
86
243
|
const callbackRoute = Object.hasOwn(options, "callbackRoute")
|
|
@@ -172,6 +329,9 @@ export function createTerminalWatchCliAdapter(dependencies) {
|
|
|
172
329
|
.map(publicTerminalWatch);
|
|
173
330
|
}
|
|
174
331
|
return Object.freeze({
|
|
332
|
+
prepareUserExplicitFallbackWatch,
|
|
333
|
+
attachUserExplicitFallbackWatch,
|
|
334
|
+
userExplicitFallbackWatchReceipt,
|
|
175
335
|
runWatch,
|
|
176
336
|
runUnwatch,
|
|
177
337
|
runWatchStatus,
|
|
@@ -185,7 +345,13 @@ function assertWatchAnchorVersion(anchor, terminal) {
|
|
|
185
345
|
const artifactVersion = anchor.schema ===
|
|
186
346
|
"agent-knock-knock/codex-human-started-active-task-anchor"
|
|
187
347
|
? anchor.codex_version
|
|
188
|
-
: anchor.
|
|
348
|
+
: anchor.schema ===
|
|
349
|
+
"agent-knock-knock/claude-human-started-active-task-anchor"
|
|
350
|
+
? anchor.claude_version
|
|
351
|
+
: undefined;
|
|
352
|
+
if (!artifactVersion) {
|
|
353
|
+
throw new Error("manual Terminal Watch requires a human-started task anchor");
|
|
354
|
+
}
|
|
189
355
|
if (artifactVersion !== runningVersion) {
|
|
190
356
|
throw new Error(`the active task artifact reports ${artifactVersion}, not the running ` +
|
|
191
357
|
`coding-agent version ${runningVersion}; no Terminal Watch was created`);
|
|
@@ -202,6 +368,37 @@ function routeBoundWatchCallbackRoute(template, watch) {
|
|
|
202
368
|
controller_session_id: watch.openclaw_session
|
|
203
369
|
});
|
|
204
370
|
}
|
|
371
|
+
function callbackRouteForUserExplicitFallback(options) {
|
|
372
|
+
if (Object.hasOwn(options, "callbackRoute")) {
|
|
373
|
+
return parseCallbackRoute(options.callbackRoute);
|
|
374
|
+
}
|
|
375
|
+
const openclawSession = stringValue(options.openclawSession);
|
|
376
|
+
return callbackRouteForConversation({
|
|
377
|
+
gateway_method: options.gatewayMethod,
|
|
378
|
+
gateway_session: options.gatewaySession ?? openclawSession,
|
|
379
|
+
openclaw_session: openclawSession,
|
|
380
|
+
openclaw_bin: options.openclawBin,
|
|
381
|
+
gateway_url: options.gatewayUrl
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
function assertSameUserExplicitFallbackTerminal(expected, observed) {
|
|
385
|
+
if (requiredString(observed.id, "terminal id") !== expected.conversationId ||
|
|
386
|
+
terminalAgent(observed) !== expected.agent ||
|
|
387
|
+
positiveInteger(observed.pid, "terminal PID") !== expected.pid ||
|
|
388
|
+
!isRecord(observed.terminal_control) ||
|
|
389
|
+
!sameTerminalControlEvidenceIncarnation(expected.terminalControl, observed.terminal_control)) {
|
|
390
|
+
throw new Error("terminal changed before the user-explicit fallback Watch anchor was captured");
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
function assertPreparedFallbackTerminal(prepared, observed) {
|
|
394
|
+
if (requiredString(observed.id, "terminal id") !== prepared.terminalId ||
|
|
395
|
+
terminalAgent(observed) !== prepared.agent ||
|
|
396
|
+
positiveInteger(observed.pid, "terminal PID") !== prepared.pid ||
|
|
397
|
+
!isRecord(observed.terminal_control) ||
|
|
398
|
+
!sameTerminalControlEvidenceIncarnation(prepared.terminalEndpoint, observed.terminal_control)) {
|
|
399
|
+
throw new Error("terminal changed before the user-explicit fallback Watch was attached");
|
|
400
|
+
}
|
|
401
|
+
}
|
|
205
402
|
async function exactTerminalForWatch(terminalId, options, dependencies) {
|
|
206
403
|
const observation = await dependencies.observeExactTerminal({
|
|
207
404
|
options,
|
|
@@ -280,8 +477,22 @@ async function observeTerminalWatch(watch, options, dependencies) {
|
|
|
280
477
|
? exactTerminal.terminal
|
|
281
478
|
: undefined;
|
|
282
479
|
const terminalMatches = rawTerminal
|
|
283
|
-
?
|
|
480
|
+
? isUserExplicitFallbackWatch(watch)
|
|
481
|
+
? terminalMatchesUserExplicitFallbackWatch(rawTerminal, watch)
|
|
482
|
+
: terminalMatchesWatch(rawTerminal, watch)
|
|
284
483
|
: false;
|
|
484
|
+
if (isUserExplicitFallbackWatch(watch)) {
|
|
485
|
+
return observeUserExplicitFallbackTerminalWatch({
|
|
486
|
+
watch,
|
|
487
|
+
exactTerminal,
|
|
488
|
+
rawTerminal,
|
|
489
|
+
projectedTerminal,
|
|
490
|
+
terminalMatches,
|
|
491
|
+
observedAt,
|
|
492
|
+
options,
|
|
493
|
+
dependencies
|
|
494
|
+
});
|
|
495
|
+
}
|
|
285
496
|
const observation = watch.anchor.schema ===
|
|
286
497
|
"agent-knock-knock/codex-human-started-active-task-anchor"
|
|
287
498
|
? observeCodexHumanStartedActiveTask({
|
|
@@ -291,13 +502,18 @@ async function observeTerminalWatch(watch, options, dependencies) {
|
|
|
291
502
|
: codexIdentityForWatch(watch),
|
|
292
503
|
resumeOffsetBytes: watch.observation_checkpoint.safe_resume_offset_bytes
|
|
293
504
|
})
|
|
294
|
-
:
|
|
295
|
-
anchor
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
505
|
+
: watch.anchor.schema ===
|
|
506
|
+
"agent-knock-knock/claude-human-started-active-task-anchor"
|
|
507
|
+
? observeClaudeHumanStartedActiveTask({
|
|
508
|
+
anchor: watch.anchor,
|
|
509
|
+
claudeHome: stringValue(options.claudeHome),
|
|
510
|
+
agentRows: dependencies.loadClaudeAgentRows(options, { required: true }),
|
|
511
|
+
resumeOffsetBytes: watch.observation_checkpoint.safe_resume_offset_bytes,
|
|
512
|
+
checkpoint: claudeObservationCheckpoint(watch)
|
|
513
|
+
})
|
|
514
|
+
: (() => {
|
|
515
|
+
throw new Error("terminal Watch anchor is unsupported");
|
|
516
|
+
})();
|
|
301
517
|
if (observation.status === "invalidated") {
|
|
302
518
|
return invalidatedObservation(watch, observedAt, "task_anchor_invalidated");
|
|
303
519
|
}
|
|
@@ -375,6 +591,342 @@ async function observeTerminalWatch(watch, options, dependencies) {
|
|
|
375
591
|
observation_checkpoint: observationCheckpoint
|
|
376
592
|
};
|
|
377
593
|
}
|
|
594
|
+
async function observeUserExplicitFallbackTerminalWatch(input) {
|
|
595
|
+
const { watch, exactTerminal, rawTerminal, terminalMatches, observedAt, options, dependencies } = input;
|
|
596
|
+
const fence = terminalWatchObservationFence(watch);
|
|
597
|
+
if (watch.anchor.schema ===
|
|
598
|
+
"agent-knock-knock/codex-user-explicit-fallback-watch-anchor") {
|
|
599
|
+
const anchor = watch.anchor.acceptance_anchor;
|
|
600
|
+
const persistedCheckpoint = codexFallbackObservationCheckpoint(watch);
|
|
601
|
+
let acceptance;
|
|
602
|
+
let currentIdentity;
|
|
603
|
+
let acceptedCheckpoint;
|
|
604
|
+
if (persistedCheckpoint.acceptance_evidence &&
|
|
605
|
+
persistedCheckpoint.accepted_identity) {
|
|
606
|
+
acceptance = persistedCheckpoint.acceptance_evidence;
|
|
607
|
+
currentIdentity = {
|
|
608
|
+
sessionId: persistedCheckpoint.accepted_identity.native_thread_id,
|
|
609
|
+
processUuid: persistedCheckpoint.accepted_identity.process_uuid,
|
|
610
|
+
processBirth: persistedCheckpoint.accepted_identity.process_birth,
|
|
611
|
+
rollout: persistedCheckpoint.accepted_identity.rollout
|
|
612
|
+
};
|
|
613
|
+
acceptedCheckpoint = persistedCheckpoint;
|
|
614
|
+
}
|
|
615
|
+
else if (anchor.version === 3) {
|
|
616
|
+
const inventory = rawTerminal?._codex_open_root_rollout_inventory;
|
|
617
|
+
if (!isRecord(inventory)) {
|
|
618
|
+
return fallbackPendingOrTerminalObservation({
|
|
619
|
+
watch,
|
|
620
|
+
exactTerminal,
|
|
621
|
+
rawTerminal,
|
|
622
|
+
terminalMatches,
|
|
623
|
+
observedAt,
|
|
624
|
+
reasonCode: "codex_rollout_inventory_unavailable"
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
const result = detectCodexCandidateSetRolloutAcceptance({
|
|
628
|
+
anchor,
|
|
629
|
+
currentInventory: inventory,
|
|
630
|
+
requestHash: watch.anchor.request_hash
|
|
631
|
+
});
|
|
632
|
+
if (result.status === "uncertain") {
|
|
633
|
+
if (result.code === "candidate_scan_invalid") {
|
|
634
|
+
return {
|
|
635
|
+
...fence,
|
|
636
|
+
kind: "unavailable",
|
|
637
|
+
observed_at: observedAt,
|
|
638
|
+
reason_code: "native_acceptance_scan_unavailable"
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
return invalidatedObservation(watch, observedAt, `native_acceptance_${result.code}`);
|
|
642
|
+
}
|
|
643
|
+
if (result.status === "pending") {
|
|
644
|
+
return fallbackPendingOrTerminalObservation({
|
|
645
|
+
watch,
|
|
646
|
+
exactTerminal,
|
|
647
|
+
rawTerminal,
|
|
648
|
+
terminalMatches,
|
|
649
|
+
observedAt
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
acceptance = result.evidence;
|
|
653
|
+
currentIdentity = result.identity;
|
|
654
|
+
acceptedCheckpoint = codexFallbackAcceptedCheckpoint(watch, acceptance, currentIdentity);
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
try {
|
|
658
|
+
currentIdentity = anchor.version === 1
|
|
659
|
+
? {
|
|
660
|
+
sessionId: anchor.native_thread_id,
|
|
661
|
+
processUuid: anchor.process_uuid,
|
|
662
|
+
processBirth: anchor.process_birth,
|
|
663
|
+
rollout: anchor.rollout
|
|
664
|
+
}
|
|
665
|
+
: rawTerminal
|
|
666
|
+
? codexIdentity(rawTerminal)
|
|
667
|
+
: (() => {
|
|
668
|
+
throw new Error("Codex pre-materialization identity is unavailable");
|
|
669
|
+
})();
|
|
670
|
+
acceptance = detectCodexRolloutAcceptance({
|
|
671
|
+
anchor,
|
|
672
|
+
currentIdentity,
|
|
673
|
+
requestHash: watch.anchor.request_hash
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
catch (error) {
|
|
677
|
+
if (retryableProviderError(error)) {
|
|
678
|
+
return {
|
|
679
|
+
...fence,
|
|
680
|
+
kind: "unavailable",
|
|
681
|
+
observed_at: observedAt,
|
|
682
|
+
reason_code: "native_acceptance_scan_unavailable"
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
return invalidatedObservation(watch, observedAt, "native_acceptance_identity_changed");
|
|
686
|
+
}
|
|
687
|
+
if (!acceptance) {
|
|
688
|
+
return fallbackPendingOrTerminalObservation({
|
|
689
|
+
watch,
|
|
690
|
+
exactTerminal,
|
|
691
|
+
rawTerminal,
|
|
692
|
+
terminalMatches,
|
|
693
|
+
observedAt
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
acceptedCheckpoint = codexFallbackAcceptedCheckpoint(watch, acceptance, currentIdentity);
|
|
697
|
+
}
|
|
698
|
+
if (!acceptance || !acceptedCheckpoint) {
|
|
699
|
+
throw new Error("Codex fallback Watch acceptance checkpoint is incomplete");
|
|
700
|
+
}
|
|
701
|
+
const completion = detectCodexBoundRolloutCompletion({
|
|
702
|
+
anchor,
|
|
703
|
+
acceptanceEvidence: acceptance,
|
|
704
|
+
currentIdentity,
|
|
705
|
+
requestHash: watch.anchor.request_hash
|
|
706
|
+
});
|
|
707
|
+
if (completion.status === "completed") {
|
|
708
|
+
return fallbackCompletionObservation(watch, observedAt, completion.completion, completion.diagnostics.observed_end_offset_bytes, codexFallbackAcceptedCheckpoint(watch, acceptance, currentIdentity));
|
|
709
|
+
}
|
|
710
|
+
if (completion.status === "failure") {
|
|
711
|
+
if (completion.diagnostics.code === "rollout_unreadable") {
|
|
712
|
+
return {
|
|
713
|
+
...fence,
|
|
714
|
+
kind: "unavailable",
|
|
715
|
+
observed_at: observedAt,
|
|
716
|
+
safe_resume_offset_bytes: acceptedCheckpoint.safe_resume_offset_bytes,
|
|
717
|
+
observation_checkpoint: acceptedCheckpoint,
|
|
718
|
+
reason_code: "accepted_rollout_unavailable"
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
return invalidatedObservation(watch, observedAt, `native_completion_${completion.diagnostics.code}`);
|
|
722
|
+
}
|
|
723
|
+
return fallbackPendingOrTerminalObservation({
|
|
724
|
+
watch,
|
|
725
|
+
exactTerminal,
|
|
726
|
+
rawTerminal,
|
|
727
|
+
terminalMatches,
|
|
728
|
+
observedAt,
|
|
729
|
+
observedEndOffsetBytes: completion.diagnostics.observed_end_offset_bytes,
|
|
730
|
+
observationCheckpoint: codexFallbackAcceptedCheckpoint(watch, acceptance, currentIdentity)
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
if (watch.anchor.schema !==
|
|
734
|
+
"agent-knock-knock/claude-user-explicit-fallback-watch-anchor") {
|
|
735
|
+
throw new Error("user-explicit fallback Watch anchor is unsupported");
|
|
736
|
+
}
|
|
737
|
+
const request = {
|
|
738
|
+
sessionId: watch.anchor.transcript_anchor.session_id,
|
|
739
|
+
cwd: watch.anchor.transcript_anchor.cwd,
|
|
740
|
+
requestHash: watch.anchor.request_hash,
|
|
741
|
+
startedAt: watch.anchor.captured_at,
|
|
742
|
+
context: {
|
|
743
|
+
claudeTranscriptAnchor: watch.anchor.transcript_anchor,
|
|
744
|
+
pid: watch.anchor.transcript_anchor.pid
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
const persistedCheckpoint = claudeFallbackObservationCheckpoint(watch);
|
|
748
|
+
const observation = observeClaudeUserExplicitFallbackTranscript(request, {
|
|
749
|
+
claudeHome: stringValue(options.claudeHome),
|
|
750
|
+
// This observer is transcript-authoritative by design. Completion must
|
|
751
|
+
// remain recoverable after the exact pane/process leaves discovery.
|
|
752
|
+
acceptanceEvidence: persistedCheckpoint.acceptance_evidence
|
|
753
|
+
});
|
|
754
|
+
if (observation.status === "unavailable") {
|
|
755
|
+
return {
|
|
756
|
+
...fence,
|
|
757
|
+
kind: "unavailable",
|
|
758
|
+
observed_at: observedAt,
|
|
759
|
+
reason_code: "native_acceptance_scan_unavailable"
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
const acceptance = observation.acceptance;
|
|
763
|
+
if (!acceptance) {
|
|
764
|
+
return fallbackPendingOrTerminalObservation({
|
|
765
|
+
watch,
|
|
766
|
+
exactTerminal,
|
|
767
|
+
rawTerminal,
|
|
768
|
+
terminalMatches,
|
|
769
|
+
observedAt
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
const acceptedCheckpoint = claudeFallbackAcceptedCheckpoint(watch, acceptance);
|
|
773
|
+
if (observation.status === "completed") {
|
|
774
|
+
return fallbackCompletionObservation(watch, observedAt, observation.completion, observation.observedEndOffsetBytes, acceptedCheckpoint);
|
|
775
|
+
}
|
|
776
|
+
return fallbackPendingOrTerminalObservation({
|
|
777
|
+
watch,
|
|
778
|
+
exactTerminal,
|
|
779
|
+
rawTerminal,
|
|
780
|
+
terminalMatches,
|
|
781
|
+
observedAt,
|
|
782
|
+
observedEndOffsetBytes: observation.observedEndOffsetBytes,
|
|
783
|
+
observationCheckpoint: acceptedCheckpoint
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
function fallbackPendingOrTerminalObservation(input) {
|
|
787
|
+
if (input.exactTerminal.state === "unavailable") {
|
|
788
|
+
return {
|
|
789
|
+
...terminalWatchObservationFence(input.watch),
|
|
790
|
+
kind: "unavailable",
|
|
791
|
+
observed_at: input.observedAt,
|
|
792
|
+
reason_code: input.reasonCode ?? "terminal_observation_unavailable"
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
if (input.exactTerminal.state === "absent" || !input.rawTerminal) {
|
|
796
|
+
return invalidatedObservation(input.watch, input.observedAt, "terminal_process_unavailable");
|
|
797
|
+
}
|
|
798
|
+
if (!input.terminalMatches) {
|
|
799
|
+
return invalidatedObservation(input.watch, input.observedAt, "terminal_identity_changed");
|
|
800
|
+
}
|
|
801
|
+
if (input.reasonCode) {
|
|
802
|
+
return {
|
|
803
|
+
...terminalWatchObservationFence(input.watch),
|
|
804
|
+
kind: "unavailable",
|
|
805
|
+
observed_at: input.observedAt,
|
|
806
|
+
reason_code: input.reasonCode
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
return fallbackPendingObservation(input.watch, input.observedAt, input.observedEndOffsetBytes, input.observationCheckpoint);
|
|
810
|
+
}
|
|
811
|
+
function fallbackPendingObservation(watch, observedAt, observedEndOffsetBytes, observationCheckpoint) {
|
|
812
|
+
const effectiveCheckpoint = observationCheckpoint ?? (watch.anchor.schema ===
|
|
813
|
+
"agent-knock-knock/codex-user-explicit-fallback-watch-anchor"
|
|
814
|
+
? watch.observation_checkpoint
|
|
815
|
+
: undefined);
|
|
816
|
+
const safeResumeOffsetBytes = effectiveCheckpoint
|
|
817
|
+
?.safe_resume_offset_bytes ?? fallbackResumeOffset(watch, observedEndOffsetBytes);
|
|
818
|
+
return {
|
|
819
|
+
...terminalWatchObservationFence(watch),
|
|
820
|
+
kind: "pending",
|
|
821
|
+
observed_at: observedAt,
|
|
822
|
+
safe_resume_offset_bytes: safeResumeOffsetBytes,
|
|
823
|
+
...(effectiveCheckpoint
|
|
824
|
+
? { observation_checkpoint: effectiveCheckpoint }
|
|
825
|
+
: {})
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
function codexFallbackObservationCheckpoint(watch) {
|
|
829
|
+
const checkpoint = watch.observation_checkpoint;
|
|
830
|
+
if (!("schema" in checkpoint) ||
|
|
831
|
+
checkpoint.schema !==
|
|
832
|
+
"agent-knock-knock/codex-user-explicit-fallback-watch-checkpoint") {
|
|
833
|
+
throw new Error("Codex fallback Watch has no exact acceptance checkpoint");
|
|
834
|
+
}
|
|
835
|
+
return checkpoint;
|
|
836
|
+
}
|
|
837
|
+
function claudeFallbackObservationCheckpoint(watch) {
|
|
838
|
+
const checkpoint = watch.observation_checkpoint;
|
|
839
|
+
if (!("schema" in checkpoint) ||
|
|
840
|
+
checkpoint.schema !==
|
|
841
|
+
"agent-knock-knock/claude-user-explicit-fallback-watch-checkpoint") {
|
|
842
|
+
throw new Error("Claude fallback Watch has no exact acceptance checkpoint");
|
|
843
|
+
}
|
|
844
|
+
return checkpoint;
|
|
845
|
+
}
|
|
846
|
+
function codexFallbackAcceptedCheckpoint(watch, acceptanceEvidence, currentIdentity) {
|
|
847
|
+
const nativeThreadId = requiredString(currentIdentity.sessionId, "accepted Codex native thread id");
|
|
848
|
+
const processUuid = requiredString(currentIdentity.processUuid, "accepted Codex process UUID");
|
|
849
|
+
const processBirth = requiredString(currentIdentity.processBirth, "accepted Codex process birth");
|
|
850
|
+
const rollout = currentIdentity.rollout;
|
|
851
|
+
if (!rollout) {
|
|
852
|
+
throw new Error("accepted Codex rollout identity is unavailable");
|
|
853
|
+
}
|
|
854
|
+
const acceptanceOffset = numericMetadata(acceptanceEvidence.metadata, "observed_end_offset_bytes");
|
|
855
|
+
if (acceptanceOffset === undefined) {
|
|
856
|
+
throw new Error("accepted Codex rollout boundary is unavailable");
|
|
857
|
+
}
|
|
858
|
+
return {
|
|
859
|
+
schema: "agent-knock-knock/codex-user-explicit-fallback-watch-checkpoint",
|
|
860
|
+
version: 1,
|
|
861
|
+
safe_resume_offset_bytes: fallbackResumeOffset(watch, acceptanceOffset),
|
|
862
|
+
acceptance_evidence: acceptanceEvidence,
|
|
863
|
+
accepted_identity: {
|
|
864
|
+
native_thread_id: nativeThreadId,
|
|
865
|
+
process_uuid: processUuid,
|
|
866
|
+
process_birth: processBirth,
|
|
867
|
+
rollout
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
function claudeFallbackAcceptedCheckpoint(watch, acceptanceEvidence) {
|
|
872
|
+
const acceptanceOffset = numericMetadata(acceptanceEvidence.metadata, "observed_end_offset_bytes");
|
|
873
|
+
if (acceptanceOffset === undefined) {
|
|
874
|
+
throw new Error("accepted Claude transcript boundary is unavailable");
|
|
875
|
+
}
|
|
876
|
+
const promptUuid = requiredString(acceptanceEvidence.acceptanceId, "accepted Claude prompt UUID");
|
|
877
|
+
return {
|
|
878
|
+
schema: "agent-knock-knock/claude-user-explicit-fallback-watch-checkpoint",
|
|
879
|
+
version: 1,
|
|
880
|
+
safe_resume_offset_bytes: fallbackResumeOffset(watch, acceptanceOffset),
|
|
881
|
+
acceptance_evidence: acceptanceEvidence,
|
|
882
|
+
accepted_prompt_uuid: promptUuid
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
function fallbackCompletionObservation(watch, observedAt, completion, observedEndOffsetBytes, observationCheckpoint) {
|
|
886
|
+
const safeResumeOffsetBytes = observationCheckpoint
|
|
887
|
+
?.safe_resume_offset_bytes ?? fallbackResumeOffset(watch, observedEndOffsetBytes);
|
|
888
|
+
const kind = completion.outcome === "failure" ? "failed" : "completed";
|
|
889
|
+
return {
|
|
890
|
+
...terminalWatchObservationFence(watch),
|
|
891
|
+
kind,
|
|
892
|
+
observed_at: observedAt,
|
|
893
|
+
safe_resume_offset_bytes: safeResumeOffsetBytes,
|
|
894
|
+
...(observationCheckpoint
|
|
895
|
+
? { observation_checkpoint: observationCheckpoint }
|
|
896
|
+
: {}),
|
|
897
|
+
evidence_fingerprint: sha256({
|
|
898
|
+
kind,
|
|
899
|
+
watch_id: watch.watch_id,
|
|
900
|
+
completion_id: completion.id ?? null,
|
|
901
|
+
completion_timestamp: completion.timestamp ?? null,
|
|
902
|
+
anchor_fingerprint: watch.anchor.anchor_fingerprint
|
|
903
|
+
}),
|
|
904
|
+
reason_code: completion.outcome === "failure"
|
|
905
|
+
? "anchored_task_failed"
|
|
906
|
+
: "anchored_task_completed",
|
|
907
|
+
completion_text: completion.text.slice(0, 4000),
|
|
908
|
+
completion_id: completion.id,
|
|
909
|
+
completion_timestamp: completion.timestamp
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
function fallbackResumeOffset(watch, observedEndOffsetBytes) {
|
|
913
|
+
return Math.max(watch.observation_checkpoint.safe_resume_offset_bytes, Number.isSafeInteger(observedEndOffsetBytes) &&
|
|
914
|
+
Number(observedEndOffsetBytes) >= 0
|
|
915
|
+
? Number(observedEndOffsetBytes)
|
|
916
|
+
: 0);
|
|
917
|
+
}
|
|
918
|
+
function numericMetadata(metadata, field) {
|
|
919
|
+
const value = metadata?.[field];
|
|
920
|
+
return Number.isSafeInteger(value) && Number(value) >= 0
|
|
921
|
+
? Number(value)
|
|
922
|
+
: undefined;
|
|
923
|
+
}
|
|
924
|
+
function retryableProviderError(error) {
|
|
925
|
+
return isRecord(error) && typeof error.code === "string" && new Set([
|
|
926
|
+
"EACCES", "EAGAIN", "EBUSY", "EIO", "EMFILE", "ENFILE", "ENOENT",
|
|
927
|
+
"EPERM", "ESTALE", "ETIMEDOUT"
|
|
928
|
+
]).has(error.code);
|
|
929
|
+
}
|
|
378
930
|
function claudeObservationCheckpoint(watch) {
|
|
379
931
|
const checkpoint = watch.observation_checkpoint;
|
|
380
932
|
if (!("schema" in checkpoint) ||
|
|
@@ -406,6 +958,31 @@ function terminalMatchesWatch(terminal, watch) {
|
|
|
406
958
|
return false;
|
|
407
959
|
}
|
|
408
960
|
}
|
|
961
|
+
function terminalMatchesUserExplicitFallbackWatch(terminal, watch) {
|
|
962
|
+
try {
|
|
963
|
+
if (!isUserExplicitFallbackWatch(watch) ||
|
|
964
|
+
terminalAgent(terminal) !== watch.agent ||
|
|
965
|
+
requiredString(terminal.id, "terminal id") !==
|
|
966
|
+
watch.terminal.terminal_id ||
|
|
967
|
+
terminalWorkspace(terminal) !== watch.terminal.workspace ||
|
|
968
|
+
!isRecord(terminal.terminal_control) ||
|
|
969
|
+
!sameTerminalControlEvidenceIncarnation(terminal.terminal_control, watch.terminal.terminal_endpoint)) {
|
|
970
|
+
return false;
|
|
971
|
+
}
|
|
972
|
+
if (watch.anchor.schema ===
|
|
973
|
+
"agent-knock-knock/codex-user-explicit-fallback-watch-anchor") {
|
|
974
|
+
return requiredString(terminal.native_agent_process_uuid, "Codex process UUID") === watch.anchor.acceptance_anchor.process_uuid &&
|
|
975
|
+
requiredString(terminal.native_agent_process_birth, "Codex process birth") === watch.anchor.acceptance_anchor.process_birth;
|
|
976
|
+
}
|
|
977
|
+
return watch.anchor.schema ===
|
|
978
|
+
"agent-knock-knock/claude-user-explicit-fallback-watch-anchor" &&
|
|
979
|
+
positiveInteger(terminal.pid, "Claude PID") ===
|
|
980
|
+
watch.anchor.transcript_anchor.pid;
|
|
981
|
+
}
|
|
982
|
+
catch {
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
409
986
|
function terminalWatchIdentity(terminal, bindingToken) {
|
|
410
987
|
const terminalControl = terminal.terminal_control;
|
|
411
988
|
return {
|
|
@@ -447,24 +1024,28 @@ function codexIdentityForWatch(watch) {
|
|
|
447
1024
|
function publicTerminalWatch(watch) {
|
|
448
1025
|
const pending = watch.notification_outbox.filter(({ status }) => status === "pending" || status === "delivering" || status === "failed").length;
|
|
449
1026
|
const compatibilityWarning = watch.agent === "codex" &&
|
|
450
|
-
watch.anchor.schema ===
|
|
451
|
-
"agent-knock-knock/codex-human-started-active-task-anchor"
|
|
1027
|
+
(watch.anchor.schema ===
|
|
1028
|
+
"agent-knock-knock/codex-human-started-active-task-anchor" ||
|
|
1029
|
+
watch.anchor.schema ===
|
|
1030
|
+
"agent-knock-knock/codex-user-explicit-fallback-watch-anchor")
|
|
452
1031
|
? codexRuntimeCompatibilityProfile(watch.anchor.codex_version)
|
|
453
1032
|
?.compatibilityWarning
|
|
454
1033
|
: watch.agent === "claude" &&
|
|
455
|
-
watch.anchor.schema ===
|
|
456
|
-
"agent-knock-knock/claude-human-started-active-task-anchor"
|
|
1034
|
+
(watch.anchor.schema ===
|
|
1035
|
+
"agent-knock-knock/claude-human-started-active-task-anchor" ||
|
|
1036
|
+
watch.anchor.schema ===
|
|
1037
|
+
"agent-knock-knock/claude-user-explicit-fallback-watch-anchor")
|
|
457
1038
|
? claudeRuntimeCompatibilityWarning(watch.anchor.claude_version)
|
|
458
1039
|
: undefined;
|
|
1040
|
+
const userExplicitFallback = isUserExplicitFallbackWatch(watch);
|
|
459
1041
|
return {
|
|
460
1042
|
watch_id: watch.watch_id,
|
|
461
|
-
source:
|
|
1043
|
+
source: userExplicitFallback
|
|
1044
|
+
? "terminal_user_explicit_fallback_watch"
|
|
1045
|
+
: "human_started_terminal_watch",
|
|
462
1046
|
agent: watch.agent,
|
|
463
1047
|
terminal_id: watch.terminal.terminal_id,
|
|
464
|
-
native_thread_id: watch
|
|
465
|
-
"agent-knock-knock/codex-human-started-active-task-anchor"
|
|
466
|
-
? watch.anchor.native_thread_id
|
|
467
|
-
: watch.anchor.session_id,
|
|
1048
|
+
native_thread_id: terminalWatchNativeThreadId(watch),
|
|
468
1049
|
workspace: watch.terminal.workspace,
|
|
469
1050
|
status: watch.status,
|
|
470
1051
|
activity_state: watch.status === "active" ? "watching" : "settled",
|
|
@@ -510,6 +1091,20 @@ function publicTerminalWatch(watch) {
|
|
|
510
1091
|
}
|
|
511
1092
|
};
|
|
512
1093
|
}
|
|
1094
|
+
function terminalWatchNativeThreadId(watch) {
|
|
1095
|
+
switch (watch.anchor.schema) {
|
|
1096
|
+
case "agent-knock-knock/codex-human-started-active-task-anchor":
|
|
1097
|
+
return watch.anchor.native_thread_id;
|
|
1098
|
+
case "agent-knock-knock/claude-human-started-active-task-anchor":
|
|
1099
|
+
return watch.anchor.session_id;
|
|
1100
|
+
case "agent-knock-knock/codex-user-explicit-fallback-watch-anchor":
|
|
1101
|
+
return watch.anchor.acceptance_anchor.version === 1
|
|
1102
|
+
? watch.anchor.acceptance_anchor.native_thread_id
|
|
1103
|
+
: undefined;
|
|
1104
|
+
case "agent-knock-knock/claude-user-explicit-fallback-watch-anchor":
|
|
1105
|
+
return watch.anchor.transcript_anchor.session_id;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
513
1108
|
function approvalFingerprint(terminal) {
|
|
514
1109
|
if (terminal.activity_state !== "awaiting_approval")
|
|
515
1110
|
return undefined;
|