praana 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/bin/praana.js +17 -0
  4. package/bin/pran.js +17 -0
  5. package/dist/app-banner.d.ts +11 -0
  6. package/dist/app-banner.js +161 -0
  7. package/dist/app-controller.d.ts +44 -0
  8. package/dist/app-controller.js +143 -0
  9. package/dist/app-identity.d.ts +18 -0
  10. package/dist/app-identity.js +52 -0
  11. package/dist/auto-compact.d.ts +16 -0
  12. package/dist/auto-compact.js +101 -0
  13. package/dist/cli-args.d.ts +14 -0
  14. package/dist/cli-args.js +69 -0
  15. package/dist/compile-classic.d.ts +21 -0
  16. package/dist/compile-classic.js +106 -0
  17. package/dist/compiler.d.ts +75 -0
  18. package/dist/compiler.js +406 -0
  19. package/dist/config.d.ts +3 -0
  20. package/dist/config.js +433 -0
  21. package/dist/context-engine/activity-log.d.ts +9 -0
  22. package/dist/context-engine/activity-log.js +109 -0
  23. package/dist/context-engine/artifact-store.d.ts +32 -0
  24. package/dist/context-engine/artifact-store.js +272 -0
  25. package/dist/context-engine/bm25.d.ts +3 -0
  26. package/dist/context-engine/bm25.js +32 -0
  27. package/dist/context-engine/checkpoint.d.ts +34 -0
  28. package/dist/context-engine/checkpoint.js +430 -0
  29. package/dist/context-engine/classify.d.ts +3 -0
  30. package/dist/context-engine/classify.js +60 -0
  31. package/dist/context-engine/db.d.ts +73 -0
  32. package/dist/context-engine/db.js +505 -0
  33. package/dist/context-engine/distiller.d.ts +30 -0
  34. package/dist/context-engine/distiller.js +67 -0
  35. package/dist/context-engine/engine-compiler.d.ts +23 -0
  36. package/dist/context-engine/engine-compiler.js +297 -0
  37. package/dist/context-engine/error-tracker.d.ts +21 -0
  38. package/dist/context-engine/error-tracker.js +74 -0
  39. package/dist/context-engine/event-lineage.d.ts +26 -0
  40. package/dist/context-engine/event-lineage.js +120 -0
  41. package/dist/context-engine/extraction.d.ts +26 -0
  42. package/dist/context-engine/extraction.js +83 -0
  43. package/dist/context-engine/index.d.ts +82 -0
  44. package/dist/context-engine/index.js +238 -0
  45. package/dist/context-engine/scoring.d.ts +13 -0
  46. package/dist/context-engine/scoring.js +47 -0
  47. package/dist/context-engine/state-snapshot.d.ts +8 -0
  48. package/dist/context-engine/state-snapshot.js +50 -0
  49. package/dist/context-engine/summarize.d.ts +6 -0
  50. package/dist/context-engine/summarize.js +32 -0
  51. package/dist/context-engine/telemetry.d.ts +25 -0
  52. package/dist/context-engine/telemetry.js +64 -0
  53. package/dist/context-engine/turn-digest.d.ts +50 -0
  54. package/dist/context-engine/turn-digest.js +250 -0
  55. package/dist/context-engine/turn-ledger.d.ts +18 -0
  56. package/dist/context-engine/turn-ledger.js +184 -0
  57. package/dist/context-engine/turn-recorder.d.ts +24 -0
  58. package/dist/context-engine/turn-recorder.js +88 -0
  59. package/dist/context-engine/types.d.ts +201 -0
  60. package/dist/context-engine/types.js +4 -0
  61. package/dist/context-pressure.d.ts +19 -0
  62. package/dist/context-pressure.js +36 -0
  63. package/dist/distillers/generic.d.ts +14 -0
  64. package/dist/distillers/generic.js +93 -0
  65. package/dist/distillers/git-diff.d.ts +8 -0
  66. package/dist/distillers/git-diff.js +119 -0
  67. package/dist/distillers/index.d.ts +2 -0
  68. package/dist/distillers/index.js +16 -0
  69. package/dist/distillers/npm-test.d.ts +8 -0
  70. package/dist/distillers/npm-test.js +50 -0
  71. package/dist/distillers/rg-results.d.ts +8 -0
  72. package/dist/distillers/rg-results.js +28 -0
  73. package/dist/distillers/tsc-errors.d.ts +8 -0
  74. package/dist/distillers/tsc-errors.js +52 -0
  75. package/dist/event-log.d.ts +56 -0
  76. package/dist/event-log.js +214 -0
  77. package/dist/llm.d.ts +29 -0
  78. package/dist/llm.js +155 -0
  79. package/dist/logger.d.ts +94 -0
  80. package/dist/logger.js +287 -0
  81. package/dist/main.d.ts +1 -0
  82. package/dist/main.js +54 -0
  83. package/dist/memory/confidence.d.ts +7 -0
  84. package/dist/memory/confidence.js +37 -0
  85. package/dist/memory/consolidation.d.ts +26 -0
  86. package/dist/memory/consolidation.js +166 -0
  87. package/dist/memory/db.d.ts +40 -0
  88. package/dist/memory/db.js +283 -0
  89. package/dist/memory/dedup.d.ts +6 -0
  90. package/dist/memory/dedup.js +50 -0
  91. package/dist/memory/embedder-factory.d.ts +3 -0
  92. package/dist/memory/embedder-factory.js +81 -0
  93. package/dist/memory/embeddings.d.ts +15 -0
  94. package/dist/memory/embeddings.js +67 -0
  95. package/dist/memory/index.d.ts +9 -0
  96. package/dist/memory/index.js +11 -0
  97. package/dist/memory/ollama-summarizer.d.ts +19 -0
  98. package/dist/memory/ollama-summarizer.js +72 -0
  99. package/dist/memory/openai-summarizer.d.ts +21 -0
  100. package/dist/memory/openai-summarizer.js +51 -0
  101. package/dist/memory/store.d.ts +61 -0
  102. package/dist/memory/store.js +502 -0
  103. package/dist/memory/summarizer-factory.d.ts +3 -0
  104. package/dist/memory/summarizer-factory.js +69 -0
  105. package/dist/memory/summarizer.d.ts +4 -0
  106. package/dist/memory/summarizer.js +112 -0
  107. package/dist/memory/types.d.ts +87 -0
  108. package/dist/memory/types.js +17 -0
  109. package/dist/model-context.d.ts +15 -0
  110. package/dist/model-context.js +212 -0
  111. package/dist/project-detector.d.ts +37 -0
  112. package/dist/project-detector.js +604 -0
  113. package/dist/render.d.ts +15 -0
  114. package/dist/render.js +46 -0
  115. package/dist/session.d.ts +118 -0
  116. package/dist/session.js +809 -0
  117. package/dist/skills/index.d.ts +69 -0
  118. package/dist/skills/index.js +885 -0
  119. package/dist/skills/types.d.ts +93 -0
  120. package/dist/skills/types.js +8 -0
  121. package/dist/slash-commands.d.ts +14 -0
  122. package/dist/slash-commands.js +301 -0
  123. package/dist/state-graph.d.ts +38 -0
  124. package/dist/state-graph.js +255 -0
  125. package/dist/status-bar.d.ts +54 -0
  126. package/dist/status-bar.js +184 -0
  127. package/dist/thinking-display.d.ts +21 -0
  128. package/dist/thinking-display.js +37 -0
  129. package/dist/tool-summary.d.ts +4 -0
  130. package/dist/tool-summary.js +67 -0
  131. package/dist/tools/index.d.ts +925 -0
  132. package/dist/tools/index.js +86 -0
  133. package/dist/tools/knowledge.d.ts +140 -0
  134. package/dist/tools/knowledge.js +260 -0
  135. package/dist/tools/memory.d.ts +39 -0
  136. package/dist/tools/memory.js +300 -0
  137. package/dist/tools/search-code.d.ts +134 -0
  138. package/dist/tools/search-code.js +390 -0
  139. package/dist/tools/system.d.ts +16 -0
  140. package/dist/tools/system.js +499 -0
  141. package/dist/tools/tool-def.d.ts +6 -0
  142. package/dist/tools/tool-def.js +3 -0
  143. package/dist/turn-control.d.ts +51 -0
  144. package/dist/turn-control.js +210 -0
  145. package/dist/turn.d.ts +20 -0
  146. package/dist/turn.js +624 -0
  147. package/dist/types.d.ts +233 -0
  148. package/dist/types.js +4 -0
  149. package/dist/ui/readline-ui.d.ts +2 -0
  150. package/dist/ui/readline-ui.js +176 -0
  151. package/dist/ui/tui/app.d.ts +13 -0
  152. package/dist/ui/tui/app.js +270 -0
  153. package/dist/ui/tui/busy-indicator.d.ts +2 -0
  154. package/dist/ui/tui/busy-indicator.js +13 -0
  155. package/dist/ui/tui/components/gutter-rule.d.ts +5 -0
  156. package/dist/ui/tui/components/gutter-rule.js +9 -0
  157. package/dist/ui/tui/components/inline-tool-row.d.ts +10 -0
  158. package/dist/ui/tui/components/inline-tool-row.js +8 -0
  159. package/dist/ui/tui/components/prompt-input.d.ts +20 -0
  160. package/dist/ui/tui/components/prompt-input.js +120 -0
  161. package/dist/ui/tui/components/system-line.d.ts +5 -0
  162. package/dist/ui/tui/components/system-line.js +6 -0
  163. package/dist/ui/tui/components/thinking-block.d.ts +11 -0
  164. package/dist/ui/tui/components/thinking-block.js +31 -0
  165. package/dist/ui/tui/components/toast-line.d.ts +4 -0
  166. package/dist/ui/tui/components/toast-line.js +8 -0
  167. package/dist/ui/tui/components/tool-result-line.d.ts +5 -0
  168. package/dist/ui/tui/components/tool-result-line.js +6 -0
  169. package/dist/ui/tui/components/turn-footer.d.ts +5 -0
  170. package/dist/ui/tui/components/turn-footer.js +7 -0
  171. package/dist/ui/tui/components/user-block.d.ts +6 -0
  172. package/dist/ui/tui/components/user-block.js +6 -0
  173. package/dist/ui/tui/logo-banner.d.ts +5 -0
  174. package/dist/ui/tui/logo-banner.js +8 -0
  175. package/dist/ui/tui/markdown-render.d.ts +16 -0
  176. package/dist/ui/tui/markdown-render.js +218 -0
  177. package/dist/ui/tui/palette.d.ts +12 -0
  178. package/dist/ui/tui/palette.js +13 -0
  179. package/dist/ui/tui/reasoning-summary.d.ts +12 -0
  180. package/dist/ui/tui/reasoning-summary.js +27 -0
  181. package/dist/ui/tui/reducer.d.ts +92 -0
  182. package/dist/ui/tui/reducer.js +260 -0
  183. package/dist/ui/tui/run.d.ts +3 -0
  184. package/dist/ui/tui/run.js +40 -0
  185. package/dist/ui/tui/sink.d.ts +4 -0
  186. package/dist/ui/tui/sink.js +89 -0
  187. package/dist/ui/tui/status-bar-view.d.ts +5 -0
  188. package/dist/ui/tui/status-bar-view.js +44 -0
  189. package/dist/ui/tui/terminal-height.d.ts +12 -0
  190. package/dist/ui/tui/terminal-height.js +20 -0
  191. package/dist/ui/tui/terminal-width.d.ts +2 -0
  192. package/dist/ui/tui/terminal-width.js +5 -0
  193. package/dist/ui/tui/tool-display.d.ts +23 -0
  194. package/dist/ui/tui/tool-display.js +217 -0
  195. package/dist/ui/tui/transcript-line.d.ts +12 -0
  196. package/dist/ui/tui/transcript-line.js +43 -0
  197. package/dist/ui/tui/transcript-replay.d.ts +12 -0
  198. package/dist/ui/tui/transcript-replay.js +117 -0
  199. package/dist/ui-events.d.ts +39 -0
  200. package/dist/ui-events.js +33 -0
  201. package/dist/ui.d.ts +77 -0
  202. package/dist/ui.js +179 -0
  203. package/package.json +73 -0
  204. package/praana.config.example.toml +231 -0
@@ -0,0 +1,430 @@
1
+ import { getSessionCheckpoint, listAllActivityEntries, listTurnDigests, upsertSessionCheckpoint, } from "./db.js";
2
+ import { estimateTokens } from "./summarize.js";
3
+ import { buildNarrativeEntry, decisionSummary, detectCompletedPlanItems, extractPlanItems, isNarrativeWorthy, normalizeTurnDigest, } from "./turn-digest.js";
4
+ const DECISION_IDLE_TURNS = 10;
5
+ const FILE_IDLE_TURNS = 10;
6
+ const MAX_FINDINGS = 30;
7
+ const MAX_FIXED_ERRORS = 20;
8
+ const MAX_NARRATIVE_ENTRIES = 30;
9
+ const NARRATIVE_RENDER_TOKENS = 400;
10
+ const RATIONALE_MAX_CHARS = 240;
11
+ const DECISIONS_SECTION_TOKENS = 800;
12
+ const FINDINGS_SECTION_TOKENS = 300;
13
+ const PLAN_SECTION_TOKENS = 400;
14
+ export function createEmptyCheckpointState() {
15
+ return {
16
+ activeRequest: "",
17
+ plans: [],
18
+ constraints: [],
19
+ decisions: [],
20
+ files: [],
21
+ findings: [],
22
+ errors: [],
23
+ questions: [],
24
+ activity: [],
25
+ narrative: [],
26
+ lastReconciledTurn: -1,
27
+ };
28
+ }
29
+ export function normalizeCheckpointState(raw) {
30
+ const state = createEmptyCheckpointState();
31
+ state.activeRequest = raw.activeRequest ?? "";
32
+ state.constraints = raw.constraints ?? [];
33
+ state.decisions = (raw.decisions ?? []).map((d) => ({ ...d }));
34
+ state.files = (raw.files ?? []).map((f) => ({ ...f }));
35
+ state.findings = (raw.findings ?? []).map((f) => ({ ...f }));
36
+ state.errors = (raw.errors ?? []).map((e) => ({ ...e }));
37
+ state.questions = (raw.questions ?? []).map((q) => ({ ...q }));
38
+ state.activity = raw.activity ?? [];
39
+ state.lastReconciledTurn = raw.lastReconciledTurn ?? -1;
40
+ if (raw.plans && raw.plans.length > 0) {
41
+ state.plans = raw.plans.map((p) => ({ ...p }));
42
+ }
43
+ else if (raw.plan?.trim()) {
44
+ state.plans = [
45
+ {
46
+ text: raw.plan.trim(),
47
+ turn: raw.lastReconciledTurn ?? 0,
48
+ superseded: false,
49
+ },
50
+ ];
51
+ }
52
+ state.narrative = (raw.narrative ?? []).map((n) => ({ ...n }));
53
+ return state;
54
+ }
55
+ export function createEmptyCheckpoint() {
56
+ return { version: 1, state: createEmptyCheckpointState() };
57
+ }
58
+ export function reconcileCheckpoint(state, digest, draft, turn) {
59
+ const normalizedDigest = normalizeTurnDigest(digest);
60
+ const previousIntent = state.activeRequest;
61
+ const next = {
62
+ ...state,
63
+ constraints: [...state.constraints],
64
+ decisions: state.decisions.map((d) => ({ ...d })),
65
+ files: state.files.map((f) => ({ ...f })),
66
+ findings: state.findings.map((f) => ({ ...f })),
67
+ errors: state.errors.map((e) => ({ ...e })),
68
+ questions: state.questions.map((q) => ({ ...q })),
69
+ plans: state.plans.map((p) => ({ ...p })),
70
+ narrative: state.narrative.map((n) => ({ ...n })),
71
+ activity: [...draft.recentActivity],
72
+ lastReconciledTurn: turn,
73
+ };
74
+ next.activeRequest = normalizedDigest.userIntent;
75
+ for (const constraint of normalizedDigest.constraints) {
76
+ if (!next.constraints.includes(constraint)) {
77
+ next.constraints.push(constraint);
78
+ }
79
+ }
80
+ for (const decision of normalizedDigest.decisions) {
81
+ const summary = decisionSummary(decision);
82
+ const rationale = typeof decision === "string" ? undefined : decision.rationale;
83
+ const existingIdx = next.decisions.findIndex((d) => d.summary === summary && d.turn === turn);
84
+ if (existingIdx < 0) {
85
+ next.decisions.push({ summary, rationale, turn });
86
+ }
87
+ else if (rationale && !next.decisions[existingIdx].rationale) {
88
+ next.decisions[existingIdx].rationale = rationale;
89
+ }
90
+ }
91
+ next.decisions = next.decisions.map((d) => ({
92
+ ...d,
93
+ compact: turn - d.turn >= DECISION_IDLE_TURNS,
94
+ }));
95
+ for (const path of normalizedDigest.filesChanged) {
96
+ const existing = next.files.find((f) => f.path === path);
97
+ if (existing) {
98
+ existing.turn = turn;
99
+ }
100
+ else {
101
+ next.files.push({ path, turn });
102
+ }
103
+ }
104
+ next.files = next.files.filter((f) => turn - f.turn < FILE_IDLE_TURNS);
105
+ for (const artifactRef of normalizedDigest.artifactRefs) {
106
+ next.findings.push({
107
+ summary: `Artifact ${artifactRef}`,
108
+ artifactRef,
109
+ turn,
110
+ });
111
+ }
112
+ while (next.findings.length > MAX_FINDINGS) {
113
+ next.findings.shift();
114
+ }
115
+ next.errors = reconcileErrors(state.errors, normalizedDigest, draft, turn);
116
+ next.plans = reconcilePlans(next.plans, normalizedDigest.extractedPlan, normalizedDigest, turn);
117
+ if (isNarrativeWorthy(normalizedDigest, previousIntent)) {
118
+ const text = buildNarrativeEntry(normalizedDigest, previousIntent);
119
+ if (text) {
120
+ next.narrative.push({ turn, text });
121
+ while (next.narrative.length > MAX_NARRATIVE_ENTRIES) {
122
+ next.narrative.shift();
123
+ }
124
+ }
125
+ }
126
+ return next;
127
+ }
128
+ function reconcilePlans(previous, extractedPlan, digest, turn) {
129
+ if (!extractedPlan) {
130
+ return previous;
131
+ }
132
+ const next = previous.map((p) => ({ ...p }));
133
+ const currentIdx = findLastActivePlanIndex(next);
134
+ if (currentIdx >= 0 && next[currentIdx].text === extractedPlan) {
135
+ return next;
136
+ }
137
+ if (currentIdx >= 0) {
138
+ const current = next[currentIdx];
139
+ const completed = detectCompletedPlanItems(extractPlanItems(current.text), digest);
140
+ next[currentIdx] = {
141
+ ...current,
142
+ superseded: true,
143
+ supersededTurn: turn,
144
+ completed: completed.length > 0
145
+ ? completed
146
+ : current.completed,
147
+ };
148
+ }
149
+ next.push({
150
+ text: extractedPlan,
151
+ turn,
152
+ superseded: false,
153
+ });
154
+ return next;
155
+ }
156
+ function reconcileErrors(previous, digest, draft, turn) {
157
+ const open = draft.openErrors.map((err) => ({
158
+ key: err.key,
159
+ message: err.message,
160
+ turn: err.turn,
161
+ fixed: false,
162
+ }));
163
+ const fixed = previous.filter((e) => e.fixed);
164
+ for (const label of digest.errorsFixed) {
165
+ const match = fixed.find((e) => e.message === label || e.message === `Fixed: ${label}`);
166
+ if (!match) {
167
+ fixed.push({
168
+ key: `fixed:${label}`,
169
+ message: `Fixed: ${label}`,
170
+ turn,
171
+ fixed: true,
172
+ fixedTurn: turn,
173
+ });
174
+ }
175
+ }
176
+ const merged = [...open, ...fixed];
177
+ const seen = new Set();
178
+ const deduped = [];
179
+ for (const entry of merged) {
180
+ const id = entry.fixed ? `fixed:${entry.message}` : entry.key;
181
+ if (seen.has(id))
182
+ continue;
183
+ seen.add(id);
184
+ deduped.push(entry);
185
+ }
186
+ const openErrors = deduped.filter((e) => !e.fixed);
187
+ const fixedErrors = deduped.filter((e) => e.fixed).slice(-MAX_FIXED_ERRORS);
188
+ return [...openErrors, ...fixedErrors];
189
+ }
190
+ export function replayCheckpointFromDigests(digests, activityEntries) {
191
+ let state = createEmptyCheckpointState();
192
+ const openErrors = [];
193
+ for (const digest of digests.map(normalizeTurnDigest)) {
194
+ for (const message of digest.errorsNew) {
195
+ if (!openErrors.some((e) => e.message === message)) {
196
+ openErrors.push({
197
+ key: message,
198
+ message,
199
+ turn: digest.turnId,
200
+ tool: "unknown",
201
+ });
202
+ }
203
+ }
204
+ for (const fixed of digest.errorsFixed) {
205
+ const idx = openErrors.findIndex((e) => e.message.includes(fixed) || fixed.includes(e.message));
206
+ if (idx >= 0)
207
+ openErrors.splice(idx, 1);
208
+ }
209
+ const draft = {
210
+ lastUserIntent: digest.userIntent,
211
+ openErrors: [...openErrors],
212
+ recentDecisions: state.decisions.map((d) => ({
213
+ summary: d.summary,
214
+ turn: d.turn,
215
+ })),
216
+ recentConstraints: [...state.constraints],
217
+ recentActivity: activityEntries
218
+ .filter((a) => a.turn <= digest.turnId)
219
+ .slice(-15),
220
+ };
221
+ state = reconcileCheckpoint(state, digest, draft, digest.turnId);
222
+ }
223
+ return state;
224
+ }
225
+ function findLastActivePlanIndex(plans) {
226
+ for (let i = plans.length - 1; i >= 0; i--) {
227
+ if (!plans[i].superseded)
228
+ return i;
229
+ }
230
+ return -1;
231
+ }
232
+ function truncateRationale(rationale) {
233
+ if (rationale.length <= RATIONALE_MAX_CHARS)
234
+ return rationale;
235
+ return rationale.slice(0, RATIONALE_MAX_CHARS - 1) + "…";
236
+ }
237
+ function renderDecision(decision, index) {
238
+ const label = `D${index + 1}`;
239
+ const rationale = decision.rationale
240
+ ? truncateRationale(decision.rationale)
241
+ : "";
242
+ if (rationale) {
243
+ return decision.compact
244
+ ? `- ${label} [turn ${decision.turn}]: ${decision.summary} — ${rationale}`
245
+ : `- ${label} [turn ${decision.turn}]: ${decision.summary} (rationale: ${rationale})`;
246
+ }
247
+ return `- ${label} [turn ${decision.turn}]: ${decision.summary}`;
248
+ }
249
+ function renderPlanSection(plans) {
250
+ if (plans.length === 0)
251
+ return [];
252
+ const currentIdx = findLastActivePlanIndex(plans);
253
+ if (currentIdx < 0)
254
+ return [];
255
+ const lines = ["### Plan"];
256
+ const current = plans[currentIdx];
257
+ lines.push(`Current (turn ${current.turn}): ${current.text.replace(/\n/g, "; ")}`);
258
+ const superseded = plans.filter((p) => p.superseded);
259
+ if (superseded.length > 0) {
260
+ lines.push("Superseded plans:");
261
+ for (const plan of superseded) {
262
+ const completed = plan.completed && plan.completed.length > 0
263
+ ? ` — completed: ${plan.completed.join(", ")}`
264
+ : "";
265
+ lines.push(`- [turn ${plan.turn}] ${plan.text.replace(/\n/g, "; ")}${completed}`);
266
+ }
267
+ }
268
+ return lines;
269
+ }
270
+ function renderNarrativeSection(narrative) {
271
+ if (narrative.length === 0)
272
+ return [];
273
+ // Token-budgeted: keep as many recent entries as fit within the render budget.
274
+ // If the full prose exceeds budget, trim oldest entries first.
275
+ let entries = [...narrative];
276
+ let prose = entries.map((e) => e.text).join(" ");
277
+ while (entries.length > 1 && estimateTokens(prose) > NARRATIVE_RENDER_TOKENS) {
278
+ entries.shift();
279
+ prose = entries.map((e) => e.text).join(" ");
280
+ }
281
+ return ["### Session narrative", prose];
282
+ }
283
+ function renderFindingsSection(findings) {
284
+ if (findings.length === 0)
285
+ return [];
286
+ const recent = findings.slice(-15);
287
+ return [
288
+ "### Findings",
289
+ ...recent.map((f, i) => `- F${i + 1} [turn ${f.turn}]: ${f.summary}${f.artifactRef ? ` (${f.artifactRef})` : ""}`),
290
+ ];
291
+ }
292
+ export function renderCheckpoint(checkpoint) {
293
+ const { state } = checkpoint;
294
+ const sections = ["## Session Checkpoint", ""];
295
+ if (state.activeRequest) {
296
+ sections.push("### Active Request", trimSection(state.activeRequest, 200), "");
297
+ }
298
+ const narrativeLines = renderNarrativeSection(state.narrative);
299
+ if (narrativeLines.length > 0) {
300
+ sections.push(...narrativeLines, "");
301
+ }
302
+ const planLines = trimSectionLines(renderPlanSection(state.plans), PLAN_SECTION_TOKENS);
303
+ if (planLines.length > 0) {
304
+ sections.push(...planLines, "");
305
+ }
306
+ if (state.constraints.length > 0) {
307
+ sections.push("### Constraints", ...state.constraints.map((c, i) => `- C${i + 1}: ${c}`), "");
308
+ }
309
+ const decisions = state.decisions.slice(-20);
310
+ if (decisions.length > 0) {
311
+ const decisionLines = trimSectionLines([
312
+ "### Decisions",
313
+ ...decisions.map((d, i) => renderDecision(d, i)),
314
+ ], DECISIONS_SECTION_TOKENS);
315
+ sections.push(...decisionLines, "");
316
+ }
317
+ if (state.files.length > 0) {
318
+ sections.push("### Files in play", ...state.files.map((f) => `- ${f.path} (turn ${f.turn})`), "");
319
+ }
320
+ const findingsLines = trimSectionLines(renderFindingsSection(state.findings), FINDINGS_SECTION_TOKENS);
321
+ if (findingsLines.length > 0) {
322
+ sections.push(...findingsLines, "");
323
+ }
324
+ const openErrors = state.errors.filter((e) => !e.fixed);
325
+ if (openErrors.length > 0) {
326
+ sections.push("### Open errors", ...openErrors.map((e) => `- ${e.message}`), "");
327
+ }
328
+ const fixedErrors = state.errors.filter((e) => e.fixed);
329
+ if (fixedErrors.length > 0) {
330
+ sections.push("### Fixed errors", ...fixedErrors.map((e) => `- [turn ${e.fixedTurn ?? e.turn}] ${e.message}`), "");
331
+ }
332
+ if (state.activity.length > 0) {
333
+ sections.push("### Recent activity", ...state.activity.map((a) => `- [turn ${a.turn}] ${a.summary}`), "");
334
+ }
335
+ return sections.join("\n").trimEnd();
336
+ }
337
+ export function renderContextSummary(checkpoint, stats) {
338
+ const { state } = checkpoint;
339
+ const lines = ["## Context Summary", ""];
340
+ if (state.activeRequest) {
341
+ lines.push("### Active intent", state.activeRequest, "");
342
+ }
343
+ const recentDecisions = state.decisions.slice(-5);
344
+ if (recentDecisions.length > 0) {
345
+ lines.push("### Recent decisions (last 5)", ...recentDecisions.map((d) => {
346
+ const rationale = d.rationale ? ` — ${d.rationale}` : "";
347
+ return `- ${d.summary}${rationale}`;
348
+ }), "");
349
+ }
350
+ const openErrors = state.errors.filter((e) => !e.fixed);
351
+ if (openErrors.length > 0) {
352
+ lines.push("### Open errors", ...openErrors.map((e) => `- ${e.message}`), "");
353
+ }
354
+ const recentActivity = state.activity.slice(-5);
355
+ if (recentActivity.length > 0) {
356
+ lines.push("### Recent activity (last 5)", ...recentActivity.map((a) => `- [turn ${a.turn}] ${a.summary}`), "");
357
+ }
358
+ const fixedCount = state.errors.filter((e) => e.fixed).length;
359
+ const turnCount = state.lastReconciledTurn >= 0 ? state.lastReconciledTurn + 1 : 0;
360
+ const artifactCount = stats?.artifactCount ?? 0;
361
+ lines.push("### Session stats", `turns: ${turnCount}, artifacts: ${artifactCount}, open errors: ${openErrors.length}, fixed errors: ${fixedCount}`);
362
+ return lines.join("\n");
363
+ }
364
+ function trimSection(text, maxTokens) {
365
+ const budgetChars = maxTokens * 4;
366
+ if (text.length <= budgetChars)
367
+ return text;
368
+ return text.slice(0, budgetChars - 16) + "\n…[truncated]";
369
+ }
370
+ function trimSectionLines(lines, maxTokens) {
371
+ if (lines.length === 0)
372
+ return lines;
373
+ const joined = lines.join("\n");
374
+ const trimmed = trimSection(joined, maxTokens);
375
+ if (trimmed === joined)
376
+ return lines;
377
+ return [trimmed];
378
+ }
379
+ export function checkpointTokenEstimate(checkpoint) {
380
+ const text = renderCheckpoint(checkpoint);
381
+ return { text, tokens: estimateTokens(text) };
382
+ }
383
+ export class CheckpointStore {
384
+ db;
385
+ sessionId;
386
+ checkpoint;
387
+ constructor(db, sessionId, checkpoint) {
388
+ this.db = db;
389
+ this.sessionId = sessionId;
390
+ this.checkpoint = checkpoint;
391
+ }
392
+ static open(db, sessionId) {
393
+ const saved = getSessionCheckpoint(db, sessionId);
394
+ if (saved) {
395
+ const normalized = {
396
+ version: 1,
397
+ state: normalizeCheckpointState(saved.state),
398
+ };
399
+ return new CheckpointStore(db, sessionId, normalized);
400
+ }
401
+ const digests = listTurnDigests(db, sessionId).map(normalizeTurnDigest);
402
+ if (digests.length > 0) {
403
+ const activity = listAllActivityEntries(db, sessionId);
404
+ const state = replayCheckpointFromDigests(digests, activity);
405
+ const rebuilt = { version: 1, state };
406
+ upsertSessionCheckpoint(db, sessionId, rebuilt);
407
+ return new CheckpointStore(db, sessionId, rebuilt);
408
+ }
409
+ return new CheckpointStore(db, sessionId, createEmptyCheckpoint());
410
+ }
411
+ reconcile(digest, draft, turn) {
412
+ this.checkpoint = {
413
+ version: 1,
414
+ state: reconcileCheckpoint(this.checkpoint.state, normalizeTurnDigest(digest), draft, turn),
415
+ };
416
+ }
417
+ persist() {
418
+ upsertSessionCheckpoint(this.db, this.sessionId, this.checkpoint);
419
+ }
420
+ getCheckpoint() {
421
+ return this.checkpoint;
422
+ }
423
+ render() {
424
+ const text = renderCheckpoint(this.checkpoint);
425
+ return text.trim().length > 0 ? text : "";
426
+ }
427
+ renderContextSummary(stats) {
428
+ return renderContextSummary(this.checkpoint, stats);
429
+ }
430
+ }
@@ -0,0 +1,3 @@
1
+ import type { ContentType } from "./types.js";
2
+ /** Fast regex-based content-type classification (<1ms). */
3
+ export declare function classifyContentType(text: string): ContentType;
@@ -0,0 +1,60 @@
1
+ /** Fast regex-based content-type classification (<1ms). */
2
+ export function classifyContentType(text) {
3
+ const trimmed = text.trim();
4
+ if (!trimmed)
5
+ return "other";
6
+ if (/^diff --git/m.test(trimmed) || /^@@ -\d+/m.test(trimmed)) {
7
+ return "diff";
8
+ }
9
+ if (/\b(PASS|FAIL|✓|✗)\b/.test(trimmed) && /\btests?\b/i.test(trimmed)) {
10
+ return "test_output";
11
+ }
12
+ if (/error TS\d+:/.test(trimmed) || /^error:.+:\d+/m.test(trimmed)) {
13
+ return "build_output";
14
+ }
15
+ if ((trimmed.startsWith("{") || trimmed.startsWith("[")) &&
16
+ looksLikeJson(trimmed)) {
17
+ return "json";
18
+ }
19
+ // Error classification: only when the content is primarily an error,
20
+ // not a larger output that happens to contain the word "Error".
21
+ // Test output and build output are already caught above.
22
+ if (trimmed.length < 500 &&
23
+ /\b(Error|error|EXCEPTION)\b/.test(trimmed) &&
24
+ !/\b(PASS|FAIL|✓|✗|tests?)\b/i.test(trimmed)) {
25
+ return "error";
26
+ }
27
+ if (/^(import |export |function |class |const |let |def )/m.test(trimmed) ||
28
+ /```[\s\S]*```/.test(trimmed)) {
29
+ return "code";
30
+ }
31
+ const lines = trimmed.split("\n");
32
+ if (lines.length > 20 && hasRepetitiveLines(lines)) {
33
+ return "log";
34
+ }
35
+ if (/\n\n/.test(trimmed) && /[a-z]{4,}/i.test(trimmed)) {
36
+ return "prose";
37
+ }
38
+ return "other";
39
+ }
40
+ function looksLikeJson(text) {
41
+ try {
42
+ JSON.parse(text);
43
+ return true;
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ }
49
+ function hasRepetitiveLines(lines) {
50
+ const counts = new Map();
51
+ for (const line of lines.slice(0, 100)) {
52
+ const key = line.trim().slice(0, 80);
53
+ if (!key)
54
+ continue;
55
+ counts.set(key, (counts.get(key) ?? 0) + 1);
56
+ if ((counts.get(key) ?? 0) >= 3)
57
+ return true;
58
+ }
59
+ return false;
60
+ }
@@ -0,0 +1,73 @@
1
+ import Database from "better-sqlite3";
2
+ import type { ActivityEntry, ContextArtifact, OpenError, SessionCheckpoint, TurnDigest, TurnRecord } from "./types.js";
3
+ export interface DistillerStatRow {
4
+ sessionId: string;
5
+ tool: string;
6
+ contentType: string;
7
+ distiller: string;
8
+ inputTokens: number;
9
+ outputTokens: number;
10
+ savingsPct: number;
11
+ execTimeMs: number;
12
+ turn: number;
13
+ }
14
+ export declare function openContextEngineDb(dbPath: string): Database.Database;
15
+ export declare function findArtifactByHash(db: Database.Database, sha256: string): ContextArtifact | null;
16
+ export declare function getArtifactById(db: Database.Database, id: string): ContextArtifact | null;
17
+ export declare function insertArtifact(db: Database.Database, artifact: ContextArtifact): void;
18
+ export declare function touchArtifactAccess(db: Database.Database, id: string, turn: number): void;
19
+ export declare function updateArtifactSummary(db: Database.Database, id: string, summary: string): void;
20
+ export declare function insertDistillerStat(db: Database.Database, row: DistillerStatRow): void;
21
+ export declare function evictStaleArtifacts(db: Database.Database, currentTurn: number, ttlTurns: number): number;
22
+ export declare function getMaxLedgerTurn(db: Database.Database, sessionId: string): number | null;
23
+ export declare function hasLedgerTurn(db: Database.Database, sessionId: string, turn: number): boolean;
24
+ export declare function insertTurnRecord(db: Database.Database, sessionId: string, record: TurnRecord, searchText: string): void;
25
+ export declare function listTurnRecords(db: Database.Database, sessionId: string): TurnRecord[];
26
+ export declare function getTurnRecord(db: Database.Database, sessionId: string, turn: number): TurnRecord | null;
27
+ export declare function listArtifactIdsForTurn(db: Database.Database, sessionId: string, turn: number): string[];
28
+ export declare function insertTurnDigest(db: Database.Database, sessionId: string, digest: TurnDigest): void;
29
+ export declare function insertActivityEntries(db: Database.Database, sessionId: string, entries: ActivityEntry[]): void;
30
+ export declare function listActivityEntries(db: Database.Database, sessionId: string, limit: number): ActivityEntry[];
31
+ export interface PersistedExtractionState {
32
+ openErrors: OpenError[];
33
+ testFailed: boolean;
34
+ recentDecisions: Array<{
35
+ summary: string;
36
+ turn: number;
37
+ }>;
38
+ recentConstraints: string[];
39
+ lastUserIntent: string;
40
+ }
41
+ export declare function getExtractionState(db: Database.Database, sessionId: string): PersistedExtractionState | null;
42
+ export declare function upsertExtractionState(db: Database.Database, sessionId: string, state: PersistedExtractionState): void;
43
+ export declare function getSessionCheckpoint(db: Database.Database, sessionId: string): SessionCheckpoint | null;
44
+ export declare function upsertSessionCheckpoint(db: Database.Database, sessionId: string, checkpoint: SessionCheckpoint): void;
45
+ export declare function getTurnDigest(db: Database.Database, sessionId: string, turn: number): TurnDigest | null;
46
+ export declare function listTurnDigests(db: Database.Database, sessionId: string): TurnDigest[];
47
+ export declare function listSessionArtifacts(db: Database.Database, sessionId: string): ContextArtifact[];
48
+ export declare function countSessionArtifacts(db: Database.Database, sessionId: string): number;
49
+ export declare function listAllActivityEntries(db: Database.Database, sessionId: string): ActivityEntry[];
50
+ export type ArtifactAccessType = "prompt_included" | "retrieved" | "checkpoint_ref";
51
+ export interface SessionStatsRow {
52
+ sessionId: string;
53
+ contextEngineEnabled: boolean;
54
+ pressureEvents: number;
55
+ compactionTriggers: number;
56
+ artifactRetrievals: number;
57
+ totalDistillerSavings: number;
58
+ totalTurns: number;
59
+ }
60
+ export declare function insertArtifactAccess(db: Database.Database, sessionId: string, artifactId: string, accessType: ArtifactAccessType, turn: number): void;
61
+ export declare function getSessionStatsRow(db: Database.Database, sessionId: string): SessionStatsRow | null;
62
+ export declare function incrementSessionStat(db: Database.Database, sessionId: string, field: "pressure_events" | "compaction_triggers" | "artifact_retrievals", amount?: number, contextEngineEnabled?: boolean): void;
63
+ export declare function finalizeSessionStats(db: Database.Database, sessionId: string, totalTurns: number, contextEngineEnabled?: boolean): SessionStatsRow;
64
+ export interface DistillerCostRow {
65
+ distiller: string;
66
+ tool: string;
67
+ runs: number;
68
+ avgInputTokens: number;
69
+ avgSavingsPct: number;
70
+ estimatedCost: number;
71
+ }
72
+ export declare function listDistillerCostRanking(db: Database.Database, sessionId: string): DistillerCostRow[];
73
+ export declare function countArtifactAccessByType(db: Database.Database, sessionId: string, accessType: ArtifactAccessType): number;