spexcode 0.7.0-next.3 → 0.7.0-next.5
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/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.js +24 -7
- package/node_modules/@spexcode/spec-cli/dist/host-resources.js +35 -0
- package/node_modules/@spexcode/spec-cli/dist/sessions.js +13 -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 +16 -7
- 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.5",
|
|
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.5",
|
|
20
|
+
"@spexcode/session-protocol": "0.7.0-next.5",
|
|
21
|
+
"@spexcode/session-runtime": "0.7.0-next.5",
|
|
22
|
+
"@spexcode/session-topology": "0.7.0-next.5"
|
|
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.5",
|
|
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.5" },
|
|
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.5",
|
|
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.5" },
|
|
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.5",
|
|
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.5",
|
|
28
|
+
"@spexcode/session-runtime": "0.7.0-next.5"
|
|
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.5",
|
|
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.5" },
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/node": "^20.16.0",
|
|
21
21
|
"tsx": "^4.19.2",
|
|
@@ -46,6 +46,10 @@ export declare function prepareCodexGenerationClose(root: string, sessionId: str
|
|
|
46
46
|
export declare function repinCodexGeneration(root: string, sessionId: string, threadId: string, generationId: string): void;
|
|
47
47
|
export declare function resolveCodexGenerationForResume(root: string, sessionId: string, threadId: string, start: (endpoint: CodexGenerationEndpoint) => Promise<void>): Promise<CodexGenerationEndpoint | null>;
|
|
48
48
|
export declare function resolveCodexGenerationForSession(root: string, sessionId: string, threadId: string): CodexGenerationEndpoint | null;
|
|
49
|
+
export declare function resolveCodexGenerationForClose(root: string, sessionId: string, threadId: string): {
|
|
50
|
+
endpoint: CodexGenerationEndpoint;
|
|
51
|
+
gone: boolean;
|
|
52
|
+
} | null;
|
|
49
53
|
export declare function codexGenerationBindingForSession(root: string, sessionId: string): CodexGenerationBinding | null;
|
|
50
54
|
export declare function currentCodexGeneration(root: string): CodexGenerationEndpoint | null;
|
|
51
55
|
export declare function codexGenerationEndpoints(root: string): CodexGenerationEndpoint[];
|
|
@@ -765,6 +765,18 @@ export function resolveCodexGenerationForSession(root, sessionId, threadId) {
|
|
|
765
765
|
const generation = ledger.generations[binding.generationId];
|
|
766
766
|
return generation && generation.state !== 'reclaimed' ? generation.endpoint : null;
|
|
767
767
|
}
|
|
768
|
+
// Close still needs the immutable endpoint record to recognize a positively retired generation. The
|
|
769
|
+
// endpoint is never used for native I/O in this state; it only identifies the empty control plane.
|
|
770
|
+
export function resolveCodexGenerationForClose(root, sessionId, threadId) {
|
|
771
|
+
const ledger = readCodexGenerationLedger(root);
|
|
772
|
+
const binding = ledger.bindings[sessionId];
|
|
773
|
+
if (!binding || binding.threadId !== threadId)
|
|
774
|
+
return null;
|
|
775
|
+
const generation = ledger.generations[binding.generationId];
|
|
776
|
+
if (!generation)
|
|
777
|
+
return null;
|
|
778
|
+
return { endpoint: generation.endpoint, gone: generation.state === 'reclaimed' };
|
|
779
|
+
}
|
|
768
780
|
export function codexGenerationBindingForSession(root, sessionId) {
|
|
769
781
|
return readCodexGenerationLedger(root).bindings[sessionId] ?? null;
|
|
770
782
|
}
|
|
@@ -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),
|
|
@@ -18,7 +18,7 @@ import { runtimeRoot, mainCheckout, readConfig, sessionArtifactPath, spexcodeHom
|
|
|
18
18
|
import { git } from '@spexcode/spec-core';
|
|
19
19
|
import { shQuote } from './sh.js';
|
|
20
20
|
import { detachedRuntimeGenerationToken, migrateLegacyDetachedRuntimeReceipt, processStartToken, verifyDetachedRuntime } from '@spexcode/spec-core';
|
|
21
|
-
import { codexGenerationEndpoints, codexGenerationSocketPath, currentCodexGeneration, legacyCodexGenerationEndpoint, readCodexGenerationLedger, resolveCodexGenerationForSession } from './codex-runtime-generations.js';
|
|
21
|
+
import { codexGenerationEndpoints, codexGenerationSocketPath, currentCodexGeneration, legacyCodexGenerationEndpoint, readCodexGenerationLedger, prepareCodexGenerationClose, resolveCodexGenerationForClose, resolveCodexGenerationForSession } from './codex-runtime-generations.js';
|
|
22
22
|
import { writeFileIfChanged } from './file-write.js';
|
|
23
23
|
import { claudeTranscript, codexRolloutPath, codexTranscript, opencodeTranscript, piTranscript, unsupportedTranscript } from '@spexcode/transcript';
|
|
24
24
|
import { harnessIdentity, HARNESS_IDENTITIES } from '@spexcode/spec-core';
|
|
@@ -220,12 +220,14 @@ function codexMutationGeneration(dir = runtimeRoot(), endpoint = legacyCodexGene
|
|
|
220
220
|
return codexRuntimeGeneration(dir, endpoint);
|
|
221
221
|
}
|
|
222
222
|
const codexDescriptorKey = (endpoint) => endpoint.id === 'legacy' ? 'codex-app-server' : `codex-app-server:${endpoint.id}`;
|
|
223
|
-
function codexEndpointForRecord(rec, dir = runtimeRoot()) {
|
|
223
|
+
function codexEndpointForRecord(rec, dir = runtimeRoot(), includeGone = false) {
|
|
224
224
|
if (!rec.harnessSessionId)
|
|
225
225
|
return null;
|
|
226
226
|
const ledger = readCodexGenerationLedger(dir);
|
|
227
227
|
if (ledger.revision === 0 && !ledger.current && !Object.keys(ledger.generations).length)
|
|
228
228
|
return legacyCodexGenerationEndpoint(dir);
|
|
229
|
+
if (includeGone)
|
|
230
|
+
return resolveCodexGenerationForClose(dir, rec.session, rec.harnessSessionId)?.endpoint ?? null;
|
|
229
231
|
return resolveCodexGenerationForSession(dir, rec.session, rec.harnessSessionId);
|
|
230
232
|
}
|
|
231
233
|
// the spex launcher (bin/spex.mjs), baked into the codex launch script (mirrors materialize.ts's SPEX) so
|
|
@@ -2784,7 +2786,9 @@ const socketListenerLiveness = (_rec, tmuxAlive, _runtimeDir, _pane, socketLive)
|
|
|
2784
2786
|
const socketListenerOrPidAliveLiveness = (_rec, tmuxAlive, _runtimeDir, pane, socketLive) => (tmuxAlive && (!!socketLive || pane?.pidAlive === true) ? 'online' : 'offline');
|
|
2785
2787
|
const panePidLiveness = (_rec, tmuxAlive, _runtimeDir, pane) => (tmuxAlive && pane?.pidAlive === true ? 'online' : 'offline');
|
|
2786
2788
|
const recordOnline = (rec) => rec.stopped ? 'offline' : 'online';
|
|
2787
|
-
|
|
2789
|
+
// Leaf-backed headless sessions are the controller process, not the tmux pane that hosts it. The pane can
|
|
2790
|
+
// survive a SIGKILL as a bare shell, so tmux presence without the launch-registered PID is never online.
|
|
2791
|
+
const sessionHomeLiveness = (_rec, tmuxAlive, _runtimeDir, pane) => (tmuxAlive && pane?.pidAlive === true ? 'online' : 'offline');
|
|
2788
2792
|
// @@@ unlinkSocks - remove ONLY the transport this teardown PROVED dead. `cleanupRuntime` unlinks *their*
|
|
2789
2793
|
// socket, and the honest test of "theirs" is that the agent it just killed is GONE. It used to unlink on
|
|
2790
2794
|
// faith, which is unsound because a socket path is derived from the session id ALONE: it is the one
|
|
@@ -2889,6 +2893,7 @@ export const claudeHeadlessHarness = {
|
|
|
2889
2893
|
paneTitleIsSelfSummary: false,
|
|
2890
2894
|
launchCmd: (id, runtimeDir, cmd) => claudeHeadlessLaunchCommand(id, runtimeDir ?? runtimeRoot(), claudeBaseCmd(cmd)),
|
|
2891
2895
|
launchEnv: noLaunchEnv,
|
|
2896
|
+
// The controller's registered PID is the liveness witness; the tmux home is only the address boundary.
|
|
2892
2897
|
liveness: sessionHomeLiveness,
|
|
2893
2898
|
deliveryTransport: unprovenDeliveryTransport,
|
|
2894
2899
|
deliver: deliverViaClaudeHeadless,
|
|
@@ -3025,7 +3030,7 @@ export const codexHarness = {
|
|
|
3025
3030
|
interrupt: interruptCodexTurn,
|
|
3026
3031
|
cleanupRuntime: async () => { },
|
|
3027
3032
|
targetDescriptorKey: (rec) => {
|
|
3028
|
-
const endpoint = codexEndpointForRecord(rec);
|
|
3033
|
+
const endpoint = codexEndpointForRecord(rec, runtimeRoot(), true);
|
|
3029
3034
|
return endpoint ? codexDescriptorKey(endpoint) : null;
|
|
3030
3035
|
},
|
|
3031
3036
|
coldRetirementPreflight: async (rec) => {
|
|
@@ -3049,8 +3054,13 @@ export const codexHarness = {
|
|
|
3049
3054
|
coldPreflight: async (rec) => {
|
|
3050
3055
|
if (!rec.harnessSessionId)
|
|
3051
3056
|
return { ok: false, reason: 'no exact Codex thread identity is registered' };
|
|
3052
|
-
const
|
|
3053
|
-
|
|
3057
|
+
const dir = runtimeRoot();
|
|
3058
|
+
const binding = resolveCodexGenerationForClose(dir, rec.session, rec.harnessSessionId);
|
|
3059
|
+
if (binding?.gone) {
|
|
3060
|
+
return { ok: true, alreadyCold: true };
|
|
3061
|
+
}
|
|
3062
|
+
const endpoint = binding?.endpoint ?? codexEndpointForRecord(rec, dir);
|
|
3063
|
+
return endpoint ? codexColdPreflight(rec.harnessSessionId, dir, undefined, endpoint)
|
|
3054
3064
|
: { ok: false, reason: 'no exact Codex generation binding is registered for this target' };
|
|
3055
3065
|
},
|
|
3056
3066
|
coldRuntime: async (rec, suppliedReceipt) => {
|
|
@@ -3058,7 +3068,12 @@ export const codexHarness = {
|
|
|
3058
3068
|
return { ok: false, reason: 'no exact Codex thread identity is registered' };
|
|
3059
3069
|
const threadId = rec.harnessSessionId;
|
|
3060
3070
|
const dir = runtimeRoot();
|
|
3061
|
-
const
|
|
3071
|
+
const binding = resolveCodexGenerationForClose(dir, rec.session, threadId);
|
|
3072
|
+
if (binding?.gone) {
|
|
3073
|
+
prepareCodexGenerationClose(dir, rec.session, threadId);
|
|
3074
|
+
return { ok: true };
|
|
3075
|
+
}
|
|
3076
|
+
const endpoint = binding?.endpoint ?? codexEndpointForRecord(rec, dir);
|
|
3062
3077
|
if (!endpoint)
|
|
3063
3078
|
return { ok: false, reason: 'no exact Codex generation binding is registered for this target' };
|
|
3064
3079
|
const sock = endpoint.socketPath;
|
|
@@ -3400,6 +3415,7 @@ export const piHeadlessHarness = {
|
|
|
3400
3415
|
runtimeOwnership: 'leaf',
|
|
3401
3416
|
paneTitleIsSelfSummary: false,
|
|
3402
3417
|
launchCmd: (id, runtimeDir, cmd) => piHeadlessLaunchCommand(id, runtimeDir ?? runtimeRoot(), piBaseCmd(cmd)),
|
|
3418
|
+
// The controller's registered PID is the liveness witness; the tmux home is only the address boundary.
|
|
3403
3419
|
liveness: sessionHomeLiveness,
|
|
3404
3420
|
deliver: deliverViaPiHeadless,
|
|
3405
3421
|
// the controller aborts its own turn child natively and confirms only once that child is gone
|
|
@@ -3513,6 +3529,7 @@ export const opencodeHeadlessHarness = {
|
|
|
3513
3529
|
runtimeOwnership: 'leaf',
|
|
3514
3530
|
deliveryTransport: unprovenDeliveryTransport,
|
|
3515
3531
|
launchCmd: (_id, _runtimeDir, cmd) => opencodeHeadlessLaunchCommand(opencodeBaseCmd(cmd)),
|
|
3532
|
+
// The controller's registered PID is the liveness witness; the tmux home is only the address boundary.
|
|
3516
3533
|
liveness: sessionHomeLiveness,
|
|
3517
3534
|
coldRuntime: async (rec) => {
|
|
3518
3535
|
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)
|
|
@@ -448,6 +471,17 @@ const sessionStopBlocker = async (id, harnessId, recs = rawRecords(), knownProbe
|
|
|
448
471
|
const entryTargetThread = entry.recs.find((rec) => rec.session_id === id)?.harness_session_id;
|
|
449
472
|
if (entryTargetThread && ownerCounts.get(entryTargetThread) !== 1)
|
|
450
473
|
return `${descriptor.label} target thread ${entryTargetThread} has no one exact governed session owner`;
|
|
474
|
+
// A retired shared generation is positive death, not an unknown control plane. Its residency census is
|
|
475
|
+
// the only proof needed here; there is no native target left to signal or unload.
|
|
476
|
+
if (descriptor.residency && entryTargetThread) {
|
|
477
|
+
let residency = null;
|
|
478
|
+
try {
|
|
479
|
+
residency = await descriptor.residency();
|
|
480
|
+
}
|
|
481
|
+
catch { /* retain the normal fail-closed path */ }
|
|
482
|
+
if (residency?.healthy && residency.rootAbsent === true && residency.referenceIds.length === 0)
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
451
485
|
if (!knownProbes && descriptor.mutationGuard) {
|
|
452
486
|
if (!targetThread)
|
|
453
487
|
return `${descriptor.label} target has no exact governed thread identity`;
|
|
@@ -722,6 +756,7 @@ export async function collectResourceReport(opts = {}) {
|
|
|
722
756
|
budget: { rssMiB: budgets.sessionRssMiB, idleCpuPercent: budgets.idleCpuPercent },
|
|
723
757
|
findings: [`session-record-corrupt:${entry.error}`], reclaim: { eligible: false, reason: 'session record is corrupt; ownership and lifecycle are unknown' },
|
|
724
758
|
});
|
|
759
|
+
owners.push(...missingHeadlessControllerOwners(inv.recs, inv.procs, budgets, (sessionId) => owners.some((owner) => owner.kind === 'session' && owner.id === sessionId)));
|
|
725
760
|
owners.push(...unresolvedHarnessOwners(inv.recs, budgets, (sessionId) => owners.some((owner) => owner.kind === 'session' && owner.id === sessionId)));
|
|
726
761
|
// A referenced shared runtime with no readable process is still operationally important: keep its live or
|
|
727
762
|
// unknown refcount visible instead of silently omitting it from a process-derived report.
|
|
@@ -2538,12 +2538,19 @@ async function establishBackendConnection(target) {
|
|
|
2538
2538
|
socket.destroy();
|
|
2539
2539
|
fn();
|
|
2540
2540
|
};
|
|
2541
|
-
const timer = setTimeout(() =>
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
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);
|
|
2547
2554
|
timer.unref?.();
|
|
2548
2555
|
socket.once('connect', () => finish(() => resolve(false)));
|
|
2549
2556
|
socket.once('error', (error) => finish(() => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/spec-cli",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.5",
|
|
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.5",
|
|
39
|
+
"@spexcode/session-selflaunch": "0.7.0-next.5",
|
|
40
|
+
"@spexcode/spec-core": "0.7.0-next.5",
|
|
41
|
+
"@spexcode/spec-eval": "0.7.0-next.5",
|
|
42
|
+
"@spexcode/spec-forge": "0.7.0-next.5",
|
|
43
|
+
"@spexcode/transcript": "0.7.0-next.5",
|
|
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.5",
|
|
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.5"
|
|
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.5",
|
|
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.5"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^20.16.0",
|
|
@@ -104,6 +104,9 @@ export function claudeEvent(value) {
|
|
|
104
104
|
}
|
|
105
105
|
return null;
|
|
106
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;
|
|
107
110
|
export function codexEvent(value) {
|
|
108
111
|
const entry = object(value);
|
|
109
112
|
const payload = object(entry?.payload);
|
|
@@ -113,16 +116,25 @@ export function codexEvent(value) {
|
|
|
113
116
|
if (eventAt === null)
|
|
114
117
|
return { at: null, turn: null };
|
|
115
118
|
const type = string(payload.type);
|
|
116
|
-
|
|
117
|
-
|
|
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') {
|
|
118
123
|
const text = typeof payload.message === 'string' ? payload.message : compact(payload.message ?? payload.content ?? '');
|
|
119
124
|
return text ? { at: eventAt, turn: { id: idOf(payload) ?? idOf(entry), at: eventAt, role: 'user', text, tools: [] } } : null;
|
|
120
125
|
}
|
|
121
|
-
|
|
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.
|
|
122
132
|
if (entry.type === 'event_msg' && type === 'agent_message') {
|
|
123
133
|
const text = string(payload.message ?? payload.text);
|
|
124
|
-
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 };
|
|
125
135
|
}
|
|
136
|
+
if (entry.type === 'response_item' && type === 'message' && payload.role === 'assistant')
|
|
137
|
+
return { at: eventAt, turn: null };
|
|
126
138
|
if (entry.type === 'response_item' && (type === 'custom_tool_call' || type === 'function_call')) {
|
|
127
139
|
const id = string(payload.call_id ?? payload.id) ?? 'tool';
|
|
128
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 }] } };
|
|
@@ -229,9 +241,6 @@ export function codexAppServerStream() {
|
|
|
229
241
|
return parsed;
|
|
230
242
|
};
|
|
231
243
|
}
|
|
232
|
-
const blockText = (content) => typeof content === 'string'
|
|
233
|
-
? string(content)
|
|
234
|
-
: items(content).map((block) => string(object(block)?.text)).filter(Boolean).join('\n') || null;
|
|
235
244
|
export function piEvent(value) {
|
|
236
245
|
const entry = object(value);
|
|
237
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.5",
|
|
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.5",
|
|
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.5"
|
|
48
48
|
},
|
|
49
49
|
"publishConfig": {
|
|
50
50
|
"access": "public"
|