pi-mega-compact 0.21.3 → 0.21.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/dist/extensions/mega-events/agent-handlers/turnEndHandler/recordTurnRow.js +12 -2
  2. package/dist/extensions/mega-events/context-handler/afterCompact.js +11 -0
  3. package/dist/extensions/mega-events/context-handler/dbMirrorAppend.js +8 -0
  4. package/dist/extensions/mega-events/health-handler.js +12 -2
  5. package/dist/extensions/mega-events/session-handlers.js +1 -0
  6. package/dist/extensions/mega-pipeline/compact/run.js +11 -0
  7. package/dist/extensions/mega-runtime/helpers.js +45 -0
  8. package/dist/extensions/mega-runtime/internal-errors.js +14 -0
  9. package/dist/extensions/mega-runtime/runtime-instrumentation.js +166 -0
  10. package/dist/extensions/mega-runtime/runtime.js +27 -193
  11. package/dist/extensions/mega-runtime/state.js +1 -1
  12. package/dist/extensions/mega-runtime/widget.js +1 -1
  13. package/dist/extensions/mega-turn-store.js +5 -0
  14. package/dist/src/bridge/factory.js +15 -1
  15. package/dist/src/contextHealth.js +13 -5
  16. package/dist/src/metrics/turns.js +6 -1
  17. package/dist/src/store/sqlite/context-health-cache-poison.js +60 -0
  18. package/dist/src/store/sqlite/context-health.js +6 -48
  19. package/dist/src/store/sqlite/embedding-cache.js +4 -0
  20. package/dist/src/store/sqlite/schema.js +4 -0
  21. package/dist/src/store/turns/memory-store/admin.js +119 -0
  22. package/dist/src/store/turns/memory-store/ctx.js +28 -0
  23. package/dist/src/store/turns/memory-store/read.js +110 -0
  24. package/dist/src/store/turns/memory-store/write.js +88 -0
  25. package/dist/src/store/turns/memory-store.js +44 -273
  26. package/dist/src/store/turns/migrations.js +4 -0
  27. package/dist/src/store/turns/schema.js +1 -0
  28. package/dist/src/store/turns/sqlite-store/read.js +13 -0
  29. package/dist/src/store/turns/sqlite-store/rows.js +4 -0
  30. package/dist/src/store/turns/sqlite-store/write.js +3 -3
  31. package/dist/src/store/turns/sqlite-store.js +4 -0
  32. package/extensions/dashboard-client/dist/assets/{AdminTab-BsA8-_bX.js → AdminTab-C4Y0_UVx.js} +2 -2
  33. package/extensions/dashboard-client/dist/assets/{AdminTab-BsA8-_bX.js.map → AdminTab-C4Y0_UVx.js.map} +1 -1
  34. package/extensions/dashboard-client/dist/assets/{AreaChart-BRHEfWhc.js → AreaChart-MCWK9rn6.js} +2 -2
  35. package/extensions/dashboard-client/dist/assets/{AreaChart-BRHEfWhc.js.map → AreaChart-MCWK9rn6.js.map} +1 -1
  36. package/extensions/dashboard-client/dist/assets/{BarChart-C-M0LaFc.js → BarChart-CvSGAiWV.js} +2 -2
  37. package/extensions/dashboard-client/dist/assets/{BarChart-C-M0LaFc.js.map → BarChart-CvSGAiWV.js.map} +1 -1
  38. package/extensions/dashboard-client/dist/assets/{CacheTab-Cj3n59U3.js → CacheTab-DnMEVRro.js} +2 -2
  39. package/extensions/dashboard-client/dist/assets/{CacheTab-Cj3n59U3.js.map → CacheTab-DnMEVRro.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/assets/{ConfigTab-C016XTNC.js → ConfigTab-BExuZYy2.js} +2 -2
  41. package/extensions/dashboard-client/dist/assets/{ConfigTab-C016XTNC.js.map → ConfigTab-BExuZYy2.js.map} +1 -1
  42. package/extensions/dashboard-client/dist/assets/{EventsTab-DE29fuqe.js → EventsTab-CspsEslY.js} +2 -2
  43. package/extensions/dashboard-client/dist/assets/{EventsTab-DE29fuqe.js.map → EventsTab-CspsEslY.js.map} +1 -1
  44. package/extensions/dashboard-client/dist/assets/HealthTab-DiWzo2ML.js +2 -0
  45. package/extensions/dashboard-client/dist/assets/HealthTab-DiWzo2ML.js.map +1 -0
  46. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-4fJ7M4S4.js → MaintenanceTab-9NJuv3dB.js} +2 -2
  47. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-4fJ7M4S4.js.map → MaintenanceTab-9NJuv3dB.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CDbcK9e4.js → MemoryMapTab-YpJK1h17.js} +2 -2
  49. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CDbcK9e4.js.map → MemoryMapTab-YpJK1h17.js.map} +1 -1
  50. package/extensions/dashboard-client/dist/assets/{MetricsCards-VUEcgGzo.js → MetricsCards-D2Mom1M7.js} +2 -2
  51. package/extensions/dashboard-client/dist/assets/{MetricsCards-VUEcgGzo.js.map → MetricsCards-D2Mom1M7.js.map} +1 -1
  52. package/extensions/dashboard-client/dist/assets/MetricsTab-DqWF7wud.js +2 -0
  53. package/extensions/dashboard-client/dist/assets/{MetricsTab-CKqJ0Zl-.js.map → MetricsTab-DqWF7wud.js.map} +1 -1
  54. package/extensions/dashboard-client/dist/assets/{OverviewTab-9Kxo79mi.js → OverviewTab-YuRxjV9U.js} +2 -2
  55. package/extensions/dashboard-client/dist/assets/{OverviewTab-9Kxo79mi.js.map → OverviewTab-YuRxjV9U.js.map} +1 -1
  56. package/extensions/dashboard-client/dist/assets/{ReposTab-C4iveqrP.js → ReposTab-sXMrR7je.js} +2 -2
  57. package/extensions/dashboard-client/dist/assets/{ReposTab-C4iveqrP.js.map → ReposTab-sXMrR7je.js.map} +1 -1
  58. package/extensions/dashboard-client/dist/assets/{SessionsTab-DSEwkMQm.js → SessionsTab-4YCXeGNR.js} +2 -2
  59. package/extensions/dashboard-client/dist/assets/{SessionsTab-DSEwkMQm.js.map → SessionsTab-4YCXeGNR.js.map} +1 -1
  60. package/extensions/dashboard-client/dist/assets/{SetupTab-CCyWgy4O.js → SetupTab-67pZcM_W.js} +2 -2
  61. package/extensions/dashboard-client/dist/assets/{SetupTab-CCyWgy4O.js.map → SetupTab-67pZcM_W.js.map} +1 -1
  62. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CyD8e_e_.js → TimeSavedCard-DInjNETP.js} +2 -2
  63. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CyD8e_e_.js.map → TimeSavedCard-DInjNETP.js.map} +1 -1
  64. package/extensions/dashboard-client/dist/assets/{TurnMemoryView-5tLbWkRv.js → TurnMemoryView-C3Bif8ix.js} +2 -2
  65. package/extensions/dashboard-client/dist/assets/{TurnMemoryView-5tLbWkRv.js.map → TurnMemoryView-C3Bif8ix.js.map} +1 -1
  66. package/extensions/dashboard-client/dist/assets/TurnsTab-BUGfiANp.js +2 -0
  67. package/extensions/dashboard-client/dist/assets/{TurnsTab-C7pm92Hg.js.map → TurnsTab-BUGfiANp.js.map} +1 -1
  68. package/extensions/dashboard-client/dist/assets/{VcStatusBadge-ObhcIyfN.js → VcStatusBadge-DKU7qz9f.js} +2 -2
  69. package/extensions/dashboard-client/dist/assets/{VcStatusBadge-ObhcIyfN.js.map → VcStatusBadge-DKU7qz9f.js.map} +1 -1
  70. package/extensions/dashboard-client/dist/assets/{VectorCortexTab-BtHZb7o7.js → VectorCortexTab-BsVSf4xm.js} +2 -2
  71. package/extensions/dashboard-client/dist/assets/{VectorCortexTab-BtHZb7o7.js.map → VectorCortexTab-BsVSf4xm.js.map} +1 -1
  72. package/extensions/dashboard-client/dist/assets/{WikiTab-lQ4l3QIK.js → WikiTab-DZw9_Fwd.js} +2 -2
  73. package/extensions/dashboard-client/dist/assets/{WikiTab-lQ4l3QIK.js.map → WikiTab-DZw9_Fwd.js.map} +1 -1
  74. package/extensions/dashboard-client/dist/assets/{button-DhQQAJGm.js → button-BLRPN489.js} +2 -2
  75. package/extensions/dashboard-client/dist/assets/{button-DhQQAJGm.js.map → button-BLRPN489.js.map} +1 -1
  76. package/extensions/dashboard-client/dist/assets/{card-Cfhz9X31.js → card-BGkkcM4Q.js} +2 -2
  77. package/extensions/dashboard-client/dist/assets/{card-Cfhz9X31.js.map → card-BGkkcM4Q.js.map} +1 -1
  78. package/extensions/dashboard-client/dist/assets/{client-extra-CKNOGEz_.js → client-extra-0OJClxHD.js} +2 -2
  79. package/extensions/dashboard-client/dist/assets/{client-extra-CKNOGEz_.js.map → client-extra-0OJClxHD.js.map} +1 -1
  80. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-JKjK9_cB.js → generateCategoricalChart-Dl5lmrHF.js} +2 -2
  81. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-JKjK9_cB.js.map → generateCategoricalChart-Dl5lmrHF.js.map} +1 -1
  82. package/extensions/dashboard-client/dist/assets/health-lSeDFTz7.js.map +1 -1
  83. package/extensions/dashboard-client/dist/assets/{index-iuxgtzEW.js → index-CH_AD-S_.js} +3 -3
  84. package/extensions/dashboard-client/dist/assets/{index-iuxgtzEW.js.map → index-CH_AD-S_.js.map} +1 -1
  85. package/extensions/dashboard-client/dist/assets/{switch-7EHEQMSP.js → switch-BPP2-d12.js} +2 -2
  86. package/extensions/dashboard-client/dist/assets/{switch-7EHEQMSP.js.map → switch-BPP2-d12.js.map} +1 -1
  87. package/extensions/dashboard-client/dist/assets/{toggle-z1lWY4UM.js → toggle-Dez0JaMV.js} +2 -2
  88. package/extensions/dashboard-client/dist/assets/{toggle-z1lWY4UM.js.map → toggle-Dez0JaMV.js.map} +1 -1
  89. package/extensions/dashboard-client/dist/assets/{useSSE-sctOZuRM.js → useSSE-Cx2W7BTo.js} +2 -2
  90. package/extensions/dashboard-client/dist/assets/{useSSE-sctOZuRM.js.map → useSSE-Cx2W7BTo.js.map} +1 -1
  91. package/extensions/dashboard-client/dist/index.html +1 -1
  92. package/extensions/dashboard-client/src/api/health.ts +1 -0
  93. package/extensions/dashboard-client/src/tabs/HealthTab.tsx +1 -0
  94. package/extensions/mega-events/agent-handlers/turnEndHandler/recordTurnRow.ts +12 -2
  95. package/extensions/mega-events/context-handler/afterCompact.ts +11 -0
  96. package/extensions/mega-events/context-handler/dbMirrorAppend.ts +8 -0
  97. package/extensions/mega-events/health-handler.ts +13 -3
  98. package/extensions/mega-events/session-handlers.ts +1 -0
  99. package/extensions/mega-pipeline/compact/run.ts +11 -0
  100. package/extensions/mega-runtime/helpers.ts +46 -0
  101. package/extensions/mega-runtime/internal-errors.ts +30 -0
  102. package/extensions/mega-runtime/runtime-instrumentation.ts +195 -0
  103. package/extensions/mega-runtime/runtime.ts +29 -209
  104. package/extensions/mega-runtime/state.ts +1 -1
  105. package/extensions/mega-runtime/widget.ts +1 -1
  106. package/extensions/mega-turn-store.ts +9 -0
  107. package/package.json +2 -2
  108. package/src/bridge/factory.ts +15 -1
  109. package/src/bridge/types.ts +2 -0
  110. package/src/contextHealth.ts +26 -6
  111. package/src/metrics/turns.ts +6 -1
  112. package/src/store/sqlite/context-health-cache-poison.ts +97 -0
  113. package/src/store/sqlite/context-health.ts +12 -84
  114. package/src/store/sqlite/embedding-cache.ts +4 -0
  115. package/src/store/sqlite/schema.ts +4 -0
  116. package/src/store/turns/memory-store/admin.ts +171 -0
  117. package/src/store/turns/memory-store/ctx.ts +62 -0
  118. package/src/store/turns/memory-store/read.ts +172 -0
  119. package/src/store/turns/memory-store/write.ts +126 -0
  120. package/src/store/turns/memory-store.ts +51 -330
  121. package/src/store/turns/migrations.ts +4 -0
  122. package/src/store/turns/schema.ts +1 -0
  123. package/src/store/turns/sqlite-store/read.ts +19 -0
  124. package/src/store/turns/sqlite-store/rows.ts +5 -0
  125. package/src/store/turns/sqlite-store/write.ts +3 -2
  126. package/src/store/turns/sqlite-store.ts +5 -0
  127. package/src/store/turns/types.ts +15 -0
  128. package/extensions/dashboard-client/dist/assets/HealthTab-D4eVI8ar.js +0 -2
  129. package/extensions/dashboard-client/dist/assets/HealthTab-D4eVI8ar.js.map +0 -1
  130. package/extensions/dashboard-client/dist/assets/MetricsTab-CKqJ0Zl-.js +0 -2
  131. package/extensions/dashboard-client/dist/assets/TurnsTab-C7pm92Hg.js +0 -2
@@ -166,6 +166,9 @@ function doCompact(
166
166
  touchSession(sid, root, runtime.currentStateDir);
167
167
  logDaily(sid, "compact", result.checkpointId, saved, runtime.currentStateDir);
168
168
  } catch {
169
+ // Sprint H (Option A): session-activity / daily-log write is an internal
170
+ // store-write failure — feed the separate `storeErrorRate` axis.
171
+ runtime.recordInternalError("store_write");
169
172
  /* non-fatal: stats bookkeeping only */
170
173
  }
171
174
 
@@ -183,10 +186,14 @@ function doCompact(
183
186
  if (n > 0) runtime.pushTicker(`${C.green}∫${C.reset} consolidated ${n} memory dup${n === 1 ? "" : "s"}`);
184
187
  },
185
188
  () => {
189
+ // Sprint H (Option A): memory-consolidation write failure.
190
+ runtime.recordInternalError("store_write");
186
191
  /* swallow: consolidate failures must never surface to the user */
187
192
  },
188
193
  );
189
194
  } catch {
195
+ // Sprint H (Option A): memory-consolidation write failure (sync throw).
196
+ runtime.recordInternalError("store_write");
190
197
  /* non-fatal */
191
198
  }
192
199
  }
@@ -259,6 +266,8 @@ function doCompact(
259
266
  }
260
267
  }
261
268
  } catch {
269
+ // Sprint H (Option A): RAPTOR tree-refresh write failure.
270
+ runtime.recordInternalError("vector_index");
262
271
  /* non-fatal: tree refresh never blocks a compaction */
263
272
  }
264
273
  }
@@ -278,6 +287,8 @@ function doCompact(
278
287
  latest.checkpointId,
279
288
  latest.embedding,
280
289
  ).catch(() => {
290
+ // Sprint H (Option A): async global vector-index upsert failure.
291
+ runtime.recordInternalError("vector_index");
281
292
  /* non-fatal: index refresh never blocks a compaction */
282
293
  });
283
294
  }
@@ -9,6 +9,7 @@
9
9
  import { join, dirname } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { readFileSync } from "node:fs";
12
+ import { normalizeSessionId } from "../../src/store.js";
12
13
 
13
14
  // ── Public string constants ────────────────────────────────────────────────
14
15
  // Exported via the barrel — consumers (mega-events.ts, mega-pipeline.ts) use
@@ -111,3 +112,48 @@ export function ownVersion(): string {
111
112
  CACHED_VERSION = v;
112
113
  return v;
113
114
  }
115
+
116
+ /**
117
+ * createSessionRuntime() — the default per-session runtime state.
118
+ *
119
+ * Extracted from runtime.ts (delegate-shell split) so the MegaRuntime class
120
+ * stays a field-declarations + 1-line-delegate shell. Returns a fresh
121
+ * SessionRuntime for each session (reset on session_start / session_tree).
122
+ * Mirrors the neuralwatt-mcr per-session closure.
123
+ */
124
+ export function createSessionRuntime(): SessionRuntime {
125
+ return {
126
+ sessionId: normalizeSessionId(undefined),
127
+ persistedThisSession: false,
128
+ lastCheckpointId: undefined,
129
+ lastCompactedFrom: 0,
130
+ lastCompactedTokens: 0,
131
+ dedupSkips: 0,
132
+ dedupAttempts: 0,
133
+ tokensSaved: 0,
134
+ lastCompactAt: null,
135
+ lastRecallAt: null,
136
+ lastInjectAt: null,
137
+ _prevCacheHitPct: null,
138
+ _lastCacheHealthScore: undefined,
139
+ lastNativeCompactAt: null,
140
+ compactCount: 0,
141
+ recallInjections: 0,
142
+ cacheHitTokens: 0,
143
+ lengthStopPending: false,
144
+ errorRetryCount: 0,
145
+ errorRetryUntil: 0,
146
+ consecutiveErrors: 0,
147
+ lastErrorRetryAt: 0,
148
+ retryNudgePending: false,
149
+ errorRetrySessionCount: 0,
150
+ lastErrorText: undefined,
151
+ errorTextRepeatCount: 0,
152
+ poisonedAdviseSent: false,
153
+ providerOutageAdvised: false,
154
+ poisonedCompactSignatures: new Set(),
155
+ recallInjectedThisTurn: false,
156
+ poisonedCount: 0,
157
+ extensionInitiatedTurn: false,
158
+ };
159
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * internal-errors.ts — Sprint H (Finding 3 / Option A) instrumentation.
3
+ *
4
+ * Free-function + context-interface sibling of `runtime.ts` (delegate-shell
5
+ * pattern). Holds the body of `recordInternalError(category)` so the
6
+ * `MegaRuntime` class stays a field-declarations + 1-line-delegate shell.
7
+ *
8
+ * Pushes a store/service-write failure category into the `recentInternalErrors`
9
+ * ring (capped at RING_MAX, shift when over). Called AT each failure emit site
10
+ * (see the §2.3a audit in docs/specs/c2-resume-and-health-fixes.md) — never by
11
+ * a central events.log filter. Feeds the distinct `storeErrorRate` 6th health
12
+ * axis (committed separately in Sprint H axis work).
13
+ */
14
+ import type { MegaRuntime } from "./runtime.js";
15
+
16
+ /**
17
+ * RING_MAX — the cap for the rolling health ring buffers
18
+ * (`recentTurnEmbeddings`, `recentErrorCategories`, `recentInternalErrors`).
19
+ * Defined here (with the ring writer) and re-exported from runtime.ts so the
20
+ * ring + its writer live together; health-handler.ts imports it.
21
+ */
22
+ export const RING_MAX = 5;
23
+
24
+ /** Push a category into the internal-error ring. Mirrors `recentErrorCategories`. */
25
+ export function recordInternalErrorImpl(rt: MegaRuntime, category: string): void {
26
+ rt.recentInternalErrors.push(category);
27
+ if (rt.recentInternalErrors.length > RING_MAX) {
28
+ rt.recentInternalErrors.shift();
29
+ }
30
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * runtime-instrumentation.ts — base class holding MegaRuntime's field
3
+ * declarations (diagnostic counters + context-health ring buffers + the Sprint-H
4
+ * internal-error ring + live display/cache-stability state).
5
+ *
6
+ * Extracted from runtime.ts (delegate-shell split) so the MegaRuntime class
7
+ * stays a field-declarations + 1-line-delegate shell. As a base class, the
8
+ * field NAMES are unchanged (`this.diagLiveTrimFires`, `this.trimCache`, etc.) —
9
+ * call sites across the codebase and tests need no edits.
10
+ */
11
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
12
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
13
+ import type { ModelSnapshot, GameState } from "../../src/store/sqlite.js";
14
+ import type { TickerEntry, WidgetData } from "./widget.js";
15
+ import type { FSWatcher } from "node:fs";
16
+
17
+ /**
18
+ * DIAG counters for the "team run doesn't relieve context" investigation.
19
+ * Plain integers, incremented at the three compaction decision points. They
20
+ * let a headless test drive the real event handlers and assert the firing
21
+ * cadence without scraping log files. Inert in production (the live-trim and
22
+ * before-compact probes also emit logger.info, but these counters are always
23
+ * updated and cost nothing).
24
+ */
25
+ export class RuntimeInstrumentation {
26
+ diagLiveTrimFires = 0; // context handler returned a trimmed view
27
+ diagLiveTrimReplays = 0; // v0.8.6: trim view returned via cached replay (skipped re-compact)
28
+ diagBeforeCompactFires = 0; // session_before_compact handler entered
29
+ diagBeforeCompactSupplied = 0; // session_before_compact supplied our trim
30
+ diagAgentEndIdle = 0; // agent_end with activeAgents===0
31
+ diagAgentEndDurable = 0; // agent_end fired ctx.compact() (mid-run durable trim)
32
+ diagAgentEndDurableSkipRecent = 0; // agent_end skipped ctx.compact() — compaction in last 10s (race guard)
33
+ // Per-skip-path counters for the team-run diagnosis.
34
+ diagCtxFastGate = 0; // returned at token fast-gate (below threshold)
35
+ diagCtxNoCompact = 0; // autoCompactCheck().shouldCompact === false
36
+ diagCtxDebounce = 0; // debounceUntil not yet elapsed
37
+ diagCtxRunSkipped = 0; // runCompact() returned skipped
38
+ diagCtxCutNull = 0; // computeLiveTrimCut returned null (anchor/boundary)
39
+ diagCtxThrown = 0; // live-trim try threw (caught)
40
+
41
+ // Context health instrumentation (v0.12): rolling ring buffers for
42
+ // drift detection + cache poison Layer 1 hash baseline.
43
+ recentTurnEmbeddings: number[][] = [];
44
+ recentErrorCategories: (string | null)[] = [];
45
+ lastPrefixHash: string | null = null;
46
+ lastErrorCategory: string | null = null;
47
+
48
+ // Sprint H (Finding 3): a SEPARATE internal-error ring fed by explicit
49
+ // `recordInternalError(category)` calls at each store/service-write failure
50
+ // emit site (Option A — NOT a central events.log filter). This captures
51
+ // internal store-write failures which the API-error `errorRate` ring never
52
+ // sees. The health-handler computes a distinct `storeErrorRate` 6th axis.
53
+ // Mirrors `recentErrorCategories` / `lastErrorCategory` exactly (cap = RING_MAX).
54
+ recentInternalErrors: string[] = [];
55
+
56
+ /**
57
+ * S26 capture instrumentation: the "model_snapshots empty → $0.00 cost card"
58
+ * bug was invisible because captureModel swallowed the DB write in a silent
59
+ * `catch {}`. These always-updated counters (zero cost) let a headless test or
60
+ * a live capture tell whether captureModel ran and whether the snapshot landed.
61
+ */
62
+ diagCaptureModelCalls = 0; // captureModel entered with a populated ctx.model
63
+ diagCaptureModelFails = 0; // recordModelSnapshot threw → model_snapshots stays empty
64
+
65
+ // ── Live display / cache-stability state (moved from runtime.ts shell) ────
66
+
67
+ // v0.8.6 cache-stability: the cached live-trim view for the current
68
+ // compaction epoch. Set after a fresh runCompact + computeLiveTrimCut, and
69
+ // replayed verbatim on subsequent gated context events in the SAME epoch
70
+ // (same checkpointId) so the provider KV-cache prefix stays stable instead
71
+ // of being invalidated by a freshly regenerated summary + sentinel every
72
+ // fire. Invalidated on session restart (resetRuntime) and on any native
73
+ // durable compaction (session_compact) that truncates the transcript.
74
+ trimCache: {
75
+ checkpointId: string;
76
+ cut: number;
77
+ summaryAgentMsg: AgentMessage;
78
+ ctxPct: number | null;
79
+ ctxTokens: number | null;
80
+ } | null = null;
81
+ debounceUntil = 0;
82
+ // S16: debounce for the agent_end resume nudge (avoid busy-loops).
83
+ resumeNudgeUntil = 0;
84
+ // Agent tracking for real-time widget updates
85
+ activeAgents = 0;
86
+ currentTurn = 0;
87
+ // S33: transient MEGA CACHE flare flag (armed by the turn_end scoring hook
88
+ // when cachePct > 100). Copied into widgetData.megaCacheFlare on the next
89
+ // snapshot() so the widget renders the oopsie gag, then reset (one cycle).
90
+ megaCacheFlare = false;
91
+ /** v0.8.3: ambient effect state for animated panel borders keyed off
92
+ * status transitions (level-up, mega-cache overshoot, achievement unlock,
93
+ * compaction start). Threaded into widgetData as `activeEffect`; the widget
94
+ * computes the per-frame phase from startedAt vs Date.now() (non-expired).
95
+ * Null when idle/expired. */
96
+ activeEffect: { type: "pulse" | "flash"; role: "accent" | "mega" | "red"; startedAt: number; durationMs: number } | null = null;
97
+ megaCacheFlarePct = 0;
98
+ levelUpFlare = false;
99
+ lastLevel = 0;
100
+ // S35: transient achievement-unlock flare (armed by the scoring hooks after
101
+ // evaluateAndUnlockAchievements returns newly-unlocked titles). Copied into
102
+ // widgetData.achievementFlare on the next snapshot() so the widget renders the
103
+ // unlock toast, then reset (one cycle — mirrors megaCacheFlare/levelUpFlare).
104
+ achievementFlare = false;
105
+ achievementFlareTitles: string[] = [];
106
+ // S33: last cumulative dedup-collapsed count seen by the session_compact
107
+ // hook, so we only record the DELTA as the dedupe score (leaderboard sums).
108
+ lastDedupCollapsed = 0;
109
+ // Recall block produced by auto-inline (resume/branch) that the next
110
+ // before_agent_start should prepend to the system prompt. Unset after use.
111
+ pendingRecallBlock: string | undefined;
112
+ // S21: memory recall block, parallel to pendingRecallBlock. Same one-shot
113
+ // semantics; composed with the checkpoint block in before_agent_start.
114
+ pendingMemoryRecallBlock: string | undefined;
115
+ statusKey: string | undefined; // current status text for dashboard
116
+ // Active model/provider (for real cost estimation). Captured from ctx.model
117
+ // on model_select + session_start; persisted to SQL so cost + the dashboard
118
+ // can read it without a live ctx.
119
+ currentModel: ModelSnapshot | undefined;
120
+ // Live "what it's doing right now" timestamp, used for the fresh-window.
121
+ lastActivityAt = 0;
122
+ // Live per-tier dedup trace (Phase 1): e.g. "L0 ✓ → L1 ✓ → L2 0.91 → stored".
123
+ // Built from the store's sync onTier callback during a compaction so the user
124
+ // watches each tier evaluate in real time. Cleared once the outcome settles.
125
+ tierTrace: string | undefined;
126
+ // Phase 3 — standout toolbar state.
127
+ // Recall/activity ticker: a small ring buffer (≤5) of recent compact/recall
128
+ // events so the widget shows a live history instead of a single last action.
129
+ ticker: TickerEntry[] = [];
130
+ readonly TICKER_MAX = 5;
131
+ // Pulsing status: set true while a compaction is in flight, cleared on result.
132
+ pulsing = false;
133
+ // S21.2: set by `applyMemoryOps` when a memory add/replace/remove lands in
134
+ // the current compaction. The pipeline reads this after a successful compact
135
+ // to decide whether to fire `consolidateMemories` (skip the work entirely
136
+ // when no memory rows changed).
137
+ memoriesTouchedThisCompaction = 0;
138
+ // Rolling "saved" goal for the progress bar — grows as we save more, so the
139
+ // bar always has a meaningful denominator (never sits at 100% forever).
140
+ savedGoal = 50_000;
141
+ // Last explain-why line (dedup reason / anchor-kept / superseded), surfaced
142
+ // while fresh.
143
+ lastWhy: string | undefined = undefined;
144
+ // v0.8.8 Perf dashboard instrumentation: turn/provider start timestamps +
145
+ // the 5s cpu/mem interval handle (one per MegaRuntime, cleared in dispose()).
146
+ perfTurnStart = 0;
147
+ perfProviderStart = 0;
148
+ perfCpuInterval: ReturnType<typeof setInterval> | null = null;
149
+ perfCpuBaseline: { user: number; sys: number } | undefined;
150
+
151
+ // Context tracking for the dashboard (updated in the context handler).
152
+ lastCtxTokens: number | null = null;
153
+ lastCtxPercent: number | null = null;
154
+ lastCtxWindow = 0;
155
+
156
+ // Latest computed widget payload (recomputed per snapshot, rendered per frame).
157
+ widgetData: WidgetData | null = null;
158
+ // v0.8.5: material-change signature from the last full snapshot() body. When
159
+ // the next snapshot()'s signature matches, the expensive recompute (6 sync
160
+ // SQLite opens) + writeFileSync(dashboard.json) are skipped — only the
161
+ // (already-registered) widget factory is refreshed. Kills the per-event
162
+ // main-thread block during typing/idle streaming with no material change.
163
+ lastSnapshotSig: string | null = null;
164
+ lastHeartbeatAt: number = 0;
165
+ // v0.8.5: bumped whenever the cached game-state memo is evicted (bumpGameState
166
+ // for in-process /mega-game writes, the fs.watch callback for cross-process
167
+ // dashboard-server writes, and bindRepo on repo switch) so the snapshot gate
168
+ // invalidates and the widget re-reads theme/mode after the change.
169
+ gameStateBump = 0;
170
+ // Cached cross-repo drift status (recomputed at most every 30s — it opens the
171
+ // machine-wide registry DB, so we don't want to do it on every render frame).
172
+ driftCache: { at: number; status: "ok" | "warn" } | null = null;
173
+ // S31: cached game-mode state (game_mode_on/theme/tui_display_mode). Lazily
174
+ // read from the game_state SQLite row on the first widget render, then
175
+ // memoized until bumpGameState() evicts it (called by /mega-game after a
176
+ // write) so the widget picks up theme/mode/level changes live without
177
+ // re-querying the DB on every render frame.
178
+ cachedGameState: GameState | undefined;
179
+ // S32: fs.watch on the current repo's sqlite.db so cross-process writes
180
+ // (e.g. the dashboard server's PUT /api/game-state, which runs as a detached
181
+ // child with no MegaRuntime ref) evict the cached game-state memo. Without
182
+ // this, /mega-game's in-process bumpGameState() is the only eviction trigger
183
+ // and the widget would keep showing stale theme/mode/toggle after a dashboard
184
+ // edit until a restart. The watcher tracks currentStateDir — closed + re-opened
185
+ // by ensureGameStateWatcher() on every bindRepo repo switch. Non-fatal: any
186
+ // fs.watch failure (missing file / platform issue) is swallowed; the next
187
+ // getCachedGameState() snapshot re-queries the DB anyway.
188
+ gameStateWatcher?: FSWatcher;
189
+ gameStateWatchDir?: string;
190
+ // P2: the last ExtensionContext handed to snapshot()/renderWidget(), stashed
191
+ // so the fs.watch game-state callback can force a widget re-render without
192
+ // a context event (cross-process dashboard edits while pi is idle). Cleared
193
+ // implicitly on construction (undefined → watcher skips until first snap).
194
+ lastWidgetCtx?: ExtensionContext;
195
+ }
@@ -15,13 +15,12 @@
15
15
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
16
16
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
17
17
  import { join } from "node:path";
18
- import type { FSWatcher } from "node:fs";
19
18
  import { VectorStore } from "../../src/vectorStore.js";
20
19
  import type { toEngineMessages } from "../../src/adapt.js";
21
- import { normalizeSessionId } from "../../src/store.js";
22
20
  import { Logger } from "../../src/log.js";
21
+
22
+ export { RING_MAX } from "./internal-errors.js";
23
23
  import type {
24
- ModelSnapshot,
25
24
  GameState,
26
25
  } from "../../src/store/sqlite.js";
27
26
  import type {
@@ -32,10 +31,7 @@ import { Dashboard } from "../mega-dashboard.js";
32
31
  import type {
33
32
  SessionRuntime,
34
33
  } from "./helpers.js";
35
- import type {
36
- TickerEntry,
37
- WidgetData,
38
- } from "./widget.js";
34
+ import { createSessionRuntime } from "./helpers.js";
39
35
  import {
40
36
  ensureGameStateWatcherImpl,
41
37
  getCachedGameStateImpl,
@@ -61,12 +57,14 @@ import {
61
57
  } from "./pressure-getters.js";
62
58
  import { resetRuntimeImpl } from "./reset-runtime.js";
63
59
  import { appendEventImpl } from "./append-event.js";
60
+ import { recordInternalErrorImpl } from "./internal-errors.js";
61
+ import { RuntimeInstrumentation } from "./runtime-instrumentation.js";
64
62
  import { getStateDirImpl } from "./get-state-dir.js";
65
63
  import { renderWidgetImpl } from "./render-widget.js";
66
64
  import { setStatusImpl } from "./status.js";
67
65
  import { engineViewImpl } from "./engine-view.js";
68
66
 
69
- export class MegaRuntime {
67
+ export class MegaRuntime extends RuntimeInstrumentation {
70
68
  config: MegaConfig;
71
69
  // Store/dashboard/logger are rebound per-repo by bindRepo() so each git repo
72
70
  // gets its own isolated state dir. They start bound to the global default.
@@ -77,208 +75,14 @@ export class MegaRuntime {
77
75
  currentStateDir: string;
78
76
 
79
77
  // The only mutable per-session state. Reset on session_start / session_tree.
80
- rt: SessionRuntime = {
81
- sessionId: normalizeSessionId(undefined),
82
- persistedThisSession: false,
83
- lastCheckpointId: undefined,
84
- lastCompactedFrom: 0,
85
- lastCompactedTokens: 0,
86
- dedupSkips: 0,
87
- dedupAttempts: 0,
88
- tokensSaved: 0,
89
- lastCompactAt: null,
90
- lastRecallAt: null,
91
- lastInjectAt: null,
92
- _prevCacheHitPct: null,
93
- _lastCacheHealthScore: undefined,
94
- lastNativeCompactAt: null,
95
- compactCount: 0,
96
- recallInjections: 0,
97
- cacheHitTokens: 0,
98
- lengthStopPending: false,
99
- errorRetryCount: 0,
100
- errorRetryUntil: 0,
101
- consecutiveErrors: 0,
102
- lastErrorRetryAt: 0,
103
- retryNudgePending: false,
104
- errorRetrySessionCount: 0,
105
- lastErrorText: undefined,
106
- errorTextRepeatCount: 0,
107
- poisonedAdviseSent: false,
108
- providerOutageAdvised: false,
109
- poisonedCompactSignatures: new Set(),
110
- // S53: tracks whether withRecallTail injected this turn (consumed at turn_end).
111
- recallInjectedThisTurn: false,
112
- poisonedCount: 0,
113
- extensionInitiatedTurn: false,
114
- };
115
- // v0.8.6 cache-stability: the cached live-trim view for the current
116
- // compaction epoch. Set after a fresh runCompact + computeLiveTrimCut, and
117
- // replayed verbatim on subsequent gated context events in the SAME epoch
118
- // (same checkpointId) so the provider KV-cache prefix stays stable instead
119
- // of being invalidated by a freshly regenerated summary + sentinel every
120
- // fire. Invalidated on session restart (resetRuntime) and on any native
121
- // durable compaction (session_compact) that truncates the transcript.
122
- trimCache: {
123
- checkpointId: string;
124
- cut: number;
125
- summaryAgentMsg: AgentMessage;
126
- ctxPct: number | null;
127
- ctxTokens: number | null;
128
- } | null = null;
129
- debounceUntil = 0;
130
- // S16: debounce for the agent_end resume nudge (avoid busy-loops).
131
- resumeNudgeUntil = 0;
132
- // Agent tracking for real-time widget updates
133
- activeAgents = 0;
134
- currentTurn = 0;
135
- // S33: transient MEGA CACHE flare flag (armed by the turn_end scoring hook
136
- // when cachePct > 100). Copied into widgetData.megaCacheFlare on the next
137
- // snapshot() so the widget renders the oopsie gag, then reset (one cycle).
138
- megaCacheFlare = false;
139
- /** v0.8.3: ambient effect state for animated panel borders keyed off
140
- * status transitions (level-up, mega-cache overshoot, achievement unlock,
141
- * compaction start). Threaded into widgetData as `activeEffect`; the widget
142
- * computes the per-frame phase from startedAt vs Date.now() (non-expired).
143
- * Null when idle/expired. */
144
- activeEffect: { type: "pulse" | "flash"; role: "accent" | "mega" | "red"; startedAt: number; durationMs: number } | null = null;
145
- megaCacheFlarePct = 0;
146
- levelUpFlare = false;
147
- lastLevel = 0;
148
- // S35: transient achievement-unlock flare (armed by the scoring hooks after
149
- // evaluateAndUnlockAchievements returns newly-unlocked titles). Copied into
150
- // widgetData.achievementFlare on the next snapshot() so the widget renders the
151
- // unlock toast, then reset (one cycle — mirrors megaCacheFlare/levelUpFlare).
152
- achievementFlare = false;
153
- achievementFlareTitles: string[] = [];
154
- // S33: last cumulative dedup-collapsed count seen by the session_compact
155
- // hook, so we only record the DELTA as the dedupe score (leaderboard sums).
156
- lastDedupCollapsed = 0;
157
- // Recall block produced by auto-inline (resume/branch) that the next
158
- // before_agent_start should prepend to the system prompt. Unset after use.
159
- pendingRecallBlock: string | undefined;
160
- // S21: memory recall block, parallel to pendingRecallBlock. Same one-shot
161
- // semantics; composed with the checkpoint block in before_agent_start.
162
- pendingMemoryRecallBlock: string | undefined;
163
- statusKey: string | undefined; // current status text for dashboard
164
- // Active model/provider (for real cost estimation). Captured from ctx.model
165
- // on model_select + session_start; persisted to SQL so cost + the dashboard
166
- // can read it without a live ctx.
167
- currentModel: ModelSnapshot | undefined;
168
- // Live "what it's doing right now" timestamp, used for the fresh-window.
169
- lastActivityAt = 0;
170
- // Live per-tier dedup trace (Phase 1): e.g. "L0 ✓ → L1 ✓ → L2 0.91 → stored".
171
- // Built from the store's sync onTier callback during a compaction so the user
172
- // watches each tier evaluate in real time. Cleared once the outcome settles.
173
- tierTrace: string | undefined;
174
- // Phase 3 — standout toolbar state.
175
- // Recall/activity ticker: a small ring buffer (≤5) of recent compact/recall
176
- // events so the widget shows a live history instead of a single last action.
177
- ticker: TickerEntry[] = [];
178
- readonly TICKER_MAX = 5;
179
- // Pulsing status: set true while a compaction is in flight, cleared on result.
180
- pulsing = false;
181
- // S21.2: set by `applyMemoryOps` when a memory add/replace/remove lands in
182
- // the current compaction. The pipeline reads this after a successful compact
183
- // to decide whether to fire `consolidateMemories` (skip the work entirely
184
- // when no memory rows changed).
185
- memoriesTouchedThisCompaction = 0;
186
- // Rolling "saved" goal for the progress bar — grows as we save more, so the
187
- // bar always has a meaningful denominator (never sits at 100% forever).
188
- savedGoal = 50_000;
189
- // Last explain-why line (dedup reason / anchor-kept / superseded), surfaced
190
- // while fresh.
191
- lastWhy: string | undefined = undefined;
192
- // v0.8.8 Perf dashboard instrumentation: turn/provider start timestamps +
193
- // the 5s cpu/mem interval handle (one per MegaRuntime, cleared in dispose()).
194
- perfTurnStart = 0;
195
- perfProviderStart = 0;
196
- perfCpuInterval: ReturnType<typeof setInterval> | null = null;
197
- perfCpuBaseline: { user: number; sys: number } | undefined;
198
-
199
- // Context tracking for the dashboard (updated in the context handler).
200
- lastCtxTokens: number | null = null;
201
- lastCtxPercent: number | null = null;
202
- lastCtxWindow = 0;
203
-
204
- // Latest computed widget payload (recomputed per snapshot, rendered per frame).
205
- widgetData: WidgetData | null = null;
206
- // v0.8.5: material-change signature from the last full snapshot() body. When
207
- // the next snapshot()'s signature matches, the expensive recompute (6 sync
208
- // SQLite opens) + writeFileSync(dashboard.json) are skipped — only the
209
- // (already-registered) widget factory is refreshed. Kills the per-event
210
- // main-thread block during typing/idle streaming with no material change.
211
- lastSnapshotSig: string | null = null;
212
- lastHeartbeatAt: number = 0;
213
- // v0.8.5: bumped whenever the cached game-state memo is evicted (bumpGameState
214
- // for in-process /mega-game writes, the fs.watch callback for cross-process
215
- // dashboard-server writes, and bindRepo on repo switch) so the snapshot gate
216
- // invalidates and the widget re-reads theme/mode after the change.
217
- gameStateBump = 0;
218
- // Cached cross-repo drift status (recomputed at most every 30s — it opens the
219
- // machine-wide registry DB, so we don't want to do it on every render frame).
220
- driftCache: { at: number; status: "ok" | "warn" } | null = null;
221
- // S31: cached game-mode state (game_mode_on/theme/tui_display_mode). Lazily
222
- // read from the game_state SQLite row on the first widget render, then
223
- // memoized until bumpGameState() evicts it (called by /mega-game after a
224
- // write) so the widget picks up theme/mode/level changes live without
225
- // re-querying the DB on every render frame.
226
- cachedGameState: GameState | undefined;
227
- // S32: fs.watch on the current repo's sqlite.db so cross-process writes
228
- // (e.g. the dashboard server's PUT /api/game-state, which runs as a detached
229
- // child with no MegaRuntime ref) evict the cached game-state memo. Without
230
- // this, /mega-game's in-process bumpGameState() is the only eviction trigger
231
- // and the widget would keep showing stale theme/mode/toggle after a dashboard
232
- // edit until a restart. The watcher tracks currentStateDir — closed + re-opened
233
- // by ensureGameStateWatcher() on every bindRepo repo switch. Non-fatal: any
234
- // fs.watch failure (missing file / platform issue) is swallowed; the next
235
- // getCachedGameState() snapshot re-queries the DB anyway.
236
- gameStateWatcher?: FSWatcher;
237
- gameStateWatchDir?: string;
238
- // P2: the last ExtensionContext handed to snapshot()/renderWidget(), stashed
239
- // so the fs.watch game-state callback can force a widget re-render without
240
- // a context event (cross-process dashboard edits while pi is idle). Cleared
241
- // implicitly on construction (undefined → watcher skips until first snap).
242
- lastWidgetCtx?: ExtensionContext;
243
-
244
- /**
245
- * DIAG counters for the "team run doesn't relieve context" investigation.
246
- * Plain integers, incremented at the three compaction decision points. They
247
- * let a headless test drive the real event handlers and assert the firing
248
- * cadence without scraping log files. Inert in production (the live-trim and
249
- * before-compact probes also emit logger.info, but these counters are always
250
- * updated and cost nothing).
251
- */
252
- diagLiveTrimFires = 0; // context handler returned a trimmed view
253
- diagLiveTrimReplays = 0; // v0.8.6: trim view returned via cached replay (skipped re-compact)
254
- diagBeforeCompactFires = 0; // session_before_compact handler entered
255
- diagBeforeCompactSupplied = 0; // session_before_compact supplied our trim
256
- diagAgentEndIdle = 0; // agent_end with activeAgents===0
257
- diagAgentEndDurable = 0; // agent_end fired ctx.compact() (mid-run durable trim)
258
- diagAgentEndDurableSkipRecent = 0; // agent_end skipped ctx.compact() — compaction in last 10s (race guard)
259
- // Per-skip-path counters for the team-run diagnosis.
260
- diagCtxFastGate = 0; // returned at token fast-gate (below threshold)
261
- diagCtxNoCompact = 0; // autoCompactCheck().shouldCompact === false
262
- diagCtxDebounce = 0; // debounceUntil not yet elapsed
263
- diagCtxRunSkipped = 0; // runCompact() returned skipped
264
- diagCtxCutNull = 0; // computeLiveTrimCut returned null (anchor/boundary)
265
- diagCtxThrown = 0; // live-trim try threw (caught)
266
-
267
- // Context health instrumentation (v0.12): rolling ring buffers for
268
- // drift detection + cache poison Layer 1 hash baseline.
269
- recentTurnEmbeddings: number[][] = [];
270
- recentErrorCategories: (string | null)[] = [];
271
- lastPrefixHash: string | null = null;
272
- lastErrorCategory: string | null = null;
78
+ // Initialized via createSessionRuntime() (helpers.ts delegate-shell split).
79
+ rt: SessionRuntime = createSessionRuntime();
273
80
 
274
- /**
275
- * S26 capture instrumentation: the "model_snapshots empty → $0.00 cost card"
276
- * bug was invisible because captureModel swallowed the DB write in a silent
277
- * `catch {}`. These always-updated counters (zero cost) let a headless test or
278
- * a live capture tell whether captureModel ran and whether the snapshot landed.
279
- */
280
- diagCaptureModelCalls = 0; // captureModel entered with a populated ctx.model
281
- diagCaptureModelFails = 0; // recordModelSnapshot threw → model_snapshots stays empty
81
+ // All other field declarations (instrumentation counters, context-health
82
+ // rings, the Sprint-H internal-error ring, live display / cache-stability
83
+ // state) live on the RuntimeInstrumentation base class — see
84
+ // runtime-instrumentation.ts. Field names are unchanged (this.diagXxx,
85
+ // this.trimCache, this.recentInternalErrors, etc).
282
86
 
283
87
  // ---- pressure accessors (bodies in pressure-getters.ts) -------------------
284
88
 
@@ -301,6 +105,7 @@ export class MegaRuntime {
301
105
  }
302
106
 
303
107
  constructor(config: MegaConfig) {
108
+ super();
304
109
  this.config = config;
305
110
  this.store = new VectorStore({
306
111
  dedupSim: config.dedupSim,
@@ -362,6 +167,21 @@ export class MegaRuntime {
362
167
  appendEventImpl(this, event, fields);
363
168
  }
364
169
 
170
+ /**
171
+ * Sprint H (Finding 3 / Option A): record an internal store/service-write
172
+ * failure category into the `recentInternalErrors` ring. Called AT each
173
+ * failure emit site (see the §2.3a audit in
174
+ * docs/specs/c2-resume-and-health-fixes.md) — never by a central log filter.
175
+ * Mirrors `recentErrorCategories` (cap = RING_MAX, shift when over).
176
+ *
177
+ * NOTE (process boundary): the ring is per-runtime / in-memory. A child
178
+ * subprocess's failures do NOT reach the parent's ring; the parent-side
179
+ * dashboard already aggregates events.log cross-process, so nothing is lost.
180
+ */
181
+ recordInternalError(category: string): void {
182
+ recordInternalErrorImpl(this, category);
183
+ }
184
+
365
185
  /** S21: state dir of the currently bound repo (where memories live) — thin
366
186
  * delegate to `getStateDirImpl` (get-state-dir.ts). */
367
187
  getStateDir(): string {
@@ -5,4 +5,4 @@
5
5
  * every existing `import { MegaRuntime } from "./state.js"` continues to
6
6
  * resolve without changes.
7
7
  */
8
- export { MegaRuntime } from "./runtime.js";
8
+ export { MegaRuntime, RING_MAX } from "./runtime.js";
@@ -129,7 +129,7 @@ export function buildWidgetLines(
129
129
  `${C.gray}all-time${C.reset} ${fmtTokens(wd.repoIn)}→${fmtTokens(wd.repoKept)} kept ${C.blue}(${wd.rTxt}% freed)${C.reset}`,
130
130
  `${wd.repoChk} chk/${wd.repoSess} sess`,
131
131
  `${C.gray}mem${C.reset} ${wd.embedderName} · ${wd.chk} chunks · ${C.blue}comp ${wd.compStr}${C.reset}`,
132
- `${C.gray}drift${C.reset} ${wd.driftStatus === "ok" ? C.green : C.amber}${wd.driftStatus}${C.reset}`,
132
+ `${C.gray}comp lag${C.reset} ${wd.driftStatus === "ok" ? C.green : C.amber}${wd.driftStatus}${C.reset}`,
133
133
  `${C.gray}compact${C.reset} ${sinceCompactStr(wd.sinceCompact)}`,
134
134
  ...(wd.perTurnCacheHitPct != null
135
135
  ? [
@@ -12,6 +12,7 @@
12
12
  import {
13
13
  createTurnStore,
14
14
  type TurnStore,
15
+ type TurnReader,
15
16
  type TurnEntry,
16
17
  type TurnRecallEntry,
17
18
  type TurnHydeTelemetry,
@@ -40,6 +41,11 @@ function storeFor(stateDir: string): TurnStore {
40
41
  return s;
41
42
  }
42
43
 
44
+ /** Read-only view of the turn store for a state dir (e.g. nextTurnIndexFor). */
45
+ export function turnReaderFor(stateDir: string): TurnReader {
46
+ return storeFor(stateDir).asReader();
47
+ }
48
+
43
49
  /** Map a legacy RecallSource onto the contract `TurnRecallEntry.source` enum. */
44
50
  function mapSource(s: string): TurnRecallEntry["source"] {
45
51
  switch (s) {
@@ -77,6 +83,8 @@ export function recordTurnWrite(
77
83
  conversationId: string;
78
84
  sessionId: string;
79
85
  turnIndex: number;
86
+ /** S49R: pi's per-session counter, carried for the raw_transcript join. */
87
+ sessionTurnIndex?: number;
80
88
  role: string;
81
89
  startedAt?: number;
82
90
  endedAt?: number;
@@ -100,6 +108,7 @@ export function recordTurnWrite(
100
108
  conversationId: input.conversationId,
101
109
  sessionId: input.sessionId,
102
110
  turnIndex: input.turnIndex,
111
+ sessionTurnIndex: input.sessionTurnIndex,
103
112
  role: input.role as TurnEntry["role"],
104
113
  endedAt: input.endedAt ?? input.startedAt ?? Date.now(),
105
114
  ctxTokens: input.ctxTokens,