spexcode 0.7.0-next.13 → 0.7.0-next.14

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/session-application",
3
- "version": "0.7.0-next.13",
3
+ "version": "0.7.0-next.14",
4
4
  "publishConfig": { "access": "public" },
5
5
  "type": "module",
6
6
  "description": "Adopter-owned composition of session protocol and topology transactions.",
@@ -16,10 +16,10 @@
16
16
  "test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
17
17
  },
18
18
  "dependencies": {
19
- "@spexcode/session-events": "0.7.0-next.13",
20
- "@spexcode/session-protocol": "0.7.0-next.13",
21
- "@spexcode/session-runtime": "0.7.0-next.13",
22
- "@spexcode/session-topology": "0.7.0-next.13"
19
+ "@spexcode/session-events": "0.7.0-next.14",
20
+ "@spexcode/session-protocol": "0.7.0-next.14",
21
+ "@spexcode/session-runtime": "0.7.0-next.14",
22
+ "@spexcode/session-topology": "0.7.0-next.14"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/node": "^20.16.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/session-events",
3
- "version": "0.7.0-next.13",
3
+ "version": "0.7.0-next.14",
4
4
  "publishConfig": { "access": "public" },
5
5
  "type": "module",
6
6
  "description": "Append-only session facts and deterministic ordered replay inputs.",
@@ -15,7 +15,7 @@
15
15
  "prepublishOnly": "node ../../scripts/release-publish.mjs --from-package-publish",
16
16
  "test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
17
17
  },
18
- "dependencies": { "@spexcode/session-protocol": "0.7.0-next.13" },
18
+ "dependencies": { "@spexcode/session-protocol": "0.7.0-next.14" },
19
19
  "devDependencies": {
20
20
  "@types/node": "^20.16.0",
21
21
  "tsx": "^4.19.2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/session-protocol",
3
- "version": "0.7.0-next.13",
3
+ "version": "0.7.0-next.14",
4
4
  "type": "module",
5
5
  "description": "A durable SQLite session message protocol.",
6
6
  "files": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/session-runtime",
3
- "version": "0.7.0-next.13",
3
+ "version": "0.7.0-next.14",
4
4
  "publishConfig": { "access": "public" },
5
5
  "type": "module",
6
6
  "description": "Adopter-owned bindings between protocol addresses and native runtime identities.",
@@ -15,7 +15,7 @@
15
15
  "prepublishOnly": "node ../../scripts/release-publish.mjs --from-package-publish",
16
16
  "test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
17
17
  },
18
- "dependencies": { "@spexcode/session-protocol": "0.7.0-next.13" },
18
+ "dependencies": { "@spexcode/session-protocol": "0.7.0-next.14" },
19
19
  "devDependencies": {
20
20
  "@types/node": "^20.16.0",
21
21
  "tsx": "^4.19.2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/session-selflaunch",
3
- "version": "0.7.0-next.13",
3
+ "version": "0.7.0-next.14",
4
4
  "publishConfig": { "access": "public" },
5
5
  "type": "module",
6
6
  "description": "A fail-closed self-launch adopter for the SpexCode session protocol.",
@@ -24,8 +24,8 @@
24
24
  "test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
25
25
  },
26
26
  "dependencies": {
27
- "@spexcode/session-protocol": "0.7.0-next.13",
28
- "@spexcode/session-runtime": "0.7.0-next.13"
27
+ "@spexcode/session-protocol": "0.7.0-next.14",
28
+ "@spexcode/session-runtime": "0.7.0-next.14"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/node": "^20.16.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/session-topology",
3
- "version": "0.7.0-next.13",
3
+ "version": "0.7.0-next.14",
4
4
  "publishConfig": { "access": "public" },
5
5
  "type": "module",
6
6
  "description": "A neutral SQLite relation model for session addresses.",
@@ -15,7 +15,7 @@
15
15
  "prepublishOnly": "node ../../scripts/release-publish.mjs --from-package-publish",
16
16
  "test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
17
17
  },
18
- "dependencies": { "@spexcode/session-protocol": "0.7.0-next.13" },
18
+ "dependencies": { "@spexcode/session-protocol": "0.7.0-next.14" },
19
19
  "devDependencies": {
20
20
  "@types/node": "^20.16.0",
21
21
  "tsx": "^4.19.2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/spec-cli",
3
- "version": "0.7.0-next.13",
3
+ "version": "0.7.0-next.14",
4
4
  "type": "module",
5
5
  "description": "SpexCode CLI + server. The root spexcode package delegates to this compiled package; dashboard assets live in @spexcode/spec-dashboard.",
6
6
  "bin": {
@@ -35,12 +35,12 @@
35
35
  "test": "tsx --import ../scripts/test-home.mjs --test src/*.test.ts"
36
36
  },
37
37
  "dependencies": {
38
- "@spexcode/session-application": "0.7.0-next.13",
39
- "@spexcode/session-selflaunch": "0.7.0-next.13",
40
- "@spexcode/spec-core": "0.7.0-next.13",
41
- "@spexcode/spec-eval": "0.7.0-next.13",
42
- "@spexcode/spec-forge": "0.7.0-next.13",
43
- "@spexcode/transcript": "0.7.0-next.13",
38
+ "@spexcode/session-application": "0.7.0-next.14",
39
+ "@spexcode/session-selflaunch": "0.7.0-next.14",
40
+ "@spexcode/spec-core": "0.7.0-next.14",
41
+ "@spexcode/spec-eval": "0.7.0-next.14",
42
+ "@spexcode/spec-forge": "0.7.0-next.14",
43
+ "@spexcode/transcript": "0.7.0-next.14",
44
44
  "smol-toml": "^1.8.0"
45
45
  },
46
46
  "devDependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/spec-core",
3
- "version": "0.7.0-next.13",
3
+ "version": "0.7.0-next.14",
4
4
  "type": "module",
5
5
  "description": "SpexCode's dependency-minimal spec graph core.",
6
6
  "files": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/spec-eval",
3
- "version": "0.7.0-next.13",
3
+ "version": "0.7.0-next.14",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js",
@@ -26,7 +26,7 @@
26
26
  "test": "tsx --import ../scripts/test-home.mjs --test src/*.test.ts"
27
27
  },
28
28
  "dependencies": {
29
- "@spexcode/spec-core": "0.7.0-next.13"
29
+ "@spexcode/spec-core": "0.7.0-next.14"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^20.16.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/spec-forge",
3
- "version": "0.7.0-next.13",
3
+ "version": "0.7.0-next.14",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "test": "tsx --test src/*.test.ts"
23
23
  },
24
24
  "dependencies": {
25
- "@spexcode/spec-core": "0.7.0-next.13"
25
+ "@spexcode/spec-core": "0.7.0-next.14"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^20.16.0",
@@ -45,6 +45,7 @@ export declare class IntervalCollector {
45
45
  private readonly evicted;
46
46
  private readonly synthesized;
47
47
  sawTimestamp: boolean;
48
+ sawRecord: boolean;
48
49
  omittedTurns: number;
49
50
  omittedBytes: number;
50
51
  outOfOrderEvents: number;
@@ -133,7 +133,14 @@ export function claudeEvent(value) {
133
133
  return { at: eventAt, turn: { id: idOf(entry) ?? idOf(message), at: eventAt, role: 'user', text, tools: [] } };
134
134
  }
135
135
  if (entry.type === 'assistant' && message.role === 'assistant') {
136
- const turn = { id: idOf(entry) ?? idOf(message), at: eventAt, role: 'assistant', tools: [] };
136
+ // ONE API MESSAGE IS ONE TURN, however many lines Claude wrote it as. It writes one CONTENT BLOCK per
137
+ // line — prose on one, each tool call on its own — and every line of the same message repeats that
138
+ // message's `id` while carrying its own `uuid`. Keying on the line made one message up to six turns:
139
+ // measured over twelve recent threads, 5,077 assistant lines carry only 2,396 distinct message ids, so a
140
+ // turn count read off the lines is more than twice the truth. Keying on the message id lets the collector
141
+ // fold the fragments back together with the rule it already has for a re-emitted turn — text kept, calls
142
+ // merged by their own ids — so nothing here has to accumulate.
143
+ const turn = { id: idOf(message) ?? idOf(entry), at: eventAt, role: 'assistant', tools: [] };
137
144
  for (const blockValue of items(message.content)) {
138
145
  const block = object(blockValue);
139
146
  if (block?.type === 'text')
@@ -517,6 +524,7 @@ export class IntervalCollector {
517
524
  evicted = new Set();
518
525
  synthesized = new Map(); // `<role>@<at>` → how many turns already wore it
519
526
  sawTimestamp = false;
527
+ sawRecord = false; // a record this harness's parser RECOGNIZED arrived, with or without a clock
520
528
  omittedTurns = 0;
521
529
  omittedBytes = 0;
522
530
  outOfOrderEvents = 0;
@@ -528,6 +536,7 @@ export class IntervalCollector {
528
536
  this.range.to = to; }
529
537
  // returns true once the source has moved past `to` (the caller may then bound its lookahead)
530
538
  add(event) {
539
+ this.sawRecord = true;
531
540
  const eventAt = event.at;
532
541
  if (eventAt === null)
533
542
  return this.pastRange;
@@ -600,7 +609,13 @@ export class IntervalCollector {
600
609
  return this.pastRange;
601
610
  }
602
611
  finish(revision, harness) {
603
- if (!this.sawTimestamp)
612
+ // THE CLOCK GATE IS ABOUT THE HARNESS, NOT THE MOMENT. It catches a source whose conversational records
613
+ // carry no usable time, which makes interval reads impossible. It must NOT catch a thread that has simply
614
+ // not spoken yet: every Claude transcript opens with clockless bookkeeping (`mode`, `permission-mode`,
615
+ // `file-history-snapshot`) before its first message — 40 of 40 real threads on this box — and those lines
616
+ // are not records this parser recognizes at all, so failing on them put an error frame on the page for the
617
+ // first moments of EVERY new session, which is exactly when someone is watching.
618
+ if (this.sawRecord && !this.sawTimestamp)
604
619
  throw new TranscriptReadError('invalid', `${harness} transcript has no reliable timestamps; interval reads are unavailable`);
605
620
  return {
606
621
  revision,
@@ -205,8 +205,13 @@ class LineFileCursor {
205
205
  catch (error) {
206
206
  throw new TranscriptReadError('unreadable', `${this.harness} transcript is unreadable: ${error instanceof Error ? error.message : String(error)}`);
207
207
  }
208
+ // AN EMPTY FILE IS A THREAD THAT HAS NOT SPOKEN YET, not a broken one. The harness creates the transcript
209
+ // before it writes the first record, so the moments right after a session starts — exactly when a person is
210
+ // watching — read as zero bytes. Failing there put an error on the page for a conversation that simply had
211
+ // not begun. Zero bytes is unambiguous in a way a garbled file is not: there is nothing to misread, so this
212
+ // is the one place the no-timestamp gate is skipped rather than tripped.
208
213
  if (size <= 0)
209
- throw new TranscriptReadError('unreadable', `${this.harness} transcript is unreadable: file is empty`);
214
+ return { revision: fileRevision(this.path) ?? '0', from: this.from, to, turns: [], truncated: false, omittedTurns: 0, omittedBytes: 0, outOfOrderEvents: 0 };
210
215
  // a source that shrank was rewritten underneath the cursor: forget the position and read the interval afresh
211
216
  if (!this.started || size < this.scan.position) {
212
217
  this.restart(size);
@@ -216,8 +221,9 @@ class LineFileCursor {
216
221
  let fd = null;
217
222
  try {
218
223
  fd = openSync(this.path, 'r');
219
- let postRangeLines = 0;
224
+ let postRangeLines = 0, parsedLines = 0, unparsableLines = 0;
220
225
  this.scan = scanLines(fd, this.scan, (value, offset) => {
226
+ parsedLines++;
221
227
  const event = this.parse(value);
222
228
  if (!event)
223
229
  return false;
@@ -226,7 +232,11 @@ class LineFileCursor {
226
232
  intervalOffsets.set(this.seekKey, offset);
227
233
  const pastRange = this.collector.add(event);
228
234
  return pastRange && ++postRangeLines >= lookahead;
229
- }, (bytes) => { this.collector.omittedBytes += bytes; });
235
+ }, (bytes) => { unparsableLines++; this.collector.omittedBytes += bytes; });
236
+ // a file in which NOTHING is JSON is not this thread's transcript at all — that is the loud case, and it
237
+ // stays loud; a file whose lines parse but say nothing conversational yet is simply not started
238
+ if (!parsedLines && unparsableLines > 0)
239
+ throw new TranscriptReadError('invalid', `${this.harness} transcript cannot be parsed: no line is JSON`);
230
240
  }
231
241
  catch (error) {
232
242
  if (error instanceof TranscriptReadError)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/transcript",
3
- "version": "0.7.0-next.13",
3
+ "version": "0.7.0-next.14",
4
4
  "type": "module",
5
5
  "description": "Normalized agent transcripts: one parser per harness, a bounded interval reader over a native thread file or an in-memory event stream, and the full/delta frame protocol every transport and renderer share.",
6
6
  "files": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spexcode",
3
- "version": "0.7.0-next.13",
3
+ "version": "0.7.0-next.14",
4
4
  "type": "module",
5
5
  "description": "SpexCode — a spec-driven, self-developing dev tool. The `spex` CLI + spec server reads the .spec tree and its git history, and serves the dashboard.",
6
6
  "license": "MIT",
@@ -44,7 +44,7 @@
44
44
  "prepack": "node scripts/sync-init-plugins.mjs --check && node scripts/prepack.mjs"
45
45
  },
46
46
  "dependencies": {
47
- "@spexcode/spec-cli": "0.7.0-next.13"
47
+ "@spexcode/spec-cli": "0.7.0-next.14"
48
48
  },
49
49
  "publishConfig": {
50
50
  "access": "public"