instar 0.28.102 → 0.28.103
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/.claude/hooks/instar/before-prompt-recall.js +90 -0
- package/dist/cli.js +28 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +14 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/jobMigrate.d.ts +46 -0
- package/dist/commands/jobMigrate.d.ts.map +1 -0
- package/dist/commands/jobMigrate.js +292 -0
- package/dist/commands/jobMigrate.js.map +1 -0
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +88 -42
- package/dist/commands/server.js.map +1 -1
- package/dist/core/BackupManager.d.ts +27 -1
- package/dist/core/BackupManager.d.ts.map +1 -1
- package/dist/core/BackupManager.js +56 -1
- package/dist/core/BackupManager.js.map +1 -1
- package/dist/core/GitStateManager.d.ts +8 -0
- package/dist/core/GitStateManager.d.ts.map +1 -1
- package/dist/core/GitStateManager.js +31 -1
- package/dist/core/GitStateManager.js.map +1 -1
- package/dist/core/MigratorStepEngine.d.ts +170 -0
- package/dist/core/MigratorStepEngine.d.ts.map +1 -0
- package/dist/core/MigratorStepEngine.js +352 -0
- package/dist/core/MigratorStepEngine.js.map +1 -0
- package/dist/core/PostUpdateMigrator.d.ts +76 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +252 -1
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/PreCompactionFlush.d.ts +101 -0
- package/dist/core/PreCompactionFlush.d.ts.map +1 -0
- package/dist/core/PreCompactionFlush.js +282 -0
- package/dist/core/PreCompactionFlush.js.map +1 -0
- package/dist/core/PromptBuildRecall.d.ts +97 -0
- package/dist/core/PromptBuildRecall.d.ts.map +1 -0
- package/dist/core/PromptBuildRecall.js +152 -0
- package/dist/core/PromptBuildRecall.js.map +1 -0
- package/dist/core/selectIntelligenceProvider.d.ts +41 -0
- package/dist/core/selectIntelligenceProvider.d.ts.map +1 -0
- package/dist/core/selectIntelligenceProvider.js +89 -0
- package/dist/core/selectIntelligenceProvider.js.map +1 -0
- package/dist/lifeline/ServerSupervisor.d.ts +183 -0
- package/dist/lifeline/ServerSupervisor.d.ts.map +1 -1
- package/dist/lifeline/ServerSupervisor.js +443 -0
- package/dist/lifeline/ServerSupervisor.js.map +1 -1
- package/dist/memory/NativeModuleHealer.d.ts +89 -0
- package/dist/memory/NativeModuleHealer.d.ts.map +1 -1
- package/dist/memory/NativeModuleHealer.js +371 -1
- package/dist/memory/NativeModuleHealer.js.map +1 -1
- package/dist/memory/SemanticMemory.d.ts +131 -0
- package/dist/memory/SemanticMemory.d.ts.map +1 -1
- package/dist/memory/SemanticMemory.js +315 -0
- package/dist/memory/SemanticMemory.js.map +1 -1
- package/dist/messaging/DeliveryRetryManager.d.ts +60 -0
- package/dist/messaging/DeliveryRetryManager.d.ts.map +1 -1
- package/dist/messaging/DeliveryRetryManager.js +104 -0
- package/dist/messaging/DeliveryRetryManager.js.map +1 -1
- package/dist/monitoring/DegradationReporter.d.ts +164 -0
- package/dist/monitoring/DegradationReporter.d.ts.map +1 -1
- package/dist/monitoring/DegradationReporter.js +309 -1
- package/dist/monitoring/DegradationReporter.js.map +1 -1
- package/dist/monitoring/ErrorCodeExtractor.d.ts +69 -0
- package/dist/monitoring/ErrorCodeExtractor.d.ts.map +1 -0
- package/dist/monitoring/ErrorCodeExtractor.js +131 -0
- package/dist/monitoring/ErrorCodeExtractor.js.map +1 -0
- package/dist/monitoring/PromptGate.d.ts +34 -0
- package/dist/monitoring/PromptGate.d.ts.map +1 -1
- package/dist/monitoring/PromptGate.js +81 -1
- package/dist/monitoring/PromptGate.js.map +1 -1
- package/dist/monitoring/Redactor.d.ts +58 -0
- package/dist/monitoring/Redactor.d.ts.map +1 -0
- package/dist/monitoring/Redactor.js +143 -0
- package/dist/monitoring/Redactor.js.map +1 -0
- package/dist/monitoring/probes/LifelineProbe.d.ts +7 -0
- package/dist/monitoring/probes/LifelineProbe.d.ts.map +1 -1
- package/dist/monitoring/probes/LifelineProbe.js +7 -0
- package/dist/monitoring/probes/LifelineProbe.js.map +1 -1
- package/dist/monitoring/probes/__shared.d.ts +50 -0
- package/dist/monitoring/probes/__shared.d.ts.map +1 -0
- package/dist/monitoring/probes/__shared.js +51 -0
- package/dist/monitoring/probes/__shared.js.map +1 -0
- package/dist/remediation/IntentJournal.d.ts +64 -0
- package/dist/remediation/IntentJournal.d.ts.map +1 -0
- package/dist/remediation/IntentJournal.js +118 -0
- package/dist/remediation/IntentJournal.js.map +1 -0
- package/dist/remediation/MachineLock.d.ts +137 -0
- package/dist/remediation/MachineLock.d.ts.map +1 -0
- package/dist/remediation/MachineLock.js +314 -0
- package/dist/remediation/MachineLock.js.map +1 -0
- package/dist/remediation/PrimaryAggregatorLease.d.ts +205 -0
- package/dist/remediation/PrimaryAggregatorLease.d.ts.map +1 -0
- package/dist/remediation/PrimaryAggregatorLease.js +450 -0
- package/dist/remediation/PrimaryAggregatorLease.js.map +1 -0
- package/dist/remediation/RemediationContext.d.ts +63 -0
- package/dist/remediation/RemediationContext.d.ts.map +1 -0
- package/dist/remediation/RemediationContext.js +109 -0
- package/dist/remediation/RemediationContext.js.map +1 -0
- package/dist/remediation/RemediationKeyVault.d.ts +123 -0
- package/dist/remediation/RemediationKeyVault.d.ts.map +1 -0
- package/dist/remediation/RemediationKeyVault.js +564 -0
- package/dist/remediation/RemediationKeyVault.js.map +1 -0
- package/dist/remediation/Remediator.d.ts +288 -0
- package/dist/remediation/Remediator.d.ts.map +1 -0
- package/dist/remediation/Remediator.js +520 -0
- package/dist/remediation/Remediator.js.map +1 -0
- package/dist/remediation/RemediatorBootstrap.d.ts +141 -0
- package/dist/remediation/RemediatorBootstrap.d.ts.map +1 -0
- package/dist/remediation/RemediatorBootstrap.js +255 -0
- package/dist/remediation/RemediatorBootstrap.js.map +1 -0
- package/dist/remediation/TrustElevationSource.d.ts +153 -0
- package/dist/remediation/TrustElevationSource.d.ts.map +1 -0
- package/dist/remediation/TrustElevationSource.js +208 -0
- package/dist/remediation/TrustElevationSource.js.map +1 -0
- package/dist/remediation/audit/AuditProjection.d.ts +37 -0
- package/dist/remediation/audit/AuditProjection.d.ts.map +1 -0
- package/dist/remediation/audit/AuditProjection.js +118 -0
- package/dist/remediation/audit/AuditProjection.js.map +1 -0
- package/dist/remediation/audit/AuditWriter.d.ts +79 -0
- package/dist/remediation/audit/AuditWriter.d.ts.map +1 -0
- package/dist/remediation/audit/AuditWriter.js +132 -0
- package/dist/remediation/audit/AuditWriter.js.map +1 -0
- package/dist/remediation/channels/CliApprovalChannel.d.ts +37 -0
- package/dist/remediation/channels/CliApprovalChannel.d.ts.map +1 -0
- package/dist/remediation/channels/CliApprovalChannel.js +45 -0
- package/dist/remediation/channels/CliApprovalChannel.js.map +1 -0
- package/dist/remediation/channels/TelegramApprovalChannel.d.ts +37 -0
- package/dist/remediation/channels/TelegramApprovalChannel.d.ts.map +1 -0
- package/dist/remediation/channels/TelegramApprovalChannel.js +45 -0
- package/dist/remediation/channels/TelegramApprovalChannel.js.map +1 -0
- package/dist/remediation/runbooks/db-corruption.d.ts +86 -0
- package/dist/remediation/runbooks/db-corruption.d.ts.map +1 -0
- package/dist/remediation/runbooks/db-corruption.js +190 -0
- package/dist/remediation/runbooks/db-corruption.js.map +1 -0
- package/dist/remediation/runbooks/messaging-delivery-failed.d.ts +108 -0
- package/dist/remediation/runbooks/messaging-delivery-failed.d.ts.map +1 -0
- package/dist/remediation/runbooks/messaging-delivery-failed.js +172 -0
- package/dist/remediation/runbooks/messaging-delivery-failed.js.map +1 -0
- package/dist/remediation/runbooks/node-abi-mismatch.d.ts +70 -0
- package/dist/remediation/runbooks/node-abi-mismatch.d.ts.map +1 -0
- package/dist/remediation/runbooks/node-abi-mismatch.js +216 -0
- package/dist/remediation/runbooks/node-abi-mismatch.js.map +1 -0
- package/dist/remediation/runbooks/supervisor-preflight.d.ts +114 -0
- package/dist/remediation/runbooks/supervisor-preflight.d.ts.map +1 -0
- package/dist/remediation/runbooks/supervisor-preflight.js +299 -0
- package/dist/remediation/runbooks/supervisor-preflight.js.map +1 -0
- package/dist/scheduler/AgentMdAtomicSave.d.ts +85 -0
- package/dist/scheduler/AgentMdAtomicSave.d.ts.map +1 -0
- package/dist/scheduler/AgentMdAtomicSave.js +169 -0
- package/dist/scheduler/AgentMdAtomicSave.js.map +1 -0
- package/dist/scheduler/AgentMdReconcile.d.ts +62 -0
- package/dist/scheduler/AgentMdReconcile.d.ts.map +1 -0
- package/dist/scheduler/AgentMdReconcile.js +212 -0
- package/dist/scheduler/AgentMdReconcile.js.map +1 -0
- package/dist/scheduler/DisabledBodyDrift.d.ts +70 -0
- package/dist/scheduler/DisabledBodyDrift.d.ts.map +1 -0
- package/dist/scheduler/DisabledBodyDrift.js +149 -0
- package/dist/scheduler/DisabledBodyDrift.js.map +1 -0
- package/dist/scheduler/InstallBuiltinJobs.d.ts +53 -0
- package/dist/scheduler/InstallBuiltinJobs.d.ts.map +1 -0
- package/dist/scheduler/InstallBuiltinJobs.js +212 -0
- package/dist/scheduler/InstallBuiltinJobs.js.map +1 -0
- package/dist/scheduler/JobLoader.d.ts.map +1 -1
- package/dist/scheduler/JobLoader.js +48 -0
- package/dist/scheduler/JobLoader.js.map +1 -1
- package/dist/scheduler/MigrationInvariants.d.ts +62 -0
- package/dist/scheduler/MigrationInvariants.d.ts.map +1 -0
- package/dist/scheduler/MigrationInvariants.js +215 -0
- package/dist/scheduler/MigrationInvariants.js.map +1 -0
- package/dist/scheduler/MigrationLedger.d.ts +84 -0
- package/dist/scheduler/MigrationLedger.d.ts.map +1 -0
- package/dist/scheduler/MigrationLedger.js +114 -0
- package/dist/scheduler/MigrationLedger.js.map +1 -0
- package/dist/server/fileRoutes.d.ts.map +1 -1
- package/dist/server/fileRoutes.js +7 -0
- package/dist/server/fileRoutes.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +141 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +3 -2
- package/scripts/classify-default-drift.mjs +238 -0
- package/scripts/eli16-overview-check.mjs +85 -0
- package/scripts/instar-dev-precommit.js +60 -1
- package/scripts/lint-degradation-emit-sites.js +116 -0
- package/scripts/lint-no-direct-destructive.js +12 -0
- package/scripts/protect-migration-guarantee.js +61 -0
- package/scripts/regen-default-job-templates.mjs +111 -0
- package/scripts/verify-runbook-pr-signature.js +366 -0
- package/src/data/builtin-manifest.json +93 -93
- package/src/scaffold/bootstrap.ts +138 -0
- package/src/scaffold/keys/instar-release-pub.pem +3 -0
- package/src/scaffold/templates/jobs/instar/.gitkeep +0 -0
- package/src/scaffold/templates/jobs/instar/commitment-detection.md +28 -0
- package/src/scaffold/templates/jobs/instar/evolution-overdue-check.md +28 -0
- package/src/scaffold/templates/jobs/instar/evolution-proposal-evaluate.md +30 -0
- package/src/scaffold/templates/jobs/instar/evolution-proposal-implement.md +25 -0
- package/src/scaffold/templates/jobs/instar/health-check.md +14 -0
- package/src/scaffold/templates/jobs/instar/identity-review.md +42 -0
- package/src/scaffold/templates/jobs/instar/insight-harvest.md +31 -0
- package/src/scaffold/templates/jobs/instar/overseer-development.md +23 -0
- package/src/scaffold/templates/jobs/instar/overseer-guardian.md +28 -0
- package/src/scaffold/templates/jobs/instar/overseer-infrastructure.md +26 -0
- package/src/scaffold/templates/jobs/instar/overseer-learning.md +28 -0
- package/src/scaffold/templates/jobs/instar/overseer-maintenance.md +27 -0
- package/src/scaffold/templates/jobs/instar/reflection-trigger.md +40 -0
- package/src/scaffold/templates/jobs/instar/relationship-maintenance.md +16 -0
- package/src/scaffold/templates.ts +1616 -0
- package/upgrades/0.28.102.md +6 -7
- package/upgrades/0.28.103.md +471 -0
- package/upgrades/side-effects/a47-primary-aggregator-lease.md +82 -0
- package/upgrades/side-effects/agentmd-atomic-save.md +66 -0
- package/upgrades/side-effects/agentmd-loader-200jobs-perf-benchmark.md +50 -0
- package/upgrades/side-effects/agentmd-reconcile-boot-lifecycle.md +66 -0
- package/upgrades/side-effects/api-safety-guard.md +80 -0
- package/upgrades/side-effects/c1-runbook-pr-gate.md +83 -0
- package/upgrades/side-effects/disabled-body-drift-detection.md +59 -0
- package/upgrades/side-effects/drift-classifier-release-time.md +71 -0
- package/upgrades/side-effects/eli16-overview-required-gate.md +77 -0
- package/upgrades/side-effects/f1-remediation-key-vault.md +195 -0
- package/upgrades/side-effects/f2-redactor-errorcode-extractor.md +130 -0
- package/upgrades/side-effects/f3-degradation-reporter-shim.md +105 -0
- package/upgrades/side-effects/f4-machine-locks-intent-journal-audit.md +117 -0
- package/upgrades/side-effects/f5-trust-elevation-source.md +258 -0
- package/upgrades/side-effects/f6-supervisor-handshake.md +234 -0
- package/upgrades/side-effects/f7-post-update-migrator-atomic-step.md +129 -0
- package/upgrades/side-effects/f8-remediator-skeleton.md +153 -0
- package/upgrades/side-effects/f8-rest-enforcement.md +72 -0
- package/upgrades/side-effects/file-viewer-jobs-instar-never-editable.md +41 -0
- package/upgrades/side-effects/frontmatter-interpolation-breakout-test.md +48 -0
- package/upgrades/side-effects/jobs-as-agentmd-phase-2.md +78 -0
- package/upgrades/side-effects/jobs-as-agentmd-phase-3.md +76 -0
- package/upgrades/side-effects/jobs-as-agentmd-phase-4-endpoints.md +62 -0
- package/upgrades/side-effects/jobs-as-agentmd-phase-5.md +67 -0
- package/upgrades/side-effects/jobs-as-agentmd-phase-6.md +53 -0
- package/upgrades/side-effects/jobs-endpoints-auth-verification.md +52 -0
- package/upgrades/side-effects/migration-guarantee-suite.md +61 -0
- package/upgrades/side-effects/migration-runtime-invariant-gate.md +59 -0
- package/upgrades/side-effects/migration-telemetry-invariant-8.md +68 -0
- package/upgrades/side-effects/npm-pack-templates-smoke.md +51 -0
- package/upgrades/side-effects/openclaw-import-before-prompt-build.md +84 -0
- package/upgrades/side-effects/openclaw-import-pre-compaction-flush.md +85 -0
- package/upgrades/side-effects/prompt-gate-no-prompt-cache.md +258 -0
- package/upgrades/side-effects/tier2-degradation-reporter-live-wire.md +146 -0
- package/upgrades/side-effects/w1-node-abi-mismatch-runbook.md +154 -0
- package/upgrades/side-effects/w2-supervisor-preflight-runbook.md +176 -0
- package/upgrades/side-effects/w3-messaging-delivery-failed-runbook.md +185 -0
- package/upgrades/side-effects/w4-db-corruption-runbook.md +114 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# Side-Effects Review — PromptGate NO_PROMPT cache (token-burn fix)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `prompt-gate-no-prompt-cache`
|
|
4
|
+
**Date:** `2026-05-15`
|
|
5
|
+
**Author:** Echo (instar developer agent)
|
|
6
|
+
**Second-pass reviewer:** required (touches a monitoring sentinel decision point)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
`src/monitoring/PromptGate.ts` — adds a bounded per-session NO_PROMPT
|
|
11
|
+
classification cache to `InputDetector.llmDetect()`. Once the LLM has
|
|
12
|
+
classified a specific terminal-output snapshot for a session as "not a
|
|
13
|
+
blocking prompt," the same snapshot will short-circuit the LLM call instead
|
|
14
|
+
of re-asking on every monitor tick. Cache is keyed on a SHA-256 fingerprint
|
|
15
|
+
of the 20-line context the LLM actually sees, capped at 32 entries per session
|
|
16
|
+
(FIFO eviction), cleared in `onInputSent()` and `cleanup()`.
|
|
17
|
+
|
|
18
|
+
Files touched:
|
|
19
|
+
- `src/monitoring/PromptGate.ts` — adds `noPromptCache` field, cache lookup
|
|
20
|
+
before LLM call, cache write on NO_PROMPT, clear in `onInputSent`/`cleanup`,
|
|
21
|
+
helper `recordNoPrompt()`.
|
|
22
|
+
- `tests/unit/PromptGate.test.ts` — 7 new regression tests covering
|
|
23
|
+
idle-session short-circuit, per-session isolation, distinct-context still
|
|
24
|
+
calls LLM, `onInputSent` clears cache, `cleanup` clears cache, cache cap
|
|
25
|
+
bounded, positive detections are not cached.
|
|
26
|
+
|
|
27
|
+
Decision points the change interacts with: only one — `InputDetector.llmDetect`,
|
|
28
|
+
which uses Haiku to classify terminal output as a blocking prompt or not.
|
|
29
|
+
The change is a memoization layer in front of that authority. The authority
|
|
30
|
+
itself is untouched.
|
|
31
|
+
|
|
32
|
+
## Decision-point inventory
|
|
33
|
+
|
|
34
|
+
- `InputDetector.llmDetect` (LLM-backed authority that classifies terminal
|
|
35
|
+
output as blocking prompt vs. not) — **modify** — adds a memoization step
|
|
36
|
+
before the LLM call. The authority's verdicts are unchanged; we just stop
|
|
37
|
+
re-asking it the same question with the same input.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 1. Over-block
|
|
42
|
+
|
|
43
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
44
|
+
|
|
45
|
+
The change cannot over-block prompt detection in the conventional sense
|
|
46
|
+
(rejecting a legitimate user input). But it could *over-suppress* LLM
|
|
47
|
+
re-classification, leading to a missed real prompt. Concrete failure mode:
|
|
48
|
+
|
|
49
|
+
- A session's terminal output is classified NO_PROMPT for snapshot X.
|
|
50
|
+
- Later, the agent prints a real blocking prompt, producing snapshot Y.
|
|
51
|
+
- Snapshot Y is a *different* fingerprint (X ≠ Y) → cache misses → LLM is
|
|
52
|
+
called normally → real prompt detected.
|
|
53
|
+
|
|
54
|
+
So the cache only suppresses LLM calls when the *exact same 20-line context*
|
|
55
|
+
recurs. A real prompt would necessarily produce different output. No false
|
|
56
|
+
suppression of real prompts is introduced.
|
|
57
|
+
|
|
58
|
+
The one edge case worth naming: if the LLM made a wrong classification (false
|
|
59
|
+
NO_PROMPT verdict on an actual prompt) and that *exact* output recurs, the
|
|
60
|
+
cache amplifies the LLM error by preventing re-classification. Mitigation:
|
|
61
|
+
the cache is bounded (32 entries) and cleared on `onInputSent()`, so the
|
|
62
|
+
amplification window is short. Tradeoff is acceptable: a wrong LLM verdict
|
|
63
|
+
gets re-asked at most 32 outputs later anyway, and the alternative was
|
|
64
|
+
burning 3B tokens/day to repeatedly get the same wrong answer.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 2. Under-block
|
|
69
|
+
|
|
70
|
+
**What failure modes does this still miss?**
|
|
71
|
+
|
|
72
|
+
- **Subtle output noise that defeats fingerprinting.** A session showing a
|
|
73
|
+
spinner, timer, or animated counter will produce slightly different
|
|
74
|
+
fingerprints each tick. Each variant gets one LLM call before being cached.
|
|
75
|
+
Existing pre-filters (`/Scampering|Thinking|Reading \d+ file/i`) already
|
|
76
|
+
catch the most common animated-output cases. Residual noise from
|
|
77
|
+
not-yet-cataloged spinners would still call the LLM once per variant.
|
|
78
|
+
- **Cache size cap (32).** A truly flapping session with > 32 distinct
|
|
79
|
+
recurring outputs would evict and re-classify. This is a deliberate
|
|
80
|
+
bounded-memory tradeoff; the cost is small (~32 SHA-256 calls per
|
|
81
|
+
flapping session per cycle) compared to the alternative of unbounded
|
|
82
|
+
growth. The existing 5-second outer cooldown still gates burst rates.
|
|
83
|
+
|
|
84
|
+
Neither of these is a regression — both were always true. The fix is purely
|
|
85
|
+
additive: same behavior on cache misses, suppressed re-classification on
|
|
86
|
+
cache hits.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 3. Level-of-abstraction fit
|
|
91
|
+
|
|
92
|
+
**Is this at the right layer?**
|
|
93
|
+
|
|
94
|
+
Yes. This is a pure memoization at the same layer as the existing
|
|
95
|
+
`llmRelayTimestamps` rate limiter. Both are private to `InputDetector` and
|
|
96
|
+
gate the same LLM call. The cache is a *signal-level* optimization: it
|
|
97
|
+
records which contexts have already been ruled out, so the authority
|
|
98
|
+
(LLM) is not re-consulted unnecessarily. The authority (Haiku via
|
|
99
|
+
`IntelligenceProvider.evaluate`) keeps full responsibility for the actual
|
|
100
|
+
classification — the cache only suppresses redundant calls.
|
|
101
|
+
|
|
102
|
+
No higher-level gate exists that this should feed instead. No lower-level
|
|
103
|
+
primitive exists that already does this. The change sits in the natural home.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 4. Signal vs authority compliance
|
|
108
|
+
|
|
109
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
110
|
+
|
|
111
|
+
**Does this change hold blocking authority with brittle logic?**
|
|
112
|
+
|
|
113
|
+
- [x] No — this change has no block/allow surface.
|
|
114
|
+
- [ ] No — this change produces a signal consumed by an existing smart gate.
|
|
115
|
+
- [ ] Yes — but the logic is a smart gate with full conversational context.
|
|
116
|
+
- [ ] ⚠️ Yes, with brittle logic.
|
|
117
|
+
|
|
118
|
+
The cache does not make any classification decisions. It memoizes verdicts
|
|
119
|
+
already produced by the LLM authority (`IntelligenceProvider.evaluate`). On
|
|
120
|
+
a cache hit, the *previous LLM verdict* is what determines behavior —
|
|
121
|
+
specifically, "the LLM said NO_PROMPT for this exact context, so we honor
|
|
122
|
+
that verdict without re-asking." That is identical to what the LLM would
|
|
123
|
+
return if re-asked on identical input (Haiku at temperature 0 with the same
|
|
124
|
+
prompt is deterministic to within margin). No brittle logic gains blocking
|
|
125
|
+
authority; the authority remains the LLM, the cache just records its prior
|
|
126
|
+
output to avoid redundant calls.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 5. Interactions
|
|
131
|
+
|
|
132
|
+
**Does this interact with existing checks, recovery paths, or infrastructure?**
|
|
133
|
+
|
|
134
|
+
- **Shadowing**: the cache check sits *after* the existing pre-filters (regex
|
|
135
|
+
rejects for status bar / "Thinking" / etc.) and *after* the existing
|
|
136
|
+
5-minute `llmRelayTimestamps` rate limiter. It runs *before* the LLM call.
|
|
137
|
+
If the existing rate limiter triggers, the cache is never consulted —
|
|
138
|
+
correct behavior, no shadowing.
|
|
139
|
+
- **Double-fire**: the cache write happens once per LLM call (`recordNoPrompt`
|
|
140
|
+
is only invoked from the NO_PROMPT response path inside `llmDetect`, and
|
|
141
|
+
`pendingLlmDetection` prevents overlapping calls per session). No
|
|
142
|
+
double-fire surface.
|
|
143
|
+
- **Races**: cache reads and writes happen on the same async path
|
|
144
|
+
(`llmDetect`). The `pendingLlmDetection` set already prevents concurrent
|
|
145
|
+
`llmDetect` calls per session. `onInputSent()`, `cleanup()`, and the LLM
|
|
146
|
+
response handler all touch `noPromptCache` from the same event loop. No
|
|
147
|
+
cross-thread / cross-process state — Node single-threaded, no race.
|
|
148
|
+
- **Feedback loops**: the cache feeds back into its own check (cache hit →
|
|
149
|
+
skip LLM → no new cache entry). This is the desired loop — it converges,
|
|
150
|
+
not diverges. Counter-direction (real prompt appears → different
|
|
151
|
+
fingerprint → cache miss → LLM called → emit) is also stable.
|
|
152
|
+
- **Adjacent cleanups**: `cleanup(sessionName)` already drops
|
|
153
|
+
`lastOutput`, `stableCount`, `emittedPrompts`, `lastEmissionTime`,
|
|
154
|
+
`llmRelayTimestamps`, `pendingLlmDetection`. We add `noPromptCache.delete`
|
|
155
|
+
to the same list. `onInputSent` similarly clears the cache so post-input
|
|
156
|
+
output gets re-classified.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## 6. External surfaces
|
|
161
|
+
|
|
162
|
+
**Does this change anything visible outside the immediate code path?**
|
|
163
|
+
|
|
164
|
+
- **Other agents on the same machine** — yes, indirectly: every instar agent
|
|
165
|
+
embeds the same `InputDetector`, so every agent's LLM burn rate for prompt
|
|
166
|
+
detection drops once the new code is installed. Behavior visible only as
|
|
167
|
+
reduced Haiku usage on the API side; no functional change in detection
|
|
168
|
+
behavior.
|
|
169
|
+
- **Other users of the install base** — same: all agents inherit the fix
|
|
170
|
+
on upgrade. Strictly cost-positive.
|
|
171
|
+
- **External systems** — Anthropic API usage drops materially. No format
|
|
172
|
+
change to any externally-visible message or event.
|
|
173
|
+
- **Persistent state** — none. Cache is purely in-memory, lost on process
|
|
174
|
+
restart. No DB schema change, no file written, no migration.
|
|
175
|
+
- **Timing / runtime conditions we don't fully control** — the cache assumes
|
|
176
|
+
Haiku's NO_PROMPT verdict on a specific input is stable enough to memoize.
|
|
177
|
+
At `temperature: 0` this is true to within stochastic noise; even if Haiku
|
|
178
|
+
flickered between NO_PROMPT and a positive response on the same input, the
|
|
179
|
+
cache would just lock in whichever came first for at most 32 outputs and
|
|
180
|
+
then evict. Acceptable.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 7. Rollback cost
|
|
185
|
+
|
|
186
|
+
**If this turns out wrong in production, what's the back-out?**
|
|
187
|
+
|
|
188
|
+
- **Hot-fix release**: revert two files (`src/monitoring/PromptGate.ts`,
|
|
189
|
+
`tests/unit/PromptGate.test.ts`), ship a patch release. Estimated 10
|
|
190
|
+
minutes once issue is detected.
|
|
191
|
+
- **Data migration**: none. No persistent state introduced.
|
|
192
|
+
- **Agent state repair**: none. Cache is per-process, in-memory; restarting
|
|
193
|
+
the agent server drops it instantly.
|
|
194
|
+
- **User visibility**: zero. The only user-visible behavior change is
|
|
195
|
+
reduced Telegram-relay duplicate-prompt spam, and the existing detection
|
|
196
|
+
behavior is preserved for first-time-seen outputs.
|
|
197
|
+
|
|
198
|
+
Lowest possible rollback cost: pure code change, no migrations, no state.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Conclusion
|
|
203
|
+
|
|
204
|
+
The change is a bounded in-memory memoization layer in front of an LLM-backed
|
|
205
|
+
authority. It introduces no new decision logic, no new blocking surface, no
|
|
206
|
+
persistent state, and no external API changes. Worst-case failure mode is
|
|
207
|
+
under-detection of a single class of recurring prompts (same exact 20-line
|
|
208
|
+
output as a prior NO_PROMPT verdict), which is mitigated by `onInputSent`
|
|
209
|
+
clearing the cache. The fix targets a specific, measured token-burn pattern
|
|
210
|
+
(108k LLM calls / 3B tokens per 24h on this machine, 2026-05-15) caused by
|
|
211
|
+
the existing 5-minute rate limit being incorrectly gated on emit. With the
|
|
212
|
+
cache, idle sessions burn one LLM call per distinct output instead of one
|
|
213
|
+
per monitor tick. Clear to ship pending second-pass concurrence.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Second-pass review
|
|
218
|
+
|
|
219
|
+
**Reviewer:** general-purpose subagent (Echo's review subagent)
|
|
220
|
+
**Independent read of the artifact: concur**
|
|
221
|
+
|
|
222
|
+
The cache is a textbook memoization layer in front of an LLM authority. On hit it short-circuits with the same behavior the LLM would have produced (`return` from NO_PROMPT branch); on miss it falls through to the unchanged authority. No new blocking surface, no decision logic, no persistent state. Signal-vs-authority compliance is clean. Worst-case under-suppression is bounded by a recurring-identical-context window of at most 32 outputs, after which FIFO eviction forces re-classification. Token spend in the degenerate >32-distinct-cyclic-context case is no worse than baseline.
|
|
223
|
+
|
|
224
|
+
Minor notes (do not block ship, but worth tracking):
|
|
225
|
+
|
|
226
|
+
- **In-flight `llmDetect` vs `onInputSent` race.** `onInputSent()` clears `noPromptCache` synchronously, but if an `llmDetect` is mid-flight for the same session (`pendingLlmDetection` set), its awaited `recordNoPrompt(...)` will land *after* the clear and repopulate the cache with a verdict computed against pre-input output. The stale entry only fires on a future byte-identical context (unlikely post-input) and would be evicted within 32 outputs or on the next `onInputSent`. Consider: capture a generation counter in `llmDetect` at start and skip `recordNoPrompt` if it doesn't match the current generation. Not urgent — failure mode is "one redundant cache entry," not "missed prompt."
|
|
227
|
+
|
|
228
|
+
- **`startsWith('NO')` permissiveness amplified by cache.** Line 416 treats any LLM response starting with `NO` (e.g. "NOT SURE", "NOW WAITING") as NO_PROMPT and now caches it. Pre-cache this was a per-call leniency; post-cache it locks in for up to 32 cycles. Consider tightening to exact-match `NO_PROMPT` or `^NO_PROMPT\b`. Tracked as a hardening followup, not a blocker.
|
|
229
|
+
|
|
230
|
+
- **Cleanup vs in-flight `llmDetect` (same race as above for terminated sessions).** `cleanup()` clears `pendingLlmDetection` and `noPromptCache`, but a mid-flight `llmDetect` whose `.finally()` has not yet run will still call `recordNoPrompt(sessionName, ...)`, creating a cache entry for a dead session. Pure memory leak (one entry per dead session per race window) until process restart. Bounded and minor.
|
|
231
|
+
|
|
232
|
+
- **Test coverage gaps.** No regression test exists for: (a) the in-flight-vs-cleanup race, (b) the `startsWith('NO')` permissiveness path (e.g. asserting that `"NOT SURE"` is cached and impacts subsequent calls), (c) lack of interaction between `onPromptRejected` and `noPromptCache` (currently fine because rejection only applies to positive emits, but worth a defensive test if those branches ever cross). Add when the followups land.
|
|
233
|
+
|
|
234
|
+
None of these break the ship criteria. The change is correct under the assumptions it states, and the failure modes I found degrade to at most "one extra LLM call" or "one stranded cache entry," never to a missed prompt. Clear to commit.
|
|
235
|
+
|
|
236
|
+
**Author response to reviewer notes (addressed before commit):**
|
|
237
|
+
|
|
238
|
+
- **In-flight race + `startsWith('NO')` permissiveness** — addressed in the
|
|
239
|
+
same commit. Added `cacheGeneration: Map<sessionName, number>` bumped by
|
|
240
|
+
`onInputSent()` and `cleanup()`. `llmDetect` captures the generation at
|
|
241
|
+
call start; `recordNoPrompt()` now drops the write if the generation has
|
|
242
|
+
advanced (covers both the input-during-flight and cleanup-during-flight
|
|
243
|
+
cases). Also tightened cache write to require strict `trimmed === 'NO_PROMPT'`;
|
|
244
|
+
the permissive `startsWith('NO')` branch still returns from `llmDetect` but
|
|
245
|
+
no longer memoizes, so a transient "NOT SURE" reply doesn't lock in.
|
|
246
|
+
- **Test coverage** — two new tests added: one for the in-flight clear race
|
|
247
|
+
(asserts no cache entry after `onInputSent` mid-flight), one for permissive
|
|
248
|
+
non-strict-NO responses (asserts they are not cached). Total NO_PROMPT-cache
|
|
249
|
+
regression tests now 9; full PromptGate suite 42/42 passing.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Evidence pointers
|
|
254
|
+
|
|
255
|
+
- Original measurement (108,782 calls / 3.03B tokens / 24h):
|
|
256
|
+
`/private/tmp/claude-501/-Users-justin--instar-agents-echo/fb63ccb9-b8ab-4b63-8f49-55d3d6427255/tasks/bwgzdb1bf.output`
|
|
257
|
+
- Unit-test verification: `npx vitest run tests/unit/PromptGate.test.ts`
|
|
258
|
+
→ 40/40 passed, 7 new NO_PROMPT-cache regression tests included.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Side-Effects Review — Tier-2 DegradationReporter live-mode wire-up
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `tier2-degradation-reporter-live-wire`
|
|
4
|
+
**Date:** `2026-05-13`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required (gated behind opt-in config flag default-false; legacy alert path + in-line healers remain the safety net)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Adds `src/remediation/RemediatorBootstrap.ts` — a single async entry point `bootstrapRemediator({ stateDir, machineId, ... })` that constructs the full Tier-2 dispatch graph: F-1 `RemediationKeyVault` (4-backend probe), F-4 `MachineLock` + `IntentJournal` + `AuditWriter`, F-5 `TrustElevationSource` (with Telegram + CLI approval channels), and the F-8 `Remediator` orchestrator. Registers the runbooks that exist on main today (W-1 `nodeAbiMismatchRunbook`, W-3 `messagingDeliveryFailedRunbook`); logs and skips W-2 supervisor-preflight and W-4 db-corruption wrappers that haven't merged yet. Returns `{disabled: true, reason: 'no-secret-backend'}` if the vault probe finds no backend.
|
|
11
|
+
|
|
12
|
+
Modifies `src/commands/server.ts` to call `bootstrapRemediator()` IFF `config.remediator?.enabled === true` (defaults FALSE). On success, wires the Remediator into the existing `DegradationReporter` singleton via `setRemediator()`. On failure or disabled, logs a clear line and continues with the legacy alert path.
|
|
13
|
+
|
|
14
|
+
Files touched:
|
|
15
|
+
- `src/remediation/RemediatorBootstrap.ts` (new, 290 LOC)
|
|
16
|
+
- `src/commands/server.ts` (boot-path wire-in, ~50 LOC inserted after `connectDownstream`)
|
|
17
|
+
- `src/monitoring/DegradationReporter.ts` (1-line type widening: `RemediatorLike.dispatch` returns `Promise<unknown>` instead of `Promise<void>` so the real Remediator's `Promise<DispatchOutcome>` typechecks)
|
|
18
|
+
- `tests/unit/RemediatorBootstrap.test.ts` (new, 12 tests)
|
|
19
|
+
- `tests/integration/remediator-live-mode.test.ts` (new, 4 tests)
|
|
20
|
+
- `upgrades/NEXT.md` (release note entry)
|
|
21
|
+
|
|
22
|
+
Decision points the change interacts with:
|
|
23
|
+
- `DegradationReporter.setRemediator()` — the F-3 hook designed for exactly this wire-in.
|
|
24
|
+
- `Remediator.dispatch()` — authority for orchestrating remediation attempts. Not modified.
|
|
25
|
+
- `Remediator.registerRunbook()` — registry-load-time validator (§A6 / §A36). Not modified; surfaced via bootstrap.
|
|
26
|
+
|
|
27
|
+
## Decision-point inventory
|
|
28
|
+
|
|
29
|
+
- `config.remediator.enabled` — **add** — structural opt-in flag (boolean) controlling whether the bootstrap runs at all. Operator authority, not a runtime judgment.
|
|
30
|
+
- `DegradationReporter.setRemediator()` registration — **modify** — the F-3 hook was previously unused; this PR is its first real consumer. The setter itself is unchanged.
|
|
31
|
+
- `Remediator` lifecycle (construction + supervisor registration) — **pass-through** — bootstrap composes existing primitives without altering their decision surfaces.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 1. Over-block
|
|
36
|
+
|
|
37
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
38
|
+
|
|
39
|
+
No block/allow surface — over-block not applicable. The bootstrap is dependency injection. The only branch decisions it makes are:
|
|
40
|
+
|
|
41
|
+
1. "Does the vault probe find a backend?" — if no, return `{disabled, reason: 'no-secret-backend'}`. This is the spec's prescribed fail-soft per §A62 (operating-state matrix → "OS Keychain Unavailable → try fallback"). Default behavior unchanged for the agent.
|
|
42
|
+
2. "Did the operator opt in via `remediator.enabled`?" — if no, skip bootstrap entirely. This is the staged-rollout default per §A57.
|
|
43
|
+
|
|
44
|
+
Neither branch rejects a legitimate degradation event. The legacy alert path remains the catch-all when the Remediator isn't wired.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 2. Under-block
|
|
49
|
+
|
|
50
|
+
**What failure modes does this still miss?**
|
|
51
|
+
|
|
52
|
+
No block/allow surface — under-block not applicable in the sense of the principle. However, two structural under-block scenarios are intentional carve-outs:
|
|
53
|
+
|
|
54
|
+
- **Operator-enabled live mode but vault throws on construction (not `no-backend-available`)**: the bootstrap re-throws, the server boot path's outer try/catch catches it and logs a red error line; legacy alert path stays active. The agent does NOT crash. This is intentional — silently downgrading the operator's explicit opt-in to legacy mode would mask a real configuration error.
|
|
55
|
+
- **Runbook registry validation fails (W-1 ever ships a misshapen prefilter)**: bootstrap rethrows. The server boot path catches and logs. Legacy path remains active. The operator sees the failure on every restart until the wrapper PR is fixed.
|
|
56
|
+
|
|
57
|
+
The legacy alert path + in-line healers (`NativeModuleHealer.openWithHeal`, `ServerSupervisor.preflightSelfHeal`) catch every degradation flow regardless of Remediator state.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 3. Level-of-abstraction fit
|
|
62
|
+
|
|
63
|
+
**Is this at the right layer?**
|
|
64
|
+
|
|
65
|
+
The bootstrap is dependency injection — its level is "the seam between server boot and remediation tree." It does NOT belong inside the Remediator (whose job is dispatch decisions), inside `DegradationReporter` (whose job is event normalization + alert routing), or inside any single F-* module (each owns one primitive). It lives at `src/remediation/RemediatorBootstrap.ts` precisely because it composes across all those modules and the lifeline. The location matches how the spec describes the wire-in (§A33 + §A57).
|
|
66
|
+
|
|
67
|
+
The optional-runbook stubs (W-2/W-3/W-4) live inline as `tryLoadOptionalRunbook()` for now — when each wrapper PR lands, the stub gains a real import; the surrounding loop is identical. This is a transition-period pattern; once Tier-2 runbooks are all merged, the stubs collapse to direct imports.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 4. Signal vs authority compliance
|
|
72
|
+
|
|
73
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
74
|
+
|
|
75
|
+
**Does this change hold blocking authority with brittle logic?**
|
|
76
|
+
|
|
77
|
+
- [x] No — this change has no block/allow surface.
|
|
78
|
+
|
|
79
|
+
The bootstrap injects dependencies into existing authorities (Remediator dispatches; TrustElevationSource decides transitions; AuditWriter verifies tokens). It does not add a new decision point.
|
|
80
|
+
|
|
81
|
+
The `remediator.enabled` flag is operator authority via configuration — explicit human intent, expressed in `config.json`, not a runtime detector judging events. It is structurally identical to "should this server enable feature X" toggles already in `ProjectConfig` (e.g., `externalOperations`, `responseReview`, `inputGuard`).
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 5. Interactions
|
|
86
|
+
|
|
87
|
+
**Does this interact with existing checks, recovery paths, or infrastructure?**
|
|
88
|
+
|
|
89
|
+
- **Shadowing:** The `RemediatorLike.dispatch` widening in `DegradationReporter.ts` (`Promise<void>` → `Promise<unknown>`) is the only modification to existing decision surface. Behavior is identical — the reporter never inspected the dispatch result; the audit log is the canonical record. Widening the type surface does not change runtime semantics. Confirmed via re-run of all 24 `degradation-reporter.test.ts` cases.
|
|
90
|
+
- **Double-fire:** When `remediator.enabled: true` AND a structured event arrives, the Remediator dispatches AND the legacy `reportEvent()` (feedback + Telegram alert) is skipped per existing F-3 contract — see DegradationReporter.report() L317-326. So no double-alert. The in-line healers in surfaces (NativeModuleHealer.openWithHeal etc.) are separate code paths that operate independently of the reporter and are unaffected.
|
|
91
|
+
- **Races:** The bootstrap runs during server start, AFTER `degradationReporter.connectDownstream()`. By the time the first `report()` could fire from any subsystem, the Remediator is either wired or the flag is off. The only race is during the `await bootstrapRemediator()` window — if a feature starts and reports a degradation in those ~hundreds of milliseconds, the event flows through the legacy path. This is acceptable: the legacy path is the safety net.
|
|
92
|
+
- **Feedback loops:** None. The Remediator's audit log is write-only from the reporter's POV; nothing routes audit entries back into `DegradationReporter.report()`.
|
|
93
|
+
- **Lifeline coexistence:** `ServerSupervisor` lives in the lifeline process (TelegramLifeline.ts), not the server. The bootstrap accepts `serverSupervisor` as optional and leaves it unwired in the server-side bootstrap. F-6's handshake remains intact: the supervisor's `registerRemediator` handshake fires only when a Remediator is constructed inside the same process. A future PR can hoist Remediator construction into the lifeline if cross-process supervision is needed.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 6. External surfaces
|
|
98
|
+
|
|
99
|
+
**Does this change anything visible outside the immediate code path?**
|
|
100
|
+
|
|
101
|
+
- **Other agents on the same machine?** No.
|
|
102
|
+
- **Other users of the install base?** No behavioral change unless `remediator.enabled: true` is explicitly set in `config.json`. Default OFF = identical behavior to today.
|
|
103
|
+
- **External systems (Telegram, Slack, GitHub, Cloudflare, etc.)?** The Remediator's `requestPlannedRestart` would talk to a supervisor, but no `requestPlannedRestart` call is made by W-1 today (it only rebuilds better-sqlite3 in-process). No external API change.
|
|
104
|
+
- **Persistent state (databases, ledgers, memory files)?** When opted in, the bootstrap writes:
|
|
105
|
+
- `<stateDir>/machine-locks/in-flight/*` (existing F-4 directory; no schema change)
|
|
106
|
+
- `<stateDir>/remediation/intent-journal-<machineId>.jsonl` (existing F-4 file)
|
|
107
|
+
- `<stateDir>/remediation/audit-projection-<machineId>.jsonl` (existing F-4 file)
|
|
108
|
+
- `<stateDir>/remediation-keys.age` or keychain entries (existing F-1 store)
|
|
109
|
+
- All paths are in the F-7 gitignore allow-list (`REMEDIATION_GITIGNORE_ENTRIES`), so multi-machine sync won't carry them.
|
|
110
|
+
- **Timing or runtime conditions we don't fully control?** The vault probe may hit OS keychain APIs (timing varies by host). Bootstrap is async and runs once at server start; failures don't crash the server.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 7. Rollback cost
|
|
115
|
+
|
|
116
|
+
**If this turns out wrong in production, what's the back-out?**
|
|
117
|
+
|
|
118
|
+
- **Hot-fix release:** Revert the three-file change (Bootstrap module, server.ts insert, DegradationReporter type widening). Ship as next patch.
|
|
119
|
+
- **Data migration:** None. Even if Remediator-written state exists on disk (audit-projection JSONL, etc.), those files are inert — nothing else in the agent reads them, and they're in the F-7 gitignore allow-list. Operator can `rm -rf .instar/remediation/` post-revert if desired.
|
|
120
|
+
- **Agent state repair:** None. The `remediator.enabled` config field, if set, becomes a no-op after revert (unused config fields are ignored).
|
|
121
|
+
- **User visibility:** Zero during rollback window. Default OFF means almost no installed agent is exposed to the bootstrap path today. The handful of operators who opted in see the legacy alert path return (which is what the rest of the install base sees).
|
|
122
|
+
|
|
123
|
+
Rollback cost: very low.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Conclusion
|
|
128
|
+
|
|
129
|
+
This is the canonical Tier-2 live-mode flip PR per §A57. The structural opt-in (`remediator.enabled` default-false) means no agent's behavior changes on day one. Operators who explicitly opt in get the full F-1..F-8 dispatch graph wired into the existing F-3 reporter pipeline. The legacy alert path and in-line healers remain the safety net regardless of Remediator state.
|
|
130
|
+
|
|
131
|
+
No new decision authority is added; no brittle blockers are introduced; the change is dependency injection at the seam between server boot and the remediation tree. Rollback is a three-file revert with no migration cost.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Second-pass review
|
|
136
|
+
|
|
137
|
+
Not required. The change introduces zero new block/allow decision logic — every check it touches is in pre-existing modules (Remediator, TrustElevationSource, AuditWriter) whose authority surfaces have already shipped through their own side-effects reviews. The only structural change is operator-authority opt-in flag default-false; that is configuration, not runtime judgment.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Evidence pointers
|
|
142
|
+
|
|
143
|
+
- Unit tests: `tests/unit/RemediatorBootstrap.test.ts` (12/12 passing).
|
|
144
|
+
- Integration test: `tests/integration/remediator-live-mode.test.ts` (4/4 passing).
|
|
145
|
+
- Regression: re-ran 97 tests across `Remediator`, `Remediator-enforcement`, `degradation-reporter`, `RemediationKeyVault`, `TrustElevationSource`, `MachineLock`, `AuditWriter`, `IntentJournal` — all green.
|
|
146
|
+
- TypeScript: `tsc --noEmit` clean across the worktree.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Side-Effects Review — W-1: node-abi-mismatch runbook + NativeModuleHealer.invokeFromRemediator
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `w1-node-abi-mismatch-runbook`
|
|
4
|
+
**Date:** `2026-05-13`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Ships the FINAL Tier-1 PR of the Self-Healing Remediator v2 build (per `docs/specs/SELF-HEALING-REMEDIATOR-V2-SPEC.md` §A6, §A9, §A28, §A36, §A45, §A55, §A57). Adds the first `ApprovedRunbook` the F-8 Remediator can dispatch — `node-abi-mismatch` — and the matching `NativeModuleHealer.invokeFromRemediator(ctx)` surface entry-point.
|
|
11
|
+
|
|
12
|
+
Two new modules:
|
|
13
|
+
|
|
14
|
+
- `src/remediation/runbooks/node-abi-mismatch.ts` — the runbook. Matches structured-provenance NATIVE_MODULE_ABI_MISMATCH events (per §A6: `provenance ∈ {native-binding, subsystem-explicit}`, NOT `free-text`). surfaceCallable delegates to `NativeModuleHealer.invokeFromRemediator`. verify() opens a sqlite handle and runs `PRAGMA integrity_check`; returns the §A21 verified-healthy / verify-failed / verify-inconclusive taxonomy. Marked `essential: true` with `blastRadius: 'machine'` (§A36 validator accepts this combination).
|
|
15
|
+
- `src/memory/NativeModuleHealer.ts` (extended) — adds `invokeFromRemediator(ctx)` as a parallel entry point. The existing `openWithHeal` in-line path remains the CLI-direct safety net, unchanged. The new method:
|
|
16
|
+
- Honours `ctx.abortSignal` (returns failure if already aborted; passes the signal to spawnSync).
|
|
17
|
+
- Respects `ctx.monotonicDeadline` to compute remaining ns budget for npm rebuild.
|
|
18
|
+
- Invokes `npm rebuild --ignore-scripts --build-from-source better-sqlite3 --prefix <installPrefix>` — never bare `npm rebuild` (§A28); always `--build-from-source` (§A45).
|
|
19
|
+
- Performs a best-effort `package-lock.json` integrity read pre-rebuild and records it in `details.packageLockIntegrity` (§A55 first step; the signed `dist/native-prebuilds.lock.json` from full A55 lands later).
|
|
20
|
+
- Computes sha256 of the rebuilt `.node` binary post-rebuild and emits `details.rebuiltBinarySha256` (§A28 binary-divergence detection).
|
|
21
|
+
|
|
22
|
+
Tier-1 IS COMPLETE after this PR: F-1, F-2, F-3, F-4, F-8 subset, and W-1 are all on main. The Remediator is dispatchable end-to-end via test fixtures. NO production wiring yet — `DegradationReporter.setRemediator()` is still uncalled, which is Tier-2 work.
|
|
23
|
+
|
|
24
|
+
Files touched:
|
|
25
|
+
- `src/remediation/runbooks/node-abi-mismatch.ts` (new)
|
|
26
|
+
- `src/memory/NativeModuleHealer.ts` (extended — `invokeFromRemediator` + helpers)
|
|
27
|
+
- `tests/unit/runbooks/node-abi-mismatch.test.ts` (new — 12 cases)
|
|
28
|
+
- `tests/unit/NativeModuleHealer-invokeFromRemediator.test.ts` (new — 12 cases)
|
|
29
|
+
- `upgrades/NEXT.md` (preserves F-1..F-4 + F-8 + Phase 4/5 + ELI16 + API-safety entries; W-1 added in three sections)
|
|
30
|
+
|
|
31
|
+
## Decision-point inventory
|
|
32
|
+
|
|
33
|
+
- `nodeAbiMismatchRunbook.match(event)` — **add** — narrow filter: returns true only when the event is about better-sqlite3 (subsystem='better-sqlite3' OR subsystem='memory' + reason text mentions better-sqlite3). Pure structural code; no LLM, no regex over arbitrary fields.
|
|
34
|
+
- `nodeAbiMismatchRunbook.preconditions(event)` — **add** — `require.resolve('better-sqlite3')` succeeds. Refuses to fire when the package isn't installed at all.
|
|
35
|
+
- `nodeAbiMismatchRunbook.verify(ctx)` — **add** — opens an in-memory sqlite handle and runs `PRAGMA integrity_check`. Returns one of three §A21 outcomes; in particular probe error → verify-inconclusive (never verify-failed).
|
|
36
|
+
- `NativeModuleHealer.invokeFromRemediator(ctx)` — **add** — Remediator-orchestrated parallel entry point. Once-per-process guard preserved from `healBetterSqlite3`.
|
|
37
|
+
|
|
38
|
+
The legacy `openWithHeal` entry point is **pass-through** (unchanged). The once-per-process guard `healAttempted` is shared between the two paths so a rebuild from either route counts against the other — they cannot both spawn a rebuild within one process lifetime, which is the desired §A2 invariant.
|
|
39
|
+
|
|
40
|
+
No new HTTP routes. No new persistent files beyond what F-4 already creates. The runbook is constructible but is NOT registered into a live Remediator from production code in this PR — registration into a live dispatcher happens in Tier-2 alongside the `DegradationReporter.setRemediator()` wiring.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 1. Over-block
|
|
45
|
+
|
|
46
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
47
|
+
|
|
48
|
+
- **Free-text-provenance NODE_MODULE_VERSION events** are NOT matched by this runbook. This is by design (§A6). The legacy `.report(...)` callers that normalize to `provenance: 'free-text'` (per F-3 shim) cannot trigger the rebuild — they route to `no-matching-runbook` and feed NovelFailureReviewer's clustering pipeline. Operators who hit ABI mismatch via the in-line path STILL get healed because `openWithHeal` remains untouched. The structural defense costs no functionality.
|
|
49
|
+
- **Events about native modules other than better-sqlite3** are rejected by `match()`. Intended — the runbook's surface (`NativeModuleHealer`) only knows how to rebuild better-sqlite3. A different native module mismatch routes to `no-matching-runbook` correctly; SystemReviewer can cluster and propose a new runbook.
|
|
50
|
+
- **Events with `subsystem !== 'better-sqlite3'` AND no `better-sqlite3` mention in `reason`** are rejected. The `match()` callback falls back to inspecting `reason.full ?? reason.redacted`; if neither mentions better-sqlite3 we conservatively decline. False-negative risk exists for legacy emit sites with surprising subsystem names, but the in-line `openWithHeal` path still catches them.
|
|
51
|
+
|
|
52
|
+
No legitimate input is rejected that should have been accepted.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 2. Under-block
|
|
57
|
+
|
|
58
|
+
**What failure modes does this still miss?**
|
|
59
|
+
|
|
60
|
+
- **No cross-process binary-divergence enforcement yet.** The sha256 is recorded in `details.rebuiltBinarySha256` and visible in the audit projection, but no consumer compares this against a pinned-sha256 lockfile. §A55's signed `dist/native-prebuilds.lock.json` is the closing piece; this PR records the data point.
|
|
61
|
+
- **Package-lock integrity check is observational, not enforcing.** §A45 says pre-rebuild integrity check; this PR reads `package-lock.json` and records the result in `details.packageLockIntegrity` but does NOT abort on mismatch. Tightening to fail-closed will be part of the A55 follow-up that adds the signed lockfile — without a signed source of truth, refusing on mismatch would over-block on benign npm-version drift.
|
|
62
|
+
- **No deadline enforcement on `verify()` separately.** The Remediator's §A4 AbortController fires around the whole `surfaceCallable + verify` chain, so a hung verify is caught. But `verify()` doesn't have a per-step budget. The integrity_check pragma on an in-memory db is fast (~microseconds in practice), so this is not a likely DoS surface.
|
|
63
|
+
- **The runbook trusts `match()` callback authorship.** If the runbook author writes `match: () => true`, the prefilter would still firewall by errorCode + provenance, but match() is unscrutinized. In W-1 the match logic is structural code shipped in-repo with the runbook; review-time the diff covers it.
|
|
64
|
+
- **The once-per-process guard `healAttempted` is shared between `openWithHeal` and `invokeFromRemediator`.** If the in-line path attempts and fails, the Remediator path will short-circuit with the failed result rather than re-trying. Intended — npm rebuild is expensive and we don't want two attempts per process. Cross-process retry is what the A7 cross-process ledger gates.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 3. Level-of-abstraction fit
|
|
69
|
+
|
|
70
|
+
**Is this at the right layer?**
|
|
71
|
+
|
|
72
|
+
Yes. Each piece sits where the spec mandates:
|
|
73
|
+
|
|
74
|
+
- **Runbook (`src/remediation/runbooks/node-abi-mismatch.ts`)** is the policy layer — declares the match contract, the verify semantics, the blast-radius, and the surfaceCallable wiring. Pure data plus a thin verify wrapper; no orchestration, no key material, no audit-write concerns.
|
|
75
|
+
- **`NativeModuleHealer.invokeFromRemediator`** is the surface layer — owns the rebuild step plus the §A28/§A45 supply-chain invariants. No knowledge of the Remediator's lock / intent / audit primitives; it accepts a context, does the work, returns an `ExecutionResult`. The dispatcher (F-8) owns everything else.
|
|
76
|
+
- **Verify helper (`verifyBetterSqlite3Ok`)** is the durability probe — owns the §A9 "assert durable, not just live" rule. Cleanly separated from the heal step so verify can run after either entry point.
|
|
77
|
+
|
|
78
|
+
No higher-level smart gate is shadowed. The Remediator's `registerRunbook()` validator (§A6, §A36) is the gate that accepts/rejects this runbook at boot; the runbook itself doesn't contain blocking authority over content beyond the structural match contract.
|
|
79
|
+
|
|
80
|
+
The signal-vs-authority principle (`docs/signal-vs-authority.md`) is honoured: the match is precise structural code (errorCode equality + provenance enum membership + subsystem string equality OR a narrow regex on REASON text limited to the literal `better-sqlite3`), not a heuristic content classifier. The authority to "rebuild a native module" lives at the surface layer (`NativeModuleHealer`), gated by the §A28/§A45 invariants.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 4. Signal vs authority compliance
|
|
85
|
+
|
|
86
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
87
|
+
|
|
88
|
+
**Does this change hold blocking authority with brittle logic?**
|
|
89
|
+
|
|
90
|
+
- [ ] No — this change produces a signal consumed by an existing smart gate.
|
|
91
|
+
- [x] No — this change has no brittle-logic blocking authority. Match is precise structural code over enum-typed fields; verify is a deterministic pragma check; the only block surface (§A36 essential-on-machine) is enforced by the F-8 dispatcher's validator, not by this runbook.
|
|
92
|
+
- [ ] Yes — but the logic is a smart gate with full conversational context.
|
|
93
|
+
- [ ] ⚠️ Yes, with brittle logic — STOP.
|
|
94
|
+
|
|
95
|
+
The runbook's structural decisions:
|
|
96
|
+
- §A6 prefilter (errorCode + provenance enum equality) — precise.
|
|
97
|
+
- §A36 essential+machine — enforced at registry-load by F-8's validator.
|
|
98
|
+
- §A28 supply-chain (`--ignore-scripts --build-from-source <single-package>`) — hard-coded; no input plumbing.
|
|
99
|
+
- §A45 package-lock integrity read — observational only.
|
|
100
|
+
- §A21 verify taxonomy — derived from PRAGMA integrity_check result type-by-type.
|
|
101
|
+
|
|
102
|
+
There is one regex in `match()`: `/better-sqlite3/i` against the reason text. This is a **subsystem-name match** (a literal package name in a structured-provenance event), not a content classifier — there is no LLM call, no token-class matching, no synonym handling. The smart gate (operator approval, /instar-dev review-time discipline) holds policy authority over which runbooks exist; this runbook's job is to be a precise structural detector for one specific failure mode.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 5. Interactions
|
|
107
|
+
|
|
108
|
+
**Does this interact with existing checks, recovery paths, or infrastructure?**
|
|
109
|
+
|
|
110
|
+
- **Shadowing:** The legacy `openWithHeal` path and the new `invokeFromRemediator` path share the `healAttempted` once-per-process flag. If both fire in the same process (e.g., CLI invocation triggers in-line heal, then a Remediator-orchestrated invocation arrives later from a different code path), the second one short-circuits with the previous outcome. **Intended.** §A2 lock-bound co-existence requires exactly this: the MachineLock prevents two simultaneous rebuilds; the once-per-process flag prevents redundant retry. Cross-process retry is the A7 ledger's concern.
|
|
111
|
+
- **Double-fire:** `Remediator.dispatch()` for the same NATIVE_MODULE_ABI_MISMATCH event called twice concurrently → second call observes the first's in-flight MachineLock with the same tupleHash and returns `covered-by-inline`. Audit projection records both attempts (one `started + verified-healthy`, one `covered-by-inline`).
|
|
112
|
+
- **Race with adjacent cleanup:** None new. The dispatcher's `finally` block already releases the lock; the runbook adds no shared mutable state beyond `healAttempted` (which is process-local).
|
|
113
|
+
- **Feedback loops:** The runbook's surfaceCallable returns `outcome: success` based on whether `npm rebuild` exited 0, not on whether the resulting binding actually works. The verify() step is what closes the loop — if rebuild "succeeded" but `PRAGMA integrity_check` doesn't return 'ok', the Remediator records `verify-failed` and the churn counter ticks. After ≥5 verify-fails in 7 days (§A8 essential-runbook threshold) the runbook auto-quarantines. This is the correct shape.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 6. External surfaces
|
|
118
|
+
|
|
119
|
+
**Does this change anything visible outside the immediate code path?**
|
|
120
|
+
|
|
121
|
+
- **Spawned subprocess.** `npm rebuild --ignore-scripts --build-from-source better-sqlite3` is invoked by `invokeFromRemediator`. The new flags vs the legacy path's `npm rebuild better-sqlite3` (no flags): `--ignore-scripts` (skips ALL deps' install scripts, not just better-sqlite3's) and `--build-from-source` (forces compilation rather than prebuild fetch). Net effect:
|
|
122
|
+
- **`--ignore-scripts`:** the legacy in-line path runs the package's install script (which `prebuild-install` uses to fetch a prebuilt .node binary from GitHub). The new flag suppresses this — the build must happen via node-gyp. On systems without a working node-gyp toolchain, this rebuild will fail where the legacy one might have succeeded by fetching a prebuilt. Acceptable: the legacy path remains as the fallback when invokeFromRemediator returns failure, AND §A45 explicitly accepts this trade-off ("build-from-source preferred"). §A55's pinned-prebuild lockfile (future PR) restores the prebuild path with sha256 enforcement.
|
|
123
|
+
- **`--build-from-source`:** parallel concern — `prebuild-install` honours this flag in better-sqlite3's install script (but `--ignore-scripts` already blocks that script). Including both is defense-in-depth in case `--ignore-scripts` doesn't honour every codepath in older npm versions.
|
|
124
|
+
- **No HTTP routes, no Telegram surfaces, no dashboard tabs, no config flags.**
|
|
125
|
+
- **No new file types on disk.** The healer's `native-module-heals.jsonl` log gains entries from `invokeFromRemediator` callers tagged `component='Remediator:node-abi-mismatch'`. Existing log readers see the entries with a new component string; no schema break.
|
|
126
|
+
- **No external API surface changes** — `RemediatorInvocationContext` and `RemediatorExecutionResult` are new public exports, but they are structurally compatible with F-8's `RemediationContext` and `ExecutionResult` types. The Remediator's `surfaceCallable: (ctx: RemediationContext) => Promise<ExecutionResult>` contract is satisfied because RemediationContext is a structural superset of RemediatorInvocationContext (it has all the latter's fields).
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 7. Rollback cost
|
|
131
|
+
|
|
132
|
+
**If this turns out wrong in production, what's the back-out?**
|
|
133
|
+
|
|
134
|
+
Low. The new code paths have ZERO production callers in this PR — `DegradationReporter.setRemediator()` is still uncalled, so the dispatcher never fires, so the runbook's `surfaceCallable` is never invoked. Revert path:
|
|
135
|
+
|
|
136
|
+
1. `git revert <pr-sha>` removes both new source files and the test files.
|
|
137
|
+
2. The `invokeFromRemediator` method goes with the revert; `openWithHeal` remains untouched. Existing CLI consumers see no change.
|
|
138
|
+
3. No state migration. No on-disk format change. No HTTP route change. No new gitignore entries. No config-flip whitelist additions.
|
|
139
|
+
|
|
140
|
+
Once a future Tier-2 PR wires `DegradationReporter.setRemediator(remediator)` AND `remediator.registerRunbook(nodeAbiMismatchRunbook)` together, the rollback shape becomes "revert the wiring PR" — leaving this PR's primitives intact. The Tier-2 wiring is where the live consumer arrives; this PR is foundation.
|
|
141
|
+
|
|
142
|
+
If a bug surfaces in the rebuild flag set (e.g., `--build-from-source` breaks on a deployment target without node-gyp), the fix is to:
|
|
143
|
+
- Either drop `--build-from-source` from the rebuild command (and rely on §A55's signed prebuild lockfile to close the supply-chain gap once that lands), or
|
|
144
|
+
- Add a runtime check for node-gyp availability and fail-closed with `precondition-failed` outcome before spawnSync.
|
|
145
|
+
|
|
146
|
+
Either is a one-line change in `healBetterSqlite3FromRemediator`.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Reviewer concurrence (Phase 5)
|
|
151
|
+
|
|
152
|
+
Not required. This PR has zero live block/allow surface, zero session lifecycle interaction, zero sentinel/gate/watchdog authority. It is foundation infrastructure that becomes load-bearing only when Tier-2 PRs wire the dispatcher into the DegradationReporter pipeline. At that point the wiring PR carries its own side-effects review.
|
|
153
|
+
|
|
154
|
+
The §A6, §A9, §A21, §A28, §A36, §A45, §A55 invariants are all structurally enforced by the existing F-8 validator + the runbook's hardcoded flag set. No reviewer subagent finding could change the shape of the invariants without changing the spec — and the spec is converged and approved.
|