@aragon-agent/cli 0.6.14 → 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 (113) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +250 -233
  3. package/dist/agent/controller.d.ts +27 -1
  4. package/dist/agent/controller.js +182 -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/commands/builtins.js +2 -2
  13. package/dist/compaction/child.d.ts +9 -1
  14. package/dist/compaction/child.js +39 -13
  15. package/dist/compaction/command.js +20 -10
  16. package/dist/compaction/compactor.d.ts +33 -204
  17. package/dist/compaction/compactor.js +311 -726
  18. package/dist/compaction/digest.d.ts +15 -51
  19. package/dist/compaction/digest.js +113 -148
  20. package/dist/compaction/limits.d.ts +19 -142
  21. package/dist/compaction/limits.js +20 -147
  22. package/dist/compaction/memory-identity.d.ts +16 -0
  23. package/dist/compaction/memory-identity.js +63 -0
  24. package/dist/compaction/memory-input.d.ts +25 -0
  25. package/dist/compaction/memory-input.js +106 -0
  26. package/dist/compaction/memory-render.d.ts +4 -0
  27. package/dist/compaction/memory-render.js +47 -0
  28. package/dist/compaction/memory-validation.d.ts +21 -0
  29. package/dist/compaction/memory-validation.js +268 -0
  30. package/dist/compaction/memory.d.ts +79 -0
  31. package/dist/compaction/memory.js +118 -0
  32. package/dist/compaction/meter.d.ts +14 -10
  33. package/dist/compaction/meter.js +53 -19
  34. package/dist/compaction/operation.d.ts +18 -0
  35. package/dist/compaction/operation.js +68 -0
  36. package/dist/compaction/pressure.d.ts +5 -20
  37. package/dist/compaction/pressure.js +13 -23
  38. package/dist/compaction/summarize-call.d.ts +3 -16
  39. package/dist/compaction/summarize-call.js +20 -20
  40. package/dist/compaction/summary-prompt.d.ts +29 -66
  41. package/dist/compaction/summary-prompt.js +132 -178
  42. package/dist/compaction/types.d.ts +21 -3
  43. package/dist/compaction/wiring.d.ts +10 -25
  44. package/dist/compaction/wiring.js +56 -66
  45. package/dist/config/schema.d.ts +1 -1
  46. package/dist/config/schema.js +1 -1
  47. package/dist/exec/events.d.ts +8 -0
  48. package/dist/exec/index.js +13 -6
  49. package/dist/exec/runner.d.ts +8 -0
  50. package/dist/exec/runner.js +26 -2
  51. package/dist/session/persist.d.ts +3 -0
  52. package/dist/session/persist.js +9 -0
  53. package/dist/session/store.js +2 -0
  54. package/dist/session/validate-session.js +34 -0
  55. package/dist/team/subagent.js +8 -0
  56. package/dist/ui/App.js +218 -116
  57. package/dist/ui/BottomStatusRow.d.ts +5 -10
  58. package/dist/ui/BottomStatusRow.js +7 -11
  59. package/dist/ui/Composer.d.ts +4 -2
  60. package/dist/ui/Composer.js +2 -2
  61. package/dist/ui/Header.d.ts +6 -2
  62. package/dist/ui/Header.js +7 -6
  63. package/dist/ui/PromptInput.d.ts +3 -1
  64. package/dist/ui/PromptInput.js +32 -41
  65. package/dist/ui/StatusBar.d.ts +6 -0
  66. package/dist/ui/StatusBar.js +8 -7
  67. package/dist/ui/Transcript.js +1 -1
  68. package/dist/ui/entries/CompactionCard.d.ts +3 -1
  69. package/dist/ui/entries/CompactionCard.js +7 -5
  70. package/dist/ui/frame-differ.js +27 -0
  71. package/dist/ui/frame-observer.js +18 -5
  72. package/dist/ui/interaction-copy.d.ts +73 -73
  73. package/dist/ui/interaction-copy.js +30 -38
  74. package/dist/ui/interaction-hints.d.ts +8 -1
  75. package/dist/ui/interaction-hints.js +56 -69
  76. package/dist/ui/layout/AppShell.d.ts +6 -2
  77. package/dist/ui/layout/AppShell.js +2 -2
  78. package/dist/ui/layout/OverlayFrame.d.ts +2 -0
  79. package/dist/ui/layout/OverlayFrame.js +23 -14
  80. package/dist/ui/layout/ScrollViewport.d.ts +3 -1
  81. package/dist/ui/layout/ScrollViewport.js +10 -2
  82. package/dist/ui/layout/budget.d.ts +25 -26
  83. package/dist/ui/layout/budget.js +29 -22
  84. package/dist/ui/layout/document-layout.d.ts +2 -2
  85. package/dist/ui/layout/document-layout.js +2 -3
  86. package/dist/ui/layout/queue-layout.js +4 -4
  87. package/dist/ui/layout/status-detail-layout.d.ts +9 -0
  88. package/dist/ui/layout/status-detail-layout.js +56 -0
  89. package/dist/ui/layout/status-layout.d.ts +19 -2
  90. package/dist/ui/layout/status-layout.js +95 -84
  91. package/dist/ui/layout/wrap-rows.d.ts +2 -0
  92. package/dist/ui/layout/wrap-rows.js +21 -7
  93. package/dist/ui/overlays/ConfirmDialog.d.ts +2 -1
  94. package/dist/ui/overlays/ConfirmDialog.js +22 -3
  95. package/dist/ui/overlays/HelpOverlay.js +9 -2
  96. package/dist/ui/overlays/ModelPicker.d.ts +5 -8
  97. package/dist/ui/overlays/ModelPicker.js +44 -10
  98. package/dist/ui/overlays/ModelProfileEditor.js +2 -0
  99. package/dist/ui/overlays/ModelProfilePicker.d.ts +1 -0
  100. package/dist/ui/overlays/ModelProfilePicker.js +4 -2
  101. package/dist/ui/overlays/PlanReviewOverlay.d.ts +2 -1
  102. package/dist/ui/overlays/PlanReviewOverlay.js +7 -4
  103. package/dist/ui/overlays/QuestionOverlay.d.ts +2 -1
  104. package/dist/ui/overlays/QuestionOverlay.js +45 -32
  105. package/dist/ui/overlays/QueueOverlay.js +2 -2
  106. package/dist/ui/overlays/SettingsScreen.d.ts +1 -0
  107. package/dist/ui/overlays/SettingsScreen.js +10 -3
  108. package/dist/ui/run-metrics.d.ts +23 -0
  109. package/dist/ui/run-metrics.js +27 -0
  110. package/dist/ui/status-feedback.d.ts +33 -0
  111. package/dist/ui/status-feedback.js +36 -0
  112. package/dist/ui/stdout-frame-writer.js +28 -0
  113. package/package.json +2 -2
@@ -1,172 +1,45 @@
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 const COMPACTION_LIMITS = {
23
- /** Ceiling on the rendered digest, before the summarizer-window check. ~30k tokens. */
24
3
  digestMaxChars: 120_000,
25
- /** Output + system prompt + framing, kept out of the digest's share of the window. */
26
4
  summarizerReserveTokens: 8_000,
27
- // --- Per-message clips in the digest (§3.6.3). ---
28
- userChars: 2_000,
29
- assistantChars: 2_000,
30
- toolArgChars: 200,
31
- toolResultChars: 600,
32
- /**
33
- * Clip bound on the TAGGED text anchor.
34
- *
35
- * A CLIP, NOT A SHAPE SWITCH (D-21 / P1-9). v1 of the design kept the anchor
36
- * verbatim-standalone past this length and merged it into the block below it,
37
- * which made the anchor rule LENGTH-DEPENDENT and therefore non-idempotent: on
38
- * the second compaction the "first user message" is a summary. A 4 000-character
39
- * first message clipped with a visible marker keeps the rule "one leading
40
- * anchor, always recognizable"; a 40 000-character first message is not a goal
41
- * statement, and truncating it with a marker is the honest outcome.
42
- */
43
- anchorChars: 4_000,
44
- /** What the summarizer prompt asks for, and what the splice clips to. */
45
- summaryMaxChars: 6_000,
46
- summaryOutputTokens: 2_048,
47
- /**
48
- * Wall-clock budget for ONE summarization call.
49
- *
50
- * Longer than the fast reviewer's 20 s because the input is ~30x larger and the
51
- * output ~4x larger, and because the cost of a timeout here is a DEGRADED RUN
52
- * rather than a missing critique.
53
- *
54
- * COUPLED TO THE ENGINE'S `COMPACTION_HARD_TIMEOUT_MS` (120 s). The worst legal
55
- * host budget is two calls at this value back to back plus the digest render,
56
- * so `2 * callTimeoutMs` must stay strictly under the engine's ceiling. Raising
57
- * this without raising that makes the §3.7 ladder unreachable and turns every
58
- * slow-but-correct summarization into `manager_timeout`.
59
- */
5
+ summaryMaxChars: 32_000,
6
+ summaryOutputTokens: 4_096,
7
+ memoryJsonChars: 32_000,
8
+ memoryEnvelopeChars: 1_024,
9
+ protectedMemoryChars: 48_000,
10
+ memoryItems: 200,
11
+ protectedUserMessages: 200,
12
+ memoryAdditions: 64,
13
+ coverageClips: 200,
14
+ jsonDepth: 32,
15
+ instructionsChars: 4_000,
16
+ itemTextChars: 1_200,
17
+ sourceExcerptChars: 512,
18
+ itemSources: 8,
19
+ toolResultEdgeChars: 1_500,
20
+ operationTimeoutMs: 120_000,
60
21
  callTimeoutMs: 45_000,
61
- // --- Anti-loop guards (§3.8). ---
62
- /** Turns between compactions. Never two in a row: a compaction that just ran
63
- * needs a real turn before its effect is measurable, because `lastUsage` is
64
- * still the pre-compaction number until the next `turn_end`. */
65
22
  minTurnsBetween: 2,
66
- /** Compactions STARTED per run, both triggers, hard. */
67
23
  maxPerRun: 5,
68
- /** A splice must project occupancy below `threshold - this`, or it bought two turns. */
69
24
  minReclaimRatio: 0.15,
70
- /** Consecutive no-progress PRESSURE compactions before the proactive trigger self-disables. */
71
25
  stuckLimit: 2,
72
- // --- Tail relief (hardening §3.3 / W2). ---
73
- /**
74
- * Clip bound on an oversized `tool_result` body inside the RETAINED tail.
75
- *
76
- * 2 000 rather than `toolResultChars`' 600, and the two are not the same
77
- * question. That one bounds a message in the DIGEST, which the summarizer
78
- * reads once and which exists to be lossy. This one bounds a message the model
79
- * keeps WORKING FROM: it is the last two-to-four turns, and the run is about to
80
- * continue on them. Relief only ever runs when the alternative is a request the
81
- * provider cannot accept.
82
- */
26
+ // Legacy capacity helpers remain callable, but v2 never mutates retained tails.
83
27
  tailToolResultChars: 2_000,
84
- /**
85
- * Room reserved for the `<compacted_context>` block in the tail-budget
86
- * projection.
87
- *
88
- * THE BLOCK IS WRITTEN AFTER THE PROJECTION, which is why this exists at all:
89
- * relief measures a spliced history whose summary body is already in it, but
90
- * the `plan === null` path measures one where nothing has been written yet, and
91
- * a budget with no allowance would let the block push the result back over.
92
- */
93
28
  blockAllowanceTokens: 2_000,
94
- // --- The archive (hardening §3.5 / W4). ---
95
- /**
96
- * Files kept for ONE `runId`, newest first.
97
- *
98
- * PER RUN, NOT PER DIRECTORY (RV-6). `<home>/compaction/` is shared by every
99
- * `aragon` on the machine — `file-sink.ts:94-95` already treats concurrent
100
- * processes as normal — so a global count-based prune would delete a live
101
- * session's archives out from under it.
102
- */
103
29
  archiveMaxFiles: 20,
104
- /**
105
- * The cross-run sweep: 7 days.
106
- *
107
- * AGE RATHER THAN COUNT, for the reason above. Age can only ever delete files
108
- * whose run finished a week ago; a count can delete the newest file another
109
- * process wrote a second ago.
110
- */
111
30
  archiveMaxAgeMs: 604_800_000,
112
- /**
113
- * Per-file ceiling before dropped message bodies are clipped from the OLDEST
114
- * end. The metadata and the summary are never clipped: a truncated archive
115
- * that still says what happened beats none.
116
- */
117
31
  archiveMaxBytes: 8_000_000,
118
- // --- Sub-agent compaction (hardening §3.4 / W3). ---
119
- /** A child's `keepRecentTurns`, delivered through its config view. */
120
32
  childKeepRecentTurns: 2,
121
- /**
122
- * A child's guard-2 ceiling, delivered through `Compactor`'s instance bound.
123
- *
124
- * HERE RATHER THAN AS A `compaction.*` KEY, and the placement is the whole
125
- * argument: `maxPerRun` is STRUCTURAL (see this file's header), there is no
126
- * config path to it by design, and a child needs a tighter one. Both numbers
127
- * therefore stay in the one file that owns structural bounds.
128
- */
129
33
  childMaxPerRun: 2,
130
- /** Rows the collapsed transcript card renders; Ctrl+O expands. */
131
34
  cardTextRows: 6,
132
- /**
133
- * Below this many columns the status chip drops.
134
- *
135
- * 100, matching all four existing peers - `retry-view.ts`, `fast/limits.ts`,
136
- * `team/limits.ts`, `todo/limits.ts` (P2-2). A status-bar degradation ladder
137
- * that is nearly-but-not-quite uniform is a reading hazard bought for nothing.
138
- * The guaranteed reporting surface is `/compact status`, which is what makes
139
- * dropping the chip acceptable at all.
140
- */
141
35
  statusCompactCols: 100,
142
36
  };
143
- /**
144
- * Bumped whenever the wording of the `<compacted_context>` block or the
145
- * summarizer system prompt changes, so a behaviour report can be tied to a block
146
- * revision with one grep - exactly what `FAST_BLOCK_VERSION` and
147
- * `TEAM_BLOCK_VERSION` are for.
148
- */
149
- export const COMPACTION_BLOCK_VERSION = 'v1-2026-08';
150
- /** Opening tag of the verbatim goal anchor (§3.6.5). */
37
+ export const COMPACTION_BLOCK_VERSION = 'v2-2026-10';
151
38
  export const ANCHOR_TAG_OPEN = '<original_task>';
152
39
  export const ANCHOR_TAG_CLOSE = '</original_task>';
153
- /** Opening tag of the spliced summary block. Recognized on the NEXT compaction. */
154
40
  export const BLOCK_TAG_OPEN = '<compacted_context';
155
41
  export const BLOCK_TAG_CLOSE = '</compacted_context>';
156
- /** Marker appended to an anchor clipped at `anchorChars`. ASCII (see the header). */
157
- export const ANCHOR_TRUNCATION_MARK = ' [truncated]';
158
- /**
159
- * What tail relief writes where it removed text (hardening §3.3.2 rule 4).
160
- *
161
- * IN THE HISTORY THE MODEL READS, at the exact place the data went missing. A
162
- * model that sees this knows the output is partial and can re-run the tool; a
163
- * model handed a silently truncated file cannot tell the difference between "the
164
- * file ends here" and "the file was cut here".
165
- *
166
- * THE HOST OWNS IT, not core: `engine/compaction.ts` has no glyph vocabulary and
167
- * must not learn one, so `relieveTail` takes this as a callback. ASCII, per this
168
- * file's header.
169
- */
42
+ /** Retained legacy helper for explicit callers; v2 compression never clips the tail. */
170
43
  export function tailClipMarker(removed) {
171
44
  return `\n[... ${removed} characters removed by context compaction ...]`;
172
45
  }
@@ -0,0 +1,16 @@
1
+ import type { Message } from '@aragon-agent/core';
2
+ export interface CompactionIdentity {
3
+ version: 1;
4
+ anchorIndex: 0;
5
+ blockIndex: 1;
6
+ generation: number;
7
+ prefixSha256: string;
8
+ }
9
+ /** Validate the optional session credential shape; throws on unknown or bad fields. */
10
+ export declare function validateCompactionIdentity(value: unknown): asserts value is CompactionIdentity;
11
+ /** Serialize the complete prefix, including content parts and timestamps, in fixed order. */
12
+ export declare function serializeCompactionPrefix(messages: readonly Message[]): string;
13
+ /** Create credentials only at the host's successful adoption boundary. */
14
+ export declare function createCompactionIdentity(messages: readonly Message[]): CompactionIdentity;
15
+ /** Verify persisted credentials without mutating history or repairing missing identity. */
16
+ export declare function verifyCompactionIdentity(messages: readonly Message[], identity: unknown): identity is CompactionIdentity;
@@ -0,0 +1,63 @@
1
+ /** Adopted-prefix credentials for process-local and saved-session continuity. */
2
+ import { createHash } from 'node:crypto';
3
+ import { canonicalUserMessage, MemoryError } from './memory.js';
4
+ import { parseCompactedBlock } from './summary-prompt.js';
5
+ /** Validate the optional session credential shape; throws on unknown or bad fields. */
6
+ export function validateCompactionIdentity(value) {
7
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
8
+ throw new MemoryError('invalid_prior_memory', '$.identity');
9
+ }
10
+ const record = value;
11
+ const keys = ['version', 'anchorIndex', 'blockIndex', 'generation', 'prefixSha256'];
12
+ if (Object.keys(record).length !== keys.length ||
13
+ Object.keys(record).some((key) => !keys.includes(key))) {
14
+ throw new MemoryError('invalid_prior_memory', '$.identity.keys');
15
+ }
16
+ if (record.version !== 1 || record.anchorIndex !== 0 || record.blockIndex !== 1) {
17
+ throw new MemoryError('invalid_prior_memory', '$.identity.prefix');
18
+ }
19
+ if (!Number.isSafeInteger(record.generation) || record.generation < 1) {
20
+ throw new MemoryError('invalid_prior_memory', '$.identity.generation');
21
+ }
22
+ if (typeof record.prefixSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(record.prefixSha256)) {
23
+ throw new MemoryError('invalid_prior_memory', '$.identity.hash');
24
+ }
25
+ }
26
+ function prefix(messages) {
27
+ const anchor = messages[0];
28
+ const block = messages[1];
29
+ if (anchor?.role !== 'user' || block?.role !== 'user') {
30
+ throw new MemoryError('invalid_prior_memory', '$.prefix.roles');
31
+ }
32
+ return [anchor, block];
33
+ }
34
+ /** Serialize the complete prefix, including content parts and timestamps, in fixed order. */
35
+ export function serializeCompactionPrefix(messages) {
36
+ return JSON.stringify(prefix(messages).map(canonicalUserMessage));
37
+ }
38
+ /** Create credentials only at the host's successful adoption boundary. */
39
+ export function createCompactionIdentity(messages) {
40
+ const [, block] = prefix(messages);
41
+ const { memory } = parseCompactedBlock(block);
42
+ return {
43
+ version: 1,
44
+ anchorIndex: 0,
45
+ blockIndex: 1,
46
+ generation: memory.generation,
47
+ prefixSha256: createHash('sha256').update(serializeCompactionPrefix(messages)).digest('hex'),
48
+ };
49
+ }
50
+ /** Verify persisted credentials without mutating history or repairing missing identity. */
51
+ export function verifyCompactionIdentity(messages, identity) {
52
+ try {
53
+ validateCompactionIdentity(identity);
54
+ const current = createCompactionIdentity(messages);
55
+ return current.generation === identity.generation
56
+ && current.prefixSha256 === identity.prefixSha256;
57
+ }
58
+ catch (error) {
59
+ if (error instanceof MemoryError)
60
+ return false;
61
+ throw error;
62
+ }
63
+ }
@@ -0,0 +1,25 @@
1
+ /** Prepare protected host input and move multimodal messages behind safe cuts. */
2
+ import { type Message, type UserMessage } from '@aragon-agent/core';
3
+ import { type CompactionMemory, type ProtectedUserMessage } from './memory.js';
4
+ import { type AnchorShape } from './summary-prompt.js';
5
+ import { type CompactionIdentity } from './memory-identity.js';
6
+ export interface PreparedMemoryInput {
7
+ anchor: UserMessage;
8
+ anchorShape: AnchorShape;
9
+ prior?: CompactionMemory;
10
+ generation: number;
11
+ originalTaskSourceId: string;
12
+ userMessages: ProtectedUserMessage[];
13
+ head: Message[];
14
+ headStartIndex: number;
15
+ tail: Message[];
16
+ cutIndex: number;
17
+ legacySummary?: string;
18
+ }
19
+ export interface PrepareMemoryOptions {
20
+ messages: readonly Message[];
21
+ cutIndex: number;
22
+ identity?: CompactionIdentity;
23
+ }
24
+ /** Prepare a snapshot without mutation; throws before payment when protection cannot fit. */
25
+ export declare function prepareMemoryInput(options: PrepareMemoryOptions): PreparedMemoryInput;
@@ -0,0 +1,106 @@
1
+ /** Prepare protected host input and move multimodal messages behind safe cuts. */
2
+ import { findSafeCutIndices } from '@aragon-agent/core';
3
+ import { assertProtectedMemoryBudget, isLegacyMemoryText, MemoryError, } from './memory.js';
4
+ import { parseCompactedBlock } from './summary-prompt.js';
5
+ import { verifyCompactionIdentity } from './memory-identity.js';
6
+ import { COMPACTION_LIMITS as LIMITS } from './limits.js';
7
+ import { buildDigest } from './digest.js';
8
+ function hasImage(message) {
9
+ return message.role !== 'assistant' && Array.isArray(message.content)
10
+ && message.content.some((part) => part.type === 'image');
11
+ }
12
+ function protectImages(messages, cutIndex, anchorIndex) {
13
+ const imageIndex = messages.findIndex((message, index) => index < cutIndex && index !== anchorIndex && hasImage(message));
14
+ if (imageIndex < 0)
15
+ return cutIndex;
16
+ return findSafeCutIndices(messages).filter((index) => index <= imageIndex).at(-1) ?? 0;
17
+ }
18
+ function legacyReference(messages) {
19
+ // This copy is read-only evidence of lost provenance; all originals stay protected.
20
+ const blocks = messages.filter(isLegacyMemoryText);
21
+ return blocks.length
22
+ ? blocks.map((message) => message.content).join('\n\n')
23
+ : undefined;
24
+ }
25
+ function preflightBudget(input) {
26
+ if (input.userMessages.length > LIMITS.protectedUserMessages) {
27
+ throw new MemoryError('protected_memory_too_large', '$.userMessages');
28
+ }
29
+ const { coverage } = buildDigest({
30
+ head: input.head,
31
+ generation: input.generation,
32
+ startIndex: input.headStartIndex,
33
+ });
34
+ const clips = [
35
+ ...(input.prior?.coverage.clippedToolResults ?? []),
36
+ ...coverage.clippedToolResults,
37
+ ];
38
+ if (clips.length > LIMITS.coverageClips) {
39
+ throw new MemoryError('protected_memory_too_large', '$.coverage');
40
+ }
41
+ const minimum = {
42
+ schemaVersion: 2,
43
+ generation: input.generation,
44
+ originalTask: { sourceId: input.originalTaskSourceId, location: 'anchor' },
45
+ userMessages: input.userMessages,
46
+ items: input.prior?.items ?? [],
47
+ ...(input.legacySummary !== undefined ? { legacySummary: input.legacySummary } : {}),
48
+ coverage: {
49
+ summarizedMessages: (input.prior?.coverage.summarizedMessages ?? 0) + coverage.summarizedMessages,
50
+ clippedToolResults: clips,
51
+ legacyIncomplete: !!input.prior?.coverage.legacyIncomplete || input.legacySummary !== undefined,
52
+ },
53
+ };
54
+ assertProtectedMemoryBudget(input.anchor, minimum);
55
+ }
56
+ /** Prepare a snapshot without mutation; throws before payment when protection cannot fit. */
57
+ export function prepareMemoryInput(options) {
58
+ const { messages, identity } = options;
59
+ if (identity && !verifyCompactionIdentity(messages, identity)) {
60
+ throw new MemoryError('invalid_prior_memory', '$.identity');
61
+ }
62
+ const parsed = identity ? parseCompactedBlock(messages[1]) : undefined;
63
+ const prior = parsed?.memory;
64
+ const anchorIndex = identity ? 0 : messages.findIndex((message) => message.role === 'user');
65
+ const anchor = messages[anchorIndex];
66
+ if (anchor?.role !== 'user')
67
+ throw new MemoryError('nothing_to_drop', '$.anchor');
68
+ if (!Number.isSafeInteger(options.cutIndex) ||
69
+ !findSafeCutIndices(messages).includes(options.cutIndex)) {
70
+ throw new MemoryError('invalid_history', '$.cutIndex');
71
+ }
72
+ const generation = (prior?.generation ?? 0) + 1;
73
+ if (!Number.isSafeInteger(generation)) {
74
+ throw new MemoryError('invalid_prior_memory', '$.generation');
75
+ }
76
+ const cutIndex = protectImages(messages, options.cutIndex, anchorIndex);
77
+ const headStartIndex = identity ? 2 : anchorIndex + 1;
78
+ // Core histories normally start with a user; never silently discard a preceding message.
79
+ if (anchorIndex !== 0)
80
+ throw new MemoryError('invalid_history', '$.anchorIndex');
81
+ if (cutIndex <= headStartIndex) {
82
+ throw new MemoryError(cutIndex < options.cutIndex ? 'protected_multimodal_tail' : 'nothing_to_drop', '$.cutIndex');
83
+ }
84
+ const userMessages = [...(prior?.userMessages ?? [])];
85
+ for (let index = headStartIndex; index < cutIndex; index += 1) {
86
+ const message = messages[index];
87
+ if (message.role === 'user')
88
+ userMessages.push({ id: `g${generation}:m${index}`, message });
89
+ }
90
+ const input = {
91
+ anchor,
92
+ anchorShape: parsed?.anchor ?? 'verbatim',
93
+ ...(prior ? { prior } : {}),
94
+ generation,
95
+ originalTaskSourceId: prior?.originalTask.sourceId ?? `g${generation}:m${anchorIndex}`,
96
+ userMessages,
97
+ head: messages.slice(headStartIndex, cutIndex),
98
+ headStartIndex,
99
+ tail: messages.slice(cutIndex),
100
+ cutIndex,
101
+ legacySummary: prior?.legacySummary
102
+ ?? (!identity ? legacyReference(messages.slice(0, cutIndex)) : undefined),
103
+ };
104
+ preflightBudget(input);
105
+ return input;
106
+ }
@@ -0,0 +1,4 @@
1
+ /** Human-readable views; never the authoritative serialized working memory. */
2
+ import type { CompactionMemory } from './memory.js';
3
+ /** Render fixed sections and active chain tails, retaining full chains in JSON only. */
4
+ export declare function renderMemoryMarkdown(memory: CompactionMemory): string;
@@ -0,0 +1,47 @@
1
+ function itemText(item) {
2
+ const status = item.status ? `[${item.status}] ` : '';
3
+ const sources = item.sources
4
+ .map((source) => `${source.messageId} (${source.role}): ${JSON.stringify(source.excerpt)}`)
5
+ .join('; ');
6
+ return `- ${status}${item.text}\n Sources (model extracted): ${sources}`;
7
+ }
8
+ /** Render fixed sections and active chain tails, retaining full chains in JSON only. */
9
+ export function renderMemoryMarkdown(memory) {
10
+ const superseded = new Set(memory.items.flatMap((item) => item.supersedes ? [item.supersedes] : []));
11
+ const active = memory.items.filter((item) => !superseded.has(item.id));
12
+ const select = (...sections) => active
13
+ .filter((item) => sections.includes(item.section))
14
+ .map(itemText)
15
+ .join('\n');
16
+ const requirements = memory.userMessages.map((entry) => {
17
+ const content = entry.message.content;
18
+ return `- ${entry.id}: ${typeof content === 'string'
19
+ ? content
20
+ : content.map((part) => part.type === 'text' ? part.text : '[image retained]').join('\n')}`;
21
+ }).join('\n');
22
+ const sections = [
23
+ ['Goal', `Original task retained verbatim in anchor (${memory.originalTask.sourceId}).`],
24
+ ['User requirements', requirements],
25
+ ['Global context', select('global')],
26
+ ['Decisions', select('decisions')],
27
+ ['Files and facts', select('files', 'facts')],
28
+ ['Tasks', select('tasks')],
29
+ ['Verification', select('verification')],
30
+ ['Pitfalls', select('pitfalls')],
31
+ ['Next steps', select('next')],
32
+ [
33
+ 'Coverage',
34
+ `Generation: ${memory.generation}`
35
+ + `\nSummarized messages: ${memory.coverage.summarizedMessages}`
36
+ + `\nClipped tool results: ${memory.coverage.clippedToolResults.length}`
37
+ + `\nLegacy incomplete: ${memory.coverage.legacyIncomplete}`
38
+ + memory.coverage.clippedToolResults
39
+ .map((clip) => `\n- ${clip.messageId}: ${clip.omittedChars} characters omitted`)
40
+ .join('')
41
+ + (memory.legacySummary
42
+ ? `\n\nLegacy reference (unverified):\n${memory.legacySummary}`
43
+ : ''),
44
+ ],
45
+ ];
46
+ return sections.map(([title, body]) => `## ${title}\n${body || '(none)'}`).join('\n\n');
47
+ }
@@ -0,0 +1,21 @@
1
+ /** Bounded syntax, schema and evidence checks for untrusted memory input. */
2
+ import type { CompactionMemory, MemoryDelta, MemorySource } from './memory.js';
3
+ /** Stable failure reason and short field path; never includes protected content. */
4
+ export declare class MemoryError extends Error {
5
+ readonly reason: string;
6
+ readonly path: string;
7
+ constructor(reason: string, path?: string);
8
+ }
9
+ /** Parse bounded JSON, rejecting duplicate decoded keys before JSON.parse. */
10
+ export declare function parseStrictJson(text: string): unknown;
11
+ /** Validate the model-owned schema only; throws MemoryError on invalid fields. */
12
+ export declare function validateDeltaSchema(value: unknown): asserts value is MemoryDelta;
13
+ /** Validate a persisted v2 body, including unique IDs and acyclic state chains. */
14
+ export declare function validateMemorySchema(value: unknown): asserts value is CompactionMemory;
15
+ export interface DeltaValidationContext {
16
+ sourceMap: ReadonlyMap<string, MemorySource>;
17
+ prior?: CompactionMemory;
18
+ hasSummarizableMessages: boolean;
19
+ }
20
+ /** Check current-source excerpts, completion evidence and append-only supersession. */
21
+ export declare function validateMemoryDelta(delta: MemoryDelta, context: DeltaValidationContext): void;