@aragon-agent/cli 0.6.13 → 0.6.15

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 +41 -0
  2. package/README.md +250 -222
  3. package/dist/agent/controller.d.ts +36 -2
  4. package/dist/agent/controller.js +244 -57
  5. package/dist/agent/model-profile-settings.js +11 -2
  6. package/dist/agent/plan-prompt.d.ts +1 -1
  7. package/dist/agent/plan-prompt.js +4 -3
  8. package/dist/agent/project-guidance-prompt.d.ts +4 -0
  9. package/dist/agent/project-guidance-prompt.js +104 -0
  10. package/dist/agent/reducer.d.ts +2 -0
  11. package/dist/agent/reducer.js +5 -0
  12. package/dist/agent/system-prompt.js +4 -0
  13. package/dist/boot/guard.d.ts +3 -3
  14. package/dist/boot/guard.js +3 -3
  15. package/dist/cli.js +33 -15
  16. package/dist/commands/builtins.js +2 -2
  17. package/dist/compaction/child.d.ts +9 -1
  18. package/dist/compaction/child.js +43 -13
  19. package/dist/compaction/command.js +20 -10
  20. package/dist/compaction/compactor.d.ts +33 -204
  21. package/dist/compaction/compactor.js +311 -723
  22. package/dist/compaction/context-command.js +5 -3
  23. package/dist/compaction/digest.d.ts +15 -51
  24. package/dist/compaction/digest.js +113 -148
  25. package/dist/compaction/limits.d.ts +19 -142
  26. package/dist/compaction/limits.js +20 -147
  27. package/dist/compaction/memory-identity.d.ts +16 -0
  28. package/dist/compaction/memory-identity.js +63 -0
  29. package/dist/compaction/memory-input.d.ts +25 -0
  30. package/dist/compaction/memory-input.js +106 -0
  31. package/dist/compaction/memory-render.d.ts +4 -0
  32. package/dist/compaction/memory-render.js +47 -0
  33. package/dist/compaction/memory-validation.d.ts +21 -0
  34. package/dist/compaction/memory-validation.js +268 -0
  35. package/dist/compaction/memory.d.ts +79 -0
  36. package/dist/compaction/memory.js +118 -0
  37. package/dist/compaction/meter.d.ts +14 -10
  38. package/dist/compaction/meter.js +55 -19
  39. package/dist/compaction/operation.d.ts +18 -0
  40. package/dist/compaction/operation.js +68 -0
  41. package/dist/compaction/pressure.d.ts +6 -20
  42. package/dist/compaction/pressure.js +14 -23
  43. package/dist/compaction/summarize-call.d.ts +3 -16
  44. package/dist/compaction/summarize-call.js +20 -20
  45. package/dist/compaction/summary-prompt.d.ts +29 -66
  46. package/dist/compaction/summary-prompt.js +132 -178
  47. package/dist/compaction/types.d.ts +23 -3
  48. package/dist/compaction/wiring.d.ts +10 -25
  49. package/dist/compaction/wiring.js +60 -67
  50. package/dist/config/load.d.ts +3 -1
  51. package/dist/config/load.js +8 -4
  52. package/dist/config/schema.d.ts +3 -3
  53. package/dist/config/schema.js +1 -1
  54. package/dist/exec/events.d.ts +8 -0
  55. package/dist/exec/index.js +13 -6
  56. package/dist/exec/runner.d.ts +8 -0
  57. package/dist/exec/runner.js +26 -2
  58. package/dist/session/persist.d.ts +3 -0
  59. package/dist/session/persist.js +9 -0
  60. package/dist/session/store.js +2 -0
  61. package/dist/session/validate-session.js +34 -0
  62. package/dist/team/subagent.js +8 -0
  63. package/dist/ui/App.js +221 -126
  64. package/dist/ui/BottomStatusRow.d.ts +5 -10
  65. package/dist/ui/BottomStatusRow.js +7 -11
  66. package/dist/ui/Composer.d.ts +4 -2
  67. package/dist/ui/Composer.js +2 -2
  68. package/dist/ui/Header.d.ts +6 -2
  69. package/dist/ui/Header.js +7 -6
  70. package/dist/ui/Logo.d.ts +3 -5
  71. package/dist/ui/Logo.js +4 -8
  72. package/dist/ui/PromptInput.d.ts +3 -1
  73. package/dist/ui/PromptInput.js +35 -42
  74. package/dist/ui/StatusBar.d.ts +6 -0
  75. package/dist/ui/StatusBar.js +8 -7
  76. package/dist/ui/Transcript.js +1 -1
  77. package/dist/ui/capabilities.d.ts +2 -0
  78. package/dist/ui/capabilities.js +6 -0
  79. package/dist/ui/color-runtime.d.ts +5 -0
  80. package/dist/ui/color-runtime.js +29 -0
  81. package/dist/ui/entries/CompactionCard.d.ts +3 -1
  82. package/dist/ui/entries/CompactionCard.js +7 -5
  83. package/dist/ui/frame-differ.js +27 -0
  84. package/dist/ui/frame-observer.js +18 -5
  85. package/dist/ui/glyphs.js +10 -8
  86. package/dist/ui/ink-runtime.d.ts +3 -0
  87. package/dist/ui/ink-runtime.js +30 -0
  88. package/dist/ui/interaction-copy.d.ts +73 -73
  89. package/dist/ui/interaction-copy.js +30 -38
  90. package/dist/ui/interaction-hints.d.ts +8 -1
  91. package/dist/ui/interaction-hints.js +56 -69
  92. package/dist/ui/layout/AppShell.d.ts +6 -2
  93. package/dist/ui/layout/AppShell.js +2 -2
  94. package/dist/ui/layout/OverlayFrame.d.ts +2 -0
  95. package/dist/ui/layout/OverlayFrame.js +23 -14
  96. package/dist/ui/layout/ScrollViewport.d.ts +3 -1
  97. package/dist/ui/layout/ScrollViewport.js +10 -2
  98. package/dist/ui/layout/budget.d.ts +25 -26
  99. package/dist/ui/layout/budget.js +29 -22
  100. package/dist/ui/layout/document-layout.d.ts +2 -2
  101. package/dist/ui/layout/document-layout.js +2 -3
  102. package/dist/ui/layout/queue-layout.js +4 -4
  103. package/dist/ui/layout/status-detail-layout.d.ts +9 -0
  104. package/dist/ui/layout/status-detail-layout.js +56 -0
  105. package/dist/ui/layout/status-layout.d.ts +19 -2
  106. package/dist/ui/layout/status-layout.js +95 -83
  107. package/dist/ui/layout/wrap-rows.d.ts +2 -0
  108. package/dist/ui/layout/wrap-rows.js +21 -7
  109. package/dist/ui/overlays/ConfirmDialog.d.ts +2 -1
  110. package/dist/ui/overlays/ConfirmDialog.js +22 -3
  111. package/dist/ui/overlays/HelpOverlay.js +9 -2
  112. package/dist/ui/overlays/ModelPicker.d.ts +5 -8
  113. package/dist/ui/overlays/ModelPicker.js +44 -10
  114. package/dist/ui/overlays/ModelProfileEditor.js +2 -0
  115. package/dist/ui/overlays/ModelProfilePicker.d.ts +1 -0
  116. package/dist/ui/overlays/ModelProfilePicker.js +4 -2
  117. package/dist/ui/overlays/PlanReviewOverlay.d.ts +2 -1
  118. package/dist/ui/overlays/PlanReviewOverlay.js +7 -4
  119. package/dist/ui/overlays/QuestionOverlay.d.ts +2 -1
  120. package/dist/ui/overlays/QuestionOverlay.js +45 -32
  121. package/dist/ui/overlays/QueueOverlay.js +2 -2
  122. package/dist/ui/overlays/SettingsScreen.d.ts +1 -0
  123. package/dist/ui/overlays/SettingsScreen.js +10 -3
  124. package/dist/ui/render-cache.d.ts +2 -2
  125. package/dist/ui/render-cache.js +4 -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
@@ -0,0 +1,268 @@
1
+ import { COMPACTION_LIMITS as LIMITS } from './limits.js';
2
+ /** Stable failure reason and short field path; never includes protected content. */
3
+ export class MemoryError extends Error {
4
+ reason;
5
+ path;
6
+ constructor(reason, path = '$') {
7
+ super(`${reason}: ${path}`);
8
+ this.reason = reason;
9
+ this.path = path;
10
+ this.name = 'MemoryError';
11
+ }
12
+ }
13
+ function invalid(path) {
14
+ throw new MemoryError('invalid_memory_delta', path);
15
+ }
16
+ /** Parse bounded JSON, rejecting duplicate decoded keys before JSON.parse. */
17
+ export function parseStrictJson(text) {
18
+ if (text.length > LIMITS.memoryJsonChars)
19
+ invalid('$.length');
20
+ const stack = [];
21
+ for (let index = 0; index < text.length; index += 1) {
22
+ const char = text[index];
23
+ if (char === '"') {
24
+ const start = index++;
25
+ while (index < text.length && text[index] !== '"') {
26
+ if (text[index] === '\\')
27
+ index += 1;
28
+ index += 1;
29
+ }
30
+ if (index >= text.length)
31
+ invalid('$.string');
32
+ let after = index + 1;
33
+ while (/\s/.test(text[after] ?? '') && after < text.length)
34
+ after += 1;
35
+ if (text[after] === ':')
36
+ checkKey(text.slice(start, index + 1), stack.at(-1));
37
+ }
38
+ else if (char === '{' || char === '[') {
39
+ stack.push(char === '{' ? new Set() : null);
40
+ if (stack.length > LIMITS.jsonDepth)
41
+ invalid('$.depth');
42
+ }
43
+ else if (char === '}' || char === ']') {
44
+ if (!stack.length)
45
+ invalid('$.nesting');
46
+ stack.pop();
47
+ }
48
+ }
49
+ try {
50
+ return JSON.parse(text);
51
+ }
52
+ catch {
53
+ return invalid('$.json');
54
+ }
55
+ }
56
+ function checkKey(token, keys) {
57
+ let key;
58
+ try {
59
+ key = JSON.parse(token);
60
+ }
61
+ catch {
62
+ return invalid('$.key');
63
+ }
64
+ if (!keys || keys.has(key))
65
+ invalid('$.duplicate_key');
66
+ keys.add(key);
67
+ }
68
+ function object(value, required, optional, path) {
69
+ if (!value || typeof value !== 'object' || Array.isArray(value))
70
+ invalid(path);
71
+ const record = value;
72
+ if (required.some((key) => !Object.hasOwn(record, key)))
73
+ invalid(path);
74
+ if (Object.keys(record).some((key) => !required.includes(key) && !optional.includes(key))) {
75
+ invalid(path);
76
+ }
77
+ return record;
78
+ }
79
+ function array(value, max, path) {
80
+ if (!Array.isArray(value) || value.length > max)
81
+ invalid(path);
82
+ return value;
83
+ }
84
+ function string(value, max, path) {
85
+ if (typeof value !== 'string' || !value.trim() || value.length > max)
86
+ invalid(path);
87
+ }
88
+ function integer(value, positive, path) {
89
+ if (!Number.isSafeInteger(value) || value < (positive ? 1 : 0))
90
+ invalid(path);
91
+ }
92
+ function id(value, kind, path) {
93
+ string(value, 64, path);
94
+ const match = new RegExp(`^g([1-9][0-9]*):${kind}([0-9]+)$`).exec(value);
95
+ if (!match)
96
+ invalid(path);
97
+ integer(Number(match[1]), true, path);
98
+ integer(Number(match[2]), false, path);
99
+ }
100
+ function source(value, path) {
101
+ const record = object(value, ['messageId', 'role', 'excerpt'], [], path);
102
+ id(record.messageId, 'm', `${path}.messageId`);
103
+ if (!['user', 'assistant', 'tool_result'].includes(record.role)) {
104
+ invalid(`${path}.role`);
105
+ }
106
+ string(record.excerpt, LIMITS.sourceExcerptChars, `${path}.excerpt`);
107
+ }
108
+ const SECTIONS = [
109
+ 'global', 'decisions', 'files', 'facts', 'tasks', 'verification', 'pitfalls', 'next',
110
+ ];
111
+ const STATUSES = ['pending', 'in_progress', 'blocked', 'done', 'cancelled'];
112
+ function item(value, stored, path) {
113
+ const record = object(value, ['section', 'text', 'sources', ...(stored ? ['id'] : [])], ['status', 'supersedes'], path);
114
+ if (stored)
115
+ id(record.id, 'i', `${path}.id`);
116
+ if (!SECTIONS.includes(record.section))
117
+ invalid(`${path}.section`);
118
+ string(record.text, LIMITS.itemTextChars, `${path}.text`);
119
+ const sources = array(record.sources, LIMITS.itemSources, `${path}.sources`);
120
+ if (!sources.length)
121
+ invalid(`${path}.sources`);
122
+ sources.forEach((entry, index) => source(entry, `${path}.sources[${index}]`));
123
+ if (record.section === 'tasks') {
124
+ if (!STATUSES.includes(record.status))
125
+ invalid(`${path}.status`);
126
+ }
127
+ else if (Object.hasOwn(record, 'status'))
128
+ invalid(`${path}.status`);
129
+ if (Object.hasOwn(record, 'supersedes'))
130
+ id(record.supersedes, 'i', `${path}.supersedes`);
131
+ }
132
+ /** Validate the model-owned schema only; throws MemoryError on invalid fields. */
133
+ export function validateDeltaSchema(value) {
134
+ const record = object(value, ['schemaVersion', 'additions'], [], '$');
135
+ if (record.schemaVersion !== 2)
136
+ invalid('$.schemaVersion');
137
+ array(record.additions, LIMITS.memoryAdditions, '$.additions').forEach((entry, index) => item(entry, false, `$.additions[${index}]`));
138
+ }
139
+ function protectedUser(value, path) {
140
+ const entry = object(value, ['id', 'message'], [], path);
141
+ id(entry.id, 'm', `${path}.id`);
142
+ const message = object(entry.message, ['role', 'content'], ['timestamp'], `${path}.message`);
143
+ if (message.role !== 'user')
144
+ invalid(`${path}.message.role`);
145
+ if (typeof message.content !== 'string') {
146
+ // Images must remain in the top-level anchor/tail, never in this ledger.
147
+ array(message.content, LIMITS.memoryJsonChars, `${path}.content`).forEach((part) => {
148
+ const record = object(part, ['type', 'text'], [], `${path}.content`);
149
+ if (record.type !== 'text' || typeof record.text !== 'string')
150
+ invalid(`${path}.content`);
151
+ });
152
+ }
153
+ if (Object.hasOwn(message, 'timestamp') && !Number.isFinite(message.timestamp)) {
154
+ invalid(`${path}.timestamp`);
155
+ }
156
+ }
157
+ function validateCoverage(value) {
158
+ const coverage = object(value, ['summarizedMessages', 'clippedToolResults', 'legacyIncomplete'], [], '$.coverage');
159
+ integer(coverage.summarizedMessages, false, '$.coverage.summarizedMessages');
160
+ if (typeof coverage.legacyIncomplete !== 'boolean')
161
+ invalid('$.coverage.legacyIncomplete');
162
+ const seen = new Set();
163
+ const clips = array(coverage.clippedToolResults, LIMITS.coverageClips, '$.coverage.clippedToolResults');
164
+ clips.forEach((value) => {
165
+ const clip = object(value, ['messageId', 'omittedChars'], [], '$.coverage.clip');
166
+ id(clip.messageId, 'm', '$.coverage.clip.messageId');
167
+ integer(clip.omittedChars, true, '$.coverage.clip.omittedChars');
168
+ if (seen.has(clip.messageId))
169
+ invalid('$.coverage.clip.duplicate');
170
+ seen.add(clip.messageId);
171
+ });
172
+ }
173
+ function validateStoredChains(memory) {
174
+ const entries = new Map();
175
+ const replaced = new Set();
176
+ for (const entry of memory.items) {
177
+ if (entries.has(entry.id) || Number(entry.id.slice(1).split(':')[0]) > memory.generation) {
178
+ invalid('$.items.id');
179
+ }
180
+ if (entry.supersedes) {
181
+ const previous = entries.get(entry.supersedes);
182
+ if (!previous || previous.section !== entry.section || replaced.has(previous.id)) {
183
+ invalid('$.items.supersedes');
184
+ }
185
+ replaced.add(previous.id);
186
+ }
187
+ entries.set(entry.id, entry);
188
+ }
189
+ const users = memory.userMessages.map((entry) => entry.id);
190
+ if (new Set(users).size !== users.length || users.includes(memory.originalTask.sourceId)) {
191
+ invalid('$.userMessages.id');
192
+ }
193
+ const references = [
194
+ memory.originalTask.sourceId,
195
+ ...users,
196
+ ...memory.items.flatMap((entry) => entry.sources.map((source) => source.messageId)),
197
+ ...memory.coverage.clippedToolResults.map((clip) => clip.messageId),
198
+ ];
199
+ if (references.some((reference) => Number(reference.slice(1).split(':')[0]) > memory.generation)) {
200
+ invalid('$.sources.generation');
201
+ }
202
+ }
203
+ /** Validate a persisted v2 body, including unique IDs and acyclic state chains. */
204
+ export function validateMemorySchema(value) {
205
+ const record = object(value, ['schemaVersion', 'generation', 'originalTask', 'userMessages', 'items', 'coverage'], ['legacySummary'], '$');
206
+ if (record.schemaVersion !== 2)
207
+ invalid('$.schemaVersion');
208
+ integer(record.generation, true, '$.generation');
209
+ const original = object(record.originalTask, ['sourceId', 'location'], [], '$.originalTask');
210
+ id(original.sourceId, 'm', '$.originalTask.sourceId');
211
+ if (original.location !== 'anchor')
212
+ invalid('$.originalTask.location');
213
+ array(record.userMessages, LIMITS.protectedUserMessages, '$.userMessages')
214
+ .forEach((entry, index) => protectedUser(entry, `$.userMessages[${index}]`));
215
+ array(record.items, LIMITS.memoryItems, '$.items')
216
+ .forEach((entry, index) => item(entry, true, `$.items[${index}]`));
217
+ if (Object.hasOwn(record, 'legacySummary')) {
218
+ string(record.legacySummary, LIMITS.memoryJsonChars, '$.legacySummary');
219
+ }
220
+ validateCoverage(record.coverage);
221
+ validateStoredChains(value);
222
+ }
223
+ function validateSource(reference, sourceMap, path) {
224
+ const original = sourceMap.get(reference.messageId);
225
+ if (!original || original.role !== reference.role)
226
+ invalid(`${path}.role`);
227
+ if (!original.visibleText.some((text) => text.includes(reference.excerpt))) {
228
+ invalid(`${path}.excerpt`);
229
+ }
230
+ return original;
231
+ }
232
+ function validateEvidence(entry, originals, path) {
233
+ const hasUser = originals.some((source) => source.role === 'user' && !source.isLegacyReference);
234
+ const hasToolResult = originals.some((source) => source.role === 'tool_result' && !!source.toolName);
235
+ const hasTool = originals.some((source) => source.role === 'tool_result' && source.isError !== true && !!source.toolName);
236
+ if (entry.status === 'done' && !hasUser && !hasTool)
237
+ invalid(`${path}.done_evidence`);
238
+ if (entry.status === 'cancelled' && !hasUser)
239
+ invalid(`${path}.cancelled_evidence`);
240
+ if (entry.section === 'verification' && !hasToolResult)
241
+ invalid(`${path}.verification_evidence`);
242
+ if (entry.supersedes && entry.section === 'global' && !hasUser)
243
+ invalid(`${path}.user_evidence`);
244
+ if (entry.status === 'done' && originals.some((source) => source.isError === true)) {
245
+ invalid(`${path}.failed_evidence`);
246
+ }
247
+ }
248
+ /** Check current-source excerpts, completion evidence and append-only supersession. */
249
+ export function validateMemoryDelta(delta, context) {
250
+ validateDeltaSchema(delta);
251
+ if (!delta.additions.length && context.hasSummarizableMessages) {
252
+ throw new MemoryError('empty_memory_delta', '$.additions');
253
+ }
254
+ const prior = new Map(context.prior?.items.map((entry) => [entry.id, entry]) ?? []);
255
+ const replaced = new Set(context.prior?.items.flatMap((entry) => entry.supersedes ? [entry.supersedes] : []) ?? []);
256
+ delta.additions.forEach((entry, index) => {
257
+ const path = `$.additions[${index}]`;
258
+ const originals = entry.sources.map((reference) => validateSource(reference, context.sourceMap, `${path}.sources`));
259
+ validateEvidence(entry, originals, path);
260
+ if (!entry.supersedes)
261
+ return;
262
+ const previous = prior.get(entry.supersedes);
263
+ if (!previous || previous.section !== entry.section || replaced.has(previous.id)) {
264
+ invalid(`${path}.supersedes`);
265
+ }
266
+ replaced.add(previous.id);
267
+ });
268
+ }
@@ -0,0 +1,79 @@
1
+ /** Host-owned append-only conversation memory; no I/O or model state. */
2
+ import type { Message, UserMessage } from '@aragon-agent/core';
3
+ import type { PreparedMemoryInput } from './memory-input.js';
4
+ export { MemoryError } from './memory-validation.js';
5
+ export interface SourceRef {
6
+ messageId: string;
7
+ role: 'user' | 'assistant' | 'tool_result';
8
+ excerpt: string;
9
+ }
10
+ export interface MemoryItem {
11
+ id: string;
12
+ section: 'global' | 'decisions' | 'files' | 'facts' | 'tasks' | 'verification' | 'pitfalls' | 'next';
13
+ text: string;
14
+ sources: SourceRef[];
15
+ status?: 'pending' | 'in_progress' | 'blocked' | 'done' | 'cancelled';
16
+ supersedes?: string;
17
+ }
18
+ export interface ProtectedUserMessage {
19
+ id: string;
20
+ message: UserMessage;
21
+ }
22
+ export interface CompactionMemory {
23
+ schemaVersion: 2;
24
+ generation: number;
25
+ originalTask: {
26
+ sourceId: string;
27
+ location: 'anchor';
28
+ };
29
+ userMessages: ProtectedUserMessage[];
30
+ items: MemoryItem[];
31
+ legacySummary?: string;
32
+ coverage: {
33
+ summarizedMessages: number;
34
+ clippedToolResults: Array<{
35
+ messageId: string;
36
+ omittedChars: number;
37
+ }>;
38
+ legacyIncomplete: boolean;
39
+ };
40
+ }
41
+ export interface MemoryDelta {
42
+ schemaVersion: 2;
43
+ additions: Array<Omit<MemoryItem, 'id'>>;
44
+ }
45
+ export interface MemorySource {
46
+ role: Message['role'];
47
+ visibleText: string[];
48
+ toolCallId?: string;
49
+ toolName?: string;
50
+ isError?: boolean;
51
+ isLegacyReference?: boolean;
52
+ }
53
+ export interface MemoryCoverage {
54
+ summarizedMessages: number;
55
+ clippedToolResults: Array<{
56
+ messageId: string;
57
+ omittedChars: number;
58
+ }>;
59
+ }
60
+ /** Recognize a legacy reference without excluding it from protected user content. */
61
+ export declare function isLegacyMemoryText(message: Message): boolean;
62
+ /** Parse the sole permitted model response; throws MemoryError on invalid input. */
63
+ export declare function parseMemoryDelta(text: string): MemoryDelta;
64
+ /** Parse a complete persisted body without inferring host identity from its shape. */
65
+ export declare function parseMemory(text: string): CompactionMemory;
66
+ /** Canonical UserMessage field order for protected budgets and prefix hashes. */
67
+ export declare function canonicalUserMessage(message: UserMessage): UserMessage;
68
+ /** Serialize fixed schema order and escaped delimiters; throws on final text budget. */
69
+ export declare function serializeMemory(memory: CompactionMemory): string;
70
+ /** Enforce the combined protected budget using complete serialized content. */
71
+ export declare function assertProtectedMemoryBudget(anchor: UserMessage, memory: CompactionMemory): void;
72
+ export interface MergeMemoryInput {
73
+ input: PreparedMemoryInput;
74
+ delta: MemoryDelta;
75
+ sourceMap: ReadonlyMap<string, MemorySource>;
76
+ coverage: MemoryCoverage;
77
+ }
78
+ /** Create a staged ledger; prior items and users are retained without rewriting. */
79
+ export declare function mergeMemory(options: MergeMemoryInput): CompactionMemory;
@@ -0,0 +1,118 @@
1
+ import { COMPACTION_LIMITS as LIMITS } from './limits.js';
2
+ import { MemoryError, parseStrictJson, validateDeltaSchema, validateMemoryDelta, validateMemorySchema, } from './memory-validation.js';
3
+ export { MemoryError } from './memory-validation.js';
4
+ /** Recognize a legacy reference without excluding it from protected user content. */
5
+ export function isLegacyMemoryText(message) {
6
+ const legacyPattern = new RegExp('^<compacted_context version="v1-2026-08"(?: [a-z]+="[^"]*")*>'
7
+ + '\\n[\\s\\S]*\\n</compacted_context>$');
8
+ return message.role === 'user' && typeof message.content === 'string'
9
+ && legacyPattern.test(message.content);
10
+ }
11
+ /** Parse the sole permitted model response; throws MemoryError on invalid input. */
12
+ export function parseMemoryDelta(text) {
13
+ const value = parseStrictJson(text);
14
+ validateDeltaSchema(value);
15
+ return value;
16
+ }
17
+ /** Parse a complete persisted body without inferring host identity from its shape. */
18
+ export function parseMemory(text) {
19
+ const value = parseStrictJson(text);
20
+ validateMemorySchema(value);
21
+ return value;
22
+ }
23
+ /** Canonical UserMessage field order for protected budgets and prefix hashes. */
24
+ export function canonicalUserMessage(message) {
25
+ return {
26
+ role: 'user',
27
+ content: typeof message.content === 'string'
28
+ ? message.content
29
+ : message.content.map((part) => part.type === 'text'
30
+ ? { type: 'text', text: part.text }
31
+ : { type: 'image', mediaType: part.mediaType, data: part.data }),
32
+ ...(message.timestamp !== undefined ? { timestamp: message.timestamp } : {}),
33
+ };
34
+ }
35
+ /** Serialize fixed schema order and escaped delimiters; throws on final text budget. */
36
+ export function serializeMemory(memory) {
37
+ validateMemorySchema(memory);
38
+ const ordered = {
39
+ schemaVersion: 2,
40
+ generation: memory.generation,
41
+ originalTask: { sourceId: memory.originalTask.sourceId, location: 'anchor' },
42
+ userMessages: memory.userMessages.map((entry) => ({
43
+ id: entry.id,
44
+ message: canonicalUserMessage(entry.message),
45
+ })),
46
+ items: memory.items.map((entry) => ({
47
+ id: entry.id,
48
+ section: entry.section,
49
+ text: entry.text,
50
+ sources: entry.sources.map((source) => ({
51
+ messageId: source.messageId,
52
+ role: source.role,
53
+ excerpt: source.excerpt,
54
+ })),
55
+ ...(entry.status !== undefined ? { status: entry.status } : {}),
56
+ ...(entry.supersedes !== undefined ? { supersedes: entry.supersedes } : {}),
57
+ })),
58
+ ...(memory.legacySummary !== undefined ? { legacySummary: memory.legacySummary } : {}),
59
+ coverage: {
60
+ summarizedMessages: memory.coverage.summarizedMessages,
61
+ clippedToolResults: memory.coverage.clippedToolResults.map((clip) => ({
62
+ messageId: clip.messageId,
63
+ omittedChars: clip.omittedChars,
64
+ })),
65
+ legacyIncomplete: memory.coverage.legacyIncomplete,
66
+ },
67
+ };
68
+ const text = JSON.stringify(ordered, null, 2).replace(/</g, '\\u003c').replace(/>/g, '\\u003e');
69
+ if (text.length > LIMITS.memoryJsonChars) {
70
+ throw new MemoryError('protected_memory_too_large', '$.serialized');
71
+ }
72
+ return text;
73
+ }
74
+ /** Enforce the combined protected budget using complete serialized content. */
75
+ export function assertProtectedMemoryBudget(anchor, memory) {
76
+ const length = JSON.stringify(canonicalUserMessage(anchor)).length
77
+ + serializeMemory(memory).length;
78
+ if (length > LIMITS.protectedMemoryChars) {
79
+ throw new MemoryError('protected_memory_too_large', '$.anchor');
80
+ }
81
+ }
82
+ /** Create a staged ledger; prior items and users are retained without rewriting. */
83
+ export function mergeMemory(options) {
84
+ const { input, delta, sourceMap, coverage } = options;
85
+ validateMemoryDelta(delta, {
86
+ sourceMap,
87
+ prior: input.prior,
88
+ hasSummarizableMessages: coverage.summarizedMessages > 0,
89
+ });
90
+ const items = [
91
+ ...(input.prior?.items ?? []),
92
+ ...delta.additions.map((entry, index) => ({ id: `g${input.generation}:i${index}`, ...entry })),
93
+ ];
94
+ const clips = [
95
+ ...(input.prior?.coverage.clippedToolResults ?? []),
96
+ ...coverage.clippedToolResults,
97
+ ];
98
+ if (items.length > LIMITS.memoryItems ||
99
+ input.userMessages.length > LIMITS.protectedUserMessages ||
100
+ clips.length > LIMITS.coverageClips) {
101
+ throw new MemoryError('protected_memory_too_large', '$.counts');
102
+ }
103
+ const memory = {
104
+ schemaVersion: 2,
105
+ generation: input.generation,
106
+ originalTask: { sourceId: input.originalTaskSourceId, location: 'anchor' },
107
+ userMessages: [...input.userMessages],
108
+ items,
109
+ ...(input.legacySummary !== undefined ? { legacySummary: input.legacySummary } : {}),
110
+ coverage: {
111
+ summarizedMessages: (input.prior?.coverage.summarizedMessages ?? 0) + coverage.summarizedMessages,
112
+ clippedToolResults: clips,
113
+ legacyIncomplete: !!input.prior?.coverage.legacyIncomplete || input.legacySummary !== undefined,
114
+ },
115
+ };
116
+ assertProtectedMemoryBudget(input.anchor, memory);
117
+ return memory;
118
+ }
@@ -62,6 +62,7 @@ export interface ContextMeterDeps {
62
62
  * measurement, so a live settings edit moves the denominator without a relaunch.
63
63
  */
64
64
  getWindowOverride(): number | null;
65
+ getRequestVersion?(): number;
65
66
  }
66
67
  export type ContextUsageListener = (usage: ContextUsageSnapshot) => void;
67
68
  /**
@@ -102,6 +103,13 @@ export declare class ContextMeter {
102
103
  private last;
103
104
  /** Whether the history has changed since the last measurement. */
104
105
  private dirty;
106
+ private prefixLast;
107
+ private sampledPrompt;
108
+ private sampledVersion;
109
+ private publishedLength;
110
+ private publishedLast;
111
+ private publishedPrompt;
112
+ private publishedVersion;
105
113
  constructor(deps: ContextMeterDeps);
106
114
  /**
107
115
  * Occupancy right now. FOR THE SCREEN.
@@ -130,13 +138,8 @@ export declare class ContextMeter {
130
138
  lastPublished(): Pressure | null;
131
139
  /** The published projection, or a zeroed one before the first measurement. */
132
140
  currentUsage(): ContextUsageSnapshot;
133
- /**
134
- * Measure with the caller's own authoritative usage. FOR A DECISION POINT.
135
- *
136
- * `Compactor.shouldCompact` has a `CompactionProbe.lastUsage` the meter may
137
- * not have seen yet, so it supplies it rather than trusting the field here.
138
- */
139
- measureWith(lastUsage: TokenUsage | undefined): Pressure;
141
+ /** Compatibility reads cannot establish or resurrect a provider sample. */
142
+ measureWith(_lastUsage: TokenUsage | undefined): Pressure;
140
143
  /**
141
144
  * `turn_end` - the one moment in the whole flow that is FULLY measured.
142
145
  *
@@ -175,13 +178,13 @@ export declare class ContextMeter {
175
178
  */
176
179
  onHistoryReplaced(): void;
177
180
  /**
178
- * Only the DENOMINATOR moved (`/model`, a `contextWindow` edit).
181
+ * Only the denominator moved (window override or model metadata refresh).
179
182
  *
180
183
  * DELIBERATELY NOT A RESET. The measured base is still a true statement about
181
184
  * the history, so throwing it away would trade a correct numerator for a whole
182
185
  * history estimate and produce a visible jump on a common operation. The
183
- * numerator's calibration is one turn stale until the next `turn_end`
184
- * refreshes it, which is the trade this method names (RV-13).
186
+ * Changing the model connection or request environment requires the deep
187
+ * invalidation path instead; its measurement is no longer transferable.
185
188
  */
186
189
  onWindowChanged(): void;
187
190
  /**
@@ -234,6 +237,7 @@ export declare class ContextMeter {
234
237
  * is cached" cannot drift, which is what makes `lastPublished()` a meaningful
235
238
  * answer rather than a stale one.
236
239
  */
240
+ private refreshValidity;
237
241
  private measureAndPublish;
238
242
  private compute;
239
243
  /**