@aexol/spectral 0.9.220 → 0.9.222

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 (153) hide show
  1. package/dist/agent/index.d.ts +7 -3
  2. package/dist/agent/index.d.ts.map +1 -1
  3. package/dist/agent/index.js +35 -8
  4. package/dist/extensions/aexol-backend-client.d.ts +85 -0
  5. package/dist/extensions/aexol-backend-client.d.ts.map +1 -0
  6. package/dist/extensions/aexol-backend-client.js +159 -0
  7. package/dist/extensions/aexol-mcp.d.ts +10 -6
  8. package/dist/extensions/aexol-mcp.d.ts.map +1 -1
  9. package/dist/extensions/aexol-mcp.js +21 -106
  10. package/dist/extensions/logic-trace-backend-draft.d.ts +68 -0
  11. package/dist/extensions/logic-trace-backend-draft.d.ts.map +1 -0
  12. package/dist/extensions/logic-trace-backend-draft.js +171 -0
  13. package/dist/extensions/logic-trace-validate.d.ts +33 -0
  14. package/dist/extensions/logic-trace-validate.d.ts.map +1 -0
  15. package/dist/extensions/logic-trace-validate.js +73 -0
  16. package/dist/extensions/logic-trace.d.ts +213 -0
  17. package/dist/extensions/logic-trace.d.ts.map +1 -0
  18. package/dist/extensions/logic-trace.js +1098 -0
  19. package/dist/generated/parser/lexer.d.ts +148 -0
  20. package/dist/generated/parser/lexer.d.ts.map +1 -0
  21. package/dist/generated/parser/lexer.js +877 -0
  22. package/dist/generated/parser/mod.d.ts +26 -0
  23. package/dist/generated/parser/mod.d.ts.map +1 -0
  24. package/dist/generated/parser/mod.js +30 -0
  25. package/dist/generated/parser/parser.d.ts +161 -0
  26. package/dist/generated/parser/parser.d.ts.map +1 -0
  27. package/dist/generated/parser/parser.js +754 -0
  28. package/dist/generated/parser/tokenizer.d.ts +107 -0
  29. package/dist/generated/parser/tokenizer.d.ts.map +1 -0
  30. package/dist/generated/parser/tokenizer.js +202 -0
  31. package/dist/generated/parser/validator.d.ts +45 -0
  32. package/dist/generated/parser/validator.d.ts.map +1 -0
  33. package/dist/generated/parser/validator.js +202 -0
  34. package/dist/generated/zeus/const.d.ts.map +1 -1
  35. package/dist/generated/zeus/const.js +4 -170
  36. package/dist/generated/zeus/index.d.ts +8 -873
  37. package/dist/generated/zeus/index.d.ts.map +1 -1
  38. package/dist/generated/zeus/index.js +0 -7
  39. package/dist/mcp/ui-stream-types.d.ts +2 -2
  40. package/dist/memory/tools/share-project-observation.js +1 -1
  41. package/dist/memory/tools/write-project-observation.d.ts.map +1 -1
  42. package/dist/memory/tools/write-project-observation.js +4 -3
  43. package/dist/sdk/agent-core/harness/types.d.ts +0 -1
  44. package/dist/sdk/agent-core/harness/types.d.ts.map +1 -1
  45. package/dist/sdk/agent-core/index.d.ts +0 -8
  46. package/dist/sdk/agent-core/index.d.ts.map +1 -1
  47. package/dist/sdk/agent-core/index.js +0 -9
  48. package/dist/sdk/coding-agent/core/extensions/index.d.ts +1 -1
  49. package/dist/sdk/coding-agent/core/extensions/index.d.ts.map +1 -1
  50. package/dist/sdk/coding-agent/core/extensions/native-extensions.js +2 -2
  51. package/dist/sdk/coding-agent/core/extensions/types.d.ts +20 -1
  52. package/dist/sdk/coding-agent/core/extensions/types.d.ts.map +1 -1
  53. package/dist/sdk/coding-agent/core/tools/tool-definition-wrapper.d.ts +9 -4
  54. package/dist/sdk/coding-agent/core/tools/tool-definition-wrapper.d.ts.map +1 -1
  55. package/dist/sdk/coding-agent/core/tools/tool-definition-wrapper.js +15 -2
  56. package/dist/sdk/coding-agent/index.d.ts +1 -1
  57. package/dist/sdk/coding-agent/index.d.ts.map +1 -1
  58. package/dist/server/agent-bridge/headless-ui.d.ts +20 -0
  59. package/dist/server/agent-bridge/headless-ui.d.ts.map +1 -0
  60. package/dist/server/agent-bridge/headless-ui.js +85 -0
  61. package/dist/server/agent-bridge/pricing.d.ts +37 -0
  62. package/dist/server/agent-bridge/pricing.d.ts.map +1 -0
  63. package/dist/server/agent-bridge/pricing.js +202 -0
  64. package/dist/server/agent-bridge/rehydrate.d.ts +35 -0
  65. package/dist/server/agent-bridge/rehydrate.d.ts.map +1 -0
  66. package/dist/server/agent-bridge/rehydrate.js +122 -0
  67. package/dist/server/agent-bridge/synthetic-model.d.ts +27 -0
  68. package/dist/server/agent-bridge/synthetic-model.d.ts.map +1 -0
  69. package/dist/server/agent-bridge/synthetic-model.js +92 -0
  70. package/dist/server/agent-bridge.d.ts +11 -1
  71. package/dist/server/agent-bridge.d.ts.map +1 -1
  72. package/dist/server/agent-bridge.js +42 -481
  73. package/dist/server/session-stream/compaction-events.d.ts +13 -0
  74. package/dist/server/session-stream/compaction-events.d.ts.map +1 -0
  75. package/dist/server/session-stream/compaction-events.js +32 -0
  76. package/dist/server/session-stream/history-prune.d.ts +35 -0
  77. package/dist/server/session-stream/history-prune.d.ts.map +1 -0
  78. package/dist/server/session-stream/history-prune.js +182 -0
  79. package/dist/server/session-stream/memory.d.ts +48 -0
  80. package/dist/server/session-stream/memory.d.ts.map +1 -0
  81. package/dist/server/session-stream/memory.js +136 -0
  82. package/dist/server/session-stream/replay-tail.d.ts +4 -0
  83. package/dist/server/session-stream/replay-tail.d.ts.map +1 -0
  84. package/dist/server/session-stream/replay-tail.js +39 -0
  85. package/dist/server/session-stream/turn-events.d.ts +33 -0
  86. package/dist/server/session-stream/turn-events.d.ts.map +1 -0
  87. package/dist/server/session-stream/turn-events.js +65 -0
  88. package/dist/server/session-stream/turn-silence.d.ts +8 -0
  89. package/dist/server/session-stream/turn-silence.d.ts.map +1 -0
  90. package/dist/server/session-stream/turn-silence.js +72 -0
  91. package/dist/server/session-stream.d.ts +137 -36
  92. package/dist/server/session-stream.d.ts.map +1 -1
  93. package/dist/server/session-stream.js +18 -509
  94. package/dist/server/storage/observation-ranking.d.ts +14 -0
  95. package/dist/server/storage/observation-ranking.d.ts.map +1 -0
  96. package/dist/server/storage/observation-ranking.js +37 -0
  97. package/dist/server/storage/preflight.d.ts +20 -0
  98. package/dist/server/storage/preflight.d.ts.map +1 -0
  99. package/dist/server/storage/preflight.js +22 -0
  100. package/dist/server/storage/project-binding.d.ts +5 -0
  101. package/dist/server/storage/project-binding.d.ts.map +1 -0
  102. package/dist/server/storage/project-binding.js +32 -0
  103. package/dist/server/storage/rows.d.ts +104 -0
  104. package/dist/server/storage/rows.d.ts.map +1 -0
  105. package/dist/server/storage/rows.js +2 -0
  106. package/dist/server/storage/schema.d.ts +20 -0
  107. package/dist/server/storage/schema.d.ts.map +1 -0
  108. package/dist/server/storage/schema.js +134 -0
  109. package/dist/server/storage/types.d.ts +66 -0
  110. package/dist/server/storage/types.d.ts.map +1 -0
  111. package/dist/server/storage/types.js +1 -0
  112. package/dist/server/storage.d.ts +7 -94
  113. package/dist/server/storage.d.ts.map +1 -1
  114. package/dist/server/storage.js +6 -224
  115. package/dist/server/wire.d.ts +6 -0
  116. package/dist/server/wire.d.ts.map +1 -1
  117. package/package.json +2 -1
  118. package/dist/sdk/agent-core/harness/agent-harness.d.ts +0 -92
  119. package/dist/sdk/agent-core/harness/agent-harness.d.ts.map +0 -1
  120. package/dist/sdk/agent-core/harness/agent-harness.js +0 -899
  121. package/dist/sdk/agent-core/harness/env/nodejs.d.ts +0 -51
  122. package/dist/sdk/agent-core/harness/env/nodejs.d.ts.map +0 -1
  123. package/dist/sdk/agent-core/harness/env/nodejs.js +0 -485
  124. package/dist/sdk/agent-core/harness/prompt-templates.d.ts +0 -48
  125. package/dist/sdk/agent-core/harness/prompt-templates.d.ts.map +0 -1
  126. package/dist/sdk/agent-core/harness/prompt-templates.js +0 -229
  127. package/dist/sdk/agent-core/harness/session/jsonl-repo.d.ts +0 -26
  128. package/dist/sdk/agent-core/harness/session/jsonl-repo.d.ts.map +0 -1
  129. package/dist/sdk/agent-core/harness/session/jsonl-repo.js +0 -100
  130. package/dist/sdk/agent-core/harness/session/jsonl-storage.d.ts +0 -33
  131. package/dist/sdk/agent-core/harness/session/jsonl-storage.d.ts.map +0 -1
  132. package/dist/sdk/agent-core/harness/session/jsonl-storage.js +0 -230
  133. package/dist/sdk/agent-core/harness/session/memory-repo.d.ts +0 -18
  134. package/dist/sdk/agent-core/harness/session/memory-repo.d.ts.map +0 -1
  135. package/dist/sdk/agent-core/harness/session/memory-repo.js +0 -41
  136. package/dist/sdk/agent-core/harness/session/memory-storage.d.ts +0 -25
  137. package/dist/sdk/agent-core/harness/session/memory-storage.d.ts.map +0 -1
  138. package/dist/sdk/agent-core/harness/session/memory-storage.js +0 -113
  139. package/dist/sdk/agent-core/harness/session/repo-utils.d.ts +0 -11
  140. package/dist/sdk/agent-core/harness/session/repo-utils.d.ts.map +0 -1
  141. package/dist/sdk/agent-core/harness/session/repo-utils.js +0 -38
  142. package/dist/sdk/agent-core/harness/skills.d.ts +0 -44
  143. package/dist/sdk/agent-core/harness/skills.d.ts.map +0 -1
  144. package/dist/sdk/agent-core/harness/skills.js +0 -310
  145. package/dist/sdk/agent-core/harness/utils/shell-output.d.ts +0 -14
  146. package/dist/sdk/agent-core/harness/utils/shell-output.d.ts.map +0 -1
  147. package/dist/sdk/agent-core/harness/utils/shell-output.js +0 -125
  148. package/dist/sdk/agent-core/node.d.ts +0 -3
  149. package/dist/sdk/agent-core/node.d.ts.map +0 -1
  150. package/dist/sdk/agent-core/node.js +0 -2
  151. package/dist/sdk/agent-core/proxy.d.ts +0 -69
  152. package/dist/sdk/agent-core/proxy.d.ts.map +0 -1
  153. package/dist/sdk/agent-core/proxy.js +0 -297
@@ -38,115 +38,36 @@
38
38
  */
39
39
  import { randomUUID } from "node:crypto";
40
40
  import { AgentBridge } from "./agent-bridge.js";
41
- import { SettingsManager, } from "../sdk/coding-agent/index.js";
42
41
  import { InterAgentBroker } from "./inter-agent-broker.js";
43
42
  import { setInterAgentBroker } from "../memory/inter-agent-broker-singleton.js";
44
43
  import { configureFanout } from "./fanout/fanout-manager.js";
45
- import { CONTEXT_LIMIT_RATIO, DEFAULT_COMPACTION_POLICY, getAutoOlderHistoryContextThresholdTokens, } from "../sdk/coding-agent/core/compaction/policy.js";
46
- import { getMemoryState, isSourceEntry, rawTokensSinceLastBound, rawTokensSinceLastCompaction, } from "../memory/branch.js";
44
+ import { getAutoOlderHistoryContextThresholdTokens } from "../sdk/coding-agent/core/compaction/policy.js";
45
+ import { getMemoryState, rawTokensSinceLastBound, rawTokensSinceLastCompaction, } from "../memory/branch.js";
47
46
  import { observationPoolTokens, renderSummary } from "../memory/compaction.js";
48
47
  import { loadConfig } from "../memory/config.js";
49
48
  import { setProjectObsStore } from "../memory/project-observations-store.js";
50
49
  import { estimateStringTokens } from "../memory/tokens.js";
51
50
  import { reflectionContent, reflectionId, } from "../memory/types.js";
52
51
  import { humanizeProviderError } from "./error-humanizer.js";
53
- import { estimateImagesTokens, parseImagesJson, } from "./image-attachments.js";
54
- /**
55
- * Extract the `creditsUsed` value from a `token_usage` event embedded in an
56
- * events JSONL string. Returns `null` if no `token_usage` event is present
57
- * (e.g. it hasn't arrived yet at persist time — the race condition this
58
- * column exists to bridge). Best-effort: malformed lines are skipped.
59
- */
60
- function extractCreditsFromEventsJsonl(eventsJsonl) {
61
- if (!eventsJsonl)
62
- return null;
63
- let creditsUsed = null;
64
- for (const line of eventsJsonl.split("\n")) {
65
- if (!line.includes("token_usage"))
66
- continue;
67
- try {
68
- const ev = JSON.parse(line);
69
- if (ev.type === "token_usage" &&
70
- typeof ev.usage.creditsUsed === "number") {
71
- creditsUsed = ev.usage.creditsUsed;
72
- }
73
- }
74
- catch {
75
- // Skip malformed lines — best-effort extraction.
76
- }
77
- }
78
- return creditsUsed;
79
- }
52
+ import { parseImagesJson } from "./image-attachments.js";
53
+ // Focused helper modules extracted from this file (bodies moved verbatim;
54
+ // names re-exported below so the public surface of session-stream.js is
55
+ // unchanged).
56
+ import { formatCompactionTokenStats, isSkippedCompactionResult, } from "./session-stream/compaction-events.js";
57
+ import { getCompactionEndPruneSkipReason, prunePersistedHistoryAfterCompaction, } from "./session-stream/history-prune.js";
58
+ import { applyDcpLiteRuntimeStatus, dcpLiteEventFromServerEvent, defaultMemoryStatus, loadCompactionPolicy, persistObservationalMemorySnapshot, } from "./session-stream/memory.js";
59
+ import { selectHistoryForBridge } from "./session-stream/replay-tail.js";
60
+ import { assistantMessageIdsForTurn, extractCreditsFromEventsJsonl, isReplayable, snapshotTurn, } from "./session-stream/turn-events.js";
61
+ import { resolveTurnSilenceMaxRearm, resolveTurnSilenceTimeoutMs, } from "./session-stream/turn-silence.js";
62
+ export { formatCompactionTokenStats, isSkippedCompactionResult, } from "./session-stream/compaction-events.js";
63
+ export { branchMessageRole, branchMessageSignature, branchMessageTimestampMs, contentAsPersistedText, getCompactionEndPruneSkipReason, getKeptBranchMessageSignatures, isRecord, matchKeptPersistedMessageIds, prunePersistedHistoryAfterCompaction, selectUniqueCandidate, } from "./session-stream/history-prune.js";
64
+ export { applyDcpLiteRuntimeStatus, buildDcpLiteThresholds, contextPercent, dcpLiteEventFromServerEvent, defaultMemoryStatus, loadCompactionPolicy, persistObservationalMemorySnapshot, } from "./session-stream/memory.js";
65
+ export { estimateStoredMessageReplayTokens, selectHistoryForBridge, } from "./session-stream/replay-tail.js";
66
+ export { assistantMessageIdsForTurn, extractCreditsFromEventsJsonl, isReplayable, snapshotTurn, } from "./session-stream/turn-events.js";
67
+ export { resolveTurnSilenceMaxRearm, resolveTurnSilenceTimeoutMs, } from "./session-stream/turn-silence.js";
80
68
  const DEFAULT_BRIDGE_FACTORY = (args) => new AgentBridge(args);
81
69
  /** Safety limit for autonomous loop iterations per session. */
82
70
  const MAX_LOOP_ITERATIONS = 100;
83
- // Fix E: safety-net for stale `currentTurn`. When an agent hangs, crashes,
84
- // or the WS drops during sleep, `agent_end` may never arrive and
85
- // `stream.currentTurn` stays non-null forever. On reconnect, `session_ready`
86
- // then reports `currentTurn != null`, so landing shows a phantom "streaming"
87
- // status. This timer clears the stale turn and emits a synthetic `agent_end`
88
- // after 30 minutes of silence on streaming-meaningful events.
89
- //
90
- // Configurable via `SPECTRAL_TURN_SILENCE_TIMEOUT_MS`:
91
- // * unset / empty / not-a-number / negative -> 30 minutes (default)
92
- // * `0` -> DISABLED, the timer is never armed (official opt-out)
93
- // * a positive fraction (e.g. `0.5`) is rounded UP to 1ms so it can never
94
- // accidentally disable the safety net
95
- // * anything larger than Node's max delay is clamped (see below)
96
- const DEFAULT_TURN_SILENCE_TIMEOUT_MS = 30 * 60 * 1000;
97
- /**
98
- * Node emits `TimeoutOverflowWarning` and fires immediately for delays above
99
- * this value, so anything larger is clamped to it.
100
- */
101
- const MAX_TIMEOUT_DELAY_MS = 2_147_483_647;
102
- /**
103
- * Upper bound on how many times the silence timer may be re-armed instead of
104
- * closing the turn while a subagent is still running. Guards against a
105
- * counter stuck at >0 forever (e.g. a `subagent_end` that never arrives after
106
- * a crash) turning the safety net off permanently.
107
- * `SPECTRAL_TURN_SILENCE_MAX_REARM=0` means unlimited re-arms.
108
- */
109
- const DEFAULT_TURN_SILENCE_MAX_REARM = 8;
110
- /**
111
- * Sanity clamp for `SPECTRAL_TURN_SILENCE_MAX_REARM`. Even with the shortest
112
- * useful timeout this is days of silence, so a typo like `1e10` cannot keep a
113
- * hung turn open effectively forever.
114
- */
115
- const MAX_TURN_SILENCE_MAX_REARM = 1000;
116
- function resolveTurnSilenceTimeoutMs() {
117
- const raw = process.env.SPECTRAL_TURN_SILENCE_TIMEOUT_MS;
118
- if (raw === undefined || raw.trim() === "") {
119
- return DEFAULT_TURN_SILENCE_TIMEOUT_MS;
120
- }
121
- const parsed = Number(raw);
122
- // `0` is the documented way to switch the safety net off entirely.
123
- if (parsed === 0)
124
- return 0;
125
- if (!Number.isFinite(parsed) || parsed < 0) {
126
- return DEFAULT_TURN_SILENCE_TIMEOUT_MS;
127
- }
128
- // `Math.max(1, ...)`: a fractional value such as `0.5` must floor to 1ms,
129
- // not to 0 — flooring to 0 would silently switch the safety net off.
130
- return Math.min(Math.max(1, Math.floor(parsed)), MAX_TIMEOUT_DELAY_MS);
131
- }
132
- function resolveTurnSilenceMaxRearm() {
133
- const raw = process.env.SPECTRAL_TURN_SILENCE_MAX_REARM;
134
- // Empty / whitespace-only must NOT fall through to `Number(...)`, which
135
- // coerces `""` to `0` and would therefore mean "unlimited re-arms" —
136
- // i.e. the original "phantom streaming forever" bug.
137
- if (raw === undefined || raw.trim() === "") {
138
- return DEFAULT_TURN_SILENCE_MAX_REARM;
139
- }
140
- const parsed = Number(raw);
141
- // `0` is the documented way to allow unlimited re-arms (opt-out of the
142
- // re-arm budget).
143
- if (parsed === 0)
144
- return 0;
145
- if (!Number.isFinite(parsed) || parsed < 0) {
146
- return DEFAULT_TURN_SILENCE_MAX_REARM;
147
- }
148
- return Math.min(Math.max(1, Math.floor(parsed)), MAX_TURN_SILENCE_MAX_REARM);
149
- }
150
71
  // Streaming-meaningful events that reset the silence timer while a turn is
151
72
  // active. Mirrors Fix C on the landing side, but EXCLUDES `token_usage`
152
73
  // (arrives post-turn) and excludes terminal/control events handled by their
@@ -176,18 +97,6 @@ const STREAMING_MEANINGFUL_EVENT_TYPES = new Set([
176
97
  "subagent_tool_progress",
177
98
  "subagent_end",
178
99
  ]);
179
- /**
180
- * Defensive cap for fresh-bridge history replay when we already have a
181
- * persisted observational-memory snapshot. spectral's own compaction keeps a
182
- * recent live tail verbatim and summarizes the older prefix; replaying the
183
- * entire SQLite transcript after restart defeats that and can explode the
184
- * next prompt. We therefore rehydrate only an approximate recent tail.
185
- *
186
- * Keep this conservative — the tail is rehydrated as raw user/assistant
187
- * messages PLUS spectral reconstructs tool-result messages from events_jsonl,
188
- * so the actual context inflation is 3-8× the stored token estimate.
189
- */
190
- const REHYDRATION_TAIL_TOKEN_BUDGET = 20_000;
191
100
  const UI_HISTORY_TAIL_MESSAGE_LIMIT = 60;
192
101
  /**
193
102
  * Number of accumulated wire events before flushing the in-flight turn
@@ -197,367 +106,6 @@ const UI_HISTORY_TAIL_MESSAGE_LIMIT = 60;
197
106
  const BATCH_FLUSH_INTERVAL = 10;
198
107
  /** Marker the agent emits in its response to signal the task is complete. */
199
108
  const LOOP_DONE_MARKER = "<LOOP_DONE>";
200
- function persistObservationalMemorySnapshot(store, sessionId, bridge) {
201
- if (!bridge.getSessionBranch)
202
- return;
203
- try {
204
- const entries = bridge.getSessionBranch();
205
- const memoryState = getMemoryState(entries);
206
- const allObservations = [
207
- ...memoryState.committedObs,
208
- ...memoryState.pendingObs,
209
- ];
210
- const hasMemory = memoryState.reflections.length > 0 || allObservations.length > 0;
211
- if (!hasMemory) {
212
- store.deleteSessionMemorySnapshot(sessionId);
213
- return;
214
- }
215
- const summary = renderSummary(memoryState.reflections, allObservations);
216
- const details = {
217
- type: "observational-memory",
218
- version: 4,
219
- observations: allObservations,
220
- reflections: memoryState.reflections,
221
- };
222
- store.upsertSessionMemorySnapshot(sessionId, {
223
- summary,
224
- details,
225
- coveredSourceCount: entries.filter((entry) => isSourceEntry(entry))
226
- .length,
227
- });
228
- }
229
- catch (err) {
230
- console.warn(`[spectral] warn: failed to persist observational memory snapshot for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
231
- }
232
- }
233
- function estimateStoredMessageReplayTokens(message) {
234
- return (estimateStringTokens(message.content) +
235
- estimateStringTokens(message.events) +
236
- estimateImagesTokens(message.images));
237
- }
238
- function formatCompactionTokenStats(event) {
239
- const parts = [];
240
- if (typeof event.compactionNumber === "number") {
241
- parts.push(`compaction #${event.compactionNumber.toLocaleString()}`);
242
- }
243
- if (typeof event.tokensBefore === "number") {
244
- parts.push(`from ~${event.tokensBefore.toLocaleString()} tokens`);
245
- }
246
- if (typeof event.tokensCompacted === "number") {
247
- parts.push(`compacted ~${event.tokensCompacted.toLocaleString()} tokens`);
248
- }
249
- if (typeof event.tokensRemoved === "number") {
250
- parts.push(`removed ~${event.tokensRemoved.toLocaleString()} tokens`);
251
- }
252
- if (typeof event.summaryTokens === "number") {
253
- parts.push(`summary +~${event.summaryTokens.toLocaleString()} tokens`);
254
- }
255
- if (typeof event.reductionPercent === "number") {
256
- parts.push(`reduction ~${event.reductionPercent.toLocaleString(undefined, { maximumFractionDigits: 2 })}%`);
257
- }
258
- if (event.largestTokenSources?.length) {
259
- const sources = event.largestTokenSources
260
- .slice(0, 3)
261
- .map((bucket) => `${bucket.type}:${bucket.name} ~${bucket.tokens.toLocaleString()}`)
262
- .join("; ");
263
- parts.push(`largest sources ${sources}`);
264
- }
265
- return parts.length > 0 ? ` (${parts.join(", ")})` : "";
266
- }
267
- function isSkippedCompactionResult(result) {
268
- return Boolean(result && typeof result === "object" && result.skipped === true);
269
- }
270
- // Persisted-history prune is deliberately stricter than timeline status
271
- // rendering: only a successful compaction_end proves Pi appended a usable
272
- // compaction entry. Aborted, skipped, or failed endings may leave the branch
273
- // unchanged or partially settled, so pruning must skip instead of guessing.
274
- function getCompactionEndPruneSkipReason(event) {
275
- if (event.aborted)
276
- return "aborted";
277
- if (event.skipped)
278
- return "skipped";
279
- if (event.errorMessage)
280
- return "failed";
281
- if (typeof event.tokensRemoved !== "number" || event.tokensRemoved <= 0)
282
- return "missing_token_reduction";
283
- return null;
284
- }
285
- const BRANCH_MESSAGE_TIMESTAMP_TOLERANCE_MS = 5_000;
286
- function isRecord(value) {
287
- return value !== null && typeof value === "object" && !Array.isArray(value);
288
- }
289
- function contentAsPersistedText(content) {
290
- if (typeof content === "string")
291
- return content;
292
- if (!Array.isArray(content))
293
- return null;
294
- const textParts = [];
295
- for (const part of content) {
296
- if (typeof part === "string") {
297
- textParts.push(part);
298
- continue;
299
- }
300
- if (isRecord(part) &&
301
- part.type === "text" &&
302
- typeof part.text === "string") {
303
- textParts.push(part.text);
304
- }
305
- }
306
- return textParts.length > 0 ? textParts.join("") : null;
307
- }
308
- function branchMessageTimestampMs(entry, message) {
309
- if (typeof message.timestamp === "number" &&
310
- Number.isFinite(message.timestamp))
311
- return message.timestamp;
312
- if (typeof entry.timestamp !== "string")
313
- return null;
314
- const parsed = Date.parse(entry.timestamp);
315
- return Number.isFinite(parsed) ? parsed : null;
316
- }
317
- function branchMessageSignature(entry) {
318
- if (entry.type !== "message" || !isRecord(entry.message))
319
- return null;
320
- const role = entry.message.role;
321
- if (role !== "user" && role !== "assistant")
322
- return null;
323
- const content = contentAsPersistedText(entry.message.content);
324
- if (content === null)
325
- return null;
326
- return {
327
- role,
328
- content,
329
- timestampMs: branchMessageTimestampMs(entry, entry.message),
330
- entryId: entry.id,
331
- };
332
- }
333
- function branchMessageRole(entry) {
334
- return entry.type === "message" && isRecord(entry.message)
335
- ? entry.message.role
336
- : undefined;
337
- }
338
- function getKeptBranchMessageSignatures(entries, keptEntryIndex) {
339
- const signatures = [];
340
- for (let i = keptEntryIndex; i < entries.length; i++) {
341
- const entry = entries[i];
342
- if (entry.type !== "message")
343
- continue;
344
- const role = branchMessageRole(entry);
345
- if (role !== "user" && role !== "assistant")
346
- continue;
347
- const signature = branchMessageSignature(entry);
348
- if (!signature) {
349
- console.log(`[spectral] prune: skipped – reason=unsafe_kept_message, ` +
350
- `kept branch message ${entry.id} cannot be matched safely`);
351
- return null;
352
- }
353
- signatures.push(signature);
354
- }
355
- return signatures;
356
- }
357
- function selectUniqueCandidate(candidates, kept) {
358
- if (candidates.length === 1)
359
- return candidates[0];
360
- if (kept.timestampMs === null)
361
- return null;
362
- const keptTimestampMs = kept.timestampMs;
363
- const timestampMatches = candidates.filter((candidate) => Math.abs(candidate.message.createdAt - keptTimestampMs) <=
364
- BRANCH_MESSAGE_TIMESTAMP_TOLERANCE_MS);
365
- return timestampMatches.length === 1 ? timestampMatches[0] : null;
366
- }
367
- function matchKeptPersistedMessageIds(existing, keptBranchMessages) {
368
- const existingConversationMessages = existing
369
- .filter((message) => message.role === "user" || message.role === "assistant")
370
- .map((message, conversationIndex) => ({ message, conversationIndex }));
371
- const matchedIds = new Set();
372
- let searchStart = 0;
373
- for (const kept of keptBranchMessages) {
374
- const candidates = existingConversationMessages.filter((candidate) => candidate.conversationIndex >= searchStart &&
375
- !matchedIds.has(candidate.message.id) &&
376
- candidate.message.role === kept.role &&
377
- candidate.message.content === kept.content);
378
- const selected = selectUniqueCandidate(candidates, kept);
379
- if (!selected) {
380
- console.log(`[spectral] prune: skipped – reason=ambiguous_kept_mapping, ` +
381
- `could not stably match kept branch entry ${kept.entryId} ` +
382
- `(${kept.role}, ${kept.content.length.toLocaleString()} chars) to SQLite history`);
383
- return null;
384
- }
385
- matchedIds.add(selected.message.id);
386
- searchStart = selected.conversationIndex + 1;
387
- }
388
- return matchedIds;
389
- }
390
- function loadCompactionPolicy(cwd) {
391
- try {
392
- return SettingsManager.create(cwd).getCompactionPolicy();
393
- }
394
- catch {
395
- return DEFAULT_COMPACTION_POLICY;
396
- }
397
- }
398
- function selectHistoryForBridge(history, hasMemorySnapshot) {
399
- if (!history || history.length === 0)
400
- return history;
401
- if (!hasMemorySnapshot)
402
- return history;
403
- let total = 0;
404
- let start = history.length - 1;
405
- for (; start >= 0; start--) {
406
- const nextTotal = total + estimateStoredMessageReplayTokens(history[start]);
407
- if (total > 0 && nextTotal > REHYDRATION_TAIL_TOKEN_BUDGET)
408
- break;
409
- total = nextTotal;
410
- }
411
- const selected = history.slice(Math.max(0, start + 1));
412
- return selected.length > 0 ? selected : [history[history.length - 1]];
413
- }
414
- function prunePersistedHistoryAfterCompaction(store, sessionId, bridge) {
415
- if (!bridge.getSessionBranch)
416
- return;
417
- try {
418
- const entries = bridge.getSessionBranch();
419
- // Pi appends a CompactionEntry (type "compaction") at the end whose
420
- // firstKeptEntryId marks the first branch entry that survives in LLM context.
421
- // We scan backwards so we find the most recent compaction (there should be
422
- // exactly one per compaction pass).
423
- const lastCompaction = [...entries]
424
- .reverse()
425
- .find((entry) => entry.type === "compaction");
426
- const firstKeptEntryId = lastCompaction?.firstKeptEntryId;
427
- if (!firstKeptEntryId) {
428
- console.log(`[spectral] prune: skipped – reason=missing_compaction_entry, ` +
429
- `no compaction entry (or no firstKeptEntryId) ` +
430
- `in branch for ${sessionId} (${entries.length} entries)`);
431
- return;
432
- }
433
- const keptEntryIndex = entries.findIndex((entry) => entry.id === firstKeptEntryId);
434
- if (keptEntryIndex === -1) {
435
- console.log(`[spectral] prune: skipped – reason=missing_kept_entry, ` +
436
- `firstKeptEntryId "${firstKeptEntryId}" ` +
437
- `not found in ${entries.length} branch entries for ${sessionId}`);
438
- return;
439
- }
440
- const keptBranchMessages = getKeptBranchMessageSignatures(entries, keptEntryIndex);
441
- if (!keptBranchMessages)
442
- return;
443
- const existing = store.getMessages(sessionId);
444
- if (existing.length === 0)
445
- return;
446
- if (keptBranchMessages.length === 0) {
447
- console.log(`[spectral] prune: skipped – reason=no_kept_conversation_messages, ` +
448
- `no kept persisted user/assistant messages for ${sessionId}`);
449
- return;
450
- }
451
- console.log(`[spectral] prune: ${sessionId} – source=compaction_end, branch has ${entries.length} entries, ` +
452
- `kept portion starts at index ${keptEntryIndex} (id=${firstKeptEntryId}), ` +
453
- `${keptBranchMessages.length} user+assistant entries kept, ` +
454
- `${existing.length} SQLite messages`);
455
- const matchedKeptIds = matchKeptPersistedMessageIds(existing, keptBranchMessages);
456
- if (!matchedKeptIds)
457
- return;
458
- const deleteIds = existing
459
- .filter((message) => (message.role === "user" || message.role === "assistant") &&
460
- !matchedKeptIds.has(message.id))
461
- .map((message) => message.id);
462
- if (deleteIds.length === 0) {
463
- console.log(`[spectral] prune: skipped – reason=already_pruned, ` +
464
- `SQLite history already matches compacted branch for ${sessionId}`);
465
- return;
466
- }
467
- const pruned = store.deleteMessagesByIdForSession(sessionId, deleteIds);
468
- console.log(`[spectral] prune: finished – reason=successful_compaction_end, ` +
469
- `dropped ${pruned} pre-compaction message(s) ` +
470
- `for ${sessionId}, ${matchedKeptIds.size} user/assistant message(s) kept`);
471
- }
472
- catch (err) {
473
- console.warn(`[spectral] warn: failed to prune persisted history after compaction for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
474
- }
475
- }
476
- function buildDcpLiteThresholds(config, policy) {
477
- return {
478
- postRunCompactionThresholdTokens: Math.max(config.compactionThresholdTokens, policy.autoOlderHistory.minRawTokens),
479
- autoContextLimitRatio: CONTEXT_LIMIT_RATIO,
480
- olderHistoryKeepRecentTokens: policy.agentDriven.olderHistoryKeepRecentTokens,
481
- olderHistoryMinCompactableTokens: policy.agentDriven.olderHistoryMinCompactableTokens,
482
- cooldownTurns: policy.agentDriven.cooldownTurns,
483
- };
484
- }
485
- function contextPercent(usedTokens, maxTokens) {
486
- if (typeof usedTokens !== "number" ||
487
- typeof maxTokens !== "number" ||
488
- maxTokens <= 0)
489
- return null;
490
- return Math.max(0, Math.min(100, (usedTokens / maxTokens) * 100));
491
- }
492
- function applyDcpLiteRuntimeStatus(status, stream) {
493
- if (!status.dcpLite || !stream)
494
- return;
495
- const usage = stream.bridge.getContextUsage?.();
496
- const usedTokens = usage ? usage.tokens : stream.contextWindowUsed;
497
- const maxTokens = usage ? usage.contextWindow : stream.contextWindowMax;
498
- const percent = usage?.percent ?? contextPercent(usedTokens, maxTokens);
499
- if (usage || usedTokens !== null || maxTokens !== null) {
500
- status.dcpLite.contextWindow = { usedTokens, maxTokens, percent };
501
- }
502
- if (stream.lastDcpEvent) {
503
- status.dcpLite.lastEvent = stream.lastDcpEvent;
504
- }
505
- }
506
- function dcpLiteEventFromServerEvent(event) {
507
- if (event.type !== "agent_notification" || !event.dcpLiteTelemetry)
508
- return null;
509
- const telemetry = event.dcpLiteTelemetry;
510
- return {
511
- kind: telemetry.event,
512
- message: event.message,
513
- timestamp: telemetry.timestamp,
514
- source: telemetry.source,
515
- mode: telemetry.mode,
516
- scope: telemetry.scope,
517
- reason: telemetry.reason,
518
- satisfiedSignals: telemetry.satisfiedSignals,
519
- rawTokens: telemetry.rawTokens,
520
- contextTokens: telemetry.contextTokens,
521
- contextPercent: telemetry.contextPercent,
522
- tokensBefore: telemetry.tokensBefore,
523
- tokensAfter: telemetry.tokensAfter,
524
- tokensRemoved: telemetry.tokensRemoved,
525
- };
526
- }
527
- function defaultMemoryStatus(cwd) {
528
- const config = loadConfig(cwd);
529
- const policy = loadCompactionPolicy(cwd);
530
- return {
531
- mode: config.passive ? "passive" : "active",
532
- phase: "idle",
533
- inFlight: {
534
- observer: false,
535
- compaction: false,
536
- reflection: false,
537
- pruner: false,
538
- },
539
- reflections: { count: 0, tokens: 0 },
540
- observations: {
541
- committedCount: 0,
542
- committedTokens: 0,
543
- pendingCount: 0,
544
- pendingTokens: 0,
545
- },
546
- thresholds: {
547
- observation: config.observationThresholdTokens,
548
- compaction: config.compactionThresholdTokens,
549
- reflection: config.reflectionThresholdTokens,
550
- },
551
- progress: {
552
- sinceObservationBoundTokens: 0,
553
- sinceLastCompactionTokens: 0,
554
- observationPoolTokens: 0,
555
- },
556
- dcpLite: {
557
- thresholds: buildDcpLiteThresholds(config, policy),
558
- },
559
- };
560
- }
561
109
  export class SessionStreamManager {
562
110
  store;
563
111
  cwd;
@@ -2830,42 +2378,3 @@ export class SessionStreamManager {
2830
2378
  return true;
2831
2379
  }
2832
2380
  }
2833
- function isReplayable(event) {
2834
- return (event.type === "message_start" ||
2835
- event.type === "text_delta" ||
2836
- event.type === "thinking_start" ||
2837
- event.type === "thinking_delta" ||
2838
- event.type === "thinking_end" ||
2839
- event.type === "tool_call" ||
2840
- event.type === "tool_result" ||
2841
- event.type === "message_end" ||
2842
- event.type === "token_usage" ||
2843
- event.type === "error" ||
2844
- event.type === "subagent_start" ||
2845
- // `subagent_progress` carries full accumulated live snapshots (streaming
2846
- // text/thinking/status) and is intentionally live-only; buffering it into
2847
- // currentTurn or batch persistence recreates O(n²) snapshot growth while a
2848
- // long subagent is running. Keep lifecycle/tool/end events replayable.
2849
- event.type === "subagent_end" ||
2850
- event.type === "subagent_tool_progress");
2851
- }
2852
- function assistantMessageIdsForTurn(turn) {
2853
- const ids = new Set();
2854
- for (const event of turn.events) {
2855
- if (event.type === "message_start")
2856
- ids.add(event.messageId);
2857
- }
2858
- return ids;
2859
- }
2860
- function snapshotTurn(turn, includePersistedEvents = false) {
2861
- // Defensive copy of the events array so the snapshot can't be mutated by
2862
- // subsequent buffer pushes. Individual event objects are safe to share —
2863
- // they're never mutated after creation.
2864
- return {
2865
- turnId: turn.turnId,
2866
- startedAt: turn.startedAt,
2867
- events: includePersistedEvents
2868
- ? [...turn.events]
2869
- : turn.events.slice(turn.persistedEventCount),
2870
- };
2871
- }
@@ -0,0 +1,14 @@
1
+ /** Minimum mean trigram Jaccard similarity for a typo-tolerant fallback match. */
2
+ export declare const TYPO_FALLBACK_MIN_MEAN_SIMILARITY = 0.3;
3
+ /**
4
+ * Trigram Jaccard similarity at or above which a new observation is treated
5
+ * as a near-duplicate rewording of an existing row and replaces it.
6
+ */
7
+ export declare const NEAR_DUPLICATE_SIMILARITY = 0.9;
8
+ /**
9
+ * Final ranking score for a candidate observation: a weighted blend of
10
+ * normalized text-match quality, relevance tag and recency decay. Every
11
+ * component is in [0, 1]; higher is better.
12
+ */
13
+ export declare function scoreObservation(textScore: number, relevance: string, createdAt: number, now: number): number;
14
+ //# sourceMappingURL=observation-ranking.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observation-ranking.d.ts","sourceRoot":"","sources":["../../../src/server/storage/observation-ranking.ts"],"names":[],"mappings":"AAiBA,kFAAkF;AAClF,eAAO,MAAM,iCAAiC,MAAM,CAAC;AACrD;;;GAGG;AACH,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAE7C;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,GACV,MAAM,CASR"}
@@ -0,0 +1,37 @@
1
+ // ---- observational-memory ranking constants ------------------------------
2
+ /** Weight of the text-match component (bm25-derived or trigram similarity). */
3
+ const WEIGHT_TEXT_MATCH = 0.6;
4
+ /** Weight of the relevance-tag component (critical=1.0 … low=0.25). */
5
+ const WEIGHT_RELEVANCE = 0.25;
6
+ /** Weight of the recency component. */
7
+ const WEIGHT_RECENCY = 0.15;
8
+ /** Recency decay constant: exp(-ageDays / RECENCY_DECAY_DAYS). */
9
+ const RECENCY_DECAY_DAYS = 30;
10
+ const MS_PER_DAY = 86_400_000;
11
+ /** Relevance-tag weights used by the ranking formula. */
12
+ const RELEVANCE_WEIGHTS = {
13
+ critical: 1.0,
14
+ high: 0.75,
15
+ medium: 0.5,
16
+ low: 0.25,
17
+ };
18
+ /** Minimum mean trigram Jaccard similarity for a typo-tolerant fallback match. */
19
+ export const TYPO_FALLBACK_MIN_MEAN_SIMILARITY = 0.3;
20
+ /**
21
+ * Trigram Jaccard similarity at or above which a new observation is treated
22
+ * as a near-duplicate rewording of an existing row and replaces it.
23
+ */
24
+ export const NEAR_DUPLICATE_SIMILARITY = 0.9;
25
+ /**
26
+ * Final ranking score for a candidate observation: a weighted blend of
27
+ * normalized text-match quality, relevance tag and recency decay. Every
28
+ * component is in [0, 1]; higher is better.
29
+ */
30
+ export function scoreObservation(textScore, relevance, createdAt, now) {
31
+ const ageDays = Math.max(0, (now - createdAt) / MS_PER_DAY);
32
+ const recency = Math.exp(-ageDays / RECENCY_DECAY_DAYS);
33
+ const relevanceWeight = RELEVANCE_WEIGHTS[relevance] ?? RELEVANCE_WEIGHTS.low;
34
+ return (textScore * WEIGHT_TEXT_MATCH +
35
+ relevanceWeight * WEIGHT_RELEVANCE +
36
+ recency * WEIGHT_RECENCY);
37
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Pre-flight: verify that the SQLite adapter loads and a writable DB can be
3
+ * opened.
4
+ *
5
+ * Called before binding the port so a broken native build fails loudly with
6
+ * a helpful hint instead of crashing later when the first WS connection
7
+ * arrives.
8
+ *
9
+ * Implementation note: we test by opening the *real* target path. Opening a
10
+ * tmpfile would only verify that the native module loads, not that the
11
+ * config dir is writable. Since `open()` already creates the file/dir
12
+ * idempotently, this is safe to call repeatedly.
13
+ */
14
+ export interface PreflightResult {
15
+ ok: boolean;
16
+ /** Human-friendly error message ready to print. Set when ok=false. */
17
+ error?: string;
18
+ }
19
+ export declare function preflightSqlite(dbPath: string): PreflightResult;
20
+ //# sourceMappingURL=preflight.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preflight.d.ts","sourceRoot":"","sources":["../../../src/server/storage/preflight.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,OAAO,CAAC;IACZ,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,CAmB/D"}
@@ -0,0 +1,22 @@
1
+ import { SessionStore } from "../storage.js";
2
+ import { getConfigDir } from "../../config.js";
3
+ export function preflightSqlite(dbPath) {
4
+ try {
5
+ const store = new SessionStore(dbPath);
6
+ // Touch the schema check to make sure the DB is readable.
7
+ store.listTables();
8
+ store.close();
9
+ return { ok: true };
10
+ }
11
+ catch (err) {
12
+ const msg = err instanceof Error ? err.message : String(err);
13
+ return {
14
+ ok: false,
15
+ error: `Failed to load the SQLite storage module (${msg}).\n` +
16
+ ` On Node >= 22.5 spectral uses the built-in node:sqlite module; on older\n` +
17
+ ` Node it falls back to better-sqlite3, which needs its native binary built.\n` +
18
+ ` Try: cd ${getConfigDir()} && npm rebuild better-sqlite3\n` +
19
+ ` Or reinstall: npm install -g @aexol/spectral`,
20
+ };
21
+ }
22
+ }
@@ -0,0 +1,5 @@
1
+ import { type StudioBinding } from "../../studio-binding.js";
2
+ import type { WireProject } from "../wire.js";
3
+ /** Augment a WireProject row with Studio binding fields when present. */
4
+ export declare function applyBindingFields(project: WireProject, rowBinding?: StudioBinding | null): WireProject;
5
+ //# sourceMappingURL=project-binding.d.ts.map