@neta-art/cohub-cli 8.1.0 → 8.2.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/app-commerce.js +2 -2
- package/dist/commands/me.js +2 -2
- package/dist/commands/references.js +2 -2
- package/dist/commands/runtime.d.ts +12 -0
- package/dist/commands/runtime.js +46 -13
- package/dist/commands/search.js +2 -2
- package/dist/commands/space-commerce.js +2 -2
- package/dist/output.d.ts +9 -2
- package/dist/output.js +28 -4
- package/dist/runtime/archive-store.d.ts +4 -0
- package/dist/runtime/archive-store.js +10 -6
- package/dist/runtime/native-archive.d.ts +4 -0
- package/dist/runtime/native-archive.js +8 -0
- package/dist/runtime/native-codex-hook.js +1 -1
- package/dist/runtime/native-ipc.d.ts +10 -1
- package/dist/runtime/native-ipc.js +18 -3
- package/dist/runtime/native-pi-extension.js +9 -9
- package/dist/runtime/native-sync-store.d.ts +14 -0
- package/dist/runtime/native-sync-store.js +151 -24
- package/dist/runtime/native-sync.d.ts +16 -1
- package/dist/runtime/native-sync.js +149 -32
- package/dist/runtime/native-transcript.d.ts +31 -3
- package/dist/runtime/native-transcript.js +289 -31
- package/dist/runtime/session-store.js +24 -10
- package/dist/runtime/supervisor.js +87 -11
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolveExecutionAppId, } from "@neta-art/cohub";
|
|
2
2
|
import { createClient } from "../client.js";
|
|
3
|
-
import { error, handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
|
|
3
|
+
import { error, formatLocalDateTime, handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
|
|
4
4
|
function collectOption(value, previous = []) {
|
|
5
5
|
return [...previous, value];
|
|
6
6
|
}
|
|
@@ -69,7 +69,7 @@ function printOrder(order) {
|
|
|
69
69
|
{ key: "status", label: "Status" },
|
|
70
70
|
{ key: "amount", label: "Amount" },
|
|
71
71
|
{ key: "paid", label: "Paid" },
|
|
72
|
-
{ key: "created", label: "Created" },
|
|
72
|
+
{ key: "created", label: "Created", format: formatLocalDateTime },
|
|
73
73
|
{ key: "paidAt", label: "Paid At" },
|
|
74
74
|
]);
|
|
75
75
|
}
|
package/dist/commands/me.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createClient } from "../client.js";
|
|
2
|
-
import { handleHttp, json as outJson, jsonRequested, table } from "../output.js";
|
|
2
|
+
import { formatLocalDateTime, handleHttp, json as outJson, jsonRequested, table } from "../output.js";
|
|
3
3
|
function parseInteger(value, name, min) {
|
|
4
4
|
const parsed = Number.parseInt(value, 10);
|
|
5
5
|
if (!Number.isSafeInteger(parsed) || parsed < min) {
|
|
@@ -70,7 +70,7 @@ export function registerMe(program) {
|
|
|
70
70
|
});
|
|
71
71
|
if (jsonRequested(opts))
|
|
72
72
|
return outJson(activity);
|
|
73
|
-
console.log(`\n ${activity.range.from} → ${activity.range.to}`);
|
|
73
|
+
console.log(`\n ${formatLocalDateTime(activity.range.from)} → ${formatLocalDateTime(activity.range.to)}`);
|
|
74
74
|
table([activity.summary], [
|
|
75
75
|
{ key: "totalTokens", label: "Tokens" },
|
|
76
76
|
{ key: "costTotal", label: "Cost ($)" },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isUuid, } from "@neta-art/cohub";
|
|
2
2
|
import { createClient } from "../client.js";
|
|
3
|
-
import { table, json as outJson, jsonRequested, error, handleHttp } from "../output.js";
|
|
3
|
+
import { formatLocalDateTime, table, json as outJson, jsonRequested, error, handleHttp } from "../output.js";
|
|
4
4
|
const RESOURCE_TYPES = new Set([
|
|
5
5
|
"turn",
|
|
6
6
|
"session",
|
|
@@ -104,7 +104,7 @@ Examples:
|
|
|
104
104
|
source: `${r.sourceType}:${r.sourceId.slice(0, 8)}`,
|
|
105
105
|
target: formatTarget(r.targetType, r.targetId),
|
|
106
106
|
count: r.count,
|
|
107
|
-
lastSeen: r.updatedAt.slice(0, 10),
|
|
107
|
+
lastSeen: formatLocalDateTime(r.updatedAt).slice(0, 10),
|
|
108
108
|
}));
|
|
109
109
|
table(rows, [
|
|
110
110
|
{ key: "kind", label: "Kind" },
|
|
@@ -1,3 +1,15 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
+
import { type NativeIpcResponse } from "../runtime/native-ipc.js";
|
|
2
3
|
export { resolveLocalSpaceName, parseRuntimeHarnesses } from "../runtime/launch.js";
|
|
4
|
+
export declare function applyNativeImportResponse(result: {
|
|
5
|
+
imported: number;
|
|
6
|
+
failed: Array<{
|
|
7
|
+
path: string;
|
|
8
|
+
message: string;
|
|
9
|
+
}>;
|
|
10
|
+
skipped: Array<{
|
|
11
|
+
path: string;
|
|
12
|
+
message: string;
|
|
13
|
+
}>;
|
|
14
|
+
}, path: string, response: NativeIpcResponse): void;
|
|
3
15
|
export declare function registerRuntime(program: Command): void;
|
package/dist/commands/runtime.js
CHANGED
|
@@ -17,6 +17,16 @@ const reportFailure = (cause) => {
|
|
|
17
17
|
process.stderr.write(`Runtime failed: ${serializeDiagnosticError(cause).message}\n`);
|
|
18
18
|
process.exitCode = 1;
|
|
19
19
|
};
|
|
20
|
+
export function applyNativeImportResponse(result, path, response) {
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
if (response.skipped) {
|
|
23
|
+
result.skipped.push({ path, message: response.message });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
throw new Error(response.message);
|
|
27
|
+
}
|
|
28
|
+
result.imported += 1;
|
|
29
|
+
}
|
|
20
30
|
export function registerRuntime(program) {
|
|
21
31
|
const runtime = program.command("runtime").description("Connect a local workspace");
|
|
22
32
|
runtime.command("up [dir]")
|
|
@@ -105,10 +115,11 @@ export function registerRuntime(program) {
|
|
|
105
115
|
},
|
|
106
116
|
});
|
|
107
117
|
const candidates = discovered.candidates.filter((candidate) => !options.session || candidate.nativeSessionId === options.session);
|
|
118
|
+
const superseded = discovered.superseded.filter((skip) => !options.session || skip.nativeSessionId === options.session);
|
|
108
119
|
const result = {
|
|
109
120
|
spaceId, root, harnesses, candidates, errors: discovered.errors,
|
|
110
|
-
imported: 0, failed: [], dryRun: Boolean(options.dryRun),
|
|
111
|
-
complete: discovered.errors.length === 0,
|
|
121
|
+
imported: 0, pendingTurns: 0, failed: [], skipped: [], superseded, dryRun: Boolean(options.dryRun),
|
|
122
|
+
complete: discovered.errors.length === 0 && superseded.length === 0,
|
|
112
123
|
};
|
|
113
124
|
if (jsonRequested(options) && options.dryRun) {
|
|
114
125
|
result.complete = result.complete && result.failed.length === 0;
|
|
@@ -122,8 +133,13 @@ export function registerRuntime(program) {
|
|
|
122
133
|
process.exitCode = 1;
|
|
123
134
|
if (jsonRequested(options))
|
|
124
135
|
outJson(result);
|
|
125
|
-
else
|
|
136
|
+
else {
|
|
126
137
|
process.stdout.write("No existing native conversations found\n");
|
|
138
|
+
for (const error of discovered.errors)
|
|
139
|
+
process.stdout.write(`Skipped ${error.path}: ${error.message}\n`);
|
|
140
|
+
for (const skip of superseded)
|
|
141
|
+
process.stdout.write(`Superseded ${skip.path}: ${skip.message}\n`);
|
|
142
|
+
}
|
|
127
143
|
return;
|
|
128
144
|
}
|
|
129
145
|
if (options.dryRun) {
|
|
@@ -135,6 +151,8 @@ export function registerRuntime(program) {
|
|
|
135
151
|
process.stdout.write(` ${candidate.harness} ${candidate.nativeSessionId} ${candidate.turnCount} Turn${candidate.turnCount === 1 ? "" : "s"} ${candidate.path}\n`);
|
|
136
152
|
for (const error of discovered.errors)
|
|
137
153
|
process.stdout.write(`Skipped ${error.path}: ${error.message}\n`);
|
|
154
|
+
for (const skip of superseded)
|
|
155
|
+
process.stdout.write(`Superseded ${skip.path}: ${skip.message}\n`);
|
|
138
156
|
}
|
|
139
157
|
if (!result.complete)
|
|
140
158
|
process.exitCode = 1;
|
|
@@ -159,28 +177,43 @@ export function registerRuntime(program) {
|
|
|
159
177
|
rl.close();
|
|
160
178
|
}
|
|
161
179
|
}
|
|
180
|
+
let processed = 0;
|
|
162
181
|
for (const candidate of candidates) {
|
|
182
|
+
if (controller.signal.aborted)
|
|
183
|
+
break;
|
|
163
184
|
try {
|
|
164
185
|
const response = await requestNativeDaemon({ harness: candidate.harness, cwd: root, path: candidate.path, nativeSessionId: candidate.nativeSessionId,
|
|
165
186
|
sessionStartedAt: candidate.sessionStartedAt, origin: "local_import", settled: true });
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
187
|
+
applyNativeImportResponse(result, candidate.path, response);
|
|
188
|
+
if (response.ok)
|
|
189
|
+
result.pendingTurns += response.pendingTurns;
|
|
169
190
|
}
|
|
170
191
|
catch (error) {
|
|
171
|
-
|
|
192
|
+
if (!controller.signal.aborted)
|
|
193
|
+
result.failed.push({ path: candidate.path, message: error instanceof Error ? error.message : String(error) });
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
processed += 1;
|
|
197
|
+
if (!jsonRequested(options) && !controller.signal.aborted) {
|
|
198
|
+
process.stderr.write(`\rSubmitted ${result.imported}/${candidates.length} · pending ${result.pendingTurns} Turns`);
|
|
199
|
+
}
|
|
172
200
|
}
|
|
173
201
|
}
|
|
174
|
-
|
|
202
|
+
const cancelled = controller.signal.aborted;
|
|
203
|
+
result.complete = result.complete && result.failed.length === 0 && !cancelled;
|
|
175
204
|
if (jsonRequested(options))
|
|
176
|
-
outJson(result);
|
|
205
|
+
outJson({ ...result, cancelled, processed });
|
|
177
206
|
else {
|
|
178
|
-
|
|
179
|
-
|
|
207
|
+
if (processed)
|
|
208
|
+
process.stderr.write("\n");
|
|
209
|
+
process.stdout.write(`Submitted ${result.imported}/${candidates.length} native conversation${candidates.length === 1 ? "" : "s"}${cancelled ? " (interrupted)" : ""}\n`);
|
|
210
|
+
for (const error of [...discovered.errors, ...result.skipped, ...result.failed])
|
|
180
211
|
process.stdout.write(`Skipped ${error.path}: ${error.message}\n`);
|
|
181
|
-
|
|
212
|
+
for (const skip of result.superseded)
|
|
213
|
+
process.stdout.write(`Superseded ${skip.path}: ${skip.message}\n`);
|
|
214
|
+
process.stdout.write("Uploads continue in the local Runtime background; use runtime status to check confirmation\n");
|
|
182
215
|
}
|
|
183
|
-
if (!result.complete)
|
|
216
|
+
if (!result.complete && !cancelled)
|
|
184
217
|
process.exitCode = 1;
|
|
185
218
|
}
|
|
186
219
|
catch (cause) {
|
package/dist/commands/search.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isUuid } from "@neta-art/cohub";
|
|
2
2
|
import { createClient } from "../client.js";
|
|
3
|
-
import { table, json as outJson, jsonRequested, error, handleHttp } from "../output.js";
|
|
3
|
+
import { formatLocalDateTime, table, json as outJson, jsonRequested, error, handleHttp } from "../output.js";
|
|
4
4
|
const DEFAULT_LIMIT = 20;
|
|
5
5
|
const MAX_TITLE_LENGTH = 72;
|
|
6
6
|
const MAX_CONTEXT_LENGTH = 42;
|
|
@@ -50,7 +50,7 @@ function rowsFor(items) {
|
|
|
50
50
|
title: truncate(item.title, MAX_TITLE_LENGTH),
|
|
51
51
|
context: truncate(contextFor(item), MAX_CONTEXT_LENGTH),
|
|
52
52
|
match: item.matchedField,
|
|
53
|
-
updated: item.updatedAt ? item.updatedAt.slice(0, 10) : "",
|
|
53
|
+
updated: item.updatedAt ? formatLocalDateTime(item.updatedAt).slice(0, 10) : "",
|
|
54
54
|
href: item.href,
|
|
55
55
|
}));
|
|
56
56
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createClient } from "../client.js";
|
|
2
|
-
import { error, handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
|
|
2
|
+
import { error, formatLocalDateTime, handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
|
|
3
3
|
import { resolveSpace } from "../space.js";
|
|
4
4
|
const PRODUCT_STATUSES = ["draft", "active"];
|
|
5
5
|
const PRODUCT_UPDATE_STATUSES = ["draft", "active", "archived"];
|
|
@@ -135,7 +135,7 @@ function printOrders(orders) {
|
|
|
135
135
|
{ key: "product", label: "Product" },
|
|
136
136
|
{ key: "status", label: "Status" },
|
|
137
137
|
{ key: "amount", label: "Amount" },
|
|
138
|
-
{ key: "created", label: "Created" },
|
|
138
|
+
{ key: "created", label: "Created", format: formatLocalDateTime },
|
|
139
139
|
]);
|
|
140
140
|
}
|
|
141
141
|
async function confirmDanger(opts, detail) {
|
package/dist/output.d.ts
CHANGED
|
@@ -10,11 +10,18 @@ export type Column = {
|
|
|
10
10
|
format?: (value: unknown, row: Row) => string;
|
|
11
11
|
};
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* Timestamp → local ISO-style date and time with the system's UTC offset.
|
|
14
|
+
*
|
|
15
|
+
* CLI tables are for people and follow the machine's local timezone. Structured
|
|
16
|
+
* output remains untouched so scripts continue to receive the server value.
|
|
17
|
+
*/
|
|
18
|
+
export declare function formatLocalDateTime(value: unknown): string;
|
|
19
|
+
/**
|
|
20
|
+
* Epoch milliseconds → local date and time.
|
|
14
21
|
*
|
|
15
22
|
* Absent values render empty rather than falling through `Number()` coercion:
|
|
16
23
|
* `null` and `""` both become 0 there, which would print a confident-looking
|
|
17
|
-
*
|
|
24
|
+
* Unix epoch for a timestamp the server never sent.
|
|
18
25
|
*/
|
|
19
26
|
export declare function formatEpochMs(value: unknown): string;
|
|
20
27
|
export declare function table(rows: Row[], columns: Column[]): void;
|
package/dist/output.js
CHANGED
|
@@ -1,11 +1,34 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
import { extractBillingPayload } from "@neta-art/cohub";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Timestamp → local ISO-style date and time with the system's UTC offset.
|
|
5
|
+
*
|
|
6
|
+
* CLI tables are for people and follow the machine's local timezone. Structured
|
|
7
|
+
* output remains untouched so scripts continue to receive the server value.
|
|
8
|
+
*/
|
|
9
|
+
export function formatLocalDateTime(value) {
|
|
10
|
+
const date = value instanceof Date
|
|
11
|
+
? value
|
|
12
|
+
: typeof value === "number"
|
|
13
|
+
? new Date(value)
|
|
14
|
+
: typeof value === "string" && value.trim() !== ""
|
|
15
|
+
? new Date(value)
|
|
16
|
+
: null;
|
|
17
|
+
if (!date || Number.isNaN(date.getTime()))
|
|
18
|
+
return "";
|
|
19
|
+
const pad = (part) => String(part).padStart(2, "0");
|
|
20
|
+
const offsetMinutes = -date.getTimezoneOffset();
|
|
21
|
+
const offsetSign = offsetMinutes >= 0 ? "+" : "-";
|
|
22
|
+
const absoluteOffset = Math.abs(offsetMinutes);
|
|
23
|
+
const offset = `${offsetSign}${pad(Math.floor(absoluteOffset / 60))}:${pad(absoluteOffset % 60)}`;
|
|
24
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())} ${offset}`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Epoch milliseconds → local date and time.
|
|
5
28
|
*
|
|
6
29
|
* Absent values render empty rather than falling through `Number()` coercion:
|
|
7
30
|
* `null` and `""` both become 0 there, which would print a confident-looking
|
|
8
|
-
*
|
|
31
|
+
* Unix epoch for a timestamp the server never sent.
|
|
9
32
|
*/
|
|
10
33
|
export function formatEpochMs(value) {
|
|
11
34
|
const ms = typeof value === "number"
|
|
@@ -15,13 +38,14 @@ export function formatEpochMs(value) {
|
|
|
15
38
|
: Number.NaN;
|
|
16
39
|
if (!Number.isFinite(ms))
|
|
17
40
|
return "";
|
|
18
|
-
|
|
19
|
-
return Number.isNaN(date.getTime()) ? "" : date.toISOString();
|
|
41
|
+
return formatLocalDateTime(ms);
|
|
20
42
|
}
|
|
21
43
|
function cellText(column, row) {
|
|
22
44
|
const raw = row[column.key];
|
|
23
45
|
if (column.format)
|
|
24
46
|
return column.format(raw, row);
|
|
47
|
+
if (/(?:At|Timestamp)$/.test(column.key))
|
|
48
|
+
return formatLocalDateTime(raw);
|
|
25
49
|
const v = raw ?? "";
|
|
26
50
|
return typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
27
51
|
}
|
|
@@ -15,6 +15,10 @@ export type ArchiveTransport = {
|
|
|
15
15
|
signal?: AbortSignal;
|
|
16
16
|
}): Promise<RuntimeArchivePage>;
|
|
17
17
|
};
|
|
18
|
+
/** Raised when local capture bytes fail integrity checks (checksum, truncation, boundary); retryable via cache rebuild. */
|
|
19
|
+
export declare class ArchiveCaptureIntegrityError extends Error {
|
|
20
|
+
constructor(message: string);
|
|
21
|
+
}
|
|
18
22
|
export declare function checksumNativeFile(path: string): Promise<string>;
|
|
19
23
|
export declare function atomicRuntimeJson(path: string, value: unknown): Promise<void>;
|
|
20
24
|
/** Immutable local segments are the outbox. Model acknowledgements never remove them. */
|
|
@@ -4,6 +4,10 @@ import { mkdir, open, readFile, readdir, rename, rm, stat, link } from "node:fs/
|
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { RUNTIME_ARCHIVE_SEGMENT_BYTES, harnessArchiveIndexSchema, validateArchiveBoundary, } from "@neta-art/cohub";
|
|
6
6
|
const missing = (error) => error?.code === "ENOENT";
|
|
7
|
+
/** Raised when local capture bytes fail integrity checks (checksum, truncation, boundary); retryable via cache rebuild. */
|
|
8
|
+
export class ArchiveCaptureIntegrityError extends Error {
|
|
9
|
+
constructor(message) { super(message); this.name = "ArchiveCaptureIntegrityError"; }
|
|
10
|
+
}
|
|
7
11
|
const hash = (bytes, algorithm = "sha256") => createHash(algorithm).update(bytes).digest("hex");
|
|
8
12
|
export async function checksumNativeFile(path) {
|
|
9
13
|
const digest = createHash("sha256");
|
|
@@ -118,7 +122,7 @@ export class RuntimeArchiveStore {
|
|
|
118
122
|
if (saved.sessionId !== state.sessionId || saved.harness !== state.harness)
|
|
119
123
|
throw new Error("Archive identity mismatch");
|
|
120
124
|
if (state.expectedChecksum && state.expectedChecksum !== saved.sha256)
|
|
121
|
-
throw new
|
|
125
|
+
throw new ArchiveCaptureIntegrityError("Native Turn bytes changed; original archive retained");
|
|
122
126
|
const committed = await stat(join(this.root, "ready", `${turnId}.json`)).catch((error) => { if (missing(error))
|
|
123
127
|
return null; throw error; });
|
|
124
128
|
if (!committed)
|
|
@@ -133,10 +137,10 @@ export class RuntimeArchiveStore {
|
|
|
133
137
|
try {
|
|
134
138
|
const before = await file.stat();
|
|
135
139
|
if (!before.isFile() || !before.size)
|
|
136
|
-
throw new
|
|
140
|
+
throw new ArchiveCaptureIntegrityError("Native archive is empty");
|
|
137
141
|
const sizeBytes = state.sizeBytes ?? before.size;
|
|
138
142
|
if (!Number.isSafeInteger(sizeBytes) || sizeBytes < 1 || sizeBytes > before.size)
|
|
139
|
-
throw new
|
|
143
|
+
throw new ArchiveCaptureIntegrityError("Native archive boundary is unavailable");
|
|
140
144
|
const buffer = Buffer.alloc(RUNTIME_ARCHIVE_SEGMENT_BYTES);
|
|
141
145
|
let offset = 0;
|
|
142
146
|
let digest = createHash("sha256");
|
|
@@ -172,14 +176,14 @@ export class RuntimeArchiveStore {
|
|
|
172
176
|
}
|
|
173
177
|
const after = await stat(state.path);
|
|
174
178
|
if (before.ino !== after.ino || after.size < sizeBytes || state.sizeBytes === undefined && (before.size !== after.size || before.mtimeMs !== after.mtimeMs))
|
|
175
|
-
throw new
|
|
179
|
+
throw new ArchiveCaptureIntegrityError("Native file changed during capture");
|
|
176
180
|
if (state.sizeBytes !== undefined) {
|
|
177
181
|
// Native clients may append while an earlier Turn is captured. Validate the exact prefix twice.
|
|
178
182
|
const verified = createHash("sha256");
|
|
179
183
|
for await (const bytes of createReadStream(state.path, { end: sizeBytes - 1 }))
|
|
180
184
|
verified.update(bytes);
|
|
181
185
|
if (verified.digest("hex") !== digest.copy().digest("hex"))
|
|
182
|
-
throw new
|
|
186
|
+
throw new ArchiveCaptureIntegrityError("Native prefix changed during capture");
|
|
183
187
|
}
|
|
184
188
|
if (process.platform !== "win32") {
|
|
185
189
|
const directory = await open(join(this.root, "objects"), "r");
|
|
@@ -195,7 +199,7 @@ export class RuntimeArchiveStore {
|
|
|
195
199
|
sizeBytes, sha256: digest.digest("hex"), segments });
|
|
196
200
|
validateArchiveBoundary(index, parent);
|
|
197
201
|
if (state.expectedChecksum && index.sha256 !== state.expectedChecksum)
|
|
198
|
-
throw new
|
|
202
|
+
throw new ArchiveCaptureIntegrityError("Native Turn bytes changed during capture; original retained");
|
|
199
203
|
}
|
|
200
204
|
finally {
|
|
201
205
|
await file.close();
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { type CodexTokenTotals } from "./codex-usage.js";
|
|
2
|
+
/** A structurally unrestorable archive (e.g. its history reference is not carried); never retried. */
|
|
3
|
+
export declare class ArchiveNotRestorableError extends Error {
|
|
4
|
+
constructor(message: string);
|
|
5
|
+
}
|
|
2
6
|
export declare function readCodexArchiveTotals(path: string): Promise<CodexTokenTotals | undefined>;
|
|
3
7
|
/** Change only the header of a working copy. Raw archive bytes remain untouched. */
|
|
4
8
|
export declare function importNativeArchive(input: {
|
|
@@ -5,6 +5,10 @@ import { dirname } from "node:path";
|
|
|
5
5
|
import { RUNTIME_MAX_FRAME_BYTES } from "@neta-art/cohub";
|
|
6
6
|
import { JsonLineDecoder } from "./json-rpc.js";
|
|
7
7
|
import { codexArchiveTotals } from "./codex-usage.js";
|
|
8
|
+
/** A structurally unrestorable archive (e.g. its history reference is not carried); never retried. */
|
|
9
|
+
export class ArchiveNotRestorableError extends Error {
|
|
10
|
+
constructor(message) { super(message); this.name = "ArchiveNotRestorableError"; }
|
|
11
|
+
}
|
|
8
12
|
export async function readCodexArchiveTotals(path) {
|
|
9
13
|
let totals;
|
|
10
14
|
const decoder = new JsonLineDecoder((row) => { totals = codexArchiveTotals([row]) ?? totals; });
|
|
@@ -35,6 +39,10 @@ export async function importNativeArchive(input) {
|
|
|
35
39
|
else {
|
|
36
40
|
if (header?.type !== "session_meta" || header.payload?.id !== input.nativeSessionId)
|
|
37
41
|
throw new Error("Codex archive identity mismatch");
|
|
42
|
+
// A leaf archive does not carry its ancestor rollout. Importing it as-is would resume with
|
|
43
|
+
// silently truncated history; failing lets the caller rebuild from durable cloud Turns.
|
|
44
|
+
if (header.payload?.history_base != null)
|
|
45
|
+
throw new ArchiveNotRestorableError("Codex archive references history it does not carry; rebuild from the Session instead");
|
|
38
46
|
header.payload.id = input.id;
|
|
39
47
|
if (header.payload.session_id != null)
|
|
40
48
|
header.payload.session_id = input.id;
|
|
@@ -9,7 +9,7 @@ export async function runCodexNativeHook(payload) {
|
|
|
9
9
|
if (typeof value.transcript_path !== "string" || !value.transcript_path)
|
|
10
10
|
return;
|
|
11
11
|
const result = await requestNativeDaemon({ harness: "codex", cwd: value.cwd, path: value.transcript_path, nativeSessionId: value.session_id });
|
|
12
|
-
if (!result.ok)
|
|
12
|
+
if (!result.ok && !result.skipped)
|
|
13
13
|
throw new Error(result.message);
|
|
14
14
|
}
|
|
15
15
|
async function main() {
|
|
@@ -10,13 +10,22 @@ type NativeIpcRequest = {
|
|
|
10
10
|
sessionStartedAt?: string;
|
|
11
11
|
origin?: "local_import";
|
|
12
12
|
};
|
|
13
|
-
type NativeIpcResponse = {
|
|
13
|
+
export type NativeIpcResponse = {
|
|
14
14
|
ok: true;
|
|
15
15
|
pendingTurns: number;
|
|
16
16
|
} | {
|
|
17
17
|
ok: false;
|
|
18
|
+
skipped?: true;
|
|
18
19
|
message: string;
|
|
19
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* `store: null` means the request was valid but nothing was applicable yet (an unbound
|
|
23
|
+
* workspace, or a transcript a harness has not written). That is not a sync failure, so
|
|
24
|
+
* clients stay silent instead of warning about it.
|
|
25
|
+
*/
|
|
26
|
+
export declare function nativeCaptureResponse(result: {
|
|
27
|
+
store: NativeSyncStore | null;
|
|
28
|
+
}): Promise<NativeIpcResponse>;
|
|
20
29
|
export declare function nativeDaemonSocketFor(cwd: string): Promise<string | null>;
|
|
21
30
|
export declare function requestNativeDaemon(input: Omit<NativeIpcRequest, "type">): Promise<NativeIpcResponse>;
|
|
22
31
|
export declare function serveNativeDaemon(input: {
|
|
@@ -6,6 +6,16 @@ import { currentIdentityKey } from "../space.js";
|
|
|
6
6
|
import { canonicalRuntimeRoot, getRuntimeSpaceBinding } from "./space-binding.js";
|
|
7
7
|
const MAX_LINE_BYTES = 4 * 1024 * 1024;
|
|
8
8
|
const socketPath = (runtimeRoot) => join(runtimeRoot, "native", "daemon.sock");
|
|
9
|
+
/**
|
|
10
|
+
* `store: null` means the request was valid but nothing was applicable yet (an unbound
|
|
11
|
+
* workspace, or a transcript a harness has not written). That is not a sync failure, so
|
|
12
|
+
* clients stay silent instead of warning about it.
|
|
13
|
+
*/
|
|
14
|
+
export async function nativeCaptureResponse(result) {
|
|
15
|
+
if (!result.store)
|
|
16
|
+
return { ok: false, skipped: true, message: "No native capture is pending" };
|
|
17
|
+
return { ok: true, pendingTurns: (await result.store.status()).pendingTurns };
|
|
18
|
+
}
|
|
9
19
|
const parse = (raw) => {
|
|
10
20
|
const value = JSON.parse(raw);
|
|
11
21
|
if (value?.type !== "native.capture" || !["pi", "codex"].includes(value.harness)
|
|
@@ -20,14 +30,19 @@ export async function nativeDaemonSocketFor(cwd) {
|
|
|
20
30
|
const identity = currentIdentityKey();
|
|
21
31
|
if (!identity)
|
|
22
32
|
return null;
|
|
23
|
-
|
|
33
|
+
// A vanished workspace is unbound, not an error; the terminal has nothing left to sync.
|
|
34
|
+
const root = await canonicalRuntimeRoot(cwd).catch((error) => { if (error.code === "ENOENT")
|
|
35
|
+
return null; throw error; });
|
|
36
|
+
if (!root)
|
|
37
|
+
return null;
|
|
24
38
|
const binding = await getRuntimeSpaceBinding(root, identity);
|
|
25
39
|
return binding ? socketPath(nativeRuntimeRoot(binding.spaceId)) : null;
|
|
26
40
|
}
|
|
27
41
|
export async function requestNativeDaemon(input) {
|
|
28
42
|
const path = await nativeDaemonSocketFor(input.cwd);
|
|
43
|
+
// An unbound workspace is a normal state, not an error the terminal should surface.
|
|
29
44
|
if (!path)
|
|
30
|
-
return { ok: false, message: "Native Runtime is not bound" };
|
|
45
|
+
return { ok: false, skipped: true, message: "Native Runtime is not bound" };
|
|
31
46
|
return new Promise((resolve, reject) => {
|
|
32
47
|
const socket = createConnection(path);
|
|
33
48
|
let buffer = "";
|
|
@@ -88,7 +103,7 @@ export async function serveNativeDaemon(input) {
|
|
|
88
103
|
void (async () => {
|
|
89
104
|
const request = parse(buffer.slice(0, newline));
|
|
90
105
|
const result = await input.handle(request);
|
|
91
|
-
respond(
|
|
106
|
+
respond(await nativeCaptureResponse(result));
|
|
92
107
|
})().catch((error) => respond({ ok: false, message: error instanceof Error ? error.message : String(error) }));
|
|
93
108
|
});
|
|
94
109
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import { requestNativeDaemon } from "./native-ipc.js";
|
|
2
3
|
/** Thin native adapter. The Runtime Daemon owns Cohub auth, WS, retries and archives. */
|
|
3
4
|
export default function cohubNativeExtension(pi) {
|
|
@@ -6,7 +7,6 @@ export default function cohubNativeExtension(pi) {
|
|
|
6
7
|
let context = null;
|
|
7
8
|
let captures = Promise.resolve();
|
|
8
9
|
let lastError = "";
|
|
9
|
-
let timer;
|
|
10
10
|
const report = (error) => {
|
|
11
11
|
const message = error instanceof Error ? error.message : String(error);
|
|
12
12
|
if (message !== lastError)
|
|
@@ -19,9 +19,16 @@ export default function cohubNativeExtension(pi) {
|
|
|
19
19
|
if (!path)
|
|
20
20
|
return Promise.resolve();
|
|
21
21
|
captures = captures.then(async () => {
|
|
22
|
+
// Pi persists the transcript lazily; a not-yet-created file is not a sync failure.
|
|
23
|
+
if (!existsSync(path))
|
|
24
|
+
return;
|
|
22
25
|
const result = await requestNativeDaemon({ harness: "pi", cwd: ctx.cwd, path, nativeSessionId: ctx.sessionManager.getSessionId(), leafId: ctx.sessionManager.getLeafId(), settled });
|
|
23
|
-
|
|
26
|
+
// Nothing to sync yet (unbound workspace, or config not installed) is silence, not a warning.
|
|
27
|
+
if (!result.ok) {
|
|
28
|
+
if (result.skipped)
|
|
29
|
+
return;
|
|
24
30
|
throw new Error(result.message);
|
|
31
|
+
}
|
|
25
32
|
ctx.ui.setStatus("cohub", "Cohub");
|
|
26
33
|
lastError = "";
|
|
27
34
|
}).catch(report);
|
|
@@ -29,17 +36,10 @@ export default function cohubNativeExtension(pi) {
|
|
|
29
36
|
};
|
|
30
37
|
pi.on("session_start", async (_event, ctx) => {
|
|
31
38
|
await capture(ctx, ctx.isIdle());
|
|
32
|
-
if (timer)
|
|
33
|
-
clearInterval(timer);
|
|
34
|
-
timer = setInterval(() => { void capture(ctx, ctx.isIdle()); }, 5000);
|
|
35
|
-
timer.unref();
|
|
36
39
|
});
|
|
37
40
|
pi.on("message_end", (_event, ctx) => capture(ctx));
|
|
38
41
|
pi.on("agent_settled", (_event, ctx) => capture(ctx, true));
|
|
39
42
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
40
|
-
if (timer)
|
|
41
|
-
clearInterval(timer);
|
|
42
|
-
timer = undefined;
|
|
43
43
|
await captures;
|
|
44
44
|
ctx.ui.setStatus("cohub", undefined);
|
|
45
45
|
context = null;
|
|
@@ -20,6 +20,16 @@ type NativeBinding = {
|
|
|
20
20
|
sizeBytes: number;
|
|
21
21
|
sha256: string;
|
|
22
22
|
}>;
|
|
23
|
+
/** After a Codex rollover: the superseded rollout's settled boundary, addressed by rollout id. */
|
|
24
|
+
ancestor?: {
|
|
25
|
+
rolloutId: string;
|
|
26
|
+
throughBytes: number;
|
|
27
|
+
anchors: Array<{
|
|
28
|
+
turnId: string;
|
|
29
|
+
sizeBytes: number;
|
|
30
|
+
sha256: string;
|
|
31
|
+
}>;
|
|
32
|
+
};
|
|
23
33
|
};
|
|
24
34
|
export type NativeTurnReceipt = {
|
|
25
35
|
version: 1;
|
|
@@ -35,6 +45,8 @@ export type NativeTurnReceipt = {
|
|
|
35
45
|
sessionStartedAt?: string;
|
|
36
46
|
origin?: "local_import";
|
|
37
47
|
progress?: NativeTurnProgress;
|
|
48
|
+
/** Merged lineage order across Codex rollout files. */
|
|
49
|
+
sequence?: number;
|
|
38
50
|
};
|
|
39
51
|
export type NativeSyncTransport = ArchiveTransport & {
|
|
40
52
|
startNativeTurn?(input: NativeTurnStart, options?: {
|
|
@@ -80,12 +92,14 @@ export declare class NativeSyncStore {
|
|
|
80
92
|
private acknowledgementPath;
|
|
81
93
|
private pendingPath;
|
|
82
94
|
private requestPath;
|
|
95
|
+
private retryPath;
|
|
83
96
|
private cloudBindingPath;
|
|
84
97
|
binding(): Promise<NativeBinding>;
|
|
85
98
|
private initialize;
|
|
86
99
|
capture(path: string, transcript: NativeTranscript, context?: {
|
|
87
100
|
sessionStartedAt?: string;
|
|
88
101
|
origin?: "local_import";
|
|
102
|
+
signal?: AbortSignal;
|
|
89
103
|
}): Promise<void>;
|
|
90
104
|
receipts(pendingOnly?: boolean): Promise<NativeTurnReceipt[]>;
|
|
91
105
|
status(): Promise<{
|