context-mode 1.0.67 → 1.0.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,41 +1,55 @@
1
1
  /**
2
- * Snapshot builder — converts stored SessionEvents into an XML resume snapshot.
2
+ * Snapshot builder — converts stored SessionEvents into a reference-based
3
+ * XML resume snapshot.
3
4
  *
4
5
  * Pure functions only. No database access, no file system, no side effects.
5
- * The output XML is injected into Claude's context after a compact event to
6
- * restore session awareness.
7
6
  *
8
- * Budget: default 2048 bytes, allocated by priority tier:
9
- * P1 (file, task, rule): 50% = ~1024 bytes
10
- * P2 (cwd, error, decision, env, git): 35% = ~716 bytes
11
- * P3-P4 (subagent, skill, role, data, intent): 15% = ~308 bytes
7
+ * The output XML is injected into the LLM's context after a compact event to
8
+ * restore session awareness. Instead of truncated inline data, each section
9
+ * contains a natural summary plus a runnable search tool call that retrieves
10
+ * full details from the indexed knowledge base on demand.
11
+ *
12
+ * Zero truncation. Zero information loss. Full data lives in SessionDB;
13
+ * the snapshot is a table of contents.
12
14
  */
13
- import { escapeXML, truncateString } from "../truncate.js";
14
- // ── Constants ────────────────────────────────────────────────────────────────
15
- const DEFAULT_MAX_BYTES = 2048;
15
+ import { escapeXML } from "../truncate.js";
16
+ // ── Helpers ──────────────────────────────────────────────────────────────────
16
17
  const MAX_ACTIVE_FILES = 10;
17
- // Priority tier category groupings
18
- const P1_CATEGORIES = new Set(["file", "task", "rule"]);
19
- const P2_CATEGORIES = new Set(["cwd", "error", "decision", "env", "git"]);
20
- // P3-P4: everything else (subagent, skill, role, data, intent, mcp)
21
- // ── Section renderers ────────────────────────────────────────────────────────
22
18
  /**
23
- * Render <active_files> from file events.
24
- * Deduplicates by path, counts operations, keeps the last 10 files.
19
+ * Extract 2-4 keyword phrases from a list of strings for BM25 search queries.
20
+ * Takes actual data values and picks representative terms.
21
+ */
22
+ function buildQueries(items, maxQueries = 4) {
23
+ const unique = [...new Set(items.filter(s => s.length > 0))];
24
+ const selected = unique.slice(0, maxQueries);
25
+ return selected.map(s => {
26
+ // Take the first ~80 chars as a query — enough for BM25 matching
27
+ const trimmed = s.length > 80 ? s.slice(0, 80) : s;
28
+ return trimmed;
29
+ });
30
+ }
31
+ /**
32
+ * Format a runnable tool call block for a section.
25
33
  */
26
- export function renderActiveFiles(fileEvents) {
34
+ function toolCall(toolName, queries) {
35
+ if (queries.length === 0)
36
+ return "";
37
+ const escaped = queries.map(q => `"${escapeXML(q)}"`).join(", ");
38
+ return `\n For full details:\n ${escapeXML(toolName)}(\n queries: [${escaped}],\n source: "session-events"\n )`;
39
+ }
40
+ // ── Section builders ─────────────────────────────────────────────────────────
41
+ function buildFilesSection(fileEvents, searchTool) {
27
42
  if (fileEvents.length === 0)
28
43
  return "";
29
- // Build per-file operation counts and track last operation
44
+ // Build per-file operation counts
30
45
  const fileMap = new Map();
31
46
  for (const ev of fileEvents) {
32
47
  const path = ev.data;
33
48
  let entry = fileMap.get(path);
34
49
  if (!entry) {
35
- entry = { ops: new Map(), last: "" };
50
+ entry = { ops: new Map() };
36
51
  fileMap.set(path, entry);
37
52
  }
38
- // Derive operation from event type
39
53
  let op;
40
54
  if (ev.type === "file_write")
41
55
  op = "write";
@@ -46,19 +60,117 @@ export function renderActiveFiles(fileEvents) {
46
60
  else
47
61
  op = ev.type;
48
62
  entry.ops.set(op, (entry.ops.get(op) ?? 0) + 1);
49
- entry.last = op;
50
63
  }
51
64
  // Limit to last MAX_ACTIVE_FILES files (by insertion order = chronological)
52
65
  const entries = Array.from(fileMap.entries());
53
66
  const limited = entries.slice(-MAX_ACTIVE_FILES);
54
- const lines = [" <active_files>"];
55
- for (const [path, { ops, last }] of limited) {
67
+ const summaryLines = [];
68
+ const queryTerms = [];
69
+ for (const [path, { ops }] of limited) {
56
70
  const opsStr = Array.from(ops.entries())
57
- .map(([k, v]) => `${k}:${v}`)
58
- .join(",");
59
- lines.push(` <file path="${escapeXML(path)}" ops="${escapeXML(opsStr)}" last="${escapeXML(last)}" />`);
71
+ .map(([k, v]) => `${k}×${v}`)
72
+ .join(", ");
73
+ // Use just the filename for concise display
74
+ const fileName = path.split("/").pop() ?? path;
75
+ summaryLines.push(` ${escapeXML(fileName)} (${escapeXML(opsStr)})`);
76
+ queryTerms.push(`${fileName} ${Array.from(ops.keys()).join(" ")}`);
77
+ }
78
+ const queries = buildQueries(queryTerms);
79
+ const lines = [
80
+ ` <files count="${fileMap.size}">`,
81
+ ...summaryLines,
82
+ toolCall(searchTool, queries),
83
+ ` </files>`,
84
+ ];
85
+ return lines.join("\n");
86
+ }
87
+ function buildErrorsSection(errorEvents, searchTool) {
88
+ if (errorEvents.length === 0)
89
+ return "";
90
+ const summaryLines = [];
91
+ const queryTerms = [];
92
+ for (const ev of errorEvents) {
93
+ summaryLines.push(` ${escapeXML(ev.data)}`);
94
+ queryTerms.push(ev.data);
60
95
  }
61
- lines.push(" </active_files>");
96
+ const queries = buildQueries(queryTerms);
97
+ const lines = [
98
+ ` <errors count="${errorEvents.length}">`,
99
+ ...summaryLines,
100
+ toolCall(searchTool, queries),
101
+ ` </errors>`,
102
+ ];
103
+ return lines.join("\n");
104
+ }
105
+ function buildDecisionsSection(decisionEvents, searchTool) {
106
+ if (decisionEvents.length === 0)
107
+ return "";
108
+ const seen = new Set();
109
+ const summaryLines = [];
110
+ const queryTerms = [];
111
+ for (const ev of decisionEvents) {
112
+ if (seen.has(ev.data))
113
+ continue;
114
+ seen.add(ev.data);
115
+ summaryLines.push(` ${escapeXML(ev.data)}`);
116
+ queryTerms.push(ev.data);
117
+ }
118
+ if (summaryLines.length === 0)
119
+ return "";
120
+ const queries = buildQueries(queryTerms);
121
+ const lines = [
122
+ ` <decisions count="${summaryLines.length}">`,
123
+ ...summaryLines,
124
+ toolCall(searchTool, queries),
125
+ ` </decisions>`,
126
+ ];
127
+ return lines.join("\n");
128
+ }
129
+ function buildRulesSection(ruleEvents, searchTool) {
130
+ if (ruleEvents.length === 0)
131
+ return "";
132
+ const seen = new Set();
133
+ const summaryLines = [];
134
+ const queryTerms = [];
135
+ for (const ev of ruleEvents) {
136
+ if (seen.has(ev.data))
137
+ continue;
138
+ seen.add(ev.data);
139
+ if (ev.type === "rule_content") {
140
+ summaryLines.push(` ${escapeXML(ev.data)}`);
141
+ }
142
+ else {
143
+ summaryLines.push(` ${escapeXML(ev.data)}`);
144
+ }
145
+ queryTerms.push(ev.data);
146
+ }
147
+ if (summaryLines.length === 0)
148
+ return "";
149
+ const queries = buildQueries(queryTerms);
150
+ const lines = [
151
+ ` <rules count="${summaryLines.length}">`,
152
+ ...summaryLines,
153
+ toolCall(searchTool, queries),
154
+ ` </rules>`,
155
+ ];
156
+ return lines.join("\n");
157
+ }
158
+ function buildGitSection(gitEvents, searchTool) {
159
+ if (gitEvents.length === 0)
160
+ return "";
161
+ const summaryLines = [];
162
+ const queryTerms = [];
163
+ for (const ev of gitEvents) {
164
+ summaryLines.push(` ${escapeXML(ev.data)}`);
165
+ queryTerms.push(ev.data);
166
+ }
167
+ const queries = buildQueries(queryTerms);
168
+ const lines = [
169
+ ` <git count="${gitEvents.length}">`,
170
+ ...summaryLines,
171
+ toolCall(searchTool, queries),
172
+ ` </git>`,
173
+ ];
62
174
  return lines.join("\n");
63
175
  }
64
176
  /**
@@ -67,7 +179,7 @@ export function renderActiveFiles(fileEvents) {
67
179
  * filters out completed tasks, and renders only pending/in-progress work.
68
180
  *
69
181
  * TaskCreate events have `{ subject }`, TaskUpdate events have `{ taskId, status }`.
70
- * Match by chronological order: creates[0] lowest taskId from updates.
182
+ * Match by chronological order: creates[0] -> lowest taskId from updates.
71
183
  */
72
184
  export function renderTaskState(taskEvents) {
73
185
  if (taskEvents.length === 0)
@@ -89,7 +201,7 @@ export function renderTaskState(taskEvents) {
89
201
  if (creates.length === 0)
90
202
  return "";
91
203
  const DONE = new Set(["completed", "deleted", "failed"]);
92
- // Match creates to updates positionally (creates[0] lowest taskId)
204
+ // Match creates to updates positionally (creates[0] -> lowest taskId)
93
205
  const sortedIds = Object.keys(updates).sort((a, b) => Number(a) - Number(b));
94
206
  const pending = [];
95
207
  for (let i = 0; i < creates.length; i++) {
@@ -102,147 +214,123 @@ export function renderTaskState(taskEvents) {
102
214
  // All tasks completed — nothing to render
103
215
  if (pending.length === 0)
104
216
  return "";
105
- const lines = [" <task_state>"];
217
+ const lines = [];
106
218
  for (const task of pending) {
107
- lines.push(` - ${escapeXML(truncateString(task, 100))}`);
219
+ lines.push(` [pending] ${escapeXML(task)}`);
108
220
  }
109
- lines.push(" </task_state>");
110
221
  return lines.join("\n");
111
222
  }
112
- /**
113
- * Render <rules> from rule events.
114
- * Lists each unique rule source path + content summaries.
115
- */
116
- export function renderRules(ruleEvents) {
117
- if (ruleEvents.length === 0)
223
+ function buildTaskSection(taskEvents, searchTool) {
224
+ const taskContent = renderTaskState(taskEvents);
225
+ if (!taskContent)
118
226
  return "";
119
- const seen = new Set();
120
- const lines = [" <rules>"];
121
- for (const ev of ruleEvents) {
122
- const key = ev.data;
123
- if (seen.has(key))
124
- continue;
125
- seen.add(key);
126
- if (ev.type === "rule_content") {
127
- // Rule content: render as content block (survives compact)
128
- lines.push(` <rule_content>${escapeXML(truncateString(ev.data, 400))}</rule_content>`);
129
- }
130
- else {
131
- // Rule path
132
- lines.push(` - ${escapeXML(truncateString(ev.data, 200))}`);
227
+ const queryTerms = [];
228
+ for (const ev of taskEvents) {
229
+ try {
230
+ const parsed = JSON.parse(ev.data);
231
+ if (typeof parsed.subject === "string") {
232
+ queryTerms.push(parsed.subject);
233
+ }
133
234
  }
235
+ catch { /* not JSON */ }
134
236
  }
135
- lines.push(" </rules>");
136
- return lines.join("\n");
137
- }
138
- /**
139
- * Render <decisions> from decision events.
140
- */
141
- export function renderDecisions(decisionEvents) {
142
- if (decisionEvents.length === 0)
143
- return "";
144
- const seen = new Set();
145
- const lines = [" <decisions>"];
146
- for (const ev of decisionEvents) {
147
- const key = ev.data;
148
- if (seen.has(key))
149
- continue;
150
- seen.add(key);
151
- lines.push(` - ${escapeXML(truncateString(ev.data, 200))}`);
152
- }
153
- lines.push(" </decisions>");
237
+ const queries = buildQueries(queryTerms);
238
+ const pendingCount = taskContent.split("\n").length;
239
+ const lines = [
240
+ ` <task_state count="${pendingCount}">`,
241
+ taskContent,
242
+ toolCall(searchTool, queries),
243
+ ` </task_state>`,
244
+ ];
154
245
  return lines.join("\n");
155
246
  }
156
- /**
157
- * Render <environment> from cwd, env, and git events.
158
- */
159
- export function renderEnvironment(cwdEvent, envEvents, gitEvent) {
160
- const parts = [];
161
- if (!cwdEvent && envEvents.length === 0 && !gitEvent)
247
+ function buildEnvironmentSection(cwdEvents, envEvents, searchTool) {
248
+ if (cwdEvents.length === 0 && envEvents.length === 0)
162
249
  return "";
163
- parts.push(" <environment>");
164
- if (cwdEvent) {
165
- parts.push(` <cwd>${escapeXML(cwdEvent.data)}</cwd>`);
166
- }
167
- if (gitEvent) {
168
- // git event data is the operation type (branch, commit, push, etc.)
169
- parts.push(` <git op="${escapeXML(gitEvent.data)}" />`);
250
+ const summaryLines = [];
251
+ const queryTerms = [];
252
+ if (cwdEvents.length > 0) {
253
+ const lastCwd = cwdEvents[cwdEvents.length - 1];
254
+ summaryLines.push(` cwd: ${escapeXML(lastCwd.data)}`);
255
+ queryTerms.push("working directory");
170
256
  }
171
257
  for (const env of envEvents) {
172
- parts.push(` <env>${escapeXML(truncateString(env.data, 150))}</env>`);
258
+ summaryLines.push(` ${escapeXML(env.data)}`);
259
+ queryTerms.push(env.data);
173
260
  }
174
- parts.push(" </environment>");
175
- return parts.join("\n");
176
- }
177
- /**
178
- * Render <errors_encountered> from error events.
179
- */
180
- export function renderErrors(errorEvents) {
181
- if (errorEvents.length === 0)
182
- return "";
183
- const lines = [" <errors_encountered>"];
184
- for (const ev of errorEvents) {
185
- lines.push(` - ${escapeXML(truncateString(ev.data, 150))}`);
186
- }
187
- lines.push(" </errors_encountered>");
261
+ const queries = buildQueries(queryTerms);
262
+ const lines = [
263
+ ` <environment>`,
264
+ ...summaryLines,
265
+ toolCall(searchTool, queries),
266
+ ` </environment>`,
267
+ ];
188
268
  return lines.join("\n");
189
269
  }
190
- /**
191
- * Render <intent> from the most recent intent event.
192
- */
193
- export function renderIntent(intentEvent) {
194
- return ` <intent mode="${escapeXML(intentEvent.data)}">${escapeXML(truncateString(intentEvent.data, 100))}</intent>`;
195
- }
196
- /**
197
- * Render <subagents> from subagent events.
198
- * Shows agent dispatch status (launched/completed) and result summaries.
199
- */
200
- export function renderSubagents(subagentEvents) {
270
+ function buildSubagentsSection(subagentEvents, searchTool) {
201
271
  if (subagentEvents.length === 0)
202
272
  return "";
203
- const lines = [" <subagents>"];
273
+ const summaryLines = [];
274
+ const queryTerms = [];
204
275
  for (const ev of subagentEvents) {
205
276
  const status = ev.type === "subagent_completed" ? "completed"
206
277
  : ev.type === "subagent_launched" ? "launched"
207
278
  : "unknown";
208
- lines.push(` <agent status="${status}">${escapeXML(truncateString(ev.data, 200))}</agent>`);
279
+ summaryLines.push(` [${status}] ${escapeXML(ev.data)}`);
280
+ queryTerms.push(`subagent ${ev.data}`);
209
281
  }
210
- lines.push(" </subagents>");
282
+ const queries = buildQueries(queryTerms);
283
+ const lines = [
284
+ ` <subagents count="${subagentEvents.length}">`,
285
+ ...summaryLines,
286
+ toolCall(searchTool, queries),
287
+ ` </subagents>`,
288
+ ];
211
289
  return lines.join("\n");
212
290
  }
213
- /**
214
- * Render <mcp_tools> from MCP tool call events.
215
- * Deduplicates by tool name, shows usage count.
216
- */
217
- export function renderMcpTools(mcpEvents) {
218
- if (mcpEvents.length === 0)
291
+ function buildSkillsSection(skillEvents, searchTool) {
292
+ if (skillEvents.length === 0)
219
293
  return "";
220
- // Count usage per tool
221
- const toolCounts = new Map();
222
- for (const ev of mcpEvents) {
223
- const tool = ev.data.split(":")[0].trim();
224
- toolCounts.set(tool, (toolCounts.get(tool) ?? 0) + 1);
294
+ // Count invocations per skill name
295
+ const skillCounts = new Map();
296
+ for (const ev of skillEvents) {
297
+ const name = ev.data.split(":")[0].trim();
298
+ skillCounts.set(name, (skillCounts.get(name) ?? 0) + 1);
225
299
  }
226
- const lines = [" <mcp_tools>"];
227
- for (const [tool, count] of toolCounts) {
228
- lines.push(` <tool name="${escapeXML(tool)}" calls="${count}" />`);
300
+ const summaryLines = [];
301
+ const queryTerms = [];
302
+ for (const [name, count] of skillCounts) {
303
+ summaryLines.push(` ${escapeXML(name)} (${count}×)`);
304
+ queryTerms.push(`skill ${name} invocation`);
229
305
  }
230
- lines.push(" </mcp_tools>");
306
+ const queries = buildQueries(queryTerms);
307
+ const lines = [
308
+ ` <skills count="${skillEvents.length}">`,
309
+ ...summaryLines,
310
+ toolCall(searchTool, queries),
311
+ ` </skills>`,
312
+ ];
231
313
  return lines.join("\n");
232
314
  }
315
+ function buildIntentSection(intentEvents) {
316
+ if (intentEvents.length === 0)
317
+ return "";
318
+ const lastIntent = intentEvents[intentEvents.length - 1];
319
+ return ` <intent mode="${escapeXML(lastIntent.data)}"/>`;
320
+ }
233
321
  // ── Main builder ─────────────────────────────────────────────────────────────
234
322
  /**
235
- * Build a resume snapshot XML string from stored session events.
323
+ * Build a reference-based resume snapshot XML string from stored session events.
236
324
  *
237
325
  * Algorithm:
238
326
  * 1. Group events by category
239
- * 2. Render each section
240
- * 3. Assemble by priority tier with budget trimming
241
- * 4. If over maxBytes, drop lowest priority sections first
327
+ * 2. For each non-empty category, build a summary section with a runnable
328
+ * search tool call containing exact queries for full details
329
+ * 3. Assemble ALL non-empty sections no priority dropping, no byte budget
242
330
  */
243
331
  export function buildResumeSnapshot(events, opts) {
244
- const maxBytes = opts?.maxBytes ?? DEFAULT_MAX_BYTES;
245
332
  const compactCount = opts?.compactCount ?? 1;
333
+ const searchTool = opts?.searchTool ?? "ctx_search";
246
334
  const now = new Date().toISOString();
247
335
  // ── Group events by category ──
248
336
  const fileEvents = [];
@@ -255,8 +343,7 @@ export function buildResumeSnapshot(events, opts) {
255
343
  const gitEvents = [];
256
344
  const subagentEvents = [];
257
345
  const intentEvents = [];
258
- const mcpEvents = [];
259
- const planEvents = [];
346
+ const skillEvents = [];
260
347
  for (const ev of events) {
261
348
  switch (ev.category) {
262
349
  case "file":
@@ -289,84 +376,56 @@ export function buildResumeSnapshot(events, opts) {
289
376
  case "intent":
290
377
  intentEvents.push(ev);
291
378
  break;
292
- case "mcp":
293
- mcpEvents.push(ev);
294
- break;
295
- case "plan":
296
- planEvents.push(ev);
379
+ case "skill":
380
+ skillEvents.push(ev);
297
381
  break;
298
382
  }
299
383
  }
300
- // ── Render sections by priority tier ──
301
- // P1 sections (50% budget): active_files, task_state, rules
302
- const p1Sections = [];
303
- const activeFiles = renderActiveFiles(fileEvents);
304
- if (activeFiles)
305
- p1Sections.push(activeFiles);
306
- const taskState = renderTaskState(taskEvents);
307
- if (taskState)
308
- p1Sections.push(taskState);
309
- const rules = renderRules(ruleEvents);
310
- if (rules)
311
- p1Sections.push(rules);
312
- // P2 sections (35% budget): decisions, environment, errors_encountered, completed subagents
313
- const p2Sections = [];
314
- const decisions = renderDecisions(decisionEvents);
384
+ // ── Build all sections ──
385
+ const sections = [];
386
+ // How-to-search instruction block (always present)
387
+ sections.push(` <how_to_search>
388
+ Each section below contains a summary of prior work.
389
+ For FULL DETAILS, run the exact tool call shown under each section.
390
+ Do NOT ask the user to re-explain prior work. Search first.
391
+ Do NOT invent your own queries — use the ones provided.
392
+ </how_to_search>`);
393
+ const files = buildFilesSection(fileEvents, searchTool);
394
+ if (files)
395
+ sections.push(files);
396
+ const errors = buildErrorsSection(errorEvents, searchTool);
397
+ if (errors)
398
+ sections.push(errors);
399
+ const decisions = buildDecisionsSection(decisionEvents, searchTool);
315
400
  if (decisions)
316
- p2Sections.push(decisions);
317
- const lastCwd = cwdEvents.length > 0 ? cwdEvents[cwdEvents.length - 1] : undefined;
318
- const lastGit = gitEvents.length > 0 ? gitEvents[gitEvents.length - 1] : undefined;
319
- const environment = renderEnvironment(lastCwd, envEvents, lastGit);
401
+ sections.push(decisions);
402
+ const rules = buildRulesSection(ruleEvents, searchTool);
403
+ if (rules)
404
+ sections.push(rules);
405
+ const git = buildGitSection(gitEvents, searchTool);
406
+ if (git)
407
+ sections.push(git);
408
+ const tasks = buildTaskSection(taskEvents, searchTool);
409
+ if (tasks)
410
+ sections.push(tasks);
411
+ const environment = buildEnvironmentSection(cwdEvents, envEvents, searchTool);
320
412
  if (environment)
321
- p2Sections.push(environment);
322
- const errors = renderErrors(errorEvents);
323
- if (errors)
324
- p2Sections.push(errors);
325
- // Completed subagents are P2 — their results must survive budget trimming
326
- const completedSubagents = subagentEvents.filter(e => e.type === "subagent_completed");
327
- const subagentsP2 = renderSubagents(completedSubagents);
328
- if (subagentsP2)
329
- p2Sections.push(subagentsP2);
330
- // Plan mode state — show if plan is active (last event is plan_enter)
331
- if (planEvents.length > 0) {
332
- const lastPlan = planEvents[planEvents.length - 1];
333
- if (lastPlan.type === "plan_enter") {
334
- p2Sections.push(` <plan_mode status="active" />`);
335
- }
336
- }
337
- // P3-P4 sections (15% budget): intent, mcp_tools, launched subagents
338
- const p3Sections = [];
339
- if (intentEvents.length > 0) {
340
- const lastIntent = intentEvents[intentEvents.length - 1];
341
- p3Sections.push(renderIntent(lastIntent));
342
- }
343
- const mcpTools = renderMcpTools(mcpEvents);
344
- if (mcpTools)
345
- p3Sections.push(mcpTools);
346
- const launchedSubagents = subagentEvents.filter(e => e.type === "subagent_launched");
347
- const subagentsP3 = renderSubagents(launchedSubagents);
348
- if (subagentsP3)
349
- p3Sections.push(subagentsP3);
350
- // ── Assemble with budget trimming ──
351
- const header = `<session_resume compact_count="${compactCount}" events_captured="${events.length}" generated_at="${now}">`;
413
+ sections.push(environment);
414
+ const subagents = buildSubagentsSection(subagentEvents, searchTool);
415
+ if (subagents)
416
+ sections.push(subagents);
417
+ const skills = buildSkillsSection(skillEvents, searchTool);
418
+ if (skills)
419
+ sections.push(skills);
420
+ const intent = buildIntentSection(intentEvents);
421
+ if (intent)
422
+ sections.push(intent);
423
+ // ── Assemble ──
424
+ const header = `<session_resume events="${events.length}" compact_count="${compactCount}" generated_at="${now}">`;
352
425
  const footer = `</session_resume>`;
353
- // Try assembling all tiers, drop lowest priority first if over budget
354
- const tiers = [p1Sections, p2Sections, p3Sections];
355
- // Start with all tiers and progressively drop from the back
356
- for (let dropFrom = tiers.length; dropFrom >= 0; dropFrom--) {
357
- const activeTiers = tiers.slice(0, dropFrom);
358
- const body = activeTiers.flat().join("\n");
359
- let xml;
360
- if (body) {
361
- xml = `${header}\n${body}\n${footer}`;
362
- }
363
- else {
364
- xml = `${header}\n${footer}`;
365
- }
366
- if (Buffer.byteLength(xml) <= maxBytes) {
367
- return xml;
368
- }
426
+ const body = sections.join("\n\n");
427
+ if (body) {
428
+ return `${header}\n\n${body}\n\n${footer}`;
369
429
  }
370
- // If even header+footer is over budget, return the minimal XML
371
430
  return `${header}\n${footer}`;
372
431
  }
@@ -5,16 +5,6 @@
5
5
  * SessionDB (snapshot building). They are extracted here so any
6
6
  * consumer can import them without pulling in the full store or executor.
7
7
  */
8
- /**
9
- * Truncate a string to at most `maxChars` characters, appending an ellipsis
10
- * when truncation occurs.
11
- *
12
- * @param str - Input string.
13
- * @param maxChars - Maximum character count (inclusive). Must be >= 3.
14
- * @returns The original string if short enough, otherwise a truncated string
15
- * ending with "...".
16
- */
17
- export declare function truncateString(str: string, maxChars: number): string;
18
8
  /**
19
9
  * Serialize a value to JSON, then truncate the result to `maxBytes` bytes.
20
10
  * If truncation occurs, the string is cut at a UTF-8-safe boundary and
package/build/truncate.js CHANGED
@@ -6,23 +6,6 @@
6
6
  * consumer can import them without pulling in the full store or executor.
7
7
  */
8
8
  // ─────────────────────────────────────────────────────────
9
- // String truncation
10
- // ─────────────────────────────────────────────────────────
11
- /**
12
- * Truncate a string to at most `maxChars` characters, appending an ellipsis
13
- * when truncation occurs.
14
- *
15
- * @param str - Input string.
16
- * @param maxChars - Maximum character count (inclusive). Must be >= 3.
17
- * @returns The original string if short enough, otherwise a truncated string
18
- * ending with "...".
19
- */
20
- export function truncateString(str, maxChars) {
21
- if (str.length <= maxChars)
22
- return str;
23
- return str.slice(0, Math.max(0, maxChars - 3)) + "...";
24
- }
25
- // ─────────────────────────────────────────────────────────
26
9
  // JSON truncation
27
10
  // ─────────────────────────────────────────────────────────
28
11
  /**