spexcode 0.7.0-next.0 → 0.7.0-next.10
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/client.js +7 -5
- package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.d.ts +4 -0
- package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.js +12 -0
- package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.js +4 -1
- package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/harness.js +48 -8
- package/node_modules/@spexcode/spec-cli/dist/host-resources.js +55 -3
- package/node_modules/@spexcode/spec-cli/dist/materialize.js +54 -1
- package/node_modules/@spexcode/spec-cli/dist/session-transcript.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/sessions.js +116 -13
- package/node_modules/@spexcode/spec-cli/package.json +7 -7
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/skills/merge/spec.md +9 -3
- 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/frames.d.ts +2 -0
- package/node_modules/@spexcode/transcript/dist/frames.js +2 -0
- package/node_modules/@spexcode/transcript/dist/parsers.d.ts +8 -0
- package/node_modules/@spexcode/transcript/dist/parsers.js +320 -22
- package/node_modules/@spexcode/transcript/dist/readers.d.ts +7 -1
- package/node_modules/@spexcode/transcript/dist/readers.js +87 -4
- package/node_modules/@spexcode/transcript/dist/turns.d.ts +1 -0
- package/node_modules/@spexcode/transcript/package.json +1 -1
- package/package.json +2 -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';
|
|
@@ -1100,7 +1102,7 @@ export function toSession(rec, status, lv, activity = null) {
|
|
|
1100
1102
|
const pp = prompt ? oneLinePreview(prompt) : null;
|
|
1101
1103
|
const parts = { id: rec.session, name: rec.name, node: rec.node, title: rec.title, branch: rec.branch, activity: act, note: rec.note, promptPreview: pp };
|
|
1102
1104
|
const harness = harnessById(rec.harness || defaultHarness.id);
|
|
1103
|
-
return { id: rec.session, node: rec.node, branch: rec.branch, label: deriveLabel(parts), title: deriveTitle(parts), raw: { name: rec.name, title: rec.title }, path: rec.worktreePath, parent: rec.parent, harness: harness.id, capabilities: { headless: harness.headless }, launcher: rec.launcher, lifecycle: rec.status, proposal: rec.proposal, merges: rec.merges, note: rec.note, status, liveness: lv, archived: rec.archived
|
|
1105
|
+
return { id: rec.session, node: rec.node, branch: rec.branch, label: deriveLabel(parts), title: deriveTitle(parts), raw: { name: rec.name, title: rec.title }, path: rec.worktreePath, parent: rec.parent, harness: harness.id, capabilities: { headless: harness.headless }, launcher: rec.launcher, lifecycle: rec.closedAt ? 'archived' : rec.status, proposal: rec.closedAt ? null : rec.proposal, merges: rec.merges, note: rec.note, status, liveness: lv, archived: rec.archived || !!rec.closedAt, closedAt: rec.closedAt, archiveHazard: null, prompt, promptPreview: pp, created: rec.createdAt, activity: act, sortKey: rec.sortKey, files: readSessionFiles(rec.session), web: readSessionWebs(rec.session), ...(rec.zcodeChildSessionIds?.length ? { zcodeChildSessionIds: [...rec.zcodeChildSessionIds] } : {}) };
|
|
1104
1106
|
}
|
|
1105
1107
|
// @@@zcode child identity - ZCode owns the child id and SpexCode owns the session record. The writer accepts
|
|
1106
1108
|
// only their exact declared pair; names, worktrees, branches, and timestamps are deliberately not candidates.
|
|
@@ -1174,7 +1176,7 @@ export async function listArchivedSessionIndex(probe) {
|
|
|
1174
1176
|
if (entry.kind !== 'ok')
|
|
1175
1177
|
continue;
|
|
1176
1178
|
const rec = fromRaw(entry.raw);
|
|
1177
|
-
if (!rec.governed || !rec.archived)
|
|
1179
|
+
if (!rec.governed || (!rec.archived && !rec.closedAt))
|
|
1178
1180
|
continue;
|
|
1179
1181
|
const parts = {
|
|
1180
1182
|
id: rec.session, name: rec.name, node: rec.node, title: rec.title, branch: rec.branch,
|
|
@@ -1303,7 +1305,11 @@ export async function listSessions(includeArchived = false) {
|
|
|
1303
1305
|
// missing durable cold proof is also legacy: leaf liveness alone cannot prove a Codex loaded thread was
|
|
1304
1306
|
// unloaded, so it remains visible until an explicit archive repair.
|
|
1305
1307
|
const cleanCold = projectedRecord.archived && !changedDuringCensus.has(id) && hasValidColdProof(projectedRecord) && physical === 'offline' && (!residentRequired || resident?.healthy === true);
|
|
1306
|
-
|
|
1308
|
+
// A published close is terminal public history even if a later census cannot prove the old adapter fully
|
|
1309
|
+
// unloaded. Only legacy archived rows without closedAt may be exposed as a working hazard for repair.
|
|
1310
|
+
const projected = projectedRecord.archived && !cleanCold && !projectedRecord.closedAt
|
|
1311
|
+
? { ...projectedRecord, archived: false, stopped: false }
|
|
1312
|
+
: projectedRecord;
|
|
1307
1313
|
const projectedLv = projected === projectedRecord
|
|
1308
1314
|
? sessionHarness.runtimeOwnership === 'adapter'
|
|
1309
1315
|
? adapterResidentLiveness(projectedRecord, resident)
|
|
@@ -1704,11 +1710,45 @@ let draining = false; // re-entrancy guard: only one drain pass runs at a time (
|
|
|
1704
1710
|
// A native receipt is bound before the readiness fence validates it. Suppress only that immediate wake so
|
|
1705
1711
|
// queued prompts cannot drain during the candidate window; the successful publication path drains normally.
|
|
1706
1712
|
const readinessWakeSuppressed = new Set();
|
|
1713
|
+
// A readiness timeout is launch-phase evidence only until the session produces another durable event. The
|
|
1714
|
+
// first event at/after the readiness marker is the launch transition itself; anything after that proves the
|
|
1715
|
+
// worker progressed (including a declaration), so a late diagnostic is moot and must not replace its note.
|
|
1716
|
+
function hasLaterLaunchReadinessEvent(rec) {
|
|
1717
|
+
const startedAt = rec.launchReadinessStartedAt;
|
|
1718
|
+
if (!Number.isFinite(startedAt))
|
|
1719
|
+
return false;
|
|
1720
|
+
const application = configuredSessionApplicationIfCutover();
|
|
1721
|
+
if (!application?.readState(rec.session))
|
|
1722
|
+
return false;
|
|
1723
|
+
const events = application.readEvents(rec.session);
|
|
1724
|
+
const statusPayload = (event) => {
|
|
1725
|
+
const payload = decodeEventJson(event.payload);
|
|
1726
|
+
return payload && typeof payload === 'object' && !Array.isArray(payload) && 'status' in payload
|
|
1727
|
+
? payload : null;
|
|
1728
|
+
};
|
|
1729
|
+
const baseline = events.find((event) => {
|
|
1730
|
+
if (event.occurredAtMs < Number(startedAt))
|
|
1731
|
+
return false;
|
|
1732
|
+
const payload = statusPayload(event);
|
|
1733
|
+
return payload?.status === 'active';
|
|
1734
|
+
});
|
|
1735
|
+
return baseline ? events.some((event) => {
|
|
1736
|
+
if (event.eventSeq <= baseline.eventSeq)
|
|
1737
|
+
return false;
|
|
1738
|
+
const payload = statusPayload(event);
|
|
1739
|
+
if (!payload)
|
|
1740
|
+
return false;
|
|
1741
|
+
const note = payload.note;
|
|
1742
|
+
return !(typeof note === 'string' && /^(?:queued launch readiness failed|launch readiness warning):/.test(note));
|
|
1743
|
+
}) : false;
|
|
1744
|
+
}
|
|
1707
1745
|
function noteQueuedLaunchFailureUnlocked(id, error, terminal = true, label, live = false) {
|
|
1746
|
+
const rec = readRecord(id);
|
|
1747
|
+
if (rec && (terminal || label === 'launch readiness warning') && hasLaterLaunchReadinessEvent(rec))
|
|
1748
|
+
return;
|
|
1708
1749
|
const reason = error instanceof Error ? error.message : String(error);
|
|
1709
1750
|
const note = `${label ?? (terminal ? 'queued launch readiness failed' : 'launch readiness warning')}: ${reason}`;
|
|
1710
1751
|
console.error(`spex: session ${id}: ${note}`);
|
|
1711
|
-
const rec = readRecord(id);
|
|
1712
1752
|
if (rec && !retirementReason(rec) && (rec.note !== note
|
|
1713
1753
|
|| (terminal && (rec.status !== 'error' || !rec.stopped || rec.launchReadinessStartedAt != null))
|
|
1714
1754
|
|| (!terminal && live && (rec.status === 'error' || rec.stopped)))) {
|
|
@@ -1788,12 +1828,16 @@ export function canonicalRecordProjection(rec, canonical) {
|
|
|
1788
1828
|
// The application row is the only lifecycle fact after cutover. A JSON status is historical envelope data,
|
|
1789
1829
|
// so it must not win merely because it says waiting/error/archived while the canonical row says otherwise.
|
|
1790
1830
|
if (!canonical) {
|
|
1791
|
-
return rec
|
|
1831
|
+
return ('closedAt' in rec && rec.closedAt
|
|
1832
|
+
? { ...rec, archived: true }
|
|
1833
|
+
: rec);
|
|
1792
1834
|
}
|
|
1835
|
+
const closed = 'closedAt' in rec && !!rec.closedAt;
|
|
1793
1836
|
return {
|
|
1794
1837
|
...rec,
|
|
1795
|
-
|
|
1796
|
-
|
|
1838
|
+
archived: closed ? true : rec.archived,
|
|
1839
|
+
status: (closed ? 'archived' : canonical.status),
|
|
1840
|
+
proposal: (closed ? null : canonical.proposal),
|
|
1797
1841
|
note: canonical.note,
|
|
1798
1842
|
parent: canonical.parentSessionId,
|
|
1799
1843
|
};
|
|
@@ -2488,9 +2532,53 @@ function isExplicitConnectionRefused(error) {
|
|
|
2488
2532
|
return errors.length > 0 && errors.every(isExplicitConnectionRefused);
|
|
2489
2533
|
return isExplicitConnectionRefused(error.cause);
|
|
2490
2534
|
}
|
|
2535
|
+
async function establishBackendConnection(target) {
|
|
2536
|
+
const parsed = new URL(target.url);
|
|
2537
|
+
const port = Number(parsed.port) || (parsed.protocol === 'https:' ? 443 : 80);
|
|
2538
|
+
return await new Promise((resolve, reject) => {
|
|
2539
|
+
const socket = createConnection({ host: parsed.hostname, port });
|
|
2540
|
+
let settled = false;
|
|
2541
|
+
const finish = (fn) => {
|
|
2542
|
+
if (settled)
|
|
2543
|
+
return;
|
|
2544
|
+
settled = true;
|
|
2545
|
+
clearTimeout(timer);
|
|
2546
|
+
socket.destroy();
|
|
2547
|
+
fn();
|
|
2548
|
+
};
|
|
2549
|
+
const timer = setTimeout(() => {
|
|
2550
|
+
// The event loop may have been blocked past the wall after the kernel completed the handshake. In that
|
|
2551
|
+
// case Node has not emitted `connect` yet, but `connecting` is already false; acceptance still proves
|
|
2552
|
+
// presence and must not be relabelled as an unavailable backend.
|
|
2553
|
+
if (!socket.connecting || socket.readyState === 'open')
|
|
2554
|
+
return finish(() => resolve(false));
|
|
2555
|
+
finish(() => {
|
|
2556
|
+
const error = new Error(`backend connection was not accepted at ${target.url} within 1500ms`);
|
|
2557
|
+
error.name = 'BackendError';
|
|
2558
|
+
Object.assign(error, { code: 'backend_availability_indeterminate' });
|
|
2559
|
+
reject(error);
|
|
2560
|
+
});
|
|
2561
|
+
}, 1500);
|
|
2562
|
+
timer.unref?.();
|
|
2563
|
+
socket.once('connect', () => finish(() => resolve(false)));
|
|
2564
|
+
socket.once('error', (error) => finish(() => {
|
|
2565
|
+
if (isExplicitConnectionRefused(error))
|
|
2566
|
+
return resolve(true);
|
|
2567
|
+
const failed = new Error(`backend availability is indeterminate at ${target.url}; refusing in-process session creation (${error instanceof Error ? error.message : error})`);
|
|
2568
|
+
failed.name = 'BackendError';
|
|
2569
|
+
Object.assign(failed, { code: 'backend_availability_indeterminate', cause: error });
|
|
2570
|
+
reject(failed);
|
|
2571
|
+
}));
|
|
2572
|
+
});
|
|
2573
|
+
}
|
|
2491
2574
|
async function probeSessionCreateAuthority(target) {
|
|
2575
|
+
// TCP acceptance establishes presence. The identity route can be delayed by a busy backend event loop,
|
|
2576
|
+
// so it gets the ordinary create request deadline instead of a short availability deadline.
|
|
2577
|
+
const refused = await establishBackendConnection(target);
|
|
2578
|
+
if (refused)
|
|
2579
|
+
return true;
|
|
2492
2580
|
const controller = new AbortController();
|
|
2493
|
-
const timer = setTimeout(() => controller.abort(),
|
|
2581
|
+
const timer = setTimeout(() => controller.abort(new Error('backend authority request timed out')), sessionCreateTimeoutMs() + 5_000);
|
|
2494
2582
|
timer.unref?.();
|
|
2495
2583
|
let response;
|
|
2496
2584
|
try {
|
|
@@ -2498,11 +2586,9 @@ async function probeSessionCreateAuthority(target) {
|
|
|
2498
2586
|
}
|
|
2499
2587
|
catch (error) {
|
|
2500
2588
|
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})`);
|
|
2589
|
+
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
2590
|
failed.name = 'BackendError';
|
|
2505
|
-
Object.assign(failed, { code: '
|
|
2591
|
+
Object.assign(failed, { code: 'backend_authority_read_failed', cause: error });
|
|
2506
2592
|
throw failed;
|
|
2507
2593
|
}
|
|
2508
2594
|
try {
|
|
@@ -2988,7 +3074,7 @@ async function prepareSession(prompt, parent, launcher, name, context) {
|
|
|
2988
3074
|
try {
|
|
2989
3075
|
gitMutationStarted = true;
|
|
2990
3076
|
traceSessionCreate(id, requestDigest, phase, 'start', 'worktree-add');
|
|
2991
|
-
const added = await withGitAbortSignal(signal, () => gitTry(['-C', root, 'worktree', 'add', '-b', branch, path, startPoint], { extraEnv: DEFER_FOOTPRINT_REFRESH }));
|
|
3077
|
+
const added = await withGitAbortSignal(signal, () => gitTry(['-C', root, 'worktree', 'add', '--no-track', '-b', branch, path, startPoint], { extraEnv: DEFER_FOOTPRINT_REFRESH }));
|
|
2992
3078
|
traceSessionCreate(id, requestDigest, phase, 'finish', 'worktree-add');
|
|
2993
3079
|
if (added.ok)
|
|
2994
3080
|
Object.assign(owned, { path: true, worktree: true, branch: true });
|
|
@@ -4711,12 +4797,25 @@ async function closeOwnedSessionUnlocked(id, wt, _source, unboundStopped = false
|
|
|
4711
4797
|
throw new ResourceConflict(`refusing to finish close for ${id}: session record disappeared before publication`);
|
|
4712
4798
|
writeRecord({
|
|
4713
4799
|
...latest,
|
|
4800
|
+
proposal: null,
|
|
4714
4801
|
archived: true,
|
|
4715
4802
|
closedAt: latest.closedAt || new Date().toISOString(),
|
|
4716
4803
|
stopped: true,
|
|
4717
4804
|
coldProof: latest.coldProof || coldProofFor(latest),
|
|
4718
4805
|
adapterRecovery: null,
|
|
4719
4806
|
});
|
|
4807
|
+
// The canonical lifecycle must settle at the same terminal boundary as the durable close fact. `archived`
|
|
4808
|
+
// is an internal terminal marker; public projections render its closed record as `retired`.
|
|
4809
|
+
const application = configuredSessionApplicationIfCutover();
|
|
4810
|
+
if (application?.readState(id)) {
|
|
4811
|
+
application.transitionSession(id, {
|
|
4812
|
+
status: 'archived',
|
|
4813
|
+
proposal: null,
|
|
4814
|
+
note: latest.note,
|
|
4815
|
+
parentSessionId: latest.parent,
|
|
4816
|
+
recipientSessionIds: canonicalWatchRecipients(application, id, 'archived'),
|
|
4817
|
+
});
|
|
4818
|
+
}
|
|
4720
4819
|
let slot = null;
|
|
4721
4820
|
try {
|
|
4722
4821
|
slot = existsSync(wt.path) ? treeSlotDir(wt.path) : null;
|
|
@@ -5289,6 +5388,8 @@ export async function drainSession(id) {
|
|
|
5289
5388
|
const removed = application.dequeuePendingMessage(id, msg.messageId);
|
|
5290
5389
|
if (!removed || removed.messageId !== msg.messageId)
|
|
5291
5390
|
throw new ResourceConflict(`canonical queue head changed while delivering ${id}`);
|
|
5391
|
+
if (!msg.senderSessionId)
|
|
5392
|
+
markHumanPromptActive(id);
|
|
5292
5393
|
}
|
|
5293
5394
|
});
|
|
5294
5395
|
return;
|
|
@@ -5315,6 +5416,8 @@ export async function drainSession(id) {
|
|
|
5315
5416
|
const removed = application.dequeueForRuntime(id, 'spex-governed', binding.bindingGeneration, msg.messageId);
|
|
5316
5417
|
if (!removed || removed.messageId !== msg.messageId)
|
|
5317
5418
|
throw new ResourceConflict(`canonical queue head changed while delivering ${id}`);
|
|
5419
|
+
if (!msg.senderSessionId)
|
|
5420
|
+
markHumanPromptActive(id);
|
|
5318
5421
|
}
|
|
5319
5422
|
});
|
|
5320
5423
|
return;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/spec-cli",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.10",
|
|
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.10",
|
|
39
|
+
"@spexcode/session-selflaunch": "0.7.0-next.10",
|
|
40
|
+
"@spexcode/spec-core": "0.7.0-next.10",
|
|
41
|
+
"@spexcode/spec-eval": "0.7.0-next.10",
|
|
42
|
+
"@spexcode/spec-forge": "0.7.0-next.10",
|
|
43
|
+
"@spexcode/transcript": "0.7.0-next.10",
|
|
44
44
|
"smol-toml": "^1.8.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/skills/merge/spec.md
CHANGED
|
@@ -19,9 +19,15 @@ Land the current SpexCode session's branch; do not dispatch another merge reques
|
|
|
19
19
|
3. Immediately before landing, verify
|
|
20
20
|
`git merge-base --is-ancestor <source-head> <session-head>`. If it fails, sync again. A clean textual merge
|
|
21
21
|
is not product proof.
|
|
22
|
-
4.
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
4. Land with one `--no-ff` merge of the already-synced session tip, without touching the source checkout's
|
|
23
|
+
dirty work. Git refuses a merge over a dirty index, so do NOT clear it: add a temporary detached worktree of
|
|
24
|
+
the source head (`git worktree add --detach <tmp> <source-branch>`), make the `--no-ff` merge there, then
|
|
25
|
+
fast-forward the source checkout to that commit (`git merge --ff-only <tmp-head>`) and remove the temporary
|
|
26
|
+
worktree. A fast-forward never rewrites a path the merge did not change, so user-owned dirty files keep
|
|
27
|
+
their bytes and their staged/unstaged state. Never `git restore`, `reset`, `checkout`, `stash`, or overwrite
|
|
28
|
+
a user-owned path — an unstaged edit has no blob anywhere, so "save and put back" loses it, and a copy of the
|
|
29
|
+
same path from another worktree is a different version, not a restoration. Do not resolve conflicts in the
|
|
30
|
+
source checkout; if the merge genuinely overlaps a user-owned path, stop and report the exact overlap.
|
|
25
31
|
5. Verify the source checkout has no `MERGE_HEAD`, the session tip is its ancestor, unrelated dirty
|
|
26
32
|
fingerprints are unchanged, and the post-merge gates pass. Push the source-of-truth branch only after
|
|
27
33
|
those checks.
|
|
@@ -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.10",
|
|
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.10"
|
|
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.10",
|
|
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.10"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^20.16.0",
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type TranscriptRead, type TranscriptReader, type TranscriptTool, type TranscriptTurn } from './turns.js';
|
|
2
|
+
export { TranscriptReadError } from './turns.js';
|
|
3
|
+
export type { TranscriptRange, TranscriptRead, TranscriptReader, TranscriptTail, TranscriptTool, TranscriptTurn } from './turns.js';
|
|
2
4
|
export type StreamTool = Readonly<Omit<TranscriptTool, 'output'> & {
|
|
3
5
|
output?: null;
|
|
4
6
|
}>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { TranscriptReadError } from './turns.js';
|
|
2
|
+
// the browser-safe entry is complete on its own: the normalized shape travels with the frames that carry it
|
|
3
|
+
export { TranscriptReadError } from './turns.js';
|
|
2
4
|
export const isErrorFrame = (frame) => 'error' in frame;
|
|
3
5
|
// The revision an absent source reports: the thread has not started writing. Not an error.
|
|
4
6
|
export const ABSENT_REVISION = 'absent';
|
|
@@ -8,6 +8,7 @@ export type MutableTool = {
|
|
|
8
8
|
output?: string;
|
|
9
9
|
outputLines: number;
|
|
10
10
|
outputBytes: number;
|
|
11
|
+
outcome?: ToolOutcome;
|
|
11
12
|
};
|
|
12
13
|
export type MutableTurn = {
|
|
13
14
|
id: string | null;
|
|
@@ -16,18 +17,25 @@ export type MutableTurn = {
|
|
|
16
17
|
text?: string;
|
|
17
18
|
tools: MutableTool[];
|
|
18
19
|
};
|
|
20
|
+
export type ToolOutcome = 'failed' | 'rejected';
|
|
19
21
|
export type ParsedEvent = {
|
|
20
22
|
at: number | null;
|
|
21
23
|
turn: MutableTurn | null;
|
|
22
24
|
toolOutputs?: readonly {
|
|
23
25
|
id: string;
|
|
24
26
|
text: string;
|
|
27
|
+
outcome?: ToolOutcome;
|
|
25
28
|
}[];
|
|
26
29
|
};
|
|
27
30
|
export type Parse = (value: unknown) => ParsedEvent | null;
|
|
28
31
|
export declare function claudeEvent(value: unknown): ParsedEvent | null;
|
|
29
32
|
export declare function codexEvent(value: unknown): ParsedEvent | null;
|
|
33
|
+
export declare function codexAppServerEvent(value: unknown): ParsedEvent | null;
|
|
34
|
+
export declare function codexAppServerStream(): Parse;
|
|
30
35
|
export declare function piEvent(value: unknown): ParsedEvent | null;
|
|
36
|
+
export declare function geminiEvent(value: unknown): ParsedEvent | null;
|
|
37
|
+
export declare function openclawEvent(value: unknown): ParsedEvent | null;
|
|
38
|
+
export declare function hermesEvents(value: unknown): ParsedEvent[];
|
|
31
39
|
export declare function opencodeEvents(value: unknown): ParsedEvent[];
|
|
32
40
|
export declare class IntervalCollector {
|
|
33
41
|
readonly turns: MutableTurn[];
|