pi-quiver 4.4.0 → 4.5.0
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/CHANGELOG.md +4 -0
- package/README.md +70 -20
- package/extensions/fast-mode.ts +1 -1
- package/extensions/provider-stall-watchdog.ts +3 -3
- package/extensions/session-name.ts +1 -1
- package/extensions/sword-header.ts +1 -1
- package/lib/extension-config.ts +46 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ Published to npm as `pi-quiver` (`pi install npm:pi-quiver`). Pushing a
|
|
|
8
8
|
via OIDC trusted publishing. The release helper at
|
|
9
9
|
`.agents/skills/release/scripts/release.sh` cuts the tag; CI publishes.
|
|
10
10
|
|
|
11
|
+
## v4.5.0 - 2026-08-29
|
|
12
|
+
|
|
13
|
+
- Settings resolution: every pi-quiver setting is now read from an optional `"quiver"` root object in `settings.json` (`quiver.<key>`), grouping the four legacy flat keys (`fastMode`, `sessionAutoName`, `swordHeader`, `providerStallWatchdog`) plus any future key. The flat top-level form keeps working, but only for those four legacy keys - it is frozen there and never extended to new settings. Within a layer, `quiver.<key>` wins over flat `<key>` by presence; malformed values and flat/nested duplicates now emit a warning instead of resolving silently.
|
|
14
|
+
|
|
11
15
|
## v4.4.0 - 2026-08-26
|
|
12
16
|
|
|
13
17
|
- doc_to_md: backend ladder now tries a system Python >= 3.12 with `pymupdf4llm` importable, then a one-time managed venv (bootstrapped at the version pin into a per-OS cache dir) between the existing `uv` and `unpdf` rungs. Data plane extracted to pi-free `lib/doc-to-md-core.ts`; new `pi-quiver doc-to-md <path>` CLI subcommand and `doc-to-md` Claude Code skill. `uv`/`soffice` detection is now spawn-based (Windows-correct). The bundled Python conversion script is now resolved from the package root, fixing a path bug that broke it under the bundled CLI.
|
package/README.md
CHANGED
|
@@ -137,45 +137,71 @@ None is a hard install-time dependency of the package; they are tools you provid
|
|
|
137
137
|
|
|
138
138
|
### Opt-in extension config
|
|
139
139
|
|
|
140
|
-
These extensions are opt-in via `settings.json` (project `.pi/settings.json` overrides the global agent-dir layer):
|
|
140
|
+
These extensions are opt-in via `settings.json` (project `.pi/settings.json` overrides the global agent-dir layer), nested under an optional `"quiver"` root:
|
|
141
141
|
|
|
142
142
|
```jsonc
|
|
143
143
|
{
|
|
144
|
-
"
|
|
145
|
-
"
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
144
|
+
"quiver": {
|
|
145
|
+
"sessionAutoName": {
|
|
146
|
+
"enabled": false,
|
|
147
|
+
"ghosttyTab": true,
|
|
148
|
+
"rules": [],
|
|
149
|
+
"deny": [],
|
|
150
|
+
"revisitFirstTurn": 0,
|
|
151
|
+
"revisitEveryTurns": 0
|
|
152
|
+
}, // or boolean shorthand
|
|
153
|
+
"swordHeader": false, // or { "enabled": true }
|
|
154
|
+
"fastMode": false, // or { "enabled": true }
|
|
155
|
+
"providerStallWatchdog": false // or { "enabled": true }
|
|
156
|
+
}
|
|
155
157
|
}
|
|
156
158
|
```
|
|
157
159
|
|
|
160
|
+
Each key resolves independently: within a layer, `quiver.<key>` wins over a
|
|
161
|
+
flat top-level `<key>` by presence alone (even when the winning value is
|
|
162
|
+
malformed); across layers, each layer's candidate is validated into a partial
|
|
163
|
+
patch and `Object.assign`ed over the accumulator in layer order (project
|
|
164
|
+
last), so project fields override matching global fields while unmatched
|
|
165
|
+
global fields survive - a flat-vs-nested shape difference between layers
|
|
166
|
+
never changes this. The flat top-level form still works, but only for four
|
|
167
|
+
legacy keys, frozen at `fastMode`, `sessionAutoName`, `swordHeader`, and
|
|
168
|
+
`providerStallWatchdog` - never extended to new settings (see [Migrating from
|
|
169
|
+
flat keys](#migrating-from-flat-keys)).
|
|
170
|
+
|
|
171
|
+
Worked mixed-shape example: global `settings.json` has flat
|
|
172
|
+
`"fastMode": false`, project `.pi/settings.json` has
|
|
173
|
+
`"quiver": { "fastMode": { "enabled": true } }`. The project layer's
|
|
174
|
+
patch (`{ "enabled": true }`) is `Object.assign`ed over the accumulator
|
|
175
|
+
seeded from global's patch, so the resolved config is `{ "enabled": true }` -
|
|
176
|
+
same outcome here because `enabled` is the only field either layer sets, but
|
|
177
|
+
the merge is per-field: a global field with no project counterpart would
|
|
178
|
+
survive untouched.
|
|
179
|
+
|
|
158
180
|
`sessionAutoName.enabled` makes one extra short LLM call per session (once, after the first turn) to title it; `false` (default) makes no model calls. `rules` appends house conventions to the naming prompt (later rules win when they conflict with the built-ins). Literal, case-insensitive `deny` phrases are stripped from every name; whitespace inside a phrase is loose, so `"acme corp"` also catches `AcmeCorp`. `revisitFirstTurn` re-evaluates the name once that many model round trips have completed, while `revisitEveryTurns` does so at every multiple; both default to `0` (off) because each revisit costs another short LLM call. For example, `10` and `100` mark round trips 10, 100, 200, 300. Revisits only run when the agent has fully settled (idle, nothing queued) - an automated multi-turn run such as a subagent chain is never renamed or delayed mid-flight; cadence points it crossed fire once, at the settle. A machine-generated name is replaced when stale. A name set by a human is never overwritten: the extension strongly prefers it, and announces a suggestion only when the work has clearly moved on. Counts come from the persisted transcript, so they survive resume.
|
|
159
181
|
|
|
160
182
|
`fastMode` only affects `claude-opus-4-8` and `claude-opus-5` requests on Anthropic's `anthropic-messages` API; enabling it opts into premium fast-mode pricing. `--fast` forces it on for one launch; `/fast on|off` toggles live. Proxy providers (opencode, cloudflare-ai-gateway) are excluded. `fastMode`'s header injection needs the `before_provider_headers` hook (pi bundling `@earendil-works/pi-coding-agent` >= 0.80.5); on older pi the beta header is silently not sent. See [doc/fetch.md](doc/fetch.md) and [doc/doc-to-md.md](doc/doc-to-md.md) for the ingestion tools' full reference; session-name/sword-header behavior above is complete.
|
|
161
183
|
|
|
162
184
|
`pi-ai` prices every fast request at standard rates - it has no `usage.speed` support and no request-level pricing modifier - so `fastMode` corrects the reported cost itself: a `message_end` handler scales all four `usage.cost` components by `FAST_MODE_COST_MULTIPLIER` (2x) and returns the corrected message. Persisted session JSONL and pi's own native cost display are always exact, since they're written from this corrected message. pi-cohort's live `Σ$` reflects the correction only when pi-quiver's `message_end` handler runs before pi-cohort's - best-effort, depending on extension load order - and is reconciled on pi-cohort's next `session_start` regardless. The upstream fix (teaching `pi-ai`'s `Usage`/`calculateCost` about `usage.speed`) is the better long-term path and is tracked separately.
|
|
163
185
|
|
|
164
|
-
Recommended explicit retry and watchdog settings
|
|
186
|
+
Recommended explicit retry and watchdog settings - `providerStallWatchdog`
|
|
187
|
+
nests under `quiver`, while pi-core's own `retry` stays flat beside it (it is
|
|
188
|
+
not a pi-quiver setting and is never nested):
|
|
165
189
|
|
|
166
190
|
```json
|
|
167
191
|
{
|
|
192
|
+
"quiver": {
|
|
193
|
+
"providerStallWatchdog": {
|
|
194
|
+
"enabled": true,
|
|
195
|
+
"firstEventMs": 20000,
|
|
196
|
+
"warningMs": 120000,
|
|
197
|
+
"recoveryMs": 240000,
|
|
198
|
+
"maxStallRetries": 3
|
|
199
|
+
}
|
|
200
|
+
},
|
|
168
201
|
"retry": {
|
|
169
202
|
"enabled": true,
|
|
170
203
|
"maxRetries": 3,
|
|
171
204
|
"baseDelayMs": 2000
|
|
172
|
-
},
|
|
173
|
-
"providerStallWatchdog": {
|
|
174
|
-
"enabled": true,
|
|
175
|
-
"firstEventMs": 20000,
|
|
176
|
-
"warningMs": 120000,
|
|
177
|
-
"recoveryMs": 240000,
|
|
178
|
-
"maxStallRetries": 3
|
|
179
205
|
}
|
|
180
206
|
}
|
|
181
207
|
```
|
|
@@ -205,6 +231,30 @@ Operational notes:
|
|
|
205
231
|
- **A watchdog abort that the provider ignores escalates after a fixed 10s.** Any post-abort stream event re-arms that deadline (bytes prove only that the connection was alive at that instant), so a stream that emits a straggler and then wedges still escalates 10s after its last event. This reduces the hang; it cannot force the provider to stop, and undici's timeouts remain the final backstop.
|
|
206
232
|
- **Headless runs report on stderr.** In `print`/`json` mode pi binds a no-op UI, so watchdog notices go out via `console.warn`. Nothing is ever written to stdout, which `json` mode uses for its protocol. In TUI and RPC the notices render as main-window notifications, not the bottom status line.
|
|
207
233
|
|
|
234
|
+
### Migrating from flat keys
|
|
235
|
+
|
|
236
|
+
The flat top-level form (`"fastMode": ...` etc. directly under `settings.json`)
|
|
237
|
+
is the outdated configuration style. It is legacy-frozen to exactly the four
|
|
238
|
+
keys above - `fastMode`, `sessionAutoName`, `swordHeader`,
|
|
239
|
+
`providerStallWatchdog` - and will never gain a fifth. To migrate, wrap your
|
|
240
|
+
existing keys under `"quiver": { ... }` and delete the flat copies:
|
|
241
|
+
|
|
242
|
+
```jsonc
|
|
243
|
+
// before
|
|
244
|
+
{ "fastMode": true }
|
|
245
|
+
|
|
246
|
+
// after
|
|
247
|
+
{ "quiver": { "fastMode": true } }
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Until you delete the flat copy, having both set is not an error - the
|
|
251
|
+
duplicate resolves per the precedence above (nested wins within a layer) -
|
|
252
|
+
but it emits a warning notification, deduped per process (each unique
|
|
253
|
+
message fires at most once per pi process - in practice once per interactive
|
|
254
|
+
session) until the flat entry is removed. Every new pi-quiver setting introduced after this change
|
|
255
|
+
(for example a future `slack` key) is nested-only from day one: it has no
|
|
256
|
+
flat form to fall back to.
|
|
257
|
+
|
|
208
258
|
## Claude Code support
|
|
209
259
|
|
|
210
260
|
`fetch`'s core (`lib/fetch-core.ts`) is also published as a CLI, so Claude Code can use the same routing, size gate, and spill behavior as pi's native tool - without pi ever seeing Claude-only files.
|
package/extensions/fast-mode.ts
CHANGED
|
@@ -116,7 +116,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
116
116
|
const readFlag = (): boolean => pi.getFlag("fast") === true;
|
|
117
117
|
|
|
118
118
|
const resolveState = (ctx: ExtensionContext): boolean => {
|
|
119
|
-
const config = resolveConfig(ctx.cwd, "fastMode", DEFAULT_CONFIG, coerce).enabled;
|
|
119
|
+
const config = resolveConfig(ctx.cwd, "fastMode", DEFAULT_CONFIG, coerce, (m) => ctx.ui.notify(m, "warning")).enabled;
|
|
120
120
|
enabled = resolveEnabled({ config, flag: readFlag(), live: liveOverride });
|
|
121
121
|
return enabled;
|
|
122
122
|
};
|
|
@@ -91,8 +91,8 @@ export function resolveRetryMaxRetries(cwd: string): number {
|
|
|
91
91
|
return maxRetries;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
export function resolveWatchdogConfig(cwd: string): ConfigValidation {
|
|
95
|
-
const candidate = resolveConfig(cwd, "providerStallWatchdog", DEFAULT_CANDIDATE, coerce);
|
|
94
|
+
export function resolveWatchdogConfig(cwd: string, warn?: (msg: string) => void): ConfigValidation {
|
|
95
|
+
const candidate = resolveConfig(cwd, "providerStallWatchdog", DEFAULT_CANDIDATE, coerce, warn);
|
|
96
96
|
if (candidate.blockIsObject === true && candidate.maxStallRetries === undefined) {
|
|
97
97
|
candidate.maxStallRetries = resolveRetryMaxRetries(cwd);
|
|
98
98
|
}
|
|
@@ -278,7 +278,7 @@ export function createProviderStallWatchdog(runtime: WatchdogRuntime = defaultRu
|
|
|
278
278
|
ui = ctx.ui;
|
|
279
279
|
hasUI = ctx.hasUI;
|
|
280
280
|
if (!config) {
|
|
281
|
-
const resolved = resolveWatchdogConfig(ctx.cwd);
|
|
281
|
+
const resolved = resolveWatchdogConfig(ctx.cwd, (m) => announce(m, "warning"));
|
|
282
282
|
if (!resolved.ok) {
|
|
283
283
|
disabled = true;
|
|
284
284
|
announce(`providerStallWatchdog disabled: ${resolved.error}`, "warning");
|
|
@@ -96,7 +96,7 @@ export function coerce(raw: unknown): Partial<Config> | undefined {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
function loadConfig(ctx: ExtensionContext): Config {
|
|
99
|
-
return resolveConfig(ctx.cwd, "sessionAutoName", DEFAULT_CONFIG, coerce);
|
|
99
|
+
return resolveConfig(ctx.cwd, "sessionAutoName", DEFAULT_CONFIG, coerce, (m) => ctx.ui.notify(m, "warning"));
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
type ContentBlock = { type?: string; text?: string };
|
|
@@ -66,7 +66,7 @@ function renderSwordLines(theme: Theme): string[] {
|
|
|
66
66
|
export default function (pi: ExtensionAPI) {
|
|
67
67
|
pi.on("session_start", async (_event, ctx) => {
|
|
68
68
|
if (ctx.mode !== "tui") return;
|
|
69
|
-
const cfg = resolveConfig(ctx.cwd, "swordHeader", DEFAULT_CONFIG, coerce);
|
|
69
|
+
const cfg = resolveConfig(ctx.cwd, "swordHeader", DEFAULT_CONFIG, coerce, (m) => ctx.ui.notify(m, "warning"));
|
|
70
70
|
if (!cfg.enabled) return;
|
|
71
71
|
ctx.ui.setHeader((_tui, theme) => ({
|
|
72
72
|
render(_width: number): string[] {
|
package/lib/extension-config.ts
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
* correct when these extensions are consumed as a git-tag-pinned package -
|
|
12
12
|
* unlike deriving the path from `import.meta.url`, which only held while an
|
|
13
13
|
* extension lived inside `<agentHome>/extensions/`.
|
|
14
|
+
*
|
|
15
|
+
* Within each layer, a nested `quiver.<key>` takes precedence over the flat
|
|
16
|
+
* `<key>` by presence alone (even when malformed); the flat top-level
|
|
17
|
+
* fallback is frozen to the pre-quiver LEGACY_FLAT_KEYS and never extended.
|
|
14
18
|
*/
|
|
15
19
|
|
|
16
20
|
import { readFileSync } from "node:fs";
|
|
@@ -29,21 +33,60 @@ export function settingsPaths(cwd: string): string[] {
|
|
|
29
33
|
return [join(getAgentDir(), "settings.json"), join(cwd, ".pi", "settings.json")];
|
|
30
34
|
}
|
|
31
35
|
|
|
36
|
+
/** Flat top-level fallback is frozen to these pre-quiver keys; never extend. */
|
|
37
|
+
const LEGACY_FLAT_KEYS = new Set(["fastMode", "sessionAutoName", "swordHeader", "providerStallWatchdog"]);
|
|
38
|
+
|
|
39
|
+
const emittedWarnings = new Set<string>();
|
|
40
|
+
|
|
41
|
+
function emitWarning(warn: ((message: string) => void) | undefined, message: string): void {
|
|
42
|
+
if (!warn || emittedWarnings.has(message)) return;
|
|
43
|
+
emittedWarnings.add(message);
|
|
44
|
+
warn(message);
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
/**
|
|
33
|
-
* Resolve a single extension config key across the settings layers.
|
|
34
|
-
*
|
|
48
|
+
* Resolve a single extension config key across the settings layers.
|
|
49
|
+
* `quiver.<key>` wins over flat `<key>` within a layer by presence (even
|
|
50
|
+
* when malformed); flat fallback exists only for LEGACY_FLAT_KEYS. `coerce`
|
|
51
|
+
* validates the layer's candidate into a partial patch (or `undefined` to
|
|
35
52
|
* skip); patches merge over `defaults` in layer order (project wins).
|
|
53
|
+
* `warn` receives one sentence per malformed or flat/nested-duplicated key,
|
|
54
|
+
* deduped per process.
|
|
36
55
|
*/
|
|
37
56
|
export function resolveConfig<T extends object>(
|
|
38
57
|
cwd: string,
|
|
39
58
|
key: string,
|
|
40
59
|
defaults: T,
|
|
41
60
|
coerce: (raw: unknown) => Partial<T> | undefined,
|
|
61
|
+
warn?: (message: string) => void,
|
|
42
62
|
): T {
|
|
43
63
|
const cfg: T = { ...defaults };
|
|
64
|
+
let nestedSeen = false;
|
|
65
|
+
let flatSeen = false;
|
|
44
66
|
for (const path of settingsPaths(cwd)) {
|
|
45
|
-
const
|
|
67
|
+
const settings = readSettings(path);
|
|
68
|
+
if (!settings) continue;
|
|
69
|
+
let root = settings.quiver;
|
|
70
|
+
if (root !== undefined && (root === null || typeof root !== "object" || Array.isArray(root))) {
|
|
71
|
+
emitWarning(warn, `pi-quiver: "quiver" in ${path} is not an object; ignored.`);
|
|
72
|
+
root = undefined;
|
|
73
|
+
}
|
|
74
|
+
const nested = root as Record<string, unknown> | undefined;
|
|
75
|
+
const hasNested = nested !== undefined && Object.hasOwn(nested, key);
|
|
76
|
+
const hasFlat = Object.hasOwn(settings, key);
|
|
77
|
+
nestedSeen ||= hasNested;
|
|
78
|
+
flatSeen ||= hasFlat;
|
|
79
|
+
if (!hasNested && !(hasFlat && LEGACY_FLAT_KEYS.has(key))) continue;
|
|
80
|
+
const candidate = hasNested ? nested![key] : settings[key];
|
|
81
|
+
const patch = coerce(candidate);
|
|
46
82
|
if (patch) Object.assign(cfg, patch);
|
|
83
|
+
else emitWarning(warn, `pi-quiver: "${key}" in ${path} has an unrecognized value; ignored.`);
|
|
84
|
+
}
|
|
85
|
+
if (nestedSeen && flatSeen) {
|
|
86
|
+
emitWarning(
|
|
87
|
+
warn,
|
|
88
|
+
`pi-quiver: "${key}" is set both flat and under "quiver" (nested wins within a layer; across layers the project layer wins regardless of shape) - move the flat entry under "quiver".`,
|
|
89
|
+
);
|
|
47
90
|
}
|
|
48
91
|
return cfg;
|
|
49
92
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-quiver",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0",
|
|
4
4
|
"description": "Personal pack of Pi coding-agent extensions: context-safe fetch, doc_to_md PDF/DOCX/PPTX-to-Markdown conversion, session naming, a themed ASCII startup header, Opus 4.8 fast mode, and a provider-stall watchdog.",
|
|
5
5
|
"author": "Jacek Juraszek",
|
|
6
6
|
"license": "MIT",
|