spexcode 0.7.0-next.1 → 0.7.0-next.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/node_modules/@spexcode/session-application/package.json +5 -5
- package/node_modules/@spexcode/session-events/package.json +2 -2
- package/node_modules/@spexcode/session-protocol/package.json +1 -1
- package/node_modules/@spexcode/session-runtime/package.json +2 -2
- package/node_modules/@spexcode/session-selflaunch/package.json +3 -3
- package/node_modules/@spexcode/session-topology/package.json +2 -2
- package/node_modules/@spexcode/spec-cli/dist/sessions.js +77 -6
- package/node_modules/@spexcode/spec-cli/package.json +7 -7
- package/node_modules/@spexcode/spec-core/package.json +1 -1
- package/node_modules/@spexcode/spec-eval/package.json +2 -2
- package/node_modules/@spexcode/spec-forge/package.json +2 -2
- package/node_modules/@spexcode/transcript/dist/parsers.d.ts +2 -0
- package/node_modules/@spexcode/transcript/dist/parsers.js +136 -10
- package/node_modules/@spexcode/transcript/dist/readers.d.ts +1 -1
- package/node_modules/@spexcode/transcript/dist/readers.js +7 -3
- package/node_modules/@spexcode/transcript/package.json +1 -1
- package/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/session-application",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.3",
|
|
4
4
|
"publishConfig": { "access": "public" },
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Adopter-owned composition of session protocol and topology transactions.",
|
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
"test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@spexcode/session-events": "0.7.0-next.
|
|
20
|
-
"@spexcode/session-protocol": "0.7.0-next.
|
|
21
|
-
"@spexcode/session-runtime": "0.7.0-next.
|
|
22
|
-
"@spexcode/session-topology": "0.7.0-next.
|
|
19
|
+
"@spexcode/session-events": "0.7.0-next.3",
|
|
20
|
+
"@spexcode/session-protocol": "0.7.0-next.3",
|
|
21
|
+
"@spexcode/session-runtime": "0.7.0-next.3",
|
|
22
|
+
"@spexcode/session-topology": "0.7.0-next.3"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/node": "^20.16.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/session-events",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.3",
|
|
4
4
|
"publishConfig": { "access": "public" },
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Append-only session facts and deterministic ordered replay inputs.",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"prepublishOnly": "node ../../scripts/release-publish.mjs --from-package-publish",
|
|
16
16
|
"test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
|
|
17
17
|
},
|
|
18
|
-
"dependencies": { "@spexcode/session-protocol": "0.7.0-next.
|
|
18
|
+
"dependencies": { "@spexcode/session-protocol": "0.7.0-next.3" },
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/node": "^20.16.0",
|
|
21
21
|
"tsx": "^4.19.2",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/session-runtime",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.3",
|
|
4
4
|
"publishConfig": { "access": "public" },
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Adopter-owned bindings between protocol addresses and native runtime identities.",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"prepublishOnly": "node ../../scripts/release-publish.mjs --from-package-publish",
|
|
16
16
|
"test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
|
|
17
17
|
},
|
|
18
|
-
"dependencies": { "@spexcode/session-protocol": "0.7.0-next.
|
|
18
|
+
"dependencies": { "@spexcode/session-protocol": "0.7.0-next.3" },
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/node": "^20.16.0",
|
|
21
21
|
"tsx": "^4.19.2",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/session-selflaunch",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.3",
|
|
4
4
|
"publishConfig": { "access": "public" },
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "A fail-closed self-launch adopter for the SpexCode session protocol.",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@spexcode/session-protocol": "0.7.0-next.
|
|
28
|
-
"@spexcode/session-runtime": "0.7.0-next.
|
|
27
|
+
"@spexcode/session-protocol": "0.7.0-next.3",
|
|
28
|
+
"@spexcode/session-runtime": "0.7.0-next.3"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/node": "^20.16.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/session-topology",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.3",
|
|
4
4
|
"publishConfig": { "access": "public" },
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "A neutral SQLite relation model for session addresses.",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"prepublishOnly": "node ../../scripts/release-publish.mjs --from-package-publish",
|
|
16
16
|
"test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
|
|
17
17
|
},
|
|
18
|
-
"dependencies": { "@spexcode/session-protocol": "0.7.0-next.
|
|
18
|
+
"dependencies": { "@spexcode/session-protocol": "0.7.0-next.3" },
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/node": "^20.16.0",
|
|
21
21
|
"tsx": "^4.19.2",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFile, execFileSync, spawn } from 'node:child_process';
|
|
2
|
+
import { createConnection } from 'node:net';
|
|
2
3
|
import { promisify } from 'node:util';
|
|
3
4
|
import { createHash, randomUUID } from 'node:crypto';
|
|
4
5
|
import { readFileSync, writeFileSync, existsSync, renameSync, linkSync, mkdirSync, rmSync, readdirSync, realpathSync, statSync, unlinkSync } from 'node:fs';
|
|
@@ -15,6 +16,7 @@ import { readSessionFiles } from './session-files.js';
|
|
|
15
16
|
import { readSessionWebs } from './session-web.js';
|
|
16
17
|
import { acquireFreshSessionApplicationForCreate, configuredSessionApplicationIfCutover, initializeFreshSessionApplication, releaseFreshSessionApplicationForCreate, sessionApplicationCutoverState, setSessionApplicationCommitWake } from './session-application.js';
|
|
17
18
|
import { jsonMigrationFencePath } from '@spexcode/session-application';
|
|
19
|
+
import { decodeEventJson } from '@spexcode/session-events';
|
|
18
20
|
import { withDeliveryLocks } from './delivery-lock.js';
|
|
19
21
|
import { withSessionRecordLock, withSessionRecordLockSync as coreWithSessionRecordLockSync } from './session-record-lock.js';
|
|
20
22
|
import { stripRefSigil } from './mentions.js';
|
|
@@ -1704,11 +1706,45 @@ let draining = false; // re-entrancy guard: only one drain pass runs at a time (
|
|
|
1704
1706
|
// A native receipt is bound before the readiness fence validates it. Suppress only that immediate wake so
|
|
1705
1707
|
// queued prompts cannot drain during the candidate window; the successful publication path drains normally.
|
|
1706
1708
|
const readinessWakeSuppressed = new Set();
|
|
1709
|
+
// A readiness timeout is launch-phase evidence only until the session produces another durable event. The
|
|
1710
|
+
// first event at/after the readiness marker is the launch transition itself; anything after that proves the
|
|
1711
|
+
// worker progressed (including a declaration), so a late diagnostic is moot and must not replace its note.
|
|
1712
|
+
function hasLaterLaunchReadinessEvent(rec) {
|
|
1713
|
+
const startedAt = rec.launchReadinessStartedAt;
|
|
1714
|
+
if (!Number.isFinite(startedAt))
|
|
1715
|
+
return false;
|
|
1716
|
+
const application = configuredSessionApplicationIfCutover();
|
|
1717
|
+
if (!application?.readState(rec.session))
|
|
1718
|
+
return false;
|
|
1719
|
+
const events = application.readEvents(rec.session);
|
|
1720
|
+
const statusPayload = (event) => {
|
|
1721
|
+
const payload = decodeEventJson(event.payload);
|
|
1722
|
+
return payload && typeof payload === 'object' && !Array.isArray(payload) && 'status' in payload
|
|
1723
|
+
? payload : null;
|
|
1724
|
+
};
|
|
1725
|
+
const baseline = events.find((event) => {
|
|
1726
|
+
if (event.occurredAtMs < Number(startedAt))
|
|
1727
|
+
return false;
|
|
1728
|
+
const payload = statusPayload(event);
|
|
1729
|
+
return payload?.status === 'active';
|
|
1730
|
+
});
|
|
1731
|
+
return baseline ? events.some((event) => {
|
|
1732
|
+
if (event.eventSeq <= baseline.eventSeq)
|
|
1733
|
+
return false;
|
|
1734
|
+
const payload = statusPayload(event);
|
|
1735
|
+
if (!payload)
|
|
1736
|
+
return false;
|
|
1737
|
+
const note = payload.note;
|
|
1738
|
+
return !(typeof note === 'string' && /^(?:queued launch readiness failed|launch readiness warning):/.test(note));
|
|
1739
|
+
}) : false;
|
|
1740
|
+
}
|
|
1707
1741
|
function noteQueuedLaunchFailureUnlocked(id, error, terminal = true, label, live = false) {
|
|
1742
|
+
const rec = readRecord(id);
|
|
1743
|
+
if (rec && (terminal || label === 'launch readiness warning') && hasLaterLaunchReadinessEvent(rec))
|
|
1744
|
+
return;
|
|
1708
1745
|
const reason = error instanceof Error ? error.message : String(error);
|
|
1709
1746
|
const note = `${label ?? (terminal ? 'queued launch readiness failed' : 'launch readiness warning')}: ${reason}`;
|
|
1710
1747
|
console.error(`spex: session ${id}: ${note}`);
|
|
1711
|
-
const rec = readRecord(id);
|
|
1712
1748
|
if (rec && !retirementReason(rec) && (rec.note !== note
|
|
1713
1749
|
|| (terminal && (rec.status !== 'error' || !rec.stopped || rec.launchReadinessStartedAt != null))
|
|
1714
1750
|
|| (!terminal && live && (rec.status === 'error' || rec.stopped)))) {
|
|
@@ -2488,9 +2524,46 @@ function isExplicitConnectionRefused(error) {
|
|
|
2488
2524
|
return errors.length > 0 && errors.every(isExplicitConnectionRefused);
|
|
2489
2525
|
return isExplicitConnectionRefused(error.cause);
|
|
2490
2526
|
}
|
|
2527
|
+
async function establishBackendConnection(target) {
|
|
2528
|
+
const parsed = new URL(target.url);
|
|
2529
|
+
const port = Number(parsed.port) || (parsed.protocol === 'https:' ? 443 : 80);
|
|
2530
|
+
return await new Promise((resolve, reject) => {
|
|
2531
|
+
const socket = createConnection({ host: parsed.hostname, port });
|
|
2532
|
+
let settled = false;
|
|
2533
|
+
const finish = (fn) => {
|
|
2534
|
+
if (settled)
|
|
2535
|
+
return;
|
|
2536
|
+
settled = true;
|
|
2537
|
+
clearTimeout(timer);
|
|
2538
|
+
socket.destroy();
|
|
2539
|
+
fn();
|
|
2540
|
+
};
|
|
2541
|
+
const timer = setTimeout(() => finish(() => {
|
|
2542
|
+
const error = new Error(`backend connection was not accepted at ${target.url} within 1500ms`);
|
|
2543
|
+
error.name = 'BackendError';
|
|
2544
|
+
Object.assign(error, { code: 'backend_availability_indeterminate' });
|
|
2545
|
+
reject(error);
|
|
2546
|
+
}), 1500);
|
|
2547
|
+
timer.unref?.();
|
|
2548
|
+
socket.once('connect', () => finish(() => resolve(false)));
|
|
2549
|
+
socket.once('error', (error) => finish(() => {
|
|
2550
|
+
if (isExplicitConnectionRefused(error))
|
|
2551
|
+
return resolve(true);
|
|
2552
|
+
const failed = new Error(`backend availability is indeterminate at ${target.url}; refusing in-process session creation (${error instanceof Error ? error.message : error})`);
|
|
2553
|
+
failed.name = 'BackendError';
|
|
2554
|
+
Object.assign(failed, { code: 'backend_availability_indeterminate', cause: error });
|
|
2555
|
+
reject(failed);
|
|
2556
|
+
}));
|
|
2557
|
+
});
|
|
2558
|
+
}
|
|
2491
2559
|
async function probeSessionCreateAuthority(target) {
|
|
2560
|
+
// TCP acceptance establishes presence. The identity route can be delayed by a busy backend event loop,
|
|
2561
|
+
// so it gets the ordinary create request deadline instead of a short availability deadline.
|
|
2562
|
+
const refused = await establishBackendConnection(target);
|
|
2563
|
+
if (refused)
|
|
2564
|
+
return true;
|
|
2492
2565
|
const controller = new AbortController();
|
|
2493
|
-
const timer = setTimeout(() => controller.abort(),
|
|
2566
|
+
const timer = setTimeout(() => controller.abort(new Error('backend authority request timed out')), sessionCreateTimeoutMs() + 5_000);
|
|
2494
2567
|
timer.unref?.();
|
|
2495
2568
|
let response;
|
|
2496
2569
|
try {
|
|
@@ -2498,11 +2571,9 @@ async function probeSessionCreateAuthority(target) {
|
|
|
2498
2571
|
}
|
|
2499
2572
|
catch (error) {
|
|
2500
2573
|
clearTimeout(timer);
|
|
2501
|
-
|
|
2502
|
-
return true;
|
|
2503
|
-
const failed = new Error(`backend availability is indeterminate at ${target.url}; refusing in-process session creation (${error instanceof Error ? error.message : error})`);
|
|
2574
|
+
const failed = new Error(`backend authority read failed after connection at ${target.url}; refusing in-process session creation (${error instanceof Error ? error.message : error})`);
|
|
2504
2575
|
failed.name = 'BackendError';
|
|
2505
|
-
Object.assign(failed, { code: '
|
|
2576
|
+
Object.assign(failed, { code: 'backend_authority_read_failed', cause: error });
|
|
2506
2577
|
throw failed;
|
|
2507
2578
|
}
|
|
2508
2579
|
try {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/spec-cli",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SpexCode CLI + server. The root spexcode package delegates to this compiled package; dashboard assets live in @spexcode/spec-dashboard.",
|
|
6
6
|
"bin": {
|
|
@@ -35,12 +35,12 @@
|
|
|
35
35
|
"test": "tsx --import ../scripts/test-home.mjs --test src/*.test.ts"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@spexcode/session-application": "0.7.0-next.
|
|
39
|
-
"@spexcode/session-selflaunch": "0.7.0-next.
|
|
40
|
-
"@spexcode/spec-core": "0.7.0-next.
|
|
41
|
-
"@spexcode/spec-eval": "0.7.0-next.
|
|
42
|
-
"@spexcode/spec-forge": "0.7.0-next.
|
|
43
|
-
"@spexcode/transcript": "0.7.0-next.
|
|
38
|
+
"@spexcode/session-application": "0.7.0-next.3",
|
|
39
|
+
"@spexcode/session-selflaunch": "0.7.0-next.3",
|
|
40
|
+
"@spexcode/spec-core": "0.7.0-next.3",
|
|
41
|
+
"@spexcode/spec-eval": "0.7.0-next.3",
|
|
42
|
+
"@spexcode/spec-forge": "0.7.0-next.3",
|
|
43
|
+
"@spexcode/transcript": "0.7.0-next.3",
|
|
44
44
|
"smol-toml": "^1.8.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/spec-eval",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./dist/index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"test": "tsx --import ../scripts/test-home.mjs --test src/*.test.ts"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@spexcode/spec-core": "0.7.0-next.
|
|
29
|
+
"@spexcode/spec-core": "0.7.0-next.3"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^20.16.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/spec-forge",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"test": "tsx --test src/*.test.ts"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@spexcode/spec-core": "0.7.0-next.
|
|
25
|
+
"@spexcode/spec-core": "0.7.0-next.3"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^20.16.0",
|
|
@@ -27,6 +27,8 @@ export type ParsedEvent = {
|
|
|
27
27
|
export type Parse = (value: unknown) => ParsedEvent | null;
|
|
28
28
|
export declare function claudeEvent(value: unknown): ParsedEvent | null;
|
|
29
29
|
export declare function codexEvent(value: unknown): ParsedEvent | null;
|
|
30
|
+
export declare function codexAppServerEvent(value: unknown): ParsedEvent | null;
|
|
31
|
+
export declare function codexAppServerStream(): Parse;
|
|
30
32
|
export declare function piEvent(value: unknown): ParsedEvent | null;
|
|
31
33
|
export declare function geminiEvent(value: unknown): ParsedEvent | null;
|
|
32
34
|
export declare function openclawEvent(value: unknown): ParsedEvent | null;
|
|
@@ -54,8 +54,22 @@ const lineCount = (value) => value ? value.split(/\r?\n/).length : 0;
|
|
|
54
54
|
// --- the seven native shapes -------------------------------------------------------------------------------
|
|
55
55
|
export function claudeEvent(value) {
|
|
56
56
|
const entry = object(value);
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
if (!entry)
|
|
58
|
+
return null;
|
|
59
|
+
// A message steered into a RUNNING turn (stream-json `type:user` on stdin, a queued command in the TUI) is not
|
|
60
|
+
// recorded as a `user` message: Claude writes it as an `attachment` of type `queued_command` carrying the prompt
|
|
61
|
+
// blocks. It is the person's turn all the same, and the one place a steer becomes observable — hooks never fire
|
|
62
|
+
// for it — so the reader draws it as a user turn at the moment it entered the conversation.
|
|
63
|
+
if (entry.type === 'attachment') {
|
|
64
|
+
const attachment = object(entry.attachment);
|
|
65
|
+
const queuedAt = at(entry);
|
|
66
|
+
if (attachment?.type !== 'queued_command' || queuedAt === null)
|
|
67
|
+
return null;
|
|
68
|
+
const text = items(attachment.prompt).map((block) => string(object(block)?.text)).filter(Boolean).join('\n') || null;
|
|
69
|
+
return text ? { at: queuedAt, turn: { id: idOf(entry), at: queuedAt, role: 'user', text, tools: [] } } : null;
|
|
70
|
+
}
|
|
71
|
+
const message = object(entry.message);
|
|
72
|
+
if (!message)
|
|
59
73
|
return null;
|
|
60
74
|
const eventAt = at(entry) ?? at(message);
|
|
61
75
|
if (eventAt === null)
|
|
@@ -120,6 +134,101 @@ export function codexEvent(value) {
|
|
|
120
134
|
}
|
|
121
135
|
return null;
|
|
122
136
|
}
|
|
137
|
+
// Codex app-server notifications are a different native stream from rollout lines. Keep this mapping stateless:
|
|
138
|
+
// a caller that needs streamed prose uses codexAppServerStream below, while file and in-memory sources still
|
|
139
|
+
// share the same one-record parser contract.
|
|
140
|
+
export function codexAppServerEvent(value) {
|
|
141
|
+
const entry = object(value);
|
|
142
|
+
const params = object(entry?.params);
|
|
143
|
+
if (!entry || !params || typeof entry.method !== 'string')
|
|
144
|
+
return null;
|
|
145
|
+
const item = object(params.item);
|
|
146
|
+
const method = entry.method;
|
|
147
|
+
const recognized = method === 'item/agentMessage/delta' || method === 'item/started' || method === 'item/completed';
|
|
148
|
+
if (!recognized)
|
|
149
|
+
return null;
|
|
150
|
+
const eventAt = timestamp(params.emittedAtMs) ?? timestamp(params.startedAtMs) ?? timestamp(params.completedAtMs);
|
|
151
|
+
if (eventAt === null)
|
|
152
|
+
return { at: null, turn: null };
|
|
153
|
+
if (method === 'item/agentMessage/delta') {
|
|
154
|
+
const id = string(params.itemId);
|
|
155
|
+
const delta = string(params.delta);
|
|
156
|
+
return id && delta !== null
|
|
157
|
+
? { at: eventAt, turn: { id, at: eventAt, role: 'assistant', text: delta, tools: [] } }
|
|
158
|
+
: null;
|
|
159
|
+
}
|
|
160
|
+
if ((method !== 'item/started' && method !== 'item/completed') || !item)
|
|
161
|
+
return null;
|
|
162
|
+
const id = string(item.id);
|
|
163
|
+
const type = string(item.type);
|
|
164
|
+
if (!id || !type)
|
|
165
|
+
return null;
|
|
166
|
+
if (type === 'userMessage') {
|
|
167
|
+
const text = blockText(item.content);
|
|
168
|
+
return text ? { at: eventAt, turn: { id, at: eventAt, role: 'user', text, tools: [] } } : null;
|
|
169
|
+
}
|
|
170
|
+
if (type === 'agentMessage') {
|
|
171
|
+
const text = string(item.text);
|
|
172
|
+
return { at: eventAt, turn: { id, at: eventAt, role: 'assistant', text: text ?? undefined, tools: [] } };
|
|
173
|
+
}
|
|
174
|
+
const toolTypes = new Set(['commandExecution', 'functionCall', 'customToolCall', 'mcpToolCall', 'dynamicToolCall']);
|
|
175
|
+
if (!toolTypes.has(type))
|
|
176
|
+
return null;
|
|
177
|
+
if (method === 'item/started') {
|
|
178
|
+
const name = type === 'commandExecution' ? 'command'
|
|
179
|
+
: string(item.name) ?? string(item.tool) ?? (type === 'mcpToolCall' ? 'mcp' : 'tool');
|
|
180
|
+
const input = item.arguments !== undefined ? item.arguments
|
|
181
|
+
: item.input !== undefined ? item.input
|
|
182
|
+
: item.command !== undefined ? item.command
|
|
183
|
+
: undefined;
|
|
184
|
+
return { at: eventAt, turn: { id, at: eventAt, role: 'assistant', tools: [{ id, name, input: input === undefined ? undefined : compact(input), outputLines: 0, outputBytes: 0 }] } };
|
|
185
|
+
}
|
|
186
|
+
let output = undefined;
|
|
187
|
+
if (type === 'commandExecution')
|
|
188
|
+
output = item.aggregatedOutput;
|
|
189
|
+
else if (type === 'functionCall' || type === 'customToolCall')
|
|
190
|
+
output = item.output ?? item.result;
|
|
191
|
+
else if (type === 'mcpToolCall')
|
|
192
|
+
output = item.result ?? item.error;
|
|
193
|
+
else if (type === 'dynamicToolCall')
|
|
194
|
+
output = item.contentItems ?? item.output ?? item.error;
|
|
195
|
+
return output === undefined || output === null
|
|
196
|
+
? { at: eventAt, turn: null }
|
|
197
|
+
: { at: eventAt, turn: null, toolOutputs: [{ id, text: compact(output) }] };
|
|
198
|
+
}
|
|
199
|
+
// Agent-message deltas are fragments of one native item. The closure remembers only that item's text and
|
|
200
|
+
// re-emits its native id, allowing IntervalCollector to replace the earlier turn in place.
|
|
201
|
+
export function codexAppServerStream() {
|
|
202
|
+
const textByItem = new Map();
|
|
203
|
+
return (value) => {
|
|
204
|
+
const parsed = codexAppServerEvent(value);
|
|
205
|
+
if (!parsed)
|
|
206
|
+
return null;
|
|
207
|
+
const entry = object(value);
|
|
208
|
+
const params = object(entry?.params);
|
|
209
|
+
const item = object(params?.item);
|
|
210
|
+
if (entry?.method === 'item/agentMessage/delta') {
|
|
211
|
+
const id = string(params?.itemId);
|
|
212
|
+
if (!id || !parsed.turn)
|
|
213
|
+
return parsed;
|
|
214
|
+
const text = (textByItem.get(id) ?? '') + (parsed.turn.text ?? '');
|
|
215
|
+
textByItem.set(id, text);
|
|
216
|
+
return { ...parsed, turn: { ...parsed.turn, text } };
|
|
217
|
+
}
|
|
218
|
+
if (item && item.type === 'agentMessage') {
|
|
219
|
+
const id = string(item.id);
|
|
220
|
+
if (id) {
|
|
221
|
+
const text = string(item.text);
|
|
222
|
+
if (text !== null || !textByItem.has(id))
|
|
223
|
+
textByItem.set(id, text ?? '');
|
|
224
|
+
const turn = parsed.turn;
|
|
225
|
+
if (turn)
|
|
226
|
+
return { ...parsed, turn: { ...turn, text: textByItem.get(id) || undefined } };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return parsed;
|
|
230
|
+
};
|
|
231
|
+
}
|
|
123
232
|
const blockText = (content) => typeof content === 'string'
|
|
124
233
|
? string(content)
|
|
125
234
|
: items(content).map((block) => string(object(block)?.text)).filter(Boolean).join('\n') || null;
|
|
@@ -366,14 +475,31 @@ export class IntervalCollector {
|
|
|
366
475
|
this.synthesized.set(base, seen + 1);
|
|
367
476
|
turn.id = seen ? `${base}#${seen}` : base;
|
|
368
477
|
}
|
|
369
|
-
this.turns.
|
|
370
|
-
|
|
371
|
-
this.
|
|
372
|
-
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
478
|
+
const existingAt = this.turns.findIndex((candidate) => candidate.id === turn.id);
|
|
479
|
+
if (existingAt >= 0) {
|
|
480
|
+
const existing = this.turns[existingAt];
|
|
481
|
+
const incomingTools = new Map(turn.tools.map((tool) => [tool.id, tool]));
|
|
482
|
+
const tools = existing.tools.map((tool) => {
|
|
483
|
+
const incoming = incomingTools.get(tool.id);
|
|
484
|
+
incomingTools.delete(tool.id);
|
|
485
|
+
return incoming ? { ...tool, ...incoming, output: incoming.output ?? tool.output } : tool;
|
|
486
|
+
});
|
|
487
|
+
tools.push(...incomingTools.values());
|
|
488
|
+
const replacement = { ...existing, ...turn, text: turn.text ?? existing.text, tools };
|
|
489
|
+
this.turns[existingAt] = replacement;
|
|
490
|
+
for (const tool of replacement.tools)
|
|
491
|
+
this.byTool.set(tool.id, tool);
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
this.turns.push(turn);
|
|
495
|
+
for (const tool of turn.tools)
|
|
496
|
+
this.byTool.set(tool.id, tool);
|
|
497
|
+
if (this.turns.length > MAX_TURNS) {
|
|
498
|
+
const dropped = this.turns.shift();
|
|
499
|
+
for (const tool of dropped.tools)
|
|
500
|
+
this.evicted.add(tool.id);
|
|
501
|
+
this.omittedTurns++;
|
|
502
|
+
}
|
|
377
503
|
}
|
|
378
504
|
}
|
|
379
505
|
return this.pastRange;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type TranscriptReader } from './turns.js';
|
|
2
2
|
export declare function claudeTranscriptPath(threadId: string, root?: string): string | null;
|
|
3
|
-
export declare function codexRolloutPath(threadId: string, root?: string): string | null;
|
|
3
|
+
export declare function codexRolloutPath(threadId: string, root?: string, archive?: string): string | null;
|
|
4
4
|
export declare function piSessionPath(threadId: string, root?: string): string | null;
|
|
5
5
|
export declare function geminiTranscriptPath(threadId: string, root?: string): string | null;
|
|
6
6
|
export declare function openclawTranscriptPath(threadId: string, root?: string): string | null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { closeSync, openSync, readFileSync, readSync, readdirSync, statSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
5
|
import { TranscriptReadError } from './turns.js';
|
|
6
6
|
import { IntervalCollector, claudeEvent, codexEvent, geminiEvent, hermesEvents, openclawEvent, opencodeEvents, piEvent } from './parsers.js';
|
|
7
7
|
// THE NATIVE-THREAD READERS. Each harness keeps its conversation somewhere private — Claude's project JSONL,
|
|
@@ -31,7 +31,10 @@ export function claudeTranscriptPath(threadId, root = projectTranscriptRoot()) {
|
|
|
31
31
|
const codexSessionsDir = () => join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'sessions');
|
|
32
32
|
// Walk newest day first and return on the first hit; the walk is exhaustive rather than capped, because
|
|
33
33
|
// future-dated junk under sessions/ sorts above every real day and a cap once masked every real rollout.
|
|
34
|
-
|
|
34
|
+
// A thread Codex has ARCHIVED (the app-server's thread/archive, which a closed session runs) keeps its
|
|
35
|
+
// rollout, moved out of the dated tree into the flat `archived_sessions/` beside it — a closed session's
|
|
36
|
+
// conversation is still on disk and still readable, so the locator looks there second.
|
|
37
|
+
export function codexRolloutPath(threadId, root = codexSessionsDir(), archive = join(dirname(root), 'archived_sessions')) {
|
|
35
38
|
for (const year of children(root))
|
|
36
39
|
for (const month of children(join(root, year)))
|
|
37
40
|
for (const day of children(join(root, year, month))) {
|
|
@@ -40,7 +43,8 @@ export function codexRolloutPath(threadId, root = codexSessionsDir()) {
|
|
|
40
43
|
if (file)
|
|
41
44
|
return join(dir, file);
|
|
42
45
|
}
|
|
43
|
-
|
|
46
|
+
const archived = children(archive).find((name) => name.includes(threadId));
|
|
47
|
+
return archived ? join(archive, archived) : null;
|
|
44
48
|
}
|
|
45
49
|
const piSessionsRoot = () => join(process.env.SPEXCODE_PI_AGENT_DIR || join(homedir(), '.pi', 'agent'), 'sessions');
|
|
46
50
|
const piSessionPaths = new Map();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/transcript",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Normalized agent transcripts: one parser per harness, a bounded interval reader over a native thread file or an in-memory event stream, and the full/delta frame protocol every transport and renderer share.",
|
|
6
6
|
"files": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spexcode",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SpexCode — a spec-driven, self-developing dev tool. The `spex` CLI + spec server reads the .spec tree and its git history, and serves the dashboard.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"prepack": "node scripts/sync-init-plugins.mjs --check && node scripts/prepack.mjs"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@spexcode/spec-cli": "0.7.0-next.
|
|
47
|
+
"@spexcode/spec-cli": "0.7.0-next.3"
|
|
48
48
|
},
|
|
49
49
|
"publishConfig": {
|
|
50
50
|
"access": "public"
|