@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.
- package/README.md +2 -0
- package/dist/package.json +1 -1
- package/dist/src/cli/help.d.ts +1 -0
- package/dist/src/cli/help.d.ts.map +1 -1
- package/dist/src/cli/help.js +41 -6
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/memory-reflect.d.ts +13 -0
- package/dist/src/cli/memory-reflect.d.ts.map +1 -0
- package/dist/src/cli/memory-reflect.js +165 -0
- package/dist/src/cli/memory-reflect.js.map +1 -0
- package/dist/src/cli/memory.d.ts.map +1 -1
- package/dist/src/cli/memory.js +8 -0
- package/dist/src/cli/memory.js.map +1 -1
- package/dist/src/cli/skills.d.ts +8 -4
- package/dist/src/cli/skills.d.ts.map +1 -1
- package/dist/src/cli/skills.js +21 -13
- package/dist/src/cli/skills.js.map +1 -1
- package/dist/src/cli.d.ts +1 -0
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +1 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/memory/observational.d.ts +187 -0
- package/dist/src/memory/observational.d.ts.map +1 -1
- package/dist/src/memory/observational.js +248 -5
- package/dist/src/memory/observational.js.map +1 -1
- package/dist/src/memory/storage.d.ts +16 -0
- package/dist/src/memory/storage.d.ts.map +1 -1
- package/dist/src/memory/storage.js +44 -0
- package/dist/src/memory/storage.js.map +1 -1
- package/dist/src/tui/app.d.ts.map +1 -1
- package/dist/src/tui/app.js +1 -14
- package/dist/src/tui/app.js.map +1 -1
- package/dist/src/tui/autocomplete.d.ts +0 -9
- package/dist/src/tui/autocomplete.d.ts.map +1 -1
- package/dist/src/tui/autocomplete.js +0 -13
- package/dist/src/tui/autocomplete.js.map +1 -1
- package/dist/src/tui/boot-screen.d.ts.map +1 -1
- package/dist/src/tui/boot-screen.js +0 -5
- package/dist/src/tui/boot-screen.js.map +1 -1
- package/dist/src/turn-runner/built-in-skills.d.ts +23 -0
- package/dist/src/turn-runner/built-in-skills.d.ts.map +1 -0
- package/dist/src/turn-runner/built-in-skills.js +75 -0
- package/dist/src/turn-runner/built-in-skills.js.map +1 -0
- package/dist/src/turn-runner/skills.d.ts +1 -1
- package/dist/src/turn-runner/skills.d.ts.map +1 -1
- package/dist/src/turn-runner/skills.js +16 -2
- package/dist/src/turn-runner/skills.js.map +1 -1
- package/dist/src/turn-runner/state-compaction.d.ts +49 -0
- package/dist/src/turn-runner/state-compaction.d.ts.map +1 -0
- package/dist/src/turn-runner/state-compaction.js +75 -0
- package/dist/src/turn-runner/state-compaction.js.map +1 -0
- package/dist/src/turn-runner/turn-runner.d.ts +8 -0
- package/dist/src/turn-runner/turn-runner.d.ts.map +1 -1
- package/dist/src/turn-runner/turn-runner.js +30 -1
- package/dist/src/turn-runner/turn-runner.js.map +1 -1
- package/dist/src/types/config.d.ts +19 -0
- package/dist/src/types/config.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/src/tui/relay-command.d.ts +0 -14
- package/dist/src/tui/relay-command.d.ts.map +0 -1
- package/dist/src/tui/relay-command.js +0 -58
- 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;
|
|
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({
|
|
139
|
-
|
|
140
|
-
|
|
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 = {
|
|
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;
|