synabun 2026.4.27 → 2026.5.2-beta
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/CHANGELOG.md +189 -9
- package/README.md +67 -7
- package/hooks/claude-code/post-plan.mjs +56 -3
- package/hooks/claude-code/session-start.mjs +28 -1
- package/hooks/claude-code/shared.mjs +13 -1
- package/hooks/claude-code/stop.mjs +23 -0
- package/mcp-server/src/index.ts +4 -0
- package/mcp-server/src/services/profiles.ts +113 -7
- package/mcp-server/src/tools/gsc/experience.ts +440 -0
- package/mcp-server/src/tools/gsc/helpers.ts +288 -0
- package/mcp-server/src/tools/gsc/indexing.ts +495 -0
- package/mcp-server/src/tools/gsc/inspect.ts +391 -0
- package/mcp-server/src/tools/gsc/navigate.ts +236 -0
- package/mcp-server/src/tools/gsc/performance.ts +352 -0
- package/mcp-server/src/tools/gsc/settings.ts +564 -0
- package/mcp-server/src/tools/gsc.ts +95 -0
- package/mcp-server/src/tools/loop.ts +62 -15
- package/mcp-server/src/tools/profile.ts +1 -0
- package/neural-interface/public/shared/cdx/cdx-icons.js +1 -0
- package/neural-interface/public/shared/cdx/cdx-panel.js +24 -5
- package/neural-interface/public/shared/cdx/cdx-tabs.js +42 -0
- package/neural-interface/public/shared/html-shell.js +35 -35
- package/neural-interface/public/shared/ocp/ocp-panel.js +255 -25
- package/neural-interface/public/shared/ocp/ocp-render.js +152 -3
- package/neural-interface/public/shared/ocp/ocp-styles.js +520 -31
- package/neural-interface/public/shared/ocp/ocp-tabs.js +786 -128
- package/neural-interface/public/shared/provider-icons.js +2 -0
- package/neural-interface/public/shared/storage.js +5 -0
- package/neural-interface/public/shared/styles.css +538 -141
- package/neural-interface/public/shared/ui-claude-panel.js +35 -17
- package/neural-interface/public/shared/ui-file-explorer.js +65 -41
- package/neural-interface/public/shared/ui-menubar.js +17 -3
- package/neural-interface/public/shared/ui-navbar.js +33 -9
- package/neural-interface/public/shared/ui-sessions.js +613 -23
- package/neural-interface/public/shared/ui-settings.js +1 -2
- package/neural-interface/public/shared/ui-sidepanel-tray.js +173 -22
- package/neural-interface/public/shared/ui-sync.js +11 -5
- package/neural-interface/public/shared/ui-tool-updates.js +35 -16
- package/neural-interface/public/shared/ui-tutorial-steps.js +3 -3
- package/neural-interface/public/shared/ui-update.js +17 -2
- package/neural-interface/server.js +791 -21
- package/neural-interface/templates/CLAUDE-template.md +19 -6
- package/neural-interface/templates/ruleset-versions.json +31 -0
- package/package.json +1 -1
- package/setup.js +96 -37
package/CHANGELOG.md
CHANGED
|
@@ -1,14 +1,194 @@
|
|
|
1
1
|
## THIS IS A NEW CHANGELOG START FRESH ##
|
|
2
2
|
|
|
3
|
+
## 2026-05-02
|
|
4
|
+
|
|
5
|
+
### Fixed — OpenCode Sidepanel: Read-Only Tools After Plan→Build / Plan→Chat Transition (Qwen + non-Anthropic models)
|
|
6
|
+
|
|
7
|
+
- **Symptom** — In OpenCode plan mode with Qwen 3.6 Plus (and likely any non-Anthropic provider), clicking "Continue with implementation" on the `PLAN COMPLETE` card put the panel into build mode but the model only saw read-only tools (`read`/`glob`/`grep`/`task`/`webfetch`/`todowrite`/`skill`/`question` + MCP). It then hallucinated `Bash` calls and got `Model tried to call unavailable tool 'Bash'` repeatedly. The missing tools matched exactly the `{write, edit, patch, bash}` set the panel disables in plan mode.
|
|
8
|
+
- **Root cause** — `neural-interface/public/shared/ocp/ocp-tabs.js:1257`. Plan-mode `sendMessage` calls send `body.tools = { write:false, edit:false, patch:false, bash:false }` as belt-and-suspenders against OpenCode sometimes ignoring `mode:plan`. OpenCode's SDK persists those per-message restrictions at the **session level** — subsequent build/chat turns that omitted `body.tools` inherited the disable. `setTabMode('build')` (post-plan card) and `setTabMode('chat')` (mode toggle) flipped the UI and the `body.mode` field, but the underlying tools remained disabled, so the build/chat agent never got its mutating tools back.
|
|
9
|
+
- **Fix** — Mirror the disable: when `tab.mode !== 'plan'`, explicitly send `body.tools = { write:true, edit:true, patch:true, bash:true }` to override the persisted restriction. Symmetric, idempotent, and applies to both build and chat (covers post-plan-card, mode-toggle, and Tab-cycle paths). No new state — each `sendMessage` decides based on the live `tab.mode`.
|
|
10
|
+
|
|
11
|
+
### Fixed — Sidepanel Tray Pills Hidden Until Toolbar Click (Cross-WindowId Orphaning)
|
|
12
|
+
|
|
13
|
+
- **Fresh browser tabs saw zero pills despite real session history** — `neural-interface/public/shared/ui-sidepanel-tray.js` only consulted the *current* `windowId`-scoped storage key (`synabun-{provider}-tabs-{windowId}`). Each new browser tab generates a fresh `windowId` via `sessionStorage`, so the placeholder system found nothing and rendered nothing — even though the user had ~70 OpenCode / ~14 Codex / 10 Claude meaningful sessions in `data/ui-state.json` under prior `windowId`-scoped keys. To see any pill, the user had to click the provider's toolbar icon to force `ensurePanel()` → `restoreTabs()` → render. Single-area fix in `ui-sidepanel-tray.js`:
|
|
14
|
+
- **`storage.keys()` accessor** — Added in `neural-interface/public/shared/storage.js` so the tray module can enumerate the in-memory cache for cross-window discovery (`return Object.keys(_cache)`).
|
|
15
|
+
- **`providerPayloads(provider)` (replaces `providerPayload`)** — Returns an array. Reads the current windowId's key first; if it has no meaningful tabs (per provider's state predicate), falls back to `pickMostRecentMeaningfulPayload(provider, currentKey)` which scans `storage.keys()` in *reverse* (insertion-order ≈ recency) for the freshest non-empty `synabun-{provider}-tabs-*` key whose tabs satisfy `hasClaudeState` / `hasCodexState` / `hasOpenCodeState`. Legacy single-session keys (`synabun-claude-panel-session`, `synabun-codex-panel-thread`) still merge in for back-compat.
|
|
16
|
+
- **`meaningfulPills(provider)` with dedupe** — Flattens every payload's tabs and dedupes by `cp:{sessionId}` / `cxp:{threadId}` / `ocp:{sessionId}` so the same session in multiple windowIds renders only once. Iteration order biases toward current windowId.
|
|
17
|
+
- **`migrateSourceToCurrentWindow(provider, sourceKey)`** — On placeholder pill click, the source key's data is moved into the current windowId's key BEFORE `toggleXxxPanel()` runs. Each panel's window-scoped `restoreTabs()` then picks it up with `saved.id` preserved, so the post-toggle `emit('xxx-panel:show', { tabId })` switches to the clicked tab. If current windowId already has meaningful tabs, the source's meaningful tabs are appended (deduped); otherwise the source replaces wholesale.
|
|
18
|
+
- **Pill carries `dataset.sourceKey`** — `createPlaceholder` now stamps `pill.dataset.sourceKey = payload.key`. Click handler passes it to `openProvider(provider, id, payload.key)`.
|
|
19
|
+
|
|
20
|
+
### Added — Sidepanel Session Lifecycle: Kill-on-Close + WindowId Storage GC
|
|
21
|
+
|
|
22
|
+
- **`POST /api/sidepanel/kill-session` server endpoint** — `neural-interface/server.js` accepts `{ provider, windowId, sessionId, threadId }` and force-kills the backend session immediately rather than waiting for the orphan grace timer:
|
|
23
|
+
- **claude** — `_orphanedProcs.get(_orphanKey(windowId, sessionId))` → `clearTimeout(orphan.killTimer)` + `orphan.kill()` + delete from map + `_releaseSessionLock(sessionId)`.
|
|
24
|
+
- **codex** — `_codexOrphanedProcs.get(_codexOrphanKey(windowId, sessionId))` → `clearTimeout` + `orphan.kill()` + delete from map.
|
|
25
|
+
- **opencode** — `ocpProxy('DELETE', '/session/{sessionId}')` because OpenCode sessions live on the OpenCode SDK server, not in an orphan map. Returns `killed:true` only on 2xx.
|
|
26
|
+
- **Placeholder pill X-button now calls kill-session before storage delete** — `ui-sidepanel-tray.js`: new `killBackendSession(provider, tab, sourceKey)` POSTs to the endpoint, with `windowIdFromKey()` extracting the windowId from `synabun-{provider}-tabs-{windowId}`. Best-effort: empty catch on the fetch so a network/404 still removes the storage entry. Wired into the close handler before `writeProviderPayload(...)`.
|
|
27
|
+
- **WindowId registry GC for OpenCode** — `neural-interface/public/shared/ocp/ocp-tabs.js` adds `STOR.windowRegistry = 'synabun-ocp-windows'` (JSON map of `windowId → epoch ms`). New `_updateOcpWindowRegistry()` runs from `saveTabs()` and at module load. New `_cleanStaleOcpWindows()` runs at module load: drops registered windowIds idle > 24h, plus an "unregistered sweep" that drops any `synabun-ocp-tabs-{wid}` whose `wid` isn't in the registry at all (covers legacy entries from before the registry existed). Both passes skip the current windowId so module-load ordering is safe.
|
|
28
|
+
- **WindowId registry GC for Codex** — Mirrored shape in `neural-interface/public/shared/cdx/cdx-icons.js` (added `STOR.windowRegistry = 'synabun-codex-panel-windows'`) and `neural-interface/public/shared/cdx/cdx-tabs.js` (`_updateCodexWindowRegistry()` + `_cleanStaleCodexWindows()` with the same 24h staleness + unregistered-sweep pattern, called from `saveTabs()` and at module load).
|
|
29
|
+
- **Result on first run after deploy** — Server-side `data/ui-state.json` cleanup: ~70 `synabun-ocp-tabs-*` orphan keys → 0; ~14 `synabun-codex-panel-tabs-*` → 0; Claude tab keys pruned to 4 live registered windows (within 24h). Going forward, every `saveTabs()` registers the current windowId, so subsequent sessions are protected by the 24h timestamp grace.
|
|
30
|
+
|
|
31
|
+
### Changed — Claude Panel WindowId GC Tightened
|
|
32
|
+
|
|
33
|
+
- **Unregistered-sweep arm added to `_cleanStaleWindows`** — `neural-interface/public/shared/ui-claude-panel.js` previously only dropped registered windowIds idle > 24h. Extended to also walk `storage.keys()` and remove any `synabun-claude-panel-tabs-{wid}` whose `wid` isn't present in `synabun-claude-panel-windows` at all (skips current windowId via early `continue`). Catches legacy entries written before the registry existed and orphans whose registry record was lost.
|
|
34
|
+
- **Module-load GC + registry update** — Added `_cleanStaleWindows(); _updateWindowRegistry();` calls at module load (immediately after the function definitions, before `restoreTabs()`'s definition). Previously GC only ran inside `restoreTabs()`, which runs lazily inside `ensurePanel()` — meaning the cleanup never fired if the user never opened the Claude panel that session. Now the registry stays accurate and stale keys get pruned on every page load.
|
|
35
|
+
|
|
36
|
+
## 2026-05-01
|
|
37
|
+
|
|
38
|
+
### Added — Notifications Drawer + Ruleset Version Alerts
|
|
39
|
+
|
|
40
|
+
- **`neural-interface/templates/ruleset-versions.json` manifest** — New file declaring per-format version + summary + updatedAt for the five condensed rulesets (`claude`, `codex`, `cursor`, `generic`, `gemini`). Version-bump-on-change model: bump only when the relevant section in `CLAUDE-template.md` ships a meaningful update; whitespace edits don't need a bump. Seeds all five formats at `1.0.0` with summary "Initial tracked version".
|
|
41
|
+
- **`GET /api/claude-code/ruleset/versions` endpoint** — `neural-interface/server.js` exposes per-format fingerprints. Manifest entry (when present) becomes `fingerprint: "v:<version>"`; when missing, server falls back to `"h:<sha1[0..16]>"` of the section text extracted via the existing `_RULESET_MARKERS` map (mirrors the markers used by `/api/claude-code/ruleset`). Cached by `max(template-mtime, manifest-mtime)` so the disk read happens at most once per change.
|
|
42
|
+
- **Updates tab in the Notifications drawer** — `neural-interface/public/shared/ui-sessions.js` adds a fifth tab (now first in the bar): `'updates'`. Shows ruleset / SynaBun / CLI-tool updates as cards grouped by kind, with `Open & copy` (acts on the update — opens Settings with the right section deep-linked) and `Mark as read` buttons. New `computeUpdates()` derives the list by comparing live data against `localStorage["synabun:notifications:acked-v1"]`. New `renderUpdatesTab()` / `renderUpdateCard()` / `handleUpdateAction()` / `handleUpdateAck()` / `refreshRulesetVersions()` / `ackRulesetUpdate()` / `ackSynabunUpdate()` / `ackToolUpdate()` helpers. Module imports `getSynabunUpdateData()` / `openSynabunUpdateModal()` from `ui-update.js`, `getToolUpdateData()` / `runToolUpdate()` / `TOOL_LABELS` from `ui-tool-updates.js`, and `openSettingsModal()` from `ui-settings.js`. New `RULESET_LABELS` map gives each format a display title, paste-target file (`CLAUDE.md` / `AGENTS.md` / `.cursorrules` / `GEMINI.md`), and Settings section ID for the deep-link. Drawer auto-defaults to the Updates tab when there is at least one unread item, otherwise falls back to Sessions.
|
|
43
|
+
- **First-run seeding** — On the very first load with no acked state in localStorage, the engine seeds every ruleset's `_acked.rulesets[fmt]` to its current fingerprint. Existing users don't get a flood of "new version" alerts on day one — they only see notifications when a version genuinely bumps after their first visit.
|
|
44
|
+
- **Navbar count badge for unread updates** — `neural-interface/public/shared/html-shell.js` adds a `<span class="count-badge" id="titlebar-notifications-count" hidden>` inside `#titlebar-sessions-btn`. `neural-interface/public/shared/ui-navbar.js` listens for the new `notifications:updated` event (dispatched by `ui-sessions.js`'s `emitNotificationsUpdate()`), writes the count, toggles `hidden`, and updates the button's tooltip to `Notifications · N updates`. Bootstraps via `fetchNotificationsForBadge()` on init plus a 5-minute polling fallback so the badge stays accurate without ever opening the drawer.
|
|
45
|
+
- **Read-only accessors on the existing update modules** — `getSynabunUpdateData()` exported from `ui-update.js`, `getToolUpdateData()` + `runToolUpdate(key)` exported from `ui-tool-updates.js`. The drawer reads cached state from those modules instead of duplicating the polling logic, so the existing top-right SynaBun update button and per-CLI-tool toolbar badges keep working unchanged and the drawer just mirrors them.
|
|
46
|
+
- **Updates-tab styling** — `neural-interface/public/shared/styles.css` adds `.sm-card-update`, `.sm-update-icon`, `.sm-update-title`, `.sm-update-version`, `.sm-update-meta`, `.sm-update-actions`, `.sm-btn-ghost`, `.sm-btn-primary`, `.sm-badge-warning`, plus an orange-pill `#titlebar-sessions-btn .count-badge` for the navbar.
|
|
47
|
+
- **Bell + per-card icons** — `ICON_BELL`, `ICON_RULESET`, `ICON_PACKAGE`, `ICON_UP_ARROW` SVG constants added to `ui-sessions.js`. Bell is the empty-state and tab icon; the rest are per-card kind icons.
|
|
48
|
+
|
|
49
|
+
### Changed — Session Monitor Renamed to Notifications Drawer
|
|
50
|
+
|
|
51
|
+
- **Window identity rewritten** — `neural-interface/public/shared/ui-sessions.js` `<h3>` reads `Notifications`, comment header rewritten to describe the window as the centralised notification surface, default `_activeTab` is now `'updates'` so the drawer opens to the most relevant tab. Tooltip on `#titlebar-sessions-btn` in `neural-interface/public/shared/html-shell.js` reads `Notifications`. Tutorial step 7 in `neural-interface/public/shared/ui-tutorial-steps.js` now labels the area `Notifications` with sublabel `Sessions, alerts, and ruleset / CLI updates`. CSS section comment in `neural-interface/public/shared/styles.css` renamed to `Notifications drawer`. Internal CSS classes (`.sm-*`, `.session-monitor-panel`), DOM IDs (`#titlebar-sessions-btn`, `#session-monitor-panel`), and event names (`session-monitor:closed`) kept as-is for backward compat with the rest of the app.
|
|
52
|
+
|
|
53
|
+
### Changed — Removed Dedicated Stale-Loops Alert Button (Titlebar)
|
|
54
|
+
|
|
55
|
+
- **Button + wiring deleted** — `#titlebar-loops-alert-btn` markup removed from `neural-interface/public/shared/html-shell.js`, the entire `if (loopsAlertBtn) { ... }` wiring block (~17 lines) removed from `neural-interface/public/shared/ui-navbar.js`, and the `.bar-loops-alert-btn` / `:hover` / `.count-badge` rules (~22 lines) removed from `neural-interface/public/shared/styles.css`. `openLoopsTab` dropped from the navbar import set; the export itself is kept in `ui-sessions.js` as a small public API surface.
|
|
56
|
+
- **Loops still surface via the drawer** — Stale loops continue to feed the `loops:updated` event and the existing `Loops` tab badge in the Notifications drawer. The 30s `fetchLoopsForBadge()` polling is preserved in `ui-navbar.js` so the Loops tab badge stays accurate without ever opening the drawer.
|
|
57
|
+
|
|
58
|
+
### Changed — Titlebar Icon Polish (Notifications, Image Gallery)
|
|
59
|
+
|
|
60
|
+
- **Notifications button now uses a bell glyph** — `#titlebar-sessions-btn` in `neural-interface/public/shared/html-shell.js` swapped from the legacy "tray with downward arrow" multi-path SVG to a clean two-path bell (24×24 viewBox, stroke-width 1.6, line-cap/join round). Count-badge `<span>` is unchanged so the orange `N` overlay still works.
|
|
61
|
+
- **Image Gallery button now uses the universal photo glyph** — `#titlebar-gallery-btn` swapped from a complex multi-path bag/basket-style icon (read as cluttered next to its cleaner siblings) to the universal photo icon: rounded rectangle frame, small circle (sun), diagonal mountain line. 24×24 viewBox, stroke-width 1.5, matches the rest of the navbar.
|
|
62
|
+
|
|
63
|
+
### Fixed — OpenCode Sidepanel Plan-Mode Post-Plan Card Silent Stall (Deepseek + non-Anthropic models)
|
|
64
|
+
|
|
65
|
+
- **Plan rendered but no `PLAN COMPLETE` card** — In OpenCode plan mode with non-Anthropic models (e.g. Deepseek v4 Pro), the plan would stream successfully but the post-plan action card (Edit / Continue / Compact) never appeared. The agent activity dock stayed "active" with no spinner, forcing the user to press Stop. Root cause: `showPostPlanUI(tab)` was only triggered from `tool.start` ExitPlanMode (Anthropic-only), `session.idle`, `session.status:idle`, and the WS-resolve fallback. When the planning subagent's `session.idle` fired but the parent never finalized (parent never received the task `tool.result`), none of the trigger sites fired. Single-file fix in `neural-interface/public/shared/ocp/ocp-tabs.js`:
|
|
66
|
+
- **`maybeShowPostPlanUI(tab, {source, forceRecapture})` helper** — Idempotent post-plan trigger gated on `mode === 'plan'`, `!showPostPlanActions`, and `planContent.length >= 40`. Centralizes the decision so non-`ExitPlanMode` terminal paths don't render an empty card.
|
|
67
|
+
- **8-second watchdog** — `schedulePlanFinalizationWatchdog(tab)` armed when `recordAgentChildEvent` records a subagent `session.idle`. If the parent's terminal signal doesn't fire by then, the watchdog force-finalizes (`tab.running = false`, `removeThinking`, `finalizeStreamingMessage`) and surfaces the post-plan card. `clearPlanFinalizationWatchdog(tab)` runs in every parent terminal path: WS-resolve fallback, `abortTab`, background-event idle, `message.completed`, `session.status:idle`, `session.idle`, `session.error`, `setTabMode` leaving plan.
|
|
68
|
+
- **ExitPlanMode mirror in `message.part.updated`** — Modern OpenCode SSE emits tools via `message.part.updated`, not legacy `tool.start`. Mirrored the ExitPlanMode/`plan_exit` detection (sets `_exitPlanDetected`, captures `toolInput.plan`, calls `showPostPlanUI`) so Anthropic models on modern OpenCode no longer rely solely on the `session.idle` fallback.
|
|
69
|
+
- **`message.completed` post-plan trigger** — Both rendered and fallback branches now call `maybeShowPostPlanUI` after clearing `tab.running`. Fixes the legacy-event silent stall.
|
|
70
|
+
- **`session.error` / `error` post-plan recovery** — When an error toast fires after plan content was already streamed, `maybeShowPostPlanUI` is called with `forceRecapture: true` so the user can still continue, edit, or retry without re-planning from scratch.
|
|
71
|
+
- **`sendMessage` catch-block recovery** — On non-abort, non-(timeout-with-recent-SSE) failures, `maybeShowPostPlanUI` runs with `forceRecapture: true` so a 4xx/5xx WS rejection after a successful plan stream doesn't strand the user.
|
|
72
|
+
- **Per-turn state reset** — `sendMessage` now resets `_exitPlanDetected` and `_pendingPostPlanCheck` (and clears the watchdog) at turn start. Previously these were only cleared on entering plan mode, breaking multi-turn plan sessions where the second turn's fallbacks would short-circuit on `!_exitPlanDetected`.
|
|
73
|
+
- **Watchdog ID not persisted** — Verified `_planFinalizationWatchdogId` is excluded from `saveTabs` serialization (only listed transient fields are persisted).
|
|
74
|
+
|
|
75
|
+
## 2026-04-30
|
|
76
|
+
|
|
77
|
+
### Fixed — Duplicate Hook Entries in `.claude/settings.json` (Claude Code Hook Installation)
|
|
78
|
+
- **Every Claude Code hook fired twice** — `addHookToSettings()` in `neural-interface/server.js` keyed dedup on exact command-string match, but `hookCommandString()` produces two valid forms for the same script depending on `targetProjectPath`: relative `node hooks/claude-code/<script>` for the SynaBun project itself, absolute `node "/abs/path/<script>"` otherwise. Different install flows (global cascade vs per-project) wrote different forms to the same `<project>/.claude/settings.json`, both passed the exact-string check, and accumulated. Symptom: every Stop-hook block reason was emitted twice (the user saw two near-identical "SynaBun Loop: Before this iteration ends..." Stop-hook errors per turn), and every recall-context block injected into `UserPromptSubmit` showed up twice. The two invocations also raced on the `pending-remember`/loop flag retry counters, so `MAX_RETRIES` could be hit in a single turn.
|
|
79
|
+
- **Variant-aware dedup with self-healing sweep** — New `hookCommandVariants(scriptName, targetProjectPath)` returns the set of every valid command-string form for a given script (relative + absolute when the target is `PACKAGE_ROOT`, absolute only otherwise). `addHookToSettings()` now walks `settings.hooks[def.event]` BEFORE pushing and removes any pre-existing entries whose command is in that set, keeping at most the first canonical match. Idempotent on re-install.
|
|
80
|
+
- **Startup self-heal across global + every registered project** — New `dedupeAllSettingsHooks()` runs in the existing startup block (`server.js`, just before the orphaned-loop cleanup). Walks `~/.claude/settings.json` and every `<projectPath>/.claude/settings.json` in the registered-projects list, applies the same variant-aware sweep across every event in `HOOK_SCRIPTS`, rewrites only when something changed. Logs `Hooks: deduped N stray entr[y/ies] across M settings file(s)` so the repair is visible. Cleans up existing damage from prior versions of the install path without user action.
|
|
81
|
+
|
|
82
|
+
### Added — Session Monitor Loops Tab + Title-Bar Stale-Loop Alert
|
|
83
|
+
- **New `Loops` tab in Session Monitor** — `neural-interface/public/shared/ui-sessions.js` adds a fourth tab between `Agents` and `Health` with `ICON_LOOP` glyph. Module-level `loops`/`staleLoopCount` state. Tab badge turns warning-orange when `staleCount > 0`. `_activeTab` enum extended with `'loops'`.
|
|
84
|
+
- **Per-loop card with state classification and live stalled timer** — `renderLoopCard()` shows a `Running` / `Finished` / `Orphaned` / `Stale` badge (driven by `loopStateClass()`/`loopStateLabel()`), a `${current}/${total}` iteration counter, a live `Stalled <Xm Ys>` timer (`now - lastIterationAt`, gray under 5min, red `.sm-loop-stalled-hot` at ≥5min), task preview, started-at relative time, terminal short-id with alive/dead pulse, and a per-loop `Stop` button that calls `POST /api/loop/stop` with that `terminalSessionId`. The stalled cell carries `data-stalled-ms`/`data-fetched-at` so the 1s `_stalledTickerTimer` can re-write `formatStalled(...)` without a server round-trip. Ticker only runs while `_activeTab === 'loops'` (`startStalledTicker()`/`stopStalledTicker()`).
|
|
85
|
+
- **Bulk action bar** — `Clear stale` (calls new `POST /api/loop/cleanup`, deletes only `active === false` files; disabled when zero stale) and `Clear all` (calls existing `POST /api/loop/stop` with empty body to kill PTY + close browser tab + delete file for every loop, with a `confirm()` prompt because it kills running work).
|
|
86
|
+
- **Title-bar alert button next to the cost value** — `neural-interface/public/shared/html-shell.js` adds `#titlebar-loops-alert-btn` immediately before `#titlebar-cost-btn` in the `bar-right` group, hidden by default. `neural-interface/public/shared/ui-navbar.js` imports the new `openLoopsTab` / `fetchLoopsForBadge` exports, wires click → `openLoopsTab()`, subscribes to the new `loops:updated` event (toggles `hidden` and writes `#titlebar-loops-count`), and bootstraps via `fetchLoopsForBadge()` plus a 30s polling fallback so the badge stays accurate even when the Session Monitor was never opened (the WS only connects when the panel opens).
|
|
87
|
+
- **Backend endpoints** — `GET /api/loop/list` returns the full inventory of `data/loop/*.json` (active + inactive) with classified `state`, `stalledMs`, `terminalAlive`, plus a `staleCount` field. `POST /api/loop/cleanup` deletes only `active === false` files (no PTY/browser side-effects since they're leftovers); corrupt JSON is treated as stale and removed too. Both endpoints filter out `pending-*.json` and `active-pointer.json` from the scan so the loop pointer file isn't classified as a loop.
|
|
88
|
+
- **`session:loops` WebSocket broadcast** — New `scanLoopInventory()` helper shared by the GET endpoint and a periodic polling loop that broadcasts on hash change (drops minute counters from the hash so it doesn't churn every tick). `handleSessionMonitorWebSocket()`'s `session:init` payload now carries `loops` + `staleCount` alongside `sessions`/`leaks`. `ui-sessions.js` `handleWsMessage()` updates state and re-renders on `session:loops`, plus emits `loops:updated` so the title-bar badge reflects WS pushes immediately when the panel is open.
|
|
89
|
+
- **CSS** — `neural-interface/public/shared/styles.css` adds `.sm-loop-running/-finished/-orphan/-stale` card borders, `.sm-loop-badge` color variants, `.sm-loop-iter` mono pill, `.sm-loop-stalled[-hot]` color states, `.sm-loops-actionbar` sticky-bottom action bar, and `.bar-loops-alert-btn` orange icon styling with a red `count-badge` overlay matching the existing `titlebar-trash-btn` pattern.
|
|
90
|
+
|
|
91
|
+
### Fixed — OpenCode Sidepanel Tables Not Rendering (Markdown Pipeline)
|
|
92
|
+
- **Root cause: missing CSS, not parser** — `marked@14` in `neural-interface/public/shared/ocp/ocp-render.js` correctly produced GFM `<table>/<thead>/<tbody>/<tr>/<th>/<td>` HTML (`gfm: true` set, `<table>` whitelisted in `renderAssistantMarkdown()`'s `hasContent` check), but `neural-interface/public/shared/ocp/ocp-styles.js` had ZERO rules targeting any table element. Default browser tables on the panel's translucent dark background produced no visible borders and dark-on-dark text — effectively invisible. The Claude sidepanel (`ui-claude-panel.js:677-687`) already had table styles, which is why tables rendered correctly there but not in OpenCode.
|
|
93
|
+
- **Markdown tables CSS block** — Added `.ocp-msg-assistant .ocp-table-wrapper` (`overflow-x: auto`, rounded border, dark bg), `table` (`border-collapse`, monospace 11px, `min-width: 100%`), `thead` background, `th` (bold, padded, bottom border, `white-space: nowrap`), `td` (padded, subtle row separators, `vertical-align: top`), `tbody tr:hover td` highlight, column dividers between adjacent cells, and `th[align]/td[align]` overrides honoring GFM column alignment.
|
|
94
|
+
- **`wrapTables()` post-processor** — New helper in `ocp-render.js` wraps every unwrapped `<table>` in `<div class="ocp-table-wrapper">` so wide tables get horizontal scroll without breaking `border-collapse` (you can't put `display: block; overflow-x: auto` directly on the table element). Idempotent via the `parent.classList.contains('ocp-table-wrapper')` guard. Called from `postProcessRenderedHtml()` so initial render, every streaming frame, finalize, and history-replay paths all wrap consistently.
|
|
95
|
+
|
|
96
|
+
### Changed — OpenCode Sidepanel Markdown Polish (Headings, HR, Task Lists)
|
|
97
|
+
- **Heading sizing fill-in** — Added explicit `h1` (15px), `h2` (13.5px), and `h4`/`h5`/`h6` (11.5px, semibold, brighter text) rules in `neural-interface/public/shared/ocp/ocp-styles.js`. Previously only `h3` had a size override; the others fell back to browser defaults that didn't match the panel scale.
|
|
98
|
+
- **HR gradient line** — `<hr>` now renders as a 1px linear-gradient line (transparent → 10%-white at 30-70% → transparent) so the rare horizontal-rule output looks intentional instead of a stark default browser bar. Note: `renderAssistantMarkdown` still strips `<hr>` to clean up CLI noise; the styling is in place if that filter is later removed.
|
|
99
|
+
- **GFM task list checkboxes** — Added `accent-color` and indent rules so `[ ]` / `[x]` checkboxes inherit the panel's off-white accent and render without the default disc bullet, using `:has(> li > input[type="checkbox"])` to scope the tweak to actual task lists only.
|
|
100
|
+
|
|
101
|
+
### Added — MCP Profile Change Sync for Codex/OpenCode Sidepanels
|
|
102
|
+
- **Live profile-change broadcast** — `neural-interface/server.js` now normalizes MCP profile names, broadcasts `mcp:profile-changed` over `/ws/sync`, and includes current `presets` plus `synced` results whenever `applyMcpProfile()` changes the active profile.
|
|
103
|
+
- **File-driven profile watcher** — New `startMcpProfileWatchers()` watches runtime `active-profile.json` locations, debounces writes from `mcp__SynaBun__profile(action=set)`, and routes external changes through `applyMcpProfile(..., 'file')` so all profile files and CLI env configs stay aligned.
|
|
104
|
+
- **Sidepanel profile listeners** — `neural-interface/public/shared/ui-sync.js` forwards `mcp:profile-changed` to the shared state bus, and `cdx-panel.js` / `ocp-panel.js` now use `applyMcpProfileState()` to rebuild dropdown items and selected labels immediately when the AI changes the profile.
|
|
105
|
+
|
|
106
|
+
### Changed — MCP Registry Profile Activation Consistency
|
|
107
|
+
- **Registry activation uses the shared profile path** — `/api/mcp/registry/activate` now calls `applyMcpProfile(profileValue, 'registry')` instead of writing only `DATA_HOME/data/active-profile.json`, so settings-driven activation updates every runtime profile file, Codex/OpenCode env sync, and the live sidepanel broadcast through the same code path.
|
|
108
|
+
|
|
109
|
+
## 2026-04-29
|
|
110
|
+
|
|
111
|
+
### Added — Google Search Console MCP Tool Group (30 tools)
|
|
112
|
+
- **Full GSC control via browser automation** — New `gsc` tool group registered in `mcp-server/src/tools/gsc.ts` (barrel) plus `mcp-server/src/tools/gsc/{helpers,navigate,inspect,performance,indexing,experience,settings}.ts` (~1300 LOC). 30 tools cover every section of Google Search Console without an API key, using the user's authenticated Google session in the SynaBun-managed Playwright browser.
|
|
113
|
+
- **Tools shipped** —
|
|
114
|
+
- **Navigation & property:** `gsc_navigate` (33 page enums incl. all 17 enhancement reports), `gsc_property` (list/select/current/add).
|
|
115
|
+
- **URL Inspection:** `gsc_inspect_url` (polls up to 30s, returns structured JSON of indexingState/canonicals/lastCrawl/sitemap/robots/enhancements), `gsc_inspect_test_live` (90s poll for live test), `gsc_inspect_request_indexing` (mutating), `gsc_inspect_view_crawled` (html/screenshot/http_response/more_info tabs).
|
|
116
|
+
- **Performance:** `gsc_performance_query` (searchType, dateRange preset/custom, dimension, filter chips, comparison, auto-scroll grid, returns totals + table JSON), `gsc_performance_export` (CSV/Excel/Sheets), `gsc_performance_chart_screenshot`.
|
|
117
|
+
- **Indexing:** `gsc_pages_report` (totals + reasons + drill-down), `gsc_pages_validate_fix` (mutating), `gsc_videos_report`, `gsc_sitemap` (list/submit/delete/view_errors), `gsc_removals` (list_temporary/list_outdated/list_safesearch/new), `gsc_removals_cancel`.
|
|
118
|
+
- **Experience & links:** `gsc_cwv_report`, `gsc_https_report`, `gsc_security_issues`, `gsc_manual_actions` (incl. reconsideration submit), `gsc_enhancements` (any of 17 structured-data reports), `gsc_links_report` (4 sections, auto-pagination), `gsc_links_export`.
|
|
119
|
+
- **Settings & meta:** `gsc_settings`, `gsc_crawl_stats`, `gsc_users` (list/add/remove/change_role), `gsc_associations` (Analytics/Merchant/Ads/Play/YouTube/Actions/Chrome Web Store), `gsc_disavow` (download/upload/delete, mutating), `gsc_shopping`, `gsc_extract_table` (generic grid → JSON), `gsc_screenshot`.
|
|
120
|
+
- **Locale tolerance** — All selectors prefer stable URL paths (`/inspect`, `/performance/search-analytics`, `/index/pages`, `/sitemaps`, `/removals`, `/links`, `/core-web-vitals/MOBILE`, etc.), material-icon ligature text (`home`, `trending_up`, `search`, `account_tree`), and aria roles. Translated UI strings are matched only as fallback regex with both English and Portuguese variants. Verified against the existing logged-in `sc-domain:synabun.ai` session running in Portuguese.
|
|
121
|
+
- **Polling helper** — `pollFor()` in `mcp-server/src/tools/gsc/helpers.ts` evaluates a JS predicate every 400ms (configurable) up to a timeout, returning the first truthy result. Used by every async-loading panel (URL Inspection result render, live-test completion, request-indexing modal, sitemap submission status, validate-fix confirmation, removal submission). Default cap 30s, bumped to 90s for live URL tests.
|
|
122
|
+
- **Generic table extractor** — `TABLE_EXTRACTOR_SCRIPT(scope)` walks `[role="grid"]` / `[role="table"]` / `<table>`, emits `{ headers, rows }` with header→cell mapping plus per-row `url` when an anchor is present. Backbone of every report tool.
|
|
123
|
+
- **Auth check** — `ensureAuth()` returns `'Not authenticated. Sign into the Google account...'` when the page redirects to `accounts.google.com`, instead of failing on missing selectors.
|
|
124
|
+
- **Profile presets** — Added `gsc` to `full` and `browser` presets and added a dedicated `gsc` preset (`git, image, browser, gsc`) in `mcp-server/src/services/profiles.ts`. `VALID_GROUPS` extended with `'gsc'`.
|
|
125
|
+
- **Index wiring** — `mcp-server/src/index.ts` imports `registerGscTools`, registers the group via `setToolGroup('gsc', ...)`, exposes the group line in `TOOL_GROUP_INSTRUCTIONS.gsc`, and tracks usage via the `gsc_` prefix in `groupToolPrefixes`.
|
|
126
|
+
- **Documentation** — Updated `README.md` (architecture box, MCP-tools heading and total `76 → 106`, added a full GSC section grouping all 30 tools by area, plus a tip about authentication and polling timeouts) and `CLAUDE.md` (architecture line bumped from `72 tools / 9 groups` to `106 / 12` with the locale-tolerance note).
|
|
127
|
+
|
|
128
|
+
## 2026-04-28
|
|
129
|
+
|
|
130
|
+
### Added — OpenCode Model Capability Explainers
|
|
131
|
+
- **Capability metadata and inline help** — Extended `CAP_BADGES` in `neural-interface/public/shared/ocp/ocp-tabs.js` with abbreviations and human-readable descriptions for `Reason`, `Tools`, `Vision`, `Audio`, `Video`, `PDF`, `ImgGen`, `TTS`, and `Files`. `populateModelDropdown()` now renders an inline `ocp-cap-help` strip that updates on capability chip hover/focus and active filter state, so users can understand each model capability without a floating tooltip covering the controls.
|
|
132
|
+
|
|
133
|
+
### Fixed — OpenCode Model Selector Filtering and Tooltip Overlap
|
|
134
|
+
- **Capability filters no longer stack additively** — Replaced the previous `activeCaps` `Set` / AND matching logic in `populateModelDropdown()` with a single `activeCapKey`, so selecting a capability replaces the previous filter and clicking the active capability clears it. Added an `All` chip and `ocp-dd-empty` clear action for no-result states.
|
|
135
|
+
- **Pill explanations no longer overlap the filter row** — Removed `data-tooltip` overlays from capability filter pills and moved their explanations into the dropdown header via `showCapHelp()`, `restoreCapHelp()`, and `bindCapHelp()`. Model-row capability badges still keep compact `data-tooltip` explanations, positioned above the badge instead of over the pill bar.
|
|
136
|
+
|
|
137
|
+
### Changed — OpenCode Model Selector Visual Refresh
|
|
138
|
+
- **Neutral OpenCode-style selector surface** — Restyled `#ocp-model-dd .ocp-dd-menu` in `neural-interface/public/shared/ocp/ocp-styles.js` with a darker glassy surface, restrained monochrome capability chips/badges, neutral favorite states, a wider 520px model menu, and grid-based model rows for model name, capabilities, and favorite star.
|
|
139
|
+
- **Subtle selector motion** — Added `ocp-model-menu-in` and `ocp-cap-help-in` animations, plus lightweight hover/active transitions for capability chips and model rows, making the selector feel fresher without the earlier rainbow-heavy visual treatment.
|
|
140
|
+
|
|
141
|
+
### Fixed — SynaBun MCP Auto-Heal After Claude Code Updates
|
|
142
|
+
- **MCP entry survives CC updates** — Claude Code updates (especially when the installer runs as root via `sudo npm i -g @anthropic-ai/claude-code`) could wipe `~/.claude.json` or strip `mcpServers.SynaBun`, leaving the side panel and CLI with SynaBun MCP disabled until the user manually re-registered it via the Neural Interface setup window. Added `healSynaBunMcp()` to `hooks/claude-code/session-start.mjs` that runs on every session start: reads `~/.claude.json`, ensures `mcpServers.SynaBun = { type: 'http', url: 'http://localhost:3344/mcp' }`, writes back only if missing or incorrect (idempotent on already-correct state). Heal events are logged to `data/compact-debug.log` with the `MCP-HEAL` prefix. Wrapped in `try/catch` — non-critical failures cannot block session start.
|
|
143
|
+
|
|
144
|
+
### Changed — OpenCode Capability Help Strip Polish
|
|
145
|
+
- **No layout jump on capability pill hover** — `.ocp-cap-help` strip in `neural-interface/public/shared/ocp/ocp-tabs.js` now stays mounted at all times, falling back to the `ALL_CAP_HELP` ("All — Showing every visible model.") badge when no pill is hovered and no filter is active. Replaced the prior `capHelp.hidden = true/false` toggling — which caused model rows below to shift on every mouseenter/leave — with a persistent strip and `applyCapHelp()` helper that tracks `lastHelpKey` to suppress redundant animation restarts.
|
|
146
|
+
- **Smooth filter-toggle collapse** — Filter-toggle button (`.ocp-cap-filter-toggle`) now toggles `.ocp-cap-help--collapsed` on the strip instead of hiding it abruptly. CSS in `neural-interface/public/shared/ocp/ocp-styles.js` animates `max-height`, `opacity`, `margin`, `padding`, and `border-width` over 220ms (cubic-bezier 0.2/0.8/0.2/1) for a smooth open/close, and `restoreCapHelp()` only fires on expand.
|
|
147
|
+
- **Crossfade-feel on badge text swap** — Added `@keyframes ocp-cap-help-swap` (opacity 0.45 → 1, 1px translateY, 0.4px blur clearing) and `.ocp-cap-help--swap` class. `applyCapHelp()` removes the class, forces reflow via `void capHelp.offsetWidth`, then re-adds it whenever `lastHelpKey` changes, producing a 180ms ease-out fade between badge descriptions instead of an abrupt text flip.
|
|
148
|
+
- **Description text wraps to two lines** — `.ocp-cap-help-text` switched from `white-space: nowrap` + ellipsis to `-webkit-line-clamp: 2` + `-webkit-box-orient: vertical` + `white-space: normal`. Long descriptions like "Stronger at multi-step reasoning, planning, and hard problem solving." now wrap fully within the strip's 32-80px height range instead of truncating with an ellipsis.
|
|
149
|
+
- **Active filter highlight** — Added `.ocp-cap-help--active` modifier that brightens the strip's border and gradient background when a real capability filter (not "All") is active, giving visual continuity between the pill and the explanation.
|
|
150
|
+
|
|
151
|
+
### Fixed — SynaBun Loop "No active loop" After /clear (Claude Code Hook-Driven Loop)
|
|
152
|
+
- **Stop hook stuck blocking with `call \`loop\` action \`update\``** — `mcp-server/src/tools/loop.ts` `resolveSessionId` returned `process.env.CLAUDE_SESSION_ID` without verifying a flag file existed. Under HTTP MCP transport (stateless, no per-call env) and after `/clear` (env stale), the lookup returned null → `loop update` reported `"No active loop"` → `hooks/claude-code/stop.mjs:489-503` re-blocked 3× per iteration with the journal-update nag, then advanced iterations with a stale journal.
|
|
153
|
+
- **`resolveSessionId` now verifies before trusting env** — added `isFreshActive()` (45-min staleness rule). New strategy chain: explicit param → verified `CLAUDE_SESSION_ID` (flag must exist + be active) → `SYNABUN_TERMINAL_SESSION` scan → `active-pointer.json` fallback → legacy unclaimed loops → exactly-one-fresh-active fallback.
|
|
154
|
+
- **Hook writes `data/loop/active-pointer.json` on every claim** — `writeActivePointer()` / `clearActivePointer()` in `hooks/claude-code/stop.mjs` persist `{sessionId, terminalSessionId, claudeSessionId, updatedAt}` on exact-match, Strategy A, and Strategy B claim paths. Cleared on iteration cap. Bridges env gap between hook (live Claude env) and HTTP MCP daemon (stale env).
|
|
155
|
+
- **`cleanupStaleLoops` aware of pointer file** — `hooks/claude-code/shared.mjs` skips `active-pointer.json` from the `.json` flag scan (different schema, would have been wrongly deleted as inactive) and drops the pointer when its target flag no longer exists.
|
|
156
|
+
- **MCP daemon rebuild** — recompiled `mcp-server/dist/tools/loop.js` via direct `node node_modules/typescript/bin/tsc` (the `node_modules/.bin/tsc` shim was broken).
|
|
157
|
+
|
|
158
|
+
### Fixed — OpenCode Sidepanel "Edit Plan" Empty Content
|
|
159
|
+
- **Robust click handler with multi-layer fallback chain** — Replaced silent-fail `setOnEditPlan` callback in `neural-interface/public/shared/ocp/ocp-panel.js` (previously bailed on `if (!tab.planContent) return`) with a handler mirroring `ui-claude-panel.js`. Chain: `tab.planFilePath` → `extractPlanTextLoose(tab)` → `requestAnimationFrame` retry → `fetchPlanFromMessagesAPI()` (WS `messages:list` proxy) → `/api/latest-plan` disk fallback. Added `setBusy()` opacity/pointer-events on the post-plan card while async work is in flight, `noFile()` rendering a visible `renderErrorMessage` instead of silently bailing, and `materialize()` posting to `/api/create-plan`.
|
|
160
|
+
- **Card element threaded through callback** — `renderPostPlanCard()` in `neural-interface/public/shared/ocp/ocp-render.js` now passes the card DOM node to `onEditPlan(card)`; `showPostPlanUI()` in `ocp-tabs.js` forwards as `_onEditPlan(tab, card)` so the click handler can disable/restore the card during async failures.
|
|
161
|
+
- **`extractPlanTextLoose(tab)` four-layer scan** — New exported helper in `ocp-tabs.js` ignores the `text.length > 80` and `tab.mode !== 'plan'` guards from the original `capturePlanContent`. Order: cached `tab.planContent` → ExitPlanMode tool card args (parses `.ocp-tool-args` JSON for `input.plan/.markdown/.content/.text`, regex-matched by `/exit\s*plan|plan[\s_-]?exit/i` on `.ocp-tool-name`) → last `.ocp-msg-assistant` text bubble → standalone `.ocp-think-block .ocp-think-content` (reasoning-only output) → Agent/task subagent `.ocp-tool-result`. Last-resort fallback to any short bubble.
|
|
162
|
+
- **ExitPlanMode tool input captured at `tool.start`** — `tool.start` handler in `ocp-tabs.js` reads `toolInput.plan || .markdown || .content || .text` and stores it on `tab.planContent` before `showPostPlanUI()`. Falls back to `capturePlanContent()` if input fields empty. The plan text lives in tool args, not the assistant message bubble.
|
|
163
|
+
- **Live capture during `message.part.updated`** — Both `text` and `reasoning|thinking|thought` part types now write to `tab.planContent` (length-monotonic) when `tab.mode === 'plan'`, so post-plan extraction has authoritative content even when the bubble rendered into a Thought block. Handles plan agents that emit all output as reasoning.
|
|
164
|
+
- **`fetchPlanFromMessagesAPI()` server-side fallback** — In `ocp-panel.js` `setOnEditPlan`, calls `requestWs('messages:list', { sessionId }, 15000)` (existing WS proxy at `server.js:5285` → `GET /session/{id}/message`). Walks the most recent assistant message, concatenates `type === 'text'` parts then `reason|think` parts (length > 40 threshold). Bypasses DOM scraping entirely.
|
|
165
|
+
- **`tab._planModeStartedAt` timestamp for disk fallback** — `setTabMode()` stamps `Date.now()` when entering plan mode. `diskFallback()` validates `/api/latest-plan` mtime against plan-mode start (skipped when unset to support pre-existing tabs).
|
|
166
|
+
|
|
167
|
+
### Fixed — OpenCode "Request message:send timed out" False Toast
|
|
168
|
+
- **WS request timeout bumped 6 min → 30 min** — `requestWs('message:send', ...)` in `neural-interface/public/shared/ocp/ocp-tabs.js` `sendMessage` was rejecting at 360000ms; Plan agent + subagent runs routinely exceed that. Bumped to 1800000ms.
|
|
169
|
+
- **SSE liveness suppresses red error** — `handleSSEEvent()` stamps `tab._lastEventAt = Date.now()` on every event. `sendMessage` catch block detects `/^Request\s+message:send\s+timed out$/i` and, when `Date.now() - tab._lastEventAt < 60000`, suppresses `renderErrorMessage` AND skips the `tab.running = false` / `endTurn` cleanup so SSE can drive completion to `session.idle` naturally.
|
|
170
|
+
|
|
171
|
+
### Changed — OpenCode Plan Mode SDK-Aligned Tool Restrictions
|
|
172
|
+
- **`body.mode` + per-message `body.tools` restrictions** — `sendMessage` in `neural-interface/public/shared/ocp/ocp-tabs.js` now sets `body.mode = tab.mode` (canonical SDK field per OpenCode `app.modes()`) and, when `tab.mode === 'plan'`, pins `body.tools = { write: false, edit: false, patch: false, bash: false }`. Belt-and-suspenders enforcement so the plan agent cannot even attempt mutating tools.
|
|
173
|
+
|
|
174
|
+
### Changed — OpenCode Plan-Mode Tool Denials Soft-Fail
|
|
175
|
+
- **Permission denials render as neutral "Skipped" tile** — `tool.result` handler in `neural-interface/public/shared/ocp/ocp-tabs.js` detects plan-mode permission errors via `/permission|not allowed|forbidden|read.?only|disabled|disallow|file_edit|edit not|write not|patch not|bash not/i`, renders the tool card with status `complete` and message "Skipped — plan mode is read-only" instead of the red error badge.
|
|
176
|
+
|
|
177
|
+
### Fixed — Restart Click Failed After Native-Module Crash During Shutdown
|
|
178
|
+
- **`Restart Server` from apps dropdown left no server running** — `/api/server/restart` in `neural-interface/server.js:1631` exited with sentinel code `75` so the `setup.js` supervisor would respawn. Native module teardown (sqlite / Playwright / node-pty) crashed mid-exit with `mutex lock failed: Invalid argument` (libc++abi → SIGABRT), clobbering the sentinel exit code. The supervisor's `child.on('exit')` in `setup.js:264` saw an abnormal non-75 code, hit the `fail()` + `process.exit(code)` branch, and died — no respawn, shell prompt returned.
|
|
179
|
+
- **Marker-file fallback makes restart resilient to crash codes** — `/api/server/restart` now writes `DATA_HOME/restart-requested` via `writeFileSync(RESTART_MARKER, ...)` before deferring shutdown. `setup.js` `child.on('exit')` treats marker presence the same as exit code `75` and respawns regardless of the actual code, then deletes the marker. Added `unlinkSync(RESTART_MARKER)` at the top of `startServer()` to clear stale markers on supervisor startup so a leftover from an unsupervised crash cannot trigger a spurious respawn. The crash-rate limiter (`5` restarts in `60s`) still applies so any crash loop stays bounded.
|
|
180
|
+
|
|
3
181
|
## 2026-04-27
|
|
4
182
|
|
|
5
|
-
### Fixed —
|
|
183
|
+
### Fixed — OpenCode Sidepanel Plan Mode Subagent Stall
|
|
184
|
+
- **Plan mode appeared frozen during `task` subagent runs** — When the OpenCode plan agent dispatched a `task` subagent (e.g. `@explore`), the panel showed no progress for 60-150+ seconds while the subagent worked, leading users to abort. Root cause: `tab.toolActivityChildSessions` (parent task entry → child session id map) was only populated by `recordToolResult()` after the subagent completed, so all child SSE events (`tool.start`, `tool.result`, `message.part.updated`, `session.idle`) failed `findAgentEntryByChildSession()` lookup at `ocp-tabs.js:1570` and were dropped at line 1577. Fix in `neural-interface/public/shared/ocp/ocp-tabs.js`:
|
|
185
|
+
- **Top-level `session.created` interceptor** in `handleSSEEvent()` — when `child.parentID` matches a tab's session id, links the first running `task`/`agent` activity entry without `childSessionId` via `linkChildSession()`.
|
|
186
|
+
- **Fallback link in `case 'message.part.updated'`** task-tool branch — reads `part.state.metadata.sessionId` when `session.created` arrived late.
|
|
187
|
+
- **Fallback link in `case 'tool.start'`** agent-tool branch — reads `event.metadata.sessionId` / `event.state.metadata.sessionId`.
|
|
188
|
+
- Once linked, child events route via `findAgentEntryByChildSession` → `recordAgentChildEvent` populates `entry.steps` → `syncAgentChildIndicator` updates thinking indicator with live child tool names instead of stuck "Thinking…".
|
|
6
189
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- **Added `injectOpenCodeMemoryPerms()`** — sets `permission.tools[<key>] = "allow"` for each memory key in `~/.config/opencode/config.json` (or `XDG_CONFIG_HOME`). Skips keys already set.
|
|
13
|
-
- **Wired into 4 MCP-enable handlers** — `POST /api/claude-code/mcp` now calls `injectClaudeMemoryPerms()` (replaces the old broken `ensureSynaBunPermissions()` block); `POST /api/setup/codex/mcp` calls `injectCodexMemoryPerms()` after the toml write; `POST /api/setup/gemini/mcp` adds `trust: true` to the SynaBun MCP entry (Gemini's server-level trust flag skips per-tool prompts); `POST /api/opencode/mcp` calls `injectOpenCodeMemoryPerms()` after `persistMcpToOpencodeConfig()`.
|
|
14
|
-
- **DELETE handlers untouched** — disabling SynaBun MCP leaves permission entries in place (orphaned but harmless; same key re-added on re-enable).
|
|
190
|
+
### Fixed — Sidepanel Plan Mode "No File Created" (Claude Code)
|
|
191
|
+
- **`ExitPlanMode.input.plan` ignored by post-plan hook** — `hooks/claude-code/post-plan.mjs` previously stored only existing plan files matched against stringified `tool_response`, so when the UI failed to capture, no file ever reached SQLite or disk. Hook now reads the authoritative markdown via `readPlanField(input.tool_input)` / `readPlanField(input.tool_response)`. When no match exists (or the matched file is stale vs `tool_input.plan`), the hook authors `data/plans/YYYY-MM-DD/<slug>.md` itself using `extractPlanTitle()` + `slugify()` + `uniqueSlugPath()` before embedding and storing. Debug log tags the new path as `method: 'authored-from-tool_input'`.
|
|
192
|
+
- **Inverted priority in `extractPlanTextFromMessage()`** — `neural-interface/public/shared/ui-claude-panel.js:5414` previously preferred any text block > 100 chars over `ExitPlanMode.input.plan`, so preamble narration was written to the plan file instead of the plan. Reordered to check `exitTool.input.plan` first, text fallback last.
|
|
193
|
+
- **Eager-capture chain captured DOM thinking text** — three `renderAssistant()` callsites at lines 4968, 5048, 5123 used `direct || extractPlanTextFromMessage(msg) || extractPlanText(tab)`, so when `ExitPlanMode.input.plan` was empty the chain fell through to DOM walks that grabbed `Thought›` thinking renders. Tightened to `const captured = direct;` — eager capture now only fires on authoritative plan input; the post-plan hook is the rescue when direct is empty.
|
|
194
|
+
- **`/api/create-plan` accepted thinking-prefix garbage** — `neural-interface/server.js:11960` slugified the first non-empty line into filenames like `thought-the-user-wants-me-to-search-....md`. Added 422 rejection when content has no H1 AND first line matches `/^(thought\b|›|thinking[:\s]|let me\b|i'?ll\b|i will\b|the user wants? me\b)/i`, preventing garbage plan files from accumulating.
|
package/README.md
CHANGED
|
@@ -203,7 +203,7 @@ No environment variables needed — SQLite and local embeddings work out of the
|
|
|
203
203
|
|
|
204
204
|
### 3. Verify
|
|
205
205
|
|
|
206
|
-
Restart Claude Code, then run `/mcp`. You should see the `SynaBun` server with
|
|
206
|
+
Restart Claude Code, then run `/mcp`. You should see the `SynaBun` server with 106 tools listed.
|
|
207
207
|
|
|
208
208
|
</details>
|
|
209
209
|
|
|
@@ -232,13 +232,13 @@ AI Assistant (any project)
|
|
|
232
232
|
┌───┴──────────────────────────┐
|
|
233
233
|
│ SynaBun MCP Server │ Node.js 22.5+ / TypeScript
|
|
234
234
|
│ │
|
|
235
|
-
│
|
|
235
|
+
│ 106 tools across 12 groups: │
|
|
236
236
|
│ Memory (8), Profile (1), │
|
|
237
237
|
│ Browser (40), Whiteboard (5)│
|
|
238
238
|
│ Cards (5), Discord (8), │
|
|
239
|
-
│ Leonardo (5),
|
|
240
|
-
│
|
|
241
|
-
│ Image (1)
|
|
239
|
+
│ Leonardo (5), GSC (30), │
|
|
240
|
+
│ Git (1), Loop (1), │
|
|
241
|
+
│ TicTacToe (1), Image (1) │
|
|
242
242
|
│ │
|
|
243
243
|
│ Transformers.js │ Local embeddings
|
|
244
244
|
│ all-MiniLM-L6-v2 │ (384 dims, ~23MB model)
|
|
@@ -253,9 +253,9 @@ AI Assistant (any project)
|
|
|
253
253
|
|
|
254
254
|
Everything runs in a single Node.js process — no external services, no Docker, no API keys.
|
|
255
255
|
|
|
256
|
-
## MCP Tools (
|
|
256
|
+
## MCP Tools (106)
|
|
257
257
|
|
|
258
|
-
SynaBun exposes
|
|
258
|
+
SynaBun exposes 106 tools via the Model Context Protocol, organized into 12 groups:
|
|
259
259
|
|
|
260
260
|
### Memory (8 tools)
|
|
261
261
|
|
|
@@ -341,6 +341,66 @@ Browser-based AI image and video generation via [Leonardo.ai](https://app.leonar
|
|
|
341
341
|
|
|
342
342
|
> **Tip:** Use the `/leonardo` skill in Claude Code or Codex for an expert-guided creation experience with a 7-phase video prompter, 6-phase image prompter, model advisor with decision matrices for 30+ models, curated prompt library, and style guide with motion controls and camera combos.
|
|
343
343
|
|
|
344
|
+
### Google Search Console (30 tools)
|
|
345
|
+
|
|
346
|
+
Browser-based control of [Google Search Console](https://search.google.com/search-console). No API key — uses the user's authenticated Google session in the SynaBun browser. Locale-tolerant (selectors keyed to URL paths, material-icon ligatures, and aria roles instead of UI strings).
|
|
347
|
+
|
|
348
|
+
**Navigation & property**
|
|
349
|
+
| Tool | Purpose |
|
|
350
|
+
|------|---------|
|
|
351
|
+
| `gsc_navigate` | Open any GSC section (overview, performance_search, performance_discover, performance_news, inspect, pages, videos, sitemaps, removals, cwv_mobile, cwv_desktop, https, security, manual_actions, links, achievements, settings, crawl_stats, users, change_address, associations, disavow, shopping, merchant, plus 17 enhancement reports). Preserves active property. |
|
|
352
|
+
| `gsc_property` | List, select, read, or add a property (domain or URL-prefix). |
|
|
353
|
+
|
|
354
|
+
**URL Inspection**
|
|
355
|
+
| Tool | Purpose |
|
|
356
|
+
|------|---------|
|
|
357
|
+
| `gsc_inspect_url` | Inspect a URL, polls up to 30s for the result panel, returns structured JSON (indexingState, canonicals, lastCrawl, sitemap, robots, enhancements, etc.). |
|
|
358
|
+
| `gsc_inspect_test_live` | Click "Test live URL" and poll up to 90s for live test results. |
|
|
359
|
+
| `gsc_inspect_request_indexing` | [mutating] Submit URL to Google's indexing queue. |
|
|
360
|
+
| `gsc_inspect_view_crawled` | Open the View Crawled Page panel — switch between html / screenshot / http_response / more_info tabs. |
|
|
361
|
+
|
|
362
|
+
**Performance**
|
|
363
|
+
| Tool | Purpose |
|
|
364
|
+
|------|---------|
|
|
365
|
+
| `gsc_performance_query` | Query a Performance report — searchType, dateRange (preset or custom), dimension (query/page/country/device/searchAppearance/date), filter chips, comparison toggle. Auto-scrolls grid up to `limit` rows and returns totals + table JSON. |
|
|
366
|
+
| `gsc_performance_export` | Trigger the report Export menu (CSV / Excel / Google Sheets). |
|
|
367
|
+
| `gsc_performance_chart_screenshot` | Screenshot the current performance chart. |
|
|
368
|
+
|
|
369
|
+
**Indexing**
|
|
370
|
+
| Tool | Purpose |
|
|
371
|
+
|------|---------|
|
|
372
|
+
| `gsc_pages_report` | Read the Pages (Coverage) report — totals, per-reason buckets, optional drill-down to URL examples. |
|
|
373
|
+
| `gsc_pages_validate_fix` | [mutating] Start a Validate Fix run for a chosen reason bucket. |
|
|
374
|
+
| `gsc_videos_report` | Read the Video Indexing report (same shape as `gsc_pages_report`). |
|
|
375
|
+
| `gsc_sitemap` | List, submit, delete, or view errors for sitemaps. |
|
|
376
|
+
| `gsc_removals` | List or create URL-removal requests (temporary 6-month, outdated content, SafeSearch). |
|
|
377
|
+
| `gsc_removals_cancel` | [mutating] Cancel a pending temporary removal. |
|
|
378
|
+
|
|
379
|
+
**Experience, Enhancements, Links**
|
|
380
|
+
| Tool | Purpose |
|
|
381
|
+
|------|---------|
|
|
382
|
+
| `gsc_cwv_report` | Core Web Vitals (mobile / desktop) — poor / needs-improvement / good URL counts and per-issue groups. |
|
|
383
|
+
| `gsc_https_report` | HTTPS coverage and non-HTTPS URL reasons. |
|
|
384
|
+
| `gsc_security_issues` | Read security issues; optionally request a review. |
|
|
385
|
+
| `gsc_manual_actions` | Read manual actions; optionally submit a reconsideration request with custom body text. |
|
|
386
|
+
| `gsc_enhancements` | Read any structured-data Enhancements report (breadcrumbs, faq, sitelinks, videos, products, recipes, review, events, jobposting, speakable, qa, logos, sitenames, dataset, practice, math, merchant). |
|
|
387
|
+
| `gsc_links_report` | Top linked pages / linking sites / linking text / internally linked. Auto-paginates. |
|
|
388
|
+
| `gsc_links_export` | Trigger Links report Export menu. |
|
|
389
|
+
|
|
390
|
+
**Settings & Management**
|
|
391
|
+
| Tool | Purpose |
|
|
392
|
+
|------|---------|
|
|
393
|
+
| `gsc_settings` | Read settings page (ownership, verification, users); change address. |
|
|
394
|
+
| `gsc_crawl_stats` | Crawl Stats — totals (requests, bytes, response time) plus host / response-code / file-type / Googlebot-type breakdowns. |
|
|
395
|
+
| `gsc_users` | List, add, remove, change-role for property users. |
|
|
396
|
+
| `gsc_associations` | List, add, remove associations (Analytics, Merchant Center, Ads, Play, YouTube, Actions, Chrome Web Store). |
|
|
397
|
+
| `gsc_disavow` | [mutating] Download / upload / delete the Disavow Links file. |
|
|
398
|
+
| `gsc_shopping` | Read the Shopping / Merchant Listings report. |
|
|
399
|
+
| `gsc_extract_table` | Generic GSC grid → JSON extractor when no specific tool covers a panel. |
|
|
400
|
+
| `gsc_screenshot` | Full-page screenshot of the active GSC tab. |
|
|
401
|
+
|
|
402
|
+
> **Auth:** GSC tools require an authenticated Google session in the SynaBun-managed browser. If a tool detects a redirect to `accounts.google.com`, it returns a clear error so you can sign in once and retry. URL Inspection polling: 30 s default, 90 s for live test (Google's own timeout). Mutating tools are clearly labelled `[mutating]` in their descriptions.
|
|
403
|
+
|
|
344
404
|
### Image (1 tool)
|
|
345
405
|
|
|
346
406
|
| Tool | Purpose |
|
|
@@ -476,17 +476,70 @@ async function main() {
|
|
|
476
476
|
// Clean up root PLAN.md if it was just created
|
|
477
477
|
cleanupRootPlanMd(input.cwd || '');
|
|
478
478
|
|
|
479
|
+
// Authoritative plan markdown lives in tool_input.plan (and is echoed in tool_response.plan).
|
|
480
|
+
// Pull it directly so we can author the file ourselves if the UI capture failed.
|
|
481
|
+
function readPlanField(obj) {
|
|
482
|
+
if (!obj) return '';
|
|
483
|
+
if (typeof obj === 'string') {
|
|
484
|
+
try {
|
|
485
|
+
const parsed = JSON.parse(obj);
|
|
486
|
+
if (parsed && typeof parsed.plan === 'string') return parsed.plan;
|
|
487
|
+
} catch { /* not JSON — fall through */ }
|
|
488
|
+
return '';
|
|
489
|
+
}
|
|
490
|
+
if (typeof obj === 'object' && typeof obj.plan === 'string') return obj.plan;
|
|
491
|
+
return '';
|
|
492
|
+
}
|
|
493
|
+
const authoritativePlan =
|
|
494
|
+
readPlanField(input.tool_input) ||
|
|
495
|
+
readPlanField(input.tool_response);
|
|
496
|
+
|
|
479
497
|
// Match plan file: content-match first (session-accurate), mtime fallback
|
|
480
498
|
const toolResponse = typeof input.tool_response === 'string'
|
|
481
499
|
? input.tool_response
|
|
482
500
|
: (input.tool_response != null ? JSON.stringify(input.tool_response) : '');
|
|
483
501
|
|
|
484
|
-
debugLog(`ExitPlanMode fired — tool_response length: ${toolResponse.length}, first 200 chars: ${toolResponse.slice(0, 200).replace(/\n/g, '\\n')}`);
|
|
502
|
+
debugLog(`ExitPlanMode fired — tool_response length: ${toolResponse.length}, authoritativePlan length: ${authoritativePlan.length}, first 200 chars: ${toolResponse.slice(0, 200).replace(/\n/g, '\\n')}`);
|
|
503
|
+
|
|
504
|
+
let planFile = findPlanByContent(authoritativePlan || toolResponse) || findLatestPlan();
|
|
505
|
+
|
|
506
|
+
// If a match was found but its content doesn't match the authoritative plan,
|
|
507
|
+
// it's stale. Drop it so we author a fresh file from tool_input.plan instead.
|
|
508
|
+
if (planFile && authoritativePlan) {
|
|
509
|
+
try {
|
|
510
|
+
const existing = readFileSync(planFile.path, 'utf-8').trim();
|
|
511
|
+
if (existing !== authoritativePlan.trim()) {
|
|
512
|
+
debugLog(`Match stale (file != tool_input.plan) — discarding ${planFile.path}, will author from tool_input`);
|
|
513
|
+
planFile = null;
|
|
514
|
+
}
|
|
515
|
+
} catch { /* unreadable — fall through to authoring */ planFile = null; }
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Author the plan file ourselves if no match and we have authoritative content.
|
|
519
|
+
if (!planFile && authoritativePlan) {
|
|
520
|
+
try {
|
|
521
|
+
const todayDir = getTodayPlanDir();
|
|
522
|
+
mkdirSync(todayDir, { recursive: true });
|
|
523
|
+
const title = extractPlanTitle(authoritativePlan);
|
|
524
|
+
const slug = slugify(title);
|
|
525
|
+
const targetPath = uniqueSlugPath(todayDir, slug);
|
|
526
|
+
writeFileSync(targetPath, authoritativePlan, 'utf-8');
|
|
527
|
+
const stat = statSync(targetPath);
|
|
528
|
+
planFile = {
|
|
529
|
+
name: targetPath.split(/[/\\]/).pop(),
|
|
530
|
+
path: targetPath,
|
|
531
|
+
mtime: stat.mtimeMs,
|
|
532
|
+
method: 'authored-from-tool_input',
|
|
533
|
+
};
|
|
534
|
+
debugLog(`Authored plan file from tool_input.plan: ${targetPath}`);
|
|
535
|
+
} catch (err) {
|
|
536
|
+
debugLog(`Failed to author plan file: ${err.message}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
485
539
|
|
|
486
|
-
const planFile = findPlanByContent(toolResponse) || findLatestPlan();
|
|
487
540
|
if (!planFile) {
|
|
488
541
|
process.stdout.write(JSON.stringify({
|
|
489
|
-
additionalContext: 'SynaBun: No
|
|
542
|
+
additionalContext: 'SynaBun: No plan file found and no plan content in tool_input — UI capture and hook authoring both failed.',
|
|
490
543
|
}));
|
|
491
544
|
return;
|
|
492
545
|
}
|
|
@@ -24,12 +24,36 @@
|
|
|
24
24
|
* }
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import { readFileSync, existsSync, unlinkSync, readdirSync, statSync, appendFileSync } from 'node:fs';
|
|
27
|
+
import { readFileSync, existsSync, unlinkSync, readdirSync, statSync, appendFileSync, writeFileSync } from 'node:fs';
|
|
28
28
|
import { execSync } from 'node:child_process';
|
|
29
29
|
import { dirname, join } from 'node:path';
|
|
30
30
|
import { fileURLToPath } from 'node:url';
|
|
31
|
+
import { homedir } from 'node:os';
|
|
31
32
|
import { getHookFeatures, detectProject, ensureProjectCategories, cleanupStaleLoops, DATA_DIR } from './shared.mjs';
|
|
32
33
|
|
|
34
|
+
// CC updates (especially when installer runs as root) can wipe ~/.claude.json
|
|
35
|
+
// or strip mcpServers.SynaBun, leaving the side panel/CLI with MCP disabled
|
|
36
|
+
// until the user manually re-registers via SynaBun setup UI. Re-add on every
|
|
37
|
+
// session start; idempotent if already present.
|
|
38
|
+
function healSynaBunMcp() {
|
|
39
|
+
try {
|
|
40
|
+
const claudeJsonPath = join(homedir(), '.claude.json');
|
|
41
|
+
let data = {};
|
|
42
|
+
if (existsSync(claudeJsonPath)) {
|
|
43
|
+
try { data = JSON.parse(readFileSync(claudeJsonPath, 'utf-8')); } catch { data = {}; }
|
|
44
|
+
}
|
|
45
|
+
if (!data.mcpServers || typeof data.mcpServers !== 'object') data.mcpServers = {};
|
|
46
|
+
const existing = data.mcpServers.SynaBun;
|
|
47
|
+
const wanted = { type: 'http', url: 'http://localhost:3344/mcp' };
|
|
48
|
+
if (existing && existing.type === wanted.type && existing.url === wanted.url) return;
|
|
49
|
+
data.mcpServers.SynaBun = wanted;
|
|
50
|
+
writeFileSync(claudeJsonPath, JSON.stringify(data, null, 2));
|
|
51
|
+
try {
|
|
52
|
+
appendFileSync(join(DATA_DIR, 'compact-debug.log'), `[${new Date().toISOString()}] MCP-HEAL re-registered SynaBun in ~/.claude.json\n`);
|
|
53
|
+
} catch { /* ok */ }
|
|
54
|
+
} catch { /* non-critical */ }
|
|
55
|
+
}
|
|
56
|
+
|
|
33
57
|
// Cross-platform safety: catch uncaught errors and output valid hook JSON
|
|
34
58
|
const _fallback = () => JSON.stringify({ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: 'SynaBun hook error. Follow CLAUDE.md memory rules manually.' } });
|
|
35
59
|
process.on('uncaughtException', () => { try { process.stdout.write(_fallback()); } catch {} process.exit(0); });
|
|
@@ -131,6 +155,9 @@ async function main() {
|
|
|
131
155
|
// Ensure all registered projects have their default category trees
|
|
132
156
|
try { ensureProjectCategories(); } catch { /* non-critical */ }
|
|
133
157
|
|
|
158
|
+
// Self-heal SynaBun MCP registration (CC updates can strip it)
|
|
159
|
+
healSynaBunMcp();
|
|
160
|
+
|
|
134
161
|
// Debug logging
|
|
135
162
|
try {
|
|
136
163
|
const cacheExists = sessionId ? existsSync(join(PRECOMPACT_DIR, `${sessionId}.json`)) : false;
|
|
@@ -365,7 +365,7 @@ export function cleanupStaleLoops(loopDir) {
|
|
|
365
365
|
|
|
366
366
|
const now = Date.now();
|
|
367
367
|
try {
|
|
368
|
-
const files = readdirSync(loopDir).filter(f => f.endsWith('.json') && !f.startsWith('pending-'));
|
|
368
|
+
const files = readdirSync(loopDir).filter(f => f.endsWith('.json') && !f.startsWith('pending-') && f !== 'active-pointer.json');
|
|
369
369
|
for (const f of files) {
|
|
370
370
|
const fp = join(loopDir, f);
|
|
371
371
|
try {
|
|
@@ -387,6 +387,18 @@ export function cleanupStaleLoops(loopDir) {
|
|
|
387
387
|
}
|
|
388
388
|
} catch { /* ok */ }
|
|
389
389
|
|
|
390
|
+
// Drop active-pointer.json if its target flag no longer exists
|
|
391
|
+
try {
|
|
392
|
+
const ptrPath = join(loopDir, 'active-pointer.json');
|
|
393
|
+
if (existsSync(ptrPath)) {
|
|
394
|
+
const ptr = JSON.parse(readFileSync(ptrPath, 'utf-8'));
|
|
395
|
+
const targetPath = ptr?.sessionId ? join(loopDir, `${ptr.sessionId}.json`) : null;
|
|
396
|
+
if (!targetPath || !existsSync(targetPath)) {
|
|
397
|
+
try { unlinkSync(ptrPath); } catch { /* ok */ }
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
} catch { /* ok */ }
|
|
401
|
+
|
|
390
402
|
// Delete stale pending-*.json files (client never claimed them)
|
|
391
403
|
try {
|
|
392
404
|
const pendingFiles = readdirSync(loopDir)
|
|
@@ -40,6 +40,23 @@ process.on('unhandledRejection', () => { try { process.stdout.write('{}'); } cat
|
|
|
40
40
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
41
41
|
const PENDING_COMPACT_DIR = join(DATA_DIR, 'pending-compact');
|
|
42
42
|
const LOOP_DIR = join(DATA_DIR, 'loop');
|
|
43
|
+
const ACTIVE_POINTER_PATH = join(LOOP_DIR, 'active-pointer.json');
|
|
44
|
+
|
|
45
|
+
function writeActivePointer(sessionId, terminalSessionId, claudeSessionId) {
|
|
46
|
+
try {
|
|
47
|
+
const payload = {
|
|
48
|
+
sessionId,
|
|
49
|
+
terminalSessionId: terminalSessionId || null,
|
|
50
|
+
claudeSessionId: claudeSessionId || null,
|
|
51
|
+
updatedAt: new Date().toISOString(),
|
|
52
|
+
};
|
|
53
|
+
writeFileSync(ACTIVE_POINTER_PATH, JSON.stringify(payload, null, 2));
|
|
54
|
+
} catch { /* ok */ }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function clearActivePointer() {
|
|
58
|
+
try { if (existsSync(ACTIVE_POINTER_PATH)) unlinkSync(ACTIVE_POINTER_PATH); } catch { /* ok */ }
|
|
59
|
+
}
|
|
43
60
|
const PENDING_REMEMBER_DIR = join(DATA_DIR, 'pending-remember');
|
|
44
61
|
const STORED_PLANS_PATH = join(DATA_DIR, 'stored-plans.json');
|
|
45
62
|
const PLANS_DIR = join(DATA_DIR, 'plans');
|
|
@@ -385,6 +402,9 @@ async function main() {
|
|
|
385
402
|
if (existsSync(loopFlagPath)) {
|
|
386
403
|
try {
|
|
387
404
|
loop = JSON.parse(readFileSync(loopFlagPath, 'utf-8'));
|
|
405
|
+
if (loop?.active) {
|
|
406
|
+
writeActivePointer(sessionId, loop.terminalSessionId || terminalSessionEnv, sessionId);
|
|
407
|
+
}
|
|
388
408
|
} catch {
|
|
389
409
|
try { unlinkSync(loopFlagPath); } catch { /* ok */ }
|
|
390
410
|
}
|
|
@@ -431,6 +451,7 @@ async function main() {
|
|
|
431
451
|
loopFlagPath = fp;
|
|
432
452
|
appendLoopLog(terminalSessionEnv, 'stop:claim', 'matched loop via Strategy A (env), already named correctly', { file: f, currentIteration: candidate.currentIteration });
|
|
433
453
|
}
|
|
454
|
+
writeActivePointer(sessionId, terminalSessionEnv, sessionId);
|
|
434
455
|
break;
|
|
435
456
|
}
|
|
436
457
|
|
|
@@ -443,6 +464,7 @@ async function main() {
|
|
|
443
464
|
} else {
|
|
444
465
|
loopFlagPath = fp;
|
|
445
466
|
}
|
|
467
|
+
writeActivePointer(sessionId, candidate.terminalSessionId || null, sessionId);
|
|
446
468
|
break;
|
|
447
469
|
}
|
|
448
470
|
} catch { continue; }
|
|
@@ -465,6 +487,7 @@ async function main() {
|
|
|
465
487
|
if (iterationsDone >= totalIterations) {
|
|
466
488
|
// Loop finished — delete the file (no history keeping)
|
|
467
489
|
try { unlinkSync(loopFlagPath); } catch { /* ok */ }
|
|
490
|
+
clearActivePointer();
|
|
468
491
|
// Fall through to remember/conversation checks
|
|
469
492
|
} else if (loop.memoryPending && (loop.memoryRetries || 0) < MAX_RETRIES) {
|
|
470
493
|
// Memory enforcement: block until Claude stores progress in memory
|