@aragon-agent/cli 0.6.14 → 0.6.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +295 -238
  3. package/dist/agent/controller.d.ts +37 -1
  4. package/dist/agent/controller.js +195 -45
  5. package/dist/agent/plan-prompt.d.ts +1 -1
  6. package/dist/agent/plan-prompt.js +4 -3
  7. package/dist/agent/project-guidance-prompt.d.ts +4 -0
  8. package/dist/agent/project-guidance-prompt.js +104 -0
  9. package/dist/agent/reducer.d.ts +2 -0
  10. package/dist/agent/reducer.js +4 -0
  11. package/dist/agent/system-prompt.js +4 -0
  12. package/dist/cli.js +49 -2
  13. package/dist/commands/builtins.js +26 -28
  14. package/dist/compaction/child.d.ts +9 -1
  15. package/dist/compaction/child.js +39 -13
  16. package/dist/compaction/command.js +20 -10
  17. package/dist/compaction/compactor.d.ts +33 -204
  18. package/dist/compaction/compactor.js +311 -726
  19. package/dist/compaction/context-command.js +14 -3
  20. package/dist/compaction/digest.d.ts +15 -51
  21. package/dist/compaction/digest.js +113 -148
  22. package/dist/compaction/limits.d.ts +19 -142
  23. package/dist/compaction/limits.js +20 -147
  24. package/dist/compaction/memory-identity.d.ts +16 -0
  25. package/dist/compaction/memory-identity.js +63 -0
  26. package/dist/compaction/memory-input.d.ts +25 -0
  27. package/dist/compaction/memory-input.js +106 -0
  28. package/dist/compaction/memory-render.d.ts +4 -0
  29. package/dist/compaction/memory-render.js +47 -0
  30. package/dist/compaction/memory-validation.d.ts +21 -0
  31. package/dist/compaction/memory-validation.js +268 -0
  32. package/dist/compaction/memory.d.ts +79 -0
  33. package/dist/compaction/memory.js +118 -0
  34. package/dist/compaction/meter.d.ts +14 -10
  35. package/dist/compaction/meter.js +53 -19
  36. package/dist/compaction/operation.d.ts +18 -0
  37. package/dist/compaction/operation.js +68 -0
  38. package/dist/compaction/pressure.d.ts +5 -20
  39. package/dist/compaction/pressure.js +13 -23
  40. package/dist/compaction/summarize-call.d.ts +3 -16
  41. package/dist/compaction/summarize-call.js +20 -20
  42. package/dist/compaction/summary-prompt.d.ts +29 -66
  43. package/dist/compaction/summary-prompt.js +132 -178
  44. package/dist/compaction/types.d.ts +27 -4
  45. package/dist/compaction/wiring.d.ts +10 -25
  46. package/dist/compaction/wiring.js +56 -66
  47. package/dist/config/app-paths.d.ts +10 -0
  48. package/dist/config/app-paths.js +12 -0
  49. package/dist/config/env.d.ts +2 -1
  50. package/dist/config/env.js +13 -1
  51. package/dist/config/load.d.ts +9 -0
  52. package/dist/config/load.js +14 -0
  53. package/dist/config/model-windows.d.ts +91 -0
  54. package/dist/config/model-windows.js +181 -0
  55. package/dist/config/schema.d.ts +18 -1
  56. package/dist/config/schema.js +2 -1
  57. package/dist/exec/events.d.ts +8 -0
  58. package/dist/exec/index.js +13 -6
  59. package/dist/exec/runner.d.ts +8 -0
  60. package/dist/exec/runner.js +26 -2
  61. package/dist/input/csiu-keys.d.ts +66 -0
  62. package/dist/input/csiu-keys.js +170 -0
  63. package/dist/input/keyboard-enhancement.d.ts +42 -0
  64. package/dist/input/keyboard-enhancement.js +65 -0
  65. package/dist/input/stdin-filter.d.ts +9 -0
  66. package/dist/input/stdin-filter.js +45 -9
  67. package/dist/input/win32-input-mode.d.ts +61 -0
  68. package/dist/input/win32-input-mode.js +251 -0
  69. package/dist/session/persist.d.ts +3 -0
  70. package/dist/session/persist.js +9 -0
  71. package/dist/session/store.js +2 -0
  72. package/dist/session/validate-session.js +34 -0
  73. package/dist/team/subagent.js +8 -0
  74. package/dist/ui/App.js +218 -116
  75. package/dist/ui/BottomStatusRow.d.ts +5 -10
  76. package/dist/ui/BottomStatusRow.js +7 -11
  77. package/dist/ui/Composer.d.ts +4 -2
  78. package/dist/ui/Composer.js +2 -2
  79. package/dist/ui/Header.d.ts +6 -2
  80. package/dist/ui/Header.js +7 -6
  81. package/dist/ui/PromptInput.d.ts +3 -1
  82. package/dist/ui/PromptInput.js +32 -41
  83. package/dist/ui/StatusBar.d.ts +6 -0
  84. package/dist/ui/StatusBar.js +8 -7
  85. package/dist/ui/Transcript.js +1 -1
  86. package/dist/ui/entries/CompactionCard.d.ts +3 -1
  87. package/dist/ui/entries/CompactionCard.js +7 -5
  88. package/dist/ui/frame-differ.js +27 -0
  89. package/dist/ui/frame-observer.js +18 -5
  90. package/dist/ui/interaction-copy.d.ts +73 -73
  91. package/dist/ui/interaction-copy.js +30 -38
  92. package/dist/ui/interaction-hints.d.ts +8 -1
  93. package/dist/ui/interaction-hints.js +56 -69
  94. package/dist/ui/layout/AppShell.d.ts +6 -2
  95. package/dist/ui/layout/AppShell.js +2 -2
  96. package/dist/ui/layout/OverlayFrame.d.ts +2 -0
  97. package/dist/ui/layout/OverlayFrame.js +23 -14
  98. package/dist/ui/layout/ScrollViewport.d.ts +3 -1
  99. package/dist/ui/layout/ScrollViewport.js +10 -2
  100. package/dist/ui/layout/budget.d.ts +25 -26
  101. package/dist/ui/layout/budget.js +29 -22
  102. package/dist/ui/layout/document-layout.d.ts +2 -2
  103. package/dist/ui/layout/document-layout.js +2 -3
  104. package/dist/ui/layout/queue-layout.js +4 -4
  105. package/dist/ui/layout/status-detail-layout.d.ts +9 -0
  106. package/dist/ui/layout/status-detail-layout.js +56 -0
  107. package/dist/ui/layout/status-layout.d.ts +19 -2
  108. package/dist/ui/layout/status-layout.js +95 -84
  109. package/dist/ui/layout/wrap-rows.d.ts +2 -0
  110. package/dist/ui/layout/wrap-rows.js +21 -7
  111. package/dist/ui/overlays/ConfirmDialog.d.ts +2 -1
  112. package/dist/ui/overlays/ConfirmDialog.js +22 -3
  113. package/dist/ui/overlays/HelpOverlay.js +9 -2
  114. package/dist/ui/overlays/ModelPicker.d.ts +5 -8
  115. package/dist/ui/overlays/ModelPicker.js +44 -10
  116. package/dist/ui/overlays/ModelProfileEditor.js +2 -0
  117. package/dist/ui/overlays/ModelProfilePicker.d.ts +1 -0
  118. package/dist/ui/overlays/ModelProfilePicker.js +4 -2
  119. package/dist/ui/overlays/PlanReviewOverlay.d.ts +2 -1
  120. package/dist/ui/overlays/PlanReviewOverlay.js +7 -4
  121. package/dist/ui/overlays/QuestionOverlay.d.ts +2 -1
  122. package/dist/ui/overlays/QuestionOverlay.js +45 -32
  123. package/dist/ui/overlays/QueueOverlay.js +2 -2
  124. package/dist/ui/overlays/SettingsScreen.d.ts +1 -0
  125. package/dist/ui/overlays/SettingsScreen.js +10 -3
  126. package/dist/ui/run-metrics.d.ts +23 -0
  127. package/dist/ui/run-metrics.js +27 -0
  128. package/dist/ui/status-feedback.d.ts +33 -0
  129. package/dist/ui/status-feedback.js +36 -0
  130. package/dist/ui/stdout-frame-writer.js +28 -0
  131. package/package.json +2 -2
@@ -74,7 +74,9 @@ function occupancyLine(usage) {
74
74
  return `${pct} ${formatTokens(usage.occupied)} of ${formatTokens(usage.window)} ${breakdown}`;
75
75
  }
76
76
  /**
77
- * Where the denominator came from. THREE SOURCES, ALL NAMED.
77
+ * Where the denominator came from. FOUR SOURCES, ALL NAMED - the fourth
78
+ * being the user's own model-windows.json, which exists precisely because
79
+ * the other three cannot know a gateway alias released last week.
78
80
  *
79
81
  * THE PLACEHOLDER BRANCH IS THE ONE THAT MATTERS. `buildRuntimeModel` returns a
80
82
  * flat 128000 for any model the static table has never seen - a custom
@@ -85,11 +87,20 @@ function occupancyLine(usage) {
85
87
  */
86
88
  function windowLine(usage) {
87
89
  if (usage.windowOverridden) {
88
- return `${usage.window} from contextWindow configuration (overrides API and model table)`;
90
+ return `${usage.window} from contextWindow configuration (overrides API, model table, and model-windows.json)`;
89
91
  }
90
92
  if (!usage.windowKnown) {
91
93
  return (`UNKNOWN - no verified model limit; internal fallback budget ${usage.window}; ` +
92
- 'set contextWindow to correct it');
94
+ 'set contextWindow or list the model in model-windows.json to correct it');
95
+ }
96
+ if (usage.windowSource === 'user') {
97
+ return `${usage.window} from model-windows.json (your per-model table)`;
98
+ }
99
+ if (usage.windowSource === undefined) {
100
+ // A child agent can be windowKnown through the pricing-table heuristic
101
+ // without naming a source; claiming 'the model table' there would be an
102
+ // evidence-free assertion in the one report that exists to be checked.
103
+ return `${usage.window} source unverified`;
93
104
  }
94
105
  return `${usage.window} from ${usage.windowSource === 'api' ? 'the model API' : 'the model table'}`;
95
106
  }
@@ -1,62 +1,26 @@
1
- /**
2
- * The head of the history, rendered as a bounded plain-text transcript
3
- * (context-auto-compaction §3.6.3).
4
- *
5
- * ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope.
6
- *
7
- * PURE. Two reasons this is a digest rather than raw messages, and the second
8
- * one is the one that bites:
9
- *
10
- * - Tool results are the bulk. Four 60 KB file reads are 240 KB of body that
11
- * says almost nothing about what was decided.
12
- * - THE SUMMARIZER MAY HAVE A SMALLER CONTEXT WINDOW THAN THE MODEL BEING
13
- * COMPACTED. The whole point of using a cheap model is that it is cheap, and
14
- * cheap models are exactly the ones with 32 k windows. Sending 180 k of
15
- * history to summarize would fail with the same error we are recovering from
16
- * (R-4).
17
- */
18
- import { type Message } from '@aragon-agent/core';
19
- /**
20
- * Is this `user` message a previous `<compacted_context>` block?
21
- *
22
- * Recognized on the OPENING TAG rather than a full parse: the block carries
23
- * attributes (`version`, `replaced`, `turns`, `anchor`, `generation`) that this
24
- * module has no business knowing about, and a stricter match would fail on the
25
- * first attribute anybody adds.
26
- */
27
- export declare function isPriorSummaryBlock(message: Message): boolean;
1
+ /** Complete transcript input with bounded tool-only clipping and trusted sources. */
2
+ import { type Message, type UserMessage } from '@aragon-agent/core';
3
+ import { type CompactionMemory, type MemoryCoverage, type MemorySource } from './memory.js';
28
4
  export interface DigestInput {
29
- /** The head — `messages[protectedPrefix..cutIndex)`. */
30
5
  head: readonly Message[];
31
- /**
32
- * The summarizer's own context window, or `undefined` when it is unknown.
33
- *
34
- * NOT the lead model's. This is the whole reason a digest exists rather than
35
- * raw messages (R-4).
36
- */
6
+ /** Retained original task is context only, never current acceptance evidence. */
7
+ anchor?: UserMessage;
37
8
  summarizerWindow?: number;
9
+ generation?: number;
10
+ startIndex?: number;
11
+ priorMemory?: CompactionMemory;
12
+ legacySummary?: string;
38
13
  }
39
14
  export interface DigestResult {
40
15
  text: string;
41
- /** Entries the budget forced out. `> 0` means the marker is present. */
42
16
  omitted: number;
43
- /** The budget actually used, after the window derivation. */
44
17
  budgetChars: number;
18
+ sourceMap: Map<string, MemorySource>;
19
+ coverage: MemoryCoverage;
45
20
  }
46
- /**
47
- * The character budget for one digest.
48
- *
49
- * `min(digestMaxChars, (summarizerWindow - summarizerReserveTokens) * CHARS_PER_TOKEN)`.
50
- * `CHARS_PER_TOKEN` comes from core (P2-3) rather than being re-spelled here: two
51
- * chars-per-token constants that must agree and nothing checking that they do is
52
- * how a digest budget and an occupancy estimate drift apart.
53
- */
21
+ /** Text alone cannot identify a host summary; use verified identity at the caller. */
22
+ export declare function isPriorSummaryBlock(_message: Message): boolean;
23
+ /** Derive the digest ceiling; known windows below reserve have no input room. */
54
24
  export declare function digestBudgetChars(summarizerWindow?: number): number;
55
- /**
56
- * Render the head into a bounded transcript.
57
- *
58
- * OLDEST ENTRIES ARE DROPPED FIRST, except the pinned `PRIOR SUMMARY`, which is
59
- * evicted last of all. A single omission marker is prepended so the summary can
60
- * say that it is incomplete instead of pretending.
61
- */
25
+ /** Build every head entry without mutating messages; rejects instead of omitting. */
62
26
  export declare function buildDigest(input: DigestInput): DigestResult;
@@ -1,164 +1,129 @@
1
- /**
2
- * The head of the history, rendered as a bounded plain-text transcript
3
- * (context-auto-compaction §3.6.3).
4
- *
5
- * ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope.
6
- *
7
- * PURE. Two reasons this is a digest rather than raw messages, and the second
8
- * one is the one that bites:
9
- *
10
- * - Tool results are the bulk. Four 60 KB file reads are 240 KB of body that
11
- * says almost nothing about what was decided.
12
- * - THE SUMMARIZER MAY HAVE A SMALLER CONTEXT WINDOW THAN THE MODEL BEING
13
- * COMPACTED. The whole point of using a cheap model is that it is cheap, and
14
- * cheap models are exactly the ones with 32 k windows. Sending 180 k of
15
- * history to summarize would fail with the same error we are recovering from
16
- * (R-4).
17
- */
1
+ /** Complete transcript input with bounded tool-only clipping and trusted sources. */
18
2
  import { CHARS_PER_TOKEN } from '@aragon-agent/core';
19
- import { BLOCK_TAG_CLOSE, BLOCK_TAG_OPEN, COMPACTION_LIMITS } from './limits.js';
20
- /** Prepended when the budget forced entries out, so the summary can say so. */
21
- const OMISSION_MARKER = (n) => `[... ${n} earlier ${n === 1 ? 'message' : 'messages'} omitted from this digest ...]`;
22
- function clip(text, max) {
23
- if (text.length <= max)
24
- return text;
25
- return `${text.slice(0, max)}...`;
3
+ import { COMPACTION_LIMITS as LIMITS } from './limits.js';
4
+ import { isLegacyMemoryText, MemoryError, serializeMemory, } from './memory.js';
5
+ /** Text alone cannot identify a host summary; use verified identity at the caller. */
6
+ export function isPriorSummaryBlock(_message) {
7
+ return false;
26
8
  }
27
- /** Flatten `string | ContentPart[]` into text, naming images rather than dropping them. */
28
- function partsToText(content) {
29
- if (typeof content === 'string')
30
- return content;
31
- if (!Array.isArray(content))
32
- return '';
33
- return content
34
- .map((part) => (typeof part?.text === 'string' ? part.text : '[image]'))
35
- .join('\n');
36
- }
37
- /**
38
- * Is this `user` message a previous `<compacted_context>` block?
39
- *
40
- * Recognized on the OPENING TAG rather than a full parse: the block carries
41
- * attributes (`version`, `replaced`, `turns`, `anchor`, `generation`) that this
42
- * module has no business knowing about, and a stricter match would fail on the
43
- * first attribute anybody adds.
44
- */
45
- export function isPriorSummaryBlock(message) {
46
- if (message.role !== 'user')
47
- return false;
48
- const text = partsToText(message.content).trimStart();
49
- return text.startsWith(BLOCK_TAG_OPEN);
9
+ /** Derive the digest ceiling; known windows below reserve have no input room. */
10
+ export function digestBudgetChars(summarizerWindow) {
11
+ if (summarizerWindow === undefined ||
12
+ !Number.isFinite(summarizerWindow) ||
13
+ summarizerWindow <= 0) {
14
+ return LIMITS.digestMaxChars;
15
+ }
16
+ return Math.min(LIMITS.digestMaxChars, Math.max(0, (summarizerWindow - LIMITS.summarizerReserveTokens) * CHARS_PER_TOKEN));
50
17
  }
51
- /** The block's body with its framing removed, for re-rendering as PRIOR SUMMARY. */
52
- function stripBlockTags(text) {
53
- const trimmed = text.trim();
54
- const open = trimmed.indexOf('>');
55
- const close = trimmed.lastIndexOf(BLOCK_TAG_CLOSE);
56
- if (open < 0)
57
- return trimmed;
58
- const body = close > open ? trimmed.slice(open + 1, close) : trimmed.slice(open + 1);
59
- return body.trim();
18
+ function contentText(content) {
19
+ if (typeof content === 'string')
20
+ return { text: content, visible: [content] };
21
+ return {
22
+ text: content
23
+ .map((part) => part.type === 'text' ? part.text : '[image unavailable to summarizer]')
24
+ .join('\n'),
25
+ visible: content.flatMap((part) => part.type === 'text' ? [part.text] : []),
26
+ };
60
27
  }
61
- function renderMessage(message) {
62
- if (message.role === 'user') {
63
- if (isPriorSummaryBlock(message)) {
64
- // CARRIED WHOLE, up to `summaryMaxChars` (D-22 / P1-9).
65
- //
66
- // A `<compacted_context>` block IS a `user` message (D-5, deliberately),
67
- // so a per-kind table that clipped it to `userChars` (2 000) would discard
68
- // up to two thirds of the previous summary - which §5.4 permits to be
69
- // 6 000 long - BEFORE summarizing it, and then report `summarized`. Every
70
- // compaction after the first would decay the record silently.
71
- const body = stripBlockTags(partsToText(message.content));
72
- return [{ text: `PRIOR SUMMARY: ${clip(body, COMPACTION_LIMITS.summaryMaxChars)}`, pinned: true }];
73
- }
74
- return [
75
- { text: `USER: ${clip(partsToText(message.content), COMPACTION_LIMITS.userChars)}`, pinned: false },
76
- ];
77
- }
78
- if (message.role === 'tool_result') {
79
- const status = message.isError ? 'error' : 'ok';
80
- const body = clip(partsToText(message.content), COMPACTION_LIMITS.toolResultChars);
81
- return [{ text: `RESULT[${status}] ${body}`, pinned: false }];
82
- }
83
- // Assistant: one entry per block, so a turn's text and its calls can be
84
- // evicted independently.
85
- const out = [];
28
+ function renderAssistant(message, calls) {
29
+ const text = [];
30
+ const visible = [];
86
31
  for (const block of message.content) {
87
32
  if (block.type === 'text') {
88
- if (block.text.trim().length === 0)
89
- continue;
90
- out.push({ text: `ASSISTANT: ${clip(block.text, COMPACTION_LIMITS.assistantChars)}`, pinned: false });
33
+ text.push(block.text);
34
+ visible.push(block.text);
91
35
  }
92
- else if (block.type === 'tool_call') {
93
- const args = clip(JSON.stringify(block.args ?? {}), COMPACTION_LIMITS.toolArgChars);
94
- out.push({ text: `CALL ${block.toolName}(${args})`, pinned: false });
36
+ if (block.type === 'tool_call') {
37
+ const args = JSON.stringify(block.args);
38
+ text.push(`CALL ${block.toolName} toolCallId=${block.toolCallId}\n${args}`);
39
+ visible.push(args);
40
+ calls.set(block.toolCallId, block.toolName);
95
41
  }
96
- // `thinking` blocks are DROPPED ENTIRELY (D-8): they are the model's private
97
- // scratch, they are the single largest contributor at `thinkingLevel: high`,
98
- // and nothing after the turn depends on them.
99
42
  }
100
- return out;
43
+ return { text: text.join('\n'), visible };
101
44
  }
102
- /**
103
- * The character budget for one digest.
104
- *
105
- * `min(digestMaxChars, (summarizerWindow - summarizerReserveTokens) * CHARS_PER_TOKEN)`.
106
- * `CHARS_PER_TOKEN` comes from core (P2-3) rather than being re-spelled here: two
107
- * chars-per-token constants that must agree and nothing checking that they do is
108
- * how a digest budget and an occupancy estimate drift apart.
109
- */
110
- export function digestBudgetChars(summarizerWindow) {
111
- const ceiling = COMPACTION_LIMITS.digestMaxChars;
112
- if (!summarizerWindow || summarizerWindow <= 0)
113
- return ceiling;
114
- const usableTokens = summarizerWindow - COMPACTION_LIMITS.summarizerReserveTokens;
115
- if (usableTokens <= 0)
116
- return ceiling;
117
- return Math.min(ceiling, usableTokens * CHARS_PER_TOKEN);
45
+ function renderEntry(message, messageId, calls, result) {
46
+ const rendered = message.role === 'assistant'
47
+ ? renderAssistant(message, calls)
48
+ : contentText(message.content);
49
+ const source = { role: message.role, visibleText: rendered.visible };
50
+ let heading = `[${messageId}] ${message.role}`;
51
+ if (isLegacyMemoryText(message)) {
52
+ source.isLegacyReference = true;
53
+ heading += ' (legacy read-only reference, not current user acceptance)';
54
+ }
55
+ if (message.role === 'tool_result') {
56
+ source.toolCallId = message.toolCallId;
57
+ source.toolName = calls.get(message.toolCallId);
58
+ calls.delete(message.toolCallId);
59
+ source.isError = message.isError;
60
+ heading += ` tool=${source.toolName ?? 'unknown'} toolCallId=${message.toolCallId}`
61
+ + ` isError=${message.isError === true}`;
62
+ const edge = LIMITS.toolResultEdgeChars;
63
+ if (rendered.text.length > 2 * edge) {
64
+ const omittedChars = rendered.text.length - 2 * edge;
65
+ const first = rendered.text.slice(0, edge);
66
+ const last = rendered.text.slice(-edge);
67
+ source.visibleText = clippedVisibleSections(rendered.visible, rendered.text, edge);
68
+ rendered.text = `${first}\n[clipped originalChars=${rendered.text.length}`
69
+ + ` omittedChars=${omittedChars}]\n${last}`;
70
+ result.coverage.clippedToolResults.push({ messageId, omittedChars });
71
+ }
72
+ }
73
+ result.sourceMap.set(messageId, source);
74
+ if (message.role !== 'user')
75
+ result.coverage.summarizedMessages += 1;
76
+ return `${heading}\n${rendered.text}`;
77
+ }
78
+ function clippedVisibleSections(parts, rendered, edge) {
79
+ const visible = [];
80
+ let from = 0;
81
+ for (const part of parts) {
82
+ const start = rendered.indexOf(part, from);
83
+ const end = start + part.length;
84
+ if (start < edge)
85
+ visible.push(part.slice(0, Math.max(0, edge - start)));
86
+ if (end > rendered.length - edge) {
87
+ visible.push(part.slice(Math.max(0, rendered.length - edge - start)));
88
+ }
89
+ from = end;
90
+ }
91
+ return visible.filter(Boolean);
118
92
  }
119
- /**
120
- * Render the head into a bounded transcript.
121
- *
122
- * OLDEST ENTRIES ARE DROPPED FIRST, except the pinned `PRIOR SUMMARY`, which is
123
- * evicted last of all. A single omission marker is prepended so the summary can
124
- * say that it is incomplete instead of pretending.
125
- */
93
+ /** Build every head entry without mutating messages; rejects instead of omitting. */
126
94
  export function buildDigest(input) {
127
- const budgetChars = digestBudgetChars(input.summarizerWindow);
128
- const entries = input.head.flatMap(renderMessage);
129
- const sep = '\n';
130
- let total = entries.reduce((acc, e) => acc + e.text.length + sep.length, 0);
131
- if (total <= budgetChars) {
132
- return { text: entries.map((e) => e.text).join(sep), omitted: 0, budgetChars };
95
+ const generation = input.generation ?? 1;
96
+ const start = input.startIndex ?? 0;
97
+ if (!Number.isSafeInteger(generation) || generation < 1 ||
98
+ !Number.isSafeInteger(start) || start < 0) {
99
+ throw new MemoryError('invalid_memory_delta', '$.sourceIds');
133
100
  }
134
- // Evict oldest-first among the unpinned. The marker itself costs characters,
135
- // so it is charged up front against a worst-case width.
136
- const kept = [...entries];
137
- let omitted = 0;
138
- let reserve = OMISSION_MARKER(entries.length).length + sep.length;
139
- for (let i = 0; i < kept.length && total + reserve > budgetChars; i += 1) {
140
- const entry = kept[i];
141
- if (entry.pinned)
142
- continue;
143
- total -= entry.text.length + sep.length;
144
- kept[i] = { text: '', pinned: false };
145
- omitted += 1;
101
+ const result = {
102
+ text: '',
103
+ omitted: 0,
104
+ budgetChars: digestBudgetChars(input.summarizerWindow),
105
+ sourceMap: new Map(),
106
+ coverage: { summarizedMessages: 0, clippedToolResults: [] },
107
+ };
108
+ const parts = [];
109
+ if (input.anchor) {
110
+ parts.push('ORIGINAL TASK (read-only reference, not current evidence):\n'
111
+ + contentText(input.anchor.content).text);
146
112
  }
147
- // Still over budget with only pinned entries left: clip the pinned text rather
148
- // than dropping it, so the densest record survives in some form.
149
- if (total + reserve > budgetChars) {
150
- const room = Math.max(0, budgetChars - reserve);
151
- const joined = kept.filter((e) => e.text.length > 0).map((e) => e.text).join(sep);
152
- return {
153
- text: `${OMISSION_MARKER(omitted)}${sep}${joined.slice(0, room)}`,
154
- omitted,
155
- budgetChars,
156
- };
113
+ if (input.priorMemory) {
114
+ parts.push(`PRIOR MEMORY (read-only):\n${serializeMemory(input.priorMemory)}`);
157
115
  }
158
- const body = kept.filter((e) => e.text.length > 0).map((e) => e.text).join(sep);
159
- return {
160
- text: omitted > 0 ? `${OMISSION_MARKER(omitted)}${sep}${body}` : body,
161
- omitted,
162
- budgetChars,
163
- };
116
+ if (input.legacySummary && !input.priorMemory?.legacySummary) {
117
+ parts.push(`LEGACY SUMMARY (read-only, incomplete provenance):\n${input.legacySummary}`);
118
+ }
119
+ const calls = new Map();
120
+ input.head.forEach((message, index) => parts.push(renderEntry(message, `g${generation}:m${start + index}`, calls, result)));
121
+ result.text = parts.join('\n\n');
122
+ if (result.text.length > result.budgetChars) {
123
+ throw new MemoryError('digest_budget_exceeded', '$.digest');
124
+ }
125
+ if (result.coverage.clippedToolResults.length > LIMITS.coverageClips) {
126
+ throw new MemoryError('protected_memory_too_large', '$.coverage');
127
+ }
128
+ return result;
164
129
  }
@@ -1,165 +1,42 @@
1
- /**
2
- * COMPACTION_LIMITS — the single authority on every bound context compaction
3
- * enforces (context-auto-compaction §5.4).
4
- *
5
- * ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope
6
- * (`glyphs.test.ts::inScope`), so no literal in this tree may hold a non-ASCII
7
- * byte. That regex is a HARDCODED DIRECTORY LIST, which is why adding this tree
8
- * and adding `compaction` to that list are the SAME change (C-7) - a scanner
9
- * that silently stops scanning is worse than no scanner. `fast/limits.ts`
10
- * records the identical trap one feature earlier.
11
- *
12
- * TWO KINDS OF NUMBER, AND THEY ARE NOT INTERCHANGEABLE - the distinction
13
- * `TEAM_LIMITS`, `TODO_LIMITS` and `FAST_LIMITS` all state:
14
- *
15
- * - The entries below are STRUCTURAL. They describe what a digest, a
16
- * summarization call and a card can physically carry. A user has no business
17
- * tuning them.
18
- * - The six `compaction.*` keys in `config/schema.ts` are POLICY: when to fire,
19
- * how much to keep, which model, what to do on failure. Those are clamped,
20
- * persisted and user-facing.
21
- */
1
+ /** Structural limits for v2 compaction. All character counts are UTF-16 units. */
22
2
  export declare const COMPACTION_LIMITS: {
23
- /** Ceiling on the rendered digest, before the summarizer-window check. ~30k tokens. */
24
3
  readonly digestMaxChars: 120000;
25
- /** Output + system prompt + framing, kept out of the digest's share of the window. */
26
4
  readonly summarizerReserveTokens: 8000;
27
- readonly userChars: 2000;
28
- readonly assistantChars: 2000;
29
- readonly toolArgChars: 200;
30
- readonly toolResultChars: 600;
31
- /**
32
- * Clip bound on the TAGGED text anchor.
33
- *
34
- * A CLIP, NOT A SHAPE SWITCH (D-21 / P1-9). v1 of the design kept the anchor
35
- * verbatim-standalone past this length and merged it into the block below it,
36
- * which made the anchor rule LENGTH-DEPENDENT and therefore non-idempotent: on
37
- * the second compaction the "first user message" is a summary. A 4 000-character
38
- * first message clipped with a visible marker keeps the rule "one leading
39
- * anchor, always recognizable"; a 40 000-character first message is not a goal
40
- * statement, and truncating it with a marker is the honest outcome.
41
- */
42
- readonly anchorChars: 4000;
43
- /** What the summarizer prompt asks for, and what the splice clips to. */
44
- readonly summaryMaxChars: 6000;
45
- readonly summaryOutputTokens: 2048;
46
- /**
47
- * Wall-clock budget for ONE summarization call.
48
- *
49
- * Longer than the fast reviewer's 20 s because the input is ~30x larger and the
50
- * output ~4x larger, and because the cost of a timeout here is a DEGRADED RUN
51
- * rather than a missing critique.
52
- *
53
- * COUPLED TO THE ENGINE'S `COMPACTION_HARD_TIMEOUT_MS` (120 s). The worst legal
54
- * host budget is two calls at this value back to back plus the digest render,
55
- * so `2 * callTimeoutMs` must stay strictly under the engine's ceiling. Raising
56
- * this without raising that makes the §3.7 ladder unreachable and turns every
57
- * slow-but-correct summarization into `manager_timeout`.
58
- */
5
+ readonly summaryMaxChars: 32000;
6
+ readonly summaryOutputTokens: 4096;
7
+ readonly memoryJsonChars: 32000;
8
+ readonly memoryEnvelopeChars: 1024;
9
+ readonly protectedMemoryChars: 48000;
10
+ readonly memoryItems: 200;
11
+ readonly protectedUserMessages: 200;
12
+ readonly memoryAdditions: 64;
13
+ readonly coverageClips: 200;
14
+ readonly jsonDepth: 32;
15
+ readonly instructionsChars: 4000;
16
+ readonly itemTextChars: 1200;
17
+ readonly sourceExcerptChars: 512;
18
+ readonly itemSources: 8;
19
+ readonly toolResultEdgeChars: 1500;
20
+ readonly operationTimeoutMs: 120000;
59
21
  readonly callTimeoutMs: 45000;
60
- /** Turns between compactions. Never two in a row: a compaction that just ran
61
- * needs a real turn before its effect is measurable, because `lastUsage` is
62
- * still the pre-compaction number until the next `turn_end`. */
63
22
  readonly minTurnsBetween: 2;
64
- /** Compactions STARTED per run, both triggers, hard. */
65
23
  readonly maxPerRun: 5;
66
- /** A splice must project occupancy below `threshold - this`, or it bought two turns. */
67
24
  readonly minReclaimRatio: 0.15;
68
- /** Consecutive no-progress PRESSURE compactions before the proactive trigger self-disables. */
69
25
  readonly stuckLimit: 2;
70
- /**
71
- * Clip bound on an oversized `tool_result` body inside the RETAINED tail.
72
- *
73
- * 2 000 rather than `toolResultChars`' 600, and the two are not the same
74
- * question. That one bounds a message in the DIGEST, which the summarizer
75
- * reads once and which exists to be lossy. This one bounds a message the model
76
- * keeps WORKING FROM: it is the last two-to-four turns, and the run is about to
77
- * continue on them. Relief only ever runs when the alternative is a request the
78
- * provider cannot accept.
79
- */
80
26
  readonly tailToolResultChars: 2000;
81
- /**
82
- * Room reserved for the `<compacted_context>` block in the tail-budget
83
- * projection.
84
- *
85
- * THE BLOCK IS WRITTEN AFTER THE PROJECTION, which is why this exists at all:
86
- * relief measures a spliced history whose summary body is already in it, but
87
- * the `plan === null` path measures one where nothing has been written yet, and
88
- * a budget with no allowance would let the block push the result back over.
89
- */
90
27
  readonly blockAllowanceTokens: 2000;
91
- /**
92
- * Files kept for ONE `runId`, newest first.
93
- *
94
- * PER RUN, NOT PER DIRECTORY (RV-6). `<home>/compaction/` is shared by every
95
- * `aragon` on the machine — `file-sink.ts:94-95` already treats concurrent
96
- * processes as normal — so a global count-based prune would delete a live
97
- * session's archives out from under it.
98
- */
99
28
  readonly archiveMaxFiles: 20;
100
- /**
101
- * The cross-run sweep: 7 days.
102
- *
103
- * AGE RATHER THAN COUNT, for the reason above. Age can only ever delete files
104
- * whose run finished a week ago; a count can delete the newest file another
105
- * process wrote a second ago.
106
- */
107
29
  readonly archiveMaxAgeMs: 604800000;
108
- /**
109
- * Per-file ceiling before dropped message bodies are clipped from the OLDEST
110
- * end. The metadata and the summary are never clipped: a truncated archive
111
- * that still says what happened beats none.
112
- */
113
30
  readonly archiveMaxBytes: 8000000;
114
- /** A child's `keepRecentTurns`, delivered through its config view. */
115
31
  readonly childKeepRecentTurns: 2;
116
- /**
117
- * A child's guard-2 ceiling, delivered through `Compactor`'s instance bound.
118
- *
119
- * HERE RATHER THAN AS A `compaction.*` KEY, and the placement is the whole
120
- * argument: `maxPerRun` is STRUCTURAL (see this file's header), there is no
121
- * config path to it by design, and a child needs a tighter one. Both numbers
122
- * therefore stay in the one file that owns structural bounds.
123
- */
124
32
  readonly childMaxPerRun: 2;
125
- /** Rows the collapsed transcript card renders; Ctrl+O expands. */
126
33
  readonly cardTextRows: 6;
127
- /**
128
- * Below this many columns the status chip drops.
129
- *
130
- * 100, matching all four existing peers - `retry-view.ts`, `fast/limits.ts`,
131
- * `team/limits.ts`, `todo/limits.ts` (P2-2). A status-bar degradation ladder
132
- * that is nearly-but-not-quite uniform is a reading hazard bought for nothing.
133
- * The guaranteed reporting surface is `/compact status`, which is what makes
134
- * dropping the chip acceptable at all.
135
- */
136
34
  readonly statusCompactCols: 100;
137
35
  };
138
- /**
139
- * Bumped whenever the wording of the `<compacted_context>` block or the
140
- * summarizer system prompt changes, so a behaviour report can be tied to a block
141
- * revision with one grep - exactly what `FAST_BLOCK_VERSION` and
142
- * `TEAM_BLOCK_VERSION` are for.
143
- */
144
- export declare const COMPACTION_BLOCK_VERSION = "v1-2026-08";
145
- /** Opening tag of the verbatim goal anchor (§3.6.5). */
36
+ export declare const COMPACTION_BLOCK_VERSION = "v2-2026-10";
146
37
  export declare const ANCHOR_TAG_OPEN = "<original_task>";
147
38
  export declare const ANCHOR_TAG_CLOSE = "</original_task>";
148
- /** Opening tag of the spliced summary block. Recognized on the NEXT compaction. */
149
39
  export declare const BLOCK_TAG_OPEN = "<compacted_context";
150
40
  export declare const BLOCK_TAG_CLOSE = "</compacted_context>";
151
- /** Marker appended to an anchor clipped at `anchorChars`. ASCII (see the header). */
152
- export declare const ANCHOR_TRUNCATION_MARK = " [truncated]";
153
- /**
154
- * What tail relief writes where it removed text (hardening §3.3.2 rule 4).
155
- *
156
- * IN THE HISTORY THE MODEL READS, at the exact place the data went missing. A
157
- * model that sees this knows the output is partial and can re-run the tool; a
158
- * model handed a silently truncated file cannot tell the difference between "the
159
- * file ends here" and "the file was cut here".
160
- *
161
- * THE HOST OWNS IT, not core: `engine/compaction.ts` has no glyph vocabulary and
162
- * must not learn one, so `relieveTail` takes this as a callback. ASCII, per this
163
- * file's header.
164
- */
41
+ /** Retained legacy helper for explicit callers; v2 compression never clips the tail. */
165
42
  export declare function tailClipMarker(removed: number): string;