@spexcode/spec-cli 0.7.0-next.3 → 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/dist/graphSnapshot.js +4 -1
- package/dist/harness.js +6 -1
- package/dist/host-resources.js +24 -0
- package/dist/sessions.js +13 -6
- package/package.json +7 -7
package/dist/graphSnapshot.js
CHANGED
|
@@ -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),
|
package/dist/harness.js
CHANGED
|
@@ -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);
|
package/dist/host-resources.js
CHANGED
|
@@ -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.
|
package/dist/sessions.js
CHANGED
|
@@ -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(() => {
|
package/package.json
CHANGED
|
@@ -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": {
|