@neta-art/cohub-cli 8.2.0 → 8.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/runtime.d.ts +0 -12
- package/dist/commands/runtime.js +129 -144
- package/dist/commands/search.js +3 -9
- package/dist/commands/spaces.js +5 -2
- package/dist/output.d.ts +6 -0
- package/dist/output.js +11 -0
- package/dist/runtime/codex-usage.d.ts +0 -2
- package/dist/runtime/codex-usage.js +0 -12
- package/dist/runtime/connection.d.ts +5 -5
- package/dist/runtime/connection.js +27 -16
- package/dist/runtime/harness.d.ts +8 -16
- package/dist/runtime/harness.js +1 -366
- package/dist/runtime/instance.d.ts +8 -1
- package/dist/runtime/instance.js +15 -8
- package/dist/runtime/launch.js +10 -5
- package/dist/runtime/native/adapters.d.ts +32 -0
- package/dist/runtime/native/adapters.js +79 -0
- package/dist/runtime/{native-archive.d.ts → native/archive.d.ts} +0 -3
- package/dist/runtime/{native-archive.js → native/archive.js} +2 -17
- package/dist/runtime/native/attach.d.ts +33 -0
- package/dist/runtime/native/attach.js +103 -0
- package/dist/runtime/native/codex-link.d.ts +40 -0
- package/dist/runtime/native/codex-link.js +249 -0
- package/dist/runtime/native/config.d.ts +16 -0
- package/dist/runtime/native/config.js +24 -0
- package/dist/runtime/native/daemon.d.ts +88 -0
- package/dist/runtime/native/daemon.js +367 -0
- package/dist/runtime/native/execution.d.ts +45 -0
- package/dist/runtime/native/execution.js +337 -0
- package/dist/runtime/native/identity.d.ts +9 -0
- package/dist/runtime/native/identity.js +15 -0
- package/dist/runtime/native/ingest.d.ts +241 -0
- package/dist/runtime/native/ingest.js +823 -0
- package/dist/runtime/native/install.d.ts +9 -0
- package/dist/runtime/native/install.js +105 -0
- package/dist/runtime/native/pi-extension.d.ts +35 -0
- package/dist/runtime/native/pi-extension.js +160 -0
- package/dist/runtime/native/pi-link.d.ts +67 -0
- package/dist/runtime/native/pi-link.js +238 -0
- package/dist/runtime/native/results.d.ts +41 -0
- package/dist/runtime/native/results.js +128 -0
- package/dist/runtime/native/sessions.d.ts +45 -0
- package/dist/runtime/native/sessions.js +92 -0
- package/dist/runtime/{native-transcript.d.ts → native/transcript.d.ts} +5 -5
- package/dist/runtime/{native-transcript.js → native/transcript.js} +37 -11
- package/dist/runtime/native/translate.d.ts +44 -0
- package/dist/runtime/native/translate.js +263 -0
- package/dist/runtime/native/transport.d.ts +7 -0
- package/dist/runtime/native/transport.js +16 -0
- package/dist/runtime/native/version.d.ts +5 -0
- package/dist/runtime/native/version.js +18 -0
- package/dist/runtime/presentation.d.ts +7 -10
- package/dist/runtime/presentation.js +35 -10
- package/dist/runtime/projection-store.d.ts +6 -13
- package/dist/runtime/projection-store.js +11 -74
- package/dist/runtime/supervisor.d.ts +2 -0
- package/dist/runtime/supervisor.js +36 -110
- package/dist/runtime/turn-projection.d.ts +1 -3
- package/dist/runtime/turn-projection.js +3 -10
- package/package.json +4 -5
- package/dist/runtime/native-attach.d.ts +0 -28
- package/dist/runtime/native-attach.js +0 -47
- package/dist/runtime/native-codex-hook.d.ts +0 -1
- package/dist/runtime/native-codex-hook.js +0 -28
- package/dist/runtime/native-install.d.ts +0 -21
- package/dist/runtime/native-install.js +0 -130
- package/dist/runtime/native-ipc.d.ts +0 -37
- package/dist/runtime/native-ipc.js +0 -118
- package/dist/runtime/native-pi-extension.d.ts +0 -20
- package/dist/runtime/native-pi-extension.js +0 -47
- package/dist/runtime/native-sync-store.d.ts +0 -116
- package/dist/runtime/native-sync-store.js +0 -495
- package/dist/runtime/native-sync.d.ts +0 -65
- package/dist/runtime/native-sync.js +0 -326
- package/dist/runtime/session-store.d.ts +0 -84
- package/dist/runtime/session-store.js +0 -601
|
@@ -1,495 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { createReadStream } from "node:fs";
|
|
3
|
-
import { mkdir, readFile, readdir, stat, rm } from "node:fs/promises";
|
|
4
|
-
import { basename, join } from "node:path";
|
|
5
|
-
import { nativeTurnCompleteSchema, nativeTurnStartSchema, nativeTurnProgressSchema, harnessArchiveIndexSchema } from "@neta-art/cohub";
|
|
6
|
-
import { ArchiveCaptureIntegrityError, RuntimeArchiveStore, atomicRuntimeJson } from "./archive-store.js";
|
|
7
|
-
import { codexRolloutCachePath, codexRolloutIdOf, codexRolloutBytes, ensurePlainCodexRollout } from "./native-transcript.js";
|
|
8
|
-
import { findRuntimeNativeSession } from "./session-store.js";
|
|
9
|
-
import { withRuntimeSpaceBindingsLock } from "./space-binding.js";
|
|
10
|
-
const missing = (error) => error.code === "ENOENT";
|
|
11
|
-
const hash = (value) => createHash("sha256").update(value).digest("hex");
|
|
12
|
-
export const nativeIdentityHash = (identity) => hash(identity);
|
|
13
|
-
export function nativeStableId(value) {
|
|
14
|
-
const hex = hash(value);
|
|
15
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-5${hex.slice(13, 16)}-a${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
|
|
16
|
-
}
|
|
17
|
-
async function readJson(path) {
|
|
18
|
-
try {
|
|
19
|
-
return JSON.parse(await readFile(path, "utf8"));
|
|
20
|
-
}
|
|
21
|
-
catch (error) {
|
|
22
|
-
if (missing(error))
|
|
23
|
-
return null;
|
|
24
|
-
throw error;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
/** Whole-Turn archive checkpoints only; native offsets never become cloud fork anchors. */
|
|
28
|
-
function matchAnchor(turn, anchors) {
|
|
29
|
-
return anchors.filter((anchor) => anchor.sizeBytes >= turn.contentEndBytes && turn.boundaries[anchor.sizeBytes] === anchor.sha256);
|
|
30
|
-
}
|
|
31
|
-
/** Archive checkpoints of one managed session, optionally bounded by a byte prefix. */
|
|
32
|
-
async function collectArchiveAnchors(runtimeRoot, managed, throughBytes = Number.POSITIVE_INFINITY) {
|
|
33
|
-
const anchors = [];
|
|
34
|
-
const versions = join(runtimeRoot, "archives", "versions");
|
|
35
|
-
const names = await readdir(versions).catch((error) => { if (missing(error))
|
|
36
|
-
return []; throw error; });
|
|
37
|
-
for (const name of names) {
|
|
38
|
-
if (!name.endsWith(".json"))
|
|
39
|
-
continue;
|
|
40
|
-
const version = harnessArchiveIndexSchema.parse(await readJson(join(versions, name)));
|
|
41
|
-
if (version.sessionId === managed.sessionId && version.harness === managed.harness && version.nativeSessionId === managed.nativeSessionId && version.sizeBytes <= throughBytes) {
|
|
42
|
-
anchors.push({ turnId: version.turnId, sizeBytes: version.sizeBytes, sha256: version.sha256 });
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return anchors;
|
|
46
|
-
}
|
|
47
|
-
/** Turn receipts are append-only. Capture never waits for the network; network ACKs live in separate files. */
|
|
48
|
-
export class NativeSyncStore {
|
|
49
|
-
options;
|
|
50
|
-
root;
|
|
51
|
-
archives;
|
|
52
|
-
archiveFailure;
|
|
53
|
-
constructor(options) {
|
|
54
|
-
this.options = options;
|
|
55
|
-
const source = options.instanceKey ? JSON.stringify([options.nativeSessionId, options.instanceKey]) : options.nativeSessionId;
|
|
56
|
-
this.root = join(options.runtimeRoot, "native", nativeIdentityHash(options.identity), `${options.harness}-${hash(source)}`);
|
|
57
|
-
const transport = options.transport;
|
|
58
|
-
this.archives = new RuntimeArchiveStore(join(this.root, "archives"), transport ? {
|
|
59
|
-
prepareRuntimeArchive: async (index, request) => transport.prepareRuntimeArchive(await this.cloudArchive(index), request),
|
|
60
|
-
commitRuntimeArchive: async (index, request) => transport.commitRuntimeArchive(await this.cloudArchive(index), request),
|
|
61
|
-
getRuntimeArchive: (...args) => transport.getRuntimeArchive(...args),
|
|
62
|
-
fetchObject: transport.fetchObject,
|
|
63
|
-
} : undefined);
|
|
64
|
-
this.archives.setErrorReporter((error) => { this.archiveFailure = error; });
|
|
65
|
-
}
|
|
66
|
-
bindingPath() { return join(this.root, "binding.json"); }
|
|
67
|
-
turnId(key) {
|
|
68
|
-
return nativeStableId(JSON.stringify([this.options.identity, this.options.spaceId, this.options.harness, this.options.nativeSessionId, this.options.instanceKey ?? null, key]));
|
|
69
|
-
}
|
|
70
|
-
receiptPath(id) { return join(this.root, "turns", `${id}.json`); }
|
|
71
|
-
acknowledgementPath(id) { return join(this.root, "acknowledged", `${id}.json`); }
|
|
72
|
-
pendingPath(id) { return join(this.root, "pending", `${id}.json`); }
|
|
73
|
-
requestPath(id) { return join(this.root, "requests", `${id}.json`); }
|
|
74
|
-
retryPath(id) { return join(this.root, "retries", `${id}.json`); }
|
|
75
|
-
cloudBindingPath(id) { return join(this.root, "bindings", `${id}.json`); }
|
|
76
|
-
async binding() {
|
|
77
|
-
const value = await readJson(this.bindingPath());
|
|
78
|
-
if (value?.version !== 1 || value.identity !== this.options.identity || value.spaceId !== this.options.spaceId || value.nativeSessionId !== this.options.nativeSessionId || value.instanceKey !== this.options.instanceKey || value.harness !== this.options.harness)
|
|
79
|
-
throw new Error("Native binding mismatch");
|
|
80
|
-
return value;
|
|
81
|
-
}
|
|
82
|
-
async initialize(path, transcript, signal) {
|
|
83
|
-
const existing = await readJson(this.bindingPath());
|
|
84
|
-
if (existing) {
|
|
85
|
-
const binding = await this.binding();
|
|
86
|
-
// Codex rollover keeps the thread id but moves appends to a fresh rollout that references
|
|
87
|
-
// the old one as its history base. Follow the reference instead of failing — matched by
|
|
88
|
-
// rollout id, so archiving or compressing the ancestor still migrates cleanly. When the
|
|
89
|
-
// previous rollout is the leaf itself (the same file merely moved or compressed), the
|
|
90
|
-
// byte boundary stays valid: only the path changes.
|
|
91
|
-
const previousRolloutId = codexRolloutIdOf(basename(binding.path));
|
|
92
|
-
const lineageRolloutIds = transcript.lineageRolloutIds ?? [];
|
|
93
|
-
if (binding.path !== path && previousRolloutId && lineageRolloutIds.includes(previousRolloutId)) {
|
|
94
|
-
const sameRollout = lineageRolloutIds.at(-1) === previousRolloutId;
|
|
95
|
-
let moved = { ...binding, path };
|
|
96
|
-
if (!sameRollout) {
|
|
97
|
-
// Byte offsets from the old rollout no longer address the new one; the settled
|
|
98
|
-
// boundary moves into the ancestor record and the binding restarts at the leaf.
|
|
99
|
-
moved = { ...moved, throughBytes: 0, anchors: [] };
|
|
100
|
-
if (binding.throughBytes > 0)
|
|
101
|
-
moved.ancestor = { rolloutId: previousRolloutId, throughBytes: binding.throughBytes, anchors: binding.anchors };
|
|
102
|
-
}
|
|
103
|
-
await atomicRuntimeJson(join(this.options.runtimeRoot, "native-owners", `${hash(path)}.json`), { path, nativeSessionId: binding.nativeSessionId });
|
|
104
|
-
await atomicRuntimeJson(this.bindingPath(), moved);
|
|
105
|
-
return moved;
|
|
106
|
-
}
|
|
107
|
-
if (binding.path !== path)
|
|
108
|
-
throw new Error("Native path changed; original binding retained");
|
|
109
|
-
return binding;
|
|
110
|
-
}
|
|
111
|
-
const managed = await findRuntimeNativeSession(this.options.runtimeRoot, this.options.harness, transcript.nativeSessionId, path);
|
|
112
|
-
let throughBytes = 0;
|
|
113
|
-
const anchors = [];
|
|
114
|
-
if (managed) {
|
|
115
|
-
if (managed.pendingTurnId)
|
|
116
|
-
throw new Error("Reconcile the managed Turn before native continuation");
|
|
117
|
-
// A Codex rollover before the first native capture: the managed projection lives on an
|
|
118
|
-
// ancestor rollout now. Its byte boundary addresses the ancestor, so express it as the
|
|
119
|
-
// ancestor record instead of validating it against the new leaf.
|
|
120
|
-
const managedRolloutId = codexRolloutIdOf(basename(managed.path));
|
|
121
|
-
const lineageRolloutIds = transcript.lineageRolloutIds ?? [];
|
|
122
|
-
const managedInLineage = this.options.harness === "codex" && managedRolloutId != null && lineageRolloutIds.slice(0, -1).includes(managedRolloutId);
|
|
123
|
-
if (managedInLineage && managed.throughTurnId) {
|
|
124
|
-
anchors.push(...await collectArchiveAnchors(this.options.runtimeRoot, managed));
|
|
125
|
-
// The managed projection is the whole ancestor rollout: verify its bytes still match the
|
|
126
|
-
// recorded checksum, measured on the logical JSONL (decompressed when the file is cold).
|
|
127
|
-
const ancestorIndex = lineageRolloutIds.indexOf(managedRolloutId);
|
|
128
|
-
const ancestorPath = transcript.lineagePaths?.[ancestorIndex] ?? managed.path;
|
|
129
|
-
const checksum = createHash("sha256");
|
|
130
|
-
let ancestorBytes = 0;
|
|
131
|
-
for await (const bytes of codexRolloutBytes(ancestorPath, null, signal)) {
|
|
132
|
-
checksum.update(bytes);
|
|
133
|
-
ancestorBytes += bytes.length;
|
|
134
|
-
}
|
|
135
|
-
if (!managed.checksum)
|
|
136
|
-
throw new Error("Managed rollout has no checksum to validate its settled boundary");
|
|
137
|
-
if (checksum.digest("hex") !== managed.checksum)
|
|
138
|
-
throw new Error("Runtime history prefix changed");
|
|
139
|
-
// The verified whole-file boundary is a synthetic anchor, mirroring the regular first
|
|
140
|
-
// binding: trailing records after the last Turn (e.g. usage) make contentEndBytes
|
|
141
|
-
// undershoot the file size, and matchAnchor resolves through `boundaries`.
|
|
142
|
-
anchors.push({ turnId: managed.throughTurnId, sizeBytes: ancestorBytes, sha256: managed.checksum });
|
|
143
|
-
const binding = { version: 1, identity: this.options.identity, spaceId: this.options.spaceId, harness: this.options.harness,
|
|
144
|
-
nativeSessionId: transcript.nativeSessionId, instanceKey: this.options.instanceKey, path, originSessionId: nativeStableId(`${this.root}:archive`),
|
|
145
|
-
sessionId: managed.sessionId, throughTurnId: managed.throughTurnId, throughBytes: 0, anchors: [],
|
|
146
|
-
ancestor: { rolloutId: managedRolloutId, throughBytes: ancestorBytes, anchors } };
|
|
147
|
-
// Persist like any first binding: the owner marker keeps the managed Runtime out of this
|
|
148
|
-
// leaf file, and flush/status read binding.json before touching any receipt.
|
|
149
|
-
await atomicRuntimeJson(join(this.options.runtimeRoot, "native-owners", `${hash(path)}.json`), { path, nativeSessionId: binding.nativeSessionId });
|
|
150
|
-
await atomicRuntimeJson(this.bindingPath(), binding);
|
|
151
|
-
return binding;
|
|
152
|
-
}
|
|
153
|
-
if (managed.throughTurnId) {
|
|
154
|
-
const index = await readJson(join(this.options.runtimeRoot, "archives", "versions", `${managed.throughTurnId}.json`));
|
|
155
|
-
if (index) {
|
|
156
|
-
const parsed = harnessArchiveIndexSchema.parse(index);
|
|
157
|
-
const checksum = createHash("sha256");
|
|
158
|
-
for await (const bytes of createReadStream(path, { end: parsed.sizeBytes - 1 }))
|
|
159
|
-
checksum.update(bytes);
|
|
160
|
-
if (checksum.digest("hex") !== parsed.sha256)
|
|
161
|
-
throw new Error("Runtime history prefix changed");
|
|
162
|
-
throughBytes = parsed.sizeBytes;
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
const checksum = createHash("sha256");
|
|
166
|
-
for await (const bytes of createReadStream(path))
|
|
167
|
-
checksum.update(bytes);
|
|
168
|
-
if (checksum.digest("hex") !== managed.checksum)
|
|
169
|
-
throw new Error("Cannot identify the last complete Runtime Turn");
|
|
170
|
-
throughBytes = (await stat(path)).size;
|
|
171
|
-
anchors.push({ turnId: managed.throughTurnId, sizeBytes: throughBytes, sha256: managed.checksum });
|
|
172
|
-
}
|
|
173
|
-
anchors.push(...await collectArchiveAnchors(this.options.runtimeRoot, managed, throughBytes));
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
const binding = { version: 1, identity: this.options.identity, spaceId: this.options.spaceId, harness: this.options.harness,
|
|
177
|
-
nativeSessionId: transcript.nativeSessionId, instanceKey: this.options.instanceKey, path, originSessionId: nativeStableId(`${this.root}:archive`),
|
|
178
|
-
sessionId: managed?.sessionId ?? transcript.cloudSessionId ?? null, throughTurnId: managed?.throughTurnId ?? null, throughBytes, anchors };
|
|
179
|
-
// A managed Runtime must rebuild a separate projection rather than write into an interactive client's file.
|
|
180
|
-
await atomicRuntimeJson(join(this.options.runtimeRoot, "native-owners", `${hash(path)}.json`), { path, nativeSessionId: binding.nativeSessionId });
|
|
181
|
-
await atomicRuntimeJson(this.bindingPath(), binding);
|
|
182
|
-
return binding;
|
|
183
|
-
}
|
|
184
|
-
async capture(path, transcript, context = {}) {
|
|
185
|
-
const signal = context.signal;
|
|
186
|
-
if (transcript.nativeSessionId !== this.options.nativeSessionId)
|
|
187
|
-
throw new Error("Native session identity mismatch");
|
|
188
|
-
await withRuntimeSpaceBindingsLock(async () => {
|
|
189
|
-
const binding = await this.initialize(path, transcript, signal);
|
|
190
|
-
if (binding.throughBytes > 0) {
|
|
191
|
-
const anchor = binding.anchors.find((entry) => entry.sizeBytes === binding.throughBytes);
|
|
192
|
-
if (!anchor || transcript.prefixes.get(binding.throughBytes) !== anchor.sha256)
|
|
193
|
-
throw new Error("Runtime history prefix changed; original binding retained");
|
|
194
|
-
}
|
|
195
|
-
let parentKey = null;
|
|
196
|
-
let parentCloudTurnId = binding.throughTurnId;
|
|
197
|
-
let knownBoundary = binding.throughBytes === 0;
|
|
198
|
-
const ancestor = binding.ancestor;
|
|
199
|
-
const leafRolloutId = transcript.lineageRolloutIds?.at(-1);
|
|
200
|
-
for (const turn of transcript.turns) {
|
|
201
|
-
// Lineage Turns keep their own rollout's offsets; only Turns from the binding's rollout
|
|
202
|
-
// (the leaf, however it is now represented on disk) address the binding's byte boundary.
|
|
203
|
-
const inLeafRollout = !turn.rolloutId || turn.rolloutId === leafRolloutId;
|
|
204
|
-
if (inLeafRollout && turn.startBytes < binding.throughBytes) {
|
|
205
|
-
const matches = matchAnchor(turn, binding.anchors ?? []);
|
|
206
|
-
if (new Set(matches.map((anchor) => anchor.turnId)).size > 1)
|
|
207
|
-
throw new Error("Ambiguous Runtime Turn boundary");
|
|
208
|
-
parentCloudTurnId = matches[0]?.turnId ?? null;
|
|
209
|
-
knownBoundary = matches.length > 0;
|
|
210
|
-
parentKey = null;
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
if (turn.cloudTurnId) {
|
|
214
|
-
parentCloudTurnId = turn.cloudTurnId;
|
|
215
|
-
knownBoundary = binding.harness === "codex" ? turn.result !== null : !["toolUse", "pending"].includes(turn.messages.at(-1)?.stopReason ?? "pending");
|
|
216
|
-
parentKey = null;
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
const turnId = this.turnId(turn.key);
|
|
220
|
-
const old = await readJson(this.receiptPath(turnId));
|
|
221
|
-
if (old?.result) {
|
|
222
|
-
if (turn.contentEndBytes < (old.contentEndBytes ?? old.endBytes) || JSON.stringify(turn.userContent) !== JSON.stringify(old.userContent) || turn.result && JSON.stringify(nativeTurnCompleteSchema.parse(turn.result)) !== JSON.stringify(old.result)) {
|
|
223
|
-
throw new Error("Native branch is inside a settled Turn; only whole-Turn forks are supported");
|
|
224
|
-
}
|
|
225
|
-
// Receipts written before lineage support lack the merged order; backfill it from the immutable chain.
|
|
226
|
-
if (turn.sequence != null && old.sequence == null)
|
|
227
|
-
await atomicRuntimeJson(this.receiptPath(turnId), { ...old, sequence: turn.sequence });
|
|
228
|
-
parentKey = turn.key;
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
// After a rollover, ancestor Turns below the superseded rollout's settled boundary are
|
|
232
|
-
// already durable cloud history (the retained ancestor record); anything above it is
|
|
233
|
-
// new native work — including Turns whose pending receipts still need finishing.
|
|
234
|
-
if (ancestor && turn.rolloutId === ancestor.rolloutId && turn.contentEndBytes <= ancestor.throughBytes) {
|
|
235
|
-
const matches = matchAnchor(turn, ancestor.anchors);
|
|
236
|
-
if (new Set(matches.map((anchor) => anchor.turnId)).size > 1)
|
|
237
|
-
throw new Error("Ambiguous Runtime Turn boundary");
|
|
238
|
-
parentCloudTurnId = matches[0]?.turnId ?? null;
|
|
239
|
-
knownBoundary = matches.length > 0;
|
|
240
|
-
parentKey = null;
|
|
241
|
-
continue;
|
|
242
|
-
}
|
|
243
|
-
if (!parentKey && !knownBoundary)
|
|
244
|
-
throw new Error("Native continuation is not at a complete Runtime Turn boundary");
|
|
245
|
-
const result = turn.result ? nativeTurnCompleteSchema.parse(turn.result) : null;
|
|
246
|
-
const receipt = { version: 1, turnId, key: turn.key, parentKey, parentCloudTurnId: parentKey ? null : parentCloudTurnId,
|
|
247
|
-
userContent: turn.userContent, startedAt: turn.startedAt, endBytes: turn.endBytes, contentEndBytes: turn.contentEndBytes, result,
|
|
248
|
-
...(turn.sequence != null ? { sequence: turn.sequence } : {}),
|
|
249
|
-
...(context.sessionStartedAt ? { sessionStartedAt: context.sessionStartedAt } : {}),
|
|
250
|
-
...(context.origin ? { origin: context.origin } : {}),
|
|
251
|
-
...(!result ? { progress: nativeTurnProgressSchema.parse({ revision: turn.endBytes, messages: turn.messages }) } : {}) };
|
|
252
|
-
if (old && (JSON.stringify(old.userContent) !== JSON.stringify(receipt.userContent) || old.parentKey !== receipt.parentKey))
|
|
253
|
-
throw new Error("Native Turn changed; original receipt retained");
|
|
254
|
-
// Capture immutable native bytes before publishing the completed receipt. Subsequent Turns may change the source.
|
|
255
|
-
if (result) {
|
|
256
|
-
const cache = join(this.options.runtimeRoot, "native", "cache", "rollouts");
|
|
257
|
-
const source = turn.path ?? path;
|
|
258
|
-
const state = { sessionId: binding.originSessionId, harness: binding.harness, nativeSessionId: binding.nativeSessionId, sizeBytes: turn.endBytes, expectedChecksum: turn.sha256 };
|
|
259
|
-
const stage = async () => this.archives.stage({ ...state, path: await ensurePlainCodexRollout(source, cache, signal) }, turnId);
|
|
260
|
-
try {
|
|
261
|
-
await stage();
|
|
262
|
-
}
|
|
263
|
-
catch (error) {
|
|
264
|
-
// A corrupted `.zst` cache copy (bad checksum, truncation) is rebuilt once. A
|
|
265
|
-
// mismatch against an already-saved index is not a cache problem: that index is
|
|
266
|
-
// immutable, so rebuilding would only repeat a full decompression before failing.
|
|
267
|
-
if (!source.endsWith(".zst") || !(error instanceof ArchiveCaptureIntegrityError))
|
|
268
|
-
throw error;
|
|
269
|
-
const savedIndex = await readJson(join(this.archives.root, "versions", `${turnId}.json`)).catch(() => { throw error; });
|
|
270
|
-
if (!savedIndex) {
|
|
271
|
-
await rm(codexRolloutCachePath(source, cache), { force: true });
|
|
272
|
-
await stage();
|
|
273
|
-
}
|
|
274
|
-
else
|
|
275
|
-
throw error;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
if (JSON.stringify(old) !== JSON.stringify(receipt)) {
|
|
279
|
-
// The pending index precedes the receipt, so a crash cannot silently strand an unacknowledged Turn.
|
|
280
|
-
await atomicRuntimeJson(this.pendingPath(turnId), { turnId });
|
|
281
|
-
await atomicRuntimeJson(this.receiptPath(turnId), receipt);
|
|
282
|
-
}
|
|
283
|
-
parentKey = turn.key;
|
|
284
|
-
}
|
|
285
|
-
}, { lockPath: join(this.root, "capture.lock") });
|
|
286
|
-
}
|
|
287
|
-
async receipts(pendingOnly = false) {
|
|
288
|
-
const names = await readdir(join(this.root, pendingOnly ? "pending" : "turns")).catch((error) => { if (missing(error))
|
|
289
|
-
return []; throw error; });
|
|
290
|
-
const receipts = [];
|
|
291
|
-
for (const name of names) {
|
|
292
|
-
if (!name.endsWith(".json"))
|
|
293
|
-
continue;
|
|
294
|
-
const receipt = await readJson(join(this.root, "turns", name));
|
|
295
|
-
if (pendingOnly && !receipt) {
|
|
296
|
-
// Crash window: the pending index was written but the receipt itself never landed.
|
|
297
|
-
// The pointer carries no data; drop it — the next capture rebuilds the receipt from the transcript.
|
|
298
|
-
await rm(join(this.root, "pending", name), { force: true });
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
if (receipt?.version !== 1 || receipt.turnId !== this.turnId(receipt.key))
|
|
302
|
-
throw new Error("Native receipt is corrupt; original retained");
|
|
303
|
-
receipts.push(receipt);
|
|
304
|
-
}
|
|
305
|
-
return receipts.sort((a, b) => (a.sequence ?? Infinity) - (b.sequence ?? Infinity) || a.endBytes - b.endBytes || a.turnId.localeCompare(b.turnId));
|
|
306
|
-
}
|
|
307
|
-
async status() {
|
|
308
|
-
const binding = await this.binding();
|
|
309
|
-
const receipts = await this.receipts();
|
|
310
|
-
let pendingTurns = 0;
|
|
311
|
-
let sessionId = binding.sessionId;
|
|
312
|
-
for (const receipt of receipts) {
|
|
313
|
-
const remote = await readJson(this.cloudBindingPath(receipt.turnId));
|
|
314
|
-
if (remote)
|
|
315
|
-
sessionId = remote.sessionId;
|
|
316
|
-
if (!await readJson(this.acknowledgementPath(receipt.turnId)))
|
|
317
|
-
pendingTurns++;
|
|
318
|
-
}
|
|
319
|
-
return { harness: binding.harness, nativeSessionId: binding.nativeSessionId, sessionId, pendingTurns, pendingArchives: await this.archives.pendingCount() };
|
|
320
|
-
}
|
|
321
|
-
async flush(signal, onAbort) {
|
|
322
|
-
const transport = this.options.transport;
|
|
323
|
-
if (!transport)
|
|
324
|
-
return;
|
|
325
|
-
await withRuntimeSpaceBindingsLock(async () => {
|
|
326
|
-
const binding = await this.binding();
|
|
327
|
-
const pending = new Map((await this.receipts(true)).map((receipt) => [receipt.key, receipt]));
|
|
328
|
-
const processed = new Set();
|
|
329
|
-
const visit = async (receipt) => {
|
|
330
|
-
signal.throwIfAborted();
|
|
331
|
-
if (await readJson(this.acknowledgementPath(receipt.turnId))) {
|
|
332
|
-
await rm(this.pendingPath(receipt.turnId), { force: true });
|
|
333
|
-
return true;
|
|
334
|
-
}
|
|
335
|
-
if (processed.has(receipt.key))
|
|
336
|
-
return false;
|
|
337
|
-
processed.add(receipt.key);
|
|
338
|
-
const retry = await readJson(this.retryPath(receipt.turnId));
|
|
339
|
-
if (retry && retry.nextAt > Date.now())
|
|
340
|
-
return false;
|
|
341
|
-
let parent = null;
|
|
342
|
-
if (receipt.parentKey) {
|
|
343
|
-
const parentId = this.turnId(receipt.parentKey);
|
|
344
|
-
parent = await readJson(this.acknowledgementPath(parentId));
|
|
345
|
-
if (!parent) {
|
|
346
|
-
const predecessor = pending.get(receipt.parentKey);
|
|
347
|
-
if (!predecessor || !await visit(predecessor))
|
|
348
|
-
return false;
|
|
349
|
-
parent = await readJson(this.acknowledgementPath(parentId));
|
|
350
|
-
}
|
|
351
|
-
if (!parent)
|
|
352
|
-
throw new Error("Parent binding is missing");
|
|
353
|
-
}
|
|
354
|
-
let request = await readJson(this.requestPath(receipt.turnId));
|
|
355
|
-
if (!request) {
|
|
356
|
-
request = nativeTurnStartSchema.parse({ turnId: receipt.turnId, sessionId: parent?.sessionId ?? binding.sessionId, parentTurnId: parent?.turnId ?? receipt.parentCloudTurnId,
|
|
357
|
-
branchSessionId: nativeStableId(`${receipt.turnId}:branch`), harness: binding.harness, nativeSessionId: binding.nativeSessionId, userContent: receipt.userContent, startedAt: receipt.startedAt,
|
|
358
|
-
...(receipt.sessionStartedAt ? { sessionStartedAt: receipt.sessionStartedAt } : {}), ...(receipt.origin ? { origin: receipt.origin } : {}) });
|
|
359
|
-
await atomicRuntimeJson(this.requestPath(receipt.turnId), request);
|
|
360
|
-
}
|
|
361
|
-
let remote = await readJson(this.cloudBindingPath(receipt.turnId));
|
|
362
|
-
if (!remote) {
|
|
363
|
-
if (!transport.startNativeTurn)
|
|
364
|
-
throw new Error("Native Runtime WS is unavailable");
|
|
365
|
-
remote = await transport.startNativeTurn(request, { signal });
|
|
366
|
-
if (remote.turnId !== receipt.turnId)
|
|
367
|
-
throw new Error("Server Turn identity mismatch");
|
|
368
|
-
await atomicRuntimeJson(this.cloudBindingPath(receipt.turnId), remote);
|
|
369
|
-
}
|
|
370
|
-
if (!receipt.result) {
|
|
371
|
-
if (receipt.progress?.messages.length && transport.updateNativeTurn) {
|
|
372
|
-
const progressPath = join(this.root, "progress", `${receipt.turnId}.json`);
|
|
373
|
-
const sent = await readJson(progressPath);
|
|
374
|
-
if (!sent || sent.revision < receipt.progress.revision) {
|
|
375
|
-
await transport.updateNativeTurn(remote.sessionId, remote.turnId, receipt.progress, { signal });
|
|
376
|
-
await atomicRuntimeJson(progressPath, { revision: receipt.progress.revision });
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
if (onAbort && transport.heartbeatNativeTurn) {
|
|
380
|
-
const status = await transport.heartbeatNativeTurn(remote.sessionId, remote.turnId, { signal });
|
|
381
|
-
if (status.abortRequested)
|
|
382
|
-
onAbort?.();
|
|
383
|
-
}
|
|
384
|
-
return false;
|
|
385
|
-
}
|
|
386
|
-
if (!transport.completeNativeTurn)
|
|
387
|
-
throw new Error("Native Runtime WS is unavailable");
|
|
388
|
-
// Artifact retries back off: the terminal state is durable, so hammering the completion
|
|
389
|
-
// endpoint every flush cycle (5s) while object storage is down only adds load.
|
|
390
|
-
const backoffPath = join(this.root, "backoff", `${receipt.turnId}.json`);
|
|
391
|
-
const backoff = await readJson(backoffPath);
|
|
392
|
-
if (backoff && Date.now() - backoff.at < 30_000)
|
|
393
|
-
return false;
|
|
394
|
-
const completion = await transport.completeNativeTurn(remote.sessionId, remote.turnId, receipt.result, { signal });
|
|
395
|
-
if (completion.artifactsPending) {
|
|
396
|
-
// Terminal state is durable; only the artifact snapshot is missing. Keep the receipt pending
|
|
397
|
-
// and retry on the next flush cycle (>= 30s) until artifacts persist.
|
|
398
|
-
await atomicRuntimeJson(backoffPath, { at: Date.now() });
|
|
399
|
-
throw new Error("Native artifacts are pending; completion replays later");
|
|
400
|
-
}
|
|
401
|
-
await rm(backoffPath, { force: true });
|
|
402
|
-
await rm(this.retryPath(receipt.turnId), { force: true });
|
|
403
|
-
await atomicRuntimeJson(this.acknowledgementPath(receipt.turnId), remote);
|
|
404
|
-
await rm(this.pendingPath(receipt.turnId), { force: true });
|
|
405
|
-
return true;
|
|
406
|
-
};
|
|
407
|
-
const failures = [];
|
|
408
|
-
for (const receipt of pending.values()) {
|
|
409
|
-
if (!processed.has(receipt.key)) {
|
|
410
|
-
try {
|
|
411
|
-
await visit(receipt);
|
|
412
|
-
}
|
|
413
|
-
catch (error) {
|
|
414
|
-
const previous = await readJson(this.retryPath(receipt.turnId));
|
|
415
|
-
const attempt = Math.min((previous?.attempt ?? 0) + 1, 6);
|
|
416
|
-
const base = Math.min(30_000, 1_000 * 2 ** (attempt - 1));
|
|
417
|
-
const jitter = base * (0.8 + Math.random() * 0.4);
|
|
418
|
-
await atomicRuntimeJson(this.retryPath(receipt.turnId), { attempt, nextAt: Date.now() + Math.round(jitter) });
|
|
419
|
-
failures.push(error);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
// Resolve Session identities before the archive dependency walk. A fork baseline must not
|
|
424
|
-
// wait for an unrelated parent Session's failed upload. Immutable local versions stay untouched.
|
|
425
|
-
const archivePending = join(this.archives.root, "pending");
|
|
426
|
-
let archiveBlocked = false;
|
|
427
|
-
const indexes = await readdir(archivePending).catch((error) => { if (missing(error))
|
|
428
|
-
return []; throw error; });
|
|
429
|
-
for (const name of indexes) {
|
|
430
|
-
if (!name.endsWith(".json"))
|
|
431
|
-
continue;
|
|
432
|
-
try {
|
|
433
|
-
const path = join(archivePending, name);
|
|
434
|
-
const index = harnessArchiveIndexSchema.parse(await readJson(path));
|
|
435
|
-
const retry = await readJson(this.retryPath(index.turnId));
|
|
436
|
-
if (retry && retry.nextAt > Date.now()) {
|
|
437
|
-
archiveBlocked = true;
|
|
438
|
-
continue;
|
|
439
|
-
}
|
|
440
|
-
const resolved = await this.cloudArchive(index);
|
|
441
|
-
if (JSON.stringify(index) !== JSON.stringify(resolved))
|
|
442
|
-
await atomicRuntimeJson(path, resolved);
|
|
443
|
-
}
|
|
444
|
-
catch (error) {
|
|
445
|
-
failures.push(error);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
this.archiveFailure = undefined;
|
|
449
|
-
if (!archiveBlocked)
|
|
450
|
-
await this.archives.flush(signal);
|
|
451
|
-
if (failures.length)
|
|
452
|
-
throw failures[0];
|
|
453
|
-
if (this.archiveFailure)
|
|
454
|
-
throw this.archiveFailure;
|
|
455
|
-
}, { lockPath: join(this.root, "flush.lock") });
|
|
456
|
-
}
|
|
457
|
-
async cloudArchive(index) {
|
|
458
|
-
const binding = await readJson(this.acknowledgementPath(index.turnId));
|
|
459
|
-
if (!binding)
|
|
460
|
-
throw new Error("Native Turn result is not confirmed");
|
|
461
|
-
if (index.parentTurnId) {
|
|
462
|
-
const parent = await readJson(this.acknowledgementPath(index.parentTurnId));
|
|
463
|
-
if (parent?.sessionId === binding.sessionId)
|
|
464
|
-
return { ...index, sessionId: binding.sessionId };
|
|
465
|
-
// Cross-Session archive parents are forbidden. Materialize a baseline using existing immutable segments.
|
|
466
|
-
const segments = [...index.segments];
|
|
467
|
-
const visited = new Set([index.turnId]);
|
|
468
|
-
let parentId = index.parentTurnId;
|
|
469
|
-
while (parentId) {
|
|
470
|
-
if (visited.has(parentId))
|
|
471
|
-
throw new Error("Cyclic native archive");
|
|
472
|
-
visited.add(parentId);
|
|
473
|
-
const previous = harnessArchiveIndexSchema.parse(await readJson(join(this.archives.root, "versions", `${parentId}.json`)));
|
|
474
|
-
segments.unshift(...previous.segments);
|
|
475
|
-
parentId = previous.parentTurnId;
|
|
476
|
-
}
|
|
477
|
-
return harnessArchiveIndexSchema.parse({ ...index, sessionId: binding.sessionId, parentTurnId: null, segments });
|
|
478
|
-
}
|
|
479
|
-
return { ...index, sessionId: binding.sessionId };
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
export async function listNativeSyncStores(runtimeRoot, spaceId, identity, transport) {
|
|
483
|
-
const root = join(runtimeRoot, "native", nativeIdentityHash(identity));
|
|
484
|
-
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
485
|
-
const stores = [];
|
|
486
|
-
for (const name of await readdir(root)) {
|
|
487
|
-
if (!/^(pi|codex)-[a-f0-9]{64}$/.test(name))
|
|
488
|
-
continue;
|
|
489
|
-
const binding = await readJson(join(root, name, "binding.json"));
|
|
490
|
-
if (!binding || binding.identity !== identity || binding.spaceId !== spaceId)
|
|
491
|
-
continue;
|
|
492
|
-
stores.push(new NativeSyncStore({ runtimeRoot, spaceId, identity, harness: binding.harness, nativeSessionId: binding.nativeSessionId, instanceKey: binding.instanceKey, transport }));
|
|
493
|
-
}
|
|
494
|
-
return stores;
|
|
495
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { NativeSyncStore, type NativeSyncTransport } from "./native-sync-store.js";
|
|
2
|
-
import type { NativeRuntimeEvent } from "@neta-art/cohub";
|
|
3
|
-
export type NativeSyncConfig = {
|
|
4
|
-
version: 1;
|
|
5
|
-
identity: string;
|
|
6
|
-
spaceId: string;
|
|
7
|
-
root: string;
|
|
8
|
-
harnesses: ("pi" | "codex")[];
|
|
9
|
-
};
|
|
10
|
-
export declare const nativeRuntimeRoot: (spaceId: string) => string;
|
|
11
|
-
export declare const nativeSyncConfigPath: (runtimeRoot: string, identity: string) => string;
|
|
12
|
-
export type NativeImportCandidate = {
|
|
13
|
-
harness: "pi" | "codex";
|
|
14
|
-
path: string;
|
|
15
|
-
nativeSessionId: string;
|
|
16
|
-
turnCount: number;
|
|
17
|
-
sessionStartedAt: string | undefined;
|
|
18
|
-
};
|
|
19
|
-
/** Find existing native transcripts for exactly one bound project. */
|
|
20
|
-
export declare function discoverNativeImportCandidates(root: string, harnesses: ("pi" | "codex")[], options?: {
|
|
21
|
-
signal?: AbortSignal;
|
|
22
|
-
onProgress?: (progress: {
|
|
23
|
-
harness: "pi" | "codex";
|
|
24
|
-
scanned: number;
|
|
25
|
-
candidates: number;
|
|
26
|
-
}) => void;
|
|
27
|
-
}): Promise<{
|
|
28
|
-
candidates: NativeImportCandidate[];
|
|
29
|
-
errors: {
|
|
30
|
-
harness: "pi" | "codex";
|
|
31
|
-
path: string;
|
|
32
|
-
message: string;
|
|
33
|
-
}[];
|
|
34
|
-
superseded: {
|
|
35
|
-
path: string;
|
|
36
|
-
nativeSessionId: string;
|
|
37
|
-
message: string;
|
|
38
|
-
}[];
|
|
39
|
-
}>;
|
|
40
|
-
export declare function nativeArchiveTransport(spaceId: string, identity: string): Pick<NativeSyncTransport, "prepareRuntimeArchive" | "commitRuntimeArchive" | "getRuntimeArchive">;
|
|
41
|
-
export declare function readNativeSyncConfig(runtimeRoot: string, identity: string): Promise<NativeSyncConfig | null>;
|
|
42
|
-
/** Local capture only. Neither Pi callbacks nor Codex hooks wait for Cohub's network. */
|
|
43
|
-
export declare function captureNativeSession(input: {
|
|
44
|
-
harness: "pi" | "codex";
|
|
45
|
-
cwd: string;
|
|
46
|
-
path: string;
|
|
47
|
-
nativeSessionId?: string;
|
|
48
|
-
settled?: boolean;
|
|
49
|
-
leafId?: string | null;
|
|
50
|
-
sessionStartedAt?: string;
|
|
51
|
-
origin?: "local_import";
|
|
52
|
-
signal?: AbortSignal;
|
|
53
|
-
}): Promise<NativeSyncStore | null>;
|
|
54
|
-
/** The existing Runtime supervisor retries receipts even after the original terminal has exited. */
|
|
55
|
-
export declare function nativeWebSocketTransport(spaceId: string, identity: string, send: (event: NativeRuntimeEvent) => Promise<unknown>): NativeSyncTransport;
|
|
56
|
-
export declare const NATIVE_SYNC_SESSION_CONCURRENCY = 4;
|
|
57
|
-
export declare function runNativeSyncPool<T>(items: readonly T[], concurrency: number, signal: AbortSignal, processItem: (item: T) => Promise<void>): Promise<void>;
|
|
58
|
-
export declare function summarizeNativeSyncErrors(errors: unknown[]): {
|
|
59
|
-
name: string;
|
|
60
|
-
message: string;
|
|
61
|
-
code?: string;
|
|
62
|
-
status?: number;
|
|
63
|
-
count: number;
|
|
64
|
-
}[];
|
|
65
|
-
export declare function flushNativeSessions(spaceId: string, identity: string, signal: AbortSignal, report: (error: unknown) => void, transportOverride?: NativeSyncTransport): Promise<boolean>;
|