@rol1n/pi-kit 0.1.0 → 0.1.2

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 (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -50
  3. package/README_zh.md +136 -0
  4. package/ask-u-questions/README.md +26 -0
  5. package/{extensions/ask-u-questions → ask-u-questions}/dialog-ui.ts +23 -0
  6. package/{extensions/ask-u-questions → ask-u-questions}/index.ts +9 -5
  7. package/{extensions/ask-u-questions → ask-u-questions}/multi-choice.ts +1 -3
  8. package/{extensions/ask-u-questions → ask-u-questions}/single-choice.ts +1 -3
  9. package/{extensions/ask-u-questions → ask-u-questions}/tool-renderers.ts +1 -3
  10. package/ask-u-questions/types.ts +192 -0
  11. package/continue-signal/README.md +20 -0
  12. package/{extensions/continue-signal → continue-signal}/index.ts +21 -3
  13. package/custom-providers/README.md +20 -0
  14. package/{extensions/custom-providers → custom-providers}/index.ts +16 -2
  15. package/image-generate/README.md +25 -0
  16. package/{extensions/image-generate → image-generate}/index.ts +22 -2
  17. package/{extensions/image-generate → image-generate}/model-selector.ts +1 -1
  18. package/memory-log/README.md +121 -0
  19. package/{extensions/memory-log → memory-log}/config.ts +1 -1
  20. package/{extensions/memory-log → memory-log}/hooks/compaction-hook.ts +58 -15
  21. package/memory-log/hooks/compaction-trigger.ts +87 -0
  22. package/{extensions/memory-log → memory-log}/hooks/observer-trigger.ts +2 -1
  23. package/{extensions/memory-log → memory-log}/index.ts +4 -0
  24. package/{extensions/memory-log → memory-log}/ledger/index.ts +1 -0
  25. package/memory-log/ledger/recall.ts +111 -0
  26. package/{extensions/memory-log → memory-log}/ledger/render.ts +3 -1
  27. package/{extensions/memory-log → memory-log}/ledger/serialize.ts +2 -4
  28. package/{extensions/memory-log → memory-log}/ledger/types.ts +4 -1
  29. package/{extensions/memory-log → memory-log}/runtime.ts +0 -5
  30. package/memory-log/tools/recall.ts +60 -0
  31. package/model-vision/README.md +22 -0
  32. package/{extensions/multi-harness → multi-harness}/README.md +3 -3
  33. package/oai-subquota/README.md +22 -0
  34. package/{extensions/oai-subquota → oai-subquota}/index.ts +16 -6
  35. package/{extensions/oai-subquota → oai-subquota}/snapshot.ts +127 -1
  36. package/{extensions/oai-subquota → oai-subquota}/ui.ts +1 -1
  37. package/package.json +46 -46
  38. package/prompt-snipaste/README.md +27 -0
  39. package/{extensions/prompt-snipaste → prompt-snipaste}/renderers.ts +0 -1
  40. package/{extensions/prompt-snipaste → prompt-snipaste}/snippets.ts +5 -2
  41. package/{extensions/prompt-snipaste → prompt-snipaste}/types.ts +0 -1
  42. package/screenshot/README.md +14 -0
  43. package/srp-theme/README.md +18 -0
  44. package/{extensions/srp-theme → srp-theme}/footer.ts +0 -1
  45. package/{extensions/srp-theme → srp-theme}/tps-meter.ts +0 -1
  46. package/{extensions/subagent → subagent}/README.md +1 -1
  47. package/tui-asr/README.md +21 -0
  48. package/{extensions/tui-asr → tui-asr}/dictation.ts +70 -3
  49. package/{extensions/tui-asr → tui-asr}/index.ts +2 -2
  50. package/{extensions/tui-asr → tui-asr}/input.ts +30 -1
  51. package/{extensions/tui-asr → tui-asr}/model-selector.ts +1 -1
  52. package/web-reach/README.md +19 -0
  53. package/windows-wheel-fix/README.md +12 -0
  54. package/extensions/ask-u-questions/README.md +0 -32
  55. package/extensions/ask-u-questions/answers.ts +0 -53
  56. package/extensions/ask-u-questions/results.ts +0 -83
  57. package/extensions/ask-u-questions/schema.ts +0 -17
  58. package/extensions/ask-u-questions/types.ts +0 -45
  59. package/extensions/ask-u-questions/ui-lock.ts +0 -24
  60. package/extensions/continue-signal/README.md +0 -28
  61. package/extensions/continue-signal/renderer.ts +0 -21
  62. package/extensions/custom-providers/README.md +0 -28
  63. package/extensions/image-generate/README.md +0 -29
  64. package/extensions/image-generate/models.ts +0 -19
  65. package/extensions/memory-log/README.md +0 -54
  66. package/extensions/memory-log/hooks/compaction-trigger.ts +0 -160
  67. package/extensions/model-vision/README.md +0 -28
  68. package/extensions/oai-subquota/README.md +0 -30
  69. package/extensions/oai-subquota/api.ts +0 -27
  70. package/extensions/oai-subquota/auth.ts +0 -34
  71. package/extensions/oai-subquota/cache.ts +0 -30
  72. package/extensions/oai-subquota/types.ts +0 -40
  73. package/extensions/prompt-snipaste/README.md +0 -33
  74. package/extensions/screenshot/README.md +0 -25
  75. package/extensions/srp-theme/README.md +0 -24
  76. package/extensions/subagent/skills/subagent/SKILL.md +0 -70
  77. package/extensions/tui-asr/README.md +0 -28
  78. package/extensions/tui-asr/audio.ts +0 -40
  79. package/extensions/tui-asr/clipboard.ts +0 -30
  80. package/extensions/tui-asr/providers.ts +0 -25
  81. package/extensions/web-reach/README.md +0 -37
  82. package/extensions/web-reach/html.ts +0 -83
  83. package/extensions/web-reach/index.ts +0 -82
  84. package/extensions/web-reach/page.ts +0 -132
  85. package/extensions/web-reach/pdf.ts +0 -42
  86. package/extensions/web-reach/reader.ts +0 -42
  87. package/extensions/web-reach/rsc.ts +0 -161
  88. package/extensions/web-reach/search.ts +0 -85
  89. package/extensions/web-reach/transport.ts +0 -35
  90. package/extensions/windows-wheel-fix/README.md +0 -21
  91. /package/{extensions/custom-providers → custom-providers}/asr/handlers/dashscope.ts +0 -0
  92. /package/{extensions/custom-providers → custom-providers}/asr/index.ts +0 -0
  93. /package/{extensions/custom-providers → custom-providers}/asr/models.ts +0 -0
  94. /package/{extensions/custom-providers → custom-providers}/asr/types.ts +0 -0
  95. /package/{extensions/custom-providers → custom-providers}/chat/index.ts +0 -0
  96. /package/{extensions/custom-providers → custom-providers}/chat/magpie.ts +0 -0
  97. /package/{extensions/custom-providers → custom-providers}/chat/models.ts +0 -0
  98. /package/{extensions/custom-providers → custom-providers}/classifier/index.ts +0 -0
  99. /package/{extensions/custom-providers → custom-providers}/classifier/models.ts +0 -0
  100. /package/{extensions/custom-providers → custom-providers}/image/handlers/dashscope.ts +0 -0
  101. /package/{extensions/custom-providers → custom-providers}/image/handlers/openrouter.ts +0 -0
  102. /package/{extensions/custom-providers → custom-providers}/image/index.ts +0 -0
  103. /package/{extensions/custom-providers → custom-providers}/image/models.ts +0 -0
  104. /package/{extensions/custom-providers → custom-providers}/image/types.ts +0 -0
  105. /package/{extensions/custom-providers → custom-providers}/types.ts +0 -0
  106. /package/{extensions/image-generate → image-generate}/files.ts +0 -0
  107. /package/{extensions/memory-log → memory-log}/agent/consolidator/prompt.ts +0 -0
  108. /package/{extensions/memory-log → memory-log}/agent/consolidator/tools.ts +0 -0
  109. /package/{extensions/memory-log → memory-log}/agent/cost.ts +0 -0
  110. /package/{extensions/memory-log → memory-log}/agent/index.ts +0 -0
  111. /package/{extensions/memory-log → memory-log}/agent/observer/prompt.ts +0 -0
  112. /package/{extensions/memory-log → memory-log}/agent/observer/tool.ts +0 -0
  113. /package/{extensions/memory-log → memory-log}/commands/compact.ts +0 -0
  114. /package/{extensions/memory-log → memory-log}/commands/consolidate.ts +0 -0
  115. /package/{extensions/memory-log → memory-log}/commands/status.ts +0 -0
  116. /package/{extensions/memory-log → memory-log}/debug-log.ts +0 -0
  117. /package/{extensions/memory-log → memory-log}/hooks/consolidator-trigger.ts +0 -0
  118. /package/{extensions/memory-log → memory-log}/ids.ts +0 -0
  119. /package/{extensions/memory-log → memory-log}/ledger/fold.ts +0 -0
  120. /package/{extensions/memory-log → memory-log}/ledger/pool.ts +0 -0
  121. /package/{extensions/memory-log → memory-log}/ledger/progress.ts +0 -0
  122. /package/{extensions/memory-log → memory-log}/ledger/projection.ts +0 -0
  123. /package/{extensions/memory-log → memory-log}/memory/index-render.ts +0 -0
  124. /package/{extensions/memory-log → memory-log}/memory/paths.ts +0 -0
  125. /package/{extensions/memory-log → memory-log}/memory/session.ts +0 -0
  126. /package/{extensions/memory-log → memory-log}/spawn/launch.ts +0 -0
  127. /package/{extensions/memory-log → memory-log}/spawn/runs.ts +0 -0
  128. /package/{extensions/memory-log → memory-log}/tokens.ts +0 -0
  129. /package/{extensions/memory-log → memory-log}/ui/status-controller.ts +0 -0
  130. /package/{extensions/memory-log → memory-log}/ui/timeline.ts +0 -0
  131. /package/{extensions/multi-harness → multi-harness}/adapters.mjs +0 -0
  132. /package/{extensions/multi-harness → multi-harness}/harness-done.mjs +0 -0
  133. /package/{extensions/multi-harness → multi-harness}/index.ts +0 -0
  134. /package/{extensions/multi-harness → multi-harness}/ipc.mjs +0 -0
  135. /package/{extensions/multi-harness → multi-harness}/opencode-plugin.mjs +0 -0
  136. /package/{extensions/multi-harness → multi-harness}/process-runner.mjs +0 -0
  137. /package/{extensions/multi-harness → multi-harness}/turn-hook.mjs +0 -0
  138. /package/{extensions/multi-harness → multi-harness}/worker-bridge.ts +0 -0
  139. /package/{extensions/prompt-snipaste → prompt-snipaste}/index.ts +0 -0
  140. /package/{extensions/prompt-snipaste → prompt-snipaste}/menu.ts +0 -0
  141. /package/{prompts → prompt-snipaste/prompts}/ask-questions.md +0 -0
  142. /package/{prompts → prompt-snipaste/prompts}/delegate-exploration.md +0 -0
  143. /package/{prompts → prompt-snipaste/prompts}/diagnose-report.md +0 -0
  144. /package/{prompts → prompt-snipaste/prompts}/frontend-tech-preset.md +0 -0
  145. /package/{prompts → prompt-snipaste/prompts}/orchestrator-mode.md +0 -0
  146. /package/{prompts → prompt-snipaste/prompts}/session-kickoff.md +0 -0
  147. /package/{prompts → prompt-snipaste/prompts}/verify-not-assume.md +0 -0
  148. /package/{extensions/srp-theme → srp-theme}/index.ts +0 -0
  149. /package/{extensions/subagent → subagent}/bridge.ts +0 -0
  150. /package/{extensions/subagent → subagent}/index.ts +0 -0
  151. /package/{extensions/subagent → subagent}/mux.ts +0 -0
  152. /package/{extensions/subagent → subagent}/session.ts +0 -0
  153. /package/{extensions/subagent → subagent}/ui.ts +0 -0
  154. /package/{extensions/tui-asr → tui-asr}/status.ts +0 -0
  155. /package/{extensions/windows-wheel-fix → windows-wheel-fix}/index.ts +0 -0
@@ -0,0 +1,20 @@
1
+ # continue-signal
2
+
3
+ Resume an idle Pi agent with a visible continuation signal.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pi install npm:@rol1n/continue-signal
9
+ ```
10
+
11
+ Also included in `@rol1n/pi-kit`.
12
+
13
+ ## Commands and shortcuts
14
+
15
+ | Action | Trigger |
16
+ |---|---|
17
+ | Resume unfinished work | `/continue` or `Alt+C` |
18
+ | Resume with custom instruction | `/continue Review the remaining test failures` |
19
+
20
+ An explicit command argument takes priority; otherwise the current editor draft is consumed, falling back to a default resume prompt.
@@ -3,13 +3,31 @@
3
3
  * Uses the editor draft when no prompt is supplied.
4
4
  */
5
5
 
6
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
7
- import { Key } from "@earendil-works/pi-tui";
8
- import { renderContinueSignal } from "./renderer.ts";
6
+ import type { ExtensionAPI, ExtensionContext, MessageRenderer } from "@earendil-works/pi-coding-agent";
7
+ import { Box, Key, Text } from "@earendil-works/pi-tui";
9
8
 
10
9
  const DEFAULT_PROMPT = "Review the conversation context and resume any unfinished work from where it stopped.";
11
10
  const CUSTOM_TYPE = "continue-signal";
12
11
 
12
+ const renderContinueSignal: MessageRenderer<{ timestamp?: number }> = (
13
+ message,
14
+ { expanded, outputPad },
15
+ theme,
16
+ ) => {
17
+ const time = new Date(message.details?.timestamp ?? Date.now()).toLocaleTimeString();
18
+ const title = `${theme.bold(theme.fg("accent", "⚡ LOOP RESUME SIGNAL"))} ${theme.fg("muted", `[${time}]` )}`;
19
+
20
+ const box = new Box(outputPad, 1, (text) => theme.bg("customMessageBg", text));
21
+ box.addChild(new Text(title, 0, 0));
22
+ box.addChild(new Text(theme.fg("text", "Re-linking conversation context & resuming agent loop..."), 0, 0));
23
+
24
+ if (expanded && message.content) {
25
+ box.addChild(new Text(theme.fg("dim", `Prompt: ${message.content}`), 0, 0));
26
+ }
27
+
28
+ return box;
29
+ };
30
+
13
31
  export default function (pi: ExtensionAPI) {
14
32
  pi.registerMessageRenderer(CUSTOM_TYPE, renderContinueSignal);
15
33
 
@@ -0,0 +1,20 @@
1
+ # custom-providers
2
+
3
+ Registers additional model providers through Pi's native model registry and `/login` interface.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pi install npm:@rol1n/custom-providers
9
+ ```
10
+
11
+ Also included in `@rol1n/pi-kit`.
12
+
13
+ ## Providers
14
+
15
+ | Provider | Configuration |
16
+ |---|---|
17
+ | Magpie | `MAGPIE_BASE_URL` (default `http://127.0.0.1:3425/v1`); authenticate via `MAGPIE_GATEWAY_KEY` or `/login magpie` |
18
+ | SHUAI API | Authenticate via `/login shuaiapi` |
19
+ | BigModel | Authenticate via `/login bigmodel` |
20
+ | DashScope | Authenticate via `/login dashscope`; `DASHSCOPE_IMAGE_URL` overrides the image API base URL |
@@ -10,9 +10,19 @@ import { imageProviders } from "./image/index.ts";
10
10
  import { classifierProviders } from "./classifier/index.ts";
11
11
  import { asrProviders } from "./asr/index.ts";
12
12
 
13
- export { asrProviders, ALL_ASR_MODELS } from "./asr/index.ts";
13
+ export { asrProviders, ALL_ASR_MODELS, createAsrProvider, type AsrModelDefinition, type StreamingAsrProvider } from "./asr/index.ts";
14
+ export { imageProviders, fetchAllAvailableImageModels, generateImage, resolveImageModel, type ImageModelDefinition } from "./image/index.ts";
15
+
16
+ const BATCH_LOCK_KEY = "__piCustomProvidersBatchLock";
17
+
18
+ export function ensureCustomProviders(pi: ExtensionAPI): void {
19
+ const g = globalThis as Record<string, unknown>;
20
+ if (g[BATCH_LOCK_KEY]) return;
21
+ g[BATCH_LOCK_KEY] = true;
22
+ setTimeout(() => {
23
+ g[BATCH_LOCK_KEY] = false;
24
+ }, 0).unref?.();
14
25
 
15
- export default function customProviders(pi: ExtensionAPI): void {
16
26
  const providerIds = new Set<string>([
17
27
  ...Object.keys(chatProviders),
18
28
  ...Object.keys(imageProviders),
@@ -48,3 +58,7 @@ export default function customProviders(pi: ExtensionAPI): void {
48
58
  pi.registerProvider(id, config);
49
59
  }
50
60
  }
61
+
62
+ export default function customProviders(pi: ExtensionAPI): void {
63
+ ensureCustomProviders(pi);
64
+ }
@@ -0,0 +1,25 @@
1
+ # image-generate
2
+
3
+ Generate images through OpenRouter or DashScope and save them to `.pi/generated-images/` in the current project. Registers the `image_generate` tool and `/image-generate` model selector.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pi install npm:@rol1n/image-generate
9
+ ```
10
+
11
+ Also included in `@rol1n/pi-kit`. Installing this package automatically installs `@rol1n/custom-providers`.
12
+
13
+ ## Use
14
+
15
+ Run `/image-generate model` to open the interactive model picker, or `/image-generate model qwen-image-3.0-pro` to switch directly.
16
+
17
+ ```json
18
+ {
19
+ "prompt": "A clean technical diagram of a terminal workflow",
20
+ "aspectRatio": "16:9",
21
+ "model": "google/gemini-3.1-flash-image"
22
+ }
23
+ ```
24
+
25
+ Authenticate via `/login openrouter` or `/login dashscope` (or `OPENROUTER_API_KEY` / `DASHSCOPE_API_KEY`).
@@ -12,13 +12,32 @@ import { Text, type AutocompleteItem } from "@earendil-works/pi-tui";
12
12
  import { Type } from "typebox";
13
13
 
14
14
  import {
15
+ ensureCustomProviders,
15
16
  fetchAllAvailableImageModels,
16
17
  generateImage,
17
18
  resolveImageModel,
18
19
  type ImageModelDefinition,
19
- } from "../custom-providers/image/index.ts";
20
+ } from "../custom-providers/index.ts";
20
21
  import { saveImageAtomically } from "./files.ts";
21
- import { DEFAULT_MODEL, registryImageModels, resolveProviderId } from "./models.ts";
22
+ /** Image catalog lookup and provider selection. */
23
+
24
+ export const DEFAULT_MODEL = "google/gemini-3.1-flash-image";
25
+
26
+ /** Image models known to Pi: OpenRouter's built-in catalog plus our DashScope registration. */
27
+ export function registryImageModels(ctx: { modelRegistry: { getModelsOfType(type: string): readonly any[] } }) {
28
+ return ctx.modelRegistry.getModelsOfType("image") as readonly { id: string; name?: string; provider: string }[];
29
+ }
30
+
31
+ /** Resolve which provider owns a model id, preferring the Pi registry over heuristics. */
32
+ export function resolveProviderId(
33
+ ctx: { modelRegistry: { getModelsOfType(type: string): readonly any[] } },
34
+ modelId: string,
35
+ ): string {
36
+ const lower = modelId.toLowerCase();
37
+ const known = registryImageModels(ctx).find((m) => m.id.toLowerCase() === lower);
38
+ if (known) return known.provider;
39
+ return lower.startsWith("qwen-image") ? "dashscope" : "openrouter";
40
+ }
22
41
  import { SearchableImageModelSelectorComponent } from "./model-selector.ts";
23
42
 
24
43
  const DEFAULT_TIMEOUT_MS = 120_000;
@@ -28,6 +47,7 @@ let currentModel = DEFAULT_MODEL;
28
47
  // ============================ Extension Entrypoint ============================
29
48
 
30
49
  export default function imageGenerate(pi: ExtensionAPI): void {
50
+ ensureCustomProviders(pi);
31
51
  // 1. Slash command: /image-generate model [name]
32
52
  pi.registerCommand("image-generate", {
33
53
  description: "Switch default image generation model (/image-generate model [name])",
@@ -1,5 +1,5 @@
1
1
  import { Container, Input, Spacer, Text, fuzzyFilter, getKeybindings } from "@earendil-works/pi-tui";
2
- import type { ImageModelDefinition } from "../custom-providers/image/index.ts";
2
+ import type { ImageModelDefinition } from "../custom-providers/index.ts";
3
3
 
4
4
  export class SearchableImageModelSelectorComponent extends Container {
5
5
  private searchInput: Input;
@@ -0,0 +1,121 @@
1
+ # memory-log
2
+
3
+ > Tiered asynchronous observational memory for long-running Pi sessions.
4
+
5
+ `memory-log` keeps multi-day coding sessions coherent across compactions without blocking your workflow. Based on the observational memory paradigm from [elpapi42/pi-observational-memory](https://github.com/elpapi42/pi-observational-memory), it moves memory distillation into background subprocesses while you work, making compaction a model-free rendering step when sufficient observations are available. If a safe cutoff or usable memory summary is unavailable, Pi's native summarizer takes over instead.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ pi install npm:@rol1n/memory-log
11
+ ```
12
+
13
+ Also included in `@rol1n/pi-kit`. The extension loads with new-session memory disabled by default (`/mem-log on` to enable for the current session, or `"defaultEnabled": true` in settings).
14
+
15
+ ---
16
+
17
+ ## Why it exists
18
+
19
+ Long AI coding sessions typically hit two walls:
20
+
21
+ 1. **The Compression Wall (Summary of a Summary)**: Traditional compaction summarizes the conversation when the context window fills up. Over multiple cycles, the agent carries a compressed summary of a previous summary, gradually losing concrete architectural rationale, rejected approaches, and subtle constraints.
22
+ 2. **Compaction Stalls**: Waiting for a model to rewrite a massive transcript right when the context window overflows interrupts flow.
23
+
24
+ `memory-log` solves both by doing the heavy lifting **asynchronously in the background** as the session progresses:
25
+
26
+ - **Background Observers**: Whenever raw conversation history accumulates a full chunk (`chunkTokens`), a background Pi worker distills that chunk into timestamped, priority-tagged (`🔴` / `🟡` / `🟢`) atomic observations stored in the session branch ledger.
27
+ - **Consolidator & Durable Topic Files**: When the active observation pool reaches `consolidateAtPoolTokens`, a background consolidator archives older observations into durable Markdown topic files under `.memory/<sessionId>/` (`INDEX.md`, `<topic>.md`) and appends project milestones to `.memory/<sessionId>/JOURNEY.md`, draining the active pool back toward `poolTargetTokens`.
28
+ - **Safe Deterministic Compaction**: After `agent_settled` (all retries and queued continuations have finished), context pressure can trigger compaction. The cutoff snaps to a completed observation boundary near `tailTokens`, never crossing an unobserved gap in newly recorded chunks or splitting a tool call from its results. The entire unobserved tail stays verbatim, even if larger than the target. The summary is rendered from `JOURNEY.md` + the **Memory Map** + active observations, without an LLM call. Missing boundaries or empty memory delegate to Pi's native summarizer, which may make an LLM call.
29
+ - **Source Recall**: `memory_recall` retrieves the original conversation chunk behind an observation by timestamp, including history hidden by compaction and observations archived to topic files.
30
+ - **TUI Observability**: Displays live footer gauges (`Next` chunk progress, `Pool` capacity, `Ctx` window usage), active worker spinners, and session memory cost.
31
+
32
+ ---
33
+
34
+ ## Commands
35
+
36
+ | Command | Action |
37
+ |---|---|
38
+ | `/mem-log` | Enable memory log (if off) or display status & timeline (if on) |
39
+ | `/mem-log on` / `/mem-log off` | Enable or disable observational memory for the current session |
40
+ | `/mem-log status` or `/mem-log:status` | View gauges, worker states, cost summary, and observation timeline |
41
+ | `/mem-log compact` or `/mem-log:compact` | Request immediate compaction (native summarization if no safe memory cutoff exists) |
42
+ | `/mem-log consolidate` or `/mem-log:consolidate` | Consolidate the active observation pool into durable topic files immediately |
43
+
44
+ ---
45
+
46
+ ## Source recall
47
+
48
+ The agent can call `memory_recall` with an observation's full timestamp-id (including a `.NN` suffix, if present), or an unambiguous prefix:
49
+
50
+ ```json
51
+ { "timestamp": "2026-10-23T14:30:00" }
52
+ ```
53
+
54
+ - New observations persist exact `sourceEntryIds`; provenance covers the **whole chunk**, not a per-fact citation.
55
+ - Older records remain readable: ranges are inferred from coverage anchors in source order, not worker completion order. Inferred ranges are explicitly labeled and may include extra conversation; old records cannot prove gaps caused by failed workers. Legacy compaction coverage retains its existing watermark interpretation.
56
+ - Recall searches only the **current branch**. Missing sources are reported as unavailable or partial, never borrowed from another branch. Archived observations remain traceable in the ledger.
57
+ - Output is paged at 12,000 source characters. Use the returned `nextOffset` as `offset` to continue; `limit` can request a smaller page. Images/non-text blocks use placeholders.
58
+ - Recall is read-only and remains available when background memory processing is disabled.
59
+
60
+ Automatic compaction requires a Pi version that exposes `agent_settled`. Aborted runs do not trigger it; there is no retry-error guessing or idle polling. Pi's own compaction mechanism remains independent.
61
+
62
+ ---
63
+
64
+ ## Configuration
65
+
66
+ Add a `memoryLog` section to `~/.pi/agent/settings.json` (global) or `.pi/settings.json` (project-level override):
67
+
68
+ ```json
69
+ {
70
+ "memoryLog": {
71
+ "defaultEnabled": false,
72
+ "chunkTokens": 10000,
73
+ "poolTargetTokens": 10000,
74
+ "consolidateAtPoolTokens": 15000,
75
+ "compactAtContextTokens": 150000,
76
+ "tailTokens": 20000,
77
+ "journeyTargetTokens": 1000,
78
+ "observerConcurrency": 4,
79
+ "passive": false,
80
+ "debugLog": false,
81
+ "models": {
82
+ "observer": { "thinking": "low" },
83
+ "consolidator": { "thinking": "medium" }
84
+ }
85
+ }
86
+ }
87
+ ```
88
+
89
+ `resumeAfterMidRunCompaction` is retained for configuration compatibility but has no effect: this extension's automatic trigger now runs only after settlement and never starts a synthetic continuation.
90
+
91
+ ### Model resolution
92
+
93
+ `models.observer` and `models.consolidator` accept `{ "provider": "...", "id": "...", "thinking": "off|minimal|low|medium|high|xhigh" }`.
94
+ Resolution priority: **explicit `memoryLog.models` -> current session model -> Pi default model (`defaultProvider` / `defaultModel`)**.
95
+
96
+ ### Environment overrides
97
+
98
+ | Variable | Purpose |
99
+ |---|---|
100
+ | `PI_OM_PASSIVE=1` | Disable automatic observer/consolidator/compaction triggers while keeping manual commands |
101
+ | `PI_OM_OBSERVER_PROVIDER` / `PI_OM_OBSERVER_MODEL` / `PI_OM_OBSERVER_THINKING` | Override observer worker model |
102
+ | `PI_OM_CONSOLIDATOR_PROVIDER` / `PI_OM_CONSOLIDATOR_MODEL` / `PI_OM_CONSOLIDATOR_THINKING` | Override consolidator worker model |
103
+
104
+ ---
105
+
106
+ ## Storage layout
107
+
108
+ ```text
109
+ <project-root>/.memory/<session-id>/
110
+ ├── INDEX.md # Orchestrator-rendered index of durable topics (injected as the Memory Map)
111
+ ├── JOURNEY.md # Chronological project history maintained by the consolidator
112
+ └── <topic-slug>.md # Durable topic notes read on demand by the agent
113
+ ```
114
+
115
+ Short-term observations and enable/disable gates are stored directly in the Pi session JSONL ledger so they branch and fork naturally with your session tree.
116
+
117
+ ---
118
+
119
+ ## Credits
120
+
121
+ Adapted from and inspired by [elpapi42/pi-observational-memory](https://github.com/elpapi42/pi-observational-memory) (MIT License), extended with file-backed durable topic consolidation (`.memory/<sessionId>/`), `JOURNEY.md` tracking, and TUI progress gauges.
@@ -42,7 +42,7 @@ export interface Config {
42
42
  * used as the last-resort fallback when no session model is available.
43
43
  */
44
44
  settingsDefaultModel?: { provider?: string; id?: string };
45
- /** Resume the agent automatically after a mid-run compaction. */
45
+ /** @deprecated Compatibility only; automatic compaction now runs after agent_settled. */
46
46
  resumeAfterMidRunCompaction: boolean;
47
47
  /** Power-user setting: disable all triggers (distinct from the on/off gate). */
48
48
  passive: boolean;
@@ -14,16 +14,50 @@ import {
14
14
  type Entry,
15
15
  } from "../ledger/index.ts";
16
16
 
17
- /** Distinct, branch-resolved coversUpToId indices of committed observation chunks, ascending. */
17
+ /**
18
+ * Completed boundaries behind the contiguous observation frontier. A later worker
19
+ * finishing first must not authorize discarding an earlier failed/in-flight chunk.
20
+ * Legacy records only have a cumulative watermark; retain that older contract.
21
+ */
18
22
  function chunkBoundaryIndices(branch: Entry[]): number[] {
19
23
  const indexes = entryIndexById(branch);
20
- const set = new Set<number>();
21
- for (const entry of branch) {
24
+ const boundaries = new Set<number>();
25
+ const coveredAt = new Map<string, number>();
26
+ let legacyFrontier = -1;
27
+ for (let i = 0; i < branch.length; i++) {
28
+ const entry = branch[i];
22
29
  if (!isObservationsRecordedEntry(entry)) continue;
23
30
  const idx = indexes.get(entry.data.coversUpToId);
24
- if (idx !== undefined) set.add(idx);
31
+ if (idx === undefined || idx >= i || !isSourceEntry(branch[idx])) continue;
32
+ if (entry.data.sourceEntryIds === undefined) {
33
+ legacyFrontier = Math.max(legacyFrontier, idx);
34
+ } else {
35
+ const ids = entry.data.sourceEntryIds;
36
+ // Reject unresolved, out-of-order, or non-source anchors.
37
+ let previous = -1;
38
+ const valid = ids.length > 0 && ids.at(-1) === entry.data.coversUpToId && ids.every((id) => {
39
+ const index = indexes.get(id);
40
+ if (index === undefined || index <= previous || index > idx || !isSourceEntry(branch[index])) return false;
41
+ previous = index;
42
+ return true;
43
+ });
44
+ if (!valid) continue;
45
+ for (const id of ids) coveredAt.set(id, Math.min(coveredAt.get(id) ?? Infinity, idx));
46
+ }
47
+ boundaries.add(idx);
48
+ }
49
+ const safeBoundaries: number[] = [];
50
+ let requiredCoverageEnd = -1;
51
+ for (let i = 0; i < branch.length; i++) {
52
+ if (isSourceEntry(branch[i])) {
53
+ const coveredEnd = i <= legacyFrontier ? i : (coveredAt.get(branch[i].id) ?? Infinity);
54
+ requiredCoverageEnd = Math.max(requiredCoverageEnd, coveredEnd);
55
+ }
56
+ // The projection must include the records that authorize this cut, not just
57
+ // records for overlapping chunks whose watermarks lie beyond it.
58
+ if (boundaries.has(i) && requiredCoverageEnd <= i) safeBoundaries.push(i);
25
59
  }
26
- return Array.from(set).sort((a, b) => a - b);
60
+ return safeBoundaries;
27
61
  }
28
62
 
29
63
  /** First source entry after `boundaryIndex` that is a valid cut point, or undefined. */
@@ -37,9 +71,9 @@ function firstKeptAfterBoundary(branch: Entry[], boundaryIndex: number): Entry |
37
71
 
38
72
  export function snapCutoff(
39
73
  branch: Entry[],
40
- proposedFirstKeptId: string,
74
+ _proposedFirstKeptId: string,
41
75
  tailTokens: number,
42
- ): { firstKeptId: string; tail: number | undefined } {
76
+ ): { firstKeptId: string | undefined; tail: number | undefined } {
43
77
  const indexes = entryIndexById(branch);
44
78
  const lastCompactionIdx = findLastCompactionIndex(branch);
45
79
  const boundaries = chunkBoundaryIndices(branch);
@@ -80,26 +114,21 @@ export function snapCutoff(
80
114
  }
81
115
  }
82
116
 
83
- if (!finalFirstKeptId) {
84
- finalFirstKeptId = proposedFirstKeptId;
85
- finalTail = undefined;
86
- }
87
-
88
117
  return { firstKeptId: finalFirstKeptId, tail: finalTail };
89
118
  }
90
119
 
91
- export function snapFirstKeptEntryId(branch: Entry[], proposedFirstKeptId: string, tailTokens: number): string {
120
+ export function snapFirstKeptEntryId(branch: Entry[], proposedFirstKeptId: string, tailTokens: number): string | undefined {
92
121
  return snapCutoff(branch, proposedFirstKeptId, tailTokens).firstKeptId;
93
122
  }
94
123
 
95
124
  export function canSkipObserverWait(
96
125
  branch: Entry[],
97
- snappedFirstKeptId: string,
126
+ snappedFirstKeptId: string | undefined,
98
127
  snappedTail: number | undefined,
99
128
  tailTokens: number,
100
129
  observersInFlight: Iterable<{ coversUpToId: string }>,
101
130
  ): boolean {
102
- if (snappedTail === undefined || snappedTail > tailTokens) return false;
131
+ if (!snappedFirstKeptId || snappedTail === undefined || snappedTail > tailTokens) return false;
103
132
 
104
133
  const indexes = entryIndexById(branch);
105
134
  const cutoffIndex = indexes.get(snappedFirstKeptId);
@@ -115,6 +144,7 @@ export function canSkipObserverWait(
115
144
  export function registerCompactionHook(pi: ExtensionAPI, runtime: Runtime): void {
116
145
  pi.on("session_before_compact", async (event: any, ctx: any) => {
117
146
  if (!runtime.enabled || runtime.config.passive) return undefined;
147
+ if (event.signal?.aborted) return { cancel: true };
118
148
 
119
149
  const hasUI = ctx.hasUI;
120
150
  if (runtime.compactHookInFlight) {
@@ -140,11 +170,24 @@ export function registerCompactionHook(pi: ExtensionAPI, runtime: Runtime): void
140
170
  snap = snapCutoff(branch, firstKeptEntryId, tailTokens);
141
171
  }
142
172
 
173
+ if (event.signal?.aborted) return { cancel: true };
174
+ if (!runtime.enabled) return undefined;
175
+
176
+ // No proven observed cut: let Pi summarize the raw history, never stamp an
177
+ // empty deterministic summary over unobserved messages (including old tails).
143
178
  const snapped = snap.firstKeptId;
179
+ if (!snapped || snap.tail === undefined) {
180
+ if (hasUI) ctx.ui.notify("memory-log: no safe observed cutoff — using Pi's native summarizer", "info");
181
+ return undefined;
182
+ }
144
183
  const projection = buildCompactionProjection(branch, snapped);
145
184
  const journey = readJourney(runtime.memoryRoot);
146
185
  const map = renderMemoryMap(listTopics(runtime.memoryRoot));
147
186
  const summary = renderSummary(journey, map, projection.observations);
187
+ if (!summary.trim()) {
188
+ if (hasUI) ctx.ui.notify("memory-log: memory summary is empty — using Pi's native summarizer", "info");
189
+ return undefined;
190
+ }
148
191
 
149
192
  return {
150
193
  compaction: {
@@ -0,0 +1,87 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ findLastCompactionIndex,
4
+ rawTokensAfterIndex,
5
+ rawTokensSinceLastCompaction,
6
+ type Entry,
7
+ } from "../ledger/index.ts";
8
+ import type { Runtime } from "../runtime.ts";
9
+
10
+ export function contextPressureTokens(
11
+ ctx: { getContextUsage?: () => { tokens: number | null } | undefined; sessionManager: { getBranch: () => Entry[] } },
12
+ threshold: number,
13
+ ): { tokens: number; due: boolean } {
14
+ const branch = ctx.sessionManager.getBranch();
15
+ if (branch.length === 0) return { tokens: 0, due: false };
16
+
17
+ // If the last entry on the branch is already a compaction, context was just compacted on this branch.
18
+ const lastEntry = branch[branch.length - 1];
19
+ if (lastEntry.type === "compaction") {
20
+ return { tokens: 0, due: false };
21
+ }
22
+
23
+ const lastCompactionIdx = findLastCompactionIndex(branch);
24
+ if (lastCompactionIdx !== -1) {
25
+ // If no new source entries exist after compaction, context has not grown since compaction.
26
+ const tokensSinceCompaction = rawTokensAfterIndex(branch, lastCompactionIdx);
27
+ if (tokensSinceCompaction === 0) {
28
+ return { tokens: 0, due: false };
29
+ }
30
+
31
+ // Check if any assistant message has responded after the compaction.
32
+ // In Pi, getContextUsage() returns tokens: null until an assistant message responds post-compaction.
33
+ let hasAssistantAfterCompaction = false;
34
+ for (let i = branch.length - 1; i > lastCompactionIdx; i--) {
35
+ const entry = branch[i];
36
+ if (entry.type === "message" && (entry.message as any)?.role === "assistant") {
37
+ hasAssistantAfterCompaction = true;
38
+ break;
39
+ }
40
+ }
41
+ if (!hasAssistantAfterCompaction) {
42
+ return { tokens: 0, due: false };
43
+ }
44
+ }
45
+
46
+ const live = ctx.getContextUsage?.()?.tokens;
47
+ if (live != null) {
48
+ return { tokens: live, due: live >= threshold };
49
+ }
50
+
51
+ const raw = rawTokensSinceLastCompaction(branch);
52
+ return { tokens: raw, due: raw >= threshold };
53
+ }
54
+
55
+ /**
56
+ * agent_end is not idle: Pi may still retry or drain queued work. agent_settled
57
+ * is the final boundary, so no retry guessing, timers, or idle polling is needed.
58
+ */
59
+ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): void {
60
+ pi.on("agent_settled", (event, ctx) => {
61
+ if (event.aborted || !runtime.enabled || runtime.config.passive) return;
62
+ if (runtime.compactInFlight || runtime.compactHookInFlight) return;
63
+ // Another handler may already have started work at this notification boundary.
64
+ if (!ctx.isIdle() || ctx.hasPendingMessages()) return;
65
+ if (!contextPressureTokens(ctx, runtime.config.compactAtContextTokens).due) return;
66
+
67
+ runtime.compactInFlight = true;
68
+ try {
69
+ if (ctx.hasUI) ctx.ui.notify("memory-log: context threshold reached — compacting (waiting for pending observers if needed)...", "info");
70
+ // compact() is callback-based. Do not await session idle from this handler.
71
+ ctx.compact({
72
+ onComplete: () => {
73
+ runtime.compactInFlight = false;
74
+ if (ctx.hasUI) ctx.ui.notify("memory-log: compaction completed", "info");
75
+ },
76
+ onError: (error) => {
77
+ runtime.compactInFlight = false;
78
+ if (error.message !== "Compaction cancelled" && ctx.hasUI) ctx.ui.notify(`memory-log: ${error.message}`, "error");
79
+ },
80
+ });
81
+ } catch (error) {
82
+ runtime.compactInFlight = false;
83
+ const message = error instanceof Error ? error.message : String(error);
84
+ if (ctx.hasUI) ctx.ui.notify(`memory-log: compaction trigger failed — ${message}`, "error");
85
+ }
86
+ });
87
+ }
@@ -102,6 +102,7 @@ async function dispatchObserver(
102
102
  runtime.observersInFlight.set(runId, { controller, coversUpToId });
103
103
 
104
104
  const { text: chunkText } = serializeSourceAddressedBranchEntries(slice.entries);
105
+ const sourceEntryIds = slice.entries.map((entry) => entry.id);
105
106
  const lastEntry = slice.entries.at(-1);
106
107
 
107
108
  runtime.status.workerStart("observer", runId);
@@ -148,7 +149,7 @@ async function dispatchObserver(
148
149
  });
149
150
 
150
151
  if (observations.length > 0) {
151
- pi.appendEntry(OM_OBSERVATIONS_RECORDED, { observations, coversUpToId });
152
+ pi.appendEntry(OM_OBSERVATIONS_RECORDED, { observations, coversUpToId, sourceEntryIds });
152
153
  }
153
154
  runtime.status.workerDone(runId, observations.length);
154
155
  runtime.refreshFooterGauges(ctx.sessionManager.getBranch(), ctx.getContextUsage?.()?.tokens ?? null);
@@ -20,6 +20,7 @@ import { registerObserverTrigger } from "./hooks/observer-trigger.ts";
20
20
  import { OM_ENABLED, type Entry } from "./ledger/index.ts";
21
21
  import { ensureSessionMemory } from "./memory/session.ts";
22
22
  import { Runtime } from "./runtime.ts";
23
+ import { registerRecallTool } from "./tools/recall.ts";
23
24
 
24
25
  function readGateFromLedger(branch: Entry[], defaultEnabled: boolean): boolean {
25
26
  for (let i = branch.length - 1; i >= 0; i--) {
@@ -136,6 +137,9 @@ export default function observationalMemory(pi: ExtensionAPI): void {
136
137
  handler: commandHandler,
137
138
  });
138
139
 
140
+ // Read-only recall remains available when background memory processing is off.
141
+ registerRecallTool(pi);
142
+
139
143
  // Triggers + hooks
140
144
  registerObserverTrigger(pi, runtime);
141
145
  registerConsolidatorTrigger(pi, runtime);
@@ -5,3 +5,4 @@ export * from "./projection.ts";
5
5
  export * from "./pool.ts";
6
6
  export * from "./render.ts";
7
7
  export * from "./serialize.ts";
8
+ export * from "./recall.ts";