pi-goosedump 0.12.55 → 0.12.56

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 (3) hide show
  1. package/README.md +21 -89
  2. package/dist/index.ts +164 -78
  3. package/package.json +14 -6
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # pi-goosedump
2
2
 
3
- Pi extension for goosedump-backed session search, durable memory, session
4
- management, and compaction. Tools use the `goose_` prefix.
3
+ Pi extension for session search, durable memory, session management, and
4
+ compaction. Tools use the `goose_` prefix.
5
5
 
6
6
  ## Install
7
7
 
@@ -9,66 +9,30 @@ management, and compaction. Tools use the `goose_` prefix.
9
9
  pi install npm:pi-goosedump
10
10
  ```
11
11
 
12
- Bundled native binaries: Linux x64 and arm64, macOS arm64, and Windows x64.
13
-
14
- Unsupported platforms are rejected when the extension loads. The extension uses
15
- that packaged binary by default and does not fall back to a `goosedump`
16
- executable on `PATH`. Set the shared `GOOSEDUMP_BINARY` environment variable to
17
- an absolute executable path to override it. An invalid override or missing
18
- packaged binary is treated as a broken installation. Local models download on
19
- first use.
12
+ The extension includes binaries for Linux x64/arm64, macOS arm64, and Windows
13
+ x64. Set `GOOSEDUMP_BINARY` to an absolute executable path to override the
14
+ packaged binary.
20
15
 
21
16
  ## Tools
22
17
 
23
- Prefer these tools whenever prior sessions or durable memory can answer a
24
- question. Expand ranked or grepped hits with `goose_get` before guessing.
25
-
26
- | Tool | When to use |
27
- | --------------------- | ---------------------------------------------------------- |
28
- | `goose_search` | Ranked session-history search; returns entry IDs |
29
- | `goose_grep` | Exact glob/substring filter over session messages |
30
- | `goose_get` | Expand entry IDs from search/grep to full content |
31
- | `goose_remember` | Queue background learning from a session after a milestone |
32
- | `goose_recall` | Recall project-scoped durable memory with provenance |
33
- | `goose_forget` | Remove a stale memory or a session's retained sources |
34
- | `goose_memory_status` | Inspect storage health, type counts, and tombstones |
35
- | `goose_compact` | Compact the current Pi session asynchronously |
36
- | `goose_sessions` | List or delete sessions in the current working directory |
37
-
38
- Defaults:
39
-
40
- | Setting | Default |
41
- | ---------------- | ------------------------------ |
42
- | Session target | Current Pi session |
43
- | History scope | `lineage` (active branch) |
44
- | Recall limit | 5 |
45
- | Non-Pi providers | Require an explicit session ID |
46
-
47
- Use each tool's schema for full options. Typical flow:
48
-
49
- 1. `goose_search` or `goose_grep` for candidate entry IDs
50
- 2. `goose_get` on the interesting IDs
51
- 3. `goose_recall` for durable facts, decisions, preferences, procedures, lessons
52
- 4. `goose_remember` after milestones worth retaining
18
+ - `goose_search` and `goose_grep` find session entries.
19
+ - `goose_get` expands entry IDs to full content.
20
+ - `goose_remember`, `goose_recall`, and `goose_forget` manage durable memory.
21
+ - `goose_memory_status` reports memory storage health.
22
+ - `goose_compact` compacts the current Pi session.
23
+ - `goose_sessions` lists or deletes sessions for the current project.
53
24
 
54
- ## Commands
25
+ Search and grep return entry IDs; call `goose_get` before relying on their
26
+ content. Non-Pi providers require an explicit session ID.
55
27
 
56
- | Command | Purpose |
57
- | --------------------------------- | ------------------------------------------------ |
58
- | `/goose-search [query]` | Search the current session |
59
- | `/goose-compact [keep:N]` | Compact now; keep the last N user turns verbatim |
60
- | `/goose-sessions` | List, resume, or delete project sessions |
61
- | `/goose-forget [id\|provider:id]` | Forget stored memory |
62
- | `/goose-memory-status` | Show memory storage and indexing status |
63
- | `/goose-settings` | Edit goosedump settings |
64
- | `/goosedump-settings` | Edit goosedump settings (alias) |
28
+ ## Commands
65
29
 
66
- Resume is available only for Pi sessions.
30
+ `/goose-search`, `/goose-compact`, `/goose-sessions`, `/goose-forget`,
31
+ `/goose-memory-status`, and `/goose-settings`.
67
32
 
68
33
  ## Settings
69
34
 
70
- - Global: `~/.pi/agent/settings.json`
71
- - Project: `.pi/settings.json`
35
+ Add `goosedump` to `~/.pi/agent/settings.json` or `.pi/settings.json`:
72
36
 
73
37
  ```json
74
38
  {
@@ -80,49 +44,17 @@ Resume is available only for Pi sessions.
80
44
  }
81
45
  ```
82
46
 
83
- | Key | Range | Default | Notes |
84
- | -------------------- | -------------- | ------: | -------------------------------------------------------------- |
85
- | `turnsBeforeCompact` | `0`–`255` | 0 | `0` disables; each completed model response counts as one turn |
86
- | `summaryMaxTokens` | `512`–`65536` | 4096 | Capped to 80% of `compaction.reserveTokens` when lower |
87
- | `enableMemory` | `true`/`false` | false | Controls whether durable memory tools/commands/learning are on |
88
-
89
- The footer shows the remaining turn count in hexadecimal.
90
-
91
- ### Compaction behavior
92
-
93
- - Pi's `compaction.enabled` controls automatic compaction.
94
- - The first enabled context or turn limit starts one compaction.
95
- - Successful compaction resets the turn counter.
96
- - Manual and automatic compaction preserve the previous summary and summarize
97
- only new entries.
98
- - `keep:N` leaves the last N user turns verbatim.
99
- - Manual requests wait until Pi is idle.
100
- - Memory learning runs in the background after a successful compaction.
101
- - If the turn limit expires during a tool loop, Pi saves the current batch,
102
- compacts at that turn boundary, and resumes automatically.
103
- - Goosedump failures fall back to Pi's compactor.
47
+ `turnsBeforeCompact: 0` disables turn-based compaction. Memory learning runs in
48
+ the background after successful compaction when memory is enabled.
104
49
 
105
50
  ## Development
106
51
 
107
- Install dependencies and run the test suite:
108
-
109
52
  ```sh
110
53
  npm install
111
54
  npm test
112
55
  npm run check
113
56
  ```
114
57
 
115
- To exercise the extension against the binary built in this repository:
116
-
117
- ```sh
118
- cargo build
119
- GOOSEDUMP_BINARY="$(cd ../.. && pwd)/target/debug/goosedump" \
120
- pi --no-extensions -e ./index.ts
121
- ```
122
-
123
- The explicit override takes precedence over the packaged platform binary. It must
124
- be an absolute path to an executable regular file.
125
-
126
- ## License
58
+ ## Licensing
127
59
 
128
- Apache-2.0. See [LICENSE](LICENSE).
60
+ [Apache 2.0](LICENSE).
package/dist/index.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Copyright (C) Jarkko Sakkinen 2026
3
3
 
4
+ // src/integration.ts
5
+ import { randomUUID as randomUUID2 } from "node:crypto";
6
+
4
7
  // src/cli.ts
5
- import { Type as Type2 } from "@sinclair/typebox";
6
- import { Value as Value2 } from "@sinclair/typebox/value";
8
+ import { Type as Type2 } from "typebox";
9
+ import { Value as Value2 } from "typebox/value";
7
10
  import { execFile } from "node:child_process";
8
11
  import { accessSync, constants as fsConstants, existsSync, readFileSync as readFileSync2, statSync } from "node:fs";
9
12
  import { createRequire } from "node:module";
@@ -11,10 +14,10 @@ import { dirname, isAbsolute, join } from "node:path";
11
14
  import { fileURLToPath } from "node:url";
12
15
 
13
16
  // src/format.ts
14
- import { Value } from "@sinclair/typebox/value";
17
+ import { Value } from "typebox/value";
15
18
 
16
19
  // src/types.ts
17
- import { Type } from "@sinclair/typebox";
20
+ import { Type } from "typebox";
18
21
  var BRANCH_SUMMARY_PREFIX = `The following is a summary of a branch that this conversation came back from:
19
22
 
20
23
  <summary>
@@ -529,8 +532,11 @@ async function goosedumpRecall(query, options = {}, signal, executablePath) {
529
532
  const output = await runGoosedumpAsync(args, signal, executablePath);
530
533
  return { hits: parseMemoryHits(output.stdout), warning: output.stderr.trim() };
531
534
  }
532
- async function goosedumpMemoryRemember(target, signal, executablePath) {
533
- const output = await runGoosedumpAsync(["memory", "learn", target, "--json"], signal, executablePath);
535
+ async function goosedumpMemoryRemember(target, signal, executablePath, leaseOwner) {
536
+ const args = ["memory", "learn", target, "--json"];
537
+ if (leaseOwner)
538
+ args.push("--lease-owner", leaseOwner);
539
+ const output = await runGoosedumpAsync(args, signal, executablePath);
534
540
  return output.stdout.trim();
535
541
  }
536
542
  async function goosedumpMemoryForget(target, signal, executablePath) {
@@ -620,7 +626,7 @@ import { findCutPoint } from "@earendil-works/pi-coding-agent";
620
626
 
621
627
  // src/settings.ts
622
628
  import { DEFAULT_COMPACTION_SETTINGS } from "@earendil-works/pi-coding-agent";
623
- import { Value as Value3 } from "@sinclair/typebox/value";
629
+ import { Value as Value3 } from "typebox/value";
624
630
  import { randomUUID } from "node:crypto";
625
631
  import {
626
632
  existsSync as existsSync2,
@@ -633,9 +639,9 @@ import {
633
639
  import { homedir } from "node:os";
634
640
  import { dirname as dirname2, join as join2 } from "node:path";
635
641
  function settingSpecFromSchema(schema) {
636
- if (schema.type === "boolean")
642
+ const { type, minimum, maximum } = schema;
643
+ if (type === "boolean")
637
644
  return { kind: "boolean" };
638
- const { minimum, maximum } = schema;
639
645
  if (typeof minimum !== "number" || typeof maximum !== "number") {
640
646
  throw new Error("goosedump setting schema is missing integer bounds");
641
647
  }
@@ -996,6 +1002,7 @@ class CompactionRuntime {
996
1002
  overrideDefaultCompaction;
997
1003
  nativeExecutablePath;
998
1004
  enqueueRemember;
1005
+ runCompactInference;
999
1006
  compactSettings = DEFAULT_GOOSEDUMP_SETTINGS;
1000
1007
  piAutoCompactionEnabled = DEFAULT_COMPACTION_SETTINGS2.enabled;
1001
1008
  piCompactionReserveTokens = DEFAULT_COMPACTION_SETTINGS2.reserveTokens;
@@ -1014,6 +1021,7 @@ class CompactionRuntime {
1014
1021
  this.overrideDefaultCompaction = deps.overrideDefaultCompaction;
1015
1022
  this.nativeExecutablePath = deps.nativeExecutablePath;
1016
1023
  this.enqueueRemember = deps.enqueueRemember;
1024
+ this.runCompactInference = deps.runCompactInference;
1017
1025
  }
1018
1026
  setActivePi(pi) {
1019
1027
  this.activePi = pi;
@@ -1224,7 +1232,7 @@ class CompactionRuntime {
1224
1232
  });
1225
1233
  }
1226
1234
  }
1227
- const compaction = await buildGoosedumpCompaction(target, event.preparation, event.branchEntries, effectiveSummaryMaxTokens(ctx.cwd, projectSettingsTrusted(ctx)), keep, signal, this.nativeExecutablePath);
1235
+ const compaction = await this.runCompactInference(() => buildGoosedumpCompaction(target, event.preparation, event.branchEntries, effectiveSummaryMaxTokens(ctx.cwd, projectSettingsTrusted(ctx)), keep, signal, this.nativeExecutablePath));
1228
1236
  if (!this.runningOperationIsCurrent(operation, generation, sessionFile)) {
1229
1237
  return { cancel: true };
1230
1238
  }
@@ -1461,36 +1469,124 @@ class CompactionRuntime {
1461
1469
  }
1462
1470
  }
1463
1471
 
1472
+ // src/inference-coordinator.ts
1473
+ class InferenceBusyError extends Error {
1474
+ constructor(message = "another inference task is already active") {
1475
+ super(message);
1476
+ this.name = "InferenceBusyError";
1477
+ }
1478
+ }
1479
+
1480
+ class InferenceCoordinator {
1481
+ activeAttempt = null;
1482
+ compactActive = false;
1483
+ idleWaiters = [];
1484
+ tryRunNonCompact(operation, externalSignal) {
1485
+ if (this.compactActive || this.activeAttempt)
1486
+ return null;
1487
+ let resolvePromise = () => {};
1488
+ let rejectPromise = () => {};
1489
+ const promise = new Promise((resolve, reject) => {
1490
+ resolvePromise = resolve;
1491
+ rejectPromise = reject;
1492
+ });
1493
+ const job = {
1494
+ operation,
1495
+ externalSignal,
1496
+ settled: false,
1497
+ resolve: (value) => {
1498
+ if (job.settled)
1499
+ return;
1500
+ job.settled = true;
1501
+ resolvePromise(value);
1502
+ },
1503
+ reject: (error) => {
1504
+ if (job.settled)
1505
+ return;
1506
+ job.settled = true;
1507
+ rejectPromise(error);
1508
+ }
1509
+ };
1510
+ this.startNonCompact(job);
1511
+ return promise;
1512
+ }
1513
+ async runCompact(operation) {
1514
+ if (this.compactActive)
1515
+ throw new InferenceBusyError("compaction inference is already active");
1516
+ this.compactActive = true;
1517
+ const interrupted = this.activeAttempt;
1518
+ if (interrupted) {
1519
+ interrupted.preempted = true;
1520
+ interrupted.controller.abort(new Error("inference paused for compaction"));
1521
+ await interrupted.completion;
1522
+ }
1523
+ try {
1524
+ return await operation();
1525
+ } finally {
1526
+ this.compactActive = false;
1527
+ if (interrupted && !interrupted.job.settled) {
1528
+ this.startNonCompact(interrupted.job);
1529
+ } else {
1530
+ this.resolveIdleWaiters();
1531
+ }
1532
+ }
1533
+ }
1534
+ async waitForIdle() {
1535
+ while (this.compactActive || this.activeAttempt) {
1536
+ await new Promise((resolve) => this.idleWaiters.push(resolve));
1537
+ }
1538
+ }
1539
+ startNonCompact(job) {
1540
+ const externalSignal = job.externalSignal;
1541
+ if (externalSignal?.aborted) {
1542
+ job.reject(externalSignal.reason ?? new Error("inference cancelled"));
1543
+ this.resolveIdleWaiters();
1544
+ return;
1545
+ }
1546
+ const controller = new AbortController;
1547
+ const forwardExternalAbort = () => controller.abort(externalSignal?.reason);
1548
+ externalSignal?.addEventListener("abort", forwardExternalAbort, { once: true });
1549
+ const attempt = {
1550
+ job,
1551
+ controller,
1552
+ preempted: false,
1553
+ completion: Promise.resolve()
1554
+ };
1555
+ this.activeAttempt = attempt;
1556
+ attempt.completion = Promise.resolve().then(() => job.operation(controller.signal)).then((value) => {
1557
+ job.resolve(value);
1558
+ }, (error) => {
1559
+ if (!attempt.preempted)
1560
+ job.reject(error);
1561
+ }).finally(() => {
1562
+ externalSignal?.removeEventListener("abort", forwardExternalAbort);
1563
+ if (this.activeAttempt === attempt)
1564
+ this.activeAttempt = null;
1565
+ this.resolveIdleWaiters();
1566
+ });
1567
+ }
1568
+ resolveIdleWaiters() {
1569
+ if (this.compactActive || this.activeAttempt)
1570
+ return;
1571
+ const waiters = this.idleWaiters;
1572
+ this.idleWaiters = [];
1573
+ for (const resolve of waiters)
1574
+ resolve();
1575
+ }
1576
+ }
1577
+
1464
1578
  // src/memory-activity.ts
1465
- import { truncateToWidth } from "@earendil-works/pi-tui";
1466
- var MEMORY_ACTIVITY_WIDGET_KEY = "goosedump-memory-activity";
1467
- var ACTIVITY_FRAMES = ["▰▱▱▱", "▱▰▱▱", "▱▱▰▱", "▱▱▱▰", "▱▱▰▱", "▱▰▱▱"];
1579
+ var MEMORY_ACTIVITY_STATUS_KEY = "goosedump-memory-activity";
1468
1580
  function showMemoryActivity(ctx) {
1469
1581
  if (!ctx.hasUI)
1470
1582
  return;
1471
- ctx.ui.setWidget(MEMORY_ACTIVITY_WIDGET_KEY, (tui, theme) => {
1472
- let frame = 0;
1473
- const timer = setInterval(() => {
1474
- frame = (frame + 1) % ACTIVITY_FRAMES.length;
1475
- tui.requestRender();
1476
- }, 140);
1477
- timer.unref();
1478
- return {
1479
- render(width) {
1480
- const bar = theme.fg("accent", ACTIVITY_FRAMES[frame] ?? ACTIVITY_FRAMES[0]);
1481
- const label = theme.fg("muted", "Remembering...");
1482
- return [truncateToWidth(`${bar} ${label}`, width, "")];
1483
- },
1484
- invalidate() {},
1485
- dispose() {
1486
- clearInterval(timer);
1487
- }
1488
- };
1489
- });
1583
+ const dot = ctx.ui.theme.fg("accent", "●");
1584
+ const label = ctx.ui.theme.fg("muted", "remembering");
1585
+ ctx.ui.setStatus(MEMORY_ACTIVITY_STATUS_KEY, `${dot} ${label}`);
1490
1586
  }
1491
1587
  function hideMemoryActivity(ctx) {
1492
1588
  if (ctx.hasUI)
1493
- ctx.ui.setWidget(MEMORY_ACTIVITY_WIDGET_KEY, undefined);
1589
+ ctx.ui.setStatus(MEMORY_ACTIVITY_STATUS_KEY, undefined);
1494
1590
  }
1495
1591
 
1496
1592
  // src/policy.ts
@@ -1650,7 +1746,7 @@ import {
1650
1746
  defineTool,
1651
1747
  truncateHead
1652
1748
  } from "@earendil-works/pi-coding-agent";
1653
- import { Type as Type3 } from "@sinclair/typebox";
1749
+ import { Type as Type3 } from "typebox";
1654
1750
  function memoryDisabledResult() {
1655
1751
  return textResult(MEMORY_DISABLED_MESSAGE);
1656
1752
  }
@@ -1739,7 +1835,7 @@ function defineGooseTool(name, parameters, handler) {
1739
1835
  });
1740
1836
  }
1741
1837
  function registerGooseTools(pi, deps) {
1742
- const { nativeExecutablePath, enqueueRemember, requestCompaction } = deps;
1838
+ const { nativeExecutablePath, enqueueRemember, requestCompaction, runNonCompactInference } = deps;
1743
1839
  pi.registerTool(defineGooseTool("goose_remember", Type3.Object({ ...historyTargetParams() }), async (params, _signal, ctx) => {
1744
1840
  const disabled = requireMemoryEnabled(ctx);
1745
1841
  if (disabled)
@@ -1747,12 +1843,12 @@ function registerGooseTools(pi, deps) {
1747
1843
  const resolved = resolveHistoryTarget(ctx, "goose_remember", params);
1748
1844
  if (!resolved.ok)
1749
1845
  return resolved.result;
1750
- enqueueRemember(resolved.target, ctx, (message) => {
1846
+ const accepted = enqueueRemember(resolved.target, ctx, (message) => {
1751
1847
  if (ctx.hasUI) {
1752
1848
  ctx.ui.notify(`goosedump memory learning failed: ${message}`, "warning");
1753
1849
  }
1754
1850
  });
1755
- return textResult(`Memory learning queued for ${resolved.target}.`);
1851
+ return textResult(accepted ? `Memory learning started for ${resolved.target}.` : `Memory learning dropped for ${resolved.target}: another inference task is active.`);
1756
1852
  }));
1757
1853
  pi.registerTool(defineGooseTool("goose_recall", Type3.Object({
1758
1854
  query: Type3.String({ description: PARAM_DESCRIPTIONS.memoryQuery }),
@@ -1775,12 +1871,16 @@ function registerGooseTools(pi, deps) {
1775
1871
  const disabled = requireMemoryEnabled(ctx);
1776
1872
  if (disabled)
1777
1873
  return disabled;
1778
- const { hits, warning } = await goosedumpRecall(params.query, {
1874
+ const inference = runNonCompactInference((inferenceSignal) => goosedumpRecall(params.query, {
1779
1875
  path: params.allProjects ? undefined : ctx.cwd,
1780
1876
  type: params.type,
1781
1877
  limit: params.limit,
1782
1878
  history: params.history
1783
- }, signal, nativeExecutablePath);
1879
+ }, inferenceSignal, nativeExecutablePath), signal);
1880
+ if (!inference) {
1881
+ return textResult("Memory recall dropped: another inference task is active.");
1882
+ }
1883
+ const { hits, warning } = await inference;
1784
1884
  if (warning && ctx.hasUI)
1785
1885
  ctx.ui.notify(warning, "warning");
1786
1886
  if (hits.length === 0)
@@ -1927,7 +2027,7 @@ import {
1927
2027
  Key,
1928
2028
  decodeKittyPrintable,
1929
2029
  matchesKey,
1930
- truncateToWidth as truncateToWidth2,
2030
+ truncateToWidth,
1931
2031
  visibleWidth,
1932
2032
  wrapTextWithAnsi
1933
2033
  } from "@earendil-works/pi-tui";
@@ -2010,7 +2110,7 @@ class NumberInputSubmenu {
2010
2110
  }
2011
2111
  }
2012
2112
  function makeRow(content, innerW, border) {
2013
- const line = truncateToWidth2(content, innerW);
2113
+ const line = truncateToWidth(content, innerW);
2014
2114
  const pad = Math.max(0, innerW - visibleWidth(line));
2015
2115
  return `${border} ${line}${" ".repeat(pad)} ${border}`;
2016
2116
  }
@@ -2162,7 +2262,7 @@ async function runSettingsEditor(ctx) {
2162
2262
  const { accent, bold, dim, muted, text } = colors;
2163
2263
  const innerW = overlayInnerWidth(width);
2164
2264
  const pad = (value, cellWidth) => {
2165
- const clipped = truncateToWidth2(value, cellWidth);
2265
+ const clipped = truncateToWidth(value, cellWidth);
2166
2266
  return `${clipped}${" ".repeat(Math.max(0, cellWidth - visibleWidth(clipped)))}`;
2167
2267
  };
2168
2268
  const tabLabel = (label, active) => active ? accent(bold(`[${label}]`)) : muted(label);
@@ -2173,7 +2273,7 @@ async function runSettingsEditor(ctx) {
2173
2273
  } else if (tab === "help") {
2174
2274
  const shortcutWidth = Math.max(visibleWidth("Shortcut"), ...helpRows.map(([shortcut]) => visibleWidth(shortcut)));
2175
2275
  const descriptionWidth = Math.max(1, innerW - shortcutWidth - 4);
2176
- content.push(dim(` ${pad("Shortcut", shortcutWidth)} Description`), ...helpRows.map(([shortcut, description]) => ` ${accent(pad(shortcut, shortcutWidth))} ${text(truncateToWidth2(description, descriptionWidth))}`));
2276
+ content.push(dim(` ${pad("Shortcut", shortcutWidth)} Description`), ...helpRows.map(([shortcut, description]) => ` ${accent(pad(shortcut, shortcutWidth))} ${text(truncateToWidth(description, descriptionWidth))}`));
2177
2277
  } else {
2178
2278
  for (const [index, entry] of specs.entries()) {
2179
2279
  const selected = index === selectedSetting;
@@ -2314,7 +2414,7 @@ async function runSearchPanel(ctx, query, executablePath) {
2314
2414
  }
2315
2415
  return {
2316
2416
  render(width) {
2317
- const header = overlayHeader(colors, width, `Goosedump search: ${truncateToWidth2(query, 44)}`);
2417
+ const header = overlayHeader(colors, width, `Goosedump search: ${truncateToWidth(query, 44)}`);
2318
2418
  if (error) {
2319
2419
  return [header, ...overlayError(colors, width, error), overlayFooter(colors, width)];
2320
2420
  }
@@ -2672,50 +2772,35 @@ function createGoosedumpIntegration(options = {}) {
2672
2772
  const shouldRegisterTool = options.registerTool ?? true;
2673
2773
  const shouldOverrideDefaultCompaction = options.overrideDefaultCompaction ?? true;
2674
2774
  const nativeExecutablePath = options.nativeExecutablePath ?? resolveNativeGoosedumpBinary();
2675
- const rememberQueue = [];
2676
- let rememberRunning = null;
2775
+ const inference = new InferenceCoordinator;
2677
2776
  function enqueueRemember(target, ctx, reportError) {
2678
2777
  if (!isMemoryEnabled(ctx.cwd, projectSettingsTrusted(ctx)))
2679
- return;
2680
- for (let i = rememberQueue.length - 1;i >= 0; i -= 1) {
2681
- if (rememberQueue[i]?.target === target)
2682
- rememberQueue.splice(i, 1);
2683
- }
2684
- rememberQueue.push({ target, ctx, reportError });
2685
- ensureRememberWorker();
2686
- }
2687
- function ensureRememberWorker() {
2688
- if (rememberRunning)
2689
- return;
2690
- rememberRunning = (async () => {
2691
- while (rememberQueue.length > 0) {
2692
- const job = rememberQueue.shift();
2693
- if (!job)
2694
- continue;
2695
- showMemoryActivity(job.ctx);
2696
- try {
2697
- await goosedumpMemoryRemember(job.target, undefined, nativeExecutablePath);
2698
- } catch (err) {
2699
- const message = err instanceof Error ? err.message : String(err);
2700
- job.reportError(message);
2701
- } finally {
2702
- hideMemoryActivity(job.ctx);
2703
- }
2778
+ return false;
2779
+ const leaseOwner = randomUUID2();
2780
+ const running = inference.tryRunNonCompact(async (signal) => {
2781
+ showMemoryActivity(ctx);
2782
+ try {
2783
+ await goosedumpMemoryRemember(target, signal, nativeExecutablePath, leaseOwner);
2784
+ } finally {
2785
+ hideMemoryActivity(ctx);
2704
2786
  }
2705
- })().finally(() => {
2706
- rememberRunning = null;
2707
- if (rememberQueue.length > 0)
2708
- ensureRememberWorker();
2709
2787
  });
2788
+ if (!running)
2789
+ return false;
2790
+ running.catch((err) => {
2791
+ const message = err instanceof Error ? err.message : String(err);
2792
+ reportError(message);
2793
+ });
2794
+ return true;
2710
2795
  }
2711
2796
  async function flushRememberQueue() {
2712
- while (rememberRunning)
2713
- await rememberRunning;
2797
+ await inference.waitForIdle();
2714
2798
  }
2715
2799
  const compaction = new CompactionRuntime({
2716
2800
  overrideDefaultCompaction: shouldOverrideDefaultCompaction,
2717
2801
  nativeExecutablePath,
2718
- enqueueRemember
2802
+ enqueueRemember,
2803
+ runCompactInference: (operation) => inference.runCompact(operation)
2719
2804
  });
2720
2805
  function register(pi) {
2721
2806
  compaction.setActivePi(pi);
@@ -2723,6 +2808,7 @@ function createGoosedumpIntegration(options = {}) {
2723
2808
  registerGooseTools(pi, {
2724
2809
  nativeExecutablePath,
2725
2810
  enqueueRemember,
2811
+ runNonCompactInference: (operation, signal) => inference.tryRunNonCompact(operation, signal),
2726
2812
  requestCompaction: (ctx, keep) => compaction.requestCompaction(ctx, keep)
2727
2813
  });
2728
2814
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goosedump",
3
- "version": "0.12.55",
3
+ "version": "0.12.56",
4
4
  "description": "Pi extension for goosedump session search, durable memory, session management, and compaction",
5
5
  "keywords": [
6
6
  "compaction",
@@ -37,24 +37,32 @@
37
37
  "ci:test": "npm test"
38
38
  },
39
39
  "dependencies": {
40
- "@earendil-works/pi-tui": "^0.82.1",
41
- "@jarkkojs/goosedump": "^0.12.55",
42
- "@sinclair/typebox": "^0.34.49"
40
+ "@jarkkojs/goosedump": "^0.12.56"
43
41
  },
44
42
  "devDependencies": {
45
- "@earendil-works/pi-coding-agent": "^0.82.1",
43
+ "@earendil-works/pi-coding-agent": "^0.84.2",
44
+ "@earendil-works/pi-tui": "^0.84.2",
46
45
  "@types/node": "^26.1.2",
47
46
  "oxfmt": "^0.60.0",
48
47
  "oxlint": "^1.68.0",
48
+ "typebox": "1.3.7",
49
49
  "typescript": "^7.0.2",
50
50
  "vitest": "^4.1.10"
51
51
  },
52
52
  "peerDependencies": {
53
- "@earendil-works/pi-coding-agent": "^0.82.1"
53
+ "@earendil-works/pi-coding-agent": "^0.84.2",
54
+ "@earendil-works/pi-tui": "^0.84.2",
55
+ "typebox": "^1.3.7"
54
56
  },
55
57
  "peerDependenciesMeta": {
56
58
  "@earendil-works/pi-coding-agent": {
57
59
  "optional": true
60
+ },
61
+ "@earendil-works/pi-tui": {
62
+ "optional": true
63
+ },
64
+ "typebox": {
65
+ "optional": true
58
66
  }
59
67
  },
60
68
  "pi": {