@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
@@ -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
  /**
@@ -103,6 +103,13 @@ export class ContextMeter {
103
103
  last = null;
104
104
  /** Whether the history has changed since the last measurement. */
105
105
  dirty = true;
106
+ prefixLast;
107
+ sampledPrompt;
108
+ sampledVersion;
109
+ publishedLength = -1;
110
+ publishedLast;
111
+ publishedPrompt;
112
+ publishedVersion;
106
113
  constructor(deps) {
107
114
  this.deps = deps;
108
115
  }
@@ -128,8 +135,7 @@ export class ContextMeter {
128
135
  // precisely the symptom. `estimateAppendedTokens` bounds-checks too, but it
129
136
  // only zeroes the DELTA and keeps the wrong measured base, so that belt does
130
137
  // not cover this.
131
- if (this.discardStalePrefix())
132
- this.dirty = true;
138
+ this.refreshValidity();
133
139
  if (!this.dirty && this.last)
134
140
  return this.last;
135
141
  return this.measureAndPublish(this.lastUsage);
@@ -154,16 +160,9 @@ export class ContextMeter {
154
160
  // =========================================================================
155
161
  // Writing
156
162
  // =========================================================================
157
- /**
158
- * Measure with the caller's own authoritative usage. FOR A DECISION POINT.
159
- *
160
- * `Compactor.shouldCompact` has a `CompactionProbe.lastUsage` the meter may
161
- * not have seen yet, so it supplies it rather than trusting the field here.
162
- */
163
- measureWith(lastUsage) {
164
- if (this.discardStalePrefix())
165
- this.dirty = true;
166
- return this.measureAndPublish(lastUsage);
163
+ /** Compatibility reads cannot establish or resurrect a provider sample. */
164
+ measureWith(_lastUsage) {
165
+ return this.current();
167
166
  }
168
167
  /**
169
168
  * `turn_end` - the one moment in the whole flow that is FULLY measured.
@@ -180,6 +179,9 @@ export class ContextMeter {
180
179
  this.lastUsage = usage;
181
180
  this.estimateOffset = computeEstimateOffset(usage, messages, systemPrompt);
182
181
  this.measuredPrefixLength = messages.length;
182
+ this.prefixLast = messages[messages.length - 1];
183
+ this.sampledPrompt = systemPrompt;
184
+ this.sampledVersion = this.deps.getRequestVersion?.() ?? 0;
183
185
  this.measureAndPublish(usage);
184
186
  }
185
187
  /**
@@ -199,6 +201,7 @@ export class ContextMeter {
199
201
  onHistorySpliced() {
200
202
  this.lastUsage = undefined;
201
203
  this.measuredPrefixLength = undefined;
204
+ this.prefixLast = undefined;
202
205
  if (this.dirty)
203
206
  return;
204
207
  this.dirty = true;
@@ -218,22 +221,25 @@ export class ContextMeter {
218
221
  onHistoryReplaced() {
219
222
  this.lastUsage = undefined;
220
223
  this.measuredPrefixLength = undefined;
224
+ this.prefixLast = undefined;
221
225
  this.estimateOffset = undefined;
226
+ this.sampledPrompt = undefined;
227
+ this.sampledVersion = undefined;
222
228
  this.dirty = true;
223
229
  this.scheduleTick();
224
230
  }
225
231
  /**
226
- * Only the DENOMINATOR moved (`/model`, a `contextWindow` edit).
232
+ * Only the denominator moved (window override or model metadata refresh).
227
233
  *
228
234
  * DELIBERATELY NOT A RESET. The measured base is still a true statement about
229
235
  * the history, so throwing it away would trade a correct numerator for a whole
230
236
  * history estimate and produce a visible jump on a common operation. The
231
- * numerator's calibration is one turn stale until the next `turn_end`
232
- * refreshes it, which is the trade this method names (RV-13).
237
+ * Changing the model connection or request environment requires the deep
238
+ * invalidation path instead; its measurement is no longer transferable.
233
239
  */
234
240
  onWindowChanged() {
235
241
  this.dirty = true;
236
- this.measureAndPublish(this.lastUsage);
242
+ this.current();
237
243
  }
238
244
  /**
239
245
  * Something was appended. Mark dirty, arrange a throttled publish, MEASURE
@@ -255,7 +261,7 @@ export class ContextMeter {
255
261
  this.timer = null;
256
262
  if (!this.dirty || this.disposed)
257
263
  return;
258
- this.measureAndPublish(this.lastUsage);
264
+ this.current();
259
265
  }, CONTEXT_METER_TICK_MS);
260
266
  // The other half of I-11: an armed tick must not keep the event loop alive.
261
267
  const unrefable = handle;
@@ -353,11 +359,13 @@ export class ContextMeter {
353
359
  discardStalePrefix() {
354
360
  if (this.measuredPrefixLength === undefined)
355
361
  return false;
356
- const length = this.deps.getMessages().length;
357
- if (length >= this.measuredPrefixLength)
362
+ const messages = this.deps.getMessages();
363
+ if (messages.length >= this.measuredPrefixLength &&
364
+ messages[this.measuredPrefixLength - 1] === this.prefixLast)
358
365
  return false;
359
366
  this.lastUsage = undefined;
360
367
  this.measuredPrefixLength = undefined;
368
+ this.prefixLast = undefined;
361
369
  return true;
362
370
  }
363
371
  /**
@@ -367,8 +375,34 @@ export class ContextMeter {
367
375
  * is cached" cannot drift, which is what makes `lastPublished()` a meaningful
368
376
  * answer rather than a stale one.
369
377
  */
378
+ refreshValidity() {
379
+ const messages = this.deps.getMessages();
380
+ const prompt = this.readSystemPrompt();
381
+ const version = this.deps.getRequestVersion?.() ?? 0;
382
+ if (this.sampledPrompt !== undefined &&
383
+ (prompt !== this.sampledPrompt || version !== this.sampledVersion)) {
384
+ this.onHistoryReplaced();
385
+ }
386
+ if (this.discardStalePrefix())
387
+ this.dirty = true;
388
+ const window = this.resolveWindow();
389
+ const last = this.last;
390
+ if (messages.length !== this.publishedLength ||
391
+ messages[messages.length - 1] !== this.publishedLast ||
392
+ prompt !== this.publishedPrompt || version !== this.publishedVersion ||
393
+ window.contextWindow !== last?.contextWindow ||
394
+ window.windowKnown !== last?.windowKnown ||
395
+ window.windowOverridden !== last?.windowOverridden ||
396
+ this.deps.getModelInfo().contextWindowSource !== last?.windowSource)
397
+ this.dirty = true;
398
+ }
370
399
  measureAndPublish(lastUsage) {
371
400
  const pressure = this.compute(lastUsage);
401
+ const messages = this.deps.getMessages();
402
+ this.publishedLength = messages.length;
403
+ this.publishedLast = messages[messages.length - 1];
404
+ this.publishedPrompt = this.readSystemPrompt();
405
+ this.publishedVersion = this.deps.getRequestVersion?.() ?? 0;
372
406
  this.last = pressure;
373
407
  this.dirty = false;
374
408
  this.publish(pressure);