@mjasnikovs/pi-task 0.40.25 → 0.40.26

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.
@@ -135,6 +135,11 @@ export function headedSlices(header, body, maxBytes) {
135
135
  if (Buffer.byteLength(prefixed, 'utf8') <= maxBytes)
136
136
  return [prefixed];
137
137
  const room = maxBytes - Buffer.byteLength(`${header}\n`, 'utf8');
138
+ // A header that fills the cap on its own leaves no room, and `sliceBytes` with a
139
+ // non-positive cap never shrinks its buffer. Slice the prefixed string instead:
140
+ // the first piece still names the source, which is all the header buys.
141
+ if (room <= 0)
142
+ return sliceBytes(prefixed, maxBytes);
138
143
  return sliceBytes(body, room).map(slice => `${header}\n${slice}`);
139
144
  }
140
145
  /**
@@ -41,6 +41,12 @@ interface DocsDetails {
41
41
  /** The project-lookup budget for this attempt is spent, so the call was refused
42
42
  * before any work. Only set when PI_TASK_PROJECT_DOCS_BUDGET is configured. */
43
43
  budgetSpent?: boolean;
44
+ /**
45
+ * The child declined to answer. Recorded HERE, at the one place the bare
46
+ * `<answer>` is in hand: the tool text a cache predicate is handed leads with a
47
+ * provenance header, and the anchored matcher scores that as a real answer.
48
+ */
49
+ abstained?: boolean;
44
50
  }
45
51
  /**
46
52
  * Pull `@see {@link https://…}` pointers out of retrieved .d.ts/README text.
@@ -89,7 +95,7 @@ export declare function registerPiWorkerDocs(pi: ExtensionAPI, internals?: PiWor
89
95
  export declare function excerptFabricated(check: {
90
96
  absent: readonly string[];
91
97
  } | undefined): boolean;
92
- export declare function docsCacheable(d: Pick<DocsDetails, 'typeOnly' | 'excerptVerified' | 'excerptFabricated'>, text: string): boolean;
98
+ export declare function docsCacheable(d: Pick<DocsDetails, 'typeOnly' | 'excerptVerified' | 'excerptFabricated' | 'abstained'>): boolean;
93
99
  /** The docs cache key: a package's answer is per (module, question), with the question
94
100
  * lowercased and its whitespace collapsed so phrasing variants share one entry. Returns
95
101
  * null for the project-source `.` lookup, which is never cached — the working tree
@@ -219,7 +219,8 @@ export function registerPiWorkerDocs(pi, internals = {}) {
219
219
  return workerAnswer(text, {
220
220
  ...baseDetails,
221
221
  excerptVerified: verified,
222
- excerptFabricated: excerptFabricated(extraction.excerptCheck)
222
+ excerptFabricated: excerptFabricated(extraction.excerptCheck),
223
+ ...(isAbstention(extraction.answer) ? { abstained: true } : {})
223
224
  });
224
225
  }
225
226
  // ── npm package lookup (existing path) ──────────────────────────
@@ -355,7 +356,8 @@ export function registerPiWorkerDocs(pi, internals = {}) {
355
356
  ...baseDetails,
356
357
  excerptVerified: verified,
357
358
  excerptFabricated: excerptFabricated(extraction.excerptCheck),
358
- ...(typeOnly.typeOnly ? { typeOnly: true } : {})
359
+ ...(typeOnly.typeOnly ? { typeOnly: true } : {}),
360
+ ...(isAbstention(extraction.answer) ? { abstained: true } : {})
359
361
  });
360
362
  },
361
363
  renderCall(args, theme) {
@@ -384,9 +386,6 @@ export function registerPiWorkerDocs(pi, internals = {}) {
384
386
  // child that ran fine and answered "unclear from this package" exits 0 — so a rule
385
387
  // keyed on exit code would memoise that non-answer and re-serve it as a hit to
386
388
  // every later sibling, with nothing left to re-trigger an escalation.
387
- //
388
- // `text` is supplied by makeWorkerTool (shared.ts) alongside details, so the
389
- // content check needs no new plumbing.
390
389
  cacheable: docsCacheable
391
390
  });
392
391
  }
@@ -409,7 +408,7 @@ export function registerPiWorkerDocs(pi, internals = {}) {
409
408
  export function excerptFabricated(check) {
410
409
  return check !== undefined && check.absent.length > 0;
411
410
  }
412
- export function docsCacheable(d, text) {
411
+ export function docsCacheable(d) {
413
412
  // Answer QUALITY only. Whether there IS an answer is `WorkerOutcome.kind`, and
414
413
  // `makeWorkerTool` has already refused an `unavailable` before reaching here —
415
414
  // opening this with `childExitCode === 0` memoises an aborted lookup for the
@@ -420,7 +419,12 @@ export function docsCacheable(d, text) {
420
419
  // classifier defect 18 added, 41 of 41 unverified excerpts across seven runs are
421
420
  // STITCHED — every span verbatim, not one absent word. The gate was refusing
422
421
  // non-contiguous quoting, and every sibling paid a fresh child for it.
423
- return d.typeOnly !== true && d.excerptFabricated !== true && !isAbstention(text);
422
+ //
423
+ // The abstention is read off DETAILS, not off the tool text. `makeWorkerTool`
424
+ // hands this the FINAL text, which leads with `Per <pkg>@<version>:` — and
425
+ // `isAbstention` is anchored, so testing that text scored every abstention as a
426
+ // real answer and memoised the dead end for the whole run.
427
+ return d.typeOnly !== true && d.excerptFabricated !== true && d.abstained !== true;
424
428
  }
425
429
  /** The docs cache key: a package's answer is per (module, question), with the question
426
430
  * lowercased and its whitespace collapsed so phrasing variants share one entry. Returns
@@ -32,7 +32,7 @@ export declare function registerPiWorkerFetch(pi: ExtensionAPI, internals?: PiWo
32
32
  * ABOUT that page, and re-fetching cannot change it — only the abstention sentinel is
33
33
  * refused.
34
34
  */
35
- export declare function fetchCacheable(_d: Pick<FetchDetails, never>, text: string): boolean;
35
+ export declare function fetchCacheable(d: Pick<FetchDetails, 'answer'>): boolean;
36
36
  /** The fetch cache key. URL verbatim (path case can matter), question normalised —
37
37
  * same page, different question is a different answer. */
38
38
  export declare function fetchCacheKey(params: {
@@ -112,12 +112,16 @@ export function registerPiWorkerFetch(pi, internals = {}) {
112
112
  * ABOUT that page, and re-fetching cannot change it — only the abstention sentinel is
113
113
  * refused.
114
114
  */
115
- export function fetchCacheable(_d, text) {
116
- // Answer QUALITY only — see docsCacheable. This predicate returns true for
117
- // `"Fetch aborted."` on its own; what keeps an aborted fetch out of the cache is
118
- // the `unavailable` outcome upstream. Leading the rule with `childExitCode === 0`
119
- // would not, because an aborted child settles at exit code 0.
120
- return !isAbstention(text);
115
+ export function fetchCacheable(d) {
116
+ // Answer QUALITY only — see docsCacheable. An aborted fetch is kept out of the
117
+ // cache by its `unavailable` outcome upstream, never by this rule; leading with
118
+ // `childExitCode === 0` would not work, because an aborted child settles at 0.
119
+ //
120
+ // It reads the child's bare answer, not the rendered tool text: `isAbstention` is
121
+ // anchored, and the text leads with an excerpt NOTE/WARNING whenever the excerpt
122
+ // did not verify — which, since rule 4 asks for the closest related text, is the
123
+ // ordinary shape of an abstention.
124
+ return d.answer !== undefined && !isAbstention(d.answer);
121
125
  }
122
126
  /** The fetch cache key. URL verbatim (path case can matter), question normalised —
123
127
  * same page, different question is a different answer. */
@@ -121,8 +121,12 @@ export interface WorkerToolSpec<TParams extends TSchema, TDetails> {
121
121
  * (type-only, an abstention, an unverified excerpt), never about process
122
122
  * health. An `unavailable` outcome never reaches this: `makeWorkerTool` has
123
123
  * already refused it. Defaults to always-cacheable when omitted.
124
+ *
125
+ * It sees `details` only. Handed the rendered tool text as well, a rule reaches
126
+ * for it and reads a string that leads with provenance and excerpt notes — and an
127
+ * anchored abstention matcher then scores every abstention as an answer.
124
128
  */
125
- cacheable?(details: TDetails, text: string): boolean;
129
+ cacheable?(details: TDetails): boolean;
126
130
  }
127
131
  /** Register a worker tool from its spec, supplying the shared registration ritual. */
128
132
  export declare function makeWorkerTool<TParams extends TSchema, TDetails>(pi: ExtensionAPI, spec: WorkerToolSpec<TParams, TDetails>): void;
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.40.25",
3
+ "version": "0.40.26",
4
4
  "description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",