pi-goosedump 0.12.54 → 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 +27 -71
  2. package/dist/index.ts +208 -103
  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,63 +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. A missing packaged
15
- binary is treated as a broken installation instead of falling back to a
16
- `goosedump` executable on `PATH`. Local models download on 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.
17
15
 
18
16
  ## Tools
19
17
 
20
- Prefer these tools whenever prior sessions or durable memory can answer a
21
- question. Expand ranked or grepped hits with `goose_get` before guessing.
22
-
23
- | Tool | When to use |
24
- | --------------------- | ---------------------------------------------------------- |
25
- | `goose_search` | Ranked session-history search; returns entry IDs |
26
- | `goose_grep` | Exact glob/substring filter over session messages |
27
- | `goose_get` | Expand entry IDs from search/grep to full content |
28
- | `goose_remember` | Queue background learning from a session after a milestone |
29
- | `goose_recall` | Recall project-scoped durable memory with provenance |
30
- | `goose_forget` | Remove a stale memory or a session's retained sources |
31
- | `goose_memory_status` | Inspect storage health, type counts, and tombstones |
32
- | `goose_compact` | Compact the current Pi session asynchronously |
33
- | `goose_sessions` | List or delete sessions in the current working directory |
34
-
35
- Defaults:
36
-
37
- | Setting | Default |
38
- | ---------------- | ------------------------------ |
39
- | Session target | Current Pi session |
40
- | History scope | `lineage` (active branch) |
41
- | Recall limit | 5 |
42
- | Non-Pi providers | Require an explicit session ID |
43
-
44
- Use each tool's schema for full options. Typical flow:
45
-
46
- 1. `goose_search` or `goose_grep` for candidate entry IDs
47
- 2. `goose_get` on the interesting IDs
48
- 3. `goose_recall` for durable facts, decisions, preferences, procedures, lessons
49
- 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.
50
24
 
51
- ## 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.
52
27
 
53
- | Command | Purpose |
54
- | --------------------------------- | ------------------------------------------------ |
55
- | `/goose-search [query]` | Search the current session |
56
- | `/goose-compact [keep:N]` | Compact now; keep the last N user turns verbatim |
57
- | `/goose-sessions` | List, resume, or delete project sessions |
58
- | `/goose-forget [id\|provider:id]` | Forget stored memory |
59
- | `/goose-memory-status` | Show memory storage and indexing status |
60
- | `/goose-settings` | Edit goosedump settings |
61
- | `/goosedump-settings` | Edit goosedump settings (alias) |
28
+ ## Commands
62
29
 
63
- Resume is available only for Pi sessions.
30
+ `/goose-search`, `/goose-compact`, `/goose-sessions`, `/goose-forget`,
31
+ `/goose-memory-status`, and `/goose-settings`.
64
32
 
65
33
  ## Settings
66
34
 
67
- - Global: `~/.pi/agent/settings.json`
68
- - Project: `.pi/settings.json`
35
+ Add `goosedump` to `~/.pi/agent/settings.json` or `.pi/settings.json`:
69
36
 
70
37
  ```json
71
38
  {
@@ -77,28 +44,17 @@ Resume is available only for Pi sessions.
77
44
  }
78
45
  ```
79
46
 
80
- | Key | Range | Default | Notes |
81
- | -------------------- | -------------- | ------: | -------------------------------------------------------------- |
82
- | `turnsBeforeCompact` | `0`–`255` | 0 | `0` disables; each completed model response counts as one turn |
83
- | `summaryMaxTokens` | `512`–`65536` | 4096 | Capped to 80% of `compaction.reserveTokens` when lower |
84
- | `enableMemory` | `true`/`false` | false | Controls whether durable memory tools/commands/learning are on |
47
+ `turnsBeforeCompact: 0` disables turn-based compaction. Memory learning runs in
48
+ the background after successful compaction when memory is enabled.
85
49
 
86
- The footer shows the remaining turn count in hexadecimal.
50
+ ## Development
87
51
 
88
- ### Compaction behavior
89
-
90
- - Pi's `compaction.enabled` controls automatic compaction.
91
- - The first enabled context or turn limit starts one compaction.
92
- - Successful compaction resets the turn counter.
93
- - Manual and automatic compaction preserve the previous summary and summarize
94
- only new entries.
95
- - `keep:N` leaves the last N user turns verbatim.
96
- - Manual requests wait until Pi is idle.
97
- - Memory learning runs in the background after a successful compaction.
98
- - If the turn limit expires during a tool loop, Pi saves the current batch,
99
- compacts at that turn boundary, and resumes automatically.
100
- - Goosedump failures fall back to Pi's compactor.
52
+ ```sh
53
+ npm install
54
+ npm test
55
+ npm run check
56
+ ```
101
57
 
102
- ## License
58
+ ## Licensing
103
59
 
104
- Apache-2.0. See [LICENSE](LICENSE).
60
+ [Apache 2.0](LICENSE).
package/dist/index.ts CHANGED
@@ -1,20 +1,23 @@
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
- import { existsSync, readFileSync } from "node:fs";
11
+ import { accessSync, constants as fsConstants, existsSync, readFileSync as readFileSync2, statSync } from "node:fs";
9
12
  import { createRequire } from "node:module";
10
- import { dirname, join } from "node:path";
13
+ 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>
@@ -160,7 +163,7 @@ var GOOSE_PROVIDERS = [
160
163
  var SUMMARIZABLE_ENTRY_TYPES = new Set(["message", "custom_message", "branch_summary"]);
161
164
 
162
165
  // src/util.ts
163
- import { closeSync, openSync, readSync } from "node:fs";
166
+ import { readFileSync } from "node:fs";
164
167
  function isRecord(value) {
165
168
  return typeof value === "object" && value !== null && !Array.isArray(value);
166
169
  }
@@ -181,21 +184,13 @@ function compactStartedMessage(keep, queued = false) {
181
184
  return keep !== null ? `${action}; keeping the last ${keep} user turn${keep === 1 ? "" : "s"}.` : `${action} up to Pi's token-based cut.`;
182
185
  }
183
186
  function readFirstLine(path) {
184
- let fd;
185
187
  try {
186
- fd = openSync(path, "r");
187
- const buf = Buffer.alloc(8192);
188
- const bytesRead = readSync(fd, buf, 0, buf.length, 0);
189
- if (bytesRead < 0)
190
- return null;
191
- const slice = buf.subarray(0, bytesRead);
192
- const newline = slice.indexOf(10);
193
- return (newline < 0 ? slice : slice.subarray(0, newline)).toString("utf8");
188
+ const content = readFileSync(path, "utf8");
189
+ const newline = content.indexOf(`
190
+ `);
191
+ return newline < 0 ? content : content.slice(0, newline);
194
192
  } catch {
195
193
  return null;
196
- } finally {
197
- if (fd !== undefined)
198
- closeSync(fd);
199
194
  }
200
195
  }
201
196
  function formatTarget(provider, id) {
@@ -206,16 +201,17 @@ function appendScope(args, scope) {
206
201
  args.push("--all");
207
202
  }
208
203
  function utf8Prefix(text, maxBytes) {
209
- const prefix = [];
210
204
  let bytes = 0;
205
+ let index = 0;
211
206
  for (const char of text) {
212
- const charBytes = Buffer.byteLength(char);
207
+ const cp = char.codePointAt(0);
208
+ const charBytes = cp <= 127 ? 1 : cp <= 2047 ? 2 : cp <= 65535 ? 3 : 4;
213
209
  if (bytes + charBytes > maxBytes)
214
210
  break;
215
- prefix.push(char);
216
211
  bytes += charBytes;
212
+ index += char.length;
217
213
  }
218
- return prefix.join("");
214
+ return text.slice(0, index);
219
215
  }
220
216
  function isControlChar(char) {
221
217
  const code = char.charCodeAt(0);
@@ -417,7 +413,7 @@ function resolveNativePackageTable() {
417
413
  throw new Error("broken pi-goosedump installation: cannot resolve the native package table from @jarkkojs/goosedump");
418
414
  }
419
415
  function loadNativeGoosedumpPackages() {
420
- const table = JSON.parse(readFileSync(resolveNativePackageTable(), "utf8"));
416
+ const table = JSON.parse(readFileSync2(resolveNativePackageTable(), "utf8"));
421
417
  if (!Value2.Check(NATIVE_GOOSEDUMP_PACKAGES_SCHEMA, table)) {
422
418
  throw new Error("broken pi-goosedump installation: native package table is invalid");
423
419
  }
@@ -442,7 +438,29 @@ function resolveGoosedumpBinary() {
442
438
  throw new Error("broken pi-goosedump installation: cannot resolve the @jarkkojs/goosedump dependency", { cause });
443
439
  }
444
440
  }
445
- function resolveNativeGoosedumpBinary() {
441
+ var GOOSEDUMP_BINARY_ENV = "GOOSEDUMP_BINARY";
442
+ function resolveGoosedumpBinaryOverride(environment = process.env) {
443
+ const binaryPath = environment[GOOSEDUMP_BINARY_ENV];
444
+ if (binaryPath === undefined)
445
+ return null;
446
+ if (!isAbsolute(binaryPath)) {
447
+ throw new Error(`${GOOSEDUMP_BINARY_ENV} must be an absolute path to the goosedump executable`);
448
+ }
449
+ try {
450
+ if (!statSync(binaryPath).isFile())
451
+ throw new Error("not a regular file");
452
+ accessSync(binaryPath, fsConstants.X_OK);
453
+ } catch (cause) {
454
+ throw new Error(`${GOOSEDUMP_BINARY_ENV} does not point to an executable file: ${binaryPath}`, {
455
+ cause
456
+ });
457
+ }
458
+ return binaryPath;
459
+ }
460
+ function resolveNativeGoosedumpBinary(environment = process.env) {
461
+ const override = resolveGoosedumpBinaryOverride(environment);
462
+ if (override)
463
+ return override;
446
464
  const target = `${process.platform}-${process.arch}`;
447
465
  const { packageName, binaryName } = nativeGoosedumpPackage(target);
448
466
  try {
@@ -514,8 +532,11 @@ async function goosedumpRecall(query, options = {}, signal, executablePath) {
514
532
  const output = await runGoosedumpAsync(args, signal, executablePath);
515
533
  return { hits: parseMemoryHits(output.stdout), warning: output.stderr.trim() };
516
534
  }
517
- async function goosedumpMemoryRemember(target, signal, executablePath) {
518
- 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);
519
540
  return output.stdout.trim();
520
541
  }
521
542
  async function goosedumpMemoryForget(target, signal, executablePath) {
@@ -605,12 +626,12 @@ import { findCutPoint } from "@earendil-works/pi-coding-agent";
605
626
 
606
627
  // src/settings.ts
607
628
  import { DEFAULT_COMPACTION_SETTINGS } from "@earendil-works/pi-coding-agent";
608
- import { Value as Value3 } from "@sinclair/typebox/value";
629
+ import { Value as Value3 } from "typebox/value";
609
630
  import { randomUUID } from "node:crypto";
610
631
  import {
611
632
  existsSync as existsSync2,
612
633
  mkdirSync,
613
- readFileSync as readFileSync2,
634
+ readFileSync as readFileSync3,
614
635
  renameSync,
615
636
  unlinkSync,
616
637
  writeFileSync
@@ -618,9 +639,9 @@ import {
618
639
  import { homedir } from "node:os";
619
640
  import { dirname as dirname2, join as join2 } from "node:path";
620
641
  function settingSpecFromSchema(schema) {
621
- if (schema.type === "boolean")
642
+ const { type, minimum, maximum } = schema;
643
+ if (type === "boolean")
622
644
  return { kind: "boolean" };
623
- const { minimum, maximum } = schema;
624
645
  if (typeof minimum !== "number" || typeof maximum !== "number") {
625
646
  throw new Error("goosedump setting schema is missing integer bounds");
626
647
  }
@@ -663,7 +684,7 @@ function readJsonObject(path) {
663
684
  if (!existsSync2(path))
664
685
  return {};
665
686
  try {
666
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
687
+ const parsed = JSON.parse(readFileSync3(path, "utf8"));
667
688
  return isRecord(parsed) ? parsed : {};
668
689
  } catch {
669
690
  return {};
@@ -674,7 +695,7 @@ function readJsonObjectStrict(path) {
674
695
  return {};
675
696
  let parsed;
676
697
  try {
677
- parsed = JSON.parse(readFileSync2(path, "utf8"));
698
+ parsed = JSON.parse(readFileSync3(path, "utf8"));
678
699
  } catch (error) {
679
700
  const message = error instanceof Error ? error.message : String(error);
680
701
  throw new Error(`${path}: ${message}`);
@@ -981,6 +1002,7 @@ class CompactionRuntime {
981
1002
  overrideDefaultCompaction;
982
1003
  nativeExecutablePath;
983
1004
  enqueueRemember;
1005
+ runCompactInference;
984
1006
  compactSettings = DEFAULT_GOOSEDUMP_SETTINGS;
985
1007
  piAutoCompactionEnabled = DEFAULT_COMPACTION_SETTINGS2.enabled;
986
1008
  piCompactionReserveTokens = DEFAULT_COMPACTION_SETTINGS2.reserveTokens;
@@ -999,6 +1021,7 @@ class CompactionRuntime {
999
1021
  this.overrideDefaultCompaction = deps.overrideDefaultCompaction;
1000
1022
  this.nativeExecutablePath = deps.nativeExecutablePath;
1001
1023
  this.enqueueRemember = deps.enqueueRemember;
1024
+ this.runCompactInference = deps.runCompactInference;
1002
1025
  }
1003
1026
  setActivePi(pi) {
1004
1027
  this.activePi = pi;
@@ -1209,7 +1232,7 @@ class CompactionRuntime {
1209
1232
  });
1210
1233
  }
1211
1234
  }
1212
- 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));
1213
1236
  if (!this.runningOperationIsCurrent(operation, generation, sessionFile)) {
1214
1237
  return { cancel: true };
1215
1238
  }
@@ -1446,36 +1469,124 @@ class CompactionRuntime {
1446
1469
  }
1447
1470
  }
1448
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
+
1449
1578
  // src/memory-activity.ts
1450
- import { truncateToWidth } from "@earendil-works/pi-tui";
1451
- var MEMORY_ACTIVITY_WIDGET_KEY = "goosedump-memory-activity";
1452
- var ACTIVITY_FRAMES = ["▰▱▱▱", "▱▰▱▱", "▱▱▰▱", "▱▱▱▰", "▱▱▰▱", "▱▰▱▱"];
1579
+ var MEMORY_ACTIVITY_STATUS_KEY = "goosedump-memory-activity";
1453
1580
  function showMemoryActivity(ctx) {
1454
1581
  if (!ctx.hasUI)
1455
1582
  return;
1456
- ctx.ui.setWidget(MEMORY_ACTIVITY_WIDGET_KEY, (tui, theme) => {
1457
- let frame = 0;
1458
- const timer = setInterval(() => {
1459
- frame = (frame + 1) % ACTIVITY_FRAMES.length;
1460
- tui.requestRender();
1461
- }, 140);
1462
- timer.unref();
1463
- return {
1464
- render(width) {
1465
- const bar = theme.fg("accent", ACTIVITY_FRAMES[frame] ?? ACTIVITY_FRAMES[0]);
1466
- const label = theme.fg("muted", "Remembering...");
1467
- return [truncateToWidth(`${bar} ${label}`, width, "")];
1468
- },
1469
- invalidate() {},
1470
- dispose() {
1471
- clearInterval(timer);
1472
- }
1473
- };
1474
- });
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}`);
1475
1586
  }
1476
1587
  function hideMemoryActivity(ctx) {
1477
1588
  if (ctx.hasUI)
1478
- ctx.ui.setWidget(MEMORY_ACTIVITY_WIDGET_KEY, undefined);
1589
+ ctx.ui.setStatus(MEMORY_ACTIVITY_STATUS_KEY, undefined);
1479
1590
  }
1480
1591
 
1481
1592
  // src/policy.ts
@@ -1635,7 +1746,7 @@ import {
1635
1746
  defineTool,
1636
1747
  truncateHead
1637
1748
  } from "@earendil-works/pi-coding-agent";
1638
- import { Type as Type3 } from "@sinclair/typebox";
1749
+ import { Type as Type3 } from "typebox";
1639
1750
  function memoryDisabledResult() {
1640
1751
  return textResult(MEMORY_DISABLED_MESSAGE);
1641
1752
  }
@@ -1724,7 +1835,7 @@ function defineGooseTool(name, parameters, handler) {
1724
1835
  });
1725
1836
  }
1726
1837
  function registerGooseTools(pi, deps) {
1727
- const { nativeExecutablePath, enqueueRemember, requestCompaction } = deps;
1838
+ const { nativeExecutablePath, enqueueRemember, requestCompaction, runNonCompactInference } = deps;
1728
1839
  pi.registerTool(defineGooseTool("goose_remember", Type3.Object({ ...historyTargetParams() }), async (params, _signal, ctx) => {
1729
1840
  const disabled = requireMemoryEnabled(ctx);
1730
1841
  if (disabled)
@@ -1732,12 +1843,12 @@ function registerGooseTools(pi, deps) {
1732
1843
  const resolved = resolveHistoryTarget(ctx, "goose_remember", params);
1733
1844
  if (!resolved.ok)
1734
1845
  return resolved.result;
1735
- enqueueRemember(resolved.target, ctx, (message) => {
1846
+ const accepted = enqueueRemember(resolved.target, ctx, (message) => {
1736
1847
  if (ctx.hasUI) {
1737
1848
  ctx.ui.notify(`goosedump memory learning failed: ${message}`, "warning");
1738
1849
  }
1739
1850
  });
1740
- 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.`);
1741
1852
  }));
1742
1853
  pi.registerTool(defineGooseTool("goose_recall", Type3.Object({
1743
1854
  query: Type3.String({ description: PARAM_DESCRIPTIONS.memoryQuery }),
@@ -1760,12 +1871,16 @@ function registerGooseTools(pi, deps) {
1760
1871
  const disabled = requireMemoryEnabled(ctx);
1761
1872
  if (disabled)
1762
1873
  return disabled;
1763
- const { hits, warning } = await goosedumpRecall(params.query, {
1874
+ const inference = runNonCompactInference((inferenceSignal) => goosedumpRecall(params.query, {
1764
1875
  path: params.allProjects ? undefined : ctx.cwd,
1765
1876
  type: params.type,
1766
1877
  limit: params.limit,
1767
1878
  history: params.history
1768
- }, 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;
1769
1884
  if (warning && ctx.hasUI)
1770
1885
  ctx.ui.notify(warning, "warning");
1771
1886
  if (hits.length === 0)
@@ -1912,7 +2027,7 @@ import {
1912
2027
  Key,
1913
2028
  decodeKittyPrintable,
1914
2029
  matchesKey,
1915
- truncateToWidth as truncateToWidth2,
2030
+ truncateToWidth,
1916
2031
  visibleWidth,
1917
2032
  wrapTextWithAnsi
1918
2033
  } from "@earendil-works/pi-tui";
@@ -1995,7 +2110,7 @@ class NumberInputSubmenu {
1995
2110
  }
1996
2111
  }
1997
2112
  function makeRow(content, innerW, border) {
1998
- const line = truncateToWidth2(content, innerW);
2113
+ const line = truncateToWidth(content, innerW);
1999
2114
  const pad = Math.max(0, innerW - visibleWidth(line));
2000
2115
  return `${border} ${line}${" ".repeat(pad)} ${border}`;
2001
2116
  }
@@ -2147,7 +2262,7 @@ async function runSettingsEditor(ctx) {
2147
2262
  const { accent, bold, dim, muted, text } = colors;
2148
2263
  const innerW = overlayInnerWidth(width);
2149
2264
  const pad = (value, cellWidth) => {
2150
- const clipped = truncateToWidth2(value, cellWidth);
2265
+ const clipped = truncateToWidth(value, cellWidth);
2151
2266
  return `${clipped}${" ".repeat(Math.max(0, cellWidth - visibleWidth(clipped)))}`;
2152
2267
  };
2153
2268
  const tabLabel = (label, active) => active ? accent(bold(`[${label}]`)) : muted(label);
@@ -2158,7 +2273,7 @@ async function runSettingsEditor(ctx) {
2158
2273
  } else if (tab === "help") {
2159
2274
  const shortcutWidth = Math.max(visibleWidth("Shortcut"), ...helpRows.map(([shortcut]) => visibleWidth(shortcut)));
2160
2275
  const descriptionWidth = Math.max(1, innerW - shortcutWidth - 4);
2161
- 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))}`));
2162
2277
  } else {
2163
2278
  for (const [index, entry] of specs.entries()) {
2164
2279
  const selected = index === selectedSetting;
@@ -2299,7 +2414,7 @@ async function runSearchPanel(ctx, query, executablePath) {
2299
2414
  }
2300
2415
  return {
2301
2416
  render(width) {
2302
- const header = overlayHeader(colors, width, `Goosedump search: ${truncateToWidth2(query, 44)}`);
2417
+ const header = overlayHeader(colors, width, `Goosedump search: ${truncateToWidth(query, 44)}`);
2303
2418
  if (error) {
2304
2419
  return [header, ...overlayError(colors, width, error), overlayFooter(colors, width)];
2305
2420
  }
@@ -2657,50 +2772,35 @@ function createGoosedumpIntegration(options = {}) {
2657
2772
  const shouldRegisterTool = options.registerTool ?? true;
2658
2773
  const shouldOverrideDefaultCompaction = options.overrideDefaultCompaction ?? true;
2659
2774
  const nativeExecutablePath = options.nativeExecutablePath ?? resolveNativeGoosedumpBinary();
2660
- const rememberQueue = [];
2661
- let rememberRunning = null;
2775
+ const inference = new InferenceCoordinator;
2662
2776
  function enqueueRemember(target, ctx, reportError) {
2663
2777
  if (!isMemoryEnabled(ctx.cwd, projectSettingsTrusted(ctx)))
2664
- return;
2665
- for (let i = rememberQueue.length - 1;i >= 0; i -= 1) {
2666
- if (rememberQueue[i]?.target === target)
2667
- rememberQueue.splice(i, 1);
2668
- }
2669
- rememberQueue.push({ target, ctx, reportError });
2670
- ensureRememberWorker();
2671
- }
2672
- function ensureRememberWorker() {
2673
- if (rememberRunning)
2674
- return;
2675
- rememberRunning = (async () => {
2676
- while (rememberQueue.length > 0) {
2677
- const job = rememberQueue.shift();
2678
- if (!job)
2679
- continue;
2680
- showMemoryActivity(job.ctx);
2681
- try {
2682
- await goosedumpMemoryRemember(job.target, undefined, nativeExecutablePath);
2683
- } catch (err) {
2684
- const message = err instanceof Error ? err.message : String(err);
2685
- job.reportError(message);
2686
- } finally {
2687
- hideMemoryActivity(job.ctx);
2688
- }
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);
2689
2786
  }
2690
- })().finally(() => {
2691
- rememberRunning = null;
2692
- if (rememberQueue.length > 0)
2693
- ensureRememberWorker();
2694
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;
2695
2795
  }
2696
2796
  async function flushRememberQueue() {
2697
- while (rememberRunning)
2698
- await rememberRunning;
2797
+ await inference.waitForIdle();
2699
2798
  }
2700
2799
  const compaction = new CompactionRuntime({
2701
2800
  overrideDefaultCompaction: shouldOverrideDefaultCompaction,
2702
2801
  nativeExecutablePath,
2703
- enqueueRemember
2802
+ enqueueRemember,
2803
+ runCompactInference: (operation) => inference.runCompact(operation)
2704
2804
  });
2705
2805
  function register(pi) {
2706
2806
  compaction.setActivePi(pi);
@@ -2708,6 +2808,7 @@ function createGoosedumpIntegration(options = {}) {
2708
2808
  registerGooseTools(pi, {
2709
2809
  nativeExecutablePath,
2710
2810
  enqueueRemember,
2811
+ runNonCompactInference: (operation, signal) => inference.tryRunNonCompact(operation, signal),
2711
2812
  requestCompaction: (ctx, keep) => compaction.requestCompaction(ctx, keep)
2712
2813
  });
2713
2814
  }
@@ -2827,7 +2928,11 @@ function pi_goosedump_default(pi) {
2827
2928
  createGoosedumpIntegration().register(pi);
2828
2929
  }
2829
2930
  export {
2931
+ resolveNativeGoosedumpBinary,
2932
+ resolveGoosedumpBinaryOverride,
2933
+ resolveGoosedumpBinary,
2830
2934
  parsePiMessages,
2831
2935
  pi_goosedump_default as default,
2832
- createGoosedumpIntegration
2936
+ createGoosedumpIntegration,
2937
+ GOOSEDUMP_BINARY_ENV
2833
2938
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goosedump",
3
- "version": "0.12.54",
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.54",
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": {