@scotthuang/agent-knock-knock 0.10.1 → 0.10.3
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 +21 -0
- package/README.md +12 -17
- package/dist/src/claude-local-transcript-provider.d.ts +16 -0
- package/dist/src/claude-local-transcript-provider.js +111 -47
- package/dist/src/claude-local-transcript-provider.js.map +1 -1
- package/dist/src/cli.js +3135 -489
- package/dist/src/cli.js.map +1 -1
- package/dist/src/live-lifecycle-smoke.js +7 -2
- package/dist/src/live-lifecycle-smoke.js.map +1 -1
- package/dist/src/openclaw-plugin-helpers.d.ts +1 -0
- package/dist/src/openclaw-plugin-helpers.js +30 -2
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin.d.ts +3 -1
- package/dist/src/openclaw-plugin.js +251 -53
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/store.d.ts +6 -0
- package/dist/src/store.js +15 -1
- package/dist/src/store.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +31 -2
- package/dist/src/terminal-agent-bridge.js +231 -11
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-submission-acceptance.d.ts +118 -0
- package/dist/src/terminal-submission-acceptance.js +785 -0
- package/dist/src/terminal-submission-acceptance.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { redactString } from "./runtime-log.js";
|
|
5
|
+
export function validateTerminalSubmissionAcceptanceEvidence(value, expected) {
|
|
6
|
+
if (!isRecord(value)) {
|
|
7
|
+
throw new Error("native acceptance evidence is unavailable");
|
|
8
|
+
}
|
|
9
|
+
const allowedKeys = new Set([
|
|
10
|
+
"source",
|
|
11
|
+
"kind",
|
|
12
|
+
"nativeThreadId",
|
|
13
|
+
"requestHash",
|
|
14
|
+
"acceptanceId",
|
|
15
|
+
"acceptedAt",
|
|
16
|
+
"anchorFingerprint",
|
|
17
|
+
"evidenceFingerprint",
|
|
18
|
+
"metadata"
|
|
19
|
+
]);
|
|
20
|
+
if (Object.keys(value).some((key) => !allowedKeys.has(key))) {
|
|
21
|
+
throw new Error("native acceptance evidence contains unsupported fields");
|
|
22
|
+
}
|
|
23
|
+
if (value.source !== expected.source || value.kind !== "native_user_turn") {
|
|
24
|
+
throw new Error("native acceptance evidence has the wrong agent or kind");
|
|
25
|
+
}
|
|
26
|
+
if (requiredString(value.nativeThreadId, "native acceptance thread") !==
|
|
27
|
+
requiredString(expected.nativeThreadId, "expected native acceptance thread") ||
|
|
28
|
+
sha256Value(value.requestHash, "native acceptance request hash") !==
|
|
29
|
+
sha256Value(expected.requestHash, "expected native acceptance request hash")) {
|
|
30
|
+
throw new Error("native acceptance evidence does not match the exact request binding");
|
|
31
|
+
}
|
|
32
|
+
requiredString(value.acceptanceId, "native acceptance id");
|
|
33
|
+
sha256Value(value.anchorFingerprint, "native acceptance anchor fingerprint");
|
|
34
|
+
sha256Value(value.evidenceFingerprint, "native acceptance evidence fingerprint");
|
|
35
|
+
if (value.acceptedAt !== undefined && !validTimestamp(value.acceptedAt)) {
|
|
36
|
+
throw new Error("native acceptance evidence timestamp is invalid");
|
|
37
|
+
}
|
|
38
|
+
if (value.metadata !== undefined) {
|
|
39
|
+
if (!isRecord(value.metadata)) {
|
|
40
|
+
throw new Error("native acceptance evidence metadata is invalid");
|
|
41
|
+
}
|
|
42
|
+
const allowedMetadata = value.source === "codex_rollout"
|
|
43
|
+
? new Set([
|
|
44
|
+
"turn_id",
|
|
45
|
+
"anchor_offset_bytes",
|
|
46
|
+
"observed_end_offset_bytes"
|
|
47
|
+
])
|
|
48
|
+
: new Set([
|
|
49
|
+
"prompt_uuid",
|
|
50
|
+
"claude_version",
|
|
51
|
+
"transcript_file_id",
|
|
52
|
+
"anchor_offset_bytes",
|
|
53
|
+
"observed_end_offset_bytes",
|
|
54
|
+
"agent_started_at_ms"
|
|
55
|
+
]);
|
|
56
|
+
for (const [key, item] of Object.entries(value.metadata)) {
|
|
57
|
+
if (!allowedMetadata.has(key) ||
|
|
58
|
+
!(typeof item === "string" ||
|
|
59
|
+
(typeof item === "number" && Number.isFinite(item)))) {
|
|
60
|
+
throw new Error("native acceptance evidence metadata is not allowlisted");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const { evidenceFingerprint, ...base } = value;
|
|
65
|
+
if (fingerprint(base) !== evidenceFingerprint) {
|
|
66
|
+
throw new Error("native acceptance evidence fingerprint does not match");
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
export function terminalSubmissionReplayReceipt(options) {
|
|
71
|
+
if (options.proofLevel !== "agent_accepted") {
|
|
72
|
+
return {
|
|
73
|
+
replayed: true,
|
|
74
|
+
delivered: false,
|
|
75
|
+
status: "submission_pending_acceptance",
|
|
76
|
+
submission_outcome: "pending_acceptance",
|
|
77
|
+
delivery_receipt: options.proofLevel,
|
|
78
|
+
do_not_retry: true
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
validateTerminalSubmissionAcceptanceEvidence(options.evidence, options.expected);
|
|
83
|
+
return {
|
|
84
|
+
replayed: true,
|
|
85
|
+
delivered: true,
|
|
86
|
+
status: "async_pending",
|
|
87
|
+
submission_outcome: "agent_accepted",
|
|
88
|
+
delivery_receipt: "agent_accepted"
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
return {
|
|
93
|
+
replayed: true,
|
|
94
|
+
delivered: false,
|
|
95
|
+
status: "submission_uncertain",
|
|
96
|
+
submission_outcome: "uncertain",
|
|
97
|
+
delivery_receipt: "enter_dispatched",
|
|
98
|
+
do_not_retry: true,
|
|
99
|
+
evidence_error: error instanceof Error ? error.message : String(error)
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const CODEX_ACCEPTANCE_MAX_BYTES = 16 * 1024 * 1024;
|
|
104
|
+
const CODEX_COMPLETION_MAX_BYTES = 256 * 1024 * 1024;
|
|
105
|
+
const CODEX_COMPLETION_MAX_TEXT_LENGTH = 4000;
|
|
106
|
+
const NATIVE_THREAD_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
|
|
107
|
+
const NO_FOLLOW_FLAG = typeof fs.constants.O_NOFOLLOW === "number"
|
|
108
|
+
? fs.constants.O_NOFOLLOW
|
|
109
|
+
: 0;
|
|
110
|
+
export function captureCodexRolloutAcceptanceAnchor(options) {
|
|
111
|
+
const nativeThreadId = exactNativeThreadId(options.nativeThreadId);
|
|
112
|
+
const processUuid = requiredString(options.processUuid, "Codex process UUID");
|
|
113
|
+
const processBirth = requiredString(options.processBirth, "Codex process birth");
|
|
114
|
+
const capturedAt = (options.now ?? new Date()).toISOString();
|
|
115
|
+
const base = {
|
|
116
|
+
schema: "agent-knock-knock/codex-rollout-acceptance-anchor",
|
|
117
|
+
version: 1,
|
|
118
|
+
native_thread_id: nativeThreadId,
|
|
119
|
+
process_uuid: processUuid,
|
|
120
|
+
process_birth: processBirth,
|
|
121
|
+
captured_at: capturedAt
|
|
122
|
+
};
|
|
123
|
+
if (options.mode === "pre_materialization") {
|
|
124
|
+
const withoutFile = {
|
|
125
|
+
...base,
|
|
126
|
+
mode: "pre_materialization",
|
|
127
|
+
file_existed: false,
|
|
128
|
+
offset_bytes: 0,
|
|
129
|
+
expected_empty_native_session: true
|
|
130
|
+
};
|
|
131
|
+
return {
|
|
132
|
+
...withoutFile,
|
|
133
|
+
anchor_fingerprint: fingerprint(withoutFile)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const rollout = normalizedRolloutIdentity(options.rollout);
|
|
137
|
+
const opened = openExactRollout(rollout);
|
|
138
|
+
try {
|
|
139
|
+
const before = opened.stat;
|
|
140
|
+
assertPrivateRegularFile(before);
|
|
141
|
+
if (before.size > 0 && !fileEndsWithNewline(opened.fd, before.size)) {
|
|
142
|
+
throw new Error("Codex rollout did not end at a complete JSONL record before terminal submission");
|
|
143
|
+
}
|
|
144
|
+
const after = fs.fstatSync(opened.fd);
|
|
145
|
+
if (!sameStableFile(before, after)) {
|
|
146
|
+
throw new Error("Codex rollout changed while its terminal submission anchor was captured");
|
|
147
|
+
}
|
|
148
|
+
const withFile = {
|
|
149
|
+
...base,
|
|
150
|
+
mode: "existing",
|
|
151
|
+
file_existed: true,
|
|
152
|
+
offset_bytes: before.size,
|
|
153
|
+
rollout
|
|
154
|
+
};
|
|
155
|
+
return {
|
|
156
|
+
...withFile,
|
|
157
|
+
anchor_fingerprint: fingerprint(withFile)
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
fs.closeSync(opened.fd);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
export function detectCodexRolloutAcceptance(options) {
|
|
165
|
+
const anchor = validateCodexRolloutAcceptanceAnchor(options.anchor);
|
|
166
|
+
const requestHash = sha256Value(options.requestHash, "terminal request hash");
|
|
167
|
+
const current = options.currentIdentity;
|
|
168
|
+
if (exactNativeThreadId(current.sessionId) !== anchor.native_thread_id ||
|
|
169
|
+
current.processUuid !== anchor.process_uuid ||
|
|
170
|
+
current.processBirth !== anchor.process_birth) {
|
|
171
|
+
throw new Error("Codex process or native thread identity changed during acceptance polling");
|
|
172
|
+
}
|
|
173
|
+
if (!current.rollout) {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
const rollout = normalizedRolloutIdentity(current.rollout);
|
|
177
|
+
if (anchor.rollout &&
|
|
178
|
+
!sameRolloutIdentity(anchor.rollout, rollout)) {
|
|
179
|
+
throw new Error("Codex rollout identity changed during acceptance polling");
|
|
180
|
+
}
|
|
181
|
+
const opened = openExactRollout(rollout);
|
|
182
|
+
try {
|
|
183
|
+
const before = opened.stat;
|
|
184
|
+
assertPrivateRegularFile(before);
|
|
185
|
+
if (before.size < anchor.offset_bytes) {
|
|
186
|
+
throw new Error("Codex rollout was truncated after terminal submission");
|
|
187
|
+
}
|
|
188
|
+
const bytesToRead = before.size - anchor.offset_bytes;
|
|
189
|
+
if (bytesToRead === 0) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
if (bytesToRead > CODEX_ACCEPTANCE_MAX_BYTES) {
|
|
193
|
+
throw new Error("Codex rollout acceptance suffix exceeded the bounded read limit");
|
|
194
|
+
}
|
|
195
|
+
if (!fileEndsWithNewline(opened.fd, before.size)) {
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
const buffer = Buffer.allocUnsafe(bytesToRead);
|
|
199
|
+
const bytesRead = fs.readSync(opened.fd, buffer, 0, bytesToRead, anchor.offset_bytes);
|
|
200
|
+
if (bytesRead !== bytesToRead) {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
const after = fs.fstatSync(opened.fd);
|
|
204
|
+
if (!sameStableFile(before, after)) {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
const accepted = acceptedCodexTurnFromSuffix(buffer.toString("utf8"), requestHash);
|
|
208
|
+
if (!accepted) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
const evidenceBase = {
|
|
212
|
+
source: "codex_rollout",
|
|
213
|
+
kind: "native_user_turn",
|
|
214
|
+
nativeThreadId: anchor.native_thread_id,
|
|
215
|
+
requestHash,
|
|
216
|
+
acceptanceId: accepted.turnId,
|
|
217
|
+
acceptedAt: accepted.userTimestamp ?? accepted.startedAt,
|
|
218
|
+
anchorFingerprint: anchor.anchor_fingerprint,
|
|
219
|
+
metadata: {
|
|
220
|
+
turn_id: accepted.turnId,
|
|
221
|
+
anchor_offset_bytes: anchor.offset_bytes,
|
|
222
|
+
observed_end_offset_bytes: before.size
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
return {
|
|
226
|
+
...evidenceBase,
|
|
227
|
+
evidenceFingerprint: fingerprint(evidenceBase)
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
fs.closeSync(opened.fd);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Scans the exact rollout and native turn proven by terminal acceptance.
|
|
236
|
+
*
|
|
237
|
+
* Unlike the general Codex context loader, this detector does not retain only
|
|
238
|
+
* a recent-turn window. It starts at the immutable pre-submission byte anchor
|
|
239
|
+
* and matches only the persisted acceptance UUID, so a delayed monitor can
|
|
240
|
+
* recover completion even after later native turns have been appended.
|
|
241
|
+
*/
|
|
242
|
+
export function detectCodexBoundRolloutCompletion(options) {
|
|
243
|
+
let anchor;
|
|
244
|
+
try {
|
|
245
|
+
anchor = validateCodexRolloutAcceptanceAnchor(options.anchor);
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
return codexCompletionFailure("invalid_anchor", error);
|
|
249
|
+
}
|
|
250
|
+
const baseDiagnostics = {
|
|
251
|
+
detector: "codex_exact_bound_rollout",
|
|
252
|
+
native_thread_id: anchor.native_thread_id,
|
|
253
|
+
anchor_fingerprint: anchor.anchor_fingerprint,
|
|
254
|
+
scan_start_offset_bytes: anchor.offset_bytes
|
|
255
|
+
};
|
|
256
|
+
let requestHash;
|
|
257
|
+
let acceptance;
|
|
258
|
+
try {
|
|
259
|
+
requestHash = sha256Value(options.requestHash, "terminal request hash");
|
|
260
|
+
acceptance = validateTerminalSubmissionAcceptanceEvidence(options.acceptanceEvidence, {
|
|
261
|
+
source: "codex_rollout",
|
|
262
|
+
nativeThreadId: anchor.native_thread_id,
|
|
263
|
+
requestHash
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
return codexCompletionFailure("invalid_acceptance_evidence", error, baseDiagnostics);
|
|
268
|
+
}
|
|
269
|
+
let acceptanceId;
|
|
270
|
+
try {
|
|
271
|
+
acceptanceId = exactNativeThreadId(acceptance.acceptanceId);
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
return codexCompletionFailure("acceptance_turn_mismatch", error, baseDiagnostics);
|
|
275
|
+
}
|
|
276
|
+
const diagnostics = {
|
|
277
|
+
...baseDiagnostics,
|
|
278
|
+
acceptance_id: acceptanceId
|
|
279
|
+
};
|
|
280
|
+
if (acceptance.anchorFingerprint !== anchor.anchor_fingerprint) {
|
|
281
|
+
return codexCompletionFailure("acceptance_anchor_mismatch", new Error("Codex acceptance evidence belongs to a different byte anchor"), diagnostics);
|
|
282
|
+
}
|
|
283
|
+
const evidenceTurnId = optionalString(acceptance.metadata?.turn_id);
|
|
284
|
+
if (evidenceTurnId !== undefined) {
|
|
285
|
+
let normalizedEvidenceTurnId;
|
|
286
|
+
try {
|
|
287
|
+
normalizedEvidenceTurnId = exactNativeThreadId(evidenceTurnId);
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
return codexCompletionFailure("acceptance_turn_mismatch", error, diagnostics);
|
|
291
|
+
}
|
|
292
|
+
if (normalizedEvidenceTurnId !== acceptanceId) {
|
|
293
|
+
return codexCompletionFailure("acceptance_turn_mismatch", new Error("Codex acceptance metadata names a different native turn"), diagnostics);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
let evidenceAnchorOffset;
|
|
297
|
+
try {
|
|
298
|
+
evidenceAnchorOffset = optionalSafeOffset(acceptance.metadata?.anchor_offset_bytes);
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
return codexCompletionFailure("invalid_acceptance_evidence", error, diagnostics);
|
|
302
|
+
}
|
|
303
|
+
if (evidenceAnchorOffset !== undefined &&
|
|
304
|
+
evidenceAnchorOffset !== anchor.offset_bytes) {
|
|
305
|
+
return codexCompletionFailure("acceptance_anchor_mismatch", new Error("Codex acceptance evidence has a different byte offset"), diagnostics);
|
|
306
|
+
}
|
|
307
|
+
const current = options.currentIdentity;
|
|
308
|
+
try {
|
|
309
|
+
if (exactNativeThreadId(current.sessionId) !== anchor.native_thread_id ||
|
|
310
|
+
requiredString(current.processUuid, "Codex process UUID") !==
|
|
311
|
+
anchor.process_uuid ||
|
|
312
|
+
requiredString(current.processBirth, "Codex process birth") !==
|
|
313
|
+
anchor.process_birth) {
|
|
314
|
+
throw new Error("Codex process or native thread identity changed during completion polling");
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
return codexCompletionFailure("binding_identity_mismatch", error, diagnostics);
|
|
319
|
+
}
|
|
320
|
+
if (!current.rollout) {
|
|
321
|
+
return codexCompletionFailure("rollout_identity_mismatch", new Error("the accepted Codex Turn lost its persisted exact rollout identity"), diagnostics);
|
|
322
|
+
}
|
|
323
|
+
let rollout;
|
|
324
|
+
try {
|
|
325
|
+
rollout = normalizedRolloutIdentity(current.rollout);
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
return codexCompletionFailure("rollout_identity_mismatch", error, diagnostics);
|
|
329
|
+
}
|
|
330
|
+
const rolloutDiagnostics = {
|
|
331
|
+
...diagnostics,
|
|
332
|
+
rollout_identity_fingerprint: fingerprint(rollout)
|
|
333
|
+
};
|
|
334
|
+
if (anchor.rollout && !sameRolloutIdentity(anchor.rollout, rollout)) {
|
|
335
|
+
return codexCompletionFailure("rollout_identity_mismatch", new Error("Codex rollout identity changed after terminal acceptance"), rolloutDiagnostics);
|
|
336
|
+
}
|
|
337
|
+
let opened;
|
|
338
|
+
try {
|
|
339
|
+
opened = openExactRollout(rollout);
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
343
|
+
return codexCompletionFailure(/identity does not match/u.test(message)
|
|
344
|
+
? "rollout_identity_mismatch"
|
|
345
|
+
: "rollout_unreadable", error, rolloutDiagnostics);
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
const before = opened.stat;
|
|
349
|
+
try {
|
|
350
|
+
assertPrivateRegularFile(before);
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
return codexCompletionFailure("rollout_unreadable", error, rolloutDiagnostics);
|
|
354
|
+
}
|
|
355
|
+
let evidenceEndOffset;
|
|
356
|
+
try {
|
|
357
|
+
evidenceEndOffset = optionalSafeOffset(acceptance.metadata?.observed_end_offset_bytes);
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
return codexCompletionFailure("invalid_acceptance_evidence", error, rolloutDiagnostics);
|
|
361
|
+
}
|
|
362
|
+
if (before.size < anchor.offset_bytes ||
|
|
363
|
+
(evidenceEndOffset !== undefined && before.size < evidenceEndOffset)) {
|
|
364
|
+
return codexCompletionFailure("rollout_truncated", new Error("Codex rollout was truncated after native acceptance"), {
|
|
365
|
+
...rolloutDiagnostics,
|
|
366
|
+
observed_end_offset_bytes: before.size
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
const bytesToRead = before.size - anchor.offset_bytes;
|
|
370
|
+
const observedDiagnostics = {
|
|
371
|
+
...rolloutDiagnostics,
|
|
372
|
+
observed_end_offset_bytes: before.size
|
|
373
|
+
};
|
|
374
|
+
if (bytesToRead > CODEX_COMPLETION_MAX_BYTES) {
|
|
375
|
+
return codexCompletionFailure("scan_limit_exceeded", new Error("Codex bound completion suffix exceeded the safe scan limit"), observedDiagnostics);
|
|
376
|
+
}
|
|
377
|
+
if (bytesToRead === 0) {
|
|
378
|
+
return codexCompletionPending("exact_turn_not_complete", observedDiagnostics, "no post-anchor Codex rollout records are available yet");
|
|
379
|
+
}
|
|
380
|
+
if (!fileEndsWithNewline(opened.fd, before.size)) {
|
|
381
|
+
return codexCompletionPending("partial_rollout_record", observedDiagnostics, "the exact Codex rollout ends with a partial JSONL record");
|
|
382
|
+
}
|
|
383
|
+
const buffer = Buffer.allocUnsafe(bytesToRead);
|
|
384
|
+
const bytesRead = fs.readSync(opened.fd, buffer, 0, bytesToRead, anchor.offset_bytes);
|
|
385
|
+
const after = fs.fstatSync(opened.fd);
|
|
386
|
+
if (bytesRead !== bytesToRead || !sameStableFile(before, after)) {
|
|
387
|
+
return codexCompletionPending("rollout_changed_during_scan", observedDiagnostics, "the exact Codex rollout changed while completion was scanned");
|
|
388
|
+
}
|
|
389
|
+
const scan = scanExactCodexTaskComplete(buffer.toString("utf8"), acceptanceId);
|
|
390
|
+
const scanDiagnostics = {
|
|
391
|
+
...observedDiagnostics,
|
|
392
|
+
scanned_records: scan.scannedRecords,
|
|
393
|
+
observed_task_complete_records: scan.observedTaskCompleteRecords
|
|
394
|
+
};
|
|
395
|
+
if (scan.status === "failure") {
|
|
396
|
+
return codexCompletionFailure(scan.code, scan.detail, scanDiagnostics);
|
|
397
|
+
}
|
|
398
|
+
if (scan.status === "pending") {
|
|
399
|
+
return codexCompletionPending("exact_turn_not_complete", scanDiagnostics, "the accepted Codex native turn has no durable task_complete record yet");
|
|
400
|
+
}
|
|
401
|
+
const completion = {
|
|
402
|
+
source: "durable",
|
|
403
|
+
outcome: "success",
|
|
404
|
+
text: truncateCompletionText(redactString(scan.text)),
|
|
405
|
+
...(scan.timestamp ? { timestamp: scan.timestamp } : {}),
|
|
406
|
+
id: acceptanceId,
|
|
407
|
+
confidence: "high",
|
|
408
|
+
metadata: {
|
|
409
|
+
match: "bound_rollout_task_complete",
|
|
410
|
+
turn_id: acceptanceId,
|
|
411
|
+
native_thread_id: anchor.native_thread_id,
|
|
412
|
+
anchor_fingerprint: anchor.anchor_fingerprint,
|
|
413
|
+
rollout_identity_fingerprint: rolloutDiagnostics.rollout_identity_fingerprint,
|
|
414
|
+
scan_start_offset_bytes: anchor.offset_bytes,
|
|
415
|
+
observed_end_offset_bytes: before.size,
|
|
416
|
+
scanned_records: scan.scannedRecords,
|
|
417
|
+
observed_task_complete_records: scan.observedTaskCompleteRecords
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
return {
|
|
421
|
+
status: "completed",
|
|
422
|
+
completion,
|
|
423
|
+
diagnostics: {
|
|
424
|
+
...scanDiagnostics,
|
|
425
|
+
code: "completion_found"
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
return codexCompletionFailure("rollout_unreadable", error, rolloutDiagnostics);
|
|
431
|
+
}
|
|
432
|
+
finally {
|
|
433
|
+
try {
|
|
434
|
+
fs.closeSync(opened.fd);
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
// The scan result is already fenced by the descriptor identity and both
|
|
438
|
+
// file snapshots. A close failure cannot make it safe to retry a native
|
|
439
|
+
// turn, so do not replace that deterministic result with an exception.
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function scanExactCodexTaskComplete(text, acceptanceId) {
|
|
444
|
+
let scannedRecords = 0;
|
|
445
|
+
let observedTaskCompleteRecords = 0;
|
|
446
|
+
const exactMatches = [];
|
|
447
|
+
for (const rawLine of text.split("\n")) {
|
|
448
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
449
|
+
if (!line) {
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
let value;
|
|
453
|
+
try {
|
|
454
|
+
value = JSON.parse(line);
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
return {
|
|
458
|
+
status: "failure",
|
|
459
|
+
code: "invalid_rollout_jsonl",
|
|
460
|
+
detail: `Codex bound completion suffix contains invalid JSONL at record ${scannedRecords + 1}`,
|
|
461
|
+
scannedRecords,
|
|
462
|
+
observedTaskCompleteRecords
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
scannedRecords += 1;
|
|
466
|
+
if (!isRecord(value) ||
|
|
467
|
+
value.type !== "event_msg" ||
|
|
468
|
+
!isRecord(value.payload) ||
|
|
469
|
+
value.payload.type !== "task_complete") {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
observedTaskCompleteRecords += 1;
|
|
473
|
+
const turnId = optionalString(value.payload.turn_id)?.toLowerCase();
|
|
474
|
+
if (turnId === acceptanceId) {
|
|
475
|
+
exactMatches.push(value);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (exactMatches.length === 0) {
|
|
479
|
+
return {
|
|
480
|
+
status: "pending",
|
|
481
|
+
scannedRecords,
|
|
482
|
+
observedTaskCompleteRecords
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
if (exactMatches.length > 1) {
|
|
486
|
+
return {
|
|
487
|
+
status: "failure",
|
|
488
|
+
code: "duplicate_exact_completion",
|
|
489
|
+
detail: "the exact accepted Codex turn has duplicate task_complete records",
|
|
490
|
+
scannedRecords,
|
|
491
|
+
observedTaskCompleteRecords
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
const match = exactMatches[0];
|
|
495
|
+
const payload = match.payload;
|
|
496
|
+
const textValue = optionalString(payload.last_agent_message);
|
|
497
|
+
if (!textValue) {
|
|
498
|
+
return {
|
|
499
|
+
status: "failure",
|
|
500
|
+
code: "invalid_exact_completion",
|
|
501
|
+
detail: "the exact Codex task_complete record has no final agent message",
|
|
502
|
+
scannedRecords,
|
|
503
|
+
observedTaskCompleteRecords
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
if (match.timestamp !== undefined && !validTimestamp(match.timestamp)) {
|
|
507
|
+
return {
|
|
508
|
+
status: "failure",
|
|
509
|
+
code: "invalid_exact_completion",
|
|
510
|
+
detail: "the exact Codex task_complete record has an invalid timestamp",
|
|
511
|
+
scannedRecords,
|
|
512
|
+
observedTaskCompleteRecords
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
status: "completed",
|
|
517
|
+
text: textValue,
|
|
518
|
+
...(match.timestamp !== undefined
|
|
519
|
+
? { timestamp: String(match.timestamp) }
|
|
520
|
+
: {}),
|
|
521
|
+
scannedRecords,
|
|
522
|
+
observedTaskCompleteRecords
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
function codexCompletionPending(code, diagnostics, detail) {
|
|
526
|
+
return {
|
|
527
|
+
status: "pending",
|
|
528
|
+
diagnostics: {
|
|
529
|
+
...diagnostics,
|
|
530
|
+
code,
|
|
531
|
+
detail
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
function codexCompletionFailure(code, error, diagnostics = {
|
|
536
|
+
detector: "codex_exact_bound_rollout"
|
|
537
|
+
}) {
|
|
538
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
539
|
+
return {
|
|
540
|
+
status: "failure",
|
|
541
|
+
diagnostics: {
|
|
542
|
+
...diagnostics,
|
|
543
|
+
code,
|
|
544
|
+
detail: truncateCompletionText(redactString(detail))
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
function optionalSafeOffset(value) {
|
|
549
|
+
if (value === undefined) {
|
|
550
|
+
return undefined;
|
|
551
|
+
}
|
|
552
|
+
if (!Number.isSafeInteger(value) || Number(value) < 0) {
|
|
553
|
+
throw new Error("Codex acceptance evidence byte offset is invalid");
|
|
554
|
+
}
|
|
555
|
+
return Number(value);
|
|
556
|
+
}
|
|
557
|
+
function truncateCompletionText(value) {
|
|
558
|
+
return value.length <= CODEX_COMPLETION_MAX_TEXT_LENGTH
|
|
559
|
+
? value
|
|
560
|
+
: `${value.slice(0, CODEX_COMPLETION_MAX_TEXT_LENGTH - 1)}…`;
|
|
561
|
+
}
|
|
562
|
+
function acceptedCodexTurnFromSuffix(text, requestHash) {
|
|
563
|
+
const startedTurns = new Map();
|
|
564
|
+
const matches = [];
|
|
565
|
+
for (const line of text.split(/\r?\n/u)) {
|
|
566
|
+
if (!line) {
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
let value;
|
|
570
|
+
try {
|
|
571
|
+
value = JSON.parse(line);
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
throw new Error("Codex rollout acceptance suffix contains invalid JSONL");
|
|
575
|
+
}
|
|
576
|
+
if (!isRecord(value) || !isRecord(value.payload)) {
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
const payload = value.payload;
|
|
580
|
+
if (value.type === "event_msg" && payload.type === "task_started") {
|
|
581
|
+
const turnId = exactNativeThreadId(payload.turn_id);
|
|
582
|
+
if (startedTurns.has(turnId)) {
|
|
583
|
+
throw new Error("Codex rollout contains duplicate post-anchor task_started evidence");
|
|
584
|
+
}
|
|
585
|
+
startedTurns.set(turnId, {
|
|
586
|
+
...(validTimestamp(value.timestamp)
|
|
587
|
+
? { startedAt: String(value.timestamp) }
|
|
588
|
+
: {})
|
|
589
|
+
});
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
if (value.type !== "response_item" ||
|
|
593
|
+
payload.type !== "message" ||
|
|
594
|
+
payload.role !== "user") {
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
const metadata = isRecord(payload.internal_chat_message_metadata_passthrough)
|
|
598
|
+
? payload.internal_chat_message_metadata_passthrough
|
|
599
|
+
: undefined;
|
|
600
|
+
const rawTurnId = optionalString(metadata?.turn_id);
|
|
601
|
+
const userText = exactCodexUserResponseText(payload.content);
|
|
602
|
+
if (!rawTurnId || userText === undefined) {
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
const turnId = exactNativeThreadId(rawTurnId);
|
|
606
|
+
if (fingerprintText(userText) !== requestHash) {
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
const started = startedTurns.get(turnId);
|
|
610
|
+
if (!started) {
|
|
611
|
+
throw new Error("matching Codex user response has no same-turn post-anchor task_started evidence");
|
|
612
|
+
}
|
|
613
|
+
matches.push({
|
|
614
|
+
turnId,
|
|
615
|
+
...started,
|
|
616
|
+
...(validTimestamp(value.timestamp)
|
|
617
|
+
? { userTimestamp: String(value.timestamp) }
|
|
618
|
+
: {})
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
if (matches.length > 1) {
|
|
622
|
+
throw new Error("multiple Codex native turns matched the terminal request");
|
|
623
|
+
}
|
|
624
|
+
return matches[0];
|
|
625
|
+
}
|
|
626
|
+
function exactCodexUserResponseText(content) {
|
|
627
|
+
if (!Array.isArray(content) || content.length !== 1) {
|
|
628
|
+
return undefined;
|
|
629
|
+
}
|
|
630
|
+
const item = content[0];
|
|
631
|
+
return isRecord(item) && item.type === "input_text" &&
|
|
632
|
+
typeof item.text === "string"
|
|
633
|
+
? item.text
|
|
634
|
+
: undefined;
|
|
635
|
+
}
|
|
636
|
+
function validateCodexRolloutAcceptanceAnchor(value) {
|
|
637
|
+
if (!isRecord(value) ||
|
|
638
|
+
value.schema !== "agent-knock-knock/codex-rollout-acceptance-anchor" ||
|
|
639
|
+
value.version !== 1 ||
|
|
640
|
+
!["existing", "pre_materialization"].includes(String(value.mode)) ||
|
|
641
|
+
!Number.isSafeInteger(value.offset_bytes) ||
|
|
642
|
+
value.offset_bytes < 0 ||
|
|
643
|
+
typeof value.file_existed !== "boolean") {
|
|
644
|
+
throw new Error("Codex rollout acceptance anchor is invalid");
|
|
645
|
+
}
|
|
646
|
+
exactNativeThreadId(value.native_thread_id);
|
|
647
|
+
requiredString(value.process_uuid, "Codex process UUID");
|
|
648
|
+
requiredString(value.process_birth, "Codex process birth");
|
|
649
|
+
if (!validTimestamp(value.captured_at)) {
|
|
650
|
+
throw new Error("Codex acceptance capture timestamp is invalid");
|
|
651
|
+
}
|
|
652
|
+
sha256Value(value.anchor_fingerprint, "Codex acceptance anchor fingerprint");
|
|
653
|
+
const { anchor_fingerprint: _fingerprint, ...base } = value;
|
|
654
|
+
if (fingerprint(base) !== value.anchor_fingerprint) {
|
|
655
|
+
throw new Error("Codex rollout acceptance anchor fingerprint does not match");
|
|
656
|
+
}
|
|
657
|
+
if (value.file_existed !== (value.mode === "existing") ||
|
|
658
|
+
value.file_existed !== Boolean(value.rollout)) {
|
|
659
|
+
throw new Error("Codex rollout acceptance anchor file state is inconsistent");
|
|
660
|
+
}
|
|
661
|
+
if (value.mode === "existing" && value.rollout) {
|
|
662
|
+
normalizedRolloutIdentity(value.rollout);
|
|
663
|
+
if (value.expected_empty_native_session !== undefined) {
|
|
664
|
+
throw new Error("existing Codex acceptance anchor has pre-materialization state");
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
else if (value.offset_bytes !== 0 ||
|
|
668
|
+
value.rollout !== undefined ||
|
|
669
|
+
value.expected_empty_native_session !== true) {
|
|
670
|
+
throw new Error("Codex pre-materialization acceptance anchor is inconsistent");
|
|
671
|
+
}
|
|
672
|
+
return value;
|
|
673
|
+
}
|
|
674
|
+
function openExactRollout(rollout) {
|
|
675
|
+
if (!path.isAbsolute(rollout.path)) {
|
|
676
|
+
throw new Error("Codex rollout path is not absolute");
|
|
677
|
+
}
|
|
678
|
+
const before = fs.lstatSync(rollout.path);
|
|
679
|
+
if (!before.isFile() || before.isSymbolicLink()) {
|
|
680
|
+
throw new Error("Codex rollout path is not a regular file");
|
|
681
|
+
}
|
|
682
|
+
const fd = fs.openSync(rollout.path, fs.constants.O_RDONLY | NO_FOLLOW_FLAG);
|
|
683
|
+
try {
|
|
684
|
+
const stat = fs.fstatSync(fd);
|
|
685
|
+
if (BigInt(stat.dev) !== parsedInteger(rollout.device) ||
|
|
686
|
+
BigInt(stat.ino) !== parsedInteger(rollout.inode)) {
|
|
687
|
+
throw new Error("Codex rollout descriptor identity does not match its open file");
|
|
688
|
+
}
|
|
689
|
+
return { fd, stat };
|
|
690
|
+
}
|
|
691
|
+
catch (error) {
|
|
692
|
+
fs.closeSync(fd);
|
|
693
|
+
throw error;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
function assertPrivateRegularFile(stat) {
|
|
697
|
+
if (!stat.isFile()) {
|
|
698
|
+
throw new Error("Codex rollout is not a regular file");
|
|
699
|
+
}
|
|
700
|
+
if (process.platform !== "win32" &&
|
|
701
|
+
typeof process.getuid === "function" &&
|
|
702
|
+
stat.uid !== process.getuid()) {
|
|
703
|
+
throw new Error("Codex rollout is not owned by the current user");
|
|
704
|
+
}
|
|
705
|
+
if (process.platform !== "win32" && (stat.mode & 0o022) !== 0) {
|
|
706
|
+
throw new Error("Codex rollout is writable by another user");
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
function fileEndsWithNewline(fd, size) {
|
|
710
|
+
if (size <= 0) {
|
|
711
|
+
return true;
|
|
712
|
+
}
|
|
713
|
+
const last = Buffer.allocUnsafe(1);
|
|
714
|
+
return fs.readSync(fd, last, 0, 1, size - 1) === 1 && last[0] === 0x0a;
|
|
715
|
+
}
|
|
716
|
+
function sameStableFile(left, right) {
|
|
717
|
+
return left.dev === right.dev &&
|
|
718
|
+
left.ino === right.ino &&
|
|
719
|
+
left.size === right.size &&
|
|
720
|
+
left.mtimeMs === right.mtimeMs &&
|
|
721
|
+
left.ctimeMs === right.ctimeMs;
|
|
722
|
+
}
|
|
723
|
+
function normalizedRolloutIdentity(value) {
|
|
724
|
+
const normalized = {
|
|
725
|
+
fd: requiredString(value.fd, "Codex rollout descriptor"),
|
|
726
|
+
device: requiredString(value.device, "Codex rollout device"),
|
|
727
|
+
inode: requiredString(value.inode, "Codex rollout inode"),
|
|
728
|
+
path: requiredString(value.path, "Codex rollout path")
|
|
729
|
+
};
|
|
730
|
+
parsedInteger(normalized.device);
|
|
731
|
+
parsedInteger(normalized.inode);
|
|
732
|
+
return normalized;
|
|
733
|
+
}
|
|
734
|
+
function sameRolloutIdentity(left, right) {
|
|
735
|
+
return left.fd === right.fd &&
|
|
736
|
+
left.device === right.device &&
|
|
737
|
+
left.inode === right.inode &&
|
|
738
|
+
left.path === right.path;
|
|
739
|
+
}
|
|
740
|
+
function exactNativeThreadId(value) {
|
|
741
|
+
const text = requiredString(value, "Codex native thread ID").toLowerCase();
|
|
742
|
+
if (!NATIVE_THREAD_ID_PATTERN.test(text)) {
|
|
743
|
+
throw new Error("Codex native thread ID is not an exact UUID");
|
|
744
|
+
}
|
|
745
|
+
return text;
|
|
746
|
+
}
|
|
747
|
+
function parsedInteger(value) {
|
|
748
|
+
try {
|
|
749
|
+
return BigInt(value);
|
|
750
|
+
}
|
|
751
|
+
catch {
|
|
752
|
+
throw new Error("Codex rollout file identity is not numeric");
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
function sha256Value(value, label) {
|
|
756
|
+
const text = requiredString(value, label).toLowerCase();
|
|
757
|
+
if (!/^[0-9a-f]{64}$/u.test(text)) {
|
|
758
|
+
throw new Error(`${label} is not a SHA-256 value`);
|
|
759
|
+
}
|
|
760
|
+
return text;
|
|
761
|
+
}
|
|
762
|
+
function requiredString(value, label) {
|
|
763
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
764
|
+
throw new Error(`${label} is unavailable`);
|
|
765
|
+
}
|
|
766
|
+
return value.trim();
|
|
767
|
+
}
|
|
768
|
+
function optionalString(value) {
|
|
769
|
+
return typeof value === "string" && value.trim().length > 0
|
|
770
|
+
? value.trim()
|
|
771
|
+
: undefined;
|
|
772
|
+
}
|
|
773
|
+
function validTimestamp(value) {
|
|
774
|
+
return typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
775
|
+
}
|
|
776
|
+
function fingerprintText(value) {
|
|
777
|
+
return createHash("sha256").update(value).digest("hex");
|
|
778
|
+
}
|
|
779
|
+
function fingerprint(value) {
|
|
780
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
781
|
+
}
|
|
782
|
+
function isRecord(value) {
|
|
783
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
784
|
+
}
|
|
785
|
+
//# sourceMappingURL=terminal-submission-acceptance.js.map
|