@duetso/agent 0.1.133 → 0.1.136

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 (66) hide show
  1. package/README.md +2 -0
  2. package/dist/package.json +1 -1
  3. package/dist/src/cli/help.d.ts +1 -0
  4. package/dist/src/cli/help.d.ts.map +1 -1
  5. package/dist/src/cli/help.js +41 -6
  6. package/dist/src/cli/help.js.map +1 -1
  7. package/dist/src/cli/memory-reflect.d.ts +13 -0
  8. package/dist/src/cli/memory-reflect.d.ts.map +1 -0
  9. package/dist/src/cli/memory-reflect.js +165 -0
  10. package/dist/src/cli/memory-reflect.js.map +1 -0
  11. package/dist/src/cli/memory.d.ts.map +1 -1
  12. package/dist/src/cli/memory.js +8 -0
  13. package/dist/src/cli/memory.js.map +1 -1
  14. package/dist/src/cli/skills.d.ts +8 -4
  15. package/dist/src/cli/skills.d.ts.map +1 -1
  16. package/dist/src/cli/skills.js +21 -13
  17. package/dist/src/cli/skills.js.map +1 -1
  18. package/dist/src/cli.d.ts +1 -0
  19. package/dist/src/cli.d.ts.map +1 -1
  20. package/dist/src/cli.js +1 -0
  21. package/dist/src/cli.js.map +1 -1
  22. package/dist/src/index.d.ts +1 -0
  23. package/dist/src/index.d.ts.map +1 -1
  24. package/dist/src/index.js +1 -0
  25. package/dist/src/index.js.map +1 -1
  26. package/dist/src/memory/observational.d.ts +187 -0
  27. package/dist/src/memory/observational.d.ts.map +1 -1
  28. package/dist/src/memory/observational.js +248 -5
  29. package/dist/src/memory/observational.js.map +1 -1
  30. package/dist/src/memory/storage.d.ts +16 -0
  31. package/dist/src/memory/storage.d.ts.map +1 -1
  32. package/dist/src/memory/storage.js +44 -0
  33. package/dist/src/memory/storage.js.map +1 -1
  34. package/dist/src/tui/app.d.ts.map +1 -1
  35. package/dist/src/tui/app.js +1 -14
  36. package/dist/src/tui/app.js.map +1 -1
  37. package/dist/src/tui/autocomplete.d.ts +0 -9
  38. package/dist/src/tui/autocomplete.d.ts.map +1 -1
  39. package/dist/src/tui/autocomplete.js +0 -13
  40. package/dist/src/tui/autocomplete.js.map +1 -1
  41. package/dist/src/tui/boot-screen.d.ts.map +1 -1
  42. package/dist/src/tui/boot-screen.js +0 -5
  43. package/dist/src/tui/boot-screen.js.map +1 -1
  44. package/dist/src/turn-runner/built-in-skills.d.ts +23 -0
  45. package/dist/src/turn-runner/built-in-skills.d.ts.map +1 -0
  46. package/dist/src/turn-runner/built-in-skills.js +75 -0
  47. package/dist/src/turn-runner/built-in-skills.js.map +1 -0
  48. package/dist/src/turn-runner/skills.d.ts +1 -1
  49. package/dist/src/turn-runner/skills.d.ts.map +1 -1
  50. package/dist/src/turn-runner/skills.js +16 -2
  51. package/dist/src/turn-runner/skills.js.map +1 -1
  52. package/dist/src/turn-runner/state-compaction.d.ts +49 -0
  53. package/dist/src/turn-runner/state-compaction.d.ts.map +1 -0
  54. package/dist/src/turn-runner/state-compaction.js +75 -0
  55. package/dist/src/turn-runner/state-compaction.js.map +1 -0
  56. package/dist/src/turn-runner/turn-runner.d.ts +8 -0
  57. package/dist/src/turn-runner/turn-runner.d.ts.map +1 -1
  58. package/dist/src/turn-runner/turn-runner.js +30 -1
  59. package/dist/src/turn-runner/turn-runner.js.map +1 -1
  60. package/dist/src/types/config.d.ts +19 -0
  61. package/dist/src/types/config.d.ts.map +1 -1
  62. package/package.json +1 -1
  63. package/dist/src/tui/relay-command.d.ts +0 -14
  64. package/dist/src/tui/relay-command.d.ts.map +0 -1
  65. package/dist/src/tui/relay-command.js +0 -58
  66. package/dist/src/tui/relay-command.js.map +0 -1
@@ -3,6 +3,7 @@ import type { Model, Usage } from "@earendil-works/pi-ai";
3
3
  import { type WireGuardHorizon } from "../turn-runner/wire-shaping.js";
4
4
  import type { MemoryContextCache } from "./store.js";
5
5
  import type { MemorySession } from "./session.js";
6
+ import { type SessionObservationsSnapshot } from "./storage.js";
6
7
  import type { Observation, ObservationalMemoryActivityEvent, ObservationalMemorySettings, ObservationalMemorySettingsInput } from "../types/memory.js";
7
8
  import { type RawMemoryMessage } from "./observational-prompts.js";
8
9
  /**
@@ -224,6 +225,192 @@ export declare function optimizeObservationsForContext(observations: string): st
224
225
  * message list whether or not the transform has run.
225
226
  */
226
227
  export declare function stripObservationalContextMessages(messages: AgentMessage[]): AgentMessage[];
228
+ /**
229
+ * Sentinel session id used by `reflectAllObservations` to stamp the
230
+ * reflection rows produced from the cross-session pool. Lets the
231
+ * loader/UI distinguish a global prune-reflection from a per-session one
232
+ * without needing a new column.
233
+ */
234
+ export declare const GLOBAL_REFLECTION_SESSION_ID = "__global_reflection__";
235
+ /**
236
+ * Default minimum age, in days, that a raw observation must reach before
237
+ * the global reflect prune (`duet memory reflect`) is allowed to fold it
238
+ * into a reflection row and delete the original.
239
+ *
240
+ * --- Why a min-age gate exists (read before changing) -----------------
241
+ *
242
+ * The global prune *deletes* the eligible source rows after condensing
243
+ * them. If a long-lived session is later resumed and finds its own local
244
+ * observation rows wiped, the runner's `getUnobservedMessageTail` loses
245
+ * its observation-group range markers — so the next observer call treats
246
+ * the entire session tail as unobserved and re-observes only the newest
247
+ * `FIXED_OBSERVER_BUDGETS.maxTranscriptTokens` (~35k tokens) of raw
248
+ * messages. Everything older than that newest slice is silently dropped
249
+ * by `trimMessagesToTranscriptBudget`, which walks newest→oldest. For
250
+ * sessions that had accumulated rich local condensation, that older
251
+ * content is genuinely lost: the actor's wire eviction horizon already
252
+ * shaped those messages off-wire, so they cannot be re-observed from the
253
+ * raw tail. The global reflection row preserves cross-session themes,
254
+ * but not the session-specific specifics.
255
+ *
256
+ * 3 days is the line where the cost-of-loss flips. After ~3 days a human
257
+ * no longer remembers session specifics anyway, only the higher-level
258
+ * shape — which is what the reflection row captures. Up to 3 days, the
259
+ * specifics still matter for resume continuity, so we hold those rows
260
+ * untouched.
261
+ *
262
+ * --- Alternatives considered and rejected (May 2026) ------------------
263
+ *
264
+ * 1. *Append-only with high-water-mark watermark.* Never delete; just
265
+ * keep appending reflection rows tagged `global-prune`. Recall
266
+ * ranks them above raw rows via `reflectionBias`. Pro: zero risk of
267
+ * resume info loss. Con: the pool grows unbounded over time and we
268
+ * still pay retrieval cost over every stale raw row that the
269
+ * reflection already supersedes. Rejected because the user
270
+ * explicitly wanted the pool to stay small.
271
+ *
272
+ * 2. *Per-session reflect+replace, batched by `session_id`.* Preserves
273
+ * attribution. Rejected because the whole point of the global
274
+ * prune is *cross-session* dedup; grouping by session forfeits
275
+ * that. Used only by the in-session reflector
276
+ * (`reflectObservations`).
277
+ *
278
+ * 3. *Reflect on existing reflections too.* Pro: keeps total row count
279
+ * bounded across many reflect runs. Rejected because reflections
280
+ * of reflections collapse already-condensed text into vaguer text,
281
+ * losing specificity each pass. The current shape preserves rows
282
+ * where `kind === "reflection"` (skipped as input and skipped from
283
+ * the deletion set), so older reflections accumulate but never
284
+ * degrade. A separate `duet memory compact` can later GC stale
285
+ * reflections by `lastUsedAt`.
286
+ *
287
+ * 4. *Smaller min-age (e.g. 1 day) for faster pruning.* Rejected
288
+ * because typical bursty work patterns (a 2-day investigation, a
289
+ * multi-day refactor) would lose mid-thread specifics exactly when
290
+ * a resume is most likely to need them.
291
+ */
292
+ export declare const DEFAULT_GLOBAL_REFLECT_MIN_AGE_DAYS = 3;
293
+ export declare const DEFAULT_GLOBAL_REFLECT_MIN_AGE_MS: number;
294
+ export interface ReflectAllOptions {
295
+ session: MemorySession;
296
+ /**
297
+ * The snapshot to reflect. Callers (the CLI) read the pool once so they
298
+ * can also print stats before the model call; passing the snapshot in
299
+ * avoids a redundant second `SELECT` inside this function.
300
+ */
301
+ snapshot: SessionObservationsSnapshot;
302
+ settings: ObservationalMemorySettings;
303
+ model: string;
304
+ /**
305
+ * Override the target token budget for the reflected log produced by
306
+ * each batch's reflector call. Defaults to
307
+ * `settings.reflection.bufferActivation` so each batch behaves the
308
+ * same way an in-session reflection does: condense to roughly the
309
+ * half-trigger budget, leaving headroom before the next reflection.
310
+ */
311
+ targetTokens?: number;
312
+ /**
313
+ * Maximum input tokens packed into a single reflector batch. Defaults
314
+ * to `settings.reflection.observationTokens` — the same trigger the
315
+ * in-session reflector uses, so each global-prune batch is the size
316
+ * of one natural reflection round. Multiple batches fire sequentially
317
+ * when the eligible pool is larger; cross-session dedup still works
318
+ * because batches are packed in chronological order, not grouped by
319
+ * `sessionId`.
320
+ */
321
+ batchTokens?: number;
322
+ /**
323
+ * Minimum age, in milliseconds, before a non-reflection row is
324
+ * eligible for prune. Defaults to {@link DEFAULT_GLOBAL_REFLECT_MIN_AGE_MS}.
325
+ * See the {@link DEFAULT_GLOBAL_REFLECT_MIN_AGE_DAYS} doc for the
326
+ * tradeoffs behind this default.
327
+ */
328
+ minAgeMs?: number;
329
+ /**
330
+ * Override of "now" for eligibility comparison. Defaults to
331
+ * `Date.now()`. Tests pin this so the cutoff is deterministic.
332
+ */
333
+ now?: number;
334
+ /**
335
+ * When true, do not write the reflected rows back. The function still
336
+ * runs the reflector model(s) and returns the result so callers can
337
+ * preview the prune (`duet memory reflect --dry-run`).
338
+ */
339
+ dryRun?: boolean;
340
+ onUsage?: (usage: Usage) => void;
341
+ }
342
+ export interface ReflectAllResult {
343
+ /** Observations as they existed before the reflect call. */
344
+ before: Observation[];
345
+ /**
346
+ * Rows preserved verbatim — either too fresh (younger than
347
+ * `minAgeMs`) or already reflection rows. These are written back
348
+ * untouched.
349
+ */
350
+ preserved: Observation[];
351
+ /**
352
+ * Raw observation rows that were eligible for condensation and were
353
+ * folded into one of `reflections`. When `written` is true, these
354
+ * rows have been deleted from the durable store.
355
+ */
356
+ eligible: Observation[];
357
+ /** One reflection row per batch processed. Empty if nothing was eligible. */
358
+ reflections: Observation[];
359
+ /** Whether the pool was rewritten (false on dryRun or when reflections is empty). */
360
+ written: boolean;
361
+ }
362
+ /** Pure-plan output of {@link planReflectionBatches}. */
363
+ export interface ReflectionBatch {
364
+ observations: Observation[];
365
+ estimatedTokens: number;
366
+ }
367
+ export interface PlanReflectionBatchesOptions {
368
+ /** Observations older than this (`createdAt <= cutoff`) are eligible. */
369
+ cutoff: number;
370
+ /** Maximum content tokens packed into a single batch. */
371
+ batchTokens: number;
372
+ }
373
+ /**
374
+ * Partition a snapshot into rows preserved verbatim vs eligible batches
375
+ * fed to the reflector. Pure function — no I/O, no model calls — so the
376
+ * batching/eligibility rules can be unit-tested without docker/LLM.
377
+ *
378
+ * Rules:
379
+ * - `kind === "reflection"` rows are always preserved (we never
380
+ * reflect on reflections; see option 3 in the
381
+ * {@link DEFAULT_GLOBAL_REFLECT_MIN_AGE_DAYS} comment).
382
+ * - Non-reflection rows with `createdAt > cutoff` are preserved (too
383
+ * fresh; resume-info-loss risk too high).
384
+ * - Everything else is eligible. Eligible rows are sorted by
385
+ * `createdAt` ascending and greedily packed into batches whose
386
+ * total `estimateTokens(content)` stays ≤ `batchTokens`. A single
387
+ * oversize row is allowed to occupy its own batch so we never drop
388
+ * rows just because they exceed the cap on their own.
389
+ */
390
+ export declare function planReflectionBatches(observations: readonly Observation[], options: PlanReflectionBatchesOptions): {
391
+ preserved: Observation[];
392
+ batches: ReflectionBatch[];
393
+ };
394
+ /**
395
+ * Cross-session reflect: condense each eligible batch of raw
396
+ * observations through the reflector and replace the eligible rows
397
+ * with one reflection row per batch. Preserved rows (fresh
398
+ * observations and existing reflections) survive verbatim. Used by
399
+ * `duet memory reflect` to prune the global memory store.
400
+ *
401
+ * Unlike `reflectObservations`, this can touch rows across all
402
+ * sessions — the caller is asking for a global prune, not a
403
+ * per-session compaction. See {@link DEFAULT_GLOBAL_REFLECT_MIN_AGE_DAYS}
404
+ * for the resume-info-loss tradeoffs that motivated the min-age gate
405
+ * and the "never reflect on reflections" rule.
406
+ *
407
+ * Returns `undefined` only when there is nothing eligible *and* nothing
408
+ * preserved (empty store). When the store has only fresh/reflection
409
+ * rows, returns a result with empty `eligible`/`reflections` and
410
+ * `written: false` so callers can report "nothing to prune" without
411
+ * conflating it with an empty store.
412
+ */
413
+ export declare function reflectAllObservations(options: ReflectAllOptions): Promise<ReflectAllResult | undefined>;
227
414
  export declare function enforceObservationTokenBudget(options: {
228
415
  text: string;
229
416
  targetTokens: number;
@@ -1 +1 @@
1
- {"version":3,"file":"observational.d.ts","sourceRoot":"","sources":["../../../src/memory/observational.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,KAAK,EAAgB,KAAK,EAAe,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAIrF,OAAO,EAQL,KAAK,gBAAgB,EACtB,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAOlD,OAAO,KAAK,EACV,WAAW,EAEX,gCAAgC,EAChC,2BAA2B,EAC3B,gCAAgC,EACjC,MAAM,oBAAoB,CAAC;AAQ5B,OAAO,EAYL,KAAK,gBAAgB,EACtB,MAAM,4BAA4B,CAAC;AAEpC;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,SAAU,CAAC;AAEjD;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB;IAC/B,iDAAiD;;IAEjD,2DAA2D;;IAE3D,yCAAyC;;CAEjC,CAAC;AAEX;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,MAAM,CAAC;AAEhC;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB;IACjC;;;;;;;;;;;;;;OAcG;;IAEH;;;;;;;;;;;OAWG;;IAEH;;;;OAIG;;CAEK,CAAC;AAeX,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE;QACX,aAAa,EAAE,MAAM,CAAC;QACtB,mBAAmB,EAAE,MAAM,CAAC;QAC5B,uBAAuB,EAAE,MAAM,CAAC;QAChC,gBAAgB,EAAE,MAAM,CAAC;QACzB,sBAAsB,EAAE,MAAM,CAAC;KAChC,CAAC;IACF,UAAU,EAAE;QACV,iBAAiB,EAAE,MAAM,CAAC;QAC1B,gBAAgB,EAAE,MAAM,CAAC;KAC1B,CAAC;IACF,wBAAwB,EAAE,MAAM,CAAC;CAClC;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,gBAAgB,EAAE,MAAM,GAAG,oBAAoB,CAqBlF;AAMD,MAAM,WAAW,cAAc;IAC7B,8EAA8E;IAC9E,SAAS,EAAE,OAAO,CAAC;IACnB,4DAA4D;IAC5D,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,gFAAgF;IAChF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAMD,MAAM,WAAW,eAAe;IAC9B,qEAAqE;IACrE,YAAY,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAoDD,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;CAClC;AAED,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA8C;gBAE7D,MAAM,EAAE,wBAAwB;IAkB5C,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC;IAYxC,aAAa,IAAI,MAAM,EAAE;CAG1B;AAED,MAAM,WAAW,oCAAoC;IACnD,MAAM,EAAE,kBAAkB,CAAC;IAC3B;;;OAGG;IACH,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,gCAAgC,CAAC;IAC5C;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B;;;;;;;;;OASG;IACH,OAAO,EAAE,gBAAgB,CAAC;CAC3B;AAED,MAAM,WAAW,gCAAgC;IAC/C;;;;;OAKG;IACH,OAAO,EAAE,aAAa,CAAC;IACvB,2GAA2G;IAC3G,MAAM,EAAE,kBAAkB,CAAC;IAC3B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,gCAAgC,CAAC;IAC5C,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,gCAAgC,KAAK,IAAI,CAAC;CAChE;AAED,MAAM,WAAW,+BAA+B;IAC9C,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,WAAW,EAAE,WAAW,EAAE,CAAC;CAC5B;AAED;;;;;GAKG;AACH,wBAAgB,kCAAkC,CAChD,gBAAgB,EAAE,MAAM,EACxB,KAAK,CAAC,EAAE,gCAAgC,GACvC,2BAA2B,CAwB7B;AAED;;;;;;;GAOG;AACH,wBAAgB,mCAAmC,CAAC,QAAQ,EAAE,2BAA2B,GAAG,IAAI,CAwB/F;AAED,wBAAgB,mCAAmC,CAAC,OAAO,EAAE,oCAAoC,IAIjF,UAAU,YAAY,EAAE,KAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAmEjE;AAoCD,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,gCAAgC,GACxC,OAAO,CAAC,+BAA+B,CAAC,CA+E1C;AAED,wBAAgB,8BAA8B,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAS3E;AAoCD;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,YAAY,EAAE,CAE1F;AAgRD,wBAAsB,6BAA6B,CAAC,OAAO,EAAE;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAClD,GAAG,OAAO,CAAC,MAAM,CAAC,CAclB;AAED,wBAAgB,gCAAgC,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAY3F;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,gBAAgB,EAAE,EAC5B,gBAAgB,EAAE,MAAM,GACvB,gBAAgB,EAAE,CAmBpB;AAqBD,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,gBAAgB,EAAE,EAC5B,YAAY,EAAE,WAAW,EAAE,GAC1B,gBAAgB,EAAE,CAMpB;AA0BD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,gBAAgB,EAAE,CAI/E;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,GAAG,gBAAgB,GAAG,SAAS,CAgBrF;AAuOD,wBAAgB,wBAAwB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAWrE;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnD"}
1
+ {"version":3,"file":"observational.d.ts","sourceRoot":"","sources":["../../../src/memory/observational.ts"],"names":[],"mappings":"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"}
@@ -2,7 +2,7 @@ import { nanoid } from "nanoid";
2
2
  import { Type } from "typebox";
3
3
  import { generateStructuredOutput } from "../core/structured-output.js";
4
4
  import { applyEvictionHorizon, calculateWireBytes, calculateWireTokens, IMAGE_WIRE_TOKEN_ESTIMATE, findEvictionHorizon, WIRE_BYTE_TARGET, WIRE_BYTE_TRIGGER, } from "../turn-runner/wire-shaping.js";
5
- import { appendObservation, bumpLastUsed, readSessionObservations, replaceSessionObservations, } from "./storage.js";
5
+ import { appendObservation, bumpLastUsed, readSessionObservations, replaceAllObservations, replaceSessionObservations, } from "./storage.js";
6
6
  import { parseObservationGroups, reconcileObservationGroupsFromReflection, renderObservationGroupsForReflection, stripObservationGroups, wrapInObservationGroup, } from "./observation-groups.js";
7
7
  import { GLOBAL_OBSERVATIONS_HEADING, GLOBAL_OBSERVATIONS_HINT, LOCAL_OBSERVATIONS_HEADING, LOCAL_OBSERVATIONS_HINT, OBSERVATION_CONTEXT_INSTRUCTIONS, OBSERVATION_CONTEXT_PROMPT, OBSERVATION_CONTINUATION_HINT, buildObserverPrompt, buildObserverSystemPrompt, buildReflectorPrompt, buildReflectorSystemPrompt, } from "./observational-prompts.js";
8
8
  /**
@@ -135,9 +135,15 @@ const observerResultSchema = Type.Object({
135
135
  usedObservationIds: Type.Optional(Type.Array(Type.String(), {
136
136
  description: "Ids of prior memories from the [memory id: mem_...] markers in the existing observations block whose content actually informed the assistant's response in this exchange. Drives the lastUsedAt freshness signal so reused memories keep surfacing. When multiple existing memories describe the same fact, cite only the single best one (most specific, highest priority, most recent) — never every duplicate, otherwise stale duplicates get uniformly refreshed and decay-ranking breaks. Omit or return [] when no prior memory was leaned on.",
137
137
  })),
138
- currentTask: Type.Optional(Type.String({ description: "Current task state distilled for continuity." })),
139
- suggestedContinuation: Type.Optional(Type.String({ description: "Hint for the actor's next response after context compression." })),
140
- threadTitle: Type.Optional(Type.String({ description: "Short 2-5 word title when thread title generation is requested." })),
138
+ currentTask: Type.Optional(Type.String({
139
+ description: "Current task state distilled for continuity.",
140
+ })),
141
+ suggestedContinuation: Type.Optional(Type.String({
142
+ description: "Hint for the actor's next response after context compression.",
143
+ })),
144
+ threadTitle: Type.Optional(Type.String({
145
+ description: "Short 2-5 word title when thread title generation is requested.",
146
+ })),
141
147
  });
142
148
  const observerResultTool = {
143
149
  name: "recordObservations",
@@ -342,7 +348,10 @@ export async function updateObservationalMemory(options) {
342
348
  : { observations: [], estimatedObservationTokens: 0 };
343
349
  const globalPack = options.memory.getContextPack().global;
344
350
  const unobservedMessages = getUnobservedMessageTail(rawMessages, localSnapshot.observations);
345
- const result = { observations: [], reflections: [] };
351
+ const result = {
352
+ observations: [],
353
+ reflections: [],
354
+ };
346
355
  if (unobservedMessages.length > 0) {
347
356
  emitMemoryActivity(options.onActivity, {
348
357
  phase: "observation",
@@ -555,6 +564,240 @@ async function reflectObservations(args) {
555
564
  await replaceSessionObservations(session, sessionId, [reflected]);
556
565
  return [reflected];
557
566
  }
567
+ /**
568
+ * Sentinel session id used by `reflectAllObservations` to stamp the
569
+ * reflection rows produced from the cross-session pool. Lets the
570
+ * loader/UI distinguish a global prune-reflection from a per-session one
571
+ * without needing a new column.
572
+ */
573
+ export const GLOBAL_REFLECTION_SESSION_ID = "__global_reflection__";
574
+ /**
575
+ * Default minimum age, in days, that a raw observation must reach before
576
+ * the global reflect prune (`duet memory reflect`) is allowed to fold it
577
+ * into a reflection row and delete the original.
578
+ *
579
+ * --- Why a min-age gate exists (read before changing) -----------------
580
+ *
581
+ * The global prune *deletes* the eligible source rows after condensing
582
+ * them. If a long-lived session is later resumed and finds its own local
583
+ * observation rows wiped, the runner's `getUnobservedMessageTail` loses
584
+ * its observation-group range markers — so the next observer call treats
585
+ * the entire session tail as unobserved and re-observes only the newest
586
+ * `FIXED_OBSERVER_BUDGETS.maxTranscriptTokens` (~35k tokens) of raw
587
+ * messages. Everything older than that newest slice is silently dropped
588
+ * by `trimMessagesToTranscriptBudget`, which walks newest→oldest. For
589
+ * sessions that had accumulated rich local condensation, that older
590
+ * content is genuinely lost: the actor's wire eviction horizon already
591
+ * shaped those messages off-wire, so they cannot be re-observed from the
592
+ * raw tail. The global reflection row preserves cross-session themes,
593
+ * but not the session-specific specifics.
594
+ *
595
+ * 3 days is the line where the cost-of-loss flips. After ~3 days a human
596
+ * no longer remembers session specifics anyway, only the higher-level
597
+ * shape — which is what the reflection row captures. Up to 3 days, the
598
+ * specifics still matter for resume continuity, so we hold those rows
599
+ * untouched.
600
+ *
601
+ * --- Alternatives considered and rejected (May 2026) ------------------
602
+ *
603
+ * 1. *Append-only with high-water-mark watermark.* Never delete; just
604
+ * keep appending reflection rows tagged `global-prune`. Recall
605
+ * ranks them above raw rows via `reflectionBias`. Pro: zero risk of
606
+ * resume info loss. Con: the pool grows unbounded over time and we
607
+ * still pay retrieval cost over every stale raw row that the
608
+ * reflection already supersedes. Rejected because the user
609
+ * explicitly wanted the pool to stay small.
610
+ *
611
+ * 2. *Per-session reflect+replace, batched by `session_id`.* Preserves
612
+ * attribution. Rejected because the whole point of the global
613
+ * prune is *cross-session* dedup; grouping by session forfeits
614
+ * that. Used only by the in-session reflector
615
+ * (`reflectObservations`).
616
+ *
617
+ * 3. *Reflect on existing reflections too.* Pro: keeps total row count
618
+ * bounded across many reflect runs. Rejected because reflections
619
+ * of reflections collapse already-condensed text into vaguer text,
620
+ * losing specificity each pass. The current shape preserves rows
621
+ * where `kind === "reflection"` (skipped as input and skipped from
622
+ * the deletion set), so older reflections accumulate but never
623
+ * degrade. A separate `duet memory compact` can later GC stale
624
+ * reflections by `lastUsedAt`.
625
+ *
626
+ * 4. *Smaller min-age (e.g. 1 day) for faster pruning.* Rejected
627
+ * because typical bursty work patterns (a 2-day investigation, a
628
+ * multi-day refactor) would lose mid-thread specifics exactly when
629
+ * a resume is most likely to need them.
630
+ */
631
+ export const DEFAULT_GLOBAL_REFLECT_MIN_AGE_DAYS = 3;
632
+ export const DEFAULT_GLOBAL_REFLECT_MIN_AGE_MS = DEFAULT_GLOBAL_REFLECT_MIN_AGE_DAYS * 24 * 60 * 60 * 1000;
633
+ /**
634
+ * Partition a snapshot into rows preserved verbatim vs eligible batches
635
+ * fed to the reflector. Pure function — no I/O, no model calls — so the
636
+ * batching/eligibility rules can be unit-tested without docker/LLM.
637
+ *
638
+ * Rules:
639
+ * - `kind === "reflection"` rows are always preserved (we never
640
+ * reflect on reflections; see option 3 in the
641
+ * {@link DEFAULT_GLOBAL_REFLECT_MIN_AGE_DAYS} comment).
642
+ * - Non-reflection rows with `createdAt > cutoff` are preserved (too
643
+ * fresh; resume-info-loss risk too high).
644
+ * - Everything else is eligible. Eligible rows are sorted by
645
+ * `createdAt` ascending and greedily packed into batches whose
646
+ * total `estimateTokens(content)` stays ≤ `batchTokens`. A single
647
+ * oversize row is allowed to occupy its own batch so we never drop
648
+ * rows just because they exceed the cap on their own.
649
+ */
650
+ export function planReflectionBatches(observations, options) {
651
+ const preserved = [];
652
+ const eligible = [];
653
+ for (const observation of observations) {
654
+ if (observation.kind === "reflection") {
655
+ preserved.push(observation);
656
+ continue;
657
+ }
658
+ if (observation.createdAt > options.cutoff) {
659
+ preserved.push(observation);
660
+ continue;
661
+ }
662
+ eligible.push(observation);
663
+ }
664
+ eligible.sort((a, b) => a.createdAt - b.createdAt);
665
+ const batches = [];
666
+ let current = [];
667
+ let currentTokens = 0;
668
+ for (const observation of eligible) {
669
+ const tokens = estimateTokens(observation.content);
670
+ // Roll over to a new batch when adding this row would exceed the
671
+ // cap — unless the current batch is empty, in which case we keep
672
+ // the (oversize) row so it isn't silently dropped.
673
+ if (current.length > 0 && currentTokens + tokens > options.batchTokens) {
674
+ batches.push({ observations: current, estimatedTokens: currentTokens });
675
+ current = [];
676
+ currentTokens = 0;
677
+ }
678
+ current.push(observation);
679
+ currentTokens += tokens;
680
+ }
681
+ if (current.length > 0) {
682
+ batches.push({ observations: current, estimatedTokens: currentTokens });
683
+ }
684
+ return { preserved, batches };
685
+ }
686
+ /**
687
+ * Cross-session reflect: condense each eligible batch of raw
688
+ * observations through the reflector and replace the eligible rows
689
+ * with one reflection row per batch. Preserved rows (fresh
690
+ * observations and existing reflections) survive verbatim. Used by
691
+ * `duet memory reflect` to prune the global memory store.
692
+ *
693
+ * Unlike `reflectObservations`, this can touch rows across all
694
+ * sessions — the caller is asking for a global prune, not a
695
+ * per-session compaction. See {@link DEFAULT_GLOBAL_REFLECT_MIN_AGE_DAYS}
696
+ * for the resume-info-loss tradeoffs that motivated the min-age gate
697
+ * and the "never reflect on reflections" rule.
698
+ *
699
+ * Returns `undefined` only when there is nothing eligible *and* nothing
700
+ * preserved (empty store). When the store has only fresh/reflection
701
+ * rows, returns a result with empty `eligible`/`reflections` and
702
+ * `written: false` so callers can report "nothing to prune" without
703
+ * conflating it with an empty store.
704
+ */
705
+ export async function reflectAllObservations(options) {
706
+ const { session, snapshot, settings, model, dryRun, onUsage } = options;
707
+ if (snapshot.observations.length === 0) {
708
+ return undefined;
709
+ }
710
+ const minAgeMs = options.minAgeMs ?? DEFAULT_GLOBAL_REFLECT_MIN_AGE_MS;
711
+ const now = options.now ?? Date.now();
712
+ const cutoff = now - minAgeMs;
713
+ const batchTokens = options.batchTokens ?? settings.reflection.observationTokens;
714
+ const targetTokens = options.targetTokens ?? settings.reflection.bufferActivation;
715
+ const { preserved, batches } = planReflectionBatches(snapshot.observations, {
716
+ cutoff,
717
+ batchTokens,
718
+ });
719
+ if (batches.length === 0) {
720
+ return {
721
+ before: snapshot.observations,
722
+ preserved,
723
+ eligible: [],
724
+ reflections: [],
725
+ written: false,
726
+ };
727
+ }
728
+ const eligible = batches.flatMap((batch) => batch.observations);
729
+ const reflections = [];
730
+ for (const batch of batches) {
731
+ const reflection = await reflectBatch({
732
+ batch,
733
+ settings,
734
+ model,
735
+ targetTokens,
736
+ onUsage,
737
+ });
738
+ if (reflection) {
739
+ reflections.push(reflection);
740
+ }
741
+ }
742
+ const written = !dryRun && reflections.length > 0;
743
+ if (written) {
744
+ // Single-write replacement keeps the storage transaction atomic:
745
+ // eligible rows disappear and new reflection rows appear together,
746
+ // never leaving a peer CLI looking at a half-pruned pool.
747
+ await replaceAllObservations(session, [...preserved, ...reflections]);
748
+ }
749
+ return {
750
+ before: snapshot.observations,
751
+ preserved,
752
+ eligible,
753
+ reflections,
754
+ written,
755
+ };
756
+ }
757
+ async function reflectBatch(args) {
758
+ const { batch, settings, model, targetTokens, onUsage } = args;
759
+ const source = batch.observations.map((observation) => observation.content).join("\n\n");
760
+ const rendered = renderObservationGroupsForReflection(source) ?? source;
761
+ const result = await generateStructuredOutput({
762
+ model,
763
+ tool: reflectorResultTool,
764
+ systemPrompt: buildReflectorSystemPrompt(settings.reflection.instruction),
765
+ prompt: buildReflectorPrompt(rendered, targetTokens),
766
+ onUsage,
767
+ });
768
+ const text = await enforceObservationTokenBudget({
769
+ text: result.observations,
770
+ targetTokens,
771
+ retry: async (actualTokens) => {
772
+ const retryResult = await generateStructuredOutput({
773
+ model,
774
+ tool: reflectorResultTool,
775
+ systemPrompt: buildReflectorSystemPrompt(settings.reflection.instruction),
776
+ prompt: buildReflectorPrompt(rendered, targetTokens, { actualTokens }),
777
+ onUsage,
778
+ });
779
+ return retryResult.observations;
780
+ },
781
+ });
782
+ if (!text || text.trim().length === 0) {
783
+ return undefined;
784
+ }
785
+ const reconciled = reconcileObservationGroupsFromReflection(text, source) ?? text;
786
+ const now = Date.now();
787
+ return {
788
+ id: createMemoryId(),
789
+ createdAt: now,
790
+ lastUsedAt: now,
791
+ kind: "reflection",
792
+ sessionId: GLOBAL_REFLECTION_SESSION_ID,
793
+ observedDate: new Date().toISOString().slice(0, 10),
794
+ timeOfDay: new Date().toISOString().slice(11, 16),
795
+ priority: "high",
796
+ source: { kind: "system" },
797
+ content: reconciled,
798
+ tags: ["observational-memory", "reflection", "global-prune"],
799
+ };
800
+ }
558
801
  async function observe(messages, previousLocalObservations, attributableMemories, settings, model, onUsage) {
559
802
  const transcriptBudget = settings.observation.maxTranscriptTokens;
560
803
  const observationLogBudget = settings.observation.maxObservationLogTokens;