spexcode 0.7.0-next.2 → 0.7.0-next.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/graphSnapshot.js +4 -1
- package/node_modules/@spexcode/spec-cli/dist/harness.js +6 -1
- package/node_modules/@spexcode/spec-cli/dist/host-resources.js +24 -0
- package/node_modules/@spexcode/spec-cli/dist/sessions.js +84 -6
- package/node_modules/@spexcode/spec-cli/package.json +7 -7
- package/node_modules/@spexcode/spec-core/dist/git.d.ts +1 -0
- package/node_modules/@spexcode/spec-core/dist/git.js +25 -0
- 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.js +32 -9
- 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.4",
|
|
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.4",
|
|
20
|
+
"@spexcode/session-protocol": "0.7.0-next.4",
|
|
21
|
+
"@spexcode/session-runtime": "0.7.0-next.4",
|
|
22
|
+
"@spexcode/session-topology": "0.7.0-next.4"
|
|
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.4",
|
|
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.4" },
|
|
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.4",
|
|
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.4" },
|
|
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.4",
|
|
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.4",
|
|
28
|
+
"@spexcode/session-runtime": "0.7.0-next.4"
|
|
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.4",
|
|
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.4" },
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/node": "^20.16.0",
|
|
21
21
|
"tsx": "^4.19.2",
|
|
@@ -3,7 +3,7 @@ import { readFileSync } from 'node:fs';
|
|
|
3
3
|
import { loadSpecs, requireGitWorkspace, headSha } from '@spexcode/spec-core';
|
|
4
4
|
import { resolveLayout } from '@spexcode/spec-core';
|
|
5
5
|
import { listSessions } from './sessions.js';
|
|
6
|
-
import { driftIndex, historyIndex, repoRoot } from '@spexcode/spec-core';
|
|
6
|
+
import { driftIndex, historyIndex, pruneHistoryCaches, repoRoot } from '@spexcode/spec-core';
|
|
7
7
|
import { residentForgeRevision, residentForgeState } from '@spexcode/spec-forge/resident';
|
|
8
8
|
import { resolveForgeHost } from '@spexcode/spec-forge/drivers';
|
|
9
9
|
import { boardThreads } from './issues.js';
|
|
@@ -56,6 +56,9 @@ export async function boardSnapshot() {
|
|
|
56
56
|
const root = repoRoot();
|
|
57
57
|
requireGitWorkspace(root);
|
|
58
58
|
const [specs, sessions] = await Promise.all([loadSpecs(), listSessions()]);
|
|
59
|
+
// Session worktrees are the live-root census for immutable history caches. Reconcile before the snapshot
|
|
60
|
+
// returns so closing a session releases its full index immediately rather than waiting for an LRU slot.
|
|
61
|
+
pruneHistoryCaches([root, ...sessions.map((session) => session.path)]);
|
|
59
62
|
const layout = await resolveLayout({ activeSessionIds: sessions.map((session) => session.id) });
|
|
60
63
|
const nodeIds = [...new Set([
|
|
61
64
|
...specs.map((node) => node.id),
|
|
@@ -2784,7 +2784,9 @@ const socketListenerLiveness = (_rec, tmuxAlive, _runtimeDir, _pane, socketLive)
|
|
|
2784
2784
|
const socketListenerOrPidAliveLiveness = (_rec, tmuxAlive, _runtimeDir, pane, socketLive) => (tmuxAlive && (!!socketLive || pane?.pidAlive === true) ? 'online' : 'offline');
|
|
2785
2785
|
const panePidLiveness = (_rec, tmuxAlive, _runtimeDir, pane) => (tmuxAlive && pane?.pidAlive === true ? 'online' : 'offline');
|
|
2786
2786
|
const recordOnline = (rec) => rec.stopped ? 'offline' : 'online';
|
|
2787
|
-
|
|
2787
|
+
// Leaf-backed headless sessions are the controller process, not the tmux pane that hosts it. The pane can
|
|
2788
|
+
// survive a SIGKILL as a bare shell, so tmux presence without the launch-registered PID is never online.
|
|
2789
|
+
const sessionHomeLiveness = (_rec, tmuxAlive, _runtimeDir, pane) => (tmuxAlive && pane?.pidAlive === true ? 'online' : 'offline');
|
|
2788
2790
|
// @@@ unlinkSocks - remove ONLY the transport this teardown PROVED dead. `cleanupRuntime` unlinks *their*
|
|
2789
2791
|
// socket, and the honest test of "theirs" is that the agent it just killed is GONE. It used to unlink on
|
|
2790
2792
|
// faith, which is unsound because a socket path is derived from the session id ALONE: it is the one
|
|
@@ -2889,6 +2891,7 @@ export const claudeHeadlessHarness = {
|
|
|
2889
2891
|
paneTitleIsSelfSummary: false,
|
|
2890
2892
|
launchCmd: (id, runtimeDir, cmd) => claudeHeadlessLaunchCommand(id, runtimeDir ?? runtimeRoot(), claudeBaseCmd(cmd)),
|
|
2891
2893
|
launchEnv: noLaunchEnv,
|
|
2894
|
+
// The controller's registered PID is the liveness witness; the tmux home is only the address boundary.
|
|
2892
2895
|
liveness: sessionHomeLiveness,
|
|
2893
2896
|
deliveryTransport: unprovenDeliveryTransport,
|
|
2894
2897
|
deliver: deliverViaClaudeHeadless,
|
|
@@ -3400,6 +3403,7 @@ export const piHeadlessHarness = {
|
|
|
3400
3403
|
runtimeOwnership: 'leaf',
|
|
3401
3404
|
paneTitleIsSelfSummary: false,
|
|
3402
3405
|
launchCmd: (id, runtimeDir, cmd) => piHeadlessLaunchCommand(id, runtimeDir ?? runtimeRoot(), piBaseCmd(cmd)),
|
|
3406
|
+
// The controller's registered PID is the liveness witness; the tmux home is only the address boundary.
|
|
3403
3407
|
liveness: sessionHomeLiveness,
|
|
3404
3408
|
deliver: deliverViaPiHeadless,
|
|
3405
3409
|
// the controller aborts its own turn child natively and confirms only once that child is gone
|
|
@@ -3513,6 +3517,7 @@ export const opencodeHeadlessHarness = {
|
|
|
3513
3517
|
runtimeOwnership: 'leaf',
|
|
3514
3518
|
deliveryTransport: unprovenDeliveryTransport,
|
|
3515
3519
|
launchCmd: (_id, _runtimeDir, cmd) => opencodeHeadlessLaunchCommand(opencodeBaseCmd(cmd)),
|
|
3520
|
+
// The controller's registered PID is the liveness witness; the tmux home is only the address boundary.
|
|
3516
3521
|
liveness: sessionHomeLiveness,
|
|
3517
3522
|
coldRuntime: async (rec) => {
|
|
3518
3523
|
const result = await opencodeHeadlessColdRuntime(rec);
|
|
@@ -216,6 +216,29 @@ const unresolvedHarnessOwners = (recs, budgets, have) => {
|
|
|
216
216
|
}
|
|
217
217
|
return out;
|
|
218
218
|
};
|
|
219
|
+
// A leaf-backed headless controller is the session's runtime. Keep its durable row visible when the controller
|
|
220
|
+
// disappears before the next resources sweep can attribute a process, and name the status/liveness contradiction
|
|
221
|
+
// explicitly so a supervisor cannot mistake the missing process for a cleanly idle session.
|
|
222
|
+
const missingHeadlessControllerOwners = (recs, procs, budgets, have) => {
|
|
223
|
+
const out = [];
|
|
224
|
+
for (const rec of recs) {
|
|
225
|
+
const harness = harnessByIdOrNull(rec.harness || defaultHarness.id);
|
|
226
|
+
if (!rec.governed || rec.stopped || rec.archived || rec.status !== 'active' && rec.status !== 'idle'
|
|
227
|
+
|| harness?.runtimeOwnership !== 'leaf' || have(rec.session_id))
|
|
228
|
+
continue;
|
|
229
|
+
const pid = runtimePid(join(runtimeRoot(), 'sessions', rec.session_id, 'agent.pid'));
|
|
230
|
+
if (pid && procs.has(pid))
|
|
231
|
+
continue;
|
|
232
|
+
out.push({
|
|
233
|
+
kind: 'session', id: rec.session_id, label: `session ${rec.session_id.slice(0, 8)}`, status: rec.status, liveness: 'offline',
|
|
234
|
+
processes: [], rssMiB: 0, pssMiB: null, cpuPercent: 0,
|
|
235
|
+
budget: { rssMiB: budgets.sessionRssMiB, idleCpuPercent: budgets.idleCpuPercent },
|
|
236
|
+
findings: [`headless-liveness-contradiction:${rec.status}/offline`],
|
|
237
|
+
reclaim: { eligible: false, reason: 'headless controller is absent; lifecycle repair must be explicit' },
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return out;
|
|
241
|
+
};
|
|
219
242
|
const sharedDescriptors = (recs, retainRegistry = false) => {
|
|
220
243
|
const out = new Map();
|
|
221
244
|
for (const harness of HARNESSES)
|
|
@@ -722,6 +745,7 @@ export async function collectResourceReport(opts = {}) {
|
|
|
722
745
|
budget: { rssMiB: budgets.sessionRssMiB, idleCpuPercent: budgets.idleCpuPercent },
|
|
723
746
|
findings: [`session-record-corrupt:${entry.error}`], reclaim: { eligible: false, reason: 'session record is corrupt; ownership and lifecycle are unknown' },
|
|
724
747
|
});
|
|
748
|
+
owners.push(...missingHeadlessControllerOwners(inv.recs, inv.procs, budgets, (sessionId) => owners.some((owner) => owner.kind === 'session' && owner.id === sessionId)));
|
|
725
749
|
owners.push(...unresolvedHarnessOwners(inv.recs, budgets, (sessionId) => owners.some((owner) => owner.kind === 'session' && owner.id === sessionId)));
|
|
726
750
|
// A referenced shared runtime with no readable process is still operationally important: keep its live or
|
|
727
751
|
// unknown refcount visible instead of silently omitting it from a process-derived report.
|
|
@@ -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,53 @@ 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(() => {
|
|
2542
|
+
// The event loop may have been blocked past the wall after the kernel completed the handshake. In that
|
|
2543
|
+
// case Node has not emitted `connect` yet, but `connecting` is already false; acceptance still proves
|
|
2544
|
+
// presence and must not be relabelled as an unavailable backend.
|
|
2545
|
+
if (!socket.connecting || socket.readyState === 'open')
|
|
2546
|
+
return finish(() => resolve(false));
|
|
2547
|
+
finish(() => {
|
|
2548
|
+
const error = new Error(`backend connection was not accepted at ${target.url} within 1500ms`);
|
|
2549
|
+
error.name = 'BackendError';
|
|
2550
|
+
Object.assign(error, { code: 'backend_availability_indeterminate' });
|
|
2551
|
+
reject(error);
|
|
2552
|
+
});
|
|
2553
|
+
}, 1500);
|
|
2554
|
+
timer.unref?.();
|
|
2555
|
+
socket.once('connect', () => finish(() => resolve(false)));
|
|
2556
|
+
socket.once('error', (error) => finish(() => {
|
|
2557
|
+
if (isExplicitConnectionRefused(error))
|
|
2558
|
+
return resolve(true);
|
|
2559
|
+
const failed = new Error(`backend availability is indeterminate at ${target.url}; refusing in-process session creation (${error instanceof Error ? error.message : error})`);
|
|
2560
|
+
failed.name = 'BackendError';
|
|
2561
|
+
Object.assign(failed, { code: 'backend_availability_indeterminate', cause: error });
|
|
2562
|
+
reject(failed);
|
|
2563
|
+
}));
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2491
2566
|
async function probeSessionCreateAuthority(target) {
|
|
2567
|
+
// TCP acceptance establishes presence. The identity route can be delayed by a busy backend event loop,
|
|
2568
|
+
// so it gets the ordinary create request deadline instead of a short availability deadline.
|
|
2569
|
+
const refused = await establishBackendConnection(target);
|
|
2570
|
+
if (refused)
|
|
2571
|
+
return true;
|
|
2492
2572
|
const controller = new AbortController();
|
|
2493
|
-
const timer = setTimeout(() => controller.abort(),
|
|
2573
|
+
const timer = setTimeout(() => controller.abort(new Error('backend authority request timed out')), sessionCreateTimeoutMs() + 5_000);
|
|
2494
2574
|
timer.unref?.();
|
|
2495
2575
|
let response;
|
|
2496
2576
|
try {
|
|
@@ -2498,11 +2578,9 @@ async function probeSessionCreateAuthority(target) {
|
|
|
2498
2578
|
}
|
|
2499
2579
|
catch (error) {
|
|
2500
2580
|
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})`);
|
|
2581
|
+
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
2582
|
failed.name = 'BackendError';
|
|
2505
|
-
Object.assign(failed, { code: '
|
|
2583
|
+
Object.assign(failed, { code: 'backend_authority_read_failed', cause: error });
|
|
2506
2584
|
throw failed;
|
|
2507
2585
|
}
|
|
2508
2586
|
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.4",
|
|
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.4",
|
|
39
|
+
"@spexcode/session-selflaunch": "0.7.0-next.4",
|
|
40
|
+
"@spexcode/spec-core": "0.7.0-next.4",
|
|
41
|
+
"@spexcode/spec-eval": "0.7.0-next.4",
|
|
42
|
+
"@spexcode/spec-forge": "0.7.0-next.4",
|
|
43
|
+
"@spexcode/transcript": "0.7.0-next.4",
|
|
44
44
|
"smol-toml": "^1.8.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
@@ -122,6 +122,7 @@ export declare function historyCacheStats(): {
|
|
|
122
122
|
historyRoots: number;
|
|
123
123
|
driftRoots: number;
|
|
124
124
|
};
|
|
125
|
+
export declare function pruneHistoryCaches(activeRoots: Iterable<string>): void;
|
|
125
126
|
export declare function resetHistoryCachesForTests(): void;
|
|
126
127
|
export declare function historyEventCachePathForTests(root: string): string;
|
|
127
128
|
export declare function ancestorsOf(idx: Reachability, sha: string): Uint8Array | undefined;
|
|
@@ -2270,6 +2270,31 @@ export function sourceIndexes(root, tip = 'HEAD') {
|
|
|
2270
2270
|
export function historyCacheStats() {
|
|
2271
2271
|
return { historyHeads: indexCache.size, driftHeads: driftIdxCache.size, historyRoots: indexRoots.size, driftRoots: driftRoots.size };
|
|
2272
2272
|
}
|
|
2273
|
+
// A backend snapshot is the authority for which checkout roots are live. Release cache ownership for roots
|
|
2274
|
+
// that disappeared from that snapshot so closed session worktrees do not keep immutable history warm until
|
|
2275
|
+
// the generic slot bound happens to evict them. Shared entries survive while another live root still names
|
|
2276
|
+
// the same immutable key; an in-flight promise remains usable by its current caller even after its cache key
|
|
2277
|
+
// is released.
|
|
2278
|
+
export function pruneHistoryCaches(activeRoots) {
|
|
2279
|
+
const active = new Set([...activeRoots].map(rootKey));
|
|
2280
|
+
for (const [root] of indexRoots)
|
|
2281
|
+
if (!active.has(root))
|
|
2282
|
+
indexRoots.delete(root);
|
|
2283
|
+
for (const [root] of driftRoots)
|
|
2284
|
+
if (!active.has(root))
|
|
2285
|
+
driftRoots.delete(root);
|
|
2286
|
+
const referencedHistory = new Set(indexRoots.values());
|
|
2287
|
+
const referencedDrift = new Set(driftRoots.values());
|
|
2288
|
+
for (const key of indexCache.keys())
|
|
2289
|
+
if (!referencedHistory.has(key))
|
|
2290
|
+
indexCache.delete(key);
|
|
2291
|
+
for (const key of driftIdxCache.keys())
|
|
2292
|
+
if (!referencedDrift.has(key))
|
|
2293
|
+
driftIdxCache.delete(key);
|
|
2294
|
+
for (const root of eventPathMemo.keys())
|
|
2295
|
+
if (!active.has(root))
|
|
2296
|
+
eventPathMemo.delete(root);
|
|
2297
|
+
}
|
|
2273
2298
|
// Tests deliberately clear process-local promises and path identity to exercise both cold and read-back paths.
|
|
2274
2299
|
// Production callers retain the bounded index caches; this is not a correctness escape hatch.
|
|
2275
2300
|
export function resetHistoryCachesForTests() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/spec-eval",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.4",
|
|
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.4"
|
|
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.4",
|
|
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.4"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^20.16.0",
|
|
@@ -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)
|
|
@@ -90,6 +104,9 @@ export function claudeEvent(value) {
|
|
|
90
104
|
}
|
|
91
105
|
return null;
|
|
92
106
|
}
|
|
107
|
+
const blockText = (content) => typeof content === 'string'
|
|
108
|
+
? string(content)
|
|
109
|
+
: items(content).map((block) => string(object(block)?.text)).filter(Boolean).join('\n') || null;
|
|
93
110
|
export function codexEvent(value) {
|
|
94
111
|
const entry = object(value);
|
|
95
112
|
const payload = object(entry?.payload);
|
|
@@ -99,16 +116,25 @@ export function codexEvent(value) {
|
|
|
99
116
|
if (eventAt === null)
|
|
100
117
|
return { at: null, turn: null };
|
|
101
118
|
const type = string(payload.type);
|
|
102
|
-
|
|
103
|
-
|
|
119
|
+
// THE PERSON'S MESSAGE is the `user_message` event. Codex also records it as a `response_item` message (the API
|
|
120
|
+
// form, `input_text` blocks) beside harness injections that no person typed (the AGENTS.md instructions), so
|
|
121
|
+
// that form is not a turn — the event is, once.
|
|
122
|
+
if (entry.type === 'event_msg' && type === 'user_message') {
|
|
104
123
|
const text = typeof payload.message === 'string' ? payload.message : compact(payload.message ?? payload.content ?? '');
|
|
105
124
|
return text ? { at: eventAt, turn: { id: idOf(payload) ?? idOf(entry), at: eventAt, role: 'user', text, tools: [] } } : null;
|
|
106
125
|
}
|
|
107
|
-
|
|
126
|
+
if (entry.type === 'response_item' && (type === 'message' || type === 'input_message') && payload.role === 'user')
|
|
127
|
+
return { at: eventAt, turn: null };
|
|
128
|
+
// WHAT THE AGENT SAID is the `agent_message` event — commentary and the final answer alike. Codex 0.146 also
|
|
129
|
+
// records the same prose as a `response_item` message (`output_text` blocks) beside it, so that form is not read:
|
|
130
|
+
// reading both would say every sentence twice. An empty event (a final answer that was a tool call) is a clock,
|
|
131
|
+
// not a turn. Structured reasoning stays private.
|
|
108
132
|
if (entry.type === 'event_msg' && type === 'agent_message') {
|
|
109
133
|
const text = string(payload.message ?? payload.text);
|
|
110
|
-
return text ? { at: eventAt, turn: { id: idOf(payload) ?? idOf(entry), at: eventAt, role: 'assistant', text, tools: [] } } : null;
|
|
134
|
+
return text ? { at: eventAt, turn: { id: idOf(payload) ?? idOf(entry), at: eventAt, role: 'assistant', text, tools: [] } } : { at: eventAt, turn: null };
|
|
111
135
|
}
|
|
136
|
+
if (entry.type === 'response_item' && type === 'message' && payload.role === 'assistant')
|
|
137
|
+
return { at: eventAt, turn: null };
|
|
112
138
|
if (entry.type === 'response_item' && (type === 'custom_tool_call' || type === 'function_call')) {
|
|
113
139
|
const id = string(payload.call_id ?? payload.id) ?? 'tool';
|
|
114
140
|
return { at: eventAt, turn: { id: idOf(payload) ?? idOf(entry), at: eventAt, role: 'assistant', tools: [{ id, name: string(payload.name ?? payload.tool_name) ?? 'tool', input: payload.input === undefined && payload.arguments === undefined ? undefined : compact(payload.input ?? payload.arguments), outputLines: 0, outputBytes: 0 }] } };
|
|
@@ -215,9 +241,6 @@ export function codexAppServerStream() {
|
|
|
215
241
|
return parsed;
|
|
216
242
|
};
|
|
217
243
|
}
|
|
218
|
-
const blockText = (content) => typeof content === 'string'
|
|
219
|
-
? string(content)
|
|
220
|
-
: items(content).map((block) => string(object(block)?.text)).filter(Boolean).join('\n') || null;
|
|
221
244
|
export function piEvent(value) {
|
|
222
245
|
const entry = object(value);
|
|
223
246
|
const message = object(entry?.message);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/transcript",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.4",
|
|
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.4",
|
|
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.4"
|
|
48
48
|
},
|
|
49
49
|
"publishConfig": {
|
|
50
50
|
"access": "public"
|