@mjasnikovs/pi-task 0.40.26 → 0.40.27

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.
@@ -99,6 +99,11 @@ export function splitAtMatches(text, re) {
99
99
  * possible hallucination. Only reachable on non-ASCII text past the chunk ceiling.
100
100
  */
101
101
  export function sliceBytes(s, maxBytes) {
102
+ // A non-positive cap never shrinks the buffer, so the loop below runs forever.
103
+ // Guarded here rather than at the callers: the cap is usually computed, and the
104
+ // next caller to compute one must not have to rediscover this.
105
+ if (maxBytes <= 0)
106
+ return s ? [s] : [];
102
107
  const out = [];
103
108
  let buf = Buffer.from(s, 'utf8');
104
109
  while (buf.length > maxBytes) {
@@ -134,13 +139,19 @@ export function headedSlices(header, body, maxBytes) {
134
139
  const prefixed = `${header}\n${body}`;
135
140
  if (Buffer.byteLength(prefixed, 'utf8') <= maxBytes)
136
141
  return [prefixed];
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.
142
+ // The header is a LABEL, and `chunkReadme` builds it from an unbounded heading line.
143
+ // Left whole it starves the body: a header two bytes short of the cap turned a 200 KB
144
+ // section into 100,000 two-byte chunks, each re-carrying the 8 KB header. Splitting the
145
+ // cap evenly is the one division that needs no tuning, and a cut label still names the
146
+ // source.
147
+ const head = sliceBytes(header, Math.floor(maxBytes / 2))[0] ?? '';
148
+ const room = maxBytes - Buffer.byteLength(`${head}\n`, 'utf8');
149
+ // Only a cap of a byte or two reaches this. Slice the prefixed string and accept the
150
+ // degenerate result: the body pieces carry no provenance — the very defect this
151
+ // function exists to fix, but the alternative here is emitting nothing.
141
152
  if (room <= 0)
142
153
  return sliceBytes(prefixed, maxBytes);
143
- return sliceBytes(body, room).map(slice => `${header}\n${slice}`);
154
+ return sliceBytes(body, room).map(slice => `${head}\n${slice}`);
144
155
  }
145
156
  /**
146
157
  * Chunk a declaration file, one chunk per declaration, each labelled with the
@@ -76,15 +76,6 @@ export interface PiWorkerDocsInternals {
76
76
  npmVersionLookup?: typeof defaultNpmVersionLookup;
77
77
  }
78
78
  export declare function registerPiWorkerDocs(pi: ExtensionAPI, internals?: PiWorkerDocsInternals): void;
79
- /**
80
- * The cache rule for the docs channel, as a NAMED export rather than an anonymous
81
- * property of an adapter literal.
82
- *
83
- * As a property of the adapter literal it would be reachable only through
84
- * `registerTool → execute()`, so a test would have to retype the rule and would then
85
- * assert against its own copy — green even after the shipped rule changed. Exported,
86
- * the test imports the rule it is checking.
87
- */
88
79
  /**
89
80
  * Did the excerpt cite a word the source never wrote?
90
81
  *
@@ -95,6 +86,15 @@ export declare function registerPiWorkerDocs(pi: ExtensionAPI, internals?: PiWor
95
86
  export declare function excerptFabricated(check: {
96
87
  absent: readonly string[];
97
88
  } | undefined): boolean;
89
+ /**
90
+ * The cache rule for the docs channel, as a NAMED export rather than an anonymous
91
+ * property of an adapter literal.
92
+ *
93
+ * As a property of the adapter literal it would be reachable only through
94
+ * `registerTool → execute()`, so a test would have to retype the rule and would then
95
+ * assert against its own copy — green even after the shipped rule changed. Exported,
96
+ * the test imports the rule it is checking.
97
+ */
98
98
  export declare function docsCacheable(d: Pick<DocsDetails, 'typeOnly' | 'excerptVerified' | 'excerptFabricated' | 'abstained'>): boolean;
99
99
  /** The docs cache key: a package's answer is per (module, question), with the question
100
100
  * lowercased and its whitespace collapsed so phrasing variants share one entry. Returns
@@ -389,15 +389,6 @@ export function registerPiWorkerDocs(pi, internals = {}) {
389
389
  cacheable: docsCacheable
390
390
  });
391
391
  }
392
- /**
393
- * The cache rule for the docs channel, as a NAMED export rather than an anonymous
394
- * property of an adapter literal.
395
- *
396
- * As a property of the adapter literal it would be reachable only through
397
- * `registerTool → execute()`, so a test would have to retype the rule and would then
398
- * assert against its own copy — green even after the shipped rule changed. Exported,
399
- * the test imports the rule it is checking.
400
- */
401
392
  /**
402
393
  * Did the excerpt cite a word the source never wrote?
403
394
  *
@@ -408,6 +399,15 @@ export function registerPiWorkerDocs(pi, internals = {}) {
408
399
  export function excerptFabricated(check) {
409
400
  return check !== undefined && check.absent.length > 0;
410
401
  }
402
+ /**
403
+ * The cache rule for the docs channel, as a NAMED export rather than an anonymous
404
+ * property of an adapter literal.
405
+ *
406
+ * As a property of the adapter literal it would be reachable only through
407
+ * `registerTool → execute()`, so a test would have to retype the rule and would then
408
+ * assert against its own copy — green even after the shipped rule changed. Exported,
409
+ * the test imports the rule it is checking.
410
+ */
411
411
  export function docsCacheable(d) {
412
412
  // Answer QUALITY only. Whether there IS an answer is `WorkerOutcome.kind`, and
413
413
  // `makeWorkerTool` has already refused an `unavailable` before reaching here —
@@ -6,6 +6,8 @@ interface FetchDetails {
6
6
  answer?: string;
7
7
  excerpt?: string;
8
8
  excerptVerified?: boolean;
9
+ coverageMiss?: boolean;
10
+ anchoredSection?: string;
9
11
  }
10
12
  interface ProcLike extends EventEmitter {
11
13
  stdout: EventEmitter | null;
@@ -61,6 +61,8 @@ export function registerPiWorkerFetch(pi, internals = {}) {
61
61
  // in the TEXT, not only in details: details are for the harness, and the
62
62
  // worker acts on what it reads.
63
63
  const text = result.nextStep ? `${body}\n\n${result.nextStep}` : body;
64
+ // Named, not inferred: `workerAnswer<T>` reads T off the literal, so a
65
+ // field added here would never reach the declaration the cache rules Pick from.
64
66
  return workerAnswer(text, {
65
67
  childExitCode: 0,
66
68
  answer: result.answer,
@@ -99,8 +99,8 @@ export interface WorkerToolSpec<TParams extends TSchema, TDetails> {
99
99
  * the network fetch + child summariser. Return `null` to opt a particular call
100
100
  * OUT of caching (e.g. a project-source `.` lookup, whose answer the working tree
101
101
  * mutates within a run). Omit entirely and the tool is never cached. The stored
102
- * key is namespaced by tool name — joined with a literal NUL, which is why the
103
- * separator is invisible to `grep` and to a plain editor.
102
+ * key is namespaced by tool name — joined with a NUL, a byte no tool name or key
103
+ * can contain, so no pair of them can collide by concatenation.
104
104
  */
105
105
  cacheKey?(params: Static<TParams>): string | null;
106
106
  /**
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.40.26",
3
+ "version": "0.40.27",
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",