@mjasnikovs/pi-task 0.18.38 → 0.18.39
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/dist/task/auto-orchestrator.js +7 -5
- package/dist/workers/pi-worker-docs.d.ts +7 -0
- package/dist/workers/pi-worker-docs.js +16 -0
- package/dist/workers/research-cache.d.ts +25 -17
- package/dist/workers/research-cache.js +122 -58
- package/dist/workers/shared.d.ts +8 -0
- package/dist/workers/shared.js +0 -0
- package/package.json +1 -1
|
@@ -1518,13 +1518,15 @@ async function handleTaskAutoResume(_args, ctx) {
|
|
|
1518
1518
|
autoRunning = true;
|
|
1519
1519
|
armTerminalCancel(ctx);
|
|
1520
1520
|
try {
|
|
1521
|
-
// Reuse the interrupted run's research-cache id
|
|
1522
|
-
//
|
|
1523
|
-
//
|
|
1524
|
-
//
|
|
1521
|
+
// Reuse the interrupted run's research-cache id, dropping only the entries whose
|
|
1522
|
+
// own package moved version (F10). mx5 run 13 resumed three times and each
|
|
1523
|
+
// resume's fresh id discarded a working 201-entry cache; run 14 then showed a
|
|
1524
|
+
// whole-file freshness gate can never hold on a greenfield run that installs
|
|
1525
|
+
// packages as it goes, so invalidation is per entry. See resumeResearchRun.
|
|
1525
1526
|
const research = await resumeResearchRun(cwd, getConfig().researchCache);
|
|
1526
1527
|
if (research.reused) {
|
|
1527
|
-
logPlanDebug(cwd, `research cache: resume reused ${research.entries} entr(ies)`
|
|
1528
|
+
logPlanDebug(cwd, `research cache: resume reused ${research.entries} entr(ies), `
|
|
1529
|
+
+ `dropped ${research.dropped} stale`);
|
|
1528
1530
|
}
|
|
1529
1531
|
const abort = new AbortController();
|
|
1530
1532
|
// Resume only runs the loop (runTask); no planning children, so the loader
|
|
@@ -5,6 +5,13 @@ import { resolvePackage as defaultResolvePackage } from './docs-resolve.js';
|
|
|
5
5
|
import { retrieveChunks as defaultRetrieveChunks } from './docs-retrieve.js';
|
|
6
6
|
import { npmVersionLookup as defaultNpmVersionLookup } from './npm-version.js';
|
|
7
7
|
import { type SpawnFn } from '../shared/child-process.js';
|
|
8
|
+
/**
|
|
9
|
+
* The package NAME a module specifier belongs to — `hono/client` → `hono`,
|
|
10
|
+
* `@scope/name/sub` → `@scope/name`. The cache stores this (not the raw specifier) as an
|
|
11
|
+
* entry's package provenance, so a subpath lookup is matched against package.json's key
|
|
12
|
+
* and invalidated with its package rather than living forever unmatched.
|
|
13
|
+
*/
|
|
14
|
+
export declare function packageRootOf(module: string): string;
|
|
8
15
|
export interface PiWorkerDocsInternals {
|
|
9
16
|
resolvePackage?: typeof defaultResolvePackage;
|
|
10
17
|
ensureIndexed?: typeof defaultEnsureIndexed;
|
|
@@ -21,6 +21,16 @@ const Params = Type.Object({
|
|
|
21
21
|
description: 'What to extract from the docs. The child pi reads ranked chunks and returns ONLY content answering this.'
|
|
22
22
|
})
|
|
23
23
|
});
|
|
24
|
+
/**
|
|
25
|
+
* The package NAME a module specifier belongs to — `hono/client` → `hono`,
|
|
26
|
+
* `@scope/name/sub` → `@scope/name`. The cache stores this (not the raw specifier) as an
|
|
27
|
+
* entry's package provenance, so a subpath lookup is matched against package.json's key
|
|
28
|
+
* and invalidated with its package rather than living forever unmatched.
|
|
29
|
+
*/
|
|
30
|
+
export function packageRootOf(module) {
|
|
31
|
+
const parts = module.trim().split('/');
|
|
32
|
+
return parts[0].startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
|
|
33
|
+
}
|
|
24
34
|
function pinDetails(pin) {
|
|
25
35
|
return pin ? { versionSource: pin.source, declaredRange: pin.range } : {};
|
|
26
36
|
}
|
|
@@ -249,6 +259,12 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
249
259
|
cacheKey: params => params.module === '.' ?
|
|
250
260
|
null
|
|
251
261
|
: `${normalizeQuery(params.module)}::${normalizeQuery(params.query)}`,
|
|
262
|
+
// Package provenance for per-entry resume invalidation: a docs digest describes
|
|
263
|
+
// one package at one declared version, so a resume drops it only when THAT
|
|
264
|
+
// package moves — an unrelated install no longer discards it. Package names are
|
|
265
|
+
// matched against package.json verbatim (npm names are case-sensitive), unlike
|
|
266
|
+
// the cache key, which normalises for phrasing collisions.
|
|
267
|
+
cachePkg: params => (params.module === '.' ? undefined : packageRootOf(params.module)),
|
|
252
268
|
// Only a completed lookup (child exited 0) is a real answer; not-installed,
|
|
253
269
|
// no-chunks, resolve/cache errors, and aborts omit childExitCode:0 and fall
|
|
254
270
|
// through to a live retry next time.
|
|
@@ -23,31 +23,32 @@ export declare function configureResearchRun(enabled: boolean): string | undefin
|
|
|
23
23
|
*/
|
|
24
24
|
export declare function normalizeQuery(s: string): string;
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* while any add/remove/version-bump does.
|
|
26
|
+
* The project's declared dependency surface as a name→range map, flattened across every
|
|
27
|
+
* dependency block (a package listed in two blocks resolves to the first range seen, in
|
|
28
|
+
* block order — a real manifest does not disagree with itself, and a disagreement can
|
|
29
|
+
* only make the comparison stricter, i.e. re-fetch).
|
|
31
30
|
*
|
|
32
|
-
* Returns undefined when the manifest is missing or unparseable
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* version, which would
|
|
31
|
+
* Returns undefined when the manifest is missing or unparseable: the caller then cannot
|
|
32
|
+
* prove any package's version and must treat every package-scoped entry as unprovable.
|
|
33
|
+
* Deliberately NOT the lockfile: it is rewritten by installs that change no resolved
|
|
34
|
+
* version, which would drop digests for no correctness gain.
|
|
36
35
|
*/
|
|
37
|
-
export declare function
|
|
36
|
+
export declare function depsMap(cwd: string): Promise<Record<string, string> | undefined>;
|
|
38
37
|
/**
|
|
39
|
-
* Resume hook:
|
|
40
|
-
*
|
|
41
|
-
* environment (reused or fresh),
|
|
38
|
+
* Resume hook: keep the interrupted run's cache id and PRUNE the entries the manifest
|
|
39
|
+
* has invalidated, rather than discarding the whole cache because anything moved. Returns
|
|
40
|
+
* the id now stamped into the environment (reused or fresh), how many entries survived,
|
|
41
|
+
* and how many were dropped; undefined id when caching is off.
|
|
42
42
|
*
|
|
43
|
-
*
|
|
44
|
-
* caching disabled, no/corrupt cache file, a file
|
|
45
|
-
*
|
|
43
|
+
* Falls through to a fresh id only where there is nothing to reuse or nothing to reason
|
|
44
|
+
* with: caching disabled, no/corrupt cache file, or a file predating per-entry package
|
|
45
|
+
* provenance (`pkgv`) whose docs entries cannot be told apart from its search entries.
|
|
46
46
|
*/
|
|
47
47
|
export declare function resumeResearchRun(cwd: string, enabled: boolean): Promise<{
|
|
48
48
|
runId: string | undefined;
|
|
49
49
|
reused: boolean;
|
|
50
50
|
entries: number;
|
|
51
|
+
dropped: number;
|
|
51
52
|
}>;
|
|
52
53
|
/**
|
|
53
54
|
* Look up a cached result for `key` in the current run. Returns undefined on a miss,
|
|
@@ -61,6 +62,13 @@ export declare function lookupResearch(cwd: string, runId: string, key: string):
|
|
|
61
62
|
* Store a successful result under `key` for the current run. A file written for a
|
|
62
63
|
* different run id is discarded and started fresh (first write of a new run drops the
|
|
63
64
|
* prior run's contents — self-healing per-run isolation without an explicit clear).
|
|
65
|
+
*
|
|
66
|
+
* `pkg` is the npm package the answer is ABOUT, supplied by package-scoped tools (docs).
|
|
67
|
+
* Its declared version is resolved HERE, at store time, so a package installed mid-run
|
|
68
|
+
* is stamped with the version its own digest was taken against — not with whatever the
|
|
69
|
+
* manifest happened to say at the run's first write. That is what makes a later resume
|
|
70
|
+
* able to prune this one entry instead of the whole file.
|
|
71
|
+
*
|
|
64
72
|
* Best-effort: any failure is swallowed, leaving the caller's live result untouched.
|
|
65
73
|
*/
|
|
66
|
-
export declare function storeResearch(cwd: string, runId: string, key: string, text: string, details: unknown): Promise<void>;
|
|
74
|
+
export declare function storeResearch(cwd: string, runId: string, key: string, text: string, details: unknown, pkg?: string): Promise<void>;
|
|
@@ -35,19 +35,38 @@
|
|
|
35
35
|
* 5-task run and fails badly for a 32-task one.
|
|
36
36
|
*
|
|
37
37
|
* So a resume now REUSES the interrupted run's id — but only on POSITIVE evidence that
|
|
38
|
-
* the digests still describe the same dependency surface.
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
38
|
+
* the digests still describe the same dependency surface.
|
|
39
|
+
*
|
|
40
|
+
* PER-PACKAGE INVALIDATION (mx5 run 14). The first shape of that evidence was one md5
|
|
41
|
+
* over the whole dependency block: any change anywhere ⇒ wipe. That gate can never hold
|
|
42
|
+
* on the projects /task actually builds — a greenfield run ADDS dependencies every few
|
|
43
|
+
* tasks (run 14: hono/zod at T10, shadcn at T25, playwright-ct at T28), so all five of
|
|
44
|
+
* its resumes saw a moved fingerprint and the run ended with ONE cached entry. Adding a
|
|
45
|
+
* package invalidates nothing that was already cached; only the package a digest is
|
|
46
|
+
* ABOUT going stale does.
|
|
47
|
+
*
|
|
48
|
+
* So invalidation is now per entry. Every docs entry records the package it describes
|
|
49
|
+
* and the version declared for it at store time (a structured field on the entry, not
|
|
50
|
+
* something re-parsed out of the key — keys embed the tool name with a \0 separator and
|
|
51
|
+
* are the wrong place to carry meaning). A resume keeps the run id and drops only the
|
|
52
|
+
* entries whose package changed version or left the manifest.
|
|
53
|
+
*
|
|
54
|
+
* STALENESS TRADE, decided deliberately: search and fetch entries are kept
|
|
55
|
+
* unconditionally, even across a dependency bump. A kept search result about package X
|
|
56
|
+
* may describe an older X. That is accepted — docs is the version-sensitive channel (it
|
|
57
|
+
* is version-pinned to the INSTALLED package since pi-worker fixes #1) and search is
|
|
58
|
+
* discovery, where re-running every query on every resume costs far more than the rare
|
|
59
|
+
* staleness costs. A run that must not tolerate it can disable the cache outright.
|
|
60
|
+
*
|
|
61
|
+
* A file written before per-entry provenance shipped (no `pkgv` marker) carries no way
|
|
62
|
+
* to tell its docs entries apart, so it still falls back to a fresh id: every
|
|
63
|
+
* inconclusive path costs time, never correctness.
|
|
44
64
|
*
|
|
45
65
|
* Stored under `.pi-tasks/` (sibling of env-notes.md / contracts.md), which the
|
|
46
66
|
* git-state guard and discardEdits both exclude. Best-effort throughout: any I/O or
|
|
47
67
|
* parse failure falls back to a live fetch — the cache only ever saves time, it can
|
|
48
68
|
* never change an answer or block a worker.
|
|
49
69
|
*/
|
|
50
|
-
import { createHash } from 'node:crypto';
|
|
51
70
|
import * as fsp from 'node:fs/promises';
|
|
52
71
|
import * as path from 'node:path';
|
|
53
72
|
import { tasksDir } from '../task/task-io.js';
|
|
@@ -60,6 +79,12 @@ export const RESEARCH_RUN_ID_ENV = 'PI_TASK_RUN_ID';
|
|
|
60
79
|
* lookups (dozens), so a real run never evicts a still-useful digest.
|
|
61
80
|
*/
|
|
62
81
|
const MAX_ENTRIES = 250;
|
|
82
|
+
/**
|
|
83
|
+
* Schema marker for per-entry package provenance. A file without it was written by a
|
|
84
|
+
* version that stored no `pkg` on its entries, so its docs entries are indistinguishable
|
|
85
|
+
* from its search entries and cannot be pruned selectively ⇒ no reuse.
|
|
86
|
+
*/
|
|
87
|
+
const PKG_PROVENANCE_VERSION = 2;
|
|
63
88
|
export function researchCacheFile(cwd) {
|
|
64
89
|
return path.join(tasksDir(cwd), RESEARCH_CACHE_FILE);
|
|
65
90
|
}
|
|
@@ -100,18 +125,17 @@ export function normalizeQuery(s) {
|
|
|
100
125
|
return s.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
101
126
|
}
|
|
102
127
|
/**
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* while any add/remove/version-bump does.
|
|
128
|
+
* The project's declared dependency surface as a name→range map, flattened across every
|
|
129
|
+
* dependency block (a package listed in two blocks resolves to the first range seen, in
|
|
130
|
+
* block order — a real manifest does not disagree with itself, and a disagreement can
|
|
131
|
+
* only make the comparison stricter, i.e. re-fetch).
|
|
108
132
|
*
|
|
109
|
-
* Returns undefined when the manifest is missing or unparseable
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* version, which would
|
|
133
|
+
* Returns undefined when the manifest is missing or unparseable: the caller then cannot
|
|
134
|
+
* prove any package's version and must treat every package-scoped entry as unprovable.
|
|
135
|
+
* Deliberately NOT the lockfile: it is rewritten by installs that change no resolved
|
|
136
|
+
* version, which would drop digests for no correctness gain.
|
|
113
137
|
*/
|
|
114
|
-
export async function
|
|
138
|
+
export async function depsMap(cwd) {
|
|
115
139
|
try {
|
|
116
140
|
const raw = await fsp.readFile(path.join(cwd, 'package.json'), 'utf8');
|
|
117
141
|
const pkg = JSON.parse(raw);
|
|
@@ -121,49 +145,77 @@ export async function depsFingerprint(cwd) {
|
|
|
121
145
|
'peerDependencies',
|
|
122
146
|
'optionalDependencies'
|
|
123
147
|
];
|
|
124
|
-
const
|
|
148
|
+
const out = {};
|
|
125
149
|
for (const block of blocks) {
|
|
126
150
|
const deps = pkg[block];
|
|
127
151
|
if (!deps || typeof deps !== 'object')
|
|
128
152
|
continue;
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
: 0)
|
|
134
|
-
.map(([k, v]) => `${k}@${String(v)}`);
|
|
135
|
-
if (entries.length > 0)
|
|
136
|
-
parts.push(`${block}:${entries.join(',')}`);
|
|
153
|
+
for (const [name, range] of Object.entries(deps)) {
|
|
154
|
+
if (typeof range === 'string' && !(name in out))
|
|
155
|
+
out[name] = range;
|
|
156
|
+
}
|
|
137
157
|
}
|
|
138
|
-
// No dependency block at all is a real, stable state (a dependency-free repo)
|
|
139
|
-
|
|
140
|
-
return createHash('sha256').update(parts.join('|')).digest('hex').slice(0, 32);
|
|
158
|
+
// No dependency block at all is a real, stable state (a dependency-free repo).
|
|
159
|
+
return out;
|
|
141
160
|
}
|
|
142
161
|
catch {
|
|
143
162
|
return undefined;
|
|
144
163
|
}
|
|
145
164
|
}
|
|
165
|
+
/** The version declared for `pkg`, or undefined when it is not in the manifest. */
|
|
166
|
+
async function declaredVersion(cwd, pkg) {
|
|
167
|
+
return (await depsMap(cwd))?.[pkg];
|
|
168
|
+
}
|
|
146
169
|
/**
|
|
147
|
-
*
|
|
148
|
-
* proves it describes the same dependency surface. Returns the id now stamped into the
|
|
149
|
-
* environment (reused or fresh), or undefined when caching is off.
|
|
170
|
+
* Is this entry still current against the manifest as it stands now?
|
|
150
171
|
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
172
|
+
* - no `pkg` (search/fetch, and docs answers about nothing installable): KEPT — see the
|
|
173
|
+
* staleness trade in the file header.
|
|
174
|
+
* - `pkg` with no recorded version (not in the manifest when stored): KEPT — nothing in
|
|
175
|
+
* package.json can have moved under it.
|
|
176
|
+
* - `pkg` whose declared version is unchanged: KEPT.
|
|
177
|
+
* - `pkg` whose version moved, which left the manifest, or an unreadable manifest
|
|
178
|
+
* (no evidence either way): DROPPED.
|
|
179
|
+
*/
|
|
180
|
+
function entryStillFresh(entry, deps) {
|
|
181
|
+
if (entry.pkg === undefined || entry.pkgVersion === undefined)
|
|
182
|
+
return true;
|
|
183
|
+
return deps?.[entry.pkg] === entry.pkgVersion;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Resume hook: keep the interrupted run's cache id and PRUNE the entries the manifest
|
|
187
|
+
* has invalidated, rather than discarding the whole cache because anything moved. Returns
|
|
188
|
+
* the id now stamped into the environment (reused or fresh), how many entries survived,
|
|
189
|
+
* and how many were dropped; undefined id when caching is off.
|
|
190
|
+
*
|
|
191
|
+
* Falls through to a fresh id only where there is nothing to reuse or nothing to reason
|
|
192
|
+
* with: caching disabled, no/corrupt cache file, or a file predating per-entry package
|
|
193
|
+
* provenance (`pkgv`) whose docs entries cannot be told apart from its search entries.
|
|
154
194
|
*/
|
|
155
195
|
export async function resumeResearchRun(cwd, enabled) {
|
|
156
196
|
if (!enabled) {
|
|
157
197
|
delete process.env[RESEARCH_RUN_ID_ENV];
|
|
158
|
-
return { runId: undefined, reused: false, entries: 0 };
|
|
198
|
+
return { runId: undefined, reused: false, entries: 0, dropped: 0 };
|
|
159
199
|
}
|
|
160
200
|
const file = await readCacheFile(cwd);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
process.env[RESEARCH_RUN_ID_ENV] = file.runId;
|
|
164
|
-
return { runId: file.runId, reused: true, entries: Object.keys(file.entries).length };
|
|
201
|
+
if (!file || file.pkgv !== PKG_PROVENANCE_VERSION) {
|
|
202
|
+
return { runId: configureResearchRun(true), reused: false, entries: 0, dropped: 0 };
|
|
165
203
|
}
|
|
166
|
-
|
|
204
|
+
const deps = await depsMap(cwd);
|
|
205
|
+
const kept = {};
|
|
206
|
+
let dropped = 0;
|
|
207
|
+
for (const [key, entry] of Object.entries(file.entries)) {
|
|
208
|
+
if (entryStillFresh(entry, deps))
|
|
209
|
+
kept[key] = entry;
|
|
210
|
+
else
|
|
211
|
+
dropped++;
|
|
212
|
+
}
|
|
213
|
+
// Persist the pruning now, so a crash between here and the first store cannot leave
|
|
214
|
+
// stale digests behind under a reused id.
|
|
215
|
+
if (dropped > 0)
|
|
216
|
+
await writeCacheFile(cwd, { runId: file.runId, entries: kept, pkgv: file.pkgv });
|
|
217
|
+
process.env[RESEARCH_RUN_ID_ENV] = file.runId;
|
|
218
|
+
return { runId: file.runId, reused: true, entries: Object.keys(kept).length, dropped };
|
|
167
219
|
}
|
|
168
220
|
async function readCacheFile(cwd) {
|
|
169
221
|
try {
|
|
@@ -193,17 +245,43 @@ export async function lookupResearch(cwd, runId, key) {
|
|
|
193
245
|
const entry = file.entries[key];
|
|
194
246
|
return entry ? { text: entry.text, details: entry.details } : undefined;
|
|
195
247
|
}
|
|
248
|
+
/** Write the cache file atomic-ish, so a concurrent reader never sees it half-written. */
|
|
249
|
+
async function writeCacheFile(cwd, out) {
|
|
250
|
+
try {
|
|
251
|
+
await fsp.mkdir(tasksDir(cwd), { recursive: true });
|
|
252
|
+
const tmp = `${researchCacheFile(cwd)}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
253
|
+
await fsp.writeFile(tmp, JSON.stringify(out), 'utf8');
|
|
254
|
+
await fsp.rename(tmp, researchCacheFile(cwd));
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// best-effort cache
|
|
258
|
+
}
|
|
259
|
+
}
|
|
196
260
|
/**
|
|
197
261
|
* Store a successful result under `key` for the current run. A file written for a
|
|
198
262
|
* different run id is discarded and started fresh (first write of a new run drops the
|
|
199
263
|
* prior run's contents — self-healing per-run isolation without an explicit clear).
|
|
264
|
+
*
|
|
265
|
+
* `pkg` is the npm package the answer is ABOUT, supplied by package-scoped tools (docs).
|
|
266
|
+
* Its declared version is resolved HERE, at store time, so a package installed mid-run
|
|
267
|
+
* is stamped with the version its own digest was taken against — not with whatever the
|
|
268
|
+
* manifest happened to say at the run's first write. That is what makes a later resume
|
|
269
|
+
* able to prune this one entry instead of the whole file.
|
|
270
|
+
*
|
|
200
271
|
* Best-effort: any failure is swallowed, leaving the caller's live result untouched.
|
|
201
272
|
*/
|
|
202
|
-
export async function storeResearch(cwd, runId, key, text, details) {
|
|
273
|
+
export async function storeResearch(cwd, runId, key, text, details, pkg) {
|
|
203
274
|
try {
|
|
204
275
|
const existing = await readCacheFile(cwd);
|
|
205
276
|
const entries = existing && existing.runId === runId ? existing.entries : {};
|
|
206
|
-
|
|
277
|
+
const pkgVersion = pkg === undefined ? undefined : await declaredVersion(cwd, pkg);
|
|
278
|
+
entries[key] = {
|
|
279
|
+
text,
|
|
280
|
+
details,
|
|
281
|
+
at: Date.now(),
|
|
282
|
+
...(pkg === undefined ? {} : { pkg }),
|
|
283
|
+
...(pkgVersion === undefined ? {} : { pkgVersion })
|
|
284
|
+
};
|
|
207
285
|
// Evict oldest by write time if over the cap.
|
|
208
286
|
const keys = Object.keys(entries);
|
|
209
287
|
if (keys.length > MAX_ENTRIES) {
|
|
@@ -211,21 +289,7 @@ export async function storeResearch(cwd, runId, key, text, details) {
|
|
|
211
289
|
for (const k of ordered.slice(0, keys.length - MAX_ENTRIES))
|
|
212
290
|
delete entries[k];
|
|
213
291
|
}
|
|
214
|
-
|
|
215
|
-
// resume can prove they are still fresh. Unreadable manifest ⇒ field omitted,
|
|
216
|
-
// which reads as "cannot prove freshness" and simply denies reuse.
|
|
217
|
-
//
|
|
218
|
-
// FROZEN at the run's first write, deliberately: if a task installs a package
|
|
219
|
-
// mid-run and we re-fingerprinted here, the new fingerprint would bless digests
|
|
220
|
-
// taken BEFORE the install as current. Keeping the original means a mid-run
|
|
221
|
-
// install makes a later resume mismatch and re-fetch — the safe direction.
|
|
222
|
-
const deps = existing && existing.runId === runId ? existing.deps : await depsFingerprint(cwd);
|
|
223
|
-
const out = deps === undefined ? { runId, entries } : { runId, entries, deps };
|
|
224
|
-
await fsp.mkdir(tasksDir(cwd), { recursive: true });
|
|
225
|
-
// Atomic-ish write so a concurrent reader never sees a half-written file.
|
|
226
|
-
const tmp = `${researchCacheFile(cwd)}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
227
|
-
await fsp.writeFile(tmp, JSON.stringify(out), 'utf8');
|
|
228
|
-
await fsp.rename(tmp, researchCacheFile(cwd));
|
|
292
|
+
await writeCacheFile(cwd, { runId, entries, pkgv: PKG_PROVENANCE_VERSION });
|
|
229
293
|
}
|
|
230
294
|
catch {
|
|
231
295
|
// best-effort cache
|
package/dist/workers/shared.d.ts
CHANGED
|
@@ -48,6 +48,14 @@ export interface WorkerToolSpec<TParams extends TSchema, TDetails> {
|
|
|
48
48
|
* key is namespaced by tool name, so keys need only be unique within a tool.
|
|
49
49
|
*/
|
|
50
50
|
cacheKey?(params: Static<TParams>): string | null;
|
|
51
|
+
/**
|
|
52
|
+
* The npm package this call's answer is ABOUT, for a package-scoped tool (docs).
|
|
53
|
+
* Recorded as structured provenance on the cache entry so a resume can drop just the
|
|
54
|
+
* entries whose package moved version, instead of the whole run's cache (run 14: a
|
|
55
|
+
* greenfield run adds packages every few tasks, so a whole-file gate never holds).
|
|
56
|
+
* Omit — or return undefined — and the entry survives every resume of its run.
|
|
57
|
+
*/
|
|
58
|
+
cachePkg?(params: Static<TParams>): string | undefined;
|
|
51
59
|
/**
|
|
52
60
|
* Whether a produced result is safe to cache. Only a SUCCESS is memoised — an
|
|
53
61
|
* error, empty, or aborted result must fall through so a transient failure never
|
package/dist/workers/shared.js
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.39",
|
|
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",
|