@evomap/evolver-core 2.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/algo/antiDistill.d.ts +35 -0
- package/dist/algo/antiDistill.js +167 -0
- package/dist/algo/bans.d.ts +6 -0
- package/dist/algo/bans.js +33 -0
- package/dist/algo/candidateAssembly.d.ts +65 -0
- package/dist/algo/candidateAssembly.js +196 -0
- package/dist/algo/capabilityCandidates.d.ts +38 -0
- package/dist/algo/capabilityCandidates.js +131 -0
- package/dist/algo/confidence.d.ts +106 -0
- package/dist/algo/confidence.js +196 -0
- package/dist/algo/cycleEngine.d.ts +143 -0
- package/dist/algo/cycleEngine.js +417 -0
- package/dist/algo/cycleFailureClassifier.d.ts +52 -0
- package/dist/algo/cycleFailureClassifier.js +104 -0
- package/dist/algo/epigenetics.d.ts +14 -0
- package/dist/algo/epigenetics.js +41 -0
- package/dist/algo/evolutionEvent.d.ts +23 -0
- package/dist/algo/evolutionEvent.js +24 -0
- package/dist/algo/exploration.d.ts +34 -0
- package/dist/algo/exploration.js +55 -0
- package/dist/algo/geneHealth.d.ts +24 -0
- package/dist/algo/geneHealth.js +16 -0
- package/dist/algo/geneIntake.d.ts +37 -0
- package/dist/algo/geneIntake.js +100 -0
- package/dist/algo/genePromotion.d.ts +48 -0
- package/dist/algo/genePromotion.js +53 -0
- package/dist/algo/geneSelection.d.ts +128 -0
- package/dist/algo/geneSelection.js +222 -0
- package/dist/algo/index.d.ts +18 -0
- package/dist/algo/index.js +18 -0
- package/dist/algo/mutation.d.ts +12 -0
- package/dist/algo/mutation.js +22 -0
- package/dist/algo/orchestrator.d.ts +65 -0
- package/dist/algo/orchestrator.js +63 -0
- package/dist/algo/solidify.d.ts +43 -0
- package/dist/algo/solidify.js +63 -0
- package/dist/algo/strategyPresets.d.ts +32 -0
- package/dist/algo/strategyPresets.js +61 -0
- package/dist/assetstore/index.d.ts +8 -0
- package/dist/assetstore/index.js +8 -0
- package/dist/assetstore/learningHistory.d.ts +25 -0
- package/dist/assetstore/learningHistory.js +38 -0
- package/dist/assetstore/localJsonl.d.ts +34 -0
- package/dist/assetstore/localJsonl.js +182 -0
- package/dist/assetstore/pendingSignals.d.ts +22 -0
- package/dist/assetstore/pendingSignals.js +171 -0
- package/dist/assetstore/provenance.d.ts +39 -0
- package/dist/assetstore/provenance.js +109 -0
- package/dist/assetstore/provider.d.ts +48 -0
- package/dist/assetstore/provider.js +36 -0
- package/dist/assetstore/remoteStub.d.ts +32 -0
- package/dist/assetstore/remoteStub.js +36 -0
- package/dist/assetstore/reviewFilter.d.ts +20 -0
- package/dist/assetstore/reviewFilter.js +44 -0
- package/dist/assetstore/reviewLedger.d.ts +55 -0
- package/dist/assetstore/reviewLedger.js +121 -0
- package/dist/benchmark/antiGeneBenchmark.d.ts +79 -0
- package/dist/benchmark/antiGeneBenchmark.js +241 -0
- package/dist/benchmark/antiGeneRollout.d.ts +80 -0
- package/dist/benchmark/antiGeneRollout.js +256 -0
- package/dist/benchmark/benchmark.d.ts +32 -0
- package/dist/benchmark/benchmark.js +26 -0
- package/dist/benchmark/evolutionThesisSolver.d.ts +25 -0
- package/dist/benchmark/evolutionThesisSolver.js +41 -0
- package/dist/benchmark/index.d.ts +5 -0
- package/dist/benchmark/index.js +5 -0
- package/dist/benchmark/thesis.d.ts +85 -0
- package/dist/benchmark/thesis.js +150 -0
- package/dist/bootstrap/envFingerprint.d.ts +45 -0
- package/dist/bootstrap/envFingerprint.js +72 -0
- package/dist/bootstrap/index.d.ts +2 -0
- package/dist/bootstrap/index.js +2 -0
- package/dist/bootstrap/selfEvolve.d.ts +36 -0
- package/dist/bootstrap/selfEvolve.js +26 -0
- package/dist/cycle/cycleTimeline.d.ts +35 -0
- package/dist/cycle/cycleTimeline.js +74 -0
- package/dist/cycle/index.d.ts +2 -0
- package/dist/cycle/index.js +2 -0
- package/dist/cycle/stateMachine.d.ts +6 -0
- package/dist/cycle/stateMachine.js +25 -0
- package/dist/daemon/cycleContext.d.ts +12 -0
- package/dist/daemon/cycleContext.js +13 -0
- package/dist/daemon/drain.d.ts +11 -0
- package/dist/daemon/drain.js +16 -0
- package/dist/daemon/guards.d.ts +87 -0
- package/dist/daemon/guards.js +142 -0
- package/dist/daemon/idempotency.d.ts +11 -0
- package/dist/daemon/idempotency.js +36 -0
- package/dist/daemon/idleScheduler.d.ts +55 -0
- package/dist/daemon/idleScheduler.js +181 -0
- package/dist/daemon/index.d.ts +8 -0
- package/dist/daemon/index.js +8 -0
- package/dist/daemon/stepRunner.d.ts +12 -0
- package/dist/daemon/stepRunner.js +31 -0
- package/dist/daemon/unhandledRejectionWindow.d.ts +21 -0
- package/dist/daemon/unhandledRejectionWindow.js +52 -0
- package/dist/daemon/watchdog.d.ts +11 -0
- package/dist/daemon/watchdog.js +14 -0
- package/dist/events/eventSchema.d.ts +132 -0
- package/dist/events/eventSchema.js +30 -0
- package/dist/events/eventStore.d.ts +32 -0
- package/dist/events/eventStore.js +137 -0
- package/dist/events/ingest.d.ts +34 -0
- package/dist/events/ingest.js +61 -0
- package/dist/events/paths.d.ts +22 -0
- package/dist/events/paths.js +44 -0
- package/dist/events/projectors.d.ts +9 -0
- package/dist/events/projectors.js +11 -0
- package/dist/events/public.d.ts +14 -0
- package/dist/events/public.js +8 -0
- package/dist/events/replayer.d.ts +26 -0
- package/dist/events/replayer.js +67 -0
- package/dist/events/reports.d.ts +90 -0
- package/dist/events/reports.js +244 -0
- package/dist/events/retention.d.ts +70 -0
- package/dist/events/retention.js +177 -0
- package/dist/events/sink.d.ts +5 -0
- package/dist/events/sink.js +1 -0
- package/dist/exec/autoExec.d.ts +137 -0
- package/dist/exec/autoExec.js +221 -0
- package/dist/exec/autonomousCycle.d.ts +48 -0
- package/dist/exec/autonomousCycle.js +78 -0
- package/dist/exec/claudeBridge.d.ts +112 -0
- package/dist/exec/claudeBridge.js +219 -0
- package/dist/exec/index.d.ts +9 -0
- package/dist/exec/index.js +11 -0
- package/dist/exec/openPrRegistry.d.ts +60 -0
- package/dist/exec/openPrRegistry.js +130 -0
- package/dist/exec/policy/blastRadius.d.ts +31 -0
- package/dist/exec/policy/blastRadius.js +67 -0
- package/dist/exec/policy/constraints.d.ts +26 -0
- package/dist/exec/policy/constraints.js +62 -0
- package/dist/exec/policy/destructive.d.ts +45 -0
- package/dist/exec/policy/destructive.js +108 -0
- package/dist/exec/policy/failureMode.d.ts +33 -0
- package/dist/exec/policy/failureMode.js +49 -0
- package/dist/exec/policy/index.d.ts +24 -0
- package/dist/exec/policy/index.js +30 -0
- package/dist/exec/policy/protectedPaths.d.ts +17 -0
- package/dist/exec/policy/protectedPaths.js +65 -0
- package/dist/exec/policyCheck.d.ts +2 -0
- package/dist/exec/policyCheck.js +7 -0
- package/dist/exec/prompt.d.ts +34 -0
- package/dist/exec/prompt.js +108 -0
- package/dist/exec/proofOfWork.d.ts +9 -0
- package/dist/exec/proofOfWork.js +11 -0
- package/dist/exec/runnerRegistry.d.ts +123 -0
- package/dist/exec/runnerRegistry.js +237 -0
- package/dist/exec/selfPr.d.ts +83 -0
- package/dist/exec/selfPr.js +104 -0
- package/dist/exec/selfPrObfuscation.d.ts +21 -0
- package/dist/exec/selfPrObfuscation.js +85 -0
- package/dist/hooks/hooks.d.ts +117 -0
- package/dist/hooks/hooks.js +89 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hub/assetCallLog.d.ts +43 -0
- package/dist/hub/assetCallLog.js +72 -0
- package/dist/hub/bindings.d.ts +28 -0
- package/dist/hub/bindings.js +90 -0
- package/dist/hub/capability.d.ts +278 -0
- package/dist/hub/capability.js +1 -0
- package/dist/hub/conversationDistiller.d.ts +88 -0
- package/dist/hub/conversationDistiller.js +264 -0
- package/dist/hub/fake.d.ts +45 -0
- package/dist/hub/fake.js +69 -0
- package/dist/hub/hubReview.d.ts +91 -0
- package/dist/hub/hubReview.js +106 -0
- package/dist/hub/index.d.ts +10 -0
- package/dist/hub/index.js +10 -0
- package/dist/hub/ingest.d.ts +15 -0
- package/dist/hub/ingest.js +15 -0
- package/dist/hub/questionGenerator.d.ts +59 -0
- package/dist/hub/questionGenerator.js +406 -0
- package/dist/hub/reuseDecision.d.ts +87 -0
- package/dist/hub/reuseDecision.js +127 -0
- package/dist/hub/sanitize.d.ts +52 -0
- package/dist/hub/sanitize.js +321 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +27 -0
- package/dist/mailbox/catalog.d.ts +17 -0
- package/dist/mailbox/catalog.js +53 -0
- package/dist/mailbox/daemon.d.ts +48 -0
- package/dist/mailbox/daemon.js +61 -0
- package/dist/mailbox/dispatch.d.ts +46 -0
- package/dist/mailbox/dispatch.js +62 -0
- package/dist/mailbox/envelope.d.ts +43 -0
- package/dist/mailbox/envelope.js +52 -0
- package/dist/mailbox/index.d.ts +8 -0
- package/dist/mailbox/index.js +8 -0
- package/dist/mailbox/ipcServer.d.ts +46 -0
- package/dist/mailbox/ipcServer.js +168 -0
- package/dist/mailbox/requestReply.d.ts +40 -0
- package/dist/mailbox/requestReply.js +77 -0
- package/dist/mailbox/store.d.ts +74 -0
- package/dist/mailbox/store.js +308 -0
- package/dist/mailbox/wake.d.ts +45 -0
- package/dist/mailbox/wake.js +60 -0
- package/dist/material/boundary.d.ts +7 -0
- package/dist/material/boundary.js +14 -0
- package/dist/material/consumer.d.ts +21 -0
- package/dist/material/consumer.js +52 -0
- package/dist/material/emit.d.ts +55 -0
- package/dist/material/emit.js +52 -0
- package/dist/material/factory.d.ts +27 -0
- package/dist/material/factory.js +25 -0
- package/dist/material/index.d.ts +8 -0
- package/dist/material/index.js +8 -0
- package/dist/material/materialStore.d.ts +24 -0
- package/dist/material/materialStore.js +76 -0
- package/dist/material/sampling.d.ts +21 -0
- package/dist/material/sampling.js +39 -0
- package/dist/material/sources.d.ts +25 -0
- package/dist/material/sources.js +33 -0
- package/dist/material/watermark.d.ts +21 -0
- package/dist/material/watermark.js +76 -0
- package/dist/observers/cursorRewriteObserver.d.ts +46 -0
- package/dist/observers/cursorRewriteObserver.js +103 -0
- package/dist/observers/distillObserver.d.ts +31 -0
- package/dist/observers/distillObserver.js +93 -0
- package/dist/observers/index.d.ts +5 -0
- package/dist/observers/index.js +5 -0
- package/dist/observers/observerBus.d.ts +44 -0
- package/dist/observers/observerBus.js +73 -0
- package/dist/observers/reflectionObserver.d.ts +31 -0
- package/dist/observers/reflectionObserver.js +124 -0
- package/dist/observers/valueDigestObserver.d.ts +50 -0
- package/dist/observers/valueDigestObserver.js +70 -0
- package/dist/ops/cleanup.d.ts +23 -0
- package/dist/ops/cleanup.js +68 -0
- package/dist/ops/health.d.ts +37 -0
- package/dist/ops/health.js +96 -0
- package/dist/ops/index.d.ts +7 -0
- package/dist/ops/index.js +7 -0
- package/dist/ops/recall.d.ts +65 -0
- package/dist/ops/recall.js +152 -0
- package/dist/ops/reuseOutcomes.d.ts +68 -0
- package/dist/ops/reuseOutcomes.js +126 -0
- package/dist/ops/savingsCore.d.ts +47 -0
- package/dist/ops/savingsCore.js +112 -0
- package/dist/ops/selfUpdate.d.ts +106 -0
- package/dist/ops/selfUpdate.js +294 -0
- package/dist/ops/valueLedger.d.ts +205 -0
- package/dist/ops/valueLedger.js +238 -0
- package/dist/ops/valueOutreach.d.ts +70 -0
- package/dist/ops/valueOutreach.js +244 -0
- package/dist/personality/drift.d.ts +45 -0
- package/dist/personality/drift.js +106 -0
- package/dist/personality/events.d.ts +25 -0
- package/dist/personality/events.js +37 -0
- package/dist/personality/evolveOps.d.ts +20 -0
- package/dist/personality/evolveOps.js +93 -0
- package/dist/personality/index.d.ts +11 -0
- package/dist/personality/index.js +11 -0
- package/dist/personality/mutate.d.ts +21 -0
- package/dist/personality/mutate.js +39 -0
- package/dist/personality/pivot.d.ts +23 -0
- package/dist/personality/pivot.js +22 -0
- package/dist/personality/prompt.d.ts +6 -0
- package/dist/personality/prompt.js +62 -0
- package/dist/personality/riskGate.d.ts +42 -0
- package/dist/personality/riskGate.js +80 -0
- package/dist/personality/schema.d.ts +255 -0
- package/dist/personality/schema.js +119 -0
- package/dist/personality/select.d.ts +46 -0
- package/dist/personality/select.js +71 -0
- package/dist/personality/stats.d.ts +46 -0
- package/dist/personality/stats.js +82 -0
- package/dist/personality/store.d.ts +35 -0
- package/dist/personality/store.js +92 -0
- package/dist/schema/common.d.ts +28 -0
- package/dist/schema/common.js +17 -0
- package/dist/schema/index.d.ts +5 -0
- package/dist/schema/index.js +5 -0
- package/dist/schema/material.d.ts +201 -0
- package/dist/schema/material.js +50 -0
- package/dist/schema/problem.d.ts +160 -0
- package/dist/schema/problem.js +45 -0
- package/dist/schema/proofOfWork.d.ts +97 -0
- package/dist/schema/proofOfWork.js +9 -0
- package/dist/schema/signal.d.ts +41 -0
- package/dist/schema/signal.js +19 -0
- package/dist/shadow/index.d.ts +6 -0
- package/dist/shadow/index.js +6 -0
- package/dist/shadow/jsonlSink.d.ts +17 -0
- package/dist/shadow/jsonlSink.js +27 -0
- package/dist/shadow/redact.d.ts +11 -0
- package/dist/shadow/redact.js +18 -0
- package/dist/shadow/shadowHub.d.ts +12 -0
- package/dist/shadow/shadowHub.js +42 -0
- package/dist/shadow/shadowMailbox.d.ts +17 -0
- package/dist/shadow/shadowMailbox.js +25 -0
- package/dist/shadow/shadowStore.d.ts +18 -0
- package/dist/shadow/shadowStore.js +55 -0
- package/dist/shadow/sink.d.ts +37 -0
- package/dist/shadow/sink.js +18 -0
- package/dist/signals/cycleHistoryFromEvents.d.ts +11 -0
- package/dist/signals/cycleHistoryFromEvents.js +96 -0
- package/dist/signals/expand.d.ts +34 -0
- package/dist/signals/expand.js +125 -0
- package/dist/signals/extractor.d.ts +25 -0
- package/dist/signals/extractor.js +57 -0
- package/dist/signals/index.d.ts +6 -0
- package/dist/signals/index.js +6 -0
- package/dist/signals/metaSignals.d.ts +57 -0
- package/dist/signals/metaSignals.js +158 -0
- package/dist/signals/signalGate.d.ts +15 -0
- package/dist/signals/signalGate.js +40 -0
- package/dist/signals/traceSignals.d.ts +44 -0
- package/dist/signals/traceSignals.js +123 -0
- package/dist/signatures/classifier.d.ts +16 -0
- package/dist/signatures/classifier.js +29 -0
- package/dist/signatures/index.d.ts +2 -0
- package/dist/signatures/index.js +2 -0
- package/dist/signatures/signatures.d.ts +22 -0
- package/dist/signatures/signatures.js +42 -0
- package/dist/strategy/experiment.d.ts +48 -0
- package/dist/strategy/experiment.js +63 -0
- package/dist/strategy/index.d.ts +2 -0
- package/dist/strategy/index.js +2 -0
- package/dist/strategy/strategyPoint.d.ts +30 -0
- package/dist/strategy/strategyPoint.js +23 -0
- package/dist/trace/index.d.ts +2 -0
- package/dist/trace/index.js +2 -0
- package/dist/trace/trajectory.d.ts +82 -0
- package/dist/trace/trajectory.js +117 -0
- package/dist/trace/trajectoryExport.d.ts +237 -0
- package/dist/trace/trajectoryExport.js +1818 -0
- package/dist/trigger/budget.d.ts +16 -0
- package/dist/trigger/budget.js +20 -0
- package/dist/trigger/engine.d.ts +12 -0
- package/dist/trigger/engine.js +23 -0
- package/dist/trigger/index.d.ts +6 -0
- package/dist/trigger/index.js +6 -0
- package/dist/trigger/problemBuilder.d.ts +24 -0
- package/dist/trigger/problemBuilder.js +32 -0
- package/dist/trigger/trigger.d.ts +22 -0
- package/dist/trigger/trigger.js +27 -0
- package/dist/trigger/valueModel.d.ts +26 -0
- package/dist/trigger/valueModel.js +25 -0
- package/dist/trigger/wfq.d.ts +13 -0
- package/dist/trigger/wfq.js +25 -0
- package/dist/util/fileLock.d.ts +24 -0
- package/dist/util/fileLock.js +227 -0
- package/dist/util/index.d.ts +2 -0
- package/dist/util/index.js +2 -0
- package/dist/util/version.d.ts +12 -0
- package/dist/util/version.js +51 -0
- package/dist/verify/index.d.ts +4 -0
- package/dist/verify/index.js +4 -0
- package/dist/verify/rollback.d.ts +22 -0
- package/dist/verify/rollback.js +33 -0
- package/dist/verify/sandboxRunner.d.ts +39 -0
- package/dist/verify/sandboxRunner.js +116 -0
- package/dist/verify/sandboxedValidation.d.ts +35 -0
- package/dist/verify/sandboxedValidation.js +56 -0
- package/dist/verify/validation.d.ts +41 -0
- package/dist/verify/validation.js +85 -0
- package/dist/wire/canonicalize.golden.d.ts +23 -0
- package/dist/wire/canonicalize.golden.js +46 -0
- package/dist/wire/geneHints.d.ts +64 -0
- package/dist/wire/geneHints.js +100 -0
- package/dist/wire/index.d.ts +91 -0
- package/dist/wire/index.js +3 -0
- package/dist/wire/schemaGate.d.ts +11 -0
- package/dist/wire/schemaGate.js +49 -0
- package/dist/workflow/dsl.d.ts +54 -0
- package/dist/workflow/dsl.js +15 -0
- package/dist/workflow/engine.d.ts +30 -0
- package/dist/workflow/engine.js +94 -0
- package/dist/workflow/index.d.ts +2 -0
- package/dist/workflow/index.js +2 -0
- package/package.json +29 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Resident-daemon preflight guards (ported from v1 evolve/guards.js — the portable, decision-relevant core).
|
|
2
|
+
// Before a resident loop starts a cycle it should YIELD when: a human put a cooperative `.evolver.lock` in the
|
|
3
|
+
// repo, a release is in flight (last commit is a release bump), or the host is under load. And when it does
|
|
4
|
+
// back off, it should persist its partial state (the "dormant hypothesis") so the next cycle resumes instead
|
|
5
|
+
// of starting cold. Pairs with ops/health (resource snapshot) — health observes, these decide.
|
|
6
|
+
//
|
|
7
|
+
// All decision logic is PURE: the caller performs the fs/git/os reads and passes the values in, so every
|
|
8
|
+
// branch is deterministically testable. The dormant-state store takes an injected path + clock.
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { dirname } from 'node:path';
|
|
11
|
+
const MIN_ASSUMED_CPU_COUNT = 4; // os.cpus() can return [] (Android/Termux) — floor so load max never collapses to 0
|
|
12
|
+
const MIN_LOCK_TTL_MS = 1000; // catches the parseInt('5m')===5 quirk → a 5ms TTL would insta-stale every lock
|
|
13
|
+
const MIN_RELEASE_WINDOW_MS = 1000;
|
|
14
|
+
/** Clamp os.loadavg() to at most 2× cores so a misreported sample (Windows zeros, Termux process-counters) can't force a permanent backoff. */
|
|
15
|
+
export function clampSystemLoad(loadavg, cpuCount) {
|
|
16
|
+
const cap = Math.max(1, cpuCount) * 2;
|
|
17
|
+
return {
|
|
18
|
+
load1m: Math.min(loadavg[0] || 0, cap),
|
|
19
|
+
load5m: Math.min(loadavg[1] || 0, cap),
|
|
20
|
+
load15m: Math.min(loadavg[2] || 0, cap),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** Sensible default load ceiling for N cores (reserve ~10% headroom); floors CPU count so it's never 0. */
|
|
24
|
+
export function defaultLoadMax(cpuCount) {
|
|
25
|
+
const cpu = cpuCount > 0 ? cpuCount : MIN_ASSUMED_CPU_COUNT;
|
|
26
|
+
return cpu <= 1 ? 0.9 : cpu * 0.9;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Cooperative `.evolver.lock`: a human (release script, IDE refactor) drops the file to make the daemon yield;
|
|
30
|
+
* whoever drops it removes it. A lock older than ttlMs is ignored (forgotten lock won't dormant the daemon
|
|
31
|
+
* forever). A future mtime (clock skew) counts as fresh. Pure — caller supplies presence + mtime.
|
|
32
|
+
*/
|
|
33
|
+
export function evaluateUserLock(input) {
|
|
34
|
+
if (!Number.isFinite(input.ttlMs) || input.ttlMs < MIN_LOCK_TTL_MS)
|
|
35
|
+
return { yield: false, reason: 'invalid_ttl' };
|
|
36
|
+
if (!input.present)
|
|
37
|
+
return { yield: false, reason: 'no_lock' };
|
|
38
|
+
if (input.mtimeMs === undefined || !Number.isFinite(input.mtimeMs))
|
|
39
|
+
return { yield: false, reason: 'stat_failed' };
|
|
40
|
+
const ageMs = input.now - input.mtimeMs;
|
|
41
|
+
if (ageMs < 0)
|
|
42
|
+
return { yield: true, reason: 'lock_active_future_mtime', ageMs };
|
|
43
|
+
if (ageMs <= input.ttlMs)
|
|
44
|
+
return { yield: true, reason: 'lock_active', ageMs };
|
|
45
|
+
return { yield: false, reason: 'lock_stale', ageMs };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Release-window quiet period: if the last commit is a release bump (chore(release)…), a human is mid-deploy —
|
|
49
|
+
* yield until the window passes so the daemon doesn't dirty the tree under a publish. windowMs===0 disables.
|
|
50
|
+
* Pure — caller supplies the last commit subject + unix ts.
|
|
51
|
+
*/
|
|
52
|
+
export function evaluateReleaseWindow(input) {
|
|
53
|
+
if (input.windowMs === 0)
|
|
54
|
+
return { yield: false, reason: 'disabled' };
|
|
55
|
+
if (!Number.isFinite(input.windowMs) || input.windowMs < MIN_RELEASE_WINDOW_MS)
|
|
56
|
+
return { yield: false, reason: 'invalid_window' };
|
|
57
|
+
if (!Number.isFinite(input.lastCommitUnixTs))
|
|
58
|
+
return { yield: false, reason: 'no_commit' };
|
|
59
|
+
if (!/^chore\(release\)/i.test(String(input.lastCommitSubject ?? '')))
|
|
60
|
+
return { yield: false, reason: 'not_release_commit' };
|
|
61
|
+
const ageMs = input.now - input.lastCommitUnixTs * 1000;
|
|
62
|
+
if (ageMs < 0)
|
|
63
|
+
return { yield: true, reason: 'release_window_future_commit', ageMs };
|
|
64
|
+
if (ageMs <= input.windowMs)
|
|
65
|
+
return { yield: true, reason: 'release_window_active', ageMs };
|
|
66
|
+
return { yield: false, reason: 'window_passed', ageMs };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Combine the preflight guards in priority order — user lock → release window → system load — and return the
|
|
70
|
+
* first that says yield. `clear` when none fire. The daemon supplies live readings; this stays pure.
|
|
71
|
+
*/
|
|
72
|
+
export function evaluateGuards(input) {
|
|
73
|
+
if (input.lock) {
|
|
74
|
+
const r = evaluateUserLock({ present: input.lock.present, mtimeMs: input.lock.mtimeMs, now: input.now, ttlMs: input.lock.ttlMs ?? 3_600_000 });
|
|
75
|
+
if (r.yield)
|
|
76
|
+
return { yield: true, reason: 'user_lock', detail: { lockReason: r.reason, ageMs: r.ageMs } };
|
|
77
|
+
}
|
|
78
|
+
if (input.release) {
|
|
79
|
+
const r = evaluateReleaseWindow({ lastCommitSubject: input.release.lastCommitSubject, lastCommitUnixTs: input.release.lastCommitUnixTs, now: input.now, windowMs: input.release.windowMs ?? 300_000 });
|
|
80
|
+
if (r.yield)
|
|
81
|
+
return { yield: true, reason: 'release_window', detail: { windowReason: r.reason, ageMs: r.ageMs } };
|
|
82
|
+
}
|
|
83
|
+
if (input.load) {
|
|
84
|
+
const max = input.load.max ?? defaultLoadMax(input.load.cpuCount ?? MIN_ASSUMED_CPU_COUNT);
|
|
85
|
+
if (input.load.load1m > max)
|
|
86
|
+
return { yield: true, reason: 'system_load', detail: { load1m: input.load.load1m, max } };
|
|
87
|
+
}
|
|
88
|
+
return { yield: false, reason: 'clear' };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Persists the daemon's partial state before a backoff so the next cycle resumes the same hypothesis instead of
|
|
92
|
+
* starting cold. TTL-bounded — a stale dormant state (older than ttlMs) is discarded on read. Atomic write.
|
|
93
|
+
*/
|
|
94
|
+
export class DormantHypothesisStore {
|
|
95
|
+
path;
|
|
96
|
+
opts;
|
|
97
|
+
constructor(path, opts = {}) {
|
|
98
|
+
this.path = path;
|
|
99
|
+
this.opts = opts;
|
|
100
|
+
}
|
|
101
|
+
ttl() { return this.opts.ttlMs ?? 3_600_000; }
|
|
102
|
+
now() { return (this.opts.now ?? (() => Date.now()))(); }
|
|
103
|
+
/** Save partial state before backing off. Never throws. */
|
|
104
|
+
write(data) {
|
|
105
|
+
try {
|
|
106
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
107
|
+
const obj = { ...data, created_at: new Date(this.now()).toISOString(), ttl_ms: this.ttl() };
|
|
108
|
+
const tmp = `${this.path}.tmp`;
|
|
109
|
+
writeFileSync(tmp, `${JSON.stringify(obj, null, 2)}\n`, 'utf8');
|
|
110
|
+
renameSync(tmp, this.path);
|
|
111
|
+
}
|
|
112
|
+
catch { /* non-fatal */ }
|
|
113
|
+
}
|
|
114
|
+
/** Recover partial state, or null if absent/expired/corrupt. Expired state is cleared as a side effect. */
|
|
115
|
+
read() {
|
|
116
|
+
try {
|
|
117
|
+
if (!existsSync(this.path))
|
|
118
|
+
return null;
|
|
119
|
+
const raw = readFileSync(this.path, 'utf8').trim();
|
|
120
|
+
if (!raw)
|
|
121
|
+
return null;
|
|
122
|
+
const obj = JSON.parse(raw);
|
|
123
|
+
const createdAt = obj.created_at ? new Date(obj.created_at).getTime() : 0;
|
|
124
|
+
const ttl = Number.isFinite(Number(obj.ttl_ms)) ? Number(obj.ttl_ms) : this.ttl();
|
|
125
|
+
if (this.now() - createdAt > ttl) {
|
|
126
|
+
this.clear();
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return obj;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
clear() {
|
|
136
|
+
try {
|
|
137
|
+
if (existsSync(this.path))
|
|
138
|
+
unlinkSync(this.path);
|
|
139
|
+
}
|
|
140
|
+
catch { /* non-fatal */ }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** 闸4: cycle_id 幂等键. 同 cycleId+opKey 的副作用只执行一次(daemon 重启 replay/重试不重复). */
|
|
2
|
+
export declare class IdempotencyGuard {
|
|
3
|
+
private readonly path?;
|
|
4
|
+
private readonly done;
|
|
5
|
+
constructor(path?: string | undefined);
|
|
6
|
+
private k;
|
|
7
|
+
has(cycleId: string, opKey: string): boolean;
|
|
8
|
+
once<T>(cycleId: string, opKey: string, fn: () => Promise<T> | T): Promise<T>;
|
|
9
|
+
private persist;
|
|
10
|
+
private load;
|
|
11
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
/** 闸4: cycle_id 幂等键. 同 cycleId+opKey 的副作用只执行一次(daemon 重启 replay/重试不重复). */
|
|
4
|
+
export class IdempotencyGuard {
|
|
5
|
+
path;
|
|
6
|
+
done = new Map();
|
|
7
|
+
constructor(path) {
|
|
8
|
+
this.path = path;
|
|
9
|
+
if (path && existsSync(path))
|
|
10
|
+
this.load();
|
|
11
|
+
}
|
|
12
|
+
k(cycleId, opKey) { return `${cycleId}\x1f${opKey}`; }
|
|
13
|
+
has(cycleId, opKey) { return this.done.has(this.k(cycleId, opKey)); }
|
|
14
|
+
async once(cycleId, opKey, fn) {
|
|
15
|
+
const key = this.k(cycleId, opKey);
|
|
16
|
+
if (this.done.has(key))
|
|
17
|
+
return this.done.get(key);
|
|
18
|
+
const r = await fn();
|
|
19
|
+
this.done.set(key, r);
|
|
20
|
+
this.persist();
|
|
21
|
+
return r;
|
|
22
|
+
}
|
|
23
|
+
persist() {
|
|
24
|
+
if (!this.path)
|
|
25
|
+
return;
|
|
26
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
27
|
+
writeFileSync(this.path, JSON.stringify(Object.fromEntries(this.done)));
|
|
28
|
+
}
|
|
29
|
+
load() {
|
|
30
|
+
if (!this.path)
|
|
31
|
+
return;
|
|
32
|
+
const o = JSON.parse(readFileSync(this.path, 'utf8'));
|
|
33
|
+
for (const [k, v] of Object.entries(o))
|
|
34
|
+
this.done.set(k, v);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
interface IdleCommandResult {
|
|
2
|
+
status: number | null;
|
|
3
|
+
stdout?: string | Buffer | null;
|
|
4
|
+
}
|
|
5
|
+
type IdleCommandRunner = (command: string, args: readonly string[]) => IdleCommandResult;
|
|
6
|
+
export type Intensity = 'normal' | 'aggressive' | 'deep';
|
|
7
|
+
export interface IdleThresholds {
|
|
8
|
+
/** Idle ≥ this (s) → aggressive. Default 300 (5min). */
|
|
9
|
+
idleSeconds: number;
|
|
10
|
+
/** Idle ≥ this (s) → deep. Default 1800 (30min). */
|
|
11
|
+
deepIdleSeconds: number;
|
|
12
|
+
}
|
|
13
|
+
export declare const DEFAULT_IDLE_THRESHOLDS: IdleThresholds;
|
|
14
|
+
/** Map idle seconds → evolution intensity. Unknown idle (-1) or active → normal. Pure. */
|
|
15
|
+
export declare function determineIntensity(idleSeconds: number, th?: IdleThresholds): Intensity;
|
|
16
|
+
export interface ScheduleRecommendation {
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
idleSeconds: number;
|
|
19
|
+
/** normal / aggressive / deep — encodes the level; a consumer derives any per-intensity action from it. */
|
|
20
|
+
intensity: Intensity;
|
|
21
|
+
/** Poll-interval multiplier: <1 runs more often when idle, 1 = normal cadence. */
|
|
22
|
+
sleepMultiplier: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Turn an idle reading into a scheduling recommendation: aggressive idle → run twice as often; deep idle → 4×.
|
|
26
|
+
* Pure given idleSeconds. enabled:false → a normal-cadence no-op. The `intensity` is the single signal a consumer
|
|
27
|
+
* acts on (the previous per-action `should*` flags were never consumed anywhere, so they are not emitted —
|
|
28
|
+
* re-derive any action mapping at the consumer from `intensity` if/when one exists).
|
|
29
|
+
*/
|
|
30
|
+
export declare function recommendSchedule(idleSeconds: number, opts?: {
|
|
31
|
+
enabled?: boolean;
|
|
32
|
+
thresholds?: IdleThresholds;
|
|
33
|
+
}): ScheduleRecommendation;
|
|
34
|
+
/** Reads the OS user-idle time in seconds; -1 when unknown. Inject a fake in tests / a better probe in the daemon. */
|
|
35
|
+
export type IdleProbe = () => number;
|
|
36
|
+
type LinuxIdleMethodName = 'xprintidle' | 'gnome-mutter' | 'loginctl';
|
|
37
|
+
export declare function tryLinuxXprintidle(): number;
|
|
38
|
+
export declare function tryLinuxGnomeMutterIdleMonitor(): number;
|
|
39
|
+
export declare function tryLinuxLoginctlIdleHint(): number;
|
|
40
|
+
export declare function getLinuxIdleSeconds(): number;
|
|
41
|
+
export declare function resetLinuxIdleProbeCacheForTests(): void;
|
|
42
|
+
export declare function setIdleCommandRunnerForTests(runner?: IdleCommandRunner): void;
|
|
43
|
+
export declare function getCachedLinuxIdleMethodForTests(): LinuxIdleMethodName | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Best-effort default idle probe. linux: xprintidle → GNOME Mutter → loginctl; darwin: ioreg HIDIdleTime (ns). win32 and any
|
|
46
|
+
* missing tool → -1 (→ normal cadence) rather than failing — idle detection is an optimization, not a gate.
|
|
47
|
+
*/
|
|
48
|
+
export declare function defaultIdleProbe(): number;
|
|
49
|
+
/** Convenience: probe idle then recommend. The probe defaults to the platform reader; inject for tests. */
|
|
50
|
+
export declare function getScheduleRecommendation(opts?: {
|
|
51
|
+
enabled?: boolean;
|
|
52
|
+
thresholds?: IdleThresholds;
|
|
53
|
+
probe?: IdleProbe;
|
|
54
|
+
}): ScheduleRecommendation;
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// Idle-aware scheduler (ported from v1 gep/idleScheduler.js). A "good neighbour" for the resident loop: when
|
|
2
|
+
// the machine has been idle a while, evolution can run harder (distill / reflect / explore) and more often;
|
|
3
|
+
// when the user is active, it backs off to a normal cadence. The DECISION logic is pure and config-driven; the
|
|
4
|
+
// platform idle reading is an injected probe seam (default best-effort, graceful → -1 → normal cadence).
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
const IDLE_PROBE_TIMEOUT_MS = 5000;
|
|
7
|
+
function defaultIdleCommandRunner(command, args) {
|
|
8
|
+
return spawnSync(command, [...args], { encoding: 'utf8', timeout: IDLE_PROBE_TIMEOUT_MS });
|
|
9
|
+
}
|
|
10
|
+
let idleCommandRunner = defaultIdleCommandRunner;
|
|
11
|
+
function readIdleCommand(command, args) {
|
|
12
|
+
try {
|
|
13
|
+
const result = idleCommandRunner(command, args);
|
|
14
|
+
if (result.status !== 0)
|
|
15
|
+
return null;
|
|
16
|
+
return String(result.stdout ?? '').trim();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// Idle detection is advisory. Treat command failures as unknown idle.
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export const DEFAULT_IDLE_THRESHOLDS = { idleSeconds: 300, deepIdleSeconds: 1800 };
|
|
24
|
+
/** Map idle seconds → evolution intensity. Unknown idle (-1) or active → normal. Pure. */
|
|
25
|
+
export function determineIntensity(idleSeconds, th = DEFAULT_IDLE_THRESHOLDS) {
|
|
26
|
+
if (!Number.isFinite(idleSeconds) || idleSeconds < 0)
|
|
27
|
+
return 'normal';
|
|
28
|
+
if (idleSeconds >= th.deepIdleSeconds)
|
|
29
|
+
return 'deep';
|
|
30
|
+
if (idleSeconds >= th.idleSeconds)
|
|
31
|
+
return 'aggressive';
|
|
32
|
+
return 'normal';
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Turn an idle reading into a scheduling recommendation: aggressive idle → run twice as often; deep idle → 4×.
|
|
36
|
+
* Pure given idleSeconds. enabled:false → a normal-cadence no-op. The `intensity` is the single signal a consumer
|
|
37
|
+
* acts on (the previous per-action `should*` flags were never consumed anywhere, so they are not emitted —
|
|
38
|
+
* re-derive any action mapping at the consumer from `intensity` if/when one exists).
|
|
39
|
+
*/
|
|
40
|
+
export function recommendSchedule(idleSeconds, opts = {}) {
|
|
41
|
+
if (opts.enabled === false)
|
|
42
|
+
return { enabled: false, idleSeconds: -1, intensity: 'normal', sleepMultiplier: 1 };
|
|
43
|
+
const intensity = determineIntensity(idleSeconds, opts.thresholds);
|
|
44
|
+
const sleepMultiplier = intensity === 'aggressive' ? 0.5 : intensity === 'deep' ? 0.25 : 1;
|
|
45
|
+
return { enabled: true, idleSeconds, intensity, sleepMultiplier };
|
|
46
|
+
}
|
|
47
|
+
export function tryLinuxXprintidle() {
|
|
48
|
+
const out = readIdleCommand('xprintidle', []);
|
|
49
|
+
if (out === null)
|
|
50
|
+
return -1;
|
|
51
|
+
const ms = Number.parseInt(out, 10);
|
|
52
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
53
|
+
return -1;
|
|
54
|
+
return Math.floor(ms / 1000);
|
|
55
|
+
}
|
|
56
|
+
export function tryLinuxGnomeMutterIdleMonitor() {
|
|
57
|
+
const out = readIdleCommand('gdbus', [
|
|
58
|
+
'call',
|
|
59
|
+
'--session',
|
|
60
|
+
'--dest',
|
|
61
|
+
'org.gnome.Mutter.IdleMonitor',
|
|
62
|
+
'--object-path',
|
|
63
|
+
'/org/gnome/Mutter/IdleMonitor/Core',
|
|
64
|
+
'--method',
|
|
65
|
+
'org.gnome.Mutter.IdleMonitor.GetIdletime',
|
|
66
|
+
]);
|
|
67
|
+
if (out === null)
|
|
68
|
+
return -1;
|
|
69
|
+
const match = /uint64\s+(\d+)/.exec(out);
|
|
70
|
+
if (!match)
|
|
71
|
+
return -1;
|
|
72
|
+
const rawMs = match[1];
|
|
73
|
+
if (rawMs === undefined)
|
|
74
|
+
return -1;
|
|
75
|
+
const ms = Number.parseInt(rawMs, 10);
|
|
76
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
77
|
+
return -1;
|
|
78
|
+
return Math.floor(ms / 1000);
|
|
79
|
+
}
|
|
80
|
+
export function tryLinuxLoginctlIdleHint() {
|
|
81
|
+
const sessionId = process.env['XDG_SESSION_ID'];
|
|
82
|
+
if (!sessionId || !isSafeLoginctlSessionId(sessionId))
|
|
83
|
+
return -1;
|
|
84
|
+
const out = readIdleCommand('loginctl', ['show-session', sessionId, '-p', 'IdleHint', '-p', 'IdleSinceHint']);
|
|
85
|
+
if (out === null)
|
|
86
|
+
return -1;
|
|
87
|
+
const hintMatch = /(?:^|\n)IdleHint=(yes|no)(?:\n|$)/.exec(out);
|
|
88
|
+
if (!hintMatch)
|
|
89
|
+
return -1;
|
|
90
|
+
const idleHint = hintMatch[1];
|
|
91
|
+
if (idleHint === undefined)
|
|
92
|
+
return -1;
|
|
93
|
+
if (idleHint === 'no')
|
|
94
|
+
return 0;
|
|
95
|
+
const sinceMatch = /(?:^|\n)IdleSinceHint=(\d+)(?:\n|$)/.exec(out);
|
|
96
|
+
if (!sinceMatch)
|
|
97
|
+
return -1;
|
|
98
|
+
const rawSinceUsec = sinceMatch[1];
|
|
99
|
+
if (rawSinceUsec === undefined)
|
|
100
|
+
return -1;
|
|
101
|
+
const sinceUsec = Number.parseInt(rawSinceUsec, 10);
|
|
102
|
+
if (!Number.isFinite(sinceUsec) || sinceUsec <= 0)
|
|
103
|
+
return -1;
|
|
104
|
+
const idleSeconds = Math.floor(((Date.now() * 1000) - sinceUsec) / 1_000_000);
|
|
105
|
+
return idleSeconds >= 0 ? idleSeconds : -1;
|
|
106
|
+
}
|
|
107
|
+
function isSafeLoginctlSessionId(sessionId) {
|
|
108
|
+
if (!/^[A-Za-z0-9]+$/.test(sessionId))
|
|
109
|
+
return false;
|
|
110
|
+
const lowerSessionId = sessionId.toLowerCase();
|
|
111
|
+
return lowerSessionId !== 'self' && lowerSessionId !== 'auto';
|
|
112
|
+
}
|
|
113
|
+
const LINUX_IDLE_PROBES = {
|
|
114
|
+
xprintidle: tryLinuxXprintidle,
|
|
115
|
+
'gnome-mutter': tryLinuxGnomeMutterIdleMonitor,
|
|
116
|
+
loginctl: tryLinuxLoginctlIdleHint,
|
|
117
|
+
};
|
|
118
|
+
function isWaylandSession() {
|
|
119
|
+
return process.env['XDG_SESSION_TYPE']?.toLowerCase() === 'wayland';
|
|
120
|
+
}
|
|
121
|
+
function isGnomeMutterDesktop() {
|
|
122
|
+
const currentDesktop = process.env['XDG_CURRENT_DESKTOP']?.toLowerCase();
|
|
123
|
+
return currentDesktop?.split(':').some((part) => part === 'gnome' || part.includes('mutter')) === true;
|
|
124
|
+
}
|
|
125
|
+
function getLinuxIdleMethodOrder() {
|
|
126
|
+
if (!isWaylandSession())
|
|
127
|
+
return ['xprintidle', 'gnome-mutter', 'loginctl'];
|
|
128
|
+
if (isGnomeMutterDesktop())
|
|
129
|
+
return ['gnome-mutter', 'loginctl', 'xprintidle'];
|
|
130
|
+
return ['loginctl', 'xprintidle', 'gnome-mutter'];
|
|
131
|
+
}
|
|
132
|
+
let cachedLinuxIdleMethod;
|
|
133
|
+
export function getLinuxIdleSeconds() {
|
|
134
|
+
if (cachedLinuxIdleMethod) {
|
|
135
|
+
const idleSeconds = LINUX_IDLE_PROBES[cachedLinuxIdleMethod]();
|
|
136
|
+
if (idleSeconds >= 0)
|
|
137
|
+
return idleSeconds;
|
|
138
|
+
cachedLinuxIdleMethod = undefined;
|
|
139
|
+
}
|
|
140
|
+
for (const name of getLinuxIdleMethodOrder()) {
|
|
141
|
+
const idleSeconds = LINUX_IDLE_PROBES[name]();
|
|
142
|
+
if (idleSeconds >= 0) {
|
|
143
|
+
cachedLinuxIdleMethod = name;
|
|
144
|
+
return idleSeconds;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return -1;
|
|
148
|
+
}
|
|
149
|
+
export function resetLinuxIdleProbeCacheForTests() {
|
|
150
|
+
cachedLinuxIdleMethod = undefined;
|
|
151
|
+
}
|
|
152
|
+
export function setIdleCommandRunnerForTests(runner) {
|
|
153
|
+
idleCommandRunner = runner ?? defaultIdleCommandRunner;
|
|
154
|
+
}
|
|
155
|
+
export function getCachedLinuxIdleMethodForTests() {
|
|
156
|
+
return cachedLinuxIdleMethod;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Best-effort default idle probe. linux: xprintidle → GNOME Mutter → loginctl; darwin: ioreg HIDIdleTime (ns). win32 and any
|
|
160
|
+
* missing tool → -1 (→ normal cadence) rather than failing — idle detection is an optimization, not a gate.
|
|
161
|
+
*/
|
|
162
|
+
export function defaultIdleProbe() {
|
|
163
|
+
if (process.platform === 'linux')
|
|
164
|
+
return getLinuxIdleSeconds();
|
|
165
|
+
if (process.platform === 'darwin') {
|
|
166
|
+
const out = readIdleCommand('ioreg', ['-c', 'IOHIDSystem']);
|
|
167
|
+
if (out !== null) {
|
|
168
|
+
const match = /"HIDIdleTime"\s*=\s*(\d+)/.exec(out);
|
|
169
|
+
if (match?.[1] !== undefined)
|
|
170
|
+
return Math.floor(Number(match[1]) / 1_000_000_000);
|
|
171
|
+
}
|
|
172
|
+
return -1;
|
|
173
|
+
}
|
|
174
|
+
return -1; // win32 / unsupported / tool missing → unknown
|
|
175
|
+
}
|
|
176
|
+
/** Convenience: probe idle then recommend. The probe defaults to the platform reader; inject for tests. */
|
|
177
|
+
export function getScheduleRecommendation(opts = {}) {
|
|
178
|
+
if (opts.enabled === false)
|
|
179
|
+
return recommendSchedule(-1, { enabled: false });
|
|
180
|
+
return recommendSchedule((opts.probe ?? defaultIdleProbe)(), { ...(opts.thresholds ? { thresholds: opts.thresholds } : {}) });
|
|
181
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './cycleContext.js';
|
|
2
|
+
export * from './idempotency.js';
|
|
3
|
+
export * from './watchdog.js';
|
|
4
|
+
export * from './drain.js';
|
|
5
|
+
export * from './stepRunner.js';
|
|
6
|
+
export * from './guards.js';
|
|
7
|
+
export * from './idleScheduler.js';
|
|
8
|
+
export * from './unhandledRejectionWindow.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './cycleContext.js';
|
|
2
|
+
export * from './idempotency.js';
|
|
3
|
+
export * from './watchdog.js';
|
|
4
|
+
export * from './drain.js';
|
|
5
|
+
export * from './stepRunner.js';
|
|
6
|
+
export * from './guards.js';
|
|
7
|
+
export * from './idleScheduler.js';
|
|
8
|
+
export * from './unhandledRejectionWindow.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { CycleContext } from './cycleContext.js';
|
|
2
|
+
import type { DrainController } from './drain.js';
|
|
3
|
+
/** loop 调度与 step 执行解耦 (批注#1): --once 与 daemon 跑同一 step 路径. */
|
|
4
|
+
export type StepFn = (ctx: CycleContext) => Promise<void> | void;
|
|
5
|
+
/** 单步 (evolver run --once 与 daemon 单轮共用). */
|
|
6
|
+
export declare function runStep(cycleId: string, step: StepFn): Promise<void>;
|
|
7
|
+
/** daemon 循环: 反复调 runStep (同一 step 路径); drain 时停接. */
|
|
8
|
+
export declare function runLoop(opts: {
|
|
9
|
+
nextCycleId: () => string | null;
|
|
10
|
+
step: StepFn;
|
|
11
|
+
drain: DrainController;
|
|
12
|
+
}): Promise<number>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { CycleContext } from './cycleContext.js';
|
|
2
|
+
/** 单步 (evolver run --once 与 daemon 单轮共用). */
|
|
3
|
+
export async function runStep(cycleId, step) {
|
|
4
|
+
const ctx = new CycleContext(cycleId); // 闸1: 每 cycle 新 context
|
|
5
|
+
try {
|
|
6
|
+
await step(ctx);
|
|
7
|
+
}
|
|
8
|
+
finally {
|
|
9
|
+
ctx.dispose();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** daemon 循环: 反复调 runStep (同一 step 路径); drain 时停接. */
|
|
13
|
+
export async function runLoop(opts) {
|
|
14
|
+
let n = 0;
|
|
15
|
+
for (;;) {
|
|
16
|
+
if (!opts.drain.acceptCycle())
|
|
17
|
+
break;
|
|
18
|
+
const id = opts.nextCycleId();
|
|
19
|
+
if (id === null)
|
|
20
|
+
break;
|
|
21
|
+
opts.drain.beginCycle();
|
|
22
|
+
try {
|
|
23
|
+
await runStep(id, opts.step);
|
|
24
|
+
n += 1;
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
opts.drain.endCycle();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return n;
|
|
31
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const DEFAULT_UNHANDLED_REJECTION_WINDOW_MS: number;
|
|
2
|
+
export declare const DEFAULT_UNHANDLED_REJECTION_THRESHOLD = 5;
|
|
3
|
+
export type UnhandledRejectionListener = (reason: unknown, promise?: Promise<unknown>) => void;
|
|
4
|
+
export interface UnhandledRejectionProcess {
|
|
5
|
+
on(event: 'unhandledRejection', listener: UnhandledRejectionListener): unknown;
|
|
6
|
+
off?(event: 'unhandledRejection', listener: UnhandledRejectionListener): unknown;
|
|
7
|
+
removeListener?(event: 'unhandledRejection', listener: UnhandledRejectionListener): unknown;
|
|
8
|
+
}
|
|
9
|
+
export interface UnhandledRejectionWindowOptions {
|
|
10
|
+
windowMs?: number;
|
|
11
|
+
threshold?: number;
|
|
12
|
+
now?: () => number;
|
|
13
|
+
write?: (line: string) => void;
|
|
14
|
+
beforeExit?: () => void;
|
|
15
|
+
exit?: (code: number) => void;
|
|
16
|
+
}
|
|
17
|
+
export interface InstallUnhandledRejectionWindowOptions extends UnhandledRejectionWindowOptions {
|
|
18
|
+
process?: UnhandledRejectionProcess;
|
|
19
|
+
}
|
|
20
|
+
export declare function createUnhandledRejectionWindowHandler(options?: UnhandledRejectionWindowOptions): UnhandledRejectionListener;
|
|
21
|
+
export declare function installUnhandledRejectionWindow(options?: InstallUnhandledRejectionWindowOptions): () => void;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export const DEFAULT_UNHANDLED_REJECTION_WINDOW_MS = 5 * 60 * 1000;
|
|
2
|
+
export const DEFAULT_UNHANDLED_REJECTION_THRESHOLD = 5;
|
|
3
|
+
export function createUnhandledRejectionWindowHandler(options = {}) {
|
|
4
|
+
const windowMs = positiveNumber(options.windowMs, DEFAULT_UNHANDLED_REJECTION_WINDOW_MS);
|
|
5
|
+
const threshold = positiveNumber(options.threshold, DEFAULT_UNHANDLED_REJECTION_THRESHOLD);
|
|
6
|
+
const now = options.now ?? (() => Date.now());
|
|
7
|
+
const write = options.write ?? ((line) => { process.stderr.write(`${line}\n`); });
|
|
8
|
+
const exit = options.exit ?? ((code) => { process.exit(code); });
|
|
9
|
+
let timestamps = [];
|
|
10
|
+
let exiting = false;
|
|
11
|
+
return (reason) => {
|
|
12
|
+
if (exiting)
|
|
13
|
+
return;
|
|
14
|
+
const at = now();
|
|
15
|
+
timestamps.push(at);
|
|
16
|
+
timestamps = timestamps.filter((t) => at - t < windowMs);
|
|
17
|
+
write(`[FATAL] Unhandled promise rejection (${timestamps.length} in window): ${formatReason(reason)}`);
|
|
18
|
+
if (timestamps.length < threshold)
|
|
19
|
+
return;
|
|
20
|
+
exiting = true;
|
|
21
|
+
write(`[FATAL] ${timestamps.length} unhandled rejections within ${windowMs / 1000}s. Exiting to avoid corrupt state.`);
|
|
22
|
+
try {
|
|
23
|
+
options.beforeExit?.();
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
write(`[FATAL] unhandled rejection shutdown hook failed: ${formatReason(e)}`);
|
|
27
|
+
}
|
|
28
|
+
exit(1);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function installUnhandledRejectionWindow(options = {}) {
|
|
32
|
+
const proc = options.process ?? process;
|
|
33
|
+
const handler = createUnhandledRejectionWindowHandler(options);
|
|
34
|
+
proc.on('unhandledRejection', handler);
|
|
35
|
+
return () => {
|
|
36
|
+
if (proc.off) {
|
|
37
|
+
proc.off('unhandledRejection', handler);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
proc.removeListener?.('unhandledRejection', handler);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function positiveNumber(value, fallback) {
|
|
44
|
+
if (value === undefined || !Number.isFinite(value) || value <= 0)
|
|
45
|
+
return fallback;
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
function formatReason(reason) {
|
|
49
|
+
if (reason instanceof Error && reason.stack)
|
|
50
|
+
return reason.stack;
|
|
51
|
+
return String(reason);
|
|
52
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** 闸3: 外部 watchdog, 按 root_events 最后写入时间判挂死 (硬化 A8). 不依赖主进程自报活. */
|
|
2
|
+
export interface WatchdogOptions {
|
|
3
|
+
lastWriteAt: () => number;
|
|
4
|
+
stallThresholdMs: number;
|
|
5
|
+
onStall: (idleMs: number) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare class Watchdog {
|
|
8
|
+
private readonly opts;
|
|
9
|
+
constructor(opts: WatchdogOptions);
|
|
10
|
+
check(now: number): boolean;
|
|
11
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class Watchdog {
|
|
2
|
+
opts;
|
|
3
|
+
constructor(opts) {
|
|
4
|
+
this.opts = opts;
|
|
5
|
+
}
|
|
6
|
+
check(now) {
|
|
7
|
+
const idle = now - this.opts.lastWriteAt();
|
|
8
|
+
if (idle >= this.opts.stallThresholdMs) {
|
|
9
|
+
this.opts.onStall(idle);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|