@duetso/agent 0.1.98 → 0.1.101
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/dist/package.json +1 -1
- package/dist/src/memory/context-pack.d.ts +2 -2
- package/dist/src/memory/context-pack.d.ts.map +1 -1
- package/dist/src/memory/context-pack.js +21 -16
- package/dist/src/memory/context-pack.js.map +1 -1
- package/dist/src/memory/embedding-worker.d.ts +8 -3
- package/dist/src/memory/embedding-worker.d.ts.map +1 -1
- package/dist/src/memory/embedding-worker.js +42 -28
- package/dist/src/memory/embedding-worker.js.map +1 -1
- package/dist/src/memory/observational.d.ts +8 -3
- package/dist/src/memory/observational.d.ts.map +1 -1
- package/dist/src/memory/observational.js +10 -10
- package/dist/src/memory/observational.js.map +1 -1
- package/dist/src/memory/pglite.d.ts +57 -17
- package/dist/src/memory/pglite.d.ts.map +1 -1
- package/dist/src/memory/pglite.js +153 -140
- package/dist/src/memory/pglite.js.map +1 -1
- package/dist/src/memory/recall.d.ts +9 -2
- package/dist/src/memory/recall.d.ts.map +1 -1
- package/dist/src/memory/recall.js +23 -20
- package/dist/src/memory/recall.js.map +1 -1
- package/dist/src/memory/session.d.ts +78 -0
- package/dist/src/memory/session.d.ts.map +1 -0
- package/dist/src/memory/session.js +198 -0
- package/dist/src/memory/session.js.map +1 -0
- package/dist/src/memory/storage.d.ts +29 -15
- package/dist/src/memory/storage.d.ts.map +1 -1
- package/dist/src/memory/storage.js +80 -54
- package/dist/src/memory/storage.js.map +1 -1
- package/dist/src/turn-runner/tools.d.ts +8 -6
- package/dist/src/turn-runner/tools.d.ts.map +1 -1
- package/dist/src/turn-runner/tools.js +3 -3
- package/dist/src/turn-runner/tools.js.map +1 -1
- package/dist/src/turn-runner/turn-runner.d.ts +17 -6
- package/dist/src/turn-runner/turn-runner.d.ts.map +1 -1
- package/dist/src/turn-runner/turn-runner.js +41 -13
- package/dist/src/turn-runner/turn-runner.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recall.js","sourceRoot":"","sources":["../../../src/memory/recall.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"recall.js","sourceRoot":"","sources":["../../../src/memory/recall.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,iGAAiG;AACjG,MAAM,cAAc,GAAG,EAAE,CAAC;AAC1B;;;;;GAKG;AACH,MAAM,KAAK,GAAG,EAAE,CAAC;AAuCjB,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,OAA4B;IAC7D,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,CAAC,CAAC;IACjC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,KAAK,CAAC;IAErC,qEAAqE;IACrE,6DAA6D;IAC7D,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC;QAC9C,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,qBAAqB,EAAE,KAAK,EAAE,qBAAqB,EAAE,KAAK,EAAE,CAAC;IAC1F,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QACvD,MAAM,WAAW,GAAG,MAAM,aAAa,CAAC,EAAE,EAAE,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;QAErF,IAAI,eAAe,GAAG,KAAK,CAAC;QAC5B,IAAI,UAAU,GAAgB,EAAE,CAAC;QACjC,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;YAClB,eAAe,GAAG,IAAI,CAAC;YACvB,IAAI,CAAC;gBACH,UAAU,GAAG,MAAM,YAAY,CAAC,EAAE,EAAE,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;YAC9F,CAAC;YAAC,MAAM,CAAC;gBACP,6DAA6D;gBAC7D,2DAA2D;gBAC3D,UAAU,GAAG,EAAE,CAAC;YAClB,CAAC;QACH,CAAC;QAED,MAAM,QAAQ,GAAG,oBAAoB,CAAC,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;QACjF,MAAM,YAAY,GAAG,MAAM,OAAO,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;QAEjD,OAAO;YACL,YAAY;YACZ,qBAAqB,EAAE,eAAe;YACtC,qBAAqB,EAAE,UAAU,CAAC,MAAM,GAAG,CAAC;SAC7C,CAAC;IACJ,CAAC,CAAC,CAAC;IACH,OAAO,MAAM,IAAI,EAAE,YAAY,EAAE,EAAE,EAAE,qBAAqB,EAAE,KAAK,EAAE,qBAAqB,EAAE,KAAK,EAAE,CAAC;AACpG,CAAC;AAOD,KAAK,UAAU,aAAa,CAC1B,EAAU,EACV,KAAa,EACb,KAAkB,EAClB,SAA6B;IAE7B,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IAExB,kEAAkE;IAClE,sEAAsE;IACtE,sEAAsE;IACtE,iBAAiB;IACjB,MAAM,OAAO,GAAG;;kFAEgE,CAAC;IACjF,MAAM,QAAQ,GAAG;;;aAGN,cAAc,EAAE,CAAC;IAE5B,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,cAAc,CACnC,EAAE,EACF,OAAO,EACP,QAAQ,EACR,CAAC,OAAO,CAAC,EACT,KAAK,EACL,SAAS,CACV,CAAC;IACF,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;AACjE,CAAC;AAED,KAAK,UAAU,YAAY,CACzB,EAAU,EACV,KAAc,EACd,KAAa,EACb,KAAkB,EAClB,SAA6B;IAE7B,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IAExB,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAC9C,MAAM,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;IAC7B,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,CAAC;IAEvB,MAAM,OAAO,GAAG;;;gBAGF,CAAC;IACf,MAAM,QAAQ,GAAG;;aAEN,cAAc,EAAE,CAAC;IAE5B,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,cAAc,CACnC,EAAE,EACF,OAAO,EACP,QAAQ,EACR,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EACzB,KAAK,EACL,SAAS;IACT,iEAAiE;IACjE,2BAA2B;IAC3B,IAAI,CACL,CAAC;IACF,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;AACjE,CAAC;AAED;;;;;;GAMG;AACH,KAAK,UAAU,cAAc,CAC3B,EAAU,EACV,OAAe,EACf,QAAgB,EAChB,UAAqB,EACrB,KAAkB,EAClB,SAA6B,EAC7B,YAAY,GAAG,EAAE;IAEjB,MAAM,SAAS,GAAG,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC;IACxC,IAAI,KAAK,KAAK,SAAS,IAAI,SAAS,EAAE,CAAC;QACrC,OAAO,EAAE,CAAC,KAAK,CAAO,GAAG,OAAO,QAAQ,YAAY,iBAAiB,SAAS,GAAG,QAAQ,EAAE,EAAE;YAC3F,GAAG,UAAU;YACb,SAAS;SACV,CAAC,CAAC;IACL,CAAC;IACD,IAAI,KAAK,KAAK,QAAQ,IAAI,SAAS,EAAE,CAAC;QACpC,OAAO,EAAE,CAAC,KAAK,CACb,GAAG,OAAO,QAAQ,YAAY,gCAAgC,SAAS,GAAG,QAAQ,EAAE,EACpF,CAAC,GAAG,UAAU,EAAE,SAAS,CAAC,CAC3B,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,CAAC,KAAK,CAAO,GAAG,OAAO,GAAG,QAAQ,EAAE,EAAE,UAAU,CAAC,CAAC;AAC7D,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,oBAAoB,CAAC,WAA0B;IAC7D,MAAM,MAAM,GAAG,IAAI,GAAG,EAAgD,CAAC;IACvE,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;QAC/B,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,MAAM,YAAY,GAAG,CAAC,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC;YAC5C,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACpC,IAAI,QAAQ,EAAE,CAAC;gBACb,QAAQ,CAAC,KAAK,IAAI,YAAY,CAAC;YACjC,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;YAClE,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;SAChC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACb,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK;YAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;QAC9D,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACzC,CAAC,CAAC;SACD,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;AACvB,CAAC;AAED,KAAK,UAAU,OAAO,CAAC,EAAU,EAAE,GAAa;IAC9C,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAChC,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,EAAE,CAAC,KAAK,CAC7B;;;gCAG4B,EAC5B,CAAC,GAAG,CAAC,CACN,CAAC;IACF,iEAAiE;IACjE,6DAA6D;IAC7D,wBAAwB;IACxB,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;IACvD,OAAO,GAAG;SACP,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;SACzB,MAAM,CAAC,CAAC,GAAG,EAAsB,EAAE,CAAC,GAAG,KAAK,SAAS,CAAC;SACtD,GAAG,CAAC,gBAAgB,CAAC,CAAC;AAC3B,CAAC;AAkBD,SAAS,gBAAgB,CAAC,GAAgB;IACxC,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,SAAS,EAAE,GAAG,CAAC,UAAU;QACzB,UAAU,EAAE,GAAG,CAAC,YAAY;QAC5B,GAAG,CAAC,GAAG,CAAC,UAAU,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjE,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,YAAY,EAAE,GAAG,CAAC,aAAa;QAC/B,GAAG,CAAC,GAAG,CAAC,eAAe,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,GAAG,CAAC,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChF,GAAG,CAAC,GAAG,CAAC,aAAa,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,GAAG,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1E,GAAG,CAAC,GAAG,CAAC,WAAW,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACnE,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,WAAW,CAA0B;QAC5D,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAa;KAC5C,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { PGlite } from "@electric-sql/pglite";
|
|
2
|
+
import { type OpenPGliteOptions } from "./pglite.js";
|
|
3
|
+
export interface MemorySessionOptions {
|
|
4
|
+
/** Absolute path to the PGlite data directory. */
|
|
5
|
+
path: string;
|
|
6
|
+
/**
|
|
7
|
+
* Passed through to `openPGliteHoldingLock` on each open — typically
|
|
8
|
+
* supplies `init` (migrations) and `onRecover` (quarantine surface).
|
|
9
|
+
*/
|
|
10
|
+
openOptions: OpenPGliteOptions;
|
|
11
|
+
/**
|
|
12
|
+
* How long to keep the handle open after the last in-flight op
|
|
13
|
+
* resolves. A new `withDb` call within this window reuses the open
|
|
14
|
+
* handle and cancels the pending close. Defaults to 2s.
|
|
15
|
+
*/
|
|
16
|
+
idleCloseMs?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Wall-time budget for waiting on the cross-process lock before a
|
|
19
|
+
* single `withDb` call gives up. On exhaustion the call resolves to
|
|
20
|
+
* `undefined` and `onWarn` is invoked once. Defaults to 30s.
|
|
21
|
+
*/
|
|
22
|
+
lockWaitBudgetMs?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Invoked once when a `withDb` call is skipped because the lock
|
|
25
|
+
* could not be acquired within `lockWaitBudgetMs`. Lets the host
|
|
26
|
+
* surface a single user-visible warning instead of repeating per
|
|
27
|
+
* skipped op.
|
|
28
|
+
*/
|
|
29
|
+
onWarn?: (message: string) => void;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Manages a single PGlite handle for one dataDir across many short
|
|
33
|
+
* bursts of work. Refcounted opens keep the handle (and the
|
|
34
|
+
* cross-process `.duet-open.lock`) alive while any `withDb` is
|
|
35
|
+
* in-flight; an idle timer releases both shortly after the last op
|
|
36
|
+
* resolves so a peer process can step in.
|
|
37
|
+
*
|
|
38
|
+
* Concurrent in-process `withDb` calls share one `PGlite.create`: the
|
|
39
|
+
* first call allocates `this.opening` and every other call awaits the
|
|
40
|
+
* same promise, so two opens cannot race into `init` and double-run
|
|
41
|
+
* migrations against a fresh dataDir.
|
|
42
|
+
*/
|
|
43
|
+
export declare class MemorySession {
|
|
44
|
+
private readonly path;
|
|
45
|
+
private readonly openOptions;
|
|
46
|
+
private readonly idleCloseMs;
|
|
47
|
+
private readonly lockWaitBudgetMs;
|
|
48
|
+
private readonly onWarn?;
|
|
49
|
+
private db;
|
|
50
|
+
private lockPath;
|
|
51
|
+
private opening;
|
|
52
|
+
private refs;
|
|
53
|
+
private idleTimer;
|
|
54
|
+
private disposed;
|
|
55
|
+
private disposing;
|
|
56
|
+
constructor(opts: MemorySessionOptions);
|
|
57
|
+
/**
|
|
58
|
+
* Run `fn` against the (lazily-opened) memory database. Returns
|
|
59
|
+
* `undefined` without invoking `fn` when the session is disposed or
|
|
60
|
+
* the lock could not be acquired within the budget; otherwise
|
|
61
|
+
* returns whatever `fn` returns. Errors thrown inside `fn`
|
|
62
|
+
* propagate.
|
|
63
|
+
*/
|
|
64
|
+
withDb<T>(fn: (db: PGlite) => Promise<T>): Promise<T | undefined>;
|
|
65
|
+
/**
|
|
66
|
+
* Wait for any in-flight `withDb` to drain, cancel a pending idle
|
|
67
|
+
* close, then close the handle and release the lock. Subsequent
|
|
68
|
+
* `withDb` calls resolve to `undefined`.
|
|
69
|
+
*/
|
|
70
|
+
dispose(): Promise<void>;
|
|
71
|
+
private runDispose;
|
|
72
|
+
private ensureOpen;
|
|
73
|
+
private openWithPolling;
|
|
74
|
+
private scheduleIdleClose;
|
|
75
|
+
private cancelIdleClose;
|
|
76
|
+
private closeNow;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/memory/session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAEL,KAAK,iBAAiB,EAIvB,MAAM,aAAa,CAAC;AA4BrB,MAAM,WAAW,oBAAoB;IACnC,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,WAAW,EAAE,iBAAiB,CAAC;IAC/B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoB;IAChD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAA4B;IAEpD,OAAO,CAAC,EAAE,CAAqB;IAC/B,OAAO,CAAC,QAAQ,CAAqB;IACrC,OAAO,CAAC,OAAO,CAA0C;IACzD,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,SAAS,CAA4C;IAC7D,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,SAAS,CAA4B;gBAEjC,IAAI,EAAE,oBAAoB;IAQtC;;;;;;OAMG;IACG,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IA0BvE;;;;OAIG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;YAMhB,UAAU;YAYV,UAAU;YAqBV,eAAe;IAwB7B,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,eAAe;YAOT,QAAQ;CAevB"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { MemoryLockTimeoutError, openPGliteHoldingLock, releaseOpenLock, tryAcquireOpenLock, } from "./pglite.js";
|
|
2
|
+
/**
|
|
3
|
+
* Default time the PGlite handle stays open after the last `withDb`
|
|
4
|
+
* call resolves before it is closed and the cross-process lock is
|
|
5
|
+
* released. Keeps short bursts of writes (observer + reflector +
|
|
6
|
+
* embedding upserts in the same turn) on one open without making a
|
|
7
|
+
* sitting CLI permanently hold the lock against a second CLI.
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_IDLE_CLOSE_MS = 2_000;
|
|
10
|
+
/**
|
|
11
|
+
* Default total wall-time budget for waiting on the cross-process
|
|
12
|
+
* open-lock. Generous enough to outlast a moderate write burst from a
|
|
13
|
+
* concurrent duet CLI but bounded so a wedged peer cannot stall the
|
|
14
|
+
* caller's turn indefinitely; on exhaustion `withDb` resolves to
|
|
15
|
+
* `undefined` and the caller degrades gracefully.
|
|
16
|
+
*/
|
|
17
|
+
const DEFAULT_LOCK_WAIT_BUDGET_MS = 30_000;
|
|
18
|
+
/**
|
|
19
|
+
* Exponential backoff schedule used while polling for the open-lock.
|
|
20
|
+
* Doubles from 50ms up to a 1s ceiling so a peer that releases the
|
|
21
|
+
* lock during an idle-close gap is picked up within ~50ms, while a
|
|
22
|
+
* long-running peer does not produce a poll storm.
|
|
23
|
+
*/
|
|
24
|
+
const POLL_BACKOFF_MS = [50, 100, 200, 400, 800, 1000];
|
|
25
|
+
/**
|
|
26
|
+
* Manages a single PGlite handle for one dataDir across many short
|
|
27
|
+
* bursts of work. Refcounted opens keep the handle (and the
|
|
28
|
+
* cross-process `.duet-open.lock`) alive while any `withDb` is
|
|
29
|
+
* in-flight; an idle timer releases both shortly after the last op
|
|
30
|
+
* resolves so a peer process can step in.
|
|
31
|
+
*
|
|
32
|
+
* Concurrent in-process `withDb` calls share one `PGlite.create`: the
|
|
33
|
+
* first call allocates `this.opening` and every other call awaits the
|
|
34
|
+
* same promise, so two opens cannot race into `init` and double-run
|
|
35
|
+
* migrations against a fresh dataDir.
|
|
36
|
+
*/
|
|
37
|
+
export class MemorySession {
|
|
38
|
+
path;
|
|
39
|
+
openOptions;
|
|
40
|
+
idleCloseMs;
|
|
41
|
+
lockWaitBudgetMs;
|
|
42
|
+
onWarn;
|
|
43
|
+
db;
|
|
44
|
+
lockPath;
|
|
45
|
+
opening;
|
|
46
|
+
refs = 0;
|
|
47
|
+
idleTimer;
|
|
48
|
+
disposed = false;
|
|
49
|
+
disposing;
|
|
50
|
+
constructor(opts) {
|
|
51
|
+
this.path = opts.path;
|
|
52
|
+
this.openOptions = opts.openOptions;
|
|
53
|
+
this.idleCloseMs = opts.idleCloseMs ?? DEFAULT_IDLE_CLOSE_MS;
|
|
54
|
+
this.lockWaitBudgetMs = opts.lockWaitBudgetMs ?? DEFAULT_LOCK_WAIT_BUDGET_MS;
|
|
55
|
+
if (opts.onWarn)
|
|
56
|
+
this.onWarn = opts.onWarn;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Run `fn` against the (lazily-opened) memory database. Returns
|
|
60
|
+
* `undefined` without invoking `fn` when the session is disposed or
|
|
61
|
+
* the lock could not be acquired within the budget; otherwise
|
|
62
|
+
* returns whatever `fn` returns. Errors thrown inside `fn`
|
|
63
|
+
* propagate.
|
|
64
|
+
*/
|
|
65
|
+
async withDb(fn) {
|
|
66
|
+
if (this.disposed)
|
|
67
|
+
return undefined;
|
|
68
|
+
this.cancelIdleClose();
|
|
69
|
+
// Increment refs before awaiting the open so a concurrent
|
|
70
|
+
// `dispose()` waits for this call to finish even if the await on
|
|
71
|
+
// `ensureOpen` resolves before fn runs. Without this, dispose
|
|
72
|
+
// could see refs=0, close the handle, and leave fn to run against
|
|
73
|
+
// a closed db.
|
|
74
|
+
this.refs++;
|
|
75
|
+
try {
|
|
76
|
+
const db = await this.ensureOpen();
|
|
77
|
+
if (!db)
|
|
78
|
+
return undefined;
|
|
79
|
+
// Once the handle is open, run fn even if dispose was called
|
|
80
|
+
// while we were waiting on `ensureOpen`. dispose's drain loop
|
|
81
|
+
// waits for refs to reach zero, so the call completes against
|
|
82
|
+
// a still-open db; without this we would silently skip fn even
|
|
83
|
+
// though the caller had already committed to running it.
|
|
84
|
+
return await fn(db);
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
this.refs--;
|
|
88
|
+
if (this.refs === 0 && !this.disposed) {
|
|
89
|
+
this.scheduleIdleClose();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Wait for any in-flight `withDb` to drain, cancel a pending idle
|
|
95
|
+
* close, then close the handle and release the lock. Subsequent
|
|
96
|
+
* `withDb` calls resolve to `undefined`.
|
|
97
|
+
*/
|
|
98
|
+
async dispose() {
|
|
99
|
+
if (this.disposing)
|
|
100
|
+
return this.disposing;
|
|
101
|
+
this.disposing = this.runDispose();
|
|
102
|
+
return this.disposing;
|
|
103
|
+
}
|
|
104
|
+
async runDispose() {
|
|
105
|
+
this.disposed = true;
|
|
106
|
+
this.cancelIdleClose();
|
|
107
|
+
// Wait for in-flight withDb fns to settle. Polling is fine here:
|
|
108
|
+
// dispose is rare and refs decrements happen on microtask
|
|
109
|
+
// boundaries, so this loop spins a few times at most.
|
|
110
|
+
while (this.refs > 0) {
|
|
111
|
+
await sleep(5);
|
|
112
|
+
}
|
|
113
|
+
await this.closeNow();
|
|
114
|
+
}
|
|
115
|
+
async ensureOpen() {
|
|
116
|
+
if (this.db)
|
|
117
|
+
return this.db;
|
|
118
|
+
if (this.opening)
|
|
119
|
+
return this.opening;
|
|
120
|
+
this.opening = this.openWithPolling()
|
|
121
|
+
.then((db) => {
|
|
122
|
+
this.db = db;
|
|
123
|
+
return db;
|
|
124
|
+
})
|
|
125
|
+
.catch((error) => {
|
|
126
|
+
if (error instanceof MemoryLockTimeoutError) {
|
|
127
|
+
this.onWarn?.("memory db busy, skipping op");
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
throw error;
|
|
131
|
+
})
|
|
132
|
+
.finally(() => {
|
|
133
|
+
this.opening = undefined;
|
|
134
|
+
});
|
|
135
|
+
return this.opening;
|
|
136
|
+
}
|
|
137
|
+
async openWithPolling() {
|
|
138
|
+
const start = Date.now();
|
|
139
|
+
let attempt = 0;
|
|
140
|
+
let lastHolderPid = 0;
|
|
141
|
+
while (true) {
|
|
142
|
+
const result = tryAcquireOpenLock(this.path);
|
|
143
|
+
if ("lockPath" in result) {
|
|
144
|
+
const opened = await openPGliteHoldingLock(this.path, this.openOptions, result.lockPath);
|
|
145
|
+
this.lockPath = opened.lockPath;
|
|
146
|
+
return opened.db;
|
|
147
|
+
}
|
|
148
|
+
lastHolderPid = result.holderPid;
|
|
149
|
+
const elapsed = Date.now() - start;
|
|
150
|
+
if (elapsed >= this.lockWaitBudgetMs) {
|
|
151
|
+
throw new MemoryLockTimeoutError(this.path, lastHolderPid, this.lockWaitBudgetMs);
|
|
152
|
+
}
|
|
153
|
+
const base = POLL_BACKOFF_MS[Math.min(attempt, POLL_BACKOFF_MS.length - 1)] ?? 1000;
|
|
154
|
+
const remaining = this.lockWaitBudgetMs - elapsed;
|
|
155
|
+
const delay = Math.max(1, Math.min(base, remaining));
|
|
156
|
+
attempt++;
|
|
157
|
+
await sleep(delay);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
scheduleIdleClose() {
|
|
161
|
+
this.cancelIdleClose();
|
|
162
|
+
this.idleTimer = setTimeout(() => {
|
|
163
|
+
this.idleTimer = undefined;
|
|
164
|
+
// Re-check refs in case a new withDb arrived between the timer
|
|
165
|
+
// firing and this callback running.
|
|
166
|
+
if (this.refs > 0 || this.disposed)
|
|
167
|
+
return;
|
|
168
|
+
void this.closeNow();
|
|
169
|
+
}, this.idleCloseMs);
|
|
170
|
+
}
|
|
171
|
+
cancelIdleClose() {
|
|
172
|
+
if (this.idleTimer) {
|
|
173
|
+
clearTimeout(this.idleTimer);
|
|
174
|
+
this.idleTimer = undefined;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async closeNow() {
|
|
178
|
+
const db = this.db;
|
|
179
|
+
const lockPath = this.lockPath;
|
|
180
|
+
this.db = undefined;
|
|
181
|
+
this.lockPath = undefined;
|
|
182
|
+
if (db) {
|
|
183
|
+
try {
|
|
184
|
+
await db.close();
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// PGlite occasionally throws on close after an aborted op;
|
|
188
|
+
// we still need to release the lock so a peer can open.
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (lockPath)
|
|
192
|
+
releaseOpenLock(lockPath);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function sleep(ms) {
|
|
196
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../../src/memory/session.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import type { PGlite } from "@electric-sql/pglite";
|
|
2
1
|
import type { Observation, ObservationalMemorySettings } from "../types/memory.js";
|
|
3
2
|
import type { EmbedFn } from "./embedding.js";
|
|
3
|
+
import { MemorySession } from "./session.js";
|
|
4
4
|
import type { MemoryContextCache } from "./store.js";
|
|
5
|
-
export type MemoryDatabase = PGlite;
|
|
6
5
|
/**
|
|
7
|
-
* Handle returned by `loadStoredMemory`. Callers
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* Handle returned by `loadStoredMemory`. Callers pass `session` into the
|
|
7
|
+
* storage helpers, which each open the underlying PGlite handle just long
|
|
8
|
+
* enough to run their queries and then hand it back to the idle-close
|
|
9
|
+
* timer so a peer duet CLI can acquire the cross-process lock between
|
|
10
|
+
* bursts. `session` is undefined when memory persistence is disabled.
|
|
11
11
|
*/
|
|
12
12
|
export interface MemoryPersistenceHandle {
|
|
13
|
-
|
|
13
|
+
session: MemorySession | undefined;
|
|
14
14
|
embed: EmbedFn | undefined;
|
|
15
15
|
dispose: () => Promise<void>;
|
|
16
16
|
}
|
|
@@ -35,40 +35,54 @@ export interface LoadStoredMemoryOptions {
|
|
|
35
35
|
backupPath: string;
|
|
36
36
|
cause: unknown;
|
|
37
37
|
}) => void;
|
|
38
|
+
/**
|
|
39
|
+
* Invoked once when a `withDb` call is skipped because the cross-process
|
|
40
|
+
* open-lock could not be acquired within the wait budget. The runner
|
|
41
|
+
* surfaces this as a single user-visible system warning so two concurrent
|
|
42
|
+
* duet CLIs can coexist (one degrades to no-memory) without spamming the
|
|
43
|
+
* UI per skipped op.
|
|
44
|
+
*/
|
|
45
|
+
onWarn?: (message: string) => void;
|
|
38
46
|
}
|
|
39
47
|
export declare function loadStoredMemory(memoryPath: string | false | undefined, cwd: string, options?: LoadStoredMemoryOptions): Promise<MemoryPersistenceHandle>;
|
|
40
48
|
/**
|
|
41
49
|
* Insert a new observation row, materializing `id`, `createdAt`, and
|
|
42
50
|
* `lastUsedAt = createdAt`. Returns the populated `Observation` so
|
|
43
51
|
* callers can refer to the freshly-assigned id (e.g. to keep
|
|
44
|
-
* observation-group range markers consistent).
|
|
52
|
+
* observation-group range markers consistent). Returns `undefined`
|
|
53
|
+
* when the cross-process lock could not be acquired within the
|
|
54
|
+
* session's wait budget — writes are silently dropped in that case so
|
|
55
|
+
* a concurrent duet CLI does not crash the foreground turn.
|
|
45
56
|
*/
|
|
46
|
-
export declare function appendObservation(
|
|
57
|
+
export declare function appendObservation(session: MemorySession, input: Omit<Observation, "id" | "createdAt" | "lastUsedAt">): Promise<Observation | undefined>;
|
|
47
58
|
/**
|
|
48
59
|
* Replace this session's rows with the given list. Used by reflection,
|
|
49
60
|
* which condenses the session's observation log into one reflection
|
|
50
61
|
* row. Scoping the delete to `session_id = $1` is what makes the
|
|
51
62
|
* cross-session global pool durable: another session's rows are never
|
|
52
|
-
* touched by the current session's reflection.
|
|
63
|
+
* touched by the current session's reflection. No-op when the
|
|
64
|
+
* cross-process lock could not be acquired.
|
|
53
65
|
*/
|
|
54
|
-
export declare function replaceSessionObservations(
|
|
66
|
+
export declare function replaceSessionObservations(session: MemorySession, sessionId: string, observations: readonly Observation[]): Promise<void>;
|
|
55
67
|
/**
|
|
56
68
|
* Read this session's observations in chronological order along with a
|
|
57
69
|
* rough token count. Used by the observer (to gate reflection on
|
|
58
70
|
* cumulative session-local observation tokens) and by
|
|
59
71
|
* `getUnobservedMessageTail` to find the highest message id already
|
|
60
|
-
* folded into an observation-group range.
|
|
72
|
+
* folded into an observation-group range. Returns an empty snapshot
|
|
73
|
+
* when the cross-process lock could not be acquired.
|
|
61
74
|
*/
|
|
62
75
|
export interface SessionObservationsSnapshot {
|
|
63
76
|
observations: Observation[];
|
|
64
77
|
estimatedObservationTokens: number;
|
|
65
78
|
}
|
|
66
|
-
export declare function readSessionObservations(
|
|
79
|
+
export declare function readSessionObservations(session: MemorySession, sessionId: string): Promise<SessionObservationsSnapshot>;
|
|
67
80
|
/**
|
|
68
81
|
* Bump `last_used_at` for the given ids to `now`. Fire-and-forget
|
|
69
82
|
* usage signal: if the model's response leaned on a particular
|
|
70
83
|
* memory, that memory's ranking refreshes so it stays surfaced. No-op
|
|
71
|
-
* when `ids` is empty
|
|
84
|
+
* when `ids` is empty or when the cross-process lock could not be
|
|
85
|
+
* acquired.
|
|
72
86
|
*/
|
|
73
|
-
export declare function bumpLastUsed(
|
|
87
|
+
export declare function bumpLastUsed(session: MemorySession, ids: readonly string[], now: number): Promise<void>;
|
|
74
88
|
//# sourceMappingURL=storage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../../src/memory/storage.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../../src/memory/storage.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,2BAA2B,EAAE,MAAM,oBAAoB,CAAC;AACnF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAI9C,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAGrD;;;;;;GAMG;AACH,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,aAAa,GAAG,SAAS,CAAC;IACnC,KAAK,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,uBAAuB;IACtC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iGAAiG;IACjG,WAAW,CAAC,EAAE;QACZ,KAAK,EAAE,kBAAkB,CAAC;QAC1B,QAAQ,EAAE,2BAA2B,CAAC;QACtC,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IACF;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;IACnE;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AAED,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS,EACtC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,uBAAuB,CAAC,CAwElC;AAMD;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,aAAa,EACtB,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,GAAG,WAAW,GAAG,YAAY,CAAC,GAC1D,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAYlC;AAED;;;;;;;GAOG;AACH,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,aAAa,EACtB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,SAAS,WAAW,EAAE,GACnC,OAAO,CAAC,IAAI,CAAC,CAiBf;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,2BAA2B;IAC1C,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,0BAA0B,EAAE,MAAM,CAAC;CACpC;AAED,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,aAAa,EACtB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,2BAA2B,CAAC,CAmBtC;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,aAAa,EACtB,GAAG,EAAE,SAAS,MAAM,EAAE,EACtB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,CAQf"}
|
|
@@ -3,14 +3,28 @@ import { join } from "node:path";
|
|
|
3
3
|
import { EmbeddingBackfillWorker } from "./embedding-worker.js";
|
|
4
4
|
import { rebuildMemoryContextPack } from "./context-pack.js";
|
|
5
5
|
import { runMigrations } from "./migrations.js";
|
|
6
|
-
import {
|
|
6
|
+
import { MemorySession } from "./session.js";
|
|
7
7
|
import { nanoid } from "nanoid";
|
|
8
8
|
export async function loadStoredMemory(memoryPath, cwd, options = {}) {
|
|
9
9
|
if (!memoryPath) {
|
|
10
10
|
const noop = async () => { };
|
|
11
|
-
return {
|
|
11
|
+
return { session: undefined, embed: undefined, dispose: noop };
|
|
12
12
|
}
|
|
13
|
-
const
|
|
13
|
+
const resolvedPath = resolveMemoryPath(memoryPath, cwd);
|
|
14
|
+
const session = new MemorySession({
|
|
15
|
+
path: resolvedPath,
|
|
16
|
+
openOptions: {
|
|
17
|
+
// Migrations double as the corruption probe: a failure inside
|
|
18
|
+
// runMigrations propagates through openPGlite's try block, which
|
|
19
|
+
// moves the directory aside and starts fresh rather than wedging
|
|
20
|
+
// the agent behind an opaque PGlite abort.
|
|
21
|
+
init: async (db) => {
|
|
22
|
+
await runMigrations(db);
|
|
23
|
+
},
|
|
24
|
+
...(options.onRecover ? { onRecover: options.onRecover } : {}),
|
|
25
|
+
},
|
|
26
|
+
...(options.onWarn ? { onWarn: options.onWarn } : {}),
|
|
27
|
+
});
|
|
14
28
|
// The backfill worker runs whenever the CLI is up. Observers and
|
|
15
29
|
// reflectors write rows during turns; embeddings catch up in the
|
|
16
30
|
// background within a few batches, never blocking the foreground.
|
|
@@ -18,20 +32,29 @@ export async function loadStoredMemory(memoryPath, cwd, options = {}) {
|
|
|
18
32
|
// and one-shot tools that do not call recall_memory.
|
|
19
33
|
const worker = options.embed
|
|
20
34
|
? new EmbeddingBackfillWorker({
|
|
21
|
-
|
|
35
|
+
session,
|
|
22
36
|
embed: options.embed,
|
|
23
37
|
logPath: options.embeddingLogPath ?? defaultEmbeddingLogPath(),
|
|
24
38
|
})
|
|
25
39
|
: undefined;
|
|
26
40
|
worker?.start();
|
|
41
|
+
// Eager probe: open + run migrations once at startup. This keeps
|
|
42
|
+
// the "fail fast on corruption" property of the old eager loader
|
|
43
|
+
// (an unreadable dataDir is quarantined and onRecover is called
|
|
44
|
+
// before any turn dispatches) without changing the steady-state
|
|
45
|
+
// lazy-open behavior. The handle idle-closes within 2s afterwards
|
|
46
|
+
// unless the contextPack rebuild below grabs it first.
|
|
47
|
+
await session.withDb(async () => { });
|
|
27
48
|
if (options.contextPack) {
|
|
28
49
|
// Initial compaction trigger: freeze the rendered memory pack
|
|
29
50
|
// before the first turn dispatches so the prefix is stable from
|
|
30
|
-
// turn 1, not turn 2.
|
|
31
|
-
//
|
|
51
|
+
// turn 1, not turn 2. Migrations ran during the probe above; the
|
|
52
|
+
// 2s idle-close window means the rebuild typically rides the
|
|
53
|
+
// same open. Failure here is non-fatal: the pack stays empty
|
|
54
|
+
// and the runner can trigger a refresh later.
|
|
32
55
|
try {
|
|
33
56
|
await rebuildMemoryContextPack({
|
|
34
|
-
|
|
57
|
+
session,
|
|
35
58
|
cache: options.contextPack.cache,
|
|
36
59
|
settings: options.contextPack.settings,
|
|
37
60
|
...(options.contextPack.sessionId !== undefined
|
|
@@ -47,87 +70,90 @@ export async function loadStoredMemory(memoryPath, cwd, options = {}) {
|
|
|
47
70
|
}
|
|
48
71
|
const dispose = async () => {
|
|
49
72
|
await worker?.stop();
|
|
50
|
-
await
|
|
73
|
+
await session.dispose();
|
|
51
74
|
};
|
|
52
|
-
return {
|
|
75
|
+
return { session, embed: options.embed, dispose };
|
|
53
76
|
}
|
|
54
77
|
function defaultEmbeddingLogPath() {
|
|
55
78
|
return join(homedir(), ".duet", "logs", "memory-backfill.log");
|
|
56
79
|
}
|
|
57
|
-
async function openMemoryDatabase(path, onRecover) {
|
|
58
|
-
// Migrations double as the corruption probe: a failure inside
|
|
59
|
-
// runMigrations propagates through openPGlite's try block, which
|
|
60
|
-
// moves the directory aside and starts fresh rather than wedging
|
|
61
|
-
// the agent behind an opaque PGlite abort.
|
|
62
|
-
return openPGlite(path, {
|
|
63
|
-
init: async (db) => {
|
|
64
|
-
await runMigrations(db);
|
|
65
|
-
},
|
|
66
|
-
...(onRecover ? { onRecover } : {}),
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
80
|
/**
|
|
70
81
|
* Insert a new observation row, materializing `id`, `createdAt`, and
|
|
71
82
|
* `lastUsedAt = createdAt`. Returns the populated `Observation` so
|
|
72
83
|
* callers can refer to the freshly-assigned id (e.g. to keep
|
|
73
|
-
* observation-group range markers consistent).
|
|
84
|
+
* observation-group range markers consistent). Returns `undefined`
|
|
85
|
+
* when the cross-process lock could not be acquired within the
|
|
86
|
+
* session's wait budget — writes are silently dropped in that case so
|
|
87
|
+
* a concurrent duet CLI does not crash the foreground turn.
|
|
74
88
|
*/
|
|
75
|
-
export async function appendObservation(
|
|
89
|
+
export async function appendObservation(session, input) {
|
|
76
90
|
const observation = {
|
|
77
91
|
...input,
|
|
78
92
|
id: createMemoryId(),
|
|
79
93
|
createdAt: Date.now(),
|
|
80
94
|
lastUsedAt: Date.now(),
|
|
81
95
|
};
|
|
82
|
-
await
|
|
83
|
-
|
|
96
|
+
const result = await session.withDb(async (db) => {
|
|
97
|
+
await upsertObservation(db, observation);
|
|
98
|
+
return observation;
|
|
99
|
+
});
|
|
100
|
+
return result;
|
|
84
101
|
}
|
|
85
102
|
/**
|
|
86
103
|
* Replace this session's rows with the given list. Used by reflection,
|
|
87
104
|
* which condenses the session's observation log into one reflection
|
|
88
105
|
* row. Scoping the delete to `session_id = $1` is what makes the
|
|
89
106
|
* cross-session global pool durable: another session's rows are never
|
|
90
|
-
* touched by the current session's reflection.
|
|
107
|
+
* touched by the current session's reflection. No-op when the
|
|
108
|
+
* cross-process lock could not be acquired.
|
|
91
109
|
*/
|
|
92
|
-
export async function replaceSessionObservations(
|
|
110
|
+
export async function replaceSessionObservations(session, sessionId, observations) {
|
|
93
111
|
const ids = observations.map((observation) => observation.id);
|
|
94
|
-
await
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
112
|
+
await session.withDb(async (db) => {
|
|
113
|
+
await db.transaction(async (tx) => {
|
|
114
|
+
if (ids.length === 0) {
|
|
115
|
+
await tx.query("DELETE FROM observations WHERE session_id = $1", [sessionId]);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
await tx.query("DELETE FROM observations WHERE session_id = $1 AND NOT (id = ANY($2::text[]))", [sessionId, ids]);
|
|
119
|
+
}
|
|
120
|
+
for (const observation of observations) {
|
|
121
|
+
await upsertObservation(tx, observation);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
104
124
|
});
|
|
105
125
|
}
|
|
106
|
-
export async function readSessionObservations(
|
|
107
|
-
const result = await
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
126
|
+
export async function readSessionObservations(session, sessionId) {
|
|
127
|
+
const result = await session.withDb(async (db) => {
|
|
128
|
+
const queryResult = await db.query(`SELECT id, created_at, last_used_at, session_id, kind, observed_date, referenced_date,
|
|
129
|
+
relative_date, time_of_day, priority, source_json, content, tags_json
|
|
130
|
+
FROM observations
|
|
131
|
+
WHERE session_id = $1
|
|
132
|
+
ORDER BY created_at ASC`, [sessionId]);
|
|
133
|
+
const observations = queryResult.rows.map(rowToObservation);
|
|
134
|
+
return {
|
|
135
|
+
observations,
|
|
136
|
+
estimatedObservationTokens: estimateTokens(observations.map((observation) => observation.content).join("\n")),
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
return result ?? { observations: [], estimatedObservationTokens: 0 };
|
|
117
140
|
}
|
|
118
141
|
/**
|
|
119
142
|
* Bump `last_used_at` for the given ids to `now`. Fire-and-forget
|
|
120
143
|
* usage signal: if the model's response leaned on a particular
|
|
121
144
|
* memory, that memory's ranking refreshes so it stays surfaced. No-op
|
|
122
|
-
* when `ids` is empty
|
|
145
|
+
* when `ids` is empty or when the cross-process lock could not be
|
|
146
|
+
* acquired.
|
|
123
147
|
*/
|
|
124
|
-
export async function bumpLastUsed(
|
|
148
|
+
export async function bumpLastUsed(session, ids, now) {
|
|
125
149
|
if (ids.length === 0)
|
|
126
150
|
return;
|
|
127
|
-
await
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
151
|
+
await session.withDb(async (db) => {
|
|
152
|
+
await db.query("UPDATE observations SET last_used_at = $1 WHERE id = ANY($2::text[])", [
|
|
153
|
+
now,
|
|
154
|
+
ids,
|
|
155
|
+
]);
|
|
156
|
+
});
|
|
131
157
|
}
|
|
132
158
|
async function upsertObservation(database, observation) {
|
|
133
159
|
await database.query(`INSERT INTO observations (
|