@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.
- package/LICENSE +21 -0
- package/README.md +87 -50
- package/README_zh.md +136 -0
- package/ask-u-questions/README.md +26 -0
- package/{extensions/ask-u-questions → ask-u-questions}/dialog-ui.ts +23 -0
- package/{extensions/ask-u-questions → ask-u-questions}/index.ts +9 -5
- package/{extensions/ask-u-questions → ask-u-questions}/multi-choice.ts +1 -3
- package/{extensions/ask-u-questions → ask-u-questions}/single-choice.ts +1 -3
- package/{extensions/ask-u-questions → ask-u-questions}/tool-renderers.ts +1 -3
- package/ask-u-questions/types.ts +192 -0
- package/continue-signal/README.md +20 -0
- package/{extensions/continue-signal → continue-signal}/index.ts +21 -3
- package/custom-providers/README.md +20 -0
- package/{extensions/custom-providers → custom-providers}/index.ts +16 -2
- package/image-generate/README.md +25 -0
- package/{extensions/image-generate → image-generate}/index.ts +22 -2
- package/{extensions/image-generate → image-generate}/model-selector.ts +1 -1
- package/memory-log/README.md +121 -0
- package/{extensions/memory-log → memory-log}/config.ts +1 -1
- package/{extensions/memory-log → memory-log}/hooks/compaction-hook.ts +58 -15
- package/memory-log/hooks/compaction-trigger.ts +87 -0
- package/{extensions/memory-log → memory-log}/hooks/observer-trigger.ts +2 -1
- package/{extensions/memory-log → memory-log}/index.ts +4 -0
- package/{extensions/memory-log → memory-log}/ledger/index.ts +1 -0
- package/memory-log/ledger/recall.ts +111 -0
- package/{extensions/memory-log → memory-log}/ledger/render.ts +3 -1
- package/{extensions/memory-log → memory-log}/ledger/serialize.ts +2 -4
- package/{extensions/memory-log → memory-log}/ledger/types.ts +4 -1
- package/{extensions/memory-log → memory-log}/runtime.ts +0 -5
- package/memory-log/tools/recall.ts +60 -0
- package/model-vision/README.md +22 -0
- package/{extensions/multi-harness → multi-harness}/README.md +3 -3
- package/oai-subquota/README.md +22 -0
- package/{extensions/oai-subquota → oai-subquota}/index.ts +16 -6
- package/{extensions/oai-subquota → oai-subquota}/snapshot.ts +127 -1
- package/{extensions/oai-subquota → oai-subquota}/ui.ts +1 -1
- package/package.json +46 -46
- package/prompt-snipaste/README.md +27 -0
- package/{extensions/prompt-snipaste → prompt-snipaste}/renderers.ts +0 -1
- package/{extensions/prompt-snipaste → prompt-snipaste}/snippets.ts +5 -2
- package/{extensions/prompt-snipaste → prompt-snipaste}/types.ts +0 -1
- package/screenshot/README.md +14 -0
- package/srp-theme/README.md +18 -0
- package/{extensions/srp-theme → srp-theme}/footer.ts +0 -1
- package/{extensions/srp-theme → srp-theme}/tps-meter.ts +0 -1
- package/{extensions/subagent → subagent}/README.md +1 -1
- package/tui-asr/README.md +21 -0
- package/{extensions/tui-asr → tui-asr}/dictation.ts +70 -3
- package/{extensions/tui-asr → tui-asr}/index.ts +2 -2
- package/{extensions/tui-asr → tui-asr}/input.ts +30 -1
- package/{extensions/tui-asr → tui-asr}/model-selector.ts +1 -1
- package/web-reach/README.md +19 -0
- package/windows-wheel-fix/README.md +12 -0
- package/extensions/ask-u-questions/README.md +0 -32
- package/extensions/ask-u-questions/answers.ts +0 -53
- package/extensions/ask-u-questions/results.ts +0 -83
- package/extensions/ask-u-questions/schema.ts +0 -17
- package/extensions/ask-u-questions/types.ts +0 -45
- package/extensions/ask-u-questions/ui-lock.ts +0 -24
- package/extensions/continue-signal/README.md +0 -28
- package/extensions/continue-signal/renderer.ts +0 -21
- package/extensions/custom-providers/README.md +0 -28
- package/extensions/image-generate/README.md +0 -29
- package/extensions/image-generate/models.ts +0 -19
- package/extensions/memory-log/README.md +0 -54
- package/extensions/memory-log/hooks/compaction-trigger.ts +0 -160
- package/extensions/model-vision/README.md +0 -28
- package/extensions/oai-subquota/README.md +0 -30
- package/extensions/oai-subquota/api.ts +0 -27
- package/extensions/oai-subquota/auth.ts +0 -34
- package/extensions/oai-subquota/cache.ts +0 -30
- package/extensions/oai-subquota/types.ts +0 -40
- package/extensions/prompt-snipaste/README.md +0 -33
- package/extensions/screenshot/README.md +0 -25
- package/extensions/srp-theme/README.md +0 -24
- package/extensions/subagent/skills/subagent/SKILL.md +0 -70
- package/extensions/tui-asr/README.md +0 -28
- package/extensions/tui-asr/audio.ts +0 -40
- package/extensions/tui-asr/clipboard.ts +0 -30
- package/extensions/tui-asr/providers.ts +0 -25
- package/extensions/web-reach/README.md +0 -37
- package/extensions/web-reach/html.ts +0 -83
- package/extensions/web-reach/index.ts +0 -82
- package/extensions/web-reach/page.ts +0 -132
- package/extensions/web-reach/pdf.ts +0 -42
- package/extensions/web-reach/reader.ts +0 -42
- package/extensions/web-reach/rsc.ts +0 -161
- package/extensions/web-reach/search.ts +0 -85
- package/extensions/web-reach/transport.ts +0 -35
- package/extensions/windows-wheel-fix/README.md +0 -21
- /package/{extensions/custom-providers → custom-providers}/asr/handlers/dashscope.ts +0 -0
- /package/{extensions/custom-providers → custom-providers}/asr/index.ts +0 -0
- /package/{extensions/custom-providers → custom-providers}/asr/models.ts +0 -0
- /package/{extensions/custom-providers → custom-providers}/asr/types.ts +0 -0
- /package/{extensions/custom-providers → custom-providers}/chat/index.ts +0 -0
- /package/{extensions/custom-providers → custom-providers}/chat/magpie.ts +0 -0
- /package/{extensions/custom-providers → custom-providers}/chat/models.ts +0 -0
- /package/{extensions/custom-providers → custom-providers}/classifier/index.ts +0 -0
- /package/{extensions/custom-providers → custom-providers}/classifier/models.ts +0 -0
- /package/{extensions/custom-providers → custom-providers}/image/handlers/dashscope.ts +0 -0
- /package/{extensions/custom-providers → custom-providers}/image/handlers/openrouter.ts +0 -0
- /package/{extensions/custom-providers → custom-providers}/image/index.ts +0 -0
- /package/{extensions/custom-providers → custom-providers}/image/models.ts +0 -0
- /package/{extensions/custom-providers → custom-providers}/image/types.ts +0 -0
- /package/{extensions/custom-providers → custom-providers}/types.ts +0 -0
- /package/{extensions/image-generate → image-generate}/files.ts +0 -0
- /package/{extensions/memory-log → memory-log}/agent/consolidator/prompt.ts +0 -0
- /package/{extensions/memory-log → memory-log}/agent/consolidator/tools.ts +0 -0
- /package/{extensions/memory-log → memory-log}/agent/cost.ts +0 -0
- /package/{extensions/memory-log → memory-log}/agent/index.ts +0 -0
- /package/{extensions/memory-log → memory-log}/agent/observer/prompt.ts +0 -0
- /package/{extensions/memory-log → memory-log}/agent/observer/tool.ts +0 -0
- /package/{extensions/memory-log → memory-log}/commands/compact.ts +0 -0
- /package/{extensions/memory-log → memory-log}/commands/consolidate.ts +0 -0
- /package/{extensions/memory-log → memory-log}/commands/status.ts +0 -0
- /package/{extensions/memory-log → memory-log}/debug-log.ts +0 -0
- /package/{extensions/memory-log → memory-log}/hooks/consolidator-trigger.ts +0 -0
- /package/{extensions/memory-log → memory-log}/ids.ts +0 -0
- /package/{extensions/memory-log → memory-log}/ledger/fold.ts +0 -0
- /package/{extensions/memory-log → memory-log}/ledger/pool.ts +0 -0
- /package/{extensions/memory-log → memory-log}/ledger/progress.ts +0 -0
- /package/{extensions/memory-log → memory-log}/ledger/projection.ts +0 -0
- /package/{extensions/memory-log → memory-log}/memory/index-render.ts +0 -0
- /package/{extensions/memory-log → memory-log}/memory/paths.ts +0 -0
- /package/{extensions/memory-log → memory-log}/memory/session.ts +0 -0
- /package/{extensions/memory-log → memory-log}/spawn/launch.ts +0 -0
- /package/{extensions/memory-log → memory-log}/spawn/runs.ts +0 -0
- /package/{extensions/memory-log → memory-log}/tokens.ts +0 -0
- /package/{extensions/memory-log → memory-log}/ui/status-controller.ts +0 -0
- /package/{extensions/memory-log → memory-log}/ui/timeline.ts +0 -0
- /package/{extensions/multi-harness → multi-harness}/adapters.mjs +0 -0
- /package/{extensions/multi-harness → multi-harness}/harness-done.mjs +0 -0
- /package/{extensions/multi-harness → multi-harness}/index.ts +0 -0
- /package/{extensions/multi-harness → multi-harness}/ipc.mjs +0 -0
- /package/{extensions/multi-harness → multi-harness}/opencode-plugin.mjs +0 -0
- /package/{extensions/multi-harness → multi-harness}/process-runner.mjs +0 -0
- /package/{extensions/multi-harness → multi-harness}/turn-hook.mjs +0 -0
- /package/{extensions/multi-harness → multi-harness}/worker-bridge.ts +0 -0
- /package/{extensions/prompt-snipaste → prompt-snipaste}/index.ts +0 -0
- /package/{extensions/prompt-snipaste → prompt-snipaste}/menu.ts +0 -0
- /package/{prompts → prompt-snipaste/prompts}/ask-questions.md +0 -0
- /package/{prompts → prompt-snipaste/prompts}/delegate-exploration.md +0 -0
- /package/{prompts → prompt-snipaste/prompts}/diagnose-report.md +0 -0
- /package/{prompts → prompt-snipaste/prompts}/frontend-tech-preset.md +0 -0
- /package/{prompts → prompt-snipaste/prompts}/orchestrator-mode.md +0 -0
- /package/{prompts → prompt-snipaste/prompts}/session-kickoff.md +0 -0
- /package/{prompts → prompt-snipaste/prompts}/verify-not-assume.md +0 -0
- /package/{extensions/srp-theme → srp-theme}/index.ts +0 -0
- /package/{extensions/subagent → subagent}/bridge.ts +0 -0
- /package/{extensions/subagent → subagent}/index.ts +0 -0
- /package/{extensions/subagent → subagent}/mux.ts +0 -0
- /package/{extensions/subagent → subagent}/session.ts +0 -0
- /package/{extensions/subagent → subagent}/ui.ts +0 -0
- /package/{extensions/tui-asr → tui-asr}/status.ts +0 -0
- /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/
|
|
20
|
+
} from "../custom-providers/index.ts";
|
|
20
21
|
import { saveImageAtomically } from "./files.ts";
|
|
21
|
-
|
|
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/
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
21
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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);
|