@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
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { readdir, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { RUNTIME_RECOVERY_BATCH_SIZE, runtimeEventSchema } from "@neta-art/cohub";
|
|
4
|
+
import { atomicRuntimeJson } from "../archive-store.js";
|
|
5
|
+
import { confirmQuiescentProcessGroup } from "../process-group.js";
|
|
6
|
+
import { readTranscript } from "./adapters.js";
|
|
7
|
+
export class ContextRequiredError extends Error {
|
|
8
|
+
}
|
|
9
|
+
const missing = (error) => error?.code === "ENOENT";
|
|
10
|
+
/** One small receipt per Cohub-executed Turn until the server acknowledges its result. */
|
|
11
|
+
export class ExecutionResults {
|
|
12
|
+
root;
|
|
13
|
+
constructor(root) {
|
|
14
|
+
this.root = root;
|
|
15
|
+
}
|
|
16
|
+
path(sessionId) { return join(this.root, "executions", `${sessionId}.json`); }
|
|
17
|
+
async read(sessionId) {
|
|
18
|
+
try {
|
|
19
|
+
const receipt = JSON.parse(await readFile(this.path(sessionId), "utf8"));
|
|
20
|
+
if (receipt.version !== 2 || receipt.sessionId !== sessionId)
|
|
21
|
+
throw new Error("Invalid execution receipt");
|
|
22
|
+
return receipt;
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (missing(error))
|
|
26
|
+
return null;
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Admit a new Turn. An unacknowledged earlier Turn is released once the server has settled it;
|
|
32
|
+
* otherwise the server must decide first, so a result is never overwritten unseen.
|
|
33
|
+
*/
|
|
34
|
+
async admit(input) {
|
|
35
|
+
const receipt = await this.read(input.sessionId);
|
|
36
|
+
if (!receipt || receipt.turnId === input.turnId)
|
|
37
|
+
return null;
|
|
38
|
+
const { resolvedTurnIds, settledTurnIds, complete } = input.context;
|
|
39
|
+
const resolved = resolvedTurnIds?.includes(receipt.turnId) === true;
|
|
40
|
+
if (resolved || settledTurnIds?.includes(receipt.turnId)) {
|
|
41
|
+
await rm(this.path(input.sessionId), { force: true });
|
|
42
|
+
return resolved ? receipt.session.path : null;
|
|
43
|
+
}
|
|
44
|
+
if (complete === false)
|
|
45
|
+
throw new ContextRequiredError("Server resolution is required for the pending native execution");
|
|
46
|
+
throw new Error(`Local turn ${receipt.turnId} has unconfirmed results; reconcile it before continuing`);
|
|
47
|
+
}
|
|
48
|
+
async start(session, input, requestId) {
|
|
49
|
+
await atomicRuntimeJson(this.path(input.sessionId), { version: 2, requestId, sessionId: input.sessionId, turnId: input.turnId, session });
|
|
50
|
+
}
|
|
51
|
+
async record(session, input, requestId, events, uncertainCleanup) {
|
|
52
|
+
await atomicRuntimeJson(this.path(input.sessionId), { version: 2, requestId, sessionId: input.sessionId, turnId: input.turnId, session, events, ...(uncertainCleanup ? { uncertainCleanup } : {}) });
|
|
53
|
+
}
|
|
54
|
+
async recover(input, requestId) {
|
|
55
|
+
const receipt = await this.read(input.sessionId);
|
|
56
|
+
if (!receipt || receipt.turnId !== input.turnId || receipt.session.harness !== input.harness)
|
|
57
|
+
return null;
|
|
58
|
+
if (requestId && receipt.requestId != null && receipt.requestId !== requestId)
|
|
59
|
+
throw new Error("Runtime result execution identity mismatch");
|
|
60
|
+
if (receipt.events) {
|
|
61
|
+
// A quiescent process group upgrades the receipt to confirmed; a live one stays uncertain.
|
|
62
|
+
if (receipt.uncertainCleanup && !receipt.uncertainCleanup.confirmedAt) {
|
|
63
|
+
if (!await confirmQuiescentProcessGroup(receipt.uncertainCleanup.processGroupId))
|
|
64
|
+
return null;
|
|
65
|
+
receipt.uncertainCleanup.confirmedAt = new Date().toISOString();
|
|
66
|
+
await atomicRuntimeJson(this.path(input.sessionId), receipt);
|
|
67
|
+
}
|
|
68
|
+
return { session: receipt.session, events: receipt.events.map((event) => runtimeEventSchema.parse(event)) };
|
|
69
|
+
}
|
|
70
|
+
const events = await this.fromTranscript(receipt);
|
|
71
|
+
if (!events)
|
|
72
|
+
return null;
|
|
73
|
+
await this.record(receipt.session, receipt, receipt.requestId, events);
|
|
74
|
+
return { session: receipt.session, events };
|
|
75
|
+
}
|
|
76
|
+
async acknowledge(sessionId, turnId) {
|
|
77
|
+
const receipt = await this.read(sessionId);
|
|
78
|
+
if (receipt?.turnId !== turnId)
|
|
79
|
+
throw new Error("Runtime acknowledgement identity mismatch");
|
|
80
|
+
await rm(this.path(sessionId), { force: true });
|
|
81
|
+
}
|
|
82
|
+
async pendingTurnIds(sessionId) {
|
|
83
|
+
const receipt = await this.read(sessionId);
|
|
84
|
+
return receipt ? [receipt.turnId] : [];
|
|
85
|
+
}
|
|
86
|
+
async *pendingBatches() {
|
|
87
|
+
const names = await readdir(join(this.root, "executions")).catch((error) => { if (missing(error))
|
|
88
|
+
return []; throw error; });
|
|
89
|
+
let batch = [];
|
|
90
|
+
for (const name of names) {
|
|
91
|
+
if (!name.endsWith(".json"))
|
|
92
|
+
continue;
|
|
93
|
+
const receipt = await this.read(name.slice(0, -5)).catch(() => null);
|
|
94
|
+
if (!receipt)
|
|
95
|
+
continue;
|
|
96
|
+
batch.push({ sessionId: receipt.sessionId, turnId: receipt.turnId, harness: receipt.session.harness });
|
|
97
|
+
if (batch.length >= RUNTIME_RECOVERY_BATCH_SIZE) {
|
|
98
|
+
yield batch;
|
|
99
|
+
batch = [];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (batch.length)
|
|
103
|
+
yield batch;
|
|
104
|
+
}
|
|
105
|
+
async fromTranscript(receipt) {
|
|
106
|
+
const transcript = await readTranscript(receipt.session.path, receipt.session.harness).catch(() => null);
|
|
107
|
+
const turn = transcript?.turns.find((entry) => entry.cloudTurnId === receipt.turnId);
|
|
108
|
+
const result = turn?.result;
|
|
109
|
+
if (!result || result.status === "interrupted" || turn !== transcript?.turns.at(-1))
|
|
110
|
+
return null;
|
|
111
|
+
const messages = result.messages.filter((message) => message.content.length);
|
|
112
|
+
if (!messages.length)
|
|
113
|
+
return null;
|
|
114
|
+
const message = (entry, ordinal) => ({
|
|
115
|
+
ordinal,
|
|
116
|
+
content: entry.content,
|
|
117
|
+
provider: entry.provider ?? null,
|
|
118
|
+
model: entry.model ?? null,
|
|
119
|
+
usage: (entry.usage ?? null),
|
|
120
|
+
stopReason: entry.stopReason ?? "stop",
|
|
121
|
+
errorMessage: entry.errorMessage ?? null,
|
|
122
|
+
});
|
|
123
|
+
return [
|
|
124
|
+
...messages.slice(0, -1).map((entry, index) => ({ type: "message.commit", message: message(entry, index) })),
|
|
125
|
+
{ type: "turn.end", message: message(messages.at(-1), messages.length - 1), resume: "native", archive: null },
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type RuntimeContext } from "@neta-art/cohub";
|
|
2
|
+
import type { SessionTurnProjectionClient } from "../turn-projection.js";
|
|
3
|
+
import type { RuntimeArchiveStore } from "../archive-store.js";
|
|
4
|
+
import type { NativeIngest, TranscriptState } from "./ingest.js";
|
|
5
|
+
/** How the native file for a Turn was obtained; reported with the Turn result. */
|
|
6
|
+
export type ResumeKind = "native" | "restored" | "handoff" | "new";
|
|
7
|
+
export type NativeSession = {
|
|
8
|
+
harness: "pi" | "codex";
|
|
9
|
+
nativeSessionId: string;
|
|
10
|
+
path: string;
|
|
11
|
+
cwd: string;
|
|
12
|
+
};
|
|
13
|
+
export type PreparedSession = {
|
|
14
|
+
session: NativeSession;
|
|
15
|
+
resume: ResumeKind;
|
|
16
|
+
};
|
|
17
|
+
/** Chooses the native file a Turn runs in. 1. */
|
|
18
|
+
export declare class NativeSessions {
|
|
19
|
+
private readonly options;
|
|
20
|
+
private readonly projections;
|
|
21
|
+
constructor(options: {
|
|
22
|
+
root: string;
|
|
23
|
+
spaceId: string;
|
|
24
|
+
projectionSource: SessionTurnProjectionClient;
|
|
25
|
+
archives: RuntimeArchiveStore;
|
|
26
|
+
ingest: NativeIngest;
|
|
27
|
+
onRestoreFailed?: (error: unknown, context: {
|
|
28
|
+
sessionId: string;
|
|
29
|
+
harness: "pi" | "codex";
|
|
30
|
+
unrestorable: boolean;
|
|
31
|
+
}) => void;
|
|
32
|
+
});
|
|
33
|
+
prepare(input: {
|
|
34
|
+
sessionId: string;
|
|
35
|
+
turnId: string;
|
|
36
|
+
harness: "pi" | "codex";
|
|
37
|
+
cwd: string;
|
|
38
|
+
provider?: string | null;
|
|
39
|
+
context: RuntimeContext;
|
|
40
|
+
resumable: (transcript: TranscriptState) => boolean;
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
}): Promise<PreparedSession>;
|
|
43
|
+
private restore;
|
|
44
|
+
private project;
|
|
45
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { serializeProjectionRecords } from "@neta-art/cohub";
|
|
6
|
+
import { ArchiveNotRestorableError, importNativeArchive } from "./archive.js";
|
|
7
|
+
import { piSessionDirectoryName, readTranscript } from "./adapters.js";
|
|
8
|
+
import { ProjectionStore, rebindProjectionNativeSession } from "../projection-store.js";
|
|
9
|
+
/** Chooses the native file a Turn runs in. 1. */
|
|
10
|
+
export class NativeSessions {
|
|
11
|
+
options;
|
|
12
|
+
projections;
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
this.projections = new ProjectionStore(options.projectionSource);
|
|
16
|
+
}
|
|
17
|
+
async prepare(input) {
|
|
18
|
+
const existing = this.options.ingest.transcriptFor(input.sessionId, input.harness, input.context.throughTurnId);
|
|
19
|
+
if (existing && input.resumable(existing)) {
|
|
20
|
+
return { session: { harness: existing.harness, nativeSessionId: existing.nativeSessionId, path: existing.path, cwd: existing.cwd }, resume: "native" };
|
|
21
|
+
}
|
|
22
|
+
const archive = input.context.archive;
|
|
23
|
+
if (archive && archive.harness === input.harness && archive.sessionId === input.sessionId && archive.turnId === input.context.throughTurnId) {
|
|
24
|
+
const restored = await this.restore(input, archive, input.signal).catch((error) => {
|
|
25
|
+
input.signal?.throwIfAborted();
|
|
26
|
+
// A structurally unrestorable archive falls back to projection; so does a transient
|
|
27
|
+
// failure, which simply tries the archive again on the next Turn.
|
|
28
|
+
this.options.onRestoreFailed?.(error, { sessionId: input.sessionId, harness: input.harness, unrestorable: error instanceof ArchiveNotRestorableError });
|
|
29
|
+
return null;
|
|
30
|
+
});
|
|
31
|
+
if (restored) {
|
|
32
|
+
// A restored copy is history the server already has, even where it carries no markers.
|
|
33
|
+
const through = (await readTranscript(restored.path, restored.harness).catch(() => null))?.turns.at(-1)?.key ?? null;
|
|
34
|
+
this.options.ingest.adopt(restored, input.sessionId, { turnKey: through, turnId: input.context.throughTurnId });
|
|
35
|
+
return { session: restored, resume: "restored" };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const projected = await this.project(input);
|
|
39
|
+
// A new Codex thread gets its file when it starts; the executor adopts it then.
|
|
40
|
+
if (projected.resume !== "new" || input.harness === "pi")
|
|
41
|
+
this.options.ingest.adopt(projected.session, input.sessionId);
|
|
42
|
+
return projected;
|
|
43
|
+
}
|
|
44
|
+
async restore(input, archive, signal) {
|
|
45
|
+
const id = randomUUID();
|
|
46
|
+
const raw = join(this.options.root, "native", "restored", `${id}.jsonl`);
|
|
47
|
+
const path = nativePath(input.harness, id, input.cwd);
|
|
48
|
+
try {
|
|
49
|
+
const restored = await this.options.archives.restore(archive, raw, signal);
|
|
50
|
+
const imported = await importNativeArchive({ source: raw, target: path, harness: input.harness, nativeSessionId: restored.nativeSessionId, id, cwd: input.cwd, signal });
|
|
51
|
+
return { harness: input.harness, nativeSessionId: imported.nativeSessionId, path, cwd: input.cwd };
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
await rm(raw, { force: true }).catch(() => undefined);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async project(input) {
|
|
58
|
+
const nativeSessionId = randomUUID();
|
|
59
|
+
const path = nativePath(input.harness, nativeSessionId, input.cwd);
|
|
60
|
+
const projection = rebindProjectionNativeSession(await this.projections.project({
|
|
61
|
+
spaceId: this.options.spaceId,
|
|
62
|
+
sessionId: input.sessionId,
|
|
63
|
+
turnId: input.turnId,
|
|
64
|
+
nativeSessionId,
|
|
65
|
+
cwd: input.cwd,
|
|
66
|
+
provider: input.provider,
|
|
67
|
+
target: input.harness,
|
|
68
|
+
throughTurnId: input.context.throughTurnId,
|
|
69
|
+
}, input.signal), nativeSessionId);
|
|
70
|
+
const session = { harness: input.harness, nativeSessionId, path, cwd: input.cwd };
|
|
71
|
+
// An empty Codex thread is created by Codex itself when the Turn starts.
|
|
72
|
+
if (!projection.turns.length && input.harness === "codex")
|
|
73
|
+
return { session, resume: "new" };
|
|
74
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
75
|
+
const temporary = `${path}.${randomUUID()}.projection`;
|
|
76
|
+
await writeFile(temporary, serializeProjectionRecords(projection.projection.records), { mode: 0o600 });
|
|
77
|
+
await rename(temporary, path);
|
|
78
|
+
return { session, resume: projection.turns.length ? "handoff" : "new" };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Where a harness keeps a new transcript for the bound directory. */
|
|
82
|
+
function nativePath(harness, nativeSessionId, cwd) {
|
|
83
|
+
const stamp = new Date().toISOString();
|
|
84
|
+
if (harness === "pi") {
|
|
85
|
+
const agent = process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent");
|
|
86
|
+
const directory = process.env.PI_CODING_AGENT_SESSION_DIR?.trim() || join(agent, "sessions", piSessionDirectoryName(cwd));
|
|
87
|
+
return join(directory, `${stamp.replace(/[:.]/g, "-")}_${nativeSessionId}.jsonl`);
|
|
88
|
+
}
|
|
89
|
+
const home = process.env.CODEX_HOME?.trim() || join(homedir(), ".codex");
|
|
90
|
+
const day = stamp.replace(/\.\d{3}Z$/, "").replaceAll(":", "-");
|
|
91
|
+
return join(home, "sessions", day.slice(0, 4), day.slice(5, 7), day.slice(8, 10), `rollout-${day}-${nativeSessionId}.jsonl`);
|
|
92
|
+
}
|
|
@@ -4,6 +4,11 @@ export type NativeTranscriptTurn = {
|
|
|
4
4
|
parentKey: string | null;
|
|
5
5
|
cloudTurnId?: string;
|
|
6
6
|
userContent: ContentBlock[];
|
|
7
|
+
/**
|
|
8
|
+
* Codex records a prompt in several steps and the final form last; until this is set the user
|
|
9
|
+
* content may still change.
|
|
10
|
+
*/
|
|
11
|
+
userFinal?: boolean;
|
|
7
12
|
messages: NativeTurnMessage[];
|
|
8
13
|
startedAt: string;
|
|
9
14
|
startBytes: number;
|
|
@@ -12,11 +17,8 @@ export type NativeTranscriptTurn = {
|
|
|
12
17
|
boundaries: Record<number, string>;
|
|
13
18
|
sha256: string;
|
|
14
19
|
result: NativeTurnComplete | null;
|
|
15
|
-
/** Codex lineage: the rollout file this Turn was read from. */
|
|
16
20
|
path?: string;
|
|
17
|
-
/** Codex lineage: the rollout id of the file this Turn was read from. */
|
|
18
21
|
rolloutId?: string;
|
|
19
|
-
/** Codex lineage: merged order across rollout files. */
|
|
20
22
|
sequence?: number;
|
|
21
23
|
};
|
|
22
24
|
export type NativeTranscript = {
|
|
@@ -25,9 +27,7 @@ export type NativeTranscript = {
|
|
|
25
27
|
cloudSessionId?: string;
|
|
26
28
|
turns: NativeTranscriptTurn[];
|
|
27
29
|
prefixes: ReadonlyMap<number, string>;
|
|
28
|
-
/** Codex lineage: every rollout file oldest-to-leaf, including the leaf itself. */
|
|
29
30
|
lineagePaths?: string[];
|
|
30
|
-
/** Codex lineage: rollout ids matching `lineagePaths` order. */
|
|
31
31
|
lineageRolloutIds?: string[];
|
|
32
32
|
};
|
|
33
33
|
export declare const NATIVE_MAX_RECORD_BYTES: number;
|
|
@@ -6,9 +6,9 @@ import { homedir } from "node:os";
|
|
|
6
6
|
import { basename, join } from "node:path";
|
|
7
7
|
import { createZstdDecompress } from "node:zlib";
|
|
8
8
|
import { pipeline } from "node:stream/promises";
|
|
9
|
-
import { piContent } from "
|
|
10
|
-
import { codexTokenTotals, codexUsage } from "
|
|
11
|
-
import { record } from "
|
|
9
|
+
import { piContent } from "../harness.js";
|
|
10
|
+
import { codexTokenTotals, codexUsage } from "../codex-usage.js";
|
|
11
|
+
import { record } from "../json-rpc.js";
|
|
12
12
|
const text = (value) => typeof value === "string" ? value : "";
|
|
13
13
|
const list = (value) => Array.isArray(value) ? value : [];
|
|
14
14
|
const iso = (value) => {
|
|
@@ -265,6 +265,7 @@ function parsePiTranscript(lines, options) {
|
|
|
265
265
|
let cloudSessionId = text(record(header.cohub).sessionId) || text(record(header.affinity).sessionId);
|
|
266
266
|
let current = null;
|
|
267
267
|
let messages = [];
|
|
268
|
+
let pendingMarker = null;
|
|
268
269
|
let completedAt = iso(header.timestamp);
|
|
269
270
|
const finish = (settled) => {
|
|
270
271
|
if (!current)
|
|
@@ -277,6 +278,10 @@ function parsePiTranscript(lines, options) {
|
|
|
277
278
|
for (const line of branch) {
|
|
278
279
|
const entry = line.value;
|
|
279
280
|
if (entry.type !== "message") {
|
|
281
|
+
// Cohub writes a marker entry immediately before a user message it sends, so a Turn it
|
|
282
|
+
// started can be matched to the cloud Turn without guessing at byte offsets.
|
|
283
|
+
if (entry.type === "custom" && text(entry.customType) === "cohub.turn")
|
|
284
|
+
pendingMarker = text(record(entry.data).turnId) || null;
|
|
280
285
|
if (current) {
|
|
281
286
|
current.endBytes = line.endBytes;
|
|
282
287
|
current.sha256 = line.sha256;
|
|
@@ -298,7 +303,9 @@ function parsePiTranscript(lines, options) {
|
|
|
298
303
|
if (message.role === "user") {
|
|
299
304
|
finish(true);
|
|
300
305
|
completedAt = timestamp;
|
|
301
|
-
const
|
|
306
|
+
const metaTurnId = text(record(message.meta).turnId);
|
|
307
|
+
const cloudTurnId = metaTurnId || pendingMarker || "";
|
|
308
|
+
pendingMarker = null;
|
|
302
309
|
if (cloudTurnId && !text(record(header.cohub).sessionId) && !text(record(header.affinity).sessionId))
|
|
303
310
|
cloudSessionId = text(record(message.meta).sourceSessionId) || cloudSessionId;
|
|
304
311
|
current = { key: text(entry.id), parentKey: turns.at(-1)?.key ?? null, ...(cloudTurnId ? { cloudTurnId } : {}), userContent: typeof message.content === "string" ? [{ type: "text", text: message.content }] : piContent(message.content), messages: [], startedAt: completedAt, startBytes: line.startBytes, endBytes: line.endBytes, contentEndBytes: line.endBytes, boundaries: {}, sha256: line.sha256, result: null };
|
|
@@ -347,8 +354,8 @@ async function resolveCodexLineage(leafPath, signal) {
|
|
|
347
354
|
if (segments.length >= CODEX_HISTORY_MAX_DEPTH)
|
|
348
355
|
throw new Error("Codex history chain is too deep; retain the original files");
|
|
349
356
|
const header = await readCodexHeader(path, signal);
|
|
350
|
-
// Every segment of one conversation must belong to the same project: a cross-project
|
|
351
|
-
//
|
|
357
|
+
// Every segment of one conversation must belong to the same project: a cross-project reference
|
|
358
|
+
// would import another workspace's history into this one.
|
|
352
359
|
if (header.cwd && leafCwd != null && header.cwd !== leafCwd)
|
|
353
360
|
throw new Error("Codex lineage crosses projects; retain the original files");
|
|
354
361
|
leafCwd ??= header.cwd;
|
|
@@ -468,9 +475,28 @@ function parseCodexLines(lines, segment) {
|
|
|
468
475
|
assistant().content.push({ type: "tool_result", tool_use_id: text(payload.call_id), content: typeof payload.output === "string" ? payload.output : JSON.stringify(payload.output ?? null) });
|
|
469
476
|
}
|
|
470
477
|
}
|
|
471
|
-
if (entry.type === "event_msg" && payload.type === "
|
|
472
|
-
|
|
478
|
+
if (entry.type === "event_msg" && payload.type === "item_completed") {
|
|
479
|
+
// Cohub tags a Turn it starts with its own cloud Turn id, so the file itself records which
|
|
480
|
+
// Turns already belong to a cloud Turn (and must not be ingested twice).
|
|
481
|
+
const item = record(payload.item);
|
|
482
|
+
if (item.type === "UserMessage") {
|
|
483
|
+
const clientId = text(item.client_id);
|
|
484
|
+
if (clientId)
|
|
485
|
+
current.cloudTurnId = clientId;
|
|
486
|
+
// The typed input, without the environment context Codex injects as a user item.
|
|
487
|
+
const typed = codexContent(item.content);
|
|
488
|
+
if (typed.length) {
|
|
489
|
+
current.userContent = typed;
|
|
490
|
+
userFromResponse = true;
|
|
491
|
+
}
|
|
492
|
+
current.userFinal = true;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (entry.type === "event_msg" && payload.type === "user_message") {
|
|
496
|
+
if (!userFromResponse)
|
|
497
|
+
current.userContent = [{ type: "text", text: text(payload.message) }];
|
|
473
498
|
userFromResponse = true;
|
|
499
|
+
current.userFinal = true;
|
|
474
500
|
}
|
|
475
501
|
if (entry.type === "event_msg" && ["turn_complete", "task_complete", "turn_aborted"].includes(text(payload.type))) {
|
|
476
502
|
if (payload.turn_id && payload.turn_id !== current.key)
|
|
@@ -500,8 +526,8 @@ function parseCodexLines(lines, segment) {
|
|
|
500
526
|
return { turns, ...(cloudSessionId ? { cloudSessionId } : {}), settled };
|
|
501
527
|
}
|
|
502
528
|
const codexSegmentMemo = new Map();
|
|
503
|
-
// Ancestor prefixes are immutable, so the cache is a pure win; parsed objects inflate in
|
|
504
|
-
//
|
|
529
|
+
// Ancestor prefixes are immutable, so the cache is a pure win; parsed objects inflate in memory
|
|
530
|
+
// several-fold over raw bytes, hence a conservative budget.
|
|
505
531
|
const CODEX_MEMO_MAX_BYTES = 128 * 1024 * 1024;
|
|
506
532
|
/** Ancestors are immutable; only their bounded prefixes are worth caching, bounded by bytes. */
|
|
507
533
|
async function parseCodexSegment(segment, signal) {
|
|
@@ -520,7 +546,7 @@ async function parseCodexSegment(segment, signal) {
|
|
|
520
546
|
throw new Error("Native transcript is empty");
|
|
521
547
|
const parsed = parseCodexLines(read.lines, segment);
|
|
522
548
|
// A paginated ancestor stamps every record with its ordinal; the referenced prefix must end
|
|
523
|
-
// exactly at the ordinal bound.
|
|
549
|
+
// exactly at the ordinal bound.
|
|
524
550
|
if (segment.endBytes != null && segment.endOrdinal != null) {
|
|
525
551
|
const ordinal = read.lines.at(-1)?.value.ordinal;
|
|
526
552
|
if (typeof ordinal !== "number" || ordinal !== segment.endOrdinal - 1)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ContentBlock, NativeTurnProgress, RuntimeExecutionEvent, RuntimeMessage } from "@neta-art/cohub";
|
|
2
|
+
import { type JsonRecord } from "../json-rpc.js";
|
|
3
|
+
/** Harness events mapped onto Cohub's streaming protocol. */
|
|
4
|
+
export type Translator = {
|
|
5
|
+
push(event: JsonRecord): boolean;
|
|
6
|
+
last(): RuntimeMessage;
|
|
7
|
+
};
|
|
8
|
+
/** Pi emits the same event shapes to its extensions as over RPC. */
|
|
9
|
+
export declare function piTranslator(emit: (event: RuntimeExecutionEvent) => void): Translator;
|
|
10
|
+
/** Codex app-server notifications for one Turn of one thread. */
|
|
11
|
+
export declare function codexTranslator(emit: (event: RuntimeExecutionEvent) => void, identity: {
|
|
12
|
+
provider: string | null;
|
|
13
|
+
model: string | null;
|
|
14
|
+
}): Translator;
|
|
15
|
+
type PreviewMessage = {
|
|
16
|
+
content: ContentBlock[];
|
|
17
|
+
provider: string | null;
|
|
18
|
+
model: string | null;
|
|
19
|
+
};
|
|
20
|
+
/** Where progress goes; `null` while the Turn is not yet recorded, so nothing can be sent yet. */
|
|
21
|
+
export type ProgressSink = (progress: NativeTurnProgress) => Promise<unknown> | null;
|
|
22
|
+
/**
|
|
23
|
+
* The live preview of a Turn a terminal user runs: harness events folded into messages and sent as
|
|
24
|
+
* ephemeral progress, at most once per interval and only from the first message that changed.
|
|
25
|
+
*/
|
|
26
|
+
export declare class LiveProgress {
|
|
27
|
+
private readonly sink;
|
|
28
|
+
private readonly intervalMs;
|
|
29
|
+
private messages;
|
|
30
|
+
private dirtyFrom;
|
|
31
|
+
private synced;
|
|
32
|
+
private timer;
|
|
33
|
+
private inFlight;
|
|
34
|
+
private disposed;
|
|
35
|
+
constructor(sink: ProgressSink, intervalMs: number);
|
|
36
|
+
apply(event: RuntimeExecutionEvent): void;
|
|
37
|
+
replace(messages: PreviewMessage[]): void;
|
|
38
|
+
dispose(): void;
|
|
39
|
+
private touched;
|
|
40
|
+
private schedule;
|
|
41
|
+
private flush;
|
|
42
|
+
private at;
|
|
43
|
+
}
|
|
44
|
+
export {};
|