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.
- package/dashboard/index.html +170 -7
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +6 -4
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/playbook.d.ts.map +1 -1
- package/dist/commands/playbook.js +2 -1
- package/dist/commands/playbook.js.map +1 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +91 -8
- package/dist/commands/server.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +5 -3
- package/dist/commands/setup.js.map +1 -1
- package/dist/core/Config.d.ts.map +1 -1
- package/dist/core/Config.js +2 -1
- package/dist/core/Config.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +4 -5
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/SessionManager.d.ts +38 -0
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +157 -23
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/UpdateChecker.d.ts.map +1 -1
- package/dist/core/UpdateChecker.js +3 -1
- package/dist/core/UpdateChecker.js.map +1 -1
- package/dist/core/UpgradeGuideProcessor.d.ts.map +1 -1
- package/dist/core/UpgradeGuideProcessor.js +3 -1
- package/dist/core/UpgradeGuideProcessor.js.map +1 -1
- package/dist/core/types.d.ts +18 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/lifeline/ServerSupervisor.d.ts.map +1 -1
- package/dist/lifeline/ServerSupervisor.js +3 -1
- package/dist/lifeline/ServerSupervisor.js.map +1 -1
- package/dist/memory/SemanticMemory.d.ts +9 -0
- package/dist/memory/SemanticMemory.d.ts.map +1 -1
- package/dist/memory/SemanticMemory.js +131 -0
- package/dist/memory/SemanticMemory.js.map +1 -1
- package/dist/monitoring/PresenceProxy.d.ts +53 -0
- package/dist/monitoring/PresenceProxy.d.ts.map +1 -1
- package/dist/monitoring/PresenceProxy.js +219 -20
- package/dist/monitoring/PresenceProxy.js.map +1 -1
- package/dist/scheduler/JobRunHistory.d.ts +6 -0
- package/dist/scheduler/JobRunHistory.d.ts.map +1 -1
- package/dist/scheduler/JobRunHistory.js +11 -0
- package/dist/scheduler/JobRunHistory.js.map +1 -1
- package/dist/scheduler/JobScheduler.d.ts +23 -0
- package/dist/scheduler/JobScheduler.d.ts.map +1 -1
- package/dist/scheduler/JobScheduler.js +84 -0
- package/dist/scheduler/JobScheduler.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +56 -0
- package/dist/server/routes.js.map +1 -1
- package/dist/threadline/ThreadlineBootstrap.d.ts.map +1 -1
- package/dist/threadline/ThreadlineBootstrap.js +3 -2
- package/dist/threadline/ThreadlineBootstrap.js.map +1 -1
- package/dist/threadline/relay/ConnectionManager.d.ts.map +1 -1
- package/dist/threadline/relay/ConnectionManager.js +34 -7
- package/dist/threadline/relay/ConnectionManager.js.map +1 -1
- package/package.json +1 -1
- package/scripts/pre-push-gate.js +26 -0
- package/src/data/builtin-manifest.json +64 -64
- package/upgrades/0.28.79.md +67 -0
- package/upgrades/0.28.80.md +93 -0
- package/upgrades/side-effects/0.28.79.md +310 -0
- package/upgrades/side-effects/assembler-context-endpoint.md +67 -0
- package/upgrades/side-effects/post-update-migrator-path-fix.md +52 -0
- package/upgrades/side-effects/presence-proxy-ack-and-baseline.md +260 -0
- package/upgrades/side-effects/semantic-memory-corruption-recovery.md +98 -0
- 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/`.
|