@duetso/agent 0.1.99 → 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.
Files changed (37) hide show
  1. package/dist/package.json +1 -1
  2. package/dist/src/memory/context-pack.d.ts +2 -2
  3. package/dist/src/memory/context-pack.d.ts.map +1 -1
  4. package/dist/src/memory/context-pack.js +21 -16
  5. package/dist/src/memory/context-pack.js.map +1 -1
  6. package/dist/src/memory/embedding-worker.d.ts +8 -3
  7. package/dist/src/memory/embedding-worker.d.ts.map +1 -1
  8. package/dist/src/memory/embedding-worker.js +42 -28
  9. package/dist/src/memory/embedding-worker.js.map +1 -1
  10. package/dist/src/memory/observational.d.ts +8 -3
  11. package/dist/src/memory/observational.d.ts.map +1 -1
  12. package/dist/src/memory/observational.js +10 -10
  13. package/dist/src/memory/observational.js.map +1 -1
  14. package/dist/src/memory/pglite.d.ts +57 -17
  15. package/dist/src/memory/pglite.d.ts.map +1 -1
  16. package/dist/src/memory/pglite.js +153 -140
  17. package/dist/src/memory/pglite.js.map +1 -1
  18. package/dist/src/memory/recall.d.ts +9 -2
  19. package/dist/src/memory/recall.d.ts.map +1 -1
  20. package/dist/src/memory/recall.js +23 -20
  21. package/dist/src/memory/recall.js.map +1 -1
  22. package/dist/src/memory/session.d.ts +78 -0
  23. package/dist/src/memory/session.d.ts.map +1 -0
  24. package/dist/src/memory/session.js +198 -0
  25. package/dist/src/memory/session.js.map +1 -0
  26. package/dist/src/memory/storage.d.ts +29 -15
  27. package/dist/src/memory/storage.d.ts.map +1 -1
  28. package/dist/src/memory/storage.js +80 -54
  29. package/dist/src/memory/storage.js.map +1 -1
  30. package/dist/src/turn-runner/tools.d.ts +8 -6
  31. package/dist/src/turn-runner/tools.d.ts.map +1 -1
  32. package/dist/src/turn-runner/tools.js +3 -3
  33. package/dist/src/turn-runner/tools.js.map +1 -1
  34. package/dist/src/turn-runner/turn-runner.d.ts.map +1 -1
  35. package/dist/src/turn-runner/turn-runner.js +14 -6
  36. package/dist/src/turn-runner/turn-runner.js.map +1 -1
  37. package/package.json +1 -1
@@ -18,6 +18,23 @@ export interface OpenPGliteOptions {
18
18
  cause: unknown;
19
19
  }) => void;
20
20
  }
21
+ /**
22
+ * Thrown when the cross-process open-lock could not be acquired within
23
+ * the caller's wait budget. `MemorySession` catches this and degrades
24
+ * gracefully (skips the op, warns once) so a second duet CLI does not
25
+ * crash when a long-running first CLI holds the memory db.
26
+ */
27
+ export declare class MemoryLockTimeoutError extends Error {
28
+ readonly dataDir: string;
29
+ readonly holderPid: number;
30
+ readonly budgetMs: number;
31
+ constructor(dataDir: string, holderPid: number, budgetMs: number);
32
+ }
33
+ export type TryAcquireOpenLockResult = {
34
+ lockPath: string;
35
+ } | {
36
+ holderPid: number;
37
+ };
21
38
  /**
22
39
  * Opens a PGlite database at `path`, recovering from three failure modes that
23
40
  * would otherwise leave the agent unable to start:
@@ -29,24 +46,33 @@ export interface OpenPGliteOptions {
29
46
  * by running the init hook as a probe. The bad directory is renamed
30
47
  * to `<path>.corrupted-<iso-timestamp>` so the user can recover it
31
48
  * later, and a fresh database is opened in its place.
32
- * 3. Two opens racing on a fresh dataDir historically both could pass
33
- * the stale-lock check, both call `PGlite.create`, and both run the
34
- * init hook concurrently, corrupting the dataDir. Now serialized by
35
- * an in-process refcounted handle plus an `O_EXCL` lock file scoped
36
- * to the dataDir.
49
+ * 3. A cross-process open-lock so two duet CLIs cannot both call
50
+ * `PGlite.create` on the same fresh dataDir and corrupt each
51
+ * other's migrations.
37
52
  *
38
- * Without (2) PGlite aborts with an opaque emscripten "Aborted()" error and
39
- * the agent is permanently wedged until the user manually deletes the data
40
- * directory.
53
+ * Throws synchronously-via-reject if the lock is held by a live foreign
54
+ * process. Callers that need to wait or degrade gracefully should use
55
+ * `MemorySession` instead, which polls `tryAcquireOpenLock`.
41
56
  */
42
57
  export declare function openPGlite(path: string, options?: OpenPGliteOptions): Promise<PGlite>;
58
+ /**
59
+ * Open the database assuming the caller already holds `lockPath` for
60
+ * the dataDir. On a quarantine recovery the directory is renamed
61
+ * aside, the old lockPath disappears with it, and this returns the
62
+ * freshly-reacquired lockPath. `MemorySession` uses this so its
63
+ * poll-acquire loop owns the lock lifecycle end-to-end.
64
+ */
65
+ export declare function openPGliteHoldingLock(path: string, options: OpenPGliteOptions, lockPath: string): Promise<{
66
+ db: PGlite;
67
+ lockPath: string;
68
+ }>;
43
69
  /**
44
70
  * Open a managed PGlite handle without running migrations or quarantining
45
71
  * on read failure. Used by the recovery tooling to read rows out of a
46
72
  * previously-quarantined `memory.db.corrupted-*` directory.
47
73
  *
48
- * Does not participate in the shared-handle / open-lock machinery; the
49
- * caller owns the returned instance and is responsible for `close()`.
74
+ * Does not participate in the open-lock machinery; the caller owns the
75
+ * returned instance and is responsible for `close()`.
50
76
  */
51
77
  export declare function openForRecovery(path: string): Promise<PGlite>;
52
78
  /**
@@ -55,19 +81,33 @@ export declare function openForRecovery(path: string): Promise<PGlite>;
55
81
  */
56
82
  export declare function quarantineDataDirectory(path: string): string;
57
83
  /**
58
- * Acquire an exclusive open-lock at `<dataDir>/.duet-open.lock`. Creates
59
- * `dataDir` if missing. The lock is created with `O_EXCL` so a parallel
60
- * acquirer either wins outright or sees `EEXIST`; on `EEXIST` we read
61
- * the pid line and only steal the lock if that pid is no longer alive.
62
- *
63
- * Throws with a clear message when the lock is held by a live foreign
64
- * process so the caller fails fast instead of double-opening the dataDir.
84
+ * Try once to acquire the cross-process open-lock at
85
+ * `<dataDir>/.duet-open.lock`. Returns `{ lockPath }` on success
86
+ * (including after a stale-pid takeover) or `{ holderPid }` when a live
87
+ * foreign process still holds the lock. Never throws on contention so
88
+ * callers can poll and retry without try/catch.
89
+ */
90
+ export declare function tryAcquireOpenLock(dataDir: string): TryAcquireOpenLockResult;
91
+ /**
92
+ * Acquire the cross-process open-lock, throwing if a live foreign
93
+ * process holds it. Used by `openPGlite` for the fast/strict path;
94
+ * polling callers should use `tryAcquireOpenLock` instead.
65
95
  */
66
96
  export declare function acquireOpenLock(dataDir: string): string;
97
+ /**
98
+ * Release a lock previously returned by `tryAcquireOpenLock` /
99
+ * `acquireOpenLock`. Idempotent and safe to call if the file has
100
+ * already disappeared.
101
+ */
102
+ export declare function releaseOpenLock(lockPath: string): void;
67
103
  /**
68
104
  * If `dataDir` contains a `postmaster.pid` whose PID is not a live process,
69
105
  * remove the lock so PGlite can start. If the PID is alive, throw with a clear
70
106
  * message rather than letting PGlite abort opaquely. Exported for testing.
71
107
  */
72
108
  export declare function clearStalePostmasterLock(dataDir: string): void;
109
+ export declare const __testing: {
110
+ heldLockPaths: Set<string>;
111
+ resolvePath: (...paths: string[]) => string;
112
+ };
73
113
  //# sourceMappingURL=pglite.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pglite.d.ts","sourceRoot":"","sources":["../../../src/memory/pglite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AA+B9C,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;CACpE;AA8CD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CA8B/F;AAuHD;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGnE;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK5D;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAIvD;AAiDD;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAkB9D"}
1
+ {"version":3,"file":"pglite.d.ts","sourceRoot":"","sources":["../../../src/memory/pglite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AA+B9C,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;CACpE;AAED;;;;;GAKG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBACd,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;CAUjE;AAED,MAAM,MAAM,wBAAwB,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAyBpF;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CAU/F;AAED;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,iBAAiB,EAC1B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA8B3C;AAiDD;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGnE;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK5D;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,wBAAwB,CA8C5E;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAOvD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAOtD;AAkBD;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAkB9D;AA+BD,eAAO,MAAM,SAAS;;;CAGrB,CAAC"}
@@ -17,24 +17,44 @@ const MEMORY_EXTENSIONS = { vector };
17
17
  * processes only proceed past O_EXCL if that pid is no longer alive.
18
18
  */
19
19
  const OPEN_LOCK_FILE = ".duet-open.lock";
20
- const sharedHandles = new Map();
20
+ /**
21
+ * Thrown when the cross-process open-lock could not be acquired within
22
+ * the caller's wait budget. `MemorySession` catches this and degrades
23
+ * gracefully (skips the op, warns once) so a second duet CLI does not
24
+ * crash when a long-running first CLI holds the memory db.
25
+ */
26
+ export class MemoryLockTimeoutError extends Error {
27
+ dataDir;
28
+ holderPid;
29
+ budgetMs;
30
+ constructor(dataDir, holderPid, budgetMs) {
31
+ super(`Timed out after ${budgetMs}ms waiting for the memory db open-lock at ${dataDir} ` +
32
+ `(held by pid ${holderPid}).`);
33
+ this.name = "MemoryLockTimeoutError";
34
+ this.dataDir = dataDir;
35
+ this.holderPid = holderPid;
36
+ this.budgetMs = budgetMs;
37
+ }
38
+ }
39
+ /**
40
+ * Process-global registry of open-lock files currently held by this
41
+ * process. Used by the exit handler to drop locks on crash so the next
42
+ * launch (and concurrent duet CLIs) are not blocked behind a stale
43
+ * lock pointing at a no-longer-living pid.
44
+ */
45
+ const heldLockPaths = new Set();
21
46
  let exitHandlerInstalled = false;
22
47
  function installExitCleanup() {
23
48
  if (exitHandlerInstalled)
24
49
  return;
25
50
  exitHandlerInstalled = true;
26
- // Last-chance cleanup: if the process exits before every caller's
27
- // `close()` runs, drop any locks we still hold so the next launch
28
- // can open the database without tripping the stale-lock check.
29
51
  process.on("exit", () => {
30
- for (const entry of sharedHandles.values()) {
31
- if (entry.lockPath) {
32
- try {
33
- unlinkSync(entry.lockPath);
34
- }
35
- catch {
36
- // Best-effort on exit; nothing to do if the file is already gone.
37
- }
52
+ for (const lockPath of heldLockPaths) {
53
+ try {
54
+ unlinkSync(lockPath);
55
+ }
56
+ catch {
57
+ // Best-effort on exit; nothing to do if the file is already gone.
38
58
  }
39
59
  }
40
60
  });
@@ -50,118 +70,47 @@ function installExitCleanup() {
50
70
  * by running the init hook as a probe. The bad directory is renamed
51
71
  * to `<path>.corrupted-<iso-timestamp>` so the user can recover it
52
72
  * later, and a fresh database is opened in its place.
53
- * 3. Two opens racing on a fresh dataDir historically both could pass
54
- * the stale-lock check, both call `PGlite.create`, and both run the
55
- * init hook concurrently, corrupting the dataDir. Now serialized by
56
- * an in-process refcounted handle plus an `O_EXCL` lock file scoped
57
- * to the dataDir.
73
+ * 3. A cross-process open-lock so two duet CLIs cannot both call
74
+ * `PGlite.create` on the same fresh dataDir and corrupt each
75
+ * other's migrations.
58
76
  *
59
- * Without (2) PGlite aborts with an opaque emscripten "Aborted()" error and
60
- * the agent is permanently wedged until the user manually deletes the data
61
- * directory.
77
+ * Throws synchronously-via-reject if the lock is held by a live foreign
78
+ * process. Callers that need to wait or degrade gracefully should use
79
+ * `MemorySession` instead, which polls `tryAcquireOpenLock`.
62
80
  */
63
81
  export async function openPGlite(path, options = {}) {
64
82
  installExitCleanup();
65
- const key = resolvePath(path);
66
- const existing = sharedHandles.get(key);
67
- if (existing) {
68
- existing.refs++;
69
- try {
70
- return await existing.promise;
71
- }
72
- catch (error) {
73
- decrementAndMaybeDelete(key);
74
- throw error;
75
- }
76
- }
77
- const entry = {
78
- promise: undefined,
79
- refs: 1,
80
- };
81
- entry.promise = openWithLock(path, options, entry).then((db) => installRefcountedClose(db, key, entry));
82
- sharedHandles.set(key, entry);
83
+ const lockPath = acquireOpenLock(path);
83
84
  try {
84
- return await entry.promise;
85
+ const opened = await openPGliteHoldingLock(path, options, lockPath);
86
+ return installLockReleasingClose(opened.db, opened.lockPath);
85
87
  }
86
88
  catch (error) {
87
- decrementAndMaybeDelete(key);
89
+ releaseOpenLock(lockPath);
88
90
  throw error;
89
91
  }
90
92
  }
91
93
  /**
92
- * Replace `close` on the underlying PGlite instance with refcounted
93
- * teardown. Each `openPGlite` call is paired with exactly one `close`;
94
- * the refcount equals the number of live opens. PGlite uses private
95
- * class fields internally, so a Proxy around the instance breaks every
96
- * method that touches one method replacement preserves identity and
97
- * keeps private-field access working.
94
+ * Open the database assuming the caller already holds `lockPath` for
95
+ * the dataDir. On a quarantine recovery the directory is renamed
96
+ * aside, the old lockPath disappears with it, and this returns the
97
+ * freshly-reacquired lockPath. `MemorySession` uses this so its
98
+ * poll-acquire loop owns the lock lifecycle end-to-end.
98
99
  */
99
- function installRefcountedClose(db, key, entry) {
100
- const originalClose = db.close.bind(db);
101
- db.close = async () => {
102
- const current = sharedHandles.get(key);
103
- if (!current || current.refs <= 0)
104
- return;
105
- current.refs--;
106
- if (current.refs > 0)
107
- return;
108
- sharedHandles.delete(key);
109
- try {
110
- await originalClose();
111
- }
112
- finally {
113
- if (entry.lockPath) {
114
- bestEffortUnlink(entry.lockPath);
115
- entry.lockPath = undefined;
116
- }
117
- }
118
- };
119
- return db;
120
- }
121
- function bestEffortUnlink(path) {
122
- try {
123
- unlinkSync(path);
124
- }
125
- catch (error) {
126
- if (error.code !== "ENOENT")
127
- throw error;
128
- }
129
- }
130
- function decrementAndMaybeDelete(key) {
131
- const entry = sharedHandles.get(key);
132
- if (!entry)
133
- return;
134
- entry.refs--;
135
- if (entry.refs > 0)
136
- return;
137
- sharedHandles.delete(key);
138
- if (entry.lockPath) {
139
- try {
140
- unlinkSync(entry.lockPath);
141
- }
142
- catch {
143
- // Lock may never have been created if we threw before acquireOpenLock returned.
144
- }
145
- }
146
- }
147
- async function openWithLock(path, options, entry) {
100
+ export async function openPGliteHoldingLock(path, options, lockPath) {
101
+ installExitCleanup();
148
102
  mkdirSync(dirname(path), { recursive: true });
149
103
  clearStalePostmasterLock(path);
150
- entry.lockPath = acquireOpenLock(path);
151
104
  try {
152
105
  const db = await openAndProbe(path, options.init);
153
- entry.db = db;
154
- return db;
106
+ return { db, lockPath };
155
107
  }
156
108
  catch (error) {
157
109
  if (!isExistingDirectory(path))
158
110
  throw error;
159
111
  // Release the lock before renaming the directory aside — the lock
160
112
  // file lives inside that directory and would be moved with it.
161
- if (entry.lockPath) {
162
- bestEffortUnlink(entry.lockPath);
163
- entry.lockPath = undefined;
164
- }
113
+ releaseOpenLock(lockPath);
165
114
  const backupPath = quarantineDataDirectory(path);
166
115
  if (options.onRecover) {
167
116
  options.onRecover({ backupPath, cause: error });
@@ -171,12 +120,34 @@ async function openWithLock(path, options, entry) {
171
120
  console.warn(`[duet] memory database at ${path} could not be opened (${reason}). ` +
172
121
  `Moved aside to ${backupPath} and starting fresh.`);
173
122
  }
174
- entry.lockPath = acquireOpenLock(path);
123
+ const fresh = acquireOpenLock(path);
175
124
  const db = await openAndProbe(path, options.init);
176
- entry.db = db;
177
- return db;
125
+ return { db, lockPath: fresh };
178
126
  }
179
127
  }
128
+ /**
129
+ * Wrap `db.close` so the cross-process lock is released alongside the
130
+ * handle. PGlite uses private class fields internally, so a Proxy
131
+ * around the instance breaks every method that touches one — method
132
+ * replacement preserves identity and keeps private-field access
133
+ * working.
134
+ */
135
+ function installLockReleasingClose(db, lockPath) {
136
+ const originalClose = db.close.bind(db);
137
+ let released = false;
138
+ db.close = async () => {
139
+ if (released)
140
+ return;
141
+ released = true;
142
+ try {
143
+ await originalClose();
144
+ }
145
+ finally {
146
+ releaseOpenLock(lockPath);
147
+ }
148
+ };
149
+ return db;
150
+ }
180
151
  async function openAndProbe(path, init) {
181
152
  // PGlite.create is the only API path that lets us register loadable
182
153
  // extensions at construction time — the bare `new PGlite(path)` form
@@ -206,8 +177,8 @@ async function openAndProbe(path, init) {
206
177
  * on read failure. Used by the recovery tooling to read rows out of a
207
178
  * previously-quarantined `memory.db.corrupted-*` directory.
208
179
  *
209
- * Does not participate in the shared-handle / open-lock machinery; the
210
- * caller owns the returned instance and is responsible for `close()`.
180
+ * Does not participate in the open-lock machinery; the caller owns the
181
+ * returned instance and is responsible for `close()`.
211
182
  */
212
183
  export async function openForRecovery(path) {
213
184
  clearStalePostmasterLock(path);
@@ -224,38 +195,22 @@ export function quarantineDataDirectory(path) {
224
195
  return backup;
225
196
  }
226
197
  /**
227
- * Acquire an exclusive open-lock at `<dataDir>/.duet-open.lock`. Creates
228
- * `dataDir` if missing. The lock is created with `O_EXCL` so a parallel
229
- * acquirer either wins outright or sees `EEXIST`; on `EEXIST` we read
230
- * the pid line and only steal the lock if that pid is no longer alive.
231
- *
232
- * Throws with a clear message when the lock is held by a live foreign
233
- * process so the caller fails fast instead of double-opening the dataDir.
198
+ * Try once to acquire the cross-process open-lock at
199
+ * `<dataDir>/.duet-open.lock`. Returns `{ lockPath }` on success
200
+ * (including after a stale-pid takeover) or `{ holderPid }` when a live
201
+ * foreign process still holds the lock. Never throws on contention so
202
+ * callers can poll and retry without try/catch.
234
203
  */
235
- export function acquireOpenLock(dataDir) {
204
+ export function tryAcquireOpenLock(dataDir) {
236
205
  mkdirSync(dataDir, { recursive: true });
237
206
  const lockPath = join(dataDir, OPEN_LOCK_FILE);
238
- return createLockFile(lockPath) ?? stealStaleLock(dataDir, lockPath);
239
- }
240
- function createLockFile(lockPath) {
241
- let fd;
242
- try {
243
- fd = openSync(lockPath, "wx");
244
- }
245
- catch (error) {
246
- if (error.code === "EEXIST")
247
- return undefined;
248
- throw error;
249
- }
250
- try {
251
- writeSync(fd, `${process.pid}\n`);
252
- }
253
- finally {
254
- closeSync(fd);
207
+ const created = createLockFile(lockPath);
208
+ if (created) {
209
+ heldLockPaths.add(created);
210
+ return { lockPath: created };
255
211
  }
256
- return lockPath;
257
- }
258
- function stealStaleLock(dataDir, lockPath) {
212
+ // Lock file existed. If a live foreign process owns it, report back
213
+ // without taking it; otherwise treat it as stale and steal it.
259
214
  const contents = tryReadFile(lockPath) ?? "";
260
215
  const firstLine = contents.split("\n", 1)[0]?.trim() ?? "";
261
216
  const holderPid = Number.parseInt(firstLine, 10);
@@ -263,8 +218,7 @@ function stealStaleLock(dataDir, lockPath) {
263
218
  holderPid > 0 &&
264
219
  holderPid !== process.pid &&
265
220
  isProcessAlive(holderPid)) {
266
- throw new Error(`PGlite data directory ${dataDir} is locked by another duet process (pid ${holderPid}). ` +
267
- "Stop that process before opening the database.");
221
+ return { holderPid };
268
222
  }
269
223
  // Stale (crashed prior run) or our own pid from a partially-cleaned
270
224
  // failed open. Replace atomically — unlink-then-create-with-O_EXCL.
@@ -275,11 +229,64 @@ function stealStaleLock(dataDir, lockPath) {
275
229
  if (error.code !== "ENOENT")
276
230
  throw error;
277
231
  }
278
- const created = createLockFile(lockPath);
279
- if (!created) {
232
+ const recreated = createLockFile(lockPath);
233
+ if (!recreated) {
234
+ // Another opener won the race after our stale-lock takeover. Report
235
+ // its pid (if live) so the caller can poll again.
236
+ const afterContents = tryReadFile(lockPath) ?? "";
237
+ const after = Number.parseInt(afterContents.split("\n", 1)[0]?.trim() ?? "", 10);
238
+ if (Number.isFinite(after) && after > 0 && after !== process.pid && isProcessAlive(after)) {
239
+ return { holderPid: after };
240
+ }
280
241
  throw new Error(`Failed to acquire open lock at ${lockPath}: another opener won the race after the stale-lock takeover.`);
281
242
  }
282
- return created;
243
+ heldLockPaths.add(recreated);
244
+ return { lockPath: recreated };
245
+ }
246
+ /**
247
+ * Acquire the cross-process open-lock, throwing if a live foreign
248
+ * process holds it. Used by `openPGlite` for the fast/strict path;
249
+ * polling callers should use `tryAcquireOpenLock` instead.
250
+ */
251
+ export function acquireOpenLock(dataDir) {
252
+ const result = tryAcquireOpenLock(dataDir);
253
+ if ("lockPath" in result)
254
+ return result.lockPath;
255
+ throw new Error(`PGlite data directory ${dataDir} is locked by another duet process (pid ${result.holderPid}). ` +
256
+ "Stop that process before opening the database.");
257
+ }
258
+ /**
259
+ * Release a lock previously returned by `tryAcquireOpenLock` /
260
+ * `acquireOpenLock`. Idempotent and safe to call if the file has
261
+ * already disappeared.
262
+ */
263
+ export function releaseOpenLock(lockPath) {
264
+ heldLockPaths.delete(lockPath);
265
+ try {
266
+ unlinkSync(lockPath);
267
+ }
268
+ catch (error) {
269
+ if (error.code !== "ENOENT")
270
+ throw error;
271
+ }
272
+ }
273
+ function createLockFile(lockPath) {
274
+ let fd;
275
+ try {
276
+ fd = openSync(lockPath, "wx");
277
+ }
278
+ catch (error) {
279
+ if (error.code === "EEXIST")
280
+ return undefined;
281
+ throw error;
282
+ }
283
+ try {
284
+ writeSync(fd, `${process.pid}\n`);
285
+ }
286
+ finally {
287
+ closeSync(fd);
288
+ }
289
+ return lockPath;
283
290
  }
284
291
  /**
285
292
  * If `dataDir` contains a `postmaster.pid` whose PID is not a live process,
@@ -330,4 +337,10 @@ function isProcessAlive(pid) {
330
337
  return error.code === "EPERM";
331
338
  }
332
339
  }
340
+ // Internal: lets tests reach into the held-lock registry to assert
341
+ // exit cleanup behavior without exporting it on the public surface.
342
+ export const __testing = {
343
+ heldLockPaths,
344
+ resolvePath,
345
+ };
333
346
  //# sourceMappingURL=pglite.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"pglite.js","sourceRoot":"","sources":["../../../src/memory/pglite.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pglite.js","sourceRoot":"","sources":["../../../src/memory/pglite.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,16 @@
1
- import type { PGlite } from "@electric-sql/pglite";
2
1
  import type { Observation } from "../types/memory.js";
3
2
  import type { EmbedFn } from "./embedding.js";
3
+ import type { MemorySession } from "./session.js";
4
4
  export type RecallScope = "session" | "global" | "all";
5
5
  export interface RecallMemoryOptions {
6
- db: PGlite;
6
+ /**
7
+ * Memory session that owns the PGlite handle and cross-process lock.
8
+ * The recall call wraps every query in a single `withDb` so keyword,
9
+ * vector, and hydration all run against the same open. When the lock
10
+ * cannot be acquired, recall returns an empty result instead of
11
+ * throwing so the tool surface degrades gracefully.
12
+ */
13
+ session: MemorySession;
7
14
  /**
8
15
  * Embedding callable. When omitted (or when it throws) the vector
9
16
  * path is skipped silently and recall falls back to keyword-only.
@@ -1 +1 @@
1
- {"version":3,"file":"recall.d.ts","sourceRoot":"","sources":["../../../src/memory/recall.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,KAAK,EAAE,WAAW,EAAwC,MAAM,oBAAoB,CAAC;AAC5F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAkC9C,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,QAAQ,GAAG,KAAK,CAAC;AAEvD,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,wCAAwC;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oFAAoF;IACpF,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,0FAA0F;IAC1F,qBAAqB,EAAE,OAAO,CAAC;IAC/B,mGAAmG;IACnG,qBAAqB,EAAE,OAAO,CAAC;CAChC;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAuC5F;AAED,UAAU,SAAS;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAsGD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,SAAS,EAAE,EAAE,GAAG,MAAM,EAAE,CAqBzE"}
1
+ {"version":3,"file":"recall.d.ts","sourceRoot":"","sources":["../../../src/memory/recall.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAwC,MAAM,oBAAoB,CAAC;AAC5F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAkClD,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,QAAQ,GAAG,KAAK,CAAC;AAEvD,MAAM,WAAW,mBAAmB;IAClC;;;;;;OAMG;IACH,OAAO,EAAE,aAAa,CAAC;IACvB;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,wCAAwC;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oFAAoF;IACpF,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,0FAA0F;IAC1F,qBAAqB,EAAE,OAAO,CAAC;IAC/B,mGAAmG;IACnG,qBAAqB,EAAE,OAAO,CAAC;CAChC;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAoC5F;AAED,UAAU,SAAS;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAsGD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,SAAS,EAAE,EAAE,GAAG,MAAM,EAAE,CAqBzE"}
@@ -36,27 +36,30 @@ export async function recallMemory(options) {
36
36
  if (scope === "session" && !options.sessionId) {
37
37
  return { observations: [], vectorSearchAttempted: false, vectorSearchSucceeded: false };
38
38
  }
39
- const keywordHits = await keywordSearch(options.db, options.query, scope, options.sessionId);
40
- let vectorAttempted = false;
41
- let vectorHits = [];
42
- if (options.embed) {
43
- vectorAttempted = true;
44
- try {
45
- vectorHits = await vectorSearch(options.db, options.embed, options.query, scope, options.sessionId);
46
- }
47
- catch {
48
- // Embedding unavailable, network blip, or empty index — drop
49
- // back to keyword-only. Caller surfaces the degraded mode.
50
- vectorHits = [];
39
+ const result = await options.session.withDb(async (db) => {
40
+ const keywordHits = await keywordSearch(db, options.query, scope, options.sessionId);
41
+ let vectorAttempted = false;
42
+ let vectorHits = [];
43
+ if (options.embed) {
44
+ vectorAttempted = true;
45
+ try {
46
+ vectorHits = await vectorSearch(db, options.embed, options.query, scope, options.sessionId);
47
+ }
48
+ catch {
49
+ // Embedding unavailable, network blip, or empty index — drop
50
+ // back to keyword-only. Caller surfaces the degraded mode.
51
+ vectorHits = [];
52
+ }
51
53
  }
52
- }
53
- const fusedIds = reciprocalRankFusion([keywordHits, vectorHits]).slice(0, limit);
54
- const observations = await hydrate(options.db, fusedIds);
55
- return {
56
- observations,
57
- vectorSearchAttempted: vectorAttempted,
58
- vectorSearchSucceeded: vectorHits.length > 0,
59
- };
54
+ const fusedIds = reciprocalRankFusion([keywordHits, vectorHits]).slice(0, limit);
55
+ const observations = await hydrate(db, fusedIds);
56
+ return {
57
+ observations,
58
+ vectorSearchAttempted: vectorAttempted,
59
+ vectorSearchSucceeded: vectorHits.length > 0,
60
+ };
61
+ });
62
+ return result ?? { observations: [], vectorSearchAttempted: false, vectorSearchSucceeded: false };
60
63
  }
61
64
  async function keywordSearch(db, query, scope, sessionId) {
62
65
  const trimmed = query.trim();