feat: add runtime storage override (#617)

* feat: add runtime storage override

Co-authored-by: Codex <noreply@openai.com>

* test: update storage path source assertions

Co-authored-by: Codex <noreply@openai.com>

* test: normalize storage path expectations

Co-authored-by: Codex <noreply@openai.com>

* fix runtime storage override resolution

Consolidate storage overrides on CONTEXT_MODE_DIR, share resolver behavior across server, hooks, and statusline, and update docs/tests around the Codex Desktop failure contract.

Co-authored-by: Codex <noreply@openai.com>

* fix windows storage override tests

Make storage-path resolver expectations platform-aware and keep statusline multi-adapter tests on adapter-default discovery instead of the root override path.

Co-authored-by: Codex <noreply@openai.com>

* address runtime storage review

Compose tool registration wrappers, memoize storage writability checks, report storage roots in ctx_doctor, keep legacy statusline session-dir compatibility, and fold resolver tests into existing server coverage.

Co-authored-by: Codex <noreply@openai.com>

* fix windows storage path test

Use an absolute temp-directory fixture instead of a POSIX-rooted default path so resolver tests assert storage behavior consistently across platforms.

Co-authored-by: Codex <noreply@openai.com>

* fold storage resolver into session db

Move CONTEXT_MODE_DIR resolver exports into the existing session DB module so hooks and statusline can use the existing session-db bundle. Drop the new storage-paths source and hook bundle entries while preserving shared storage behavior.

Co-authored-by: Codex <noreply@openai.com>

* consolidate default storage session roots

Add the shared default session-dir helper to the existing session DB bundle boundary, route server/hooks/statusline through it, and document why storage resolution lives there. Expand storage resolver tests for the shared helper.

Co-authored-by: Codex <noreply@openai.com>

* fix codex default session guard

Update the source assertion for the shared default-session-dir helper so the CI guard checks CODEX_HOME via configDirEnvForSessionSegments instead of the removed direct Codex config import.

Co-authored-by: Codex <noreply@openai.com>

* Fix Claude plugin skills path and pack integrity guard (#661)

* ci: update server.bundle.mjs, cli.bundle.mjs, session hook & security bundles

* ci: update install stats

* ci: update install stats

* ci: update install stats

* Fix Claude plugin skills manifest path

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* fix(pi): prevent mixed-width TUI over-width crashes (CJK/Korean/emoji) (#676)

* fix(pi): CJK wide-character width-aware truncation in PiTextComponent (#665)

PiTextComponent/truncateAnsiLine counted every JS character as width 1,
but CJK ideographs occupy 2 terminal columns. This produced lines whose
actual visibleWidth exceeded the requested width, triggering a pi-tui
crash: 'visible width: 162 > terminal width: 147'.

Root cause: truncateAnsiLine() iterated chars with visible++ (always 1)
instead of accounting for east-asian-width W/F codepoints.

Fix:
- Add charWidth() helper that returns 2 for CJK/Hangul/fullwidth ranges.
- Change truncateAnsiLine to use charWidth and check visible + w > maxWidth
  (not >=), so a 2-wide char still fits when exactly 2 columns remain.
- Export PiTextComponent and truncateAnsiLine for testability.

Tests (Slice 10 in pi-mcp-bridge.test.ts):
- Pure CJK text respects requested width.
- Mixed ASCII + CJK is correctly truncated.
- ANSI escape sequences are preserved and not counted toward width.
- The real crash line from pi-crash.log fits within terminal width 147.
- Edge case: maxWidth 0 or negative returns empty string.

Fixes #665

* test(asymmetric-drift): make npm pack dry-run windows-safe

---------

Co-authored-by: baifan <ubuntu@BaiFanPC.localdomain>

* fix(server): add z.preprocess coercions to ctx_fetch_and_index force and requests params (#679)

fix(server): add z.preprocess coercions to ctx_fetch_and_index params

The OpenCode/Kilo in-process native plugin bridge stringifies primitive types. Other tools already use z.preprocess(coerceBoolean/coerceJsonArray) to handle this, but ctx_fetch_and_index was missing these wrappers on its force and requests parameters.

Added schema-level test verifying preprocess wrappers are present. Updated existing schema tests to match new structure.

* feat(fetch): add per-call cache ttl (#666)

* feat(fetch): add per-call cache ttl

* test: run npm pack dry-run on Windows

* fix(batch_execute): preserve heredoc commands (#657)

Stops appending `2>&1` to user command strings in `runBatchCommands()` — that mutation broke heredoc terminators (`NODE` → `NODE 2>&1`). Now executes commands as-written and merges executor-captured stdout+stderr via new `combineExecOutput()` helper. Symmetric across serial + parallel paths.

Adds serial + parallel regression tests for heredoc commands with stderr. Updates the nodeOptsPrefix edge-case test to reflect the new behavior.

Bundles taken from `next` (CI-authoritative); regenerate on next main push.

Fixes #656.

Co-Authored-By: Noctivoro <nick@movermarketing.ai>

* fix(server): surrogate-safe preview truncation in ctx_fetch_and_index (#659) (#660)

The fetch-preview path at src/server.ts truncated `f.markdown` with
`String.prototype.slice(0, FETCH_PREVIEW_LIMIT)` where the limit
(3072) is a UTF-16 code-unit count. When the cut fell between the two
halves of an astral-plane character (e.g. 🟡 = U+1F7E1 = 🟡),
the high surrogate remained and the low surrogate was dropped.
JSON.stringify then emitted the orphan as a literal `\uD83D` escape
in the tool_result body, causing RFC 8259-strict consumers (the host
LLM API) to reject the next request with `400 ... no low surrogate
in string`. Sessions could not recover without removing the bad
message from the transcript.

Adds a new `charSafePrefix(str, maxChars)` export to `src/truncate.ts`
mirroring the existing internal `byteSafePrefix` semantics: cap by
UTF-16 code units, back off one unit if the cut would split a
surrogate pair. Wires it into the fetch-preview construction.

Tests cover the helper directly plus a regression that walks the
exact preview-construction pattern with an emoji at the LIMIT
boundary and asserts the resulting JSON contains no orphan high
surrogate and round-trips through a strict parser.

Other `.slice(0, N)` sites in src/ (small label/error/timestamp
truncations) are out of scope for this PR — they have low bounds and
their inputs are unlikely to contain emoji at the boundary. Happy to
extend coverage in a follow-up if the maintainer wants to remove the
class of bug entirely.

Fixes #659

* fix(auto-memory): scope memory dir by projectDir to stop cross-project leak (#663) (#664)

* fix(auto-memory): scope memory dir by projectDir to stop cross-project leak (#663)

getMemoryDir() ignored projectDir, so every adapter (except OpenClaw, whose
configDir IS the project root) returned a path shared by every project on
the machine. Two terminals open in different repos read each other's .md
memory files via searchAutoMemory(), then those notes contaminated
ctx_search timeline results.

Fix: HookAdapter.getMemoryDir now accepts an optional projectDir. When
supplied, the path is scoped via hashProjectDirCanonical(projectDir) under
the existing base. searchAutoMemory passes projectDir through; the
adapterless legacy fallback applies the same hash directly so the contract
holds at both call sites. Reuses the same canonicalization as ContentStore
and searchEvents, so all three project-scoped surfaces share one identity.

Backwards compat: getMemoryDir() without projectDir keeps returning the
unscoped path so external adapter consumers don't break.

Tests: leak canary (write under projectA scope, search from projectB,
assert 0 hits) + positive control + #663 negative test pinning that the
old unscoped path is no longer surfaced.

* test(windows): fix two flaky Windows-only CI failures

- asymmetric-drift-assert: spawnSync("npm", ...) needs shell:true on
  Windows because npm resolves to npm.cmd. Without it Node returned
  status=null/stderr=null/stdout=null and the assertion failed with no
  diagnostic. Also surface r.error in the assertion message.
- server.test.ts "python: cap works with python scripts": replace the
  10k-iteration Python loop with a single 100KB write. The cap still
  triggers (and stderr still gets "output capped"), but the test no
  longer races the 10s timeout on slow Windows CI VMs.

---------

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Baijack-star <71923891+Baijack-star@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: ByF <222546298+ByronFinn@users.noreply.github.com>
Co-authored-by: baifan <ubuntu@BaiFanPC.localdomain>
Co-authored-by: LeoNardo <58056860+LeoNardo-LB@users.noreply.github.com>
Co-authored-by: NgoQuocViet2001 <123613986+NgoQuocViet2001@users.noreply.github.com>
Co-authored-by: /noctivoro-x <nick@movermarketing.ai>
Co-authored-by: ccheng555 <ccheng5@gmail.com>
Co-authored-by: Seba Breguel <62109266+sebastianbreguel@users.noreply.github.com>
Co-authored-by: Mert Koseoglu <bm.ksglu@gmail.com>
This commit is contained in:
Tyce Herrman
2026-05-23 19:39:34 -04:00
committed by GitHub
parent afd109b73d
commit 3d8db08bd7
11 changed files with 820 additions and 101 deletions
+22 -9
View File
@@ -26,10 +26,14 @@
*/
import { existsSync, readFileSync } from "node:fs";
import { join, dirname, resolve } from "node:path";
import { dirname, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { homedir } from "node:os";
import { execFileSync } from "node:child_process";
import {
ensureWritableStorageDir,
resolveDefaultSessionDir,
resolveSessionStorageDir,
} from "../hooks/session-db.bundle.mjs";
// ── Analytics import — resolved relative to this script ─────────────────
// statusline.mjs ships in `bin/`; the compiled analytics module lives in
@@ -68,10 +72,10 @@ function platform() {
// Single-shot stderr warning latch — keep noise out of Claude Code's
// statusline output even when our parent runs us repeatedly per session.
let __winWarned = false;
const __warnedKeys = new Set();
function warnOnce(key, msg) {
if (key === "win" && __winWarned) return;
if (key === "win") __winWarned = true;
if (__warnedKeys.has(key)) return;
__warnedKeys.add(key);
try { process.stderr.write(`context-mode statusline: ${msg}\n`); } catch { /* ignore */ }
}
@@ -98,10 +102,19 @@ function readStdinJson() {
}
function resolveSessionDir() {
if (process.env.CONTEXT_MODE_SESSION_DIR) {
return process.env.CONTEXT_MODE_SESSION_DIR;
}
return join(homedir(), ".claude", "context-mode", "sessions");
return ensureWritableStorageDir(
resolveSessionStorageDir(() => resolveDefaultSessionDir({
configDir: ".claude",
configDirEnv: "CLAUDE_CONFIG_DIR",
legacySessionDirEnv: "CONTEXT_MODE_SESSION_DIR",
onLegacySessionDir: () => {
warnOnce(
"legacy-session-dir",
"CONTEXT_MODE_SESSION_DIR is deprecated; set CONTEXT_MODE_DIR to the parent context-mode root.",
);
},
})),
);
}
/**