spexcode 0.6.6 → 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.
Files changed (205) hide show
  1. package/README.md +12 -7
  2. package/node_modules/@spexcode/session-application/dist/index.d.ts +17 -0
  3. package/node_modules/@spexcode/session-application/dist/index.js +35 -0
  4. package/node_modules/@spexcode/session-application/dist/migration.d.ts +51 -0
  5. package/node_modules/@spexcode/session-application/dist/migration.js +694 -0
  6. package/node_modules/@spexcode/session-application/dist/production.d.ts +109 -0
  7. package/node_modules/@spexcode/session-application/dist/production.js +438 -0
  8. package/node_modules/@spexcode/session-application/dist/schema.d.ts +2 -0
  9. package/node_modules/@spexcode/session-application/dist/schema.js +39 -0
  10. package/node_modules/@spexcode/session-application/package.json +29 -0
  11. package/node_modules/@spexcode/session-events/dist/errors.d.ts +6 -0
  12. package/node_modules/@spexcode/session-events/dist/errors.js +11 -0
  13. package/node_modules/@spexcode/session-events/dist/index.d.ts +46 -0
  14. package/node_modules/@spexcode/session-events/dist/index.js +245 -0
  15. package/node_modules/@spexcode/session-events/dist/schema.d.ts +3 -0
  16. package/node_modules/@spexcode/session-events/dist/schema.js +44 -0
  17. package/node_modules/@spexcode/session-events/package.json +24 -0
  18. package/node_modules/@spexcode/session-protocol/dist/canonical.d.ts +36 -0
  19. package/node_modules/@spexcode/session-protocol/dist/canonical.js +139 -0
  20. package/node_modules/@spexcode/session-protocol/dist/engine.d.ts +22 -0
  21. package/node_modules/@spexcode/session-protocol/dist/engine.js +478 -0
  22. package/node_modules/@spexcode/session-protocol/dist/errors.d.ts +8 -0
  23. package/node_modules/@spexcode/session-protocol/dist/errors.js +39 -0
  24. package/node_modules/@spexcode/session-protocol/dist/index.d.ts +60 -0
  25. package/node_modules/@spexcode/session-protocol/dist/index.js +4 -0
  26. package/node_modules/@spexcode/session-protocol/dist/schema.d.ts +36 -0
  27. package/node_modules/@spexcode/session-protocol/dist/schema.js +192 -0
  28. package/node_modules/@spexcode/{session-core → session-protocol}/package.json +4 -8
  29. package/node_modules/@spexcode/session-runtime/dist/errors.d.ts +6 -0
  30. package/node_modules/@spexcode/session-runtime/dist/errors.js +11 -0
  31. package/node_modules/@spexcode/session-runtime/dist/index.d.ts +33 -0
  32. package/node_modules/@spexcode/session-runtime/dist/index.js +193 -0
  33. package/node_modules/@spexcode/session-runtime/dist/schema.d.ts +2 -0
  34. package/node_modules/@spexcode/session-runtime/dist/schema.js +37 -0
  35. package/node_modules/@spexcode/session-runtime/package.json +24 -0
  36. package/node_modules/@spexcode/session-selflaunch/bin/spex-session.mjs +4 -0
  37. package/node_modules/@spexcode/session-selflaunch/dist/cli.d.ts +25 -0
  38. package/node_modules/@spexcode/session-selflaunch/dist/cli.js +156 -0
  39. package/node_modules/@spexcode/session-selflaunch/dist/index.d.ts +28 -0
  40. package/node_modules/@spexcode/session-selflaunch/dist/index.js +25 -0
  41. package/node_modules/@spexcode/session-selflaunch/dist/locality.d.ts +20 -0
  42. package/node_modules/@spexcode/session-selflaunch/dist/locality.js +83 -0
  43. package/node_modules/@spexcode/session-selflaunch/dist/path.d.ts +13 -0
  44. package/node_modules/@spexcode/session-selflaunch/dist/path.js +45 -0
  45. package/node_modules/@spexcode/session-selflaunch/package.json +35 -0
  46. package/node_modules/@spexcode/session-topology/dist/errors.d.ts +6 -0
  47. package/node_modules/@spexcode/session-topology/dist/errors.js +11 -0
  48. package/node_modules/@spexcode/session-topology/dist/index.d.ts +24 -0
  49. package/node_modules/@spexcode/session-topology/dist/index.js +173 -0
  50. package/node_modules/@spexcode/session-topology/dist/schema.d.ts +3 -0
  51. package/node_modules/@spexcode/session-topology/dist/schema.js +27 -0
  52. package/node_modules/@spexcode/session-topology/package.json +24 -0
  53. package/node_modules/@spexcode/spec-cli/bin/spex.mjs +56 -19
  54. package/node_modules/@spexcode/spec-cli/dist/cli.js +102 -59
  55. package/node_modules/@spexcode/spec-cli/dist/client.d.ts +1 -3
  56. package/node_modules/@spexcode/spec-cli/dist/client.js +49 -30
  57. package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.d.ts +11 -0
  58. package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.js +46 -9
  59. package/node_modules/@spexcode/spec-cli/dist/delivery-lock.d.ts +2 -0
  60. package/node_modules/@spexcode/spec-cli/dist/delivery-lock.js +58 -0
  61. package/node_modules/@spexcode/spec-cli/dist/doctor.js +53 -11
  62. package/node_modules/@spexcode/spec-cli/dist/execution-trace.d.ts +1 -0
  63. package/node_modules/@spexcode/spec-cli/dist/execution-trace.js +2 -2
  64. package/node_modules/@spexcode/spec-cli/dist/gateway-hub.js +2 -1
  65. package/node_modules/@spexcode/spec-cli/dist/gateway.js +6 -3
  66. package/node_modules/@spexcode/spec-cli/dist/graphCache.js +32 -2
  67. package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.js +57 -2
  68. package/node_modules/@spexcode/spec-cli/dist/graphStream.d.ts +2 -0
  69. package/node_modules/@spexcode/spec-cli/dist/graphStream.js +83 -3
  70. package/node_modules/@spexcode/spec-cli/dist/guide.js +20 -7
  71. package/node_modules/@spexcode/spec-cli/dist/harness-select.js +16 -3
  72. package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +15 -3
  73. package/node_modules/@spexcode/spec-cli/dist/harness.js +331 -50
  74. package/node_modules/@spexcode/spec-cli/dist/help.js +11 -8
  75. package/node_modules/@spexcode/spec-cli/dist/hook-prompts.js +8 -0
  76. package/node_modules/@spexcode/spec-cli/dist/host-resources.js +29 -8
  77. package/node_modules/@spexcode/spec-cli/dist/host.d.ts +7 -0
  78. package/node_modules/@spexcode/spec-cli/dist/host.js +93 -0
  79. package/node_modules/@spexcode/spec-cli/dist/index.js +324 -22
  80. package/node_modules/@spexcode/spec-cli/dist/init.js +1 -1
  81. package/node_modules/@spexcode/spec-cli/dist/lint.js +70 -35
  82. package/node_modules/@spexcode/spec-cli/dist/listen.d.ts +3 -2
  83. package/node_modules/@spexcode/spec-cli/dist/listen.js +14 -2
  84. package/node_modules/@spexcode/spec-cli/dist/machine-peer.js +1 -1
  85. package/node_modules/@spexcode/spec-cli/dist/materialize.d.ts +2 -2
  86. package/node_modules/@spexcode/spec-cli/dist/materialize.js +176 -35
  87. package/node_modules/@spexcode/spec-cli/dist/pty-bridge.js +14 -14
  88. package/node_modules/@spexcode/spec-cli/dist/reviews.js +12 -7
  89. package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.d.ts +11 -0
  90. package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.js +79 -1
  91. package/node_modules/@spexcode/spec-cli/dist/session-application.d.ts +23 -0
  92. package/node_modules/@spexcode/spec-cli/dist/session-application.js +189 -0
  93. package/node_modules/@spexcode/spec-cli/dist/session-declarations.js +13 -1
  94. package/node_modules/@spexcode/spec-cli/dist/session-files.d.ts +6 -0
  95. package/node_modules/@spexcode/spec-cli/dist/session-files.js +13 -1
  96. package/node_modules/@spexcode/spec-cli/dist/session-follow.js +39 -22
  97. package/node_modules/@spexcode/{session-core/dist/record-lock.d.ts → spec-cli/dist/session-record-lock.d.ts} +0 -4
  98. package/node_modules/@spexcode/spec-cli/dist/session-record-lock.js +94 -0
  99. package/node_modules/@spexcode/spec-cli/dist/session-runtime-adapter.d.ts +44 -0
  100. package/node_modules/@spexcode/spec-cli/dist/session-runtime-adapter.js +37 -0
  101. package/node_modules/@spexcode/spec-cli/dist/session-timeline.d.ts +25 -2
  102. package/node_modules/@spexcode/spec-cli/dist/session-timeline.js +68 -11
  103. package/node_modules/@spexcode/spec-cli/dist/session-web.js +4 -4
  104. package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +108 -15
  105. package/node_modules/@spexcode/spec-cli/dist/sessions.js +1465 -744
  106. package/node_modules/@spexcode/spec-cli/dist/source-list.d.ts +13 -0
  107. package/node_modules/@spexcode/spec-cli/dist/source-list.js +99 -0
  108. package/node_modules/@spexcode/spec-cli/dist/source-read.d.ts +16 -0
  109. package/node_modules/@spexcode/spec-cli/dist/source-read.js +84 -0
  110. package/node_modules/@spexcode/spec-cli/dist/spec-attachments.d.ts +7 -0
  111. package/node_modules/@spexcode/spec-cli/dist/spec-attachments.js +89 -0
  112. package/node_modules/@spexcode/spec-cli/dist/spec-body-edit.d.ts +23 -0
  113. package/node_modules/@spexcode/spec-cli/dist/spec-body-edit.js +138 -0
  114. package/node_modules/@spexcode/spec-cli/dist/supervise.js +15 -6
  115. package/node_modules/@spexcode/spec-cli/dist/transcript-reader.d.ts +36 -0
  116. package/node_modules/@spexcode/spec-cli/dist/transcript-reader.js +251 -0
  117. package/node_modules/@spexcode/spec-cli/hooks/dispatch.sh +19 -31
  118. package/node_modules/@spexcode/spec-cli/hooks/harness.sh +6 -6
  119. package/node_modules/@spexcode/spec-cli/package.json +6 -6
  120. package/node_modules/@spexcode/spec-cli/templates/hooks/post-checkout +4 -2
  121. package/node_modules/@spexcode/spec-cli/templates/hooks/post-merge +2 -1
  122. package/node_modules/@spexcode/spec-cli/templates/hooks/pre-commit +5 -3
  123. package/node_modules/@spexcode/spec-cli/templates/hooks/reference-transaction +5 -3
  124. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/commands/spec.md +2 -7
  125. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/idle/idle.sh +4 -10
  126. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/idle/spec.md +1 -1
  127. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/mark-active/mark-active.sh +22 -24
  128. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/mark-active/spec.md +10 -2
  129. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-fail/fail.sh +8 -7
  130. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-fail/spec.md +3 -1
  131. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-listen/session-listen.sh +133 -0
  132. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-listen/spec.md +36 -0
  133. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/spec.md +2 -0
  134. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/stop-gate/spec.md +1 -1
  135. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +17 -20
  136. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/skills/merge/spec.md +33 -0
  137. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/skills/spec.md +2 -6
  138. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/spec.md +7 -0
  139. package/node_modules/@spexcode/spec-core/dist/anchors.d.ts +13 -2
  140. package/node_modules/@spexcode/spec-core/dist/anchors.js +311 -15
  141. package/node_modules/@spexcode/spec-core/dist/git.d.ts +4 -0
  142. package/node_modules/@spexcode/spec-core/dist/git.js +9 -9
  143. package/node_modules/@spexcode/spec-core/dist/layout.d.ts +12 -0
  144. package/node_modules/@spexcode/spec-core/dist/layout.js +4 -2
  145. package/node_modules/@spexcode/spec-core/dist/resilience.d.ts +1 -0
  146. package/node_modules/@spexcode/spec-core/dist/resilience.js +8 -0
  147. package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.d.ts +1 -0
  148. package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.js +7 -3
  149. package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.d.ts +1 -0
  150. package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.js +4 -0
  151. package/node_modules/@spexcode/spec-core/dist/specs.d.ts +2 -0
  152. package/node_modules/@spexcode/spec-core/dist/specs.js +30 -0
  153. package/node_modules/@spexcode/spec-core/package.json +4 -1
  154. package/node_modules/@spexcode/spec-eval/dist/cli.js +1 -1
  155. package/node_modules/@spexcode/spec-eval/dist/evaltab.d.ts +1 -0
  156. package/node_modules/@spexcode/spec-eval/dist/evaltab.js +6 -1
  157. package/node_modules/@spexcode/spec-eval/dist/freshness.d.ts +1 -1
  158. package/node_modules/@spexcode/spec-eval/dist/freshness.js +12 -12
  159. package/node_modules/@spexcode/spec-eval/dist/host.d.ts +2 -1
  160. package/node_modules/@spexcode/spec-eval/dist/sessioneval.d.ts +2 -1
  161. package/node_modules/@spexcode/spec-eval/dist/sessioneval.js +20 -3
  162. package/node_modules/@spexcode/spec-eval/package.json +2 -2
  163. package/node_modules/@spexcode/spec-forge/package.json +2 -2
  164. package/node_modules/@vscode/tree-sitter-wasm/LICENSE +21 -0
  165. package/node_modules/@vscode/tree-sitter-wasm/README.md +36 -0
  166. package/node_modules/@vscode/tree-sitter-wasm/SECURITY.md +41 -0
  167. package/node_modules/@vscode/tree-sitter-wasm/cgmanifest.json +16 -0
  168. package/node_modules/@vscode/tree-sitter-wasm/package.json +42 -0
  169. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-bash.wasm +0 -0
  170. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-c-sharp.wasm +0 -0
  171. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-cpp.wasm +0 -0
  172. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-css.wasm +0 -0
  173. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-go.wasm +0 -0
  174. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ini.wasm +0 -0
  175. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-java.wasm +0 -0
  176. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-javascript.wasm +0 -0
  177. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-php.wasm +0 -0
  178. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-powershell.wasm +0 -0
  179. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-python.wasm +0 -0
  180. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-regex.wasm +0 -0
  181. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ruby.wasm +0 -0
  182. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-rust.wasm +0 -0
  183. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-tsx.wasm +0 -0
  184. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-typescript.wasm +0 -0
  185. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.js +4075 -0
  186. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.wasm +0 -0
  187. package/node_modules/@vscode/tree-sitter-wasm/wasm/web-tree-sitter.d.ts +1027 -0
  188. package/package.json +9 -3
  189. package/node_modules/@spexcode/session-core/dist/delivery-queue.d.ts +0 -23
  190. package/node_modules/@spexcode/session-core/dist/delivery-queue.js +0 -195
  191. package/node_modules/@spexcode/session-core/dist/index.d.ts +0 -5
  192. package/node_modules/@spexcode/session-core/dist/index.js +0 -5
  193. package/node_modules/@spexcode/session-core/dist/internal.d.ts +0 -3
  194. package/node_modules/@spexcode/session-core/dist/internal.js +0 -3
  195. package/node_modules/@spexcode/session-core/dist/message.d.ts +0 -22
  196. package/node_modules/@spexcode/session-core/dist/message.js +0 -52
  197. package/node_modules/@spexcode/session-core/dist/record-lock.js +0 -152
  198. package/node_modules/@spexcode/session-core/dist/runtime-session.d.ts +0 -50
  199. package/node_modules/@spexcode/session-core/dist/runtime-session.js +0 -286
  200. package/node_modules/@spexcode/session-core/dist/session-cursors.d.ts +0 -14
  201. package/node_modules/@spexcode/session-core/dist/session-cursors.js +0 -82
  202. package/node_modules/@spexcode/session-core/dist/session-timeline.d.ts +0 -46
  203. package/node_modules/@spexcode/session-core/dist/session-timeline.js +0 -216
  204. package/node_modules/@spexcode/spec-cli/hooks/compat/mark-active-0.5.2-eef1.fixture +0 -53
  205. package/node_modules/@spexcode/spec-cli/hooks/compat/mark-active-sed-v0.fixture +0 -46
@@ -1,286 +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
- function scalar(value, field) {
18
- const normalized = value.trim();
19
- if (!normalized)
20
- throw new RuntimeSessionConflict(`${field} must be a non-empty string`);
21
- return normalized;
22
- }
23
- function metadata(value) {
24
- if (!value)
25
- return {};
26
- const entries = Object.entries(value).map(([rawKey, rawValue]) => {
27
- const key = scalar(rawKey, 'runtimeMetadata key');
28
- if (typeof rawValue !== 'string')
29
- throw new RuntimeSessionConflict(`runtimeMetadata.${key} must be a string`);
30
- return [key, rawValue];
31
- }).sort(([left], [right]) => left.localeCompare(right));
32
- if (new Set(entries.map(([key]) => key)).size !== entries.length)
33
- throw new RuntimeSessionConflict('runtimeMetadata keys must be unique after normalization');
34
- return Object.fromEntries(entries);
35
- }
36
- function metadataKey(value) {
37
- return JSON.stringify(metadata(value));
38
- }
39
- function readRaw(id) {
40
- const entry = readRecordEntry(id);
41
- if (entry.kind === 'absent')
42
- return null;
43
- if (entry.kind === 'corrupt')
44
- throw new RuntimeSessionConflict(`session ${id} record is corrupt: ${entry.error}`);
45
- return entry.raw;
46
- }
47
- function runtimeRecord(raw) {
48
- const runtimeOwner = raw.runtime_owner?.trim();
49
- if (!runtimeOwner)
50
- throw new RuntimeSessionConflict(`session ${raw.session_id} is not owned by an external runtime`);
51
- if (!isSessionLifecycle(raw.status))
52
- throw new RuntimeSessionConflict(`session ${raw.session_id} has invalid lifecycle ${JSON.stringify(raw.status)}`);
53
- if (!(raw.proposal == null || raw.proposal === '' || isSessionProposal(raw.proposal)))
54
- throw new RuntimeSessionConflict(`session ${raw.session_id} has invalid proposal ${JSON.stringify(raw.proposal)}`);
55
- return {
56
- sessionId: raw.session_id,
57
- runtimeOwner,
58
- runtimeState: raw.runtime_state?.trim() || null,
59
- revision: raw.runtime_revision?.trim() || null,
60
- worktreePath: raw.worktree_path,
61
- branch: raw.branch || null,
62
- parentSessionId: raw.parent || null,
63
- title: raw.title || null,
64
- node: raw.node || null,
65
- runtimeMetadata: metadata(raw.runtime_metadata),
66
- lifecycle: raw.status,
67
- proposal: isSessionProposal(raw.proposal) ? raw.proposal : null,
68
- note: raw.note || null,
69
- createdAt: Number(raw.createdAt) || 0,
70
- };
71
- }
72
- function writeRaw(raw) {
73
- const dir = sessionStoreDir(raw.session_id);
74
- mkdirSync(dir, { recursive: true });
75
- const path = sessionRecordPath(raw.session_id);
76
- const tmp = join(dir, `.session.json.${process.pid}.${randomUUID()}.tmp`);
77
- writeFileSync(tmp, JSON.stringify(raw, null, 2) + '\n');
78
- renameSync(tmp, path);
79
- }
80
- function readWatches(target) {
81
- let parsed;
82
- try {
83
- parsed = JSON.parse(readFileSync(watchPath(target), 'utf8'));
84
- }
85
- catch (error) {
86
- if (error.code === 'ENOENT')
87
- return [];
88
- throw new RuntimeSessionConflict(`session ${target} watch record is unreadable: ${error instanceof Error ? error.message : String(error)}`);
89
- }
90
- if (!Array.isArray(parsed))
91
- throw new RuntimeSessionConflict(`session ${target} watch record is not an array`);
92
- return parsed.map((candidate, index) => {
93
- if (!candidate || typeof candidate !== 'object')
94
- throw new RuntimeSessionConflict(`session ${target} watch row ${index} is invalid`);
95
- const row = candidate;
96
- if (typeof row.watcher !== 'string' || typeof row.createdAt !== 'string' || !Array.isArray(row.sources)
97
- || row.sources.some((source) => source !== 'manual' && source !== 'parent')
98
- || (row.snapshotPending !== undefined && typeof row.snapshotPending !== 'string'))
99
- throw new RuntimeSessionConflict(`session ${target} watch row ${index} is invalid`);
100
- return row;
101
- });
102
- }
103
- function writeWatches(target, entries) {
104
- const dir = sessionStoreDir(target);
105
- mkdirSync(dir, { recursive: true });
106
- const path = watchPath(target);
107
- const tmp = join(dir, `.watchers.json.${process.pid}.${randomUUID()}.tmp`);
108
- writeFileSync(tmp, JSON.stringify(entries, null, 2) + '\n');
109
- renameSync(tmp, path);
110
- }
111
- function registrationRaw(input) {
112
- const sessionId = scalar(input.sessionId, 'sessionId');
113
- const runtimeOwner = scalar(input.runtimeOwner, 'runtimeOwner');
114
- return {
115
- session_id: sessionId,
116
- governed: false,
117
- worktree_path: scalar(input.worktreePath, 'worktreePath'),
118
- branch: input.branch ?? '',
119
- node: input.node?.trim() || '',
120
- title: input.title?.trim() || '',
121
- name: '',
122
- parent: input.parentSessionId?.trim() || '',
123
- status: 'active',
124
- proposal: '',
125
- merges: 0,
126
- note: '',
127
- sortkey: null,
128
- createdAt: input.createdAt ?? Date.now(),
129
- harness: runtimeOwner,
130
- harness_session_id: sessionId,
131
- stopped: false,
132
- archived: false,
133
- cold_proof: '',
134
- adapter_recovery: '',
135
- launcher: '',
136
- launch_cmd: '',
137
- create_request_id: '',
138
- create_payload_hash: '',
139
- runtime_owner: runtimeOwner,
140
- runtime_state: 'registered',
141
- runtime_revision: '',
142
- runtime_metadata: metadata(input.runtimeMetadata),
143
- launch_readiness_pending: '',
144
- };
145
- }
146
- function sameRegistration(raw, input) {
147
- return raw.runtime_owner === input.runtimeOwner.trim()
148
- && raw.worktree_path === input.worktreePath.trim()
149
- && (raw.branch || null) === (input.branch || null)
150
- && (raw.parent || null) === (input.parentSessionId?.trim() || null)
151
- && metadataKey(raw.runtime_metadata) === metadataKey(input.runtimeMetadata);
152
- }
153
- export async function registerRuntimeSession(input) {
154
- const id = scalar(input.sessionId, 'sessionId');
155
- const parent = input.parentSessionId?.trim() || null;
156
- return withSessionRecordLocks([id, ...(parent ? [parent] : [])], async () => {
157
- if (parent) {
158
- const parentRaw = readRaw(parent);
159
- if (!parentRaw)
160
- throw new RuntimeSessionConflict(`parent runtime session ${parent} is not registered`);
161
- if (parentRaw.runtime_owner !== input.runtimeOwner.trim())
162
- throw new RuntimeSessionConflict(`parent runtime session ${parent} belongs to another runtime`);
163
- }
164
- const existing = readRaw(id);
165
- if (existing) {
166
- if (!sameRegistration(existing, input))
167
- throw new RuntimeSessionConflict(`session ${id} is already registered with different runtime coordinates`);
168
- return { replayed: true };
169
- }
170
- writeRaw(registrationRaw(input));
171
- if (parent)
172
- writeWatches(id, [{
173
- watcher: parent,
174
- createdAt: new Date().toISOString(),
175
- sources: ['parent'],
176
- snapshotPending: randomUUID(),
177
- }]);
178
- return { replayed: false };
179
- });
180
- }
181
- function stateMessage(record) {
182
- const proposal = record.lifecycle === 'awaiting' && record.proposal ? `/${record.proposal}` : '';
183
- const note = record.note ? ` — ${record.note}` : '';
184
- return `[spex watch] ${record.sessionId} is ${record.runtimeState ?? record.lifecycle}${proposal}${note}`;
185
- }
186
- function pendingFor(receipt, mid) {
187
- return {
188
- mid,
189
- text: receipt.delivery.text,
190
- from: receipt.delivery.from,
191
- dispatch: { operation: receipt.operation, requestDigest: receipt.requestDigest },
192
- };
193
- }
194
- export async function publishRuntimeSessionState(input) {
195
- const id = scalar(input.sessionId, 'sessionId');
196
- const owner = scalar(input.runtimeOwner, 'runtimeOwner');
197
- const revision = scalar(input.revision, 'revision');
198
- const runtimeState = scalar(input.runtimeState, 'runtimeState');
199
- const initial = readRaw(id);
200
- if (!initial)
201
- throw new RuntimeSessionConflict(`runtime session ${id} is not registered`);
202
- const watchers = readWatches(id).map((entry) => entry.watcher);
203
- return withSessionRecordLocks([id, ...watchers], async () => withDeliveryLocks(watchers, async () => {
204
- const raw = readRaw(id);
205
- if (!raw)
206
- throw new RuntimeSessionConflict(`runtime session ${id} disappeared during publication`);
207
- if (raw.runtime_owner !== owner)
208
- throw new RuntimeSessionConflict(`runtime session ${id} belongs to ${raw.runtime_owner || 'no external runtime'}, not ${owner}`);
209
- const proposal = input.proposal ?? null;
210
- const note = input.note ?? null;
211
- const sameRevision = raw.runtime_revision === revision;
212
- const sameState = raw.runtime_state === runtimeState && raw.status === input.lifecycle
213
- && (raw.proposal || null) === proposal && (raw.note || null) === note;
214
- if (sameRevision && !sameState)
215
- throw new RuntimeSessionConflict(`runtime revision ${revision} for session ${id} is already bound to another state`);
216
- const candidate = runtimeRecord({
217
- ...raw,
218
- status: input.lifecycle,
219
- proposal: proposal ?? '',
220
- note: note ?? '',
221
- runtime_state: runtimeState,
222
- runtime_revision: revision,
223
- });
224
- const message = stateMessage(candidate);
225
- const historical = watchers.map((watcher) => {
226
- const operation = `runtime-state:${id}`;
227
- const requestDigest = digest(`${id}\0${watcher}\0${revision}`);
228
- const payloadHash = digest(`${operation}\0${requestDigest}\0${message}`);
229
- const prior = sentDispatchReceipt(watcher, operation, requestDigest);
230
- if (prior && prior.payloadHash !== payloadHash)
231
- throw new RuntimeSessionConflict(`runtime revision ${revision} notification is already bound to different bytes`);
232
- return { watcher, operation, requestDigest, payloadHash, prior };
233
- });
234
- const historicalReplay = !sameRevision && historical.length > 0 && historical.every(({ prior }) => prior);
235
- const replayed = sameRevision || historicalReplay;
236
- if (!sameRevision) {
237
- if (!historicalReplay) {
238
- writeRaw({
239
- ...raw,
240
- status: input.lifecycle,
241
- proposal: proposal ?? '',
242
- note: note ?? '',
243
- runtime_state: runtimeState,
244
- runtime_revision: revision,
245
- });
246
- recordStatus(id, input.lifecycle, proposal, note);
247
- }
248
- }
249
- const notified = [];
250
- for (const { watcher, operation, requestDigest, payloadHash, prior } of historical) {
251
- const receipt = {
252
- operation,
253
- requestDigest,
254
- payloadHash,
255
- delivery: { text: message, from: id },
256
- };
257
- if (prior) {
258
- if (!prior.delivered)
259
- ensurePendingWhileLocked(watcher, pendingFor(receipt, prior.mid));
260
- }
261
- else {
262
- const { mid } = appendSent(watcher, message, id, undefined, receipt);
263
- enqueue(watcher, pendingFor(receipt, mid));
264
- }
265
- notified.push(watcher);
266
- }
267
- const entries = readWatches(id);
268
- if (entries.some((entry) => entry.snapshotPending)) {
269
- writeWatches(id, entries.map(({ snapshotPending: _pending, ...entry }) => entry));
270
- }
271
- return { notified, replayed };
272
- }));
273
- }
274
- export function readRuntimeSession(sessionId) {
275
- const raw = readRaw(scalar(sessionId, 'sessionId'));
276
- return raw?.runtime_owner ? runtimeRecord(raw) : null;
277
- }
278
- export function runtimeSessionChildren(parentSessionId, runtimeOwner) {
279
- const parent = scalar(parentSessionId, 'parentSessionId');
280
- return listSessionIds().flatMap((id) => {
281
- const record = readRuntimeSession(id);
282
- if (!record || record.parentSessionId !== parent || (runtimeOwner && record.runtimeOwner !== runtimeOwner))
283
- return [];
284
- return [record];
285
- }).sort((left, right) => left.createdAt - right.createdAt || left.sessionId.localeCompare(right.sessionId));
286
- }
@@ -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,46 +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
- };
25
- };
26
- export declare function recordStatus(id: string, status: SessionLifecycle, proposal: SessionProposal | null, note: string | null): void;
27
- export declare function appendSent(id: string, text: string, from: string | null, replyVia?: 'note', dispatchReceipt?: SentDispatchReceipt): {
28
- mid: string;
29
- };
30
- export type SentDispatchState = {
31
- mid: string;
32
- payloadHash: string;
33
- delivery: SentDispatchReceipt['delivery'] | null;
34
- delivered: boolean;
35
- };
36
- export declare function sentDispatchReceipt(id: string, operation: SentDispatchReceipt['operation'], requestDigest: string): SentDispatchState | null;
37
- export declare function settleSentDispatch(id: string, mid: string): void;
38
- export declare function timelineEvents(id: string): TimelineEvent[];
39
- export declare function timelineStamp(id: string): string | null;
40
- export declare function lastHumanSendVia(id: string): 'note' | null;
41
- export type SessionTurn = Readonly<{
42
- token: string;
43
- acceptedAt: string;
44
- }>;
45
- export declare function currentHumanTurn(id: string): SessionTurn | null;
46
- export declare function timelineTail(id: string, limit?: number): TimelineEvent[];
@@ -1,216 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { existsSync, readFileSync, appendFileSync, mkdirSync, statSync, readdirSync, openSync, closeSync, readSync } from 'node:fs';
3
- import { basename, join } from 'node:path';
4
- import { sessionStoreDir, sessionArtifactPath } from '@spexcode/spec-core';
5
- const timelinePath = (id) => sessionArtifactPath(id, 'timeline.ndjson');
6
- const segmentsDir = (id) => sessionArtifactPath(id, 'timeline');
7
- const SEGMENT = /^(\d+)\.ndjson$/;
8
- const SEGMENT_NAME_WIDTH = 12;
9
- const TAIL_BLOCK_BYTES = 64 * 1024;
10
- // One logical timeline is legacy timeline.ndjson followed by immutable numbered segments. The directory
11
- // listing is its only index: numbering is append order, so there is no mutable manifest to repair.
12
- function segmentFiles(id) {
13
- try {
14
- const dir = segmentsDir(id);
15
- const names = readdirSync(dir).filter((name) => SEGMENT.test(name)).sort((a, b) => {
16
- const an = BigInt(SEGMENT.exec(a)[1]), bn = BigInt(SEGMENT.exec(b)[1]);
17
- return an < bn ? -1 : an > bn ? 1 : 0;
18
- });
19
- return names.map((name) => join(dir, name));
20
- }
21
- catch { /* no numbered segments yet */ }
22
- return [];
23
- }
24
- function timelineFiles(id) {
25
- const files = [];
26
- const legacy = timelinePath(id);
27
- if (existsSync(legacy))
28
- files.push(legacy);
29
- files.push(...segmentFiles(id));
30
- return files;
31
- }
32
- const segmentLimit = () => {
33
- const configured = Number(process.env.SPEXCODE_TIMELINE_SEGMENT_BYTES);
34
- return Number.isFinite(configured) ? Math.max(1024, Math.floor(configured)) : 4 * 1024 * 1024;
35
- };
36
- function activeSegment(id, bytes) {
37
- const dir = segmentsDir(id);
38
- mkdirSync(dir, { recursive: true });
39
- const segments = segmentFiles(id);
40
- const current = segments.at(-1);
41
- if (!current)
42
- return join(dir, `${String(1).padStart(SEGMENT_NAME_WIDTH, '0')}.ndjson`);
43
- try {
44
- if (statSync(current).size === 0 || statSync(current).size + bytes <= segmentLimit())
45
- return current;
46
- }
47
- catch { /* a vanished active segment is recreated under its next number */ }
48
- const n = BigInt(SEGMENT.exec(basename(current))[1]) + 1n;
49
- return join(dir, `${String(n).padStart(SEGMENT_NAME_WIDTH, '0')}.ndjson`);
50
- }
51
- function append(id, ev) {
52
- mkdirSync(sessionStoreDir(id), { recursive: true });
53
- const line = JSON.stringify(ev) + '\n';
54
- appendFileSync(activeSegment(id, Buffer.byteLength(line)), line);
55
- }
56
- function parseLines(lines) {
57
- return lines.map((l) => {
58
- try {
59
- return JSON.parse(l);
60
- }
61
- catch {
62
- return null;
63
- }
64
- }).filter((e) => e != null && (e.kind === 'status' || e.kind === 'sent' || e.kind === 'dispatch-settled'));
65
- }
66
- function tailPublicEvents(path, limit) {
67
- let fd = null;
68
- try {
69
- const size = statSync(path).size;
70
- fd = openSync(path, 'r');
71
- let start = size;
72
- let text = '';
73
- while (start > 0) {
74
- const next = Math.max(0, start - TAIL_BLOCK_BYTES);
75
- const buf = Buffer.alloc(start - next);
76
- readSync(fd, buf, 0, buf.length, next);
77
- text = buf.toString('utf8') + text;
78
- const publicCount = parseLines(text.split('\n').filter(Boolean)).filter((event) => event.kind !== 'dispatch-settled').length;
79
- if (publicCount >= limit || next === 0)
80
- break;
81
- start = next;
82
- }
83
- return parseLines(text.split('\n').filter(Boolean))
84
- .filter((event) => event.kind !== 'dispatch-settled')
85
- .slice(-limit);
86
- }
87
- catch {
88
- return [];
89
- }
90
- finally {
91
- if (fd !== null)
92
- closeSync(fd);
93
- }
94
- }
95
- // Record a lifecycle value that has already landed in session.json. TypeScript state writers call this
96
- // synchronously before returning, so a later write cannot erase an intermediate declaration note from the
97
- // conversation. Best-effort: history is an accessory to the state machine, and failing to write it must never
98
- // break the transition that already happened.
99
- export function recordStatus(id, status, proposal, note) {
100
- try {
101
- append(id, { ts: new Date().toISOString(), kind: 'status', status, proposal, note });
102
- }
103
- catch { /* the record already moved; the history line is the only loss */ }
104
- }
105
- // The DELIVERY ([[dispatch]]): appending this line IS the send, so unlike a status line it must fail LOUD —
106
- // the caller reports the throw rather than a false success. `text` is the message BEFORE any mechanism insert
107
- // (hints are transport, not conversation); `replyVia` is the effective channel the prompt seam chose. Returns
108
- // the new line's `mid`, which a best-effort poke carries.
109
- export function appendSent(id, text, from, replyVia, dispatchReceipt) {
110
- const mid = randomUUID();
111
- append(id, { ts: new Date().toISOString(), kind: 'sent', mid, text, from, ...(replyVia ? { replyVia } : {}), ...(dispatchReceipt ? { dispatchReceipt } : {}) });
112
- return { mid };
113
- }
114
- export function sentDispatchReceipt(id, operation, requestDigest) {
115
- let found = null;
116
- const settled = new Set();
117
- for (const path of timelineFiles(id)) {
118
- for (const event of parseLines(readFileSync(path, 'utf8').split('\n').filter(Boolean))) {
119
- if (event.kind === 'sent' && !found && event.dispatchReceipt?.operation === operation && event.dispatchReceipt.requestDigest === requestDigest) {
120
- found = { mid: event.mid, payloadHash: event.dispatchReceipt.payloadHash, delivery: event.dispatchReceipt.delivery ?? null };
121
- }
122
- else if (event.kind === 'dispatch-settled' && event.operation === operation && event.requestDigest === requestDigest) {
123
- settled.add(event.mid);
124
- }
125
- }
126
- }
127
- return found ? { ...found, delivered: settled.has(found.mid) } : null;
128
- }
129
- export function settleSentDispatch(id, mid) {
130
- let receipt = null;
131
- let settled = false;
132
- for (const path of timelineFiles(id)) {
133
- for (const event of parseLines(readFileSync(path, 'utf8').split('\n').filter(Boolean))) {
134
- if (event.kind === 'sent' && event.mid === mid && event.dispatchReceipt?.delivery)
135
- receipt = event.dispatchReceipt;
136
- if (event.kind === 'dispatch-settled' && event.mid === mid)
137
- settled = true;
138
- }
139
- }
140
- if (!receipt || settled)
141
- return;
142
- append(id, { ts: new Date().toISOString(), kind: 'dispatch-settled', operation: receipt.operation, requestDigest: receipt.requestDigest, mid });
143
- }
144
- // The unowned read: any process may take it with nothing but filesystem access, and taking it perturbs
145
- // nothing. Index = event position, which is what a cursor names ([[session-cursors]]).
146
- export function timelineEvents(id) {
147
- try {
148
- return timelineFiles(id).flatMap((path) => parseLines(readFileSync(path, 'utf8').split('\n').filter(Boolean)))
149
- .flatMap((stored) => {
150
- if (stored.kind === 'dispatch-settled')
151
- return [];
152
- if (stored.kind === 'status')
153
- return [stored];
154
- const { dispatchReceipt: _receipt, ...event } = stored;
155
- return [event];
156
- });
157
- }
158
- catch {
159
- return [];
160
- }
161
- }
162
- // the same L0 read taken as CHEAPLY as it can be: a follower ([[session-follow]]) ticks over many logs, so it
163
- // stats first and parses only what grew. null = no log yet (a session that has authored nothing).
164
- export function timelineStamp(id) {
165
- try {
166
- const path = timelineFiles(id).at(-1);
167
- if (!path)
168
- return null;
169
- const s = statSync(path);
170
- return `${path}:${s.size}:${s.mtimeMs}`;
171
- }
172
- catch {
173
- return null;
174
- }
175
- }
176
- // the channel of the LAST HUMAN send (from == null): 'note' when the note-reply hint rode along, else null.
177
- // This is what makes the reply-channel hints SYMMETRIC ([[session-timeline]]): a human send with no note flag
178
- // arriving after a note-send is the "back at a terminal" transition, and the delivery gets the counter-insert.
179
- // Derived from the durable log — no new state, and it survives a server restart. Agent senders (`from` set)
180
- // say nothing about where the HUMAN is reading, so they neither set nor clear it.
181
- export function lastHumanSendVia(id) {
182
- const evs = timelineEvents(id);
183
- for (let i = evs.length - 1; i >= 0; i--) {
184
- const e = evs[i];
185
- if (e.kind === 'sent' && e.from == null)
186
- return e.replyVia === 'note' ? 'note' : null;
187
- }
188
- return null;
189
- }
190
- export function currentHumanTurn(id) {
191
- const evs = timelineEvents(id);
192
- for (let i = evs.length - 1; i >= 0; i--) {
193
- const e = evs[i];
194
- if (e.kind === 'sent' && e.from == null)
195
- return { token: e.mid, acceptedAt: e.ts };
196
- }
197
- return null;
198
- }
199
- // Durable tail read with no board/governance policy. A runtime that owns a compatible session directory can
200
- // consume the same protocol; product surfaces decide separately whether that id is visible to them.
201
- export function timelineTail(id, limit = 500) {
202
- const wanted = Math.max(1, limit);
203
- const tail = [];
204
- for (const path of timelineFiles(id).reverse()) {
205
- const remaining = wanted - tail.length;
206
- if (remaining <= 0)
207
- break;
208
- tail.unshift(...tailPublicEvents(path, remaining));
209
- }
210
- return tail.map((e) => {
211
- if (e.kind === 'status')
212
- return e;
213
- const { dispatchReceipt: _receipt, ...event } = e;
214
- return event;
215
- });
216
- }