instar 1.2.24 → 1.2.26

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.
@@ -0,0 +1,92 @@
1
+ # Side-Effects Review — fix(heal): resilient process.execPath resolution
2
+
3
+ **Branch:** `echo/fix-heal-execpath-staleness`
4
+ **Origin:** Luna self-heal failure 2026-05-21 (topic 9976, "topic-intent-layer"). Justin's framing: "the deeper issue that needs to be fixed: why couldn't Luna self heal?"
5
+ **Scope guardrail:** sibling fix to the Topic Intent Layer work, not a fold-in. Heal-path only.
6
+
7
+ ## Summary
8
+
9
+ When Homebrew updates Node mid-session, `process.execPath` becomes ENOENT. The native-module self-heal spawned subprocesses via that path, so the heal failed with `spawnSync … ENOENT` and the agent's SQLite-backed subsystems degraded. The fix resolves a stable Node binary through a fallback chain before spawning.
10
+
11
+ Two new files:
12
+ - `src/utils/resolveNodeBinary.ts` — TypeScript resolver used by `ensureSqliteBindings`.
13
+ - `scripts/resolve-node-binary.cjs` — CommonJS twin used by `fix-better-sqlite3.cjs`.
14
+
15
+ Two modified files:
16
+ - `src/commands/server.ts` — `ensureSqliteBindings` resolves the spawn target before invoking the rebuild path; emits DegradationReporter events on resolver-null and rebuild-failed paths.
17
+ - `scripts/fix-better-sqlite3.cjs` — Module-load resolution; every internal spawn uses the resolved `NODE_BIN`. `findNpmCli` resolves npm relative to the resolved Node, not raw `process.execPath`.
18
+
19
+ Two new test files:
20
+ - `tests/unit/resolveNodeBinary.test.ts` — 7 tests covering the fallback chain branch-by-branch.
21
+ - `tests/integration/heal-execpath-staleness.test.ts` — 6 tests reproducing the failure shape and verifying the fix.
22
+
23
+ ## Over-block check
24
+
25
+ Does this fix do MORE than what was asked?
26
+
27
+ - The resolver does NOT change the once-per-process heal guard in `NativeModuleHealer`. That guard exists to prevent expensive rebuild loops and was deliberately left in place.
28
+ - The resolver does NOT bypass `verifyChildAbiMatches` in the fix script. The ABI-divergence defence is still in place — it now runs against a resolved Node binary instead of process.execPath, which is what makes the defence actually work in the failure mode we're fixing.
29
+ - The DegradationReporter events are NEW emissions but match the existing pattern (subsystem-init-failure events already use the same shape). They surface a previously-silent failure rather than introducing new noise.
30
+
31
+ ## Under-block check
32
+
33
+ Does this fix leave the failure mode reachable through another path?
34
+
35
+ Other call sites that spawn via `process.execPath` were audited:
36
+ - `ServerSupervisor.preflightSelfHeal` — runs in the supervisor process, not the child server. Different process, different execPath lifecycle. Out of scope for this fix; supervisor process is short-lived and re-spawns on each cycle.
37
+ - `NativeModuleHealer.healBetterSqlite3Sync` — spawns `npmPath` (a separate variable resolved via `findNpmPath`) with `process.execPath` as args[0]. ENOENT on execPath here would also fail. **Follow-up:** apply the same resolver to this path in a separate small PR. Out of scope for this commit to keep the diff tight per the original ask.
38
+
39
+ The follow-up is tracked at the bottom of this artifact, not punted as an orphan TODO. (Per the `no-out-of-scope-trap` memory.)
40
+
41
+ ## Level-of-abstraction fit
42
+
43
+ The resolver is a utility, not a subsystem. It does one thing: resolve a path. It's called from the existing heal-path call sites; it does not own any state, does not log autonomously, does not own the rebuild flow. The DegradationReporter emissions live in `ensureSqliteBindings`, not in the resolver — the resolver returns data, the caller decides whether to escalate.
44
+
45
+ This matches the signal-vs-authority pattern: the resolver is the signal layer (returns "here's a working Node, here's how I found it, or null if nothing works"); the caller is the authority that decides what to do with that signal.
46
+
47
+ ## Signal-vs-authority compliance
48
+
49
+ The resolver emits no degradation events, no logs, no side effects beyond resolving a path. It returns a structured value or null.
50
+
51
+ The caller (`ensureSqliteBindings`) is the authority — it decides:
52
+ - Whether to proceed with the rebuild (only if resolver returned a path).
53
+ - Whether to emit a DegradationReporter event (yes when resolver returns null OR when rebuild fails after resolution).
54
+ - What recovery hint to surface in the degradation impact field.
55
+
56
+ Two emission sites, both with explicit recovery actions:
57
+ - `ensureSqliteBindings.nodeBinaryResolution` — fires when the resolver returns null. Recovery: reinstall Node and restart.
58
+ - `ensureSqliteBindings.rebuildFailed` — fires when rebuild fails after resolution. Recovery: restart agent OR run `npx instar update`.
59
+
60
+ ## Interactions with other systems
61
+
62
+ - **NativeModuleHealer.openWithHeal** (the in-line heal path for direct constructor calls): NOT modified by this PR. That path still uses `process.execPath` internally. The follow-up applies the same resolver there.
63
+ - **ServerSupervisor.preflightSelfHeal**: NOT modified. Supervisor runs in a separate process with its own execPath lifecycle.
64
+ - **UpdateChecker.applyUpdate**: Already had its own rebuild path that worked on luna (visible in the server log at 02:35 and 03:21 — both successful rebuilds while the server kept running with stale bindings). No conflict: UpdateChecker's rebuilds happen in long-running processes that already hold the broken in-memory binding; they take effect on next restart. This fix ensures the next restart's preflight can actually run.
65
+ - **PostUpdateMigrator**: NOT touched. Migrator script propagation works as before; no agent-side script needs migration for this fix because both new files (.ts + .cjs) live in the instar package itself.
66
+
67
+ ## Rollback cost
68
+
69
+ Single-commit revert removes the resolver, the import, the call-site change in `ensureSqliteBindings`, and the resolver-pinned spawns in `fix-better-sqlite3.cjs`. The behavior reverts to "heal fails when execPath is stale" — the failure mode we're fixing, not a new one. No data migration, no config changes, no agent-side script propagation required.
70
+
71
+ ## Cross-framework portability (v1.0+)
72
+
73
+ The heal path is framework-agnostic — it operates on Node binaries, not on the agent runtime (Claude Code vs Codex). The resolver uses standard Node APIs (`fs`, `os`, `child_process`) with no framework-specific code paths. Verified by:
74
+ - Integration test passes under the test-running Node regardless of framework.
75
+ - The Codex CLI runtime spawns differently but uses the same Node-binary substrate, so the resolver applies equally there.
76
+
77
+ No `INSTAR_FRAMEWORK`-conditional code added; no framework-aware adapters needed for this fix.
78
+
79
+ ## Telemetry / observability
80
+
81
+ Heal-attempt visibility before this PR: a single yellow log line on failure.
82
+ Heal-attempt visibility after this PR: that log line PLUS a DegradationReporter event with structured fields (feature, primary, fallback, reason, impact). The event surfaces through `GET /degradations` (existing route) and any subscribers to the degradation event channel (existing pattern).
83
+
84
+ When the resolver fires a non-execPath fallback, a `console.log(pc.dim(...))` line names which fallback fired and the resolved path. This is one extra log line per server startup that needed it; on healthy startups it doesn't fire.
85
+
86
+ ## Follow-ups (tracked, not orphaned)
87
+
88
+ 1. Apply `resolveStableNodeBinary` to `NativeModuleHealer.healBetterSqlite3Sync` so the in-line CLI-direct-construction heal path (CLI commands like `instar memory ...`, TokenLedger constructor, etc.) also benefits. Should be a small commit on a separate branch — the heal-path-fix scope intentionally excluded it to keep this diff tight.
89
+
90
+ 2. Consider whether the once-per-process heal guard in `NativeModuleHealer` should distinguish "heal failed due to spawn-target ENOENT" (transient, retryable when operator restores Node) from "heal failed due to rebuild error" (durable, don't retry). The current guard treats both the same. Lower priority than (1).
91
+
92
+ Both items will be filed as initiatives so they don't drop. Not part of this commit.
@@ -0,0 +1,88 @@
1
+ # Side-effects review — org-intent tradeoff helper (Phase 3)
2
+
3
+ Spec: `docs/specs/ORG-INTENT-TRADEOFF-HELPER-SPEC.md`
4
+ ELI16: `docs/specs/ORG-INTENT-TRADEOFF-HELPER-SPEC.eli16.md`
5
+ Phase: 3 of 4. Phase 1 shipped as v1.2.23 (PR #315). Phase 2 shipped in PR #317. Phase 4 (drift detection job) queued.
6
+
7
+ ## Surface map
8
+
9
+ | Change | File | Type |
10
+ |---|---|---|
11
+ | Pure `resolveTradeoff()` function + `TradeoffResolution` type | `src/core/TradeoffResolver.ts` (new file) | Additive module |
12
+ | New `POST /intent/tradeoff-resolve` HTTP route | `src/server/routes.ts` | Additive route |
13
+ | CLAUDE.md ORG-INTENT subsection adds Phase 3 curl line | `src/scaffold/templates.ts` + `src/core/PostUpdateMigrator.ts` | Doc + migration |
14
+ | Tier 1 unit tests (resolver + migration) | `tests/unit/TradeoffResolver.test.ts` (new), `tests/unit/PostUpdateMigrator-org-intent-runtime.test.ts` (extended) | Test addition |
15
+ | Tier 2 integration tests | `tests/integration/org-intent-routes.test.ts` (extended) | Test addition |
16
+ | Tier 3 E2E test | `tests/e2e/org-intent-tradeoff-lifecycle.test.ts` (new) | Test addition |
17
+
18
+ ## Over-block analysis
19
+
20
+ **Could the new helper ever block an outbound message?**
21
+
22
+ No. The resolver is pure deterministic logic with no authority over outcomes. The Coherence Gate from Phase 1 remains the only place where ORG-INTENT can block a message. Per `feedback_signal_vs_authority`, the resolver is SIGNAL — it tells callers what the hierarchy says; it never refuses anything.
23
+
24
+ **Could the route be DoS'd?**
25
+
26
+ The route reads `ORG-INTENT.md` from the agent's state dir on every request — bounded local disk read, no LLM call, no DB query. Auth middleware enforces Bearer-token gating. No new abuse surface beyond what `GET /intent/org` already exposes.
27
+
28
+ **Could the resolver return a wrong winner?**
29
+
30
+ Three risk vectors:
31
+
32
+ 1. **String matching false positives** — value "speed" might substring-match "execution speed of light." The resolver's case-insensitive substring containment is permissive by design. For ambiguous cases the caller should be specific in their value strings. This is documented in the resolver's source comments.
33
+
34
+ 2. **Same-entry tie** — if both values appear in the same hierarchy entry without a "X over Y" pair pattern, the resolver returns `basis: 'tie'` and `winner: null`. Caller decides what to do — typically escalates to the value-alignment reviewer.
35
+
36
+ 3. **Pair-pattern misparsing** — the resolver only recognizes the six listed patterns (`over`, `before`, `above`, `trumps`, `wins over`, `beats`). Other phrasings fall through to list-order matching. This is acceptable; future iterations can extend the pattern list.
37
+
38
+ ## Under-block analysis
39
+
40
+ **What does the resolver NOT catch?**
41
+
42
+ - Multi-value tradeoffs (three or more contending values). The function signature is `(valueA, valueB)`. Callers wanting to resolve a three-way race must call the resolver twice and compose. Future iteration may add `resolveTradeoffN([...])` if demand surfaces.
43
+ - Context-dependent applicability. A constraint like "customer trust over speed" may not apply in some channels (e.g. an internal status update where speed is fine). The resolver doesn't model channels; the caller decides applicability.
44
+ - Stale state. The route reads `ORG-INTENT.md` on every request, so edits propagate immediately — but if the hierarchy is changed mid-session, the agent's session-start injection (Phase 2) still shows the old version until next session.
45
+
46
+ ## Level-of-abstraction fit
47
+
48
+ `TradeoffResolver` is pure logic, single file, no dependencies — the right level for a deterministic helper. The HTTP route lives next to the other `/intent/*` routes. No new abstractions introduced. The helper is intentionally NOT exposed as a class — a free function is the right shape for a stateless pure computation.
49
+
50
+ ## Signal-vs-authority compliance
51
+
52
+ The resolver is **SIGNAL** — pure deterministic computation, no authority over outcomes. The Coherence Gate from Phase 1 remains **AUTHORITY** for any value-alignment block. Callers who consult the resolver are free to override its conclusion. The reviewer in the gate is also free to disagree with the resolver's `basis: 'list-order'` reading if it has better contextual reasoning.
53
+
54
+ This is exactly the signal/authority separation per `feedback_signal_vs_authority`. Future Phase 4 drift detection should follow the same pattern — emit a digest signal, never block.
55
+
56
+ ## Interactions with existing systems
57
+
58
+ | System | Interaction | Risk |
59
+ |---|---|---|
60
+ | Coherence Gate (Phase 1) | None — gate's value-alignment reviewer continues using LLM-based resolution | None |
61
+ | Session-start injection (Phase 2) | None — independent surface | None |
62
+ | `GET /intent/org` | Sibling route — same source data, different output shape | None |
63
+ | `OrgIntentManager.parse()` | Resolver consumes the `tradeoffHierarchy` field | Read-only — no mutation |
64
+ | Existing routes | No conflict — `/intent/tradeoff-resolve` is a new path | None |
65
+
66
+ ## Rollback cost
67
+
68
+ Low. Three options:
69
+
70
+ 1. **Code revert**: `git revert <PR-merge-sha>` removes the new file, the new route, and the new test files. No data migration to roll back.
71
+ 2. **Soft revert via 404**: rename the new route in `routes.ts`. The `TradeoffResolver` module remains as dead code; nothing else consumes it.
72
+ 3. **Just don't call it**: the route is opt-in. No code path that existed before this PR calls the new route, so leaving it un-called is equivalent to it not existing.
73
+
74
+ ## Test coverage summary
75
+
76
+ | Tier | File | Tests | Status |
77
+ |---|---|---|---|
78
+ | 1 (unit) | `tests/unit/TradeoffResolver.test.ts` | 16 | ✓ passing |
79
+ | 1 (unit) | `tests/unit/PostUpdateMigrator-org-intent-runtime.test.ts` (extended) | 8 (1 new for Phase 3) | ✓ passing |
80
+ | 2 (integration) | `tests/integration/org-intent-routes.test.ts` (extended) | 16 (5 new for Phase 3) | ✓ passing |
81
+ | 3 (E2E lifecycle) | `tests/e2e/org-intent-tradeoff-lifecycle.test.ts` | 5 | ✓ passing |
82
+
83
+ ## Open follow-ups (deferred to later phases, NOT this PR)
84
+
85
+ - Phase 4: periodic drift detection job sampling recent outbound actions vs intent.
86
+ - `resolveTradeoffN([...])` for multi-way tradeoff resolution.
87
+ - Channel/recipient scoping on constraints + tradeoffs.
88
+ - Optional LLM fallback when `basis: 'no-match'` — caller-controlled escalation path.