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.
- package/README.md +27 -71
- package/dist/index.ts +208 -103
- package/package.json +14 -6
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# pi-goosedump
|
|
2
2
|
|
|
3
|
-
Pi extension for
|
|
4
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
+
`/goose-search`, `/goose-compact`, `/goose-sessions`, `/goose-forget`,
|
|
31
|
+
`/goose-memory-status`, and `/goose-settings`.
|
|
64
32
|
|
|
65
33
|
## Settings
|
|
66
34
|
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
+
## Development
|
|
87
51
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
##
|
|
58
|
+
## Licensing
|
|
103
59
|
|
|
104
|
-
Apache
|
|
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 "
|
|
6
|
-
import { Value as Value2 } from "
|
|
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 "
|
|
17
|
+
import { Value } from "typebox/value";
|
|
15
18
|
|
|
16
19
|
// src/types.ts
|
|
17
|
-
import { Type } from "
|
|
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 {
|
|
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
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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 "
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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.
|
|
1457
|
-
|
|
1458
|
-
|
|
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.
|
|
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 "
|
|
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
|
|
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
|
|
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
|
-
},
|
|
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
|
|
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 =
|
|
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 =
|
|
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(
|
|
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: ${
|
|
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
|
|
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
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
"@
|
|
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.
|
|
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.
|
|
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": {
|