spexcode 0.6.7 → 0.6.8
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/README.md +12 -7
- package/node_modules/@spexcode/session-application/dist/index.d.ts +17 -0
- package/node_modules/@spexcode/session-application/dist/index.js +35 -0
- package/node_modules/@spexcode/session-application/dist/migration.d.ts +51 -0
- package/node_modules/@spexcode/session-application/dist/migration.js +694 -0
- package/node_modules/@spexcode/session-application/dist/production.d.ts +109 -0
- package/node_modules/@spexcode/session-application/dist/production.js +438 -0
- package/node_modules/@spexcode/session-application/dist/schema.d.ts +2 -0
- package/node_modules/@spexcode/session-application/dist/schema.js +39 -0
- package/node_modules/@spexcode/session-application/package.json +29 -0
- package/node_modules/@spexcode/session-events/dist/errors.d.ts +6 -0
- package/node_modules/@spexcode/session-events/dist/errors.js +11 -0
- package/node_modules/@spexcode/session-events/dist/index.d.ts +46 -0
- package/node_modules/@spexcode/session-events/dist/index.js +245 -0
- package/node_modules/@spexcode/session-events/dist/schema.d.ts +3 -0
- package/node_modules/@spexcode/session-events/dist/schema.js +44 -0
- package/node_modules/@spexcode/session-events/package.json +24 -0
- package/node_modules/@spexcode/session-protocol/dist/canonical.d.ts +36 -0
- package/node_modules/@spexcode/session-protocol/dist/canonical.js +139 -0
- package/node_modules/@spexcode/session-protocol/dist/engine.d.ts +22 -0
- package/node_modules/@spexcode/session-protocol/dist/engine.js +478 -0
- package/node_modules/@spexcode/session-protocol/dist/errors.d.ts +8 -0
- package/node_modules/@spexcode/session-protocol/dist/errors.js +39 -0
- package/node_modules/@spexcode/session-protocol/dist/index.d.ts +60 -0
- package/node_modules/@spexcode/session-protocol/dist/index.js +4 -0
- package/node_modules/@spexcode/session-protocol/dist/schema.d.ts +36 -0
- package/node_modules/@spexcode/session-protocol/dist/schema.js +192 -0
- package/node_modules/@spexcode/{session-core → session-protocol}/package.json +4 -8
- package/node_modules/@spexcode/session-runtime/dist/errors.d.ts +6 -0
- package/node_modules/@spexcode/session-runtime/dist/errors.js +11 -0
- package/node_modules/@spexcode/session-runtime/dist/index.d.ts +33 -0
- package/node_modules/@spexcode/session-runtime/dist/index.js +193 -0
- package/node_modules/@spexcode/session-runtime/dist/schema.d.ts +2 -0
- package/node_modules/@spexcode/session-runtime/dist/schema.js +37 -0
- package/node_modules/@spexcode/session-runtime/package.json +24 -0
- package/node_modules/@spexcode/session-selflaunch/bin/spex-session.mjs +4 -0
- package/node_modules/@spexcode/session-selflaunch/dist/cli.d.ts +25 -0
- package/node_modules/@spexcode/session-selflaunch/dist/cli.js +156 -0
- package/node_modules/@spexcode/session-selflaunch/dist/index.d.ts +28 -0
- package/node_modules/@spexcode/session-selflaunch/dist/index.js +25 -0
- package/node_modules/@spexcode/session-selflaunch/dist/locality.d.ts +20 -0
- package/node_modules/@spexcode/session-selflaunch/dist/locality.js +83 -0
- package/node_modules/@spexcode/session-selflaunch/dist/path.d.ts +13 -0
- package/node_modules/@spexcode/session-selflaunch/dist/path.js +45 -0
- package/node_modules/@spexcode/session-selflaunch/package.json +35 -0
- package/node_modules/@spexcode/session-topology/dist/errors.d.ts +6 -0
- package/node_modules/@spexcode/session-topology/dist/errors.js +11 -0
- package/node_modules/@spexcode/session-topology/dist/index.d.ts +24 -0
- package/node_modules/@spexcode/session-topology/dist/index.js +173 -0
- package/node_modules/@spexcode/session-topology/dist/schema.d.ts +3 -0
- package/node_modules/@spexcode/session-topology/dist/schema.js +27 -0
- package/node_modules/@spexcode/session-topology/package.json +24 -0
- package/node_modules/@spexcode/spec-cli/bin/spex.mjs +56 -19
- package/node_modules/@spexcode/spec-cli/dist/cli.js +102 -59
- package/node_modules/@spexcode/spec-cli/dist/client.d.ts +1 -3
- package/node_modules/@spexcode/spec-cli/dist/client.js +49 -30
- package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.d.ts +11 -0
- package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.js +46 -9
- package/node_modules/@spexcode/spec-cli/dist/delivery-lock.d.ts +2 -0
- package/node_modules/@spexcode/spec-cli/dist/delivery-lock.js +58 -0
- package/node_modules/@spexcode/spec-cli/dist/doctor.js +53 -11
- package/node_modules/@spexcode/spec-cli/dist/execution-trace.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/execution-trace.js +2 -2
- package/node_modules/@spexcode/spec-cli/dist/gateway-hub.js +2 -1
- package/node_modules/@spexcode/spec-cli/dist/gateway.js +6 -3
- package/node_modules/@spexcode/spec-cli/dist/graphCache.js +32 -2
- package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.js +57 -2
- package/node_modules/@spexcode/spec-cli/dist/graphStream.d.ts +2 -0
- package/node_modules/@spexcode/spec-cli/dist/graphStream.js +83 -3
- package/node_modules/@spexcode/spec-cli/dist/guide.js +20 -7
- package/node_modules/@spexcode/spec-cli/dist/harness-select.js +16 -3
- package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +15 -3
- package/node_modules/@spexcode/spec-cli/dist/harness.js +331 -50
- package/node_modules/@spexcode/spec-cli/dist/help.js +11 -8
- package/node_modules/@spexcode/spec-cli/dist/hook-prompts.js +8 -0
- package/node_modules/@spexcode/spec-cli/dist/host-resources.js +29 -8
- package/node_modules/@spexcode/spec-cli/dist/host.d.ts +7 -0
- package/node_modules/@spexcode/spec-cli/dist/host.js +93 -0
- package/node_modules/@spexcode/spec-cli/dist/index.js +324 -22
- package/node_modules/@spexcode/spec-cli/dist/init.js +1 -1
- package/node_modules/@spexcode/spec-cli/dist/lint.js +70 -35
- package/node_modules/@spexcode/spec-cli/dist/listen.d.ts +3 -2
- package/node_modules/@spexcode/spec-cli/dist/listen.js +14 -2
- package/node_modules/@spexcode/spec-cli/dist/machine-peer.js +1 -1
- package/node_modules/@spexcode/spec-cli/dist/materialize.d.ts +2 -2
- package/node_modules/@spexcode/spec-cli/dist/materialize.js +176 -35
- package/node_modules/@spexcode/spec-cli/dist/pty-bridge.js +14 -14
- package/node_modules/@spexcode/spec-cli/dist/reviews.js +12 -7
- package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.d.ts +11 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.js +79 -1
- package/node_modules/@spexcode/spec-cli/dist/session-application.d.ts +23 -0
- package/node_modules/@spexcode/spec-cli/dist/session-application.js +189 -0
- package/node_modules/@spexcode/spec-cli/dist/session-declarations.js +13 -1
- package/node_modules/@spexcode/spec-cli/dist/session-files.d.ts +6 -0
- package/node_modules/@spexcode/spec-cli/dist/session-files.js +13 -1
- package/node_modules/@spexcode/spec-cli/dist/session-follow.js +39 -22
- package/node_modules/@spexcode/{session-core/dist/record-lock.d.ts → spec-cli/dist/session-record-lock.d.ts} +0 -4
- package/node_modules/@spexcode/spec-cli/dist/session-record-lock.js +94 -0
- package/node_modules/@spexcode/spec-cli/dist/session-runtime-adapter.d.ts +44 -0
- package/node_modules/@spexcode/spec-cli/dist/session-runtime-adapter.js +37 -0
- package/node_modules/@spexcode/spec-cli/dist/session-timeline.d.ts +25 -2
- package/node_modules/@spexcode/spec-cli/dist/session-timeline.js +68 -11
- package/node_modules/@spexcode/spec-cli/dist/session-web.js +4 -4
- package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +108 -15
- package/node_modules/@spexcode/spec-cli/dist/sessions.js +1465 -744
- package/node_modules/@spexcode/spec-cli/dist/source-list.d.ts +13 -0
- package/node_modules/@spexcode/spec-cli/dist/source-list.js +99 -0
- package/node_modules/@spexcode/spec-cli/dist/source-read.d.ts +16 -0
- package/node_modules/@spexcode/spec-cli/dist/source-read.js +84 -0
- package/node_modules/@spexcode/spec-cli/dist/spec-attachments.d.ts +7 -0
- package/node_modules/@spexcode/spec-cli/dist/spec-attachments.js +89 -0
- package/node_modules/@spexcode/spec-cli/dist/spec-body-edit.d.ts +23 -0
- package/node_modules/@spexcode/spec-cli/dist/spec-body-edit.js +138 -0
- package/node_modules/@spexcode/spec-cli/dist/supervise.js +15 -6
- package/node_modules/@spexcode/spec-cli/dist/transcript-reader.d.ts +36 -0
- package/node_modules/@spexcode/spec-cli/dist/transcript-reader.js +251 -0
- package/node_modules/@spexcode/spec-cli/hooks/dispatch.sh +19 -31
- package/node_modules/@spexcode/spec-cli/hooks/harness.sh +6 -6
- package/node_modules/@spexcode/spec-cli/package.json +6 -6
- package/node_modules/@spexcode/spec-cli/templates/hooks/post-checkout +4 -2
- package/node_modules/@spexcode/spec-cli/templates/hooks/post-merge +2 -1
- package/node_modules/@spexcode/spec-cli/templates/hooks/pre-commit +5 -3
- package/node_modules/@spexcode/spec-cli/templates/hooks/reference-transaction +5 -3
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/commands/spec.md +2 -7
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/idle/idle.sh +4 -10
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/idle/spec.md +1 -1
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/mark-active/mark-active.sh +22 -24
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/mark-active/spec.md +10 -2
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-fail/fail.sh +8 -7
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-fail/spec.md +3 -1
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-listen/session-listen.sh +133 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-listen/spec.md +36 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/spec.md +2 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/stop-gate/spec.md +1 -1
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +17 -20
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/skills/merge/spec.md +33 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/skills/spec.md +2 -6
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/spec.md +7 -0
- package/node_modules/@spexcode/spec-core/dist/anchors.d.ts +13 -2
- package/node_modules/@spexcode/spec-core/dist/anchors.js +311 -15
- package/node_modules/@spexcode/spec-core/dist/git.d.ts +4 -0
- package/node_modules/@spexcode/spec-core/dist/git.js +9 -9
- package/node_modules/@spexcode/spec-core/dist/layout.d.ts +12 -0
- package/node_modules/@spexcode/spec-core/dist/layout.js +4 -2
- package/node_modules/@spexcode/spec-core/dist/resilience.d.ts +1 -0
- package/node_modules/@spexcode/spec-core/dist/resilience.js +8 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.d.ts +1 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.js +7 -3
- package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.d.ts +1 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.js +4 -0
- package/node_modules/@spexcode/spec-core/dist/specs.d.ts +2 -0
- package/node_modules/@spexcode/spec-core/dist/specs.js +30 -0
- package/node_modules/@spexcode/spec-core/package.json +4 -1
- package/node_modules/@spexcode/spec-eval/dist/cli.js +1 -1
- package/node_modules/@spexcode/spec-eval/dist/evaltab.d.ts +1 -0
- package/node_modules/@spexcode/spec-eval/dist/evaltab.js +6 -1
- package/node_modules/@spexcode/spec-eval/dist/freshness.d.ts +1 -1
- package/node_modules/@spexcode/spec-eval/dist/freshness.js +12 -12
- package/node_modules/@spexcode/spec-eval/dist/host.d.ts +2 -1
- package/node_modules/@spexcode/spec-eval/dist/sessioneval.d.ts +2 -1
- package/node_modules/@spexcode/spec-eval/dist/sessioneval.js +20 -3
- package/node_modules/@spexcode/spec-eval/package.json +2 -2
- package/node_modules/@spexcode/spec-forge/package.json +2 -2
- package/node_modules/@vscode/tree-sitter-wasm/LICENSE +21 -0
- package/node_modules/@vscode/tree-sitter-wasm/README.md +36 -0
- package/node_modules/@vscode/tree-sitter-wasm/SECURITY.md +41 -0
- package/node_modules/@vscode/tree-sitter-wasm/cgmanifest.json +16 -0
- package/node_modules/@vscode/tree-sitter-wasm/package.json +42 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-bash.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-c-sharp.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-cpp.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-css.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-go.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ini.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-java.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-javascript.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-php.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-powershell.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-python.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-regex.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ruby.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-rust.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-tsx.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-typescript.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.js +4075 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/web-tree-sitter.d.ts +1027 -0
- package/package.json +9 -3
- package/node_modules/@spexcode/session-core/dist/delivery-queue.d.ts +0 -24
- package/node_modules/@spexcode/session-core/dist/delivery-queue.js +0 -206
- package/node_modules/@spexcode/session-core/dist/index.d.ts +0 -5
- package/node_modules/@spexcode/session-core/dist/index.js +0 -5
- package/node_modules/@spexcode/session-core/dist/internal.d.ts +0 -3
- package/node_modules/@spexcode/session-core/dist/internal.js +0 -3
- package/node_modules/@spexcode/session-core/dist/message.d.ts +0 -22
- package/node_modules/@spexcode/session-core/dist/message.js +0 -53
- package/node_modules/@spexcode/session-core/dist/record-lock.js +0 -152
- package/node_modules/@spexcode/session-core/dist/runtime-session.d.ts +0 -62
- package/node_modules/@spexcode/session-core/dist/runtime-session.js +0 -326
- package/node_modules/@spexcode/session-core/dist/session-cursors.d.ts +0 -14
- package/node_modules/@spexcode/session-core/dist/session-cursors.js +0 -82
- package/node_modules/@spexcode/session-core/dist/session-timeline.d.ts +0 -47
- package/node_modules/@spexcode/session-core/dist/session-timeline.js +0 -216
- package/node_modules/@spexcode/spec-cli/hooks/compat/mark-active-0.5.2-eef1.fixture +0 -53
- package/node_modules/@spexcode/spec-cli/hooks/compat/mark-active-sed-v0.fixture +0 -46
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { isSessionLifecycle, isSessionProposal, listSessionIds, readRecordEntry, sessionArtifactPath, sessionRecordPath, sessionStoreDir, } from '@spexcode/spec-core';
|
|
5
|
-
import { enqueue, ensurePendingWhileLocked, withDeliveryLocks } from './delivery-queue.js';
|
|
6
|
-
import { withSessionRecordLocks } from './record-lock.js';
|
|
7
|
-
import { appendSent, recordStatus, sentDispatchReceipt } from './session-timeline.js';
|
|
8
|
-
export class RuntimeSessionConflict extends Error {
|
|
9
|
-
code = 'runtime_session_conflict';
|
|
10
|
-
constructor(message) {
|
|
11
|
-
super(message);
|
|
12
|
-
this.name = 'RuntimeSessionConflict';
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
const digest = (value) => createHash('sha256').update(value).digest('hex');
|
|
16
|
-
const watchPath = (target) => sessionArtifactPath(target, 'watchers.json');
|
|
17
|
-
const RUNTIME_NOTIFICATION_KIND = 'spex.runtime-state.v1';
|
|
18
|
-
function scalar(value, field) {
|
|
19
|
-
const normalized = value.trim();
|
|
20
|
-
if (!normalized)
|
|
21
|
-
throw new RuntimeSessionConflict(`${field} must be a non-empty string`);
|
|
22
|
-
return normalized;
|
|
23
|
-
}
|
|
24
|
-
function metadata(value) {
|
|
25
|
-
if (!value)
|
|
26
|
-
return {};
|
|
27
|
-
const entries = Object.entries(value).map(([rawKey, rawValue]) => {
|
|
28
|
-
const key = scalar(rawKey, 'runtimeMetadata key');
|
|
29
|
-
if (typeof rawValue !== 'string')
|
|
30
|
-
throw new RuntimeSessionConflict(`runtimeMetadata.${key} must be a string`);
|
|
31
|
-
return [key, rawValue];
|
|
32
|
-
}).sort(([left], [right]) => left.localeCompare(right));
|
|
33
|
-
if (new Set(entries.map(([key]) => key)).size !== entries.length)
|
|
34
|
-
throw new RuntimeSessionConflict('runtimeMetadata keys must be unique after normalization');
|
|
35
|
-
return Object.fromEntries(entries);
|
|
36
|
-
}
|
|
37
|
-
function metadataKey(value) {
|
|
38
|
-
return JSON.stringify(metadata(value));
|
|
39
|
-
}
|
|
40
|
-
function readRaw(id) {
|
|
41
|
-
const entry = readRecordEntry(id);
|
|
42
|
-
if (entry.kind === 'absent')
|
|
43
|
-
return null;
|
|
44
|
-
if (entry.kind === 'corrupt')
|
|
45
|
-
throw new RuntimeSessionConflict(`session ${id} record is corrupt: ${entry.error}`);
|
|
46
|
-
return entry.raw;
|
|
47
|
-
}
|
|
48
|
-
function runtimeRecord(raw) {
|
|
49
|
-
const runtimeOwner = raw.runtime_owner?.trim();
|
|
50
|
-
if (!runtimeOwner)
|
|
51
|
-
throw new RuntimeSessionConflict(`session ${raw.session_id} is not owned by an external runtime`);
|
|
52
|
-
if (!isSessionLifecycle(raw.status))
|
|
53
|
-
throw new RuntimeSessionConflict(`session ${raw.session_id} has invalid lifecycle ${JSON.stringify(raw.status)}`);
|
|
54
|
-
if (!(raw.proposal == null || raw.proposal === '' || isSessionProposal(raw.proposal)))
|
|
55
|
-
throw new RuntimeSessionConflict(`session ${raw.session_id} has invalid proposal ${JSON.stringify(raw.proposal)}`);
|
|
56
|
-
return {
|
|
57
|
-
sessionId: raw.session_id,
|
|
58
|
-
runtimeOwner,
|
|
59
|
-
runtimeState: raw.runtime_state?.trim() || null,
|
|
60
|
-
revision: raw.runtime_revision?.trim() || null,
|
|
61
|
-
worktreePath: raw.worktree_path,
|
|
62
|
-
branch: raw.branch || null,
|
|
63
|
-
parentSessionId: raw.parent || null,
|
|
64
|
-
title: raw.title || null,
|
|
65
|
-
node: raw.node || null,
|
|
66
|
-
runtimeMetadata: metadata(raw.runtime_metadata),
|
|
67
|
-
lifecycle: raw.status,
|
|
68
|
-
proposal: isSessionProposal(raw.proposal) ? raw.proposal : null,
|
|
69
|
-
note: raw.note || null,
|
|
70
|
-
createdAt: Number(raw.createdAt) || 0,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
function writeRaw(raw) {
|
|
74
|
-
const dir = sessionStoreDir(raw.session_id);
|
|
75
|
-
mkdirSync(dir, { recursive: true });
|
|
76
|
-
const path = sessionRecordPath(raw.session_id);
|
|
77
|
-
const tmp = join(dir, `.session.json.${process.pid}.${randomUUID()}.tmp`);
|
|
78
|
-
writeFileSync(tmp, JSON.stringify(raw, null, 2) + '\n');
|
|
79
|
-
renameSync(tmp, path);
|
|
80
|
-
}
|
|
81
|
-
function readWatches(target) {
|
|
82
|
-
let parsed;
|
|
83
|
-
try {
|
|
84
|
-
parsed = JSON.parse(readFileSync(watchPath(target), 'utf8'));
|
|
85
|
-
}
|
|
86
|
-
catch (error) {
|
|
87
|
-
if (error.code === 'ENOENT')
|
|
88
|
-
return [];
|
|
89
|
-
throw new RuntimeSessionConflict(`session ${target} watch record is unreadable: ${error instanceof Error ? error.message : String(error)}`);
|
|
90
|
-
}
|
|
91
|
-
if (!Array.isArray(parsed))
|
|
92
|
-
throw new RuntimeSessionConflict(`session ${target} watch record is not an array`);
|
|
93
|
-
return parsed.map((candidate, index) => {
|
|
94
|
-
if (!candidate || typeof candidate !== 'object')
|
|
95
|
-
throw new RuntimeSessionConflict(`session ${target} watch row ${index} is invalid`);
|
|
96
|
-
const row = candidate;
|
|
97
|
-
if (typeof row.watcher !== 'string' || typeof row.createdAt !== 'string' || !Array.isArray(row.sources)
|
|
98
|
-
|| row.sources.some((source) => source !== 'manual' && source !== 'parent')
|
|
99
|
-
|| (row.snapshotPending !== undefined && typeof row.snapshotPending !== 'string'))
|
|
100
|
-
throw new RuntimeSessionConflict(`session ${target} watch row ${index} is invalid`);
|
|
101
|
-
return row;
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
function writeWatches(target, entries) {
|
|
105
|
-
const dir = sessionStoreDir(target);
|
|
106
|
-
mkdirSync(dir, { recursive: true });
|
|
107
|
-
const path = watchPath(target);
|
|
108
|
-
const tmp = join(dir, `.watchers.json.${process.pid}.${randomUUID()}.tmp`);
|
|
109
|
-
writeFileSync(tmp, JSON.stringify(entries, null, 2) + '\n');
|
|
110
|
-
renameSync(tmp, path);
|
|
111
|
-
}
|
|
112
|
-
function registrationRaw(input) {
|
|
113
|
-
const sessionId = scalar(input.sessionId, 'sessionId');
|
|
114
|
-
const runtimeOwner = scalar(input.runtimeOwner, 'runtimeOwner');
|
|
115
|
-
return {
|
|
116
|
-
session_id: sessionId,
|
|
117
|
-
governed: false,
|
|
118
|
-
worktree_path: scalar(input.worktreePath, 'worktreePath'),
|
|
119
|
-
branch: input.branch ?? '',
|
|
120
|
-
node: input.node?.trim() || '',
|
|
121
|
-
title: input.title?.trim() || '',
|
|
122
|
-
name: '',
|
|
123
|
-
parent: input.parentSessionId?.trim() || '',
|
|
124
|
-
status: 'active',
|
|
125
|
-
proposal: '',
|
|
126
|
-
merges: 0,
|
|
127
|
-
note: '',
|
|
128
|
-
sortkey: null,
|
|
129
|
-
createdAt: input.createdAt ?? Date.now(),
|
|
130
|
-
harness: runtimeOwner,
|
|
131
|
-
harness_session_id: sessionId,
|
|
132
|
-
stopped: false,
|
|
133
|
-
archived: false,
|
|
134
|
-
cold_proof: '',
|
|
135
|
-
adapter_recovery: '',
|
|
136
|
-
launcher: '',
|
|
137
|
-
launch_cmd: '',
|
|
138
|
-
create_request_id: '',
|
|
139
|
-
create_payload_hash: '',
|
|
140
|
-
runtime_owner: runtimeOwner,
|
|
141
|
-
runtime_state: 'registered',
|
|
142
|
-
runtime_revision: '',
|
|
143
|
-
runtime_metadata: metadata(input.runtimeMetadata),
|
|
144
|
-
launch_readiness_pending: '',
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
function sameRegistration(raw, input) {
|
|
148
|
-
return raw.runtime_owner === input.runtimeOwner.trim()
|
|
149
|
-
&& raw.worktree_path === input.worktreePath.trim()
|
|
150
|
-
&& (raw.branch || null) === (input.branch || null)
|
|
151
|
-
&& (raw.parent || null) === (input.parentSessionId?.trim() || null)
|
|
152
|
-
&& metadataKey(raw.runtime_metadata) === metadataKey(input.runtimeMetadata);
|
|
153
|
-
}
|
|
154
|
-
export async function registerRuntimeSession(input) {
|
|
155
|
-
const id = scalar(input.sessionId, 'sessionId');
|
|
156
|
-
const parent = input.parentSessionId?.trim() || null;
|
|
157
|
-
return withSessionRecordLocks([id, ...(parent ? [parent] : [])], async () => {
|
|
158
|
-
if (parent) {
|
|
159
|
-
const parentRaw = readRaw(parent);
|
|
160
|
-
if (!parentRaw)
|
|
161
|
-
throw new RuntimeSessionConflict(`parent runtime session ${parent} is not registered`);
|
|
162
|
-
if (parentRaw.runtime_owner !== input.runtimeOwner.trim())
|
|
163
|
-
throw new RuntimeSessionConflict(`parent runtime session ${parent} belongs to another runtime`);
|
|
164
|
-
}
|
|
165
|
-
const existing = readRaw(id);
|
|
166
|
-
if (existing) {
|
|
167
|
-
if (!sameRegistration(existing, input))
|
|
168
|
-
throw new RuntimeSessionConflict(`session ${id} is already registered with different runtime coordinates`);
|
|
169
|
-
return { replayed: true };
|
|
170
|
-
}
|
|
171
|
-
writeRaw(registrationRaw(input));
|
|
172
|
-
if (parent)
|
|
173
|
-
writeWatches(id, [{
|
|
174
|
-
watcher: parent,
|
|
175
|
-
createdAt: new Date().toISOString(),
|
|
176
|
-
sources: ['parent'],
|
|
177
|
-
snapshotPending: randomUUID(),
|
|
178
|
-
}]);
|
|
179
|
-
return { replayed: false };
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
function stateMessage(record) {
|
|
183
|
-
const proposal = record.lifecycle === 'awaiting' && record.proposal ? `/${record.proposal}` : '';
|
|
184
|
-
const note = record.note ? ` — ${record.note}` : '';
|
|
185
|
-
return `[spex watch] ${record.sessionId} is ${record.runtimeState ?? record.lifecycle}${proposal}${note}`;
|
|
186
|
-
}
|
|
187
|
-
function notificationAttributes(record) {
|
|
188
|
-
if (!record.revision || !record.runtimeState)
|
|
189
|
-
throw new RuntimeSessionConflict(`runtime session ${record.sessionId} has no publishable state revision`);
|
|
190
|
-
return {
|
|
191
|
-
kind: RUNTIME_NOTIFICATION_KIND,
|
|
192
|
-
runtimeOwner: record.runtimeOwner,
|
|
193
|
-
runtimeState: record.runtimeState,
|
|
194
|
-
revision: record.revision,
|
|
195
|
-
lifecycle: record.lifecycle,
|
|
196
|
-
proposal: record.proposal ?? '',
|
|
197
|
-
note: record.note ?? '',
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
function pendingFor(receipt, mid) {
|
|
201
|
-
return {
|
|
202
|
-
mid,
|
|
203
|
-
text: receipt.delivery.text,
|
|
204
|
-
from: receipt.delivery.from,
|
|
205
|
-
...(receipt.delivery.attributes ? { attributes: receipt.delivery.attributes } : {}),
|
|
206
|
-
dispatch: { operation: receipt.operation, requestDigest: receipt.requestDigest },
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
export function runtimeSessionNotification(parentSessionId, message) {
|
|
210
|
-
const parent = scalar(parentSessionId, 'parentSessionId');
|
|
211
|
-
const child = message.from?.trim();
|
|
212
|
-
const attributes = message.attributes;
|
|
213
|
-
if (!child || !attributes || attributes.kind !== RUNTIME_NOTIFICATION_KIND)
|
|
214
|
-
return null;
|
|
215
|
-
const record = readRuntimeSession(child);
|
|
216
|
-
if (!record || record.parentSessionId !== parent || record.runtimeOwner !== attributes.runtimeOwner)
|
|
217
|
-
return null;
|
|
218
|
-
if (!attributes.revision || !attributes.runtimeState || !isSessionLifecycle(attributes.lifecycle))
|
|
219
|
-
return null;
|
|
220
|
-
if (attributes.proposal && !isSessionProposal(attributes.proposal))
|
|
221
|
-
return null;
|
|
222
|
-
return {
|
|
223
|
-
childSessionId: child,
|
|
224
|
-
runtimeOwner: attributes.runtimeOwner,
|
|
225
|
-
runtimeState: attributes.runtimeState,
|
|
226
|
-
revision: attributes.revision,
|
|
227
|
-
lifecycle: attributes.lifecycle,
|
|
228
|
-
proposal: isSessionProposal(attributes.proposal) ? attributes.proposal : null,
|
|
229
|
-
note: attributes.note || null,
|
|
230
|
-
runtimeMetadata: record.runtimeMetadata,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
export async function publishRuntimeSessionState(input) {
|
|
234
|
-
const id = scalar(input.sessionId, 'sessionId');
|
|
235
|
-
const owner = scalar(input.runtimeOwner, 'runtimeOwner');
|
|
236
|
-
const revision = scalar(input.revision, 'revision');
|
|
237
|
-
const runtimeState = scalar(input.runtimeState, 'runtimeState');
|
|
238
|
-
const initial = readRaw(id);
|
|
239
|
-
if (!initial)
|
|
240
|
-
throw new RuntimeSessionConflict(`runtime session ${id} is not registered`);
|
|
241
|
-
const watchers = readWatches(id).map((entry) => entry.watcher);
|
|
242
|
-
return withSessionRecordLocks([id, ...watchers], async () => withDeliveryLocks(watchers, async () => {
|
|
243
|
-
const raw = readRaw(id);
|
|
244
|
-
if (!raw)
|
|
245
|
-
throw new RuntimeSessionConflict(`runtime session ${id} disappeared during publication`);
|
|
246
|
-
if (raw.runtime_owner !== owner)
|
|
247
|
-
throw new RuntimeSessionConflict(`runtime session ${id} belongs to ${raw.runtime_owner || 'no external runtime'}, not ${owner}`);
|
|
248
|
-
const proposal = input.proposal ?? null;
|
|
249
|
-
const note = input.note ?? null;
|
|
250
|
-
const sameRevision = raw.runtime_revision === revision;
|
|
251
|
-
const sameState = raw.runtime_state === runtimeState && raw.status === input.lifecycle
|
|
252
|
-
&& (raw.proposal || null) === proposal && (raw.note || null) === note;
|
|
253
|
-
if (sameRevision && !sameState)
|
|
254
|
-
throw new RuntimeSessionConflict(`runtime revision ${revision} for session ${id} is already bound to another state`);
|
|
255
|
-
const candidate = runtimeRecord({
|
|
256
|
-
...raw,
|
|
257
|
-
status: input.lifecycle,
|
|
258
|
-
proposal: proposal ?? '',
|
|
259
|
-
note: note ?? '',
|
|
260
|
-
runtime_state: runtimeState,
|
|
261
|
-
runtime_revision: revision,
|
|
262
|
-
});
|
|
263
|
-
const message = stateMessage(candidate);
|
|
264
|
-
const attributes = notificationAttributes(candidate);
|
|
265
|
-
const historical = watchers.map((watcher) => {
|
|
266
|
-
const operation = `runtime-state:${id}`;
|
|
267
|
-
const requestDigest = digest(`${id}\0${watcher}\0${revision}`);
|
|
268
|
-
const payloadHash = digest(`${operation}\0${requestDigest}\0${message}\0${JSON.stringify(attributes)}`);
|
|
269
|
-
const prior = sentDispatchReceipt(watcher, operation, requestDigest);
|
|
270
|
-
if (prior && prior.payloadHash !== payloadHash)
|
|
271
|
-
throw new RuntimeSessionConflict(`runtime revision ${revision} notification is already bound to different bytes`);
|
|
272
|
-
return { watcher, operation, requestDigest, payloadHash, prior };
|
|
273
|
-
});
|
|
274
|
-
const historicalReplay = !sameRevision && historical.length > 0 && historical.every(({ prior }) => prior);
|
|
275
|
-
const replayed = sameRevision || historicalReplay;
|
|
276
|
-
if (!sameRevision) {
|
|
277
|
-
if (!historicalReplay) {
|
|
278
|
-
writeRaw({
|
|
279
|
-
...raw,
|
|
280
|
-
status: input.lifecycle,
|
|
281
|
-
proposal: proposal ?? '',
|
|
282
|
-
note: note ?? '',
|
|
283
|
-
runtime_state: runtimeState,
|
|
284
|
-
runtime_revision: revision,
|
|
285
|
-
});
|
|
286
|
-
recordStatus(id, input.lifecycle, proposal, note);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
const notified = [];
|
|
290
|
-
for (const { watcher, operation, requestDigest, payloadHash, prior } of historical) {
|
|
291
|
-
const receipt = {
|
|
292
|
-
operation,
|
|
293
|
-
requestDigest,
|
|
294
|
-
payloadHash,
|
|
295
|
-
delivery: { text: message, from: id, attributes },
|
|
296
|
-
};
|
|
297
|
-
if (prior) {
|
|
298
|
-
if (!prior.delivered)
|
|
299
|
-
ensurePendingWhileLocked(watcher, pendingFor(receipt, prior.mid));
|
|
300
|
-
}
|
|
301
|
-
else {
|
|
302
|
-
const { mid } = appendSent(watcher, message, id, undefined, receipt);
|
|
303
|
-
enqueue(watcher, pendingFor(receipt, mid));
|
|
304
|
-
}
|
|
305
|
-
notified.push(watcher);
|
|
306
|
-
}
|
|
307
|
-
const entries = readWatches(id);
|
|
308
|
-
if (entries.some((entry) => entry.snapshotPending)) {
|
|
309
|
-
writeWatches(id, entries.map(({ snapshotPending: _pending, ...entry }) => entry));
|
|
310
|
-
}
|
|
311
|
-
return { notified, replayed };
|
|
312
|
-
}));
|
|
313
|
-
}
|
|
314
|
-
export function readRuntimeSession(sessionId) {
|
|
315
|
-
const raw = readRaw(scalar(sessionId, 'sessionId'));
|
|
316
|
-
return raw?.runtime_owner ? runtimeRecord(raw) : null;
|
|
317
|
-
}
|
|
318
|
-
export function runtimeSessionChildren(parentSessionId, runtimeOwner) {
|
|
319
|
-
const parent = scalar(parentSessionId, 'parentSessionId');
|
|
320
|
-
return listSessionIds().flatMap((id) => {
|
|
321
|
-
const record = readRuntimeSession(id);
|
|
322
|
-
if (!record || record.parentSessionId !== parent || (runtimeOwner && record.runtimeOwner !== runtimeOwner))
|
|
323
|
-
return [];
|
|
324
|
-
return [record];
|
|
325
|
-
}).sort((left, right) => left.createdAt - right.createdAt || left.sessionId.localeCompare(right.sessionId));
|
|
326
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { TimelineEvent } from './session-timeline.js';
|
|
2
|
-
export type Cursors = {
|
|
3
|
-
version: 1;
|
|
4
|
-
follows: Record<string, number>;
|
|
5
|
-
};
|
|
6
|
-
export declare function readCursors(id: string): Cursors;
|
|
7
|
-
export declare const followCursor: (id: string, target: string) => number | null;
|
|
8
|
-
export declare function advanceFollow(id: string, target: string, to: number): void;
|
|
9
|
-
export declare const followedSessions: (id: string) => string[];
|
|
10
|
-
export declare function unreadSince(events: TimelineEvent[], from: number): {
|
|
11
|
-
events: TimelineEvent[];
|
|
12
|
-
at: number[];
|
|
13
|
-
next: number;
|
|
14
|
-
};
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { sessionArtifactPath, sessionStoreDir } from '@spexcode/spec-core';
|
|
4
|
-
const cursorsPath = (id) => sessionArtifactPath(id, 'cursors.json');
|
|
5
|
-
const at = (v) => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? Math.floor(v) : 0);
|
|
6
|
-
// Never throws: a missing, empty, or unparseable file reads as "nothing consumed", because re-showing a
|
|
7
|
-
// message is the honest recovery for a lost position and skipping one is not. Followed entries whose target
|
|
8
|
-
// store dir is gone are dropped here — expiry is this read, and the next write persists it.
|
|
9
|
-
export function readCursors(id) {
|
|
10
|
-
let raw = null;
|
|
11
|
-
try {
|
|
12
|
-
raw = JSON.parse(readFileSync(cursorsPath(id), 'utf8'));
|
|
13
|
-
}
|
|
14
|
-
catch { /* no cursors yet */ }
|
|
15
|
-
const follows = {};
|
|
16
|
-
const stored = raw?.follows;
|
|
17
|
-
if (stored && typeof stored === 'object') {
|
|
18
|
-
for (const [target, pos] of Object.entries(stored)) {
|
|
19
|
-
if (!target || !existsSync(sessionStoreDir(target)))
|
|
20
|
-
continue;
|
|
21
|
-
follows[target] = at(pos);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return { version: 1, follows };
|
|
25
|
-
}
|
|
26
|
-
// Written whole and atomically, one field per line — the same shape as the session record, so a position stays
|
|
27
|
-
// readable by an exact whole-line match where a value regex would not be.
|
|
28
|
-
function writeCursors(id, cursors) {
|
|
29
|
-
const dir = sessionStoreDir(id);
|
|
30
|
-
mkdirSync(dir, { recursive: true });
|
|
31
|
-
const tmp = join(dir, `.cursors.json.${process.pid}.tmp`);
|
|
32
|
-
writeFileSync(tmp, JSON.stringify(cursors, null, 2) + '\n');
|
|
33
|
-
renameSync(tmp, cursorsPath(id));
|
|
34
|
-
}
|
|
35
|
-
export const followCursor = (id, target) => {
|
|
36
|
-
const stored = readCursors(id).follows[target];
|
|
37
|
-
return stored === undefined ? null : stored;
|
|
38
|
-
};
|
|
39
|
-
// Start or advance a follow. Following IS this entry existing, so the first call registers the relationship.
|
|
40
|
-
// Monotonic: a stale read can leave a position too low (an event read twice), never too high (one lost).
|
|
41
|
-
export function advanceFollow(id, target, to) {
|
|
42
|
-
const cursors = readCursors(id);
|
|
43
|
-
const stored = cursors.follows[target];
|
|
44
|
-
if (stored !== undefined && to <= stored)
|
|
45
|
-
return;
|
|
46
|
-
writeCursors(id, { ...cursors, follows: { ...cursors.follows, [target]: Math.max(0, Math.floor(to)) } });
|
|
47
|
-
}
|
|
48
|
-
export const followedSessions = (id) => Object.keys(readCursors(id).follows);
|
|
49
|
-
const sameStatus = (a, b) => a.kind === 'status' && b.kind === 'status' && a.status === b.status
|
|
50
|
-
&& (a.proposal ?? null) === (b.proposal ?? null) && (a.note ?? null) === (b.note ?? null);
|
|
51
|
-
// @@@ edges, not lines - what a reader has not yet seen, with repeated status lines dropped. X→X IS NOT A
|
|
52
|
-
// TRANSITION: the log is append-only and already holds runs of identical status lines from before the
|
|
53
|
-
// timeline observer was deleted (every stray `spex serve` process ran an fs.watch over the same store and
|
|
54
|
-
// re-recorded each real move, so ONE transition landed as up to 6 lines within ~200ms). Those bytes are
|
|
55
|
-
// history and stay. Any consumer that decides "did it move?" must therefore compare VALUES, never adjacency,
|
|
56
|
-
// and must compare against the last status the reader ALREADY saw — otherwise a duplicate straddling the
|
|
57
|
-
// cursor boundary reads as a fresh move on the very next tick. `next` is where the cursor goes after consuming
|
|
58
|
-
// the WHOLE slice: the full length, because a dropped duplicate is still consumed. `at[i]` is the absolute
|
|
59
|
-
// index of `events[i]`, which is what lets a reader that STOPS on one event ([[session-follow]]'s take-one
|
|
60
|
-
// wait) advance to exactly `at[i] + 1` and leave the lines behind it unread for the next reader.
|
|
61
|
-
export function unreadSince(events, from) {
|
|
62
|
-
const start = Math.min(Math.max(0, Math.floor(from)), events.length);
|
|
63
|
-
let prev = null;
|
|
64
|
-
for (let i = start - 1; i >= 0; i--)
|
|
65
|
-
if (events[i].kind === 'status') {
|
|
66
|
-
prev = events[i];
|
|
67
|
-
break;
|
|
68
|
-
}
|
|
69
|
-
const out = [];
|
|
70
|
-
const at = [];
|
|
71
|
-
for (let i = start; i < events.length; i++) {
|
|
72
|
-
const e = events[i];
|
|
73
|
-
if (e.kind === 'status') {
|
|
74
|
-
if (prev && sameStatus(prev, e))
|
|
75
|
-
continue;
|
|
76
|
-
prev = e;
|
|
77
|
-
}
|
|
78
|
-
out.push(e);
|
|
79
|
-
at.push(i);
|
|
80
|
-
}
|
|
81
|
-
return { events: out, at, next: events.length };
|
|
82
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import type { SessionLifecycle, SessionProposal } from '@spexcode/spec-core';
|
|
2
|
-
export type TimelineEvent = {
|
|
3
|
-
ts: string;
|
|
4
|
-
kind: 'status';
|
|
5
|
-
status: SessionLifecycle;
|
|
6
|
-
proposal: SessionProposal | null;
|
|
7
|
-
note: string | null;
|
|
8
|
-
display?: string;
|
|
9
|
-
} | {
|
|
10
|
-
ts: string;
|
|
11
|
-
kind: 'sent';
|
|
12
|
-
mid: string;
|
|
13
|
-
text: string;
|
|
14
|
-
from: string | null;
|
|
15
|
-
replyVia?: 'note';
|
|
16
|
-
};
|
|
17
|
-
export type SentDispatchReceipt = {
|
|
18
|
-
operation: string;
|
|
19
|
-
requestDigest: string;
|
|
20
|
-
payloadHash: string;
|
|
21
|
-
delivery?: {
|
|
22
|
-
text: string;
|
|
23
|
-
from: string | null;
|
|
24
|
-
attributes?: Record<string, string>;
|
|
25
|
-
};
|
|
26
|
-
};
|
|
27
|
-
export declare function recordStatus(id: string, status: SessionLifecycle, proposal: SessionProposal | null, note: string | null): void;
|
|
28
|
-
export declare function appendSent(id: string, text: string, from: string | null, replyVia?: 'note', dispatchReceipt?: SentDispatchReceipt): {
|
|
29
|
-
mid: string;
|
|
30
|
-
};
|
|
31
|
-
export type SentDispatchState = {
|
|
32
|
-
mid: string;
|
|
33
|
-
payloadHash: string;
|
|
34
|
-
delivery: SentDispatchReceipt['delivery'] | null;
|
|
35
|
-
delivered: boolean;
|
|
36
|
-
};
|
|
37
|
-
export declare function sentDispatchReceipt(id: string, operation: SentDispatchReceipt['operation'], requestDigest: string): SentDispatchState | null;
|
|
38
|
-
export declare function settleSentDispatch(id: string, mid: string): void;
|
|
39
|
-
export declare function timelineEvents(id: string): TimelineEvent[];
|
|
40
|
-
export declare function timelineStamp(id: string): string | null;
|
|
41
|
-
export declare function lastHumanSendVia(id: string): 'note' | null;
|
|
42
|
-
export type SessionTurn = Readonly<{
|
|
43
|
-
token: string;
|
|
44
|
-
acceptedAt: string;
|
|
45
|
-
}>;
|
|
46
|
-
export declare function currentHumanTurn(id: string): SessionTurn | null;
|
|
47
|
-
export declare function timelineTail(id: string, limit?: number): TimelineEvent[];
|