instar 0.28.102 → 0.28.104
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 +109 -1
- package/dist/memory/NativeModuleHealer.d.ts.map +1 -1
- package/dist/memory/NativeModuleHealer.js +418 -2
- 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/TokenLedger.d.ts.map +1 -1
- package/dist/monitoring/TokenLedger.js +11 -1
- package/dist/monitoring/TokenLedger.js.map +1 -1
- 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/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +8 -0
- package/dist/server/AgentServer.js.map +1 -1
- 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 +94 -94
- 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/0.28.104.md +77 -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/token-ledger-native-heal.md +136 -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,85 @@
|
|
|
1
|
+
# Side-Effects Review — Pre-Compaction Memory Flush
|
|
2
|
+
|
|
3
|
+
Spec: `docs/specs/OPENCLAW-IMPORT-PRE-COMPACTION-FLUSH-SPEC.md`
|
|
4
|
+
ELI16: `docs/specs/OPENCLAW-IMPORT-PRE-COMPACTION-FLUSH-SPEC.eli16.md`
|
|
5
|
+
Driving approval: Telegram topic 9003, Justin, 2026-05-13.
|
|
6
|
+
|
|
7
|
+
## What's IN
|
|
8
|
+
|
|
9
|
+
1. New `src/core/PreCompactionFlush.ts` — pure class with `handle(payload)` method, fact parser, file writer, audit logger.
|
|
10
|
+
2. New `tests/unit/PreCompactionFlush.test.ts` — 16 assertions covering all 9 outcomes + parsing variants + slug coercion + audit shape.
|
|
11
|
+
3. Wired into `src/commands/server.ts` directly after the `compactionSentinel.report` PreCompact listener. Conditional on `config.preCompactionFlush.enabled === true`.
|
|
12
|
+
4. Default config off (`enabled: false`). Operators flip to opt in.
|
|
13
|
+
5. Audit log at `<projectDir>/.instar/audit/pre-compaction-flush.jsonl` for every fire.
|
|
14
|
+
6. Per-fact memory files written under `<projectDir>/.instar/memory/learning_precompact_*.md`.
|
|
15
|
+
7. MEMORY.md index entries under a `## Pre-Compaction Saves` section (best effort; only if MEMORY.md exists).
|
|
16
|
+
|
|
17
|
+
## What's DEFERRED
|
|
18
|
+
|
|
19
|
+
- **Per-model knob.** Could pin flush to Haiku-class or local Ollama independent of session's main model. Current design uses the shared intelligence provider (subscription default). Tracked as v2 enhancement; no demonstrated need today.
|
|
20
|
+
- **Cross-session dedup.** Repeated flushes may write similar facts. Memory consumers tolerate near-duplicates on read; explicit dedup is v2.
|
|
21
|
+
- **Option B (in-session silent turn).** Considered; rejected for v1 because user-visible outcome is identical and server-side is simpler/lower-risk. Recorded as possible v2 refinement.
|
|
22
|
+
- **Migration to existing agents' settings.json.** Not needed — the change is server-side, hooked via the existing PreCompact event that all agents already report. New agents and existing agents benefit equally on the next instar update.
|
|
23
|
+
|
|
24
|
+
## Over-block analysis
|
|
25
|
+
|
|
26
|
+
The new behavior:
|
|
27
|
+
- Writes at most 5 files per compaction event (each capped at ~700 bytes after frontmatter).
|
|
28
|
+
- Appends one block of ≤5 lines to `MEMORY.md` per fire.
|
|
29
|
+
- Sends a single LLM call per fire, prompt body capped at ~31 KB (30 KB transcript + ~1 KB instruction).
|
|
30
|
+
|
|
31
|
+
No file ever gets overwritten — slugs include the flush timestamp, so collisions across two flushes within the same second produce two distinct files. No risk of clobbering existing memory entries.
|
|
32
|
+
|
|
33
|
+
If the LLM goes haywire (returns 100 facts), `maxFactsPerFlush` caps the writes at 5. If a fact slug coerces to empty after sanitization, that fact is dropped (asserted in test).
|
|
34
|
+
|
|
35
|
+
## Under-block analysis
|
|
36
|
+
|
|
37
|
+
When the LLM returns `NONE` or `[]`, the flush exits at `no-facts`. This is the correct under-block — the prompt explicitly invites this response when nothing durable surfaces. No false-positive memory pollution.
|
|
38
|
+
|
|
39
|
+
When the LLM returns garbage, the flush exits at `parse-failure` with the truncated response captured in audit. The user's MEMORY.md is not polluted.
|
|
40
|
+
|
|
41
|
+
## Level-of-abstraction fit
|
|
42
|
+
|
|
43
|
+
- **`PreCompactionFlush` is a single class.** All flush logic — transcript reading, prompt construction, parsing, file writes, audit — lives in one file. Testable in isolation.
|
|
44
|
+
- **Wired into server.ts at the existing PreCompact event listener block.** ~22 lines of new wiring code, conditional on config. The class is loaded via dynamic `await import` so the cold-path cost is zero when disabled.
|
|
45
|
+
- **No new HTTP endpoints, no new hook scripts.** The change rides on existing infrastructure (HookEventReceiver, hook-event-reporter.js, shared intelligence). One new listener, one new class.
|
|
46
|
+
|
|
47
|
+
This is the minimal vertical that delivers the user-visible win. Anything larger would be premature.
|
|
48
|
+
|
|
49
|
+
## Signal-vs-authority compliance
|
|
50
|
+
|
|
51
|
+
The flush itself is an **authority** path (it writes durable files). Signal-vs-authority is preserved by:
|
|
52
|
+
- The LLM is asked to make a *signal-quality* judgment (which facts are durable).
|
|
53
|
+
- The class enforces **hard caps** as the **authority** layer (max facts, max length, slug coercion, write-or-audit). No LLM output ever bypasses the caps.
|
|
54
|
+
- A bad LLM call can't damage the user's memory — only audit junk surfaces.
|
|
55
|
+
|
|
56
|
+
## Interactions
|
|
57
|
+
|
|
58
|
+
- **`CompactionSentinel`** (post-compaction recovery): unchanged. Both run on the same PreCompact event but in independent listeners. The Sentinel handles re-injection AFTER compaction; this class handles flush BEFORE compaction. They don't race because the Sentinel's work is keyed on a 10-second post-compaction delay (line 4477).
|
|
59
|
+
- **`HookEventReceiver`**: emits PreCompact unchanged. New listener does not modify the event payload or the emitter's behaviour.
|
|
60
|
+
- **`shared intelligence`** (PR #198 chokepoint): used per the subscription-by-default contract. If `sharedIntelligence` is null (e.g., CLI unavailable + no API opt-in), flush audits `no-intelligence` and exits.
|
|
61
|
+
- **MEMORY.md**: appended under a dedicated `## Pre-Compaction Saves` heading. If that heading does not exist, it is created at the file tail. Pre-existing memory entries are untouched.
|
|
62
|
+
- **`.instar/memory/` directory**: new files only; existing files untouched.
|
|
63
|
+
- **`.instar/audit/`**: new jsonl file; existing audit files untouched.
|
|
64
|
+
|
|
65
|
+
## Rollback cost
|
|
66
|
+
|
|
67
|
+
- `config.preCompactionFlush.enabled: false` reverts to legacy behaviour. The class stays in the codebase but the listener is never registered.
|
|
68
|
+
- Files already written to `.instar/memory/learning_precompact_*.md` are harmless markdown — they can be reviewed and deleted at the operator's discretion.
|
|
69
|
+
- Audit jsonl can be moved aside or deleted at any time.
|
|
70
|
+
- No schema migrations, no state mutations beyond memory files.
|
|
71
|
+
|
|
72
|
+
## CI surface
|
|
73
|
+
|
|
74
|
+
Touched:
|
|
75
|
+
- `src/core/PreCompactionFlush.ts` — new file, fully covered by 16 unit tests.
|
|
76
|
+
- `src/commands/server.ts` — 22 lines of new wiring inside the existing compaction block. The path is gated on config, so existing server-startup tests (which don't enable the new flag) exercise the gate-off branch.
|
|
77
|
+
|
|
78
|
+
No CI workflow changes. No husky changes. No native module changes.
|
|
79
|
+
|
|
80
|
+
## Open questions
|
|
81
|
+
|
|
82
|
+
None. The design is fully constrained by:
|
|
83
|
+
- OpenClaw's documented pattern (audit doc §3).
|
|
84
|
+
- Justin's approval of "Option A — server-side, agent-noninterruptive."
|
|
85
|
+
- Existing PR #198 safety-guard contract for the intelligence provider.
|
|
@@ -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,136 @@
|
|
|
1
|
+
# Side-Effects Review — Token Ledger Native-Module Heal
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `token-ledger-native-heal`
|
|
4
|
+
**Date:** `2026-05-15`
|
|
5
|
+
**Author:** Echo (instar developer agent)
|
|
6
|
+
**Second-pass reviewer:** required (touches monitoring infrastructure + healer surface)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Adds a sync surface (`openWithHealSync<T>`) to `NativeModuleHealer` and wires `TokenLedger`'s constructor to use it. Restores `/tokens/*` endpoints on every active agent on this machine. Without this fix, a Node version upgrade after instar install silently breaks the token ledger forever.
|
|
11
|
+
|
|
12
|
+
Files touched:
|
|
13
|
+
- `src/memory/NativeModuleHealer.ts` — adds `openWithHealSync<T>(component, opener)` and `healBetterSqlite3Sync()`. Existing async `healBetterSqlite3()` is now a thin wrapper over the sync impl. No behavior change for existing async callers.
|
|
14
|
+
- `src/monitoring/TokenLedger.ts` — replaces `new Database(opts.dbPath)` with `NativeModuleHealer.openWithHealSync('TokenLedger', () => new Database(opts.dbPath))`. Adds the import.
|
|
15
|
+
- `src/server/AgentServer.ts` — adds `NativeModuleHealer.configure({ stateDir })` before TokenLedger construction so heal events log to the agent's state directory. Adds the import.
|
|
16
|
+
- `tests/unit/NativeModuleHealer.test.ts` — adds 5 regression tests covering `openWithHealSync` success / non-mismatch passthrough / heal-failure / heal-then-retry / no-retry-after-prior-failure.
|
|
17
|
+
- `tests/unit/token-ledger.test.ts` — adds 1 regression test pinning that the TokenLedger constructor routes the DB open through the healer.
|
|
18
|
+
|
|
19
|
+
Decision points the change interacts with: only one — `NativeModuleHealer`. The fix extends its public surface with a sync variant. The healer remains the authority over native-module rebuilds; this PR just exposes that authority to sync call sites.
|
|
20
|
+
|
|
21
|
+
## Decision-point inventory
|
|
22
|
+
|
|
23
|
+
- `NativeModuleHealer.openWithHeal` (LLM-backed authority? no — deterministic authority over native-module rebuild) — **extend** — adds a sync surface variant. Decision logic identical to existing async path.
|
|
24
|
+
- `TokenLedger.constructor → new Database` (low-level resource acquisition) — **modify** — routes the failure path through the existing healer authority instead of bubbling unhandled.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 1. Over-block
|
|
29
|
+
|
|
30
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
31
|
+
|
|
32
|
+
No new block/allow surface. The change adds a heal-on-throw safety net to a code path that previously had no safety net. The only behavioral change is: where today a NODE_MODULE_VERSION mismatch produces `tokenLedger = null` forever, after this change the healer attempts a rebuild and retries the open. If the rebuild succeeds, the ledger opens normally; if it fails, the original mismatch error is rethrown with heal-context (same final state as today).
|
|
33
|
+
|
|
34
|
+
No legitimate input shapes are newly rejected.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 2. Under-block
|
|
39
|
+
|
|
40
|
+
**What failure modes does this still miss?**
|
|
41
|
+
|
|
42
|
+
- **Rebuild fails repeatedly across process restarts.** The healer's once-per-process guard means each restart gets one rebuild attempt; if the rebuild keeps failing (e.g., disabled C compiler, no node-gyp), every server restart retries and burns ~30s. This is the existing healer behavior, not a regression — and it surfaces visibly via the heal log + `lastResult`.
|
|
43
|
+
- **Other native modules.** The healer only knows about better-sqlite3. If a different native module (sqlite-vec, faiss-node, etc.) had a NODE_MODULE_VERSION mismatch, this PR wouldn't help. Out of scope.
|
|
44
|
+
- **Concurrent heal storms.** PROP-399's W-1 extension (`invokeFromRemediator`) deals with multi-process storm coalescing; this PR uses the legacy in-line path which has no storm protection. Acceptable because TokenLedger init runs once per process inside the server constructor, before any other heal-capable component has a chance to race.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 3. Level-of-abstraction fit
|
|
49
|
+
|
|
50
|
+
**Is this at the right layer?**
|
|
51
|
+
|
|
52
|
+
Yes. The `NativeModuleHealer` is the existing canonical home for "rebuild a native module when it ABI-mismatches." Three other modules in tree already use its `openWithHeal()` async surface. Adding a sync mirror is the natural extension. The fix doesn't introduce a new layer; it consumes an existing one.
|
|
53
|
+
|
|
54
|
+
`TokenLedger`'s constructor is where the better-sqlite3 open happens; that's where the heal call belongs. Moving it to AgentServer.start() would have made the constructor async (intrusive) without benefit — heal events still need a stateDir, which AgentServer.start() doesn't have any earlier than the constructor.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 4. Signal vs authority compliance
|
|
59
|
+
|
|
60
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
61
|
+
|
|
62
|
+
**Does this change hold blocking authority with brittle logic?**
|
|
63
|
+
|
|
64
|
+
- [x] No — this change has no block/allow surface.
|
|
65
|
+
- [ ] No — this change produces a signal consumed by an existing smart gate.
|
|
66
|
+
- [ ] Yes — but the logic is a smart gate with full conversational context.
|
|
67
|
+
- [ ] ⚠️ Yes, with brittle logic.
|
|
68
|
+
|
|
69
|
+
The new sync surface is a mechanical extension of an existing authority. It does not make any classification or judgment decisions. The healer's existing rebuild logic is the authority — sync vs async is only the consumer-side calling convention. No brittle logic, no new blocking surface.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 5. Interactions
|
|
74
|
+
|
|
75
|
+
**Does this interact with existing checks, recovery paths, or infrastructure?**
|
|
76
|
+
|
|
77
|
+
- **Shadowing**: existing healer call sites (SemanticMemory/TopicMemory/MemoryIndex) still use async `openWithHeal`. The sync variant is opt-in per call site. No shadowing.
|
|
78
|
+
- **Double-fire**: `healAttempted` (singleton state) is shared across sync + async paths — by design, so a once-per-process rebuild stays once-per-process even if both surfaces are used in the same process. The first caller wins; subsequent callers see "heal already attempted" and rethrow with hint. Correct behavior.
|
|
79
|
+
- **Races**: the healer is a process singleton; all paths through it serialize on a single boolean. Node single-threaded, no race.
|
|
80
|
+
- **Adjacent cleanups**: `resetForTesting()` clears `healAttempted` — verified to also reset for the new sync path (no separate sync state).
|
|
81
|
+
- **AgentServer ordering**: `NativeModuleHealer.configure({ stateDir })` runs before `new TokenLedger(...)` in AgentServer.constructor. The configure call only sets a private field; it cannot fail.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 6. External surfaces
|
|
86
|
+
|
|
87
|
+
**Does this change anything visible outside the immediate code path?**
|
|
88
|
+
|
|
89
|
+
- **Other agents on the same machine** — yes, in a positive way: every agent's TokenLedger init now heals through the rebuild path automatically. Visible to the operator as: `/tokens/summary` starts returning live data instead of `{"error":"token ledger unavailable"}` after the next agent upgrade.
|
|
90
|
+
- **Other users of the install base** — same. Every instar agent benefits from the heal once it upgrades to this version.
|
|
91
|
+
- **External systems** — npm gets one extra invocation (`npm rebuild better-sqlite3`) on each agent where the binding was previously mismatched. One-shot, ~30 seconds per agent, fully bounded by spawnSync timeout.
|
|
92
|
+
- **Persistent state** — `<stateDir>/native-module-heals.jsonl` may gain entries (existing log path from PROP-399). No new state files. No schema migration.
|
|
93
|
+
- **Timing / runtime conditions we don't fully control** — the rebuild requires npm + a working C compiler. On agents missing those (rare), the heal logs failure and the ledger remains null. Same final state as today.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 7. Rollback cost
|
|
98
|
+
|
|
99
|
+
**If this turns out wrong in production, what's the back-out?**
|
|
100
|
+
|
|
101
|
+
- **Hot-fix release**: revert four files + their tests, ship a patch release. ~10 minutes once detected.
|
|
102
|
+
- **Data migration**: none. No persistent state introduced.
|
|
103
|
+
- **Agent state repair**: none. Any `native-module-heals.jsonl` entries from this PR are append-only observability, not load-bearing.
|
|
104
|
+
- **User visibility**: zero. Worst-case rollback returns the ledger to its current "unavailable" state, which is the baseline today.
|
|
105
|
+
|
|
106
|
+
Pure code change, no migrations, no state to roll back.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Conclusion
|
|
111
|
+
|
|
112
|
+
The change is the smallest possible adapter between an existing healer authority and the TokenLedger constructor that has been bypassing it. No new decision logic, no new blocking surface, no persistent state, no API contract change for existing callers. Worst-case rollback is a four-file revert at zero migration cost. The fix unblocks the observability that was meant to detect bleeding patterns automatically — without it, the next "bleeding out" scenario will again be invisible in the agent's own data. Clear to ship pending second-pass concurrence.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Second-pass review
|
|
117
|
+
|
|
118
|
+
**Reviewer:** general-purpose subagent (Echo's review subagent)
|
|
119
|
+
**Independent read of the artifact: concur**
|
|
120
|
+
|
|
121
|
+
The change is the minimal correct extension of the existing healer authority into the sync constructor of TokenLedger; the `healAttempted` singleton is shared across both surfaces by design (single-threaded Node + sync spawnSync means no interleave is possible), all 33 unit tests pass locally, AgentServer's outer try/catch preserves the exact same final state when the post-heal retry throws a different error (e.g., permission denied), and the only production call site of `new TokenLedger(...)` is AgentServer itself.
|
|
122
|
+
|
|
123
|
+
Minor non-blocking notes:
|
|
124
|
+
- `NativeModuleHealer.configure({ stateDir })` is last-write-wins; today only AgentServer calls it, so a future second caller with a different stateDir would silently redirect heal logs. Not a current bug — flag if another configure caller is ever added.
|
|
125
|
+
- Test-mode constructions of `TokenLedger` (7 call sites in `tests/unit/token-ledger.test.ts`) skip the configure step and would log heal events to the os-tmp fallback if a real rebuild ever fired in tests. In practice the heal is fully mocked, so this never executes — but worth noting if integration tests start exercising the rebuild path for real.
|
|
126
|
+
- Existing async tests spy on `healBetterSqlite3`; the sync path goes directly to `healBetterSqlite3Sync` and intentionally bypasses the wrapper. The two surfaces share state but not method identity — that's the right separation and the new test correctly mocks `healBetterSqlite3Sync` directly.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Evidence pointers
|
|
131
|
+
|
|
132
|
+
- Original failure log (server.log, accumulated over 28 restarts since 2026-05-13):
|
|
133
|
+
`[instar] token-ledger init failed (non-fatal): Error: The module ... was compiled against a different Node.js version using NODE_MODULE_VERSION ...`
|
|
134
|
+
- `/tokens/summary` baseline on Echo before fix: `{"error":"token ledger unavailable"}`
|
|
135
|
+
- Test verification: `npx vitest run tests/unit/NativeModuleHealer tests/unit/token-ledger.test.ts` → 49/49 passed (6 new regression tests included).
|
|
136
|
+
- Acceptance: post-publish + post-upgrade, `/tokens/summary` returns a live JSON payload on Echo + at least one other agent.
|