create-principles-disciple 1.132.2 → 1.132.4
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/codex-adapter/dist/codec/index.d.ts +6 -0
- package/codex-adapter/dist/codec/index.js +5 -0
- package/codex-adapter/dist/codec/input-decoder.d.ts +13 -0
- package/codex-adapter/dist/codec/input-decoder.js +103 -0
- package/codex-adapter/dist/codec/output-encoder.d.ts +32 -0
- package/codex-adapter/dist/codec/output-encoder.js +51 -0
- package/codex-adapter/dist/host-adapter.d.ts +30 -0
- package/codex-adapter/dist/host-adapter.js +28 -0
- package/codex-adapter/dist/index.d.ts +25 -0
- package/codex-adapter/dist/index.js +19 -0
- package/codex-adapter/dist/ingestion/admission.d.ts +54 -0
- package/codex-adapter/dist/ingestion/admission.js +137 -0
- package/codex-adapter/dist/ingestion/catch-up.d.ts +41 -0
- package/codex-adapter/dist/ingestion/catch-up.js +94 -0
- package/codex-adapter/dist/ingestion/codex-home.d.ts +19 -0
- package/codex-adapter/dist/ingestion/codex-home.js +43 -0
- package/codex-adapter/dist/ingestion/codex-version.d.ts +30 -0
- package/codex-adapter/dist/ingestion/codex-version.js +53 -0
- package/codex-adapter/dist/ingestion/ingestion-fields.d.ts +17 -0
- package/codex-adapter/dist/ingestion/ingestion-fields.js +28 -0
- package/codex-adapter/dist/ingestion/ingestion.d.ts +71 -0
- package/codex-adapter/dist/ingestion/ingestion.js +248 -0
- package/codex-adapter/dist/ingestion/transcript-decoder.d.ts +72 -0
- package/codex-adapter/dist/ingestion/transcript-decoder.js +485 -0
- package/codex-adapter/dist/ingestion/transcript-locate.d.ts +13 -0
- package/codex-adapter/dist/ingestion/transcript-locate.js +75 -0
- package/codex-adapter/dist/ingestion/transcript-path.d.ts +25 -0
- package/codex-adapter/dist/ingestion/transcript-path.js +87 -0
- package/codex-adapter/dist/pd-hook.d.ts +9 -0
- package/codex-adapter/dist/pd-hook.js +159 -0
- package/codex-adapter/dist/tool-semantics.d.ts +20 -0
- package/codex-adapter/dist/tool-semantics.js +27 -0
- package/codex-adapter/dist/worker/workspace-worker.d.ts +59 -0
- package/codex-adapter/dist/worker/workspace-worker.js +270 -0
- package/codex-adapter/package.json +52 -0
- package/console/dist/server/routes/update.js +187 -19
- package/console/dist/server/update/mutation-controller.d.ts +18 -0
- package/console/dist/server/update/mutation-controller.js +29 -0
- package/console/dist/server/utils/installed-layout.d.ts +1 -0
- package/console/dist/server/utils/installed-layout.js +2 -0
- package/console/package.json +1 -0
- package/core/dist/runtime-v2/__tests__/architecture-regression.test.js +15 -0
- package/core/dist/runtime-v2/__tests__/architecture-regression.test.js.map +1 -1
- package/core/dist/runtime-v2/adapter/__tests__/pd-stream-simple.test.d.ts +2 -0
- package/core/dist/runtime-v2/adapter/__tests__/pd-stream-simple.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/adapter/__tests__/pd-stream-simple.test.js +71 -0
- package/core/dist/runtime-v2/adapter/__tests__/pd-stream-simple.test.js.map +1 -0
- package/core/dist/runtime-v2/adapter/__tests__/pi-ai-http-transport.test.d.ts +2 -0
- package/core/dist/runtime-v2/adapter/__tests__/pi-ai-http-transport.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/adapter/__tests__/pi-ai-http-transport.test.js +219 -0
- package/core/dist/runtime-v2/adapter/__tests__/pi-ai-http-transport.test.js.map +1 -0
- package/core/dist/runtime-v2/adapter/artificer-l2-adapter.d.ts.map +1 -1
- package/core/dist/runtime-v2/adapter/artificer-l2-adapter.js +2 -3
- package/core/dist/runtime-v2/adapter/artificer-l2-adapter.js.map +1 -1
- package/core/dist/runtime-v2/adapter/l2-agent-loop-adapter.d.ts +12 -1
- package/core/dist/runtime-v2/adapter/l2-agent-loop-adapter.d.ts.map +1 -1
- package/core/dist/runtime-v2/adapter/l2-agent-loop-adapter.js +15 -2
- package/core/dist/runtime-v2/adapter/l2-agent-loop-adapter.js.map +1 -1
- package/core/dist/runtime-v2/adapter/pi-ai-http-transport.d.ts +50 -0
- package/core/dist/runtime-v2/adapter/pi-ai-http-transport.d.ts.map +1 -0
- package/core/dist/runtime-v2/adapter/pi-ai-http-transport.js +93 -0
- package/core/dist/runtime-v2/adapter/pi-ai-http-transport.js.map +1 -0
- package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.d.ts.map +1 -1
- package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.js +5 -0
- package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.js.map +1 -1
- package/core/dist/runtime-v2/internalization/__tests__/owner-decision.test.js +152 -0
- package/core/dist/runtime-v2/internalization/__tests__/owner-decision.test.js.map +1 -1
- package/core/dist/runtime-v2/internalization/owner-review.d.ts +7 -1
- package/core/dist/runtime-v2/internalization/owner-review.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/owner-review.js +22 -3
- package/core/dist/runtime-v2/internalization/owner-review.js.map +1 -1
- package/core/package.json +3 -2
- package/dist/installer.d.ts +32 -0
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +198 -2
- package/dist/installer.js.map +1 -1
- package/dist/installers/codex-host-installer.d.ts.map +1 -1
- package/dist/installers/codex-host-installer.js +5 -3
- package/dist/installers/codex-host-installer.js.map +1 -1
- package/dist/update/release-manager-authority.d.ts +69 -0
- package/dist/update/release-manager-authority.d.ts.map +1 -0
- package/dist/update/release-manager-authority.js +125 -0
- package/dist/update/release-manager-authority.js.map +1 -0
- package/dist/utils/workspace-divergence.d.ts +36 -0
- package/dist/utils/workspace-divergence.d.ts.map +1 -0
- package/dist/utils/workspace-divergence.js +87 -0
- package/dist/utils/workspace-divergence.js.map +1 -0
- package/install-layout/dist/index.d.ts +5 -0
- package/install-layout/dist/index.js +1 -0
- package/install-layout/package.json +1 -1
- package/package.json +4 -2
- package/plugin/dist/bundle.js +870 -734
- package/plugin/dist/governance-audit.js +381 -247
- package/plugin/dist/openclaw-sdk.d.ts +3 -0
- package/plugin/dist/rulehost-evidence.js +420 -286
- package/plugin/dist/service/subagent-workflow/types.d.ts +2 -2
- package/plugin/dist/utils/retry.d.ts +1 -1
- package/plugin/dist/utils/workspace-resolver.d.ts +35 -4
- package/plugin/openclaw.plugin.json +1 -1
- package/plugin/package.json +1 -1
- package/release-manager/dist/i18n.d.ts +6 -0
- package/release-manager/dist/i18n.d.ts.map +1 -0
- package/release-manager/dist/i18n.js +234 -0
- package/release-manager/dist/i18n.js.map +1 -0
- package/release-manager/dist/index.d.ts +7 -0
- package/release-manager/dist/index.d.ts.map +1 -0
- package/release-manager/dist/index.js +474 -0
- package/release-manager/dist/index.js.map +1 -0
- package/release-manager/dist/installer.d.ts +217 -0
- package/release-manager/dist/installer.d.ts.map +1 -0
- package/release-manager/dist/installer.js +2451 -0
- package/release-manager/dist/installer.js.map +1 -0
- package/release-manager/dist/installers/codex-host-installer.d.ts +29 -0
- package/release-manager/dist/installers/codex-host-installer.d.ts.map +1 -0
- package/release-manager/dist/installers/codex-host-installer.js +496 -0
- package/release-manager/dist/installers/codex-host-installer.js.map +1 -0
- package/release-manager/dist/installers/index.d.ts +40 -0
- package/release-manager/dist/installers/index.d.ts.map +1 -0
- package/release-manager/dist/installers/index.js +35 -0
- package/release-manager/dist/installers/index.js.map +1 -0
- package/release-manager/dist/installers/openclaw-host-installer.d.ts +18 -0
- package/release-manager/dist/installers/openclaw-host-installer.d.ts.map +1 -0
- package/release-manager/dist/installers/openclaw-host-installer.js +414 -0
- package/release-manager/dist/installers/openclaw-host-installer.js.map +1 -0
- package/release-manager/dist/mvp-config.d.ts +197 -0
- package/release-manager/dist/mvp-config.d.ts.map +1 -0
- package/release-manager/dist/mvp-config.js +509 -0
- package/release-manager/dist/mvp-config.js.map +1 -0
- package/release-manager/dist/prompts.d.ts +30 -0
- package/release-manager/dist/prompts.d.ts.map +1 -0
- package/release-manager/dist/prompts.js +231 -0
- package/release-manager/dist/prompts.js.map +1 -0
- package/release-manager/dist/skill-language.d.ts +32 -0
- package/release-manager/dist/skill-language.d.ts.map +1 -0
- package/release-manager/dist/skill-language.js +107 -0
- package/release-manager/dist/skill-language.js.map +1 -0
- package/release-manager/dist/uninstaller.d.ts +55 -0
- package/release-manager/dist/uninstaller.d.ts.map +1 -0
- package/release-manager/dist/uninstaller.js +409 -0
- package/release-manager/dist/uninstaller.js.map +1 -0
- package/release-manager/dist/update/atomic-file.d.ts +35 -0
- package/release-manager/dist/update/atomic-file.d.ts.map +1 -0
- package/release-manager/dist/update/atomic-file.js +120 -0
- package/release-manager/dist/update/atomic-file.js.map +1 -0
- package/release-manager/dist/update/bootstrap-protocol.d.ts +55 -0
- package/release-manager/dist/update/bootstrap-protocol.d.ts.map +1 -0
- package/release-manager/dist/update/bootstrap-protocol.js +132 -0
- package/release-manager/dist/update/bootstrap-protocol.js.map +1 -0
- package/release-manager/dist/update/channel-metadata.d.ts +28 -0
- package/release-manager/dist/update/channel-metadata.d.ts.map +1 -0
- package/release-manager/dist/update/channel-metadata.js +90 -0
- package/release-manager/dist/update/channel-metadata.js.map +1 -0
- package/release-manager/dist/update/channel-promotion.d.ts +39 -0
- package/release-manager/dist/update/channel-promotion.d.ts.map +1 -0
- package/release-manager/dist/update/channel-promotion.js +55 -0
- package/release-manager/dist/update/channel-promotion.js.map +1 -0
- package/release-manager/dist/update/data-compatibility.d.ts +35 -0
- package/release-manager/dist/update/data-compatibility.d.ts.map +1 -0
- package/release-manager/dist/update/data-compatibility.js +45 -0
- package/release-manager/dist/update/data-compatibility.js.map +1 -0
- package/release-manager/dist/update/install-layout.d.ts +59 -0
- package/release-manager/dist/update/install-layout.d.ts.map +1 -0
- package/release-manager/dist/update/install-layout.js +127 -0
- package/release-manager/dist/update/install-layout.js.map +1 -0
- package/release-manager/dist/update/legacy-migration.d.ts +56 -0
- package/release-manager/dist/update/legacy-migration.d.ts.map +1 -0
- package/release-manager/dist/update/legacy-migration.js +254 -0
- package/release-manager/dist/update/legacy-migration.js.map +1 -0
- package/release-manager/dist/update/product-identity.d.ts +50 -0
- package/release-manager/dist/update/product-identity.d.ts.map +1 -0
- package/release-manager/dist/update/product-identity.js +63 -0
- package/release-manager/dist/update/product-identity.js.map +1 -0
- package/release-manager/dist/update/release-asset-manifest.d.ts +31 -0
- package/release-manager/dist/update/release-asset-manifest.d.ts.map +1 -0
- package/release-manager/dist/update/release-asset-manifest.js +161 -0
- package/release-manager/dist/update/release-asset-manifest.js.map +1 -0
- package/release-manager/dist/update/release-identity.d.ts +33 -0
- package/release-manager/dist/update/release-identity.d.ts.map +1 -0
- package/release-manager/dist/update/release-identity.js +108 -0
- package/release-manager/dist/update/release-identity.js.map +1 -0
- package/release-manager/dist/update/release-manager-authority.d.ts +69 -0
- package/release-manager/dist/update/release-manager-authority.d.ts.map +1 -0
- package/release-manager/dist/update/release-manager-authority.js +125 -0
- package/release-manager/dist/update/release-manager-authority.js.map +1 -0
- package/release-manager/dist/update/release-manager.d.ts +96 -0
- package/release-manager/dist/update/release-manager.d.ts.map +1 -0
- package/release-manager/dist/update/release-manager.js +253 -0
- package/release-manager/dist/update/release-manager.js.map +1 -0
- package/release-manager/dist/update/release-metadata.d.ts +57 -0
- package/release-manager/dist/update/release-metadata.d.ts.map +1 -0
- package/release-manager/dist/update/release-metadata.js +200 -0
- package/release-manager/dist/update/release-metadata.js.map +1 -0
- package/release-manager/dist/update/release-policy.d.ts +51 -0
- package/release-manager/dist/update/release-policy.d.ts.map +1 -0
- package/release-manager/dist/update/release-policy.js +103 -0
- package/release-manager/dist/update/release-policy.js.map +1 -0
- package/release-manager/dist/update/rollback-policy.d.ts +56 -0
- package/release-manager/dist/update/rollback-policy.d.ts.map +1 -0
- package/release-manager/dist/update/rollback-policy.js +61 -0
- package/release-manager/dist/update/rollback-policy.js.map +1 -0
- package/release-manager/dist/update/transaction-journal.d.ts +131 -0
- package/release-manager/dist/update/transaction-journal.d.ts.map +1 -0
- package/release-manager/dist/update/transaction-journal.js +346 -0
- package/release-manager/dist/update/transaction-journal.js.map +1 -0
- package/release-manager/dist/update/trust-metadata.d.ts +55 -0
- package/release-manager/dist/update/trust-metadata.d.ts.map +1 -0
- package/release-manager/dist/update/trust-metadata.js +170 -0
- package/release-manager/dist/update/trust-metadata.js.map +1 -0
- package/release-manager/dist/update/update-history.d.ts +46 -0
- package/release-manager/dist/update/update-history.d.ts.map +1 -0
- package/release-manager/dist/update/update-history.js +123 -0
- package/release-manager/dist/update/update-history.js.map +1 -0
- package/release-manager/dist/utils/config-file-io.d.ts +26 -0
- package/release-manager/dist/utils/config-file-io.d.ts.map +1 -0
- package/release-manager/dist/utils/config-file-io.js +128 -0
- package/release-manager/dist/utils/config-file-io.js.map +1 -0
- package/release-manager/dist/utils/env.d.ts +60 -0
- package/release-manager/dist/utils/env.d.ts.map +1 -0
- package/release-manager/dist/utils/env.js +308 -0
- package/release-manager/dist/utils/env.js.map +1 -0
- package/release-manager/dist/utils/logger.d.ts +15 -0
- package/release-manager/dist/utils/logger.d.ts.map +1 -0
- package/release-manager/dist/utils/logger.js +30 -0
- package/release-manager/dist/utils/logger.js.map +1 -0
- package/release-manager/dist/utils/workspace-divergence.d.ts +36 -0
- package/release-manager/dist/utils/workspace-divergence.d.ts.map +1 -0
- package/release-manager/dist/utils/workspace-divergence.js +87 -0
- package/release-manager/dist/utils/workspace-divergence.js.map +1 -0
- package/release-manager/package.json +79 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex rollout transcript incremental decoder (Codex Governance Closure
|
|
3
|
+
* Slice A; SPEC rev 2 §9, G1 probe report §6).
|
|
4
|
+
*
|
|
5
|
+
* Decodes a bounded byte window from the durable checkpoint offset and
|
|
6
|
+
* projects ONLY governance-relevant visible facts (SPEC §12 privacy
|
|
7
|
+
* boundary): genuine visible user messages (`content_item_kinds[0] ===
|
|
8
|
+
* "user.text"` — host-injected context arrives as user-role records with
|
|
9
|
+
* other kinds), visible assistant commentary/final messages, and tool
|
|
10
|
+
* execution facts anchored on the `event_msg item_completed
|
|
11
|
+
* CommandExecution` record whose `item.id` IS the hook `tool_use_id` id
|
|
12
|
+
* space (the G1-verified bridge; the model-level `call_id` is a different
|
|
13
|
+
* space). Hidden reasoning, system/developer prompts, world_state,
|
|
14
|
+
* host-injected context, and unknown record bodies are identified by shape
|
|
15
|
+
* and dropped before persistence — never projected, logged, or emitted.
|
|
16
|
+
*
|
|
17
|
+
* Failure classes are distinct (SPEC §14): an incomplete final line (no
|
|
18
|
+
* trailing newline at EOF) is a transient append/flush boundary — the
|
|
19
|
+
* checkpoint does not advance past it and the next ingestion retries; a
|
|
20
|
+
* complete line that fails JSON parsing or envelope validation is a stable
|
|
21
|
+
* malformed record — decoding stops at it without advancing, with a
|
|
22
|
+
* structured reason. Unknown-but-well-formed record types are skipped with
|
|
23
|
+
* a bounded warning and DO advance.
|
|
24
|
+
*/
|
|
25
|
+
import fs from 'node:fs';
|
|
26
|
+
export const CODEX_INGESTION_MAX_BATCH_BYTES = 1024 * 1024;
|
|
27
|
+
export const CODEX_INGESTION_MAX_BATCH_RECORDS = 256;
|
|
28
|
+
/** Thrown by the port when the opened object is not the validated one (replacement/TOCTOU). */
|
|
29
|
+
export class TranscriptReplacedError extends Error {
|
|
30
|
+
constructor() {
|
|
31
|
+
super('transcript_replaced_after_validation');
|
|
32
|
+
this.name = 'TranscriptReplacedError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function identityMatches(opened, expected) {
|
|
36
|
+
// Size+mtime fingerprint must ALWAYS hold. When the platform reports a
|
|
37
|
+
// Number-safe inode on both sides, the inode must match too (it is what
|
|
38
|
+
// catches delete+recreate replacements whose inode got reused on Linux CI
|
|
39
|
+
// with a same-ms write; a same-size same-ms recreate remains outside the
|
|
40
|
+
// threat model for an append-only host-owned file). On platforms whose
|
|
41
|
+
// ino exceeds Number.MAX_SAFE_INTEGER (Windows file IDs), the inode is not
|
|
42
|
+
// reliably comparable and the fingerprint alone decides. A false refusal
|
|
43
|
+
// is the safe direction: the hook degrades observably and the next Stop
|
|
44
|
+
// revalidates and retries.
|
|
45
|
+
if (opened.size !== expected.size || opened.mtimeMs !== expected.mtimeMs)
|
|
46
|
+
return false;
|
|
47
|
+
const comparableIno = expected.ino !== 0 && expected.dev !== 0
|
|
48
|
+
&& opened.ino !== 0 && Number.isSafeInteger(expected.ino) && Number.isSafeInteger(opened.ino);
|
|
49
|
+
if (comparableIno)
|
|
50
|
+
return opened.ino === expected.ino && opened.dev === expected.dev;
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
export function createNodeTranscriptPort() {
|
|
54
|
+
return {
|
|
55
|
+
read({ canonicalPath, offset, maxBytes, expectedIdentity }) {
|
|
56
|
+
const fd = fs.openSync(canonicalPath, 'r');
|
|
57
|
+
try {
|
|
58
|
+
// Post-open revalidation (G1 §7 / SPEC §9): fstat the OPEN OBJECT —
|
|
59
|
+
// not the path — and re-prove it is the regular file the validator
|
|
60
|
+
// approved, still at the expected identity. A path swapped between
|
|
61
|
+
// validation and open (replacement/symlink TOCTOU) is refused before
|
|
62
|
+
// a single byte is read.
|
|
63
|
+
const stats = fs.fstatSync(fd);
|
|
64
|
+
if (!stats.isFile())
|
|
65
|
+
throw new Error('transcript_is_not_regular_file');
|
|
66
|
+
if (expectedIdentity !== undefined && !identityMatches(stats, expectedIdentity)) {
|
|
67
|
+
throw new TranscriptReplacedError();
|
|
68
|
+
}
|
|
69
|
+
const length = Math.max(0, Math.min(maxBytes, stats.size - offset));
|
|
70
|
+
if (length === 0)
|
|
71
|
+
return { bytes: Buffer.alloc(0), fileSize: stats.size };
|
|
72
|
+
const buffer = Buffer.alloc(length);
|
|
73
|
+
const bytesRead = fs.readSync(fd, buffer, 0, length, offset);
|
|
74
|
+
// Post-read byte bound re-verification (EP-08): never trust the
|
|
75
|
+
// pre-read stat for the enforced cap.
|
|
76
|
+
const actual = bytesRead < 0 ? 0 : bytesRead;
|
|
77
|
+
if (actual > maxBytes)
|
|
78
|
+
throw new Error('transcript_read_exceeded_bound');
|
|
79
|
+
return { bytes: buffer.subarray(0, actual), fileSize: stats.size };
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
fs.closeSync(fd);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function isRecord(value) {
|
|
88
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
89
|
+
}
|
|
90
|
+
function own(value, key) {
|
|
91
|
+
return Object.hasOwn(value, key) ? Object.getOwnPropertyDescriptor(value, key)?.value : undefined;
|
|
92
|
+
}
|
|
93
|
+
function metadataTurnId(payload) {
|
|
94
|
+
const passthrough = own(payload, 'internal_chat_message_metadata_passthrough');
|
|
95
|
+
if (!isRecord(passthrough))
|
|
96
|
+
return null;
|
|
97
|
+
const turnId = own(passthrough, 'turn_id');
|
|
98
|
+
return typeof turnId === 'string' && turnId.length > 0 ? turnId : null;
|
|
99
|
+
}
|
|
100
|
+
function contentItemKinds(payload) {
|
|
101
|
+
const passthrough = own(payload, 'internal_chat_message_metadata_passthrough');
|
|
102
|
+
if (!isRecord(passthrough))
|
|
103
|
+
return [];
|
|
104
|
+
const kinds = own(passthrough, 'content_item_kinds');
|
|
105
|
+
if (!Array.isArray(kinds))
|
|
106
|
+
return [];
|
|
107
|
+
return kinds.filter((kind) => typeof kind === 'string');
|
|
108
|
+
}
|
|
109
|
+
function joinContentText(payload, type) {
|
|
110
|
+
const content = own(payload, 'content');
|
|
111
|
+
if (!Array.isArray(content))
|
|
112
|
+
return null;
|
|
113
|
+
const parts = [];
|
|
114
|
+
for (const item of content) {
|
|
115
|
+
if (!isRecord(item))
|
|
116
|
+
continue;
|
|
117
|
+
if (own(item, 'type') === type && typeof own(item, 'text') === 'string')
|
|
118
|
+
parts.push(own(item, 'text'));
|
|
119
|
+
}
|
|
120
|
+
return parts.length > 0 ? parts.join('') : null;
|
|
121
|
+
}
|
|
122
|
+
function warnBounded(context, warning) {
|
|
123
|
+
if (context.warnings.length < 16)
|
|
124
|
+
context.warnings.push(warning);
|
|
125
|
+
}
|
|
126
|
+
function applySessionMeta(context, payload) {
|
|
127
|
+
const sessionId = own(payload, 'session_id');
|
|
128
|
+
if (typeof sessionId === 'string' && sessionId.length > 0 && context.meta.rootSessionId === null) {
|
|
129
|
+
// Root-session lineage only. For subagent rollouts this value is the
|
|
130
|
+
// PARENT thread id (G1 §4 collision trap) — rollout identity comes from
|
|
131
|
+
// the file uuid, never from session_meta.session_id.
|
|
132
|
+
context.meta.rootSessionId = sessionId;
|
|
133
|
+
}
|
|
134
|
+
const cliVersion = own(payload, 'cli_version');
|
|
135
|
+
if (typeof cliVersion === 'string' && cliVersion.length > 0 && context.meta.cliVersion === null) {
|
|
136
|
+
context.meta.cliVersion = cliVersion;
|
|
137
|
+
}
|
|
138
|
+
const source = own(payload, 'source');
|
|
139
|
+
const spawn = isRecord(source) ? own(source, 'subagent') : undefined;
|
|
140
|
+
const threadSpawn = isRecord(spawn) ? own(spawn, 'thread_spawn') : undefined;
|
|
141
|
+
if (isRecord(threadSpawn)) {
|
|
142
|
+
const parentThreadId = own(threadSpawn, 'parent_thread_id');
|
|
143
|
+
if (typeof parentThreadId === 'string' && parentThreadId.length > 0)
|
|
144
|
+
context.meta.parentRolloutIdentity = parentThreadId;
|
|
145
|
+
const nickname = own(threadSpawn, 'agent_nickname');
|
|
146
|
+
const agentPath = own(threadSpawn, 'agent_path');
|
|
147
|
+
const identity = typeof nickname === 'string' && nickname.length > 0 ? nickname : typeof agentPath === 'string' && agentPath.length > 0 ? agentPath : null;
|
|
148
|
+
if (identity !== null)
|
|
149
|
+
context.meta.agentIdentity = identity;
|
|
150
|
+
const depth = own(threadSpawn, 'depth');
|
|
151
|
+
if (typeof depth === 'number' && Number.isInteger(depth) && depth >= 0)
|
|
152
|
+
context.meta.agentDepth = depth;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
const forkedFrom = own(payload, 'forked_from_id');
|
|
156
|
+
if (typeof forkedFrom === 'string' && forkedFrom.length > 0 && context.meta.parentRolloutIdentity === null) {
|
|
157
|
+
context.meta.parentRolloutIdentity = forkedFrom;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function joinItemText(item) {
|
|
162
|
+
const content = own(item, 'content');
|
|
163
|
+
if (!Array.isArray(content))
|
|
164
|
+
return null;
|
|
165
|
+
const parts = [];
|
|
166
|
+
for (const entry of content) {
|
|
167
|
+
if (!isRecord(entry))
|
|
168
|
+
continue;
|
|
169
|
+
// UserMessage items use "text", AgentMessage items use "Text".
|
|
170
|
+
const type = own(entry, 'type');
|
|
171
|
+
if ((type === 'text' || type === 'Text') && typeof own(entry, 'text') === 'string') {
|
|
172
|
+
parts.push(own(entry, 'text'));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return parts.length > 0 ? parts.join('') : null;
|
|
176
|
+
}
|
|
177
|
+
function observationBase(context, turnId) {
|
|
178
|
+
return {
|
|
179
|
+
hostKind: 'codex',
|
|
180
|
+
rolloutIdentity: context.rolloutIdentity,
|
|
181
|
+
rootSessionId: context.meta.rootSessionId ?? context.fallbackRootSessionId ?? turnId,
|
|
182
|
+
hostTurnId: turnId,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function projectRecord(context, record) {
|
|
186
|
+
const { type, payload, ordinal, recordByteStart, timestamp } = record;
|
|
187
|
+
const observedAt = timestamp ?? context.nowIso;
|
|
188
|
+
if (type === 'session_meta') {
|
|
189
|
+
applySessionMeta(context, payload);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (type === 'compacted') {
|
|
193
|
+
// Marker: replacement_history becomes the logical history going forward;
|
|
194
|
+
// replaced records must not be re-imported as new turns (G1 §6). The
|
|
195
|
+
// store tombstones prior unpromoted observations at ingest time.
|
|
196
|
+
context.compactionTimestamp = observedAt;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (type === 'world_state' || type === 'inter_agent_communication_metadata') {
|
|
200
|
+
// Environment/host snapshots — identified and dropped (privacy boundary).
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (type === 'turn_context') {
|
|
204
|
+
// Per-turn host configuration (sandbox/permissions/model) — known type,
|
|
205
|
+
// no governance projection, no warning.
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (type === 'response_item') {
|
|
209
|
+
const payloadType = own(payload, 'type');
|
|
210
|
+
if (payloadType === 'reasoning')
|
|
211
|
+
return; // hidden/encrypted reasoning — never projected
|
|
212
|
+
if (payloadType === 'custom_tool_call' || payloadType === 'function_call') {
|
|
213
|
+
const turnId = metadataTurnId(payload);
|
|
214
|
+
const callId = own(payload, 'call_id');
|
|
215
|
+
const name = own(payload, 'name');
|
|
216
|
+
if (turnId !== null) {
|
|
217
|
+
const queue = context.modelCallsByTurn.get(turnId) ?? [];
|
|
218
|
+
queue.push({ callId: typeof callId === 'string' ? callId : null, name: typeof name === 'string' ? name : null });
|
|
219
|
+
context.modelCallsByTurn.set(turnId, queue);
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (payloadType === 'message' && own(payload, 'role') === 'user') {
|
|
224
|
+
// 0.150.1 marks genuine visible user input with content_item_kinds[0]
|
|
225
|
+
// === "user.text"; host-injected context carries other kinds. 0.148.0
|
|
226
|
+
// does not emit content_item_kinds at all, so this channel is used ONLY
|
|
227
|
+
// when the discriminator exists — the authoritative cross-version user
|
|
228
|
+
// channel is the item_completed UserMessage record below.
|
|
229
|
+
const kinds = contentItemKinds(payload);
|
|
230
|
+
if (kinds.length === 0 || kinds[0] !== 'user.text')
|
|
231
|
+
return;
|
|
232
|
+
const turnId = metadataTurnId(payload);
|
|
233
|
+
if (turnId === null) {
|
|
234
|
+
warnBounded(context, `observation_skipped_missing_identity:${ordinal}`);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const text = joinContentText(payload, 'input_text');
|
|
238
|
+
context.observations.push({
|
|
239
|
+
...observationBase(context, turnId),
|
|
240
|
+
kind: 'user_turn',
|
|
241
|
+
logicalObservationKey: `codex|${context.rolloutIdentity}|${turnId}|user`,
|
|
242
|
+
transcriptRecordKey: `codex|${context.rolloutIdentity}|${ordinal}`,
|
|
243
|
+
recordOrdinal: ordinal,
|
|
244
|
+
recordByteStart,
|
|
245
|
+
visibleText: text ?? undefined,
|
|
246
|
+
source: 'transcript',
|
|
247
|
+
completeness: text !== null ? 'complete' : 'partial',
|
|
248
|
+
observedAt,
|
|
249
|
+
});
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
// Assistant/developer response_item messages are not projected here: the
|
|
253
|
+
// item_completed AgentMessage channel is the cross-version visible
|
|
254
|
+
// assistant source (0.148.0 lacks content_item_kinds discriminators).
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (type === 'event_msg') {
|
|
258
|
+
const payloadType = own(payload, 'type');
|
|
259
|
+
if (payloadType === 'item_completed') {
|
|
260
|
+
const item = own(payload, 'item');
|
|
261
|
+
if (!isRecord(item))
|
|
262
|
+
return;
|
|
263
|
+
const itemType = own(item, 'type');
|
|
264
|
+
const turnId = own(payload, 'turn_id');
|
|
265
|
+
if (itemType === 'UserMessage') {
|
|
266
|
+
// Only genuine visible user turns produce UserMessage completions —
|
|
267
|
+
// host-injected context never does (both supported versions).
|
|
268
|
+
if (typeof turnId !== 'string' || turnId.length === 0) {
|
|
269
|
+
warnBounded(context, `observation_skipped_missing_identity:${ordinal}`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const text = joinItemText(item);
|
|
273
|
+
context.observations.push({
|
|
274
|
+
...observationBase(context, turnId),
|
|
275
|
+
kind: 'user_turn',
|
|
276
|
+
logicalObservationKey: `codex|${context.rolloutIdentity}|${turnId}|user`,
|
|
277
|
+
transcriptRecordKey: `codex|${context.rolloutIdentity}|${ordinal}`,
|
|
278
|
+
recordOrdinal: ordinal,
|
|
279
|
+
recordByteStart,
|
|
280
|
+
visibleText: text ?? undefined,
|
|
281
|
+
source: 'transcript',
|
|
282
|
+
completeness: text !== null ? 'complete' : 'partial',
|
|
283
|
+
observedAt,
|
|
284
|
+
});
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (itemType === 'AgentMessage') {
|
|
288
|
+
if (typeof turnId !== 'string' || turnId.length === 0) {
|
|
289
|
+
warnBounded(context, `observation_skipped_missing_identity:${ordinal}`);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const itemId = own(item, 'id');
|
|
293
|
+
if (typeof itemId !== 'string' || itemId.length === 0) {
|
|
294
|
+
warnBounded(context, `observation_skipped_missing_identity:${ordinal}`);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const text = joinItemText(item);
|
|
298
|
+
// AgentMessage phase sits on the item (fixture-verified; keep the
|
|
299
|
+
// payload sibling as a tolerated alternative).
|
|
300
|
+
const phase = own(item, 'phase') ?? own(payload, 'phase');
|
|
301
|
+
context.observations.push({
|
|
302
|
+
...observationBase(context, turnId),
|
|
303
|
+
kind: 'assistant_turn',
|
|
304
|
+
logicalObservationKey: `codex|${context.rolloutIdentity}|${turnId}|${itemId}`,
|
|
305
|
+
transcriptRecordKey: `codex|${context.rolloutIdentity}|${ordinal}`,
|
|
306
|
+
recordOrdinal: ordinal,
|
|
307
|
+
recordByteStart,
|
|
308
|
+
assistantItemId: itemId,
|
|
309
|
+
phase: typeof phase === 'string' ? phase : undefined,
|
|
310
|
+
visibleText: text ?? undefined,
|
|
311
|
+
source: 'transcript',
|
|
312
|
+
completeness: text !== null ? 'complete' : 'partial',
|
|
313
|
+
observedAt,
|
|
314
|
+
});
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (itemType === 'CommandExecution') {
|
|
318
|
+
const toolUseId = own(item, 'id');
|
|
319
|
+
if (typeof toolUseId !== 'string' || toolUseId.length === 0 || typeof turnId !== 'string' || turnId.length === 0) {
|
|
320
|
+
warnBounded(context, `observation_skipped_missing_identity:${ordinal}`);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
// FIFO-pair with the model-level call in the same turn for the tool
|
|
324
|
+
// name / call_id enrichment (execution order; G1 bridge fixture).
|
|
325
|
+
const queue = context.modelCallsByTurn.get(turnId);
|
|
326
|
+
const paired = queue !== undefined && queue.length > 0 ? queue.shift() : undefined;
|
|
327
|
+
if (queue !== undefined && queue.length === 0)
|
|
328
|
+
context.modelCallsByTurn.delete(turnId);
|
|
329
|
+
const exitCode = own(item, 'exit_code');
|
|
330
|
+
const command = own(item, 'command');
|
|
331
|
+
const stdout = own(item, 'stdout');
|
|
332
|
+
const stderr = own(item, 'stderr');
|
|
333
|
+
context.observations.push({
|
|
334
|
+
...observationBase(context, turnId),
|
|
335
|
+
kind: 'tool_call',
|
|
336
|
+
logicalObservationKey: `codex|${context.rolloutIdentity}|${toolUseId}`,
|
|
337
|
+
transcriptRecordKey: `codex|${context.rolloutIdentity}|${ordinal}`,
|
|
338
|
+
recordOrdinal: ordinal,
|
|
339
|
+
recordByteStart,
|
|
340
|
+
toolUseId,
|
|
341
|
+
transcriptToolCallId: paired?.callId ?? undefined,
|
|
342
|
+
toolFacts: {
|
|
343
|
+
toolName: paired?.name ?? null,
|
|
344
|
+
exitCode: typeof exitCode === 'number' ? exitCode : null,
|
|
345
|
+
command: Array.isArray(command) ? command.filter((part) => typeof part === 'string').slice(0, 8) : null,
|
|
346
|
+
stdout: typeof stdout === 'string' ? stdout : null,
|
|
347
|
+
stderr: typeof stderr === 'string' ? stderr : null,
|
|
348
|
+
},
|
|
349
|
+
source: 'transcript',
|
|
350
|
+
completeness: 'complete',
|
|
351
|
+
observedAt,
|
|
352
|
+
});
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (payloadType === 'thread_rolled_back') {
|
|
358
|
+
// Documented rule (G1 runtime contract, thread_rollout_truncation.rs):
|
|
359
|
+
// rollback appends a marker carrying num_turns; physical records remain
|
|
360
|
+
// while effective logical history is truncated.
|
|
361
|
+
const numTurns = own(payload, 'num_turns');
|
|
362
|
+
if (typeof numTurns === 'number' && Number.isInteger(numTurns) && numTurns > 0) {
|
|
363
|
+
context.rollbackTurns.push(numTurns);
|
|
364
|
+
}
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
// task_started / turn_context / token_count / task_complete /
|
|
368
|
+
// thread_settings_applied: known turn bookkeeping — no projection.
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
warnBounded(context, `unknown_record_type_skipped:${type}`);
|
|
372
|
+
}
|
|
373
|
+
function decodeLine(lineBytes) {
|
|
374
|
+
const text = lineBytes.toString('utf8');
|
|
375
|
+
let parsed;
|
|
376
|
+
try {
|
|
377
|
+
parsed = JSON.parse(text);
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
return { record: null, type: null, ordinal: null, timestamp: null, payload: null };
|
|
381
|
+
}
|
|
382
|
+
if (!isRecord(parsed))
|
|
383
|
+
return { record: null, type: null, ordinal: null, timestamp: null, payload: null };
|
|
384
|
+
const type = own(parsed, 'type');
|
|
385
|
+
const ordinal = own(parsed, 'ordinal');
|
|
386
|
+
const timestamp = own(parsed, 'timestamp');
|
|
387
|
+
const payload = own(parsed, 'payload');
|
|
388
|
+
const validEnvelope = typeof type === 'string' && type.length > 0
|
|
389
|
+
&& typeof ordinal === 'number' && Number.isInteger(ordinal)
|
|
390
|
+
&& (payload === undefined || isRecord(payload));
|
|
391
|
+
if (!validEnvelope) {
|
|
392
|
+
return { record: parsed, type: null, ordinal: typeof ordinal === 'number' && Number.isInteger(ordinal) ? ordinal : null, timestamp: null, payload: null };
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
record: parsed,
|
|
396
|
+
type,
|
|
397
|
+
ordinal,
|
|
398
|
+
timestamp: typeof timestamp === 'string' ? timestamp : null,
|
|
399
|
+
payload: payload === undefined ? {} : payload,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function finish(context, nextByteOffset, lastOrdinal) {
|
|
403
|
+
return {
|
|
404
|
+
observations: context.observations,
|
|
405
|
+
rolloutMeta: context.meta,
|
|
406
|
+
compactionTimestamp: context.compactionTimestamp,
|
|
407
|
+
rollbackTurns: context.rollbackTurns,
|
|
408
|
+
nextByteOffset,
|
|
409
|
+
lastOrdinal,
|
|
410
|
+
warnings: context.warnings,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
export function decodeTranscriptWindow(input) {
|
|
414
|
+
const context = {
|
|
415
|
+
rolloutIdentity: input.rolloutIdentity,
|
|
416
|
+
fallbackRootSessionId: input.fallbackRootSessionId,
|
|
417
|
+
nowIso: input.nowIso,
|
|
418
|
+
observations: [],
|
|
419
|
+
meta: { rootSessionId: null, cliVersion: null, parentRolloutIdentity: null, agentIdentity: null, agentDepth: null },
|
|
420
|
+
modelCallsByTurn: new Map(),
|
|
421
|
+
compactionTimestamp: null,
|
|
422
|
+
rollbackTurns: [],
|
|
423
|
+
warnings: [],
|
|
424
|
+
};
|
|
425
|
+
const { bytes } = input;
|
|
426
|
+
if (bytes.length === 0) {
|
|
427
|
+
return { ...finish(context, input.fileOffset, -1), stop: { kind: 'eof' } };
|
|
428
|
+
}
|
|
429
|
+
// UTF-8 multi-byte sequences never contain 0x0A, so splitting the byte
|
|
430
|
+
// window at newline bytes is safe and keeps the checkpoint byte-exact.
|
|
431
|
+
const lastNewline = bytes.lastIndexOf(0x0a);
|
|
432
|
+
const completeEnd = lastNewline + 1; // exclusive byte end of the newline-terminated region
|
|
433
|
+
const tailBytes = bytes.subarray(completeEnd); // bytes after the final newline (no terminator)
|
|
434
|
+
let cursor = input.fileOffset;
|
|
435
|
+
let lastOrdinal = -1;
|
|
436
|
+
let recordsConsumed = 0;
|
|
437
|
+
let lineStart = 0;
|
|
438
|
+
while (lineStart < completeEnd) {
|
|
439
|
+
const lineEnd = bytes.indexOf(0x0a, lineStart);
|
|
440
|
+
if (lineEnd === -1 || lineEnd >= completeEnd)
|
|
441
|
+
break;
|
|
442
|
+
const lineBytes = bytes.subarray(lineStart, lineEnd);
|
|
443
|
+
const recordByteStart = cursor;
|
|
444
|
+
cursor = input.fileOffset + lineEnd + 1;
|
|
445
|
+
lineStart = lineEnd + 1;
|
|
446
|
+
if (lineBytes.length === 0 || lineBytes.toString('utf8').trim().length === 0)
|
|
447
|
+
continue;
|
|
448
|
+
if (recordsConsumed >= CODEX_INGESTION_MAX_BATCH_RECORDS) {
|
|
449
|
+
return { ...finish(context, recordByteStart, lastOrdinal), stop: { kind: 'byte_bound' } };
|
|
450
|
+
}
|
|
451
|
+
const line = decodeLine(lineBytes);
|
|
452
|
+
if (line.record === null || line.type === null) {
|
|
453
|
+
// A newline-terminated line that does not parse (or fails the record
|
|
454
|
+
// envelope) is a stable malformed record: stop here without advancing
|
|
455
|
+
// past it (SPEC §14.2).
|
|
456
|
+
return { ...finish(context, recordByteStart, lastOrdinal), stop: { kind: 'malformed', ordinal: line.ordinal } };
|
|
457
|
+
}
|
|
458
|
+
recordsConsumed += 1;
|
|
459
|
+
lastOrdinal = line.ordinal;
|
|
460
|
+
projectRecord(context, { type: line.type, payload: line.payload ?? {}, ordinal: line.ordinal, recordByteStart, timestamp: line.timestamp });
|
|
461
|
+
}
|
|
462
|
+
if (tailBytes.length > 0) {
|
|
463
|
+
const tailStart = cursor; // byte offset where the unterminated tail begins
|
|
464
|
+
if (input.byteBoundReached) {
|
|
465
|
+
// The window ended mid-line because of the byte bound — the line is not
|
|
466
|
+
// known-incomplete; leave it wholly unread and report bounded lag.
|
|
467
|
+
if (lastNewline === -1) {
|
|
468
|
+
return { ...finish(context, input.fileOffset, lastOrdinal), stop: { kind: 'oversized_record' } };
|
|
469
|
+
}
|
|
470
|
+
return { ...finish(context, tailStart, lastOrdinal), stop: { kind: 'byte_bound' } };
|
|
471
|
+
}
|
|
472
|
+
const line = decodeLine(tailBytes);
|
|
473
|
+
if (line.record !== null && line.type !== null) {
|
|
474
|
+
// A parseable final line without a trailing newline is a complete
|
|
475
|
+
// record; consume it and advance the checkpoint to EOF.
|
|
476
|
+
lastOrdinal = line.ordinal;
|
|
477
|
+
projectRecord(context, { type: line.type, payload: line.payload ?? {}, ordinal: line.ordinal, recordByteStart: tailStart, timestamp: line.timestamp });
|
|
478
|
+
return { ...finish(context, tailStart + tailBytes.length, lastOrdinal), stop: { kind: 'eof' } };
|
|
479
|
+
}
|
|
480
|
+
// Incomplete final line: transient append/flush boundary — do not
|
|
481
|
+
// advance past the previous record; the next ingestion retries (SPEC §14.1).
|
|
482
|
+
return { ...finish(context, tailStart, lastOrdinal), stop: { kind: 'incomplete_tail' } };
|
|
483
|
+
}
|
|
484
|
+
return { ...finish(context, cursor, lastOrdinal), stop: input.byteBoundReached ? { kind: 'byte_bound' } : { kind: 'eof' } };
|
|
485
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type CodexTranscriptLookup = {
|
|
2
|
+
ok: true;
|
|
3
|
+
transcriptPath: string;
|
|
4
|
+
} | {
|
|
5
|
+
ok: false;
|
|
6
|
+
reason: 'catch_up_rollout_identity_invalid' | 'catch_up_sessions_root_missing' | 'catch_up_transcript_missing' | 'catch_up_transcript_ambiguous' | 'catch_up_lookup_exhausted';
|
|
7
|
+
nextAction: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Resolve one previously-authenticated rollout identity to its transcript
|
|
11
|
+
* path by exact-uuid filename match under `<codexHome>/sessions`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function locateCodexTranscriptByRolloutIdentity(codexHome: string, rolloutIdentity: string): CodexTranscriptLookup;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex transcript locator for catch-up (PRI-624 Slice C).
|
|
3
|
+
*
|
|
4
|
+
* The durable checkpoint stores only the rollout uuid (SPEC §18 scenario 9
|
|
5
|
+
* forbids raw paths in the DB), so catch-up must resolve a checkpointed
|
|
6
|
+
* rollout back to its transcript file. This is NOT session discovery: the
|
|
7
|
+
* lookup searches for the EXACT rollout uuid of a rollout the authenticated
|
|
8
|
+
* Workspace hook previously delivered (only hooks write checkpoints). It
|
|
9
|
+
* never guesses a "latest session", never returns a partial match, and
|
|
10
|
+
* refuses ambiguities (ADR-0020 §11.2 / SPEC §9).
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { parseRolloutFileName } from './transcript-path.js';
|
|
15
|
+
/** Bounded walk: hard cap on visited directory entries so a pathological sessions tree cannot stall the worker. */
|
|
16
|
+
const MAX_LOOKUP_ENTRIES = 5000;
|
|
17
|
+
const ROLLOUT_IDENTITY_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
18
|
+
/**
|
|
19
|
+
* Resolve one previously-authenticated rollout identity to its transcript
|
|
20
|
+
* path by exact-uuid filename match under `<codexHome>/sessions`.
|
|
21
|
+
*/
|
|
22
|
+
export function locateCodexTranscriptByRolloutIdentity(codexHome, rolloutIdentity) {
|
|
23
|
+
if (!ROLLOUT_IDENTITY_PATTERN.test(rolloutIdentity)) {
|
|
24
|
+
return { ok: false, reason: 'catch_up_rollout_identity_invalid', nextAction: 'the checkpointed rollout identity is not a rollout uuid; inspect the workspace trajectory database.' };
|
|
25
|
+
}
|
|
26
|
+
const sessionsRoot = path.join(codexHome, 'sessions');
|
|
27
|
+
let rootStats;
|
|
28
|
+
try {
|
|
29
|
+
rootStats = fs.statSync(sessionsRoot);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return { ok: false, reason: 'catch_up_sessions_root_missing', nextAction: 'the configured CODEX_HOME has no sessions root; verify the Codex home used by the hook and by catch-up matches.' };
|
|
33
|
+
}
|
|
34
|
+
if (!rootStats.isDirectory()) {
|
|
35
|
+
return { ok: false, reason: 'catch_up_sessions_root_missing', nextAction: 'the configured CODEX_HOME sessions path is not a directory; verify the Codex home configuration.' };
|
|
36
|
+
}
|
|
37
|
+
const matches = [];
|
|
38
|
+
let visited = 0;
|
|
39
|
+
const stack = [sessionsRoot];
|
|
40
|
+
while (stack.length > 0 && matches.length < 2) {
|
|
41
|
+
const dir = stack.pop();
|
|
42
|
+
if (dir === undefined)
|
|
43
|
+
break;
|
|
44
|
+
let entries;
|
|
45
|
+
try {
|
|
46
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
continue; // unreadable subtree — other subtrees may still hold the rollout
|
|
50
|
+
}
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
visited += 1;
|
|
53
|
+
if (visited > MAX_LOOKUP_ENTRIES) {
|
|
54
|
+
return { ok: false, reason: 'catch_up_lookup_exhausted', nextAction: 'the sessions tree exceeded the bounded catch-up lookup; keep CODEX_HOME/sessions pruned or catch up rollouts manually.' };
|
|
55
|
+
}
|
|
56
|
+
if (entry.isDirectory()) {
|
|
57
|
+
stack.push(path.join(dir, entry.name));
|
|
58
|
+
}
|
|
59
|
+
else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
|
|
60
|
+
if (parseRolloutFileName(entry.name) === rolloutIdentity) {
|
|
61
|
+
matches.push(path.join(dir, entry.name));
|
|
62
|
+
if (matches.length >= 2)
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (matches.length === 0) {
|
|
69
|
+
return { ok: false, reason: 'catch_up_transcript_missing', nextAction: 'the checkpointed rollout has no transcript under the Codex sessions root (rotated or cleaned by Codex); its committed observations remain, the pending lag cannot be recovered.' };
|
|
70
|
+
}
|
|
71
|
+
if (matches.length > 1) {
|
|
72
|
+
return { ok: false, reason: 'catch_up_transcript_ambiguous', nextAction: 'multiple transcripts match the rollout identity; refuse to guess — inspect the Codex sessions tree.' };
|
|
73
|
+
}
|
|
74
|
+
return { ok: true, transcriptPath: matches[0] };
|
|
75
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File identity captured at validation time. The open/read seam re-proves
|
|
3
|
+
* the opened object still has this identity BEFORE any byte is read — the
|
|
4
|
+
* G1 §7 / SPEC §9 post-open revalidation that closes the validate→open
|
|
5
|
+
* TOCTOU window (path replacement, symlink swap).
|
|
6
|
+
*/
|
|
7
|
+
export interface TranscriptFileIdentity {
|
|
8
|
+
readonly dev: number;
|
|
9
|
+
readonly ino: number;
|
|
10
|
+
readonly size: number;
|
|
11
|
+
readonly mtimeMs: number;
|
|
12
|
+
}
|
|
13
|
+
export type TranscriptPathValidation = {
|
|
14
|
+
ok: true;
|
|
15
|
+
canonicalPath: string;
|
|
16
|
+
rolloutIdentity: string;
|
|
17
|
+
identity: TranscriptFileIdentity;
|
|
18
|
+
} | {
|
|
19
|
+
ok: false;
|
|
20
|
+
reason: 'transcript_path_invalid' | 'transcript_path_outside_codex_home';
|
|
21
|
+
nextAction: string;
|
|
22
|
+
};
|
|
23
|
+
/** rollout-<timestamp>-<uuid>.jsonl — returns the rollout uuid, or null when the name is off-contract. */
|
|
24
|
+
export declare function parseRolloutFileName(fileName: string): string | null;
|
|
25
|
+
export declare function validateCodexTranscriptPath(transcriptPath: string, codexHome: string): TranscriptPathValidation;
|