@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,61 @@
1
+ /**
2
+ * Win32-input-mode translation (win32 keyboard-enhancement section of the
3
+ * Shift+Enter newline fix).
4
+ *
5
+ * WHY THIS EXISTS. With `CSI ? 9001 h` the Windows console (conhost inside
6
+ * ConPTY included -- verified byte-level on both conhost and Windows Terminal
7
+ * 1.24, see docs/diagnoses/shift-enter-newline-dead-by-default) reports EVERY
8
+ * key as `ESC [ vk ; scan ; char ; keyDown ; mods ; repeat _`. That is the
9
+ * only Windows-side encoding in which Shift+Enter is distinguishable from
10
+ * Enter: mods carries SHIFT_PRESSED (0x10) on the record. Nothing else
11
+ * survives ConPTY: kitty `CSI > 1 u` is swallowed (a `CSI ? u` query never
12
+ * answers) and modifyOtherKeys is a no-op.
13
+ *
14
+ * THE CONTRACT. `splitWin32KeySequences` recognises the records in a chunk;
15
+ * `translateWin32Key` rewrites each one to the LEGACY bytes the same keypress
16
+ * would have produced with the mode off -- except the modified-Enter family,
17
+ * which becomes `ENTER_NEWLINE_FRAME` exactly like `enter-sequences.ts` maps
18
+ * its CSI-u cousins. Ink therefore sees the stream it has always seen. Key-up
19
+ * records are dropped (every press arrives as a down/up pair in one chunk).
20
+ *
21
+ * Pure: no node/React imports, callable from any test environment.
22
+ */
23
+ /** Enable / disable win32-input-mode (private mode 9001). */
24
+ export declare const WIN32_INPUT_ENABLE = "\u001B[?9001h";
25
+ export declare const WIN32_INPUT_DISABLE = "\u001B[?9001l";
26
+ /** One decoded `ESC [ vk ; sc ; char ; down ; mods ; rep _` record. */
27
+ export interface Win32KeyRecord {
28
+ readonly vk: number;
29
+ readonly scan: number;
30
+ readonly char: number;
31
+ readonly down: boolean;
32
+ readonly mods: number;
33
+ readonly repeat: number;
34
+ }
35
+ export type Win32Segment = {
36
+ kind: 'text';
37
+ text: string;
38
+ } | {
39
+ kind: 'key';
40
+ record: Win32KeyRecord;
41
+ };
42
+ /**
43
+ * Split a chunk into text spans and win32-input-mode records.
44
+ *
45
+ * MATCHES ONLY `ESC [ <digits/semicolons> _`: mouse reports need `<`, paste
46
+ * markers end in `~`, CSI-u ends in `u`, DECRPM replies start with `?`. A
47
+ * `_`-terminated all-numeric-parameter sequence is therefore unambiguous in
48
+ * the stream this filter ever sees.
49
+ */
50
+ export declare function splitWin32KeySequences(text: string): Win32Segment[];
51
+ /**
52
+ * Translate one record to the legacy bytes the same keypress would have
53
+ * produced with win32-input-mode off (Enter family excepted: a modified Enter
54
+ * becomes the newline frame, mirroring `ENTER_SEQUENCES`).
55
+ */
56
+ export declare function translateWin32Key(record: Win32KeyRecord): string;
57
+ /**
58
+ * Length of the trailing run of `text` that could still complete into a
59
+ * win32-input-mode record, or 0. Longest match, mirroring the other families.
60
+ */
61
+ export declare function trailingWin32PrefixLength(text: string): number;
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Win32-input-mode translation (win32 keyboard-enhancement section of the
3
+ * Shift+Enter newline fix).
4
+ *
5
+ * WHY THIS EXISTS. With `CSI ? 9001 h` the Windows console (conhost inside
6
+ * ConPTY included -- verified byte-level on both conhost and Windows Terminal
7
+ * 1.24, see docs/diagnoses/shift-enter-newline-dead-by-default) reports EVERY
8
+ * key as `ESC [ vk ; scan ; char ; keyDown ; mods ; repeat _`. That is the
9
+ * only Windows-side encoding in which Shift+Enter is distinguishable from
10
+ * Enter: mods carries SHIFT_PRESSED (0x10) on the record. Nothing else
11
+ * survives ConPTY: kitty `CSI > 1 u` is swallowed (a `CSI ? u` query never
12
+ * answers) and modifyOtherKeys is a no-op.
13
+ *
14
+ * THE CONTRACT. `splitWin32KeySequences` recognises the records in a chunk;
15
+ * `translateWin32Key` rewrites each one to the LEGACY bytes the same keypress
16
+ * would have produced with the mode off -- except the modified-Enter family,
17
+ * which becomes `ENTER_NEWLINE_FRAME` exactly like `enter-sequences.ts` maps
18
+ * its CSI-u cousins. Ink therefore sees the stream it has always seen. Key-up
19
+ * records are dropped (every press arrives as a down/up pair in one chunk).
20
+ *
21
+ * Pure: no node/React imports, callable from any test environment.
22
+ */
23
+ import { ENTER_NEWLINE_FRAME } from './limits.js';
24
+ /** Enable / disable win32-input-mode (private mode 9001). */
25
+ export const WIN32_INPUT_ENABLE = '\x1b[?9001h';
26
+ export const WIN32_INPUT_DISABLE = '\x1b[?9001l';
27
+ /** Windows console `dwControlKeyState` modifier bits this module cares about. */
28
+ const RIGHT_ALT = 0x1;
29
+ const LEFT_ALT = 0x2;
30
+ const RIGHT_CTRL = 0x4;
31
+ const LEFT_CTRL = 0x8;
32
+ const SHIFT = 0x10;
33
+ /** Lock states, ENHANCED_KEY and friends must never look like modifiers. */
34
+ const MODIFIER_MASK = SHIFT | LEFT_CTRL | RIGHT_CTRL | LEFT_ALT | RIGHT_ALT;
35
+ /** Virtual-key codes the translator names explicitly. */
36
+ const VK_BACK = 0x08;
37
+ const VK_TAB = 0x09;
38
+ const VK_ENTER = 0x0d;
39
+ const VK_ESCAPE = 0x1b;
40
+ const VK_PACKET = 0xe7;
41
+ /**
42
+ * Parse one candidate body (`digits and semicolons`, the `_` excluded).
43
+ * Returns null when the field count is impossible for a real record -- that
44
+ * is what keeps a random `ESC [ 1 ; 2 3 _`-shaped paste from being eaten.
45
+ */
46
+ function parseRecord(body) {
47
+ const fields = body.split(';');
48
+ if (fields.length !== 6)
49
+ return null;
50
+ const nums = fields.map(Number);
51
+ if (nums.some((n) => !Number.isInteger(n) || n < 0 || n > 0x10ffff))
52
+ return null;
53
+ const [vk, , char, down, mods, repeat] = nums;
54
+ if (vk === undefined || char === undefined || down === undefined)
55
+ return null;
56
+ if (mods === undefined || repeat === undefined)
57
+ return null;
58
+ if (vk > 0xff)
59
+ return null;
60
+ return { vk, scan: nums[1] ?? 0, char, down: down === 1, mods, repeat };
61
+ }
62
+ /**
63
+ * Split a chunk into text spans and win32-input-mode records.
64
+ *
65
+ * MATCHES ONLY `ESC [ <digits/semicolons> _`: mouse reports need `<`, paste
66
+ * markers end in `~`, CSI-u ends in `u`, DECRPM replies start with `?`. A
67
+ * `_`-terminated all-numeric-parameter sequence is therefore unambiguous in
68
+ * the stream this filter ever sees.
69
+ */
70
+ export function splitWin32KeySequences(text) {
71
+ const segments = [];
72
+ let start = 0;
73
+ let index = 0;
74
+ while (index < text.length) {
75
+ if (text[index] !== '\x1b' || text[index + 1] !== '[') {
76
+ index += 1;
77
+ continue;
78
+ }
79
+ let end = index + 2;
80
+ while (end < text.length && text[end] !== '_') {
81
+ const c = text[end];
82
+ if (c < '0' || c > '9') {
83
+ if (c !== ';') {
84
+ end = -1;
85
+ break;
86
+ }
87
+ }
88
+ end += 1;
89
+ }
90
+ if (end === -1 || end >= text.length) {
91
+ index += 1;
92
+ continue;
93
+ }
94
+ const record = parseRecord(text.slice(index + 2, end));
95
+ if (!record) {
96
+ index += 1;
97
+ continue;
98
+ }
99
+ if (index > start)
100
+ segments.push({ kind: 'text', text: text.slice(start, index) });
101
+ segments.push({ kind: 'key', record });
102
+ index = end + 1;
103
+ start = index;
104
+ }
105
+ if (start < text.length)
106
+ segments.push({ kind: 'text', text: text.slice(start) });
107
+ return segments;
108
+ }
109
+ /** Legacy modifier parameter (xterm): 1 + shift + 2*alt + 4*ctrl. */
110
+ function legacyModifierParam(m) {
111
+ let p = 1;
112
+ if (m & SHIFT)
113
+ p += 1;
114
+ if (m & (LEFT_ALT | RIGHT_ALT))
115
+ p += 2;
116
+ if (m & (LEFT_CTRL | RIGHT_CTRL))
117
+ p += 4;
118
+ return p;
119
+ }
120
+ /** Functional keys named by vk, as (letter | tilde-code) legacy shapes. */
121
+ const FUNCTIONAL_KEYS = new Map([
122
+ [0x21, { code: 5 }], // PgUp
123
+ [0x22, { code: 6 }], // PgDn
124
+ [0x23, { letter: 'F' }], // End
125
+ [0x24, { letter: 'H' }], // Home
126
+ [0x25, { letter: 'D' }], // Left
127
+ [0x26, { letter: 'A' }], // Up
128
+ [0x27, { letter: 'C' }], // Right
129
+ [0x28, { letter: 'B' }], // Down
130
+ [0x2d, { code: 2 }], // Insert
131
+ [0x2e, { code: 3 }], // Delete
132
+ [0x70, { letter: 'P' }], // F1
133
+ [0x71, { letter: 'Q' }], // F2
134
+ [0x72, { letter: 'R' }], // F3
135
+ [0x73, { letter: 'S' }], // F4
136
+ [0x74, { code: 15 }], // F5
137
+ [0x75, { code: 17 }], // F6
138
+ [0x76, { code: 18 }], // F7
139
+ [0x77, { code: 19 }], // F8
140
+ [0x78, { code: 20 }], // F9
141
+ [0x79, { code: 21 }], // F10
142
+ [0x7a, { code: 23 }], // F11
143
+ [0x7b, { code: 24 }], // F12
144
+ ]);
145
+ /** Emit the legacy form of a functional key under `m` modifiers. */
146
+ function functionalLegacy(vk, m) {
147
+ const key = FUNCTIONAL_KEYS.get(vk);
148
+ if (!key)
149
+ return '';
150
+ if (m === 0) {
151
+ return 'letter' in key
152
+ ? (key.letter === 'P' || key.letter === 'Q' || key.letter === 'R' || key.letter === 'S'
153
+ ? `\x1bO${key.letter}`
154
+ : `\x1b[${key.letter}`)
155
+ : `\x1b[${key.code}~`;
156
+ }
157
+ const p = legacyModifierParam(m);
158
+ return 'letter' in key ? `\x1b[1;${p}${key.letter}` : `\x1b[${key.code};${p}~`;
159
+ }
160
+ /** Control code for Ctrl+letter, or the char itself when not a letter. */
161
+ function ctrlByte(char) {
162
+ if (char >= 97 && char <= 122)
163
+ return char - 96; // a-z -> 1-26
164
+ if (char >= 65 && char <= 90)
165
+ return char - 64; // A-Z -> 1-26
166
+ return char;
167
+ }
168
+ const isPrintable = (char) => char >= 0x20 && char !== 0x7f;
169
+ /**
170
+ * Translate one record to the legacy bytes the same keypress would have
171
+ * produced with win32-input-mode off (Enter family excepted: a modified Enter
172
+ * becomes the newline frame, mirroring `ENTER_SEQUENCES`).
173
+ */
174
+ export function translateWin32Key(record) {
175
+ if (!record.down)
176
+ return '';
177
+ const m = record.mods & MODIFIER_MASK;
178
+ const ctrl = (m & (LEFT_CTRL | RIGHT_CTRL)) !== 0;
179
+ const alt = (m & (LEFT_ALT | RIGHT_ALT)) !== 0;
180
+ const shift = (m & SHIFT) !== 0;
181
+ const char = record.char === 0 ? '' : String.fromCodePoint(record.char);
182
+ // AltGr arrives as RightCtrl|RightAlt (or lone RightAlt) WITH the composed
183
+ // character already in `char`; prefixing ESC or control-translating it would
184
+ // break every non-US layout. Plain typing first, exotic combos never.
185
+ if (isPrintable(record.char) && (m === RIGHT_CTRL + RIGHT_ALT || m === RIGHT_ALT)) {
186
+ return char;
187
+ }
188
+ switch (record.vk) {
189
+ case VK_ENTER:
190
+ // A modified Enter NEVER submits -- same rule as `ENTER_SEQUENCES`.
191
+ return m === 0 ? '\r' : ENTER_NEWLINE_FRAME;
192
+ case VK_TAB:
193
+ return shift ? '\x1b[Z' : '\t';
194
+ case VK_ESCAPE:
195
+ return alt ? '\x1b\x1b' : '\x1b';
196
+ case VK_BACK:
197
+ // POST-normalisation bytes: `handleText` rewrites DEL (0x7f) to BS
198
+ // (0x08) for Ink, and translated records bypass that walk -- emitting
199
+ // the final form keeps the stream byte-identical to the baseline.
200
+ if (alt)
201
+ return '\x1b\x08';
202
+ return '\x08';
203
+ default:
204
+ break;
205
+ }
206
+ if (record.char !== 0 && record.char < 0x20 && record.vk !== VK_PACKET) {
207
+ // Ctrl combos: Windows already resolves the control character (Ctrl+C is
208
+ // 0x03). Alt keeps the ESC prefix; Ctrl+Alt prefixes ESC over the code.
209
+ if (!ctrl && !alt && !shift)
210
+ return char;
211
+ if (!ctrl && alt)
212
+ return `\x1b${char}`;
213
+ return ctrl ? `${alt ? '\x1b' : ''}${char}` : char;
214
+ }
215
+ if (isPrintable(record.char)) {
216
+ if (ctrl) {
217
+ const code = ctrlByte(record.char);
218
+ return `${alt ? '\x1b' : ''}${code === record.char ? char : String.fromCharCode(code)}`;
219
+ }
220
+ if (alt)
221
+ return `\x1b${char}`;
222
+ return char;
223
+ }
224
+ return functionalLegacy(record.vk, m);
225
+ }
226
+ /** True when `s` is a PROPER prefix of some win32-input-mode record. */
227
+ function isWin32Prefix(s) {
228
+ // A bare ESC (or ESC + anything else) belongs to the Enter/mouse/paste
229
+ // families' hold; this family only ever extends `\x1b[` with digits and
230
+ // semicolons, up to but never including the `_`.
231
+ if (s.length < 3 || s[0] !== '\x1b' || s[1] !== '[')
232
+ return false;
233
+ for (let i = 2; i < s.length; i += 1) {
234
+ const c = s[i];
235
+ if ((c < '0' || c > '9') && c !== ';')
236
+ return false;
237
+ }
238
+ return true;
239
+ }
240
+ /**
241
+ * Length of the trailing run of `text` that could still complete into a
242
+ * win32-input-mode record, or 0. Longest match, mirroring the other families.
243
+ */
244
+ export function trailingWin32PrefixLength(text) {
245
+ const max = Math.min(text.length, 32);
246
+ for (let k = max; k >= 1; k -= 1) {
247
+ if (isWin32Prefix(text.slice(text.length - k)))
248
+ return k;
249
+ }
250
+ return 0;
251
+ }
@@ -8,6 +8,7 @@
8
8
  import type { Message, ModelRef } from '@aragon-agent/core';
9
9
  import type { Entry } from '../agent/reducer.js';
10
10
  import type { TodoItem } from '../todo/types.js';
11
+ import { type CompactionIdentity } from '../compaction/memory-identity.js';
11
12
  /**
12
13
  * Bookkeeping `aragon exec` attaches to the sessions it owns
13
14
  * (cli-integration-surface §5.2).
@@ -41,6 +42,7 @@ export interface SavedSession {
41
42
  savedAt: number;
42
43
  model?: ModelRef;
43
44
  messages: Message[];
45
+ compactionIdentity?: CompactionIdentity;
44
46
  entries: Entry[];
45
47
  /**
46
48
  * The live todo list at save time (todo-plan-execution §3.13).
@@ -79,6 +81,7 @@ export declare function resolveSessionPath(nameOrPath: string | undefined, cwd:
79
81
  export declare function saveSession(filePath: string, data: {
80
82
  model: ModelRef;
81
83
  messages: Message[];
84
+ compactionIdentity?: CompactionIdentity;
82
85
  entries: Entry[];
83
86
  todos: TodoItem[];
84
87
  meta?: SessionMeta;
@@ -9,6 +9,8 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
9
9
  import { dirname, isAbsolute, join, resolve } from 'node:path';
10
10
  import { getSessionsDir } from '../config/store.js';
11
11
  import { validateSession } from './validate-session.js';
12
+ import { validateCompactionIdentity, verifyCompactionIdentity, } from '../compaction/memory-identity.js';
13
+ import { getLogger } from '../logging/logger.js';
12
14
  const SESSION_VERSION = 1;
13
15
  /** Resolve a `/save` target: absolute/relative path, or a name in the data dir. */
14
16
  export function resolveSessionPath(nameOrPath, cwd) {
@@ -92,6 +94,12 @@ function stripServiceTail(entries) {
92
94
  * payload with no such key and a file byte-identical to today's.
93
95
  */
94
96
  export function saveSession(filePath, data) {
97
+ if (data.compactionIdentity) {
98
+ validateCompactionIdentity(data.compactionIdentity);
99
+ if (!verifyCompactionIdentity(data.messages, data.compactionIdentity)) {
100
+ getLogger().debug('agent', 'invalid_compaction_session_identity');
101
+ }
102
+ }
95
103
  const dir = dirname(filePath);
96
104
  if (!existsSync(dir))
97
105
  mkdirSync(dir, { recursive: true });
@@ -100,6 +108,7 @@ export function saveSession(filePath, data) {
100
108
  savedAt: Date.now(),
101
109
  model: data.model,
102
110
  messages: data.messages,
111
+ ...(data.compactionIdentity ? { compactionIdentity: data.compactionIdentity } : {}),
103
112
  entries: stripServiceTail(stripLiveToolOutput(data.entries)),
104
113
  todos: data.todos,
105
114
  ...(data.meta ? { meta: data.meta } : {}),
@@ -159,6 +159,8 @@ export function writeSession(input) {
159
159
  saveSession(path, {
160
160
  model: input.session.model,
161
161
  messages: input.session.messages,
162
+ ...(input.session.compactionIdentity
163
+ ? { compactionIdentity: input.session.compactionIdentity } : {}),
162
164
  entries: input.session.entries,
163
165
  todos: input.session.todos ?? [],
164
166
  meta: input.meta,
@@ -1,6 +1,8 @@
1
1
  import { isAdapterProvider } from '../config/schema.js';
2
2
  import { normalizeTodos } from '../todo/normalize.js';
3
3
  import { TODO_LIMITS } from '../todo/limits.js';
4
+ import { validateCompactionIdentity } from '../compaction/memory-identity.js';
5
+ import { MemoryError } from '../compaction/memory.js';
4
6
  function fail(path, expected) {
5
7
  throw new Error(`Invalid session file: ${path} must be ${expected}.`);
6
8
  }
@@ -26,6 +28,25 @@ const boolean = (value, path) => {
26
28
  if (typeof value !== 'boolean')
27
29
  fail(path, 'a boolean');
28
30
  };
31
+ const nonnegative = (value, path) => {
32
+ number(value, path);
33
+ if (value < 0)
34
+ fail(path, 'non-negative');
35
+ };
36
+ const positive = (value, path) => {
37
+ number(value, path);
38
+ if (value <= 0)
39
+ fail(path, 'positive');
40
+ };
41
+ const threshold = (value, path) => {
42
+ number(value, path);
43
+ if (value < 0.5 || value > 0.95)
44
+ fail(path, 'between 0.5 and 0.95');
45
+ };
46
+ const memoryVersion = (value, path) => {
47
+ if (value !== 2)
48
+ fail(path, '2');
49
+ };
29
50
  const object = (value, path) => { record(value, path); };
30
51
  function optional(check) {
31
52
  return (value, path) => { if (value !== undefined)
@@ -149,6 +170,9 @@ const ENTRY_FIELDS = {
149
170
  text: optional(string), detail: optional(string), turn: number,
150
171
  durationMs: optional(number), live: boolean },
151
172
  compaction: {
173
+ decision: optional(shape({ occupied: nonnegative, contextWindow: positive,
174
+ threshold, source: enumeration('usage', 'estimate'), deltaTokens: nonnegative })),
175
+ memoryVersion: optional(memoryVersion),
152
176
  index: number, trigger: enumeration('pressure', 'overflow', 'manual'),
153
177
  mode: enumeration('summarized', 'truncated', 'relieved', 'none'), applied: boolean,
154
178
  reason: optional(string), messagesBefore: number, messagesAfter: number,
@@ -198,6 +222,16 @@ export function validateSession(value) {
198
222
  validateEntries(source.entries);
199
223
  if (source.model !== undefined)
200
224
  validateModel(source.model);
225
+ if (source.compactionIdentity !== undefined) {
226
+ try {
227
+ validateCompactionIdentity(source.compactionIdentity);
228
+ }
229
+ catch (error) {
230
+ if (!(error instanceof MemoryError))
231
+ throw error;
232
+ fail('compactionIdentity', 'a valid adopted-memory identity');
233
+ }
234
+ }
201
235
  // Missing fields remain missing in old files, including exec metadata.
202
236
  const prepared = { ...source };
203
237
  if (source.todos !== undefined) {
@@ -331,6 +331,14 @@ export function createSubagent(spec, deps, hooks) {
331
331
  }
332
332
  break;
333
333
  }
334
+ case 'compaction_end':
335
+ contextManager?.onCompactionEnd({
336
+ applied: event.applied,
337
+ ...(event.reason !== undefined ? { reason: event.reason } : {}),
338
+ tokensBefore: event.estimatedTokensBefore,
339
+ tokensAfter: event.estimatedTokensAfter,
340
+ });
341
+ break;
334
342
  case 'tool_execution_start':
335
343
  run.phase = event.toolName === 'team_wait' ? 'waiting' : 'tool';
336
344
  run.lastTool = event.toolName;