@redue/cli 0.1.0-alpha.2 → 0.1.0-alpha.20
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 +467 -0
- package/README.md +107 -2
- package/bin/redue.mjs +130 -49
- package/docs/README.md +2 -1
- package/docs/user/agents.md +299 -15
- package/docs/user/configuration.md +28 -2
- package/docs/user/direct-typescript.md +209 -0
- package/docs/user/getting-started.md +23 -2
- package/docs/user/json.md +2 -0
- package/docs/user/node-script.md +153 -0
- package/docs/user/npm-launcher-notice.md +42 -0
- package/docs/user/pilot.md +146 -0
- package/docs/user/receipt-selection-notice.md +60 -0
- package/docs/user/supported-projects.md +49 -21
- package/docs/user/tests-direct.md +132 -0
- package/package.json +4 -3
- package/src/agent-integration.mjs +388 -0
- package/src/agent-policy.md +28 -0
- package/src/cached-state.mjs +46 -8
- package/src/claude-hook.mjs +449 -0
- package/src/cli.mjs +263 -40
- package/src/daemon.mjs +433 -94
- package/src/decision-profile.mjs +15 -1
- package/src/decision-validation.mjs +10 -0
- package/src/declared-plan.mjs +29 -6
- package/src/direct-run-certificate.mjs +252 -0
- package/src/direct-typescript-probe.mjs +24 -0
- package/src/direct-typescript.mjs +185 -0
- package/src/glob.mjs +8 -0
- package/src/history-worker.mjs +21 -13
- package/src/index.mjs +72 -19
- package/src/installed-inputs.mjs +7 -1
- package/src/linux-inotify.py +39 -10
- package/src/node-onboarding.mjs +157 -48
- package/src/node-script.mjs +362 -0
- package/src/npm-integrity.mjs +124 -0
- package/src/npm-typecheck-environment.mjs +36 -0
- package/src/npmrc.mjs +16 -0
- package/src/observation.mjs +34 -12
- package/src/path-identity.mjs +8 -0
- package/src/plan-worker.mjs +17 -17
- package/src/pnpm-inputs.mjs +114 -54
- package/src/pnpm-shim-templates.json +180 -0
- package/src/pnpm-shims.mjs +54 -0
- package/src/pnpm-typecheck-probe.mjs +11 -6
- package/src/presentation.mjs +9 -1
- package/src/probe-deadline.mjs +37 -0
- package/src/probe-result.mjs +7 -3
- package/src/project-location.mjs +34 -0
- package/src/run-recorder.cjs +91 -0
- package/src/state-store.mjs +37 -1
- package/src/test-direct.mjs +560 -0
- package/src/test-recorder.cjs +406 -0
- package/src/typescript-contract-probe.mjs +4 -187
- package/src/typescript-input-facts.mjs +92 -0
- package/src/typescript-invocation.mjs +92 -0
- package/src/typescript-list.mjs +108 -13
- package/src/typescript-qualification.mjs +29 -49
- package/src/workspace-manifests.mjs +22 -0
- package/src/yarn-workspace-typecheck-probe.mjs +2 -1
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,16 +8,23 @@ 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
|
-
import {atomicJson,readReceipts} from './state-store.mjs';
|
|
18
|
-
import {history} from './observation.mjs';
|
|
19
|
-
import {inputKey} from './decision-validation.mjs';
|
|
20
|
-
import {
|
|
17
|
+
import {atomicJson,readReceipts,readReceiptSnapshot,receiptRevision} from './state-store.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 {nextProbeHint,probeDeadline,probeFloorMs} from './probe-deadline.mjs';
|
|
22
|
+
import {statusSummary} from './cached-state.mjs';
|
|
23
|
+
import {directProject,directProbeValue} from './direct-typescript.mjs';
|
|
24
|
+
import {npmCacheDir} from './npm-integrity.mjs';
|
|
25
|
+
import {runFromArgv} from './typescript-invocation.mjs';
|
|
26
|
+
import {buildInfoPath,recordedCertificate} from './direct-run-certificate.mjs';
|
|
27
|
+
import {timing,mark,traceBase,traceContext,traceScope,traceEnvironment,traceId} from './decision-profile.mjs';
|
|
21
28
|
|
|
22
29
|
const configFile=path.resolve(process.argv[2]);
|
|
23
30
|
const config=JSON.parse(fs.readFileSync(configFile));
|
|
@@ -31,8 +38,10 @@ const endpoint=controlEndpoint(state),socket=endpoint.address,
|
|
|
31
38
|
fs.mkdirSync(lock); // Existing lock requires operator inspection; never steal it.
|
|
32
39
|
fs.writeFileSync(path.join(lock,'pid'),String(process.pid),{mode:0o600});
|
|
33
40
|
const generation=randomUUID(), startedAt=Date.now();
|
|
41
|
+
traceBase({generation,work:'startup'});
|
|
34
42
|
let index, receipts={}, watcher, healthy=false, reason='initial reconciliation', stopping=false;
|
|
35
43
|
let receiptStateError=null;
|
|
44
|
+
let receiptStateRevision=null;
|
|
36
45
|
const external=new Map();
|
|
37
46
|
for(const check of config.checks)for(const boundary of permittedRoots(root,check.allowedExternalRoots||[]))
|
|
38
47
|
if(!external.has(boundary))external.set(boundary,{root:boundary,watcher:null,index:null,
|
|
@@ -43,6 +52,9 @@ let pending=new Set(), pendingChecks=new Set(), flushTimer, planDirty=false, pla
|
|
|
43
52
|
let decisionRequests=0;
|
|
44
53
|
let decisionSequence=0;
|
|
45
54
|
let inputEventSerial=new Map();
|
|
55
|
+
// The last input-event paths per check, with their serials, so a test run can
|
|
56
|
+
// show that every event during it was one of its own outputs.
|
|
57
|
+
const inputEventPaths=new Map(),eventPathLimit=10000;
|
|
46
58
|
let diagnosticInputEvents=0;
|
|
47
59
|
let synchronizing=null;
|
|
48
60
|
let installedRecheck=null;
|
|
@@ -77,9 +89,20 @@ function event(kind,fields={}) {
|
|
|
77
89
|
repository:root,...fields})+'\n');
|
|
78
90
|
}
|
|
79
91
|
function loadReceipts() {
|
|
80
|
-
try {
|
|
81
|
-
|
|
82
|
-
|
|
92
|
+
try {const snapshot=readReceiptSnapshot(state);receipts=snapshot.value.checks;
|
|
93
|
+
receiptStateRevision=snapshot.revision;receiptStateError=null;}
|
|
94
|
+
catch(e){receiptStateRevision=null;
|
|
95
|
+
// A readable selector can still supply explicitly historical outcomes after
|
|
96
|
+
// restart. The failed snapshot guard remains authoritative for applicability.
|
|
97
|
+
try{receipts=readReceipts(state).checks;}catch{/* retain previous historical rows */}
|
|
98
|
+
const next=`latest receipt unavailable; prior outcome only: ${e.message}`;
|
|
99
|
+
if(receiptStateError!==next)event('receipt_state_unavailable',
|
|
100
|
+
{classification:e.name||'Error'});
|
|
101
|
+
receiptStateError=next;}
|
|
102
|
+
}
|
|
103
|
+
function refreshReceipts() {
|
|
104
|
+
try {if(receiptStateRevision===null||receiptRevision(state)!==receiptStateRevision)loadReceipts();}
|
|
105
|
+
catch {loadReceipts();}
|
|
83
106
|
}
|
|
84
107
|
function persistedPlan(bundle) {
|
|
85
108
|
return {schema:1,provider:bundle.provider,root:bundle.root,discoveredAt:bundle.discoveredAt,
|
|
@@ -98,54 +121,150 @@ function persistedPlan(bundle) {
|
|
|
98
121
|
return [name,safe];
|
|
99
122
|
}))};
|
|
100
123
|
}
|
|
101
|
-
|
|
124
|
+
// Continuity for in-process certificate reuse: every observed change is applied.
|
|
125
|
+
// macOS needs its history cursor; Linux has the synchronize barrier in catchUp.
|
|
126
|
+
// Windows reconciles content at each decision and keeps its own reuse path.
|
|
127
|
+
function continuityBarrier() {
|
|
128
|
+
return process.platform!=='win32'&&(process.platform!=='darwin'||hasHistory&&!historyDisabled)&&
|
|
129
|
+
healthy&&watcher&&!planning&&!recovering&&!planDirty&&!index?.unavailable&&
|
|
130
|
+
!pending.size&&!pendingChecks.size&&!external.size&&verifyRoot();
|
|
131
|
+
}
|
|
132
|
+
// The basis a full direct probe is checked against: plan guard, observation
|
|
133
|
+
// continuity, and the compiler tree and Node identity taken before the probe.
|
|
134
|
+
function certificateBasis(row) {
|
|
135
|
+
const argv=row.plan.probes[0],compiler=directProject(argv[2],runFromArgv(argv.slice(3)),{verify:false});
|
|
136
|
+
return {guard:planGuard(index.bundle),gaps:metrics.gaps,
|
|
137
|
+
compiler:JSON.stringify([compiler.tsRoot,compiler.entry,compiler.digest]),
|
|
138
|
+
toolchain:toolchainKey(compiler.tsRoot,argv[0])};
|
|
139
|
+
}
|
|
140
|
+
// Returns null after reusing the last full probe, otherwise why a full probe is needed.
|
|
141
|
+
function reuseCertificate(row,key,allowReuse,forceFull,checked) {
|
|
142
|
+
const started=performance.now(),cache=row.probeCache;
|
|
143
|
+
if(forceFull)return 'forced_full_reference';
|
|
144
|
+
if(!allowReuse||!continuityBarrier())return 'continuity_unavailable';
|
|
145
|
+
if(row.plan.unresolved.length)return 'coverage_unresolved';
|
|
146
|
+
if(!cache?.queries?.length)return 'no_validated_result';
|
|
147
|
+
if(cache.input!==key)return 'inputs_changed';
|
|
148
|
+
if(cache.gaps!==metrics.gaps)return 'observation_gap';
|
|
149
|
+
let reason=null;
|
|
150
|
+
try{const basis=checked.basis=certificateBasis(row);
|
|
151
|
+
if(basis.guard!==cache.guard){planDirty=true;reason='plan_guard_changed';}
|
|
152
|
+
else if(basis.compiler!==cache.compiler||basis.toolchain!==cache.toolchain)reason='toolchain_changed';
|
|
153
|
+
else if(!queriesHold(cache.queries,cache.identities))reason='compiler_queries_changed';}
|
|
154
|
+
catch{reason='validation_unavailable';}
|
|
155
|
+
timing('daemon.probe_validation',started,{outcome:reason?'changed_or_unavailable':'reused',
|
|
156
|
+
reason:reason||'content_and_context_unchanged',queries:cache.queries.length});
|
|
157
|
+
if(reason)return reason;
|
|
158
|
+
row.probeHash=cache.hash;row.probeAt=Date.now();row.probeError=null;
|
|
159
|
+
metrics.probeReuses=(metrics.probeReuses||0)+1;
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
// The current full-probe deadline for a check; see probe-deadline.mjs. Hints
|
|
163
|
+
// are kept per check name, so a plan rebuild (each Windows decision) keeps them.
|
|
164
|
+
const probeHints=new Map();
|
|
165
|
+
function fullProbeDeadline(row) {
|
|
166
|
+
const run=receipts[row.plan.name]?.invocation;
|
|
167
|
+
return probeDeadline({floor:probeFloorMs(row.plan.qualification),hint:probeHints.get(row.plan.name),
|
|
168
|
+
runMs:run?.status==='exited'?run.durationMs:0});
|
|
169
|
+
}
|
|
170
|
+
function probe(row,allowReuse=false,{launch=true}={}) {
|
|
102
171
|
const started=performance.now();
|
|
103
172
|
const supported=new Map([
|
|
104
173
|
['typescript-noemit-v1','typescript-contract-probe.mjs'],
|
|
174
|
+
['npm-typescript-direct-v1','direct-typescript-probe.mjs'],
|
|
105
175
|
['yarn-workspace-tsc-v1','yarn-workspace-typecheck-probe.mjs'],
|
|
106
176
|
['pnpm-tsc-v1','pnpm-typecheck-probe.mjs']]);
|
|
107
177
|
const expected=supported.get(row.plan.qualification);
|
|
108
|
-
const
|
|
178
|
+
const builtIn=row.plan.probes.length===1&&expected&&
|
|
109
179
|
row.plan.probes[0][1]===fileURLToPath(new URL(expected,import.meta.url));
|
|
180
|
+
const inProcess=builtIn&&process.platform!=='win32'&&row.plan.qualification==='npm-typescript-direct-v1';
|
|
181
|
+
const cacheable=builtIn&&(process.platform==='win32'||inProcess);
|
|
110
182
|
const key=inputKey(row);
|
|
111
|
-
|
|
183
|
+
const forceFull=process.env.VSTATE_TEST_FAULTS==='1'&&process.env.VSTATE_TEST_FULL_PROBES==='1';
|
|
184
|
+
let fullReason=null;const checked={};
|
|
185
|
+
if(inProcess){
|
|
186
|
+
fullReason=reuseCertificate(row,key,allowReuse,forceFull,checked);
|
|
187
|
+
if(!fullReason)return;
|
|
188
|
+
}
|
|
189
|
+
// Idle revalidation never launches a process. An unconfirmed result expires.
|
|
190
|
+
if(!launch&&row.plan.probes.length)return;
|
|
191
|
+
if(process.platform==='win32'&&cacheable&&allowReuse&&!forceFull&&row.probeCache?.input===key){
|
|
112
192
|
const argv=row.plan.probes[0];
|
|
193
|
+
mark('daemon.probe_launch',{mode:'certificate',deadlineMs:10000});
|
|
113
194
|
const out=spawnSync(argv[0],[...argv.slice(1),'--redue-context'],{
|
|
114
|
-
cwd:row.plan.cwd,timeout:10000,maxBuffer:1024*1024,stdio:['ignore','pipe','pipe']});
|
|
115
|
-
|
|
116
|
-
|
|
195
|
+
cwd:row.plan.cwd,timeout:10000,maxBuffer:1024*1024,env:traceEnvironment(),stdio:['ignore','pipe','pipe']});
|
|
196
|
+
mark('daemon.probe_complete',{mode:'certificate',childPid:out.pid,exit:out.status,signal:out.signal,code:out.error?.code});
|
|
197
|
+
metrics.probeContextProcesses=(metrics.probeContextProcesses||0)+1;
|
|
198
|
+
if(!out.error&&!out.signal&&out.status===0&&out.stdout.toString()===row.probeCache.context){
|
|
117
199
|
row.probeHash=row.probeCache.hash;row.probeAt=Date.now();row.probeError=null;
|
|
118
200
|
metrics.probeReuses=(metrics.probeReuses||0)+1;
|
|
119
201
|
timing('daemon.probe_context_validation',started,{outcome:'reused',reason:'content_and_context_unchanged'});
|
|
120
202
|
return;
|
|
121
203
|
}
|
|
204
|
+
fullReason='context_or_compiler_queries_changed';
|
|
122
205
|
timing('daemon.probe_context_validation',started,{outcome:'changed_or_unavailable'});
|
|
123
206
|
}
|
|
124
|
-
|
|
125
|
-
!row.probeCache?'no_validated_result':row.probeCache.input!==key?'inputs_changed':'context_changed'
|
|
207
|
+
fullReason??=forceFull?'forced_full_reference':!cacheable?'generic_probe':
|
|
208
|
+
!row.probeCache?'no_validated_result':row.probeCache.input!==key?'inputs_changed':'context_changed';
|
|
209
|
+
if(row.plan.probes.length){
|
|
210
|
+
timing('daemon.probe_reason',started,{reason:fullReason});
|
|
211
|
+
metrics.fullProbeReasons??={};metrics.fullProbeReasons[fullReason]=(metrics.fullProbeReasons[fullReason]||0)+1;
|
|
212
|
+
metrics.fullProbes=(metrics.fullProbes||0)+1;
|
|
213
|
+
}
|
|
214
|
+
// A failed or incomplete full probe must not leave an older certificate usable.
|
|
215
|
+
let basis=checked.basis||null;
|
|
216
|
+
if(inProcess){
|
|
217
|
+
if(!basis)try{basis=certificateBasis(row);}catch{}
|
|
218
|
+
if(row.probeCache?.guard&&basis&&basis.guard!==row.probeCache.guard)planDirty=true;
|
|
219
|
+
row.probeCache=null;
|
|
220
|
+
}
|
|
126
221
|
const values=[];let checkpoint=null;
|
|
222
|
+
// The slowest of this check's probes sets the hint for all of them.
|
|
223
|
+
const deadline=fullProbeDeadline(row);let hint=0;
|
|
224
|
+
const learn=()=>{if(hint)probeHints.set(row.plan.name,hint);};
|
|
127
225
|
for(const argv of row.plan.probes) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
//
|
|
131
|
-
const
|
|
132
|
-
(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
226
|
+
const timeout=deadline,launched=performance.now();
|
|
227
|
+
mark('daemon.probe_launch',{mode:'full',reason:fullReason,deadlineMs:timeout});
|
|
228
|
+
// A built-in probe gets its deadline, so its inner compiler calls share it.
|
|
229
|
+
const out=spawnSync(argv[0],[...argv.slice(1),...(cacheable?['--redue-checkpoint']:[]),
|
|
230
|
+
...(builtIn?[`--redue-deadline=${timeout}`]:[])],{cwd:row.plan.cwd,
|
|
231
|
+
timeout,maxBuffer:1024*1024,stdio:['ignore','pipe','pipe'],env:traceEnvironment()});
|
|
232
|
+
mark('daemon.probe_complete',{mode:'full',childPid:out.pid,exit:out.status,signal:out.signal,code:out.error?.code});
|
|
233
|
+
const timedOut=out.error?.code==='ETIMEDOUT';
|
|
234
|
+
if(!out.error||timedOut)hint=Math.max(hint,nextProbeHint({durationMs:performance.now()-launched,deadlineMs:timeout,timedOut}));
|
|
136
235
|
if(out.error||out.signal||out.status!==0) {
|
|
236
|
+
learn();
|
|
137
237
|
if(!row.probeError){row.revision++;row.lastReason='declared state probe became unavailable';}
|
|
138
238
|
row.probeHash=null;row.probeAt=0;
|
|
139
|
-
const code=['typescript-noemit-v1','yarn-workspace-tsc-v1','pnpm-tsc-v1']
|
|
239
|
+
const code=['typescript-noemit-v1','npm-typescript-direct-v1','yarn-workspace-tsc-v1','pnpm-tsc-v1']
|
|
140
240
|
.includes(row.plan.qualification)?
|
|
141
241
|
/VSTATE_REASON:([a-z-]+)/.exec(out.stderr?.toString()||'')?.[1]:null;
|
|
142
242
|
const explanations={
|
|
243
|
+
'direct-typescript-implementation-unreviewed':'installed TypeScript files differ from the reviewed compiler or from the tarball package-lock.json names',
|
|
244
|
+
'direct-typescript-version-unsupported':'direct compiler recipe supports TypeScript 5.x and 6.0',
|
|
245
|
+
'direct-typescript-lockfile-source-unsupported':'package-lock.json installs TypeScript from somewhere other than the registry\'s typescript package (an alias, a file or a git source)',
|
|
246
|
+
'npm-shrinkwrap-unsupported':'npm-shrinkwrap.json is not read; the reviewed TypeScript releases still qualify',
|
|
247
|
+
'direct-typescript-lockfile-integrity-missing':'package-lock.json records no integrity for the installed TypeScript',
|
|
248
|
+
'direct-typescript-lockfile-version-differs':'installed TypeScript version differs from package-lock.json; run npm ci',
|
|
249
|
+
'npm-lockfile-unreadable':'package-lock.json cannot be read',
|
|
250
|
+
'npm-lockfile-integrity-unsupported':'package-lock.json records TypeScript integrity with neither sha512 nor sha1',
|
|
251
|
+
'npm-tarball-uncached':`the TypeScript tarball package-lock.json names is not in npm's cache (${npmCacheDir(root)}); run npm ci or npm cache add`,
|
|
252
|
+
'npm-tarball-corrupt':"npm's cached TypeScript tarball does not match its integrity",
|
|
253
|
+
'npm-tarball-invalid':"npm's cached TypeScript tarball cannot be read",
|
|
254
|
+
'npm-tarball-link-unsupported':"npm's cached TypeScript tarball holds links",
|
|
255
|
+
'direct-execution-environment-unsupported':'direct compiler rejects development helpers, Node cache/preloads, and unreviewed runtime overrides',
|
|
256
|
+
'direct-node-version-unsupported':'direct compiler recipe supports Node 22 and 24',
|
|
257
|
+
'npm-launcher-unobserved-inputs':'npm launcher uses unobserved notifier inputs; execution is recording-only',
|
|
143
258
|
'typecheck-script-changed':'typecheck script or lifecycle changed; rerun init or review it',
|
|
144
259
|
'execution-environment-unsupported':'Node/npm preload or configuration environment is unsupported',
|
|
145
260
|
'typescript-executable-changed':'local TypeScript executable changed or is missing',
|
|
146
261
|
'typescript-config-unavailable':'TypeScript configuration cannot be resolved',
|
|
147
262
|
'typescript-config-graph-unsupported':'TypeScript configuration extends, references, or uses compiler features outside this contract',
|
|
148
|
-
'typescript-incremental-unsupported':'TypeScript
|
|
263
|
+
'typescript-incremental-unsupported':'TypeScript composite compilation needs explicit review',
|
|
264
|
+
'typescript-buildinfo-outside-repository':'TypeScript incremental build-info file is outside the observed repository',
|
|
265
|
+
'typescript-incremental-unsound':'TypeScript assumeChangesOnlyAffectDirectDependencies makes an incremental result depend on build history',
|
|
266
|
+
'typescript-buildinfo-name-unsupported':'TypeScript build-info file needs a plain name ending in .tsbuildinfo',
|
|
267
|
+
'typescript-project-reference-consumed':'TypeScript program uses files of a referenced project, which plain tsc does not build',
|
|
149
268
|
'typescript-config-outside-repository':'TypeScript configuration extends outside the observed repository',
|
|
150
269
|
'typescript-plugin-unsupported':'TypeScript compiler plugin needs explicit review',
|
|
151
270
|
'typescript-input-list-unavailable':'TypeScript input listing did not complete',
|
|
@@ -154,6 +273,9 @@ function probe(row,allowReuse=false) {
|
|
|
154
273
|
'npm-installation-unobservable':'npm installation contains unobservable entries',
|
|
155
274
|
'npm-configuration-unsupported':'npm script configuration needs review',
|
|
156
275
|
'typescript-contract-unavailable':'TypeScript applicability probe could not establish inputs',
|
|
276
|
+
'npm-proxy-version-unsupported':'npm proxy context is reviewed only for npm 10.9.2',
|
|
277
|
+
'npm-proxy-context-ambiguous':'conflicting npm proxy environment keys require resolution',
|
|
278
|
+
'npm-proxy-value-unsupported':'npm proxy environment contains an unsupported value',
|
|
157
279
|
'workspace-execution-environment-unsupported':'Yarn/Node launcher environment override needs review',
|
|
158
280
|
'yarn-toolchain-unavailable':'pinned Yarn version or executable is unavailable',
|
|
159
281
|
'yarn-distribution-unobservable':'Corepack Yarn distribution cannot be inspected',
|
|
@@ -161,7 +283,7 @@ function probe(row,allowReuse=false) {
|
|
|
161
283
|
'workspace-typecheck-contract-unavailable':'workspace TypeScript applicability probe could not establish inputs'
|
|
162
284
|
,'pnpm-execution-environment-unsupported':'pnpm or Node launcher environment override needs review'
|
|
163
285
|
,'pnpm-distribution-unobservable':'pnpm executable contents cannot be observed'
|
|
164
|
-
,'pnpm-version-mismatch':'pnpm executable differs from the
|
|
286
|
+
,'pnpm-version-mismatch':'pnpm executable differs from the installed version'
|
|
165
287
|
,'typescript-input-outside-pnpm-contract':'TypeScript reads outside the selected workspace, linked outputs, or installed inputs'
|
|
166
288
|
,'pnpm-input-coverage-unavailable':'pnpm installed-input coverage became unavailable; inspect check details'
|
|
167
289
|
,'pnpm-typecheck-contract-unavailable':'pnpm TypeScript input contract could not be established'
|
|
@@ -175,18 +297,26 @@ function probe(row,allowReuse=false) {
|
|
|
175
297
|
try{checkpoint=JSON.parse(out.stdout);
|
|
176
298
|
checkpoint.queries=checkpoint.queryData?JSON.parse(inflateSync(
|
|
177
299
|
Buffer.from(checkpoint.queryData,'base64'),{maxOutputLength:8*1024*1024})):null;
|
|
300
|
+
// Optional: without a layout, a run after an edit falls back to a full probe.
|
|
301
|
+
try{checkpoint.layout=checkpoint.layoutData?JSON.parse(inflateSync(
|
|
302
|
+
Buffer.from(checkpoint.layoutData,'base64'),{maxOutputLength:8*1024*1024})):null;}
|
|
303
|
+
catch{checkpoint.layout=null;}
|
|
178
304
|
if(!/^[a-f0-9]{64}$/.test(checkpoint.output)||!/^[a-f0-9]{64}$/.test(checkpoint.context))
|
|
179
305
|
throw Error('invalid supported probe checkpoint');}
|
|
180
306
|
catch{row.probeHash=null;row.probeAt=0;row.probeError='probe checkpoint unavailable';return;}
|
|
181
307
|
}
|
|
182
308
|
values.push([argv,out.status,sha(cacheable?checkpoint.output:out.stdout),sha(out.stderr)]);
|
|
183
309
|
}
|
|
310
|
+
learn();
|
|
184
311
|
const value=sha(JSON.stringify(values));
|
|
185
312
|
if((row.probeHash!==null&&row.probeHash!==value)||row.probeError){
|
|
186
313
|
row.revision++;row.lastReason='declared state probe changed or recovered';}
|
|
187
314
|
row.probeHash=value;row.probeAt=Date.now();row.probeError=null;
|
|
188
|
-
if(checkpoint)row.probeCache=checkpoint.queries?{input:key,context:checkpoint.context,
|
|
189
|
-
hash:value,queries:checkpoint.queries
|
|
315
|
+
if(checkpoint)row.probeCache=checkpoint.queries&&(!inProcess||basis)?{input:key,context:checkpoint.context,
|
|
316
|
+
hash:value,queries:checkpoint.queries,...(inProcess?{schema:1,...basis,
|
|
317
|
+
identities:new Map(),
|
|
318
|
+
output:checkpoint.output,queryData:checkpoint.queryData,layout:checkpoint.layout,
|
|
319
|
+
stderrHash:values[0][3]}: {})}:null;
|
|
190
320
|
timing('daemon.probe',started,{outcome:'ok',commands:values.length,
|
|
191
321
|
reusable:row.probeCache?1:0,queries:row.probeCache?.queries.length||0});
|
|
192
322
|
}
|
|
@@ -221,7 +351,10 @@ function rows(contextHashes,synchronizedInstalled=false,synchronizedFilesystem=f
|
|
|
221
351
|
const item={name,result,freshness:'UNVERIFIED',reason:'no receipt',changed_inputs:[],
|
|
222
352
|
plan_id:row.plan.id,observed_at:lastObservation,
|
|
223
353
|
invocation:r?.invocation||null,target_provenance:r?.target||null,
|
|
224
|
-
|
|
354
|
+
...(r?.verificationRecipe?{verification_recipe:r.verificationRecipe}:{}),
|
|
355
|
+
...(r?.recipe?{recipe:r.recipe}:{}),
|
|
356
|
+
coverage_at_run:r?.coverage||null,declared_inputs_match:null,reuse_eligible:false,
|
|
357
|
+
...(r?.runFacts?.length?{run_facts:r.runFacts.length}:{})};
|
|
225
358
|
if(receiptStateError){item.reason=receiptStateError;return item;}
|
|
226
359
|
if(!healthy||planning||recovering||index.unavailable){item.reason=
|
|
227
360
|
(planning||recovering)?'input observation reconciliation pending':
|
|
@@ -232,7 +365,8 @@ function rows(contextHashes,synchronizedInstalled=false,synchronizedFilesystem=f
|
|
|
232
365
|
if(pendingChecks.has(name)){item.reason='filesystem change pending content inspection';return item;}
|
|
233
366
|
if(r&&r.planId) item.declared_inputs_match=r.planId===row.plan.id&&
|
|
234
367
|
r.fingerprint===combinedSummary(name).fingerprint;
|
|
235
|
-
if(r
|
|
368
|
+
if(r&&!(r.fingerprintVersion===1||r.fingerprintVersion===2&&Array.isArray(r.runFacts))){
|
|
369
|
+
item.reason='receipt uses earlier input-resolution format';return item;}
|
|
236
370
|
if(row.plan.unresolved.length){
|
|
237
371
|
const checkpoint=item.declared_inputs_match===false?
|
|
238
372
|
r.planId!==row.plan.id?'; observed input plan changed':
|
|
@@ -241,7 +375,13 @@ function rows(contextHashes,synchronizedInstalled=false,synchronizedFilesystem=f
|
|
|
241
375
|
item.reason='input coverage unresolved: '+row.plan.unresolved.join('; ')+checkpoint+probeIssue;
|
|
242
376
|
return item;
|
|
243
377
|
}
|
|
244
|
-
if(row.probeError
|
|
378
|
+
if(row.probeError){item.reason=row.probeError;return item;}
|
|
379
|
+
// The plan leaves out the build-info file it saw at start; the probe sees the current one.
|
|
380
|
+
if(row.probeCache?.layout&&(row.probeCache.layout.buildInfo||null)!==(row.plan.buildInfo||null)){
|
|
381
|
+
item.reason='the incremental build-info setting changed after the observer started; restart the observer';return item;}
|
|
382
|
+
// An expired probe or missing execution context cannot confirm CURRENT, but
|
|
383
|
+
// observed input content that already differs from the receipt is STALE.
|
|
384
|
+
let unconfirmed=!row.probeAt||Date.now()-row.probeAt>10000?'state probe observation expired':null;
|
|
245
385
|
if(row.plan.synchronizedInstalledRead&&!synchronizedInstalled){
|
|
246
386
|
item.reason='installed inputs require a synchronized content read';return item;}
|
|
247
387
|
if(process.platform==='win32'&&!synchronizedFilesystem){
|
|
@@ -254,15 +394,15 @@ function rows(contextHashes,synchronizedInstalled=false,synchronizedFilesystem=f
|
|
|
254
394
|
item.reason=`target execution ${r.target?.status||'unavailable'}`;return item;}
|
|
255
395
|
if(r.target.status==='failed'&&result!=='FAIL'){
|
|
256
396
|
item.reason='target failure conflicts with outer invocation outcome';return item;}
|
|
257
|
-
if(!r.stable){
|
|
397
|
+
if(!r.stable){const issues=r.checkpoint?.issues?.length?` (${r.checkpoint.issues.join('; ')})`:'';
|
|
398
|
+
item.reason=`inputs changed during execution or observation gap${issues}`;return item;}
|
|
258
399
|
const names=[...(row.plan.environment.variables||[]),
|
|
259
400
|
...(row.plan.environment.prefixes||[]).map(prefix=>
|
|
260
401
|
`__vstate_env_prefix_${prefix.toLowerCase()}__`),
|
|
261
402
|
...(row.plan.environment.pathExecutables||[]).map(name=>
|
|
262
403
|
`__vstate_path_executable_${name}__`),
|
|
263
404
|
...(row.plan.environment.executableIdentity?['__vstate_node_identity__']:[])];
|
|
264
|
-
if(names.length&&!contextHashes?.[name])
|
|
265
|
-
item.reason='execution environment context not supplied';return item;}
|
|
405
|
+
if(names.length&&!contextHashes?.[name])unconfirmed??='execution environment context not supplied';
|
|
266
406
|
item.freshness='STALE';
|
|
267
407
|
if(r.planId!==row.plan.id){
|
|
268
408
|
const before=new Map((r.resolutionLinks||[]).map(link=>[link.path,link.targetHash]));
|
|
@@ -270,8 +410,16 @@ function rows(contextHashes,synchronizedInstalled=false,synchronizedFilesystem=f
|
|
|
270
410
|
item.reason=changed?`installed input link changed: ${changed.path}`:'input plan changed';
|
|
271
411
|
if(changed)item.changed_inputs=[changed.path];
|
|
272
412
|
}
|
|
273
|
-
else if(names.some(key=>contextHashes[name]
|
|
413
|
+
else if(contextHashes?.[name]&&names.some(key=>contextHashes[name][key]!==r.environmentHashes?.[key]))
|
|
274
414
|
item.reason='declared execution environment changed';
|
|
415
|
+
// Every variable a test run read directly must have the same value. The
|
|
416
|
+
// caller compares them and names the list it used; another list confirms nothing.
|
|
417
|
+
else if(r.environmentReads&&contextHashes?.[name]&&
|
|
418
|
+
(contextHashes[name].__redue_env_reads__!==sha(JSON.stringify(r.environmentReads))||contextHashes[name].__redue_env_changed__)){
|
|
419
|
+
if(contextHashes[name].__redue_env_reads__!==sha(JSON.stringify(r.environmentReads))){
|
|
420
|
+
item.freshness='UNVERIFIED';item.reason='environment variables the last run read were not compared';}
|
|
421
|
+
else item.reason=`environment variable ${contextHashes[name].__redue_env_changed__} differs from the last run`;
|
|
422
|
+
}
|
|
275
423
|
else if(r.fingerprint!==combinedSummary(name).fingerprint) {
|
|
276
424
|
const key=r.fingerprint+'\0'+combinedSummary(name).fingerprint;
|
|
277
425
|
if(row.staleReasonKey!==key){
|
|
@@ -285,27 +433,98 @@ function rows(contextHashes,synchronizedInstalled=false,synchronizedFilesystem=f
|
|
|
285
433
|
if(r.files[f]!==current[f]){changed=describeChangedInput(row.plan,f);row.staleInputs=[f];break;}
|
|
286
434
|
}
|
|
287
435
|
row.staleReason=changed||
|
|
288
|
-
(['typescript-noemit-v1','yarn-workspace-tsc-v1','pnpm-tsc-v1']
|
|
436
|
+
(['typescript-noemit-v1','npm-typescript-direct-v1','yarn-workspace-tsc-v1','pnpm-tsc-v1']
|
|
289
437
|
.includes(row.plan.qualification)?
|
|
290
438
|
'TypeScript input set, generated file, or toolchain context changed':
|
|
291
439
|
'declared input content or membership changed');
|
|
292
440
|
row.staleReasonKey=key;
|
|
293
441
|
}
|
|
294
442
|
item.reason=row.staleReason;item.changed_inputs=row.staleInputs||[];
|
|
443
|
+
if(unconfirmed&&!(r.files&&row.staleInputs?.length)){item.freshness='UNVERIFIED';
|
|
444
|
+
item.reason=unconfirmed;item.changed_inputs=[];}
|
|
445
|
+
} else if(unconfirmed){item.freshness='UNVERIFIED';item.reason=unconfirmed;
|
|
446
|
+
// Lookups the run made outside the certificate are asked again for every
|
|
447
|
+
// answer, cached or synchronized, before it can say CURRENT.
|
|
448
|
+
// A direct run recorded before run facts may have made lookups no probe
|
|
449
|
+
// asks; only Windows, which never records direct runs, keeps reusing them.
|
|
450
|
+
} else if(r.fingerprintVersion===1&&r.verificationRecipe==='npm-direct-typescript@1'&&process.platform!=='win32'){
|
|
451
|
+
item.freshness='UNVERIFIED';item.reason='receipt predates run facts; run the check again';
|
|
452
|
+
} else if(r.runFacts&&(item.reason=runFactReason(r.runFacts))){item.changed_inputs=[];
|
|
453
|
+
// A recorded test run that used something REDUE does not observe, or
|
|
454
|
+
// whose result differed from an earlier run on the same inputs.
|
|
455
|
+
} else if(r.guardIssues?.length){item.freshness='UNVERIFIED';
|
|
456
|
+
item.reason=`the run used inputs REDUE does not observe: ${r.guardIssues.slice(0,3).join('; ')}`+
|
|
457
|
+
(r.guardIssues.length>3?`; and ${r.guardIssues.length-3} more`:'');
|
|
458
|
+
} else if(r.nondeterministic){item.freshness='UNVERIFIED';item.reason='runs on these exact inputs had different results';
|
|
295
459
|
} else {item.freshness='CURRENT';item.reason=result==='FAIL'?'last command failed':'declared inputs match';
|
|
296
460
|
item.reuse_eligible=result==='PASS';}
|
|
297
461
|
return item;
|
|
298
462
|
});
|
|
299
463
|
}
|
|
464
|
+
// Null when every run fact in a receipt answers as it did during the run.
|
|
465
|
+
function runFactReason(facts) {
|
|
466
|
+
const started=performance.now(),reason=runFactChanged(facts);
|
|
467
|
+
timing('daemon.run_facts',started,{facts:facts.length,changed:reason?1:0});
|
|
468
|
+
return reason;
|
|
469
|
+
}
|
|
470
|
+
// The indexed inputs already differ from the latest receipt, so no probe result
|
|
471
|
+
// can make this check CURRENT. A sync answers STALE without a full probe. A run
|
|
472
|
+
// probes, or builds the certificate from its recording (adopt), and a revert
|
|
473
|
+
// makes the inputs match and probes again.
|
|
474
|
+
function knownStale(name,row) {
|
|
475
|
+
const r=receipts[name];
|
|
476
|
+
if(!r?.files||r.planId!==row.plan.id||row.probeError||pendingChecks.has(name)||
|
|
477
|
+
row.probeCache?.input===inputKey(row))return false;
|
|
478
|
+
const current=combinedFiles(name),files=Object.keys(current);
|
|
479
|
+
return files.length!==Object.keys(r.files).length||files.some(file=>r.files[file]!==current[file]);
|
|
480
|
+
}
|
|
481
|
+
// A run can rebuild a certificate that no longer applies only because indexed
|
|
482
|
+
// inputs changed: same plan guard, compiler, toolchain and observation.
|
|
483
|
+
function adoptable(row) {
|
|
484
|
+
const cache=row.probeCache;
|
|
485
|
+
if(process.platform==='win32'||row.plan.qualification!=='npm-typescript-direct-v1'||
|
|
486
|
+
row.plan.unresolved.length||row.probeError||cache?.schema!==1||!cache.layout||!cache.queries?.length||
|
|
487
|
+
cache.input===inputKey(row)||cache.gaps!==metrics.gaps||!continuityBarrier())return false;
|
|
488
|
+
try{const basis=certificateBasis(row);
|
|
489
|
+
return basis.guard===cache.guard&&basis.compiler===cache.compiler&&basis.toolchain===cache.toolchain;}
|
|
490
|
+
catch{return false;}
|
|
491
|
+
}
|
|
492
|
+
// The end checkpoint of a run that started without a certificate. Its recorded
|
|
493
|
+
// answers become the candidate certificate when nothing observed changed during
|
|
494
|
+
// the run. The probe that follows validates it, or runs in full as before.
|
|
495
|
+
function adopt(name,row,token) {
|
|
496
|
+
const started=performance.now(),pending=row.adoption,file=path.join(state,`run-record-${token}.json`);
|
|
497
|
+
row.adoption=null;
|
|
498
|
+
let text=null;
|
|
499
|
+
try{text=fs.readFileSync(file,'utf8');}catch{}
|
|
500
|
+
const done=issue=>{timing('daemon.recorded_certificate',started,{outcome:issue?'rejected':'accepted',
|
|
501
|
+
...(issue?{reason:issue}:{})});return issue;};
|
|
502
|
+
if(!pending||pending.token!==token||text===null)return done('recording unavailable');
|
|
503
|
+
if(inputEventSerial.get(name)!==pending.serial||metrics.gaps!==pending.gaps||inputKey(row)!==pending.input||
|
|
504
|
+
row.probeCache!==pending.cache||row.probeHash!==null)
|
|
505
|
+
return done('inputs or certificate changed during the run');
|
|
506
|
+
const cache=pending.cache,argv=row.plan.probes[0];
|
|
507
|
+
let result;
|
|
508
|
+
try{result=recordedCertificate(text,cache,directProject(argv[2],runFromArgv(argv.slice(3)),{verify:false}),buildInfoPath(row.plan));}
|
|
509
|
+
catch{return done('recording unreadable');}
|
|
510
|
+
if(result.issue)return done(result.issue);
|
|
511
|
+
const identities=new Map(cache.identities);
|
|
512
|
+
for(const changed of result.changed)identities.delete(changed);
|
|
513
|
+
row.probeCache={...cache,hash:directProbeValue(argv,result.output,cache.stderrHash),
|
|
514
|
+
queries:result.queries,identities,output:result.output,layout:result.layout,input:pending.input,recorded:token,
|
|
515
|
+
queryData:deflateSync(JSON.stringify(result.queries)).toString('base64')};
|
|
516
|
+
metrics.recordedCertificates=(metrics.recordedCertificates||0)+1;
|
|
517
|
+
return done(null);
|
|
518
|
+
}
|
|
300
519
|
function status(contextHashes,synchronizedInstalled=false,synchronizedFilesystem=false) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
const now=Date.now();return {schema:1,
|
|
308
|
-
|
|
520
|
+
// Reload notifications accelerate visibility but are not required for it: a
|
|
521
|
+
// wrapper may persist its outcome even when its control connection is denied.
|
|
522
|
+
refreshReceipts();
|
|
523
|
+
const checks=rows(contextHashes,synchronizedInstalled,synchronizedFilesystem);
|
|
524
|
+
// A decision may run every check's probes in turn: callers wait for all of them.
|
|
525
|
+
const probing=[...(index?.checks.values()||[])].filter(row=>row.plan.probes.length);
|
|
526
|
+
const now=Date.now();return {schema:1,receipt_revision:receiptStateRevision,...statusSummary(checks),checks,
|
|
527
|
+
...(probing.length?{probe_deadline_ms:probing.reduce((sum,row)=>sum+fullProbeDeadline(row)*row.plan.probes.length,0)}:{}),
|
|
309
528
|
observation:{healthy:healthy&&!planning&&!recovering&&!index?.unavailable,
|
|
310
529
|
generation,observed_at:lastObservation,
|
|
311
530
|
events:(index?.eventCount||0)+[...external.values()].reduce((n,e)=>n+(e.index?.eventCount||0),0),
|
|
@@ -340,16 +559,10 @@ function hydrate(bundle,saved) {
|
|
|
340
559
|
}
|
|
341
560
|
next.eventCount=saved.eventCount;next.rehashedFiles=saved.rehashedFiles;
|
|
342
561
|
next.rehashedBytes=saved.rehashedBytes;next.profile=saved.profile;
|
|
562
|
+
next.racy=new Map(saved.racy||[]);
|
|
343
563
|
return next;
|
|
344
564
|
}
|
|
345
|
-
|
|
346
|
-
return {files:[...value.files],
|
|
347
|
-
aliasDependencies:[...value.aliasDependencies].map(([name,refs])=>[name,[...refs]]),
|
|
348
|
-
rows:[...value.checks].map(([name,row])=>({name,files:[...row.files],
|
|
349
|
-
sum:row.sum.toString('hex'),revision:row.revision,lastReason:row.lastReason})),
|
|
350
|
-
eventCount:value.eventCount,rehashedFiles:value.rehashedFiles,
|
|
351
|
-
rehashedBytes:value.rehashedBytes,profile:value.profile};
|
|
352
|
-
}
|
|
565
|
+
const dehydrate=dehydrateIndex;
|
|
353
566
|
function retainIndex() {
|
|
354
567
|
if(!index||!healthy||planning||pending.size||planDirty||historyDisabled||
|
|
355
568
|
!fs.existsSync(snapshotPath))return;
|
|
@@ -369,7 +582,7 @@ function buildInWorker(forceCold=false,reuse=null) {
|
|
|
369
582
|
return new Promise((resolve,reject)=>{
|
|
370
583
|
const workerStarted=performance.now();
|
|
371
584
|
const file=path.join(path.dirname(fileURLToPath(import.meta.url)),'plan-worker.mjs');
|
|
372
|
-
const worker=new Worker(file,{workerData:{config:configFile,state,forceCold,reuse,
|
|
585
|
+
const worker=new Worker(file,{workerData:{config:configFile,state,forceCold,reuse,diagnostic:traceContext(),
|
|
373
586
|
reconcileFaultFile:process.env.VSTATE_TEST_RECONCILE_FAULT_FILE}});
|
|
374
587
|
planningWorker=worker;let settled=false;
|
|
375
588
|
const deadline=setTimeout(()=>{
|
|
@@ -394,7 +607,7 @@ function buildInWorker(forceCold=false,reuse=null) {
|
|
|
394
607
|
reject(Error(`input-plan worker exited ${code}`));}});
|
|
395
608
|
});
|
|
396
609
|
}
|
|
397
|
-
async function refreshPlan({forceCold=false,reuse=false}={}) {
|
|
610
|
+
async function refreshPlan({forceCold=false,reuse=false,skip=null}={}) {
|
|
398
611
|
if(planning||stopping)return;
|
|
399
612
|
const totalStarted=performance.now();
|
|
400
613
|
const previousProbes=new Map([...(index?.checks||[])].map(([name,row])=>[name,row.probeCache]));
|
|
@@ -477,7 +690,8 @@ async function refreshPlan({forceCold=false,reuse=false}={}) {
|
|
|
477
690
|
atomic(path.join(state,'plans-v1.json'),persistedPlan(bundle));
|
|
478
691
|
metrics.lastPlanPersistenceMs=performance.now()-persistenceStarted;
|
|
479
692
|
const probeStarted=performance.now();
|
|
480
|
-
|
|
693
|
+
if(skip)refreshReceipts();
|
|
694
|
+
for(const [name,row] of index.checks)if(!skip?.(name,row))probe(row,built.planReused);
|
|
481
695
|
timing('daemon.plan_probes',probeStarted,{checks:index.checks.size});
|
|
482
696
|
lastObservation=Date.now();
|
|
483
697
|
event(built.planReused?'plan_validated':'plan_rebuilt',{checks:Object.keys(bundle.plans),workspace_count:bundle.workspaceCount});
|
|
@@ -573,14 +787,25 @@ function pnpmTopologyChanged(type,rel) {
|
|
|
573
787
|
return stat.isDirectory()||stat.isSymbolicLink();}
|
|
574
788
|
catch(e){return e.code==='ENOENT';}
|
|
575
789
|
}
|
|
576
|
-
function markInputEvent(name,rel,type) {
|
|
790
|
+
function markInputEvent(name,rel,type,replay) {
|
|
577
791
|
pendingChecks.add(name);
|
|
578
|
-
|
|
792
|
+
// macOS delivers each edit twice: live, and again from history replay. The
|
|
793
|
+
// live copy can arrive late, inside a later run. With history, count only the
|
|
794
|
+
// replay copy: the history cursor places it in the right interval.
|
|
795
|
+
if(!hasHistory||historyDisabled||replay){
|
|
796
|
+
const serial=(inputEventSerial.get(name)||0)+1;
|
|
797
|
+
inputEventSerial.set(name,serial);
|
|
798
|
+
// Kept only for recorded test and script checks; trimmed in batches.
|
|
799
|
+
if(['npm-tests-direct-v1','node-script-v1'].includes(index?.checks.get(name)?.plan.qualification)){
|
|
800
|
+
const list=inputEventPaths.get(name)||[];list.push([serial,rel]);
|
|
801
|
+
if(list.length>2*eventPathLimit)list.splice(0,list.length-eventPathLimit);inputEventPaths.set(name,list);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
579
804
|
// Disposable public CI only. Never enabled for normal/local repositories.
|
|
580
805
|
if(process.env.REDUE_PUBLIC_INPUT_TRACE==='1'&&rel&&diagnosticInputEvents++<5000)
|
|
581
|
-
event('public_input_notification',{path:rel,type:type||'unknown',planning});
|
|
806
|
+
event('public_input_notification',{path:rel,type:type||'unknown',planning,replay});
|
|
582
807
|
}
|
|
583
|
-
function notification(type,filename) {
|
|
808
|
+
function notification(type,filename,replay=false) {
|
|
584
809
|
metrics.notifications++;
|
|
585
810
|
metrics.lastNotificationAt=Date.now();
|
|
586
811
|
if(!filename){observationGap('unnamed_event');return;}
|
|
@@ -588,7 +813,7 @@ function notification(type,filename) {
|
|
|
588
813
|
if(rel==='.git'||rel.startsWith('.git/'))return;
|
|
589
814
|
if(planning) {
|
|
590
815
|
if(index)for(const name of index.candidates(rel))
|
|
591
|
-
if(index.matches(index.checks.get(name),rel))markInputEvent(name,rel,type);
|
|
816
|
+
if(index.matches(index.checks.get(name),rel))markInputEvent(name,rel,type,replay);
|
|
592
817
|
pending.add(rel);metrics.maxQueue=Math.max(metrics.maxQueue,pending.size);
|
|
593
818
|
if(planTrigger(rel)||candidateDescendants.has(rel)||pnpmTopologyChanged(type,rel))
|
|
594
819
|
planDirty=true;
|
|
@@ -612,9 +837,9 @@ function notification(type,filename) {
|
|
|
612
837
|
pending.add(rel);metrics.maxQueue=Math.max(metrics.maxQueue,pending.size);
|
|
613
838
|
if(index) {
|
|
614
839
|
for(const name of index.candidates(rel))
|
|
615
|
-
if(index.matches(index.checks.get(name),rel))markInputEvent(name,rel,type);
|
|
840
|
+
if(index.matches(index.checks.get(name),rel))markInputEvent(name,rel,type,replay);
|
|
616
841
|
for(const [prefix,names] of index.prefixes)
|
|
617
|
-
if(prefix.startsWith(rel+'/'))for(const name of names)markInputEvent(name,rel,type);
|
|
842
|
+
if(prefix.startsWith(rel+'/'))for(const name of names)markInputEvent(name,rel,type,replay);
|
|
618
843
|
}
|
|
619
844
|
if(pending.size>50000){healthy=false;reason='observation backlog overflow';metrics.gaps++;
|
|
620
845
|
event('observation_gap',{classification:'backlog_overflow'});publish();
|
|
@@ -623,23 +848,23 @@ function notification(type,filename) {
|
|
|
623
848
|
if(planDirty){healthy=false;reason='input plan possibly changed';}
|
|
624
849
|
if(planDirty||pendingChecks.size)publish();
|
|
625
850
|
}
|
|
626
|
-
function externalNotification(entry,type,filename) {
|
|
851
|
+
function externalNotification(entry,type,filename,replay=false) {
|
|
627
852
|
metrics.notifications++;
|
|
628
853
|
metrics.lastNotificationAt=Date.now();
|
|
629
854
|
if(!filename){externalGap(entry,'unnamed_event');return;}
|
|
630
855
|
const rel=filename.toString().split(path.sep).join('/');
|
|
631
856
|
const absolute=path.join(entry.root,rel);
|
|
632
857
|
if(planning&&entry.index)for(const name of entry.index.candidates(rel))
|
|
633
|
-
if(entry.index.matches(entry.index.checks.get(name),rel))markInputEvent(name
|
|
858
|
+
if(entry.index.matches(entry.index.checks.get(name),rel))markInputEvent(name,`external:${entry.root}:${rel}`,type,replay);
|
|
634
859
|
if(planTrigger(absolute)&&triggerChanged(absolute))planDirty=true;
|
|
635
860
|
if(candidateTriggerChanged(absolute))planDirty=true;
|
|
636
861
|
entry.pending.add(rel);metrics.maxQueue=Math.max(metrics.maxQueue,
|
|
637
862
|
pending.size+[...external.values()].reduce((n,e)=>n+e.pending.size,0));
|
|
638
863
|
if(entry.index) {
|
|
639
864
|
for(const name of entry.index.candidates(rel))
|
|
640
|
-
if(entry.index.matches(entry.index.checks.get(name),rel))markInputEvent(name
|
|
865
|
+
if(entry.index.matches(entry.index.checks.get(name),rel))markInputEvent(name,`external:${entry.root}:${rel}`,type,replay);
|
|
641
866
|
for(const [prefix,names] of entry.index.prefixes)
|
|
642
|
-
if(prefix.startsWith(rel+'/'))for(const name of names)markInputEvent(name
|
|
867
|
+
if(prefix.startsWith(rel+'/'))for(const name of names)markInputEvent(name,`external:${entry.root}:${rel}`,type,replay);
|
|
643
868
|
}
|
|
644
869
|
if(planDirty){healthy=false;reason='input plan possibly changed';}
|
|
645
870
|
publish();clearTimeout(flushTimer);flushTimer=setTimeout(flush,60);
|
|
@@ -657,7 +882,8 @@ function observationGap(classification,error) {
|
|
|
657
882
|
// Once continuity is uncertain, a cursor is no longer authority. Do not
|
|
658
883
|
// retry native history in this observer generation after reconciliation.
|
|
659
884
|
historyDisabled=true;
|
|
660
|
-
|
|
885
|
+
const cause=error?.message?` (${error.message})`:'';
|
|
886
|
+
healthy=false;reason=`observation unavailable: ${classification}${cause}; deterministic reconciliation pending`;
|
|
661
887
|
metrics.gaps++;
|
|
662
888
|
event('observation_gap',{classification,error_code:error?.code||null});publish();
|
|
663
889
|
// One bounded attempt per gap; a later independent gap may try again.
|
|
@@ -743,14 +969,14 @@ async function catchUp() {
|
|
|
743
969
|
lastObservation=Date.now();return;
|
|
744
970
|
}
|
|
745
971
|
if(historyDisabled)return;
|
|
746
|
-
if(synchronizing)return synchronizing;
|
|
972
|
+
if(synchronizing){mark('daemon.catch_up_join');return synchronizing;}
|
|
747
973
|
synchronizing=(async()=>{
|
|
748
974
|
if(!verifyRoot())throw Error('checkout identity changed');
|
|
749
975
|
if(!watcher)throw Error('subscription unavailable');
|
|
750
976
|
if(!fs.existsSync(snapshotPath))throw Error('historical cursor missing');
|
|
751
977
|
{
|
|
752
978
|
const changes=await queryHistory(root,snapshotPath,'root replay');
|
|
753
|
-
for(const change of changes)notification(change.type,path.relative(root,change.path));
|
|
979
|
+
for(const change of changes)notification(change.type,path.relative(root,change.path),true);
|
|
754
980
|
if(!planning&&(flushTimer||pending.size||planDirty))flush();
|
|
755
981
|
for(const entry of external.values())if(entry.index) {
|
|
756
982
|
try{
|
|
@@ -760,7 +986,7 @@ async function catchUp() {
|
|
|
760
986
|
if(!entry.watcher||!fs.existsSync(entry.snapshot))throw Error('external cursor unavailable');
|
|
761
987
|
const changes=await queryHistory(entry.root,entry.snapshot,'external replay');
|
|
762
988
|
for(const change of changes)externalNotification(entry,change.type,
|
|
763
|
-
path.relative(entry.root,change.path));
|
|
989
|
+
path.relative(entry.root,change.path),true);
|
|
764
990
|
}catch(e){if(e.code==='ETIMEDOUT')throw e;
|
|
765
991
|
externalGap(entry,'historical_replay_failed',e);}
|
|
766
992
|
}
|
|
@@ -774,7 +1000,8 @@ async function catchUp() {
|
|
|
774
1000
|
function respond(client,value) {
|
|
775
1001
|
if(client.destroyed){event('control_reply_unavailable',{classification:'client_destroyed'});return;}
|
|
776
1002
|
try{const payload=JSON.stringify(value)+'\n';
|
|
777
|
-
|
|
1003
|
+
mark('server.response_constructed',{bytes:payload.length});
|
|
1004
|
+
client.end(payload,()=>mark('server.response_delivered'));client.vstateReplied=true;
|
|
778
1005
|
if(process.env.VSTATE_DEBUG_CONTROL==='1')event('control_reply_sent',{bytes:payload.length});}
|
|
779
1006
|
catch(e){event('control_reply_unavailable',{classification:e.name||'serialization_error'});
|
|
780
1007
|
if(!client.destroyed)client.end('{"error":"control response unavailable"}\n');}
|
|
@@ -792,10 +1019,11 @@ async function request(message) {
|
|
|
792
1019
|
if(message.action==='sync'||message.action==='snapshot'||message.action==='prelaunch') {
|
|
793
1020
|
const totalStarted=performance.now();
|
|
794
1021
|
const decision=++decisionSequence;
|
|
1022
|
+
mark('server.decision_start',{decision,pending:pending.size,planDirty,planning,recovering,healthy,historyDisabled,decisionRequests,historyDeadlineMs,reconciliationDeadlineMs});
|
|
795
1023
|
timing('daemon.decision_reason',totalStarted,{decision,action:message.action,
|
|
796
1024
|
plan:process.platform==='win32'?'validate_dependencies_before_reuse':'existing_plan',
|
|
797
|
-
probe:'reuse_only_with_identical_content_and_context',installed:'
|
|
798
|
-
|
|
1025
|
+
probe:'reuse_only_with_identical_content_and_context',installed:'current_identity_required',
|
|
1026
|
+
planPhase:metrics.planPhase||'initializing',pending:pending.size,
|
|
799
1027
|
notifications:metrics.notifications,planRebuilds:metrics.planRebuilds});
|
|
800
1028
|
if(process.platform==='win32'&&process.env.VSTATE_TEST_FAULTS==='1'&&
|
|
801
1029
|
process.env.VSTATE_TEST_WINDOWS_GAP_FILE&&
|
|
@@ -810,9 +1038,17 @@ async function request(message) {
|
|
|
810
1038
|
// has been disabled: a recovered subscription may still have queued events.
|
|
811
1039
|
const needsContentReconciliation=process.platform==='win32'||(hasHistory&&historyDisabled);
|
|
812
1040
|
if(needsContentReconciliation&&message.action!=='prelaunch'){
|
|
813
|
-
|
|
1041
|
+
// Windows probes inside the reconciliation, so a known-STALE sync skips it there.
|
|
1042
|
+
const started=performance.now();
|
|
1043
|
+
await refreshPlan({forceCold:true,reuse:true,skip:message.action==='sync'?knownStale:null});
|
|
814
1044
|
timing('daemon.request_content_validation',started,{healthy:healthy?1:0});}
|
|
815
|
-
|
|
1045
|
+
// Without synchronized installed reads, the continuity barrier runs once,
|
|
1046
|
+
// after probing: in-process validation reads the live filesystem, and the
|
|
1047
|
+
// barrier then applies every change before the response is computed.
|
|
1048
|
+
const deferBarrier=process.platform!=='win32'&&!needsContentReconciliation&&
|
|
1049
|
+
message.action!=='prelaunch'&&index&&healthy&&!planning&&![...index.checks].some(([name,row])=>
|
|
1050
|
+
row.plan.synchronizedInstalledRead&&(!message.name||message.name===name));
|
|
1051
|
+
if(healthy&&!deferBarrier)try{const started=performance.now();await catchUp();
|
|
816
1052
|
timing('daemon.request_catch_up',started);}catch(e){observationGap('historical_query',e);}
|
|
817
1053
|
if(flushTimer||pending.size||planDirty||[...external.values()].some(e=>e.pending.size))flush();
|
|
818
1054
|
let synchronizedInstalled=false;
|
|
@@ -847,10 +1083,66 @@ async function request(message) {
|
|
|
847
1083
|
}catch(e){installedRecheck=null;observationGap('installed_recheck',e);}
|
|
848
1084
|
}
|
|
849
1085
|
const probeStarted=performance.now();
|
|
850
|
-
|
|
851
|
-
|
|
1086
|
+
// A run's start checkpoint skips the full probe when its recording can
|
|
1087
|
+
// replace the certificate; the end checkpoint offers that recording.
|
|
1088
|
+
let adoptIntent=false,recorded;const adopting=new Set();
|
|
1089
|
+
if(index&&healthy&&!validatedColdIndex&&message.action!=='prelaunch'){
|
|
1090
|
+
const continuous=continuityBarrier(),probed=index,gaps=metrics.gaps,decided=Date.now();
|
|
1091
|
+
const selected=[...index.checks].filter(([name])=>!message.name||message.name===name);
|
|
1092
|
+
if(message.action==='snapshot'&&typeof message.adopt==='string'&&/^[0-9a-f-]{36}$/.test(message.adopt)){
|
|
1093
|
+
const row=index.checks.get(message.name);
|
|
1094
|
+
recorded=row?adopt(message.name,row,message.adopt):'recording unavailable';
|
|
1095
|
+
}
|
|
1096
|
+
adoptIntent=message.action==='snapshot'&&message.runCertificate===1&&!message.adopt&&continuous;
|
|
1097
|
+
const keys=new Map(selected.map(([name,row])=>[name,inputKey(row)]));
|
|
1098
|
+
const skip=message.action==='sync'?knownStale:()=>false;
|
|
1099
|
+
if(message.action==='sync')refreshReceipts();
|
|
1100
|
+
// With a deferred barrier, validate in-process first and launch any full
|
|
1101
|
+
// probe only after the barrier, so a queued edit costs one probe, not two.
|
|
1102
|
+
for(const [name,row] of selected)if(!skip(name,row))probe(row,continuous,{launch:!deferBarrier&&!adoptIntent});
|
|
1103
|
+
if(continuous&&process.env.VSTATE_TEST_FAULTS==='1'&&process.env.VSTATE_TEST_MAC_PROBE_SIGNAL){
|
|
1104
|
+
fs.writeFileSync(process.env.VSTATE_TEST_MAC_PROBE_SIGNAL,'validated');
|
|
1105
|
+
await new Promise(resolve=>setTimeout(resolve,200));
|
|
1106
|
+
}
|
|
1107
|
+
// Validation and probes block JS event delivery. Finish with a continuity
|
|
1108
|
+
// barrier so queued edits/gaps cannot be published using the pre-probe index.
|
|
1109
|
+
if(continuous||deferBarrier)try{const started=performance.now();
|
|
1110
|
+
await catchUp();if(planDirty||pending.size||flushTimer)flush();
|
|
1111
|
+
timing('daemon.request_catch_up',started,{after:'probes'});
|
|
1112
|
+
// Rows not yet confirmed, or whose inputs moved meanwhile, are probed
|
|
1113
|
+
// from the barrier, as an undeferred decision would have done.
|
|
1114
|
+
// Decided once, after the barrier. The start probe value is already out
|
|
1115
|
+
// of date; clearing it means whichever probe comes next sets the value
|
|
1116
|
+
// for this unchanged interval without counting as a change during the run.
|
|
1117
|
+
if(adoptIntent&&index===probed&&healthy&&!planning)for(const [name,row] of selected)
|
|
1118
|
+
if(row.probeAt<decided&&adoptable(row)){
|
|
1119
|
+
adopting.add(name);row.probeHash=null;
|
|
1120
|
+
row.adoption={token:randomUUID(),serial:inputEventSerial.get(name)||0,gaps:metrics.gaps,
|
|
1121
|
+
input:inputKey(row),cache:row.probeCache};
|
|
1122
|
+
}
|
|
1123
|
+
const rest=index===probed&&healthy&&!planning?selected.filter(([name,row])=>!skip(name,row)&&
|
|
1124
|
+
!adopting.has(name)&&(row.probeAt<decided||metrics.gaps!==gaps||inputKey(row)!==keys.get(name))):[];
|
|
1125
|
+
const launched=metrics.fullProbes||0;
|
|
1126
|
+
for(const [,row] of rest)probe(row,continuityBarrier());
|
|
1127
|
+
if(continuous&&(metrics.fullProbes||0)>launched){await catchUp();if(planDirty||pending.size||flushTimer)flush();}
|
|
1128
|
+
}catch(e){observationGap('post_probe_history',e);}
|
|
1129
|
+
}
|
|
852
1130
|
timing('daemon.request_probes',probeStarted,{checks:index?.checks.size||0});
|
|
853
1131
|
lastObservation=Date.now();publish();
|
|
1132
|
+
// A run checkpoint never holds an installed identity from the racy window:
|
|
1133
|
+
// a same-size write in the same timestamp tick would keep it. Wait the
|
|
1134
|
+
// window out (at most 2 s), record those files again, and mark the
|
|
1135
|
+
// checkpoint unusable if one is still inside it.
|
|
1136
|
+
let racy=false;
|
|
1137
|
+
if(message.action==='snapshot'||message.action==='prelaunch'){
|
|
1138
|
+
const names=message.name?[message.name]:null;
|
|
1139
|
+
const indexes=()=>[index,...[...external.values()].map(e=>e.index)].filter(Boolean);
|
|
1140
|
+
const wait=Math.min(racyWindowMs,Math.max(0,...indexes().map(i=>i.racyUntil(names)))-Date.now());
|
|
1141
|
+
if(wait>0){metrics.racyWaits=(metrics.racyWaits||0)+1;await new Promise(resolve=>setTimeout(resolve,wait+5));}
|
|
1142
|
+
for(const i of indexes())i.settleRacy();
|
|
1143
|
+
racy=indexes().some(i=>i.racyUntil(names)>Date.now());
|
|
1144
|
+
if(racy)metrics.racyCheckpoints=(metrics.racyCheckpoints||0)+1;
|
|
1145
|
+
}
|
|
854
1146
|
const applicabilityStarted=performance.now();
|
|
855
1147
|
const data=status(message.contextHashes,synchronizedInstalled,
|
|
856
1148
|
process.platform==='win32'&&healthy&&!planning&&!planDirty);
|
|
@@ -860,8 +1152,43 @@ async function request(message) {
|
|
|
860
1152
|
data.snapshots[name]={...combinedSummary(name),files:combinedFiles(name),
|
|
861
1153
|
planGeneration:row.plan.id,
|
|
862
1154
|
inputEventSerial:inputEventSerial.get(name)||0,
|
|
863
|
-
|
|
864
|
-
|
|
1155
|
+
// Complete only when the oldest kept event follows the given serial.
|
|
1156
|
+
...(Number.isSafeInteger(message.sinceSerial)?(()=>{const list=inputEventPaths.get(name)||[],
|
|
1157
|
+
since=message.sinceSerial,later=list.filter(([serial])=>serial>since);
|
|
1158
|
+
return {inputEventPaths:later.map(([,rel])=>rel),
|
|
1159
|
+
inputEventPathsComplete:(inputEventSerial.get(name)||0)-since===later.length};})():{}),
|
|
1160
|
+
probeHash:row.probeHash,inputKey:inputKey(row),observationGaps:metrics.gaps,
|
|
1161
|
+
observationHealthy:healthy&&!planning&&!recovering&&!racy&&applicableExternal(name).every(e=>e.healthy)};
|
|
1162
|
+
// Export only a currently guarded, check-specific discovery certificate.
|
|
1163
|
+
// The caller compares it with the answers its own compile received. Schema 2
|
|
1164
|
+
// certifies the realpath of every directory readDirectory visits. The
|
|
1165
|
+
// snapshot stays the start of the observed interval, including validation.
|
|
1166
|
+
if(message.runCertificate===1&&continuityBarrier()){
|
|
1167
|
+
const row=index?.checks.get(message.name),cache=row?.probeCache;
|
|
1168
|
+
// Also after an accepted recording: the caller compares its record with
|
|
1169
|
+
// this certificate and finds its run facts with it.
|
|
1170
|
+
if(row?.plan.qualification==='npm-typescript-direct-v1'&&
|
|
1171
|
+
!row.plan.unresolved.length&&cache?.schema===1&&cache.input===inputKey(row)&&
|
|
1172
|
+
cache.hash===row.probeHash&&cache.guard===planGuard(index.bundle)&&
|
|
1173
|
+
cache.output&&cache.queryData&&data.snapshots[message.name])
|
|
1174
|
+
data.snapshots[message.name].discoveryCertificate={schema:2,planId:row.plan.id,
|
|
1175
|
+
cwd:row.plan.cwd,probeDefinition:sha(JSON.stringify(row.plan.probes)),
|
|
1176
|
+
context:cache.context,output:cache.output,stderrHash:cache.stderrHash,
|
|
1177
|
+
queryData:cache.queryData};
|
|
1178
|
+
}
|
|
1179
|
+
// For a run checked by full probes: the last probe's questions, only to
|
|
1180
|
+
// find the run's lookups outside them. Never evidence for reuse.
|
|
1181
|
+
if(message.runFacts===1&&data.snapshots[message.name]){
|
|
1182
|
+
const row=index?.checks.get(message.name),cache=row?.probeCache;
|
|
1183
|
+
if(row?.plan.qualification==='npm-typescript-direct-v1'&&cache?.queryData&&
|
|
1184
|
+
typeof cache.context==='string'&&cache.input===inputKey(row)&&cache.hash===row.probeHash&&
|
|
1185
|
+
cache.guard===planGuard(index.bundle))
|
|
1186
|
+
data.snapshots[message.name].probeQuestions={context:cache.context,queryData:cache.queryData};
|
|
1187
|
+
}
|
|
1188
|
+
const row=index?.checks.get(message.name);
|
|
1189
|
+
if(adopting.has(message.name)&&row?.adoption&&data.snapshots[message.name])
|
|
1190
|
+
data.snapshots[message.name].recordedCertificate=row.adoption.token;
|
|
1191
|
+
if(typeof recorded==='string'&&data.snapshots[message.name])data.snapshots[message.name].recordingIssue=recorded;
|
|
865
1192
|
}
|
|
866
1193
|
timing('daemon.request_applicability',applicabilityStarted,{checks:data.checks.length});
|
|
867
1194
|
timing('daemon.request_total',totalStarted,{action:message.action,
|
|
@@ -885,7 +1212,7 @@ async function shutdown() {
|
|
|
885
1212
|
healthy=true;retainIndex();healthy=false;}
|
|
886
1213
|
} catch(e){event('index_retention_unavailable',{classification:e.name||'Error'});}
|
|
887
1214
|
}
|
|
888
|
-
planningWorker?.terminate().catch(()=>{});
|
|
1215
|
+
closeHistory();planningWorker?.terminate().catch(()=>{});
|
|
889
1216
|
clearInterval(heartbeat);clearInterval(probeTimer);clearTimeout(recoveryTimer);
|
|
890
1217
|
watcher?.unsubscribe().catch(()=>{});server.close();
|
|
891
1218
|
for(const entry of external.values()){
|
|
@@ -903,15 +1230,19 @@ const server=net.createServer(client=>{
|
|
|
903
1230
|
event('control_reply_unavailable',{classification:handled?'closed_without_reply':
|
|
904
1231
|
'closed_before_request',request_bytes:text.length});});
|
|
905
1232
|
client.on('data',d=>{
|
|
906
|
-
|
|
1233
|
+
// A sync request carries each check's context digests, about 1.5 KB per
|
|
1234
|
+
// check; 1 MiB leaves room for hundreds of checks.
|
|
1235
|
+
if(handled)return;text+=d;if(text.length>1024*1024){event('control_request_rejected',
|
|
907
1236
|
{classification:'request_too_large',request_bytes:text.length});client.destroy();return;}
|
|
908
1237
|
if(!text.includes('\n'))return;handled=true;
|
|
909
1238
|
try{const message=JSON.parse(text);
|
|
910
1239
|
const decision=message.action==='sync'||message.action==='snapshot';
|
|
911
1240
|
if(decision)decisionRequests++;
|
|
912
|
-
|
|
913
|
-
.
|
|
914
|
-
.
|
|
1241
|
+
traceScope({request:message.diagnostic?.request||traceId(),work:'request'},()=>{
|
|
1242
|
+
mark('server.request_received',{activeDecisions:decisionRequests});
|
|
1243
|
+
Promise.resolve(request(message)).then(v=>respond(client,v))
|
|
1244
|
+
.catch(e=>respond(client,{error:e.message}))
|
|
1245
|
+
.finally(()=>{mark('server.request_complete');if(decision)decisionRequests--;});});}
|
|
915
1246
|
catch(e){respond(client,{error:e.message});}
|
|
916
1247
|
});
|
|
917
1248
|
});
|
|
@@ -971,15 +1302,23 @@ const heartbeat=setInterval(()=>{
|
|
|
971
1302
|
}
|
|
972
1303
|
publish();
|
|
973
1304
|
},1000);
|
|
974
|
-
|
|
1305
|
+
// Idle revalidation keeps probe results fresh for cached status without
|
|
1306
|
+
// launching a process: certificates are replayed in-process, and one that no
|
|
1307
|
+
// longer holds expires so the next decision runs the full probe. macOS history
|
|
1308
|
+
// replay would fork, so cached status there relies on live notifications.
|
|
1309
|
+
const probeTimer=setInterval(()=>traceScope({request:traceId(),work:'periodic'},async()=>{
|
|
1310
|
+
mark('server.periodic_start',{pending:pending.size,planDirty,planning,decisionRequests});
|
|
975
1311
|
// Windows cached reads are conservatively UNVERIFIED until a full decision
|
|
976
|
-
// reconciliation
|
|
977
|
-
// but can block control requests behind several seconds of child work.
|
|
1312
|
+
// reconciliation, so idle revalidation cannot strengthen them.
|
|
978
1313
|
if(process.platform==='win32')return;
|
|
979
|
-
if(healthy&&index&&!planning&&!decisionRequests){
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
}
|
|
1314
|
+
if(healthy&&index&&!planning&&!recovering&&!decisionRequests){
|
|
1315
|
+
try{if(!hasHistory)await catchUp();}catch(e){observationGap('periodic_query',e);}
|
|
1316
|
+
if(healthy&&index&&!planning&&!recovering)
|
|
1317
|
+
for(const row of index.checks.values())probe(row,true,{launch:false});
|
|
1318
|
+
if(planDirty)flush();else publish();
|
|
1319
|
+
}
|
|
1320
|
+
mark('server.periodic_complete');
|
|
1321
|
+
}),5000);
|
|
983
1322
|
process.on('uncaughtExceptionMonitor',error=>{
|
|
984
1323
|
try{event('observer_uncaught_exception',{pid:process.pid,code:error.code||null,
|
|
985
1324
|
message:error.message,stack:error.stack});}catch{}
|