@buildaharness/aielia 0.3.4 → 0.3.6

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/README.md +156 -35
  2. package/dist/PlanGraphPane.d.ts +19 -0
  3. package/dist/_memory-core-generated.d.ts +1764 -0
  4. package/dist/action-approval-service.d.ts +34 -2
  5. package/dist/action-record.d.ts +13 -0
  6. package/dist/activity-log.d.ts +4 -0
  7. package/dist/agent-loop.d.ts +106 -9
  8. package/dist/ambiguity-guard-flag.d.ts +12 -0
  9. package/dist/ambiguity-guard.d.ts +20 -0
  10. package/dist/answer-claim.d.ts +13 -1
  11. package/dist/ask-clarification-service.d.ts +12 -1
  12. package/dist/ask-response-format.d.ts +6 -0
  13. package/dist/assistant-session.d.ts +32 -2
  14. package/dist/assistant-source.d.ts +7 -0
  15. package/dist/assistant-types.d.ts +20 -2
  16. package/dist/assistant.d.ts +160 -6
  17. package/dist/benchmark-injected-failure.d.ts +6 -0
  18. package/dist/bin-BYRl4xXM.js +4985 -0
  19. package/dist/bin-BYRl4xXM.js.map +1 -0
  20. package/dist/cli-config.d.ts +3 -0
  21. package/dist/cli-session.d.ts +19 -5
  22. package/dist/cli.d.ts +26 -2
  23. package/dist/cli.js +1 -1
  24. package/dist/code-plan-templates.d.ts +21 -0
  25. package/dist/config.d.ts +31 -0
  26. package/dist/constraint-check.d.ts +35 -0
  27. package/dist/contradiction-checker.d.ts +7 -0
  28. package/dist/debug-log.d.ts +10 -1
  29. package/dist/decomposed-answer.d.ts +13 -0
  30. package/dist/decomposition-classifier.d.ts +2 -0
  31. package/dist/doctor-checks.d.ts +3 -0
  32. package/dist/email.d.ts +5 -3
  33. package/dist/episodic-digest.d.ts +89 -0
  34. package/dist/fact-extraction.d.ts +39 -0
  35. package/dist/file-tools-mcp-server.mjs +288 -37
  36. package/dist/file-tools.d.ts +6 -0
  37. package/dist/first-run.d.ts +0 -2
  38. package/dist/git-exclude.d.ts +9 -0
  39. package/dist/goal-graph-store.d.ts +18 -18
  40. package/dist/grounding-check.d.ts +33 -0
  41. package/dist/harness-bridge.d.ts +75 -2
  42. package/dist/harness-token-budget.d.ts +9 -0
  43. package/dist/index.d.ts +21 -2
  44. package/dist/index.js +166 -94
  45. package/dist/index.js.map +1 -1
  46. package/dist/ink-select-prompt.d.ts +2 -0
  47. package/dist/ink-test-render.d.ts +2 -0
  48. package/dist/layer-policy-flag.d.ts +13 -0
  49. package/dist/layer-policy-wiring.d.ts +77 -0
  50. package/dist/layer-settings.d.ts +49 -0
  51. package/dist/layer-telemetry.d.ts +2 -0
  52. package/dist/lexical/lexical-mode.d.ts +29 -0
  53. package/dist/memory-consolidation.d.ts +80 -0
  54. package/dist/memory-governance.d.ts +30 -0
  55. package/dist/memory-reviewer.d.ts +114 -0
  56. package/dist/memory-service.d.ts +356 -9
  57. package/dist/model-json.d.ts +17 -0
  58. package/dist/next-step-context.d.ts +51 -0
  59. package/dist/next-step-proposer.d.ts +10 -2
  60. package/dist/plan-drafting-service.d.ts +3 -0
  61. package/dist/plan-graph-flag.d.ts +20 -0
  62. package/dist/plan-question.d.ts +60 -0
  63. package/dist/plan-service.d.ts +6 -2
  64. package/dist/plan-step-prompt.d.ts +15 -0
  65. package/dist/plan-store.d.ts +12 -1
  66. package/dist/plan-viz/normalize.d.ts +14 -0
  67. package/dist/plan-viz/plan-detail-lines.d.ts +3 -0
  68. package/dist/plan-viz/plan-layout.d.ts +48 -0
  69. package/dist/plan-viz/plan-nav.d.ts +29 -0
  70. package/dist/plan-viz/plan-raster.d.ts +36 -0
  71. package/dist/plan-viz/plan-routes.d.ts +6 -0
  72. package/dist/plan-viz/plan-snapshot.d.ts +73 -0
  73. package/dist/plan-viz/plan-viewport.d.ts +29 -0
  74. package/dist/plan-viz/plan-width.d.ts +15 -0
  75. package/dist/plan-viz/testkit/display-width.d.ts +12 -0
  76. package/dist/plan-viz/testkit/generators.d.ts +61 -0
  77. package/dist/plan-viz/testkit/grid-oracles.d.ts +38 -0
  78. package/dist/plan-viz/testkit/nav-oracles.d.ts +79 -0
  79. package/dist/plan-viz/testkit/types.d.ts +8 -0
  80. package/dist/plan-viz/types.d.ts +34 -0
  81. package/dist/{provider-setup-CPbB5y_V.js → provider-setup-CV3S_TAS.js} +10792 -5731
  82. package/dist/provider-setup-CV3S_TAS.js.map +1 -0
  83. package/dist/recall-tool.d.ts +43 -0
  84. package/dist/recovery-note.d.ts +22 -0
  85. package/dist/reply-audit.d.ts +67 -0
  86. package/dist/response-service.d.ts +11 -1
  87. package/dist/review-checker.d.ts +10 -4
  88. package/dist/reviewer-revision.d.ts +23 -0
  89. package/dist/risk-classifier.d.ts +7 -0
  90. package/dist/scripted-llm-client.d.ts +26 -1
  91. package/dist/semantic-compaction.d.ts +15 -0
  92. package/dist/semantic-hypotheses.d.ts +40 -0
  93. package/dist/side-call-policy.d.ts +30 -0
  94. package/dist/source-reliability.d.ts +71 -0
  95. package/dist/task-completion-check.d.ts +24 -0
  96. package/dist/tool-control-plane.d.ts +33 -1
  97. package/dist/tool-effect-class.d.ts +9 -0
  98. package/dist/tool-step.d.ts +6 -0
  99. package/dist/trace-events.d.ts +1 -1
  100. package/dist/transcript-compaction.d.ts +9 -0
  101. package/dist/trust-tagging.d.ts +2 -0
  102. package/dist/{tui-app-DeyZ4Pr9.js → tui-app-CJq6YaiP.js} +436 -21
  103. package/dist/tui-app-CJq6YaiP.js.map +1 -0
  104. package/dist/tui-app.d.ts +73 -0
  105. package/dist/turn-abort.d.ts +18 -0
  106. package/dist/turn-intent-classifier.d.ts +81 -14
  107. package/dist/turn-interpreter.d.ts +27 -3
  108. package/dist/version.d.ts +1 -1
  109. package/package.json +6 -6
  110. package/dist/bin-CdVTAe7j.js +0 -2209
  111. package/dist/bin-CdVTAe7j.js.map +0 -1
  112. package/dist/provider-setup-CPbB5y_V.js.map +0 -1
  113. package/dist/tui-app-DeyZ4Pr9.js.map +0 -1
@@ -1,10 +1,58 @@
1
1
  import { ExperienceStore, StrategyWeightKey, DecompositionEntry, RecoverySequenceEntry, ExperienceStoreData, ExternalContradictionInput } from '@buildaharness/harness';
2
- import { MemoryAdapter, ReminderStore, ReminderRecord, ILLMClient, TokenUsage } from '@buildaharness/runtime';
2
+ import { MemoryAdapter, ReminderStore, ReminderRecord, ILLMClient, TokenUsage, ChatMessage } from '@buildaharness/runtime';
3
3
  import { UserFact } from './fact-extraction.js';
4
4
  import { Corroboration } from './contradiction-checker.js';
5
+ import { DigestStore, SessionDigest } from './episodic-digest.js';
5
6
  import { StatedFact, FactCategory } from './turn-intent-classifier.js';
7
+ import { MemoryWriteMode, CrossTurnWriter } from './memory-governance.js';
8
+ import { ConsolidationProposal } from './memory-consolidation.js';
9
+ import { VerifiedReviewOp } from './memory-reviewer.js';
6
10
  export declare const FACT_CAP = 20;
7
- export declare const DURABLE_FACTS_KEY = "facts:durable";
11
+ /** Default character budget for the rendered facts block under `AUDIT_MEMORY_BUDGETED_RENDER` (M0 measured ~195 tokens ≈ 800 chars for 20 facts; this leaves room for roughly 5x that). */
12
+ export declare const DEFAULT_MEMORY_BUDGET_CHARS: number;
13
+ /** Append-only store of entries a keyed fact replaced (M1); the M2 audit log will subsume it. */
14
+ export declare const RETIRED_FACTS_KEY: string;
15
+ /** M2: append-only change log with pre-images. Global, never cleared by `/new` (same convention as DURABLE_FACTS_KEY). */
16
+ export declare const AUDIT_LOG_KEY: string;
17
+ /** M2/M5: highest audit `seq` already consolidated; the audit log never rotates away an entry newer than this. Absent until M5 writes it. */
18
+ export declare const CONSOLIDATION_STATE_KEY: string;
19
+ /** M2: how many audit entries rotation keeps (plus any newer than the consolidation watermark). */
20
+ export declare const AUDIT_LOG_KEEP: number;
21
+ /**
22
+ * M6: `/memory off` — when `{ off: true }`, nothing writes memory for this install (facts, pending
23
+ * queue, and every later writer: M3 digests, M4 reviewer, M5 consolidation must check
24
+ * `MemoryService.isMemoryOff()` first). Reads, forget, reject and undo still work: switching writes
25
+ * off never traps the user's existing data. Global, never cleared by `/new`.
26
+ */
27
+ export declare const MEMORY_OFF_KEY: string;
28
+ /** M5: staged proposals awaiting the user (`/memory consolidate`). Global, never cleared by `/new`. */
29
+ export declare const CONSOLIDATION_PROPOSALS_KEY: string;
30
+ /** M5: entries the user accepted an archive proposal for. Recoverable (`/memory archive restore`), never deleted by this phase. */
31
+ export declare const ARCHIVED_FACTS_KEY: string;
32
+ export type AuditOp = 'add' | 'replace' | 'retire' | 'remove' | 'confirm' | 'reject' | 'undo' | 'archive' | 'restore';
33
+ export interface AuditEntry {
34
+ seq: number;
35
+ at: string;
36
+ op: AuditOp;
37
+ /** `text|extractedAt` of the fact the entry concerns (M1's id convention). */
38
+ factId: string;
39
+ /** Pre-image (redacted text only — the gate redacts before anything reaches the log). */
40
+ before?: UserFact;
41
+ after?: UserFact;
42
+ /** Which store the change was made to. */
43
+ store: 'durable' | 'pending' | 'rejected';
44
+ writer: string;
45
+ turn: string;
46
+ /** Set on an `undo` entry: the seq it reverted. */
47
+ undoes?: number;
48
+ /** M6: the user erased this fact from history (`/memory archive forget`); before/after were removed and the entry can no longer be undone. */
49
+ erased?: boolean;
50
+ /** M5: entries applied together (one consolidation proposal) share a group; undoing one undoes the group. */
51
+ group?: string;
52
+ /** M5: position the pre-image held in the durable list, so undo restores order exactly. */
53
+ index?: number;
54
+ }
55
+ export declare const DURABLE_FACTS_KEY: string;
8
56
  /**
9
57
  * Global, never cleared by `/new` — same retention shape as DURABLE_FACTS_KEY (a pending fact
10
58
  * shouldn't vanish just because the session changed). Phase 3 of
@@ -13,7 +61,7 @@ export declare const DURABLE_FACTS_KEY = "facts:durable";
13
61
  * promoted or silently dropped — see `recordFacts()`'s promotion policy and
14
62
  * `confirmPendingFact()`/`rejectPendingFact()` below.
15
63
  */
16
- export declare const PENDING_CONFIRMATION_KEY = "facts:pending-confirmation";
64
+ export declare const PENDING_CONFIRMATION_KEY: string;
17
65
  /**
18
66
  * Global, never cleared by `/new`. Holds facts removed from `facts:${sessionId}`/
19
67
  * PENDING_CONFIRMATION_KEY either by an entry-time retraction (a later statement contradicts an
@@ -22,7 +70,7 @@ export declare const PENDING_CONFIRMATION_KEY = "facts:pending-confirmation";
22
70
  * too, so a matching restatement can re-enter PENDING_CONFIRMATION_KEY tagged
23
71
  * `previouslyRejected` instead of being silently re-asked forever or silently refused forever.
24
72
  */
25
- export declare const REJECTED_FACTS_KEY = "facts:rejected";
73
+ export declare const REJECTED_FACTS_KEY: string;
26
74
  /** Bound on how many learned decompositions/recovery sequences `getMemorySummary()` includes — see MemorySummary's doc comment. */
27
75
  export declare const MEMORY_SUMMARY_PREVIEW_LIMIT = 20;
28
76
  /** A `model_inferred` fact sitting in PENDING_CONFIRMATION_KEY, awaiting `/memory confirm`/`/memory reject`. */
@@ -30,6 +78,22 @@ export interface PendingFact extends UserFact {
30
78
  category: FactCategory;
31
79
  /** True when this exact fact previously lived in REJECTED_FACTS_KEY and was corroborated back in by a later, differently-phrased restatement — see `recordFacts()`'s corroboration handling. `/memory` surfaces this so the user sees the prior rejection instead of the fact looking brand new. */
32
80
  previouslyRejected?: boolean;
81
+ /** M4: `retire` marks a reviewer proposal to retire an existing durable fact (`retireTargetId` = its `text|extractedAt`) rather than add one; confirming it retires, never adds. */
82
+ proposedOp?: 'retire';
83
+ retireTargetId?: string;
84
+ /** M4: who staged this entry and what the semantic verifier said, shown in `/memory`. */
85
+ stagedBy?: 'reviewer';
86
+ verification?: 'supported' | 'unsupported' | 'not_checked';
87
+ }
88
+ /** What `stageReviewerOps()` did with a batch, for tests and `/memory` diagnostics. */
89
+ export interface StageReviewerResult {
90
+ staged: number;
91
+ /** Unsupported by the verifier, kept session-scoped only. */
92
+ sessionScoped: number;
93
+ /** Refused by a plain-code gate (retire of a user_asserted fact, unknown target) or dropped by the write gate. */
94
+ refused: number;
95
+ /** The signal fired before the write: nothing was written. */
96
+ aborted: boolean;
33
97
  }
34
98
  /** An entry in REJECTED_FACTS_KEY — see that constant's doc comment. */
35
99
  export interface RejectedFact {
@@ -63,7 +127,60 @@ export interface MemoryExport {
63
127
  reminders: ReminderRecord[];
64
128
  pending: PendingFact[];
65
129
  experience: ExperienceStoreData;
130
+ /** M3: every stored episodic digest, unbounded and regardless of retention. */
131
+ digests?: SessionDigest[];
132
+ /** M6: facts a keyed update replaced (and, after M5, archived) — covered by export so nothing the assistant holds about the user is missing from it. */
133
+ retired?: UserFact[];
134
+ /** M6: the audit log (redacted text only, by the M2 gate). Empty when the audit-log flag is off. */
135
+ audit?: AuditEntry[];
136
+ /** M6: governance state at export time. */
137
+ governance?: {
138
+ mode: MemoryWriteMode;
139
+ off: boolean;
140
+ };
141
+ }
142
+ /** M6: what the facts block of the most recent turn actually contained ("Why?" panel, `/why`). */
143
+ export interface MemoryInjection {
144
+ /** Facts placed in the prompt, in render order. */
145
+ facts: {
146
+ text: string;
147
+ unconfirmed: boolean;
148
+ }[];
149
+ /** In-scope live facts that did NOT make it into the prompt (budget or the legacy 20-fact cap). */
150
+ notShown: number;
66
151
  }
152
+ /** M6: store-health snapshot for `/doctor` and the memory panel. Read-only; never touches usage counters. */
153
+ export interface MemoryStatus {
154
+ mode: MemoryWriteMode;
155
+ off: boolean;
156
+ budgetedRender: boolean;
157
+ budgetChars: number;
158
+ /** Characters the in-scope live facts would need if all were rendered. */
159
+ storeChars: number;
160
+ liveFacts: number;
161
+ pending: number;
162
+ flaggedPending: number;
163
+ retired: number;
164
+ auditEnabled: boolean;
165
+ auditEntries: number;
166
+ /** Highest audit seq already consolidated (M5 writes `memory:consolidation-state`); undefined until then. */
167
+ lastConsolidatedSeq?: number;
168
+ lastConsolidationAt?: string;
169
+ lastInjection?: MemoryInjection;
170
+ }
171
+ /** M6 extension point for M5: what a consolidation run reports back to `/memory consolidate`. */
172
+ export type ConsolidationOutcome = {
173
+ status: 'done';
174
+ message: string;
175
+ } | {
176
+ status: 'nothing_to_do';
177
+ message: string;
178
+ };
179
+ /** M5 registers one of these via `MemoryService.registerConsolidator()`; it proposes through `submit` so governance, the gate and the audit log apply. */
180
+ export type MemoryConsolidator = (ctx: {
181
+ submit: MemoryService['submitCandidate'];
182
+ sessionId: string;
183
+ }) => Promise<ConsolidationOutcome>;
67
184
  /** Result of a single fact's promotion-time (`/memory confirm`) or corroboration-driven (medium→high) admission into DURABLE_FACTS_KEY — see `confirmPendingFact()`. */
68
185
  export interface PendingConfirmationOutcome {
69
186
  fact: UserFact;
@@ -75,6 +192,47 @@ export interface RecordFactsResult {
75
192
  contradictions: ExternalContradictionInput[];
76
193
  corroborations: Corroboration[];
77
194
  }
195
+ /**
196
+ * `AUDIT_MEMORY_BUDGETED_RENDER` gate (M1 of the agent memory framework plan). Gates budgeted/
197
+ * priority rendering, keyed supersession and usage fields. `=0`/`off` restores `slice(-FACT_CAP)`
198
+ * and append-only writes. Default **ON** (M7 corrections scenario, 2 of 2 seeds: lexicalOff FAIL, memoryM12On PASS);
199
+ * `0`/`false`/`off`/`no`/`disabled` restores the legacy path. Read fresh each call; this is the one read point.
200
+ */
201
+ export declare function memoryBudgetedRenderEnabled(env?: Record<string, string | undefined>): boolean;
202
+ /** `AUDIT_MEMORY_WRITE_GATE`: semantic secret redaction + injection judgement before promotion, failing closed. Default OFF until the real-model pilot (plan defaults rule); `1/true/on/yes/enabled` enables. */
203
+ export declare function memoryWriteGateEnabled(env?: Record<string, string | undefined>): boolean;
204
+ /** `AUDIT_MEMORY_AUDIT_LOG`: append-only `memory:audit`. Default OFF until the pilot; `=0` restores the unlogged path. */
205
+ export declare function memoryAuditLogEnabled(env?: Record<string, string | undefined>): boolean;
206
+ /** Outcome of `admitCandidate()` for one candidate. `session` = kept session-scoped only, never durable or pending. */
207
+ export interface AdmitDecision {
208
+ action: 'admit' | 'session' | 'flag' | 'drop';
209
+ fact: UserFact;
210
+ }
211
+ /**
212
+ * The single judgement step every writer's candidate passes through before it can be promoted (M2).
213
+ * Pure: the judgements were produced by the same LLM call that produced the fact. Always strips the
214
+ * transient `judgement`. With the gate off the candidate is returned unchanged (`admit`). With it on,
215
+ * for a `model_inferred` candidate: a missing judgement fails closed (`session`, durable bit cleared);
216
+ * a secret is replaced by the model's secret-free text, or dropped when the claim is the secret; an
217
+ * instruction-shaped claim is `flag`ged for the pending queue. A non-`user` origin never promotes.
218
+ */
219
+ export declare function admitCandidate(candidate: UserFact, gateOn: boolean): AdmitDecision;
220
+ /** Removes verbatim copies of the injected memory block from text bound for candidate extraction (feedback-loop prevention). Structural: it removes the exact block this service rendered, not a pattern. */
221
+ export declare function excludeInjectedBlock(text: string, injectedBlock: string): string;
222
+ export interface RenderedFacts {
223
+ block: string;
224
+ /** Facts actually placed in the block, in render order. */
225
+ shown: UserFact[];
226
+ /** In-scope live facts that did not fit the budget. */
227
+ droppedCount: number;
228
+ }
229
+ /**
230
+ * Renders facts by priority and character budget, not by position: identity/preference first, then
231
+ * semantic, then episodic/session facts; within a tier newest statement first, then most recently
232
+ * injected. A fact is dropped only when the budget is spent, and the drop is counted. No
233
+ * query-relevance ranking (D2: no lexical scoring). Pure.
234
+ */
235
+ export declare function renderFactsBlock(inScope: UserFact[], budgetChars: number): RenderedFacts;
78
236
  /**
79
237
  * `AUDIT_MODEL_INFERRED_FACTS` gate — feature-value audit (Phase C3 of the internal plan).
80
238
  * Default **ON**: `classifyTurnIntent`'s `statesDurableFacts` (source `model_inferred`) reach
@@ -96,7 +254,7 @@ export declare function modelInferredFactsEnabled(env?: Record<string, string |
96
254
  * it) each stamp their own `extractedAt` — harmless, since neither call's result is compared
97
255
  * against the other's by identity.
98
256
  */
99
- export declare function buildTurnFacts(sessionId: string, userMessage: string, statedFacts: StatedFact[]): UserFact[];
257
+ export declare function buildTurnFacts(sessionId: string, userMessage: string, statedFacts: StatedFact[], policyEnabled?: boolean, now?: () => string): UserFact[];
100
258
  /**
101
259
  * Owns fact/reminder/experience reads that feed each turn's system prompt, fact capture, and the
102
260
  * `/memory`/`/memory export` read-only snapshots — the "what has this assistant learned" surface
@@ -114,9 +272,105 @@ export declare class MemoryService {
114
272
  private readonly model;
115
273
  /** Resolves to `config.activeProject` if the user set one via `/project <name>`, else the workspace root — read through a getter (same convention as `model` above) so a mid-session `/project`/`/config set activeProject` change takes effect on the very next turn. See UserFact.project's doc comment for how this is used. */
116
274
  private readonly currentProject;
275
+ /** M1: character budget for the facts block under `AUDIT_MEMORY_BUDGETED_RENDER` (config `memoryBudgetChars`). */
276
+ private readonly memoryBudgetChars;
277
+ /** M6: governance mode (config `memoryWriteMode`), read through a getter like the others so a live change applies on the next write. Resolved here and nowhere else. */
278
+ private readonly writeMode;
279
+ /** Time source for every timestamp this service writes (extractedAt of stated facts, audit `at`, retiredAt, usage flush). Injectable so the cross-runtime conformance fixtures are byte-reproducible; default is wall-clock, i.e. no behaviour change. */
280
+ private readonly clock;
117
281
  constructor(memory: MemoryAdapter, reminderStore: ReminderStore, experienceStore: ExperienceStore, llmClient: ILLMClient, model: () => string | undefined,
118
282
  /** Resolves to `config.activeProject` if the user set one via `/project <name>`, else the workspace root — read through a getter (same convention as `model` above) so a mid-session `/project`/`/config set activeProject` change takes effect on the very next turn. See UserFact.project's doc comment for how this is used. */
119
- currentProject?: () => string);
283
+ currentProject?: () => string,
284
+ /** M1: character budget for the facts block under `AUDIT_MEMORY_BUDGETED_RENDER` (config `memoryBudgetChars`). */
285
+ memoryBudgetChars?: () => number,
286
+ /** M6: governance mode (config `memoryWriteMode`), read through a getter like the others so a live change applies on the next write. Resolved here and nowhere else. */
287
+ writeMode?: () => MemoryWriteMode,
288
+ /** Time source for every timestamp this service writes (extractedAt of stated facts, audit `at`, retiredAt, usage flush). Injectable so the cross-runtime conformance fixtures are byte-reproducible; default is wall-clock, i.e. no behaviour change. */
289
+ clock?: () => string);
290
+ private consolidator?;
291
+ private lastInjection?;
292
+ /** `/memory off` state — M3/M4/M5 writers must call this before any write or LLM spend. */
293
+ isMemoryOff(): Promise<boolean>;
294
+ setMemoryOff(off: boolean): Promise<void>;
295
+ /** M5 extension point (not built here): until a consolidator is registered `/memory consolidate` says so instead of pretending. */
296
+ registerConsolidator(fn: MemoryConsolidator | undefined): void;
297
+ consolidate(sessionId: string): Promise<ConsolidationOutcome | {
298
+ status: 'unavailable' | 'blocked';
299
+ message: string;
300
+ }>;
301
+ /**
302
+ * M6 extension point for M3 (digest), M4 (reviewer) and M5 (consolidation) writers: the ONE way a
303
+ * cross-turn candidate enters memory. It applies, in order: `/memory off`, the M2 gate
304
+ * (`admitCandidate`: redaction, injection flag, fail-closed judgement, non-user origin), then the
305
+ * governance route for `memoryWriteMode` (`resolveWriteRoute`), and writes through the same
306
+ * single durable commit point / pending queue / audit log as every other writer. Add-only: removal
307
+ * and retirement stay with the user or a staged proposal.
308
+ */
309
+ submitCandidate(writer: CrossTurnWriter, candidate: UserFact, sessionId: string, opts?: {
310
+ pendingExtras?: Partial<PendingFact>;
311
+ forceGate?: boolean;
312
+ }): Promise<{
313
+ route: 'durable' | 'pending' | 'session' | 'dropped' | 'blocked';
314
+ fact?: UserFact;
315
+ }>;
316
+ /** Facts rendered since the last flush, keyed by `text|extractedAt`; written lazily so loadFacts() stays read-only (M1). */
317
+ private pendingInjections;
318
+ /** Number of in-scope facts the last budgeted render could not show — `/memory` says "N facts not shown this turn". */
319
+ lastDroppedCount: number;
320
+ /** The facts block most recently rendered into a prompt; excluded verbatim from candidate extraction (M2 feedback-loop prevention). */
321
+ private lastInjectedBlock;
322
+ private digestStoreInstance?;
323
+ /** Incremented whenever recordFacts() or stageReviewerOps() changes any memory store; the M4 trigger compares it before and after a turn. */
324
+ writeCount: number;
325
+ /** The facts block most recently injected into a prompt (what the M4 reviewer must strip from its input). */
326
+ getInjectedBlock(): string;
327
+ /**
328
+ * Keys of the live durable facts, with their text, for the classifier prompt (M1 key stability:
329
+ * the model reuses a stored key for a new value of the same attribute instead of inventing a
330
+ * variant that would miss supersession). Newest first, capped so the prompt stays small. Empty
331
+ * when keyed supersession is off.
332
+ */
333
+ getKnownFactKeys(limit?: number): Promise<{
334
+ key: string;
335
+ text: string;
336
+ }[]>;
337
+ /** Durable facts in stored order — the list a reviewer `retire` op's `targetId` indexes into. */
338
+ getDurableFacts(): Promise<UserFact[]>;
339
+ /**
340
+ * M4: stages the post-turn reviewer's operations. Per decision D1 nothing here reaches durable memory: surviving ops
341
+ * wait in the pending queue with their evidence (shown by `/memory`). Plain-code gates, no language judgement:
342
+ * a `retire` of a `user_asserted` fact (or of an unknown target) is refused; every op passes `admitCandidate()`;
343
+ * an op the verifier called unsupported is kept session-scoped only. All writes happen after the abort check, in one
344
+ * batch per store, so an abort leaves no partial write. `durableSnapshot` is the list the reviewer's `targetId`s index.
345
+ */
346
+ stageReviewerOps(sessionId: string, ops: readonly VerifiedReviewOp[], durableSnapshot: readonly UserFact[], signal?: AbortSignal): Promise<StageReviewerResult>;
347
+ /**
348
+ * M2: the ONLY place `DURABLE_FACTS_KEY` is written. Every add, replace, retire, remove, confirm
349
+ * and usage-flush goes through here, so a new writer cannot skip the audit log. A structural test
350
+ * (memory-write-gate.test.ts) asserts this is the only call in the file that writes the durable key.
351
+ */
352
+ private commitDurable;
353
+ /** Tail of the in-process chain serialising the audit log's read-modify-write (concurrent appends lost entries and reused seq numbers). */
354
+ private auditChain;
355
+ /** Same for the usage flush's read-modify-write of the fact stores. */
356
+ private flushChain;
357
+ private serialized;
358
+ private appendAudit;
359
+ private appendAuditNow;
360
+ /** `/memory history` — newest last; the last `limit` entries. */
361
+ getAuditLog(limit?: number): Promise<AuditEntry[]>;
362
+ /**
363
+ * `/memory undo <seq>` — restores the pre-image of one audit entry exactly (and removes what that
364
+ * entry added), then appends an `undo` entry. An entry can be undone once. Returns a message for
365
+ * the caller, or undefined when the seq is unknown.
366
+ */
367
+ undoAudit(seq: number, sessionId?: string): Promise<{
368
+ ok: boolean;
369
+ message: string;
370
+ }>;
371
+ /** Undo of the change that created a side store must leave it absent again, not an empty array (exact restoration). */
372
+ private setOrClear;
373
+ private undoEntry;
120
374
  /**
121
375
  * Durable + session facts for `sessionId`, plus the ready-to-splice system-prompt block — see
122
376
  * runTurn's former factsBlock. `facts` is the full inventory, unfiltered by project — `/memory`
@@ -125,9 +379,15 @@ export declare class MemoryService {
125
379
  * global-or-current-project only, so an unrelated project's facts don't leak into context (see
126
380
  * UserFact.project's doc comment).
127
381
  */
128
- loadFacts(sessionId: string): Promise<{
382
+ loadFacts(sessionId: string, opts?: {
383
+ record?: boolean;
384
+ }): Promise<{
129
385
  facts: UserFact[];
130
386
  factsBlock: string;
387
+ knownFactKeys: {
388
+ key: string;
389
+ text: string;
390
+ }[];
131
391
  }>;
132
392
  /**
133
393
  * `/memory forget <n>` — removes the nth entry (1-based in `/memory`'s "Facts I know" display,
@@ -139,7 +399,7 @@ export declare class MemoryService {
139
399
  * happened to win the display dedup. Returns undefined for an out-of-range index (the caller's
140
400
  * `/memory` view is stale — nothing to forget).
141
401
  */
142
- forgetFact(index: number, sessionId: string): Promise<UserFact | undefined>;
402
+ forgetFact(index: number, sessionId: string, erase?: boolean): Promise<UserFact | undefined>;
143
403
  /**
144
404
  * reminderStore is cross-session durable (clearSession() never touches it, same tier as
145
405
  * DURABLE_FACTS_KEY) but, unlike facts, was never actually surfaced into context: a plain
@@ -157,7 +417,7 @@ export declare class MemoryService {
157
417
  }>;
158
418
  /**
159
419
  * Captures this turn's durable/session facts into the session's fact store. A no-op when
160
- * neither pass finds anything. Facts that clear `shouldAutoPromote()` (see that function's doc
420
+ * neither pass finds anything. Facts routed to durable by `resolveWriteRoute()` (memory-governance.ts; see its doc
161
421
  * comment for the current three-way policy) are ALSO appended to DURABLE_FACTS_KEY, a store
162
422
  * clearSession() never touches, so they survive /new instead of vanishing with the rest of the
163
423
  * session's facts. A `model_inferred` fact at `durable: true, confidence: 'medium'` is queued
@@ -190,6 +450,50 @@ export declare class MemoryService {
190
450
  * computed but never surfaced in the turn's own reply.
191
451
  */
192
452
  recordFacts(sessionId: string, userMessage: string, statedFacts: StatedFact[], onUsage?: (usage: TokenUsage) => void): Promise<RecordFactsResult>;
453
+ private consolidating;
454
+ /**
455
+ * M5: proposes consolidation and archive changes; applies NOTHING (D1: staged). Work is for the
456
+ * stores as the previous session left them: callers run it at session start or `/new`, or on the
457
+ * manual `/memory consolidate` (`manual` forces the model pass).
458
+ *
459
+ * Cost gate, deterministic and before any model call: the model pass runs only when the audit log
460
+ * has entries newer than `memory:consolidation-state.lastSeq` or the curated (durable) store is
461
+ * over the character budget, and there are at least two live facts; otherwise zero model calls.
462
+ * Archive proposals use no model: only M1's usage timestamps and the retention window. A failed
463
+ * or unusable model call changes nothing (the watermark does not move, so the next run retries).
464
+ * Requires the audit log: a change that cannot be undone is never proposed.
465
+ */
466
+ runConsolidation(opts?: {
467
+ manual?: boolean;
468
+ onUsage?: (usage: TokenUsage) => void;
469
+ }): Promise<{
470
+ status: 'disabled' | 'needs_audit_log' | 'busy' | 'skipped' | 'ran' | 'failed';
471
+ proposed: number;
472
+ modelCalls: number;
473
+ projectedChars?: number;
474
+ usedChars?: number;
475
+ }>;
476
+ getConsolidationProposals(): Promise<ConsolidationProposal[]>;
477
+ /** `/memory archive` — what staged forgetting has set aside. Nothing in it is deleted. */
478
+ getArchivedFacts(): Promise<UserFact[]>;
479
+ /** `/memory consolidate dismiss <n>`: drops the proposal and remembers its signature so it is not raised again. */
480
+ dismissProposal(index: number): Promise<ConsolidationProposal | undefined>;
481
+ /**
482
+ * `/memory consolidate accept <n>`: the user's explicit yes. Applies one staged proposal to the
483
+ * durable store through `commitDurable` as one audit group (`/memory undo <seq>` reverts all of it,
484
+ * restoring the store and its order exactly). Merged/superseded originals go to the retired store,
485
+ * archived ones to the archive store; nothing is deleted. Refuses without the audit log, and treats
486
+ * a proposal whose facts changed since it was staged as stale.
487
+ */
488
+ acceptProposal(index: number, sessionId?: string): Promise<{
489
+ ok: boolean;
490
+ message: string;
491
+ }>;
492
+ /** `/memory archive restore <n>`: puts an archived entry back among the live facts (audited; undoable). */
493
+ restoreArchivedFact(listIndex: number, sessionId?: string): Promise<UserFact | undefined>;
494
+ /** Writes the batched `injectedCount`/`lastInjectedAt` updates collected by budgeted renders, to whichever store(s) hold each fact. A no-op (no writes) when nothing was rendered. */
495
+ private flushInjectionUsage;
496
+ private flushInjectionUsageNow;
193
497
  /**
194
498
  * `/memory confirm <n>` — promotes the nth (1-based in `/memory`'s display, 0-based here) entry
195
499
  * in PENDING_CONFIRMATION_KEY to DURABLE_FACTS_KEY. Runs the same promotion-time check as
@@ -226,4 +530,47 @@ export declare class MemoryService {
226
530
  * export`.
227
531
  */
228
532
  exportMemory(sessionId: string): Promise<MemoryExport>;
533
+ /** What the most recent turn's facts block contained (undefined before the first turn). Set only by the turn path, never by read-only views. */
534
+ getLastInjection(): MemoryInjection | undefined;
535
+ /** `/memory archive`: entries a keyed update replaced, newest first. M5 EXTENSION POINT: its archive store joins here (and in `exportMemory`) when it exists. */
536
+ listArchive(): Promise<UserFact[]>;
537
+ /**
538
+ * `/memory archive forget <n>`: permanently erases one archived entry (index over `listArchive()`),
539
+ * including its pre-images in the audit log (those entries stay, marked `erased`, and can no longer
540
+ * be undone). The only path that edits the audit log other than appending, and only on an explicit user request.
541
+ */
542
+ forgetArchived(index: number): Promise<UserFact | undefined>;
543
+ /** Strips one fact's pre/post-images from the audit log (entries stay, marked `erased`, and can no longer be undone) and unlinks `supersedes` references to it. */
544
+ private eraseFromAuditLog;
545
+ private eraseFromAuditLogNow;
546
+ /** Read-only health snapshot (`/doctor`, memory panel). Never touches usage counters or the last-injection record. */
547
+ getMemoryStatus(sessionId: string): Promise<MemoryStatus>;
548
+ /** M3 (D4): `/memory forget digest <id>` or, with no id, every digest. Returns how many were removed. */
549
+ forgetDigests(id?: string): Promise<number>;
550
+ /** M3: read-only digest reader over the same store the recall tool uses. */
551
+ get digests(): DigestStore;
552
+ /** Token usage of the most recent digest/flush calls made by this service (cost per session edge). */
553
+ digestUsage: {
554
+ inputTokens: number;
555
+ outputTokens: number;
556
+ costUsd: number;
557
+ };
558
+ private trackDigestUsage;
559
+ /**
560
+ * M3 session-edge digest (`/new`, exit). One bounded call, fail-open: any failure leaves the previous digest (if any)
561
+ * untouched. Skipped when the conversation has no user message. Output is judged and redacted by `admitCandidate`
562
+ * (see episodic-digest.ts) and stored at `episodic:<id>`; it never touches durable facts. Callers gate on
563
+ * `episodicDigestEnabled()`; never call from inside a turn.
564
+ */
565
+ writeSessionDigest(sessionId: string, transcript: ChatMessage[], onUsage?: (usage: TokenUsage) => void): Promise<SessionDigest | null>;
566
+ /** M3: ends the conversation's digest identity so the next conversation under the same session id gets a new digest. Call after `writeSessionDigest`. */
567
+ endDigestConversation(sessionId: string): Promise<void>;
568
+ /**
569
+ * M3 pre-compaction flush: called with exactly the messages compaction is about to drop. One call extracts candidate
570
+ * facts and a digest delta (folded into the conversation's digest). Candidates are judged by `admitCandidate` and go
571
+ * to the pending queue only, never durable. Fail-open: returns 0 on any failure and compaction proceeds regardless.
572
+ */
573
+ flushBeforeCompaction(sessionId: string, older: ChatMessage[], onUsage?: (usage: TokenUsage) => void): Promise<number>;
574
+ /** Judges flush candidates (gate forced on: a missing judgement drops the candidate) and appends the survivors to the pending queue. Exact-text duplicates of anything pending, durable or rejected are skipped. */
575
+ private queueFlushCandidates;
229
576
  }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Tolerant parsing of JSON a model was asked to return.
3
+ *
4
+ * A backend that can't enforce a schema (claude-cli in particular) sometimes wraps the JSON in a code
5
+ * fence, puts a stray tag or a sentence of reasoning in front of it, or trails prose after it. A strict
6
+ * `JSON.parse` then throws, and every classifier in this package fails open — silently losing the
7
+ * check. This is the one place that recovers the JSON from that, so no call site needs its own copy.
8
+ *
9
+ * It is a strict superset of `JSON.parse`: text that already parses parses to exactly the same value,
10
+ * and only text that would have thrown is looked at further.
11
+ */
12
+ /**
13
+ * Parses `text` as JSON, or, failing that, the first (outermost, leftmost) JSON object/array embedded in
14
+ * it. Throws a `SyntaxError` when there is none — the same failure a plain `JSON.parse` gives, so a
15
+ * caller's existing `catch` keeps working.
16
+ */
17
+ export declare function parseModelJson(text: string): unknown;
@@ -0,0 +1,51 @@
1
+ import { ChatMessage } from '@buildaharness/runtime';
2
+ import { GoalGraphRecord } from './goal-graph-store.js';
3
+ import { AssistantSource } from './assistant-source.js';
4
+ export interface NextStepConversationTurn {
5
+ role: 'user' | 'assistant';
6
+ content: string;
7
+ }
8
+ export interface NextStepGoalOverview {
9
+ goal: string;
10
+ status: string;
11
+ /** True for the thread the suggestion is about (the finished thread, or the active one at turn end). */
12
+ focus: boolean;
13
+ tasks: {
14
+ description: string;
15
+ status: string;
16
+ }[];
17
+ /**
18
+ * Next steps already proposed for this goal when it finished (the high/medium-confidence ones the
19
+ * thread persisted), for goals other than the focus one. They are how an open item of an earlier
20
+ * goal survives after that goal has left the conversation window. Absent when there are none.
21
+ */
22
+ openSuggestions?: string[];
23
+ }
24
+ export interface NextStepContext {
25
+ /** Earlier user/assistant messages, oldest first, excluding the exchange the suggestion is about. */
26
+ conversation?: NextStepConversationTurn[];
27
+ /** Every goal thread the session tracks, unfinished ones first. */
28
+ goals?: NextStepGoalOverview[];
29
+ /** Files read / searches made while producing this turn's reply. */
30
+ stepsThisTurn?: string[];
31
+ }
32
+ /**
33
+ * The last few user/assistant messages, oldest first. `currentExchange`, when given, drops the
34
+ * trailing user+assistant pair that is the exchange being suggested for (the proposer already gets
35
+ * it in full), so it is not sent twice. Tool messages and empty messages are skipped.
36
+ */
37
+ export declare function summarizeConversation(transcript: ChatMessage[], currentExchange?: {
38
+ userMessage: string;
39
+ }): NextStepConversationTurn[];
40
+ /** Every goal thread with its tasks, the focus thread and unfinished threads first. Abandoned threads are left out. */
41
+ export declare function summarizeGoalGraph(record: GoalGraphRecord | undefined, focusThreadId?: string): NextStepGoalOverview[];
42
+ export declare function summarizeSteps(sources: AssistantSource[] | undefined): string[];
43
+ export declare function buildNextStepContext(parts: {
44
+ transcript?: ChatMessage[];
45
+ currentExchange?: {
46
+ userMessage: string;
47
+ };
48
+ goalGraph?: GoalGraphRecord;
49
+ focusThreadId?: string;
50
+ sources?: AssistantSource[];
51
+ }): NextStepContext;
@@ -1,6 +1,7 @@
1
1
  import { ILLMClient, TokenUsage } from '@buildaharness/runtime';
2
2
  import { GoalThread } from './goal-graph-store.js';
3
3
  import { GoalGraphSuggestMode } from './goal-graph-suggest-flag.js';
4
+ import { NextStepContext } from './next-step-context.js';
4
5
  export type SuggestionConfidence = 'high' | 'medium' | 'low';
5
6
  /** Mirrors `shouldAutoPromote()`'s three-way policy *shape* (memory-service.ts) — not its bands, since Q10 found no numeric threshold to reuse: `high` auto-promotes, `medium` queues for explicit confirmation, `low` stays advisory-only and is never persisted past the turn that produced it. */
6
7
  export type SuggestionPromotion = 'auto' | 'pending_confirm' | 'session_only';
@@ -26,6 +27,13 @@ export interface SuggestedNextStepNode {
26
27
  promotion: SuggestionPromotion;
27
28
  createdAt: string;
28
29
  }
30
+ /**
31
+ * Names the language the suggestions must be written in. The model drifted into Chinese, Spanish or French
32
+ * on English conversations when it was only told to "match the user's language", so English is named
33
+ * outright when the user's own text clearly is English (two distinct common English words); anything else
34
+ * keeps the generic instruction.
35
+ */
36
+ export declare function suggestionLanguage(userTexts: string[]): string;
29
37
  /** Q10's three-way policy shape, applied to this mechanism's own confidence rubric — see this module's doc comment on why the bands aren't reused from `shouldAutoPromote()`. */
30
38
  export declare function classifySuggestionPromotion(confidence: SuggestionConfidence): SuggestionPromotion;
31
39
  /**
@@ -35,7 +43,7 @@ export declare function classifySuggestionPromotion(confidence: SuggestionConfid
35
43
  * `thread` hasn't actually reached `DONE` yet, since a suggestion only makes sense once the goal
36
44
  * it follows from is finished.
37
45
  */
38
- export declare function proposeNextSteps(thread: GoalThread, llmClient: ILLMClient, suggestMode: GoalGraphSuggestMode, model?: string, onUsage?: (usage: TokenUsage) => void): Promise<SuggestedNextStepNode[]>;
46
+ export declare function proposeNextSteps(thread: GoalThread, llmClient: ILLMClient, suggestMode: GoalGraphSuggestMode, model?: string, onUsage?: (usage: TokenUsage) => void, bigPicture?: NextStepContext): Promise<SuggestedNextStepNode[]>;
39
47
  /**
40
48
  * The turn-end entry point: after a full (non-trivial) turn completes, propose up to three
41
49
  * concrete next steps for the user to pick from — shown under the reply, like Claude Code's prompt
@@ -49,4 +57,4 @@ export declare function proposeNextSteps(thread: GoalThread, llmClient: ILLMClie
49
57
  export declare function proposeTurnNextSteps(turn: {
50
58
  userMessage: string;
51
59
  reply: string;
52
- }, llmClient: ILLMClient, suggestMode: GoalGraphSuggestMode, model?: string, onUsage?: (usage: TokenUsage) => void): Promise<NextStepSuggestion[]>;
60
+ }, llmClient: ILLMClient, suggestMode: GoalGraphSuggestMode, model?: string, onUsage?: (usage: TokenUsage) => void, bigPicture?: NextStepContext): Promise<NextStepSuggestion[]>;
@@ -68,6 +68,9 @@ export declare class PlanDraftingService {
68
68
  private readonly memory?;
69
69
  constructor(planService: PlanService, session: AssistantSession, llmClient: ILLMClient, model: () => string | undefined, planApproval: PlanApprovalService, onTrace: ((event: TraceEvent) => void) | undefined, agentLoop?: Pick<AgentLoop, "runSupervisorInvestigation"> | undefined, memory?: MemoryAdapter | undefined);
70
70
  private planAskPendingKey;
71
+ /** Points at the one nested question a session's draft currently has staged, so cancelling the draft (or staging a newer question) can drop it. */
72
+ private planAskSessionKey;
73
+ private dropStagedAsk;
71
74
  /** Peeked by `assistant.ts`'s runTurn before it decides whether a `pendingClarificationId` belongs to this nested-ask side channel (P8) or AskClarificationService's harness-resume path (Q2) — the two staging stores are otherwise independent, so an opaque ID has no other way to say which one it came from. */
72
75
  isPendingAsk(id: string): Promise<boolean>;
73
76
  /**
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Pure logic backing the plan-graph visualization (read-only plan view on the CLI and desktop/web
3
+ * surfaces) — see plans/plan_visualization_plan.html. Same pattern as goal-graph-flag.ts.
4
+ *
5
+ * Presentation only: the flag never changes a plan, the harness or any persisted state, so it is
6
+ * deliberately NOT in LAYER_SETTINGS. 'disabled' (the default) leaves every surface byte-identical
7
+ * to the behaviour before the feature existed (INV-43). `AssistantConfig.planGraphMode` stays out of
8
+ * DEFAULT_CONFIG (undefined = "the package owns the default"), so consumers go through
9
+ * `isPlanGraphEnabled` rather than comparing against a possibly-undefined value.
10
+ */
11
+ export type PlanGraphMode = 'enabled' | 'disabled';
12
+ export declare const DEFAULT_PLAN_GRAPH_MODE: PlanGraphMode;
13
+ export declare function isPlanGraphEnabled(mode: PlanGraphMode | undefined): boolean;
14
+ /**
15
+ * Shared by cli-config.ts (via `resolvePlanGraphMode`) and chat-ui's browser-config.ts (Vite's
16
+ * `import.meta.env.VITE_ASSISTANT_PLAN_GRAPH`, which has no `process`). An unset or empty value
17
+ * falls back silently; an unrecognized value falls back with a warning naming `varName`.
18
+ */
19
+ export declare function normalizePlanGraphMode(raw: string | undefined, varName?: string): PlanGraphMode;
20
+ export declare function resolvePlanGraphMode(env: NodeJS.ProcessEnv): PlanGraphMode;