@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/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
- function macProbeBarrier() {
115
- return process.platform==='darwin'&&hasHistory&&!historyDisabled&&healthy&&
116
- watcher&&!planning&&!recovering&&!planDirty&&!index?.unavailable&&
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
- function probe(row,allowReuse=false) {
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 macDirect=process.platform==='darwin'&&row.plan.qualification==='npm-typescript-direct-v1';
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
- if(cacheable&&allowReuse&&macGuarded&&!forceFull&&cache?.input===key){
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),macDirect?'--redue-validate-queries':'--redue-context'],{
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
- let stillValid=!macDirect;
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
- guardReason='context_or_compiler_queries_changed';
190
+ fullReason='context_or_compiler_queries_changed';
157
191
  timing('daemon.probe_context_validation',started,{outcome:'changed_or_unavailable'});
158
192
  }
159
- if(macDirect&&cache?.guard&&guard!==cache.guard)planDirty=true;
160
- const fullReason=forceFull?'forced_full_reference':guardReason||
161
- (macDirect&&!macGuarded?'mac_validation_basis_unavailable':!cacheable?'generic_probe':
162
- !row.probeCache?'no_validated_result':row.probeCache.input!==key?'inputs_changed':'context_changed');
163
- timing('daemon.probe_reason',started,{reason:fullReason});
164
- metrics.fullProbeReasons??={};metrics.fullProbeReasons[fullReason]=(metrics.fullProbeReasons[fullReason]||0)+1;
165
- metrics.fullProbes=(metrics.fullProbes||0)+1;
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
- if(macDirect)row.probeCache=null;
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':'direct compiler recipe supports reviewed TypeScript 5.5.2, 5.6.3 and 5.9.3 only',
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 incremental or composite compilation needs explicit review',
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,...(macDirect?{schema:1,guard,
242
- output:checkpoint.output,queryData:checkpoint.queryData,stderrHash:values[0][3]}: {})}:null;
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&&r.fingerprintVersion!==1){item.reason='receipt uses earlier input-resolution format';return item;}
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||!row.probeAt||Date.now()-row.probeAt>10000){item.reason=row.probeError||'state probe observation expired';return item;}
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){item.reason='inputs changed during execution or observation gap';return item;}
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]?.[key]!==r.environmentHashes?.[key]))
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), current=checks.filter(r=>r.freshness==='CURRENT'&&r.result==='PASS').length,
359
- failed=checks.filter(r=>r.freshness==='CURRENT'&&r.result==='FAIL').length,
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
- function dehydrate(value) {
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
- for(const row of index.checks.values())probe(row,built.planReused);
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
- inputEventSerial.set(name,(inputEventSerial.get(name)||0)+1);
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
- healthy=false;reason=`observation unavailable: ${classification}; deterministic reconciliation pending`;
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:'current_content_required',
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
- const started=performance.now();await refreshPlan({forceCold:true,reuse:true});
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
- if(healthy)try{const started=performance.now();await catchUp();
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 macReuse=macProbeBarrier();
911
- for(const row of index.checks.values())probe(row,macReuse);
912
- if(macReuse&&process.env.VSTATE_TEST_FAULTS==='1'&&process.env.VSTATE_TEST_MAC_PROBE_SIGNAL){
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
- // Child validation blocks JS event delivery. Finish with another continuity
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(macReuse)try{await catchUp();if(planDirty||pending.size||flushTimer)flush();}
919
- catch(e){observationGap('post_probe_history',e);}
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 must independently validate it in its own environment. The
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&&macProbeBarrier()){
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:1,planId:row.plan.id,
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. Periodic synchronous probes cannot strengthen that cache,
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
- try{await catchUp();
1067
- if(healthy&&!planning&&!recovering){const macReuse=macProbeBarrier();
1068
- for(const row of index.checks.values())probe(row,macReuse);
1069
- if(macReuse){await catchUp();if(planDirty||pending.size||flushTimer)flush();}
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=>{