instar 0.28.78 → 0.28.80

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.
Files changed (71) hide show
  1. package/dashboard/index.html +170 -7
  2. package/dist/commands/init.d.ts.map +1 -1
  3. package/dist/commands/init.js +6 -4
  4. package/dist/commands/init.js.map +1 -1
  5. package/dist/commands/playbook.d.ts.map +1 -1
  6. package/dist/commands/playbook.js +2 -1
  7. package/dist/commands/playbook.js.map +1 -1
  8. package/dist/commands/server.d.ts.map +1 -1
  9. package/dist/commands/server.js +91 -8
  10. package/dist/commands/server.js.map +1 -1
  11. package/dist/commands/setup.d.ts.map +1 -1
  12. package/dist/commands/setup.js +5 -3
  13. package/dist/commands/setup.js.map +1 -1
  14. package/dist/core/Config.d.ts.map +1 -1
  15. package/dist/core/Config.js +2 -1
  16. package/dist/core/Config.js.map +1 -1
  17. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  18. package/dist/core/PostUpdateMigrator.js +4 -5
  19. package/dist/core/PostUpdateMigrator.js.map +1 -1
  20. package/dist/core/SessionManager.d.ts +38 -0
  21. package/dist/core/SessionManager.d.ts.map +1 -1
  22. package/dist/core/SessionManager.js +157 -23
  23. package/dist/core/SessionManager.js.map +1 -1
  24. package/dist/core/UpdateChecker.d.ts.map +1 -1
  25. package/dist/core/UpdateChecker.js +3 -1
  26. package/dist/core/UpdateChecker.js.map +1 -1
  27. package/dist/core/UpgradeGuideProcessor.d.ts.map +1 -1
  28. package/dist/core/UpgradeGuideProcessor.js +3 -1
  29. package/dist/core/UpgradeGuideProcessor.js.map +1 -1
  30. package/dist/core/types.d.ts +18 -0
  31. package/dist/core/types.d.ts.map +1 -1
  32. package/dist/core/types.js.map +1 -1
  33. package/dist/lifeline/ServerSupervisor.d.ts.map +1 -1
  34. package/dist/lifeline/ServerSupervisor.js +3 -1
  35. package/dist/lifeline/ServerSupervisor.js.map +1 -1
  36. package/dist/memory/SemanticMemory.d.ts +9 -0
  37. package/dist/memory/SemanticMemory.d.ts.map +1 -1
  38. package/dist/memory/SemanticMemory.js +131 -0
  39. package/dist/memory/SemanticMemory.js.map +1 -1
  40. package/dist/monitoring/PresenceProxy.d.ts +53 -0
  41. package/dist/monitoring/PresenceProxy.d.ts.map +1 -1
  42. package/dist/monitoring/PresenceProxy.js +219 -20
  43. package/dist/monitoring/PresenceProxy.js.map +1 -1
  44. package/dist/scheduler/JobRunHistory.d.ts +6 -0
  45. package/dist/scheduler/JobRunHistory.d.ts.map +1 -1
  46. package/dist/scheduler/JobRunHistory.js +11 -0
  47. package/dist/scheduler/JobRunHistory.js.map +1 -1
  48. package/dist/scheduler/JobScheduler.d.ts +23 -0
  49. package/dist/scheduler/JobScheduler.d.ts.map +1 -1
  50. package/dist/scheduler/JobScheduler.js +84 -0
  51. package/dist/scheduler/JobScheduler.js.map +1 -1
  52. package/dist/server/routes.d.ts.map +1 -1
  53. package/dist/server/routes.js +56 -0
  54. package/dist/server/routes.js.map +1 -1
  55. package/dist/threadline/ThreadlineBootstrap.d.ts.map +1 -1
  56. package/dist/threadline/ThreadlineBootstrap.js +3 -2
  57. package/dist/threadline/ThreadlineBootstrap.js.map +1 -1
  58. package/dist/threadline/relay/ConnectionManager.d.ts.map +1 -1
  59. package/dist/threadline/relay/ConnectionManager.js +34 -7
  60. package/dist/threadline/relay/ConnectionManager.js.map +1 -1
  61. package/package.json +1 -1
  62. package/scripts/pre-push-gate.js +26 -0
  63. package/src/data/builtin-manifest.json +64 -64
  64. package/upgrades/0.28.79.md +67 -0
  65. package/upgrades/0.28.80.md +93 -0
  66. package/upgrades/side-effects/0.28.79.md +310 -0
  67. package/upgrades/side-effects/assembler-context-endpoint.md +67 -0
  68. package/upgrades/side-effects/post-update-migrator-path-fix.md +52 -0
  69. package/upgrades/side-effects/presence-proxy-ack-and-baseline.md +260 -0
  70. package/upgrades/side-effects/semantic-memory-corruption-recovery.md +98 -0
  71. package/upgrades/side-effects/url-pathname-path-encoding-fix.md +45 -0
@@ -0,0 +1,260 @@
1
+ ---
2
+ title: PresenceProxy — brief-ack tolerance + post-message baseline
3
+ slug: presence-proxy-ack-and-baseline
4
+ date: 2026-05-04
5
+ author: echo
6
+ second_pass_required: false
7
+ ---
8
+
9
+ ## Summary of the change
10
+
11
+ PresenceProxy emits tiered standby updates (20s / 2m / 5m) when an agent
12
+ hasn't replied to a user message yet. Two regressions had silently
13
+ broken the user-facing behavior:
14
+
15
+ 1. **Brief acks were cancelling all tier timers.** Telegram-bridged agents
16
+ are now instructed to send an immediate ack ("Got it, looking into
17
+ this", "On it") on every inbound message. The proxy interpreted that
18
+ ack as the agent's response and cancelled every pending tier check.
19
+ Result: the user never saw a 20s/2m/5m progressive update again — the
20
+ feature looked broken even though the timer machinery was intact.
21
+
22
+ 2. **Tier-summary prompts described pre-message work.** The prompts
23
+ read whatever was visible in the agent's tmux pane right now, which
24
+ is the rolling window — so older work from BEFORE the user's latest
25
+ message often dominated the snapshot. The user got summaries of work
26
+ the agent was already doing, not work the agent was doing in response
27
+ to their message.
28
+
29
+ This change adds two things:
30
+
31
+ - `isBriefAck(text)` — a length-bounded, opener-only pattern matcher that
32
+ classifies short forward-looking acks ("On it", "Got it, looking into
33
+ this") as non-cancelling. `onMessageLogged` now skips the cancellation
34
+ branch for brief acks (still records them on conversation history so
35
+ subsequent prompts know an ack went out).
36
+ - `userMessageBaselineSnapshot` on `PresenceState`, captured in
37
+ `handleUserMessage` at the moment the user message arrives. Plus an
38
+ `extractDeltaSinceBaseline()` helper and a `buildScopedSnapshotBlock()`
39
+ method that the four tier-prompt builders now use to feed the LLM only
40
+ the post-baseline delta.
41
+
42
+ Files touched:
43
+ - `src/monitoring/PresenceProxy.ts` — new helpers, baseline capture,
44
+ ack-aware message handling, scoped prompt blocks.
45
+ - `tests/unit/presence-proxy-ack-and-baseline.test.ts` — 15 new tests
46
+ across `isBriefAck`, `extractDeltaSinceBaseline`, brief-ack handling,
47
+ and baseline capture.
48
+
49
+ ## Decision-point inventory
50
+
51
+ The change touches one decision point: PresenceProxy's
52
+ "is this agent message a real response?" predicate, which gates timer
53
+ cancellation. `isSystemOrProxyMessage` was already shared with several
54
+ other subsystems (compaction recovery, stall triage, log scans); we
55
+ intentionally did NOT modify that helper. Brief acks ARE real agent
56
+ messages — they just shouldn't end the standby cycle. So the new check
57
+ lives inside PresenceProxy's `onMessageLogged` branch only and does not
58
+ leak into other subsystems' definition of "real reply."
59
+
60
+ ---
61
+
62
+ ## 1. Over-block
63
+
64
+ The brief-ack filter is the only thing that could over-block. The
65
+ "block" here is "block cancellation" — i.e., a real substantive reply
66
+ gets misclassified as an ack and tier timers keep running. The user
67
+ sees one extra standby message after the agent has already answered.
68
+
69
+ Mitigations:
70
+
71
+ - **Length cap of 200 chars.** Substantive replies tend to be longer.
72
+ 200 is generous enough to cover compound acks ("Got it — looking into
73
+ both: foo and bar. On it.") but tight enough to exclude short
74
+ substantive answers in practice.
75
+ - **Opening-only match (first 60 chars).** Patterns like `\bi['']?ll\s+(?:dig|look|...)` only fire when the message STARTS with that
76
+ phrase — a 200-char substantive reply that mentions "I'll get to that
77
+ next" deep in the body won't match.
78
+ - **Conservative pattern list.** Generic "I will" / "let me" alone is
79
+ not enough — must be followed by an action verb (`dig`, `look`,
80
+ `check`, etc.). This was tightened in response to a failing test that
81
+ caught an over-match on a 267-char substantive plan.
82
+
83
+ Worst case: tier 1 fires after a real reply, the user sees one
84
+ "🔭 the-agent is currently …" message immediately after the substantive
85
+ answer. No data loss, no repeated tier 2/3 because tier 1 reads the
86
+ post-message terminal pane (which now contains the substantive reply)
87
+ and produces a brief, accurate snapshot summary. Then the timers re-arm
88
+ on the next user message.
89
+
90
+ ---
91
+
92
+ ## 2. Under-block
93
+
94
+ Under-blocking the cancellation = a real substantive reply incorrectly
95
+ classified as ack → timers fire when they shouldn't. Covered above.
96
+
97
+ The other direction is "ack misclassified as substantive" → timers
98
+ cancel as before, user sees no progressive updates. This is the bug we
99
+ were already living with; our change can't make it worse than the
100
+ status quo.
101
+
102
+ ---
103
+
104
+ ## 3. Level-of-abstraction fit
105
+
106
+ Both new helpers are pure, exported, and live next to the other
107
+ detectors (`detectQuotaExhaustion`, `detectSessionIdle`,
108
+ `isLongRunningProcess`) in PresenceProxy.ts — same level of abstraction
109
+ the file already operates at. No new modules, no new framework, no new
110
+ queue. The state field (`userMessageBaselineSnapshot`) is a sibling of
111
+ existing snapshot fields. The prompt-scoping helper
112
+ (`buildScopedSnapshotBlock`) is a private method on the proxy class,
113
+ co-located with the four prompt builders that consume it.
114
+
115
+ The baseline snapshot is intentionally NOT persisted to disk
116
+ (consistent with the existing policy for `tier1Snapshot` /
117
+ `tier2Snapshot`) — too large, contains potentially sensitive content,
118
+ and a session restart loses the original user-message moment anyway.
119
+ After restart, `recoverFromRestart` sets the baseline to null and
120
+ prompts fall back to the legacy "full pane" path with a `[scope: full
121
+ pane — baseline anchor scrolled off]` label.
122
+
123
+ ---
124
+
125
+ ## 4. Signal vs authority compliance
126
+
127
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
128
+
129
+ - [x] **`isBriefAck` is a brittle pattern-matching filter — a SIGNAL,
130
+ not an authority.** It does not block cancellation outright; it
131
+ simply withholds the cancellation that the proxy was about to
132
+ perform. The "authority" in this flow remains the natural one:
133
+ either a substantive reply lands and cancels timers, or the
134
+ agent's tier message comes from the LLM-backed prompt builder
135
+ (which has full context of conversation history). No brittle
136
+ filter is making a final decision on user experience.
137
+ - [x] **Baseline scoping is also a signal-shaping change**, not an
138
+ authority change. The LLM still decides what to say in the tier
139
+ message; we just narrow the input so the decision happens on the
140
+ right context. If the baseline anchor can't be located, we
141
+ conservatively widen back to the full pane and label the prompt
142
+ so the LLM knows scope is best-effort.
143
+
144
+ The dangerous failure mode in this kind of work is "brittle filter
145
+ silently determines the user-facing outcome." Both fixes are
146
+ specifically scoped to AVOID that: false positives on `isBriefAck`
147
+ produce a slightly redundant user message (recoverable in the next
148
+ message); false positives on the baseline anchor produce slightly less
149
+ focused tier summaries (no worse than the pre-change behavior).
150
+
151
+ ---
152
+
153
+ ## 5. Interactions with adjacent subsystems
154
+
155
+ - **CompactionSentinel.recoverFn** uses `findLastRealMessage` /
156
+ `isSystemOrProxyMessage` to decide whether to re-inject after
157
+ compaction. We did NOT modify those helpers — brief acks are still
158
+ considered "real" by that subsystem (which is correct: an ack IS a
159
+ real outbound message that the user saw). Only PresenceProxy's
160
+ cancellation logic treats brief acks specially.
161
+ - **PromiseBeacon / shared LLM queue** — unchanged. Tier messages still
162
+ go through the same `interactive` lane and respect the daily spend
163
+ cap.
164
+ - **ProxyCoordinator mutex** — unchanged. The mutex acquisition order
165
+ in `sendProxyMessage` is the same; we only changed prompt content
166
+ and added a new pre-cancel branch.
167
+ - **Persisted state files** — schema unchanged; the new
168
+ `userMessageBaselineSnapshot` is an in-memory-only field. Existing
169
+ state files still load correctly (the spread in `recoverFromRestart`
170
+ picks up undefined for the new field, then we explicitly set it to
171
+ null).
172
+ - **Tier 1 fallback (no LLM, intelligence: null)** — unchanged. Falls
173
+ back to the same templated message; the new snapshot scoping only
174
+ affects the LLM prompt, never the fallback path.
175
+
176
+ ---
177
+
178
+ ## 6. Rollback cost
179
+
180
+ Single-file revert + test deletion. No schema changes, no on-disk
181
+ artifacts, no API contract changes. If the brief-ack filter
182
+ misbehaves in production we can:
183
+
184
+ 1. Empty the `BRIEF_ACK_PATTERNS` array — every agent message is
185
+ substantive again, behavior matches v0.28.79.
186
+ 2. Or revert the entire commit — same outcome, plus the prompts go
187
+ back to full-pane scope.
188
+
189
+ Both rollbacks are atomic and require no migration.
190
+
191
+ ---
192
+
193
+ ## 7. Test coverage
194
+
195
+ New tests in `tests/unit/presence-proxy-ack-and-baseline.test.ts`:
196
+
197
+ - `isBriefAck` (5 tests):
198
+ - very short messages always ack
199
+ - forward-looking phrases under 200 chars are acks
200
+ - substantive multi-sentence replies (267 chars) are NOT acks
201
+ - empty/null/whitespace not classified as ack
202
+ - 280-char substantive cap (boundary)
203
+
204
+ - `extractDeltaSinceBaseline` (5 tests):
205
+ - null/empty baseline → full current
206
+ - null current → empty
207
+ - anchor found → returns post-anchor content, anchored=true
208
+ - identical baseline+current → hasNewActivity=false
209
+ - anchor missing (terminal scrolled) → falls back to full current,
210
+ anchored=false
211
+
212
+ - `PresenceProxy brief-ack handling` (3 tests):
213
+ - tier 1 + tier 2 both fire after brief ack
214
+ - substantive reply DOES cancel tiers
215
+ - multiple acks in sequence don't cancel; substantive reply finally does
216
+
217
+ - `PresenceProxy baseline capture` (2 tests):
218
+ - baseline captured at user-message arrival
219
+ - capture failure doesn't crash, baseline stays null
220
+
221
+ All 64 pre-existing PresenceProxy tests still pass — no regression in
222
+ cancel-race, build-heartbeat suppression, idle detection,
223
+ context-exhaustion, quota detection, or long-tool-wait paths.
224
+
225
+ ---
226
+
227
+ ## 8. Evidence
228
+
229
+ - Repro source: Justin's message in topic 8882 (2026-05-04, 03:52
230
+ UTC) reporting "this feature no longer seems to give progressive
231
+ updates" and "messages from standby mode often seem to be
232
+ summarizing what the agent was working on BEFORE the user's last
233
+ message."
234
+ - Root cause for #1: `handleAgentMessage` is called from
235
+ `onMessageLogged` for any non-system, non-proxy outbound message.
236
+ Telegram bridge instructions added an "On it" ack as the first
237
+ outbound message on every inbound user message → cancellation
238
+ fires before tier 1 can run.
239
+ - Root cause for #2: tier prompts at lines 1192/1211/1244/1271
240
+ passed `snapshot.slice(0, 3000)` directly. The snapshot is the
241
+ full visible pane, with no boundary marker for "what was here
242
+ when the user's message arrived."
243
+
244
+ ---
245
+
246
+ ## 9. What this does NOT change
247
+
248
+ - Tier 1 / 2 / 3 timing (still 20s / 2m / 5m by default).
249
+ - LLM cost or model selection.
250
+ - Persistence schema or on-disk state files.
251
+ - `isSystemOrProxyMessage` / `findLastRealMessage` (shared with
252
+ compaction + stall triage).
253
+ - Conversation-history capping.
254
+ - Proxy mutex acquisition / release semantics.
255
+ - `triggerManualTriage`, unstick, restart, quiet, resume command flows.
256
+
257
+ The change is intentionally surgical: two well-defined behaviors
258
+ (timer cancellation predicate + prompt input scoping) modified in
259
+ their natural locations, with new pure helpers exposed for direct
260
+ testing.
@@ -0,0 +1,98 @@
1
+ # Side-Effects Review — SemanticMemory corruption detection and auto-recovery
2
+
3
+ **Version / slug:** `semantic-memory-corruption-recovery`
4
+ **Date:** 2026-04-27
5
+ **Author:** gfrankgva (contributor)
6
+ **Second-pass reviewer:** Echo (EchoOfDawn), 3 review rounds
7
+
8
+ ## Summary of the change
9
+
10
+ Three files touched:
11
+
12
+ 1. `src/core/types.ts` — `SemanticMemoryConfig` gains an optional `autoRebuildMaxBytes?: number` field (default 50 MB). No existing code passes this field, so all callers keep current behavior.
13
+
14
+ 2. `src/memory/SemanticMemory.ts` — `open()` gains an integrity check block mirroring TopicMemory's pattern:
15
+ - After opening the DB, runs `PRAGMA integrity_check`. If result is not `'ok'`, or if the pragma itself throws (severely corrupt DB), triggers recovery.
16
+ - **Secondary probe read**: If `integrity_check` passes, reads 100 rows from each existing table. Catches torn interior pages that `integrity_check` misses (pages not reachable from the B-tree schema walk).
17
+ - Recovery: calls `quarantineCorruptDb()` which renames the DB to `.corrupt.<timestamp>`, removes WAL/SHM sidecars, writes a JSON marker file. Falls back to delete if rename fails.
18
+ - After schema creation and vector init, checks `_needsRebuild` flag. If JSONL exists and is within the size gate, rebuilds synchronously. If JSONL exceeds `autoRebuildMaxBytes`, logs warning, starts empty, and writes a `skipped-rebuild` marker file.
19
+
20
+ 3. `tests/unit/semantic-memory-corruption-recovery.test.ts` — Test file with 12 contract-style tests covering: open-without-throwing, quarantine preservation, marker shape, sidecar cleanup (strengthened WAL/SHM assertions), JSONL rebuild, no-JSONL fresh start, healthy-DB no-op, severe-corruption pragma-throws path, partial-corruption (valid header + 4KB corrupted data page in 5000-row DB), size-gate skip, skipped-rebuild marker file, and subsequent-open stability.
21
+
22
+ ## Decision-point inventory
23
+
24
+ - `SemanticMemoryConfig.autoRebuildMaxBytes` — **add** (type: optional number, default 50 MB).
25
+ - `SemanticMemory.open()` integrity check block — **add** (new code path between DB constructor and pragma setup).
26
+ - `SemanticMemory.quarantineCorruptDb()` — **add** (new private method).
27
+ - `SemanticMemory._needsRebuild` — **add** (new private field, transient between integrity check and rebuild).
28
+ - Auto-rebuild size gate — **add** (checks `fs.statSync(jsonlPath).size` against config limit).
29
+ - `SemanticMemory` probe-read block — **add** (secondary detection after integrity_check passes; reads 100 rows from each existing table).
30
+ - `SemanticMemory.writeSkippedRebuildMarker()` — **add** (new private method; writes `.skipped-rebuild.<ts>.marker.json` when size gate triggers).
31
+
32
+ ---
33
+
34
+ ## 1. Over-block
35
+
36
+ **What legitimate inputs does this change reject that it shouldn't?**
37
+
38
+ When JSONL exceeds `autoRebuildMaxBytes` (default 50 MB), the DB starts empty after corruption recovery. This means an operator with a large knowledge graph (> ~500k entities) would need to trigger `importFromJsonl()` manually after startup. This is deliberate — blocking server startup for minutes on a synchronous import is worse than starting with empty memory. The operator can rebuild during a maintenance window.
39
+
40
+ The integrity check itself runs on every `open()` call, adding measurable startup latency for large DBs. TopicMemory pays the same cost, so consistency wins. If semantic DBs grow very large, a `quick_check` pragma (subset of integrity_check) could be a future optimization.
41
+
42
+ ## 2. Under-block
43
+
44
+ **What failure modes does this still miss?**
45
+
46
+ - **Mid-session corruption**: Only detected on `open()`. If a disk block goes bad during a running session, individual SQLite operations will throw but no automatic recovery triggers. This is out of scope — mid-session recovery would require connection pooling or shadow-DB switching, far beyond this PR's scope.
47
+ - **Probe-read coverage**: The secondary probe reads 100 rows from each non-FTS table. Very large tables with corruption only in pages beyond the first 100 rows could theoretically pass the probe. In practice, 100 rows spans multiple 4KB pages, making this unlikely. Full table scans at startup would have unacceptable latency on large DBs.
48
+ - **JSONL truncation**: If the JSONL was itself truncated (disk-full event during a write), the rebuild will be partial — some entities may be missing. The `importFromJsonl()` method handles malformed lines gracefully (skips them), so the rebuild is best-effort. The quarantined DB is preserved for forensic comparison.
49
+ - **Writes not flushed to JSONL**: All mutation paths in SemanticMemory go through `remember()` / `addEdge()` which write to JSONL first (append), then to DB. The JSONL is the source of truth. There is no path where the DB is written first.
50
+
51
+ ## 3. Level-of-abstraction fit
52
+
53
+ **Is this at the right layer?**
54
+
55
+ Yes. SemanticMemory owns its DB lifecycle — `open()` is the correct place for integrity checks, matching TopicMemory's pattern. The quarantine logic is a private method, not exposed to callers. The size-gate config is on the existing `SemanticMemoryConfig` interface, which is the established place for tuning knobs.
56
+
57
+ The alternative (adding a "health check" service layer above SemanticMemory) would scatter recovery logic across modules and require SemanticMemory to expose its DB state — worse encapsulation.
58
+
59
+ ## 4. Blocking authority
60
+
61
+ - [x] No — this is a startup-time recovery mechanism. It does not gate any runtime operation. The only "decision" is quarantine-vs-keep, which is always quarantine (corruption is binary).
62
+
63
+ ## 5. Interactions
64
+
65
+ - **Shadowing:** No existing corruption detection to shadow — SemanticMemory had none before this PR.
66
+ - **Double-fire:** `_needsRebuild` is reset after the rebuild block. A second `open()` on the recovered DB is a no-op (tested).
67
+ - **Races:** `open()` is async but the integrity check is synchronous (better-sqlite3 is sync). No concurrent access during startup.
68
+ - **Downstream consumers:** Callers of `SemanticMemory.open()` (currently only `src/commands/server.ts`) see no behavioral change on healthy DBs. On corrupt DBs, `open()` succeeds instead of potentially throwing — strictly better.
69
+
70
+ ## 6. External surfaces
71
+
72
+ - **Agents:** After corruption recovery, the knowledge graph may be rebuilt from JSONL (common case) or start empty (large JSONL). Agents notice "fewer memories" but server stays up — preferable to a crash loop.
73
+ - **File system:** New files created during recovery: `.corrupt.<ts>` (quarantined DB), `.corrupt-recovery.<ts>.marker.json` (recovery marker). These accumulate over time — an operator might want periodic cleanup, but each occurrence is exceptional (disk errors).
74
+ - **Persistent state:** The JSONL append log is never modified — only read during rebuild. The SQLite DB is replaced (quarantined + fresh). No other persistent state is touched.
75
+
76
+ ## 7. Rollback cost
77
+
78
+ Pure code change. Revert removes the integrity check — corrupt DBs would again cause `open()` to either throw or silently serve bad data. No migration, no data repair needed on rollback.
79
+
80
+ ---
81
+
82
+ ## 8. Destructive-tool containment compliance
83
+
84
+ `quarantineCorruptDb()` uses `fs.unlinkSync` to remove the corrupt DB and its WAL/SHM sidecars. Per the Comprehensive Destructive-Tool Containment spec (PRs #98/#99), all destructive filesystem calls must go through `SafeFsExecutor`. Updated:
85
+
86
+ - `fs.unlinkSync(this.config.dbPath)` → `SafeFsExecutor.safeUnlinkSync(this.config.dbPath, { operation: 'SemanticMemory.quarantineCorruptDb' })`
87
+ - `fs.unlinkSync(this.config.dbPath + ext)` → `SafeFsExecutor.safeUnlinkSync(this.config.dbPath + ext, { operation: 'SemanticMemory.quarantineCorruptDb:sidecar' })`
88
+
89
+ The test file uses `fs.rmSync` in `afterEach` cleanup only (temp directory in `os.tmpdir()`). Annotated with `// safe-git-allow:` escape comment per the lint spec.
90
+
91
+ ---
92
+
93
+ ## Evidence pointers
94
+
95
+ - Typecheck: `tsc --noEmit` — 0 errors.
96
+ - Lint: `node scripts/lint-no-direct-destructive.js` — 0 violations.
97
+ - Tests: 12 contract tests covering all recovery paths including partial corruption (valid SQLite header + 4KB corrupted data page in 5000-row DB), size-gate behavior, and skipped-rebuild marker.
98
+ - TopicMemory parity: pattern mirrors `TopicMemory.open()` which has been production-stable since v0.27.x.
@@ -0,0 +1,45 @@
1
+ # Side-Effects Review — Eliminate URL.pathname path encoding across the codebase
2
+
3
+ **Version / slug:** `url-pathname-path-encoding-fix`
4
+ **Date:** 2026-04-28
5
+ **Author:** gfrankgva (contributor)
6
+
7
+ ## Summary of the change
8
+
9
+ Systematic replacement of `new URL(import.meta.url).pathname` with `__dirname` (or `fileURLToPath()`) across 13 source files. The former preserves `%20`-encoded spaces in filesystem paths, causing `fs.readFileSync`, `path.resolve`, and similar operations to fail when the project directory contains spaces.
10
+
11
+ **Files changed (source):**
12
+ - `src/commands/init.ts` (4 occurrences)
13
+ - `src/commands/playbook.ts` (1)
14
+ - `src/commands/server.ts` (4)
15
+ - `src/commands/setup.ts` (3)
16
+ - `src/core/Config.ts` (1)
17
+ - `src/core/PostUpdateMigrator.ts` (2)
18
+ - `src/core/SessionManager.ts` (1)
19
+ - `src/core/UpdateChecker.ts` (1)
20
+ - `src/core/UpgradeGuideProcessor.ts` (1)
21
+ - `src/threadline/ThreadlineBootstrap.ts` (1)
22
+ - `src/lifeline/ServerSupervisor.ts` (1)
23
+
24
+ **Files changed (tests):** 5 test files with unquoted `execSync` paths or test expectation updates.
25
+
26
+ **Files changed (generated):** `src/data/builtin-manifest.json` — content hashes updated to reflect changed source files.
27
+
28
+ **Files changed (infrastructure):** `scripts/pre-push-gate.js` — added regression guard (check 5) that prevents re-introduction of the `URL.pathname` antipattern.
29
+
30
+ ## Decision-point inventory
31
+
32
+ - All `new URL(import.meta.url).pathname` usages — **fix** (replace with `__dirname` or `fileURLToPath`).
33
+ - No behavioral changes — every replacement produces the same decoded path, just without the `%20` encoding bug.
34
+
35
+ ---
36
+
37
+ ## 1–7. Analysis
38
+
39
+ This is a pure bug fix with no behavioral, architectural, or security implications. Every replacement produces the identical filesystem path on systems without spaces, and the correct path on systems with spaces. No new code paths, no new dependencies, no new failure modes. Fully reversible by reverting the commit.
40
+
41
+ ## Evidence pointers
42
+
43
+ - Typecheck: `tsc --noEmit` — 0 errors.
44
+ - Full test suite: 740 files passed, 0 failed, 17171 individual tests passed.
45
+ - Zero instances of `new URL(import.meta.url).pathname` remain in `src/`.