@redue/cli 0.1.0-alpha.10 → 0.1.0-alpha.12
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 +106 -0
- package/README.md +1 -1
- package/bin/redue.mjs +35 -19
- package/docs/user/agents.md +70 -7
- package/docs/user/configuration.md +17 -0
- package/docs/user/direct-typescript.md +62 -6
- package/docs/user/json.md +1 -0
- package/docs/user/pilot.md +10 -7
- package/docs/user/supported-projects.md +5 -5
- package/package.json +2 -2
- package/src/agent-integration.mjs +115 -25
- package/src/agent-policy.md +24 -66
- package/src/cached-state.mjs +24 -1
- package/src/claude-hook.mjs +148 -0
- package/src/cli.mjs +67 -12
- package/src/daemon.mjs +291 -105
- package/src/decision-validation.mjs +10 -0
- package/src/declared-plan.mjs +14 -2
- package/src/direct-run-certificate.mjs +231 -19
- package/src/direct-typescript-probe.mjs +6 -40
- package/src/direct-typescript.mjs +77 -15
- package/src/history-worker.mjs +19 -16
- package/src/index.mjs +41 -11
- package/src/linux-inotify.py +10 -0
- package/src/observation.mjs +32 -12
- package/src/path-identity.mjs +8 -0
- package/src/plan-worker.mjs +3 -15
- package/src/probe-result.mjs +7 -3
- package/src/run-recorder.cjs +91 -0
- package/src/typescript-input-facts.mjs +33 -7
- package/src/typescript-list.mjs +99 -9
package/src/daemon.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {inflateSync} from 'node:zlib';
|
|
1
|
+
import {inflateSync,deflateSync} from 'node:zlib';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import net from 'node:net';
|
|
@@ -8,15 +8,19 @@ import {Worker} from 'node:worker_threads';
|
|
|
8
8
|
import {fileURLToPath} from 'node:url';
|
|
9
9
|
import {subscribe,hasHistory,backend} from './platform-observation.mjs';
|
|
10
10
|
import {controlEndpoint} from './control-endpoint.mjs';
|
|
11
|
-
import {observedRelative,withinPath,realObservedPath} from './path-identity.mjs';
|
|
11
|
+
import {observedRelative,withinPath,realObservedPath,racyWindowMs} from './path-identity.mjs';
|
|
12
12
|
import micromatch from './glob.mjs';
|
|
13
13
|
import {sha} from './plan.mjs';
|
|
14
|
-
import {InputIndex} from './index.mjs';
|
|
14
|
+
import {InputIndex,dehydrateIndex} from './index.mjs';
|
|
15
15
|
import {permittedRoots} from './installed-inputs.mjs';
|
|
16
16
|
import {assertOwnedStatePlacement} from './owned-state.mjs';
|
|
17
17
|
import {atomicJson,readReceipts,readReceiptSnapshot,receiptRevision} from './state-store.mjs';
|
|
18
|
-
import {history} from './observation.mjs';
|
|
19
|
-
import {inputKey,planGuard} from './decision-validation.mjs';
|
|
18
|
+
import {history,closeHistory} from './observation.mjs';
|
|
19
|
+
import {inputKey,planGuard,toolchainKey} from './decision-validation.mjs';
|
|
20
|
+
import {queriesHold,runFactReason as runFactChanged} from './typescript-list.mjs';
|
|
21
|
+
import {statusSummary} from './cached-state.mjs';
|
|
22
|
+
import {directProject,directProbeValue,reviewedTypeScriptVersions} from './direct-typescript.mjs';
|
|
23
|
+
import {buildInfoPath,recordedCertificate} from './direct-run-certificate.mjs';
|
|
20
24
|
import {timing,mark,traceBase,traceContext,traceScope,traceEnvironment,traceId} from './decision-profile.mjs';
|
|
21
25
|
|
|
22
26
|
const configFile=path.resolve(process.argv[2]);
|
|
@@ -111,12 +115,45 @@ function persistedPlan(bundle) {
|
|
|
111
115
|
return [name,safe];
|
|
112
116
|
}))};
|
|
113
117
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
118
|
+
// Continuity for in-process certificate reuse: every observed change is applied.
|
|
119
|
+
// macOS needs its history cursor; Linux has the synchronize barrier in catchUp.
|
|
120
|
+
// Windows reconciles content at each decision and keeps its own reuse path.
|
|
121
|
+
function continuityBarrier() {
|
|
122
|
+
return process.platform!=='win32'&&(process.platform!=='darwin'||hasHistory&&!historyDisabled)&&
|
|
123
|
+
healthy&&watcher&&!planning&&!recovering&&!planDirty&&!index?.unavailable&&
|
|
117
124
|
!pending.size&&!pendingChecks.size&&!external.size&&verifyRoot();
|
|
118
125
|
}
|
|
119
|
-
|
|
126
|
+
// The basis a full direct probe is checked against: plan guard, observation
|
|
127
|
+
// continuity, and the compiler tree and Node identity taken before the probe.
|
|
128
|
+
function certificateBasis(row) {
|
|
129
|
+
const argv=row.plan.probes[0],compiler=directProject(argv[2],argv[3],{verify:false});
|
|
130
|
+
return {guard:planGuard(index.bundle),gaps:metrics.gaps,
|
|
131
|
+
compiler:JSON.stringify([compiler.tsRoot,compiler.entry,compiler.digest]),
|
|
132
|
+
toolchain:toolchainKey(compiler.tsRoot,argv[0])};
|
|
133
|
+
}
|
|
134
|
+
// Returns null after reusing the last full probe, otherwise why a full probe is needed.
|
|
135
|
+
function reuseCertificate(row,key,allowReuse,forceFull,checked) {
|
|
136
|
+
const started=performance.now(),cache=row.probeCache;
|
|
137
|
+
if(forceFull)return 'forced_full_reference';
|
|
138
|
+
if(!allowReuse||!continuityBarrier())return 'continuity_unavailable';
|
|
139
|
+
if(row.plan.unresolved.length)return 'coverage_unresolved';
|
|
140
|
+
if(!cache?.queries?.length)return 'no_validated_result';
|
|
141
|
+
if(cache.input!==key)return 'inputs_changed';
|
|
142
|
+
if(cache.gaps!==metrics.gaps)return 'observation_gap';
|
|
143
|
+
let reason=null;
|
|
144
|
+
try{const basis=checked.basis=certificateBasis(row);
|
|
145
|
+
if(basis.guard!==cache.guard){planDirty=true;reason='plan_guard_changed';}
|
|
146
|
+
else if(basis.compiler!==cache.compiler||basis.toolchain!==cache.toolchain)reason='toolchain_changed';
|
|
147
|
+
else if(!queriesHold(cache.queries,cache.identities))reason='compiler_queries_changed';}
|
|
148
|
+
catch{reason='validation_unavailable';}
|
|
149
|
+
timing('daemon.probe_validation',started,{outcome:reason?'changed_or_unavailable':'reused',
|
|
150
|
+
reason:reason||'content_and_context_unchanged',queries:cache.queries.length});
|
|
151
|
+
if(reason)return reason;
|
|
152
|
+
row.probeHash=cache.hash;row.probeAt=Date.now();row.probeError=null;
|
|
153
|
+
metrics.probeReuses=(metrics.probeReuses||0)+1;
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
function probe(row,allowReuse=false,{launch=true}={}) {
|
|
120
157
|
const started=performance.now();
|
|
121
158
|
const supported=new Map([
|
|
122
159
|
['typescript-noemit-v1','typescript-contract-probe.mjs'],
|
|
@@ -124,47 +161,49 @@ function probe(row,allowReuse=false) {
|
|
|
124
161
|
['yarn-workspace-tsc-v1','yarn-workspace-typecheck-probe.mjs'],
|
|
125
162
|
['pnpm-tsc-v1','pnpm-typecheck-probe.mjs']]);
|
|
126
163
|
const expected=supported.get(row.plan.qualification);
|
|
127
|
-
const
|
|
128
|
-
const cacheable=(process.platform==='win32'||macDirect)&&row.plan.probes.length===1&&expected&&
|
|
164
|
+
const builtIn=row.plan.probes.length===1&&expected&&
|
|
129
165
|
row.plan.probes[0][1]===fileURLToPath(new URL(expected,import.meta.url));
|
|
166
|
+
const inProcess=builtIn&&process.platform!=='win32'&&row.plan.qualification==='npm-typescript-direct-v1';
|
|
167
|
+
const cacheable=builtIn&&(process.platform==='win32'||inProcess);
|
|
130
168
|
const key=inputKey(row);
|
|
131
|
-
let guard=null,guardReason=null;
|
|
132
|
-
if(macDirect&&cacheable)try{guard=planGuard(index.bundle);}catch{guardReason='plan_guard_unavailable';}
|
|
133
|
-
const cache=row.probeCache;
|
|
134
|
-
const macGuarded=!macDirect||(macProbeBarrier()&&!row.plan.unresolved.length&&
|
|
135
|
-
guard!==null&&cache?.schema===1&&cache.guard===guard&&
|
|
136
|
-
Array.isArray(cache.queries)&&cache.queries.length>0);
|
|
137
169
|
const forceFull=process.env.VSTATE_TEST_FAULTS==='1'&&process.env.VSTATE_TEST_FULL_PROBES==='1';
|
|
138
|
-
|
|
170
|
+
let fullReason=null;const checked={};
|
|
171
|
+
if(inProcess){
|
|
172
|
+
fullReason=reuseCertificate(row,key,allowReuse,forceFull,checked);
|
|
173
|
+
if(!fullReason)return;
|
|
174
|
+
}
|
|
175
|
+
// Idle revalidation never launches a process. An unconfirmed result expires.
|
|
176
|
+
if(!launch&&row.plan.probes.length)return;
|
|
177
|
+
if(process.platform==='win32'&&cacheable&&allowReuse&&!forceFull&&row.probeCache?.input===key){
|
|
139
178
|
const argv=row.plan.probes[0];
|
|
140
179
|
mark('daemon.probe_launch',{mode:'certificate',deadlineMs:10000});
|
|
141
|
-
const out=spawnSync(argv[0],[...argv.slice(1),
|
|
142
|
-
cwd:row.plan.cwd,timeout:10000,maxBuffer:1024*1024,env:traceEnvironment(),
|
|
143
|
-
...(macDirect?{input:JSON.stringify({schema:cache.schema,context:cache.context,queries:cache.queries})}:
|
|
144
|
-
{stdio:['ignore','pipe','pipe']})});
|
|
180
|
+
const out=spawnSync(argv[0],[...argv.slice(1),'--redue-context'],{
|
|
181
|
+
cwd:row.plan.cwd,timeout:10000,maxBuffer:1024*1024,env:traceEnvironment(),stdio:['ignore','pipe','pipe']});
|
|
145
182
|
mark('daemon.probe_complete',{mode:'certificate',childPid:out.pid,exit:out.status,signal:out.signal,code:out.error?.code});
|
|
146
183
|
metrics.probeContextProcesses=(metrics.probeContextProcesses||0)+1;
|
|
147
|
-
|
|
148
|
-
if(macDirect)try{stillValid=macProbeBarrier()&&inputKey(row)===key&&planGuard(index.bundle)===guard;}catch{}
|
|
149
|
-
if(!out.error&&!out.signal&&out.status===0&&
|
|
150
|
-
out.stdout.toString()===cache.context&&stillValid){
|
|
184
|
+
if(!out.error&&!out.signal&&out.status===0&&out.stdout.toString()===row.probeCache.context){
|
|
151
185
|
row.probeHash=row.probeCache.hash;row.probeAt=Date.now();row.probeError=null;
|
|
152
186
|
metrics.probeReuses=(metrics.probeReuses||0)+1;
|
|
153
187
|
timing('daemon.probe_context_validation',started,{outcome:'reused',reason:'content_and_context_unchanged'});
|
|
154
188
|
return;
|
|
155
189
|
}
|
|
156
|
-
|
|
190
|
+
fullReason='context_or_compiler_queries_changed';
|
|
157
191
|
timing('daemon.probe_context_validation',started,{outcome:'changed_or_unavailable'});
|
|
158
192
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
193
|
+
fullReason??=forceFull?'forced_full_reference':!cacheable?'generic_probe':
|
|
194
|
+
!row.probeCache?'no_validated_result':row.probeCache.input!==key?'inputs_changed':'context_changed';
|
|
195
|
+
if(row.plan.probes.length){
|
|
196
|
+
timing('daemon.probe_reason',started,{reason:fullReason});
|
|
197
|
+
metrics.fullProbeReasons??={};metrics.fullProbeReasons[fullReason]=(metrics.fullProbeReasons[fullReason]||0)+1;
|
|
198
|
+
metrics.fullProbes=(metrics.fullProbes||0)+1;
|
|
199
|
+
}
|
|
166
200
|
// A failed or incomplete full probe must not leave an older certificate usable.
|
|
167
|
-
|
|
201
|
+
let basis=checked.basis||null;
|
|
202
|
+
if(inProcess){
|
|
203
|
+
if(!basis)try{basis=certificateBasis(row);}catch{}
|
|
204
|
+
if(row.probeCache?.guard&&basis&&basis.guard!==row.probeCache.guard)planDirty=true;
|
|
205
|
+
row.probeCache=null;
|
|
206
|
+
}
|
|
168
207
|
const values=[];let checkpoint=null;
|
|
169
208
|
for(const argv of row.plan.probes) {
|
|
170
209
|
// Supported TypeScript probes perform bounded input/toolchain hashing.
|
|
@@ -185,7 +224,7 @@ function probe(row,allowReuse=false) {
|
|
|
185
224
|
/VSTATE_REASON:([a-z-]+)/.exec(out.stderr?.toString()||'')?.[1]:null;
|
|
186
225
|
const explanations={
|
|
187
226
|
'direct-typescript-implementation-unreviewed':'installed TypeScript bytes or membership differ from the reviewed compiler',
|
|
188
|
-
'direct-typescript-version-unsupported'
|
|
227
|
+
'direct-typescript-version-unsupported':`direct compiler recipe supports reviewed TypeScript ${reviewedTypeScriptVersions.join(', ')} only`,
|
|
189
228
|
'direct-execution-environment-unsupported':'direct compiler rejects development helpers, Node cache/preloads, and unreviewed runtime overrides',
|
|
190
229
|
'direct-node-version-unsupported':'direct compiler recipe supports Node 22 and 24',
|
|
191
230
|
'npm-launcher-unobserved-inputs':'npm launcher uses unobserved notifier inputs; execution is recording-only',
|
|
@@ -194,7 +233,11 @@ function probe(row,allowReuse=false) {
|
|
|
194
233
|
'typescript-executable-changed':'local TypeScript executable changed or is missing',
|
|
195
234
|
'typescript-config-unavailable':'TypeScript configuration cannot be resolved',
|
|
196
235
|
'typescript-config-graph-unsupported':'TypeScript configuration extends, references, or uses compiler features outside this contract',
|
|
197
|
-
'typescript-incremental-unsupported':'TypeScript
|
|
236
|
+
'typescript-incremental-unsupported':'TypeScript composite compilation needs explicit review',
|
|
237
|
+
'typescript-buildinfo-outside-repository':'TypeScript incremental build-info file is outside the observed repository',
|
|
238
|
+
'typescript-incremental-unsound':'TypeScript assumeChangesOnlyAffectDirectDependencies makes an incremental result depend on build history',
|
|
239
|
+
'typescript-buildinfo-name-unsupported':'TypeScript build-info file needs a plain name ending in .tsbuildinfo',
|
|
240
|
+
'typescript-project-reference-consumed':'TypeScript program uses files of a referenced project, which plain tsc does not build',
|
|
198
241
|
'typescript-config-outside-repository':'TypeScript configuration extends outside the observed repository',
|
|
199
242
|
'typescript-plugin-unsupported':'TypeScript compiler plugin needs explicit review',
|
|
200
243
|
'typescript-input-list-unavailable':'TypeScript input listing did not complete',
|
|
@@ -227,6 +270,10 @@ function probe(row,allowReuse=false) {
|
|
|
227
270
|
try{checkpoint=JSON.parse(out.stdout);
|
|
228
271
|
checkpoint.queries=checkpoint.queryData?JSON.parse(inflateSync(
|
|
229
272
|
Buffer.from(checkpoint.queryData,'base64'),{maxOutputLength:8*1024*1024})):null;
|
|
273
|
+
// Optional: without a layout, a run after an edit falls back to a full probe.
|
|
274
|
+
try{checkpoint.layout=checkpoint.layoutData?JSON.parse(inflateSync(
|
|
275
|
+
Buffer.from(checkpoint.layoutData,'base64'),{maxOutputLength:8*1024*1024})):null;}
|
|
276
|
+
catch{checkpoint.layout=null;}
|
|
230
277
|
if(!/^[a-f0-9]{64}$/.test(checkpoint.output)||!/^[a-f0-9]{64}$/.test(checkpoint.context))
|
|
231
278
|
throw Error('invalid supported probe checkpoint');}
|
|
232
279
|
catch{row.probeHash=null;row.probeAt=0;row.probeError='probe checkpoint unavailable';return;}
|
|
@@ -237,9 +284,11 @@ function probe(row,allowReuse=false) {
|
|
|
237
284
|
if((row.probeHash!==null&&row.probeHash!==value)||row.probeError){
|
|
238
285
|
row.revision++;row.lastReason='declared state probe changed or recovered';}
|
|
239
286
|
row.probeHash=value;row.probeAt=Date.now();row.probeError=null;
|
|
240
|
-
if(checkpoint)row.probeCache=checkpoint.queries?{input:key,context:checkpoint.context,
|
|
241
|
-
hash:value,queries:checkpoint.queries,...(
|
|
242
|
-
|
|
287
|
+
if(checkpoint)row.probeCache=checkpoint.queries&&(!inProcess||basis)?{input:key,context:checkpoint.context,
|
|
288
|
+
hash:value,queries:checkpoint.queries,...(inProcess?{schema:1,...basis,
|
|
289
|
+
identities:new Map(),
|
|
290
|
+
output:checkpoint.output,queryData:checkpoint.queryData,layout:checkpoint.layout,
|
|
291
|
+
stderrHash:values[0][3]}: {})}:null;
|
|
243
292
|
timing('daemon.probe',started,{outcome:'ok',commands:values.length,
|
|
244
293
|
reusable:row.probeCache?1:0,queries:row.probeCache?.queries.length||0});
|
|
245
294
|
}
|
|
@@ -275,7 +324,8 @@ function rows(contextHashes,synchronizedInstalled=false,synchronizedFilesystem=f
|
|
|
275
324
|
plan_id:row.plan.id,observed_at:lastObservation,
|
|
276
325
|
invocation:r?.invocation||null,target_provenance:r?.target||null,
|
|
277
326
|
...(r?.verificationRecipe?{verification_recipe:r.verificationRecipe}:{}),
|
|
278
|
-
coverage_at_run:r?.coverage||null,declared_inputs_match:null,reuse_eligible:false
|
|
327
|
+
coverage_at_run:r?.coverage||null,declared_inputs_match:null,reuse_eligible:false,
|
|
328
|
+
...(r?.runFacts?.length?{run_facts:r.runFacts.length}:{})};
|
|
279
329
|
if(receiptStateError){item.reason=receiptStateError;return item;}
|
|
280
330
|
if(!healthy||planning||recovering||index.unavailable){item.reason=
|
|
281
331
|
(planning||recovering)?'input observation reconciliation pending':
|
|
@@ -286,7 +336,8 @@ function rows(contextHashes,synchronizedInstalled=false,synchronizedFilesystem=f
|
|
|
286
336
|
if(pendingChecks.has(name)){item.reason='filesystem change pending content inspection';return item;}
|
|
287
337
|
if(r&&r.planId) item.declared_inputs_match=r.planId===row.plan.id&&
|
|
288
338
|
r.fingerprint===combinedSummary(name).fingerprint;
|
|
289
|
-
if(r
|
|
339
|
+
if(r&&!(r.fingerprintVersion===1||r.fingerprintVersion===2&&Array.isArray(r.runFacts))){
|
|
340
|
+
item.reason='receipt uses earlier input-resolution format';return item;}
|
|
290
341
|
if(row.plan.unresolved.length){
|
|
291
342
|
const checkpoint=item.declared_inputs_match===false?
|
|
292
343
|
r.planId!==row.plan.id?'; observed input plan changed':
|
|
@@ -295,7 +346,13 @@ function rows(contextHashes,synchronizedInstalled=false,synchronizedFilesystem=f
|
|
|
295
346
|
item.reason='input coverage unresolved: '+row.plan.unresolved.join('; ')+checkpoint+probeIssue;
|
|
296
347
|
return item;
|
|
297
348
|
}
|
|
298
|
-
if(row.probeError
|
|
349
|
+
if(row.probeError){item.reason=row.probeError;return item;}
|
|
350
|
+
// The plan leaves out the build-info file it saw at start; the probe sees the current one.
|
|
351
|
+
if(row.probeCache?.layout&&(row.probeCache.layout.buildInfo||null)!==(row.plan.buildInfo||null)){
|
|
352
|
+
item.reason='the incremental build-info setting changed after the observer started; restart the observer';return item;}
|
|
353
|
+
// An expired probe or missing execution context cannot confirm CURRENT, but
|
|
354
|
+
// observed input content that already differs from the receipt is STALE.
|
|
355
|
+
let unconfirmed=!row.probeAt||Date.now()-row.probeAt>10000?'state probe observation expired':null;
|
|
299
356
|
if(row.plan.synchronizedInstalledRead&&!synchronizedInstalled){
|
|
300
357
|
item.reason='installed inputs require a synchronized content read';return item;}
|
|
301
358
|
if(process.platform==='win32'&&!synchronizedFilesystem){
|
|
@@ -308,15 +365,15 @@ function rows(contextHashes,synchronizedInstalled=false,synchronizedFilesystem=f
|
|
|
308
365
|
item.reason=`target execution ${r.target?.status||'unavailable'}`;return item;}
|
|
309
366
|
if(r.target.status==='failed'&&result!=='FAIL'){
|
|
310
367
|
item.reason='target failure conflicts with outer invocation outcome';return item;}
|
|
311
|
-
if(!r.stable){
|
|
368
|
+
if(!r.stable){const issues=r.checkpoint?.issues?.length?` (${r.checkpoint.issues.join('; ')})`:'';
|
|
369
|
+
item.reason=`inputs changed during execution or observation gap${issues}`;return item;}
|
|
312
370
|
const names=[...(row.plan.environment.variables||[]),
|
|
313
371
|
...(row.plan.environment.prefixes||[]).map(prefix=>
|
|
314
372
|
`__vstate_env_prefix_${prefix.toLowerCase()}__`),
|
|
315
373
|
...(row.plan.environment.pathExecutables||[]).map(name=>
|
|
316
374
|
`__vstate_path_executable_${name}__`),
|
|
317
375
|
...(row.plan.environment.executableIdentity?['__vstate_node_identity__']:[])];
|
|
318
|
-
if(names.length&&!contextHashes?.[name])
|
|
319
|
-
item.reason='execution environment context not supplied';return item;}
|
|
376
|
+
if(names.length&&!contextHashes?.[name])unconfirmed??='execution environment context not supplied';
|
|
320
377
|
item.freshness='STALE';
|
|
321
378
|
if(r.planId!==row.plan.id){
|
|
322
379
|
const before=new Map((r.resolutionLinks||[]).map(link=>[link.path,link.targetHash]));
|
|
@@ -324,7 +381,7 @@ function rows(contextHashes,synchronizedInstalled=false,synchronizedFilesystem=f
|
|
|
324
381
|
item.reason=changed?`installed input link changed: ${changed.path}`:'input plan changed';
|
|
325
382
|
if(changed)item.changed_inputs=[changed.path];
|
|
326
383
|
}
|
|
327
|
-
else if(names.some(key=>contextHashes[name]
|
|
384
|
+
else if(contextHashes?.[name]&&names.some(key=>contextHashes[name][key]!==r.environmentHashes?.[key]))
|
|
328
385
|
item.reason='declared execution environment changed';
|
|
329
386
|
else if(r.fingerprint!==combinedSummary(name).fingerprint) {
|
|
330
387
|
const key=r.fingerprint+'\0'+combinedSummary(name).fingerprint;
|
|
@@ -346,23 +403,82 @@ function rows(contextHashes,synchronizedInstalled=false,synchronizedFilesystem=f
|
|
|
346
403
|
row.staleReasonKey=key;
|
|
347
404
|
}
|
|
348
405
|
item.reason=row.staleReason;item.changed_inputs=row.staleInputs||[];
|
|
406
|
+
if(unconfirmed&&!(r.files&&row.staleInputs?.length)){item.freshness='UNVERIFIED';
|
|
407
|
+
item.reason=unconfirmed;item.changed_inputs=[];}
|
|
408
|
+
} else if(unconfirmed){item.freshness='UNVERIFIED';item.reason=unconfirmed;
|
|
409
|
+
// Lookups the run made outside the certificate are asked again for every
|
|
410
|
+
// answer, cached or synchronized, before it can say CURRENT.
|
|
411
|
+
// A direct run recorded before run facts may have made lookups no probe
|
|
412
|
+
// asks; only Windows, which never records direct runs, keeps reusing them.
|
|
413
|
+
} else if(r.fingerprintVersion===1&&r.verificationRecipe==='npm-direct-typescript@1'&&process.platform!=='win32'){
|
|
414
|
+
item.freshness='UNVERIFIED';item.reason='receipt predates run facts; run the check again';
|
|
415
|
+
} else if(r.runFacts&&(item.reason=runFactReason(r.runFacts))){item.changed_inputs=[];
|
|
349
416
|
} else {item.freshness='CURRENT';item.reason=result==='FAIL'?'last command failed':'declared inputs match';
|
|
350
417
|
item.reuse_eligible=result==='PASS';}
|
|
351
418
|
return item;
|
|
352
419
|
});
|
|
353
420
|
}
|
|
421
|
+
// Null when every run fact in a receipt answers as it did during the run.
|
|
422
|
+
function runFactReason(facts) {
|
|
423
|
+
const started=performance.now(),reason=runFactChanged(facts);
|
|
424
|
+
timing('daemon.run_facts',started,{facts:facts.length,changed:reason?1:0});
|
|
425
|
+
return reason;
|
|
426
|
+
}
|
|
427
|
+
// The indexed inputs already differ from the latest receipt, so no probe result
|
|
428
|
+
// can make this check CURRENT. A sync answers STALE without a full probe. A run
|
|
429
|
+
// probes, or builds the certificate from its recording (adopt), and a revert
|
|
430
|
+
// makes the inputs match and probes again.
|
|
431
|
+
function knownStale(name,row) {
|
|
432
|
+
const r=receipts[name];
|
|
433
|
+
if(!r?.files||r.planId!==row.plan.id||row.probeError||pendingChecks.has(name)||
|
|
434
|
+
row.probeCache?.input===inputKey(row))return false;
|
|
435
|
+
const current=combinedFiles(name),files=Object.keys(current);
|
|
436
|
+
return files.length!==Object.keys(r.files).length||files.some(file=>r.files[file]!==current[file]);
|
|
437
|
+
}
|
|
438
|
+
// A run can rebuild a certificate that no longer applies only because indexed
|
|
439
|
+
// inputs changed: same plan guard, compiler, toolchain and observation.
|
|
440
|
+
function adoptable(row) {
|
|
441
|
+
const cache=row.probeCache;
|
|
442
|
+
if(process.platform==='win32'||row.plan.qualification!=='npm-typescript-direct-v1'||
|
|
443
|
+
row.plan.unresolved.length||row.probeError||cache?.schema!==1||!cache.layout||!cache.queries?.length||
|
|
444
|
+
cache.input===inputKey(row)||cache.gaps!==metrics.gaps||!continuityBarrier())return false;
|
|
445
|
+
try{const basis=certificateBasis(row);
|
|
446
|
+
return basis.guard===cache.guard&&basis.compiler===cache.compiler&&basis.toolchain===cache.toolchain;}
|
|
447
|
+
catch{return false;}
|
|
448
|
+
}
|
|
449
|
+
// The end checkpoint of a run that started without a certificate. Its recorded
|
|
450
|
+
// answers become the candidate certificate when nothing observed changed during
|
|
451
|
+
// the run. The probe that follows validates it, or runs in full as before.
|
|
452
|
+
function adopt(name,row,token) {
|
|
453
|
+
const started=performance.now(),pending=row.adoption,file=path.join(state,`run-record-${token}.json`);
|
|
454
|
+
row.adoption=null;
|
|
455
|
+
let text=null;
|
|
456
|
+
try{text=fs.readFileSync(file,'utf8');}catch{}
|
|
457
|
+
const done=issue=>{timing('daemon.recorded_certificate',started,{outcome:issue?'rejected':'accepted',
|
|
458
|
+
...(issue?{reason:issue}:{})});return issue;};
|
|
459
|
+
if(!pending||pending.token!==token||text===null)return done('recording unavailable');
|
|
460
|
+
if(inputEventSerial.get(name)!==pending.serial||metrics.gaps!==pending.gaps||inputKey(row)!==pending.input||
|
|
461
|
+
row.probeCache!==pending.cache||row.probeHash!==null)
|
|
462
|
+
return done('inputs or certificate changed during the run');
|
|
463
|
+
const cache=pending.cache,argv=row.plan.probes[0];
|
|
464
|
+
let result;
|
|
465
|
+
try{result=recordedCertificate(text,cache,directProject(argv[2],argv[3],{verify:false}),buildInfoPath(row.plan));}
|
|
466
|
+
catch{return done('recording unreadable');}
|
|
467
|
+
if(result.issue)return done(result.issue);
|
|
468
|
+
const identities=new Map(cache.identities);
|
|
469
|
+
for(const changed of result.changed)identities.delete(changed);
|
|
470
|
+
row.probeCache={...cache,hash:directProbeValue(argv,result.output,cache.stderrHash),
|
|
471
|
+
queries:result.queries,identities,output:result.output,layout:result.layout,input:pending.input,recorded:token,
|
|
472
|
+
queryData:deflateSync(JSON.stringify(result.queries)).toString('base64')};
|
|
473
|
+
metrics.recordedCertificates=(metrics.recordedCertificates||0)+1;
|
|
474
|
+
return done(null);
|
|
475
|
+
}
|
|
354
476
|
function status(contextHashes,synchronizedInstalled=false,synchronizedFilesystem=false) {
|
|
355
477
|
// Reload notifications accelerate visibility but are not required for it: a
|
|
356
478
|
// wrapper may persist its outcome even when its control connection is denied.
|
|
357
479
|
refreshReceipts();
|
|
358
|
-
const checks=rows(contextHashes,synchronizedInstalled,synchronizedFilesystem)
|
|
359
|
-
|
|
360
|
-
observedFailed=checks.filter(r=>r.result==='FAIL').length,
|
|
361
|
-
stale=checks.filter(r=>r.freshness==='STALE').length,
|
|
362
|
-
unverified=checks.filter(r=>r.freshness==='UNVERIFIED').length||(!checks.length?1:0);
|
|
363
|
-
const stateName=failed?'failed':unverified?'unverified':stale?'stale':'current';
|
|
364
|
-
const now=Date.now();return {schema:1,receipt_revision:receiptStateRevision,state:stateName,current,failed,observed_failed:observedFailed,
|
|
365
|
-
stale,unverified,checks,
|
|
480
|
+
const checks=rows(contextHashes,synchronizedInstalled,synchronizedFilesystem);
|
|
481
|
+
const now=Date.now();return {schema:1,receipt_revision:receiptStateRevision,...statusSummary(checks),checks,
|
|
366
482
|
observation:{healthy:healthy&&!planning&&!recovering&&!index?.unavailable,
|
|
367
483
|
generation,observed_at:lastObservation,
|
|
368
484
|
events:(index?.eventCount||0)+[...external.values()].reduce((n,e)=>n+(e.index?.eventCount||0),0),
|
|
@@ -397,16 +513,10 @@ function hydrate(bundle,saved) {
|
|
|
397
513
|
}
|
|
398
514
|
next.eventCount=saved.eventCount;next.rehashedFiles=saved.rehashedFiles;
|
|
399
515
|
next.rehashedBytes=saved.rehashedBytes;next.profile=saved.profile;
|
|
516
|
+
next.racy=new Map(saved.racy||[]);
|
|
400
517
|
return next;
|
|
401
518
|
}
|
|
402
|
-
|
|
403
|
-
return {files:[...value.files],
|
|
404
|
-
aliasDependencies:[...value.aliasDependencies].map(([name,refs])=>[name,[...refs]]),
|
|
405
|
-
rows:[...value.checks].map(([name,row])=>({name,files:[...row.files],
|
|
406
|
-
sum:row.sum.toString('hex'),revision:row.revision,lastReason:row.lastReason})),
|
|
407
|
-
eventCount:value.eventCount,rehashedFiles:value.rehashedFiles,
|
|
408
|
-
rehashedBytes:value.rehashedBytes,profile:value.profile};
|
|
409
|
-
}
|
|
519
|
+
const dehydrate=dehydrateIndex;
|
|
410
520
|
function retainIndex() {
|
|
411
521
|
if(!index||!healthy||planning||pending.size||planDirty||historyDisabled||
|
|
412
522
|
!fs.existsSync(snapshotPath))return;
|
|
@@ -451,7 +561,7 @@ function buildInWorker(forceCold=false,reuse=null) {
|
|
|
451
561
|
reject(Error(`input-plan worker exited ${code}`));}});
|
|
452
562
|
});
|
|
453
563
|
}
|
|
454
|
-
async function refreshPlan({forceCold=false,reuse=false}={}) {
|
|
564
|
+
async function refreshPlan({forceCold=false,reuse=false,skip=null}={}) {
|
|
455
565
|
if(planning||stopping)return;
|
|
456
566
|
const totalStarted=performance.now();
|
|
457
567
|
const previousProbes=new Map([...(index?.checks||[])].map(([name,row])=>[name,row.probeCache]));
|
|
@@ -534,7 +644,8 @@ async function refreshPlan({forceCold=false,reuse=false}={}) {
|
|
|
534
644
|
atomic(path.join(state,'plans-v1.json'),persistedPlan(bundle));
|
|
535
645
|
metrics.lastPlanPersistenceMs=performance.now()-persistenceStarted;
|
|
536
646
|
const probeStarted=performance.now();
|
|
537
|
-
|
|
647
|
+
if(skip)refreshReceipts();
|
|
648
|
+
for(const [name,row] of index.checks)if(!skip?.(name,row))probe(row,built.planReused);
|
|
538
649
|
timing('daemon.plan_probes',probeStarted,{checks:index.checks.size});
|
|
539
650
|
lastObservation=Date.now();
|
|
540
651
|
event(built.planReused?'plan_validated':'plan_rebuilt',{checks:Object.keys(bundle.plans),workspace_count:bundle.workspaceCount});
|
|
@@ -630,14 +741,18 @@ function pnpmTopologyChanged(type,rel) {
|
|
|
630
741
|
return stat.isDirectory()||stat.isSymbolicLink();}
|
|
631
742
|
catch(e){return e.code==='ENOENT';}
|
|
632
743
|
}
|
|
633
|
-
function markInputEvent(name,rel,type) {
|
|
744
|
+
function markInputEvent(name,rel,type,replay) {
|
|
634
745
|
pendingChecks.add(name);
|
|
635
|
-
|
|
746
|
+
// macOS delivers each edit twice: live, and again from history replay. The
|
|
747
|
+
// live copy can arrive late, inside a later run. With history, count only the
|
|
748
|
+
// replay copy: the history cursor places it in the right interval.
|
|
749
|
+
if(!hasHistory||historyDisabled||replay)
|
|
750
|
+
inputEventSerial.set(name,(inputEventSerial.get(name)||0)+1);
|
|
636
751
|
// Disposable public CI only. Never enabled for normal/local repositories.
|
|
637
752
|
if(process.env.REDUE_PUBLIC_INPUT_TRACE==='1'&&rel&&diagnosticInputEvents++<5000)
|
|
638
|
-
event('public_input_notification',{path:rel,type:type||'unknown',planning});
|
|
753
|
+
event('public_input_notification',{path:rel,type:type||'unknown',planning,replay});
|
|
639
754
|
}
|
|
640
|
-
function notification(type,filename) {
|
|
755
|
+
function notification(type,filename,replay=false) {
|
|
641
756
|
metrics.notifications++;
|
|
642
757
|
metrics.lastNotificationAt=Date.now();
|
|
643
758
|
if(!filename){observationGap('unnamed_event');return;}
|
|
@@ -645,7 +760,7 @@ function notification(type,filename) {
|
|
|
645
760
|
if(rel==='.git'||rel.startsWith('.git/'))return;
|
|
646
761
|
if(planning) {
|
|
647
762
|
if(index)for(const name of index.candidates(rel))
|
|
648
|
-
if(index.matches(index.checks.get(name),rel))markInputEvent(name,rel,type);
|
|
763
|
+
if(index.matches(index.checks.get(name),rel))markInputEvent(name,rel,type,replay);
|
|
649
764
|
pending.add(rel);metrics.maxQueue=Math.max(metrics.maxQueue,pending.size);
|
|
650
765
|
if(planTrigger(rel)||candidateDescendants.has(rel)||pnpmTopologyChanged(type,rel))
|
|
651
766
|
planDirty=true;
|
|
@@ -669,9 +784,9 @@ function notification(type,filename) {
|
|
|
669
784
|
pending.add(rel);metrics.maxQueue=Math.max(metrics.maxQueue,pending.size);
|
|
670
785
|
if(index) {
|
|
671
786
|
for(const name of index.candidates(rel))
|
|
672
|
-
if(index.matches(index.checks.get(name),rel))markInputEvent(name,rel,type);
|
|
787
|
+
if(index.matches(index.checks.get(name),rel))markInputEvent(name,rel,type,replay);
|
|
673
788
|
for(const [prefix,names] of index.prefixes)
|
|
674
|
-
if(prefix.startsWith(rel+'/'))for(const name of names)markInputEvent(name,rel,type);
|
|
789
|
+
if(prefix.startsWith(rel+'/'))for(const name of names)markInputEvent(name,rel,type,replay);
|
|
675
790
|
}
|
|
676
791
|
if(pending.size>50000){healthy=false;reason='observation backlog overflow';metrics.gaps++;
|
|
677
792
|
event('observation_gap',{classification:'backlog_overflow'});publish();
|
|
@@ -680,23 +795,23 @@ function notification(type,filename) {
|
|
|
680
795
|
if(planDirty){healthy=false;reason='input plan possibly changed';}
|
|
681
796
|
if(planDirty||pendingChecks.size)publish();
|
|
682
797
|
}
|
|
683
|
-
function externalNotification(entry,type,filename) {
|
|
798
|
+
function externalNotification(entry,type,filename,replay=false) {
|
|
684
799
|
metrics.notifications++;
|
|
685
800
|
metrics.lastNotificationAt=Date.now();
|
|
686
801
|
if(!filename){externalGap(entry,'unnamed_event');return;}
|
|
687
802
|
const rel=filename.toString().split(path.sep).join('/');
|
|
688
803
|
const absolute=path.join(entry.root,rel);
|
|
689
804
|
if(planning&&entry.index)for(const name of entry.index.candidates(rel))
|
|
690
|
-
if(entry.index.matches(entry.index.checks.get(name),rel))markInputEvent(name,rel,type);
|
|
805
|
+
if(entry.index.matches(entry.index.checks.get(name),rel))markInputEvent(name,rel,type,replay);
|
|
691
806
|
if(planTrigger(absolute)&&triggerChanged(absolute))planDirty=true;
|
|
692
807
|
if(candidateTriggerChanged(absolute))planDirty=true;
|
|
693
808
|
entry.pending.add(rel);metrics.maxQueue=Math.max(metrics.maxQueue,
|
|
694
809
|
pending.size+[...external.values()].reduce((n,e)=>n+e.pending.size,0));
|
|
695
810
|
if(entry.index) {
|
|
696
811
|
for(const name of entry.index.candidates(rel))
|
|
697
|
-
if(entry.index.matches(entry.index.checks.get(name),rel))markInputEvent(name,rel,type);
|
|
812
|
+
if(entry.index.matches(entry.index.checks.get(name),rel))markInputEvent(name,rel,type,replay);
|
|
698
813
|
for(const [prefix,names] of entry.index.prefixes)
|
|
699
|
-
if(prefix.startsWith(rel+'/'))for(const name of names)markInputEvent(name,rel,type);
|
|
814
|
+
if(prefix.startsWith(rel+'/'))for(const name of names)markInputEvent(name,rel,type,replay);
|
|
700
815
|
}
|
|
701
816
|
if(planDirty){healthy=false;reason='input plan possibly changed';}
|
|
702
817
|
publish();clearTimeout(flushTimer);flushTimer=setTimeout(flush,60);
|
|
@@ -714,7 +829,8 @@ function observationGap(classification,error) {
|
|
|
714
829
|
// Once continuity is uncertain, a cursor is no longer authority. Do not
|
|
715
830
|
// retry native history in this observer generation after reconciliation.
|
|
716
831
|
historyDisabled=true;
|
|
717
|
-
|
|
832
|
+
const cause=error?.message?` (${error.message})`:'';
|
|
833
|
+
healthy=false;reason=`observation unavailable: ${classification}${cause}; deterministic reconciliation pending`;
|
|
718
834
|
metrics.gaps++;
|
|
719
835
|
event('observation_gap',{classification,error_code:error?.code||null});publish();
|
|
720
836
|
// One bounded attempt per gap; a later independent gap may try again.
|
|
@@ -807,7 +923,7 @@ async function catchUp() {
|
|
|
807
923
|
if(!fs.existsSync(snapshotPath))throw Error('historical cursor missing');
|
|
808
924
|
{
|
|
809
925
|
const changes=await queryHistory(root,snapshotPath,'root replay');
|
|
810
|
-
for(const change of changes)notification(change.type,path.relative(root,change.path));
|
|
926
|
+
for(const change of changes)notification(change.type,path.relative(root,change.path),true);
|
|
811
927
|
if(!planning&&(flushTimer||pending.size||planDirty))flush();
|
|
812
928
|
for(const entry of external.values())if(entry.index) {
|
|
813
929
|
try{
|
|
@@ -817,7 +933,7 @@ async function catchUp() {
|
|
|
817
933
|
if(!entry.watcher||!fs.existsSync(entry.snapshot))throw Error('external cursor unavailable');
|
|
818
934
|
const changes=await queryHistory(entry.root,entry.snapshot,'external replay');
|
|
819
935
|
for(const change of changes)externalNotification(entry,change.type,
|
|
820
|
-
path.relative(entry.root,change.path));
|
|
936
|
+
path.relative(entry.root,change.path),true);
|
|
821
937
|
}catch(e){if(e.code==='ETIMEDOUT')throw e;
|
|
822
938
|
externalGap(entry,'historical_replay_failed',e);}
|
|
823
939
|
}
|
|
@@ -853,7 +969,7 @@ async function request(message) {
|
|
|
853
969
|
mark('server.decision_start',{decision,pending:pending.size,planDirty,planning,recovering,healthy,historyDisabled,decisionRequests,historyDeadlineMs,reconciliationDeadlineMs});
|
|
854
970
|
timing('daemon.decision_reason',totalStarted,{decision,action:message.action,
|
|
855
971
|
plan:process.platform==='win32'?'validate_dependencies_before_reuse':'existing_plan',
|
|
856
|
-
probe:'reuse_only_with_identical_content_and_context',installed:'
|
|
972
|
+
probe:'reuse_only_with_identical_content_and_context',installed:'current_identity_required',
|
|
857
973
|
planPhase:metrics.planPhase||'initializing',pending:pending.size,
|
|
858
974
|
notifications:metrics.notifications,planRebuilds:metrics.planRebuilds});
|
|
859
975
|
if(process.platform==='win32'&&process.env.VSTATE_TEST_FAULTS==='1'&&
|
|
@@ -869,9 +985,17 @@ async function request(message) {
|
|
|
869
985
|
// has been disabled: a recovered subscription may still have queued events.
|
|
870
986
|
const needsContentReconciliation=process.platform==='win32'||(hasHistory&&historyDisabled);
|
|
871
987
|
if(needsContentReconciliation&&message.action!=='prelaunch'){
|
|
872
|
-
|
|
988
|
+
// Windows probes inside the reconciliation, so a known-STALE sync skips it there.
|
|
989
|
+
const started=performance.now();
|
|
990
|
+
await refreshPlan({forceCold:true,reuse:true,skip:message.action==='sync'?knownStale:null});
|
|
873
991
|
timing('daemon.request_content_validation',started,{healthy:healthy?1:0});}
|
|
874
|
-
|
|
992
|
+
// Without synchronized installed reads, the continuity barrier runs once,
|
|
993
|
+
// after probing: in-process validation reads the live filesystem, and the
|
|
994
|
+
// barrier then applies every change before the response is computed.
|
|
995
|
+
const deferBarrier=process.platform!=='win32'&&!needsContentReconciliation&&
|
|
996
|
+
message.action!=='prelaunch'&&index&&healthy&&!planning&&![...index.checks].some(([name,row])=>
|
|
997
|
+
row.plan.synchronizedInstalledRead&&(!message.name||message.name===name));
|
|
998
|
+
if(healthy&&!deferBarrier)try{const started=performance.now();await catchUp();
|
|
875
999
|
timing('daemon.request_catch_up',started);}catch(e){observationGap('historical_query',e);}
|
|
876
1000
|
if(flushTimer||pending.size||planDirty||[...external.values()].some(e=>e.pending.size))flush();
|
|
877
1001
|
let synchronizedInstalled=false;
|
|
@@ -906,20 +1030,66 @@ async function request(message) {
|
|
|
906
1030
|
}catch(e){installedRecheck=null;observationGap('installed_recheck',e);}
|
|
907
1031
|
}
|
|
908
1032
|
const probeStarted=performance.now();
|
|
1033
|
+
// A run's start checkpoint skips the full probe when its recording can
|
|
1034
|
+
// replace the certificate; the end checkpoint offers that recording.
|
|
1035
|
+
let adoptIntent=false,recorded;const adopting=new Set();
|
|
909
1036
|
if(index&&healthy&&!validatedColdIndex&&message.action!=='prelaunch'){
|
|
910
|
-
const
|
|
911
|
-
|
|
912
|
-
if(
|
|
1037
|
+
const continuous=continuityBarrier(),probed=index,gaps=metrics.gaps,decided=Date.now();
|
|
1038
|
+
const selected=[...index.checks].filter(([name])=>!message.name||message.name===name);
|
|
1039
|
+
if(message.action==='snapshot'&&typeof message.adopt==='string'&&/^[0-9a-f-]{36}$/.test(message.adopt)){
|
|
1040
|
+
const row=index.checks.get(message.name);
|
|
1041
|
+
recorded=row?adopt(message.name,row,message.adopt):'recording unavailable';
|
|
1042
|
+
}
|
|
1043
|
+
adoptIntent=message.action==='snapshot'&&message.runCertificate===1&&!message.adopt&&continuous;
|
|
1044
|
+
const keys=new Map(selected.map(([name,row])=>[name,inputKey(row)]));
|
|
1045
|
+
const skip=message.action==='sync'?knownStale:()=>false;
|
|
1046
|
+
if(message.action==='sync')refreshReceipts();
|
|
1047
|
+
// With a deferred barrier, validate in-process first and launch any full
|
|
1048
|
+
// probe only after the barrier, so a queued edit costs one probe, not two.
|
|
1049
|
+
for(const [name,row] of selected)if(!skip(name,row))probe(row,continuous,{launch:!deferBarrier&&!adoptIntent});
|
|
1050
|
+
if(continuous&&process.env.VSTATE_TEST_FAULTS==='1'&&process.env.VSTATE_TEST_MAC_PROBE_SIGNAL){
|
|
913
1051
|
fs.writeFileSync(process.env.VSTATE_TEST_MAC_PROBE_SIGNAL,'validated');
|
|
914
1052
|
await new Promise(resolve=>setTimeout(resolve,200));
|
|
915
1053
|
}
|
|
916
|
-
//
|
|
1054
|
+
// Validation and probes block JS event delivery. Finish with a continuity
|
|
917
1055
|
// barrier so queued edits/gaps cannot be published using the pre-probe index.
|
|
918
|
-
if(
|
|
919
|
-
|
|
1056
|
+
if(continuous||deferBarrier)try{const started=performance.now();
|
|
1057
|
+
await catchUp();if(planDirty||pending.size||flushTimer)flush();
|
|
1058
|
+
timing('daemon.request_catch_up',started,{after:'probes'});
|
|
1059
|
+
// Rows not yet confirmed, or whose inputs moved meanwhile, are probed
|
|
1060
|
+
// from the barrier, as an undeferred decision would have done.
|
|
1061
|
+
// Decided once, after the barrier. The start probe value is already out
|
|
1062
|
+
// of date; clearing it means whichever probe comes next sets the value
|
|
1063
|
+
// for this unchanged interval without counting as a change during the run.
|
|
1064
|
+
if(adoptIntent&&index===probed&&healthy&&!planning)for(const [name,row] of selected)
|
|
1065
|
+
if(row.probeAt<decided&&adoptable(row)){
|
|
1066
|
+
adopting.add(name);row.probeHash=null;
|
|
1067
|
+
row.adoption={token:randomUUID(),serial:inputEventSerial.get(name)||0,gaps:metrics.gaps,
|
|
1068
|
+
input:inputKey(row),cache:row.probeCache};
|
|
1069
|
+
}
|
|
1070
|
+
const rest=index===probed&&healthy&&!planning?selected.filter(([name,row])=>!skip(name,row)&&
|
|
1071
|
+
!adopting.has(name)&&(row.probeAt<decided||metrics.gaps!==gaps||inputKey(row)!==keys.get(name))):[];
|
|
1072
|
+
const launched=metrics.fullProbes||0;
|
|
1073
|
+
for(const [,row] of rest)probe(row,continuityBarrier());
|
|
1074
|
+
if(continuous&&(metrics.fullProbes||0)>launched){await catchUp();if(planDirty||pending.size||flushTimer)flush();}
|
|
1075
|
+
}catch(e){observationGap('post_probe_history',e);}
|
|
920
1076
|
}
|
|
921
1077
|
timing('daemon.request_probes',probeStarted,{checks:index?.checks.size||0});
|
|
922
1078
|
lastObservation=Date.now();publish();
|
|
1079
|
+
// A run checkpoint never holds an installed identity from the racy window:
|
|
1080
|
+
// a same-size write in the same timestamp tick would keep it. Wait the
|
|
1081
|
+
// window out (at most 2 s), record those files again, and mark the
|
|
1082
|
+
// checkpoint unusable if one is still inside it.
|
|
1083
|
+
let racy=false;
|
|
1084
|
+
if(message.action==='snapshot'||message.action==='prelaunch'){
|
|
1085
|
+
const names=message.name?[message.name]:null;
|
|
1086
|
+
const indexes=()=>[index,...[...external.values()].map(e=>e.index)].filter(Boolean);
|
|
1087
|
+
const wait=Math.min(racyWindowMs,Math.max(0,...indexes().map(i=>i.racyUntil(names)))-Date.now());
|
|
1088
|
+
if(wait>0){metrics.racyWaits=(metrics.racyWaits||0)+1;await new Promise(resolve=>setTimeout(resolve,wait+5));}
|
|
1089
|
+
for(const i of indexes())i.settleRacy();
|
|
1090
|
+
racy=indexes().some(i=>i.racyUntil(names)>Date.now());
|
|
1091
|
+
if(racy)metrics.racyCheckpoints=(metrics.racyCheckpoints||0)+1;
|
|
1092
|
+
}
|
|
923
1093
|
const applicabilityStarted=performance.now();
|
|
924
1094
|
const data=status(message.contextHashes,synchronizedInstalled,
|
|
925
1095
|
process.platform==='win32'&&healthy&&!planning&&!planDirty);
|
|
@@ -929,22 +1099,38 @@ async function request(message) {
|
|
|
929
1099
|
data.snapshots[name]={...combinedSummary(name),files:combinedFiles(name),
|
|
930
1100
|
planGeneration:row.plan.id,
|
|
931
1101
|
inputEventSerial:inputEventSerial.get(name)||0,
|
|
932
|
-
probeHash:row.probeHash,
|
|
933
|
-
observationHealthy:healthy&&!planning&&!recovering&&applicableExternal(name).every(e=>e.healthy)};
|
|
1102
|
+
probeHash:row.probeHash,inputKey:inputKey(row),observationGaps:metrics.gaps,
|
|
1103
|
+
observationHealthy:healthy&&!planning&&!recovering&&!racy&&applicableExternal(name).every(e=>e.healthy)};
|
|
934
1104
|
// Export only a currently guarded, check-specific discovery certificate.
|
|
935
|
-
// The caller
|
|
1105
|
+
// The caller compares it with the answers its own compile received. Schema 2
|
|
1106
|
+
// certifies the realpath of every directory readDirectory visits. The
|
|
936
1107
|
// snapshot stays the start of the observed interval, including validation.
|
|
937
|
-
if(message.runCertificate===1&&
|
|
1108
|
+
if(message.runCertificate===1&&continuityBarrier()){
|
|
938
1109
|
const row=index?.checks.get(message.name),cache=row?.probeCache;
|
|
1110
|
+
// Also after an accepted recording: the caller compares its record with
|
|
1111
|
+
// this certificate and finds its run facts with it.
|
|
939
1112
|
if(row?.plan.qualification==='npm-typescript-direct-v1'&&
|
|
940
1113
|
!row.plan.unresolved.length&&cache?.schema===1&&cache.input===inputKey(row)&&
|
|
941
1114
|
cache.hash===row.probeHash&&cache.guard===planGuard(index.bundle)&&
|
|
942
1115
|
cache.output&&cache.queryData&&data.snapshots[message.name])
|
|
943
|
-
data.snapshots[message.name].discoveryCertificate={schema:
|
|
1116
|
+
data.snapshots[message.name].discoveryCertificate={schema:2,planId:row.plan.id,
|
|
944
1117
|
cwd:row.plan.cwd,probeDefinition:sha(JSON.stringify(row.plan.probes)),
|
|
945
1118
|
context:cache.context,output:cache.output,stderrHash:cache.stderrHash,
|
|
946
1119
|
queryData:cache.queryData};
|
|
947
1120
|
}
|
|
1121
|
+
// For a run checked by full probes: the last probe's questions, only to
|
|
1122
|
+
// find the run's lookups outside them. Never evidence for reuse.
|
|
1123
|
+
if(message.runFacts===1&&data.snapshots[message.name]){
|
|
1124
|
+
const row=index?.checks.get(message.name),cache=row?.probeCache;
|
|
1125
|
+
if(row?.plan.qualification==='npm-typescript-direct-v1'&&cache?.queryData&&
|
|
1126
|
+
typeof cache.context==='string'&&cache.input===inputKey(row)&&cache.hash===row.probeHash&&
|
|
1127
|
+
cache.guard===planGuard(index.bundle))
|
|
1128
|
+
data.snapshots[message.name].probeQuestions={context:cache.context,queryData:cache.queryData};
|
|
1129
|
+
}
|
|
1130
|
+
const row=index?.checks.get(message.name);
|
|
1131
|
+
if(adopting.has(message.name)&&row?.adoption&&data.snapshots[message.name])
|
|
1132
|
+
data.snapshots[message.name].recordedCertificate=row.adoption.token;
|
|
1133
|
+
if(typeof recorded==='string'&&data.snapshots[message.name])data.snapshots[message.name].recordingIssue=recorded;
|
|
948
1134
|
}
|
|
949
1135
|
timing('daemon.request_applicability',applicabilityStarted,{checks:data.checks.length});
|
|
950
1136
|
timing('daemon.request_total',totalStarted,{action:message.action,
|
|
@@ -968,7 +1154,7 @@ async function shutdown() {
|
|
|
968
1154
|
healthy=true;retainIndex();healthy=false;}
|
|
969
1155
|
} catch(e){event('index_retention_unavailable',{classification:e.name||'Error'});}
|
|
970
1156
|
}
|
|
971
|
-
planningWorker?.terminate().catch(()=>{});
|
|
1157
|
+
closeHistory();planningWorker?.terminate().catch(()=>{});
|
|
972
1158
|
clearInterval(heartbeat);clearInterval(probeTimer);clearTimeout(recoveryTimer);
|
|
973
1159
|
watcher?.unsubscribe().catch(()=>{});server.close();
|
|
974
1160
|
for(const entry of external.values()){
|
|
@@ -1056,21 +1242,21 @@ const heartbeat=setInterval(()=>{
|
|
|
1056
1242
|
}
|
|
1057
1243
|
publish();
|
|
1058
1244
|
},1000);
|
|
1245
|
+
// Idle revalidation keeps probe results fresh for cached status without
|
|
1246
|
+
// launching a process: certificates are replayed in-process, and one that no
|
|
1247
|
+
// longer holds expires so the next decision runs the full probe. macOS history
|
|
1248
|
+
// replay would fork, so cached status there relies on live notifications.
|
|
1059
1249
|
const probeTimer=setInterval(()=>traceScope({request:traceId(),work:'periodic'},async()=>{
|
|
1060
1250
|
mark('server.periodic_start',{pending:pending.size,planDirty,planning,decisionRequests});
|
|
1061
1251
|
// Windows cached reads are conservatively UNVERIFIED until a full decision
|
|
1062
|
-
// reconciliation
|
|
1063
|
-
// but can block control requests behind several seconds of child work.
|
|
1252
|
+
// reconciliation, so idle revalidation cannot strengthen them.
|
|
1064
1253
|
if(process.platform==='win32')return;
|
|
1065
|
-
if(healthy&&index&&!planning&&!decisionRequests){
|
|
1066
|
-
|
|
1067
|
-
if(healthy&&!planning&&!recovering)
|
|
1068
|
-
for(const row of index.checks.values())probe(row,
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
publish();}
|
|
1072
|
-
catch(e){observationGap('periodic_query',e);}
|
|
1073
|
-
}
|
|
1254
|
+
if(healthy&&index&&!planning&&!recovering&&!decisionRequests){
|
|
1255
|
+
try{if(!hasHistory)await catchUp();}catch(e){observationGap('periodic_query',e);}
|
|
1256
|
+
if(healthy&&index&&!planning&&!recovering)
|
|
1257
|
+
for(const row of index.checks.values())probe(row,true,{launch:false});
|
|
1258
|
+
if(planDirty)flush();else publish();
|
|
1259
|
+
}
|
|
1074
1260
|
mark('server.periodic_complete');
|
|
1075
1261
|
}),5000);
|
|
1076
1262
|
process.on('uncaughtExceptionMonitor',error=>{
|