pi-goosedump 0.12.55 → 0.12.57

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 +172 -82
  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>
@@ -136,6 +139,11 @@ var DEFAULT_GOOSEDUMP_SETTINGS = {
136
139
  summaryMaxTokens: 4096,
137
140
  enableMemory: false
138
141
  };
142
+ var GOOSEDUMP_SETTING_LABELS = {
143
+ turnsBeforeCompact: "Turns before compact",
144
+ summaryMaxTokens: "Summary max tokens",
145
+ enableMemory: "Enable memory"
146
+ };
139
147
  var GOOSEDUMP_SESSION_SCHEMA = Type.Object({
140
148
  id: Type.String(),
141
149
  parent: Type.Union([Type.String(), Type.Null()]),
@@ -529,8 +537,11 @@ async function goosedumpRecall(query, options = {}, signal, executablePath) {
529
537
  const output = await runGoosedumpAsync(args, signal, executablePath);
530
538
  return { hits: parseMemoryHits(output.stdout), warning: output.stderr.trim() };
531
539
  }
532
- async function goosedumpMemoryRemember(target, signal, executablePath) {
533
- const output = await runGoosedumpAsync(["memory", "learn", target, "--json"], signal, executablePath);
540
+ async function goosedumpMemoryRemember(target, signal, executablePath, leaseOwner) {
541
+ const args = ["memory", "learn", target, "--json"];
542
+ if (leaseOwner)
543
+ args.push("--lease-owner", leaseOwner);
544
+ const output = await runGoosedumpAsync(args, signal, executablePath);
534
545
  return output.stdout.trim();
535
546
  }
536
547
  async function goosedumpMemoryForget(target, signal, executablePath) {
@@ -620,7 +631,7 @@ import { findCutPoint } from "@earendil-works/pi-coding-agent";
620
631
 
621
632
  // src/settings.ts
622
633
  import { DEFAULT_COMPACTION_SETTINGS } from "@earendil-works/pi-coding-agent";
623
- import { Value as Value3 } from "@sinclair/typebox/value";
634
+ import { Value as Value3 } from "typebox/value";
624
635
  import { randomUUID } from "node:crypto";
625
636
  import {
626
637
  existsSync as existsSync2,
@@ -633,9 +644,9 @@ import {
633
644
  import { homedir } from "node:os";
634
645
  import { dirname as dirname2, join as join2 } from "node:path";
635
646
  function settingSpecFromSchema(schema) {
636
- if (schema.type === "boolean")
647
+ const { type, minimum, maximum } = schema;
648
+ if (type === "boolean")
637
649
  return { kind: "boolean" };
638
- const { minimum, maximum } = schema;
639
650
  if (typeof minimum !== "number" || typeof maximum !== "number") {
640
651
  throw new Error("goosedump setting schema is missing integer bounds");
641
652
  }
@@ -996,6 +1007,7 @@ class CompactionRuntime {
996
1007
  overrideDefaultCompaction;
997
1008
  nativeExecutablePath;
998
1009
  enqueueRemember;
1010
+ runCompactInference;
999
1011
  compactSettings = DEFAULT_GOOSEDUMP_SETTINGS;
1000
1012
  piAutoCompactionEnabled = DEFAULT_COMPACTION_SETTINGS2.enabled;
1001
1013
  piCompactionReserveTokens = DEFAULT_COMPACTION_SETTINGS2.reserveTokens;
@@ -1014,6 +1026,7 @@ class CompactionRuntime {
1014
1026
  this.overrideDefaultCompaction = deps.overrideDefaultCompaction;
1015
1027
  this.nativeExecutablePath = deps.nativeExecutablePath;
1016
1028
  this.enqueueRemember = deps.enqueueRemember;
1029
+ this.runCompactInference = deps.runCompactInference;
1017
1030
  }
1018
1031
  setActivePi(pi) {
1019
1032
  this.activePi = pi;
@@ -1224,7 +1237,7 @@ class CompactionRuntime {
1224
1237
  });
1225
1238
  }
1226
1239
  }
1227
- const compaction = await buildGoosedumpCompaction(target, event.preparation, event.branchEntries, effectiveSummaryMaxTokens(ctx.cwd, projectSettingsTrusted(ctx)), keep, signal, this.nativeExecutablePath);
1240
+ const compaction = await this.runCompactInference(() => buildGoosedumpCompaction(target, event.preparation, event.branchEntries, effectiveSummaryMaxTokens(ctx.cwd, projectSettingsTrusted(ctx)), keep, signal, this.nativeExecutablePath));
1228
1241
  if (!this.runningOperationIsCurrent(operation, generation, sessionFile)) {
1229
1242
  return { cancel: true };
1230
1243
  }
@@ -1461,36 +1474,124 @@ class CompactionRuntime {
1461
1474
  }
1462
1475
  }
1463
1476
 
1477
+ // src/inference-coordinator.ts
1478
+ class InferenceBusyError extends Error {
1479
+ constructor(message = "another inference task is already active") {
1480
+ super(message);
1481
+ this.name = "InferenceBusyError";
1482
+ }
1483
+ }
1484
+
1485
+ class InferenceCoordinator {
1486
+ activeAttempt = null;
1487
+ compactActive = false;
1488
+ idleWaiters = [];
1489
+ tryRunNonCompact(operation, externalSignal) {
1490
+ if (this.compactActive || this.activeAttempt)
1491
+ return null;
1492
+ let resolvePromise = () => {};
1493
+ let rejectPromise = () => {};
1494
+ const promise = new Promise((resolve, reject) => {
1495
+ resolvePromise = resolve;
1496
+ rejectPromise = reject;
1497
+ });
1498
+ const job = {
1499
+ operation,
1500
+ externalSignal,
1501
+ settled: false,
1502
+ resolve: (value) => {
1503
+ if (job.settled)
1504
+ return;
1505
+ job.settled = true;
1506
+ resolvePromise(value);
1507
+ },
1508
+ reject: (error) => {
1509
+ if (job.settled)
1510
+ return;
1511
+ job.settled = true;
1512
+ rejectPromise(error);
1513
+ }
1514
+ };
1515
+ this.startNonCompact(job);
1516
+ return promise;
1517
+ }
1518
+ async runCompact(operation) {
1519
+ if (this.compactActive)
1520
+ throw new InferenceBusyError("compaction inference is already active");
1521
+ this.compactActive = true;
1522
+ const interrupted = this.activeAttempt;
1523
+ if (interrupted) {
1524
+ interrupted.preempted = true;
1525
+ interrupted.controller.abort(new Error("inference paused for compaction"));
1526
+ await interrupted.completion;
1527
+ }
1528
+ try {
1529
+ return await operation();
1530
+ } finally {
1531
+ this.compactActive = false;
1532
+ if (interrupted && !interrupted.job.settled) {
1533
+ this.startNonCompact(interrupted.job);
1534
+ } else {
1535
+ this.resolveIdleWaiters();
1536
+ }
1537
+ }
1538
+ }
1539
+ async waitForIdle() {
1540
+ while (this.compactActive || this.activeAttempt) {
1541
+ await new Promise((resolve) => this.idleWaiters.push(resolve));
1542
+ }
1543
+ }
1544
+ startNonCompact(job) {
1545
+ const externalSignal = job.externalSignal;
1546
+ if (externalSignal?.aborted) {
1547
+ job.reject(externalSignal.reason ?? new Error("inference cancelled"));
1548
+ this.resolveIdleWaiters();
1549
+ return;
1550
+ }
1551
+ const controller = new AbortController;
1552
+ const forwardExternalAbort = () => controller.abort(externalSignal?.reason);
1553
+ externalSignal?.addEventListener("abort", forwardExternalAbort, { once: true });
1554
+ const attempt = {
1555
+ job,
1556
+ controller,
1557
+ preempted: false,
1558
+ completion: Promise.resolve()
1559
+ };
1560
+ this.activeAttempt = attempt;
1561
+ attempt.completion = Promise.resolve().then(() => job.operation(controller.signal)).then((value) => {
1562
+ job.resolve(value);
1563
+ }, (error) => {
1564
+ if (!attempt.preempted)
1565
+ job.reject(error);
1566
+ }).finally(() => {
1567
+ externalSignal?.removeEventListener("abort", forwardExternalAbort);
1568
+ if (this.activeAttempt === attempt)
1569
+ this.activeAttempt = null;
1570
+ this.resolveIdleWaiters();
1571
+ });
1572
+ }
1573
+ resolveIdleWaiters() {
1574
+ if (this.compactActive || this.activeAttempt)
1575
+ return;
1576
+ const waiters = this.idleWaiters;
1577
+ this.idleWaiters = [];
1578
+ for (const resolve of waiters)
1579
+ resolve();
1580
+ }
1581
+ }
1582
+
1464
1583
  // 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 = ["▰▱▱▱", "▱▰▱▱", "▱▱▰▱", "▱▱▱▰", "▱▱▰▱", "▱▰▱▱"];
1584
+ var MEMORY_ACTIVITY_STATUS_KEY = "goosedump-memory-activity";
1468
1585
  function showMemoryActivity(ctx) {
1469
1586
  if (!ctx.hasUI)
1470
1587
  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
- });
1588
+ const dot = ctx.ui.theme.fg("accent", "●");
1589
+ const label = ctx.ui.theme.fg("muted", "remembering");
1590
+ ctx.ui.setStatus(MEMORY_ACTIVITY_STATUS_KEY, `${dot} ${label}`);
1490
1591
  }
1491
1592
  function hideMemoryActivity(ctx) {
1492
1593
  if (ctx.hasUI)
1493
- ctx.ui.setWidget(MEMORY_ACTIVITY_WIDGET_KEY, undefined);
1594
+ ctx.ui.setStatus(MEMORY_ACTIVITY_STATUS_KEY, undefined);
1494
1595
  }
1495
1596
 
1496
1597
  // src/policy.ts
@@ -1650,7 +1751,7 @@ import {
1650
1751
  defineTool,
1651
1752
  truncateHead
1652
1753
  } from "@earendil-works/pi-coding-agent";
1653
- import { Type as Type3 } from "@sinclair/typebox";
1754
+ import { Type as Type3 } from "typebox";
1654
1755
  function memoryDisabledResult() {
1655
1756
  return textResult(MEMORY_DISABLED_MESSAGE);
1656
1757
  }
@@ -1739,7 +1840,7 @@ function defineGooseTool(name, parameters, handler) {
1739
1840
  });
1740
1841
  }
1741
1842
  function registerGooseTools(pi, deps) {
1742
- const { nativeExecutablePath, enqueueRemember, requestCompaction } = deps;
1843
+ const { nativeExecutablePath, enqueueRemember, requestCompaction, runNonCompactInference } = deps;
1743
1844
  pi.registerTool(defineGooseTool("goose_remember", Type3.Object({ ...historyTargetParams() }), async (params, _signal, ctx) => {
1744
1845
  const disabled = requireMemoryEnabled(ctx);
1745
1846
  if (disabled)
@@ -1747,12 +1848,12 @@ function registerGooseTools(pi, deps) {
1747
1848
  const resolved = resolveHistoryTarget(ctx, "goose_remember", params);
1748
1849
  if (!resolved.ok)
1749
1850
  return resolved.result;
1750
- enqueueRemember(resolved.target, ctx, (message) => {
1851
+ const accepted = enqueueRemember(resolved.target, ctx, (message) => {
1751
1852
  if (ctx.hasUI) {
1752
1853
  ctx.ui.notify(`goosedump memory learning failed: ${message}`, "warning");
1753
1854
  }
1754
1855
  });
1755
- return textResult(`Memory learning queued for ${resolved.target}.`);
1856
+ return textResult(accepted ? `Memory learning started for ${resolved.target}.` : `Memory learning dropped for ${resolved.target}: another inference task is active.`);
1756
1857
  }));
1757
1858
  pi.registerTool(defineGooseTool("goose_recall", Type3.Object({
1758
1859
  query: Type3.String({ description: PARAM_DESCRIPTIONS.memoryQuery }),
@@ -1775,12 +1876,16 @@ function registerGooseTools(pi, deps) {
1775
1876
  const disabled = requireMemoryEnabled(ctx);
1776
1877
  if (disabled)
1777
1878
  return disabled;
1778
- const { hits, warning } = await goosedumpRecall(params.query, {
1879
+ const inference = runNonCompactInference((inferenceSignal) => goosedumpRecall(params.query, {
1779
1880
  path: params.allProjects ? undefined : ctx.cwd,
1780
1881
  type: params.type,
1781
1882
  limit: params.limit,
1782
1883
  history: params.history
1783
- }, signal, nativeExecutablePath);
1884
+ }, inferenceSignal, nativeExecutablePath), signal);
1885
+ if (!inference) {
1886
+ return textResult("Memory recall dropped: another inference task is active.");
1887
+ }
1888
+ const { hits, warning } = await inference;
1784
1889
  if (warning && ctx.hasUI)
1785
1890
  ctx.ui.notify(warning, "warning");
1786
1891
  if (hits.length === 0)
@@ -1927,7 +2032,7 @@ import {
1927
2032
  Key,
1928
2033
  decodeKittyPrintable,
1929
2034
  matchesKey,
1930
- truncateToWidth as truncateToWidth2,
2035
+ truncateToWidth,
1931
2036
  visibleWidth,
1932
2037
  wrapTextWithAnsi
1933
2038
  } from "@earendil-works/pi-tui";
@@ -2010,7 +2115,7 @@ class NumberInputSubmenu {
2010
2115
  }
2011
2116
  }
2012
2117
  function makeRow(content, innerW, border) {
2013
- const line = truncateToWidth2(content, innerW);
2118
+ const line = truncateToWidth(content, innerW);
2014
2119
  const pad = Math.max(0, innerW - visibleWidth(line));
2015
2120
  return `${border} ${line}${" ".repeat(pad)} ${border}`;
2016
2121
  }
@@ -2078,7 +2183,7 @@ async function runSettingsEditor(ctx) {
2078
2183
  let globalSettings = { ...DEFAULT_GOOSEDUMP_SETTINGS };
2079
2184
  let projectSettings = {};
2080
2185
  function settingLabel(key) {
2081
- return `Project ${key}`;
2186
+ return GOOSEDUMP_SETTING_LABELS[key];
2082
2187
  }
2083
2188
  function reloadSettings() {
2084
2189
  globalSettings = readScopedGoosedumpSettings(location, "global");
@@ -2162,7 +2267,7 @@ async function runSettingsEditor(ctx) {
2162
2267
  const { accent, bold, dim, muted, text } = colors;
2163
2268
  const innerW = overlayInnerWidth(width);
2164
2269
  const pad = (value, cellWidth) => {
2165
- const clipped = truncateToWidth2(value, cellWidth);
2270
+ const clipped = truncateToWidth(value, cellWidth);
2166
2271
  return `${clipped}${" ".repeat(Math.max(0, cellWidth - visibleWidth(clipped)))}`;
2167
2272
  };
2168
2273
  const tabLabel = (label, active) => active ? accent(bold(`[${label}]`)) : muted(label);
@@ -2173,8 +2278,9 @@ async function runSettingsEditor(ctx) {
2173
2278
  } else if (tab === "help") {
2174
2279
  const shortcutWidth = Math.max(visibleWidth("Shortcut"), ...helpRows.map(([shortcut]) => visibleWidth(shortcut)));
2175
2280
  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))}`));
2281
+ content.push(dim(` ${pad("Shortcut", shortcutWidth)} Description`), ...helpRows.map(([shortcut, description]) => ` ${accent(pad(shortcut, shortcutWidth))} ${text(truncateToWidth(description, descriptionWidth))}`));
2177
2282
  } else {
2283
+ const labelWidth = Math.max(...specs.map(({ key }) => visibleWidth(settingLabel(key))));
2178
2284
  for (const [index, entry] of specs.entries()) {
2179
2285
  const selected = index === selectedSetting;
2180
2286
  const inherited = projectSettings[entry.key] === undefined;
@@ -2182,10 +2288,8 @@ async function runSettingsEditor(ctx) {
2182
2288
  const valueText = `[ ${formatSettingDisplayValue(entry.key, value)} ]`;
2183
2289
  const renderedValue = inherited ? dim(valueText) : selected ? accent(valueText) : text(valueText);
2184
2290
  const cursor = selected ? accent("›") : " ";
2185
- content.push(`${cursor} ${renderedValue} ${text(entry.key)}`);
2291
+ content.push(`${cursor} ${text(pad(settingLabel(entry.key), labelWidth))} ${renderedValue}`);
2186
2292
  }
2187
- const effective = resolveGoosedumpSettings(location, projectTrusted);
2188
- content.push("", dim(`Effective: turns=${formatCompactSetting(effective.turnsBeforeCompact)} · summary=${effectiveSummaryMaxTokens(location, projectTrusted)} tokens · memory=${effective.enableMemory ? "on" : "off"}`));
2189
2293
  }
2190
2294
  return [
2191
2295
  overlayHeader(colors, width, "Goosedump Settings"),
@@ -2314,7 +2418,7 @@ async function runSearchPanel(ctx, query, executablePath) {
2314
2418
  }
2315
2419
  return {
2316
2420
  render(width) {
2317
- const header = overlayHeader(colors, width, `Goosedump search: ${truncateToWidth2(query, 44)}`);
2421
+ const header = overlayHeader(colors, width, `Goosedump search: ${truncateToWidth(query, 44)}`);
2318
2422
  if (error) {
2319
2423
  return [header, ...overlayError(colors, width, error), overlayFooter(colors, width)];
2320
2424
  }
@@ -2672,50 +2776,35 @@ function createGoosedumpIntegration(options = {}) {
2672
2776
  const shouldRegisterTool = options.registerTool ?? true;
2673
2777
  const shouldOverrideDefaultCompaction = options.overrideDefaultCompaction ?? true;
2674
2778
  const nativeExecutablePath = options.nativeExecutablePath ?? resolveNativeGoosedumpBinary();
2675
- const rememberQueue = [];
2676
- let rememberRunning = null;
2779
+ const inference = new InferenceCoordinator;
2677
2780
  function enqueueRemember(target, ctx, reportError) {
2678
2781
  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
- }
2782
+ return false;
2783
+ const leaseOwner = randomUUID2();
2784
+ const running = inference.tryRunNonCompact(async (signal) => {
2785
+ showMemoryActivity(ctx);
2786
+ try {
2787
+ await goosedumpMemoryRemember(target, signal, nativeExecutablePath, leaseOwner);
2788
+ } finally {
2789
+ hideMemoryActivity(ctx);
2704
2790
  }
2705
- })().finally(() => {
2706
- rememberRunning = null;
2707
- if (rememberQueue.length > 0)
2708
- ensureRememberWorker();
2709
2791
  });
2792
+ if (!running)
2793
+ return false;
2794
+ running.catch((err) => {
2795
+ const message = err instanceof Error ? err.message : String(err);
2796
+ reportError(message);
2797
+ });
2798
+ return true;
2710
2799
  }
2711
2800
  async function flushRememberQueue() {
2712
- while (rememberRunning)
2713
- await rememberRunning;
2801
+ await inference.waitForIdle();
2714
2802
  }
2715
2803
  const compaction = new CompactionRuntime({
2716
2804
  overrideDefaultCompaction: shouldOverrideDefaultCompaction,
2717
2805
  nativeExecutablePath,
2718
- enqueueRemember
2806
+ enqueueRemember,
2807
+ runCompactInference: (operation) => inference.runCompact(operation)
2719
2808
  });
2720
2809
  function register(pi) {
2721
2810
  compaction.setActivePi(pi);
@@ -2723,6 +2812,7 @@ function createGoosedumpIntegration(options = {}) {
2723
2812
  registerGooseTools(pi, {
2724
2813
  nativeExecutablePath,
2725
2814
  enqueueRemember,
2815
+ runNonCompactInference: (operation, signal) => inference.tryRunNonCompact(operation, signal),
2726
2816
  requestCompaction: (ctx, keep) => compaction.requestCompaction(ctx, keep)
2727
2817
  });
2728
2818
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goosedump",
3
- "version": "0.12.55",
3
+ "version": "0.12.57",
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.57"
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": {