synabun 2026.7.32 → 2026.9.5
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 +76 -0
- package/README.md +1 -1
- package/hooks/claude-code/post-plan.mjs +18 -16
- package/hooks/claude-code/post-remember.mjs +99 -66
- package/hooks/claude-code/prompt-submit.mjs +6 -2
- package/hooks/claude-code/session-start.mjs +16 -12
- package/hooks/claude-code/shared.mjs +6 -2
- package/hooks/claude-code/stop.mjs +42 -30
- package/lib/client-config-repair.js +11 -3
- package/mcp-server/dist/config.d.ts.map +1 -1
- package/mcp-server/dist/config.js +15 -1
- package/mcp-server/dist/config.js.map +1 -1
- package/mcp-server/dist/index.d.ts +14 -5
- package/mcp-server/dist/index.d.ts.map +1 -1
- package/mcp-server/dist/index.js +70 -50
- package/mcp-server/dist/index.js.map +1 -1
- package/mcp-server/dist/services/codex-config-heal.d.ts +2 -2
- package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
- package/mcp-server/dist/services/codex-config-heal.js +20 -9
- package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
- package/mcp-server/dist/services/neural-interface.d.ts +8 -2
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
- package/mcp-server/dist/services/neural-interface.js +27 -12
- package/mcp-server/dist/services/neural-interface.js.map +1 -1
- package/mcp-server/dist/services/profiles.d.ts +50 -12
- package/mcp-server/dist/services/profiles.d.ts.map +1 -1
- package/mcp-server/dist/services/profiles.js +125 -76
- package/mcp-server/dist/services/profiles.js.map +1 -1
- package/mcp-server/dist/tools/choice.d.ts +4 -4
- package/mcp-server/dist/tools/profile.d.ts +3 -2
- package/mcp-server/dist/tools/profile.d.ts.map +1 -1
- package/mcp-server/dist/tools/profile.js +43 -7
- package/mcp-server/dist/tools/profile.js.map +1 -1
- package/mcp-server/dist/tools/remember.d.ts +4 -4
- package/mcp-server/dist/tools/remember.d.ts.map +1 -1
- package/mcp-server/dist/tools/remember.js +19 -6
- package/mcp-server/dist/tools/remember.js.map +1 -1
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/src/config.ts +25 -12
- package/mcp-server/src/index.ts +77 -53
- package/mcp-server/src/services/codex-config-heal.ts +21 -7
- package/mcp-server/src/services/neural-interface.ts +65 -32
- package/mcp-server/src/services/profiles.ts +135 -76
- package/mcp-server/src/tools/profile.ts +43 -8
- package/mcp-server/src/tools/remember.ts +21 -6
- package/neural-interface/lib/augmented-path.js +50 -0
- package/neural-interface/lib/claude-agent-bridge.js +119 -10
- package/neural-interface/lib/claude-model-catalog.js +16 -3
- package/neural-interface/lib/cli-update-planner.js +312 -0
- package/neural-interface/lib/codex-model-context.js +202 -0
- package/neural-interface/lib/codex-runtime-path.js +102 -35
- package/neural-interface/lib/codex-session-catalog.js +12 -1
- package/neural-interface/lib/codex-thread-history.js +142 -0
- package/neural-interface/lib/codex-writer-lifecycle.js +152 -0
- package/neural-interface/lib/db.js +166 -63
- package/neural-interface/lib/mcp-client-config.js +81 -1
- package/neural-interface/lib/mcp-profile-refresh-coordinator.js +78 -0
- package/neural-interface/lib/mcp-profile-servers.js +120 -0
- package/neural-interface/lib/native-binary-runtime.js +650 -0
- package/neural-interface/lib/native-loop-providers.js +29 -1
- package/neural-interface/lib/opencode-v2-client.js +15 -3
- package/neural-interface/package-lock.json +50 -38
- package/neural-interface/package.json +2 -2
- package/neural-interface/public/claude-chat.css +7 -0
- package/neural-interface/public/claude-chat.js +18 -0
- package/neural-interface/public/shared/agent-runtime-options.js +18 -1
- package/neural-interface/public/shared/cdx/cdx-panel.js +57 -9
- package/neural-interface/public/shared/cdx/cdx-protocol.js +27 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +13 -0
- package/neural-interface/public/shared/cdx/cdx-tabs.js +339 -58
- package/neural-interface/public/shared/cdx/cdx-ws.js +89 -26
- package/neural-interface/public/shared/cli-status.js +15 -4
- package/neural-interface/public/shared/ocp/ocp-panel.js +50 -9
- package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +6 -0
- package/neural-interface/public/shared/styles.css +14 -3
- package/neural-interface/public/shared/ui-claude-panel.js +67 -9
- package/neural-interface/public/shared/ui-settings.js +172 -19
- package/neural-interface/public/shared/ui-tool-updates.js +25 -6
- package/neural-interface/scripts/rebuild-pty.js +37 -23
- package/neural-interface/server.js +1650 -372
- package/neural-interface/templates/CLAUDE-template.md +6 -6
- package/neural-interface/templates/ruleset-versions.json +15 -15
- package/package.json +1 -1
- package/skills/synabun/SKILL.md +1 -1
- package/skills/synabun/modules/audit.md +1 -1
- package/skills/synabun/modules/changelog.md +1 -1
- package/skills/synabun/modules/idea.md +1 -1
- package/skills/synabun/modules/memorize.md +1 -1
- package/skills/synabun/modules/schedule.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,79 @@
|
|
|
1
|
+
# SynaBun v.2026.09.05
|
|
2
|
+
|
|
3
|
+
A Claude Code model release: the model picker stops quietly serving a stale built-in list, sidepanel sessions run the model the picker actually offered, and Fable 5.1 is priced correctly.
|
|
4
|
+
|
|
5
|
+
## 🛠 Fixed
|
|
6
|
+
|
|
7
|
+
- **The Claude model picker silently served a static fallback list** — Model discovery spawns the Claude Code CLI to read its live model table, and that spawn goes through a shell whenever the resolved binary is a bare command name rather than a full path. A shell concatenates its arguments instead of passing them through, so the empty string in `--setting-sources ''` vanished, the CLI exited with `option '--setting-sources <sources>' argument missing`, and discovery came back empty. The flag is now a single `--setting-sources=` token, which survives both spawn paths. The failure was invisible by design: an empty result falls back to a four-entry list of unversioned aliases with no Fable entry at all, so the picker looked merely out of date rather than broken.
|
|
8
|
+
- **Discovery missed a Claude CLI installed outside the login shell's `PATH`** — SynaBun is routinely started from Finder or launchd with a minimal environment, where a CLI in `~/.local/bin`, Homebrew, Bun, or an npm global prefix is invisible. The same `PATH` augmentation the rest of the server already used is now applied to the model-discovery and version probes, so those installations are found.
|
|
9
|
+
- **The sidepanel offered Fable 5.1 but ran Fable 5** — The picker reads the CLI you have installed, while sidepanel chats run the CLI bundled inside `@anthropic-ai/claude-agent-sdk`. The two had drifted apart, and the older bundle's model table knows only `claude-fable-5` — so choosing Fable 5.1, or the `fable`/`best` aliases, quietly launched Fable 5 with no symptom beyond the model naming a different version of itself. The SDK is bumped `0.3.220` → `0.3.258` to close the gap. The guard test that exists to catch exactly this had been passing vacuously, because it probed for a CLI under the un-augmented `PATH` and skipped whenever it could not find one; it now runs on the machines it was written to protect.
|
|
10
|
+
- **Fable 5.1 sessions reported the wrong cost** — With no pricing row of its own, `claude-fable-5-1` fell through to the Sonnet fallback and understated input and output by roughly 3.3x. It now carries its own rates, which are deliberately not a copy of the Fable 5 row: same input and output pricing, cheaper cache reads.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# SynaBun v.2026.09.01
|
|
15
|
+
|
|
16
|
+
A Codex and memory-hooks release: Codex sidepanels no longer strand `already has an active writer` errors across tab switches, thread history moves off Codex's deprecated hydration path, and the Claude Code memory hooks stop re-issuing a block they were never able to clear.
|
|
17
|
+
|
|
18
|
+
## 🛠 Fixed
|
|
19
|
+
|
|
20
|
+
- **The "Unstored file edits" block never cleared** — A `remember` call without a `category` reset nothing, because the entire pending-remember reset was gated behind a branch that Claude Code rarely reaches: it routinely sends `remember` with `{ content }` alone. The flag was never cleared, so the Stop hook re-issued the identical block on every turn for the rest of the session — confirmed against a real session flag showing `rememberCount: 0` after four successful `remember` calls. The reset is now unconditional for any successful `remember` or `reflect`. Failure detection fails open by design: only an explicit `isError` / `success: false` / `status: "error"` counts as a failure, so a future payload change cannot silently reintroduce a block that never clears. A new `MAX_TASK_BLOCKS` session ceiling bounds the worst case.
|
|
21
|
+
- **Edit nudges never reached the model** — Every PostToolUse nudge was written as a bare top-level `{ additionalContext }`, but PostToolUse only reads `hookSpecificOutput: { hookEventName, additionalContext }`. The output was valid JSON, so it failed silently rather than erroring, and no "N file edits — remember to store this work" message was ever delivered. Fixed in `post-remember.mjs`, and in the five emission sites in `post-plan.mjs` that had the same bug — all now routed through one shared helper.
|
|
22
|
+
- **`reflect` was excluded by the hook matcher** — `post-remember.mjs` already handled `reflect`, but the registered PostToolUse matcher ended at `remember`, making that code unreachable: `reflect` calls never reached the hook. The matcher now covers both, anchored so it still never fires for `recall`, `memories`, or `forget`, and matches both Claude Code's `mcp__SynaBun__remember` and OpenCode's `SynaBun_remember` spelling.
|
|
23
|
+
- **Every hook invocation stalled about two seconds** — `readStdin()` resolved promptly, but its 2000 ms fallback timer stayed pending afterwards and held the event loop open, so each hook lingered for the full timeout before exiting — measured at 2.05 s against a configured 3 s hook timeout. Clearing and unref-ing the guard brings it to 0.02 s across every hook.
|
|
24
|
+
- **Memories were filed under the wrong project** — The MCP server read `claude-code-projects.json` from a data directory that has never existed, so registered project labels were silently ignored and every project fell back to a raw directory basename — `/Apps/EllaCred` was stored as `ellacred` rather than the registered label `elacred`. Over the HTTP transport `process.cwd()` is the Neural Interface's own directory, which labeled every HTTP-sourced memory `neural-interface` regardless of the caller; that case now resolves to `global`, a recoverable mislabel rather than one indistinguishable from a real project.
|
|
25
|
+
- **Codex sidepanels stranded "already has an active writer"** — A tab's intentional socket close was invisible to the server, so it orphaned an idle Codex app-server for the full 30-minute grace period and the next `thread/resume` collided with SynaBun's own abandoned writer. Ownership is now handed back through a release handshake before the socket closes, retirement waits for observed process exit rather than a kill flag, and only connections with genuinely reattachable work — active turns, pending approvals, in-flight RPCs — keep the grace period. Rapid tab switching can no longer strand or resurrect a hidden writer, and a genuinely external writer still reports an actionable error instead of requiring a restart.
|
|
26
|
+
- **Codex thread history rode a deprecated hydration path** — Full history was requested through `thread/read` and `thread/resume`. SynaBun now rebuilds the same client shape by paging `thread/turns/list` and `thread/items/list` in chronological order, and falls back to paginated turns with `itemsView: "full"` when a store reports that item pagination is unsupported. Repeated cursors, invalid pages, duplicate ids, and items with no matching turn are rejected before they can loop or poison a transcript; timeouts and unrelated protocol failures still surface rather than being misread as compatibility gaps.
|
|
27
|
+
- **The Codex Resume dropdown blocked on a full account scan** — It now serves from the cached session catalog when rows exist and refreshes in the background, falling back to a full multi-account walk only when the cache is cold; the same cached catalog backs Codex search metadata. Windows path matching resolves with `path.win32` and compares case-insensitively, so drive-letter case and slash direction no longer drop valid sessions from project grouping. Cache warmup also starts earlier at boot, so the dropdown is usually warm by the time it is opened.
|
|
28
|
+
- **Agent-selected MCP profiles did not expose their tools** — Profile state was module-global even though the HTTP transport hosts multiple stateful MCP sessions in one process, so a profile switch could mutate the most recently registered server instead of the caller's. Each stdio process and HTTP client session now owns an isolated profile runtime. The `tools/list_changed` notification is also no longer emitted synchronously from inside `profile set` — where OpenCode could abort or discard the in-flight roundtrip — but delayed and coalesced until after the tool result returns, so newly enabled tools are usable within the same agent turn.
|
|
29
|
+
- **Codex could not call newly enabled tools mid-turn** — Codex snapshots deferred MCP tools when a turn starts, so a successful refresh updated the app-server inventory without making anything callable by the running agent. Canonical Codex registrations now start SynaBun with the full deferred catalog, while per-sidepanel and automation profiles remain isolated focus selections. Profile changes no longer trigger a global `config/mcpServer/reload` that restarted unrelated MCP servers and reported success before startup had finished, and repeating a valid `profile.set` now re-reports the runtime and emits a fresh notification so a strictly profiled host can heal a stale catalog.
|
|
30
|
+
|
|
31
|
+
## ⚙️ Changed
|
|
32
|
+
|
|
33
|
+
- **`remember` now requires `category` and `project`** — Both were declared optional in the tool schema, and the `category` description merely noted that "if omitted, a project default is used." Hosts took the hint and sent `{ content }` alone — which is exactly what left the Stop hook blocked. A JSON Schema `required` entry is enforcement; a description is advice, and the advice was already being ignored. `importance` stays optional, since nothing downstream depends on it. Category names that are present but misspelled are still remapped rather than lost.
|
|
34
|
+
- **The memory ruleset now states this accurately** — The shipped `CLAUDE.md` template claimed no recall-and-reflect was needed afterwards, without mentioning that omitting `category` misfiles the memory and leaves the stop hook blocked. Corrected across all six template sites, the README, and the `/synabun` skill modules. The ruleset version is now `1.2.0`, so existing installs are offered the update.
|
|
35
|
+
- **Installed hook matchers repair themselves at startup** — A matcher change previously reached fresh installs but left every existing `.claude/settings.json` on the old value indefinitely, because the startup sweep compared only the command string and short-circuited. It now compares the matcher too, counts repairs, reports them on the startup `Hooks:` line, and is idempotent — so a stale matcher cannot keep a hook unreachable across upgrades.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
# SynaBun v.2026.07.34
|
|
40
|
+
|
|
41
|
+
A reliability release: the Claude sidepanel no longer dies with a misleading "does not match this system's libc" error, and every bundled native binary is checked and repaired at startup.
|
|
42
|
+
|
|
43
|
+
## 🛠 Fixed
|
|
44
|
+
|
|
45
|
+
- **"Native binary exists but failed to launch"** — The Claude sidepanel could fail every turn with *"Claude Code native binary at `…/claude` exists but failed to launch. This usually means the binary does not match this system's libc…"*. On macOS and Windows that explanation is never right: the real cause is almost always a missing execute bit. npm does not preserve the execute bit for files a package ships without a `bin` entry, which is exactly how the Claude Agent SDK and Codex deliver their native payloads, so a perfectly good binary can land as mode `0644` and fail to spawn. SynaBun now stats the binary the SDK is about to launch, restores the execute bit when it is missing, and retries — and when it cannot repair it, it says what is actually wrong: a permission problem on macOS and Linux, Gatekeeper quarantine (with the `xattr -d com.apple.quarantine` command) on macOS, a genuine libc mismatch only on Linux, and an unlaunchable `.cmd`/`.bat`/`.ps1` shim on Windows.
|
|
46
|
+
- **Only the current machine's binaries were repaired** — The startup repair looked at one directory, `prebuilds/<your platform>-<your arch>`, so a checkout carrying several platform builds left the others broken — including `darwin-x64`, which is what an Intel Mac needs for terminals. Startup and postinstall now sweep every vendored binary that is present (Claude Agent SDK, Codex, `node-pty`) by walking the tree instead of consulting a hardcoded list, so new platform directories are covered automatically.
|
|
47
|
+
- **The postinstall repair had stopped doing anything** — `scripts/rebuild-pty.js` was still fixing permissions on `node_modules/.bin/claude` and `@anthropic-ai/claude-code/cli.js`, neither of which has existed since that package was replaced by the Agent SDK. Its prebuild list named Linux directories that are not shipped while missing ones that are. It now runs the shared sweep, and runs it *before* the "node-pty not found, skipping" guard — previously an install without node-pty skipped the Claude and Codex repair entirely.
|
|
48
|
+
- **A `.cjs` executable override was launched the wrong way** — A custom Claude executable ending in `.cjs` was treated as a script to run through Node, but the SDK spawns it directly, so it needed the execute bit and did not get it.
|
|
49
|
+
|
|
50
|
+
## ✨ New
|
|
51
|
+
|
|
52
|
+
- **Runtime notices in the Claude sidepanel** — A new amber notice sits between grey status text and a red fatal error: something went wrong, was worked around, and the session is continuing in a degraded state. You will see it when the bundled binary is repaired and retried, and when a session falls back to your globally installed Claude CLI. It appears in both the sidepanel and the standalone chat page.
|
|
53
|
+
- **Fallback to your installed Claude CLI** — If the bundled runtime cannot be launched or repaired, sessions and unattended automation loops now fall back to the Claude CLI you have installed rather than failing the turn. Because model aliases and effort levels resolve per-binary, a version difference between your CLI and the bundled runtime is reported alongside the fallback instead of silently changing which model runs.
|
|
54
|
+
|
|
55
|
+
## ⚙️ Changed
|
|
56
|
+
|
|
57
|
+
- **A custom Claude executable path is now validated before use** — The `claude-skin` → `sdkExecutable` override was passed straight through to the SDK, which uses it verbatim with no checks of its own. It is now vetted first, and a bare command name such as `claude` is rejected with an explanation: the SDK spawns without a shell and cannot resolve a name from `PATH`, so handing one over produces a worse and more confusing failure than the one being recovered from. Overrides may be either a script entrypoint or a native binary path, and the same rule now applies to automation loops, which previously accepted anything.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
# SynaBun v.2026.07.33
|
|
62
|
+
|
|
63
|
+
A Windows release: Codex schedules and sidepanels now find a globally installed Codex runtime instead of failing at launch.
|
|
64
|
+
|
|
65
|
+
## 🛠 Fixed
|
|
66
|
+
|
|
67
|
+
- **Codex could not be found on Windows even with the global CLI installed** — SynaBun located the Codex runtime by looking it up on the inherited `PATH`, and the PATH it built for that lookup only added POSIX directories (`~/.local/bin`, `/usr/local/bin`, `~/.npm-global/bin`) that do not exist on Windows. Started from anywhere whose environment did not already carry the npm global bin — a shortcut, a service, a fresh terminal after an install — Codex was invisible and every Codex schedule failed. Discovery no longer depends on `PATH` at all: `@openai/codex` is now located under `npm root -g`, `%APPDATA%\npm\node_modules`, an `npm_config_prefix` override, the `node_modules` directory that contains SynaBun itself, and Node's own install directory. The Windows PATH augmentation also picks up `%APPDATA%\npm`, the configured npm prefix, and Node's directory, deduplicated, so the shell-aware launch paths improve too.
|
|
68
|
+
- **The native Codex executable was missed under current npm layouts** — The resolver looked in exactly one place, `vendor/<target>/codex/codex.exe` inside the platform package resolved through `require.resolve`. Current Codex releases ship the executable at `vendor/<target>/bin/`, and depending on the npm version the platform package sits either beside `@openai/codex` or nested inside its own `node_modules` — and package `exports` can block the resolve entirely. Both vendor layouts and all three package locations are now checked directly, so hoisting differences no longer read as "Codex is not installed".
|
|
69
|
+
- **Windows launched npm's shell shim instead of the real launcher** — `npm install -g` writes both an extensionless `codex` sh shim and a `codex.cmd`, and `where codex` lists the shim first, which Windows cannot spawn. The sidepanel launch path now prefers a `.cmd`, `.exe`, `.bat`, or `.ps1` launcher and keeps the first result only as a fallback for non-npm installations.
|
|
70
|
+
|
|
71
|
+
## ⚙️ Improved
|
|
72
|
+
|
|
73
|
+
- **A missing Codex runtime says which problem it is** — The failure notice now distinguishes "the global Codex package is installed but its Windows runtime is missing" from "Codex is not installed globally", and both point at `npm install -g @openai/codex@latest --include=optional`. The platform executable ships as an optional dependency, so an install that skipped optional packages leaves a Codex that looks present and cannot run. Successful resolution logs which layout it came from, and failures log the candidate paths that were checked.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
1
77
|
# SynaBun v.2026.07.32
|
|
2
78
|
|
|
3
79
|
A follow-up to 2026.07.31 that makes updating fast again on a large data home.
|
package/README.md
CHANGED
|
@@ -797,7 +797,7 @@ Also available: profile (runtime tool-group toggle), browser automation (40 tool
|
|
|
797
797
|
- Session-ending context: remember ongoing work as conversation
|
|
798
798
|
|
|
799
799
|
### Memory Tool Quirks
|
|
800
|
-
- `remember` accepts `tags` and `importance` directly and returns the full UUID.
|
|
800
|
+
- `remember` requires `category` and `project` on every call, accepts `tags` and `importance` directly, and returns the full UUID.
|
|
801
801
|
- `forget` is a soft delete — use `restore` to undo.
|
|
802
802
|
- The `reflect` tool's ID parameter is `memory_id` (not `id`). It requires the FULL UUID format.
|
|
803
803
|
- Make memory MCP calls sequentially, not in parallel — one failure cascades to all sibling calls in the batch.
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
* { session_id, tool_name, tool_input, tool_response, cwd }
|
|
20
20
|
*
|
|
21
21
|
* Output (stdout JSON):
|
|
22
|
-
* {
|
|
22
|
+
* { hookSpecificOutput: { hookEventName, additionalContext } } on success,
|
|
23
|
+
* or {} on skip/error. See emitContext().
|
|
23
24
|
*/
|
|
24
25
|
|
|
25
26
|
import { readFileSync, writeFileSync, appendFileSync, readdirSync, statSync, existsSync, mkdirSync, renameSync, unlinkSync } from 'node:fs';
|
|
@@ -35,6 +36,17 @@ process.on('unhandledRejection', () => { try { process.stdout.write('{}'); } cat
|
|
|
35
36
|
|
|
36
37
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
37
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Emit additionalContext in the shape PostToolUse actually reads. A bare
|
|
41
|
+
* top-level { additionalContext } is accepted as valid JSON and then silently
|
|
42
|
+
* discarded, so every message written that way never reaches the model.
|
|
43
|
+
*/
|
|
44
|
+
function emitContext(text) {
|
|
45
|
+
process.stdout.write(JSON.stringify({
|
|
46
|
+
hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: text },
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
// Debug log (append-only, survives across invocations)
|
|
39
51
|
const DEBUG_LOG_PATH = join(DATA_DIR, 'plan-debug.log');
|
|
40
52
|
function debugLog(msg) {
|
|
@@ -456,9 +468,7 @@ async function main() {
|
|
|
456
468
|
if (toolName === 'EnterPlanMode') {
|
|
457
469
|
debugLog('EnterPlanMode fired — injecting AskUserQuestion reminder');
|
|
458
470
|
const todayDir = getTodayPlanDir();
|
|
459
|
-
|
|
460
|
-
additionalContext: `SynaBun: You are now in plan mode.\n\n**CRITICAL CONSTRAINT**: Do NOT make code changes (Edit, Write, NotebookEdit). Plan mode is RESEARCH AND PLANNING ONLY. Investigate, read files, search code — then present your plan. Do NOT implement anything until the user explicitly approves the plan and you exit plan mode.\n\nWhen you have questions or need clarification, you MUST use the \`AskUserQuestion\` tool — do NOT write questions as plain text. Load it via \`ToolSearch\` if its schema is not yet available. Use \`ExitPlanMode\` for final plan approval.\n\n**Plan file location**: Write plan files to \`${todayDir}/your-plan-slug.md\` — use a short descriptive slug derived from the plan title (e.g., \`fix-session-crosstalk.md\`, \`opencode-sidepanel.md\`). Do NOT create \`PLAN.md\` at the project root. Do NOT write to \`~/.claude/plans/\`.`,
|
|
461
|
-
}));
|
|
471
|
+
emitContext(`SynaBun: You are now in plan mode.\n\n**CRITICAL CONSTRAINT**: Do NOT make code changes (Edit, Write, NotebookEdit). Plan mode is RESEARCH AND PLANNING ONLY. Investigate, read files, search code — then present your plan. Do NOT implement anything until the user explicitly approves the plan and you exit plan mode.\n\nWhen you have questions or need clarification, you MUST use the \`AskUserQuestion\` tool — do NOT write questions as plain text. Load it via \`ToolSearch\` if its schema is not yet available. Use \`ExitPlanMode\` for final plan approval.\n\n**Plan file location**: Write plan files to \`${todayDir}/your-plan-slug.md\` — use a short descriptive slug derived from the plan title (e.g., \`fix-session-crosstalk.md\`, \`opencode-sidepanel.md\`). Do NOT create \`PLAN.md\` at the project root. Do NOT write to \`~/.claude/plans/\`.`);
|
|
462
472
|
return;
|
|
463
473
|
}
|
|
464
474
|
|
|
@@ -516,9 +526,7 @@ async function main() {
|
|
|
516
526
|
const stored = loadStoredPlans();
|
|
517
527
|
if (stored[base]) {
|
|
518
528
|
debugLog(`ExitPlanMode: ${base} already stored — skipping`);
|
|
519
|
-
|
|
520
|
-
additionalContext: `SynaBun: Plan already stored (${base}).`,
|
|
521
|
-
}));
|
|
529
|
+
emitContext(`SynaBun: Plan already stored (${base}).`);
|
|
522
530
|
return;
|
|
523
531
|
}
|
|
524
532
|
try {
|
|
@@ -575,9 +583,7 @@ async function main() {
|
|
|
575
583
|
}
|
|
576
584
|
|
|
577
585
|
if (!planFile) {
|
|
578
|
-
|
|
579
|
-
additionalContext: 'SynaBun: No plan file found and no plan content in tool_input — UI capture and hook authoring both failed.',
|
|
580
|
-
}));
|
|
586
|
+
emitContext('SynaBun: No plan file found and no plan content in tool_input — UI capture and hook authoring both failed.');
|
|
581
587
|
return;
|
|
582
588
|
}
|
|
583
589
|
|
|
@@ -624,14 +630,10 @@ async function main() {
|
|
|
624
630
|
const shortTitle = planTitle.length > 60 ? planTitle.slice(0, 60) + '...' : planTitle;
|
|
625
631
|
const matchInfo = planFile.method || 'unknown';
|
|
626
632
|
debugLog(`Stored plan: ${planFile.path} [${matchInfo}] → memory ${id} (project: ${project})`);
|
|
627
|
-
|
|
628
|
-
additionalContext: `SynaBun: Plan stored in memory [${id}] — "${shortTitle}" (category: ${categoryName}, project: ${project}). Plan file: ${planFile.path} [matched: ${matchInfo}]`,
|
|
629
|
-
}));
|
|
633
|
+
emitContext(`SynaBun: Plan stored in memory [${id}] — "${shortTitle}" (category: ${categoryName}, project: ${project}). Plan file: ${planFile.path} [matched: ${matchInfo}]`);
|
|
630
634
|
}
|
|
631
635
|
|
|
632
636
|
main().catch((err) => {
|
|
633
637
|
// On error, still output valid JSON so Claude Code doesn't break
|
|
634
|
-
|
|
635
|
-
additionalContext: `SynaBun: Plan storage failed — ${err.message}. The plan file is still saved at ${PLANS_DIR}.`,
|
|
636
|
-
}));
|
|
638
|
+
emitContext(`SynaBun: Plan storage failed — ${err.message}. The plan file is still saved at ${PLANS_DIR}.`);
|
|
637
639
|
});
|
|
@@ -3,24 +3,31 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* SynaBun PostToolUse Hook for Claude Code (unified handler)
|
|
5
5
|
*
|
|
6
|
-
* Matches: ^Edit$|^Write$|^NotebookEdit$|
|
|
6
|
+
* Matches: ^Edit$|^Write$|^NotebookEdit$|Syna[Bb]un_+(remember|reflect)
|
|
7
7
|
*
|
|
8
8
|
* Two responsibilities:
|
|
9
9
|
*
|
|
10
10
|
* 1. EDIT TRACKING — When Claude uses Edit, Write, or NotebookEdit,
|
|
11
|
-
* increments a pending-remember counter.
|
|
12
|
-
*
|
|
11
|
+
* increments a pending-remember counter. Once EDIT_THRESHOLD edits are
|
|
12
|
+
* unremembered, the Stop hook blocks until a memory is stored.
|
|
13
13
|
*
|
|
14
|
-
* 2. FLAG MANAGEMENT — When Claude calls `remember`:
|
|
15
|
-
* -
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* 2. FLAG MANAGEMENT — When Claude calls `remember` or `reflect`:
|
|
15
|
+
* - ALWAYS resets the pending-remember flag (editCount→0, retries→0,
|
|
16
|
+
* files→[]), keeping rememberCount/totalEdits so subsequent edits start
|
|
17
|
+
* a fresh segment. This is deliberately NOT conditional on `category`.
|
|
18
|
+
* - category "conversations" ALSO clears pending-compact (compaction).
|
|
19
|
+
*
|
|
20
|
+
* History: this used to be `else if (category)`, so a `remember` call that
|
|
21
|
+
* omitted `category` — which is what Claude Code actually sends when the
|
|
22
|
+
* schema lets it — cleared nothing, and the Stop hook blocked forever.
|
|
23
|
+
* Never re-gate the reset on a field the caller may legitimately omit.
|
|
18
24
|
*
|
|
19
25
|
* Input (stdin JSON):
|
|
20
26
|
* { session_id, tool_name, tool_input: { ... }, tool_response: { ... } }
|
|
21
27
|
*
|
|
22
28
|
* Output (stdout JSON):
|
|
23
|
-
* - On edit
|
|
29
|
+
* - On an edit that hits the threshold:
|
|
30
|
+
* { hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext } }
|
|
24
31
|
* - Otherwise: {} (side effects only)
|
|
25
32
|
*/
|
|
26
33
|
|
|
@@ -45,14 +52,38 @@ for (const dir of [PENDING_COMPACT_DIR, PENDING_REMEMBER_DIR]) {
|
|
|
45
52
|
const EDIT_TOOLS = new Set(['Edit', 'Write', 'NotebookEdit']);
|
|
46
53
|
const EDIT_THRESHOLD = 1;
|
|
47
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Explicit-failure detection ONLY — everything ambiguous counts as SUCCESS.
|
|
57
|
+
*
|
|
58
|
+
* MCP tools return a CallToolResult: { content: [{type:'text',text}], isError? }.
|
|
59
|
+
* Claude Code passes it through and snake_cases some fields depending on version,
|
|
60
|
+
* so accept both spellings. A missing tool_response, a plain string, or an object
|
|
61
|
+
* of unknown shape MUST fail open: a format change on the Claude Code side can
|
|
62
|
+
* never be allowed to reintroduce the never-clears bug this hook exists to avoid.
|
|
63
|
+
*
|
|
64
|
+
* A schema rejection (InvalidParams) doesn't fire PostToolUse at all, which is
|
|
65
|
+
* also correct — nothing was stored, so nothing should clear.
|
|
66
|
+
*/
|
|
67
|
+
function toolCallFailed(resp) {
|
|
68
|
+
if (!resp || typeof resp !== 'object' || Array.isArray(resp)) return false;
|
|
69
|
+
if (resp.isError === true || resp.is_error === true) return true;
|
|
70
|
+
if (resp.success === false) return true;
|
|
71
|
+
if (typeof resp.status === 'string' && resp.status.toLowerCase() === 'error') return true;
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
48
75
|
function readStdin() {
|
|
49
76
|
return new Promise((resolve) => {
|
|
50
77
|
if (process.stdin.isTTY) return resolve('{}');
|
|
51
78
|
let data = '';
|
|
52
79
|
process.stdin.setEncoding('utf8');
|
|
53
80
|
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
54
|
-
process.stdin.on('end', () => resolve(data));
|
|
55
|
-
|
|
81
|
+
process.stdin.on('end', () => { clearTimeout(guard); resolve(data); });
|
|
82
|
+
// unref + clearTimeout: without both, this timer keeps the event loop alive
|
|
83
|
+
// for its full duration AFTER 'end' already resolved, so every hook
|
|
84
|
+
// invocation stalled ~2s against a 3s configured timeout.
|
|
85
|
+
const guard = setTimeout(() => resolve(data || '{}'), 2000);
|
|
86
|
+
guard.unref?.();
|
|
56
87
|
});
|
|
57
88
|
}
|
|
58
89
|
|
|
@@ -85,6 +116,12 @@ async function main() {
|
|
|
85
116
|
} catch { /* start fresh */ }
|
|
86
117
|
}
|
|
87
118
|
|
|
119
|
+
// A new edit segment starts a fresh obligation. `retries` is per-segment
|
|
120
|
+
// backoff, not a session budget — without this reset, MAX_RETRIES blocks
|
|
121
|
+
// permanently disable task-memory enforcement for the rest of the session
|
|
122
|
+
// (the only other reset paths require editCount to already be 0).
|
|
123
|
+
if ((flag.editCount || 0) === 0) flag.retries = 0;
|
|
124
|
+
|
|
88
125
|
// Increment edit counters
|
|
89
126
|
flag.editCount = (flag.editCount || 0) + 1;
|
|
90
127
|
flag.totalEdits = (flag.totalEdits || 0) + 1;
|
|
@@ -101,7 +138,7 @@ async function main() {
|
|
|
101
138
|
writeFileSync(flagPath, JSON.stringify(flag));
|
|
102
139
|
} catch { /* ok */ }
|
|
103
140
|
|
|
104
|
-
// Proactive nudge at every multiple of
|
|
141
|
+
// Proactive nudge at every multiple of EDIT_THRESHOLD
|
|
105
142
|
if (flag.editCount > 0 && flag.editCount % EDIT_THRESHOLD === 0) {
|
|
106
143
|
let nudgeText;
|
|
107
144
|
if (flag.editCount <= EDIT_THRESHOLD) {
|
|
@@ -124,7 +161,12 @@ async function main() {
|
|
|
124
161
|
} catch { /* category tree optional — nudge still works without it */ }
|
|
125
162
|
}
|
|
126
163
|
|
|
127
|
-
|
|
164
|
+
// PostToolUse reads additionalContext from hookSpecificOutput. A bare
|
|
165
|
+
// top-level { additionalContext } is silently discarded — which is why
|
|
166
|
+
// these nudges never reached the model.
|
|
167
|
+
process.stdout.write(JSON.stringify({
|
|
168
|
+
hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: nudgeText },
|
|
169
|
+
}));
|
|
128
170
|
} else {
|
|
129
171
|
process.stdout.write(JSON.stringify({}));
|
|
130
172
|
}
|
|
@@ -152,13 +194,18 @@ async function main() {
|
|
|
152
194
|
}
|
|
153
195
|
}
|
|
154
196
|
|
|
155
|
-
// ───
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
197
|
+
// ─── MEMORY-STORED CLEARING ───
|
|
198
|
+
// ANY successful remember/reflect resets edit tracking, regardless of category.
|
|
199
|
+
// Claude Code routinely calls remember with { content } only, and reflect rarely
|
|
200
|
+
// carries a category — gating this reset on category truthiness is what made the
|
|
201
|
+
// Stop hook block forever. Category only decides the EXTRA compaction clear below.
|
|
202
|
+
const storedMemory = (toolName.includes('remember') || toolName.includes('reflect'))
|
|
203
|
+
&& !toolCallFailed(input.tool_response);
|
|
159
204
|
|
|
160
|
-
|
|
161
|
-
|
|
205
|
+
if (storedMemory) {
|
|
206
|
+
// A "conversations" memory ALSO satisfies the compaction obligation.
|
|
207
|
+
// Clear every pending-compact flag — the session ID changes after compaction.
|
|
208
|
+
if ((toolInput.category || '') === 'conversations') {
|
|
162
209
|
try {
|
|
163
210
|
if (existsSync(PENDING_COMPACT_DIR)) {
|
|
164
211
|
for (const f of readdirSync(PENDING_COMPACT_DIR).filter(f => f.endsWith('.json'))) {
|
|
@@ -166,58 +213,44 @@ async function main() {
|
|
|
166
213
|
}
|
|
167
214
|
}
|
|
168
215
|
} catch { /* ok */ }
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
flag.files = [];
|
|
202
|
-
flag.firstEditAt = null;
|
|
203
|
-
flag.lastEditAt = null;
|
|
204
|
-
flag.firstMessageAt = null;
|
|
205
|
-
flag.lastMessageAt = null;
|
|
206
|
-
flag.lastRememberedAt = new Date().toISOString();
|
|
207
|
-
flag.rememberCount = prevRememberCount + 1;
|
|
208
|
-
flag.totalEdits = prevTotal;
|
|
209
|
-
|
|
210
|
-
writeFileSync(rememberFlagPath, JSON.stringify(flag));
|
|
211
|
-
} catch {
|
|
212
|
-
// If reset fails, fall back to deleting
|
|
213
|
-
try { unlinkSync(rememberFlagPath); } catch { /* ok */ }
|
|
214
|
-
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Reset pending-remember (keep the file, zero counters for the next segment).
|
|
219
|
+
// Never create it here — prompt-submit.mjs owns creation, and manufacturing a
|
|
220
|
+
// flag for a session that never edited would resurrect stale-file buildup.
|
|
221
|
+
const rememberFlagPath = join(PENDING_REMEMBER_DIR, `${sessionId}.json`);
|
|
222
|
+
if (existsSync(rememberFlagPath)) {
|
|
223
|
+
try {
|
|
224
|
+
let flag = {};
|
|
225
|
+
try { flag = JSON.parse(readFileSync(rememberFlagPath, 'utf-8')); } catch { /* start fresh */ }
|
|
226
|
+
|
|
227
|
+
// Preserve session-level stats before zeroing the segment counters.
|
|
228
|
+
flag.totalEdits = flag.totalEdits || flag.editCount || 0;
|
|
229
|
+
flag.rememberCount = (flag.rememberCount || 0) + 1;
|
|
230
|
+
flag.lastRememberedAt = new Date().toISOString();
|
|
231
|
+
|
|
232
|
+
flag.editCount = 0;
|
|
233
|
+
flag.messageCount = 0;
|
|
234
|
+
flag.retries = 0;
|
|
235
|
+
flag.taskBlockTotal = 0;
|
|
236
|
+
flag.files = [];
|
|
237
|
+
flag.firstEditAt = null;
|
|
238
|
+
flag.lastEditAt = null;
|
|
239
|
+
flag.firstMessageAt = null;
|
|
240
|
+
flag.lastMessageAt = null;
|
|
241
|
+
// categoryTreeInjected is intentionally NOT reset — the category tree is
|
|
242
|
+
// injected once per session, not once per task segment.
|
|
243
|
+
|
|
244
|
+
writeFileSync(rememberFlagPath, JSON.stringify(flag));
|
|
245
|
+
} catch {
|
|
246
|
+
// If reset fails, fall back to deleting
|
|
247
|
+
try { unlinkSync(rememberFlagPath); } catch { /* ok */ }
|
|
215
248
|
}
|
|
216
249
|
}
|
|
217
250
|
}
|
|
218
251
|
|
|
219
|
-
// Update loop memory tracking when
|
|
220
|
-
if (
|
|
252
|
+
// Update loop memory tracking when a memory is stored during an active loop
|
|
253
|
+
if (storedMemory) {
|
|
221
254
|
const LOOP_DIR = join(DATA_DIR, 'loop');
|
|
222
255
|
const loopPath = join(LOOP_DIR, `${sessionId}.json`);
|
|
223
256
|
if (existsSync(loopPath)) {
|
|
@@ -197,8 +197,12 @@ function readStdin() {
|
|
|
197
197
|
let data = '';
|
|
198
198
|
process.stdin.setEncoding('utf8');
|
|
199
199
|
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
200
|
-
process.stdin.on('end', () => resolve(data));
|
|
201
|
-
|
|
200
|
+
process.stdin.on('end', () => { clearTimeout(guard); resolve(data); });
|
|
201
|
+
// unref + clearTimeout: without both, this timer keeps the event loop alive
|
|
202
|
+
// for its full duration AFTER 'end' already resolved, so every hook
|
|
203
|
+
// invocation stalled ~2s against a 3s configured timeout.
|
|
204
|
+
const guard = setTimeout(() => resolve(data || '{}'), 2000);
|
|
205
|
+
guard.unref?.();
|
|
202
206
|
});
|
|
203
207
|
}
|
|
204
208
|
|
|
@@ -99,8 +99,12 @@ function readStdin() {
|
|
|
99
99
|
let data = '';
|
|
100
100
|
process.stdin.setEncoding('utf8');
|
|
101
101
|
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
102
|
-
process.stdin.on('end', () => resolve(data));
|
|
103
|
-
|
|
102
|
+
process.stdin.on('end', () => { clearTimeout(guard); resolve(data); });
|
|
103
|
+
// unref + clearTimeout: without both, this timer keeps the event loop alive
|
|
104
|
+
// for its full duration AFTER 'end' already resolved, so every hook
|
|
105
|
+
// invocation stalled ~2s against a 3s configured timeout.
|
|
106
|
+
const guard = setTimeout(() => resolve(data || '{}'), 2000);
|
|
107
|
+
guard.unref?.();
|
|
104
108
|
});
|
|
105
109
|
}
|
|
106
110
|
|
|
@@ -185,9 +189,9 @@ async function main() {
|
|
|
185
189
|
// Sweep pending-remember flags from long-dead sessions (never deleted otherwise).
|
|
186
190
|
cleanupStalePendingRemember();
|
|
187
191
|
const LOOP_STALE_MS = 10 * 60 * 1000;
|
|
188
|
-
const terminalSessionEnv = process.env.SYNABUN_TERMINAL_SESSION || '';
|
|
189
|
-
let isLoopSession = false;
|
|
190
|
-
let isNativeLoopSession = false;
|
|
192
|
+
const terminalSessionEnv = process.env.SYNABUN_TERMINAL_SESSION || '';
|
|
193
|
+
let isLoopSession = false;
|
|
194
|
+
let isNativeLoopSession = false;
|
|
191
195
|
try {
|
|
192
196
|
if (existsSync(LOOP_DIR)) {
|
|
193
197
|
const now = Date.now();
|
|
@@ -210,10 +214,10 @@ async function main() {
|
|
|
210
214
|
if (data.terminalSessionId !== terminalSessionEnv) continue;
|
|
211
215
|
} else {
|
|
212
216
|
if (data.terminalSessionId) continue;
|
|
213
|
-
}
|
|
214
|
-
isLoopSession = true;
|
|
215
|
-
isNativeLoopSession = data.driverType === 'native';
|
|
216
|
-
break;
|
|
217
|
+
}
|
|
218
|
+
isLoopSession = true;
|
|
219
|
+
isNativeLoopSession = data.driverType === 'native';
|
|
220
|
+
break;
|
|
217
221
|
}
|
|
218
222
|
} catch { /* skip */ }
|
|
219
223
|
}
|
|
@@ -227,9 +231,9 @@ async function main() {
|
|
|
227
231
|
``,
|
|
228
232
|
`SynaBun memory is active. CLAUDE.md contains the memory rules. Follow them throughout this session.`,
|
|
229
233
|
``,
|
|
230
|
-
isNativeLoopSession
|
|
231
|
-
? `**This is a native autonomous loop session.** Do NOT output any greeting or call recall at startup. The server sends the complete iteration task directly; execute it immediately.`
|
|
232
|
-
: `**This is an autonomous loop session.** Do NOT output any greeting. Do NOT call recall at startup. Wait for the loop task injection from the UserPromptSubmit hook, then execute immediately.`,
|
|
234
|
+
isNativeLoopSession
|
|
235
|
+
? `**This is a native autonomous loop session.** Do NOT output any greeting or call recall at startup. The server sends the complete iteration task directly; execute it immediately.`
|
|
236
|
+
: `**This is an autonomous loop session.** Do NOT output any greeting. Do NOT call recall at startup. Wait for the loop task injection from the UserPromptSubmit hook, then execute immediately.`,
|
|
233
237
|
``,
|
|
234
238
|
`Current project: **${project}**`,
|
|
235
239
|
);
|
|
@@ -100,8 +100,12 @@ export function readStdin() {
|
|
|
100
100
|
let data = '';
|
|
101
101
|
process.stdin.setEncoding('utf8');
|
|
102
102
|
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
103
|
-
process.stdin.on('end', () => resolve(data));
|
|
104
|
-
|
|
103
|
+
process.stdin.on('end', () => { clearTimeout(guard); resolve(data); });
|
|
104
|
+
// unref + clearTimeout: without both, this timer keeps the event loop alive
|
|
105
|
+
// for its full duration AFTER 'end' already resolved, so every hook
|
|
106
|
+
// invocation stalled ~2s against a 3s configured timeout.
|
|
107
|
+
const guard = setTimeout(() => resolve(data || '{}'), 2000);
|
|
108
|
+
guard.unref?.();
|
|
105
109
|
});
|
|
106
110
|
}
|
|
107
111
|
|
|
@@ -62,6 +62,11 @@ const STORED_PLANS_PATH = join(DATA_DIR, 'stored-plans.json');
|
|
|
62
62
|
const PLAN_DEBUG_LOG = join(DATA_DIR, 'plan-debug.log');
|
|
63
63
|
const MAX_RETRIES = 3;
|
|
64
64
|
const EDIT_THRESHOLD = 1;
|
|
65
|
+
// Session ceiling on task-memory blocks. `retries` resets whenever a new edit
|
|
66
|
+
// segment starts (post-remember.mjs), so it alone can no longer bound the
|
|
67
|
+
// block↔respond cycle. `stop_hook_active` is not honored by this hook, so this
|
|
68
|
+
// is the only backstop against an agent that edits-then-stops forever.
|
|
69
|
+
const MAX_TASK_BLOCKS = 12;
|
|
65
70
|
|
|
66
71
|
function planDebug(msg) {
|
|
67
72
|
try { appendCapped(PLAN_DEBUG_LOG, `[${new Date().toISOString()}] ${msg}\n`); } catch { /* ok */ }
|
|
@@ -81,6 +86,7 @@ function softCleanupFlag(flagPath) {
|
|
|
81
86
|
const cleaned = {
|
|
82
87
|
editCount: 0,
|
|
83
88
|
retries: 0,
|
|
89
|
+
taskBlockTotal: 0,
|
|
84
90
|
files: [],
|
|
85
91
|
messageCount: flag.messageCount || 0,
|
|
86
92
|
totalSessionMessages: flag.totalSessionMessages || 0,
|
|
@@ -232,8 +238,12 @@ function readStdin() {
|
|
|
232
238
|
let data = '';
|
|
233
239
|
process.stdin.setEncoding('utf8');
|
|
234
240
|
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
235
|
-
process.stdin.on('end', () => resolve(data));
|
|
236
|
-
|
|
241
|
+
process.stdin.on('end', () => { clearTimeout(guard); resolve(data); });
|
|
242
|
+
// unref + clearTimeout: without both, this timer keeps the event loop alive
|
|
243
|
+
// for its full duration AFTER 'end' already resolved, so every hook
|
|
244
|
+
// invocation stalled ~2s against a 3s configured timeout.
|
|
245
|
+
const guard = setTimeout(() => resolve(data || '{}'), 2000);
|
|
246
|
+
guard.unref?.();
|
|
237
247
|
});
|
|
238
248
|
}
|
|
239
249
|
|
|
@@ -391,31 +401,31 @@ async function main() {
|
|
|
391
401
|
}
|
|
392
402
|
}
|
|
393
403
|
} catch { /* ok */ }
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// ─── CHECK 1.5: Active loop ───
|
|
397
|
-
// Native loops keep their state at {runId}.json for the server-owned runtime.
|
|
398
|
-
// Return before the legacy /clear ownership scan can rename that file to the
|
|
399
|
-
// Claude session ID and make the runtime believe its state was deleted.
|
|
400
|
-
if (terminalSessionEnv) {
|
|
401
|
-
try {
|
|
402
|
-
const nativePath = join(LOOP_DIR, `${terminalSessionEnv}.json`);
|
|
403
|
-
if (existsSync(nativePath)) {
|
|
404
|
-
const nativeLoop = JSON.parse(readFileSync(nativePath, 'utf-8'));
|
|
405
|
-
if (nativeLoop?.active && nativeLoop.driverType === 'native'
|
|
406
|
-
&& nativeLoop.terminalSessionId === terminalSessionEnv) {
|
|
407
|
-
appendLoopLog(terminalSessionEnv, 'stop:native', 'leaving iteration transition to native runtime', {
|
|
408
|
-
sessionId,
|
|
409
|
-
currentIteration: nativeLoop.currentIteration || 0,
|
|
410
|
-
});
|
|
411
|
-
process.stdout.write(JSON.stringify({}));
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
} catch { /* fall through to legacy ownership scan */ }
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Deactivate stale loops first (session died, terminal closed, time expired)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ─── CHECK 1.5: Active loop ───
|
|
407
|
+
// Native loops keep their state at {runId}.json for the server-owned runtime.
|
|
408
|
+
// Return before the legacy /clear ownership scan can rename that file to the
|
|
409
|
+
// Claude session ID and make the runtime believe its state was deleted.
|
|
410
|
+
if (terminalSessionEnv) {
|
|
411
|
+
try {
|
|
412
|
+
const nativePath = join(LOOP_DIR, `${terminalSessionEnv}.json`);
|
|
413
|
+
if (existsSync(nativePath)) {
|
|
414
|
+
const nativeLoop = JSON.parse(readFileSync(nativePath, 'utf-8'));
|
|
415
|
+
if (nativeLoop?.active && nativeLoop.driverType === 'native'
|
|
416
|
+
&& nativeLoop.terminalSessionId === terminalSessionEnv) {
|
|
417
|
+
appendLoopLog(terminalSessionEnv, 'stop:native', 'leaving iteration transition to native runtime', {
|
|
418
|
+
sessionId,
|
|
419
|
+
currentIteration: nativeLoop.currentIteration || 0,
|
|
420
|
+
});
|
|
421
|
+
process.stdout.write(JSON.stringify({}));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
} catch { /* fall through to legacy ownership scan */ }
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Deactivate stale loops first (session died, terminal closed, time expired)
|
|
419
429
|
cleanupStaleLoops(LOOP_DIR);
|
|
420
430
|
// After /clear, session ID may change. Try exact match first, then scan for any active loop.
|
|
421
431
|
let loopFlagPath = join(LOOP_DIR, `${sessionId}.json`);
|
|
@@ -494,8 +504,8 @@ async function main() {
|
|
|
494
504
|
} catch { /* ok */ }
|
|
495
505
|
}
|
|
496
506
|
|
|
497
|
-
// Skip server-driven exec/native loops. Their runtimes own iteration transitions.
|
|
498
|
-
if (loop?.active && (loop.driverType === 'exec' || loop.driverType === 'native')) {
|
|
507
|
+
// Skip server-driven exec/native loops. Their runtimes own iteration transitions.
|
|
508
|
+
if (loop?.active && (loop.driverType === 'exec' || loop.driverType === 'native')) {
|
|
499
509
|
process.stdout.write(JSON.stringify({}));
|
|
500
510
|
return;
|
|
501
511
|
}
|
|
@@ -642,8 +652,10 @@ async function main() {
|
|
|
642
652
|
// CHECK 2: Task remember (edits without memory entry)
|
|
643
653
|
if (editCount >= EDIT_THRESHOLD) {
|
|
644
654
|
const retries = flag.retries || 0;
|
|
645
|
-
|
|
655
|
+
const taskBlockTotal = flag.taskBlockTotal || 0;
|
|
656
|
+
if (retries < MAX_RETRIES && taskBlockTotal < MAX_TASK_BLOCKS) {
|
|
646
657
|
flag.retries = retries + 1;
|
|
658
|
+
flag.taskBlockTotal = taskBlockTotal + 1;
|
|
647
659
|
const files = Array.isArray(flag.files) ? flag.files : [];
|
|
648
660
|
const fileList = files.length > 0
|
|
649
661
|
? ` Files: ${files.slice(0, 5).join(', ')}${files.length > 5 ? ` (+${files.length - 5} more)` : ''}`
|