pi-goosedump 0.12.13 → 0.12.15

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 +12 -8
  2. package/dist/index.ts +430 -223
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -23,8 +23,8 @@ report that they are unavailable.
23
23
  2. **Recall:** lexical and entity matches come first. Cosine-similar BGE vectors
24
24
  fill the remaining slots. There is no separate vector database.
25
25
 
26
- The extension queues remembering after its own successful compactions. Learning
27
- runs in the background and does not delay compaction.
26
+ Explicit `goose_remember` requests and learning after successful compactions are
27
+ queued in the background, so model extraction and embedding do not block Pi.
28
28
 
29
29
  ## Tools
30
30
 
@@ -32,8 +32,8 @@ runs in the background and does not delay compaction.
32
32
  entry IDs.
33
33
  - **`goose_get`** expands entry IDs returned by `goose_search` or `goose_grep`.
34
34
  - **`goose_grep`** filters session messages with a glob pattern.
35
- - **`goose_remember`** learns durable memory from a session. It defaults to the
36
- current Pi session.
35
+ - **`goose_remember`** queues durable memory learning from a session. It defaults
36
+ to the current Pi session and returns before extraction and embedding finish.
37
37
  - **`goose_recall`** recalls typed memories with source provenance. It searches
38
38
  active memories in the current project by default; `history` includes
39
39
  superseded statements and `allProjects` searches every project.
@@ -97,10 +97,12 @@ compaction. Each completed model response counts as one turn, including response
97
97
  that call tools. The countdown is restored when a session resumes and appears in
98
98
  the footer with a status dot (green when >25% turns remain, yellow when ≤25%, red
99
99
  when ≤12.5%) followed by a hexadecimal countdown. `● GD:0A` means ten turns
100
- remain, `● GD:00` is ready to compact, and `● GD:--` is disabled. If zero is
101
- reached during a tool loop, the extension finishes the current tool batch, stops
102
- the continuation, compacts after Pi settles, and resumes automatically. A failed
103
- compaction leaves the task paused and the counter armed at zero.
100
+ remain, `● GD:00` is ready to compact, and `● GD:--` is disabled. Pi remains the
101
+ sole scheduler for its normal context threshold. Whichever limit is reached first
102
+ compacts once, and every successful compaction resets the countdown. A turn or
103
+ tool request waits until the agent run is fully settled. A failed or cancelled
104
+ compaction leaves the counter armed at zero and is not retried in the same
105
+ settlement.
104
106
 
105
107
  `summaryMaxTokens` is an integer from 512 to 65536 (default 4096). The effective
106
108
  summary budget is the lower of this value and 80% of Pi's `compaction.reserveTokens`,
@@ -112,6 +114,8 @@ global setting.
112
114
  The extension uses `goosedump session compact` for manual, scheduled, and automatic
113
115
  compaction. It preserves the previous summary, summarizes only the new range, and
114
116
  passes the effective token budget. `keep:N` leaves the last N user turns verbatim.
117
+ Tool requests are queued until Pi is idle; if Pi reaches its context threshold
118
+ first, the queued request is coalesced into that compaction.
115
119
 
116
120
  After success, memory learning is queued in the background. A learning failure
117
121
  keeps the summary and displays a warning. Custom instructions use Pi's normal
package/dist/index.ts CHANGED
@@ -27,9 +27,7 @@ import { dirname, join } from "node:path";
27
27
  import {
28
28
  Input,
29
29
  Key,
30
- SettingsList,
31
30
  decodeKittyPrintable,
32
- getKeybindings,
33
31
  matchesKey,
34
32
  truncateToWidth,
35
33
  visibleWidth,
@@ -66,9 +64,6 @@ function isTurnsBeforeCompact(value) {
66
64
  function isSummaryMaxTokens(value) {
67
65
  return typeof value === "number" && Number.isSafeInteger(value) && value >= SUMMARY_MAX_TOKENS_MIN && value <= SUMMARY_MAX_TOKENS_MAX;
68
66
  }
69
- function isGoosedumpSettingValue(key, value) {
70
- return key === "turnsBeforeCompact" ? isTurnsBeforeCompact(value) : isSummaryMaxTokens(value);
71
- }
72
67
  function piAgentDir() {
73
68
  return process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
74
69
  }
@@ -81,6 +76,10 @@ function projectSettingsPath(cwd) {
81
76
  function isRecord(value) {
82
77
  return typeof value === "object" && value !== null && !Array.isArray(value);
83
78
  }
79
+ function projectSettingsTrusted(ctx) {
80
+ const trustContext = ctx;
81
+ return trustContext.isProjectTrusted?.() ?? true;
82
+ }
84
83
  function readJsonObject(path) {
85
84
  if (!existsSync(path))
86
85
  return {};
@@ -148,15 +147,19 @@ function validateGoosedumpSettings(path) {
148
147
  throw new Error(`${path}: goosedump.summaryMaxTokens must be an integer from ` + `${SUMMARY_MAX_TOKENS_MIN} to ${SUMMARY_MAX_TOKENS_MAX}`);
149
148
  }
150
149
  }
151
- function resolveGoosedumpSettings(cwd) {
150
+ function resolveGoosedumpSettings(cwd, includeProject) {
152
151
  return {
153
152
  ...DEFAULT_GOOSEDUMP_SETTINGS,
154
153
  ...readGoosedumpSettingsFrom(globalSettingsPath()),
155
- ...readGoosedumpSettingsFrom(projectSettingsPath(cwd))
154
+ ...includeProject ? readGoosedumpSettingsFrom(projectSettingsPath(cwd)) : {}
156
155
  };
157
156
  }
158
157
  function notifyInvalidSettings(ctx) {
159
- for (const path of [globalSettingsPath(), projectSettingsPath(ctx.cwd)]) {
158
+ const paths = [
159
+ globalSettingsPath(),
160
+ ...projectSettingsTrusted(ctx) ? [projectSettingsPath(ctx.cwd)] : []
161
+ ];
162
+ for (const path of paths) {
160
163
  try {
161
164
  validateGoosedumpSettings(path);
162
165
  } catch (error) {
@@ -400,8 +403,9 @@ var GOOSEDUMP_UNAVAILABLE_MESSAGE = "goosedump is not available. Install with: n
400
403
  function goosedumpUnavailableResult() {
401
404
  return textResult(GOOSEDUMP_UNAVAILABLE_MESSAGE);
402
405
  }
403
- function compactStartedMessage(keep) {
404
- return keep !== null ? `Compacting with goosedump; keeping the last ${keep} user turn${keep === 1 ? "" : "s"}.` : "Compacting with goosedump up to Pi's token-based cut.";
406
+ function compactStartedMessage(keep, queued = false) {
407
+ const action = queued ? "Compaction queued with goosedump" : "Compacting with goosedump";
408
+ return keep !== null ? `${action}; keeping the last ${keep} user turn${keep === 1 ? "" : "s"}.` : `${action} up to Pi's token-based cut.`;
405
409
  }
406
410
  function runGoosedumpAsync(args, signal, executablePath = resolveNativeGoosedumpBinary()) {
407
411
  return new Promise((resolve, reject) => {
@@ -593,8 +597,9 @@ function isCompletedAssistant(message) {
593
597
  return message.role === "assistant" && message.stopReason !== "error" && message.stopReason !== "aborted";
594
598
  }
595
599
  var SUMMARIZABLE_ENTRY_TYPES = new Set(["message", "custom_message", "branch_summary"]);
596
- function piKeepRecentTokens(cwd) {
597
- for (const path of [projectSettingsPath(cwd), globalSettingsPath()]) {
600
+ function piKeepRecentTokens(cwd, includeProject) {
601
+ const paths = includeProject ? [projectSettingsPath(cwd), globalSettingsPath()] : [globalSettingsPath()];
602
+ for (const path of paths) {
598
603
  const compaction = readJsonObject(path).compaction;
599
604
  if (isRecord(compaction) && typeof compaction.keepRecentTokens === "number") {
600
605
  return compaction.keepRecentTokens;
@@ -602,8 +607,9 @@ function piKeepRecentTokens(cwd) {
602
607
  }
603
608
  return DEFAULT_COMPACTION_SETTINGS.keepRecentTokens;
604
609
  }
605
- function piReserveTokens(cwd) {
606
- for (const path of [projectSettingsPath(cwd), globalSettingsPath()]) {
610
+ function piReserveTokens(cwd, includeProject) {
611
+ const paths = includeProject ? [projectSettingsPath(cwd), globalSettingsPath()] : [globalSettingsPath()];
612
+ for (const path of paths) {
607
613
  const compaction = readJsonObject(path).compaction;
608
614
  if (isRecord(compaction) && typeof compaction.reserveTokens === "number") {
609
615
  return compaction.reserveTokens;
@@ -611,9 +617,9 @@ function piReserveTokens(cwd) {
611
617
  }
612
618
  return DEFAULT_COMPACTION_SETTINGS.reserveTokens;
613
619
  }
614
- function effectiveSummaryMaxTokens(cwd) {
615
- const configured = resolveGoosedumpSettings(cwd).summaryMaxTokens;
616
- const reserveTokens = piReserveTokens(cwd);
620
+ function effectiveSummaryMaxTokens(cwd, includeProject) {
621
+ const configured = resolveGoosedumpSettings(cwd, includeProject).summaryMaxTokens;
622
+ const reserveTokens = piReserveTokens(cwd, includeProject);
617
623
  if (reserveTokens <= 0)
618
624
  return configured;
619
625
  return Math.max(1, Math.min(configured, Math.floor(reserveTokens * 0.8)));
@@ -625,7 +631,7 @@ function hasCompactableHistory(ctx) {
625
631
  if (branch[branch.length - 1].type === "compaction")
626
632
  return false;
627
633
  const boundaryStart = activeHistoryStartIndex(branch);
628
- const cut = findCutPoint(branch, boundaryStart, branch.length, piKeepRecentTokens(ctx.cwd));
634
+ const cut = findCutPoint(branch, boundaryStart, branch.length, piKeepRecentTokens(ctx.cwd, projectSettingsTrusted(ctx)));
629
635
  if (!branch[cut.firstKeptEntryIndex]?.id)
630
636
  return false;
631
637
  const historyEnd = cut.isSplitTurn ? cut.turnStartIndex : cut.firstKeptEntryIndex;
@@ -782,8 +788,7 @@ class NumberInputSubmenu {
782
788
  this.hint = getSettingsListTheme().hint;
783
789
  }
784
790
  handleInput(data) {
785
- const kb = getKeybindings();
786
- if (kb.matches(data, "tui.select.confirm") || data === `
791
+ if (matchesKey(data, Key.enter) || matchesKey(data, Key.return) || data === `
787
792
  `) {
788
793
  const value = this.input.getValue();
789
794
  if (value === "") {
@@ -805,7 +810,7 @@ class NumberInputSubmenu {
805
810
  this.onSubmitValue(parsed);
806
811
  return;
807
812
  }
808
- if (kb.matches(data, "tui.select.cancel")) {
813
+ if (matchesKey(data, Key.escape)) {
809
814
  this.onCancelEdit();
810
815
  return;
811
816
  }
@@ -836,109 +841,190 @@ function makeRow(content, innerW, border) {
836
841
  async function runSettingsEditor(ctx) {
837
842
  const OVERLAY_WIDTH = 84;
838
843
  const location = ctx.cwd;
844
+ const projectTrusted = projectSettingsTrusted(ctx);
845
+ const specs = [
846
+ {
847
+ key: "turnsBeforeCompact",
848
+ description: "LLM turns before compaction; 0 disables.",
849
+ min: TURNS_BEFORE_COMPACT_MIN,
850
+ max: TURNS_BEFORE_COMPACT_MAX
851
+ },
852
+ {
853
+ key: "summaryMaxTokens",
854
+ description: "Maximum estimated tokens in each summary.",
855
+ min: SUMMARY_MAX_TOKENS_MIN,
856
+ max: SUMMARY_MAX_TOKENS_MAX
857
+ }
858
+ ];
859
+ const helpRows = [
860
+ ["Tab", "Next tab"],
861
+ ["Shift+Tab / S", "Switch scope in Settings"],
862
+ ["↑ / ↓", "Select setting"],
863
+ ["Enter", "Edit setting or submit value"],
864
+ ["Esc", "Cancel edit or close"],
865
+ ["Ctrl+C", "Close"],
866
+ ["Empty + Enter", "Use the Global value in Project scope"]
867
+ ];
839
868
  await ctx.ui.custom((tui, theme, _kb, done) => {
840
- let settingsList;
841
- function settingLabel(scope, key) {
842
- const scopeLabel = scope === "global" ? "Global" : "Project";
869
+ let tab = "settings";
870
+ let scope = projectTrusted ? "project" : "global";
871
+ let selectedSetting = 0;
872
+ let editor;
873
+ let globalSettings = { ...DEFAULT_GOOSEDUMP_SETTINGS };
874
+ let projectSettings = {};
875
+ function settingLabel(editScope, key) {
876
+ const scopeLabel = editScope === "global" ? "Global" : "Project";
843
877
  return `${scopeLabel} ${key}`;
844
878
  }
845
- function buildSettingItems() {
846
- const global = readScopedGoosedumpSettings(location, "global");
847
- const projectPath = projectSettingsPath(location);
848
- const projectRaw = readJsonObjectStrict(projectPath).goosedump;
849
- const project = isRecord(projectRaw) ? projectRaw : {};
850
- const specs = [
851
- {
852
- key: "turnsBeforeCompact",
853
- description: "LLM turns before compaction; 0 disables.",
854
- min: TURNS_BEFORE_COMPACT_MIN,
855
- max: TURNS_BEFORE_COMPACT_MAX
856
- },
857
- {
858
- key: "summaryMaxTokens",
859
- description: "Maximum estimated tokens in each summary.",
860
- min: SUMMARY_MAX_TOKENS_MIN,
861
- max: SUMMARY_MAX_TOKENS_MAX
862
- }
863
- ];
864
- return ["global", "project"].flatMap((scope) => specs.map(({ key, description, min, max }) => {
865
- const projectValue = isGoosedumpSettingValue(key, project[key]) ? project[key] : undefined;
866
- return {
867
- id: `${scope}:${key}`,
868
- label: settingLabel(scope, key),
869
- description: scope === "project" ? `${description} Empty inherits Global.` : description,
870
- currentValue: String(scope === "project" ? projectValue ?? "" : global[key]),
871
- submenu: (currentValue, subDone) => new NumberInputSubmenu(settingLabel(scope, key), currentValue === "" ? null : Number(currentValue), (value) => subDone(String(value)), () => subDone(), {
872
- min,
873
- max,
874
- onClear: scope === "project" ? () => subDone("") : undefined,
875
- onInvalid: (message) => ctx.ui.notify(message, "error")
876
- })
877
- };
878
- }));
879
+ function reloadSettings() {
880
+ globalSettings = readScopedGoosedumpSettings(location, "global");
881
+ projectSettings = {};
882
+ if (!projectTrusted)
883
+ return;
884
+ const projectRaw = readJsonObjectStrict(projectSettingsPath(location)).goosedump;
885
+ if (!isRecord(projectRaw))
886
+ return;
887
+ if (isTurnsBeforeCompact(projectRaw.turnsBeforeCompact)) {
888
+ projectSettings.turnsBeforeCompact = projectRaw.turnsBeforeCompact;
889
+ }
890
+ if (isSummaryMaxTokens(projectRaw.summaryMaxTokens)) {
891
+ projectSettings.summaryMaxTokens = projectRaw.summaryMaxTokens;
892
+ }
879
893
  }
880
- function rebuildSettingsList() {
881
- settingsList = new SettingsList(buildSettingItems(), 10, getSettingsListTheme(), (id, newValue) => {
882
- const [scope, key] = id.split(":");
883
- if (scope === "project" && newValue === "") {
884
- try {
885
- clearScopedGoosedumpSetting(location, scope, key);
886
- settingsList.updateValue(id, "");
887
- ctx.ui.notify(`${settingLabel(scope, key)} inherits Global`, "info");
888
- tui.requestRender();
889
- } catch (error) {
890
- const message = error instanceof Error ? error.message : String(error);
891
- ctx.ui.notify(`goosedump settings error: ${message}`, "error");
892
- }
893
- return;
894
- }
895
- const parsed = Number(newValue);
896
- if (!isGoosedumpSettingValue(key, parsed))
897
- return;
894
+ function finishEdit() {
895
+ editor = undefined;
896
+ reloadSettings();
897
+ tui.requestRender();
898
+ }
899
+ function reportSettingsError(error) {
900
+ const message = error instanceof Error ? error.message : String(error);
901
+ ctx.ui.notify(`goosedump settings error: ${message}`, "error");
902
+ }
903
+ function openEditor() {
904
+ const spec = specs[selectedSetting];
905
+ if (!spec)
906
+ return;
907
+ const editScope = scope;
908
+ const currentValue = editScope === "global" ? globalSettings[spec.key] : projectSettings[spec.key] ?? null;
909
+ editor = new NumberInputSubmenu(settingLabel(editScope, spec.key), currentValue, (value) => {
898
910
  try {
899
- writeScopedGoosedumpSetting(location, scope, key, parsed);
900
- settingsList.updateValue(id, newValue);
901
- ctx.ui.notify(`${settingLabel(scope, key)} = ${key === "turnsBeforeCompact" ? formatCompactSetting(parsed) : parsed}`, "info");
902
- tui.requestRender();
911
+ writeScopedGoosedumpSetting(location, editScope, spec.key, value);
912
+ ctx.ui.notify(`${settingLabel(editScope, spec.key)} = ${spec.key === "turnsBeforeCompact" ? formatCompactSetting(value) : value}`, "info");
913
+ finishEdit();
903
914
  } catch (error) {
904
- const message = error instanceof Error ? error.message : String(error);
905
- ctx.ui.notify(`goosedump settings error: ${message}`, "error");
915
+ reportSettingsError(error);
906
916
  }
907
- tui.requestRender();
908
917
  }, () => {
909
- done(undefined);
918
+ editor = undefined;
919
+ tui.requestRender();
920
+ }, {
921
+ min: spec.min,
922
+ max: spec.max,
923
+ onClear: editScope === "project" ? () => {
924
+ try {
925
+ clearScopedGoosedumpSetting(location, editScope, spec.key);
926
+ ctx.ui.notify(`${settingLabel(editScope, spec.key)} inherits Global`, "info");
927
+ finishEdit();
928
+ } catch (error) {
929
+ reportSettingsError(error);
930
+ }
931
+ } : undefined,
932
+ onInvalid: (message) => ctx.ui.notify(message, "error")
910
933
  });
911
934
  }
912
- rebuildSettingsList();
935
+ reloadSettings();
913
936
  return {
914
937
  render(width) {
915
- const innerW = width - 4;
938
+ const innerW = Math.max(1, width - 4);
916
939
  const border = theme.fg("border", "│");
917
- const borderFg = (s) => theme.fg("border", s);
918
- const accent = (s) => theme.fg("accent", s);
919
- const dim = (s) => theme.fg("dim", s);
920
- const lines = [];
940
+ const borderFg = (text2) => theme.fg("border", text2);
941
+ const accent = (text2) => theme.fg("accent", text2);
942
+ const dim = (text2) => theme.fg("dim", text2);
943
+ const text = (value) => theme.fg("text", value);
944
+ const pad = (value, cellWidth) => {
945
+ const clipped = truncateToWidth(value, cellWidth);
946
+ return `${clipped}${" ".repeat(Math.max(0, cellWidth - visibleWidth(clipped)))}`;
947
+ };
948
+ const tabLabel = (label, active) => active ? accent(theme.bold(`[${label}]`)) : theme.fg("muted", label);
949
+ const tabs = `${tabLabel("Settings", tab === "settings")} ${tabLabel("Help", tab === "help")}${dim(` · Scope: ${scope === "project" ? "Project" : "Global"}`)}`;
950
+ const content = [tabs, ""];
951
+ if (editor) {
952
+ content.push(...editor.render(innerW));
953
+ } else if (tab === "help") {
954
+ const shortcutWidth = Math.max(visibleWidth("Shortcut"), ...helpRows.map(([shortcut]) => visibleWidth(shortcut)));
955
+ const descriptionWidth = Math.max(1, innerW - shortcutWidth - 4);
956
+ content.push(dim(` ${pad("Shortcut", shortcutWidth)} Description`), ...helpRows.map(([shortcut, description]) => ` ${accent(pad(shortcut, shortcutWidth))} ${text(truncateToWidth(description, descriptionWidth))}`));
957
+ } else {
958
+ for (const [index, spec] of specs.entries()) {
959
+ const selected = index === selectedSetting;
960
+ const inherited = scope === "project" && projectSettings[spec.key] === undefined;
961
+ const value = scope === "global" ? globalSettings[spec.key] : projectSettings[spec.key] ?? globalSettings[spec.key];
962
+ const valueText = `[ ${value} ]`;
963
+ const renderedValue = inherited ? dim(valueText) : selected ? accent(valueText) : text(valueText);
964
+ const cursor = selected ? accent("›") : " ";
965
+ content.push(`${cursor} ${renderedValue} ${text(spec.key)}`);
966
+ }
967
+ const selectedSpec = specs[selectedSetting];
968
+ if (selectedSpec) {
969
+ const inherited = scope === "project" && projectSettings[selectedSpec.key] === undefined;
970
+ content.push("", dim(inherited ? `${selectedSpec.description} Using the Global value.` : selectedSpec.description));
971
+ }
972
+ const effective = resolveGoosedumpSettings(location, projectTrusted);
973
+ content.push("", dim(`Effective: turns=${formatCompactSetting(effective.turnsBeforeCompact)} · summary=${effectiveSummaryMaxTokens(location, projectTrusted)} tokens`));
974
+ }
921
975
  const title = accent(" Goosedump Settings ");
922
976
  const topFill = borderFg("─".repeat(Math.max(0, width - 4 - visibleWidth(title))));
923
- lines.push(`${borderFg("╭─")}${title}${topFill}${borderFg("─╮")}`);
924
- for (const line of settingsList.render(innerW))
925
- lines.push(makeRow(line, innerW, border));
926
- lines.push(makeRow("", innerW, border));
927
- const effective = resolveGoosedumpSettings(location);
928
- lines.push(makeRow(dim(`Effective: turns=${formatCompactSetting(effective.turnsBeforeCompact)} · summary=${effectiveSummaryMaxTokens(location)} tokens`), innerW, border));
929
- lines.push(makeRow(dim("↑↓ select enter/space edit esc close"), innerW, border));
930
- lines.push(`${borderFg("╰")}${borderFg("─".repeat(width - 2))}${borderFg("╯")}`);
931
- return lines;
977
+ return [
978
+ `${borderFg("╭─")}${title}${topFill}${borderFg("─╮")}`,
979
+ ...content.map((line) => makeRow(line, innerW, border)),
980
+ `${borderFg("╰")}${borderFg("─".repeat(Math.max(0, width - 2)))}${borderFg("╯")}`
981
+ ];
932
982
  },
933
983
  handleInput(data) {
934
- if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
984
+ if (matchesKey(data, Key.ctrl("c"))) {
985
+ done(undefined);
986
+ return;
987
+ }
988
+ if (editor) {
989
+ editor.handleInput(data);
990
+ tui.requestRender();
991
+ return;
992
+ }
993
+ if (matchesKey(data, Key.tab)) {
994
+ tab = tab === "settings" ? "help" : "settings";
995
+ tui.requestRender();
996
+ return;
997
+ }
998
+ if (tab === "settings" && (matchesKey(data, Key.shift(Key.tab)) || data.toLowerCase() === "s")) {
999
+ if (!projectTrusted && scope === "global") {
1000
+ ctx.ui.notify("Project settings require a trusted project", "warning");
1001
+ return;
1002
+ }
1003
+ scope = scope === "project" ? "global" : "project";
1004
+ reloadSettings();
1005
+ tui.requestRender();
1006
+ return;
1007
+ }
1008
+ if (matchesKey(data, Key.escape)) {
935
1009
  done(undefined);
936
1010
  return;
937
1011
  }
938
- settingsList.handleInput(data);
1012
+ if (tab === "help")
1013
+ return;
1014
+ if (matchesKey(data, Key.up)) {
1015
+ selectedSetting = selectedSetting === 0 ? specs.length - 1 : Math.max(0, selectedSetting - 1);
1016
+ } else if (matchesKey(data, Key.down)) {
1017
+ selectedSetting = (selectedSetting + 1) % specs.length;
1018
+ } else if (matchesKey(data, Key.enter) || matchesKey(data, Key.return)) {
1019
+ openEditor();
1020
+ } else {
1021
+ return;
1022
+ }
939
1023
  tui.requestRender();
940
1024
  },
941
- invalidate() {}
1025
+ invalidate() {
1026
+ editor?.invalidate();
1027
+ }
942
1028
  };
943
1029
  }, {
944
1030
  overlay: true,
@@ -1287,19 +1373,16 @@ async function runSessionsPanel(ctx, executablePath) {
1287
1373
  ctx.ui.notify(`Only Pi sessions can be resumed (session ${s.id} is ${s.provider})`, "warning");
1288
1374
  return;
1289
1375
  }
1290
- const sessionDir = ctx.sessionManager.getSessionDir();
1291
- const sessionPath = join(sessionDir, s.id.endsWith(".jsonl") ? s.id : `${s.id}.jsonl`);
1376
+ const sessionPath = s.path;
1292
1377
  if (!existsSync(sessionPath)) {
1293
1378
  ctx.ui.notify(`Session transcript file not found: ${sessionPath}`, "error");
1294
1379
  return;
1295
1380
  }
1296
1381
  done(undefined);
1297
- const cmdCtx = ctx;
1298
- if (typeof cmdCtx.switchSession === "function") {
1299
- cmdCtx.switchSession(sessionPath);
1300
- } else {
1301
- ctx.ui.notify(`Session switched to ${sessionPath}`, "info");
1302
- }
1382
+ ctx.switchSession(sessionPath).catch((err) => {
1383
+ const message = err instanceof Error ? err.message : String(err);
1384
+ ctx.ui.notify(`Failed to switch session: ${message}`, "error");
1385
+ });
1303
1386
  return;
1304
1387
  }
1305
1388
  },
@@ -1322,10 +1405,12 @@ function createGoosedumpIntegration(options = {}) {
1322
1405
  let compactSettings = DEFAULT_GOOSEDUMP_SETTINGS;
1323
1406
  let turnsRemaining = 0;
1324
1407
  let counterTriggered = false;
1325
- let compactionQueued = false;
1326
- let compactionHookActive = false;
1327
- let pendingKeep = null;
1328
- let counterInterruptPending = false;
1408
+ let compactionState = { phase: "idle" };
1409
+ let sessionGeneration = 0;
1410
+ let settlementGeneration = 0;
1411
+ let operationGeneration = 0;
1412
+ let activeSessionFile = null;
1413
+ let sessionActive = false;
1329
1414
  const rememberQueue = [];
1330
1415
  let rememberRunning = null;
1331
1416
  function enqueueRemember(target, reportError) {
@@ -1361,13 +1446,11 @@ function createGoosedumpIntegration(options = {}) {
1361
1446
  while (rememberRunning)
1362
1447
  await rememberRunning;
1363
1448
  }
1364
- function syncCompactCounters(ctx, clearPending = false) {
1365
- compactSettings = resolveGoosedumpSettings(ctx.cwd);
1449
+ function syncCompactCounters(ctx) {
1450
+ compactSettings = resolveGoosedumpSettings(ctx.cwd, projectSettingsTrusted(ctx));
1366
1451
  const completedTurns = completedTurnsSinceCompaction(ctx.sessionManager.getBranch());
1367
1452
  turnsRemaining = Math.max(0, compactSettings.turnsBeforeCompact - completedTurns);
1368
1453
  counterTriggered = compactSettings.turnsBeforeCompact > 0 && completedTurns >= compactSettings.turnsBeforeCompact;
1369
- if (clearPending)
1370
- pendingKeep = null;
1371
1454
  }
1372
1455
  function updateStatus(ctx) {
1373
1456
  if (!goosedumpAvailable)
@@ -1392,18 +1475,7 @@ function createGoosedumpIntegration(options = {}) {
1392
1475
  return;
1393
1476
  syncCompactCounters(ctx);
1394
1477
  updateStatus(ctx);
1395
- }
1396
- function interruptActiveRunForCompaction(ctx, hasToolResults) {
1397
- if (!counterTriggered || !hasToolResults || ctx.isIdle())
1398
- return;
1399
- if (counterInterruptPending)
1400
- return;
1401
- if (compactionQueued || compactionHookActive || ctx.hasPendingMessages())
1402
- return;
1403
- if (!hasCompactableHistory(ctx))
1404
- return;
1405
- counterInterruptPending = true;
1406
- ctx.abort();
1478
+ queueTurnCompaction(ctx, settlementGeneration + 1);
1407
1479
  }
1408
1480
  function checkGoosedump(ctx) {
1409
1481
  try {
@@ -1416,34 +1488,100 @@ function createGoosedumpIntegration(options = {}) {
1416
1488
  return false;
1417
1489
  }
1418
1490
  }
1419
- function requestCompaction(ctx, keep) {
1420
- if (compactionQueued || compactionHookActive)
1421
- return "busy";
1422
- if (keep === null && !hasCompactableHistory(ctx))
1423
- return "nothing-to-compact";
1424
- compactionQueued = true;
1425
- pendingKeep = keep;
1491
+ function requestIsCurrent(request) {
1492
+ return request.sessionGeneration === sessionGeneration && request.sessionFile === activeSessionFile;
1493
+ }
1494
+ function contextIsCurrent(ctx) {
1495
+ return sessionActive && (ctx.sessionManager.getSessionFile?.() ?? null) === activeSessionFile;
1496
+ }
1497
+ function runningOperationIsCurrent(operation, generation, sessionFile) {
1498
+ return generation === sessionGeneration && sessionFile === activeSessionFile && compactionState.phase === "running" && compactionState.operation === operation;
1499
+ }
1500
+ function requestHasCompactableHistory(ctx, keep) {
1501
+ return keep === null ? hasCompactableHistory(ctx) : keepBoundaryId(ctx.sessionManager.getBranch(), keep) !== undefined;
1502
+ }
1503
+ function queueTurnCompaction(ctx, eligibleSettlement) {
1504
+ if (!counterTriggered || compactionState.phase !== "idle")
1505
+ return;
1506
+ if (!hasCompactableHistory(ctx))
1507
+ return;
1508
+ compactionState = {
1509
+ phase: "queued",
1510
+ request: {
1511
+ keep: null,
1512
+ origin: "turn",
1513
+ sessionGeneration,
1514
+ sessionFile: activeSessionFile,
1515
+ eligibleSettlement
1516
+ }
1517
+ };
1518
+ }
1519
+ function startQueuedCompaction(ctx) {
1520
+ if (compactionState.phase !== "queued")
1521
+ return false;
1522
+ const request = compactionState.request;
1523
+ if (!requestIsCurrent(request)) {
1524
+ compactionState = { phase: "idle" };
1525
+ return false;
1526
+ }
1527
+ if (!ctx.isIdle() || ctx.hasPendingMessages())
1528
+ return false;
1529
+ if (!requestHasCompactableHistory(ctx, request.keep)) {
1530
+ compactionState = { phase: "idle" };
1531
+ return false;
1532
+ }
1533
+ const operation = ++operationGeneration;
1534
+ const generation = request.sessionGeneration;
1535
+ const sessionFile = request.sessionFile;
1536
+ compactionState = {
1537
+ phase: "running",
1538
+ request,
1539
+ owner: "extension",
1540
+ operation,
1541
+ sessionGeneration: generation,
1542
+ sessionFile,
1543
+ hookActive: false
1544
+ };
1426
1545
  ctx.compact({
1427
1546
  onComplete: () => {
1428
- compactionQueued = false;
1429
- pendingKeep = null;
1547
+ if (!runningOperationIsCurrent(operation, generation, sessionFile))
1548
+ return;
1549
+ compactionState = { phase: "idle" };
1430
1550
  },
1431
1551
  onError: (error) => {
1432
- compactionQueued = false;
1433
- pendingKeep = null;
1552
+ if (!runningOperationIsCurrent(operation, generation, sessionFile))
1553
+ return;
1554
+ compactionState = { phase: "idle" };
1434
1555
  const message = error instanceof Error ? error.message : String(error);
1435
1556
  if (ctx.hasUI)
1436
1557
  ctx.ui.notify(`goosedump compaction failed: ${message}`, "error");
1437
1558
  }
1438
1559
  });
1439
- return "started";
1560
+ return true;
1561
+ }
1562
+ function requestCompaction(ctx, keep, origin) {
1563
+ if (compactionState.phase !== "idle")
1564
+ return "busy";
1565
+ if (!requestHasCompactableHistory(ctx, keep))
1566
+ return "nothing-to-compact";
1567
+ compactionState = {
1568
+ phase: "queued",
1569
+ request: {
1570
+ keep,
1571
+ origin,
1572
+ sessionGeneration,
1573
+ sessionFile: activeSessionFile,
1574
+ eligibleSettlement: settlementGeneration + 1
1575
+ }
1576
+ };
1577
+ return startQueuedCompaction(ctx) ? "started" : "queued";
1440
1578
  }
1441
1579
  function register(pi) {
1442
1580
  if (shouldRegisterTool) {
1443
1581
  pi.registerTool(defineTool({
1444
1582
  name: "goose_remember",
1445
1583
  label: "goose_remember",
1446
- description: "Use local gpt-oss-20b to extract typed memories and provenance from a session; store BGE embeddings in SQLite. Defaults to the current Pi session.",
1584
+ description: "Queue local gpt-oss-20b memory extraction and BGE embedding in the background. Defaults to the current Pi session.",
1447
1585
  promptSnippet: "Persist important context from the current or another session into durable memory",
1448
1586
  promptGuidelines: [
1449
1587
  "Use goose_remember after a milestone when important facts, decisions, preferences, procedures, or lessons should be retained.",
@@ -1453,20 +1591,19 @@ function createGoosedumpIntegration(options = {}) {
1453
1591
  provider: historyProviderParam("read"),
1454
1592
  session: historySessionParam()
1455
1593
  }),
1456
- async execute(_id, params, signal, _onUpdate, ctx) {
1594
+ async execute(_id, params, _signal, _onUpdate, ctx) {
1457
1595
  if (!goosedumpAvailable)
1458
1596
  return goosedumpUnavailableResult();
1459
- try {
1460
- const provider = params.provider ?? "pi";
1461
- const target = resolveTarget(ctx, provider, params.session);
1462
- if (!target)
1463
- return missingSessionId("goose_remember", provider);
1464
- const report = await goosedumpMemoryRemember(target, signal, nativeExecutablePath);
1465
- return textResult(report);
1466
- } catch (err) {
1467
- const message = err instanceof Error ? err.message : String(err);
1468
- return textResult(`goosedump error: ${message}`);
1469
- }
1597
+ const provider = params.provider ?? "pi";
1598
+ const target = resolveTarget(ctx, provider, params.session);
1599
+ if (!target)
1600
+ return missingSessionId("goose_remember", provider);
1601
+ enqueueRemember(target, (message) => {
1602
+ if (ctx.hasUI) {
1603
+ ctx.ui.notify(`goosedump memory learning failed: ${message}`, "warning");
1604
+ }
1605
+ });
1606
+ return textResult(`Memory learning queued for ${target}.`);
1470
1607
  }
1471
1608
  }));
1472
1609
  pi.registerTool(defineTool({
@@ -1711,13 +1848,13 @@ function createGoosedumpIntegration(options = {}) {
1711
1848
  if (!goosedumpAvailable)
1712
1849
  return goosedumpUnavailableResult();
1713
1850
  const keep = params.keep ?? null;
1714
- const result = requestCompaction(ctx, keep);
1851
+ const result = requestCompaction(ctx, keep, "tool");
1715
1852
  if (result === "busy")
1716
1853
  return textResult("Compaction already in progress.");
1717
1854
  if (result === "nothing-to-compact") {
1718
1855
  return textResult("Nothing to compact (session too small).");
1719
1856
  }
1720
- return textResult(compactStartedMessage(keep));
1857
+ return textResult(compactStartedMessage(keep, result === "queued"));
1721
1858
  }
1722
1859
  }));
1723
1860
  pi.registerTool(defineTool({
@@ -1787,112 +1924,182 @@ ${lines.join(`
1787
1924
  }
1788
1925
  }));
1789
1926
  }
1790
- function maybeAutoCompact(ctx) {
1927
+ function handleAgentSettled(ctx) {
1791
1928
  if (!goosedumpAvailable || !shouldOverrideDefaultCompaction)
1792
1929
  return;
1793
- if (!counterTriggered || compactionQueued)
1794
- return;
1795
- if (ctx.hasPendingMessages())
1796
- return;
1797
- if (!ctx.isIdle())
1930
+ settlementGeneration += 1;
1931
+ if (compactionState.phase === "running" && compactionState.owner === "pi") {
1932
+ const request = compactionState.request;
1933
+ compactionState = request && requestIsCurrent(request) ? {
1934
+ phase: "queued",
1935
+ request: { ...request, eligibleSettlement: settlementGeneration + 1 }
1936
+ } : { phase: "idle" };
1798
1937
  return;
1799
- if (!hasCompactableHistory(ctx))
1800
- return;
1801
- const resumeAfter = counterInterruptPending;
1802
- counterInterruptPending = false;
1803
- syncCompactCounters(ctx, true);
1804
- updateStatus(ctx);
1805
- compactionQueued = true;
1806
- ctx.compact({
1807
- onComplete: () => {
1808
- compactionQueued = false;
1809
- syncCompactCounters(ctx, true);
1810
- updateStatus(ctx);
1811
- resumeInterruptedRun(ctx, resumeAfter);
1812
- },
1813
- onError: (error) => {
1814
- compactionQueued = false;
1815
- syncCompactCounters(ctx, true);
1816
- updateStatus(ctx);
1817
- const message = error instanceof Error ? error.message : String(error);
1818
- if (ctx.hasUI)
1819
- ctx.ui.notify(`goosedump compaction failed: ${message}`, "error");
1820
- }
1821
- });
1822
- }
1823
- function resumeInterruptedRun(ctx, resume) {
1824
- if (!resume || !ctx.isIdle() || ctx.hasPendingMessages())
1825
- return;
1826
- pi.sendMessage({
1827
- customType: "goosedump-autocompact-resume",
1828
- content: "The session was compacted to reduce context size. Continue the task you were working on from where you left off.",
1829
- display: false
1830
- }, { triggerTurn: true });
1938
+ }
1939
+ if (compactionState.phase === "queued" && !requestIsCurrent(compactionState.request)) {
1940
+ compactionState = { phase: "idle" };
1941
+ }
1942
+ if (compactionState.phase === "idle") {
1943
+ queueTurnCompaction(ctx, settlementGeneration);
1944
+ }
1945
+ if (compactionState.phase === "queued" && compactionState.request.eligibleSettlement <= settlementGeneration) {
1946
+ startQueuedCompaction(ctx);
1947
+ }
1831
1948
  }
1832
1949
  pi.on("session_start", async (_event, ctx) => {
1950
+ sessionGeneration += 1;
1951
+ settlementGeneration = 0;
1952
+ activeSessionFile = ctx.sessionManager.getSessionFile?.() ?? null;
1953
+ sessionActive = true;
1954
+ compactionState = { phase: "idle" };
1833
1955
  goosedumpAvailable = checkGoosedump(ctx);
1834
- counterInterruptPending = false;
1835
1956
  notifyInvalidSettings(ctx);
1836
- syncCompactCounters(ctx, true);
1957
+ syncCompactCounters(ctx);
1837
1958
  updateStatus(ctx);
1959
+ queueTurnCompaction(ctx, 1);
1960
+ });
1961
+ pi.on("session_shutdown", async () => {
1962
+ sessionGeneration += 1;
1963
+ settlementGeneration = 0;
1964
+ activeSessionFile = null;
1965
+ sessionActive = false;
1966
+ compactionState = { phase: "idle" };
1838
1967
  });
1839
1968
  pi.on("turn_end", async (event, ctx) => {
1840
1969
  if (isCompletedAssistant(event.message))
1841
1970
  noteCompletedTurn(ctx);
1842
- interruptActiveRunForCompaction(ctx, event.toolResults.length > 0);
1843
1971
  });
1844
1972
  pi.on("agent_settled", async (_event, ctx) => {
1845
- maybeAutoCompact(ctx);
1973
+ handleAgentSettled(ctx);
1846
1974
  });
1847
1975
  pi.on("session_before_compact", async (event, ctx) => {
1848
1976
  if (!shouldOverrideDefaultCompaction || !goosedumpAvailable)
1849
1977
  return;
1850
- if (event.customInstructions?.trim())
1851
- return;
1852
- if (event.signal.aborted)
1853
- return;
1854
- const { messagesToSummarize, turnPrefixMessages } = event.preparation;
1855
- if (pendingKeep === null && messagesToSummarize.length === 0 && turnPrefixMessages.length === 0) {
1978
+ if (event.customInstructions?.trim()) {
1979
+ if (compactionState.phase === "running")
1980
+ return { cancel: true };
1981
+ if (compactionState.phase === "queued") {
1982
+ if (compactionState.request.origin !== "turn")
1983
+ return { cancel: true };
1984
+ compactionState = {
1985
+ phase: "queued",
1986
+ request: {
1987
+ ...compactionState.request,
1988
+ eligibleSettlement: Math.max(compactionState.request.eligibleSettlement, settlementGeneration + 2)
1989
+ }
1990
+ };
1991
+ }
1856
1992
  return;
1857
1993
  }
1858
- const target = currentSessionTarget(ctx);
1859
- if (!target)
1994
+ if (event.signal.aborted) {
1995
+ if (compactionState.phase === "queued" && requestIsCurrent(compactionState.request)) {
1996
+ compactionState = event.reason === "manual" ? { phase: "idle" } : {
1997
+ phase: "queued",
1998
+ request: {
1999
+ ...compactionState.request,
2000
+ eligibleSettlement: Math.max(compactionState.request.eligibleSettlement, settlementGeneration + 2)
2001
+ }
2002
+ };
2003
+ }
1860
2004
  return;
1861
- if (compactionHookActive)
1862
- return { cancel: true };
1863
- compactionHookActive = true;
2005
+ }
2006
+ let operation;
2007
+ let generation;
2008
+ let sessionFile;
2009
+ let request;
2010
+ let owner;
2011
+ if (compactionState.phase === "running") {
2012
+ if (compactionState.hookActive || compactionState.owner === "pi") {
2013
+ return { cancel: true };
2014
+ }
2015
+ operation = compactionState.operation;
2016
+ generation = compactionState.sessionGeneration;
2017
+ sessionFile = compactionState.sessionFile;
2018
+ request = compactionState.request;
2019
+ owner = "extension";
2020
+ compactionState = { ...compactionState, hookActive: true };
2021
+ } else {
2022
+ request = compactionState.phase === "queued" && requestIsCurrent(compactionState.request) ? compactionState.request : null;
2023
+ operation = ++operationGeneration;
2024
+ generation = sessionGeneration;
2025
+ sessionFile = activeSessionFile;
2026
+ owner = "pi";
2027
+ compactionState = {
2028
+ phase: "running",
2029
+ request,
2030
+ owner,
2031
+ operation,
2032
+ sessionGeneration: generation,
2033
+ sessionFile,
2034
+ hookActive: true
2035
+ };
2036
+ }
1864
2037
  try {
1865
- const keep = pendingKeep ?? undefined;
1866
- const compaction = await buildGoosedumpCompaction(target, event.preparation, event.branchEntries, effectiveSummaryMaxTokens(ctx.cwd), keep, event.signal, nativeExecutablePath);
2038
+ const { messagesToSummarize, turnPrefixMessages } = event.preparation;
2039
+ if ((request?.keep ?? null) === null && messagesToSummarize.length === 0 && turnPrefixMessages.length === 0) {
2040
+ return;
2041
+ }
2042
+ const target = currentSessionTarget(ctx);
2043
+ if (!target)
2044
+ return;
2045
+ let keep = request?.keep ?? undefined;
2046
+ if (keep !== undefined && keepBoundaryId(event.branchEntries, keep) === undefined) {
2047
+ if (ctx.hasUI) {
2048
+ ctx.ui.notify(`fewer than ${keep} user turns; using Pi's token-based cut`, "info");
2049
+ }
2050
+ keep = undefined;
2051
+ if (runningOperationIsCurrent(operation, generation, sessionFile)) {
2052
+ compactionState = { ...compactionState, request: null };
2053
+ }
2054
+ }
2055
+ const compaction = await buildGoosedumpCompaction(target, event.preparation, event.branchEntries, effectiveSummaryMaxTokens(ctx.cwd, projectSettingsTrusted(ctx)), keep, event.signal, nativeExecutablePath);
2056
+ if (!runningOperationIsCurrent(operation, generation, sessionFile)) {
2057
+ return { cancel: true };
2058
+ }
1867
2059
  if (compaction && "cancel" in compaction) {
1868
2060
  if (ctx.hasUI)
1869
2061
  ctx.ui.notify(compaction.reason, "info");
1870
- return { cancel: true };
2062
+ return owner === "pi" && event.reason === "manual" ? { cancel: true } : undefined;
1871
2063
  }
1872
2064
  const warning = compaction?.details?.warning;
1873
2065
  if (warning && ctx.hasUI) {
1874
2066
  ctx.ui.notify(warning, "warning");
1875
2067
  }
1876
- return compaction ? { compaction } : undefined;
2068
+ if (compaction)
2069
+ return { compaction };
2070
+ return owner === "pi" && event.reason === "manual" ? { cancel: true } : undefined;
1877
2071
  } catch (err) {
2072
+ if (!runningOperationIsCurrent(operation, generation, sessionFile)) {
2073
+ return { cancel: true };
2074
+ }
1878
2075
  if (event.signal.aborted)
1879
2076
  return { cancel: true };
2077
+ const useFallback = owner === "extension" || event.reason !== "manual";
1880
2078
  if (ctx.hasUI) {
1881
2079
  const message = err instanceof Error ? err.message : String(err);
1882
- ctx.ui.notify(`goosedump compaction failed; falling back to Pi compaction: ${message}`, "error");
2080
+ const action = useFallback ? "; falling back to Pi compaction" : "";
2081
+ ctx.ui.notify(`goosedump compaction failed${action}: ${message}`, "error");
1883
2082
  }
1884
- return;
2083
+ return useFallback ? undefined : { cancel: true };
1885
2084
  } finally {
1886
- compactionHookActive = false;
1887
- pendingKeep = null;
2085
+ if (runningOperationIsCurrent(operation, generation, sessionFile)) {
2086
+ compactionState = owner === "pi" && event.reason === "manual" ? { phase: "idle" } : { ...compactionState, hookActive: false };
2087
+ }
1888
2088
  }
1889
2089
  });
1890
2090
  pi.on("session_compact", async (event, ctx) => {
1891
- if (!goosedumpAvailable || !shouldOverrideDefaultCompaction)
2091
+ if (!shouldOverrideDefaultCompaction)
2092
+ return;
2093
+ if (!contextIsCurrent(ctx))
1892
2094
  return;
1893
- syncCompactCounters(ctx, true);
2095
+ if (compactionState.phase === "queued" && compactionState.request.keep !== null && ctx.hasUI) {
2096
+ ctx.ui.notify("Queued keep request was superseded by another compaction.", "info");
2097
+ }
2098
+ compactionState = { phase: "idle" };
2099
+ syncCompactCounters(ctx);
1894
2100
  updateStatus(ctx);
1895
- counterInterruptPending = false;
2101
+ if (!goosedumpAvailable)
2102
+ return;
1896
2103
  const compactionEntry = event.compactionEntry;
1897
2104
  if (!event.fromExtension || !compactionEntry || !isRecord(compactionEntry.details) || compactionEntry.details.source !== "goosedump") {
1898
2105
  return;
@@ -1951,13 +2158,13 @@ ${lines.join(`
1951
2158
  return;
1952
2159
  }
1953
2160
  }
1954
- const result = requestCompaction(ctx, keep);
2161
+ const result = requestCompaction(ctx, keep, "command");
1955
2162
  if (result === "busy") {
1956
2163
  ctx.ui.notify("Compaction already in progress", "info");
1957
2164
  } else if (result === "nothing-to-compact") {
1958
2165
  ctx.ui.notify("Nothing to compact (session too small)", "info");
1959
2166
  } else {
1960
- ctx.ui.notify(compactStartedMessage(keep), "info");
2167
+ ctx.ui.notify(compactStartedMessage(keep, result === "queued"), "info");
1961
2168
  }
1962
2169
  }
1963
2170
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goosedump",
3
- "version": "0.12.13",
3
+ "version": "0.12.15",
4
4
  "description": "Pi extension for goosedump session search, durable memory, and compaction",
5
5
  "keywords": [
6
6
  "compaction",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@earendil-works/pi-tui": "^0.82.1",
41
- "@jarkkojs/goosedump": "^0.12.13",
41
+ "@jarkkojs/goosedump": "^0.12.15",
42
42
  "@sinclair/typebox": "^0.34.49"
43
43
  },
44
44
  "devDependencies": {