spexcode 0.6.7 → 0.7.0-next.0
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 +31 -0
- package/node_modules/@spexcode/session-selflaunch/dist/locality.js +135 -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 +175 -21
- package/node_modules/@spexcode/spec-cli/dist/cli.js +121 -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/file-write.js +14 -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 +29 -18
- package/node_modules/@spexcode/spec-cli/dist/harness-select.js +16 -3
- package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +24 -6
- package/node_modules/@spexcode/spec-cli/dist/harness.js +510 -72
- 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 +98 -1
- package/node_modules/@spexcode/spec-cli/dist/index.js +294 -36
- 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.d.ts +2 -1
- package/node_modules/@spexcode/spec-cli/dist/machine-peer.js +46 -15
- 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/opencode.js +10 -2
- package/node_modules/@spexcode/spec-cli/dist/pi-harness.js +18 -5
- package/node_modules/@spexcode/spec-cli/dist/pi-headless.d.ts +2 -0
- package/node_modules/@spexcode/spec-cli/dist/pi-headless.js +27 -0
- 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-transcript.d.ts +57 -0
- package/node_modules/@spexcode/spec-cli/dist/session-transcript.js +137 -0
- package/node_modules/@spexcode/spec-cli/dist/session-web.js +4 -4
- package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +112 -15
- package/node_modules/@spexcode/spec-cli/dist/sessions.js +1553 -764
- package/node_modules/@spexcode/spec-cli/dist/shim-runtime.js +14 -0
- 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/hooks/dispatch.sh +19 -31
- package/node_modules/@spexcode/spec-cli/hooks/harness.sh +6 -6
- package/node_modules/@spexcode/spec-cli/package.json +8 -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 -1
- 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-core/templates/spexcode.json +36 -13
- 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/@spexcode/transcript/dist/frames.d.ts +44 -0
- package/node_modules/@spexcode/transcript/dist/frames.js +85 -0
- package/node_modules/@spexcode/transcript/dist/index.d.ts +5 -0
- package/node_modules/@spexcode/transcript/dist/index.js +5 -0
- package/node_modules/@spexcode/transcript/dist/live.d.ts +16 -0
- package/node_modules/@spexcode/transcript/dist/live.js +65 -0
- package/node_modules/@spexcode/transcript/dist/parsers.d.ts +47 -0
- package/node_modules/@spexcode/transcript/dist/parsers.js +285 -0
- package/node_modules/@spexcode/transcript/dist/readers.d.ts +10 -0
- package/node_modules/@spexcode/transcript/dist/readers.js +296 -0
- package/node_modules/@spexcode/transcript/dist/turns.d.ts +42 -0
- package/node_modules/@spexcode/transcript/dist/turns.js +11 -0
- package/node_modules/@spexcode/transcript/package.json +30 -0
- 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/node_modules/smol-toml/LICENSE +24 -0
- package/node_modules/smol-toml/README.md +418 -0
- package/node_modules/smol-toml/dist/date.d.ts +41 -0
- package/node_modules/smol-toml/dist/date.js +127 -0
- package/node_modules/smol-toml/dist/error.d.ts +38 -0
- package/node_modules/smol-toml/dist/error.js +63 -0
- package/node_modules/smol-toml/dist/extract.js +69 -0
- package/node_modules/smol-toml/dist/index.cjs +734 -0
- package/node_modules/smol-toml/dist/index.d.ts +43 -0
- package/node_modules/smol-toml/dist/index.js +33 -0
- package/node_modules/smol-toml/dist/parse.d.ts +36 -0
- package/node_modules/smol-toml/dist/parse.js +149 -0
- package/node_modules/smol-toml/dist/primitive.js +238 -0
- package/node_modules/smol-toml/dist/stringify.d.ts +31 -0
- package/node_modules/smol-toml/dist/stringify.js +181 -0
- package/node_modules/smol-toml/dist/struct.js +179 -0
- package/node_modules/smol-toml/dist/util.d.ts +38 -0
- package/node_modules/smol-toml/dist/util.js +89 -0
- package/node_modules/smol-toml/package.json +68 -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/dist/execution-trace.d.ts +0 -26
- package/node_modules/@spexcode/spec-cli/dist/execution-trace.js +0 -475
- package/node_modules/@spexcode/spec-cli/dist/session-execution.d.ts +0 -10
- package/node_modules/@spexcode/spec-cli/dist/session-execution.js +0 -70
- 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
|
@@ -71,7 +71,19 @@ export function listSessionFiles(id) {
|
|
|
71
71
|
requireSession(id);
|
|
72
72
|
return readSessionFiles(id);
|
|
73
73
|
}
|
|
74
|
-
|
|
74
|
+
export function inspectSessionFiles(id) {
|
|
75
|
+
requireSession(id);
|
|
76
|
+
return readFiles(id).map((path) => {
|
|
77
|
+
try {
|
|
78
|
+
currentFile(path);
|
|
79
|
+
return { path, valid: true };
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
return { path, valid: false, reason: error instanceof Error ? error.message : String(error) };
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
// Session projections already hold a parsed runtime envelope; re-checking it here would make an in-memory
|
|
75
87
|
// projection depend on the public route's existence guard. The route-facing list keeps that guard above.
|
|
76
88
|
export function readSessionFiles(id) {
|
|
77
89
|
return readFiles(id);
|
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { sessionStoreDir } from '@spexcode/spec-core';
|
|
3
|
-
import { advanceFollow, followCursor, unreadSince } from '@spexcode/session-core';
|
|
1
|
+
import { configuredSessionApplicationIfCutover } from './session-application.js';
|
|
4
2
|
import { timelineDisplay, timelineEvents, timelineStamp } from './session-timeline.js';
|
|
5
3
|
import { sessionTitle } from './sessions.js';
|
|
6
|
-
// @@@ session-follow - supervision
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// sessions cost the control plane zero, where the old poll cost one board build (a connect + a tmux spawn per
|
|
10
|
-
// live session) per follower per interval.
|
|
4
|
+
// @@@ session-follow - supervision follows canonical SQLite events past an application-owned cursor, never a
|
|
5
|
+
// derived board. The bounded observation tick reads the event sequence and cursor only; it never opens a backend,
|
|
6
|
+
// rendezvous socket, or tmux. M followers over N sessions therefore cost the control plane zero native probes.
|
|
11
7
|
// Actionable = a state whose arrival means "a human/supervisor must now act". `offline` is deliberately ABSENT
|
|
12
8
|
// where the old poll had it: liveness is a present-tense probe derivation, never authored, so it can never
|
|
13
9
|
// appear on a log ([[state]]). A follower learns what a session DECLARED and never that it died.
|
|
@@ -51,11 +47,19 @@ export async function followSessions(emit, opts) {
|
|
|
51
47
|
return f.path;
|
|
52
48
|
return [];
|
|
53
49
|
};
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
const application = configuredSessionApplicationIfCutover();
|
|
51
|
+
const canonicalSelf = self && application?.readState(self) ? self : null;
|
|
52
|
+
const readCursor = (id) => {
|
|
53
|
+
if (!self)
|
|
54
|
+
return memo.get(id) ?? null;
|
|
55
|
+
return canonicalSelf ? application.readFollowCursor(canonicalSelf, id) : null;
|
|
56
|
+
};
|
|
57
|
+
const writeCursor = (id, to) => {
|
|
58
|
+
if (!self)
|
|
59
|
+
memo.set(id, to);
|
|
60
|
+
else if (canonicalSelf)
|
|
61
|
+
application.advanceFollowCursor(canonicalSelf, id, to);
|
|
62
|
+
};
|
|
59
63
|
const line = (id, st, note, first) => {
|
|
60
64
|
const s = row?.(id, st, note);
|
|
61
65
|
if (!s)
|
|
@@ -74,7 +78,7 @@ export async function followSessions(emit, opts) {
|
|
|
74
78
|
if (id === self)
|
|
75
79
|
continue; // the follower's own log is its INBOX, followed below on its own cursor rule
|
|
76
80
|
seen.add(id);
|
|
77
|
-
if (!
|
|
81
|
+
if (!application?.readState(id)) {
|
|
78
82
|
if (state.delete(id))
|
|
79
83
|
emit(`${tag}[spex] closed · removed [id ${id}]`);
|
|
80
84
|
if (take)
|
|
@@ -111,7 +115,12 @@ export async function followSessions(emit, opts) {
|
|
|
111
115
|
continue; // THE cheap tick: nothing appended, nothing parsed
|
|
112
116
|
f.stamp = stamp;
|
|
113
117
|
const evs = timelineEvents(id);
|
|
114
|
-
const
|
|
118
|
+
const unread = evs.slice(f.pos);
|
|
119
|
+
const slice = {
|
|
120
|
+
events: unread,
|
|
121
|
+
at: unread.map((_, index) => f.pos + index),
|
|
122
|
+
next: evs.length,
|
|
123
|
+
};
|
|
115
124
|
let hit = null;
|
|
116
125
|
for (let k = 0; k < slice.events.length && !hit; k++) {
|
|
117
126
|
const e = slice.events[k];
|
|
@@ -151,18 +160,26 @@ export async function followSessions(emit, opts) {
|
|
|
151
160
|
state.delete(id);
|
|
152
161
|
emit(`${tag}[spex] closed · removed [id ${id}]`);
|
|
153
162
|
}
|
|
154
|
-
// THE FOLLOWER'S OWN LOG — watched exactly like any other target, on its own
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
if (self &&
|
|
159
|
-
const
|
|
163
|
+
// THE FOLLOWER'S OWN LOG — watched exactly like any other target, on its own canonical cursor row. In take
|
|
164
|
+
// mode the returned message is this wait's consumed result, so advance past it before returning; otherwise
|
|
165
|
+
// an empty cursor returns the same oldest inbox message forever. A turn-boundary delivery may still present
|
|
166
|
+
// the message through the adapter, but it does not own this read cursor.
|
|
167
|
+
if (self && application?.readState(self)) {
|
|
168
|
+
const ownEvents = timelineEvents(self);
|
|
169
|
+
const ownStart = readCursor(self) ?? 0;
|
|
170
|
+
const ownUnread = ownEvents.slice(ownStart);
|
|
171
|
+
const mine = {
|
|
172
|
+
events: ownUnread,
|
|
173
|
+
at: ownUnread.map((_, index) => ownStart + index),
|
|
174
|
+
};
|
|
160
175
|
for (let k = 0; k < mine.events.length; k++) {
|
|
161
176
|
const e = mine.events[k];
|
|
162
177
|
if (e.kind !== 'sent')
|
|
163
178
|
continue;
|
|
164
|
-
if (take)
|
|
179
|
+
if (take) {
|
|
180
|
+
writeCursor(self, mine.at[k] + 1);
|
|
165
181
|
return { mail: { from: e.from, text: e.text } };
|
|
182
|
+
}
|
|
166
183
|
// a stream has no turn boundary to advance that cursor, so its own high-water mark — process-local,
|
|
167
184
|
// never written — is what keeps an unread line from being re-printed on every tick.
|
|
168
185
|
if (mine.at[k] <= shown)
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
export declare class SessionRecordLockTimeout extends Error {
|
|
2
|
-
constructor(id: string);
|
|
3
|
-
}
|
|
4
1
|
export declare function withSessionRecordLock<T>(id: string, body: () => Promise<T>, signal?: AbortSignal): Promise<T>;
|
|
5
2
|
export declare function withSessionRecordLockSync<T>(id: string, body: () => T): T;
|
|
6
3
|
export declare function trySessionRecordLockSync(id: string): (() => void) | null;
|
|
7
|
-
export declare function withSessionRecordLocks<T>(rawIds: string[], body: () => Promise<T>, index?: number, ids?: string[]): Promise<T>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { closeSync, mkdirSync, openSync, readFileSync, unlinkSync, writeSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { runtimeRoot } from '@spexcode/spec-core';
|
|
4
|
+
const root = () => join(runtimeRoot(), '.session-record-locks');
|
|
5
|
+
const pathFor = (id) => join(root(), `${id}.lock`);
|
|
6
|
+
const pause = (signal) => new Promise((resolve, reject) => {
|
|
7
|
+
if (signal?.aborted) {
|
|
8
|
+
reject(Object.assign(new Error('The operation was aborted'), { name: 'AbortError', code: 'ABORT_ERR' }));
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const timer = setTimeout(resolve, 10);
|
|
12
|
+
if (signal)
|
|
13
|
+
signal.addEventListener('abort', () => { clearTimeout(timer); reject(Object.assign(new Error('The operation was aborted'), { name: 'AbortError', code: 'ABORT_ERR' })); }, { once: true });
|
|
14
|
+
});
|
|
15
|
+
async function acquire(id, timeoutMs = 30_000, signal) {
|
|
16
|
+
mkdirSync(root(), { recursive: true });
|
|
17
|
+
const path = pathFor(id), deadline = Date.now() + timeoutMs;
|
|
18
|
+
for (;;) {
|
|
19
|
+
if (signal?.aborted)
|
|
20
|
+
throw Object.assign(new Error('The operation was aborted'), { name: 'AbortError', code: 'ABORT_ERR' });
|
|
21
|
+
try {
|
|
22
|
+
const fd = openSync(path, 'wx');
|
|
23
|
+
writeSync(fd, String(process.pid));
|
|
24
|
+
closeSync(fd);
|
|
25
|
+
return () => { try {
|
|
26
|
+
unlinkSync(path);
|
|
27
|
+
}
|
|
28
|
+
catch { /* dead-owner recovery may have removed it */ } };
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error.code !== 'EEXIST')
|
|
32
|
+
throw error;
|
|
33
|
+
let owner = 0;
|
|
34
|
+
try {
|
|
35
|
+
owner = Number(readFileSync(path, 'utf8').trim()) || 0;
|
|
36
|
+
}
|
|
37
|
+
catch { /* creator/releaser race */ }
|
|
38
|
+
if (owner && owner !== process.pid) {
|
|
39
|
+
try {
|
|
40
|
+
process.kill(owner, 0);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
try {
|
|
44
|
+
unlinkSync(path);
|
|
45
|
+
}
|
|
46
|
+
catch { /* race */ }
|
|
47
|
+
;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (Date.now() >= deadline)
|
|
52
|
+
throw new Error(`session record ${id}: timed out waiting for transaction lock`);
|
|
53
|
+
await pause(signal);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function withSessionRecordLock(id, body, signal) {
|
|
58
|
+
const release = await acquire(id, 30_000, signal);
|
|
59
|
+
try {
|
|
60
|
+
return await body();
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
release();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export function withSessionRecordLockSync(id, body) {
|
|
67
|
+
const release = trySessionRecordLockSync(id);
|
|
68
|
+
if (!release)
|
|
69
|
+
throw new Error(`session record ${id}: transaction lock is busy`);
|
|
70
|
+
try {
|
|
71
|
+
return body();
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
release();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function trySessionRecordLockSync(id) {
|
|
78
|
+
mkdirSync(root(), { recursive: true });
|
|
79
|
+
const path = pathFor(id);
|
|
80
|
+
try {
|
|
81
|
+
const fd = openSync(path, 'wx');
|
|
82
|
+
writeSync(fd, String(process.pid));
|
|
83
|
+
closeSync(fd);
|
|
84
|
+
return () => { try {
|
|
85
|
+
unlinkSync(path);
|
|
86
|
+
}
|
|
87
|
+
catch { /* dead-owner recovery may have removed it */ } };
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (error.code === 'EEXIST')
|
|
91
|
+
return null;
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface GovernedRuntimeIdentity {
|
|
2
|
+
protocolSessionId: string;
|
|
3
|
+
harnessId: string;
|
|
4
|
+
harnessSessionId: string;
|
|
5
|
+
nativeStartToken: string;
|
|
6
|
+
metadata?: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
export interface GovernedRuntimeBindingOptions {
|
|
9
|
+
expectedGeneration?: number;
|
|
10
|
+
now?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface GovernedRuntimeBinding {
|
|
13
|
+
namespace: string;
|
|
14
|
+
protocolSessionId: string;
|
|
15
|
+
runtimeKind: string;
|
|
16
|
+
nativeSessionId: string;
|
|
17
|
+
nativeStartToken: string;
|
|
18
|
+
bindingGeneration: number;
|
|
19
|
+
status: 'bound' | 'unbound';
|
|
20
|
+
boundAtMs: number;
|
|
21
|
+
unboundAtMs: number | null;
|
|
22
|
+
metadata: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
export interface GovernedProtocolTransactionHost<Transaction = unknown> {
|
|
25
|
+
withTransaction<T>(body: (transaction: Transaction) => T): T;
|
|
26
|
+
}
|
|
27
|
+
export interface GovernedRuntimeBindingStore<Transaction = unknown> {
|
|
28
|
+
bind(transaction: Transaction, protocolSessionId: string, identity: {
|
|
29
|
+
namespace: string;
|
|
30
|
+
runtimeKind: string;
|
|
31
|
+
nativeSessionId: string;
|
|
32
|
+
nativeStartToken: string;
|
|
33
|
+
metadata?: Record<string, unknown>;
|
|
34
|
+
}, options?: GovernedRuntimeBindingOptions): GovernedRuntimeBinding;
|
|
35
|
+
resolve(namespace: string, protocolSessionId: string, transaction?: Transaction): GovernedRuntimeBinding | null;
|
|
36
|
+
unbind(transaction: Transaction, namespace: string, protocolSessionId: string, options?: GovernedRuntimeBindingOptions): GovernedRuntimeBinding;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Bind an identity whose storage and native start token were already established by Spex composition.
|
|
40
|
+
* Database placement, locality, launch, and legacy record publication deliberately remain outside this seam.
|
|
41
|
+
*/
|
|
42
|
+
export declare function bindSpexGovernedRuntime<Transaction>(protocol: GovernedProtocolTransactionHost<Transaction>, bindings: GovernedRuntimeBindingStore<Transaction>, identity: GovernedRuntimeIdentity, options?: GovernedRuntimeBindingOptions): GovernedRuntimeBinding;
|
|
43
|
+
export declare function resolveSpexGovernedRuntime<Transaction>(bindings: GovernedRuntimeBindingStore<Transaction>, protocolSessionId: string, transaction?: Transaction): GovernedRuntimeBinding | null;
|
|
44
|
+
export declare function unbindSpexGovernedRuntime<Transaction>(protocol: GovernedProtocolTransactionHost<Transaction>, bindings: GovernedRuntimeBindingStore<Transaction>, protocolSessionId: string, options?: GovernedRuntimeBindingOptions): GovernedRuntimeBinding;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const SPEX_GOVERNED_NAMESPACE = 'spex-governed';
|
|
2
|
+
function requireNonempty(value, field) {
|
|
3
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
4
|
+
throw new TypeError(`Spex governed runtime binding requires ${field}`);
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Bind an identity whose storage and native start token were already established by Spex composition.
|
|
9
|
+
* Database placement, locality, launch, and legacy record publication deliberately remain outside this seam.
|
|
10
|
+
*/
|
|
11
|
+
export function bindSpexGovernedRuntime(protocol, bindings, identity, options = {}) {
|
|
12
|
+
requireNonempty(identity.protocolSessionId, 'protocolSessionId');
|
|
13
|
+
requireNonempty(identity.harnessId, 'harnessId');
|
|
14
|
+
requireNonempty(identity.harnessSessionId, 'harnessSessionId');
|
|
15
|
+
requireNonempty(identity.nativeStartToken, 'nativeStartToken');
|
|
16
|
+
if (!protocol || typeof protocol.withTransaction !== 'function') {
|
|
17
|
+
throw new TypeError('Spex governed runtime binding requires an open protocol transaction host');
|
|
18
|
+
}
|
|
19
|
+
if (!bindings || typeof bindings.bind !== 'function') {
|
|
20
|
+
throw new TypeError('Spex governed runtime binding requires an open runtime binding store');
|
|
21
|
+
}
|
|
22
|
+
return protocol.withTransaction(transaction => bindings.bind(transaction, identity.protocolSessionId, {
|
|
23
|
+
namespace: SPEX_GOVERNED_NAMESPACE,
|
|
24
|
+
runtimeKind: identity.harnessId,
|
|
25
|
+
nativeSessionId: identity.harnessSessionId,
|
|
26
|
+
nativeStartToken: identity.nativeStartToken,
|
|
27
|
+
metadata: identity.metadata,
|
|
28
|
+
}, options));
|
|
29
|
+
}
|
|
30
|
+
export function resolveSpexGovernedRuntime(bindings, protocolSessionId, transaction) {
|
|
31
|
+
requireNonempty(protocolSessionId, 'protocolSessionId');
|
|
32
|
+
return bindings.resolve(SPEX_GOVERNED_NAMESPACE, protocolSessionId, transaction);
|
|
33
|
+
}
|
|
34
|
+
export function unbindSpexGovernedRuntime(protocol, bindings, protocolSessionId, options = {}) {
|
|
35
|
+
requireNonempty(protocolSessionId, 'protocolSessionId');
|
|
36
|
+
return protocol.withTransaction(transaction => bindings.unbind(transaction, SPEX_GOVERNED_NAMESPACE, protocolSessionId, options));
|
|
37
|
+
}
|
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
import { type SessionLifecycle, type SessionProposal } from '@spexcode/spec-core';
|
|
2
|
-
|
|
3
|
-
|
|
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 declare const timelineEvents: (id: string) => TimelineEvent[];
|
|
18
|
+
export declare const timelineStamp: (id: string) => string | null;
|
|
19
|
+
export declare const timelineTail: (id: string, limit?: number) => TimelineEvent[];
|
|
20
|
+
export declare const lastHumanSendVia: (id: string) => "note" | null;
|
|
21
|
+
export declare const currentHumanTurn: (id: string) => {
|
|
22
|
+
token: string;
|
|
23
|
+
acceptedAt: string;
|
|
24
|
+
} | null;
|
|
25
|
+
export declare const recordStatus: (id: string, status: SessionLifecycle, proposal: SessionProposal | null, note: string | null) => void;
|
|
4
26
|
type DisplayWord = 'working' | 'idle' | 'review' | 'done' | 'close-pending' | 'parked' | 'error' | 'asking' | 'queued';
|
|
5
27
|
export declare const timelineDisplay: (event: {
|
|
6
28
|
status: SessionLifecycle;
|
|
@@ -9,3 +31,4 @@ export declare const timelineDisplay: (event: {
|
|
|
9
31
|
export declare function readTimeline(id: string, limit?: number): {
|
|
10
32
|
events: TimelineEvent[];
|
|
11
33
|
} | null;
|
|
34
|
+
export {};
|
|
@@ -1,20 +1,77 @@
|
|
|
1
1
|
import { readAliasedRawRecord } from '@spexcode/spec-core';
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { decodeEventJson } from '@spexcode/session-events';
|
|
3
|
+
import { MIGRATED_MESSAGE_EVENT, MIGRATED_STATE_EVENT } from '@spexcode/session-application';
|
|
4
|
+
import { configuredSessionApplicationIfCutover } from './session-application.js';
|
|
5
|
+
// The timeline is a history: events read in occurrence order (sequence breaks ties). Migrated legacy history
|
|
6
|
+
// lands after the live events in sequence but before them in time, and it is shown where it happened.
|
|
7
|
+
const canonicalSessionId = (id) => readAliasedRawRecord(id)?.session_id ?? id;
|
|
8
|
+
const canonicalTimelineEvents = (id) => {
|
|
9
|
+
const canonicalId = canonicalSessionId(id);
|
|
10
|
+
const application = configuredSessionApplicationIfCutover();
|
|
11
|
+
if (!application || !application.readState(canonicalId))
|
|
12
|
+
return null;
|
|
13
|
+
const ordered = [...application.readEvents(canonicalId)].sort((a, b) => a.occurredAtMs - b.occurredAtMs || a.eventSeq - b.eventSeq);
|
|
14
|
+
return ordered.flatMap((event) => {
|
|
15
|
+
const decoded = decodeEventJson(event.payload);
|
|
16
|
+
if (!decoded || typeof decoded !== 'object' || Array.isArray(decoded))
|
|
17
|
+
return [];
|
|
18
|
+
const payload = decoded;
|
|
19
|
+
if (event.type === 'session.state.changed.v1' || event.type === MIGRATED_STATE_EVENT)
|
|
20
|
+
return [{
|
|
21
|
+
ts: new Date(event.occurredAtMs).toISOString(),
|
|
22
|
+
kind: 'status',
|
|
23
|
+
status: String(payload.status),
|
|
24
|
+
proposal: payload.proposal === null || payload.proposal === undefined ? null : String(payload.proposal),
|
|
25
|
+
note: payload.note === null || payload.note === undefined ? null : String(payload.note),
|
|
26
|
+
}];
|
|
27
|
+
if (event.type === 'session.message.sent.v1' || event.type === MIGRATED_MESSAGE_EVENT)
|
|
28
|
+
return [{
|
|
29
|
+
ts: new Date(event.occurredAtMs).toISOString(),
|
|
30
|
+
kind: 'sent',
|
|
31
|
+
mid: String(payload.messageId),
|
|
32
|
+
text: String(payload.text),
|
|
33
|
+
from: payload.from === null || payload.from === undefined ? null : String(payload.from),
|
|
34
|
+
...(payload.replyVia === 'note' ? { replyVia: 'note' } : {}),
|
|
35
|
+
}];
|
|
36
|
+
return [];
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
export const timelineEvents = (id) => canonicalTimelineEvents(id) ?? [];
|
|
40
|
+
export const timelineStamp = (id) => {
|
|
41
|
+
const canonicalId = canonicalSessionId(id);
|
|
42
|
+
const application = configuredSessionApplicationIfCutover();
|
|
43
|
+
if (application?.readState(canonicalId)) {
|
|
44
|
+
const events = application.readEvents(canonicalId);
|
|
45
|
+
return events.length === 0 ? null : String(events.at(-1).eventSeq);
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
};
|
|
49
|
+
export const timelineTail = (id, limit = 500) => timelineEvents(id).slice(-Math.max(1, limit));
|
|
50
|
+
export const lastHumanSendVia = (id) => {
|
|
51
|
+
const events = timelineEvents(id);
|
|
52
|
+
const sent = [...events].reverse().find((event) => event.kind === 'sent' && event.from === null);
|
|
53
|
+
return sent?.replyVia ?? null;
|
|
54
|
+
};
|
|
55
|
+
export const currentHumanTurn = (id) => {
|
|
56
|
+
const sent = [...timelineEvents(id)].reverse().find((event) => event.kind === 'sent' && event.from === null);
|
|
57
|
+
return sent ? { token: sent.mid, acceptedAt: sent.ts } : null;
|
|
58
|
+
};
|
|
59
|
+
export const recordStatus = (id, status, proposal, note) => {
|
|
60
|
+
const application = configuredSessionApplicationIfCutover();
|
|
61
|
+
if (!application?.readState(id))
|
|
62
|
+
throw new Error(`cannot record status for unknown canonical session ${id}`);
|
|
63
|
+
application.transitionSession(id, { status, proposal, note, reason: 'cli-status' });
|
|
64
|
+
};
|
|
4
65
|
const PROPOSAL_DISPLAY = { merge: 'review', nothing: 'done', close: 'close-pending' };
|
|
5
66
|
export const timelineDisplay = (event) => event.status === 'awaiting' ? (PROPOSAL_DISPLAY[event.proposal ?? 'nothing'] ?? 'done')
|
|
6
67
|
: event.status === 'active' ? 'working' : event.status;
|
|
7
68
|
// The HTTP projection remains a SpexCode concern: it resolves aliases, hides unmanaged records, and adds the
|
|
8
69
|
// board's display vocabulary. The package beneath it only reads the durable file protocol.
|
|
9
70
|
export function readTimeline(id, limit = 500) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
71
|
+
const application = configuredSessionApplicationIfCutover();
|
|
72
|
+
const canonicalId = canonicalSessionId(id);
|
|
73
|
+
if (application && application.readState(canonicalId)) {
|
|
74
|
+
return { events: canonicalTimelineEvents(id).slice(-Math.max(1, limit)).map((event) => event.kind === 'status' ? { ...event, display: timelineDisplay(event) } : event) };
|
|
13
75
|
}
|
|
14
|
-
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
if (!raw || !raw.governed)
|
|
18
|
-
return null;
|
|
19
|
-
return { events: timelineTail(raw.session_id, limit).map((event) => event.kind === 'status' ? { ...event, display: timelineDisplay(event) } : event) };
|
|
76
|
+
return null;
|
|
20
77
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
import { type Harness } from './harness.js';
|
|
3
|
+
type Target = {
|
|
4
|
+
ok: true;
|
|
5
|
+
harness: Harness;
|
|
6
|
+
threadId: string;
|
|
7
|
+
} | {
|
|
8
|
+
ok: false;
|
|
9
|
+
status: 404 | 409 | 500;
|
|
10
|
+
error: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function resolveTranscriptTarget(id: string): Target;
|
|
13
|
+
export declare function readSessionTranscript(c: Context): Promise<(Response & import("hono").TypedResponse<{
|
|
14
|
+
error: string;
|
|
15
|
+
}, 400, "json">) | (Response & import("hono").TypedResponse<{
|
|
16
|
+
error: string;
|
|
17
|
+
}, 500 | 404 | 409, "json">) | (Response & import("hono").TypedResponse<{
|
|
18
|
+
readonly revision: string;
|
|
19
|
+
readonly from: number;
|
|
20
|
+
readonly to: number;
|
|
21
|
+
readonly turns: readonly {
|
|
22
|
+
readonly id: string;
|
|
23
|
+
readonly at: number;
|
|
24
|
+
readonly role: "user" | "assistant";
|
|
25
|
+
readonly text?: string | undefined;
|
|
26
|
+
readonly tools?: readonly {
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly input?: string | undefined;
|
|
30
|
+
readonly output?: string | undefined;
|
|
31
|
+
readonly outputLines: number;
|
|
32
|
+
readonly outputBytes: number;
|
|
33
|
+
}[] | undefined;
|
|
34
|
+
}[];
|
|
35
|
+
readonly truncated: boolean;
|
|
36
|
+
readonly omittedTurns: number;
|
|
37
|
+
readonly omittedBytes: number;
|
|
38
|
+
readonly outOfOrderEvents: number;
|
|
39
|
+
}, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
|
|
40
|
+
error: string;
|
|
41
|
+
reason: string;
|
|
42
|
+
}, 409 | 422 | 501, "json">)>;
|
|
43
|
+
export declare function readSessionTranscriptTool(c: Context): Promise<(Response & import("hono").TypedResponse<{
|
|
44
|
+
error: string;
|
|
45
|
+
reason: string;
|
|
46
|
+
}, 409 | 422 | 501, "json">) | (Response & import("hono").TypedResponse<{
|
|
47
|
+
error: string;
|
|
48
|
+
}, 400, "json">) | (Response & import("hono").TypedResponse<{
|
|
49
|
+
error: string;
|
|
50
|
+
}, 500 | 404 | 409, "json">) | (Response & import("hono").TypedResponse<{
|
|
51
|
+
id: string;
|
|
52
|
+
output: string | null;
|
|
53
|
+
outputLines: number;
|
|
54
|
+
outputBytes: number;
|
|
55
|
+
}, import("hono/utils/http-status").ContentfulStatusCode, "json">)>;
|
|
56
|
+
export declare function sessionTranscriptStream(c: Context): Promise<Response>;
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { streamSSE } from 'hono/streaming';
|
|
2
|
+
import { readAliasedRawRecord } from '@spexcode/spec-core';
|
|
3
|
+
import { defaultHarness, harnessById } from './harness.js';
|
|
4
|
+
import { TranscriptReadError, openFrameStream } from '@spexcode/transcript';
|
|
5
|
+
export function resolveTranscriptTarget(id) {
|
|
6
|
+
let raw;
|
|
7
|
+
try {
|
|
8
|
+
raw = readAliasedRawRecord(id);
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
return { ok: false, status: 500, error: `session ${id} record is unreadable: ${error instanceof Error ? error.message : String(error)}` };
|
|
12
|
+
}
|
|
13
|
+
if (!raw || !raw.governed)
|
|
14
|
+
return { ok: false, status: 404, error: `session ${id} does not exist` };
|
|
15
|
+
let harness;
|
|
16
|
+
try {
|
|
17
|
+
harness = harnessById(typeof raw.harness === 'string' && raw.harness ? raw.harness : defaultHarness.id);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
return { ok: false, status: 500, error: error instanceof Error ? error.message : String(error) };
|
|
21
|
+
}
|
|
22
|
+
const threadId = harness.exactNativeTargetId({
|
|
23
|
+
session: raw.session_id,
|
|
24
|
+
harnessSessionId: typeof raw.harness_session_id === 'string' ? raw.harness_session_id : null,
|
|
25
|
+
stopped: !!raw.stopped,
|
|
26
|
+
archived: !!raw.archived,
|
|
27
|
+
});
|
|
28
|
+
if (!threadId)
|
|
29
|
+
return { ok: false, status: 409, error: `session ${id} transcript is unavailable: native harness identity is missing` };
|
|
30
|
+
return { ok: true, harness, threadId };
|
|
31
|
+
}
|
|
32
|
+
const epoch = (value) => {
|
|
33
|
+
if (value == null || value === '')
|
|
34
|
+
return null;
|
|
35
|
+
const number = Number(value);
|
|
36
|
+
return Number.isFinite(number) && Number.isInteger(number) ? number : null;
|
|
37
|
+
};
|
|
38
|
+
const failure = (error) => {
|
|
39
|
+
if (!(error instanceof TranscriptReadError))
|
|
40
|
+
throw error;
|
|
41
|
+
return { body: { error: error.message, reason: error.reason }, status: error.reason === 'unsupported' ? 501 : error.reason === 'invalid' ? 422 : 409 };
|
|
42
|
+
};
|
|
43
|
+
// GET /api/sessions/:id/transcript?from=<ms>&to=<ms> — a closed interval, both bounds explicit so the route
|
|
44
|
+
// never guesses which stretch the caller meant.
|
|
45
|
+
export async function readSessionTranscript(c) {
|
|
46
|
+
const id = c.req.param('id') || '';
|
|
47
|
+
const fromRaw = c.req.query('from');
|
|
48
|
+
const toRaw = c.req.query('to');
|
|
49
|
+
if (fromRaw == null || toRaw == null || fromRaw === '' || toRaw === '')
|
|
50
|
+
return c.json({ error: 'transcript needs both from and to epoch milliseconds' }, 400);
|
|
51
|
+
const from = epoch(fromRaw);
|
|
52
|
+
const to = epoch(toRaw);
|
|
53
|
+
if (from === null || to === null || from >= to)
|
|
54
|
+
return c.json({ error: 'transcript interval is invalid: from and to must be integer epoch milliseconds with from < to' }, 400);
|
|
55
|
+
const target = resolveTranscriptTarget(id);
|
|
56
|
+
if (!target.ok)
|
|
57
|
+
return c.json({ error: target.error }, target.status);
|
|
58
|
+
try {
|
|
59
|
+
return c.json(await target.harness.transcript.read(target.threadId, { from, to }));
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
const { body, status } = failure(error);
|
|
63
|
+
return c.json(body, status);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// GET /api/sessions/:id/transcript/tool/:toolId?from=<ms> — one call's recorded output, read when a person
|
|
67
|
+
// opens it. The live stream withholds output bodies (below), so this is where a body comes from; the interval
|
|
68
|
+
// is `[from, now]`, the same stretch the stream reads, and a call outside it is a 404.
|
|
69
|
+
export async function readSessionTranscriptTool(c) {
|
|
70
|
+
const id = c.req.param('id') || '';
|
|
71
|
+
const toolId = c.req.param('toolId') || '';
|
|
72
|
+
const from = epoch(c.req.query('from'));
|
|
73
|
+
if (from === null || !toolId)
|
|
74
|
+
return c.json({ error: 'transcript tool needs a tool id and from as integer epoch milliseconds' }, 400);
|
|
75
|
+
const target = resolveTranscriptTarget(id);
|
|
76
|
+
if (!target.ok)
|
|
77
|
+
return c.json({ error: target.error }, target.status);
|
|
78
|
+
try {
|
|
79
|
+
const read = await target.harness.transcript.read(target.threadId, { from, to: Math.max(from + 1, Date.now()) });
|
|
80
|
+
for (const turn of read.turns)
|
|
81
|
+
for (const tool of turn.tools ?? []) {
|
|
82
|
+
if (tool.id !== toolId)
|
|
83
|
+
continue;
|
|
84
|
+
return c.json({ id: tool.id, output: tool.output ?? null, outputLines: tool.outputLines, outputBytes: tool.outputBytes });
|
|
85
|
+
}
|
|
86
|
+
return c.json({ error: `tool ${toolId} is not in this interval` }, 404);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
const { body, status } = failure(error);
|
|
90
|
+
return c.json(body, status);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const TICK_MS = 500;
|
|
94
|
+
const HEARTBEAT_TICKS = 20;
|
|
95
|
+
// GET /api/sessions/:id/transcript/stream?from=<ms> — the open interval [from, now], carried over SSE. What a
|
|
96
|
+
// frame holds, when one is worth sending, and how a subscriber merges it are [[transcript-frames]]'
|
|
97
|
+
// `openFrameStream`, shared with every other transport; this route owns only the carriage: a tick that asks
|
|
98
|
+
// the stream to publish, one `transcript` SSE event per frame it yields, and periodic `ping` heartbeats so the
|
|
99
|
+
// browser's dead-man can reopen a silent stream. Reading happens only while a client is subscribed and stops
|
|
100
|
+
// on abort, closing the cursor.
|
|
101
|
+
export async function sessionTranscriptStream(c) {
|
|
102
|
+
const id = c.req.param('id') || '';
|
|
103
|
+
const from = epoch(c.req.query('from'));
|
|
104
|
+
if (from === null)
|
|
105
|
+
return c.json({ error: 'transcript stream needs from as integer epoch milliseconds' }, 400);
|
|
106
|
+
const target = resolveTranscriptTarget(id);
|
|
107
|
+
if (!target.ok)
|
|
108
|
+
return c.json({ error: target.error }, target.status);
|
|
109
|
+
const frames = openFrameStream(target.harness.transcript, target.threadId, from);
|
|
110
|
+
return streamSSE(c, async (stream) => {
|
|
111
|
+
let aborted = false;
|
|
112
|
+
let ticks = 0;
|
|
113
|
+
const publish = async () => {
|
|
114
|
+
const frame = await frames.publish();
|
|
115
|
+
if (frame)
|
|
116
|
+
await stream.writeSSE({ event: 'transcript', data: JSON.stringify(frame) });
|
|
117
|
+
};
|
|
118
|
+
stream.onAbort(() => { aborted = true; frames.close(); });
|
|
119
|
+
try {
|
|
120
|
+
await publish();
|
|
121
|
+
while (!aborted) {
|
|
122
|
+
await stream.sleep(TICK_MS);
|
|
123
|
+
if (aborted)
|
|
124
|
+
break;
|
|
125
|
+
await publish();
|
|
126
|
+
if (++ticks % HEARTBEAT_TICKS === 0)
|
|
127
|
+
await stream.writeSSE({ event: 'ping', data: 'x' });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// EventSource reconnects a dropped stream; a native read must never take the session server down.
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
frames.close();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { readRecordEntry, sessionArtifactPath,
|
|
4
|
+
import { readRecordEntry, sessionArtifactPath, sessionRecordPath } from '@spexcode/spec-core';
|
|
5
5
|
import { projectRuntimeRoot } from '@spexcode/spec-core';
|
|
6
6
|
export class SessionWebError extends Error {
|
|
7
7
|
status;
|
|
@@ -15,8 +15,8 @@ const webPath = (id, projectRoot) => projectRoot
|
|
|
15
15
|
? join(projectRuntimeRoot(join(projectRoot, '.git')), 'sessions', id, 'web.json')
|
|
16
16
|
: sessionArtifactPath(id, 'web.json');
|
|
17
17
|
const recordPath = (id, projectRoot) => projectRoot
|
|
18
|
-
? join(projectRuntimeRoot(join(projectRoot, '.git')), 'sessions', id, '
|
|
19
|
-
:
|
|
18
|
+
? join(projectRuntimeRoot(join(projectRoot, '.git')), 'sessions', id, 'runtime.json')
|
|
19
|
+
: sessionRecordPath(id);
|
|
20
20
|
function validSessionId(id) {
|
|
21
21
|
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(id);
|
|
22
22
|
}
|
|
@@ -104,7 +104,7 @@ export function listSessionWebs(id, projectRoot) {
|
|
|
104
104
|
requireSession(id, projectRoot);
|
|
105
105
|
return readSessionWebs(id, projectRoot);
|
|
106
106
|
}
|
|
107
|
-
// Session projections already hold a parsed
|
|
107
|
+
// Session projections already hold a parsed runtime envelope; re-checking it here would make an in-memory
|
|
108
108
|
// projection depend on the public route's existence guard. The route-facing list keeps that guard above.
|
|
109
109
|
export function readSessionWebs(id, projectRoot) {
|
|
110
110
|
return readWebs(id, projectRoot).map(toWeb);
|