chore: import upstream snapshot with attribution
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:10:34 +08:00
commit a789495a98
1551 changed files with 718128 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
prompt=${1:-"Use the bash tool to run 'pwd', then use the ls tool to list the current directory, then respond with DONE."}
provider=${JCODE_PROVIDER:-auto}
cargo_exec="$repo_root/scripts/cargo_exec.sh"
if [[ ! -x "$repo_root/target/release/jcode" ]]; then
(cd "$repo_root" && "$cargo_exec" build --release)
fi
workdir=$(mktemp -d)
trap 'rm -rf "$workdir"' EXIT
JCODE_HOME="$workdir" PATH="$repo_root/target/release:$PATH" \
jcode run --no-update --trace --provider "$provider" "$prompt"
+197
View File
@@ -0,0 +1,197 @@
#!/usr/bin/env python3
"""Analyze OpenAI persistent-websocket reuse and KV-cache effectiveness from jcode logs.
Motivation
----------
OpenAI prompt caching on the ChatGPT/Codex backend is driven by the persistent
websocket reuse path: it sends only a delta + ``previous_response_id`` on the
same connection, so the server already holds the KV tensors for that prefix.
When the socket is torn down, the chain is lost (``store=false``) and the next
turn re-sends the full conversation, relying on OpenAI prefix-hash routing which
frequently lands on a cold machine (zero cache read).
This script quantifies:
* connection mix (persistent-reuse vs persistent-fresh)
* why fresh connections happen (state-reset reasons, idle reconnects)
* realized cache hit rate per provider
* OpenAI zero/low-read events
Use it before/after changing ``JCODE_OPENAI_WS_IDLE_RECONNECT_SECS`` (or the
default) to confirm idle-reconnect churn drops and reuse/cache rates rise.
Usage
-----
python3 scripts/analyze_openai_ws_cache.py [LOGFILE ...]
With no arguments it scans ~/.jcode/logs/jcode-*.log.
"""
from __future__ import annotations
import collections
import glob
import os
import re
import sys
def _log_files(argv: list[str]) -> list[str]:
if argv:
return argv
home = os.environ.get("HOME", "")
return sorted(glob.glob(os.path.join(home, ".jcode", "logs", "jcode-*.log")))
_KV_FIELD_RE = re.compile(r"(\w+)=([^\s]+)")
def analyze(files: list[str]) -> dict:
conn = collections.Counter() # persistent-reuse / persistent-fresh
reset_reason = collections.Counter() # persistent_state_reset reason=...
reuse_detail = collections.Counter() # persistent_reuse_unavailable_detail reason=...
idle_reconnect_secs: list[int] = [] # observed idle durations that triggered reconnect
cache = collections.defaultdict(lambda: [0, 0, 0]) # provider -> [new_input, read, n]
# OpenAI read_pct distribution, using the harness's own authoritative
# read_pct field rather than a token-ratio proxy (the harness computes
# read_pct against cache-reportable input, not read+new_input).
oa_readpct = collections.Counter()
oa_readpct_n = 0
oa_zero = 0
oa_zero_tokens = 0
idle_re = re.compile(r"Persistent WS idle for (\d+)s; reconnecting")
for path in files:
try:
fh = open(path, errors="replace")
except OSError:
continue
with fh:
for line in fh:
if "persistent-reuse" in line:
conn["reuse"] += 1
elif "persistent-fresh" in line:
conn["fresh"] += 1
if "persistent_state_reset" in line:
m = re.search(r"reason=([a-z_]+)", line)
if m:
reset_reason[m.group(1)] += 1
if "persistent_reuse_unavailable_detail" in line:
m = re.search(r"reason=([a-z_]+)", line)
if m:
reuse_detail[m.group(1)] += 1
m = idle_re.search(line)
if m:
idle_reconnect_secs.append(int(m.group(1)))
if "KV_CACHE_USAGE" in line:
d = dict(_KV_FIELD_RE.findall(line))
provider = d.get("provider", "?")
try:
new_input = int(d.get("input", "0"))
read = int(d.get("cache_read", "0"))
except ValueError:
continue
bucket = cache[provider]
bucket[0] += new_input
bucket[1] += read
bucket[2] += 1
if provider == "OpenAI":
prompt = new_input + read
if prompt > 1024 and read == 0:
oa_zero += 1
oa_zero_tokens += new_input
read_pct = d.get("read_pct")
if read_pct not in (None, "None"):
try:
v = float(read_pct)
except ValueError:
v = None
if v is not None:
oa_readpct_n += 1
if v >= 90:
oa_readpct[">=90%"] += 1
elif v >= 70:
oa_readpct["70-90%"] += 1
elif v >= 50:
oa_readpct["50-70%"] += 1
elif v > 0:
oa_readpct["1-50%"] += 1
else:
oa_readpct["0%"] += 1
return {
"conn": conn,
"reset_reason": reset_reason,
"reuse_detail": reuse_detail,
"idle_reconnect_secs": idle_reconnect_secs,
"cache": cache,
"oa_readpct": oa_readpct,
"oa_readpct_n": oa_readpct_n,
"oa_zero": oa_zero,
"oa_zero_tokens": oa_zero_tokens,
}
def main(argv: list[str]) -> int:
files = _log_files(argv)
if not files:
print("no log files found", file=sys.stderr)
return 1
print(f"Scanned {len(files)} log file(s)")
r = analyze(files)
conn = r["conn"]
total_conn = conn["reuse"] + conn["fresh"]
print("\n== Connection mix ==")
if total_conn:
print(f" reuse : {conn['reuse']:>6} ({100*conn['reuse']/total_conn:.1f}%)")
print(f" fresh : {conn['fresh']:>6} ({100*conn['fresh']/total_conn:.1f}%)")
else:
print(" (no ConnectionType events)")
print("\n== Fresh-connection causes ==")
print(" persistent_reuse_unavailable_detail:")
for reason, n in r["reuse_detail"].most_common():
print(f" {reason:24s} {n}")
print(" persistent_state_reset:")
for reason, n in r["reset_reason"].most_common():
print(f" {reason:24s} {n}")
idle = r["idle_reconnect_secs"]
print("\n== Idle-reconnect events (the avoidable churn) ==")
if idle:
idle_sorted = sorted(idle)
print(f" count={len(idle)} min={idle_sorted[0]}s "
f"median={idle_sorted[len(idle_sorted)//2]}s max={idle_sorted[-1]}s")
# how many would be saved by a higher threshold
for thr in (90, 300, 600, 900):
saved = sum(1 for s in idle if s < thr)
print(f" threshold {thr:>4}s would have avoided {saved}/{len(idle)} reconnects")
else:
print(" count=0 (no idle reconnects logged)")
print("\n== Realized cache hit rate (read / (read + new_input)) ==")
for provider, (new_input, read, n) in sorted(r["cache"].items()):
total = new_input + read
if total:
print(f" {provider:8s} hit={100*read/total:5.1f}% "
f"read={read:>13,} new_input={new_input:>13,} n={n}")
print("\n== OpenAI cold-prefill cost ==")
print(f" zero-read prompts (>1024 tok): {r['oa_zero']} "
f"(~{r['oa_zero_tokens']:,} full-price input tokens)")
n = r["oa_readpct_n"]
if n:
print(f" read_pct distribution (harness field, n={n}):")
for k in (">=90%", "70-90%", "50-70%", "1-50%", "0%"):
c = r["oa_readpct"][k]
print(f" {k:8s}: {c:>5} ({100*c/n:.1f}%)")
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
+526
View File
@@ -0,0 +1,526 @@
#!/usr/bin/env python3
"""Analyze the monolithic root `jcode` crate to plan a bottom-up split.
For every top-level module under src/ (a `foo.rs` file or a `foo/` dir) it
computes:
- loc: total lines of Rust (incl. submodules for dir modules)
- facade: whether it is already just `pub use jcode_*::*;`
- inbound: how many *other* top-level modules reference `crate::<mod>`
- outbound: which other in-root (non-facade) modules it references
The goal is to find low-coupling, high-line-count leaves to extract next, and
to compute a topological-ish extraction order (extract modules whose in-root
outbound deps are already crates/facades first).
Pure static analysis: safe to run while builds are in progress.
"""
from __future__ import annotations
import os
import re
import sys
import json
from collections import defaultdict
SRC = os.path.join(os.path.dirname(__file__), "..", "src")
SRC = os.path.normpath(SRC)
CRATE_RE = re.compile(r"\bcrate::([a-z_][a-z0-9_]*)")
FACADE_RE = re.compile(r"pub use jcode_[a-z0-9_]+::")
# `use crate::...;` statement body (may span lines); group 1 is everything
# between `crate::` and the terminating `;`.
USE_CRATE_RE = re.compile(r"\buse\s+crate::([^;]*);", re.DOTALL)
def _split_top_level(body: str) -> list[str]:
"""Split a brace-group body on top-level commas (ignoring nested braces)."""
parts: list[str] = []
depth = 0
cur = []
for ch in body:
if ch == "{":
depth += 1
cur.append(ch)
elif ch == "}":
depth -= 1
cur.append(ch)
elif ch == "," and depth == 0:
parts.append("".join(cur))
cur = []
else:
cur.append(ch)
if cur:
parts.append("".join(cur))
return [p.strip() for p in parts if p.strip()]
def _module_imports(joined: str):
"""Parse `use crate::...;` statements in `joined` source text.
Returns (group_edge_counts, alias_set):
* group_edge_counts: target module -> number of grouped-import references
that `CRATE_RE` cannot see (because `{` immediately follows `crate::`).
Non-grouped `use crate::name...;` imports are intentionally left to
`CRATE_RE` so we never double count.
* alias_set: module names brought into local scope as a bare `name::`
path prefix (so later bare `name::` usages can be attributed).
"""
group_edges: dict[str, int] = defaultdict(int)
aliases: set[str] = set()
ident = re.compile(r"^[a-z_][a-z0-9_]*$")
for m in USE_CRATE_RE.finditer(joined):
rest = m.group(1).strip()
if rest.startswith("{"):
# Grouped import: `use crate::{a, b, c::Foo, d::{self, X}};`
inner = rest[1 : rest.rfind("}")] if "}" in rest else rest[1:]
for entry in _split_top_level(inner):
if entry in ("self", "*"):
continue
head = entry.split("::", 1)[0].split(" as ")[0].strip()
if not ident.match(head):
continue
group_edges[head] += 1
# Bare `head` (no `::`) or `head::{self...}` brings `head` itself
# into scope as a usable module path prefix.
if "::" not in entry:
aliases.add(head)
elif re.match(rf"^{re.escape(head)}::\{{\s*self\b", entry):
aliases.add(head)
else:
# Non-grouped: `use crate::name;`, `name::Item`, `name::{...}`,
# `name as X`. CRATE_RE already counts the `crate::name` edge, so we
# only track whether `name` becomes a bare path alias here.
head = rest.split("::", 1)[0].split(" as ")[0].strip()
if not ident.match(head):
continue
if "::" not in rest:
aliases.add(head)
elif re.match(rf"^{re.escape(head)}::\{{\s*self\b", rest):
aliases.add(head)
return dict(group_edges), aliases
def top_level_modules() -> dict[str, list[str]]:
"""Return {module_name: [file paths]} for each top-level module.
A module `foo` may be backed by `src/foo.rs`, `src/foo/` (dir), or BOTH (a
facade `foo.rs` that re-exports a crate plus a small local `foo/` submodule).
We collect all backing files under one logical module and remember the
canonical entry file (the `.rs` sibling, else `foo/mod.rs`).
"""
mods: dict[str, list[str]] = {}
entries: dict[str, str] = {}
dir_entries: dict[str, str] = {}
for entry in sorted(os.listdir(SRC)):
path = os.path.join(SRC, entry)
if entry.endswith(".rs") and os.path.isfile(path):
name = entry[:-3]
if name in ("lib", "main"):
continue
mods.setdefault(name, []).append(path)
entries[name] = path # foo.rs is always the canonical entry
elif os.path.isdir(path):
files = []
for root, _dirs, fnames in os.walk(path):
for fn in fnames:
if fn.endswith(".rs"):
files.append(os.path.join(root, fn))
if files:
mods.setdefault(entry, []).extend(sorted(files))
modrs = os.path.join(path, "mod.rs")
dir_entries[entry] = modrs if os.path.exists(modrs) else sorted(files)[0]
# Canonical entry: prefer the `.rs` sibling; fall back to the dir entry.
for name, dir_entry in dir_entries.items():
entries.setdefault(name, dir_entry)
# stash entries on the function for the caller
top_level_modules.entries = entries # type: ignore[attr-defined]
return mods
def loc(files: list[str]) -> int:
total = 0
for f in files:
try:
with open(f, encoding="utf-8", errors="ignore") as fh:
total += sum(1 for _ in fh)
except OSError:
pass
return total
def facade_ratio(name: str) -> tuple[float, int, int]:
"""Return (re-export ratio, facade_lines, code_lines) for the entry file.
A high ratio means the module's public surface mostly lives in a crate
already; a low ratio means real local logic still lives in the root crate.
"""
entry = getattr(top_level_modules, "entries", {}).get(name)
if entry is None or not os.path.exists(entry):
return (0.0, 0, 0)
try:
with open(entry, encoding="utf-8", errors="ignore") as fh:
text = fh.read()
except OSError:
return (0.0, 0, 0)
code_lines = [
ln.strip()
for ln in text.splitlines()
if ln.strip()
and not ln.strip().startswith("//")
and not ln.strip().startswith("#![")
and not ln.strip().startswith("#[")
]
if not code_lines:
return (0.0, 0, 0)
facade_lines = [ln for ln in code_lines if FACADE_RE.search(ln)]
return (len(facade_lines) / len(code_lines), len(facade_lines), len(code_lines))
def classify_facade(name: str, total_loc: int = 0) -> str:
"""fully | thick | none.
fully: entry is essentially just re-exports (already a crate facade).
thick: re-exports a crate but keeps a small residual of local API/logic.
none: no crate re-export, OR a large module whose bulk still lives locally
(a re-export line in mod.rs does not make a 30K-line dir a facade).
"""
ratio, facade_lines, code_lines = facade_ratio(name)
if facade_lines == 0:
return "none"
# A large module still carries its weight in-root regardless of a convenience
# re-export in its entry file; only small modules can be "extracted enough".
if total_loc > 600:
return "none"
# Mostly re-exports, only a tiny tail of local helpers -> fully extracted.
if ratio >= 0.5 or code_lines <= max(8, facade_lines + 4):
return "fully"
return "thick"
def is_facade(name: str, files: list[str]) -> bool:
# "Extracted enough" to no longer count as a real in-root blocker for bulk.
return classify_facade(name, loc(files)) == "fully"
def outbound_refs(files: list[str], self_name: str, exclude_tests: bool = True):
"""Return (ref_set, ref_counts) for `crate::<mod>` references.
For crate-split planning we care about the *library* dependency graph, so by
default we skip test-only files (`*_tests.rs`, `tests.rs`) and lines guarded
by an immediately-preceding `#[cfg(test)]`. Test deps would become
dev-dependencies and do not constrain how the lib is split into crates.
ref_counts maps target module -> number of referencing lines, used as an edge
weight: a cheap edge (few references) is easy to invert/cut to break a cycle.
"""
counts: dict[str, int] = defaultdict(int)
for f in files:
base = os.path.basename(f)
if exclude_tests and (base == "tests.rs" or base.endswith("_tests.rs")):
continue
try:
with open(f, encoding="utf-8", errors="ignore") as fh:
lines = fh.readlines()
except OSError:
continue
in_test_block = False
test_block_depth = 0
depth = 0
pending_cfg_test = False
code_lines: list[str] = []
for ln in lines:
stripped = ln.strip()
if exclude_tests and stripped.startswith("#[cfg(test)]"):
pending_cfg_test = True
continue
if exclude_tests and pending_cfg_test and "{" in ln:
in_test_block = True
test_block_depth = depth
pending_cfg_test = False
if in_test_block:
depth += ln.count("{") - ln.count("}")
if depth <= test_block_depth:
in_test_block = False
continue
depth += ln.count("{") - ln.count("}")
code_lines.append(ln)
for m in CRATE_RE.finditer(ln):
counts[m.group(1)] += 1
# Grouped `use crate::{...}` edges (invisible to CRATE_RE) and bare
# `alias::` usages from imported module aliases. Without this, any
# module pulled in via a grouped import (e.g. `use crate::{id, tui};`)
# and then referenced as `tui::App` would be entirely uncounted,
# badly undercounting edge weights for crate-split planning.
joined = "".join(code_lines)
group_edges, aliases = _module_imports(joined)
for name, c in group_edges.items():
counts[name] += c
aliases.discard(self_name)
if aliases:
alias_re = re.compile(
r"(?<![\w:])(" + "|".join(re.escape(a) for a in sorted(aliases)) + r")::"
)
for ln in code_lines:
if ln.lstrip().startswith("use "):
continue
for m in alias_re.finditer(ln):
counts[m.group(1)] += 1
counts.pop(self_name, None)
return set(counts), dict(counts)
def strongly_connected_components(graph: dict[str, set[str]]) -> list[list[str]]:
"""Tarjan's SCC over the module dependency graph.
A component with >1 node (or a self-loop) is a dependency cycle: those
modules cannot be split into separate crates without first breaking the
cycle (e.g. by extracting a shared trait/interface crate). Returned in
reverse-topological order (leaves first).
"""
index_counter = [0]
stack: list[str] = []
on_stack: dict[str, bool] = {}
index: dict[str, int] = {}
lowlink: dict[str, int] = {}
result: list[list[str]] = []
import sys as _sys
_sys.setrecursionlimit(10000)
def strongconnect(v: str) -> None:
index[v] = index_counter[0]
lowlink[v] = index_counter[0]
index_counter[0] += 1
stack.append(v)
on_stack[v] = True
for w in sorted(graph.get(v, ())):
if w not in index:
strongconnect(w)
lowlink[v] = min(lowlink[v], lowlink[w])
elif on_stack.get(w):
lowlink[v] = min(lowlink[v], index[w])
if lowlink[v] == index[v]:
comp = []
while True:
w = stack.pop()
on_stack[w] = False
comp.append(w)
if w == v:
break
result.append(comp)
for v in sorted(graph):
if v not in index:
strongconnect(v)
return result
def main() -> int:
mods = top_level_modules()
names = set(mods)
info = {}
for name, files in mods.items():
module_loc = loc(files)
refs, ref_counts = outbound_refs(files, name)
info[name] = {
"loc": module_loc,
"facade": classify_facade(name, module_loc) == "fully",
"facade_class": classify_facade(name, module_loc),
"outbound": refs,
"ref_counts": ref_counts,
}
# inbound: count of other modules referencing crate::<name>
inbound = defaultdict(set)
for name, meta in info.items():
for dep in meta["outbound"]:
if dep in names and dep != name:
inbound[dep].add(name)
# "in-root blockers": outbound deps that are still real in-root modules with
# substantive local logic. A `thick` facade (re-exports a crate + a tiny tail
# of local helpers) is NOT a bulk blocker: its weight already moved to a crate.
def is_blocker(dep: str) -> bool:
return dep in names and info[dep]["facade_class"] == "none"
for name, meta in info.items():
blockers = {d for d in meta["outbound"] if is_blocker(d) and d != name}
meta["in_root_blockers"] = blockers
meta["inbound_count"] = len(inbound.get(name, ()))
extractable_now = sorted(
(
n
for n, m in info.items()
if m["facade_class"] == "none"
and not n.endswith("_tests")
and not m["in_root_blockers"]
),
key=lambda n: -info[n]["loc"],
)
if "--json" in sys.argv:
out = {
n: {
"loc": m["loc"],
"facade_class": m["facade_class"],
"inbound": m["inbound_count"],
"in_root_blockers": sorted(m["in_root_blockers"]),
}
for n, m in info.items()
}
print(json.dumps({"modules": out, "extractable_now": extractable_now}, indent=2))
return 0
total = sum(m["loc"] for m in info.values())
nonfacade = sum(m["loc"] for m in info.values() if m["facade_class"] == "none")
thick = sum(m["loc"] for m in info.values() if m["facade_class"] == "thick")
print(f"root crate total loc (top-level modules): {total}")
print(f" fully in-root (no crate yet): {nonfacade}")
print(f" thick facades (crate + residual local): {thick}")
print(f" fully-facade loc: {total - nonfacade - thick}")
print()
print(f"{'module':24} {'loc':>7} {'fac':>5} {'in':>4} in-root blockers")
print("-" * 90)
for n in sorted(info, key=lambda n: -info[n]["loc"]):
m = info[n]
if m["facade_class"] == "fully" or n.endswith("_tests"):
continue
blk = ", ".join(sorted(m["in_root_blockers"])) or "-- (none: extractable now)"
print(f"{n:24} {m['loc']:>7} {m['facade_class']:>5} {m['inbound_count']:>4} {blk}")
print()
print("=== Extractable now (no in-root blockers), largest first ===")
for n in extractable_now:
print(f" {n:24} {info[n]['loc']:>7} loc, inbound={info[n]['inbound_count']}")
# SCC analysis over the in-root dependency graph (only real, non-fully-facade
# modules count as nodes/edges). Multi-node components are dependency cycles
# that must be broken before those modules can become independent crates.
graph = {
n: {d for d in m["outbound"] if d in info and info[d]["facade_class"] != "fully" and d != n}
for n, m in info.items()
if m["facade_class"] != "fully"
}
sccs = strongly_connected_components(graph)
cycles = [c for c in sccs if len(c) > 1]
cycles.sort(key=lambda c: -sum(info[n]["loc"] for n in c))
print()
print("=== Dependency cycles (SCCs > 1 node) — must break before clean split ===")
if not cycles:
print(" (none: the in-root module graph is already a DAG)")
for c in cycles:
cloc = sum(info[n]["loc"] for n in c)
member_str = ", ".join(sorted(c, key=lambda n: -info[n]["loc"]))
print(f" [{len(c)} modules, {cloc} loc] {member_str}")
# For the largest cycle, suggest the cheapest edges to cut/invert to make it
# acyclic (a feedback arc set). We use Eades' greedy linear-arrangement
# heuristic to get a vertex order, then report edges that go "backwards" in
# that order, weighted by reference count (cheap edges = easy refactors).
if cycles:
big = max(cycles, key=lambda c: sum(info[n]["loc"] for n in c))
sub = set(big)
# Build weighted subgraph restricted to the cycle.
out_edges = {
n: {d: info[n]["ref_counts"].get(d, 1) for d in info[n]["outbound"] if d in sub and d != n}
for n in big
}
order = eades_order(big, out_edges)
pos = {n: i for i, n in enumerate(order)}
back_edges = []
for u in big:
for v, w in out_edges[u].items():
if pos[v] < pos[u]: # edge points backwards => part of feedback set
back_edges.append((w, u, v))
back_edges.sort() # cheapest first
total_back = sum(w for w, _u, _v in back_edges)
print()
print(
f"=== Feedback arc set for the {len(big)}-module cycle "
f"({len(back_edges)} back-edges, {total_back} refs to invert) ==="
)
print(" Invert/cut these edges (cheapest first) to make the cycle a DAG:")
limit = 1000 if "--full" in sys.argv else 30
for w, u, v in back_edges[:limit]:
print(f" {u} -> {v} ({w} refs)")
if len(back_edges) > limit:
print(f" ... and {len(back_edges) - limit} more (use --full to list all)")
# Per-node eviction cost: a module only leaves the SCC once ALL of its
# out-edges into the cycle are cut. Rank cycle members by how few/cheap
# those out-edges are -- those are the cheapest modules to evict next
# (turning the SCC strictly smaller, which is what shrinks the largest
# compile unit). For each node, list its in-cycle out-edges.
evict = []
for n in big:
edges = sorted(
((w, d) for d, w in out_edges[n].items()),
key=lambda x: (x[0], x[1]),
)
n_edges = len(edges)
total_w = sum(w for w, _ in edges)
evict.append((n_edges, total_w, n, edges))
evict.sort(key=lambda x: (x[0], x[1], -info[x[2]]["loc"]))
print()
print(
"=== Cheapest modules to evict next from the cycle "
"(fewest in-cycle out-edges first) ==="
)
print(" A module leaves the SCC once all these out-edges are inverted/cut:")
ev_limit = 1000 if "--full" in sys.argv else 12
for n_edges, total_w, n, edges in evict[:ev_limit]:
tgt = ", ".join(f"{d}({w})" for w, d in edges) or "-- (none)"
print(
f" {n:<20} {info[n]['loc']:>7} loc "
f"{n_edges} edges / {total_w} refs -> {tgt}"
)
if len(evict) > ev_limit:
print(f" ... and {len(evict) - ev_limit} more (use --full to list all)")
return 0
def eades_order(nodes, out_edges):
"""Eades-Lin-Smyth greedy heuristic returning a vertex order that minimizes
backward edges (an approximate minimum feedback arc set)."""
remaining = set(nodes)
in_w = {n: 0 for n in nodes}
out_w = {n: 0 for n in nodes}
for u in nodes:
for v, w in out_edges[u].items():
out_w[u] += w
in_w[v] += w
left = []
right = []
# Work on mutable copies of degrees.
while remaining:
# Remove sinks (no outgoing within remaining) to the right.
changed = True
while changed:
changed = False
for n in list(remaining):
if all(v not in remaining for v in out_edges[n]):
right.insert(0, n)
remaining.discard(n)
changed = True
for n in list(remaining):
if all(u not in remaining for u in nodes if n in out_edges[u]):
# source (no incoming within remaining) to the left
left.append(n)
remaining.discard(n)
changed = True
if not remaining:
break
# Pick the node maximizing (out_w - in_w) within remaining.
def score(n):
o = sum(w for v, w in out_edges[n].items() if v in remaining)
i = sum(w for u in remaining for vv, w in out_edges[u].items() if vv == n)
return o - i
pick = max(remaining, key=score)
left.append(pick)
remaining.discard(pick)
return left + right
if __name__ == "__main__":
raise SystemExit(main())
+918
View File
@@ -0,0 +1,918 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import os
import statistics
from collections import Counter
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Iterable
DEFAULT_LOG_GLOB = "*runtime-memory-*.jsonl"
DEFAULT_TOP_N = 8
@dataclass
class Sample:
path: Path
line_no: int
raw: dict[str, Any]
timestamp_ms: int
kind: str
target: str
source: str
trigger_category: str
trigger_reason: str
sessions: dict[str, Any] | None
totals: dict[str, Any] | None
@property
def pss_bytes(self) -> int | None:
os_info = self.raw.get("process", {}).get("os") or {}
value = os_info.get("pss_bytes")
return int(value) if isinstance(value, int | float) else None
@property
def rss_bytes(self) -> int | None:
value = self.raw.get("process", {}).get("rss_bytes")
return int(value) if isinstance(value, int | float) else None
@property
def allocator_allocated_bytes(self) -> int | None:
value = (((self.raw.get("process") or {}).get("allocator") or {}).get("stats") or {}).get(
"allocated_bytes"
)
return int(value) if isinstance(value, int | float) else None
@property
def allocator_resident_bytes(self) -> int | None:
value = (((self.raw.get("process") or {}).get("allocator") or {}).get("stats") or {}).get(
"resident_bytes"
)
return int(value) if isinstance(value, int | float) else None
@property
def allocator_retained_bytes(self) -> int | None:
value = (((self.raw.get("process") or {}).get("allocator") or {}).get("stats") or {}).get(
"retained_bytes"
)
return int(value) if isinstance(value, int | float) else None
@property
def os_info(self) -> dict[str, Any]:
value = (self.raw.get("process") or {}).get("os")
return value if isinstance(value, dict) else {}
@property
def process_info(self) -> dict[str, Any]:
value = self.raw.get("process")
return value if isinstance(value, dict) else {}
@property
def process_diagnostics(self) -> dict[str, Any]:
value = self.raw.get("process_diagnostics")
return value if isinstance(value, dict) else {}
def first_int(mapping: dict[str, Any], *keys: str) -> int | None:
"""Return the first present integer value among candidate key names."""
for key in keys:
value = mapping.get(key)
if isinstance(value, int | float):
return int(value)
return None
@dataclass
class Spike:
start: Sample
end: Sample
delta_pss_bytes: int
@dataclass
class AttributionDelta:
start: Sample
end: Sample
delta_total_json_bytes: int
delta_payload_text_bytes: int
delta_provider_cache_json_bytes: int
delta_tool_result_bytes: int
delta_large_blob_bytes: int
delta_live_count: int
delta_memory_enabled_session_count: int
@property
def magnitude_bytes(self) -> int:
return max(
abs(self.delta_total_json_bytes),
abs(self.delta_provider_cache_json_bytes),
abs(self.delta_tool_result_bytes),
abs(self.delta_large_blob_bytes),
abs(self.delta_payload_text_bytes),
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Analyze jcode runtime memory JSONL logs for growth, spikes, attribution, and optimization hints"
)
parser.add_argument("paths", nargs="*", help="Specific JSONL files or directories to analyze")
parser.add_argument(
"--log-dir",
help="Directory containing runtime memory JSONL logs (default: ~/.jcode/logs/memory or $JCODE_HOME/logs/memory)",
)
parser.add_argument("--days", type=int, default=None, help="Only include files from the last N daily logs")
parser.add_argument("--top", type=int, default=DEFAULT_TOP_N, help="How many spikes/sessions/deltas to show")
parser.add_argument("--json", action="store_true", help="Emit machine-readable JSON summary")
parser.add_argument(
"--min-spike-mb",
type=float,
default=8.0,
help="Minimum absolute PSS delta in MB to include in spike lists",
)
return parser.parse_args()
def default_log_dir() -> Path:
jcode_home = os.environ.get("JCODE_HOME")
if jcode_home:
return Path(jcode_home).expanduser() / "logs" / "memory"
return Path.home() / ".jcode" / "logs" / "memory"
def resolve_paths(args: argparse.Namespace) -> list[Path]:
raw_paths = [Path(value).expanduser() for value in args.paths]
if args.log_dir:
raw_paths.append(Path(args.log_dir).expanduser())
if not raw_paths:
raw_paths.append(default_log_dir())
files: list[Path] = []
for raw in raw_paths:
if raw.is_file():
files.append(raw)
continue
if raw.is_dir():
files.extend(sorted(raw.glob(DEFAULT_LOG_GLOB)))
files = sorted(dict.fromkeys(path.resolve() for path in files))
if args.days is not None and args.days > 0:
selected_dates = []
for path in reversed(files):
date = extract_log_date(path)
if date is None or date in selected_dates:
continue
selected_dates.append(date)
if len(selected_dates) >= args.days:
break
files = [path for path in files if extract_log_date(path) in selected_dates]
return files
def extract_log_date(path: Path) -> str | None:
name = path.name
if not name.endswith('.jsonl'):
return None
stem = name[:-len('.jsonl')]
if '-' not in stem:
return None
return stem.rsplit('-', 3)[-3] + '-' + stem.rsplit('-', 3)[-2] + '-' + stem.rsplit('-', 3)[-1]
def load_samples(paths: Iterable[Path]) -> list[Sample]:
samples: list[Sample] = []
for path in paths:
try:
lines = path.read_text().splitlines()
except FileNotFoundError:
continue
for idx, line in enumerate(lines, start=1):
line = line.strip()
if not line:
continue
try:
raw = json.loads(line)
except json.JSONDecodeError:
continue
trigger = raw.get("trigger") or {}
source = str(raw.get("source") or "")
kind = infer_kind(raw, source)
target = infer_target(raw, path)
trigger_category, trigger_reason = infer_trigger(raw, kind, source, trigger)
samples.append(
Sample(
path=path,
line_no=idx,
raw=raw,
timestamp_ms=int(raw.get("timestamp_ms") or 0),
kind=kind,
target=target,
source=source,
trigger_category=trigger_category,
trigger_reason=trigger_reason,
sessions=raw.get("sessions") if isinstance(raw.get("sessions"), dict) else None,
totals=raw.get("totals") if isinstance(raw.get("totals"), dict) else None,
)
)
samples.sort(key=lambda sample: (sample.timestamp_ms, str(sample.path), sample.line_no))
return samples
def infer_kind(raw: dict[str, Any], source: str) -> str:
kind = raw.get("kind")
if isinstance(kind, str) and kind:
return kind
if isinstance(raw.get("sessions"), dict):
return "attribution"
if source.startswith("process:"):
return "process"
if source.startswith("attribution:"):
return "attribution"
return "legacy"
def infer_target(raw: dict[str, Any], path: Path) -> str:
if isinstance(raw.get("client"), dict):
return "client"
if isinstance(raw.get("server"), dict):
return "server"
name = path.name
if name.startswith("client-runtime-memory-"):
return "client"
return "server"
def infer_trigger(
raw: dict[str, Any], kind: str, source: str, trigger: dict[str, Any]
) -> tuple[str, str]:
category = str(trigger.get("category") or "")
reason = str(trigger.get("reason") or "")
if category and reason:
return category, reason
if source == "startup" or source.endswith(":startup"):
return category or "startup", reason or "server_start"
if source == "interval" or source.startswith("process:heartbeat"):
return category or "process_heartbeat", reason or "periodic"
if source.startswith("attribution:heartbeat"):
return category or "attribution_heartbeat", reason or "periodic"
if source.startswith("attribution:event:") or source.startswith("process:event:"):
suffix = source.split(":event:", 1)[1]
return category or suffix, reason or "event"
if source.startswith("server:runtime-log:"):
suffix = source.rsplit(":", 1)[-1]
return category or suffix, reason or ("periodic" if suffix == "interval" else kind)
return category or kind, reason or "legacy"
def bytes_to_mb(value: int | None) -> float | None:
if value is None:
return None
return round(value / (1024.0 * 1024.0), 1)
def fmt_mb(value: int | None) -> str:
if value is None:
return "n/a"
return f"{value / (1024.0 * 1024.0):.1f} MB"
def fmt_signed_mb(value: int | None) -> str:
if value is None:
return "n/a"
sign = "+" if value >= 0 else "-"
return f"{sign}{abs(value) / (1024.0 * 1024.0):.1f} MB"
def fmt_duration_ms(ms: int) -> str:
seconds = ms / 1000.0
if seconds < 60:
return f"{seconds:.1f}s"
minutes = seconds / 60.0
if minutes < 60:
return f"{minutes:.1f}m"
hours = minutes / 60.0
return f"{hours:.1f}h"
def fmt_ts(timestamp_ms: int) -> str:
dt = datetime.fromtimestamp(timestamp_ms / 1000.0, tz=timezone.utc)
return dt.isoformat().replace("+00:00", "Z")
def attributed_total_bytes(sample: Sample) -> int | None:
if sample.sessions:
value = sample.sessions.get("total_json_bytes")
return int(value) if isinstance(value, int | float) else None
if sample.totals:
value = sample.totals.get("total_attributed_bytes")
return int(value) if isinstance(value, int | float) else None
return None
def compute_spikes(samples: list[Sample], min_spike_bytes: int) -> list[Spike]:
process_samples = [sample for sample in samples if sample.pss_bytes is not None]
spikes: list[Spike] = []
for prev, curr in zip(process_samples, process_samples[1:]):
if prev.pss_bytes is None or curr.pss_bytes is None:
continue
delta = curr.pss_bytes - prev.pss_bytes
if abs(delta) >= min_spike_bytes:
spikes.append(Spike(start=prev, end=curr, delta_pss_bytes=delta))
spikes.sort(key=lambda spike: abs(spike.delta_pss_bytes), reverse=True)
return spikes
def compute_attribution_deltas(samples: list[Sample]) -> list[AttributionDelta]:
attribution = [sample for sample in samples if sample.sessions]
deltas: list[AttributionDelta] = []
for prev, curr in zip(attribution, attribution[1:]):
prev_sessions = prev.sessions or {}
curr_sessions = curr.sessions or {}
deltas.append(
AttributionDelta(
start=prev,
end=curr,
delta_total_json_bytes=int(curr_sessions.get("total_json_bytes", 0))
- int(prev_sessions.get("total_json_bytes", 0)),
delta_payload_text_bytes=int(curr_sessions.get("total_payload_text_bytes", 0))
- int(prev_sessions.get("total_payload_text_bytes", 0)),
delta_provider_cache_json_bytes=int(curr_sessions.get("total_provider_cache_json_bytes", 0))
- int(prev_sessions.get("total_provider_cache_json_bytes", 0)),
delta_tool_result_bytes=int(curr_sessions.get("total_tool_result_bytes", 0))
- int(prev_sessions.get("total_tool_result_bytes", 0)),
delta_large_blob_bytes=int(curr_sessions.get("total_large_blob_bytes", 0))
- int(prev_sessions.get("total_large_blob_bytes", 0)),
delta_live_count=int(curr_sessions.get("live_count", 0))
- int(prev_sessions.get("live_count", 0)),
delta_memory_enabled_session_count=int(curr_sessions.get("memory_enabled_session_count", 0))
- int(prev_sessions.get("memory_enabled_session_count", 0)),
)
)
deltas.sort(key=lambda delta: delta.magnitude_bytes, reverse=True)
return deltas
def collect_session_peaks(samples: list[Sample]) -> list[dict[str, Any]]:
session_stats: dict[str, dict[str, Any]] = {}
for sample in samples:
sessions = sample.sessions or {}
top = sessions.get("top_by_json_bytes") or []
if not isinstance(top, list):
continue
for entry in top:
if not isinstance(entry, dict):
continue
session_id = str(entry.get("session_id") or "")
if not session_id:
continue
json_bytes = int(entry.get("json_bytes") or 0)
current = session_stats.get(session_id)
if current is None or json_bytes > current["peak_json_bytes"]:
session_stats[session_id] = {
"session_id": session_id,
"provider": entry.get("provider"),
"model": entry.get("model"),
"memory_enabled": bool(entry.get("memory_enabled")),
"peak_json_bytes": json_bytes,
"peak_payload_text_bytes": int(entry.get("payload_text_bytes") or 0),
"peak_provider_cache_json_bytes": int(entry.get("provider_cache_json_bytes") or 0),
"peak_tool_result_bytes": int(entry.get("tool_result_bytes") or 0),
"peak_large_blob_bytes": int(entry.get("large_blob_bytes") or 0),
"message_count": int(entry.get("message_count") or 0),
"last_seen_timestamp_ms": sample.timestamp_ms,
}
return sorted(session_stats.values(), key=lambda item: item["peak_json_bytes"], reverse=True)
def last_attribution_sample(samples: list[Sample]) -> Sample | None:
for sample in reversed(samples):
if sample.sessions or sample.totals:
return sample
return None
def build_coverage_report(sample: Sample) -> dict[str, Any]:
"""Decompose PSS into attributed live, unattributed live heap, allocator
retention, file-backed, and stack buckets.
Uses allocator stats (mallinfo2/jemalloc) plus the newer smaps_rollup and
process_diagnostics fields when present; older logs degrade gracefully to
whatever fields exist. The key outputs are two coverage ratios:
- coverage_ratio_pss: attributed / PSS (the historical, misleading one; the
denominator includes allocator retention and file maps).
- coverage_ratio_live_heap: attributed / allocator live bytes (estimator
quality against the memory the app actually holds).
Explained PSS follows the in-binary summary definition:
total_attributed + allocator_retained_resident_estimate + pss_file +
thread_stack_estimate.
"""
pss = sample.pss_bytes
attributed = attributed_total_bytes(sample)
allocator_live = sample.allocator_allocated_bytes
allocator_retained = sample.allocator_retained_bytes
process_info = sample.process_info
os_info = sample.os_info
pss_file = first_int(os_info, "pss_file_bytes")
pss_anon = first_int(os_info, "pss_anon_bytes")
pss_shmem = first_int(os_info, "pss_shmem_bytes")
anon_huge_pages = first_int(os_info, "anon_huge_pages_bytes")
rss_file = first_int(os_info, "rss_file_bytes")
stack_bytes = first_int(process_info, "main_stack_bytes") or first_int(
os_info, "main_stack_bytes", "stack_bytes", "vm_stk_bytes"
)
thread_count = first_int(process_info, "thread_count") or first_int(
os_info, "thread_count", "threads"
)
diagnostics = sample.process_diagnostics
retained_resident = first_int(
diagnostics,
"allocator_retained_resident_estimate_bytes",
"allocator_retained_bytes",
)
if retained_resident is None:
retained_resident = allocator_retained
thread_stack_estimate = first_int(diagnostics, "thread_stack_estimate_bytes")
if thread_stack_estimate is None:
thread_stack_estimate = stack_bytes
report: dict[str, Any] = {
"timestamp_ms": sample.timestamp_ms,
"pss_bytes": pss,
"pss_anon_bytes": pss_anon,
"pss_file_bytes": pss_file,
"pss_shmem_bytes": pss_shmem,
"anon_huge_pages_bytes": anon_huge_pages,
"rss_file_bytes": rss_file,
"main_stack_bytes": stack_bytes,
"thread_stack_estimate_bytes": thread_stack_estimate,
"thread_count": thread_count,
"attributed_live_bytes": attributed,
"allocator_live_bytes": allocator_live,
"allocator_retained_bytes": allocator_retained,
"allocator_retained_resident_estimate_bytes": retained_resident,
}
if attributed is not None and allocator_live is not None:
report["unattributed_live_heap_bytes"] = max(0, allocator_live - attributed)
if pss is not None and attributed is not None:
report["coverage_ratio_pss"] = round(attributed / pss, 4) if pss else 0.0
if allocator_live is not None and attributed is not None:
report["coverage_ratio_live_heap"] = (
round(attributed / allocator_live, 4) if allocator_live else 0.0
)
# Explained PSS matches the in-binary summary: attributed live + allocator
# retention (resident estimate) + file-backed PSS + thread stacks. The
# remainder is what the buckets still miss (unattributed live heap, shared
# anon, allocator metadata).
if pss is not None:
explained = 0
for key in (
"attributed_live_bytes",
"allocator_retained_resident_estimate_bytes",
"pss_file_bytes",
"thread_stack_estimate_bytes",
):
value = report.get(key)
if isinstance(value, int):
explained += value
report["explained_pss_bytes"] = explained
report["unexplained_pss_bytes"] = max(0, pss - explained)
report["explained_ratio"] = round(explained / pss, 4) if pss else 0.0
return report
def count_event_categories(samples: list[Sample]) -> Counter[str]:
counter: Counter[str] = Counter()
for sample in samples:
category = sample.trigger_category or sample.kind
counter[category] += 1
return counter
def process_summary(samples: list[Sample]) -> dict[str, Any]:
process_samples = [sample for sample in samples if sample.pss_bytes is not None]
if not process_samples:
return {}
first = process_samples[0]
last = process_samples[-1]
peak = max(process_samples, key=lambda sample: sample.pss_bytes or -1)
pss_values = [sample.pss_bytes for sample in process_samples if sample.pss_bytes is not None]
median_pss = int(statistics.median(pss_values)) if pss_values else None
return {
"sample_count": len(process_samples),
"first_timestamp_ms": first.timestamp_ms,
"last_timestamp_ms": last.timestamp_ms,
"duration_ms": max(0, last.timestamp_ms - first.timestamp_ms),
"baseline_pss_bytes": first.pss_bytes,
"final_pss_bytes": last.pss_bytes,
"net_pss_growth_bytes": (last.pss_bytes or 0) - (first.pss_bytes or 0),
"peak_pss_bytes": peak.pss_bytes,
"peak_growth_vs_baseline_bytes": (peak.pss_bytes or 0) - (first.pss_bytes or 0),
"median_pss_bytes": median_pss,
"peak_timestamp_ms": peak.timestamp_ms,
"peak_trigger_category": peak.trigger_category,
"peak_trigger_reason": peak.trigger_reason,
"allocator_allocated_bytes": last.allocator_allocated_bytes,
"allocator_resident_bytes": last.allocator_resident_bytes,
"allocator_retained_bytes": last.allocator_retained_bytes,
}
def build_server_hints(samples: list[Sample], session_peaks: list[dict[str, Any]]) -> list[str]:
hints: list[str] = []
last_attr = last_attribution_sample(samples)
if not last_attr or not last_attr.sessions:
return ["Need at least one attribution sample before generating optimization hints."]
sessions = last_attr.sessions
total_json = int(sessions.get("total_json_bytes") or 0)
provider_cache_json = int(sessions.get("total_provider_cache_json_bytes") or 0)
tool_result_bytes = int(sessions.get("total_tool_result_bytes") or 0)
large_blob_bytes = int(sessions.get("total_large_blob_bytes") or 0)
payload_text_bytes = int(sessions.get("total_payload_text_bytes") or 0)
if total_json > 0 and provider_cache_json / total_json >= 0.35:
hints.append(
f"Provider cache is a large share of attributed state ({provider_cache_json / total_json:.0%} of total JSON). Prioritize cache compaction, cache invalidation discipline, and avoiding redundant provider-message mirrors."
)
if total_json > 0 and tool_result_bytes / total_json >= 0.25:
hints.append(
f"Tool results are heavy ({tool_result_bytes / total_json:.0%} of total JSON). Consider truncating stored tool output, summarizing verbose results, or storing large artifacts out-of-line."
)
if total_json > 0 and large_blob_bytes / total_json >= 0.15:
hints.append(
f"Large blobs are materially retained ({large_blob_bytes / total_json:.0%} of total JSON). Focus on blob thresholds, attachment retention, and aggressive post-use slimming."
)
if payload_text_bytes > 0 and total_json > 0 and payload_text_bytes / total_json >= 0.45:
hints.append(
f"Transcript payload text dominates attributed state ({payload_text_bytes / total_json:.0%} of total JSON). Compaction and transcript summarization will likely pay off."
)
last_process = samples[-1] if samples else None
process_diag = (last_process.raw.get("process_diagnostics") or {}) if last_process else {}
resident_minus_active = process_diag.get("allocator_resident_minus_active_bytes")
pss_minus_allocated = process_diag.get("pss_minus_allocator_allocated_bytes")
if isinstance(resident_minus_active, int) and resident_minus_active >= 64 * 1024 * 1024:
hints.append(
f"Allocator resident slack is high ({fmt_mb(resident_minus_active)} above active). Some memory pressure may be allocator retention rather than live app state."
)
if isinstance(pss_minus_allocated, int) and pss_minus_allocated >= 64 * 1024 * 1024:
hints.append(
f"PSS is materially above allocator allocated ({fmt_mb(pss_minus_allocated)} delta), suggesting shared mappings, allocator overhead, or retained pages are worth checking alongside app-owned structures."
)
embedding_events = [s for s in samples if s.trigger_category in {"embedding_loaded", "embedding_unloaded"}]
if embedding_events:
hints.append(
f"Embedding lifecycle events were observed ({len(embedding_events)} samples). Compare memory before/after those windows to decide whether local embeddings should unload more aggressively."
)
if session_peaks:
heaviest = session_peaks[0]
hints.append(
f"Heaviest observed session was {heaviest['session_id']} at {fmt_mb(heaviest['peak_json_bytes'])} attributed JSON. Start optimization work with that sessions transcript and tool-result profile."
)
if not hints:
hints.append("No single dominant culprit stood out yet. Collect more runtime history and compare multiple attribution samples after heavier real usage.")
return hints
def collect_client_peaks(samples: list[Sample]) -> list[dict[str, Any]]:
client_stats: dict[str, dict[str, Any]] = {}
for sample in samples:
if not sample.totals:
continue
client = sample.raw.get("client") or {}
session_id = str(client.get("session_id") or "")
if not session_id:
continue
total = int(sample.totals.get("total_attributed_bytes") or 0)
current = client_stats.get(session_id)
if current is None or total > current["peak_total_attributed_bytes"]:
client_stats[session_id] = {
"session_id": session_id,
"client_instance_id": client.get("client_instance_id"),
"provider": client.get("provider"),
"model": client.get("model"),
"is_remote": bool(client.get("is_remote")),
"peak_total_attributed_bytes": total,
"peak_display_messages_estimate_bytes": int(sample.totals.get("display_messages_estimate_bytes") or 0),
"peak_provider_messages_json_bytes": int(sample.totals.get("provider_messages_json_bytes") or 0),
"peak_side_panel_estimate_bytes": int(sample.totals.get("side_panel_estimate_bytes") or 0),
"peak_remote_state_bytes": int(sample.totals.get("remote_state_bytes") or 0),
"last_seen_timestamp_ms": sample.timestamp_ms,
}
return sorted(client_stats.values(), key=lambda item: item["peak_total_attributed_bytes"], reverse=True)
def build_client_hints(samples: list[Sample], client_peaks: list[dict[str, Any]]) -> list[str]:
hints: list[str] = []
last_attr = last_attribution_sample(samples)
if not last_attr or not last_attr.totals:
return ["Need at least one client attribution sample before generating optimization hints."]
totals = last_attr.totals
total = int(totals.get("total_attributed_bytes") or 0)
display = int(totals.get("display_messages_estimate_bytes") or 0)
provider_messages = int(totals.get("provider_messages_json_bytes") or 0)
side_panel = int(totals.get("side_panel_estimate_bytes") or 0)
remote_state = int(totals.get("remote_state_bytes") or 0)
if total > 0 and display / total >= 0.45:
hints.append(
f"Display-message state is a large share of attributed client memory ({display / total:.0%}). Tighten UI duplication and display-history retention first."
)
if total > 0 and provider_messages / total >= 0.20:
hints.append(
f"Resident provider-message copies are a meaningful share of client memory ({provider_messages / total:.0%}). Prefer borrowing or lazy hydration where possible."
)
if total > 0 and side_panel / total >= 0.15:
hints.append(
f"Side-panel state is materially retained ({side_panel / total:.0%} of attributed client memory). Focus on page content retention and render-cache discipline."
)
if total > 0 and remote_state / total >= 0.10:
hints.append(
f"Remote session metadata is non-trivial ({remote_state / total:.0%} of attributed client memory). Review retained model/session lists and remote bootstrap state."
)
coverage = build_coverage_report(last_attr)
pss = coverage.get("pss_bytes")
retained = coverage.get("allocator_retained_resident_estimate_bytes")
if isinstance(pss, int) and isinstance(retained, int) and pss > 0 and retained / pss >= 0.30:
hints.append(
f"Allocator retention dominates PSS ({retained / pss:.0%}, {fmt_mb(retained)}). This is freed-but-held heap, not live app state; malloc_trim/purge cadence matters more than estimator coverage here."
)
unattributed_live = coverage.get("unattributed_live_heap_bytes")
live = coverage.get("allocator_live_bytes")
if (
isinstance(unattributed_live, int)
and isinstance(live, int)
and live > 0
and unattributed_live / live >= 0.40
):
hints.append(
f"Estimators miss {unattributed_live / live:.0%} of live heap ({fmt_mb(unattributed_live)}). The real estimator gap is tokio/render/runtime structures, not allocator slack."
)
if client_peaks:
heaviest = client_peaks[0]
hints.append(
f"Heaviest observed client session was {heaviest['session_id']} at {fmt_mb(heaviest['peak_total_attributed_bytes'])} attributed client memory. Start with that sessions display and provider-message layers."
)
if not hints:
hints.append("No single dominant client-side culprit stood out yet. Collect more client runtime history during heavier UI usage.")
return hints
def summarize_target(samples: list[Sample], top_n: int, min_spike_bytes: int) -> dict[str, Any]:
spikes = compute_spikes(samples, min_spike_bytes=min_spike_bytes)
target = samples[0].target if samples else "unknown"
deltas = compute_attribution_deltas(samples) if target == "server" else []
session_peaks = collect_session_peaks(samples) if target == "server" else []
client_peaks = collect_client_peaks(samples) if target == "client" else []
event_counts = count_event_categories(samples)
proc = process_summary(samples)
last_attr = last_attribution_sample(samples)
coverage = build_coverage_report(last_attr) if last_attr else None
summary = {
"target": target,
"sample_count": len(samples),
"first_timestamp_ms": samples[0].timestamp_ms if samples else None,
"last_timestamp_ms": samples[-1].timestamp_ms if samples else None,
"kinds": Counter(sample.kind for sample in samples),
"process": proc,
"coverage": coverage,
"last_attribution": {
"timestamp_ms": last_attr.timestamp_ms,
"sessions": last_attr.sessions,
"totals": last_attr.totals,
"client": last_attr.raw.get("client") if target == "client" else None,
"trigger_category": last_attr.trigger_category,
"trigger_reason": last_attr.trigger_reason,
}
if last_attr
else None,
"top_spikes": [
{
"from": spike.start.timestamp_ms,
"to": spike.end.timestamp_ms,
"delta_pss_bytes": spike.delta_pss_bytes,
"from_source": spike.start.source,
"to_source": spike.end.source,
"to_trigger_category": spike.end.trigger_category,
"to_trigger_reason": spike.end.trigger_reason,
}
for spike in spikes[:top_n]
],
"top_attribution_deltas": [
{
"from": delta.start.timestamp_ms,
"to": delta.end.timestamp_ms,
"to_trigger_category": delta.end.trigger_category,
"to_trigger_reason": delta.end.trigger_reason,
"delta_total_json_bytes": delta.delta_total_json_bytes,
"delta_payload_text_bytes": delta.delta_payload_text_bytes,
"delta_provider_cache_json_bytes": delta.delta_provider_cache_json_bytes,
"delta_tool_result_bytes": delta.delta_tool_result_bytes,
"delta_large_blob_bytes": delta.delta_large_blob_bytes,
"delta_live_count": delta.delta_live_count,
"delta_memory_enabled_session_count": delta.delta_memory_enabled_session_count,
}
for delta in deltas[:top_n]
],
"top_sessions": session_peaks[:top_n],
"top_clients": client_peaks[:top_n],
"event_counts": dict(event_counts.most_common()),
"hints": build_server_hints(samples, session_peaks[:top_n])
if target == "server"
else build_client_hints(samples, client_peaks[:top_n]),
}
return summary
def summarize(samples: list[Sample], top_n: int, min_spike_bytes: int) -> dict[str, Any]:
targets = sorted({sample.target for sample in samples})
if len(targets) <= 1:
return summarize_target(samples, top_n=top_n, min_spike_bytes=min_spike_bytes)
return {
"targets": {
target: summarize_target(
[sample for sample in samples if sample.target == target],
top_n=top_n,
min_spike_bytes=min_spike_bytes,
)
for target in targets
}
}
def print_human(summary: dict[str, Any], paths: list[Path]) -> None:
if "targets" in summary:
print("Runtime Memory Log Analysis")
print("===========================")
if paths:
print(f"files: {len(paths)}")
for path in paths:
print(f" - {path}")
for target, target_summary in summary["targets"].items():
print(f"\n[{target}]")
print_human(target_summary, [])
return
print("Runtime Memory Log Analysis")
print("===========================")
if paths:
print(f"files: {len(paths)}")
for path in paths:
print(f" - {path}")
print(f"samples: {summary['sample_count']}")
if summary.get("first_timestamp_ms") is not None:
print(f"window: {fmt_ts(summary['first_timestamp_ms'])} -> {fmt_ts(summary['last_timestamp_ms'])}")
print(
f"duration: {fmt_duration_ms(summary['last_timestamp_ms'] - summary['first_timestamp_ms'])}"
)
proc = summary.get("process") or {}
if proc:
print("\nProcess memory")
print("--------------")
print(f"baseline PSS: {fmt_mb(proc.get('baseline_pss_bytes'))}")
print(f"final PSS: {fmt_mb(proc.get('final_pss_bytes'))} ({fmt_signed_mb(proc.get('net_pss_growth_bytes'))})")
print(f"peak PSS: {fmt_mb(proc.get('peak_pss_bytes'))} ({fmt_signed_mb(proc.get('peak_growth_vs_baseline_bytes'))} vs baseline)")
print(f"median PSS: {fmt_mb(proc.get('median_pss_bytes'))}")
peak_ts = proc.get("peak_timestamp_ms")
if peak_ts is not None:
print(
f"peak trigger: {fmt_ts(peak_ts)} | {proc.get('peak_trigger_category') or 'unknown'} / {proc.get('peak_trigger_reason') or 'unknown'}"
)
print(
f"allocator: allocated {fmt_mb(proc.get('allocator_allocated_bytes'))} | resident {fmt_mb(proc.get('allocator_resident_bytes'))} | retained {fmt_mb(proc.get('allocator_retained_bytes'))}"
)
coverage = summary.get("coverage") or {}
if coverage:
print("\nAttribution coverage (last attribution sample)")
print("----------------------------------------------")
print(f"PSS: {fmt_mb(coverage.get('pss_bytes'))}")
if coverage.get("pss_anon_bytes") is not None or coverage.get("pss_file_bytes") is not None:
print(
f"PSS split: anon {fmt_mb(coverage.get('pss_anon_bytes'))} | file {fmt_mb(coverage.get('pss_file_bytes'))} | shmem {fmt_mb(coverage.get('pss_shmem_bytes'))}"
)
print(f"attributed live: {fmt_mb(coverage.get('attributed_live_bytes'))}")
print(f"allocator live: {fmt_mb(coverage.get('allocator_live_bytes'))}")
if coverage.get("unattributed_live_heap_bytes") is not None:
print(f"unattributed live: {fmt_mb(coverage.get('unattributed_live_heap_bytes'))}")
print(
f"allocator retained: {fmt_mb(coverage.get('allocator_retained_resident_estimate_bytes'))}"
)
if coverage.get("main_stack_bytes") is not None or coverage.get("thread_count") is not None:
threads = coverage.get("thread_count")
print(
f"stacks/threads: main stack {fmt_mb(coverage.get('main_stack_bytes'))} | stack estimate {fmt_mb(coverage.get('thread_stack_estimate_bytes'))} | threads {threads if threads is not None else 'n/a'}"
)
ratio_pss = coverage.get("coverage_ratio_pss")
ratio_live = coverage.get("coverage_ratio_live_heap")
if ratio_pss is not None or ratio_live is not None:
pss_text = f"{ratio_pss:.1%}" if isinstance(ratio_pss, int | float) else "n/a"
live_text = f"{ratio_live:.1%}" if isinstance(ratio_live, int | float) else "n/a"
print(f"coverage: vs PSS {pss_text} | vs live heap {live_text}")
if coverage.get("explained_pss_bytes") is not None:
explained_ratio = coverage.get("explained_ratio")
ratio_text = (
f"{explained_ratio:.1%}" if isinstance(explained_ratio, int | float) else "n/a"
)
print(
f"explained PSS: {fmt_mb(coverage.get('explained_pss_bytes'))} ({ratio_text}) | unexplained {fmt_mb(coverage.get('unexplained_pss_bytes'))}"
)
print("\nEvent counts")
print("------------")
for category, count in list((summary.get("event_counts") or {}).items())[:12]:
print(f"{category}: {count}")
print("\nTop PSS spikes")
print("-------------")
spikes = summary.get("top_spikes") or []
if not spikes:
print("No spikes above threshold.")
for spike in spikes:
print(
f"{fmt_ts(spike['from'])} -> {fmt_ts(spike['to'])} | {fmt_signed_mb(spike['delta_pss_bytes'])} | {spike['to_trigger_category'] or 'unknown'} / {spike['to_trigger_reason'] or 'unknown'}"
)
print("\nTop attribution deltas")
print("----------------------")
deltas = summary.get("top_attribution_deltas") or []
if not deltas:
print("Need at least two attribution samples.")
for delta in deltas:
print(
f"{fmt_ts(delta['from'])} -> {fmt_ts(delta['to'])} | total {fmt_signed_mb(delta['delta_total_json_bytes'])} | cache {fmt_signed_mb(delta['delta_provider_cache_json_bytes'])} | tool {fmt_signed_mb(delta['delta_tool_result_bytes'])} | blob {fmt_signed_mb(delta['delta_large_blob_bytes'])} | text {fmt_signed_mb(delta['delta_payload_text_bytes'])} | {delta['to_trigger_category'] or 'unknown'}"
)
target = summary.get("target") or "server"
section_title = "Heaviest sessions" if target == "server" else "Heaviest clients"
print(f"\n{section_title}")
print("-" * len(section_title))
sessions = summary.get("top_sessions") or []
clients = summary.get("top_clients") or []
if not sessions:
if target == "server":
print("No per-session attribution data yet.")
for item in sessions:
print(
f"{item['session_id']} | peak json {fmt_mb(item['peak_json_bytes'])} | provider cache {fmt_mb(item['peak_provider_cache_json_bytes'])} | tool results {fmt_mb(item['peak_tool_result_bytes'])} | large blobs {fmt_mb(item['peak_large_blob_bytes'])} | provider={item.get('provider') or 'unknown'} model={item.get('model') or 'unknown'}"
)
if target == "client":
if not clients:
print("No per-client attribution data yet.")
for item in clients:
print(
f"{item['session_id']} | peak attributed {fmt_mb(item['peak_total_attributed_bytes'])} | display {fmt_mb(item['peak_display_messages_estimate_bytes'])} | provider view {fmt_mb(item['peak_provider_messages_json_bytes'])} | side panel {fmt_mb(item['peak_side_panel_estimate_bytes'])} | provider={item.get('provider') or 'unknown'} model={item.get('model') or 'unknown'}"
)
print("\nOptimization hints")
print("------------------")
for hint in summary.get("hints") or []:
print(f"- {hint}")
def to_jsonable(value: Any) -> Any:
if isinstance(value, Counter):
return dict(value)
if isinstance(value, dict):
return {key: to_jsonable(inner) for key, inner in value.items()}
if isinstance(value, list):
return [to_jsonable(item) for item in value]
return value
def main() -> int:
args = parse_args()
paths = resolve_paths(args)
if not paths:
raise SystemExit("No runtime memory log files found.")
samples = load_samples(paths)
if not samples:
raise SystemExit("No runtime memory samples found in selected files.")
summary = summarize(samples, top_n=args.top, min_spike_bytes=int(args.min_spike_mb * 1024 * 1024))
if args.json:
payload = to_jsonable(summary)
payload["files"] = [str(path) for path in paths]
print(json.dumps(payload, indent=2))
else:
print_human(summary, paths)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# Live provider test coverage for Antigravity models.
# For each model: (1) chat smoke -> expect token, (2) multi-turn tool smoke
# (two bash calls, exercises thought_signature replay) -> expect both outputs.
set -uo pipefail
JC=./target/selfdev/jcode
CHAT_PROMPT='Reply with exactly: SMOKE_OK'
TOOL_PROMPT="Run 'echo aa11' with bash, then in a SECOND separate bash call run 'echo bb22', then report both outputs."
CHAT_TIMEOUT=90
TOOL_TIMEOUT=160
# Models to test (chat-capable). Skip tab_*/chat_* (autocomplete/internal) and 'default' alias dup.
MODELS=(
claude-opus-4-6-thinking
claude-sonnet-4-6
gemini-3.1-pro-high
gemini-3.1-pro-low
gemini-3-flash
gemini-3-flash-agent
gemini-3.5-flash-low
gpt-oss-120b-medium
gemini-2.5-flash
gemini-2.5-flash-lite
gemini-2.5-flash-thinking
gemini-2.5-pro
gemini-3.1-flash-lite
gemini-3.5-flash-extra-low
gemini-pro-agent
default
)
printf "%-30s | %-8s | %-10s | %s\n" "MODEL" "CHAT" "TOOL" "NOTES"
printf -- "------------------------------------------------------------------------------------\n"
total=${#MODELS[@]}
i=0
for m in "${MODELS[@]}"; do
i=$((i+1))
echo "JCODE_PROGRESS {\"current\":$i,\"total\":$total,\"unit\":\"models\",\"message\":\"$m\"}" >&2
# --- chat smoke ---
chat_out=$(timeout "$CHAT_TIMEOUT" "$JC" run --provider antigravity -m "$m" --no-update --no-selfdev "$CHAT_PROMPT" 2>&1)
chat_rc=$?
if [[ $chat_rc -ne 0 ]]; then
chat="FAIL"
elif grep -q "SMOKE_OK" <<<"$chat_out"; then
chat="PASS"
else
chat="NO_TOKEN"
fi
# --- tool smoke (multi-turn) ---
note=""
tool_out=$(timeout "$TOOL_TIMEOUT" "$JC" run --provider antigravity -m "$m" --no-update --no-selfdev "$TOOL_PROMPT" 2>&1)
tool_rc=$?
if [[ $tool_rc -ne 0 ]]; then
tool="FAIL"
note=$(grep -oiE "missing a thought_signature|400|schema|draft 2020|HTTP [0-9]+|error[^\"]{0,40}" <<<"$tool_out" | head -1 | tr -d '\n')
[[ $tool_rc -eq 124 ]] && note="timeout"
elif grep -q "aa11" <<<"$tool_out" && grep -q "bb22" <<<"$tool_out"; then
tool="PASS"
else
tool="PARTIAL"
note="missing one output"
fi
printf "%-30s | %-8s | %-10s | %s\n" "$m" "$chat" "$tool" "$note"
done
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# Isolate which JSON-Schema construct the Antigravity Cloud Code backend's
# Gemini->Anthropic translation rejects for Claude models.
#
# For each JCODE_SCHEMA_STRIP combination, run a single tool-using prompt
# against a Claude model and report PASS (tool ran) / SCHEMA (draft-2020-12
# rejection) / OTHER. Requires a valid Antigravity OAuth session.
set -uo pipefail
JC="${JC:-./target/selfdev/jcode}"
MODEL="${MODEL:-claude-sonnet-4-6}"
PROMPT="${PROMPT:-Run 'echo PROBE_OK' with the bash tool and report the output.}"
TIMEOUT="${TIMEOUT:-120}"
STRIPS=(
"" # baseline (current behaviour)
"anyof"
"addprops"
"numitems"
"anyof,addprops"
"anyof,addprops,numitems"
)
printf "%-28s | %-8s | %s\n" "STRIP" "RESULT" "DETAIL"
printf -- "----------------------------------------------------------------------\n"
for strip in "${STRIPS[@]}"; do
out=$(JCODE_SCHEMA_STRIP="$strip" timeout "$TIMEOUT" \
"$JC" run --provider antigravity -m "$MODEL" --no-update --no-selfdev "$PROMPT" 2>&1)
rc=$?
label="${strip:-<none>}"
if [[ $rc -eq 124 ]]; then
printf "%-28s | %-8s | %s\n" "$label" "TIMEOUT" ""
elif grep -q "PROBE_OK" <<<"$out"; then
printf "%-28s | %-8s | %s\n" "$label" "PASS" "tool executed"
elif grep -qiE "draft 2020|2020-12|schema" <<<"$out"; then
detail=$(grep -oiE "[^\"]{0,60}(draft 2020|2020-12|schema)[^\"]{0,60}" <<<"$out" | head -1 | tr -s ' \n' ' ')
printf "%-28s | %-8s | %s\n" "$label" "SCHEMA" "$detail"
else
detail=$(grep -oiE "HTTP [0-9]+[^\"]{0,80}" <<<"$out" | head -1 | tr -s ' \n' ' ')
printf "%-28s | %-8s | %s\n" "$label" "OTHER" "${detail:-see logs}"
fi
done
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
DISALLOWED_NON_NULL_KEYS = {
"override_timeout_sec",
"max_timeout_sec",
"override_setup_timeout_sec",
"override_cpus",
"override_memory_mb",
"override_storage_mb",
"override_gpus",
"agent_timeout_multiplier",
"verifier_timeout_multiplier",
"agent_setup_timeout_multiplier",
"environment_build_timeout_multiplier",
}
FORBIDDEN_LOG_TERMS = (
"tbench.ai",
"terminal-bench.org",
"github.com/harbor-framework/terminal-bench",
"github.com/laude-institute/terminal-bench",
"terminal-bench leaderboard",
)
def iter_json_values(value: Any, path: str = ""):
if isinstance(value, dict):
for key, child in value.items():
child_path = f"{path}.{key}" if path else key
yield child_path, key, child
yield from iter_json_values(child, child_path)
elif isinstance(value, list):
for idx, child in enumerate(value):
yield from iter_json_values(child, f"{path}[{idx}]")
def load_json(path: Path) -> dict[str, Any]:
return json.loads(path.read_text())
def main() -> int:
parser = argparse.ArgumentParser(description="Audit a Harbor Terminal-Bench 2.0 campaign for leaderboard-submission rule compatibility.")
parser.add_argument("campaign_dir", type=Path)
parser.add_argument("--min-trials", type=int, default=5)
args = parser.parse_args()
campaign_dir = args.campaign_dir.expanduser().resolve()
jobs_root = campaign_dir / "harbor-jobs"
if not jobs_root.is_dir():
raise SystemExit(f"Missing harbor-jobs directory: {jobs_root}")
failures: list[str] = []
warnings: list[str] = []
submit_ready_jobs: list[str] = []
partial_jobs: list[str] = []
manifest_path = campaign_dir / "campaign.json"
if manifest_path.exists():
manifest = load_json(manifest_path)
if manifest.get("timeout_multiplier") != 1.0:
failures.append(f"campaign timeout_multiplier is {manifest.get('timeout_multiplier')!r}, expected 1.0")
if manifest.get("attempts_per_task") and manifest.get("attempts_per_task") < args.min_trials:
failures.append(f"campaign attempts_per_task is {manifest.get('attempts_per_task')!r}, expected >= {args.min_trials}")
else:
warnings.append("campaign.json not found; validating job configs only")
task_dirs = sorted(path for path in jobs_root.iterdir() if path.is_dir())
for task_dir in task_dirs:
run_dirs = sorted(path for path in task_dir.iterdir() if path.is_dir())
if not run_dirs:
continue
for run_dir in run_dirs:
rel_run = run_dir.relative_to(campaign_dir)
config_path = run_dir / "config.json"
if not config_path.exists():
failures.append(f"{rel_run}: missing config.json")
continue
config = load_json(config_path)
if config.get("timeout_multiplier") != 1.0:
failures.append(f"{rel_run}: timeout_multiplier is {config.get('timeout_multiplier')!r}, expected 1.0")
for json_path, key, value in iter_json_values(config):
if key in DISALLOWED_NON_NULL_KEYS and value is not None:
# suppress_override_warnings is harmless bookkeeping, not a resource override.
failures.append(f"{rel_run}: disallowed non-null config field {json_path}={value!r}")
trial_results = sorted(run_dir.glob("*__/result.json")) + sorted(run_dir.glob("*__*/result.json"))
# Glob patterns can overlap on some shells/filesystems, dedupe while preserving order.
seen: set[Path] = set()
trial_results = [p for p in trial_results if not (p in seen or seen.add(p))]
if len(trial_results) < args.min_trials:
partial_jobs.append(f"{rel_run}: only {len(trial_results)} trial result.json files, expected >= {args.min_trials}")
continue
invalid_trials = []
missing_artifacts = []
for result_path in trial_results:
try:
load_json(result_path)
except Exception as exc: # noqa: BLE001
invalid_trials.append(f"{result_path.relative_to(campaign_dir)}: invalid JSON: {exc}")
continue
siblings = [p for p in result_path.parent.iterdir() if p.name != "result.json"]
if not siblings:
missing_artifacts.append(str(result_path.parent.relative_to(campaign_dir)))
if invalid_trials:
failures.extend(invalid_trials)
if missing_artifacts:
failures.append(f"{rel_run}: trial dirs missing non-result artifacts: {missing_artifacts[:5]}")
if not invalid_trials and not missing_artifacts:
submit_ready_jobs.append(str(rel_run))
log_name_allowlist = {
"events.ndjson",
"stderr.txt",
"exec_stderr.txt",
"exec_stdout.txt",
"instruction.txt",
"download_error.txt",
}
for text_path in jobs_root.rglob("*"):
if not text_path.is_file() or text_path.name not in log_name_allowlist or text_path.stat().st_size > 2_000_000:
continue
try:
text = text_path.read_text(errors="ignore").lower()
except Exception:
continue
matches = [term for term in FORBIDDEN_LOG_TERMS if term in text]
if matches:
warnings.append(f"possible forbidden benchmark-site/repo mention in agent log {text_path.relative_to(campaign_dir)}: {matches}")
print(f"campaign: {campaign_dir}")
print(f"submit-ready job runs: {len(submit_ready_jobs)}")
for job in submit_ready_jobs:
print(f" OK {job}")
print(f"partial/non-submittable job runs: {len(partial_jobs)}")
for item in partial_jobs:
print(f" PARTIAL {item}")
print(f"failures: {len(failures)}")
for item in failures:
print(f" FAIL {item}")
print(f"warnings: {len(warnings)}")
for item in warnings[:50]:
print(f" WARN {item}")
return 1 if failures else 0
if __name__ == "__main__":
raise SystemExit(main())
+216
View File
@@ -0,0 +1,216 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
command=${1:-help}
if [[ $# -gt 0 ]]; then
shift
fi
sandbox_name=${JCODE_ONBOARDING_SANDBOX:-default}
sandbox_root_default="$repo_root/.tmp/onboarding/$sandbox_name"
sandbox_root=${JCODE_ONBOARDING_DIR:-$sandbox_root_default}
jcode_home="$sandbox_root/home"
runtime_dir="$sandbox_root/runtime"
fixture_root_default="$repo_root/.tmp/auth-fixtures"
fixture_root=${JCODE_AUTH_FIXTURE_DIR:-$fixture_root_default}
usage() {
cat <<EOF
Usage: $(basename "$0") <command> [args...]
Commands:
list List saved local auth fixtures
path [name] Print fixture root, or a specific fixture path
save <name> Save the current sandbox JCODE_HOME as a fixture
load <name> Replace the sandbox JCODE_HOME with a saved fixture
reset-sandbox Remove only the current sandbox JCODE_HOME
delete <name> Delete a saved fixture
env <name> Print exports for running against a loaded fixture
run <name> -- <args...> Load fixture, then run jcode with args in sandbox
help Show this help
Environment overrides:
JCODE_ONBOARDING_SANDBOX Sandbox name to load into/from (default: default)
JCODE_ONBOARDING_DIR Explicit onboarding sandbox directory
JCODE_AUTH_FIXTURE_DIR Fixture store (default: .tmp/auth-fixtures)
Notes:
Fixtures are local developer state under .tmp by default. They may contain real
tokens copied from sandbox logins, so do not move them into tracked paths.
EOF
}
require_name() {
if [[ $# -lt 1 || -z "${1:-}" ]]; then
echo "missing fixture name" >&2
exit 2
fi
if [[ "$1" == *"/"* || "$1" == *".."* || "$1" =~ [^A-Za-z0-9._-] ]]; then
echo "invalid fixture name: $1" >&2
echo "Use only letters, numbers, dot, underscore, and dash." >&2
exit 2
fi
printf '%s' "$1"
}
fixture_path() {
local name
name=$(require_name "$1")
printf '%s/%s/home' "$fixture_root" "$name"
}
metadata_path() {
local name
name=$(require_name "$1")
printf '%s/%s/metadata.txt' "$fixture_root" "$name"
}
ensure_parent_dirs() {
mkdir -p "$fixture_root" "$sandbox_root" "$runtime_dir"
chmod 700 "$fixture_root" "$sandbox_root" 2>/dev/null || true
}
copy_dir_contents() {
local src=$1
local dst=$2
rm -rf "$dst"
mkdir -p "$dst"
if [[ -d "$src" ]]; then
shopt -s dotglob nullglob
local entries=("$src"/*)
if [[ ${#entries[@]} -gt 0 ]]; then
cp -a "${entries[@]}" "$dst"/
fi
shopt -u dotglob nullglob
fi
}
run_jcode() {
local binary_path="$repo_root/target/debug/jcode"
(
cd "$repo_root"
if [[ -x "$binary_path" ]]; then
env JCODE_HOME="$jcode_home" JCODE_RUNTIME_DIR="$runtime_dir" "$binary_path" "$@"
else
env JCODE_HOME="$jcode_home" JCODE_RUNTIME_DIR="$runtime_dir" cargo run --bin jcode -- "$@"
fi
)
}
list_fixtures() {
ensure_parent_dirs
if [[ ! -d "$fixture_root" ]]; then
return 0
fi
find "$fixture_root" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort
}
save_fixture() {
local name=$1
local dst meta
dst=$(fixture_path "$name")
meta=$(metadata_path "$name")
ensure_parent_dirs
if [[ ! -d "$jcode_home" ]]; then
echo "sandbox JCODE_HOME does not exist: $jcode_home" >&2
exit 1
fi
mkdir -p "$(dirname "$dst")"
copy_dir_contents "$jcode_home" "$dst"
chmod -R go-rwx "$(dirname "$dst")" 2>/dev/null || true
cat > "$meta" <<EOF
name=$name
saved_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
sandbox_name=$sandbox_name
source_jcode_home=$jcode_home
warning=May contain real local auth tokens. Do not commit or share.
EOF
echo "Saved auth fixture '$name' from $jcode_home"
echo "Fixture path: $(dirname "$dst")"
}
load_fixture() {
local name=$1
local src
src=$(fixture_path "$name")
ensure_parent_dirs
if [[ ! -d "$src" ]]; then
echo "fixture does not exist: $name" >&2
echo "Expected: $src" >&2
exit 1
fi
copy_dir_contents "$src" "$jcode_home"
chmod -R go-rwx "$jcode_home" 2>/dev/null || true
echo "Loaded auth fixture '$name' into sandbox '$sandbox_name'"
echo "JCODE_HOME=$jcode_home"
}
delete_fixture() {
local name=$1
local fixture_dir="$fixture_root/$name"
if [[ ! -d "$fixture_dir" ]]; then
echo "fixture does not exist: $name" >&2
exit 1
fi
rm -rf "$fixture_dir"
echo "Deleted auth fixture '$name'"
}
case "$command" in
list)
list_fixtures
;;
path)
ensure_parent_dirs
if [[ $# -gt 0 ]]; then
name=$(require_name "$1")
dirname "$(fixture_path "$name")"
else
printf '%s\n' "$fixture_root"
fi
;;
save)
name=$(require_name "${1:-}")
save_fixture "$name"
;;
load)
name=$(require_name "${1:-}")
load_fixture "$name"
;;
reset-sandbox)
rm -rf "$jcode_home"
mkdir -p "$jcode_home" "$runtime_dir"
echo "Reset sandbox JCODE_HOME: $jcode_home"
;;
delete)
name=$(require_name "${1:-}")
delete_fixture "$name"
;;
env)
name=$(require_name "${1:-}")
load_fixture "$name" >/dev/null
cat <<EOF
export JCODE_HOME="$jcode_home"
export JCODE_RUNTIME_DIR="$runtime_dir"
EOF
;;
run)
name=$(require_name "${1:-}")
shift || true
if [[ "${1:-}" == "--" ]]; then
shift
fi
load_fixture "$name" >/dev/null
run_jcode "$@"
;;
help|-h|--help)
usage
;;
*)
echo "Unknown command: $command" >&2
echo >&2
usage >&2
exit 1
;;
esac
+222
View File
@@ -0,0 +1,222 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
cd "$repo_root"
bin=${JCODE_AUTH_MATRIX_BIN:-}
out_dir=${JCODE_AUTH_MATRIX_OUT:-"$repo_root/target/auth-test-reports"}
prompt=${JCODE_AUTH_MATRIX_PROMPT:-"Reply with exactly AUTH_TEST_OK and nothing else. Do not call tools."}
providers=${JCODE_AUTH_MATRIX_PROVIDERS:-"claude copilot openrouter deepseek zai alibaba-coding-plan openai-compatible"}
mode=${JCODE_AUTH_MATRIX_MODE:-configured}
keep_going=${JCODE_AUTH_MATRIX_KEEP_GOING:-1}
per_command_timeout=${JCODE_AUTH_MATRIX_TIMEOUT:-90}
usage() {
cat <<'EOF'
Usage: scripts/auth_regression_matrix.sh [options]
Runs jcode auth-test across the auth/provider matrix and writes one JSON report per provider.
By default it only tests providers that are configured enough for auth-test to run.
Options:
--all Try every provider in the matrix, even if not configured
--configured Test only configured providers (default)
--provider NAME Test one provider. Can be repeated.
--out DIR Report directory (default: target/auth-test-reports)
--bin PATH jcode binary to run (default: cargo run --bin jcode --)
--login Run login before validation for each provider
--no-smoke Skip runtime model smoke
--no-tool-smoke Skip tool-enabled runtime smoke
--fail-fast Stop after the first failed provider
--prompt TEXT Custom smoke prompt
--timeout SECONDS Per auth-test command timeout (default: 90)
-h, --help Show this help
Environment equivalents:
JCODE_AUTH_MATRIX_BIN=/path/to/jcode
JCODE_AUTH_MATRIX_OUT=target/auth-test-reports
JCODE_AUTH_MATRIX_PROVIDERS="claude deepseek zai"
JCODE_AUTH_MATRIX_MODE=configured|all
JCODE_AUTH_MATRIX_LOGIN=1
JCODE_AUTH_MATRIX_NO_SMOKE=1
JCODE_AUTH_MATRIX_NO_TOOL_SMOKE=1
JCODE_AUTH_MATRIX_KEEP_GOING=0
JCODE_AUTH_MATRIX_TIMEOUT=90
Examples:
scripts/auth_regression_matrix.sh --configured --no-smoke
scripts/auth_regression_matrix.sh --provider deepseek --provider zai
JCODE_AUTH_MATRIX_BIN=target/selfdev/jcode scripts/auth_regression_matrix.sh --all
EOF
}
selected=()
extra_args=()
while [[ $# -gt 0 ]]; do
case "$1" in
--all)
mode=all
shift
;;
--configured)
mode=configured
shift
;;
--provider)
[[ $# -ge 2 ]] || { echo "error: --provider requires a value" >&2; exit 2; }
selected+=("$2")
shift 2
;;
--out)
[[ $# -ge 2 ]] || { echo "error: --out requires a value" >&2; exit 2; }
out_dir=$2
shift 2
;;
--bin)
[[ $# -ge 2 ]] || { echo "error: --bin requires a value" >&2; exit 2; }
bin=$2
shift 2
;;
--login)
extra_args+=(--login)
shift
;;
--no-smoke)
extra_args+=(--no-smoke)
shift
;;
--no-tool-smoke)
extra_args+=(--no-tool-smoke)
shift
;;
--fail-fast)
keep_going=0
shift
;;
--prompt)
[[ $# -ge 2 ]] || { echo "error: --prompt requires a value" >&2; exit 2; }
prompt=$2
shift 2
;;
--timeout)
[[ $# -ge 2 ]] || { echo "error: --timeout requires a value" >&2; exit 2; }
per_command_timeout=$2
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "error: unknown argument: $1" >&2
usage >&2
exit 2
;;
esac
done
if [[ "${JCODE_AUTH_MATRIX_LOGIN:-0}" == "1" ]]; then
extra_args+=(--login)
fi
if [[ "${JCODE_AUTH_MATRIX_NO_SMOKE:-0}" == "1" ]]; then
extra_args+=(--no-smoke)
fi
if [[ "${JCODE_AUTH_MATRIX_NO_TOOL_SMOKE:-0}" == "1" ]]; then
extra_args+=(--no-tool-smoke)
fi
if [[ ${#selected[@]} -eq 0 ]]; then
# shellcheck disable=SC2206
selected=($providers)
fi
mkdir -p "$out_dir"
run_jcode() {
if [[ -n "$bin" ]]; then
timeout "$per_command_timeout" "$bin" "$@"
else
timeout "$per_command_timeout" cargo run --quiet --bin jcode -- "$@"
fi
}
configured_json="$out_dir/configured-providers.json"
if [[ "$mode" == "configured" ]]; then
echo "Discovering configured providers..."
rm -f "$configured_json"
if ! run_jcode auth-test --all-configured --no-smoke --no-tool-smoke --json --output "$configured_json" >/tmp/jcode-auth-matrix-discovery.out 2>/tmp/jcode-auth-matrix-discovery.err; then
if [[ -s "$configured_json" ]]; then
echo "note: configured-provider discovery reported non-ready providers; continuing with per-provider classification" >&2
else
cat /tmp/jcode-auth-matrix-discovery.err >&2 || true
echo "warning: configured-provider discovery failed; continuing with explicit matrix and skipping only obvious unconfigured failures" >&2
fi
fi
fi
failed=()
passed=()
skipped=()
blocked=()
is_unconfigured_failure() {
grep -Eiq 'not configured|missing|no credentials|not found in environment|requires.*token|requires.*api key' "$1"
}
is_external_account_blocked_failure() {
# These are upstream account/entitlement states, not auth-regression signal.
# Keep this list intentionally narrow so real code/provider failures still fail.
grep -Eiq 'feature_flag_blocked|can_signup_for_limited|Contact Support|not entitled|not eligible|subscription required|quota exceeded|rate limit' "$1"
}
echo "Auth regression matrix"
echo "Mode: $mode"
echo "Reports: $out_dir"
echo "Providers: ${selected[*]}"
echo "Timeout: ${per_command_timeout}s per command"
echo
for provider in "${selected[@]}"; do
report="$out_dir/${provider}.json"
log="$out_dir/${provider}.log"
args=(auth-test --provider "$provider" --prompt "$prompt" --json --output "$report" "${extra_args[@]}")
echo "=== auth-test: $provider ==="
set +e
run_jcode "${args[@]}" >"$log" 2>&1
status=$?
set -e
if [[ $status -eq 0 ]]; then
passed+=("$provider")
echo "PASS $provider"
else
if [[ "$mode" == "configured" ]] && is_unconfigured_failure "$log"; then
skipped+=("$provider")
echo "SKIP $provider (not configured, see $log)"
elif [[ "$mode" == "configured" ]] && is_external_account_blocked_failure "$log"; then
blocked+=("$provider")
echo "BLOCKED $provider (upstream account/entitlement unavailable, see $log)"
else
failed+=("$provider")
echo "FAIL $provider (exit $status, see $log)"
if [[ "$keep_going" != "1" ]]; then
break
fi
fi
fi
echo
done
summary="$out_dir/summary.txt"
{
echo "passed: ${passed[*]:-<none>}"
echo "skipped: ${skipped[*]:-<none>}"
echo "blocked: ${blocked[*]:-<none>}"
echo "failed: ${failed[*]:-<none>}"
} | tee "$summary"
if [[ ${#failed[@]} -gt 0 ]]; then
exit 1
fi
+41
View File
@@ -0,0 +1,41 @@
#!/bin/bash
# Autonomous screenshot capture for jcode documentation
# Uses niri window management + screenshot capabilities
#
# Usage: ./auto_screenshot.sh <window_id> <output_name> [setup_command]
#
# Examples:
# ./auto_screenshot.sh 77 main-ui
# ./auto_screenshot.sh 77 info-widget "/info"
# ./auto_screenshot.sh 77 command-palette "/"
set -e
WINDOW_ID="${1:?Usage: $0 <window_id> <output_name> [setup_command]}"
OUTPUT_NAME="${2:?Usage: $0 <window_id> <output_name> [setup_command]}"
SETUP_CMD="${3:-}"
OUTPUT_DIR="$(dirname "$0")/../docs/screenshots"
OUTPUT_PATH="$OUTPUT_DIR/${OUTPUT_NAME}.png"
mkdir -p "$OUTPUT_DIR"
echo "📸 Capturing window $WINDOW_ID as $OUTPUT_NAME"
# Focus the target window
niri msg action focus-window --id "$WINDOW_ID"
sleep 0.3 # Let the window focus settle
# If there's a setup command, we'd need to inject it somehow
# For now, this is a placeholder - see below for the full solution
if [ -n "$SETUP_CMD" ]; then
echo "⚠️ Setup command '$SETUP_CMD' - manual injection needed for now"
echo " Press Enter after setting up the UI state..."
read -r
fi
# Screenshot the focused window
niri msg action screenshot-window --path "$OUTPUT_PATH"
echo "✅ Saved: $OUTPUT_PATH"
ls -lh "$OUTPUT_PATH"
+274
View File
@@ -0,0 +1,274 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
cd "$repo_root"
usage() {
cat <<'USAGE'
Usage:
scripts/bench_compile.sh <target> [options] [-- <extra cargo args>]
Targets:
check Run cargo check --quiet
build Run cargo build --quiet
release-jcode Run scripts/dev_cargo.sh build --release -p jcode --bin jcode --quiet
selfdev-jcode Run scripts/dev_cargo.sh build --profile selfdev -p jcode --bin jcode --quiet
Options:
--cold Run cargo clean before timing the first run
--touch <path> Touch a source file before each timed run to simulate an edit
--edit <path> Toggle a harmless text edit before each run (restored afterward)
--runs <n> Number of timed runs to execute (default: 1)
--json Print per-run + summary data as JSON
-h, --help Show this help
Examples:
scripts/bench_compile.sh check
scripts/bench_compile.sh check --runs 3 --touch src/server.rs
scripts/bench_compile.sh check --runs 3 --edit src/server.rs
scripts/bench_compile.sh build -- --package jcode --bin test_api
scripts/bench_compile.sh release-jcode --json
scripts/bench_compile.sh selfdev-jcode --json
USAGE
}
if [[ $# -gt 0 ]] && [[ "$1" == "-h" || "$1" == "--help" ]]; then
usage
exit 0
fi
target="${1:-}"
shift || true
if [[ -z "$target" ]]; then
usage
exit 1
fi
cold=0
touch_path=""
edit_path=""
runs=1
json_output=0
extra_args=()
while [[ $# -gt 0 ]]; do
case "$1" in
--cold)
cold=1
;;
--touch)
if [[ $# -lt 2 ]]; then
printf 'error: --touch requires a path\n' >&2
exit 1
fi
touch_path="$2"
shift
;;
--edit)
if [[ $# -lt 2 ]]; then
printf 'error: --edit requires a path\n' >&2
exit 1
fi
edit_path="$2"
shift
;;
--runs)
if [[ $# -lt 2 ]]; then
printf 'error: --runs requires a positive integer\n' >&2
exit 1
fi
runs="$2"
shift
;;
--json)
json_output=1
;;
--)
shift
extra_args=("$@")
break
;;
-h|--help)
usage
exit 0
;;
*)
printf 'error: unknown argument: %s\n' "$1" >&2
exit 1
;;
esac
shift
done
if ! [[ "$runs" =~ ^[1-9][0-9]*$ ]]; then
printf 'error: --runs must be a positive integer (got %s)\n' "$runs" >&2
exit 1
fi
if [[ -n "$touch_path" && -n "$edit_path" ]]; then
printf 'error: --touch and --edit are mutually exclusive\n' >&2
exit 1
fi
case "$target" in
check)
cmd=(cargo check --quiet)
;;
build)
cmd=(cargo build --quiet)
;;
release-jcode)
cmd=(scripts/dev_cargo.sh build --release -p jcode --bin jcode --quiet)
;;
selfdev-jcode)
cmd=(scripts/dev_cargo.sh build --profile selfdev -p jcode --bin jcode --quiet)
;;
*)
printf 'error: unsupported target: %s\n' "$target" >&2
usage
exit 1
;;
esac
if [[ ${#extra_args[@]} -gt 0 ]]; then
cmd+=("${extra_args[@]}")
fi
if [[ -n "$touch_path" ]] && [[ ! -e "$touch_path" ]]; then
printf 'error: touch path does not exist: %s\n' "$touch_path" >&2
exit 1
fi
if [[ -n "$edit_path" ]] && [[ ! -f "$edit_path" ]]; then
printf 'error: edit path must be an existing file: %s\n' "$edit_path" >&2
exit 1
fi
edit_backup=""
cleanup() {
if [[ -n "$edit_backup" && -n "$edit_path" && -f "$edit_backup" ]]; then
cp "$edit_backup" "$edit_path"
rm -f "$edit_backup"
fi
}
trap cleanup EXIT
if [[ -n "$edit_path" ]]; then
edit_backup=$(mktemp)
cp "$edit_path" "$edit_backup"
fi
if [[ $cold -eq 1 ]]; then
echo 'bench_compile: running cargo clean' >&2
cargo clean
fi
printf 'bench_compile: target=%s cold=%s runs=%s\n' "$target" "$cold" "$runs" >&2
printf 'bench_compile: touch=%s\n' "${touch_path:-<none>}" >&2
printf 'bench_compile: edit=%s\n' "${edit_path:-<none>}" >&2
printf 'bench_compile: command=%s\n' "${cmd[*]}" >&2
run_times=()
run_once() {
local run_index="$1"
if [[ -n "$touch_path" ]]; then
echo "bench_compile: touching $touch_path (run $run_index/$runs)" >&2
touch "$touch_path"
elif [[ -n "$edit_path" ]]; then
echo "bench_compile: editing $edit_path (run $run_index/$runs)" >&2
python3 - "$edit_backup" "$edit_path" "$run_index" <<'PY'
from pathlib import Path
import sys
backup = Path(sys.argv[1]).read_bytes()
target = Path(sys.argv[2])
run_index = int(sys.argv[3])
if run_index % 2 == 1:
target.write_bytes(backup + b"\n")
else:
target.write_bytes(backup)
PY
fi
local start_ns end_ns elapsed_ns elapsed_secs
start_ns=$(python3 - <<'PY'
import time
print(time.perf_counter_ns())
PY
)
"${cmd[@]}"
end_ns=$(python3 - <<'PY'
import time
print(time.perf_counter_ns())
PY
)
elapsed_ns=$((end_ns - start_ns))
elapsed_secs=$(python3 - "$elapsed_ns" <<'PY'
import sys
print(f"{int(sys.argv[1]) / 1_000_000_000:.3f}")
PY
)
run_times+=("$elapsed_secs")
if [[ $json_output -eq 0 ]]; then
printf 'bench_compile: run %s/%s real %ss\n' "$run_index" "$runs" "$elapsed_secs" >&2
fi
}
for ((i = 1; i <= runs; i++)); do
run_once "$i"
done
summary_json=$(python3 - "$target" "$cold" "$touch_path" "$edit_path" "$runs" "${cmd[*]}" "${run_times[@]}" <<'PY'
import json
import statistics
import sys
target = sys.argv[1]
cold = sys.argv[2] == "1"
touch = sys.argv[3]
edit = sys.argv[4]
runs = int(sys.argv[5])
command = sys.argv[6]
times = [float(v) for v in sys.argv[7:]]
summary = {
"target": target,
"cold": cold,
"touch": touch or None,
"edit": edit or None,
"runs": runs,
"command": command,
"times_seconds": times,
"min_seconds": min(times),
"max_seconds": max(times),
"avg_seconds": sum(times) / len(times),
"median_seconds": statistics.median(times),
}
print(json.dumps(summary))
PY
)
if [[ $json_output -eq 1 ]]; then
printf '%s\n' "$summary_json"
else
python3 - "$summary_json" <<'PY' >&2
import json
import sys
summary = json.loads(sys.argv[1])
print(
"bench_compile: summary "
f"min={summary['min_seconds']:.3f}s "
f"median={summary['median_seconds']:.3f}s "
f"avg={summary['avg_seconds']:.3f}s "
f"max={summary['max_seconds']:.3f}s"
)
PY
fi
+475
View File
@@ -0,0 +1,475 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import os
import pty
import re
import select
import shutil
import signal
import socket
import subprocess
import tempfile
import time
from dataclasses import asdict, dataclass
from pathlib import Path
ANSI_RE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x1b\x07]*(?:\x07|\x1b\\))")
PROBE = "jqx92"
DEFAULT_TIMEOUT_S = 20.0
DEFAULT_SETTLE_S = 1.0
DEFAULT_TOOLS = [
"jcode_memory_off",
"jcode_memory_on",
"pi",
"codex",
"opencode",
"copilot_cli",
"cursor_agent",
"claude_code",
"antigravity_cli",
]
@dataclass
class ToolSpec:
name: str
argv: list[str]
version_argv: list[str]
env: dict[str, str] | None = None
jcode: bool = False
@dataclass
class SessionLaunch:
root_pid: int
pgid: int
master_fd: int
ready: bool
input_ready: bool
excerpt: str | None
seconds_to_visible: float | None
seconds_to_input_ready: float | None
buffer_excerpt: str | None
@dataclass
class ToolRunResult:
tool: str
sessions: int
pss_mb: float
process_count: int
version: str
notes: list[str]
def shutil_which(name: str) -> str | None:
return subprocess.run(
["bash", "-lc", f"command -v {name}"], capture_output=True, text=True, check=False
).stdout.strip() or None
def detect_pi_bin() -> str:
direct = shutil_which("pi")
if direct:
return direct
prefix = subprocess.check_output(["npm", "prefix", "-g"], text=True).strip()
candidate = Path(prefix) / "bin" / "pi"
if candidate.exists():
return str(candidate)
raise FileNotFoundError("could not find pi binary")
def build_specs() -> dict[str, ToolSpec]:
jcode = shutil.which("jcode") or str(Path.home() / ".local/bin/jcode")
codex = shutil.which("codex") or "/usr/bin/codex"
opencode = shutil.which("opencode") or "/usr/bin/opencode"
copilot = shutil.which("copilot") or str(Path.home() / ".local/bin/copilot")
cursor_agent = shutil.which("cursor-agent") or str(Path.home() / ".local/bin/cursor-agent")
claude = shutil.which("claude") or str(Path.home() / ".local/bin/claude")
agy = shutil.which("agy") or str(Path.home() / ".local/bin/agy")
specs = {
"jcode_memory_off": ToolSpec(
name="jcode_memory_off",
argv=[jcode, "--no-update", "--no-selfdev"],
version_argv=[jcode, "version"],
env={"JCODE_NO_TELEMETRY": "1", "JCODE_MEMORY_ENABLED": "0"},
jcode=True,
),
"jcode_memory_on": ToolSpec(
name="jcode_memory_on",
argv=[jcode, "--no-update", "--no-selfdev"],
version_argv=[jcode, "version"],
env={"JCODE_NO_TELEMETRY": "1", "JCODE_MEMORY_ENABLED": "1"},
jcode=True,
),
"pi": ToolSpec(
name="pi",
argv=[detect_pi_bin()],
version_argv=[detect_pi_bin(), "--version"],
),
"codex": ToolSpec(
name="codex",
argv=[codex],
version_argv=[codex, "--version"],
),
"opencode": ToolSpec(
name="opencode",
argv=[opencode],
version_argv=[opencode, "--version"],
),
"copilot_cli": ToolSpec(
name="copilot_cli",
argv=[copilot],
version_argv=[copilot, "--version"],
),
"cursor_agent": ToolSpec(
name="cursor_agent",
argv=[cursor_agent],
version_argv=[cursor_agent, "--version"],
),
"claude_code": ToolSpec(
name="claude_code",
argv=[claude],
version_argv=[claude, "--version"],
),
"antigravity_cli": ToolSpec(
name="antigravity_cli",
argv=[agy],
version_argv=[agy, "--version"],
),
}
return specs
def reply_queries(master_fd: int, buffer: bytes) -> bytes:
replies = [
(b"\x1b[6n", b"\x1b[1;1R"),
(b"\x1b[c", b"\x1b[?62;c"),
(b"\x1b]10;?\x1b\\", b"\x1b]10;rgb:ffff/ffff/ffff\x1b\\"),
(b"\x1b]11;?\x1b\\", b"\x1b]11;rgb:0000/0000/0000\x1b\\"),
(b"\x1b]10;?\x07", b"\x1b]10;rgb:ffff/ffff/ffff\x07"),
(b"\x1b]11;?\x07", b"\x1b]11;rgb:0000/0000/0000\x07"),
(b"\x1b]4;0;?\x07", b"\x1b]4;0;rgb:0000/0000/0000\x07"),
(b"\x1b[14t", b"\x1b[4;600;800t"),
(b"\x1b[16t", b"\x1b[6;16;8t"),
(b"\x1b[18t", b"\x1b[8;24;80t"),
(b"\x1b[?1016$p", b"\x1b[?1016;1$y"),
(b"\x1b[?2027$p", b"\x1b[?2027;1$y"),
(b"\x1b[?2031$p", b"\x1b[?2031;1$y"),
(b"\x1b[?1004$p", b"\x1b[?1004;1$y"),
(b"\x1b[?2004$p", b"\x1b[?2004;1$y"),
(b"\x1b[?2026$p", b"\x1b[?2026;1$y"),
]
changed = True
while changed:
changed = False
for query, response in replies:
if query in buffer:
os.write(master_fd, response)
buffer = buffer.replace(query, b"")
changed = True
return buffer
def strip_ansi(text: str) -> str:
return ANSI_RE.sub("", text).replace("\r", "\n")
def first_meaningful_line(text: str) -> str | None:
for raw_line in text.splitlines():
line = " ".join(raw_line.split())
if not line:
continue
alnum_count = sum(ch.isalnum() for ch in line)
if alnum_count >= 3 and len(line) >= 4:
return line[:160]
return None
def wait_for_socket(path: str, timeout_s: float) -> bool:
deadline = time.time() + timeout_s
while time.time() < deadline:
if os.path.exists(path):
try:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(path)
sock.close()
return True
except OSError:
pass
time.sleep(0.05)
return False
def launch_interactive(argv: list[str], cwd: Path, env: dict[str, str], timeout_s: float, settle_s: float) -> SessionLaunch:
master_fd, slave_fd = pty.openpty()
proc = subprocess.Popen(
argv,
cwd=str(cwd),
env=env,
stdin=slave_fd,
stdout=slave_fd,
stderr=slave_fd,
preexec_fn=os.setsid,
)
os.close(slave_fd)
os.set_blocking(master_fd, False)
start = time.perf_counter()
buf = b""
ready = False
input_ready = False
probe_sent = False
excerpt = None
while time.perf_counter() - start < timeout_s:
rlist, _, _ = select.select([master_fd], [], [], 0.05)
if rlist:
try:
chunk = os.read(master_fd, 65536)
except BlockingIOError:
chunk = b""
if chunk:
buf += chunk
buf = reply_queries(master_fd, buf)
plain = strip_ansi(buf.decode("utf-8", "replace"))
excerpt = first_meaningful_line(plain)
if excerpt:
ready = True
if not probe_sent:
try:
os.write(master_fd, PROBE.encode())
probe_sent = True
except OSError:
break
if probe_sent and PROBE in plain:
input_ready = True
break
if proc.poll() is not None:
break
if input_ready or ready:
time.sleep(settle_s)
elapsed = time.perf_counter() - start
return SessionLaunch(
root_pid=proc.pid,
pgid=os.getpgid(proc.pid),
master_fd=master_fd,
ready=ready,
input_ready=input_ready,
excerpt=excerpt,
seconds_to_visible=elapsed if ready else None,
seconds_to_input_ready=elapsed if input_ready else None,
buffer_excerpt=(strip_ansi(buf.decode("utf-8", "replace"))[:300] or None),
)
def iter_proc_stat() -> dict[int, tuple[int, int]]:
out: dict[int, tuple[int, int]] = {}
for entry in Path("/proc").iterdir():
if not entry.name.isdigit():
continue
try:
stat = (entry / "stat").read_text()
except Exception:
continue
try:
close = stat.rfind(")")
rest = stat[close + 2 :].split()
ppid = int(rest[1])
pgid = int(rest[2])
out[int(entry.name)] = (ppid, pgid)
except Exception:
continue
return out
def collect_descendants(root_pids: list[int]) -> set[int]:
ppid_of = iter_proc_stat()
children: dict[int, list[int]] = {}
for pid, (ppid, _pgid) in ppid_of.items():
children.setdefault(ppid, []).append(pid)
seen: set[int] = set()
stack = list(root_pids)
while stack:
pid = stack.pop()
if pid in seen:
continue
seen.add(pid)
stack.extend(children.get(pid, []))
return seen
def collect_process_group_pids(pgids: list[int]) -> set[int]:
proc_map = iter_proc_stat()
wanted = set(pgids)
return {pid for pid, (_ppid, pgid) in proc_map.items() if pgid in wanted}
def read_pss_mb(pid: int) -> float | None:
path = Path(f"/proc/{pid}/smaps_rollup")
try:
for line in path.read_text().splitlines():
if line.startswith("Pss:"):
return int(line.split()[1]) / 1024.0
except Exception:
return None
return None
def sum_tree_pss(root_pids: list[int], pgids: list[int]) -> tuple[float, int]:
all_pids = collect_descendants(root_pids) | collect_process_group_pids(pgids)
total = 0.0
counted = 0
for pid in sorted(all_pids):
pss = read_pss_mb(pid)
if pss is None:
continue
total += pss
counted += 1
return round(total, 1), counted
def terminate_pgroup(pgid: int) -> None:
for sig in (signal.SIGTERM, signal.SIGKILL):
try:
os.killpg(pgid, sig)
time.sleep(0.2)
except ProcessLookupError:
return
def version_for(spec: ToolSpec) -> str:
proc = subprocess.run(spec.version_argv, capture_output=True, text=True, check=False)
output = (proc.stdout + proc.stderr).strip().splitlines()
return output[0] if output else f"exit {proc.returncode}"
def run_tool(spec: ToolSpec, sessions: int, cwd: Path, timeout_s: float, settle_s: float) -> ToolRunResult:
notes: list[str] = []
version = version_for(spec)
launches: list[SessionLaunch] = []
cleanup_pgids: list[int] = []
temp_root: str | None = None
try:
if spec.jcode:
temp_root = tempfile.mkdtemp(prefix="jcode-memory-bench-")
env = os.environ.copy()
if spec.env:
env.update(spec.env)
env["JCODE_HOME"] = os.path.join(temp_root, "home")
env["JCODE_RUNTIME_DIR"] = os.path.join(temp_root, "run")
env["JCODE_TEMP_SERVER"] = "1"
env["JCODE_SERVER_OWNER_PID"] = str(os.getpid())
os.makedirs(env["JCODE_HOME"], exist_ok=True)
os.makedirs(env["JCODE_RUNTIME_DIR"], exist_ok=True)
for auth_name in (
"anthropic-auth.json",
"openai-auth.json",
"antigravity_oauth.json",
"gemini_oauth.json",
"config.toml",
):
real_auth = Path.home() / ".jcode" / auth_name
bench_auth = Path(env["JCODE_HOME"]) / auth_name
if real_auth.exists() and not bench_auth.exists():
bench_auth.symlink_to(real_auth)
if spec.name == "jcode_memory_on":
real_models = Path.home() / ".jcode" / "models"
bench_models = Path(env["JCODE_HOME"]) / "models"
if real_models.exists() and not bench_models.exists():
bench_models.symlink_to(real_models)
socket_path = os.path.join(env["JCODE_RUNTIME_DIR"], "bench.sock")
server_proc = subprocess.Popen(
[spec.argv[0], "--no-update", "--no-selfdev", "serve", "--socket", socket_path],
cwd=str(cwd),
env=env,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
preexec_fn=os.setsid,
)
cleanup_pgids.append(os.getpgid(server_proc.pid))
if not wait_for_socket(socket_path, timeout_s):
raise RuntimeError("jcode server did not become ready")
if spec.name == "jcode_memory_on":
time.sleep(max(settle_s, 5.0))
per_session_settle = max(settle_s, 2.0) if spec.name == "jcode_memory_on" else settle_s
for _ in range(sessions):
launches.append(
launch_interactive(
[spec.argv[0], "--no-update", "--no-selfdev", "--socket", socket_path],
cwd,
env,
timeout_s,
per_session_settle,
)
)
cleanup_pgids.append(launches[-1].pgid)
root_pids = [server_proc.pid] + [launch.root_pid for launch in launches]
sample_pgids = cleanup_pgids.copy()
else:
env = os.environ.copy()
if spec.env:
env.update(spec.env)
for _ in range(sessions):
launches.append(launch_interactive(spec.argv, cwd, env, timeout_s, settle_s))
cleanup_pgids.append(launches[-1].pgid)
root_pids = [launch.root_pid for launch in launches]
sample_pgids = cleanup_pgids.copy()
for idx, launch in enumerate(launches, start=1):
if not launch.ready:
notes.append(f"session {idx}: no meaningful screen content before timeout")
elif launch.excerpt:
notes.append(f"session {idx}: {launch.excerpt}")
pss_mb, process_count = sum_tree_pss(root_pids, sample_pgids)
return ToolRunResult(
tool=spec.name,
sessions=sessions,
pss_mb=pss_mb,
process_count=process_count,
version=version,
notes=notes,
)
finally:
for launch in launches:
try:
os.close(launch.master_fd)
except Exception:
pass
for pgid in reversed(cleanup_pgids):
terminate_pgroup(pgid)
if temp_root:
shutil.rmtree(temp_root, ignore_errors=True)
def main() -> int:
parser = argparse.ArgumentParser(description="Benchmark interactive CLI memory using process-tree PSS")
parser.add_argument("--sessions", type=int, required=True)
parser.add_argument("--tools", nargs="*", default=DEFAULT_TOOLS)
parser.add_argument("--timeout", type=float, default=DEFAULT_TIMEOUT_S)
parser.add_argument("--settle", type=float, default=DEFAULT_SETTLE_S)
parser.add_argument("--cwd", default=os.getcwd())
parser.add_argument("--json-out", default=None)
args = parser.parse_args()
specs = build_specs()
cwd = Path(args.cwd).resolve()
results = []
for name in args.tools:
spec = specs[name]
print(f"=== {name} ({args.sessions} session{'s' if args.sessions != 1 else ''}) ===", flush=True)
result = run_tool(spec, args.sessions, cwd, args.timeout, args.settle)
print(json.dumps(asdict(result), indent=2), flush=True)
results.append(asdict(result))
payload = {"cwd": str(cwd), "sessions": args.sessions, "results": results}
if args.json_out:
Path(args.json_out).write_text(json.dumps(payload, indent=2))
else:
print(json.dumps(payload, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env bash
# Repeatable self-dev build benchmark (issue #392, part C).
#
# Measures the wall-clock cost of the selfdev build paths that matter day to
# day, so build-pipeline changes are hill-climbable:
#
# 1. warm no-op - rebuild with zero changes on the same commit
# 2. leaf touch - 1-line change in the root `jcode` bin crate
# 3. tui touch - 1-line change in jcode-tui (largest UI crate)
# 4. core touch - 1-line change in jcode-app-core (mid-stack crate)
# 5. commit blast - simulate HEAD moving (JCODE_BUILD_GIT_HASH change),
# which reruns jcode-build-meta's build script and
# recompiles every crate that depends on it
# (base, app-core, tui, setup-hints, telemetry-core, root)
#
# Touches are reverted after each run. Requires a clean-enough tree that
# `scripts/dev_cargo.sh build --profile selfdev -p jcode --bin jcode` succeeds.
#
# Usage: scripts/bench_selfdev_build.sh [--skip-warmup]
set -euo pipefail
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$repo_root"
build_cmd=(scripts/dev_cargo.sh build --profile selfdev -p jcode --bin jcode)
run_build() {
# Per-phase logs survive later phases, so a mid-benchmark failure (often a
# concurrent agent breaking the shared tree) stays diagnosable.
local label="$1"
local log="/tmp/bench_selfdev_build_${label}.log"
local start end elapsed
start=$(date +%s.%N)
if ! "${build_cmd[@]}" >"$log" 2>&1; then
echo " ${label}: BUILD FAILED (see ${log})"
return 1
fi
end=$(date +%s.%N)
elapsed=$(echo "$end $start" | awk '{printf "%.1f", $1 - $2}')
echo " ${label}: ${elapsed}s"
}
touch_file() {
# Append and immediately strip a trailing comment marker so the file
# content changes (mtime + hash) without altering semantics.
local file="$1"
printf '\n// bench_selfdev_build touch\n' >> "$file"
}
revert_file() {
local file="$1"
# Remove the exact marker we appended (plus its preceding blank line).
python3 - "$file" << 'EOF'
import sys
path = sys.argv[1]
with open(path) as fh:
content = fh.read()
marker = "\n// bench_selfdev_build touch\n"
if content.endswith(marker):
content = content[: -len(marker)]
with open(path, "w") as fh:
fh.write(content)
EOF
}
echo "selfdev build benchmark ($(git rev-parse --short HEAD), $(nproc) cpus)"
echo "command: ${build_cmd[*]}"
echo
if [[ "${1:-}" != "--skip-warmup" ]]; then
echo "warmup (populate incremental caches)..."
run_build "warmup" || exit 1
echo
fi
echo "1. warm no-op (same commit, no changes):"
run_build "no-op"
leaf_file="src/main.rs"
echo "2. leaf touch (${leaf_file}):"
touch_file "$leaf_file"
run_build "leaf" || true
revert_file "$leaf_file"
tui_file="crates/jcode-tui/src/lib.rs"
echo "3. tui touch (${tui_file}):"
touch_file "$tui_file"
run_build "tui" || true
revert_file "$tui_file"
core_file="crates/jcode-app-core/src/lib.rs"
echo "4. core touch (${core_file}):"
touch_file "$core_file"
run_build "core" || true
revert_file "$core_file"
echo "5. commit blast radius (simulated HEAD move via JCODE_BUILD_GIT_HASH):"
fake_hash="bench$(date +%s | tail -c 4)"
start=$(date +%s.%N)
if JCODE_BUILD_GIT_HASH="$fake_hash" "${build_cmd[@]}" >/tmp/bench_selfdev_build_commit-blast.log 2>&1; then
end=$(date +%s.%N)
echo "$end $start" | awk '{printf " commit-blast: %.1fs\n", $1 - $2}'
else
echo " commit-blast: BUILD FAILED (see /tmp/bench_selfdev_build_commit-blast.log)"
fi
echo " (restoring real-hash build so the next selfdev build is warm)"
run_build "restore" || true
echo
echo "done. Interpretation:"
echo " - no-op should be a few seconds (cargo fingerprinting + link check)."
echo " - leaf/tui/core touches show per-crate recompile + link cost."
echo " - commit-blast shows the cost every commit pays because"
echo " jcode-build-meta embeds GIT_HASH via env! and jcode-base/app-core/"
echo " tui/setup-hints/telemetry-core all depend on it."
+207
View File
@@ -0,0 +1,207 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
cd "$repo_root"
usage() {
cat <<'USAGE'
Usage:
scripts/bench_selfdev_checkpoints.sh [options]
Runs the standard compile checkpoints for the self-dev loop using scripts/bench_compile.sh.
Options:
--touch <path> Source file to touch for warm edit-loop runs (default: src/server.rs)
--runs <n> Number of warm runs per checkpoint (default: 3)
--skip-cold Skip cold checkpoints and only run warm edit-loop measurements
--json Print a single JSON object with all checkpoint summaries
-h, --help Show this help
Checkpoints:
cold_check cargo check after cargo clean
warm_check_edit touched-file cargo check loop
cold_selfdev_build selfdev jcode build after cargo clean
warm_selfdev_edit touched-file selfdev jcode build loop
USAGE
}
runs=3
touch_path="src/server.rs"
json_output=0
skip_cold=0
while [[ $# -gt 0 ]]; do
case "$1" in
--touch)
if [[ $# -lt 2 ]]; then
printf 'error: --touch requires a path\n' >&2
exit 1
fi
touch_path="$2"
shift
;;
--runs)
if [[ $# -lt 2 ]]; then
printf 'error: --runs requires a positive integer\n' >&2
exit 1
fi
runs="$2"
shift
;;
--json)
json_output=1
;;
--skip-cold)
skip_cold=1
;;
-h|--help)
usage
exit 0
;;
*)
printf 'error: unknown argument: %s\n' "$1" >&2
exit 1
;;
esac
shift
done
if ! [[ "$runs" =~ ^[1-9][0-9]*$ ]]; then
printf 'error: --runs must be a positive integer (got %s)\n' "$runs" >&2
exit 1
fi
if [[ ! -e "$touch_path" ]]; then
printf 'error: touch path does not exist: %s\n' "$touch_path" >&2
exit 1
fi
run_bench() {
local name="$1"
shift
local stdout_file stderr_file status
stdout_file=$(mktemp)
stderr_file=$(mktemp)
if scripts/bench_compile.sh "$@" --json >"$stdout_file" 2>"$stderr_file"; then
python3 - "$name" "$stdout_file" <<'PY'
import json
import pathlib
import sys
name = sys.argv[1]
payload = json.loads(pathlib.Path(sys.argv[2]).read_text())
payload["checkpoint"] = name
payload["ok"] = True
print(json.dumps(payload))
PY
else
status=$?
python3 - "$name" "$status" "$stderr_file" <<'PY'
import json
import pathlib
import sys
name = sys.argv[1]
status = int(sys.argv[2])
stderr = pathlib.Path(sys.argv[3]).read_text().strip()
print(json.dumps({
"checkpoint": name,
"ok": False,
"exit_code": status,
"error": stderr,
}))
PY
fi
rm -f "$stdout_file" "$stderr_file"
}
cold_check_json=$(python3 - <<'PY' "$skip_cold"
import json
import sys
skip = sys.argv[1] == "1"
print(json.dumps({"checkpoint": "cold_check", "ok": None, "skipped": skip}))
PY
)
cold_selfdev_json=$(python3 - <<'PY' "$skip_cold"
import json
import sys
skip = sys.argv[1] == "1"
print(json.dumps({"checkpoint": "cold_selfdev_build", "ok": None, "skipped": skip}))
PY
)
if [[ $skip_cold -eq 0 ]]; then
cold_check_json=$(run_bench cold_check check --cold)
cold_selfdev_json=$(run_bench cold_selfdev_build selfdev-jcode --cold)
fi
warm_check_json=$(run_bench warm_check_edit check --runs "$runs" --touch "$touch_path")
warm_selfdev_json=$(run_bench warm_selfdev_edit selfdev-jcode --runs "$runs" --touch "$touch_path")
summary_json=$(python3 - <<'PY' "$touch_path" "$runs" "$cold_check_json" "$warm_check_json" "$cold_selfdev_json" "$warm_selfdev_json"
import json
import sys
touch_path = sys.argv[1]
runs = int(sys.argv[2])
cold_check = json.loads(sys.argv[3])
warm_check = json.loads(sys.argv[4])
cold_selfdev = json.loads(sys.argv[5])
warm_selfdev = json.loads(sys.argv[6])
skip = bool(cold_check.get("skipped") and cold_selfdev.get("skipped"))
summary = {
"touch_path": touch_path,
"warm_runs": runs,
"skip_cold": skip == True,
"checkpoints": {
"cold_check": cold_check,
"warm_check_edit": warm_check,
"cold_selfdev_build": cold_selfdev,
"warm_selfdev_edit": warm_selfdev,
},
"failed_checkpoints": [
name for name, payload in {
"cold_check": cold_check,
"warm_check_edit": warm_check,
"cold_selfdev_build": cold_selfdev,
"warm_selfdev_edit": warm_selfdev,
}.items()
if payload.get("ok") is False
],
}
print(json.dumps(summary))
PY
)
if [[ $json_output -eq 1 ]]; then
printf '%s\n' "$summary_json"
else
python3 - <<'PY' "$summary_json"
import json
import sys
summary = json.loads(sys.argv[1])
print("selfdev compile checkpoints")
print(f" touch_path: {summary['touch_path']}")
print(f" warm_runs: {summary['warm_runs']}")
print(f" skip_cold: {summary['skip_cold']}")
for name, payload in summary["checkpoints"].items():
if payload.get("skipped"):
print(f" {name}: SKIPPED")
elif payload.get("ok", False):
print(
f" {name}: min={payload['min_seconds']:.3f}s "
f"median={payload['median_seconds']:.3f}s avg={payload['avg_seconds']:.3f}s "
f"max={payload['max_seconds']:.3f}s"
)
else:
print(
f" {name}: FAILED exit={payload.get('exit_code')} error={payload.get('error', '')[:160]}"
)
if summary["failed_checkpoints"]:
print(f" failed_checkpoints: {', '.join(summary['failed_checkpoints'])}")
PY
fi
+367
View File
@@ -0,0 +1,367 @@
#!/usr/bin/env python3
"""Benchmark and optionally regression-check jcode startup time.
This script runs isolated startup measurements under a temporary JCODE_HOME and
JCODE_RUNTIME_DIR so it does not interfere with the user's real server, logs, or
credentials.
Cold client startup is measured by launching the normal default client path in a
pseudo-terminal, then parsing the built-in startup profile written to the
isolated log.
"""
from __future__ import annotations
import argparse
import os
import re
import shutil
import socket
import statistics
import subprocess
import sys
import tempfile
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable
PROFILE_TOTAL_RE = re.compile(r"Startup Profile \(([0-9.]+)ms total\)")
PROFILE_LINE_RE = re.compile(
r"\[INFO\]\s+([0-9.]+)ms\s+([0-9.]+)ms\s+[0-9.]+%\s+([a-zA-Z0-9_]+)"
)
REMOTE_HISTORY_RE = re.compile(r"remote bootstrap: history after ([0-9.]+)ms")
@dataclass
class StartupProfile:
total_ms: float
deltas_ms: dict[str, float]
remote_history_ms: float | None = None
@dataclass
class Budget:
name: str
actual_ms: float
limit_ms: float
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("binary", nargs="?", default="./target/release/jcode")
parser.add_argument("--runs", type=int, default=5, help="number of startup runs")
parser.add_argument(
"--check",
action="store_true",
help="fail if startup budgets are exceeded",
)
parser.add_argument("--max-help-ms", type=float, default=20.0)
parser.add_argument("--max-version-ms", type=float, default=20.0)
parser.add_argument("--max-server-ready-ms", type=float, default=80.0)
parser.add_argument("--max-cold-total-ms", type=float, default=150.0)
parser.add_argument("--max-cold-server-check-ms", type=float, default=20.0)
parser.add_argument("--max-cold-server-spawn-ms", type=float, default=20.0)
parser.add_argument("--max-cold-app-new-ms", type=float, default=20.0)
parser.add_argument("--max-remote-bootstrap-history-ms", type=float, default=250.0)
return parser.parse_args()
def median(values: Iterable[float]) -> float:
vals = list(values)
if not vals:
raise ValueError("no values")
return statistics.median(vals)
def median_or_none(values: Iterable[float]) -> float | None:
vals = list(values)
if not vals:
return None
return statistics.median(vals)
def print_stats(name: str, times: list[float]) -> None:
if not times:
print(f"\n{name}: No successful runs")
return
print(f"\n{name}:")
print(f" Min: {min(times):.2f} ms")
print(f" Max: {max(times):.2f} ms")
print(f" Mean: {statistics.mean(times):.2f} ms")
print(f" Median: {statistics.median(times):.2f} ms")
if len(times) > 1:
print(f" Stdev: {statistics.stdev(times):.2f} ms")
def run_simple_timing(binary: str, *args: str, runs: int) -> list[float]:
times: list[float] = []
for _ in range(runs):
start = time.perf_counter()
subprocess.run([binary, *args], capture_output=True, check=False)
times.append((time.perf_counter() - start) * 1000)
return times
def isolated_env(root: str) -> dict[str, str]:
env = os.environ.copy()
env["JCODE_HOME"] = os.path.join(root, "home")
env["JCODE_RUNTIME_DIR"] = os.path.join(root, "run")
env["JCODE_SOCKET"] = os.path.join(env["JCODE_RUNTIME_DIR"], "jcode.sock")
env["JCODE_NO_TELEMETRY"] = "1"
os.makedirs(env["JCODE_HOME"], exist_ok=True)
os.makedirs(env["JCODE_RUNTIME_DIR"], exist_ok=True)
return env
def wait_for_socket(path: str, timeout_s: float) -> bool:
deadline = time.perf_counter() + timeout_s
while time.perf_counter() < deadline:
if os.path.exists(path):
try:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(path)
sock.close()
return True
except OSError:
pass
time.sleep(0.005)
return False
def measure_server_startup(binary: str, runs: int) -> list[float]:
times: list[float] = []
for _ in range(runs):
root = tempfile.mkdtemp(prefix="jcode-server-bench-")
env = isolated_env(root)
socket_path = env["JCODE_SOCKET"]
proc = None
try:
start = time.perf_counter()
proc = subprocess.Popen(
[binary, "serve"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
env=env,
)
if wait_for_socket(socket_path, 5.0):
times.append((time.perf_counter() - start) * 1000)
finally:
if proc is not None:
proc.terminate()
try:
proc.wait(timeout=2)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait(timeout=2)
shutil.rmtree(root, ignore_errors=True)
return times
def require_script_binary() -> str:
script_bin = shutil.which("script")
if not script_bin:
raise RuntimeError("'script' utility not found; required for TTY startup benchmark")
return script_bin
def parse_startup_profile(log_path: Path) -> StartupProfile:
lines = log_path.read_text().splitlines()
last_block: list[str] = []
remote_history_ms = None
for i, line in enumerate(lines):
if "=== Startup Profile (" in line:
last_block = lines[i : i + 40]
remote_match = REMOTE_HISTORY_RE.search(line)
if remote_match:
remote_history_ms = float(remote_match.group(1))
if not last_block:
raise RuntimeError(f"no startup profile found in {log_path}")
total_ms = None
deltas: dict[str, float] = {}
for line in last_block:
total_match = PROFILE_TOTAL_RE.search(line)
if total_match:
total_ms = float(total_match.group(1))
phase_match = PROFILE_LINE_RE.search(line)
if phase_match:
_from_start, delta_ms, name = phase_match.groups()
deltas[name] = float(delta_ms)
if total_ms is None:
raise RuntimeError(f"could not parse startup profile total from {log_path}")
return StartupProfile(
total_ms=total_ms,
deltas_ms=deltas,
remote_history_ms=remote_history_ms,
)
def measure_cold_client_startup(binary: str, runs: int) -> list[StartupProfile]:
script_bin = require_script_binary()
profiles: list[StartupProfile] = []
for _ in range(runs):
root = tempfile.mkdtemp(prefix="jcode-cold-bench-")
env = isolated_env(root)
log_path = Path(env["JCODE_HOME"]) / "logs" / f"jcode-{time.strftime('%Y-%m-%d')}.log"
try:
command = (
f"{binary} --no-update --debug-socket "
f"--socket {env['JCODE_SOCKET']}"
)
subprocess.run(
["timeout", "3s", script_bin, "-qefc", command, "/dev/null"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
env=env,
check=False,
)
profiles.append(parse_startup_profile(log_path))
finally:
shutil.rmtree(root, ignore_errors=True)
return profiles
def print_cold_profile_stats(profiles: list[StartupProfile]) -> None:
totals = [p.total_ms for p in profiles]
print_stats("Cold client startup total", totals)
for phase in ["server_check", "server_spawn_start", "server_ready", "app_new_for_remote"]:
values = [p.deltas_ms[phase] for p in profiles if phase in p.deltas_ms]
print_stats(f"Cold client phase: {phase}", values)
remote_history = [p.remote_history_ms for p in profiles if p.remote_history_ms is not None]
print_stats("Cold client phase: remote bootstrap history", remote_history)
def collect_budgets(
help_times: list[float],
version_times: list[float],
server_times: list[float],
cold_profiles: list[StartupProfile],
args: argparse.Namespace,
) -> list[Budget]:
cold_total = [p.total_ms for p in cold_profiles]
cold_server_check = [p.deltas_ms.get("server_check", 0.0) for p in cold_profiles]
cold_server_spawn = [p.deltas_ms.get("server_spawn_start", 0.0) for p in cold_profiles]
cold_app_new = [p.deltas_ms.get("app_new_for_remote", 0.0) for p in cold_profiles]
cold_remote_history = [
p.remote_history_ms for p in cold_profiles if p.remote_history_ms is not None
]
budgets = [
Budget("--help median", median(help_times), args.max_help_ms),
Budget("--version median", median(version_times), args.max_version_ms),
Budget("cold startup total median", median(cold_total), args.max_cold_total_ms),
Budget(
"cold startup server_check median",
median(cold_server_check),
args.max_cold_server_check_ms,
),
Budget(
"cold startup server_spawn_start median",
median(cold_server_spawn),
args.max_cold_server_spawn_ms,
),
Budget(
"cold startup app_new_for_remote median",
median(cold_app_new),
args.max_cold_app_new_ms,
),
]
cold_remote_history_median = median_or_none(cold_remote_history)
if cold_remote_history_median is not None:
budgets.append(
Budget(
"cold startup remote bootstrap history median",
cold_remote_history_median,
args.max_remote_bootstrap_history_ms,
)
)
server_ready_median = median_or_none(server_times)
if server_ready_median is not None:
budgets.insert(2, Budget("server ready median", server_ready_median, args.max_server_ready_ms))
return budgets
def main() -> int:
args = parse_args()
binary = args.binary
if not os.path.exists(binary):
print(f"Binary not found: {binary}")
print("Run: cargo build --release")
return 1
print(f"Benchmarking: {binary}")
print("=" * 60)
subprocess.run([binary, "--version"], capture_output=True, check=False)
help_times = run_simple_timing(binary, "--help", runs=args.runs)
print_stats("--help (binary load)", help_times)
version_times = run_simple_timing(binary, "--version", runs=args.runs)
print_stats("--version", version_times)
print(f"\nMeasuring isolated server startup ({args.runs} runs)...")
server_times = measure_server_startup(binary, args.runs)
print_stats("Server ready (isolated socket connectable)", server_times)
print(f"\nMeasuring isolated cold client startup ({args.runs} runs)...")
cold_profiles = measure_cold_client_startup(binary, args.runs)
print_cold_profile_stats(cold_profiles)
print("\n" + "=" * 60)
print("Summary:")
help_median = median_or_none(help_times)
server_median = median_or_none(server_times)
cold_median = median_or_none(p.total_ms for p in cold_profiles)
remote_history_median = median_or_none(
p.remote_history_ms for p in cold_profiles if p.remote_history_ms is not None
)
print(
f" Binary load median: ~{help_median:.1f} ms"
if help_median is not None
else " Binary load median: n/a"
)
print(
f" Server ready median: ~{server_median:.1f} ms"
if server_median is not None
else " Server ready median: n/a"
)
print(
f" Cold client total median:~{cold_median:.1f} ms"
if cold_median is not None
else " Cold client total median:n/a"
)
print(
f" Remote history median: ~{remote_history_median:.1f} ms"
if remote_history_median is not None
else " Remote history median: n/a"
)
budgets = collect_budgets(help_times, version_times, server_times, cold_profiles, args)
if args.check:
failures = [b for b in budgets if b.actual_ms > b.limit_ms]
print("\nBudget check:")
for budget in budgets:
status = "FAIL" if budget.actual_ms > budget.limit_ms else "PASS"
print(
f" [{status}] {budget.name}: {budget.actual_ms:.1f} ms <= {budget.limit_ms:.1f} ms"
)
if failures:
print("\nStartup regression detected.")
return 2
print("\nAll startup budgets passed.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+330
View File
@@ -0,0 +1,330 @@
#!/usr/bin/env python3
"""Benchmark interactive CLI startup using user-visible metrics.
Measures two UX-focused metrics for interactive PTY launches:
1. time to first visible content
2. time until typed probe text appears on the rendered screen (input-ready)
The benchmark drives a pseudo-terminal, answers common terminal capability
queries, renders the output through a terminal screen model, and detects when
meaningful text becomes visible.
"""
from __future__ import annotations
import argparse
import json
import os
import pty
import select
import signal
import statistics
import struct
import subprocess
import sys
import tempfile
import termios
import time
from dataclasses import dataclass
from pathlib import Path
try:
import fcntl
except ImportError as exc: # pragma: no cover
raise SystemExit(f"fcntl unavailable: {exc}")
try:
import pyte
except ImportError as exc: # pragma: no cover
raise SystemExit(
"pyte is required. Install with: python3 -m pip install pyte\n"
f"Import error: {exc}"
)
PROBE = "jqx92"
DEFAULT_RUNS = 10
DEFAULT_TIMEOUT_S = 10.0
@dataclass(frozen=True)
class ToolSpec:
name: str
argv: list[str]
no_telem_env: dict[str, str] | None = None
disable_selfdev: bool = False
input_ready_log_marker: str | None = None
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--runs", type=int, default=DEFAULT_RUNS)
parser.add_argument("--timeout", type=float, default=DEFAULT_TIMEOUT_S)
parser.add_argument("--cwd", default=os.getcwd())
parser.add_argument(
"--tools",
nargs="*",
default=None,
help="subset of tool names to benchmark",
)
parser.add_argument(
"--json-out",
default="/var/tmp/startup_visible_ready_results.json",
help="where to write the JSON results",
)
return parser.parse_args()
def detect_pi_bin() -> str:
pi = shutil_which("pi")
if pi:
return pi
prefix = subprocess.check_output(["npm", "prefix", "-g"], text=True).strip()
candidate = Path(prefix) / "bin" / "pi"
if candidate.exists():
return str(candidate)
raise FileNotFoundError("could not find pi binary")
def shutil_which(name: str) -> str | None:
return subprocess.run(
["bash", "-lc", f"command -v {name}"],
capture_output=True,
text=True,
check=False,
).stdout.strip() or None
def build_tool_specs() -> list[ToolSpec]:
specs = [
ToolSpec(
name="jcode",
argv=["jcode", "--no-update", "--no-selfdev"],
no_telem_env={"JCODE_NO_TELEMETRY": "1"},
disable_selfdev=True,
),
ToolSpec(name="pi", argv=[detect_pi_bin()]),
ToolSpec(name="opencode", argv=["opencode"]),
ToolSpec(name="codex", argv=["codex"]),
ToolSpec(name="claude_code", argv=["claude"]),
ToolSpec(name="cursor_agent", argv=["cursor-agent"]),
ToolSpec(name="copilot_cli", argv=["copilot"]),
ToolSpec(
name="antigravity_cli",
argv=["agy"],
no_telem_env={"AGY_CLI_DISABLE_AUTO_UPDATE": "1"},
input_ready_log_marker="CLI ready for user input",
),
]
return specs
def configure_pty(slave_fd: int, rows: int = 24, cols: int = 80) -> None:
fcntl.ioctl(slave_fd, termios.TIOCSWINSZ, struct.pack("HHHH", rows, cols, 0, 0))
attrs = termios.tcgetattr(slave_fd)
attrs[3] &= ~(termios.ECHO | termios.ICANON)
attrs[0] &= ~(termios.ICRNL | termios.IXON)
termios.tcsetattr(slave_fd, termios.TCSANOW, attrs)
def reply_queries(master_fd: int, buffer: bytes) -> bytes:
replies = [
(b"\x1b[6n", b"\x1b[1;1R"),
(b"\x1b[c", b"\x1b[?62;c"),
(b"\x1b]10;?\x1b\\", b"\x1b]10;rgb:ffff/ffff/ffff\x1b\\"),
(b"\x1b]11;?\x1b\\", b"\x1b]11;rgb:0000/0000/0000\x1b\\"),
(b"\x1b]10;?\x07", b"\x1b]10;rgb:ffff/ffff/ffff\x07"),
(b"\x1b]11;?\x07", b"\x1b]11;rgb:0000/0000/0000\x07"),
(b"\x1b]4;0;?\x07", b"\x1b]4;0;rgb:0000/0000/0000\x07"),
(b"\x1b[14t", b"\x1b[4;600;800t"),
(b"\x1b[16t", b"\x1b[6;16;8t"),
(b"\x1b[18t", b"\x1b[8;24;80t"),
(b"\x1b[?1016$p", b"\x1b[?1016;1$y"),
(b"\x1b[?2027$p", b"\x1b[?2027;1$y"),
(b"\x1b[?2031$p", b"\x1b[?2031;1$y"),
(b"\x1b[?1004$p", b"\x1b[?1004;1$y"),
(b"\x1b[?2004$p", b"\x1b[?2004;1$y"),
(b"\x1b[?2026$p", b"\x1b[?2026;1$y"),
]
changed = True
while changed:
changed = False
for query, response in replies:
if query in buffer:
os.write(master_fd, response)
buffer = buffer.replace(query, b"")
changed = True
return buffer
def first_meaningful_line(screen: pyte.Screen) -> str | None:
for line in screen.display:
normalized = " ".join(line.split())
if not normalized or PROBE in normalized:
continue
alnum_count = sum(ch.isalnum() for ch in normalized)
if alnum_count >= 3 and len(normalized) >= 4:
return normalized[:120]
return None
def run_once(spec: ToolSpec, cwd: Path, timeout_s: float) -> dict[str, object]:
master_fd, slave_fd = pty.openpty()
configure_pty(slave_fd)
env = os.environ.copy()
env["TERM"] = "xterm-256color"
env["COLORTERM"] = "truecolor"
if spec.no_telem_env:
env.update(spec.no_telem_env)
argv = spec.argv
input_ready_log_path: Path | None = None
if spec.input_ready_log_marker:
log_file = tempfile.NamedTemporaryFile(prefix=f"{spec.name}-", suffix=".log", delete=False)
input_ready_log_path = Path(log_file.name)
log_file.close()
argv = [*spec.argv, "--log-file", str(input_ready_log_path)]
proc = subprocess.Popen(
argv,
cwd=str(cwd),
env=env,
stdin=slave_fd,
stdout=slave_fd,
stderr=slave_fd,
preexec_fn=os.setsid,
)
os.close(slave_fd)
os.set_blocking(master_fd, False)
screen = pyte.Screen(80, 24)
stream = pyte.Stream(screen)
start = time.perf_counter()
query_buffer = b""
first_visible_ms: float | None = None
first_visible_excerpt: str | None = None
input_ready_ms: float | None = None
probe_sent = False
try:
while time.perf_counter() - start < timeout_s:
rlist, _, _ = select.select([master_fd], [], [], 0.05)
if rlist:
try:
chunk = os.read(master_fd, 65536)
except BlockingIOError:
chunk = b""
if chunk:
query_buffer += chunk
query_buffer = reply_queries(master_fd, query_buffer)
stream.feed(chunk.decode("utf-8", "replace"))
if first_visible_ms is None:
excerpt = first_meaningful_line(screen)
if excerpt:
first_visible_ms = (time.perf_counter() - start) * 1000.0
first_visible_excerpt = excerpt
os.write(master_fd, PROBE.encode())
probe_sent = True
elif probe_sent and input_ready_ms is None:
if PROBE in "\n".join(screen.display):
input_ready_ms = (time.perf_counter() - start) * 1000.0
break
if (
spec.input_ready_log_marker
and first_visible_ms is not None
and input_ready_ms is None
and input_ready_log_path
):
try:
if spec.input_ready_log_marker in input_ready_log_path.read_text(errors="replace"):
input_ready_ms = (time.perf_counter() - start) * 1000.0
break
except OSError:
pass
return {
"first_visible_ms": first_visible_ms,
"first_visible_excerpt": first_visible_excerpt,
"input_ready_ms": input_ready_ms,
"input_ready_source": "log_marker" if spec.input_ready_log_marker else "probe_echo",
}
finally:
for sig in (signal.SIGTERM, signal.SIGKILL):
try:
os.killpg(proc.pid, sig)
time.sleep(0.1)
except ProcessLookupError:
break
try:
proc.wait(timeout=1)
except Exception:
pass
os.close(master_fd)
if input_ready_log_path:
try:
input_ready_log_path.unlink()
except OSError:
pass
def summarize(samples: list[float | None]) -> dict[str, float | int] | None:
values = [sample for sample in samples if sample is not None]
if not values:
return None
return {
"median_ms": statistics.median(values),
"min_ms": min(values),
"max_ms": max(values),
"mean_ms": statistics.mean(values),
"runs_completed": len(values),
"runs_total": len(samples),
}
def version_for(spec: ToolSpec) -> str:
argv = spec.argv[:1]
if spec.name == "jcode":
argv = [spec.argv[0], "version"]
else:
argv = [spec.argv[0], "--version"]
proc = subprocess.run(argv, capture_output=True, text=True, check=False)
output = (proc.stdout + proc.stderr).strip().splitlines()
return output[0] if output else f"exit {proc.returncode}"
def main() -> None:
args = parse_args()
selected = set(args.tools or [])
specs = build_tool_specs()
if selected:
specs = [spec for spec in specs if spec.name in selected]
cwd = Path(args.cwd).resolve()
results: dict[str, object] = {
"runs": args.runs,
"timeout_s": args.timeout,
"cwd": str(cwd),
"tools": {},
}
for spec in specs:
print(f"=== {spec.name} ===", flush=True)
runs: list[dict[str, object]] = []
for i in range(args.runs):
run = run_once(spec, cwd, args.timeout)
runs.append(run)
print(
f"run {i + 1}/{args.runs}: "
f"visible={run['first_visible_ms']} ready={run['input_ready_ms']} "
f"excerpt={run['first_visible_excerpt']}",
flush=True,
)
results["tools"][spec.name] = {
"version": version_for(spec),
"runs": runs,
"first_visible_summary": summarize([r["first_visible_ms"] for r in runs]),
"input_ready_summary": summarize([r["input_ready_ms"] for r in runs]),
}
out_path = Path(args.json_out)
out_path.write_text(json.dumps(results, indent=2))
print(f"WROTE {out_path}", flush=True)
if __name__ == "__main__":
main()
+588
View File
@@ -0,0 +1,588 @@
#!/usr/bin/env python3
"""
Benchmark: single agent vs swarm on the Anthropic Performance Take-Home.
Compares jcode's swarm (multi-agent coordination) with single-agent performance
on the VLIW SIMD kernel optimization challenge.
Usage:
python scripts/benchmark_swarm.py # Run both trials
python scripts/benchmark_swarm.py --single-only # Single agent only
python scripts/benchmark_swarm.py --swarm-only # Swarm only
python scripts/benchmark_swarm.py --timeout 30 # 30 minute timeout per trial
python scripts/benchmark_swarm.py --check-interval 15 # Check cycles every 15s
Environment:
Requires jcode server running with debug_control enabled:
touch ~/.jcode/debug_control
jcode serve
"""
import argparse
import json
import os
import select
import shutil
import socket
import subprocess
import sys
import time
from pathlib import Path
DEBUG_SOCKET = f"/run/user/{os.getuid()}/jcode-debug.sock"
MAIN_SOCKET = f"/run/user/{os.getuid()}/jcode.sock"
TAKEHOME_SOURCE = os.environ.get(
"TAKEHOME_SOURCE", str(Path.home() / "original_performance_takehome")
)
BENCHMARK_DIR = "/tmp/takehome-benchmark"
BASELINE = 147734
# ---------------------------------------------------------------------------
# Socket helpers
# ---------------------------------------------------------------------------
def send_cmd(cmd: str, session_id: str = None, timeout: float = 300) -> tuple:
"""Send a debug command and return (ok, output, error)."""
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(DEBUG_SOCKET)
sock.setblocking(False)
req = {"type": "debug_command", "id": 1, "command": cmd}
if session_id:
req["session_id"] = session_id
sock.send((json.dumps(req) + "\n").encode())
start = time.time()
data = b""
while time.time() - start < timeout:
ready, _, _ = select.select([sock], [], [], 1.0)
if ready:
try:
chunk = sock.recv(65536)
if not chunk:
break
data += chunk
if b"\n" in data:
break
except BlockingIOError:
continue
sock.close()
if not data:
return False, "", "Timeout"
try:
resp = json.loads(data.decode().strip())
return resp.get("ok", False), resp.get("output", ""), resp.get("error", "")
except json.JSONDecodeError as e:
return False, "", f"JSON error: {e}"
def create_session(working_dir: str) -> tuple:
"""Create a headless session. Returns (session_id, friendly_name)."""
ok, output, err = send_cmd(f"create_session:{working_dir}", timeout=120)
if not ok:
raise RuntimeError(f"Failed to create session: {err}")
data = json.loads(output)
return data["session_id"], data.get("friendly_name", data["session_id"][:12])
def destroy_session(session_id: str):
"""Destroy a session."""
send_cmd(f"destroy_session:{session_id}")
# ---------------------------------------------------------------------------
# Workspace helpers
# ---------------------------------------------------------------------------
def setup_workspace(name: str) -> str:
"""Create a clean copy of the take-home challenge."""
workspace = os.path.join(BENCHMARK_DIR, name)
if os.path.exists(workspace):
shutil.rmtree(workspace)
shutil.copytree(TAKEHOME_SOURCE, workspace)
# Initialize a git repo so swarm_id detection works
subprocess.run(["git", "init"], cwd=workspace, capture_output=True)
subprocess.run(["git", "add", "."], cwd=workspace, capture_output=True)
subprocess.run(
["git", "commit", "-m", "initial"],
cwd=workspace,
capture_output=True,
env={**os.environ, "GIT_AUTHOR_NAME": "bench", "GIT_AUTHOR_EMAIL": "b@b",
"GIT_COMMITTER_NAME": "bench", "GIT_COMMITTER_EMAIL": "b@b"},
)
return workspace
def get_cycles(workspace: str) -> int:
"""Run submission_tests.py and extract cycle count."""
try:
result = subprocess.run(
[sys.executable, "tests/submission_tests.py", "-v"],
cwd=workspace,
capture_output=True,
text=True,
timeout=120,
)
for line in (result.stdout + result.stderr).split("\n"):
if "CYCLES:" in line:
return int(line.split("CYCLES:")[1].strip())
except Exception as e:
print(f" Error getting cycles: {e}")
return BASELINE
def get_test_summary(workspace: str) -> str:
"""Run submission tests and return the full output summary."""
try:
result = subprocess.run(
[sys.executable, "tests/submission_tests.py", "-v"],
cwd=workspace,
capture_output=True,
text=True,
timeout=120,
)
return result.stdout + result.stderr
except Exception as e:
return f"Error: {e}"
# ---------------------------------------------------------------------------
# Optimization prompt
# ---------------------------------------------------------------------------
OPTIMIZATION_PROMPT_TEMPLATE = """Optimize the build_kernel() method in perf_takehome.py to minimize cycle count \
on the VLIW SIMD machine simulator. The baseline is 147,734 cycles.
IMPORTANT: You MUST work in this directory: {workspace}
All file paths should be relative to or within this directory.
Key files (in {workspace}):
- problem.py: Defines the Machine, instruction set, slot limits, engines
- perf_takehome.py: Contains KernelBuilder.build_kernel() - THIS is what you optimize
- tests/submission_tests.py: Run to verify correctness and see cycle count. DO NOT modify tests/.
Machine details (read problem.py for full spec):
- VLEN=8 vector width, N_CORES=1
- VLIW bundles: multiple operations per cycle, subject to slot limits per engine
- Engines: load, store, alu, flow, debug, vload, vstore, valu
- Scratch memory for temporaries (SCRATCH_SIZE limit)
Focus on these optimization strategies:
1. Vectorization - use VALU/VLOAD/VSTORE engines with VLEN=8 to process 8 elements at once
2. VLIW instruction packing - bundle independent operations into the same cycle
3. Loop structure - unroll loops, reduce iteration overhead
4. Hash function optimization - it runs many times; pack hash stages
5. Efficient memory access patterns - batch loads/stores, reduce address computation
After each change, verify with: cd {workspace} && python tests/submission_tests.py
Work efficiently - focus on the highest-impact optimizations first."""
# ---------------------------------------------------------------------------
# Poll loop for async jobs
# ---------------------------------------------------------------------------
def poll_job(
job_id: str,
session_id: str,
workspace: str,
start_time: float,
timeout_seconds: float,
check_interval: float,
label: str,
) -> int:
"""Poll a job until completion, printing cycle updates. Returns best cycle count."""
best_cycles = BASELINE
last_cycles = BASELINE
while time.time() - start_time < timeout_seconds:
elapsed = time.time() - start_time
# Check job status
ok, status_output, _ = send_cmd(f"job_status:{job_id}", session_id, timeout=10)
if ok:
try:
status = json.loads(status_output)
job_status = status.get("status", "unknown")
if job_status == "completed":
print(f"\n [{label}] [{elapsed/60:.1f}m] Job completed")
break
elif job_status == "failed":
error = status.get("error", "unknown")
print(f"\n [{label}] [{elapsed/60:.1f}m] Job failed: {error}")
break
except (json.JSONDecodeError, ValueError):
pass
# Check current cycles in workspace
cycles = get_cycles(workspace)
if cycles < best_cycles:
best_cycles = cycles
speedup = BASELINE / cycles
print(f" [{label}] [{elapsed/60:.1f}m] NEW BEST: {cycles} cycles ({speedup:.2f}x speedup)")
elif cycles != last_cycles:
print(f" [{label}] [{elapsed/60:.1f}m] Cycles: {cycles}")
last_cycles = cycles
time.sleep(check_interval)
# Final check
cycles = get_cycles(workspace)
if cycles < best_cycles:
best_cycles = cycles
return best_cycles
# ---------------------------------------------------------------------------
# Trial A: Single Agent
# ---------------------------------------------------------------------------
def run_single_agent(timeout_minutes: float, check_interval: float) -> dict:
"""Run a single agent on the optimization task."""
print("\n" + "=" * 70)
print(f" TRIAL A: SINGLE AGENT (timeout: {timeout_minutes}m)")
print("=" * 70)
workspace = setup_workspace("single")
print(f" Workspace: {workspace}")
start_time = time.time()
session_id = None
try:
session_id, name = create_session(workspace)
print(f" Session: {name} ({session_id[:12]}...)")
baseline_cycles = get_cycles(workspace)
print(f" Baseline: {baseline_cycles} cycles")
# Build prompt
prompt = OPTIMIZATION_PROMPT_TEMPLATE.format(workspace=workspace)
# Start async job
print("\n Starting optimization (message_async)...")
ok, output, err = send_cmd(f"message_async:{prompt}", session_id, timeout=30)
if not ok:
print(f" Failed to start async job: {err}")
return {
"approach": "single",
"cycles": BASELINE,
"time_seconds": 0,
"error": err,
}
job_data = json.loads(output)
job_id = job_data.get("job_id")
print(f" Job started: {job_id}")
# Poll until done
timeout_seconds = timeout_minutes * 60
best_cycles = poll_job(
job_id, session_id, workspace, start_time, timeout_seconds,
check_interval, "single",
)
elapsed = time.time() - start_time
speedup = BASELINE / best_cycles if best_cycles > 0 else 0
print(f"\n SINGLE AGENT RESULT: {best_cycles} cycles in {elapsed/60:.1f}m ({speedup:.2f}x)")
# Get full test output
test_output = get_test_summary(workspace)
print(f"\n Test output:\n{test_output}")
return {
"approach": "single",
"cycles": best_cycles,
"time_seconds": elapsed,
"workspace": workspace,
}
except Exception as e:
elapsed = time.time() - start_time
print(f" Error: {e}")
return {
"approach": "single",
"cycles": BASELINE,
"time_seconds": elapsed,
"error": str(e),
}
finally:
if session_id:
print(f" Cleaning up session {session_id[:12]}...")
destroy_session(session_id)
# ---------------------------------------------------------------------------
# Trial B: Swarm (Multi-Agent)
# ---------------------------------------------------------------------------
def run_swarm(timeout_minutes: float, check_interval: float) -> dict:
"""Run swarm multi-agent on the optimization task."""
print("\n" + "=" * 70)
print(f" TRIAL B: SWARM / MULTI-AGENT (timeout: {timeout_minutes}m)")
print("=" * 70)
workspace = setup_workspace("swarm")
print(f" Workspace: {workspace}")
start_time = time.time()
session_id = None
try:
session_id, name = create_session(workspace)
print(f" Coordinator: {name} ({session_id[:12]}...)")
baseline_cycles = get_cycles(workspace)
print(f" Baseline: {baseline_cycles} cycles")
# Build prompt (same optimization goal)
prompt = OPTIMIZATION_PROMPT_TEMPLATE.format(workspace=workspace)
# Start swarm async job - this automatically plans subtasks and spawns agents
print("\n Starting swarm (swarm_message_async)...")
ok, output, err = send_cmd(f"swarm_message_async:{prompt}", session_id, timeout=30)
if not ok:
print(f" Failed to start swarm: {err}")
return {
"approach": "swarm",
"cycles": BASELINE,
"time_seconds": 0,
"error": err,
}
job_data = json.loads(output)
job_id = job_data.get("job_id")
print(f" Swarm job started: {job_id}")
timeout_seconds = timeout_minutes * 60
best_cycles = BASELINE
last_cycles = BASELINE
member_info_printed = False
while time.time() - start_time < timeout_seconds:
elapsed = time.time() - start_time
# Check job status
ok, status_output, _ = send_cmd(f"job_status:{job_id}", session_id, timeout=10)
if ok:
try:
status = json.loads(status_output)
job_status = status.get("status", "unknown")
if job_status == "completed":
print(f"\n [swarm] [{elapsed/60:.1f}m] Swarm completed!")
break
elif job_status == "failed":
error = status.get("error", "unknown")
print(f"\n [swarm] [{elapsed/60:.1f}m] Swarm failed: {error}")
break
except (json.JSONDecodeError, ValueError):
pass
# Show swarm members (once, early on)
if not member_info_printed and elapsed > 10:
ok, swarm_output, _ = send_cmd("swarm:members", session_id, timeout=10)
if ok:
try:
members = json.loads(swarm_output)
print(f" [swarm] [{elapsed/60:.1f}m] {len(members)} agent(s) in swarm")
for m in members[:5]:
sid = m.get("session_id", "?")[:12]
st = m.get("status", "?")
print(f" - {sid}... ({st})")
member_info_printed = True
except (json.JSONDecodeError, ValueError):
pass
# Check current cycles
cycles = get_cycles(workspace)
if cycles < best_cycles:
best_cycles = cycles
speedup = BASELINE / cycles
print(f" [swarm] [{elapsed/60:.1f}m] NEW BEST: {cycles} cycles ({speedup:.2f}x speedup)")
elif cycles != last_cycles:
print(f" [swarm] [{elapsed/60:.1f}m] Cycles: {cycles}")
last_cycles = cycles
time.sleep(check_interval)
# Final check
cycles = get_cycles(workspace)
if cycles < best_cycles:
best_cycles = cycles
elapsed = time.time() - start_time
speedup = BASELINE / best_cycles if best_cycles > 0 else 0
print(f"\n SWARM RESULT: {best_cycles} cycles in {elapsed/60:.1f}m ({speedup:.2f}x)")
# Get full test output
test_output = get_test_summary(workspace)
print(f"\n Test output:\n{test_output}")
return {
"approach": "swarm",
"cycles": best_cycles,
"time_seconds": elapsed,
"workspace": workspace,
}
except Exception as e:
elapsed = time.time() - start_time
print(f" Error: {e}")
return {
"approach": "swarm",
"cycles": BASELINE,
"time_seconds": elapsed,
"error": str(e),
}
finally:
if session_id:
print(f" Cleaning up session {session_id[:12]}...")
destroy_session(session_id)
# ---------------------------------------------------------------------------
# Results comparison
# ---------------------------------------------------------------------------
def print_comparison(results: dict):
"""Print a comparison table of all trials."""
print("\n" + "=" * 70)
print(" BENCHMARK RESULTS")
print("=" * 70)
header = f" {'Approach':<15} {'Cycles':<12} {'Time':<12} {'Speedup':<12} {'Status'}"
print(header)
print(" " + "-" * 66)
for name, data in results.items():
cycles = data["cycles"]
time_m = data["time_seconds"] / 60
speedup = BASELINE / cycles if cycles > 0 else 0
status = "ERROR" if "error" in data else "OK"
print(f" {name:<15} {cycles:<12} {time_m:<12.1f}m {speedup:<12.2f}x {status}")
if len(results) > 1:
print()
winner = min(results.items(), key=lambda x: x[1]["cycles"])
loser = max(results.items(), key=lambda x: x[1]["cycles"])
winner_name, winner_data = winner
loser_name, loser_data = loser
print(f" Winner: {winner_name} ({winner_data['cycles']} cycles)")
if loser_data["cycles"] > 0 and winner_data["cycles"] > 0:
relative = loser_data["cycles"] / winner_data["cycles"]
print(f" {winner_name} is {relative:.2f}x better than {loser_name}")
# Time comparison
if winner_data["time_seconds"] > 0 and loser_data["time_seconds"] > 0:
time_ratio = loser_data["time_seconds"] / winner_data["time_seconds"]
if time_ratio > 1:
print(f" {winner_name} was {time_ratio:.1f}x faster in wall time")
else:
print(f" {loser_name} was {1/time_ratio:.1f}x faster in wall time")
# Threshold analysis
print("\n Threshold Analysis:")
thresholds = [
("Baseline", BASELINE),
("Updated starter", 18532),
("Opus 4 many hours", 2164),
("Opus 4.5 casual (best human 2hr)", 1790),
("Opus 4.5 2hr harness", 1579),
("Sonnet 4.5 many hours", 1548),
("Opus 4.5 11.5hr harness", 1487),
("Opus 4.5 improved harness", 1363),
]
for name, data in results.items():
cycles = data["cycles"]
print(f"\n {name} ({cycles} cycles):")
for thresh_name, thresh_val in thresholds:
passed = "PASS" if cycles < thresh_val else "FAIL"
print(f" [{passed}] {thresh_name}: < {thresh_val}")
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description="Benchmark single agent vs swarm on VLIW SIMD optimization task",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument(
"--timeout", type=float, default=10,
help="Timeout in minutes per trial (default: 10)",
)
parser.add_argument(
"--check-interval", type=float, default=30,
help="How often to check cycle count, in seconds (default: 30)",
)
parser.add_argument(
"--single-only", action="store_true",
help="Only run single agent trial",
)
parser.add_argument(
"--swarm-only", action="store_true",
help="Only run swarm trial",
)
args = parser.parse_args()
# Validate environment
if not os.path.exists(DEBUG_SOCKET):
print(f"Error: Debug socket not found: {DEBUG_SOCKET}")
print("Make sure jcode server is running with debug_control enabled:")
print(" touch ~/.jcode/debug_control")
print(" jcode serve")
sys.exit(1)
if not os.path.exists(TAKEHOME_SOURCE):
print(f"Error: Take-home source not found: {TAKEHOME_SOURCE}")
sys.exit(1)
os.makedirs(BENCHMARK_DIR, exist_ok=True)
print("=" * 70)
print(" SWARM vs SINGLE-AGENT BENCHMARK")
print("=" * 70)
print(f" Timeout: {args.timeout} minutes per trial")
print(f" Check interval: {args.check_interval} seconds")
print(f" Source: {TAKEHOME_SOURCE}")
print(f" Baseline: {BASELINE} cycles")
print()
results = {}
run_single = not args.swarm_only
run_multi = not args.single_only
if run_single:
results["single"] = run_single_agent(args.timeout, args.check_interval)
if run_multi:
results["swarm"] = run_swarm(args.timeout, args.check_interval)
if results:
print_comparison(results)
else:
print("No trials were run.")
# Write results to JSON
results_file = os.path.join(BENCHMARK_DIR, "results.json")
with open(results_file, "w") as f:
json.dump(results, f, indent=2, default=str)
print(f"\n Results saved to: {results_file}")
if __name__ == "__main__":
main()
+408
View File
@@ -0,0 +1,408 @@
#!/usr/bin/env python3
"""
Benchmark single agent vs swarm on the Anthropic Performance Take-Home.
Usage:
BENCHMARK_TIMEOUT=5 python scripts/benchmark_takehome.py single
BENCHMARK_TIMEOUT=10 python scripts/benchmark_takehome.py swarm
BENCHMARK_TIMEOUT=10 python scripts/benchmark_takehome.py both
"""
import socket
import json
import os
import sys
import time
import select
import shutil
import subprocess
import threading
from pathlib import Path
DEBUG_SOCKET = f"/run/user/{os.getuid()}/jcode-debug.sock"
TAKEHOME_SOURCE = os.environ.get(
"TAKEHOME_SOURCE", str(Path.home() / "original_performance_takehome")
)
BENCHMARK_DIR = "/tmp/takehome-benchmark"
TIMEOUT_MINUTES = int(os.environ.get('BENCHMARK_TIMEOUT', '10'))
BASELINE = 147734
def send_cmd(cmd: str, session_id: str = None, timeout: float = 300) -> tuple:
"""Send a debug command and get response."""
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(DEBUG_SOCKET)
sock.setblocking(False)
req = {"type": "debug_command", "id": 1, "command": cmd}
if session_id:
req["session_id"] = session_id
sock.send((json.dumps(req) + '\n').encode())
start = time.time()
data = b""
while time.time() - start < timeout:
ready, _, _ = select.select([sock], [], [], 1.0)
if ready:
try:
chunk = sock.recv(65536)
if not chunk:
break
data += chunk
if b'\n' in data:
break
except BlockingIOError:
continue
sock.close()
if not data:
return False, "", "Timeout"
try:
resp = json.loads(data.decode().strip())
return resp.get('ok', False), resp.get('output', ''), resp.get('error', '')
except json.JSONDecodeError as e:
return False, "", f"JSON error: {e}"
def create_session(working_dir: str) -> tuple:
"""Create a session and return (session_id, friendly_name)."""
ok, output, err = send_cmd(f"create_session:{working_dir}", timeout=120)
if not ok:
raise RuntimeError(f"Failed to create session: {err}")
data = json.loads(output)
return data['session_id'], data.get('friendly_name', data['session_id'][:12])
def destroy_session(session_id: str):
"""Destroy a session."""
send_cmd(f"destroy_session:{session_id}")
def setup_workspace(name: str) -> str:
"""Create a clean copy of the take-home."""
workspace = os.path.join(BENCHMARK_DIR, name)
if os.path.exists(workspace):
shutil.rmtree(workspace)
shutil.copytree(TAKEHOME_SOURCE, workspace)
return workspace
def get_cycles(workspace: str) -> int:
"""Run tests and return cycle count."""
try:
result = subprocess.run(
["python", "tests/submission_tests.py", "-v"],
cwd=workspace,
capture_output=True,
text=True,
timeout=120
)
for line in (result.stdout + result.stderr).split('\n'):
if 'CYCLES:' in line:
return int(line.split('CYCLES:')[1].strip())
except Exception as e:
print(f"Error getting cycles: {e}")
return BASELINE
def make_single_prompt(workspace: str) -> str:
return f"""You are optimizing a VLIW SIMD kernel for Anthropic's performance take-home.
IMPORTANT: You MUST work in this directory: {workspace}
All file paths should be relative to or within this directory.
Goal: Reduce the cycle count from 147,734 to as low as possible.
Key files (in {workspace}):
- problem.py: Defines the Machine, instruction set (VLIW bundles, vector ops, VLEN=16)
- perf_takehome.py: Contains KernelBuilder.build_kernel() - this is what you optimize
- tests/submission_tests.py: Run to verify correctness and see cycle count
DO NOT modify tests/ folder.
Key optimizations to try:
1. VLIW parallelism - pack independent operations into single bundles
2. Vector operations - use VLEN=16 to process 16 elements at once
3. Reduce memory access latency - batch loads/stores
4. Optimize the hash function - it runs many times per element
Start by reading {workspace}/problem.py to understand the machine, then optimize build_kernel().
After each change, run `cd {workspace} && python tests/submission_tests.py` to check correctness and cycles.
Work efficiently - focus on the highest-impact optimizations first."""
def run_single_agent() -> dict:
"""Run a single agent benchmark using async messaging."""
print("\n" + "=" * 60)
print(f"SINGLE AGENT BENCHMARK (timeout: {TIMEOUT_MINUTES}m)")
print("=" * 60)
workspace = setup_workspace("single")
print(f"Workspace: {workspace}")
start_time = time.time()
session_id = None
best_cycles = BASELINE
try:
session_id, name = create_session(workspace)
print(f"Session: {name}")
# Initial cycles
cycles = get_cycles(workspace)
print(f"Baseline: {cycles} cycles")
# Send optimization task asynchronously
print("\nStarting optimization (async)...")
prompt = make_single_prompt(workspace)
# Use message_async to start the job
ok, output, err = send_cmd(f"message_async:{prompt}", session_id, timeout=30)
if not ok:
print(f"Failed to start async job: {err}")
return {"approach": "single", "cycles": BASELINE, "time_seconds": 0, "error": err}
job_data = json.loads(output)
job_id = job_data.get("job_id")
print(f"Job started: {job_id}")
timeout_seconds = TIMEOUT_MINUTES * 60
last_cycles = BASELINE
check_interval = 30 # Check every 30 seconds
while time.time() - start_time < timeout_seconds:
elapsed = time.time() - start_time
# Check job status
ok, status_output, _ = send_cmd(f"job_status:{job_id}", session_id, timeout=10)
if ok:
try:
status = json.loads(status_output)
job_status = status.get("status", "unknown")
if job_status in ["completed", "failed"]:
print(f"\n[{elapsed/60:.1f}m] Job {job_status}")
break
except:
pass
# Check current cycles in workspace
cycles = get_cycles(workspace)
if cycles < best_cycles:
best_cycles = cycles
print(f"[{elapsed/60:.1f}m] NEW BEST: {cycles} cycles ({BASELINE/cycles:.2f}x speedup)")
elif cycles != last_cycles:
print(f"[{elapsed/60:.1f}m] Cycles: {cycles}")
last_cycles = cycles
time.sleep(check_interval)
# Final check
cycles = get_cycles(workspace)
if cycles < best_cycles:
best_cycles = cycles
elapsed = time.time() - start_time
print(f"\nFinal: {best_cycles} cycles in {elapsed/60:.1f}m ({BASELINE/best_cycles:.2f}x)")
return {
"approach": "single",
"cycles": best_cycles,
"time_seconds": elapsed,
"workspace": workspace
}
except Exception as e:
print(f"Error: {e}")
return {
"approach": "single",
"cycles": BASELINE,
"time_seconds": time.time() - start_time,
"error": str(e)
}
finally:
if session_id:
destroy_session(session_id)
def run_swarm(n_agents: int = 2) -> dict:
"""Run autonomous swarm benchmark using swarm_message_async.
This uses the full swarm capability where ONE agent becomes coordinator,
creates a plan, and spawns subagents automatically.
"""
print("\n" + "=" * 60)
print(f"AUTONOMOUS SWARM BENCHMARK (timeout: {TIMEOUT_MINUTES}m)")
print("=" * 60)
workspace = setup_workspace("swarm")
print(f"Workspace: {workspace}")
start_time = time.time()
session_id = None
best_cycles = BASELINE
try:
# Create ONE session - it becomes coordinator and spawns agents
session_id, name = create_session(workspace)
print(f"Coordinator: {name}")
baseline = get_cycles(workspace)
print(f"Baseline: {baseline} cycles")
# Use swarm_message_async - this will:
# 1. Plan subtasks automatically
# 2. Spawn subagents to work in parallel
# 3. Integrate results
prompt = f"""Optimize the VLIW SIMD kernel in {workspace}/perf_takehome.py to minimize cycle count.
Current baseline: 147,734 cycles. Goal: as low as possible.
The problem:
- {workspace}/problem.py defines the machine (VLEN=16 vectors, VLIW bundles, slot limits)
- {workspace}/perf_takehome.py has build_kernel() which needs optimization
- Run `cd {workspace} && python tests/submission_tests.py` to verify correctness and check cycles
Key optimization strategies:
1. Vectorization - use VLEN=16 to process 16 elements at once
2. VLIW packing - bundle independent operations together
3. Reduce memory latency - batch loads/stores
4. Optimize hash function - it runs many times per element
Break this into parallel subtasks and spawn agents to work on different optimizations.
DO NOT modify tests/ folder."""
print("\nStarting autonomous swarm (swarm_message_async)...")
ok, output, err = send_cmd(f"swarm_message_async:{prompt}", session_id, timeout=30)
if not ok:
print(f"Failed to start swarm: {err}")
return {"approach": "swarm", "cycles": BASELINE, "time_seconds": 0, "error": err}
job_data = json.loads(output)
job_id = job_data.get("job_id")
print(f"Swarm job started: {job_id}")
timeout_seconds = TIMEOUT_MINUTES * 60
last_cycles = BASELINE
check_interval = 30
while time.time() - start_time < timeout_seconds:
elapsed = time.time() - start_time
# Check job status
ok, status_output, _ = send_cmd(f"job_status:{job_id}", session_id, timeout=10)
if ok:
try:
status = json.loads(status_output)
job_status = status.get("status", "unknown")
if job_status == "completed":
print(f"\n[{elapsed/60:.1f}m] Swarm completed!")
break
elif job_status == "failed":
print(f"\n[{elapsed/60:.1f}m] Swarm failed: {status.get('error', 'unknown')}")
break
except:
pass
# Check swarm members (to see how many agents were spawned)
ok, swarm_output, _ = send_cmd("swarm:members", session_id, timeout=10)
if ok and elapsed < 60: # Only print once early on
try:
print(f"[{elapsed/60:.1f}m] Swarm: {swarm_output[:100]}...")
except:
pass
# Check current cycles
cycles = get_cycles(workspace)
if cycles < best_cycles:
best_cycles = cycles
print(f"[{elapsed/60:.1f}m] NEW BEST: {cycles} cycles ({BASELINE/cycles:.2f}x)")
elif cycles != last_cycles:
print(f"[{elapsed/60:.1f}m] Cycles: {cycles}")
last_cycles = cycles
time.sleep(check_interval)
# Final check
cycles = get_cycles(workspace)
if cycles < best_cycles:
best_cycles = cycles
elapsed = time.time() - start_time
print(f"\nFinal: {best_cycles} cycles in {elapsed/60:.1f}m ({BASELINE/best_cycles:.2f}x)")
return {
"approach": "swarm",
"cycles": best_cycles,
"time_seconds": elapsed,
"workspace": workspace
}
except Exception as e:
print(f"Error: {e}")
return {
"approach": "swarm",
"cycles": BASELINE,
"time_seconds": time.time() - start_time,
"error": str(e)
}
finally:
if session_id:
destroy_session(session_id)
def print_results(results: dict):
"""Print comparison table."""
print("\n" + "=" * 60)
print("RESULTS")
print("=" * 60)
print(f"{'Approach':<15} {'Cycles':<12} {'Time':<10} {'Speedup':<10}")
print("-" * 60)
for name, data in results.items():
cycles = data['cycles']
time_m = data['time_seconds'] / 60
speedup = BASELINE / cycles
print(f"{name:<15} {cycles:<12} {time_m:<10.1f}m {speedup:<10.2f}x")
if len(results) > 1:
winner = min(results.items(), key=lambda x: x[1]['cycles'])
print(f"\nWinner: {winner[0]} ({winner[1]['cycles']} cycles)")
def main():
if len(sys.argv) < 2:
print(__doc__)
sys.exit(1)
mode = sys.argv[1].lower()
os.makedirs(BENCHMARK_DIR, exist_ok=True)
print(f"Benchmark timeout: {TIMEOUT_MINUTES} minutes per approach")
print(f"Set BENCHMARK_TIMEOUT env var to change (e.g., BENCHMARK_TIMEOUT=30)")
if mode == "single":
r = run_single_agent()
print_results({"single": r})
elif mode == "swarm":
r = run_swarm()
print_results({"swarm": r})
elif mode == "both":
results = {}
results['single'] = run_single_agent()
results['swarm'] = run_swarm()
print_results(results)
else:
print(f"Unknown mode: {mode}")
print(__doc__)
sys.exit(1)
if __name__ == "__main__":
main()
+213
View File
@@ -0,0 +1,213 @@
#!/bin/bash
# Tool call benchmarking script
# Measures execution time for each tool with representative inputs
# Run from the jcode repo root
set -euo pipefail
ITERATIONS=${1:-5}
RESULTS_FILE="/tmp/jcode_tool_benchmark_$(date +%Y%m%d_%H%M%S).csv"
echo "=== jcode Tool Call Benchmark ==="
echo "Iterations per tool: $ITERATIONS"
echo "Results file: $RESULTS_FILE"
echo ""
# CSV header
echo "tool,iteration,time_ms,input_size_bytes,output_size_bytes" > "$RESULTS_FILE"
# Helper: benchmark a tool via the debug socket
benchmark_tool() {
local tool_name="$1"
local tool_input="$2"
local label="${3:-$tool_name}"
local input_size=${#tool_input}
local total_ms=0
local min_ms=999999
local max_ms=0
local times=()
for i in $(seq 1 "$ITERATIONS"); do
local start_ns=$(date +%s%N)
# Execute via debug socket
local output
output=$(echo "{\"type\":\"debug_command\",\"id\":1,\"command\":\"tool:$tool_name $tool_input\",\"session_id\":\"$SESSION_ID\"}" | \
socat - UNIX-CONNECT:"$DEBUG_SOCK" 2>/dev/null || echo '{"error":"timeout"}')
local end_ns=$(date +%s%N)
local elapsed_ms=$(( (end_ns - start_ns) / 1000000 ))
local output_size=${#output}
echo "$label,$i,$elapsed_ms,$input_size,$output_size" >> "$RESULTS_FILE"
times+=("$elapsed_ms")
total_ms=$((total_ms + elapsed_ms))
if [ "$elapsed_ms" -lt "$min_ms" ]; then min_ms=$elapsed_ms; fi
if [ "$elapsed_ms" -gt "$max_ms" ]; then max_ms=$elapsed_ms; fi
done
local avg_ms=$((total_ms / ITERATIONS))
# Compute p50
IFS=$'\n' sorted_times=($(sort -n <<<"${times[*]}")); unset IFS
local p50_idx=$(( ITERATIONS / 2 ))
local p50_ms=${sorted_times[$p50_idx]}
printf " %-30s avg=%4dms p50=%4dms min=%4dms max=%4dms\n" \
"$label" "$avg_ms" "$p50_ms" "$min_ms" "$max_ms"
}
# Find debug socket
DEBUG_SOCK="${JCODE_DEBUG_SOCK:-/run/user/$(id -u)/jcode-debug.sock}"
if [ ! -S "$DEBUG_SOCK" ]; then
echo "ERROR: Debug socket not found at $DEBUG_SOCK"
echo "Make sure jcode is running with debug control enabled."
exit 1
fi
# Get session ID
echo "Getting session ID..."
SESSION_RESP=$(echo '{"type":"debug_command","id":1,"command":"state"}' | \
socat -t5 - UNIX-CONNECT:"$DEBUG_SOCK" 2>/dev/null || echo '{}')
SESSION_ID=$(echo "$SESSION_RESP" | python3 -c "
import sys, json
try:
d = json.loads(sys.stdin.read())
out = d.get('output', '{}')
if isinstance(out, str):
d2 = json.loads(out)
else:
d2 = out
print(d2.get('session_id', ''))
except:
print('')
" 2>/dev/null)
if [ -z "$SESSION_ID" ]; then
echo "ERROR: Could not get session ID from debug socket"
echo "Response: $SESSION_RESP"
exit 1
fi
echo "Session: $SESSION_ID"
echo ""
# Create temp files for testing
TMPDIR=$(mktemp -d)
echo "hello world" > "$TMPDIR/test.txt"
echo -e "line 1\nline 2\nline 3\nfoo bar\nbaz qux" > "$TMPDIR/multiline.txt"
mkdir -p "$TMPDIR/subdir"
echo "nested" > "$TMPDIR/subdir/nested.txt"
# Large file for read benchmarks
python3 -c "
for i in range(1000):
print(f'Line {i}: This is a test line with some content for benchmarking purposes. The quick brown fox jumps over the lazy dog.')
" > "$TMPDIR/large.txt"
echo "=== File System Tools ==="
benchmark_tool "read" "{\"file_path\":\"$TMPDIR/test.txt\"}" "read (tiny file)"
benchmark_tool "read" "{\"file_path\":\"$TMPDIR/large.txt\"}" "read (1000 lines)"
benchmark_tool "read" "{\"file_path\":\"$TMPDIR/large.txt\",\"offset\":500,\"limit\":10}" "read (10 lines @ offset)"
benchmark_tool "read" "{\"file_path\":\"src/main.rs\"}" "read (main.rs)"
echo ""
echo "=== Write/Edit Tools ==="
benchmark_tool "write" "{\"file_path\":\"$TMPDIR/write_test.txt\",\"content\":\"hello world\"}" "write (small)"
benchmark_tool "write" "{\"file_path\":\"$TMPDIR/write_test.txt\",\"content\":\"$(python3 -c "print('x' * 10000)")\"}" "write (10KB)"
# Setup file for edit
echo "The quick brown fox jumps over the lazy dog" > "$TMPDIR/edit_test.txt"
benchmark_tool "edit" "{\"file_path\":\"$TMPDIR/edit_test.txt\",\"old_string\":\"quick brown\",\"new_string\":\"slow red\"}" "edit (simple replace)"
# Reset for next iteration
for i in $(seq 1 "$ITERATIONS"); do
echo "The quick brown fox jumps over the lazy dog" > "$TMPDIR/edit_test.txt"
done
echo ""
echo "=== Search/Navigation Tools ==="
benchmark_tool "agentgrep" "{\"mode\":\"grep\",\"query\":\"fn main\",\"path\":\"src\",\"type\":\"rs\"}" "agentgrep (fn main in src/)"
benchmark_tool "agentgrep" "{\"mode\":\"grep\",\"query\":\"async fn\",\"path\":\"src/tool\",\"type\":\"rs\"}" "agentgrep (async fn in tools)"
benchmark_tool "agentgrep" "{\"mode\":\"grep\",\"query\":\"tokio::spawn\",\"path\":\"src\"}" "agentgrep (tokio::spawn)"
benchmark_tool "agentgrep" "{\"mode\":\"find\",\"glob\":\"**/*.rs\"}" "agentgrep find (**/*.rs)"
benchmark_tool "agentgrep" "{\"mode\":\"find\",\"glob\":\"**/*.rs\",\"path\":\"src/tool\"}" "agentgrep find (tool/*.rs)"
benchmark_tool "ls" "{}" "ls (repo root)"
benchmark_tool "ls" "{\"path\":\"src\"}" "ls (src/)"
benchmark_tool "ls" "{\"path\":\"src/tool\"}" "ls (src/tool/)"
echo ""
echo "=== Shell Tools ==="
benchmark_tool "bash" "{\"command\":\"echo hello\"}" "bash (echo)"
benchmark_tool "bash" "{\"command\":\"true\"}" "bash (true)"
benchmark_tool "bash" "{\"command\":\"ls -la src/tool/\"}" "bash (ls -la)"
benchmark_tool "bash" "{\"command\":\"wc -l src/main.rs\"}" "bash (wc -l)"
benchmark_tool "bash" "{\"command\":\"cat /dev/null\"}" "bash (cat /dev/null)"
benchmark_tool "bash" "{\"command\":\"git log --oneline -5\"}" "bash (git log -5)"
benchmark_tool "bash" "{\"command\":\"cargo --version\"}" "bash (cargo --version)"
echo ""
echo "=== Memory/Search Tools ==="
benchmark_tool "todoread" "{}" "todoread"
benchmark_tool "todowrite" "{\"todos\":[{\"id\":\"bench1\",\"content\":\"benchmark test\",\"status\":\"pending\",\"priority\":\"low\"}]}" "todowrite"
benchmark_tool "conversation_search" "{\"stats\":true}" "conversation_search (stats)"
benchmark_tool "memory" "{\"action\":\"recall\",\"query\":\"benchmark test\",\"limit\":3}" "memory (recall)"
benchmark_tool "memory" "{\"action\":\"list\",\"limit\":5}" "memory (list)"
echo ""
echo "=== Tool Dispatch Overhead ==="
benchmark_tool "invalid" "{\"tool\":\"test\",\"error\":\"benchmark\"}" "invalid (no-op)"
echo ""
echo "=== Results Summary ==="
echo ""
# Parse and summarize
python3 << 'PYEOF'
import csv
from collections import defaultdict
results = defaultdict(list)
with open("RESULTS_FILE_PLACEHOLDER") as f:
reader = csv.DictReader(f)
for row in reader:
results[row['tool']].append(int(row['time_ms']))
# Sort by average time (descending)
summary = []
for tool, times in results.items():
avg = sum(times) / len(times)
p50 = sorted(times)[len(times) // 2]
summary.append((tool, avg, p50, min(times), max(times)))
summary.sort(key=lambda x: x[1], reverse=True)
print(f"{'Tool':<35} {'Avg':>7} {'P50':>7} {'Min':>7} {'Max':>7}")
print("-" * 70)
for tool, avg, p50, mn, mx in summary:
bar = "█" * max(1, int(avg / 10))
print(f"{tool:<35} {avg:6.0f}ms {p50:5d}ms {mn:5d}ms {mx:5d}ms {bar}")
total_avg = sum(s[1] for s in summary)
print(f"\n{'Total (all tools avg sum)':<35} {total_avg:6.0f}ms")
print(f"\nSlowest tool: {summary[0][0]} ({summary[0][1]:.0f}ms avg)")
print(f"Fastest tool: {summary[-1][0]} ({summary[-1][1]:.0f}ms avg)")
PYEOF
# Cleanup
rm -rf "$TMPDIR"
echo ""
echo "Full CSV results: $RESULTS_FILE"
+199
View File
@@ -0,0 +1,199 @@
#!/usr/bin/env bash
set -euo pipefail
# Build a Linux x86_64 release artifact against the CentOS 7 / manylinux2014
# glibc 2.17 baseline so the resulting binary runs on older distributions as
# well as newer Debian/Ubuntu containers used by Terminal-Bench tasks.
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
out_dir="${1:-$repo_root/dist}"
if [[ "$#" -gt 1 ]]; then
echo "Usage: $0 [out-dir]" >&2
exit 1
fi
if [[ "$out_dir" != /* ]]; then
out_dir="$repo_root/$out_dir"
fi
artifact="${JCODE_COMPAT_ARTIFACT:-jcode-linux-x86_64}"
profile="${JCODE_COMPAT_PROFILE:-release}"
image="${JCODE_COMPAT_IMAGE:-quay.io/pypa/manylinux2014_x86_64}"
cache_root="${JCODE_COMPAT_CACHE_DIR:-$HOME/.cache/jcode-linux-compat}"
target="x86_64-unknown-linux-gnu"
mkdir -p "$out_dir" \
"$cache_root/cargo-registry" \
"$cache_root/cargo-git" \
"$cache_root/rustup"
host_uid="$(id -u)"
host_gid="$(id -g)"
# Compute git build metadata on the HOST and hand it to the container via a
# metadata file (read by jcode-build-meta/build.rs through
# JCODE_BUILD_METADATA_FILE). The repo is bind-mounted into the container and
# owned by the host UID while git inside the container runs as root, so any
# in-container `git` call trips git's "dubious ownership" guard
# (CVE-2022-24765) and fails. That previously zeroed out the embedded git hash,
# date, AND changelog, shipping release binaries that report
# "vX.Y.Z (unknown) (unknown)" with an empty /changelog overlay. Computing the
# values here makes the embedded metadata independent of container-git. This
# mirrors scripts/remote_build.sh.
git_hash=""
git_date=""
git_tag=""
git_dirty="0"
changelog_raw=""
if command -v git >/dev/null 2>&1 && git -C "$repo_root" rev-parse --git-dir >/dev/null 2>&1; then
git_hash="$(git -C "$repo_root" rev-parse --short HEAD 2>/dev/null || true)"
git_date="$(git -C "$repo_root" log -1 --format=%ci 2>/dev/null || true)"
git_tag="$(git -C "$repo_root" describe --tags --always 2>/dev/null || true)"
changelog_raw="$(git -C "$repo_root" log -700 --format='%h|%ct|%D|%s' 2>/dev/null || true)"
if [[ -n "$(git -C "$repo_root" status --porcelain 2>/dev/null || true)" ]]; then
git_dirty="1"
fi
else
echo "warning: git metadata unavailable on host; embedded changelog/version may be empty" >&2
fi
metadata_file="$(mktemp)"
trap 'rm -f "$metadata_file"' EXIT
{
printf 'git_hash=%s\n' "$git_hash"
printf 'git_date=%s\n' "$git_date"
printf 'git_tag=%s\n' "$git_tag"
printf 'git_dirty=%s\n' "$git_dirty"
printf 'changelog_raw<<JCODE_CHANGELOG_EOF\n%s\nJCODE_CHANGELOG_EOF\n' "$changelog_raw"
} > "$metadata_file"
echo "Building portable Linux release in Docker image: $image"
echo "Output dir: $out_dir"
echo "Embedding git metadata: hash=${git_hash:-<none>} tag=${git_tag:-<none>} dirty=$git_dirty changelog_lines=$(printf '%s' "$changelog_raw" | grep -c '' || true)"
docker run --rm \
-e CARGO_TERM_COLOR=always \
-e JCODE_RELEASE_BUILD="${JCODE_RELEASE_BUILD:-1}" \
-e JCODE_BUILD_SEMVER="${JCODE_BUILD_SEMVER:-}" \
-e JCODE_BUILD_METADATA_FILE=/jcode-build-meta \
-e JCODE_COMPAT_PROFILE="$profile" \
-e JCODE_COMPAT_TARGET="$target" \
-e HOST_UID="$host_uid" \
-e HOST_GID="$host_gid" \
-v "$repo_root:/work" \
-v "$metadata_file:/jcode-build-meta:ro" \
-v "$out_dir:/out" \
-v "$cache_root/cargo-registry:/root/.cargo/registry" \
-v "$cache_root/cargo-git:/root/.cargo/git" \
-v "$cache_root/rustup:/root/.rustup" \
-w /work \
"$image" \
bash -lc '
set -euo pipefail
if command -v apt-get >/dev/null 2>&1; then
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq \
build-essential \
ca-certificates \
curl \
git \
libssl-dev \
pkg-config
elif command -v yum >/dev/null 2>&1; then
yum install -y \
ca-certificates \
curl \
gcc \
gcc-c++ \
git \
make \
openssl-devel \
pkgconfig \
tar \
gzip
update-ca-trust || true
else
echo "Unsupported build image: expected apt-get or yum" >&2
exit 1
fi
if [[ ! -x /root/.cargo/bin/cargo ]]; then
curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal --default-toolchain stable
fi
source /root/.cargo/env
# Belt-and-suspenders: the host-computed metadata file
# (JCODE_BUILD_METADATA_FILE=/jcode-build-meta) is the primary source of
# git hash/date/changelog, but mark the bind-mounted repo as a safe
# directory so any in-container git fallback still works despite the
# host-UID/root-git ownership mismatch (CVE-2022-24765 guard).
git config --global --add safe.directory /work 2>/dev/null || true
export CARGO_TARGET_DIR=/work/target/linux-compat
export CARGO_BUILD_JOBS="${CARGO_BUILD_JOBS:-1}"
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS="${CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS:--C link-arg=-static-libgcc}"
cargo build --profile "$JCODE_COMPAT_PROFILE" --target "$JCODE_COMPAT_TARGET" -p jcode --bin jcode
cp "$CARGO_TARGET_DIR/$JCODE_COMPAT_TARGET/$JCODE_COMPAT_PROFILE/jcode" "/out/'"$artifact"'.bin"
chmod +x "/out/'"$artifact"'.bin"
cat > "/out/'"$artifact"'" <<WRAPPER
#!/usr/bin/env sh
set -eu
self_path=\$0
if command -v readlink >/dev/null 2>&1; then
resolved=\$(readlink -f -- "\$0" 2>/dev/null || true)
if [ -n "\$resolved" ]; then
self_path=\$resolved
fi
fi
case "\$self_path" in
*/*) self_dir=\$(CDPATH= cd -- "\$(dirname -- "\$self_path")" && pwd) ;;
*) self_dir=\$(pwd) ;;
esac
if [ -n "\${LD_LIBRARY_PATH:-}" ]; then
export LD_LIBRARY_PATH="\$self_dir:\$LD_LIBRARY_PATH"
else
export LD_LIBRARY_PATH="\$self_dir"
fi
exec "\$self_dir/'"$artifact"'.bin" "\$@"
WRAPPER
chmod +x "/out/'"$artifact"'"
# Preserve the OpenSSL runtime libraries used by the build image. Some
# Terminal-Bench containers are older than the build host and either lack
# libssl entirely or expose a different SONAME. The Harbor adapter uploads
# these sibling libraries and sets LD_LIBRARY_PATH for the jcode process.
ldd "/out/'"$artifact"'.bin" \
| awk "/lib(ssl|crypto)[.]so/ { print \$3 }" \
| while read -r lib; do
if [[ -n "$lib" && -f "$lib" ]]; then
cp -L "$lib" /out/
fi
done
extra_libs=()
for pattern in libssl.so\* libcrypto.so\*; do
for lib in $pattern; do
[[ -e "$lib" ]] && extra_libs+=("$lib")
done
done
if (( ${#extra_libs[@]} > 0 )); then
(cd /out && tar czf '"$artifact"'.tar.gz '"$artifact"' '"$artifact"'.bin "${extra_libs[@]}")
else
(cd /out && tar czf '"$artifact"'.tar.gz '"$artifact"' '"$artifact"'.bin)
fi
chown_inputs=("/out/'"$artifact"'" "/out/'"$artifact"'.bin" "/out/'"$artifact"'.tar.gz")
if (( ${#extra_libs[@]} > 0 )); then
for lib in "${extra_libs[@]}"; do
chown_inputs+=("/out/$lib")
done
fi
chown "$HOST_UID:$HOST_GID" "${chown_inputs[@]}" 2>/dev/null || true
'
echo "Built artifacts:"
ls -lh "$out_dir/$artifact" "$out_dir/$artifact.tar.gz"
+81
View File
@@ -0,0 +1,81 @@
#!/bin/bash
# Full autonomous demo capture for jcode
# Captures screenshots of various UI states using niri + wtype
#
# Usage: ./capture_demo.sh [window_id]
# If window_id not provided, uses the focused window
set -e
SCRIPT_DIR="$(dirname "$0")"
OUTPUT_DIR="$SCRIPT_DIR/../docs/screenshots"
WINDOW_ID="${1:-}"
mkdir -p "$OUTPUT_DIR"
# Get window ID if not provided
if [ -z "$WINDOW_ID" ]; then
WINDOW_ID=$(niri msg focused-window 2>&1 | head -1 | grep -oP 'Window ID \K\d+')
echo "Using focused window: $WINDOW_ID"
fi
capture() {
local name="$1"
local keys="$2"
local delay="${3:-0.5}"
echo "📸 Capturing: $name"
# Focus the window
niri msg action focus-window --id "$WINDOW_ID"
sleep 0.2
# Inject keystrokes if provided
if [ -n "$keys" ]; then
echo " Typing: $keys"
wtype "$keys"
sleep "$delay"
fi
# Screenshot
niri msg action screenshot-window --path "$OUTPUT_DIR/${name}.png"
echo " ✅ Saved: $OUTPUT_DIR/${name}.png"
}
clear_input() {
# Clear any existing input with Ctrl+U, then Escape to close popups
wtype -M ctrl -k u
sleep 0.1
wtype -k Escape
sleep 0.2
}
echo "🎬 jcode Demo Capture"
echo " Window ID: $WINDOW_ID"
echo " Output: $OUTPUT_DIR"
echo ""
# Capture sequence
niri msg action focus-window --id "$WINDOW_ID"
sleep 0.3
# 1. Main UI (clean state)
clear_input
capture "main-ui" "" 0.3
# 2. Command palette (type /)
clear_input
capture "command-palette" "/" 0.3
# 3. Close palette, show help
wtype -k Escape
sleep 0.2
capture "help-view" "/help" 0.5
# Clean up - close any popups
wtype -k Escape
sleep 0.2
echo ""
echo "🎉 Done! Screenshots saved to $OUTPUT_DIR/"
ls -la "$OUTPUT_DIR/"*.png 2>/dev/null || echo "No screenshots found"
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
# Capture jcode screenshots with your actual terminal theme
# Usage: ./capture_screenshot.sh [output_name]
set -e
OUTPUT_DIR="$(dirname "$0")/../docs/screenshots"
OUTPUT_NAME="${1:-jcode-screenshot}"
OUTPUT_PATH="$OUTPUT_DIR/${OUTPUT_NAME}.png"
mkdir -p "$OUTPUT_DIR"
echo "📸 jcode Screenshot Capture"
echo ""
echo "Instructions:"
echo " 1. Make sure jcode is running in a visible terminal"
echo " 2. Set up the UI state you want to capture"
echo " 3. Press Enter here, then click on the jcode window"
echo ""
read -p "Press Enter when ready..."
# Use slurp to let user select a window/region, then capture with grim
GEOMETRY=$(slurp)
if [ -n "$GEOMETRY" ]; then
grim -g "$GEOMETRY" "$OUTPUT_PATH"
echo "✅ Saved to: $OUTPUT_PATH"
# Show the image dimensions
if command -v file &>/dev/null; then
file "$OUTPUT_PATH"
fi
else
echo "❌ No region selected"
exit 1
fi
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
# Thin wrapper used by the test/refactor helper scripts (test_fast.sh,
# test_e2e.sh, refactor_phase1_verify.sh, agent_trace.sh, real_provider_smoke.sh).
#
# Historically this exec'd bare `cargo "$@"`, which meant the test/check
# workflows ran with NONE of the adaptive build controls the selfdev path gets
# via dev_cargo.sh: no memory-aware job sizing, no fast linker (clang+lld/mold),
# and no remote-cargo offload beyond a manual env check. On a memory-constrained
# host that let a `cargo test`/`cargo check` here assume the full core count and
# trip earlyoom/OOM while a concurrent selfdev build was running -- the two
# wrappers fragmenting the build instead of cooperating.
#
# Delegating to dev_cargo.sh unifies both paths: test/check builds now get the
# same MemAvailable-based CARGO_BUILD_JOBS throttle, the same fast linker, and
# the same remote-cargo handling (dev_cargo.sh performs its own JCODE_REMOTE_CARGO
# preflight, so we no longer duplicate it here). dev_cargo.sh passes every arg
# straight through to cargo, only layering env/setup, so `test`/`check`/`build`
# semantics are unchanged. The low-memory *selfdev profile* overrides remain
# gated to --profile selfdev inside dev_cargo.sh, so plain `cargo test` (test
# profile, debug-assertions on) is untouched aside from the job/linker tuning.
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
exec "$repo_root/scripts/dev_cargo.sh" "$@"
+175
View File
@@ -0,0 +1,175 @@
#!/usr/bin/env python3
"""Enforce a ratcheting Rust file-size budget.
This script keeps the current oversized-file debt from getting worse while the
larger refactor program is underway.
Policy:
- Production Rust files above the configured LOC threshold are tracked in a
baseline file.
- Existing tracked oversized files may not grow.
- New oversized production files may not be introduced.
- If oversized files shrink or disappear, the script reports the improvement.
- `--update` refreshes the baseline after intentional cleanup.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Any
REPO_ROOT = Path(__file__).resolve().parent.parent
BASELINE_FILE = REPO_ROOT / "scripts" / "code_size_budget.json"
DEFAULT_THRESHOLD = 1200
SCAN_ROOTS = (REPO_ROOT / "src", REPO_ROOT / "crates")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--update",
action="store_true",
help="refresh the baseline to the current oversized-file set",
)
return parser.parse_args()
def is_production_rust_file(path: Path) -> bool:
rel = path.relative_to(REPO_ROOT).as_posix()
if path.suffix != ".rs":
return False
parts = rel.split("/")
if parts[0] == "tests" or any(
part == "tests" or part.endswith("_tests") or part.endswith("_test") or part.startswith("tests_")
for part in parts
):
return False
name = path.name
if (
name == "tests.rs"
or name.endswith("_tests.rs")
or name.endswith("_test.rs")
or name.startswith("tests_")
):
return False
return True
def rust_file_line_count(path: Path) -> int:
with path.open("r", encoding="utf-8") as handle:
return sum(1 for _ in handle)
def current_oversized_files(threshold: int) -> dict[str, int]:
files: dict[str, int] = {}
for root in SCAN_ROOTS:
if not root.exists():
continue
for path in sorted(root.rglob("*.rs")):
if not is_production_rust_file(path):
continue
line_count = rust_file_line_count(path)
if line_count > threshold:
files[path.relative_to(REPO_ROOT).as_posix()] = line_count
return files
def load_baseline() -> dict[str, Any]:
if not BASELINE_FILE.exists():
raise SystemExit(f"error: missing baseline file: {BASELINE_FILE}")
data = json.loads(BASELINE_FILE.read_text(encoding="utf-8"))
if not isinstance(data, dict):
raise SystemExit(f"error: invalid baseline file format: {BASELINE_FILE}")
threshold = data.get("threshold_loc")
tracked = data.get("tracked_files")
if not isinstance(threshold, int) or threshold <= 0:
raise SystemExit(f"error: invalid threshold_loc in {BASELINE_FILE}")
if not isinstance(tracked, dict) or any(
not isinstance(k, str) or not isinstance(v, int) or v <= 0
for k, v in tracked.items()
):
raise SystemExit(f"error: invalid tracked_files in {BASELINE_FILE}")
return data
def write_baseline(threshold: int, tracked_files: dict[str, int]) -> None:
payload = {
"version": 1,
"threshold_loc": threshold,
"tracked_files": tracked_files,
}
BASELINE_FILE.write_text(
json.dumps(payload, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
def main() -> int:
args = parse_args()
baseline = load_baseline()
threshold = baseline["threshold_loc"]
current = current_oversized_files(threshold)
if args.update:
write_baseline(threshold, current)
print(
"Updated code-size baseline: "
f"tracked={len(baseline['tracked_files'])} -> {len(current)} oversized files"
)
return 0
tracked: dict[str, int] = baseline["tracked_files"]
regressions: list[str] = []
improvements: list[str] = []
for path, lines in sorted(current.items()):
old_lines = tracked.get(path)
if old_lines is None:
regressions.append(
f"new oversized file exceeds {threshold} LOC: {path} ({lines} LOC)"
)
elif lines > old_lines:
regressions.append(
f"oversized file grew: {path} ({old_lines} -> {lines} LOC)"
)
elif lines < old_lines:
improvements.append(f"oversized file shrank: {path} ({old_lines} -> {lines} LOC)")
for path, old_lines in sorted(tracked.items()):
if path not in current:
improvements.append(
f"oversized file no longer exceeds {threshold} LOC: {path} ({old_lines} -> OK)"
)
if regressions:
print(
"Code-size budget exceeded. Existing oversized Rust files must shrink or stay flat, "
"and new oversized production files are not allowed:",
file=sys.stderr,
)
for entry in regressions:
print(f" - {entry}", file=sys.stderr)
print(
"Run scripts/check_code_size_budget.py --update only after intentional cleanup.",
file=sys.stderr,
)
return 1
if improvements:
print("Code-size budget improved:")
for entry in improvements:
print(f" - {entry}")
print("Consider running: scripts/check_code_size_budget.py --update")
else:
print(
"Code-size budget OK: "
f"tracked={len(tracked)} threshold={threshold}LOC no oversized-file regressions"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+115
View File
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""Check lightweight crate dependency boundaries.
Type crates should remain data-contract crates. This guard intentionally starts
small: it blocks direct dependencies from any `jcode-*-types` crate to root or
runtime-heavy internal crates. It allows external dependencies for now, while
making internal domain leaks visible and easy to extend.
"""
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
# Internal crates that are allowed as dependencies of type crates.
# Keep this list narrow. Add a crate only if it is itself a data-contract crate.
ALLOWED_INTERNAL_TYPE_DEPS = {
"jcode-message-types",
}
# Internal crates that type crates must not depend on directly. Most are runtime,
# provider, UI, storage, or root behavior crates. `jcode-core` is intentionally
# blocked so it does not become the backdoor catch-all dependency for DTO crates.
FORBIDDEN_INTERNAL_DEPS = {
"jcode",
"jcode-agent-runtime",
"jcode-azure-auth",
"jcode-core",
"jcode-desktop",
"jcode-embedding",
"jcode-mobile-core",
"jcode-mobile-sim",
"jcode-notify-email",
"jcode-pdf",
"jcode-plan",
"jcode-provider-core",
"jcode-provider-gemini",
"jcode-provider-metadata",
"jcode-provider-openrouter",
"jcode-protocol",
"jcode-terminal-launch",
"jcode-tui-core",
"jcode-tui-markdown",
"jcode-tui-mermaid",
"jcode-tui-render",
"jcode-tui-workspace",
}
def cargo_metadata() -> dict:
result = subprocess.run(
["cargo", "metadata", "--no-deps", "--format-version", "1"],
cwd=ROOT,
check=True,
text=True,
stdout=subprocess.PIPE,
)
return json.loads(result.stdout)
def is_type_crate(name: str) -> bool:
return name.startswith("jcode-") and name.endswith("-types")
def main() -> int:
metadata = cargo_metadata()
package_by_id = {package["id"]: package for package in metadata["packages"]}
workspace_ids = set(metadata["workspace_members"])
workspace_names = {
package_by_id[package_id]["name"] for package_id in workspace_ids if package_id in package_by_id
}
errors: list[str] = []
warnings: list[str] = []
for package_id in sorted(workspace_ids, key=lambda pid: package_by_id[pid]["name"]):
package = package_by_id[package_id]
name = package["name"]
if not is_type_crate(name):
continue
for dep in package.get("dependencies", []):
dep_name = dep["name"]
if dep_name not in workspace_names:
continue
if dep_name in ALLOWED_INTERNAL_TYPE_DEPS:
continue
if is_type_crate(dep_name):
continue
if dep_name in FORBIDDEN_INTERNAL_DEPS:
errors.append(f"{name} must not depend on runtime/internal crate {dep_name}")
else:
warnings.append(
f"{name} depends on internal non-type crate {dep_name}; review boundary policy"
)
for warning in warnings:
print(f"warning: {warning}", file=sys.stderr)
for error in errors:
print(f"error: {error}", file=sys.stderr)
if errors:
print(f"dependency boundary check failed: {len(errors)} error(s)", file=sys.stderr)
return 1
print("dependency boundary check passed")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+200
View File
@@ -0,0 +1,200 @@
#!/usr/bin/env python3
"""Enforce a ratcheting production panic-prone usage budget.
Counts production Rust occurrences of:
- `.unwrap(`
- `.expect(`
- `panic!`, `todo!`, `unimplemented!`
Policy:
- Existing files may not increase their count.
- New production files may not introduce panic-prone usage.
- Total count may not increase.
- `--update` refreshes the baseline after intentional cleanup.
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
from typing import Any
REPO_ROOT = Path(__file__).resolve().parent.parent
BASELINE_FILE = REPO_ROOT / "scripts" / "panic_budget.json"
SCAN_ROOTS = (REPO_ROOT / "src", REPO_ROOT / "crates")
PATTERN = re.compile(r"\.unwrap\(|\.expect\(|\b(?:panic!|todo!|unimplemented!)")
CFG_TEST_RE = re.compile(r"^\s*#\s*\[\s*cfg\s*\(\s*test\s*\)\s*\]")
ITEM_START_RE = re.compile(r"^\s*(?:pub(?:\([^)]*\))?\s+)?(?:mod|fn)\b")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--update", action="store_true", help="refresh the baseline")
return parser.parse_args()
def is_test_rust_file(path: Path) -> bool:
rel = path.relative_to(REPO_ROOT).as_posix()
if path.suffix != ".rs":
return False
parts = rel.split("/")
if parts[0] == "tests" or any(
part == "tests" or part.endswith("_tests") or part.endswith("_test") or part.startswith("tests_")
for part in parts
):
return True
name = path.name
return (
name == "tests.rs"
or name.endswith("_tests.rs")
or name.endswith("_test.rs")
or name.startswith("tests_")
)
def production_rust_files() -> list[Path]:
files: list[Path] = []
for root in SCAN_ROOTS:
if not root.exists():
continue
for path in sorted(root.rglob("*.rs")):
if path.suffix == ".rs" and not is_test_rust_file(path):
files.append(path)
return files
def brace_delta(line: str) -> int:
"""Approximate Rust block nesting for budget classification.
The panic budget is a ratchet, not a parser. This intentionally simple scan
ignores comments and strings, which is acceptable for excluding normal
`#[cfg(test)] mod tests { ... }` blocks from production counts.
"""
return line.count("{") - line.count("}")
def production_lines(path: Path) -> list[str]:
lines = path.read_text(encoding="utf-8", errors="ignore").splitlines()
output: list[str] = []
skip_stack: list[int] = []
pending_cfg_test = False
for line in lines:
stripped = line.strip()
current_depth = sum(skip_stack)
if current_depth == 0:
if pending_cfg_test and ITEM_START_RE.match(line):
delta = brace_delta(line)
if delta > 0:
skip_stack.append(delta)
pending_cfg_test = False
continue
if pending_cfg_test and stripped and not stripped.startswith("#"):
pending_cfg_test = False
if CFG_TEST_RE.match(line):
pending_cfg_test = True
continue
output.append(line)
else:
skip_stack[-1] += brace_delta(line)
if skip_stack[-1] <= 0:
skip_stack.pop()
return output
def current_counts() -> dict[str, int]:
counts: dict[str, int] = {}
for path in production_rust_files():
count = sum(1 for line in production_lines(path) if PATTERN.search(line))
if count:
counts[path.relative_to(REPO_ROOT).as_posix()] = count
return counts
def load_baseline() -> dict[str, Any]:
if not BASELINE_FILE.exists():
return {"version": 1, "total": 0, "tracked_files": {}}
data = json.loads(BASELINE_FILE.read_text(encoding="utf-8"))
if not isinstance(data, dict):
raise SystemExit(f"error: invalid baseline file format: {BASELINE_FILE}")
total = data.get("total")
tracked = data.get("tracked_files")
if not isinstance(total, int) or total < 0:
raise SystemExit(f"error: invalid total in {BASELINE_FILE}")
if not isinstance(tracked, dict) or any(
not isinstance(k, str) or not isinstance(v, int) or v <= 0 for k, v in tracked.items()
):
raise SystemExit(f"error: invalid tracked_files in {BASELINE_FILE}")
return data
def write_baseline(counts: dict[str, int]) -> None:
BASELINE_FILE.write_text(
json.dumps(
{"version": 1, "total": sum(counts.values()), "tracked_files": counts},
indent=2,
sort_keys=True,
)
+ "\n",
encoding="utf-8",
)
def main() -> int:
args = parse_args()
baseline = load_baseline()
current = current_counts()
current_total = sum(current.values())
if args.update:
write_baseline(current)
print(
"Updated panic-prone baseline: "
f"total={baseline['total']} -> {current_total}, files={len(baseline['tracked_files'])} -> {len(current)}"
)
return 0
tracked: dict[str, int] = baseline["tracked_files"]
regressions: list[str] = []
improvements: list[str] = []
if current_total > baseline["total"]:
regressions.append(f"total panic-prone count grew: {baseline['total']} -> {current_total}")
elif current_total < baseline["total"]:
improvements.append(f"total panic-prone count shrank: {baseline['total']} -> {current_total}")
for path, count in sorted(current.items()):
old_count = tracked.get(path)
if old_count is None:
regressions.append(f"new production panic-prone usage: {path} ({count})")
elif count > old_count:
regressions.append(f"production panic-prone usage grew: {path} ({old_count} -> {count})")
elif count < old_count:
improvements.append(f"production panic-prone usage shrank: {path} ({old_count} -> {count})")
for path, old_count in sorted(tracked.items()):
if path not in current:
improvements.append(f"production panic-prone usage removed: {path} ({old_count} -> 0)")
if regressions:
print("Panic-prone usage budget exceeded:", file=sys.stderr)
for entry in regressions:
print(f" - {entry}", file=sys.stderr)
print("Run scripts/check_panic_budget.py --update only after intentional cleanup.", file=sys.stderr)
return 1
if improvements:
print("Panic-prone usage budget improved:")
for entry in improvements:
print(f" - {entry}")
print("Consider running: scripts/check_panic_budget.py --update")
else:
print(f"Panic-prone budget OK: total={current_total} files={len(current)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+44
View File
@@ -0,0 +1,44 @@
param(
[string[]]$Paths = @("scripts")
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$scriptFiles = @()
foreach ($path in $Paths) {
if (-not (Test-Path -LiteralPath $path)) {
continue
}
$scriptFiles += Get-ChildItem -LiteralPath $path -Recurse -File -Filter '*.ps1'
}
if (-not $scriptFiles -or $scriptFiles.Count -eq 0) {
Write-Host 'No PowerShell scripts found.' -ForegroundColor Yellow
exit 0
}
$hadErrors = $false
foreach ($file in $scriptFiles | Sort-Object FullName -Unique) {
$tokens = $null
$errors = $null
[System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$errors) | Out-Null
if ($errors -and $errors.Count -gt 0) {
$hadErrors = $true
Write-Host "Parse errors in $($file.FullName):" -ForegroundColor Red
foreach ($error in $errors) {
$line = $error.Extent.StartLineNumber
$column = $error.Extent.StartColumnNumber
Write-Host " Line ${line}, Col ${column}: $($error.Message)" -ForegroundColor Red
}
} else {
Write-Host "OK: $($file.FullName)" -ForegroundColor Green
}
}
if ($hadErrors) {
exit 1
}
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
binary=${1:-"$repo_root/target/release/jcode"}
if [[ ! -x "$binary" ]]; then
echo "Binary not found or not executable: $binary" >&2
echo "Build it first with: cargo build --release" >&2
exit 1
fi
exec python3 "$repo_root/scripts/bench_startup.py" "$binary" --check --runs 3
+252
View File
@@ -0,0 +1,252 @@
#!/usr/bin/env python3
"""Enforce a ratcheting budget for swallowed-error-like Rust patterns.
This is intentionally a broad guardrail. It tracks production occurrences of
patterns that commonly hide failures and should either be removed, logged,
propagated, or explicitly accepted as best-effort:
- `let _ = ...`
- `.ok()`
- `.unwrap_or_default()`
Policy:
- Existing files may not increase their count.
- New production files may not introduce these patterns.
- Total count may not increase.
- `--update` refreshes the baseline after intentional cleanup.
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
from typing import Any
REPO_ROOT = Path(__file__).resolve().parent.parent
BASELINE_FILE = REPO_ROOT / "scripts" / "swallowed_error_budget.json"
SCAN_ROOTS = (REPO_ROOT / "src", REPO_ROOT / "crates")
PATTERNS = {
"let_underscore": re.compile(r"\blet\s+_\s*="),
"dot_ok": re.compile(r"\.ok\(\)"),
"unwrap_or_default": re.compile(r"\.unwrap_or_default\(\)"),
}
CFG_TEST_RE = re.compile(r"^\s*#\s*\[\s*cfg\s*\(\s*test\s*\)\s*\]")
ITEM_START_RE = re.compile(r"^\s*(?:pub(?:\([^)]*\))?\s+)?(?:mod|fn)\b")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--update", action="store_true", help="refresh the baseline")
return parser.parse_args()
def is_test_rust_file(path: Path) -> bool:
rel = path.relative_to(REPO_ROOT).as_posix()
if path.suffix != ".rs":
return False
parts = rel.split("/")
if parts[0] == "tests" or any(
part == "tests" or part.endswith("_tests") or part.endswith("_test") or part.startswith("tests_")
for part in parts
):
return True
name = path.name
return (
name == "tests.rs"
or name.endswith("_tests.rs")
or name.endswith("_test.rs")
or name.startswith("tests_")
)
def production_rust_files() -> list[Path]:
files: list[Path] = []
for root in SCAN_ROOTS:
if not root.exists():
continue
for path in sorted(root.rglob("*.rs")):
if path.suffix == ".rs" and not is_test_rust_file(path):
files.append(path)
return files
def brace_delta(line: str) -> int:
return line.count("{") - line.count("}")
def production_lines(path: Path) -> list[str]:
lines = path.read_text(encoding="utf-8", errors="ignore").splitlines()
output: list[str] = []
skip_stack: list[int] = []
pending_cfg_test = False
for line in lines:
stripped = line.strip()
current_depth = sum(skip_stack)
if current_depth == 0:
if pending_cfg_test and ITEM_START_RE.match(line):
delta = brace_delta(line)
if delta > 0:
skip_stack.append(delta)
pending_cfg_test = False
continue
if pending_cfg_test and stripped and not stripped.startswith("#"):
pending_cfg_test = False
if CFG_TEST_RE.match(line):
pending_cfg_test = True
continue
output.append(line)
else:
skip_stack[-1] += brace_delta(line)
if skip_stack[-1] <= 0:
skip_stack.pop()
return output
def zero_counts() -> dict[str, int]:
return {name: 0 for name in PATTERNS}
def current_counts() -> dict[str, dict[str, int]]:
counts: dict[str, dict[str, int]] = {}
for path in production_rust_files():
file_counts = zero_counts()
for line in production_lines(path):
for name, pattern in PATTERNS.items():
if pattern.search(line):
file_counts[name] += 1
if sum(file_counts.values()) > 0:
counts[path.relative_to(REPO_ROOT).as_posix()] = file_counts
return counts
def file_total(counts: dict[str, int]) -> int:
return sum(counts.values())
def total_counts(counts: dict[str, dict[str, int]]) -> dict[str, int]:
totals = zero_counts()
for file_counts in counts.values():
for name, count in file_counts.items():
totals[name] = totals.get(name, 0) + count
return totals
def grand_total(counts: dict[str, dict[str, int]]) -> int:
return sum(file_total(file_counts) for file_counts in counts.values())
def load_baseline() -> dict[str, Any]:
if not BASELINE_FILE.exists():
return {"version": 1, "total": 0, "totals_by_pattern": zero_counts(), "tracked_files": {}}
data = json.loads(BASELINE_FILE.read_text(encoding="utf-8"))
if not isinstance(data, dict):
raise SystemExit(f"error: invalid baseline file format: {BASELINE_FILE}")
tracked = data.get("tracked_files")
totals_by_pattern = data.get("totals_by_pattern")
total = data.get("total")
if not isinstance(total, int) or total < 0:
raise SystemExit(f"error: invalid total in {BASELINE_FILE}")
if not isinstance(totals_by_pattern, dict):
raise SystemExit(f"error: invalid totals_by_pattern in {BASELINE_FILE}")
if not isinstance(tracked, dict):
raise SystemExit(f"error: invalid tracked_files in {BASELINE_FILE}")
for path, file_counts in tracked.items():
if not isinstance(path, str) or not isinstance(file_counts, dict):
raise SystemExit(f"error: invalid tracked_files entry in {BASELINE_FILE}")
if any(not isinstance(v, int) or v < 0 for v in file_counts.values()):
raise SystemExit(f"error: invalid count in tracked_files entry for {path}")
return data
def write_baseline(counts: dict[str, dict[str, int]]) -> None:
BASELINE_FILE.write_text(
json.dumps(
{
"version": 1,
"total": grand_total(counts),
"totals_by_pattern": total_counts(counts),
"tracked_files": counts,
},
indent=2,
sort_keys=True,
)
+ "\n",
encoding="utf-8",
)
def main() -> int:
args = parse_args()
baseline = load_baseline()
current = current_counts()
current_total = grand_total(current)
current_pattern_totals = total_counts(current)
if args.update:
write_baseline(current)
print(
"Updated swallowed-error baseline: "
f"total={baseline['total']} -> {current_total}, "
f"files={len(baseline['tracked_files'])} -> {len(current)}"
)
return 0
tracked: dict[str, dict[str, int]] = baseline["tracked_files"]
regressions: list[str] = []
improvements: list[str] = []
if current_total > baseline["total"]:
regressions.append(f"total swallowed-error-like count grew: {baseline['total']} -> {current_total}")
elif current_total < baseline["total"]:
improvements.append(f"total swallowed-error-like count shrank: {baseline['total']} -> {current_total}")
baseline_pattern_totals: dict[str, int] = baseline["totals_by_pattern"]
for name, count in sorted(current_pattern_totals.items()):
old_count = baseline_pattern_totals.get(name, 0)
if count > old_count:
regressions.append(f"{name} count grew: {old_count} -> {count}")
elif count < old_count:
improvements.append(f"{name} count shrank: {old_count} -> {count}")
for path, file_counts in sorted(current.items()):
old_counts = tracked.get(path)
if old_counts is None:
regressions.append(f"new swallowed-error-like usage: {path} ({file_total(file_counts)})")
continue
old_total = file_total(old_counts)
new_total = file_total(file_counts)
if new_total > old_total:
regressions.append(f"swallowed-error-like usage grew: {path} ({old_total} -> {new_total})")
elif new_total < old_total:
improvements.append(f"swallowed-error-like usage shrank: {path} ({old_total} -> {new_total})")
for path, old_counts in sorted(tracked.items()):
if path not in current:
improvements.append(f"swallowed-error-like usage removed: {path} ({file_total(old_counts)} -> 0)")
if regressions:
print("Swallowed-error budget exceeded:", file=sys.stderr)
for entry in regressions:
print(f" - {entry}", file=sys.stderr)
print("Run scripts/check_swallowed_error_budget.py --update only after intentional cleanup.", file=sys.stderr)
return 1
if improvements:
print("Swallowed-error budget improved:")
for entry in improvements:
print(f" - {entry}")
print("Consider running: scripts/check_swallowed_error_budget.py --update")
else:
print(
"Swallowed-error budget OK: "
f"total={current_total} files={len(current)} patterns={current_pattern_totals}"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""Enforce a ratcheting Rust test-file size budget.
Policy:
- Test Rust files above the configured LOC threshold are tracked in a baseline.
- Existing tracked oversized test files may not grow.
- New oversized test files may not be introduced.
- `--update` refreshes the baseline after intentional cleanup.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Any
REPO_ROOT = Path(__file__).resolve().parent.parent
BASELINE_FILE = REPO_ROOT / "scripts" / "test_size_budget.json"
DEFAULT_THRESHOLD = 1200
SCAN_ROOTS = (REPO_ROOT / "src", REPO_ROOT / "crates", REPO_ROOT / "tests")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--update", action="store_true", help="refresh the baseline")
return parser.parse_args()
def is_test_rust_file(path: Path) -> bool:
rel = path.relative_to(REPO_ROOT).as_posix()
if path.suffix != ".rs":
return False
parts = rel.split("/")
if parts[0] == "tests" or any(
part == "tests" or part.endswith("_tests") or part.endswith("_test") or part.startswith("tests_")
for part in parts
):
return True
name = path.name
return (
name == "tests.rs"
or name.endswith("_tests.rs")
or name.endswith("_test.rs")
or name.startswith("tests_")
)
def rust_file_line_count(path: Path) -> int:
with path.open("r", encoding="utf-8") as handle:
return sum(1 for _ in handle)
def current_oversized_files(threshold: int) -> dict[str, int]:
files: dict[str, int] = {}
for root in SCAN_ROOTS:
if not root.exists():
continue
for path in sorted(root.rglob("*.rs")):
if not is_test_rust_file(path):
continue
line_count = rust_file_line_count(path)
if line_count > threshold:
files[path.relative_to(REPO_ROOT).as_posix()] = line_count
return files
def load_baseline() -> dict[str, Any]:
if not BASELINE_FILE.exists():
return {"version": 1, "threshold_loc": DEFAULT_THRESHOLD, "tracked_files": {}}
data = json.loads(BASELINE_FILE.read_text(encoding="utf-8"))
if not isinstance(data, dict):
raise SystemExit(f"error: invalid baseline file format: {BASELINE_FILE}")
threshold = data.get("threshold_loc")
tracked = data.get("tracked_files")
if not isinstance(threshold, int) or threshold <= 0:
raise SystemExit(f"error: invalid threshold_loc in {BASELINE_FILE}")
if not isinstance(tracked, dict) or any(
not isinstance(k, str) or not isinstance(v, int) or v <= 0 for k, v in tracked.items()
):
raise SystemExit(f"error: invalid tracked_files in {BASELINE_FILE}")
return data
def write_baseline(threshold: int, tracked_files: dict[str, int]) -> None:
BASELINE_FILE.write_text(
json.dumps(
{"version": 1, "threshold_loc": threshold, "tracked_files": tracked_files},
indent=2,
sort_keys=True,
)
+ "\n",
encoding="utf-8",
)
def main() -> int:
args = parse_args()
baseline = load_baseline()
threshold = baseline["threshold_loc"]
current = current_oversized_files(threshold)
if args.update:
write_baseline(threshold, current)
print(
"Updated test-size baseline: "
f"tracked={len(baseline['tracked_files'])} -> {len(current)} oversized test files"
)
return 0
tracked: dict[str, int] = baseline["tracked_files"]
regressions: list[str] = []
improvements: list[str] = []
for path, lines in sorted(current.items()):
old_lines = tracked.get(path)
if old_lines is None:
regressions.append(f"new oversized test file exceeds {threshold} LOC: {path} ({lines} LOC)")
elif lines > old_lines:
regressions.append(f"oversized test file grew: {path} ({old_lines} -> {lines} LOC)")
elif lines < old_lines:
improvements.append(f"oversized test file shrank: {path} ({old_lines} -> {lines} LOC)")
for path, old_lines in sorted(tracked.items()):
if path not in current:
improvements.append(
f"oversized test file no longer exceeds {threshold} LOC: {path} ({old_lines} -> OK)"
)
if regressions:
print("Test-size budget exceeded:", file=sys.stderr)
for entry in regressions:
print(f" - {entry}", file=sys.stderr)
print("Run scripts/check_test_size_budget.py --update only after intentional cleanup.", file=sys.stderr)
return 1
if improvements:
print("Test-size budget improved:")
for entry in improvements:
print(f" - {entry}")
print("Consider running: scripts/check_test_size_budget.py --update")
else:
print(f"Test-size budget OK: tracked={len(tracked)} threshold={threshold}LOC")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
baseline_file="$repo_root/scripts/warning_budget.txt"
usage() {
cat <<'USAGE'
Usage:
scripts/check_warning_budget.sh # fail if warnings exceed baseline
scripts/check_warning_budget.sh --update # update baseline to current warning count
Notes:
- Counts Rust compiler lines that begin with "warning:" from `cargo check -q`
- Baseline is stored in scripts/warning_budget.txt
USAGE
}
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
usage
exit 0
fi
if [[ ! -f "$baseline_file" ]]; then
echo "error: missing baseline file: $baseline_file" >&2
exit 1
fi
current=$(cd "$repo_root" && CARGO_TERM_COLOR=never cargo check -q 2>&1 | rg -c '^warning:' || printf '0\n')
baseline=$(tr -d '[:space:]' < "$baseline_file")
if [[ "${1:-}" == "--update" ]]; then
printf '%s\n' "$current" > "$baseline_file"
echo "Updated warning baseline: $baseline"
echo "New warning baseline: $current"
exit 0
fi
if ! [[ "$baseline" =~ ^[0-9]+$ ]]; then
echo "error: invalid warning baseline in $baseline_file: '$baseline'" >&2
exit 1
fi
if (( current > baseline )); then
echo "Warning budget exceeded: current=$current baseline=$baseline" >&2
echo "Run scripts/check_warning_budget.sh --update only after intentional cleanup." >&2
exit 1
fi
if (( current < baseline )); then
echo "Warning budget improved: current=$current baseline=$baseline"
echo "Consider running: scripts/check_warning_budget.sh --update"
else
echo "Warning budget OK: current=$current baseline=$baseline"
fi
+165
View File
@@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""Enforce a ratcheting cross-crate wildcard re-export budget.
Counts production Rust occurrences of `pub use jcode_<crate>::*;` (cross-crate
wildcard re-exports). These re-exports erase crate boundaries: every symbol in
the re-exported crate becomes reachable through the re-exporting crate, so new
code silently couples across layers and edits to the re-exported crate rebuild
the whole spine.
Module-internal wildcards (`pub use self::...`, `pub use super::...`,
`pub use crate::...`, `pub use <module>::*` within a crate) are allowed; only
`jcode_*` cross-crate wildcards are budgeted.
Policy:
- Existing files may not increase their count.
- New production files may not introduce cross-crate wildcard re-exports.
- Total count may not increase.
- `--update` refreshes the baseline after intentional cleanup.
The long-term goal is to drive this budget to zero as the migration-era
re-export spine (base -> app-core -> tui -> root) is dismantled. See
docs/CRATE_OWNERSHIP_BOUNDARIES.md.
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
from typing import Any
REPO_ROOT = Path(__file__).resolve().parent.parent
BASELINE_FILE = REPO_ROOT / "scripts" / "wildcard_reexport_budget.json"
SCAN_ROOTS = (REPO_ROOT / "src", REPO_ROOT / "crates")
PATTERN = re.compile(r"^\s*pub\s+use\s+(?:::)?jcode_[a-z0-9_]+(?:::[A-Za-z0-9_]+)*::\*\s*;")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--update", action="store_true", help="refresh the baseline")
return parser.parse_args()
def production_rust_files() -> list[Path]:
files: list[Path] = []
for root in SCAN_ROOTS:
if not root.exists():
continue
for path in sorted(root.rglob("*.rs")):
rel = path.relative_to(REPO_ROOT).as_posix()
if "/target/" in f"/{rel}/":
continue
files.append(path)
return files
def count_wildcards(path: Path) -> int:
count = 0
try:
text = path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
return 0
for line in text.splitlines():
stripped = line.strip()
if stripped.startswith("//"):
continue
if PATTERN.match(line):
count += 1
return count
def collect_counts() -> dict[str, int]:
counts: dict[str, int] = {}
for path in production_rust_files():
count = count_wildcards(path)
if count > 0:
rel = path.relative_to(REPO_ROOT).as_posix()
counts[rel] = count
return counts
def load_baseline() -> dict[str, Any] | None:
if not BASELINE_FILE.exists():
return None
try:
return json.loads(BASELINE_FILE.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return None
def write_baseline(counts: dict[str, int]) -> None:
payload = {
"total": sum(counts.values()),
"files": dict(sorted(counts.items())),
}
BASELINE_FILE.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
def main() -> int:
args = parse_args()
counts = collect_counts()
total = sum(counts.values())
if args.update:
write_baseline(counts)
print(f"wildcard re-export baseline updated: total={total} files={len(counts)}")
return 0
baseline = load_baseline()
if baseline is None:
print(
"error: missing or invalid baseline file "
f"{BASELINE_FILE.relative_to(REPO_ROOT)}; run with --update to create it",
file=sys.stderr,
)
return 1
baseline_files: dict[str, int] = baseline.get("files", {})
baseline_total: int = baseline.get("total", sum(baseline_files.values()))
errors: list[str] = []
for rel, count in sorted(counts.items()):
allowed = baseline_files.get(rel)
if allowed is None:
errors.append(
f"{rel}: new file introduces {count} cross-crate wildcard re-export(s); "
"use explicit `pub use crate::path::{Item, ...}` instead"
)
elif count > allowed:
errors.append(
f"{rel}: cross-crate wildcard re-exports increased from {allowed} to {count}"
)
if total > baseline_total:
errors.append(
f"total cross-crate wildcard re-exports increased from {baseline_total} to {total}"
)
if errors:
for error in errors:
print(f"error: {error}", file=sys.stderr)
print(
"wildcard re-export budget check failed. These re-exports erase crate "
"boundaries; prefer explicit item re-exports. If a removal made the "
"baseline stale, run scripts/check_wildcard_reexport_budget.py --update",
file=sys.stderr,
)
return 1
if total < baseline_total or any(
counts.get(rel, 0) < allowed for rel, allowed in baseline_files.items()
):
print(
f"wildcard re-export budget check passed (total={total}, baseline={baseline_total}); "
"consider running --update to ratchet the baseline down"
)
else:
print(f"wildcard re-export budget check passed (total={total})")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+148
View File
@@ -0,0 +1,148 @@
#!/usr/bin/env bash
# Safely reclaim disk space from the Cargo target directory.
#
# This is designed to be safe to run even while other builds are in progress on
# this machine (e.g. parallel self-dev agents). It will:
# - never touch a target/<profile> dir that has an active rustc/cargo process
# or that was written to within a recent activity window
# - by default only remove cross-compile / compat caches and obviously stale
# profile dirs, plus run `cargo clean` on stale profiles
#
# Usage:
# scripts/clean_target.sh # dry-run: report what would be freed
# scripts/clean_target.sh --apply # actually delete safe items
# scripts/clean_target.sh --apply --aggressive # also sweep stale per-profile artifacts
#
# Env:
# JCODE_CLEAN_ACTIVE_WINDOW_MIN activity window in minutes (default 20)
set -euo pipefail
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
cd "$repo_root"
target_dir="${CARGO_TARGET_DIR:-$repo_root/target}"
apply="false"
aggressive="false"
activity_window_min="${JCODE_CLEAN_ACTIVE_WINDOW_MIN:-20}"
for arg in "$@"; do
case "$arg" in
--apply) apply="true" ;;
--aggressive) aggressive="true" ;;
-h|--help)
sed -n '2,20p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit 0
;;
*)
printf 'clean_target: unknown arg: %s\n' "$arg" >&2
exit 2
;;
esac
done
log() { printf 'clean_target: %s\n' "$*" >&2; }
human() {
# Bytes -> human readable
numfmt --to=iec --suffix=B "${1:-0}" 2>/dev/null || printf '%sB' "${1:-0}"
}
dir_bytes() {
du -sb "$1" 2>/dev/null | awk '{print $1}'
}
# Is any rustc/cargo process currently operating inside this path?
path_has_active_process() {
local path="$1"
local p
for p in $(pgrep -x rustc 2>/dev/null) $(pgrep -x cargo 2>/dev/null); do
if tr '\0' ' ' < "/proc/$p/cmdline" 2>/dev/null | grep -qF "$path"; then
return 0
fi
done
return 1
}
# Was this path written to within the activity window?
path_recently_active() {
local path="$1"
[[ -d "$path" ]] || return 1
local recent
recent=$(find "$path" -maxdepth 3 -type f -newermt "-${activity_window_min} min" 2>/dev/null | head -1)
[[ -n "$recent" ]]
}
is_safe_to_remove() {
local path="$1"
[[ -d "$path" ]] || return 1
if path_has_active_process "$path"; then
log "SKIP (active process): $path"
return 1
fi
if path_recently_active "$path"; then
log "SKIP (written <${activity_window_min}min ago): $path"
return 1
fi
return 0
}
total_reclaimed=0
remove_path() {
local path="$1" reason="$2"
[[ -e "$path" ]] || return 0
local bytes
bytes=$(dir_bytes "$path")
bytes=${bytes:-0}
if ! is_safe_to_remove "$path"; then
return 0
fi
if [[ "$apply" == "true" ]]; then
if rm -rf "$path" 2>/dev/null; then
log "removed ($reason): $path [$(human "$bytes")]"
total_reclaimed=$((total_reclaimed + bytes))
else
log "FAILED to remove (permissions? try sudo): $path [$(human "$bytes")]"
fi
else
log "would remove ($reason): $path [$(human "$bytes")]"
total_reclaimed=$((total_reclaimed + bytes))
fi
}
log "target dir: $target_dir (activity window: ${activity_window_min}min, apply=$apply, aggressive=$aggressive)"
# 1) Cross-compile / compat caches: not part of the local dev inner loop. They
# are regenerated on demand by release/compat scripts.
for d in "$target_dir"/*-apple-darwin "$target_dir"/*-pc-windows-* "$target_dir"/linux-compat; do
[[ -d "$d" ]] || continue
remove_path "$d" "cross-compile/compat cache"
done
# 2) Aggressive: cargo clean on stale (not-recently-active, no active process)
# profiles to drop accumulated fingerprints/old artifact generations.
if [[ "$aggressive" == "true" ]]; then
for profile_dir in "$target_dir"/debug "$target_dir"/release "$target_dir"/selfdev; do
[[ -d "$profile_dir" ]] || continue
profile=$(basename "$profile_dir")
[[ "$profile" == "debug" ]] && profile="dev"
if ! is_safe_to_remove "$profile_dir"; then
continue
fi
before=$(dir_bytes "$profile_dir"); before=${before:-0}
if [[ "$apply" == "true" ]]; then
log "cargo clean --profile $profile (stale) ..."
cargo clean --profile "$profile" 2>/dev/null || log " cargo clean failed for $profile"
after=$(dir_bytes "$profile_dir"); after=${after:-0}
freed=$((before - after))
(( freed > 0 )) && total_reclaimed=$((total_reclaimed + freed))
log " freed $(human "$freed") from $profile"
else
log "would cargo clean --profile $profile [up to $(human "$before")]"
total_reclaimed=$((total_reclaimed + before))
fi
done
fi
log "total $([ "$apply" == true ] && echo reclaimed || echo reclaimable): $(human "$total_reclaimed")"
+100
View File
@@ -0,0 +1,100 @@
{
"threshold_loc": 1200,
"tracked_files": {
"crates/jcode-app-core/src/agent/turn_streaming_mpsc.rs": 1641,
"crates/jcode-app-core/src/overnight.rs": 1274,
"crates/jcode-app-core/src/server.rs": 2034,
"crates/jcode-app-core/src/server/client_lifecycle.rs": 3075,
"crates/jcode-app-core/src/server/client_session.rs": 1448,
"crates/jcode-app-core/src/server/comm_control.rs": 2625,
"crates/jcode-app-core/src/server/comm_session.rs": 1341,
"crates/jcode-app-core/src/server/jade_relay.rs": 1428,
"crates/jcode-app-core/src/server/provider_control.rs": 1439,
"crates/jcode-app-core/src/server/swarm.rs": 2824,
"crates/jcode-app-core/src/tool/communicate.rs": 3156,
"crates/jcode-app-core/src/tool/session_search.rs": 1892,
"crates/jcode-app-core/src/update.rs": 1715,
"crates/jcode-base/src/auth/lifecycle.rs": 2034,
"crates/jcode-base/src/auth/mod.rs": 1455,
"crates/jcode-base/src/auth/oauth.rs": 1519,
"crates/jcode-base/src/background.rs": 1390,
"crates/jcode-base/src/compaction.rs": 1790,
"crates/jcode-base/src/gmail.rs": 1213,
"crates/jcode-base/src/memory.rs": 2076,
"crates/jcode-base/src/memory_agent.rs": 1892,
"crates/jcode-base/src/provider/catalog_routes.rs": 1227,
"crates/jcode-base/src/provider/mod.rs": 2602,
"crates/jcode-base/src/session.rs": 1581,
"crates/jcode-base/src/sidecar.rs": 1299,
"crates/jcode-base/src/skill.rs": 1257,
"crates/jcode-config-types/src/lib.rs": 1500,
"crates/jcode-desktop/src/canvas.rs": 2400,
"crates/jcode-desktop/src/desktop_benchmarks_scroll.rs": 1822,
"crates/jcode-desktop/src/desktop_rich_text.rs": 2069,
"crates/jcode-desktop/src/main.rs": 2386,
"crates/jcode-desktop/src/render_helpers.rs": 1347,
"crates/jcode-desktop/src/session_launch.rs": 1240,
"crates/jcode-desktop/src/single_session.rs": 1872,
"crates/jcode-desktop/src/single_session_render.rs": 1377,
"crates/jcode-desktop/src/single_session_render/handwriting.rs": 3005,
"crates/jcode-desktop/src/single_session_render/inline_widget_chrome.rs": 1554,
"crates/jcode-desktop/src/single_session_render/motion_transcript.rs": 1278,
"crates/jcode-desktop/src/workspace.rs": 1640,
"crates/jcode-import-core/src/lib.rs": 1645,
"crates/jcode-protocol/src/wire.rs": 1431,
"crates/jcode-provider-anthropic-runtime/src/lib.rs": 2403,
"crates/jcode-provider-bedrock/src/lib.rs": 1820,
"crates/jcode-provider-core/src/lib.rs": 1609,
"crates/jcode-provider-doctor/src/lifecycle_driver.rs": 1985,
"crates/jcode-provider-doctor/src/live_provider_probes.rs": 2029,
"crates/jcode-provider-doctor/src/provider_e2e.rs": 2680,
"crates/jcode-provider-openai-runtime/src/openai_stream_runtime.rs": 1603,
"crates/jcode-provider-openrouter-runtime/src/lib.rs": 2641,
"crates/jcode-setup-hints/src/lib.rs": 1811,
"crates/jcode-telemetry-core/src/lib.rs": 1846,
"crates/jcode-tui-mermaid/src/mermaid_cache_render.rs": 1288,
"crates/jcode-tui-mermaid/src/mermaid_viewport.rs": 1317,
"crates/jcode-tui-render/src/swarm_gallery.rs": 2556,
"crates/jcode-tui/src/tui/app.rs": 2365,
"crates/jcode-tui/src/tui/app/auth.rs": 2955,
"crates/jcode-tui/src/tui/app/auth_account_picker.rs": 1220,
"crates/jcode-tui/src/tui/app/commands.rs": 3277,
"crates/jcode-tui/src/tui/app/debug_bench.rs": 1281,
"crates/jcode-tui/src/tui/app/helpers.rs": 1572,
"crates/jcode-tui/src/tui/app/inline_interactive.rs": 3348,
"crates/jcode-tui/src/tui/app/input.rs": 3336,
"crates/jcode-tui/src/tui/app/model_context.rs": 1956,
"crates/jcode-tui/src/tui/app/navigation.rs": 1751,
"crates/jcode-tui/src/tui/app/onboarding_flow_control.rs": 1546,
"crates/jcode-tui/src/tui/app/remote.rs": 1853,
"crates/jcode-tui/src/tui/app/remote/key_handling.rs": 2542,
"crates/jcode-tui/src/tui/app/remote/server_events.rs": 2698,
"crates/jcode-tui/src/tui/app/state_ui.rs": 2186,
"crates/jcode-tui/src/tui/app/state_ui_input_helpers.rs": 1880,
"crates/jcode-tui/src/tui/app/tui_lifecycle.rs": 1242,
"crates/jcode-tui/src/tui/app/tui_state.rs": 2253,
"crates/jcode-tui/src/tui/app/turn.rs": 1470,
"crates/jcode-tui/src/tui/backend.rs": 1697,
"crates/jcode-tui/src/tui/info_widget.rs": 2209,
"crates/jcode-tui/src/tui/mod.rs": 2001,
"crates/jcode-tui/src/tui/session_picker.rs": 2174,
"crates/jcode-tui/src/tui/session_picker/loading.rs": 2998,
"crates/jcode-tui/src/tui/ui.rs": 3373,
"crates/jcode-tui/src/tui/ui_frame_metrics.rs": 1336,
"crates/jcode-tui/src/tui/ui_header.rs": 1626,
"crates/jcode-tui/src/tui/ui_inline_image.rs": 1251,
"crates/jcode-tui/src/tui/ui_input.rs": 2643,
"crates/jcode-tui/src/tui/ui_messages.rs": 2399,
"crates/jcode-tui/src/tui/ui_pinned.rs": 2011,
"crates/jcode-tui/src/tui/ui_prepare.rs": 2360,
"crates/jcode-tui/src/tui/ui_tools.rs": 1481,
"crates/jcode-tui/src/tui/ui_viewport.rs": 1336,
"src/bin/memory_recall_bench.rs": 2667,
"src/bin/tui_bench.rs": 1747,
"src/cli/commands.rs": 3353,
"src/cli/dispatch.rs": 1209,
"src/cli/login.rs": 1452,
"src/cli/provider_init.rs": 1801
},
"version": 1
}
+500
View File
@@ -0,0 +1,500 @@
#!/usr/bin/env python3
"""
Compare token usage between jcode and Claude Code CLI.
This script runs the same prompts through both tools and compares their token usage.
The goal is to verify that jcode's token consumption is within expected bounds
compared to the official Claude Code CLI.
NOTE: jcode typically uses FEWER tokens than Claude CLI because:
1. jcode has a smaller/simpler system prompt
2. jcode registers fewer tools (Claude CLI has many built-in tools)
3. Different prompt caching behavior
The test PASSES if jcode uses fewer tokens OR at most 50% more tokens.
Using more tokens would indicate a problem with the system prompt or tool registration.
Usage:
python scripts/compare_token_usage.py [--verbose] [--runs N]
Requirements:
- jcode built and in PATH or at target/release/jcode
- claude CLI installed and authenticated
- Both should use the same model (claude-opus-4-5-20251101 by default)
"""
import argparse
import json
import os
import subprocess
import sys
import tempfile
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
@dataclass
class TokenUsage:
"""Token usage from a single run."""
input_tokens: int
output_tokens: int
cache_read_tokens: int
cache_creation_tokens: int
total_cost_usd: Optional[float] = None
duration_ms: Optional[int] = None
@property
def total_input(self) -> int:
"""Total input tokens including cache."""
return self.input_tokens + self.cache_read_tokens + self.cache_creation_tokens
@property
def total(self) -> int:
"""Total tokens (input + output)."""
return self.total_input + self.output_tokens
@dataclass
class RunResult:
"""Result of a single tool run."""
tool: str
prompt: str
usage: TokenUsage
success: bool
output: str
error: Optional[str] = None
def find_jcode_binary() -> str:
"""Find the jcode binary."""
# Check target/release first
repo_root = Path(__file__).parent.parent
release_binary = repo_root / "target" / "release" / "jcode"
if release_binary.exists():
return str(release_binary)
# Check PATH
result = subprocess.run(["which", "jcode"], capture_output=True, text=True)
if result.returncode == 0:
return result.stdout.strip()
raise FileNotFoundError("jcode binary not found. Run 'cargo build --release' first.")
def run_claude_cli(prompt: str, workdir: str, model: str = "opus") -> RunResult:
"""Run the Claude Code CLI and capture token usage."""
try:
result = subprocess.run(
[
"claude",
"-p",
"--output-format", "json",
"--dangerously-skip-permissions",
"--model", model,
prompt,
],
capture_output=True,
text=True,
cwd=workdir,
timeout=120,
)
if result.returncode != 0 and not result.stdout:
return RunResult(
tool="claude",
prompt=prompt,
usage=TokenUsage(0, 0, 0, 0),
success=False,
output="",
error=result.stderr or f"Exit code {result.returncode}",
)
# Parse JSON output
data = json.loads(result.stdout)
usage = data.get("usage", {})
token_usage = TokenUsage(
input_tokens=usage.get("input_tokens", 0),
output_tokens=usage.get("output_tokens", 0),
cache_read_tokens=usage.get("cache_read_input_tokens", 0),
cache_creation_tokens=usage.get("cache_creation_input_tokens", 0),
total_cost_usd=data.get("total_cost_usd"),
duration_ms=data.get("duration_ms"),
)
return RunResult(
tool="claude",
prompt=prompt,
usage=token_usage,
success=not data.get("is_error", False),
output=data.get("result", ""),
)
except subprocess.TimeoutExpired:
return RunResult(
tool="claude",
prompt=prompt,
usage=TokenUsage(0, 0, 0, 0),
success=False,
output="",
error="Timeout after 120s",
)
except json.JSONDecodeError as e:
return RunResult(
tool="claude",
prompt=prompt,
usage=TokenUsage(0, 0, 0, 0),
success=False,
output=result.stdout if 'result' in dir() else "",
error=f"JSON parse error: {e}",
)
except Exception as e:
return RunResult(
tool="claude",
prompt=prompt,
usage=TokenUsage(0, 0, 0, 0),
success=False,
output="",
error=str(e),
)
def run_jcode(prompt: str, workdir: str, jcode_binary: str, model: str = "claude-opus-4-5-20251101") -> RunResult:
"""Run jcode and capture token usage from trace output."""
try:
# Create a temporary JCODE_HOME to avoid polluting user's sessions
with tempfile.TemporaryDirectory() as tmpdir:
env = os.environ.copy()
env["JCODE_HOME"] = tmpdir
env["JCODE_TRACE"] = "1"
result = subprocess.run(
[
jcode_binary,
"run",
"--no-update",
"--model", model,
prompt,
],
capture_output=True,
text=True,
cwd=workdir,
timeout=120,
env=env,
)
# Parse token usage from trace output in stderr
# Format: [trace] token_usage input=X output=Y cache_read=Z cache_write=W
input_tokens = 0
output_tokens = 0
cache_read = 0
cache_write = 0
for line in result.stderr.split("\n"):
if "[trace] token_usage" in line:
parts = line.split()
for part in parts:
if part.startswith("input="):
input_tokens = int(part.split("=")[1])
elif part.startswith("output="):
output_tokens = int(part.split("=")[1])
elif part.startswith("cache_read="):
cache_read = int(part.split("=")[1])
elif part.startswith("cache_write="):
cache_write = int(part.split("=")[1])
token_usage = TokenUsage(
input_tokens=input_tokens,
output_tokens=output_tokens,
cache_read_tokens=cache_read,
cache_creation_tokens=cache_write,
)
return RunResult(
tool="jcode",
prompt=prompt,
usage=token_usage,
success=result.returncode == 0,
output=result.stdout,
error=None if result.returncode == 0 else result.stderr,
)
except subprocess.TimeoutExpired:
return RunResult(
tool="jcode",
prompt=prompt,
usage=TokenUsage(0, 0, 0, 0),
success=False,
output="",
error="Timeout after 120s",
)
except Exception as e:
return RunResult(
tool="jcode",
prompt=prompt,
usage=TokenUsage(0, 0, 0, 0),
success=False,
output="",
error=str(e),
)
def compare_usage(claude_result: RunResult, jcode_result: RunResult, verbose: bool = False) -> dict:
"""Compare token usage between Claude CLI and jcode."""
c = claude_result.usage
j = jcode_result.usage
# Calculate differences
input_diff = j.input_tokens - c.input_tokens
output_diff = j.output_tokens - c.output_tokens
cache_read_diff = j.cache_read_tokens - c.cache_read_tokens
cache_write_diff = j.cache_creation_tokens - c.cache_creation_tokens
total_diff = j.total - c.total
# Calculate percentages (avoid division by zero)
def pct_diff(a: int, b: int) -> float:
if b == 0:
return 0.0 if a == 0 else float('inf')
return ((a - b) / b) * 100
input_pct = pct_diff(j.input_tokens, c.input_tokens)
output_pct = pct_diff(j.output_tokens, c.output_tokens)
total_pct = pct_diff(j.total, c.total)
return {
"claude": {
"input": c.input_tokens,
"output": c.output_tokens,
"cache_read": c.cache_read_tokens,
"cache_write": c.cache_creation_tokens,
"total": c.total,
"cost_usd": c.total_cost_usd,
"duration_ms": c.duration_ms,
},
"jcode": {
"input": j.input_tokens,
"output": j.output_tokens,
"cache_read": j.cache_read_tokens,
"cache_write": j.cache_creation_tokens,
"total": j.total,
},
"diff": {
"input": input_diff,
"output": output_diff,
"cache_read": cache_read_diff,
"cache_write": cache_write_diff,
"total": total_diff,
},
"pct_diff": {
"input": input_pct,
"output": output_pct,
"total": total_pct,
},
}
def print_comparison(comparison: dict, prompt: str, verbose: bool = False):
"""Print a formatted comparison."""
print(f"\n{'='*60}")
print(f"Prompt: {prompt[:50]}..." if len(prompt) > 50 else f"Prompt: {prompt}")
print(f"{'='*60}")
c = comparison["claude"]
j = comparison["jcode"]
d = comparison["diff"]
p = comparison["pct_diff"]
print(f"\n{'Metric':<20} {'Claude':<15} {'jcode':<15} {'Diff':<15} {'% Diff':<10}")
print("-" * 75)
print(f"{'Input tokens':<20} {c['input']:<15} {j['input']:<15} {d['input']:+<15} {p['input']:+.1f}%")
print(f"{'Output tokens':<20} {c['output']:<15} {j['output']:<15} {d['output']:+<15} {p['output']:+.1f}%")
print(f"{'Cache read':<20} {c['cache_read']:<15} {j['cache_read']:<15} {d['cache_read']:+<15}")
print(f"{'Cache write':<20} {c['cache_write']:<15} {j['cache_write']:<15} {d['cache_write']:+<15}")
print("-" * 75)
print(f"{'TOTAL':<20} {c['total']:<15} {j['total']:<15} {d['total']:+<15} {p['total']:+.1f}%")
if c.get("cost_usd"):
print(f"\nClaude CLI cost: ${c['cost_usd']:.6f}")
if c.get("duration_ms"):
print(f"Claude CLI duration: {c['duration_ms']}ms")
def run_test_suite(verbose: bool = False, runs: int = 1) -> list:
"""Run the full test suite."""
# Test prompts - simple ones that don't require tools
prompts = [
"Reply with a single word: test",
"What is 2 + 2? Reply with just the number.",
"List three primary colors, one per line.",
]
jcode_binary = find_jcode_binary()
print(f"Using jcode binary: {jcode_binary}")
print(f"Running {len(prompts)} prompts, {runs} run(s) each\n")
results = []
with tempfile.TemporaryDirectory() as workdir:
for prompt in prompts:
for run_num in range(runs):
print(f"\n[Run {run_num + 1}/{runs}] Testing: {prompt[:40]}...")
# Run both tools
print(" Running Claude CLI...", end=" ", flush=True)
claude_result = run_claude_cli(prompt, workdir)
if claude_result.success:
print(f"OK ({claude_result.usage.total} tokens)")
else:
print(f"FAILED: {claude_result.error}")
if verbose:
print(f" Output: {claude_result.output[:200]}")
# Small delay to avoid rate limiting
time.sleep(1)
print(" Running jcode...", end=" ", flush=True)
jcode_result = run_jcode(prompt, workdir, jcode_binary)
if jcode_result.success:
print(f"OK ({jcode_result.usage.total} tokens)")
else:
print(f"FAILED: {jcode_result.error}")
if verbose:
print(f" Output: {jcode_result.output[:200]}")
if claude_result.success and jcode_result.success:
comparison = compare_usage(claude_result, jcode_result, verbose)
results.append({
"prompt": prompt,
"run": run_num + 1,
"comparison": comparison,
})
if verbose:
print_comparison(comparison, prompt, verbose)
# Delay between prompts
time.sleep(2)
return results
def summarize_results(results: list) -> bool:
"""Print summary of all results. Returns True if test passed."""
if not results:
print("\nNo successful results to summarize.")
return False
print(f"\n{'='*60}")
print("SUMMARY")
print(f"{'='*60}")
total_claude = sum(r["comparison"]["claude"]["total"] for r in results)
total_jcode = sum(r["comparison"]["jcode"]["total"] for r in results)
total_diff = total_jcode - total_claude
# Also compare just input+output (excluding cache)
total_claude_io = sum(
r["comparison"]["claude"]["input"] + r["comparison"]["claude"]["output"]
for r in results
)
total_jcode_io = sum(
r["comparison"]["jcode"]["input"] + r["comparison"]["jcode"]["output"]
for r in results
)
if total_claude > 0:
pct_diff = ((total_jcode - total_claude) / total_claude) * 100
else:
pct_diff = 0
print(f"\nTotal runs: {len(results)}")
print(f"\n--- Total Tokens (including cache) ---")
print(f"Claude CLI: {total_claude}")
print(f"jcode: {total_jcode}")
print(f"Difference: {total_diff:+} ({pct_diff:+.1f}%)")
print(f"\n--- Input + Output only (excluding cache) ---")
print(f"Claude CLI: {total_claude_io}")
print(f"jcode: {total_jcode_io}")
if total_claude_io > 0:
io_pct_diff = ((total_jcode_io - total_claude_io) / total_claude_io) * 100
print(f"Difference: {total_jcode_io - total_claude_io:+} ({io_pct_diff:+.1f}%)")
# Check if within acceptable bounds
# jcode using fewer tokens is always good (negative diff)
# jcode using more tokens is acceptable up to MAX_OVERHEAD_PCT
MAX_OVERHEAD_PCT = 50 # Allow up to 50% more tokens (for different system prompts)
passed = True
if pct_diff <= 0:
print(f"\n✅ PASS: jcode uses {abs(pct_diff):.1f}% fewer tokens than Claude CLI")
elif pct_diff <= MAX_OVERHEAD_PCT:
print(f"\n✅ PASS: jcode uses {pct_diff:.1f}% more tokens (within {MAX_OVERHEAD_PCT}% threshold)")
else:
print(f"\n❌ FAIL: jcode uses {pct_diff:.1f}% more tokens (exceeds {MAX_OVERHEAD_PCT}% threshold)")
passed = False
# Per-prompt breakdown
print("\nPer-prompt breakdown:")
print(f"{'Prompt':<40} {'Claude':<10} {'jcode':<10} {'Diff':<10}")
print("-" * 70)
for r in results:
prompt = r["prompt"][:37] + "..." if len(r["prompt"]) > 40 else r["prompt"]
c_total = r["comparison"]["claude"]["total"]
j_total = r["comparison"]["jcode"]["total"]
diff = j_total - c_total
print(f"{prompt:<40} {c_total:<10} {j_total:<10} {diff:+<10}")
return passed
def main():
parser = argparse.ArgumentParser(
description="Compare token usage between jcode and Claude Code CLI"
)
parser.add_argument(
"--verbose", "-v",
action="store_true",
help="Show detailed output for each run",
)
parser.add_argument(
"--runs", "-n",
type=int,
default=1,
help="Number of runs per prompt (default: 1)",
)
parser.add_argument(
"--json",
action="store_true",
help="Output results as JSON",
)
args = parser.parse_args()
print("Token Usage Comparison: jcode vs Claude Code CLI")
print("=" * 50)
try:
results = run_test_suite(verbose=args.verbose, runs=args.runs)
if args.json:
print(json.dumps(results, indent=2))
# For JSON mode, check if all runs succeeded
sys.exit(0 if results else 1)
else:
passed = summarize_results(results)
sys.exit(0 if passed else 1)
except FileNotFoundError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
except KeyboardInterrupt:
print("\nInterrupted by user")
sys.exit(130)
if __name__ == "__main__":
main()
+251
View File
@@ -0,0 +1,251 @@
#!/usr/bin/env python3
"""Report compile-time isolation risks in the Jcode crate graph.
This is advisory by default. Use --strict-target-state only when a migration phase
has removed the listed temporary violations and we want to prevent regressions.
"""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
WATCHED_CRATES = ["jcode-base", "jcode-app-core", "jcode-tui", "jcode"]
@dataclass
class CrateStats:
name: str
manifest_path: str
src_path: str | None
rust_files: int
loc: int
cfg_test_count: int
test_attr_count: int
async_trait_count: int
derive_count: int
glob_reexports: list[str]
normal_workspace_deps: list[str]
normal_external_deps: list[str]
dev_workspace_deps: list[str]
dev_external_deps: list[str]
build_workspace_deps: list[str]
build_external_deps: list[str]
def run_metadata() -> dict[str, Any]:
result = subprocess.run(
["cargo", "metadata", "--no-deps", "--format-version", "1"],
cwd=ROOT,
check=True,
text=True,
stdout=subprocess.PIPE,
)
return json.loads(result.stdout)
def lib_or_root_src(package: dict[str, Any]) -> Path | None:
manifest = Path(package["manifest_path"])
for target in package.get("targets", []):
if "lib" in target.get("kind", []):
return Path(target["src_path"])
if package["name"] == "jcode":
return ROOT / "src"
src = manifest.parent / "src"
return src if src.exists() else None
def iter_rust_files(src_path: Path | None) -> list[Path]:
if src_path is None:
return []
if src_path.is_file():
root = src_path.parent
else:
root = src_path
if not root.exists():
return []
return sorted(path for path in root.rglob("*.rs") if path.is_file())
def count_file(path: Path) -> tuple[int, str]:
text = path.read_text(errors="replace")
return text.count("\n") + (0 if text.endswith("\n") or not text else 1), text
def collect_stats(package: dict[str, Any], workspace_names: set[str]) -> CrateStats:
src_path = lib_or_root_src(package)
rust_files = iter_rust_files(src_path)
loc = 0
cfg_test_count = 0
test_attr_count = 0
async_trait_count = 0
derive_count = 0
glob_reexports: list[str] = []
for path in rust_files:
file_loc, text = count_file(path)
loc += file_loc
cfg_test_count += len(re.findall(r"#\s*\[\s*cfg\s*\(\s*test\s*\)\s*\]", text))
test_attr_count += len(re.findall(r"#\s*\[\s*(?:tokio::)?test(?:\s*\([^\]]*\))?\s*\]", text))
async_trait_count += len(re.findall(r"#\s*\[\s*(?:async_trait::)?async_trait\s*\]", text))
derive_count += len(re.findall(r"#\s*\[\s*derive\s*\(", text))
for line_number, line in enumerate(text.splitlines(), 1):
stripped = line.strip()
if stripped.startswith("//"):
continue
if re.fullmatch(r"pub\s+use\s+[^;\n]+::\s*\*\s*;", stripped):
rel = path.relative_to(ROOT)
glob_reexports.append(f"{rel}:{line_number}: {stripped}")
workspace_deps_by_kind: dict[str, list[str]] = {"normal": [], "dev": [], "build": []}
external_deps_by_kind: dict[str, list[str]] = {"normal": [], "dev": [], "build": []}
for dep in package.get("dependencies", []):
dep_name = dep["name"]
dep_kind = dep.get("kind") or "normal"
if dep_kind not in workspace_deps_by_kind:
dep_kind = "normal"
buckets = workspace_deps_by_kind if dep_name in workspace_names else external_deps_by_kind
buckets[dep_kind].append(dep_name)
return CrateStats(
name=package["name"],
manifest_path=str(Path(package["manifest_path"]).relative_to(ROOT)),
src_path=str(src_path.relative_to(ROOT)) if src_path and src_path.exists() else None,
rust_files=len(rust_files),
loc=loc,
cfg_test_count=cfg_test_count,
test_attr_count=test_attr_count,
async_trait_count=async_trait_count,
derive_count=derive_count,
glob_reexports=glob_reexports,
normal_workspace_deps=sorted(workspace_deps_by_kind["normal"]),
normal_external_deps=sorted(external_deps_by_kind["normal"]),
dev_workspace_deps=sorted(workspace_deps_by_kind["dev"]),
dev_external_deps=sorted(external_deps_by_kind["dev"]),
build_workspace_deps=sorted(workspace_deps_by_kind["build"]),
build_external_deps=sorted(external_deps_by_kind["build"]),
)
def target_state_violations(stats_by_name: dict[str, CrateStats]) -> list[str]:
violations: list[str] = []
tui = stats_by_name.get("jcode-tui")
if tui and "jcode-app-core" in tui.normal_workspace_deps:
violations.append("target-state: jcode-tui still directly depends on jcode-app-core")
app_core = stats_by_name.get("jcode-app-core")
if app_core and "jcode-base" in app_core.normal_workspace_deps:
violations.append("target-state: jcode-app-core still directly depends on jcode-base")
base = stats_by_name.get("jcode-base")
if base:
for dep in base.normal_workspace_deps:
if dep in {
"jcode-azure-auth",
"jcode-provider-gemini",
"jcode-provider-openai",
"jcode-provider-openrouter",
"jcode-notify-email",
"jcode-build-support",
}:
violations.append(f"target-state: jcode-base still depends on leaf/runtime crate {dep}")
for dep in base.normal_external_deps:
if dep.startswith("aws-") or dep in {"aws-types"}:
violations.append(f"target-state: jcode-base still depends directly on AWS crate {dep}")
for crate in stats_by_name.values():
for glob in crate.glob_reexports:
if crate.name in {"jcode", "jcode-app-core", "jcode-tui"}:
violations.append(f"target-state: broad glob re-export remains in {glob}")
return violations
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--json", action="store_true", help="print machine-readable JSON")
parser.add_argument(
"--strict-target-state",
action="store_true",
help="exit non-zero for target-state violations (advisory by default)",
)
parser.add_argument(
"--top",
type=int,
default=12,
help="number of largest workspace crates to print in text mode",
)
args = parser.parse_args()
metadata = run_metadata()
package_by_id = {package["id"]: package for package in metadata["packages"]}
workspace_packages = [package_by_id[package_id] for package_id in metadata["workspace_members"]]
workspace_names = {package["name"] for package in workspace_packages}
stats = [collect_stats(package, workspace_names) for package in workspace_packages]
stats_by_name = {crate.name: crate for crate in stats}
violations = target_state_violations(stats_by_name)
largest = sorted(stats, key=lambda crate: crate.loc, reverse=True)
payload = {
"watched_crates": {name: asdict(stats_by_name[name]) for name in WATCHED_CRATES if name in stats_by_name},
"largest_crates": [asdict(crate) for crate in largest[: args.top]],
"target_state_violations": violations,
}
if args.json:
print(json.dumps(payload, indent=2, sort_keys=True))
else:
print("compile isolation static report")
print("largest workspace crates by Rust LOC:")
for crate in largest[: args.top]:
print(
f" - {crate.name}: {crate.loc} LOC, {crate.rust_files} files, "
f"#[test] {crate.test_attr_count}, cfg(test) {crate.cfg_test_count}, "
f"async_trait {crate.async_trait_count}, derive {crate.derive_count}"
)
print("watched crates:")
for name in WATCHED_CRATES:
crate = stats_by_name.get(name)
if not crate:
continue
print(
f" - {name}: {crate.loc} LOC, "
f"normal workspace deps={len(crate.normal_workspace_deps)}, "
f"normal external deps={len(crate.normal_external_deps)}"
)
if crate.dev_workspace_deps or crate.dev_external_deps or crate.build_workspace_deps or crate.build_external_deps:
print(
f" non-normal deps: dev workspace={len(crate.dev_workspace_deps)}, "
f"dev external={len(crate.dev_external_deps)}, "
f"build workspace={len(crate.build_workspace_deps)}, "
f"build external={len(crate.build_external_deps)}"
)
if crate.glob_reexports:
print(" glob re-exports:")
for glob in crate.glob_reexports[:8]:
print(f" {glob}")
if len(crate.glob_reexports) > 8:
print(f" ... {len(crate.glob_reexports) - 8} more")
if violations:
print("target-state violations/advisories:")
for violation in violations:
print(f" - {violation}")
else:
print("target-state violations/advisories: none")
if args.strict_target_state and violations:
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+344
View File
@@ -0,0 +1,344 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
cd "$repo_root"
usage() {
cat <<'USAGE'
Usage:
scripts/compile_time_probe.sh [options]
Runs a full-feature selfdev jcode build with Cargo timings enabled and summarizes
critical-path-ish rustc units from target/cargo-timings/cargo-timing.html.
Options:
--skip-build Parse the latest timing HTML without running cargo
--timing-html <path> Parse a specific cargo timing HTML file
--touch <path> Touch a file before building to simulate an edit
--profile <name> Cargo profile to build (default: selfdev)
--package <name> Cargo package to build (default: jcode)
--bin <name> Cargo binary to build (default: jcode)
--feature-profile <name> JCODE_DEV_FEATURE_PROFILE for dev_cargo.sh (default: default)
--json <path> Write the parsed summary JSON to this path
--top <n> Number of slowest units to print (default: 12)
-h, --help Show this help
Examples:
scripts/compile_time_probe.sh --skip-build
scripts/compile_time_probe.sh --touch crates/jcode-tui/src/tui/app/input.rs
scripts/compile_time_probe.sh --json target/compile-time-probe.json
Notes:
- This intentionally defaults to the full/default feature set. It is for
compile-time isolation work that keeps debug/selfdev behavior production-like.
- The "jcode serial stack" summary is not a formal Cargo critical path. It is a
focused view of the known long-pole crates: jcode-base, jcode-app-core,
jcode-tui, root jcode lib, and jcode bin.
USAGE
}
skip_build=0
timing_html=""
touch_path=""
profile="selfdev"
package="jcode"
bin="jcode"
feature_profile="default"
json_path=""
top_n=12
while [[ $# -gt 0 ]]; do
case "$1" in
--skip-build)
skip_build=1
;;
--timing-html)
[[ $# -ge 2 ]] || { echo 'error: --timing-html requires a path' >&2; exit 1; }
timing_html="$2"
shift
;;
--touch)
[[ $# -ge 2 ]] || { echo 'error: --touch requires a path' >&2; exit 1; }
touch_path="$2"
shift
;;
--profile)
[[ $# -ge 2 ]] || { echo 'error: --profile requires a value' >&2; exit 1; }
profile="$2"
shift
;;
--package|-p)
[[ $# -ge 2 ]] || { echo 'error: --package requires a value' >&2; exit 1; }
package="$2"
shift
;;
--bin)
[[ $# -ge 2 ]] || { echo 'error: --bin requires a value' >&2; exit 1; }
bin="$2"
shift
;;
--feature-profile)
[[ $# -ge 2 ]] || { echo 'error: --feature-profile requires a value' >&2; exit 1; }
feature_profile="$2"
shift
;;
--json)
[[ $# -ge 2 ]] || { echo 'error: --json requires a path' >&2; exit 1; }
json_path="$2"
shift
;;
--top)
[[ $# -ge 2 ]] || { echo 'error: --top requires a positive integer' >&2; exit 1; }
top_n="$2"
shift
;;
-h|--help)
usage
exit 0
;;
*)
printf 'error: unknown argument: %s\n' "$1" >&2
usage >&2
exit 1
;;
esac
shift
done
if ! [[ "$top_n" =~ ^[1-9][0-9]*$ ]]; then
printf 'error: --top must be a positive integer (got %s)\n' "$top_n" >&2
exit 1
fi
if [[ -n "$touch_path" && ! -e "$touch_path" ]]; then
printf 'error: touch path does not exist: %s\n' "$touch_path" >&2
exit 1
fi
if [[ $skip_build -eq 0 ]]; then
if [[ -n "$touch_path" ]]; then
printf 'compile_time_probe: touching %s\n' "$touch_path" >&2
touch "$touch_path"
fi
printf 'compile_time_probe: building %s/%s profile=%s feature_profile=%s with --timings\n' \
"$package" "$bin" "$profile" "$feature_profile" >&2
start_ns=$(python3 - <<'PY'
import time
print(time.perf_counter_ns())
PY
)
JCODE_DEV_FEATURE_PROFILE="$feature_profile" \
scripts/dev_cargo.sh build --profile "$profile" -p "$package" --bin "$bin" --timings
end_ns=$(python3 - <<'PY'
import time
print(time.perf_counter_ns())
PY
)
elapsed_seconds=$(python3 - "$start_ns" "$end_ns" <<'PY'
import sys
start = int(sys.argv[1])
end = int(sys.argv[2])
print(f"{(end - start) / 1_000_000_000:.3f}")
PY
)
else
elapsed_seconds=""
fi
if [[ -z "$timing_html" ]]; then
timing_html=$(find target/cargo-timings -maxdepth 1 -type f -name 'cargo-timing*.html' -printf '%T@ %p\n' 2>/dev/null | sort -n | tail -1 | cut -d' ' -f2- || true)
fi
if [[ -z "$timing_html" || ! -f "$timing_html" ]]; then
printf 'error: no cargo timing HTML found; run without --skip-build or pass --timing-html\n' >&2
exit 1
fi
python3 - "$timing_html" "$elapsed_seconds" "$json_path" "$top_n" "$profile" "$package" "$bin" "$feature_profile" <<'PY'
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
from typing import Any
html_path = Path(sys.argv[1])
elapsed_arg = sys.argv[2]
json_path = Path(sys.argv[3]) if sys.argv[3] else None
top_n = int(sys.argv[4])
profile = sys.argv[5]
package = sys.argv[6]
bin_name = sys.argv[7]
feature_profile = sys.argv[8]
text = html_path.read_text(errors="replace")
def extract_duration() -> float | None:
match = re.search(r"(?:const\s+)?DURATION\s*=\s*([0-9]+(?:\.[0-9]+)?)\s*;", text)
return float(match.group(1)) if match else None
def extract_unit_data() -> list[dict[str, Any]]:
marker = "const UNIT_DATA = "
start = text.find(marker)
if start < 0:
raise SystemExit(f"error: {html_path} does not contain Cargo UNIT_DATA")
idx = start + len(marker)
while idx < len(text) and text[idx].isspace():
idx += 1
if idx >= len(text) or text[idx] != "[":
raise SystemExit("error: Cargo UNIT_DATA did not start with '['")
depth = 0
in_string = False
escape = False
end = idx
while end < len(text):
ch = text[end]
if in_string:
if escape:
escape = False
elif ch == "\\":
escape = True
elif ch == '"':
in_string = False
else:
if ch == '"':
in_string = True
elif ch == "[":
depth += 1
elif ch == "]":
depth -= 1
if depth == 0:
end += 1
break
end += 1
return json.loads(text[idx:end])
def section_duration(unit: dict[str, Any], section_name: str) -> float | None:
sections = unit.get("sections")
if not sections:
return None
for name, payload in sections:
if name == section_name:
return float(payload["end"]) - float(payload["start"])
return None
def fmt_seconds(value: float | None) -> str:
if value is None:
return "n/a"
return f"{value:.2f}s"
units = extract_unit_data()
for unit in units:
unit["end"] = float(unit.get("start", 0.0)) + float(unit.get("duration", 0.0))
unit["frontend_duration"] = section_duration(unit, "frontend")
unit["codegen_duration"] = section_duration(unit, "codegen")
# Slowest rustc-ish units by duration. Keep build-script run units visible but low-noise.
top_units = sorted(units, key=lambda unit: float(unit.get("duration", 0.0)), reverse=True)[:top_n]
def is_jcode_stack_unit(unit: dict[str, Any]) -> bool:
name = unit.get("name")
target = unit.get("target") or ""
if name in {"jcode-base", "jcode-app-core", "jcode-tui"} and "build script" not in target:
return True
if name == "jcode" and (target == "" or f'bin "{bin_name}"' in target):
return True
return False
jcode_stack = sorted([unit for unit in units if is_jcode_stack_unit(unit)], key=lambda unit: float(unit.get("start", 0.0)))
stack_span = None
if jcode_stack:
stack_span = max(float(unit["end"]) for unit in jcode_stack) - min(float(unit.get("start", 0.0)) for unit in jcode_stack)
stack_sum = sum(float(unit.get("duration", 0.0)) for unit in jcode_stack)
stack_frontend_sum = sum(float(unit.get("frontend_duration") or 0.0) for unit in jcode_stack)
stack_codegen_sum = sum(float(unit.get("codegen_duration") or 0.0) for unit in jcode_stack)
summary = {
"timing_html": str(html_path),
"profile": profile,
"package": package,
"bin": bin_name,
"feature_profile": feature_profile,
"wall_seconds_from_cargo_timing": extract_duration(),
"wall_seconds_measured_by_probe": float(elapsed_arg) if elapsed_arg else None,
"unit_count": len(units),
"top_units": [
{
"name": unit.get("name"),
"version": unit.get("version"),
"target": unit.get("target") or "",
"features": unit.get("features") or [],
"start_seconds": round(float(unit.get("start", 0.0)), 3),
"duration_seconds": round(float(unit.get("duration", 0.0)), 3),
"frontend_seconds": round(unit["frontend_duration"], 3) if unit.get("frontend_duration") is not None else None,
"codegen_seconds": round(unit["codegen_duration"], 3) if unit.get("codegen_duration") is not None else None,
}
for unit in top_units
],
"jcode_serial_stack": {
"span_seconds": round(stack_span, 3) if stack_span is not None else None,
"sum_unit_seconds": round(stack_sum, 3),
"sum_frontend_seconds": round(stack_frontend_sum, 3),
"sum_codegen_seconds": round(stack_codegen_sum, 3),
"units": [
{
"name": unit.get("name"),
"target": unit.get("target") or "",
"start_seconds": round(float(unit.get("start", 0.0)), 3),
"end_seconds": round(float(unit.get("end", 0.0)), 3),
"duration_seconds": round(float(unit.get("duration", 0.0)), 3),
"frontend_seconds": round(unit["frontend_duration"], 3) if unit.get("frontend_duration") is not None else None,
"codegen_seconds": round(unit["codegen_duration"], 3) if unit.get("codegen_duration") is not None else None,
"features": unit.get("features") or [],
}
for unit in jcode_stack
],
},
}
if json_path:
json_path.parent.mkdir(parents=True, exist_ok=True)
json_path.write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n")
print("compile_time_probe summary")
print(f" timing html: {html_path}")
print(f" cargo timing wall: {fmt_seconds(summary['wall_seconds_from_cargo_timing'])}")
if summary["wall_seconds_measured_by_probe"] is not None:
print(f" measured wall: {fmt_seconds(summary['wall_seconds_measured_by_probe'])}")
print(f" units: {len(units)}")
print(" jcode serial stack:")
print(f" span: {fmt_seconds(stack_span)}")
print(f" sum: {stack_sum:.2f}s (frontend {stack_frontend_sum:.2f}s, codegen {stack_codegen_sum:.2f}s)")
for unit in jcode_stack:
target = unit.get("target") or "lib"
frontend = fmt_seconds(unit.get("frontend_duration"))
codegen = fmt_seconds(unit.get("codegen_duration"))
print(
f" - {unit.get('name')} {target}: "
f"start {float(unit.get('start', 0.0)):.2f}s, "
f"dur {float(unit.get('duration', 0.0)):.2f}s, "
f"frontend {frontend}, codegen {codegen}"
)
print(f" top {top_n} units:")
for unit in top_units:
target = unit.get("target") or "lib"
frontend = fmt_seconds(unit.get("frontend_duration"))
codegen = fmt_seconds(unit.get("codegen_duration"))
print(
f" - {unit.get('name')} {target}: "
f"{float(unit.get('duration', 0.0)):.2f}s "
f"(frontend {frontend}, codegen {codegen})"
)
if json_path:
print(f" wrote json: {json_path}")
PY
+48
View File
@@ -0,0 +1,48 @@
#!/bin/bash
# Test script to capture and analyze debug socket events
# Usage: ./scripts/debug_socket_test.sh [capture|compare]
DEBUG_SOCKET="${XDG_RUNTIME_DIR:-/tmp}/jcode-debug.sock"
CAPTURE_FILE="/tmp/jcode_debug_capture.jsonl"
case "${1:-capture}" in
capture)
echo "Connecting to debug socket: $DEBUG_SOCKET"
echo "Saving events to: $CAPTURE_FILE"
echo "Press Ctrl+C to stop"
echo "---"
nc -U "$DEBUG_SOCKET" | tee "$CAPTURE_FILE" | jq -c '.'
;;
snapshot)
echo "Getting state snapshot from debug socket..."
# Connect and get just the first message (snapshot)
timeout 1 nc -U "$DEBUG_SOCKET" | head -1 | jq '.'
;;
watch)
echo "Watching debug socket events (pretty print)..."
nc -U "$DEBUG_SOCKET" | jq '.'
;;
analyze)
if [ -f "$CAPTURE_FILE" ]; then
echo "Analyzing captured events..."
echo ""
echo "Event types:"
jq -r '.type' "$CAPTURE_FILE" | sort | uniq -c | sort -rn
echo ""
echo "Total events: $(wc -l < "$CAPTURE_FILE")"
else
echo "No capture file found. Run 'capture' first."
fi
;;
*)
echo "Usage: $0 [capture|snapshot|watch|analyze]"
echo " capture - Capture events to file and display"
echo " snapshot - Get initial state snapshot"
echo " watch - Watch events in real-time (pretty)"
echo " analyze - Analyze captured events"
;;
esac
+159
View File
@@ -0,0 +1,159 @@
#!/usr/bin/env python3
"""Golden-image regression checks for Jcode Desktop gallery captures.
Renders every gallery fixture state headlessly via
`jcode-desktop --capture-gallery-screens` and compares the PNGs against
checked-in baselines.
Usage:
scripts/desktop_gallery_golden.py check # compare against baselines
scripts/desktop_gallery_golden.py update # rewrite baselines
scripts/desktop_gallery_golden.py check --size 640x400
Exit codes: 0 ok, 1 regressions found, 2 setup/usage error.
A state regresses when more than --threshold fraction of pixels differ
by more than --tolerance per channel (defaults: 0.5% of pixels, 8/255).
Diff images for failing states are written next to the captures.
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
import tempfile
from pathlib import Path
try:
from PIL import Image, ImageChops
except ImportError:
print("error: pillow is required (pip install pillow)", file=sys.stderr)
sys.exit(2)
ROOT = Path(__file__).resolve().parent.parent
DEFAULT_BIN = ROOT / "target" / "debug" / "jcode-desktop"
DEFAULT_BASELINE_DIR = ROOT / "tests" / "desktop-gallery-golden"
def capture(binary: Path, out_dir: Path, size: str | None) -> list[dict]:
cmd = [str(binary), "--capture-gallery-screens", str(out_dir)]
if size:
cmd += ["--capture-size", size]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
if result.returncode != 0:
print(f"error: capture failed: {result.stderr.strip()}", file=sys.stderr)
sys.exit(2)
manifest = json.loads(result.stdout)
return manifest["screens"]
def compare(
baseline_path: Path, candidate_path: Path, tolerance: int, threshold: float
) -> tuple[bool, float, Image.Image | None]:
baseline = Image.open(baseline_path).convert("RGB")
candidate = Image.open(candidate_path).convert("RGB")
if baseline.size != candidate.size:
return False, 1.0, None
diff = ImageChops.difference(baseline, candidate)
# Count pixels where any channel differs by more than `tolerance`.
mask = diff.convert("L").point(lambda value: 255 if value > tolerance else 0)
histogram = mask.histogram()
differing = sum(histogram[1:])
total = baseline.size[0] * baseline.size[1]
fraction = differing / total
return fraction <= threshold, fraction, diff if fraction > threshold else None
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("mode", choices=["check", "update"])
parser.add_argument("--binary", type=Path, default=DEFAULT_BIN)
parser.add_argument("--baseline-dir", type=Path, default=DEFAULT_BASELINE_DIR)
parser.add_argument("--size", default=None, help="WxH render size override")
parser.add_argument("--tolerance", type=int, default=8, help="per-channel delta ignored")
parser.add_argument(
"--threshold",
type=float,
default=0.005,
help="max fraction of differing pixels before failing",
)
args = parser.parse_args()
if not args.binary.exists():
print(
f"error: desktop binary not found at {args.binary}; build with "
"`cargo build -p jcode-desktop --bin jcode-desktop`",
file=sys.stderr,
)
return 2
baseline_dir = args.baseline_dir
if args.size:
baseline_dir = baseline_dir / args.size
with tempfile.TemporaryDirectory(prefix="jcode-gallery-golden-") as tmp:
out_dir = Path(tmp)
screens = capture(args.binary, out_dir, args.size)
if args.mode == "update":
baseline_dir.mkdir(parents=True, exist_ok=True)
for screen in screens:
source = out_dir / screen["file"]
target = baseline_dir / screen["file"]
target.write_bytes(source.read_bytes())
print(f"updated {len(screens)} baselines in {baseline_dir}")
return 0
if not baseline_dir.exists():
print(
f"error: no baselines at {baseline_dir}; run "
f"`{sys.argv[0]} update` first",
file=sys.stderr,
)
return 2
failures = []
for screen in screens:
name = screen["file"]
baseline_path = baseline_dir / name
candidate_path = out_dir / name
if not baseline_path.exists():
failures.append((name, "missing baseline", None))
continue
ok, fraction, diff = compare(
baseline_path, candidate_path, args.tolerance, args.threshold
)
if ok:
print(f"ok {name} diff={fraction:.4%}")
else:
diff_dir = ROOT / "target" / "gallery-golden-diffs"
diff_dir.mkdir(parents=True, exist_ok=True)
candidate_copy = diff_dir / name
candidate_copy.write_bytes(candidate_path.read_bytes())
diff_path = None
if diff is not None:
diff_path = diff_dir / f"diff-{name}"
diff.save(diff_path)
failures.append((name, f"{fraction:.4%} pixels differ", diff_path))
print(f"FAIL {name} diff={fraction:.4%}")
if failures:
print(f"\n{len(failures)} state(s) regressed:")
for name, reason, diff_path in failures:
suffix = f" (diff: {diff_path})" if diff_path else ""
print(f" {name}: {reason}{suffix}")
print(
"\nIf the change is intentional, refresh with: "
f"{sys.argv[0]} update"
+ (f" --size {args.size}" if args.size else "")
)
return 1
print(f"\nall {len(screens)} gallery states match baselines")
return 0
if __name__ == "__main__":
sys.exit(main())
+195
View File
@@ -0,0 +1,195 @@
#!/usr/bin/env bash
set -euo pipefail
# Real-window E2E user-journey test for jcode-desktop under niri.
#
# Launches the desktop app in a real compositor window, replays scripted
# "user journeys" with wtype (typing, overlays, scrolling, resizing), and
# verifies after every step that:
# - the app process is still alive
# - the compositor window still exists
# At the end it summarizes no-paint gaps from the persistent performance log
# and fails if any gap during the journey exceeded the budget.
#
# Usage:
# scripts/desktop_journey_e2e.sh [journey ...]
# Journeys (default: all):
# typing type a draft, clear it
# overlays open/close hotkey help and model picker
# scrolling keyboard scroll up/down/top/bottom
# resize shrink and regrow the window via niri
#
# Env:
# JCODE_DESKTOP_BIN binary (default target/debug/jcode-desktop)
# JCODE_JOURNEY_TIMEOUT_SECS per-wait timeout (default 15)
# JCODE_JOURNEY_GAP_BUDGET_MS max acceptable no-paint gap (default 1000)
# JCODE_JOURNEY_SCREENSHOT_DIR if set, save a grim screenshot per step
#
# Requirements: niri, jq, wtype, a Wayland session; grim for screenshots.
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BIN="${JCODE_DESKTOP_BIN:-$ROOT_DIR/target/debug/jcode-desktop}"
TIMEOUT_SECS="${JCODE_JOURNEY_TIMEOUT_SECS:-15}"
GAP_BUDGET_MS="${JCODE_JOURNEY_GAP_BUDGET_MS:-1000}"
SCREENSHOT_DIR="${JCODE_JOURNEY_SCREENSHOT_DIR:-}"
LOG_FILE="$(mktemp -t jcode-desktop-journey.XXXXXX.log)"
PERF_LOG="${XDG_CACHE_HOME:-$HOME/.cache}/jcode/desktop/performance.log"
if [[ ! -x "$BIN" ]]; then
echo "desktop binary not found: $BIN" >&2
echo "hint: cargo build -p jcode-desktop --bin jcode-desktop" >&2
exit 2
fi
for tool in niri jq wtype; do
command -v "$tool" >/dev/null 2>&1 || { echo "missing tool: $tool" >&2; exit 2; }
done
if [[ -n "$SCREENSHOT_DIR" ]]; then
command -v grim >/dev/null 2>&1 || { echo "missing tool: grim (needed for screenshots)" >&2; exit 2; }
mkdir -p "$SCREENSHOT_DIR"
fi
JOURNEYS=("$@")
if [[ ${#JOURNEYS[@]} -eq 0 ]]; then
JOURNEYS=(typing overlays scrolling resize)
fi
APP_PID=""
WINDOW_ID=""
STEP_INDEX=0
FAILURES=()
cleanup() {
if [[ -n "$APP_PID" ]] && kill -0 "$APP_PID" 2>/dev/null; then
kill "$APP_PID" 2>/dev/null || true
wait "$APP_PID" 2>/dev/null || true
fi
}
trap cleanup EXIT
window_json() {
niri msg -j windows | jq -c --argjson pid "$APP_PID" 'map(select(.pid == $pid)) | first // empty'
}
assert_alive() {
local step="$1"
if ! kill -0 "$APP_PID" 2>/dev/null; then
echo "FAIL [$step]: app process died" >&2
tail -40 "$LOG_FILE" >&2 || true
FAILURES+=("$step: process died")
return 1
fi
if [[ -z "$(window_json)" ]]; then
echo "FAIL [$step]: compositor window vanished" >&2
FAILURES+=("$step: window vanished")
return 1
fi
return 0
}
step() {
local name="$1"
STEP_INDEX=$((STEP_INDEX + 1))
sleep 0.3
if ! assert_alive "$name"; then
return 1
fi
if [[ -n "$SCREENSHOT_DIR" ]]; then
local geom
geom="$(window_json | jq -r '"\(.layout.tile_pos_in_workspace_view[0]),\(.layout.tile_pos_in_workspace_view[1]) \(.layout.window_size[0])x\(.layout.window_size[1])"' 2>/dev/null || true)"
grim -g "$geom" "$SCREENSHOT_DIR/$(printf '%02d' "$STEP_INDEX")-$name.png" 2>/dev/null \
|| grim "$SCREENSHOT_DIR/$(printf '%02d' "$STEP_INDEX")-$name.png" 2>/dev/null || true
fi
echo "ok [$STEP_INDEX] $name"
}
journey_typing() {
wtype 'hello from the journey test'
step typing-draft || return 1
# clear the draft (Ctrl+U deletes to line start)
wtype -M ctrl u -m ctrl
step typing-clear || return 1
}
journey_overlays() {
# hotkey help: Ctrl+/ toggles, Escape closes
wtype -M ctrl '/' -m ctrl
step overlay-hotkey-help || return 1
wtype -k Escape
step overlay-hotkey-help-close || return 1
# model picker via slash command
wtype '/model'
wtype -k Return
step overlay-model-picker || return 1
wtype -k Escape
wtype -M ctrl u -m ctrl
step overlay-model-picker-close || return 1
}
journey_scrolling() {
for key in Page_Up Page_Up Page_Down End Home End; do
wtype -k "$key"
done
step scrolling-keys || return 1
}
journey_resize() {
niri msg action set-window-width --id "$WINDOW_ID" "50%" >/dev/null 2>&1 || true
step resize-narrow || return 1
niri msg action set-window-width --id "$WINDOW_ID" "100%" >/dev/null 2>&1 || true
step resize-restore || return 1
}
JOURNEY_STARTED_MS=$(( $(date +%s%N) / 1000000 ))
"$BIN" >"$LOG_FILE" 2>&1 &
APP_PID=$!
deadline=$((SECONDS + TIMEOUT_SECS))
WINDOW_JSON=""
while (( SECONDS < deadline )); do
kill -0 "$APP_PID" 2>/dev/null || { echo "app exited at startup" >&2; tail -40 "$LOG_FILE" >&2; exit 1; }
WINDOW_JSON="$(window_json)"
[[ -n "$WINDOW_JSON" ]] && break
sleep 0.1
done
[[ -n "$WINDOW_JSON" ]] || { echo "timed out waiting for window" >&2; exit 1; }
WINDOW_ID="$(jq -r '.id' <<<"$WINDOW_JSON")"
niri msg action focus-window --id "$WINDOW_ID" >/dev/null
sleep 0.5
step launch || true
for journey in "${JOURNEYS[@]}"; do
case "$journey" in
typing) journey_typing || true ;;
overlays) journey_overlays || true ;;
scrolling) journey_scrolling || true ;;
resize) journey_resize || true ;;
*) echo "unknown journey: $journey" >&2; exit 2 ;;
esac
done
cleanup
APP_PID=""
# Smoothness oracle: inspect no-paint gaps recorded during this run.
if [[ -f "$PERF_LOG" ]]; then
WORST_GAP="$(jq -s --argjson since "$JOURNEY_STARTED_MS" '
[ .[] | select(.timestamp_unix_ms? >= $since)
| select(.event? == "no_paint_gap" or (.gap_ms? != null))
| (.gap_ms? // .duration_ms? // 0) ] | max // 0
' "$PERF_LOG" 2>/dev/null || echo 0)"
echo "worst no-paint gap during journey: ${WORST_GAP}ms (budget ${GAP_BUDGET_MS}ms)"
if (( $(printf '%.0f' "$WORST_GAP") > GAP_BUDGET_MS )); then
FAILURES+=("smoothness: no-paint gap ${WORST_GAP}ms exceeded ${GAP_BUDGET_MS}ms")
fi
fi
if (( ${#FAILURES[@]} > 0 )); then
echo
echo "${#FAILURES[@]} failure(s):"
printf ' %s\n' "${FAILURES[@]}"
exit 1
fi
echo
echo "all journeys passed ($STEP_INDEX steps)"
+327
View File
@@ -0,0 +1,327 @@
#!/usr/bin/env python3
"""Summarize Jcode Desktop persistent performance logs.
The desktop app writes JSONL events to ~/.cache/jcode/desktop/performance.log.
This helper groups events by launch_id when available, reports the latest run by
default, and ranks the largest no-paint gaps, frame stalls, event queue delays,
and background disk work.
"""
from __future__ import annotations
import argparse
import collections
import datetime as _dt
import json
import os
from pathlib import Path
from typing import Any
DEFAULT_TOP = 8
def default_log_path() -> Path:
xdg = os.environ.get("XDG_CACHE_HOME")
if xdg:
return Path(xdg) / "jcode" / "desktop" / "performance.log"
return Path.home() / ".cache" / "jcode" / "desktop" / "performance.log"
def ms(value: Any) -> float:
if isinstance(value, (int, float)):
return float(value)
return 0.0
def event_ts(event: dict[str, Any]) -> int:
value = event.get("timestamp_unix_ms")
return int(value) if isinstance(value, int) else 0
def format_ts(timestamp_ms: int) -> str:
if timestamp_ms <= 0:
return "unknown-time"
return _dt.datetime.fromtimestamp(timestamp_ms / 1000, tz=_dt.UTC).isoformat(timespec="seconds")
def load_events(path: Path) -> tuple[list[dict[str, Any]], int]:
events: list[dict[str, Any]] = []
malformed = 0
if not path.exists():
return events, malformed
with path.open("r", encoding="utf-8", errors="replace") as handle:
for line in handle:
line = line.strip()
if not line:
continue
try:
event = json.loads(line)
except json.JSONDecodeError:
malformed += 1
continue
if isinstance(event, dict):
events.append(event)
else:
malformed += 1
return events, malformed
def latest_launch_id(events: list[dict[str, Any]]) -> str | None:
latest: tuple[int, str] | None = None
for event in events:
launch_id = event.get("launch_id")
if not isinstance(launch_id, str) or not launch_id:
continue
candidate = (event_ts(event), launch_id)
if latest is None or candidate[0] >= latest[0]:
latest = candidate
return latest[1] if latest else None
def select_events(
events: list[dict[str, Any]], *, launch_id: str | None, include_all: bool
) -> tuple[list[dict[str, Any]], str]:
if include_all:
return events, "all launches"
if launch_id:
return [event for event in events if event.get("launch_id") == launch_id], f"launch_id={launch_id}"
latest = latest_launch_id(events)
if latest:
return [event for event in events if event.get("launch_id") == latest], f"latest launch_id={latest}"
return events, "legacy events without launch_id"
def top_events(
events: list[dict[str, Any]], event_name: str, key_path: tuple[str, ...], limit: int
) -> list[dict[str, Any]]:
def key(event: dict[str, Any]) -> float:
value: Any = event
for key_part in key_path:
if not isinstance(value, dict):
return 0.0
value = value.get(key_part)
return ms(value)
return sorted(
(event for event in events if event.get("event") == event_name),
key=key,
reverse=True,
)[:limit]
def stage_summary(payload: dict[str, Any]) -> tuple[str, float]:
stages = payload.get("stages")
if not isinstance(stages, list):
return ("unknown", 0.0)
best_name = "unknown"
best_ms = 0.0
for stage in stages:
if not isinstance(stage, dict):
continue
stage_ms = ms(stage.get("ms"))
if stage_ms > best_ms:
best_ms = stage_ms
best_name = str(stage.get("name", "unknown"))
return best_name, best_ms
def summarize(events: list[dict[str, Any]], scope: str, malformed: int, top: int) -> dict[str, Any]:
counts = collections.Counter(str(event.get("event", "unknown")) for event in events)
timestamps = [event_ts(event) for event in events if event_ts(event) > 0]
launch_ids = sorted({event.get("launch_id") for event in events if isinstance(event.get("launch_id"), str)})
build_hashes = sorted({event.get("build_hash") for event in events if isinstance(event.get("build_hash"), str)})
pids = sorted({event.get("pid") for event in events if isinstance(event.get("pid"), int)})
no_paint = top_events(events, "jcode-desktop-no-paint-profile", ("payload", "gap_ms"), top)
frames = top_events(events, "jcode-desktop-frame-profile", ("payload", "worst_wall_ms"), top)
session_events = top_events(events, "jcode-desktop-session-event-profile", ("payload", "ui_queue_delay_ms"), top)
card_loads = top_events(events, "jcode-desktop-session-cards-load-profile", ("payload", "loaded_in_ms"), top)
single_card_loads = top_events(events, "jcode-desktop-session-card-refresh-profile", ("payload", "loaded_in_ms"), top)
preference_saves = top_events(events, "jcode-desktop-preferences-save-profile", ("payload", "saved_in_ms"), top)
restores = top_events(events, "jcode-desktop-crashed-sessions-restore-profile", ("payload", "elapsed_ms"), top)
worst_frame_details = []
for event in frames:
payload = event.get("payload") if isinstance(event.get("payload"), dict) else {}
stage_name, stage_ms = stage_summary(payload)
worst_frame_details.append(
{
"timestamp_unix_ms": event_ts(event),
"worst_wall_ms": ms(payload.get("worst_wall_ms")),
"worst_cpu_ms": ms(payload.get("worst_cpu_ms")),
"present_ms": ms(payload.get("present_ms")),
"queue_submit_ms": ms(payload.get("queue_submit_ms")),
"submit_present_ms": ms(payload.get("submit_present_ms")),
"dominant_stage": stage_name,
"dominant_stage_ms": stage_ms,
"mode": payload.get("mode"),
}
)
diagnoses: list[str] = []
if no_paint:
payload = no_paint[0].get("payload") if isinstance(no_paint[0].get("payload"), dict) else {}
gap = ms(payload.get("gap_ms"))
pending = payload.get("pending_interaction_kind")
background = payload.get("has_background_work")
diagnoses.append(
f"Largest no-paint gap was {gap:.1f} ms; pending={pending!r}, background_work={background}."
)
if worst_frame_details:
frame = worst_frame_details[0]
diagnoses.append(
f"Worst frame was {frame['worst_wall_ms']:.1f} ms; dominant stage {frame['dominant_stage']}={frame['dominant_stage_ms']:.1f} ms, present={frame['present_ms']:.1f} ms."
)
if frame["present_ms"] >= 33 or frame["dominant_stage"] in {"surface_acquire", "present", "queue_submit"}:
diagnoses.append("Likely GPU/window-system stall when frame/present dominates rather than app CPU.")
if session_events:
payload = session_events[0].get("payload") if isinstance(session_events[0].get("payload"), dict) else {}
queue_delay = ms(payload.get("ui_queue_delay_ms"))
apply_ms = ms(payload.get("apply_ms"))
if queue_delay >= 250:
diagnoses.append(
f"Session events waited {queue_delay:.1f} ms in the UI queue while apply cost was {apply_ms:.3f} ms, indicating event-loop starvation."
)
if card_loads or single_card_loads or preference_saves or restores:
slow_disk = []
if card_loads:
slow_disk.append(f"cards={ms(card_loads[0].get('payload', {}).get('loaded_in_ms')):.1f} ms")
if single_card_loads:
slow_disk.append(f"single_card={ms(single_card_loads[0].get('payload', {}).get('loaded_in_ms')):.1f} ms")
if preference_saves:
slow_disk.append(f"prefs={ms(preference_saves[0].get('payload', {}).get('saved_in_ms')):.1f} ms")
if restores:
slow_disk.append(f"restore={ms(restores[0].get('payload', {}).get('elapsed_ms')):.1f} ms")
diagnoses.append("Slow disk/process work was observed off the UI thread: " + ", ".join(slow_disk) + ".")
return {
"scope": scope,
"events": len(events),
"malformed_lines": malformed,
"launch_ids": launch_ids,
"build_hashes": build_hashes,
"pids": pids,
"start": min(timestamps) if timestamps else None,
"end": max(timestamps) if timestamps else None,
"event_counts": dict(counts.most_common()),
"top_no_paint": [
{
"timestamp_unix_ms": event_ts(event),
"gap_ms": ms(event.get("payload", {}).get("gap_ms")),
"pending_interaction_kind": event.get("payload", {}).get("pending_interaction_kind"),
"has_background_work": event.get("payload", {}).get("has_background_work"),
"pending_backend_redraw": event.get("payload", {}).get("pending_backend_redraw"),
"mode": event.get("payload", {}).get("mode"),
}
for event in no_paint
],
"top_frames": worst_frame_details,
"top_session_event_queue": [
{
"timestamp_unix_ms": event_ts(event),
"ui_queue_delay_ms": ms(event.get("payload", {}).get("ui_queue_delay_ms")),
"apply_ms": ms(event.get("payload", {}).get("apply_ms")),
"forwarder_accumulated_ms": ms(event.get("payload", {}).get("forwarder_accumulated_ms")),
"raw_events": event.get("payload", {}).get("raw_events"),
"text_delta_bytes": event.get("payload", {}).get("text_delta_bytes"),
}
for event in session_events
],
"top_background_disk": {
"session_cards": [event.get("payload", {}) for event in card_loads],
"single_card_refresh": [event.get("payload", {}) for event in single_card_loads],
"preferences": [event.get("payload", {}) for event in preference_saves],
"restore_crashed_sessions": [event.get("payload", {}) for event in restores],
},
"diagnosis": diagnoses,
}
def print_text(summary: dict[str, Any], path: Path) -> None:
print("Jcode Desktop performance report")
print(f"log: {path}")
print(f"scope: {summary['scope']}")
print(f"events: {summary['events']} malformed_lines: {summary['malformed_lines']}")
if summary["start"]:
print(f"range: {format_ts(summary['start'])} -> {format_ts(summary['end'])}")
if summary["launch_ids"]:
print(f"launch_ids: {', '.join(summary['launch_ids'][-3:])}")
if summary["build_hashes"]:
print(f"build_hashes: {', '.join(summary['build_hashes'][-3:])}")
if summary["pids"]:
print(f"pids: {', '.join(str(pid) for pid in summary['pids'][-5:])}")
print("\nevent counts:")
for event, count in summary["event_counts"].items():
print(f" {event}: {count}")
print("\ntop no-paint gaps:")
if not summary["top_no_paint"]:
print(" none")
for item in summary["top_no_paint"]:
print(
" {gap_ms:8.1f} ms {ts} pending={pending_interaction_kind!r} background={has_background_work} backend_redraw={pending_backend_redraw} mode={mode}".format(
ts=format_ts(item["timestamp_unix_ms"]), **item
)
)
print("\ntop frame stalls:")
if not summary["top_frames"]:
print(" none")
for item in summary["top_frames"]:
print(
" {worst_wall_ms:8.1f} ms wall cpu={worst_cpu_ms:7.1f} present={present_ms:7.1f} submit={queue_submit_ms:7.1f} dominant={dominant_stage}:{dominant_stage_ms:.1f} {ts}".format(
ts=format_ts(item["timestamp_unix_ms"]), **item
)
)
print("\ntop session event queue delays:")
if not summary["top_session_event_queue"]:
print(" none")
for item in summary["top_session_event_queue"]:
print(
" queue={ui_queue_delay_ms:8.1f} ms apply={apply_ms:7.3f} ms forwarder={forwarder_accumulated_ms:7.1f} ms raw={raw_events} bytes={text_delta_bytes} {ts}".format(
ts=format_ts(item["timestamp_unix_ms"]), **item
)
)
print("\nbackground disk/process work:")
disk = summary["top_background_disk"]
printed = False
for label, items in disk.items():
for payload in items[:3]:
printed = True
elapsed = payload.get("loaded_in_ms", payload.get("saved_in_ms", payload.get("elapsed_ms", 0.0)))
print(f" {label}: {ms(elapsed):8.1f} ms {payload}")
if not printed:
print(" none")
print("\ndiagnosis:")
if not summary["diagnosis"]:
print(" No slow profile events in this scope.")
for line in summary["diagnosis"]:
print(f" - {line}")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("log", nargs="?", type=Path, default=default_log_path())
parser.add_argument("--launch-id", help="summarize only one launch_id")
parser.add_argument("--all", action="store_true", help="summarize all launches instead of the latest launch")
parser.add_argument("--top", type=int, default=DEFAULT_TOP, help="number of rows per top list")
parser.add_argument("--json", action="store_true", help="emit machine-readable JSON")
args = parser.parse_args()
events, malformed = load_events(args.log)
selected, scope = select_events(events, launch_id=args.launch_id, include_all=args.all)
summary = summarize(selected, scope, malformed, max(1, args.top))
if args.json:
print(json.dumps(summary, indent=2, sort_keys=True))
else:
print_text(summary, args.log)
return 0 if summary["events"] else 1
if __name__ == "__main__":
raise SystemExit(main())
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env bash
set -euo pipefail
# End-to-end smoke test for desktop stable-host /reload behavior under niri.
#
# It launches jcode-desktop in stable-host mode, records the compositor window id
# and layout, injects `/reload`, then verifies the same OS window is still present
# with the same niri placement and the app-worker child process changed. This catches
# regressions where slash reload falls back to the old full-process handoff path
# that closes/reopens the desktop window.
#
# Requirements: niri, jq, wtype, a Wayland session, and a built jcode-desktop.
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BIN="${JCODE_DESKTOP_BIN:-$ROOT_DIR/target/debug/jcode-desktop}"
TIMEOUT_SECS="${JCODE_DESKTOP_RELOAD_E2E_TIMEOUT_SECS:-15}"
LOG_FILE="${JCODE_DESKTOP_RELOAD_E2E_LOG:-$(mktemp -t jcode-desktop-reload-e2e.XXXXXX.log)}"
if [[ ! -x "$BIN" ]]; then
echo "desktop binary not found or not executable: $BIN" >&2
echo "hint: cargo build -p jcode-desktop --bin jcode-desktop" >&2
exit 2
fi
for tool in niri jq wtype; do
if ! command -v "$tool" >/dev/null 2>&1; then
echo "required tool not found: $tool" >&2
exit 2
fi
done
cleanup() {
if [[ -n "${APP_PID:-}" ]] && kill -0 "$APP_PID" 2>/dev/null; then
kill "$APP_PID" 2>/dev/null || true
wait "$APP_PID" 2>/dev/null || true
fi
}
trap cleanup EXIT
child_pids() {
pgrep -P "$APP_PID" 2>/dev/null | sort | tr '\n' ' ' | sed 's/[[:space:]]*$//'
}
layouts_match_with_tolerance() {
local before="$1"
local after="$2"
jq -e -n --argjson before "$before" --argjson after "$after" '
def abs: if . < 0 then -. else . end;
def close($a; $b): (($a - $b) | abs) <= 2;
($before.id == $after.id)
and ($before.pid == $after.pid)
and ($before.workspace_id == $after.workspace_id)
and ($before.is_floating == $after.is_floating)
and ($before.layout.pos_in_scrolling_layout == $after.layout.pos_in_scrolling_layout)
and ($before.layout.tile_pos_in_workspace_view == $after.layout.tile_pos_in_workspace_view)
and ($before.layout.window_offset_in_tile == $after.layout.window_offset_in_tile)
and close($before.layout.tile_size[0]; $after.layout.tile_size[0])
and close($before.layout.tile_size[1]; $after.layout.tile_size[1])
and close($before.layout.window_size[0]; $after.layout.window_size[0])
and close($before.layout.window_size[1]; $after.layout.window_size[1])
' >/dev/null
}
"$BIN" \
--desktop-process-role stable-host \
--startup-log \
>"$LOG_FILE" 2>&1 &
APP_PID=$!
deadline=$((SECONDS + TIMEOUT_SECS))
WINDOW_JSON=""
while (( SECONDS < deadline )); do
if ! kill -0 "$APP_PID" 2>/dev/null; then
echo "desktop process exited before window appeared" >&2
cat "$LOG_FILE" >&2 || true
exit 1
fi
WINDOW_JSON="$(niri msg -j windows | jq -c --argjson pid "$APP_PID" 'map(select(.pid == $pid)) | first // empty')"
if [[ -n "$WINDOW_JSON" ]]; then
break
fi
sleep 0.1
done
if [[ -z "$WINDOW_JSON" ]]; then
echo "timed out waiting for desktop window for pid $APP_PID" >&2
cat "$LOG_FILE" >&2 || true
exit 1
fi
WINDOW_ID="$(jq -r '.id' <<<"$WINDOW_JSON")"
BEFORE_LAYOUT="$(jq -c '{id, pid, workspace_id, is_floating, layout}' <<<"$WINDOW_JSON")"
BEFORE_CHILDREN=""
deadline=$((SECONDS + TIMEOUT_SECS))
while (( SECONDS < deadline )); do
BEFORE_CHILDREN="$(child_pids)"
if [[ -n "$BEFORE_CHILDREN" ]]; then
break
fi
sleep 0.1
done
if [[ -z "$BEFORE_CHILDREN" ]]; then
echo "timed out waiting for app-worker child process" >&2
cat "$LOG_FILE" >&2 || true
exit 1
fi
niri msg action focus-window --id "$WINDOW_ID" >/dev/null
sleep 0.2
# Trigger the user-visible slash command path. In stable-host mode this should
# request a host-side app-worker restart, not a full desktop process handoff.
wtype '/reload'
wtype -k Return
# Wait for the reload request to be processed. The stable-host path should keep
# the same compositor window alive while restarting only the app worker.
deadline=$((SECONDS + TIMEOUT_SECS))
AFTER_CHILDREN=""
while (( SECONDS < deadline )); do
AFTER_CHILDREN="$(child_pids)"
if [[ -n "$AFTER_CHILDREN" && "$AFTER_CHILDREN" != "$BEFORE_CHILDREN" ]]; then
break
fi
sleep 0.1
done
if [[ -z "$AFTER_CHILDREN" || "$AFTER_CHILDREN" == "$BEFORE_CHILDREN" ]]; then
echo "app-worker child process did not change after hot reload trigger" >&2
echo "before children: $BEFORE_CHILDREN" >&2
echo "after children: $AFTER_CHILDREN" >&2
cat "$LOG_FILE" >&2 || true
exit 1
fi
AFTER_JSON="$(niri msg -j windows | jq -c --argjson id "$WINDOW_ID" 'map(select(.id == $id)) | first // empty')"
if [[ -z "$AFTER_JSON" ]]; then
echo "window id $WINDOW_ID disappeared after /reload" >&2
echo "before: $BEFORE_LAYOUT" >&2
cat "$LOG_FILE" >&2 || true
exit 1
fi
AFTER_LAYOUT="$(jq -c '{id, pid, workspace_id, is_floating, layout}' <<<"$AFTER_JSON")"
if ! layouts_match_with_tolerance "$BEFORE_LAYOUT" "$AFTER_LAYOUT"; then
echo "window layout changed after /reload" >&2
echo "before: $BEFORE_LAYOUT" >&2
echo "after: $AFTER_LAYOUT" >&2
cat "$LOG_FILE" >&2 || true
exit 1
fi
echo "desktop reload window e2e ok: window_id=$WINDOW_ID host_pid=$APP_PID before_worker='$BEFORE_CHILDREN' after_worker='$AFTER_CHILDREN' log=$LOG_FILE"
+302
View File
@@ -0,0 +1,302 @@
#!/usr/bin/env python3
"""Visual reporting tool for jcode-desktop debug frames.
Composes gallery frame captures into filmstrips, animated GIFs, and
before/after pixel-diff reports. Requires python3 + Pillow only.
Frame naming convention: sequences look like ``gallery-<state>-f000.png``,
``gallery-<state>-f001.png``, ... Single captures without the ``-fNNN``
suffix (e.g. ``gallery-empty.png``) are treated as one-frame sequences.
Usage examples:
# One filmstrip PNG: each state is a row of horizontally-composed frames.
scripts/desktop_visual_report.py filmstrip \
--frames-dir /tmp/desktop-vis/baseline --out /tmp/filmstrip.png
# Animated GIF for a single state's frame sequence.
scripts/desktop_visual_report.py gif \
--frames-dir /tmp/desktop-vis/frames --state streaming \
--out /tmp/streaming.gif --duration-ms 90
# Pixel diff every PNG shared by two directories.
scripts/desktop_visual_report.py diff \
--before /tmp/desktop-vis/baseline --after /tmp/desktop-vis/candidate \
--out-dir /tmp/desktop-vis/diff --threshold 8
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
try:
from PIL import Image, ImageChops, ImageDraw, ImageOps
except ImportError: # pragma: no cover - environment guard
sys.stderr.write("error: Pillow is required (pip install Pillow)\n")
sys.exit(2)
FRAME_RE = re.compile(r"^(?P<state>.+)-f(?P<idx>\d+)$")
# Percent of changed pixels above which a side-by-side composite is emitted.
COMPOSITE_PERCENT_THRESHOLD = 0.01
def collect_sequences(frames_dir: Path) -> dict[str, list[Path]]:
"""Group PNGs in ``frames_dir`` into named frame sequences.
Files matching ``<state>-fNNN.png`` are grouped under ``<state>`` and
ordered by frame index. Any other PNG becomes a single-frame sequence
keyed by its stem. Returns an insertion-ordered dict sorted by state name.
"""
sequences: dict[str, list[tuple[int, Path]]] = {}
for path in sorted(frames_dir.glob("*.png")):
match = FRAME_RE.match(path.stem)
if match:
state = match.group("state")
idx = int(match.group("idx"))
else:
state = path.stem
idx = 0
sequences.setdefault(state, []).append((idx, path))
return {
state: [path for _, path in sorted(frames)]
for state, frames in sorted(sequences.items())
}
def match_state(sequences: dict[str, list[Path]], state: str) -> list[Path] | None:
"""Find a sequence by exact state name, with a ``gallery-`` prefix fallback."""
for candidate in (state, f"gallery-{state}"):
if candidate in sequences:
return sequences[candidate]
return None
# ---------------------------------------------------------------------------
# filmstrip
# ---------------------------------------------------------------------------
def cmd_filmstrip(args: argparse.Namespace) -> int:
frames_dir = Path(args.frames_dir)
sequences = collect_sequences(frames_dir)
if not sequences:
sys.stderr.write(f"error: no PNG files found in {frames_dir}\n")
return 1
max_width = args.max_width
label_height = 18
pad = 4
rows: list[tuple[str, list[Image.Image]]] = []
for state, paths in sequences.items():
frames = [Image.open(p).convert("RGB") for p in paths]
total_w = sum(im.width for im in frames) + pad * (len(frames) - 1)
scale = min(1.0, (max_width - 2 * pad) / total_w) if total_w > 0 else 1.0
if scale < 1.0:
frames = [
im.resize(
(max(1, round(im.width * scale)), max(1, round(im.height * scale))),
Image.LANCZOS,
)
for im in frames
]
rows.append((state, frames))
strip_width = max(
sum(im.width for im in frames) + pad * (len(frames) - 1) + 2 * pad
for _, frames in rows
)
strip_height = sum(
label_height + max(im.height for im in frames) + 2 * pad for _, frames in rows
)
canvas = Image.new("RGB", (strip_width, strip_height), (24, 24, 28))
draw = ImageDraw.Draw(canvas)
y = 0
for state, frames in rows:
frame_note = f" ({len(frames)} frames)" if len(frames) > 1 else ""
draw.text((pad, y + 2), state + frame_note, fill=(220, 220, 220))
y += label_height
x = pad
row_h = max(im.height for im in frames)
for im in frames:
canvas.paste(im, (x, y + pad))
x += im.width + pad
y += row_h + 2 * pad
out = Path(args.out)
out.parent.mkdir(parents=True, exist_ok=True)
canvas.save(out)
print(
f"filmstrip: {len(rows)} state row(s), "
f"{sum(len(f) for _, f in rows)} frame(s) -> {out} "
f"({canvas.width}x{canvas.height})"
)
return 0
# ---------------------------------------------------------------------------
# gif
# ---------------------------------------------------------------------------
def cmd_gif(args: argparse.Namespace) -> int:
frames_dir = Path(args.frames_dir)
sequences = collect_sequences(frames_dir)
paths = match_state(sequences, args.state)
if paths is None:
available = ", ".join(sequences) or "<none>"
sys.stderr.write(
f"error: no frames for state '{args.state}' in {frames_dir}\n"
f"available states: {available}\n"
)
return 1
frames = [Image.open(p).convert("RGB") for p in paths]
out = Path(args.out)
out.parent.mkdir(parents=True, exist_ok=True)
frames[0].save(
out,
save_all=True,
append_images=frames[1:],
duration=args.duration_ms,
loop=0,
optimize=False,
)
print(
f"gif: state '{args.state}' {len(frames)} frame(s) "
f"@ {args.duration_ms}ms -> {out}"
)
return 0
# ---------------------------------------------------------------------------
# diff
# ---------------------------------------------------------------------------
def max_channel_diff(a: Image.Image, b: Image.Image) -> Image.Image:
"""Per-pixel max absolute channel difference of two RGB images (mode L)."""
diff = ImageChops.difference(a, b)
r, g, bl = diff.split()
return ImageChops.lighter(ImageChops.lighter(r, g), bl)
def cmd_diff(args: argparse.Namespace) -> int:
before_dir = Path(args.before)
after_dir = Path(args.after)
out_dir = Path(args.out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
threshold = args.threshold
before_names = {p.name for p in before_dir.glob("*.png")}
after_names = {p.name for p in after_dir.glob("*.png")}
shared = sorted(before_names & after_names)
if not shared:
sys.stderr.write("error: no PNG filenames shared between the two dirs\n")
return 1
rows: list[str] = []
composites = 0
for name in shared:
img_a = Image.open(before_dir / name).convert("RGB")
img_b = Image.open(after_dir / name).convert("RGB")
if img_a.size != img_b.size:
rows.append(
f"| {name} | - | - | size mismatch "
f"{img_a.width}x{img_a.height} vs {img_b.width}x{img_b.height}, "
f"pixel diff skipped |"
)
continue
gray = max_channel_diff(img_a, img_b)
mask = gray.point(lambda p: 255 if p > threshold else 0)
changed = mask.histogram()[255]
total = img_a.width * img_a.height
percent = 100.0 * changed / total if total else 0.0
note = ""
if percent > COMPOSITE_PERCENT_THRESHOLD:
composite_path = out_dir / f"{Path(name).stem}-diff.png"
heat = ImageOps.colorize(
ImageOps.autocontrast(gray), black=(0, 0, 0), white=(255, 32, 32)
)
gap = 8
composite = Image.new(
"RGB",
(img_a.width * 3 + gap * 2, img_a.height),
(24, 24, 28),
)
composite.paste(img_a, (0, 0))
composite.paste(img_b, (img_a.width + gap, 0))
composite.paste(heat, ((img_a.width + gap) * 2, 0))
composite.save(composite_path)
composites += 1
note = f"composite: {composite_path.name}"
rows.append(f"| {name} | {changed} | {percent:.4f}% | {note} |")
only_before = sorted(before_names - after_names)
only_after = sorted(after_names - before_names)
report = out_dir / "diff-report.md"
lines = [
"# Visual diff report",
"",
f"- before: `{before_dir}`",
f"- after: `{after_dir}`",
f"- per-channel threshold: {threshold}",
f"- composite emitted when changed pixels > {COMPOSITE_PERCENT_THRESHOLD}%",
"",
"| file | changed pixels | changed % | notes |",
"| --- | ---: | ---: | --- |",
*rows,
]
if only_before:
lines += ["", "Only in before: " + ", ".join(only_before)]
if only_after:
lines += ["", "Only in after: " + ", ".join(only_after)]
lines.append("")
report.write_text("\n".join(lines))
print(
f"diff: {len(shared)} file(s) compared, {composites} composite(s) "
f"-> {report}"
)
return 0
# ---------------------------------------------------------------------------
# main
# ---------------------------------------------------------------------------
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description="Filmstrip/GIF/diff reporting for jcode-desktop frame captures."
)
sub = parser.add_subparsers(dest="command", required=True)
p_strip = sub.add_parser("filmstrip", help="compose frames into one filmstrip PNG")
p_strip.add_argument("--frames-dir", required=True)
p_strip.add_argument("--out", required=True)
p_strip.add_argument("--max-width", type=int, default=1600)
p_strip.set_defaults(func=cmd_filmstrip)
p_gif = sub.add_parser("gif", help="assemble an animated GIF for one state")
p_gif.add_argument("--frames-dir", required=True)
p_gif.add_argument("--state", required=True)
p_gif.add_argument("--out", required=True)
p_gif.add_argument("--duration-ms", type=int, default=90)
p_gif.set_defaults(func=cmd_gif)
p_diff = sub.add_parser("diff", help="pixel-diff PNGs shared by two directories")
p_diff.add_argument("--before", required=True)
p_diff.add_argument("--after", required=True)
p_diff.add_argument("--out-dir", required=True)
p_diff.add_argument("--threshold", type=int, default=8)
p_diff.set_defaults(func=cmd_diff)
args = parser.parse_args(argv)
return args.func(args)
if __name__ == "__main__":
sys.exit(main())
+876
View File
@@ -0,0 +1,876 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
cd "$repo_root"
# shellcheck source=scripts/remote_config.sh
source "$repo_root/scripts/remote_config.sh"
jcode_load_remote_config
log() {
printf 'dev_cargo: %s\n' "$*" >&2
}
selected_linker_mode="not-configured"
selected_linker_desc=""
sccache_status="disabled"
selfdev_low_memory_status="disabled"
feature_profile_status="default"
build_jobs_status="cargo-default"
git_meta_status="not-configured"
append_rustflags() {
local new_flag="$1"
if [[ -z "${CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS:-}" ]]; then
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS="$new_flag"
else
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS="${CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS} ${new_flag}"
fi
}
selected_profile() {
# Print the Cargo profile selected by the args, defaulting to "dev" (cargo's
# default for build/check) when no --profile/--release is present.
local expect_profile_name="false"
local profile="dev"
for arg in "$@"; do
if [[ "$expect_profile_name" == "true" ]]; then
profile="$arg"
expect_profile_name="false"
continue
fi
case "$arg" in
--release|-r) profile="release" ;;
--profile=*) profile="${arg#--profile=}" ;;
--profile) expect_profile_name="true" ;;
esac
done
printf '%s\n' "$profile"
}
# Determine whether the effective build will use incremental compilation.
# sccache cannot cache incremental units, so this gates whether sccache is
# useful at all. CARGO_INCREMENTAL (if set) wins; otherwise infer from the
# profile's incremental setting in Cargo.toml.
build_is_incremental() {
case "${CARGO_INCREMENTAL:-}" in
0|false|no|off) return 1 ;;
1|true|yes|on) return 0 ;;
esac
case "$(selected_profile "$@")" in
# Non-incremental profiles (see Cargo.toml): sccache can produce hits here.
release-lto) return 1 ;;
# selfdev/dev/release/test and unknown profiles default to incremental.
*) return 0 ;;
esac
}
maybe_enable_sccache() {
case "${SCCACHE_DISABLE:-}" in
1|true|yes|on)
if [[ -n "${RUSTC_WRAPPER:-}" && "${RUSTC_WRAPPER}" == *sccache* ]]; then
unset RUSTC_WRAPPER
fi
sccache_status="disabled-by-env"
log "sccache disabled by SCCACHE_DISABLE"
return
;;
esac
if [[ -n "${RUSTC_WRAPPER:-}" ]]; then
sccache_status="external:${RUSTC_WRAPPER}"
log "keeping existing RUSTC_WRAPPER=${RUSTC_WRAPPER}"
return
fi
# sccache cannot cache incremental compilations, so for our default
# incremental profiles it produces 0% hits while adding wrapper overhead and
# misleading "enabled" status. Skip it for incremental builds unless the
# caller explicitly forces it via JCODE_SCCACHE=1/on/force.
local force_sccache="${JCODE_SCCACHE:-auto}"
case "$force_sccache" in
1|true|yes|on|force) force_sccache="1" ;;
0|false|no|off|never)
sccache_status="disabled-by-jcode-sccache"
log "sccache disabled by JCODE_SCCACHE"
return
;;
*) force_sccache="auto" ;;
esac
if [[ "$force_sccache" != "1" ]] && build_is_incremental "$@"; then
sccache_status="skipped-incremental"
log "sccache skipped for incremental build (it cannot cache incremental units; set JCODE_SCCACHE=on to force, or use a non-incremental profile)"
return
fi
if command -v sccache >/dev/null 2>&1; then
sccache --start-server >/dev/null 2>&1 || true
export RUSTC_WRAPPER=sccache
sccache_status="enabled"
log "using sccache"
else
sccache_status="not-found"
log "sccache not found; using direct rustc"
fi
}
uses_selfdev_profile() {
local expect_profile_name="false"
for arg in "$@"; do
if [[ "$expect_profile_name" == "true" ]]; then
[[ "$arg" == "selfdev" ]] && return 0
expect_profile_name="false"
continue
fi
case "$arg" in
--profile=selfdev)
return 0
;;
--profile)
expect_profile_name="true"
;;
esac
done
return 1
}
has_explicit_feature_args() {
local expect_value="false"
for arg in "$@"; do
if [[ "$expect_value" == "true" ]]; then
expect_value="false"
continue
fi
case "$arg" in
--)
return 1
;;
--features|--no-default-features)
return 0
;;
--features=*|--no-default-features=*)
return 0
;;
esac
done
return 1
}
feature_args_from_profile() {
local profile="$1"
case "$profile" in
""|default)
return 0
;;
minimal|none)
printf '%s\0' --no-default-features
;;
pdf)
printf '%s\0' --no-default-features --features pdf
;;
embeddings)
printf '%s\0' --no-default-features --features embeddings
;;
full)
printf '%s\0' --features embeddings,pdf,bedrock
;;
*)
return 1
;;
esac
}
validate_feature_profile() {
local profile="${JCODE_DEV_FEATURE_PROFILE:-default}"
case "$profile" in
""|default|minimal|none|pdf|embeddings|full)
;;
*)
printf 'error: unsupported JCODE_DEV_FEATURE_PROFILE=%s (expected default|minimal|pdf|embeddings|full)\n' "$profile" >&2
exit 1
;;
esac
}
build_cargo_argv() {
local profile="${JCODE_DEV_FEATURE_PROFILE:-default}"
if [[ "$profile" == "default" || -z "$profile" ]]; then
feature_profile_status="default"
printf '%s\0' "$@"
return 0
fi
if has_explicit_feature_args "$@"; then
feature_profile_status="ignored-explicit-cargo-args"
printf '%s\0' "$@"
return 0
fi
local -a feature_args=()
while IFS= read -r -d '' arg; do
feature_args+=("$arg")
done < <(feature_args_from_profile "$profile")
feature_profile_status="$profile"
local inserted="false"
for arg in "$@"; do
if [[ "$arg" == "--" && "$inserted" == "false" ]]; then
printf '%s\0' "${feature_args[@]}"
inserted="true"
fi
printf '%s\0' "$arg"
done
if [[ "$inserted" == "false" ]]; then
printf '%s\0' "${feature_args[@]}"
fi
}
meminfo_kib() {
local key="$1"
awk -v key="$key" '$1 == key ":" { print $2; exit }' /proc/meminfo 2>/dev/null || true
}
selfdev_low_memory_default_needed() {
[[ "$(uname -s)" == "Linux" ]] || return 1
[[ -r /proc/meminfo ]] || return 1
command -v pgrep >/dev/null 2>&1 || return 1
pgrep -x earlyoom >/dev/null 2>&1 || return 1
local mem_total_kib mem_available_kib swap_total_kib
mem_total_kib=$(meminfo_kib MemTotal)
mem_available_kib=$(meminfo_kib MemAvailable)
swap_total_kib=$(meminfo_kib SwapTotal)
[[ -n "$mem_total_kib" && -n "$mem_available_kib" && -n "$swap_total_kib" ]] || return 1
# On small no-swap machines, earlyoom can terminate the root jcode rustc
# around 1-3 GiB RSS before the kernel OOM killer would report anything.
# Keep this adaptive so larger workstations, and currently-idle smaller
# workstations with enough headroom, retain the faster inherited selfdev
# profile by default.
(( swap_total_kib == 0 && mem_total_kib < 24576 * 1024 && mem_available_kib < 8192 * 1024 ))
}
cpu_count() {
local n
n=$(nproc 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1)
[[ "$n" =~ ^[0-9]+$ && "$n" -ge 1 ]] || n=1
printf '%s\n' "$n"
}
# Choose a Cargo job count from *currently available* memory so concurrent
# builds (e.g. several self-dev agents on one machine) self-throttle instead of
# all assuming the full core count and tripping earlyoom/OOM.
#
# After the monolith was split into the jcode-base/app-core/tui/cli crate DAG,
# the largest single rustc unit is jcode-base, which has grown back to a
# measured ~1.6 GiB RSS peak (selfdev profile, sampled VmRSS while building the
# lib), down from the old 2.5-3 GiB monolith but above the original ~1.28 GiB
# post-split figure. We budget ~1.75 GiB of currently-available memory per job:
# a deliberate cushion above the measured peak (rustc's true VmHWM can exceed a
# coarse sample, and jcode-base keeps growing) so a fresh build under load backs
# off before earlyoom SIGTERMs it. Clamp into [1, cpus]. On an idle 15 GiB
# machine this still uses ~7 of 8 cores; under memory pressure a fresh build
# backs off further. An explicit CARGO_BUILD_JOBS / JCODE_BUILD_JOBS always
# wins, and non-Linux hosts fall back to the cargo/.cargo default.
select_build_jobs() {
# Respect an explicit override from either env var.
local override="${JCODE_BUILD_JOBS:-${CARGO_BUILD_JOBS:-}}"
if [[ -n "$override" ]]; then
if [[ "$override" =~ ^[0-9]+$ && "$override" -ge 1 ]]; then
export CARGO_BUILD_JOBS="$override"
build_jobs_status="override:$override"
return
fi
# Invalid override: warn and fall through to adaptive sizing.
log "ignoring invalid job override '$override' (expected a positive integer); using adaptive sizing"
unset CARGO_BUILD_JOBS
fi
# Adaptive sizing only on Linux where /proc/meminfo is available; elsewhere we
# leave cargo to honor the .cargo/config.toml default.
if [[ "$(uname -s)" != "Linux" || ! -r /proc/meminfo ]]; then
build_jobs_status="cargo-default"
return
fi
local cpus mem_available_kib mib_per_job_default mib_per_job
cpus=$(cpu_count)
mem_available_kib=$(meminfo_kib MemAvailable)
[[ -n "$mem_available_kib" && "$mem_available_kib" =~ ^[0-9]+$ ]] || mem_available_kib=0
# Per-job memory budget (MiB). Sized with a cushion above the largest measured
# rustc unit (jcode-base, ~1.6 GiB RSS sampled) so an idle machine uses nearly
# every core while a memory-pressured one backs off before earlyoom kills a
# build. Tunable per host via JCODE_BUILD_MIB_PER_JOB.
mib_per_job_default=1792
mib_per_job="${JCODE_BUILD_MIB_PER_JOB:-$mib_per_job_default}"
[[ "$mib_per_job" =~ ^[0-9]+$ && "$mib_per_job" -ge 256 ]] || mib_per_job="$mib_per_job_default"
local mem_available_mib jobs_by_mem jobs
mem_available_mib=$(( mem_available_kib / 1024 ))
jobs_by_mem=$(( mem_available_mib / mib_per_job ))
(( jobs_by_mem < 1 )) && jobs_by_mem=1
# Final job count is the smaller of "fits in memory" and core count.
jobs="$jobs_by_mem"
(( jobs > cpus )) && jobs="$cpus"
(( jobs < 1 )) && jobs=1
export CARGO_BUILD_JOBS="$jobs"
build_jobs_status="adaptive:${jobs} (cpus=${cpus}, mem_avail=${mem_available_mib}MiB, budget=${mib_per_job}MiB/job)"
if (( jobs < cpus )); then
log "limiting cargo to ${jobs} job(s) under memory pressure (${mem_available_mib}MiB available, ~${mib_per_job}MiB/job); override with JCODE_BUILD_JOBS"
fi
}
# Keep `jcode-build-meta`'s embedded git metadata in lockstep with HEAD without
# making it watch `.git` mtimes.
#
# `jcode-build-meta/build.rs` deliberately does NOT declare `.git/HEAD`/`.git/index`
# as `rerun-if-changed` inputs, because their mtimes change on every `git add`,
# `git status`, commit, and concurrent-agent git op -- which would force a
# full-tree recompile (base -> app-core -> tui -> cli) on every incremental
# build. The trade-off was that after a commit the binary kept embedding the
# previous short hash, so the self-dev publish guard rejected it with
# "binary was built from git hash X, but source state is Y" until someone
# manually touched Cargo.toml.
#
# Instead, export the *value* of the current git hash/date. build.rs declares
# `cargo:rerun-if-env-changed=JCODE_BUILD_GIT_HASH` (and _DATE), so cargo reruns
# the build script ONLY when these values change -- i.e. exactly when HEAD moves
# -- and never on a bare `git add`/`status` or repeated builds on the same
# commit. This keeps the embedded hash correct after every commit while keeping
# same-commit incremental builds fully incremental. We intentionally do NOT
# export JCODE_BUILD_GIT_DIRTY here: the dirty flag flips on every edit and would
# reintroduce per-build churn; the publish guard validates dirty builds via the
# source fingerprint / mtime path instead of the embedded flag.
export_git_build_metadata() {
# Respect any value the caller already set (e.g. release/CI pipelines).
if [[ -n "${JCODE_BUILD_GIT_HASH:-}" ]]; then
git_meta_status="external:${JCODE_BUILD_GIT_HASH}"
return
fi
if ! command -v git >/dev/null 2>&1; then
git_meta_status="git-not-found"
return
fi
local hash date
hash="$(git -C "$repo_root" rev-parse --short HEAD 2>/dev/null || true)"
if [[ -z "$hash" ]]; then
git_meta_status="no-head"
return
fi
export JCODE_BUILD_GIT_HASH="$hash"
date="$(git -C "$repo_root" log -1 --format=%ci 2>/dev/null || true)"
if [[ -n "$date" ]]; then
export JCODE_BUILD_GIT_DATE="$date"
fi
git_meta_status="hash=${hash}"
}
maybe_configure_low_memory_selfdev() {
if ! uses_selfdev_profile "$@"; then
selfdev_low_memory_status="not-selfdev"
return
fi
local mode="${JCODE_SELFDEV_LOW_MEMORY:-auto}"
case "$mode" in
1|true|yes|on|force)
;;
0|false|no|off|never)
selfdev_low_memory_status="disabled-by-env"
return
;;
auto|"")
if ! selfdev_low_memory_default_needed; then
selfdev_low_memory_status="auto-not-needed"
return
fi
;;
*)
printf 'error: unsupported JCODE_SELFDEV_LOW_MEMORY=%s (expected auto|on|off)\n' "$mode" >&2
exit 1
;;
esac
export CARGO_INCREMENTAL="${CARGO_INCREMENTAL:-1}"
export CARGO_PROFILE_SELFDEV_INCREMENTAL="${CARGO_PROFILE_SELFDEV_INCREMENTAL:-true}"
export CARGO_PROFILE_SELFDEV_CODEGEN_UNITS="${CARGO_PROFILE_SELFDEV_CODEGEN_UNITS:-256}"
# The low-memory profile deliberately keeps incremental compilation enabled
# to avoid repeatedly rebuilding large root crates. sccache rejects Cargo
# incremental builds, so default to direct rustc unless the caller opts out
# of low-memory mode entirely.
export SCCACHE_DISABLE="${SCCACHE_DISABLE:-1}"
selfdev_low_memory_status="enabled:incremental=${CARGO_PROFILE_SELFDEV_INCREMENTAL},codegen-units=${CARGO_PROFILE_SELFDEV_CODEGEN_UNITS}"
log "using low-memory selfdev overrides (${selfdev_low_memory_status#enabled:})"
}
# Enable rustc's parallel front-end (`-Zthreads`) for iterative dev/selfdev/test
# builds. The jcode monoliths (jcode-base/app-core/tui) are ~80% single-threaded
# front-end (type-check + borrow-check + monomorphization collection); at
# opt-level 0 that front-end, not codegen, dominates wall time. The parallel
# front-end is a nightly-only `-Z` flag, so this is gated on a nightly toolchain
# being available and only applies to the unoptimized iteration profiles.
#
# Measured on this repo (Intel Ultra 7, 8 logical cores, selfdev profile):
# jcode-base clean recompile 25.3s -> 12.7s (-Zthreads=4)
# base-edit full-chain rebuild ~16s -> ~10s
# Cranelift was tried too and was *slower* here (16.5s) because the bottleneck is
# the front-end, not codegen, so we deliberately do not enable it.
#
# Controls:
# JCODE_PARALLEL_FRONTEND=auto|0|1 (default auto)
# JCODE_FRONTEND_THREADS=<n> (default 4; diminishing returns past 4)
# JCODE_DEV_TOOLCHAIN=<name> (default: nightly when present)
parallel_frontend_status="disabled"
parallel_frontend_toolchain=""
dev_nightly_toolchain() {
# Prefer an explicit override, else a `+toolchain` already on the argv, else
# the first installed nightly toolchain.
if [[ -n "${JCODE_DEV_TOOLCHAIN:-}" ]]; then
printf '%s\n' "$JCODE_DEV_TOOLCHAIN"
return 0
fi
local tc
tc=$(rustup toolchain list 2>/dev/null | awk '/^nightly/ {print $1; exit}')
tc=${tc%% *}
[[ -n "$tc" ]] && printf '%s\n' "$tc"
return 0
}
configure_parallel_frontend() {
local requested="${JCODE_PARALLEL_FRONTEND:-auto}"
local forced="false"
case "$requested" in
0|false|no|off)
parallel_frontend_status="disabled-by-env"
return 0
;;
1|true|yes|on|force) forced="true" ;;
auto) ;;
*)
parallel_frontend_status="disabled-bad-env:${requested}"
return 0
;;
esac
# Only worth it for the unoptimized iteration profiles where the front-end is
# the bottleneck; release/release-lto keep their own (codegen-bound) path.
#
# By default (`auto`) we restrict to the `selfdev` profile: it builds into the
# isolated `target/selfdev` dir that only this script + `selfdev build` use, so
# adding `-Zthreads` to RUSTFLAGS (which changes cargo's unit fingerprint)
# cannot thrash rust-analyzer's `target/debug` cache. Forcing the flag on
# (`JCODE_PARALLEL_FRONTEND=1`) opts dev/test in too, accepting that potential
# cache contention.
local profile
profile=$(selected_profile "$@")
case "$profile" in
selfdev) ;;
dev|test)
if [[ "$forced" != "true" ]]; then
parallel_frontend_status="skipped-profile-shared-target:${profile}"
return 0
fi
;;
*)
parallel_frontend_status="skipped-profile:${profile}"
return 0
;;
esac
# If the caller already pinned a toolchain via `cargo +foo`, don't override it.
for arg in "$@"; do
case "$arg" in
+*)
parallel_frontend_status="skipped-explicit-toolchain:${arg}"
return 0
;;
esac
done
command -v rustup >/dev/null 2>&1 || {
parallel_frontend_status="skipped-no-rustup"
return 0
}
local tc
tc=$(dev_nightly_toolchain)
if [[ -z "$tc" ]]; then
parallel_frontend_status="skipped-no-nightly"
return 0
fi
# Confirm the toolchain actually resolves (installed, not just configured).
if ! rustup run "$tc" rustc --version >/dev/null 2>&1; then
parallel_frontend_status="skipped-nightly-unavailable:${tc}"
return 0
fi
local threads="${JCODE_FRONTEND_THREADS:-4}"
[[ "$threads" =~ ^[0-9]+$ && "$threads" -ge 1 ]] || threads=4
parallel_frontend_toolchain="$tc"
export RUSTUP_TOOLCHAIN="$tc"
append_rustflags "-Zthreads=${threads}"
parallel_frontend_status="enabled:${tc}:threads=${threads}"
log "using parallel rustc front-end (${tc}, -Zthreads=${threads})"
}
configure_linux_linker() {
local requested_mode="${JCODE_FAST_LINKER:-auto}"
local mode="$requested_mode"
case "$mode" in
auto)
# Prefer mold over lld: on this repo's large statically-linked binary
# (~300 MB .text), mold links the jcode bin in ~2.0s vs lld's ~2.9s
# (measured, warm, selfdev profile). The bin relinks on every build, so
# that ~0.8s is a per-build win. Both need clang as the linker driver.
if command -v mold >/dev/null 2>&1 && command -v clang >/dev/null 2>&1; then
mode="mold"
elif command -v ld.lld >/dev/null 2>&1 && command -v clang >/dev/null 2>&1; then
mode="lld"
else
mode="system"
fi
;;
lld|mold|system)
;;
*)
printf 'error: unsupported JCODE_FAST_LINKER=%s (expected auto|lld|mold|system)\n' "$mode" >&2
exit 1
;;
esac
selected_linker_mode="$mode"
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER="${CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER:-clang}"
case "$mode" in
lld)
append_rustflags "-C link-arg=-fuse-ld=lld"
selected_linker_desc="clang + lld"
log "using clang + lld"
;;
mold)
append_rustflags "-C link-arg=-fuse-ld=mold"
selected_linker_desc="clang + mold"
log "using clang + mold"
;;
system)
selected_linker_desc="system linker settings"
if [[ "$requested_mode" == "auto" ]]; then
log "no supported fast linker detected; using system linker settings"
else
log "using system linker settings"
fi
;;
esac
}
print_setup() {
if [[ -n "${JCODE_DEV_FEATURE_PROFILE:-}" && "${JCODE_DEV_FEATURE_PROFILE}" != "default" ]]; then
feature_profile_status="${JCODE_DEV_FEATURE_PROFILE}"
fi
cat <<EOF
repo_root=$repo_root
os=$(uname -s)
arch=$(uname -m)
sccache_status=$sccache_status
selfdev_low_memory_status=$selfdev_low_memory_status
parallel_frontend_status=$parallel_frontend_status
build_jobs_status=$build_jobs_status
cargo_build_jobs=${CARGO_BUILD_JOBS:-<unset>}
feature_profile_status=$feature_profile_status
git_meta_status=$git_meta_status
build_git_hash=${JCODE_BUILD_GIT_HASH:-<unset>}
rustc_wrapper=${RUSTC_WRAPPER:-<unset>}
linker_mode=$selected_linker_mode
linker_desc=${selected_linker_desc:-<none>}
linker=${CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER:-<unset>}
rustflags=${CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS:-<unset>}
EOF
}
remote_connect_timeout() {
local value="${JCODE_REMOTE_CONNECT_TIMEOUT:-5}"
if [[ ! "$value" =~ ^[0-9]+$ || "$value" -lt 1 ]]; then
value=5
fi
printf '%s\n' "$value"
}
remote_tcp_timeout() {
# Bounded probe used the first time we contact a host (or after the recovery
# window expires) so an unreachable host fails fast instead of waiting for
# the full SSH ConnectTimeout. Accepts fractional seconds (GNU timeout).
local value="${JCODE_REMOTE_TCP_TIMEOUT:-1}"
if [[ ! "$value" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
value=1
fi
printf '%s\n' "$value"
}
remote_recovery_tcp_timeout() {
# Shorter probe used while a host was recently seen down. An up host always
# answers in a few ms, so this still detects recovery on the next build while
# keeping per-build cost low during an outage.
local value="${JCODE_REMOTE_RECOVERY_TCP_TIMEOUT:-0.3}"
if [[ ! "$value" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
value=0.3
fi
printf '%s\n' "$value"
}
remote_down_cache_ttl() {
# How long (seconds) a recent failure keeps using the shorter recovery probe
# timeout. Recovery is still detected on the very next build; this only
# controls how long downtime builds stay cheap before reverting to the full
# probe timeout. Set to 0 to always use the full timeout.
local value="${JCODE_REMOTE_DOWN_TTL:-300}"
if [[ ! "$value" =~ ^[0-9]+$ ]]; then
value=300
fi
printf '%s\n' "$value"
}
remote_down_cache_path() {
local remote="${JCODE_REMOTE_HOST:-}"
local key
if command -v cksum >/dev/null 2>&1; then
key="$(printf '%s' "$remote" | cksum | awk '{print $1}')"
else
key="${remote//[^A-Za-z0-9_-]/_}"
fi
printf '%s/jcode-remote-down-%s\n' "${TMPDIR:-/tmp}" "$key"
}
remote_down_cache_fresh() {
local ttl
ttl="$(remote_down_cache_ttl)"
[[ "$ttl" -gt 0 ]] || return 1
local cache
cache="$(remote_down_cache_path)"
[[ -f "$cache" ]] || return 1
local recorded now age
recorded="$(cat "$cache" 2>/dev/null || printf '0')"
[[ "$recorded" =~ ^[0-9]+$ ]] || return 1
now="$(date +%s)"
age=$((now - recorded))
(( age >= 0 && age < ttl ))
}
record_remote_down() {
local ttl
ttl="$(remote_down_cache_ttl)"
[[ "$ttl" -gt 0 ]] || return 0
local cache
cache="$(remote_down_cache_path)"
date +%s >"$cache" 2>/dev/null || true
}
clear_remote_down() {
local cache
cache="$(remote_down_cache_path)"
rm -f "$cache" 2>/dev/null || true
}
# Resolve the effective hostname/port for the remote alias and report whether
# the connection goes through a ProxyJump/ProxyCommand (where a direct TCP
# probe to the final hostname would be misleading).
remote_resolve_endpoint() {
local ssh_bin="$1" remote="$2"
local hostname="$remote" port=22 uses_proxy="false"
local config line key value
if config="$("$ssh_bin" -G "$remote" 2>/dev/null)"; then
while IFS=' ' read -r key value; do
case "$key" in
hostname) [[ -n "$value" ]] && hostname="$value" ;;
port) [[ "$value" =~ ^[0-9]+$ ]] && port="$value" ;;
proxyjump|proxycommand)
[[ -n "$value" && "$value" != "none" ]] && uses_proxy="true"
;;
esac
done <<<"$config"
fi
printf '%s\t%s\t%s\n' "$hostname" "$port" "$uses_proxy"
}
# Fast TCP reachability check using bash /dev/tcp with a hard timeout.
remote_tcp_reachable() {
local hostname="$1" port="$2"
local tcp_timeout="${3:-}"
if [[ -z "$tcp_timeout" ]]; then
tcp_timeout="$(remote_tcp_timeout)"
fi
timeout "$tcp_timeout" bash -c "exec 3<>/dev/tcp/$hostname/$port" 2>/dev/null
}
remote_cargo_preflight() {
local remote="${JCODE_REMOTE_HOST:-}"
if [[ -z "$remote" ]]; then
log "remote cargo requested but JCODE_REMOTE_HOST is not configured"
return 1
fi
local ssh_bin="${JCODE_REMOTE_SSH_BIN:-ssh}"
if ! command -v "$ssh_bin" >/dev/null 2>&1; then
log "remote cargo requested but ssh binary is unavailable: $ssh_bin"
return 1
fi
# Choose probe timeout: while a host was recently seen down, use a short
# recovery timeout so downtime builds stay cheap. An up host answers in a few
# ms regardless, so recovery is still detected on the very next build.
local tcp_timeout
if remote_down_cache_fresh; then
tcp_timeout="$(remote_recovery_tcp_timeout)"
else
tcp_timeout="$(remote_tcp_timeout)"
fi
# Fast TCP pre-probe to fail fast when the host is offline, unless the
# connection is proxied (where a direct probe would be wrong) or explicitly
# disabled via JCODE_REMOTE_TCP_PROBE=0.
local tcp_probe="${JCODE_REMOTE_TCP_PROBE:-1}"
case "$tcp_probe" in
0|false|no|off) tcp_probe="0" ;;
*) tcp_probe="1" ;;
esac
if [[ "$tcp_probe" == "1" ]]; then
local endpoint hostname port uses_proxy
endpoint="$(remote_resolve_endpoint "$ssh_bin" "$remote")"
IFS=$'\t' read -r hostname port uses_proxy <<<"$endpoint"
if [[ "$uses_proxy" != "true" ]]; then
if ! remote_tcp_reachable "$hostname" "$port" "$tcp_timeout"; then
log "remote host $remote ($hostname:$port) unreachable within ${tcp_timeout}s TCP probe; using local cargo"
record_remote_down
return 1
fi
fi
fi
local connect_timeout
connect_timeout="$(remote_connect_timeout)"
local server_alive_interval="${JCODE_REMOTE_SERVER_ALIVE_INTERVAL:-10}"
local server_alive_count="${JCODE_REMOTE_SERVER_ALIVE_COUNT_MAX:-1}"
local output
if ! output=$("$ssh_bin" \
-o BatchMode=yes \
-o ConnectTimeout="$connect_timeout" \
-o ServerAliveInterval="$server_alive_interval" \
-o ServerAliveCountMax="$server_alive_count" \
"$remote" "printf 'jcode-remote-ok\\n'" 2>&1); then
log "remote cargo preflight failed for $remote after ~${connect_timeout}s: $output"
record_remote_down
return 1
fi
clear_remote_down
return 0
}
remote_cargo_fallback_mode() {
local mode="${JCODE_REMOTE_CARGO_FALLBACK:-local}"
case "$mode" in
local|1|true|yes|on)
printf 'local\n'
;;
error|fail|0|false|no|off|never)
printf 'error\n'
;;
*)
printf 'error: unsupported JCODE_REMOTE_CARGO_FALLBACK=%s (expected local|error)\n' "$mode" >&2
exit 2
;;
esac
}
cargo_test_has_explicit_filter() {
[[ "${1:-}" == "test" ]] || return 1
local expect_value=""
shift
for arg in "$@"; do
if [[ -n "$expect_value" ]]; then
expect_value=""
continue
fi
case "$arg" in
--)
return 1
;;
--bench|--bin|--example|--features|--manifest-path|--message-format|--package|-p|--profile|--target|--target-dir)
expect_value="$arg"
;;
--bench=*|--bin=*|--example=*|--features=*|--manifest-path=*|--message-format=*|--package=*|-p=*|--profile=*|--target=*|--target-dir=*)
;;
--*)
;;
-*)
;;
*)
return 0
;;
esac
done
return 1
}
run_local_cargo() {
if cargo_test_has_explicit_filter "${cargo_argv[@]}" && [[ "${JCODE_DEV_CARGO_ALLOW_ZERO_TESTS:-0}" != "1" ]]; then
local output_file
output_file=$(mktemp "${TMPDIR:-/tmp}/jcode-dev-cargo.XXXXXX")
local status=0
cargo "${cargo_argv[@]}" 2>&1 | tee "$output_file" || status=${PIPESTATUS[0]}
if [[ "$status" -eq 0 ]] \
&& grep -qE '^running 0 tests$' "$output_file" \
&& ! grep -qE '^running [1-9][0-9]* tests$' "$output_file"; then
printf 'dev_cargo: explicit cargo test filter matched zero tests; check the test path/name or set JCODE_DEV_CARGO_ALLOW_ZERO_TESTS=1 to allow this intentionally\n' >&2
rm -f "$output_file"
return 97
fi
rm -f "$output_file"
return "$status"
fi
exec cargo "${cargo_argv[@]}"
}
validate_feature_profile
export_git_build_metadata
maybe_configure_low_memory_selfdev "$@"
maybe_enable_sccache "$@"
configure_parallel_frontend "$@"
select_build_jobs
if [[ "$(uname -s)" == "Linux" ]] && [[ "$(uname -m)" == "x86_64" ]]; then
configure_linux_linker
fi
if [[ "${1:-}" == "--print-setup" ]]; then
print_setup
exit 0
fi
cargo_argv=()
while IFS= read -r -d '' arg; do
cargo_argv+=("$arg")
done < <(build_cargo_argv "$@")
if [[ "${JCODE_REMOTE_CARGO:-0}" == "1" ]]; then
if remote_cargo_preflight; then
log "using remote cargo via scripts/remote_build.sh"
exec "$repo_root/scripts/remote_build.sh" "${cargo_argv[@]}"
fi
if [[ "$(remote_cargo_fallback_mode)" == "local" ]]; then
log "remote cargo unavailable; falling back to local cargo (set JCODE_REMOTE_CARGO_FALLBACK=error to fail instead)"
else
log "remote cargo unavailable and fallback disabled"
exit 75
fi
fi
run_local_cargo
+159
View File
@@ -0,0 +1,159 @@
#!/usr/bin/env bash
set -euo pipefail
# Generate a human-readable release notes body for a jcode release (issue #435).
#
# Usage:
# scripts/generate_release_notes.sh <tag> [previous-tag]
#
# Behavior:
# 1. If changelog/v<version>.json exists (schema in changelog/README.md),
# render it into Highlights / Improvements / Fixes markdown sections.
# 2. Otherwise, group commit subjects between the previous tag and <tag>
# by conventional-commit prefix (feat/fix/perf/docs/other).
# Always appends the GitHub compare link at the bottom.
TAG="${1:?Usage: scripts/generate_release_notes.sh <tag> [previous-tag]}"
PREV_TAG="${2:-}"
cd "$(git rev-parse --show-toplevel)"
VERSION_NUM="${TAG#v}"
CHANGELOG_FILE="changelog/v${VERSION_NUM}.json"
# Determine the owner/repo slug for the compare link.
repo_slug() {
if [[ -n "${GITHUB_REPOSITORY:-}" ]]; then
echo "$GITHUB_REPOSITORY"
return
fi
local url
url="$(git remote get-url origin 2>/dev/null || true)"
if [[ "$url" =~ github\.com[:/]+([^/]+/[^/ ]+) ]]; then
echo "${BASH_REMATCH[1]%.git}"
return
fi
echo "1jehuang/jcode"
}
REPO="$(repo_slug)"
# Determine the previous tag if not supplied: nearest ancestor tag first,
# falling back to the next entry in version-sorted tag order.
if [[ -z "$PREV_TAG" ]]; then
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
PREV_TAG="$(git describe --tags --abbrev=0 "$TAG^" 2>/dev/null || true)"
fi
if [[ -z "$PREV_TAG" ]]; then
PREV_TAG="$(git tag -l 'v*' --sort=-v:refname \
| grep -A1 -Fx "$TAG" | tail -n +2 | head -1 || true)"
fi
fi
# Render changelog/v<version>.json into markdown sections.
render_changelog_json() {
python3 - "$CHANGELOG_FILE" << 'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as f:
entry = json.load(f)
out = []
title = entry.get("title")
if title:
out.append(f"**{title}**")
out.append("")
for key, heading in (
("highlights", "Highlights"),
("improvements", "Improvements"),
("fixes", "Fixes"),
):
items = entry.get(key) or []
if not items:
continue
out.append(f"### {heading}")
out.append("")
out.extend(f"- {item}" for item in items)
out.append("")
print("\n".join(out).rstrip())
PY
}
# Fallback: group commit subjects since the previous tag by
# conventional-commit prefix.
render_commit_fallback() {
local range
local end_ref="$TAG"
# Support pre-tag dry runs where the tag does not exist yet.
if ! git rev-parse -q --verify "$TAG^{commit}" >/dev/null; then
end_ref="HEAD"
fi
if [[ -n "$PREV_TAG" ]]; then
range="$PREV_TAG..$end_ref"
else
range="$end_ref"
fi
python3 - "$range" << 'PY'
import re
import subprocess
import sys
subjects = subprocess.run(
["git", "log", "--no-merges", "--pretty=format:%s", sys.argv[1]],
check=True,
capture_output=True,
text=True,
).stdout.splitlines()
groups = {"feat": [], "fix": [], "perf": [], "docs": [], "other": []}
pattern = re.compile(r"^(feat|fix|perf|docs)(\([^)]*\))?!?:\s*(.+)$", re.IGNORECASE)
for subject in subjects:
subject = subject.strip()
if not subject:
continue
match = pattern.match(subject)
if match:
groups[match.group(1).lower()].append(match.group(3).strip())
else:
groups["other"].append(subject)
out = []
for key, heading in (
("feat", "Features"),
("fix", "Fixes"),
("perf", "Performance"),
("docs", "Documentation"),
("other", "Other changes"),
):
items = groups[key]
if not items:
continue
out.append(f"### {heading}")
out.append("")
out.extend(f"- {item}" for item in items)
out.append("")
print("\n".join(out).rstrip())
PY
}
if [[ -f "$CHANGELOG_FILE" ]]; then
BODY="$(render_changelog_json)"
else
BODY="$(render_commit_fallback)"
fi
if [[ -n "$BODY" ]]; then
printf '%s\n\n' "$BODY"
fi
if [[ -n "$PREV_TAG" ]]; then
printf '**Full changelog**: https://github.com/%s/compare/%s...%s\n' \
"$REPO" "$PREV_TAG" "$TAG"
else
printf '**Full changelog**: https://github.com/%s/commits/%s\n' "$REPO" "$TAG"
fi
+601
View File
@@ -0,0 +1,601 @@
<#
.SYNOPSIS
Install jcode on Windows.
.DESCRIPTION
Downloads the latest jcode release and installs it to %LOCALAPPDATA%\jcode\bin.
One-liner install:
irm https://raw.githubusercontent.com/1jehuang/jcode/master/scripts/install.ps1 | iex
Or download and run (allows parameters):
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/1jehuang/jcode/master/scripts/install.ps1)))
.PARAMETER InstallDir
Override the installation directory (default: $env:LOCALAPPDATA\jcode\bin)
.PARAMETER Version
Override the version tag to install. Required when using a local artifact path.
.PARAMETER ArtifactExePath
Use a local jcode.exe artifact instead of downloading from GitHub.
.PARAMETER ArtifactTgzPath
Use a local jcode .tar.gz artifact instead of downloading from GitHub.
.PARAMETER SkipAlacrittySetup
Skip Alacritty install/setup helpers.
.PARAMETER SkipHotkeySetup
Skip Alt+; hotkey setup helpers.
#>
param(
[string]$InstallDir,
[string]$Version,
[string]$ArtifactExePath,
[string]$ArtifactTgzPath,
[switch]$SkipAlacrittySetup,
[switch]$SkipHotkeySetup
)
$ErrorActionPreference = 'Stop'
if ($PSVersionTable.PSVersion.Major -lt 5) {
Write-Host "error: PowerShell 5.1 or later is required" -ForegroundColor Red
exit 1
}
$Repo = "1jehuang/jcode"
if (-not $InstallDir) {
$InstallDir = Join-Path $env:LOCALAPPDATA "jcode\bin"
}
$JcodeHome = if ($env:JCODE_HOME) {
$env:JCODE_HOME
} elseif ($env:USERPROFILE) {
Join-Path $env:USERPROFILE ".jcode"
} else {
Join-Path ([Environment]::GetFolderPath("UserProfile")) ".jcode"
}
$HotkeyDir = Join-Path $JcodeHome "hotkey"
$SetupHintsPath = Join-Path $JcodeHome "setup_hints.json"
function Write-Info($msg) { Write-Host $msg -ForegroundColor Blue }
function Write-Err($msg) { Write-Host "error: $msg" -ForegroundColor Red; exit 1 }
function Write-Warn($msg) { Write-Host "warning: $msg" -ForegroundColor Yellow }
function Resolve-OptionalPath([string]$PathValue) {
if (-not $PathValue) {
return $null
}
try {
return (Resolve-Path -LiteralPath $PathValue -ErrorAction Stop).Path
} catch {
Write-Err "Provided path does not exist: $PathValue"
}
}
function Stop-ProcessTree([int]$ProcessId) {
try {
Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
Where-Object { $_.ParentProcessId -eq $ProcessId } |
ForEach-Object { Stop-ProcessTree -ProcessId $_.ProcessId }
} catch {}
try {
Stop-Process -Id $ProcessId -Force -ErrorAction SilentlyContinue
} catch {}
}
function Invoke-ProcessWithTimeout {
param(
[Parameter(Mandatory = $true)][string]$FilePath,
[string[]]$ArgumentList = @(),
[Parameter(Mandatory = $true)][int]$TimeoutSeconds,
[Parameter(Mandatory = $true)][string]$FriendlyName,
[switch]$CaptureOutput
)
$startParams = @{
FilePath = $FilePath
ArgumentList = $ArgumentList
PassThru = $true
NoNewWindow = $true
}
$stdoutPath = $null
$stderrPath = $null
if ($CaptureOutput) {
$stdoutPath = Join-Path $env:TEMP ("jcode-{0}-{1}-stdout.log" -f $FriendlyName, [guid]::NewGuid().ToString('N'))
$stderrPath = Join-Path $env:TEMP ("jcode-{0}-{1}-stderr.log" -f $FriendlyName, [guid]::NewGuid().ToString('N'))
$startParams.RedirectStandardOutput = $stdoutPath
$startParams.RedirectStandardError = $stderrPath
}
$process = Start-Process @startParams
$timedOut = -not ($process | Wait-Process -Timeout $TimeoutSeconds -PassThru -ErrorAction SilentlyContinue)
if ($timedOut) {
Stop-ProcessTree -ProcessId $process.Id
return [pscustomobject]@{
TimedOut = $true
ExitCode = $null
StdoutPath = $stdoutPath
StderrPath = $stderrPath
}
}
$process.Refresh()
return [pscustomobject]@{
TimedOut = $false
ExitCode = $process.ExitCode
StdoutPath = $stdoutPath
StderrPath = $stderrPath
}
}
function Write-LogTail([string]$Path, [string]$Label) {
if (-not $Path -or -not (Test-Path $Path)) {
return
}
$lines = Get-Content -Path $Path -Tail 40 -ErrorAction SilentlyContinue
if ($lines -and $lines.Count -gt 0) {
Write-Warn "$Label (last 40 lines):"
$lines | ForEach-Object { Write-Host $_ }
}
}
function Test-CommandExists([string]$CommandName) {
return [bool](Get-Command $CommandName -ErrorAction SilentlyContinue)
}
function Test-AlacrittyInstalled {
return [bool](Find-AlacrittyPath)
}
function Find-AlacrittyPath {
$candidates = @(
"C:\Program Files\Alacritty\alacritty.exe",
"C:\Program Files (x86)\Alacritty\alacritty.exe"
)
if ($env:LOCALAPPDATA) {
$candidates += (Join-Path $env:LOCALAPPDATA "Microsoft\WinGet\Links\alacritty.exe")
}
foreach ($candidate in $candidates) {
if ($candidate -and (Test-Path $candidate)) {
return $candidate
}
}
try {
$command = Get-Command alacritty -ErrorAction Stop
if ($command -and $command.Source) {
return $command.Source
}
} catch {}
return $null
}
function Install-Alacritty {
if (Test-AlacrittyInstalled) {
Write-Info "Alacritty is already installed"
return $true
}
if (-not (Test-CommandExists "winget")) {
Write-Warn "winget was not found, so Alacritty could not be installed automatically"
Write-Warn "Install App Installer / winget from Microsoft, then run: winget install -e --id Alacritty.Alacritty"
return $false
}
Write-Info "Installing Alacritty..."
$wingetArgs = @(
"install",
"-e",
"--id", "Alacritty.Alacritty",
"--accept-source-agreements",
"--accept-package-agreements",
"--disable-interactivity"
)
$wingetResult = Invoke-ProcessWithTimeout -FilePath "winget" -ArgumentList $wingetArgs -TimeoutSeconds 180 -FriendlyName "winget-install"
if ($wingetResult.TimedOut) {
Write-Warn "Alacritty install timed out after 180 seconds; skipping automatic setup"
return $false
}
if ($wingetResult.ExitCode -ne 0) {
Write-Warn "Alacritty install failed (winget exit code: $($wingetResult.ExitCode))"
return $false
}
$alacrittyPath = Find-AlacrittyPath
if (-not $alacrittyPath) {
Write-Warn "Alacritty install finished, but alacritty.exe was not found on PATH yet"
return $false
}
Write-Info "Alacritty installed: $alacrittyPath"
return $true
}
function Stop-JcodeHotkeyListeners {
try {
Get-CimInstance Win32_Process -Filter "Name = 'powershell.exe' OR Name = 'pwsh.exe'" -ErrorAction SilentlyContinue |
Where-Object { $_.CommandLine -like '*jcode-hotkey*' } |
ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
} catch {}
}
function Set-SetupHintsState([bool]$AlacrittyConfigured, [bool]$HotkeyConfigured) {
New-Item -ItemType Directory -Path $JcodeHome -Force | Out-Null
$state = @{
launch_count = 0
hotkey_configured = $HotkeyConfigured
hotkey_dismissed = $HotkeyConfigured
alacritty_configured = $AlacrittyConfigured
alacritty_dismissed = $AlacrittyConfigured
desktop_shortcut_created = $false
mac_ghostty_guided = $false
mac_ghostty_dismissed = $false
}
if (Test-Path $SetupHintsPath) {
try {
$existing = Get-Content $SetupHintsPath -Raw | ConvertFrom-Json -ErrorAction Stop
foreach ($property in $existing.PSObject.Properties) {
$state[$property.Name] = $property.Value
}
} catch {
Write-Warn "Could not read existing setup hints state; overwriting it"
}
}
if ($AlacrittyConfigured) {
$state.alacritty_configured = $true
$state.alacritty_dismissed = $true
}
if ($HotkeyConfigured) {
$state.hotkey_configured = $true
$state.hotkey_dismissed = $true
}
$state | ConvertTo-Json | Set-Content -Path $SetupHintsPath -Encoding UTF8
}
function Install-JcodeHotkey([string]$JcodeExePath) {
$alacrittyPath = Find-AlacrittyPath
if (-not $alacrittyPath) {
Write-Warn "Skipping Alt+; hotkey because Alacritty is not installed"
return $false
}
New-Item -ItemType Directory -Path $HotkeyDir -Force | Out-Null
Stop-JcodeHotkeyListeners
$escapedAlacritty = $alacrittyPath.Replace("'", "''")
$escapedJcodeExe = $JcodeExePath.Replace("'", "''")
$ps1Path = Join-Path $HotkeyDir "jcode-hotkey.ps1"
$ps1Lines = @(
'# jcode Alt+; global hotkey listener',
'# Auto-generated by scripts/install.ps1. Runs at login via startup shortcut.',
'',
'Add-Type @"',
'using System;',
'using System.Runtime.InteropServices;',
'public class HotKeyHelper {',
' [DllImport("user32.dll")]',
' public static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);',
' [DllImport("user32.dll")]',
' public static extern bool UnregisterHotKey(IntPtr hWnd, int id);',
' [DllImport("user32.dll")]',
' public static extern int GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);',
' [StructLayout(LayoutKind.Sequential)]',
' public struct MSG {',
' public IntPtr hwnd;',
' public uint message;',
' public IntPtr wParam;',
' public IntPtr lParam;',
' public uint time;',
' public int pt_x;',
' public int pt_y;',
' }',
'}',
'"@',
'',
'$MOD_ALT = 0x0001',
'$MOD_NOREPEAT = 0x4000',
'$VK_OEM_1 = 0xBA',
'$WM_HOTKEY = 0x0312',
'$HOTKEY_ID = 0x4A43',
'',
'if (-not [HotKeyHelper]::RegisterHotKey([IntPtr]::Zero, $HOTKEY_ID, $MOD_ALT -bor $MOD_NOREPEAT, $VK_OEM_1)) {',
' Write-Error "Failed to register Alt+; hotkey (another program may have claimed it)"',
' exit 1',
'}',
'',
'try {',
' $msg = New-Object HotKeyHelper+MSG',
' while ([HotKeyHelper]::GetMessage([ref]$msg, [IntPtr]::Zero, $WM_HOTKEY, $WM_HOTKEY) -ne 0) {',
' if ($msg.message -eq $WM_HOTKEY -and $msg.wParam.ToInt32() -eq $HOTKEY_ID) {',
" Start-Process '$escapedAlacritty' -ArgumentList '-e', '$escapedJcodeExe'",
' }',
' }',
'} finally {',
' [HotKeyHelper]::UnregisterHotKey([IntPtr]::Zero, $HOTKEY_ID)',
'}'
)
$ps1Content = $ps1Lines -join "`r`n"
Set-Content -Path $ps1Path -Value $ps1Content -Encoding UTF8
$vbsPath = Join-Path $HotkeyDir "jcode-hotkey-launcher.vbs"
$vbsContent = @(
'Set objShell = CreateObject("WScript.Shell")',
('objShell.Run "powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File ""{0}""", 0, False' -f $ps1Path)
) -join "`r`n"
Set-Content -Path $vbsPath -Value $vbsContent -Encoding ASCII
$startupDir = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Startup"
New-Item -ItemType Directory -Path $startupDir -Force | Out-Null
$startupShortcutPath = (Join-Path $startupDir "jcode-hotkey.lnk").Replace("'", "''")
$escapedVbsPath = $vbsPath.Replace("'", "''")
$shortcutLines = @(
'$shell = New-Object -ComObject WScript.Shell',
"`$shortcut = `$shell.CreateShortcut('$startupShortcutPath')",
"`$shortcut.TargetPath = 'wscript.exe'",
("`$shortcut.Arguments = '""{0}""'" -f $escapedVbsPath),
"`$shortcut.Description = 'jcode Alt+; hotkey listener'",
'`$shortcut.WindowStyle = 7',
'`$shortcut.Save()',
"Write-Output 'OK'"
)
$shortcutScript = $shortcutLines -join "`r`n"
$shortcutOutput = & powershell -NoProfile -Command $shortcutScript
if ($LASTEXITCODE -ne 0 -or -not ($shortcutOutput -match 'OK')) {
Write-Warn "Created hotkey files, but could not create the Startup shortcut"
return $false
}
$launchHotkeyCommand = "Start-Process wscript.exe -ArgumentList '""{0}""' -WindowStyle Hidden" -f $vbsPath
& powershell -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -Command $launchHotkeyCommand | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Warn "Hotkey will start on next login, but could not be launched immediately"
}
Write-Info "Configured Alt+; to launch jcode in Alacritty"
return $true
}
function Get-JcodeWindowsArtifact {
$candidates = @()
try {
$runtimeArch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture
if ($runtimeArch) { $candidates += [string]$runtimeArch }
} catch {}
foreach ($envArch in @($env:PROCESSOR_ARCHITECTURE, $env:PROCESSOR_ARCHITEW6432)) {
if ($envArch) { $candidates += [string]$envArch }
}
foreach ($arch in $candidates) {
switch -Regex ($arch.Trim()) {
'^(X64|AMD64|x86_64)$' { return "jcode-windows-x86_64" }
'^(Arm64|ARM64|AARCH64|aarch64)$' { return "jcode-windows-aarch64" }
}
}
$displayArch = if ($candidates.Count -gt 0) { $candidates -join ", " } else { "<unknown>" }
Write-Err "Unsupported architecture: $displayArch (supported: x86_64, ARM64)"
}
$Artifact = Get-JcodeWindowsArtifact
$ResolvedArtifactExePath = Resolve-OptionalPath $ArtifactExePath
$ResolvedArtifactTgzPath = Resolve-OptionalPath $ArtifactTgzPath
if ($ResolvedArtifactExePath -and $ResolvedArtifactTgzPath) {
Write-Err "Provide only one of -ArtifactExePath or -ArtifactTgzPath"
}
if (-not $Version) {
if ($ResolvedArtifactExePath -or $ResolvedArtifactTgzPath) {
Write-Err "-Version is required when using a local artifact path"
}
Write-Info "Fetching latest release..."
try {
$Release = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repo/releases/latest"
$Version = $Release.tag_name
} catch {
Write-Err "Failed to determine latest version: $_"
}
}
if (-not $Version) { Write-Err "Failed to determine latest version" }
$VersionNum = $Version.TrimStart('v')
$TgzUrl = "https://github.com/$Repo/releases/download/$Version/$Artifact.tar.gz"
$ExeUrl = "https://github.com/$Repo/releases/download/$Version/$Artifact.exe"
$BuildsDir = Join-Path $env:LOCALAPPDATA "jcode\builds"
$StableDir = Join-Path $BuildsDir "stable"
$VersionDir = Join-Path $BuildsDir "versions\$VersionNum"
$LauncherPath = Join-Path $InstallDir "jcode.exe"
$Existing = ""
if (Test-Path $LauncherPath) {
try { $Existing = & $LauncherPath --version 2>$null | Select-Object -First 1 } catch {}
}
if ($Existing) {
if ($Existing -match [regex]::Escape($VersionNum)) {
Write-Info "jcode $Version is already installed - reinstalling"
} else {
Write-Info "Updating jcode $Existing -> $Version"
}
} else {
Write-Info "Installing jcode $Version"
}
Write-Info " launcher: $LauncherPath"
foreach ($d in @($InstallDir, $StableDir, $VersionDir)) {
if (-not (Test-Path $d)) { New-Item -ItemType Directory -Path $d -Force | Out-Null }
}
$TempDir = Join-Path $env:TEMP "jcode-install-$(Get-Random)"
New-Item -ItemType Directory -Path $TempDir -Force | Out-Null
$DownloadMode = ""
$DownloadPath = Join-Path $TempDir "jcode.download"
if ($ResolvedArtifactExePath) {
Write-Info "Using local artifact exe: $ResolvedArtifactExePath"
Copy-Item -Path $ResolvedArtifactExePath -Destination $DownloadPath -Force
$DownloadMode = "bin"
} elseif ($ResolvedArtifactTgzPath) {
Write-Info "Using local artifact archive: $ResolvedArtifactTgzPath"
Copy-Item -Path $ResolvedArtifactTgzPath -Destination $DownloadPath -Force
$DownloadMode = "tar"
} else {
try {
Write-Info "Downloading $Artifact.exe..."
Invoke-WebRequest -Uri $ExeUrl -OutFile $DownloadPath
$DownloadMode = "bin"
} catch {
try {
Write-Info "Trying archive download..."
Invoke-WebRequest -Uri $TgzUrl -OutFile $DownloadPath
$DownloadMode = "tar"
} catch {
$DownloadMode = ""
}
}
}
$DestBin = Join-Path $VersionDir "jcode.exe"
if ($DownloadMode -eq "tar") {
Write-Info "Extracting..."
tar xzf $DownloadPath -C $TempDir 2>$null
$SrcBin = Join-Path $TempDir "$Artifact.exe"
if (-not (Test-Path $SrcBin)) {
Write-Err "Downloaded archive did not contain expected binary: $Artifact.exe"
}
Move-Item -Path $SrcBin -Destination $DestBin -Force
} elseif ($DownloadMode -eq "bin") {
Move-Item -Path $DownloadPath -Destination $DestBin -Force
} else {
Write-Info "No prebuilt asset found for $Artifact in $Version; building from source..."
if (-not (Get-Command git -ErrorAction SilentlyContinue)) { Write-Err "git is required to build from source" }
if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) { Write-Err "cargo is required to build from source" }
$SrcDir = Join-Path $TempDir "jcode-src"
Write-Info "Cloning $Repo at $Version..."
$gitCloneResult = Invoke-ProcessWithTimeout -FilePath "git" -ArgumentList @(
"clone",
"--depth", "1",
"--branch", $Version,
"https://github.com/$Repo.git",
$SrcDir
) -TimeoutSeconds 600 -FriendlyName "git-clone" -CaptureOutput
if ($gitCloneResult.TimedOut) {
Write-LogTail -Path $gitCloneResult.StdoutPath -Label "git stdout"
Write-LogTail -Path $gitCloneResult.StderrPath -Label "git stderr"
Write-Err "git clone timed out after 600 seconds"
}
if ($gitCloneResult.ExitCode -ne 0) {
Write-LogTail -Path $gitCloneResult.StdoutPath -Label "git stdout"
Write-LogTail -Path $gitCloneResult.StderrPath -Label "git stderr"
Write-Err "Failed to clone $Repo at $Version (exit code: $($gitCloneResult.ExitCode))"
}
Write-Info "Building jcode from source (this can take several minutes)..."
$cargoResult = Invoke-ProcessWithTimeout -FilePath "cargo" -ArgumentList @("build", "--release", "--manifest-path", (Join-Path $SrcDir "Cargo.toml")) -TimeoutSeconds 1800 -FriendlyName "cargo-build" -CaptureOutput
if ($cargoResult.TimedOut) {
Write-LogTail -Path $cargoResult.StdoutPath -Label "cargo stdout"
Write-LogTail -Path $cargoResult.StderrPath -Label "cargo stderr"
Write-Err "cargo build timed out after 1800 seconds"
}
if ($cargoResult.ExitCode -ne 0) {
Write-LogTail -Path $cargoResult.StdoutPath -Label "cargo stdout"
Write-LogTail -Path $cargoResult.StderrPath -Label "cargo stderr"
Write-Err "cargo build failed (exit code: $($cargoResult.ExitCode))"
}
$BuiltBin = Join-Path $SrcDir "target\release\jcode.exe"
if (-not (Test-Path $BuiltBin)) { Write-Err "Built binary not found at $BuiltBin" }
Copy-Item -Path $BuiltBin -Destination $DestBin -Force
}
Copy-Item -Path $DestBin -Destination (Join-Path $StableDir "jcode.exe") -Force
Set-Content -Path (Join-Path $BuildsDir "stable-version") -Value $VersionNum
Copy-Item -Path (Join-Path $StableDir "jcode.exe") -Destination $LauncherPath -Force
# Gracefully reload any running background server onto the freshly installed
# binary (issue #291). `server reload` only reloads a genuinely-older daemon,
# hands its live sessions to the new process, and is a no-op when nothing is
# running, so it is safe to call unconditionally. Best-effort: never fail the
# install over it.
if ($env:JCODE_SKIP_SERVER_RELOAD -ne "1") {
try {
& $LauncherPath server reload 2>$null | Out-Null
} catch {
}
}
Remove-Item -Path $TempDir -Recurse -Force -ErrorAction SilentlyContinue
$UserPath = [Environment]::GetEnvironmentVariable("Path", "User")
if ($UserPath -notlike "*$InstallDir*") {
[Environment]::SetEnvironmentVariable("Path", "$InstallDir;$UserPath", "User")
Write-Info "Added $InstallDir to user PATH"
}
$env:Path = "$InstallDir;$env:Path"
$installedAlacritty = $false
$configuredHotkey = $false
if ($SkipAlacrittySetup) {
Write-Info "Skipping Alacritty setup"
$installedAlacritty = Test-AlacrittyInstalled
} else {
$installedAlacritty = Install-Alacritty
}
if ($SkipHotkeySetup) {
Write-Info "Skipping Alt+; hotkey setup"
} elseif ($installedAlacritty) {
$configuredHotkey = Install-JcodeHotkey -JcodeExePath $LauncherPath
}
Set-SetupHintsState -AlacrittyConfigured:(Test-AlacrittyInstalled) -HotkeyConfigured:$configuredHotkey
Write-Host ""
Write-Info "jcode $Version installed successfully!"
Write-Host ""
if (Test-AlacrittyInstalled) {
$alacrittyPath = Find-AlacrittyPath
if ($alacrittyPath) {
Write-Info "Alacritty ready: $alacrittyPath"
}
}
if ($configuredHotkey) {
Write-Info "Global hotkey ready: Alt+; opens jcode in Alacritty"
Write-Host ""
}
if (Get-Command jcode -ErrorAction SilentlyContinue) {
Write-Info "Run 'jcode' to get started."
} else {
Write-Host " Open a new terminal window, then run:"
Write-Host ""
Write-Host " jcode" -ForegroundColor Green
}
+314
View File
@@ -0,0 +1,314 @@
#!/usr/bin/env bash
set -euo pipefail
REPO="1jehuang/jcode"
IS_WINDOWS=false
IS_TERMUX=false
info() { printf '\033[1;34m%s\033[0m\n' "$*"; }
err() { printf '\033[1;31merror: %s\033[0m\n' "$*" >&2; exit 1; }
OS="$(uname -s)"
ARCH="$(uname -m)"
if [ -n "${TERMUX_VERSION:-}" ] || [ "${PREFIX:-}" = "/data/data/com.termux/files/usr" ] || [ -d "/data/data/com.termux/files/usr" ]; then
IS_TERMUX=true
fi
case "$OS" in
Linux)
case "$ARCH" in
x86_64) ARTIFACT="jcode-linux-x86_64" ;;
aarch64|arm64) ARTIFACT="jcode-linux-aarch64" ;;
*) err "Unsupported Linux architecture: $ARCH" ;;
esac
;;
Darwin)
case "$ARCH" in
arm64) ARTIFACT="jcode-macos-aarch64" ;;
x86_64) ARTIFACT="jcode-macos-x86_64" ;;
*) err "Unsupported macOS architecture: $ARCH" ;;
esac
;;
MINGW*|MSYS*|CYGWIN*)
IS_WINDOWS=true
case "$ARCH" in
x86_64|AMD64) ARTIFACT="jcode-windows-x86_64" ;;
aarch64|arm64|ARM64) ARTIFACT="jcode-windows-aarch64" ;;
*) err "Unsupported Windows architecture: $ARCH" ;;
esac
;;
*)
err "Unsupported OS: $OS (try building from source: https://github.com/$REPO)"
;;
esac
if [ "$IS_WINDOWS" = true ]; then
INSTALL_DIR="${JCODE_INSTALL_DIR:-$LOCALAPPDATA/jcode/bin}"
else
INSTALL_DIR="${JCODE_INSTALL_DIR:-$HOME/.local/bin}"
fi
# Extract the tag_name value, working for both pretty-printed (multi-line) and
# compact (single-line) GitHub API JSON. `grep -o` isolates just the tag_name
# field so `cut` no longer matches an unrelated string like the release url.
VERSION=$(curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" | grep -o '"tag_name" *: *"[^"]*"' | head -1 | cut -d'"' -f4)
[ -n "$VERSION" ] || err "Failed to determine latest version"
URL_TGZ="https://github.com/$REPO/releases/download/$VERSION/$ARTIFACT.tar.gz"
URL_BIN="https://github.com/$REPO/releases/download/$VERSION/$ARTIFACT"
if [ "$IS_WINDOWS" = true ]; then
EXE=".exe"
builds_dir="$LOCALAPPDATA/jcode/builds"
else
EXE=""
builds_dir="$HOME/.jcode/builds"
fi
stable_dir="$builds_dir/stable"
current_dir="$builds_dir/current"
version_dir="$builds_dir/versions"
launcher_path="$INSTALL_DIR/jcode${EXE}"
EXISTING=""
if [ -x "$launcher_path" ]; then
EXISTING=$("$launcher_path" --version 2>/dev/null | head -1 || echo "unknown")
fi
if [ -n "$EXISTING" ]; then
if echo "$EXISTING" | grep -qF "${VERSION#v}"; then
info "jcode $VERSION is already installed — reinstalling"
else
info "Updating jcode $EXISTING$VERSION"
fi
else
info "Installing jcode $VERSION"
fi
info " launcher: $launcher_path"
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
download_mode=""
if curl -fsSL "$URL_TGZ" -o "$tmpdir/jcode.download" 2>/dev/null; then
download_mode="tar"
elif curl -fsSL "$URL_BIN" -o "$tmpdir/jcode.download" 2>/dev/null; then
download_mode="bin"
fi
mkdir -p "$INSTALL_DIR" "$stable_dir" "$current_dir" "$version_dir"
version="${VERSION#v}"
dest_version_dir="$version_dir/$version"
mkdir -p "$dest_version_dir"
bin_name="jcode${EXE}"
if [ "$download_mode" = "tar" ]; then
tar xzf "$tmpdir/jcode.download" -C "$tmpdir"
src_bin="$tmpdir/${ARTIFACT}${EXE}"
[ -f "$src_bin" ] || err "Downloaded archive did not contain expected binary: ${ARTIFACT}${EXE}"
find "$tmpdir" -maxdepth 1 -type f \( -name "${ARTIFACT}${EXE}.bin" -o -name 'libssl.so*' -o -name 'libcrypto.so*' \) \
-exec cp -f {} "$dest_version_dir/" \;
mv "$src_bin" "$dest_version_dir/$bin_name"
elif [ "$download_mode" = "bin" ]; then
mv "$tmpdir/jcode.download" "$dest_version_dir/$bin_name"
else
info "No prebuilt asset found for $ARTIFACT in $VERSION; building from source..."
command -v git >/dev/null 2>&1 || err "git is required to build from source"
command -v cargo >/dev/null 2>&1 || err "cargo is required to build from source"
src_dir="$tmpdir/jcode-src"
git clone --depth 1 --branch "$VERSION" "https://github.com/$REPO.git" "$src_dir" \
|| err "Failed to clone $REPO at $VERSION"
cargo build --release --manifest-path "$src_dir/Cargo.toml" \
|| err "cargo build failed while building $REPO from source"
src_bin="$src_dir/target/release/$bin_name"
[ -f "$src_bin" ] || err "Built binary not found at $src_bin"
cp "$src_bin" "$dest_version_dir/$bin_name"
fi
chmod +x "$dest_version_dir/$bin_name" 2>/dev/null || true
if [ "$IS_TERMUX" = true ] && [ "$IS_WINDOWS" = false ]; then
termux_glibc_dir="/data/data/com.termux/files/usr/glibc/lib"
termux_glibc_linker=""
case "$ARCH" in
aarch64|arm64) termux_glibc_linker="$termux_glibc_dir/ld-linux-aarch64.so.1" ;;
x86_64) termux_glibc_linker="$termux_glibc_dir/ld-linux-x86-64.so.2" ;;
esac
if [ "$OS" = "Linux" ] && [ -n "$termux_glibc_linker" ]; then
if [ -x "$termux_glibc_linker" ]; then
if command -v patchelf >/dev/null 2>&1; then
patchelf --set-interpreter "$termux_glibc_linker" "$dest_version_dir/$bin_name" \
|| err "Failed to patch jcode ELF interpreter for Termux glibc"
info "Patched Termux glibc ELF interpreter: $termux_glibc_linker"
else
info "Termux detected: install patchelf with 'pkg install patchelf' and rerun this installer if jcode fails to start."
fi
else
info "Termux detected: install glibc with 'pkg install glibc' if jcode fails due to a missing dynamic linker."
fi
fi
fi
if [ "$IS_WINDOWS" = true ]; then
cp -f "$dest_version_dir/$bin_name" "$stable_dir/$bin_name"
printf '%s\n' "$version" > "$builds_dir/stable-version"
cp -f "$stable_dir/$bin_name" "$launcher_path"
else
ln -sfn "$dest_version_dir/$bin_name" "$stable_dir/$bin_name"
printf '%s\n' "$version" > "$builds_dir/stable-version"
if [ "$IS_TERMUX" = true ]; then
rm -f "$launcher_path"
cat > "$launcher_path" <<EOF
#!/usr/bin/env bash
unset LD_PRELOAD
exec "$stable_dir/$bin_name" "\$@"
EOF
chmod +x "$launcher_path"
else
ln -sfn "$stable_dir/$bin_name" "$launcher_path"
fi
fi
if [ "$(uname -s)" = "Darwin" ]; then
xattr -d com.apple.quarantine "$dest_version_dir/$bin_name" 2>/dev/null || true
fi
if [ "$(uname -s)" = "Darwin" ]; then
if "$launcher_path" setup-hotkey </dev/null >/dev/null 2>&1; then
mac_hotkey_ready=true
else
mac_hotkey_ready=false
fi
fi
# Retire any background server still running the old binary so the freshly
# installed version is picked up without the user having to kill a daemon by
# hand (issue #291). We use the graceful `server reload` path, which hands live
# headless/swarm sessions to a newly-exec'd server instead of dropping them, and
# only reloads when the running server is genuinely older than what we just
# installed (so a newer/dev daemon is never downgraded). This is best-effort:
# it must never fail the install, and it is skipped when no server is running.
if [ "${JCODE_SKIP_SERVER_RELOAD:-}" != "1" ]; then
reload_bin="$launcher_path"
[ -x "$reload_bin" ] || reload_bin="$stable_dir/$bin_name"
if [ -x "$reload_bin" ]; then
if "$reload_bin" server reload </dev/null >/dev/null 2>&1; then
info "Reloaded the running jcode server onto $VERSION (if one was active)."
fi
fi
fi
if [ "$IS_WINDOWS" = true ]; then
win_install_dir=$(cygpath -w "$INSTALL_DIR" 2>/dev/null || echo "$INSTALL_DIR")
echo ""
info "✅ jcode $VERSION installed successfully!"
echo ""
if command -v jcode >/dev/null 2>&1; then
info "Run 'jcode' to get started."
else
echo " To start using jcode right now, run:"
echo ""
printf ' \033[1;32mexport PATH="%s:$PATH" && jcode\033[0m\n' "$INSTALL_DIR"
echo ""
echo " To add jcode to PATH permanently (PowerShell):"
echo ""
printf ' \033[1;32m[Environment]::SetEnvironmentVariable("Path", "%s;" + [Environment]::GetEnvironmentVariable("Path", "User"), "User")\033[0m\n' "$win_install_dir"
fi
else
PATH_LINE="export PATH=\"$INSTALL_DIR:\$PATH\""
added_to=""
_have() { command -v "$1" >/dev/null 2>&1; }
# Append the POSIX (bash/zsh/sh) PATH line to an rc file, idempotently.
# ensure_posix_rc <rc-file> <create:yes|no>
# With create=yes the file (and parent dir) is created if missing; with
# create=no we only touch files that already exist, so we never change how a
# login shell resolves its startup files (e.g. creating ~/.bash_profile would
# stop bash from reading ~/.profile).
ensure_posix_rc() {
rc="$1"; create="$2"
if [ ! -f "$rc" ]; then
[ "$create" = "yes" ] || return 0
mkdir -p "$(dirname "$rc")"
fi
if ! grep -qF "$INSTALL_DIR" "$rc" 2>/dev/null; then
printf '\n# Added by jcode installer\n%s\n' "$PATH_LINE" >> "$rc"
added_to="$added_to $rc"
fi
}
# fish uses its own syntax and does not read POSIX rc files.
ensure_fish_rc() {
create="$1"
rc="${XDG_CONFIG_HOME:-$HOME/.config}/fish/config.fish"
if [ ! -f "$rc" ]; then
[ "$create" = "yes" ] || return 0
mkdir -p "$(dirname "$rc")"
fi
if ! grep -qF "$INSTALL_DIR" "$rc" 2>/dev/null; then
{
printf '\n# Added by jcode installer\n'
printf 'if not contains "%s" $PATH\n' "$INSTALL_DIR"
printf ' set -gx PATH "%s" $PATH\n' "$INSTALL_DIR"
printf 'end\n'
} >> "$rc"
added_to="$added_to $rc"
fi
}
# zsh: ~/.zshenv is read for every zsh invocation (login, interactive and
# scripts), so it is the most reliable single place to export PATH.
if _have zsh || [ "$(uname -s)" = "Darwin" ] || [ -f "$HOME/.zshenv" ] || [ -f "$HOME/.zshrc" ]; then
ensure_posix_rc "$HOME/.zshenv" yes
fi
# bash: ~/.bashrc for interactive shells, ~/.profile for login shells (macOS
# Terminal, ssh, etc.). We only create ~/.profile, never ~/.bash_profile, so
# we don't override an existing login-file lookup order.
if _have bash || [ -f "$HOME/.bashrc" ] || [ -f "$HOME/.bash_profile" ]; then
ensure_posix_rc "$HOME/.bashrc" yes
fi
ensure_posix_rc "$HOME/.profile" yes
# fish: only set it up when fish is installed or already configured.
if _have fish || [ -f "${XDG_CONFIG_HOME:-$HOME/.config}/fish/config.fish" ]; then
ensure_fish_rc yes
fi
# Also patch other common startup files when they already exist, so we cover
# users with custom login-shell setups without creating new files.
for rc in "$HOME/.zshrc" "$HOME/.zprofile" "$HOME/.bash_profile"; do
ensure_posix_rc "$rc" no
done
if [ -n "$added_to" ]; then
info "Added $INSTALL_DIR to PATH in:$added_to"
fi
echo ""
info "✅ jcode $VERSION installed successfully!"
echo ""
if [ "$(uname -s)" = "Darwin" ]; then
if [ "${mac_hotkey_ready:-false}" = true ]; then
info "Global hotkey ready: Cmd+; launches a new jcode from anywhere, system-wide"
else
info "Tip: run 'jcode setup-hotkey' so Cmd+; launches jcode system-wide on macOS"
fi
fi
if command -v jcode >/dev/null 2>&1; then
info "Run 'jcode' to get started."
else
echo " To start using jcode right now, run:"
echo ""
printf ' \033[1;32mexport PATH="%s:\$PATH" && jcode\033[0m\n' "$INSTALL_DIR"
echo ""
echo " Future terminal sessions will have jcode on PATH automatically."
fi
fi
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env bash
# Install the current release binary into the immutable version store,
# update the stable + current channel symlinks, and point the launcher at current.
#
# Paths after install:
# - ~/.jcode/builds/versions/<hash>/jcode (immutable)
# - ~/.jcode/builds/stable/jcode -> .../versions/<hash>/jcode
# - ~/.jcode/builds/current/jcode -> .../versions/<hash>/jcode
# - ~/.local/bin/jcode -> ~/.jcode/builds/current/jcode (launcher)
set -euo pipefail
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
profile="${JCODE_RELEASE_PROFILE:-release-lto}"
if [[ "${1:-}" == "--fast" ]]; then
profile="release"
shift
fi
if [[ "$#" -gt 0 ]]; then
echo "Usage: $0 [--fast]" >&2
exit 1
fi
case "$profile" in
release-lto)
echo "Building with LTO (this takes a few minutes)..."
;;
release)
echo "Building fast release profile (no LTO)..."
;;
*)
echo "Unsupported profile: $profile (expected: release or release-lto)" >&2
exit 1
;;
esac
cargo build --profile "$profile" --manifest-path "$repo_root/Cargo.toml"
bin="$repo_root/target/$profile/jcode"
if [[ ! -x "$bin" ]]; then
echo "Release binary not found: $bin" >&2
exit 1
fi
hash=""
if command -v git >/dev/null 2>&1; then
if git -C "$repo_root" rev-parse --git-dir >/dev/null 2>&1; then
hash="$(git -C "$repo_root" rev-parse --short HEAD 2>/dev/null || true)"
if [[ -n "${hash}" ]] && [[ -n "$(git -C "$repo_root" status --porcelain 2>/dev/null || true)" ]]; then
hash="${hash}-dirty"
fi
fi
fi
if [[ -z "$hash" ]]; then
hash="$(date +%Y%m%d%H%M%S)"
fi
# Install versioned binary into ~/.jcode/builds/versions/<hash>/
builds_dir="$HOME/.jcode/builds"
version_dir="$builds_dir/versions/$hash"
mkdir -p "$version_dir"
install -m 755 "$bin" "$version_dir/jcode"
# Update stable symlink
stable_dir="$builds_dir/stable"
mkdir -p "$stable_dir"
ln -sfn "$version_dir/jcode" "$stable_dir/jcode"
# Update stable-version marker
printf '%s\n' "$hash" > "$builds_dir/stable-version"
# Update current symlink + marker
current_dir="$builds_dir/current"
mkdir -p "$current_dir"
ln -sfn "$version_dir/jcode" "$current_dir/jcode"
printf '%s\n' "$hash" > "$builds_dir/current-version"
# Update launcher path to current channel
install_dir="${JCODE_INSTALL_DIR:-$HOME/.local/bin}"
mkdir -p "$install_dir"
ln -sfn "$current_dir/jcode" "$install_dir/jcode"
echo "Installed: $version_dir/jcode"
echo "Updated stable symlink: $stable_dir/jcode -> $version_dir/jcode"
echo "Updated current symlink: $current_dir/jcode -> $version_dir/jcode"
echo "Updated launcher symlink: $install_dir/jcode -> $current_dir/jcode"
# Gracefully reload any running background server onto the binary we just
# installed (issue #291). `server reload` only reloads when the running daemon
# is genuinely older, hands live headless/swarm sessions to the new process, and
# is a no-op when no server is running, so it is safe to call unconditionally.
if [ "${JCODE_SKIP_SERVER_RELOAD:-}" != "1" ]; then
if "$install_dir/jcode" server reload </dev/null >/dev/null 2>&1; then
echo "Reloaded the running jcode server onto $hash (if one was active)."
fi
fi
if ! echo "$PATH" | tr ':' '\n' | grep -qx "$install_dir"; then
echo ""
echo "Tip: add $install_dir to PATH if needed."
fi
# Ensure the launcher dir is on PATH for bash, zsh and fish in future shells.
# shellcheck source=scripts/lib/configure_path.sh
. "$(dirname "$0")/lib/configure_path.sh"
jcode_configure_path "$install_dir"
+54
View File
@@ -0,0 +1,54 @@
param(
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]$Name,
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string[]]$CargoArgs,
[ValidateRange(1, 86400)]
[int]$TimeoutSeconds = 300
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
function Stop-ProcessTree {
param(
[Parameter(Mandatory=$true)]
[System.Diagnostics.Process]$Process
)
if ($Process.HasExited) {
return
}
$taskkill = Get-Command taskkill.exe -ErrorAction SilentlyContinue
if ($taskkill) {
& $taskkill.Source /PID $Process.Id /T /F | ForEach-Object { Write-Host $_ }
return
}
Stop-Process -Id $Process.Id -Force -ErrorAction SilentlyContinue
}
$timeoutMilliseconds = $TimeoutSeconds * 1000
Write-Host "::group::$Name"
try {
Write-Host "cargo $($CargoArgs -join ' ')"
$process = Start-Process -FilePath 'cargo' -ArgumentList $CargoArgs -NoNewWindow -PassThru
if (-not $process.WaitForExit($timeoutMilliseconds)) {
Stop-ProcessTree -Process $process
throw "$Name timed out after $TimeoutSeconds seconds"
}
$exitCode = $process.ExitCode
if ($exitCode -ne 0) {
throw "$Name failed with exit code $exitCode"
}
} finally {
Write-Host '::endgroup::'
}
+320
View File
@@ -0,0 +1,320 @@
from __future__ import annotations
import json
import os
from pathlib import Path
from typing import Any
from harbor.agents.base import BaseAgent
from harbor.environments.base import BaseEnvironment
from harbor.models.agent.context import AgentContext
IN_CONTAINER_HOME = "/tmp/jcode-home"
IN_CONTAINER_RUNTIME = "/tmp/jcode-runtime"
IN_CONTAINER_INPUT = "/tmp/jcode-input"
IN_CONTAINER_OUTPUT = "/tmp/jcode-output"
IN_CONTAINER_BINARY = "/usr/local/bin/jcode"
IN_CONTAINER_LIB_DIR = f"{IN_CONTAINER_RUNTIME}/lib"
IN_CONTAINER_CA_BUNDLE = f"{IN_CONTAINER_HOME}/ca-certificates.crt"
DEFAULT_BINARY_PATH = "/tmp/jcode-compat-dist/jcode-linux-x86_64"
DEFAULT_OPENAI_AUTH_PATH = "~/.jcode/openai-auth.json"
CA_BUNDLE_CANDIDATES = (
os.environ.get("JCODE_HARBOR_CA_BUNDLE"),
"/etc/ca-certificates/extracted/tls-ca-bundle.pem",
"/etc/ssl/certs/ca-certificates.crt",
)
def _resolve_existing_file(*, env_name: str, default_path: str | None = None, candidates: tuple[str | None, ...] = ()) -> Path:
raw_value = os.environ.get(env_name) or default_path
values = [raw_value, *candidates] if raw_value is not None else list(candidates)
checked: list[str] = []
for value in values:
if not value:
continue
candidate = Path(value).expanduser()
checked.append(str(candidate))
if candidate.exists() and candidate.is_file():
return candidate.resolve()
raise FileNotFoundError(f"Could not find a readable file for {env_name}. Checked: {checked}")
def _resolve_optional_existing_file(*, candidates: tuple[str | None, ...]) -> Path | None:
for value in candidates:
if not value:
continue
candidate = Path(value).expanduser()
if candidate.exists() and candidate.is_file():
return candidate.resolve()
return None
def _sibling_runtime_lib_candidates(binary: Path, stem: str) -> tuple[str, ...]:
return tuple(str(path) for path in sorted(binary.parent.glob(f"{stem}.so*")) if path.is_file())
JCODE_BINARY = _resolve_existing_file(
env_name="JCODE_HARBOR_BINARY",
default_path=DEFAULT_BINARY_PATH,
)
OPENAI_AUTH = _resolve_existing_file(
env_name="JCODE_HARBOR_OPENAI_AUTH",
default_path=DEFAULT_OPENAI_AUTH_PATH,
)
CA_BUNDLE = _resolve_existing_file(
env_name="JCODE_HARBOR_CA_BUNDLE",
candidates=CA_BUNDLE_CANDIDATES,
)
OPENSSL_RUNTIME_LIBS = tuple(
lib
for lib in (
_resolve_optional_existing_file(
candidates=(
os.environ.get("JCODE_HARBOR_LIBSSL"),
*_sibling_runtime_lib_candidates(JCODE_BINARY, "libssl"),
"/usr/lib/libssl.so.3",
"/usr/lib/x86_64-linux-gnu/libssl.so.3",
"/lib/x86_64-linux-gnu/libssl.so.3",
"/usr/lib/libssl.so.1.1",
"/usr/lib/x86_64-linux-gnu/libssl.so.1.1",
"/lib/x86_64-linux-gnu/libssl.so.1.1",
)
),
_resolve_optional_existing_file(
candidates=(
os.environ.get("JCODE_HARBOR_LIBCRYPTO"),
*_sibling_runtime_lib_candidates(JCODE_BINARY, "libcrypto"),
"/usr/lib/libcrypto.so.3",
"/usr/lib/x86_64-linux-gnu/libcrypto.so.3",
"/lib/x86_64-linux-gnu/libcrypto.so.3",
"/usr/lib/libcrypto.so.1.1",
"/usr/lib/x86_64-linux-gnu/libcrypto.so.1.1",
"/lib/x86_64-linux-gnu/libcrypto.so.1.1",
)
),
)
if lib is not None
)
LEGACY_BENCHMARK_INSTRUCTION_PREAMBLE = """You are operating inside an official Terminal-Bench evaluation environment.
The user explicitly authorizes you to complete the benchmark task using the files, media, and URLs provided by the task.
Do not access Terminal-Bench websites, Terminal-Bench GitHub repositories, leaderboard pages, public task solutions, or external benchmark answer sources.
Do not refuse solely because the task involves copyrighted code, text, images, audio, or video when those materials are provided for this evaluation.
Still refuse only actions that are clearly harmful, illegal, or outside the benchmark task.
Complete the task by changing the container state, not merely by explaining a solution.
Before finishing, run the strongest local validation available from the prompt or workspace.
If the task asks for a file, verify that exact path and contents exist.
If the task asks for a server, VM, socket, or background process, leave it running and verify the specified client command can connect before finishing.
Prefer deterministic, minimal solutions over long exploratory work.
Task instruction follows:
"""
def _benchmark_instruction_preamble() -> str:
# Keep Harbor runs aligned with normal TUI/jcode-run prompting by default.
# The legacy preamble can still be enabled explicitly for reproducing older
# runs, but new benchmark runs should rely on jcode's normal system prompt
# and the official Terminal-Bench task instruction.
if os.environ.get("JCODE_HARBOR_LEGACY_PREAMBLE"):
return LEGACY_BENCHMARK_INSTRUCTION_PREAMBLE
return os.environ.get("JCODE_HARBOR_EXTRA_PREAMBLE", "")
def _load_task_hint() -> str:
if not os.environ.get("JCODE_HARBOR_ENABLE_HINTS"):
return ""
task_name = os.environ.get("JCODE_HARBOR_CURRENT_TASK", "").strip()
hints_path = os.environ.get("JCODE_HARBOR_TASK_HINTS_FILE", "").strip()
extra = os.environ.get("JCODE_HARBOR_EXTRA_PREAMBLE", "").strip()
parts: list[str] = []
if extra:
parts.append(extra)
if task_name and hints_path:
path = Path(hints_path).expanduser()
try:
hints = json.loads(path.read_text())
except Exception: # noqa: BLE001
hints = {}
hint = hints.get(task_name) if isinstance(hints, dict) else None
if isinstance(hint, str) and hint.strip():
parts.append(hint.strip())
if not parts:
return ""
return "\nAdditional benchmark retry guidance:\n" + "\n\n".join(parts) + "\n\n"
def _load_final_payload(output_dir: Path) -> dict[str, Any] | None:
result_json_path = output_dir / "result.json"
if result_json_path.exists():
raw = result_json_path.read_text()
if raw.strip():
return json.loads(raw)
events_path = output_dir / "events.ndjson"
if not events_path.exists():
return None
final_done: dict[str, Any] | None = None
for line in events_path.read_text().splitlines():
line = line.strip()
if not line:
continue
try:
event = json.loads(line)
except json.JSONDecodeError:
continue
if event.get("type") == "done":
final_done = event
if final_done is None:
return None
payload = {
"session_id": final_done.get("session_id"),
"provider": final_done.get("provider"),
"model": final_done.get("model"),
"text": final_done.get("text", ""),
"usage": final_done.get("usage") or {},
}
result_json_path.write_text(json.dumps(payload, indent=2) + "\n")
return payload
class JcodeHarborAgent(BaseAgent):
def __init__(self, logs_dir: Path, model_name: str | None = None, *args, **kwargs):
super().__init__(logs_dir, model_name, *args, **kwargs)
self._model_arg = model_name or "openai/gpt-5.4"
if "/" in self._model_arg:
self._provider_arg, self._jcode_model = self._model_arg.split("/", 1)
else:
self._provider_arg, self._jcode_model = "openai", self._model_arg
@staticmethod
def name() -> str:
return "jcode-harbor"
def version(self) -> str | None:
return "compat-openai-oauth"
async def setup(self, environment: BaseEnvironment) -> None:
await environment.exec(
(
"mkdir -p "
f"{IN_CONTAINER_HOME} {IN_CONTAINER_RUNTIME} {IN_CONTAINER_INPUT} {IN_CONTAINER_OUTPUT} "
f"{IN_CONTAINER_LIB_DIR} /usr/local/bin /usr/lib/ssl && "
f"ln -snf {IN_CONTAINER_HOME} /usr/lib/ssl/certs"
),
timeout_sec=30,
)
await environment.upload_file(JCODE_BINARY, IN_CONTAINER_BINARY)
await environment.exec(f"chmod +x {IN_CONTAINER_BINARY}", timeout_sec=30)
for lib in OPENSSL_RUNTIME_LIBS:
await environment.upload_file(lib, f"{IN_CONTAINER_LIB_DIR}/{lib.name}")
await environment.upload_file(OPENAI_AUTH, f"{IN_CONTAINER_HOME}/openai-auth.json")
await environment.upload_file(CA_BUNDLE, IN_CONTAINER_CA_BUNDLE)
version_result = await environment.exec(
f"{IN_CONTAINER_BINARY} --quiet --no-update --no-selfdev version --json",
env={
"HOME": IN_CONTAINER_HOME,
"JCODE_HOME": IN_CONTAINER_HOME,
"JCODE_RUNTIME_DIR": IN_CONTAINER_RUNTIME,
"JCODE_NO_TELEMETRY": "1",
"LD_LIBRARY_PATH": IN_CONTAINER_LIB_DIR,
},
timeout_sec=60,
)
(self.logs_dir / "setup_version.json").write_text(version_result.stdout or "")
(self.logs_dir / "setup_version.stderr.txt").write_text(version_result.stderr or "")
(self.logs_dir / "setup_version.return_code.txt").write_text(str(version_result.return_code))
async def run(self, instruction: str, environment: BaseEnvironment, context: AgentContext) -> None:
self.logs_dir.mkdir(parents=True, exist_ok=True)
benchmark_instruction = f"{_benchmark_instruction_preamble()}{_load_task_hint()}{instruction}"
local_instruction = self.logs_dir / "instruction.txt"
local_instruction.write_text(benchmark_instruction)
await environment.upload_file(local_instruction, f"{IN_CONTAINER_INPUT}/instruction.txt")
env = {
"HOME": IN_CONTAINER_HOME,
"JCODE_HOME": IN_CONTAINER_HOME,
"JCODE_RUNTIME_DIR": IN_CONTAINER_RUNTIME,
"JCODE_NO_TELEMETRY": "1",
"JCODE_PROVIDER": self._provider_arg,
"JCODE_MODEL": self._jcode_model,
"JCODE_OPENAI_REASONING_EFFORT": os.environ.get("JCODE_OPENAI_REASONING_EFFORT", "high"),
"JCODE_OPENAI_SERVICE_TIER": os.environ.get("JCODE_OPENAI_SERVICE_TIER", "priority"),
"SSL_CERT_FILE": IN_CONTAINER_CA_BUNDLE,
"OPENSSL_CERT_FILE": IN_CONTAINER_CA_BUNDLE,
"LD_LIBRARY_PATH": IN_CONTAINER_LIB_DIR,
}
result = await environment.exec(
command=(
'set -e; '
'workdir="${JCODE_TASK_WORKDIR:-}"; '
'if [ -z "$workdir" ]; then '
' if [ -d /app ]; then workdir=/app; else workdir="$(pwd)"; fi; '
'fi; '
f'instruction="$(cat {IN_CONTAINER_INPUT}/instruction.txt)"; '
f'{IN_CONTAINER_BINARY} --quiet --no-update --no-selfdev '
'--provider "$JCODE_PROVIDER" --model "$JCODE_MODEL" '
'-C "$workdir" run --ndjson "$instruction" '
f'> {IN_CONTAINER_OUTPUT}/events.ndjson 2> {IN_CONTAINER_OUTPUT}/stderr.txt'
),
env=env
)
(self.logs_dir / "exec_stdout.txt").write_text(result.stdout or "")
(self.logs_dir / "exec_stderr.txt").write_text(result.stderr or "")
(self.logs_dir / "exec_return_code.txt").write_text(str(result.return_code))
try:
await environment.download_dir(IN_CONTAINER_OUTPUT, self.logs_dir / "jcode-output")
except Exception as e: # noqa: BLE001
(self.logs_dir / "download_error.txt").write_text(str(e))
metadata: dict[str, Any] = {
"return_code": result.return_code,
"provider": self._provider_arg,
"model": self._jcode_model,
"jcode_binary": str(JCODE_BINARY),
}
output_dir = self.logs_dir / "jcode-output"
payload = _load_final_payload(output_dir)
if payload is not None:
usage = payload.get("usage") or {}
context.n_input_tokens = usage.get("input_tokens")
context.n_output_tokens = usage.get("output_tokens")
cache_read = usage.get("cache_read_input_tokens")
cache_create = usage.get("cache_creation_input_tokens")
if isinstance(cache_read, int) and isinstance(cache_create, int):
context.n_cache_tokens = cache_read + cache_create
elif isinstance(cache_read, int):
context.n_cache_tokens = cache_read
metadata["jcode_result"] = payload
result_json_path = output_dir / "result.json"
if payload is None and result_json_path.exists():
raw = result_json_path.read_text()
if raw.strip():
try:
payload = json.loads(raw)
usage = payload.get("usage") or {}
context.n_input_tokens = usage.get("input_tokens")
context.n_output_tokens = usage.get("output_tokens")
cache_read = usage.get("cache_read_input_tokens")
cache_create = usage.get("cache_creation_input_tokens")
if isinstance(cache_read, int) and isinstance(cache_create, int):
context.n_cache_tokens = cache_read + cache_create
elif isinstance(cache_read, int):
context.n_cache_tokens = cache_read
metadata["jcode_result"] = payload
except Exception as e: # noqa: BLE001
metadata["result_parse_error"] = str(e)
metadata["raw_result_prefix"] = raw[:1000]
context.metadata = metadata
+329
View File
@@ -0,0 +1,329 @@
from __future__ import annotations
import json
import os
from pathlib import Path
from typing import Any
from harbor.agents.base import BaseAgent
from harbor.environments.base import BaseEnvironment
from harbor.models.agent.context import AgentContext
IN_CONTAINER_HOME = "/tmp/jcode-home"
IN_CONTAINER_RUNTIME = "/tmp/jcode-runtime"
IN_CONTAINER_INPUT = "/tmp/jcode-input"
IN_CONTAINER_OUTPUT = "/tmp/jcode-output"
IN_CONTAINER_BINARY = "/usr/local/bin/jcode"
IN_CONTAINER_LIB_DIR = f"{IN_CONTAINER_RUNTIME}/lib"
IN_CONTAINER_CA_BUNDLE = f"{IN_CONTAINER_HOME}/ca-certificates.crt"
DEFAULT_BINARY_PATH = "/tmp/jcode-compat-dist/jcode-linux-x86_64.bin"
DEFAULT_CLAUDE_AUTH_PATH = "~/.jcode/auth.json"
DEFAULT_OPENROUTER_ENV_PATH = "~/.config/jcode/openrouter.env"
DEFAULT_ANTHROPIC_ENV_PATH = "~/.config/jcode/anthropic.env"
CA_BUNDLE_CANDIDATES = (
os.environ.get("JCODE_HARBOR_CA_BUNDLE"),
"/etc/ca-certificates/extracted/tls-ca-bundle.pem",
"/etc/ssl/certs/ca-certificates.crt",
)
def _resolve_existing_file(*, env_name: str, default_path: str | None = None, candidates: tuple[str | None, ...] = ()) -> Path:
raw_value = os.environ.get(env_name) or default_path
values = [raw_value, *candidates] if raw_value is not None else list(candidates)
checked: list[str] = []
for value in values:
if not value:
continue
candidate = Path(value).expanduser()
checked.append(str(candidate))
if candidate.exists() and candidate.is_file():
return candidate.resolve()
raise FileNotFoundError(f"Could not find a readable file for {env_name}. Checked: {checked}")
def _resolve_optional_existing_file(*, candidates: tuple[str | None, ...]) -> Path | None:
for value in candidates:
if not value:
continue
candidate = Path(value).expanduser()
if candidate.exists() and candidate.is_file():
return candidate.resolve()
return None
def _sibling_runtime_lib_candidates(binary: Path, stem: str) -> tuple[str, ...]:
return tuple(str(path) for path in sorted(binary.parent.glob(f"{stem}.so*")) if path.is_file())
def _load_key_from_env_file(env_path: str, env_var: str, *direct_env: str) -> str | None:
for name in direct_env:
value = os.environ.get(name)
if value and value.strip():
return value.strip()
path = Path(env_path).expanduser()
if path.exists() and path.is_file():
for line in path.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" in line:
key, _, value = line.partition("=")
if key.strip() != env_var:
continue
value = value.strip().strip('"').strip("'")
else:
value = line
if value:
return value
return None
def _load_anthropic_key() -> str | None:
return _load_key_from_env_file(
os.environ.get("JCODE_HARBOR_ANTHROPIC_ENV", DEFAULT_ANTHROPIC_ENV_PATH),
"ANTHROPIC_API_KEY",
"ANTHROPIC_API_KEY",
"JCODE_HARBOR_ANTHROPIC_KEY",
)
def _load_openrouter_key() -> str | None:
# Priority: explicit env, then the jcode openrouter.env file (raw key per line).
direct = os.environ.get("OPENROUTER_API_KEY") or os.environ.get("JCODE_HARBOR_OPENROUTER_KEY")
if direct and direct.strip():
return direct.strip()
path = Path(os.environ.get("JCODE_HARBOR_OPENROUTER_ENV", DEFAULT_OPENROUTER_ENV_PATH)).expanduser()
if path.exists() and path.is_file():
for line in path.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
# Support both "KEY=value" and bare "value" formats.
if "=" in line:
_, _, value = line.partition("=")
value = value.strip().strip('"').strip("'")
else:
value = line
if value:
return value
return None
JCODE_BINARY = _resolve_existing_file(
env_name="JCODE_HARBOR_BINARY",
default_path=DEFAULT_BINARY_PATH,
)
CA_BUNDLE = _resolve_existing_file(
env_name="JCODE_HARBOR_CA_BUNDLE",
candidates=CA_BUNDLE_CANDIDATES,
)
OPENSSL_RUNTIME_LIBS = tuple(
lib
for lib in (
_resolve_optional_existing_file(
candidates=(
os.environ.get("JCODE_HARBOR_LIBSSL"),
*_sibling_runtime_lib_candidates(JCODE_BINARY, "libssl"),
"/usr/lib/libssl.so.3",
"/usr/lib/x86_64-linux-gnu/libssl.so.3",
"/lib/x86_64-linux-gnu/libssl.so.3",
)
),
_resolve_optional_existing_file(
candidates=(
os.environ.get("JCODE_HARBOR_LIBCRYPTO"),
*_sibling_runtime_lib_candidates(JCODE_BINARY, "libcrypto"),
"/usr/lib/libcrypto.so.3",
"/usr/lib/x86_64-linux-gnu/libcrypto.so.3",
"/lib/x86_64-linux-gnu/libcrypto.so.3",
)
),
)
if lib is not None
)
def _benchmark_instruction_preamble() -> str:
return os.environ.get("JCODE_HARBOR_EXTRA_PREAMBLE", "")
def _load_final_payload(output_dir: Path) -> dict[str, Any] | None:
result_json_path = output_dir / "result.json"
if result_json_path.exists():
raw = result_json_path.read_text()
if raw.strip():
return json.loads(raw)
events_path = output_dir / "events.ndjson"
if not events_path.exists():
return None
final_done: dict[str, Any] | None = None
for line in events_path.read_text().splitlines():
line = line.strip()
if not line:
continue
try:
event = json.loads(line)
except json.JSONDecodeError:
continue
if event.get("type") == "done":
final_done = event
if final_done is None:
return None
payload = {
"session_id": final_done.get("session_id"),
"provider": final_done.get("provider"),
"model": final_done.get("model"),
"text": final_done.get("text", ""),
"usage": final_done.get("usage") or {},
}
result_json_path.write_text(json.dumps(payload, indent=2) + "\n")
return payload
class JcodeClaudeHarborAgent(BaseAgent):
"""Harbor adapter that runs jcode with Opus 4.8.
Default route is the native Anthropic API (provider=anthropic-api,
model=claude-opus-4-8) using ANTHROPIC_API_KEY. OpenRouter
(provider=openrouter, model=anthropic/claude-opus-4.8) and native Claude
OAuth (provider=claude via ~/.jcode/auth.json) are also supported.
"""
def __init__(self, logs_dir: Path, model_name: str | None = None, *args, **kwargs):
super().__init__(logs_dir, model_name, *args, **kwargs)
self._model_arg = model_name or "anthropic-api/claude-opus-4-8"
if "/" in self._model_arg:
self._provider_arg, self._jcode_model = self._model_arg.split("/", 1)
else:
self._provider_arg, self._jcode_model = "anthropic-api", self._model_arg
self._openrouter_key = _load_openrouter_key() if self._provider_arg == "openrouter" else None
self._anthropic_key = _load_anthropic_key() if self._provider_arg == "anthropic-api" else None
self._claude_auth: Path | None = None
if self._provider_arg == "claude":
self._claude_auth = _resolve_existing_file(
env_name="JCODE_HARBOR_CLAUDE_AUTH",
default_path=DEFAULT_CLAUDE_AUTH_PATH,
)
@staticmethod
def name() -> str:
return "jcode-harbor-claude"
def version(self) -> str | None:
return "compat-opus-4-8"
async def setup(self, environment: BaseEnvironment) -> None:
# NOTE: Do NOT symlink/override the container's system OpenSSL cert dir
# (e.g. ln -s ... /usr/lib/ssl/certs). The TB base images are often bare
# ubuntu:24.04 with no /etc/ssl/certs, and the per-task verifier runs
# `apt-get install ca-certificates curl` then bootstraps uv over https.
# Hijacking the system cert dir breaks the verifier's curl (error 77,
# "error setting certificate file") so tests never run. jcode itself
# gets its CA bundle via SSL_CERT_FILE/OPENSSL_CERT_FILE below, so no
# global cert override is needed.
await environment.exec(
(
"mkdir -p "
f"{IN_CONTAINER_HOME} {IN_CONTAINER_RUNTIME} {IN_CONTAINER_INPUT} {IN_CONTAINER_OUTPUT} "
f"{IN_CONTAINER_LIB_DIR} /usr/local/bin"
),
timeout_sec=30,
)
await environment.upload_file(JCODE_BINARY, IN_CONTAINER_BINARY)
await environment.exec(f"chmod +x {IN_CONTAINER_BINARY}", timeout_sec=30)
for lib in OPENSSL_RUNTIME_LIBS:
await environment.upload_file(lib, f"{IN_CONTAINER_LIB_DIR}/{lib.name}")
if self._claude_auth is not None:
await environment.upload_file(self._claude_auth, f"{IN_CONTAINER_HOME}/auth.json")
await environment.exec(f"chmod 600 {IN_CONTAINER_HOME}/auth.json", timeout_sec=30)
await environment.upload_file(CA_BUNDLE, IN_CONTAINER_CA_BUNDLE)
version_result = await environment.exec(
f"{IN_CONTAINER_BINARY} --quiet --no-update --no-selfdev version --json",
env={
"HOME": IN_CONTAINER_HOME,
"JCODE_HOME": IN_CONTAINER_HOME,
"JCODE_RUNTIME_DIR": IN_CONTAINER_RUNTIME,
"JCODE_NO_TELEMETRY": "1",
"LD_LIBRARY_PATH": IN_CONTAINER_LIB_DIR,
},
timeout_sec=60,
)
(self.logs_dir / "setup_version.json").write_text(version_result.stdout or "")
(self.logs_dir / "setup_version.stderr.txt").write_text(version_result.stderr or "")
(self.logs_dir / "setup_version.return_code.txt").write_text(str(version_result.return_code))
async def run(self, instruction: str, environment: BaseEnvironment, context: AgentContext) -> None:
self.logs_dir.mkdir(parents=True, exist_ok=True)
benchmark_instruction = f"{_benchmark_instruction_preamble()}{instruction}"
local_instruction = self.logs_dir / "instruction.txt"
local_instruction.write_text(benchmark_instruction)
await environment.upload_file(local_instruction, f"{IN_CONTAINER_INPUT}/instruction.txt")
env = {
"HOME": IN_CONTAINER_HOME,
"JCODE_HOME": IN_CONTAINER_HOME,
"JCODE_RUNTIME_DIR": IN_CONTAINER_RUNTIME,
"JCODE_NO_TELEMETRY": "1",
"JCODE_PROVIDER": self._provider_arg,
"JCODE_MODEL": self._jcode_model,
"JCODE_ANTHROPIC_REASONING_EFFORT": os.environ.get("JCODE_ANTHROPIC_REASONING_EFFORT", "high"),
"SSL_CERT_FILE": IN_CONTAINER_CA_BUNDLE,
"OPENSSL_CERT_FILE": IN_CONTAINER_CA_BUNDLE,
"LD_LIBRARY_PATH": IN_CONTAINER_LIB_DIR,
}
if self._openrouter_key:
env["OPENROUTER_API_KEY"] = self._openrouter_key
if self._anthropic_key:
env["ANTHROPIC_API_KEY"] = self._anthropic_key
result = await environment.exec(
command=(
'set -e; '
'workdir="${JCODE_TASK_WORKDIR:-}"; '
'if [ -z "$workdir" ]; then '
' if [ -d /app ]; then workdir=/app; else workdir="$(pwd)"; fi; '
'fi; '
f'instruction="$(cat {IN_CONTAINER_INPUT}/instruction.txt)"; '
f'{IN_CONTAINER_BINARY} --quiet --no-update --no-selfdev '
'--provider "$JCODE_PROVIDER" --model "$JCODE_MODEL" '
'-C "$workdir" run --ndjson "$instruction" '
f'> {IN_CONTAINER_OUTPUT}/events.ndjson 2> {IN_CONTAINER_OUTPUT}/stderr.txt'
),
env=env
)
(self.logs_dir / "exec_stdout.txt").write_text(result.stdout or "")
(self.logs_dir / "exec_stderr.txt").write_text(result.stderr or "")
(self.logs_dir / "exec_return_code.txt").write_text(str(result.return_code))
try:
await environment.download_dir(IN_CONTAINER_OUTPUT, self.logs_dir / "jcode-output")
except Exception as e: # noqa: BLE001
(self.logs_dir / "download_error.txt").write_text(str(e))
metadata: dict[str, Any] = {
"return_code": result.return_code,
"provider": self._provider_arg,
"model": self._jcode_model,
"jcode_binary": str(JCODE_BINARY),
}
output_dir = self.logs_dir / "jcode-output"
payload = _load_final_payload(output_dir)
if payload is not None:
usage = payload.get("usage") or {}
context.n_input_tokens = usage.get("input_tokens")
context.n_output_tokens = usage.get("output_tokens")
cache_read = usage.get("cache_read_input_tokens")
cache_create = usage.get("cache_creation_input_tokens")
if isinstance(cache_read, int) and isinstance(cache_create, int):
context.n_cache_tokens = cache_read + cache_create
elif isinstance(cache_read, int):
context.n_cache_tokens = cache_read
metadata["jcode_result"] = payload
context.metadata = metadata
+260
View File
@@ -0,0 +1,260 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import os
import re
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Iterable
DEFAULT_SOCKET = f"/run/user/{os.getuid()}/jcode.sock"
@dataclass
class ProcMem:
pid: int
role: str
cmd: str
session_id: str | None
socket_path: str | None
rss_mb: float
pss_mb: float
anon_mb: float
shared_clean_mb: float
private_clean_mb: float
private_dirty_mb: float
swap_mb: float
@dataclass
class Totals:
count: int
rss_mb: float
pss_mb: float
anon_mb: float
private_dirty_mb: float
shared_clean_mb: float
swap_mb: float
SMAPS_KEYS = {
"Rss": "rss_mb",
"Pss": "pss_mb",
"Anonymous": "anon_mb",
"Shared_Clean": "shared_clean_mb",
"Private_Clean": "private_clean_mb",
"Private_Dirty": "private_dirty_mb",
"Swap": "swap_mb",
}
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="Summarize jcode server/client process memory using smaps_rollup")
p.add_argument("--json", action="store_true", help="Print JSON instead of a human summary")
p.add_argument(
"--include-aux",
action="store_true",
help="Include non-default-socket helper/test jcode processes in the output",
)
p.add_argument(
"--socket",
default=DEFAULT_SOCKET,
help=f"Main jcode socket to treat as the primary instance (default: {DEFAULT_SOCKET})",
)
return p.parse_args()
def read_text(path: Path, binary: bool = False) -> str | None:
try:
if binary:
return path.read_bytes().replace(b"\x00", b" ").decode("utf-8", "ignore").strip()
return path.read_text().strip()
except Exception:
return None
def read_argv(path: Path) -> list[str] | None:
try:
raw = path.read_bytes()
except Exception:
return None
if not raw:
return None
return [part.decode("utf-8", "ignore") for part in raw.split(b"\x00") if part]
def parse_socket_path(cmd: str) -> str | None:
m = re.search(r"(?:^| )--socket\s+(\S+)", cmd)
return m.group(1) if m else None
def parse_session_id(cmd: str) -> str | None:
m = re.search(r"--resume\s+(session_[^\s]+)", cmd)
return m.group(1) if m else None
def first_non_option(argv: list[str]) -> str | None:
skip_next = False
for arg in argv[1:]:
if skip_next:
skip_next = False
continue
if arg in {"--socket", "--resume", "-C", "--chdir", "--model", "--provider"}:
skip_next = True
continue
if arg.startswith("-"):
continue
return arg
return None
def classify_process(argv: list[str], cmd: str, main_socket: str) -> tuple[str | None, bool]:
argv0 = Path(argv[0]).name if argv else ""
if not argv0.startswith("jcode"):
return None, False
if "jcode serve" in cmd:
socket_path = parse_socket_path(cmd) or main_socket
if socket_path == main_socket:
return "server", True
return "server_aux", False
socket_path = parse_socket_path(cmd) or main_socket
is_main = socket_path == main_socket
if "cargo build" in cmd or "rustc --crate-name jcode" in cmd or "handle_resume_session" in cmd:
return None, False
subcommand = first_non_option(argv)
if subcommand in {"auth", "login", "logout", "serve", "self-dev"} and "--resume session_" not in cmd:
if subcommand == "self-dev" and "--resume session_" in cmd:
pass
else:
return None, False
if "--resume session_" in cmd or " --fresh-spawn " in cmd:
return ("client_session" if is_main else "client_aux"), is_main
return ("client_interactive" if is_main else "client_aux"), is_main
def parse_smaps_rollup(pid: int) -> dict[str, float] | None:
path = Path(f"/proc/{pid}/smaps_rollup")
txt = read_text(path)
if not txt:
return None
out = {value: 0.0 for value in SMAPS_KEYS.values()}
for line in txt.splitlines():
for key, out_key in SMAPS_KEYS.items():
if line.startswith(f"{key}:"):
parts = line.split()
if len(parts) >= 2:
out[out_key] = round(int(parts[1]) / 1024.0, 1)
return out
def iter_jcode_processes(main_socket: str, include_aux: bool) -> Iterable[ProcMem]:
for pid_dir in Path("/proc").iterdir():
if not pid_dir.name.isdigit():
continue
pid = int(pid_dir.name)
argv = read_argv(pid_dir / "cmdline")
if not argv:
continue
cmd = " ".join(argv)
if "jcode" not in cmd:
continue
role, is_main = classify_process(argv, cmd, main_socket)
if role is None:
continue
if not include_aux and not is_main and role not in {"server"}:
continue
smaps = parse_smaps_rollup(pid)
if not smaps:
continue
yield ProcMem(
pid=pid,
role=role,
cmd=cmd,
session_id=parse_session_id(cmd),
socket_path=parse_socket_path(cmd),
**smaps,
)
def sum_totals(procs: list[ProcMem]) -> Totals:
return Totals(
count=len(procs),
rss_mb=round(sum(p.rss_mb for p in procs), 1),
pss_mb=round(sum(p.pss_mb for p in procs), 1),
anon_mb=round(sum(p.anon_mb for p in procs), 1),
private_dirty_mb=round(sum(p.private_dirty_mb for p in procs), 1),
shared_clean_mb=round(sum(p.shared_clean_mb for p in procs), 1),
swap_mb=round(sum(p.swap_mb for p in procs), 1),
)
def print_human(server: list[ProcMem], clients: list[ProcMem], aux: list[ProcMem]) -> None:
def print_group(name: str, procs: list[ProcMem]) -> None:
totals = sum_totals(procs)
print(f"\n{name} ({totals.count})")
print("-" * len(f"{name} ({totals.count})"))
print(
f"total: RSS {totals.rss_mb:.1f} MB | PSS {totals.pss_mb:.1f} MB | "
f"Anon {totals.anon_mb:.1f} MB | Private dirty {totals.private_dirty_mb:.1f} MB"
)
for p in sorted(procs, key=lambda x: (x.role, -x.pss_mb, x.pid)):
label = p.session_id or p.role
print(
f" pid {p.pid:<7} {label:<48} RSS {p.rss_mb:>6.1f} | PSS {p.pss_mb:>6.1f} | "
f"Anon {p.anon_mb:>6.1f} | PrivDirty {p.private_dirty_mb:>6.1f}"
)
print_group("Server", server)
print_group("Clients", clients)
if aux:
print_group("Auxiliary", aux)
grand = sum_totals(server + clients)
print("\nPrimary total (server + clients)")
print("--------------------------------")
print(
f"RSS {grand.rss_mb:.1f} MB | PSS {grand.pss_mb:.1f} MB | "
f"Anon {grand.anon_mb:.1f} MB | Private dirty {grand.private_dirty_mb:.1f} MB | "
f"Shared clean {grand.shared_clean_mb:.1f} MB"
)
print(
f"Overcount if you sum RSS instead of PSS: {round(grand.rss_mb - grand.pss_mb, 1):.1f} MB"
)
def main() -> int:
args = parse_args()
procs = list(iter_jcode_processes(args.socket, args.include_aux))
server = [p for p in procs if p.role == "server"]
clients = [p for p in procs if p.role.startswith("client_") and p.role != "client_aux"]
aux = [p for p in procs if p.role.endswith("aux")]
if args.json:
payload = {
"socket": args.socket,
"server": [asdict(p) for p in sorted(server, key=lambda x: x.pid)],
"clients": [asdict(p) for p in sorted(clients, key=lambda x: x.pid)],
"auxiliary": [asdict(p) for p in sorted(aux, key=lambda x: x.pid)],
"totals": {
"server": asdict(sum_totals(server)),
"clients": asdict(sum_totals(clients)),
"primary": asdict(sum_totals(server + clients)),
"auxiliary": asdict(sum_totals(aux)),
},
}
print(json.dumps(payload, indent=2))
else:
print_human(server, clients, aux)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+341
View File
@@ -0,0 +1,341 @@
#!/usr/bin/env python3
"""
jcode Live Monitor - Real-time activity dashboard
Connects to jcode's debug socket and displays live streaming events.
Run jcode serve in one terminal, then this monitor in another.
Usage: ./jcode_monitor.py [--socket PATH]
"""
import json
import os
import socket
import sys
import time
from dataclasses import dataclass, field
from typing import Optional
# ANSI color codes
class Colors:
RESET = "\033[0m"
BOLD = "\033[1m"
DIM = "\033[2m"
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
BLUE = "\033[34m"
MAGENTA = "\033[35m"
CYAN = "\033[36m"
WHITE = "\033[37m"
BG_BLUE = "\033[44m"
# Clear screen and move cursor
CLEAR = "\033[2J\033[H"
CLEAR_LINE = "\033[2K"
@dataclass
class MonitorState:
"""Current state of the monitor"""
connected: bool = False
session_id: str = ""
is_processing: bool = False
input_tokens: int = 0
output_tokens: int = 0
current_text: str = ""
active_tools: dict = field(default_factory=dict) # id -> name
tool_history: list = field(default_factory=list) # recent tool completions
events_received: int = 0
last_event_time: float = 0
errors: list = field(default_factory=list)
def get_socket_path() -> str:
"""Get the jcode debug socket path"""
runtime_dir = os.environ.get("XDG_RUNTIME_DIR", f"/run/user/{os.getuid()}")
return os.path.join(runtime_dir, "jcode-debug.sock")
def connect_to_socket(path: str) -> Optional[socket.socket]:
"""Connect to the Unix socket"""
try:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(path)
sock.setblocking(False)
return sock
except Exception as e:
return None
def send_request(sock: socket.socket, request: dict) -> bool:
"""Send a JSON request to the socket"""
try:
data = json.dumps(request) + "\n"
sock.send(data.encode())
return True
except:
return False
def read_events(sock: socket.socket) -> list:
"""Read available events from socket (non-blocking)"""
events = []
buffer = ""
try:
while True:
data = sock.recv(4096)
if not data:
break
buffer += data.decode()
while "\n" in buffer:
line, buffer = buffer.split("\n", 1)
if line.strip():
try:
events.append(json.loads(line))
except json.JSONDecodeError:
pass
except BlockingIOError:
pass
except:
pass
return events
def format_tokens(n: int) -> str:
"""Format token count with color based on size"""
if n == 0:
return f"{Colors.DIM}0{Colors.RESET}"
elif n < 1000:
return f"{Colors.GREEN}{n}{Colors.RESET}"
elif n < 10000:
return f"{Colors.YELLOW}{n:,}{Colors.RESET}"
else:
return f"{Colors.RED}{n:,}{Colors.RESET}"
def format_tool(name: str, status: str = "active") -> str:
"""Format a tool name with appropriate color"""
if status == "active":
return f"{Colors.CYAN}{Colors.BOLD}{name}{Colors.RESET}"
elif status == "done":
return f"{Colors.GREEN}{name}{Colors.RESET}"
elif status == "error":
return f"{Colors.RED}{name}{Colors.RESET}"
return name
def truncate(s: str, max_len: int) -> str:
"""Truncate string with ellipsis"""
if len(s) <= max_len:
return s
return s[:max_len-3] + "..."
def render_dashboard(state: MonitorState, width: int = 80):
"""Render the monitoring dashboard"""
lines = []
# Header
header = f" JCODE MONITOR "
padding = (width - len(header)) // 2
lines.append(f"{Colors.BG_BLUE}{Colors.WHITE}{Colors.BOLD}{' ' * padding}{header}{' ' * padding}{Colors.RESET}")
lines.append("")
# Connection status
if state.connected:
status = f"{Colors.GREEN}CONNECTED{Colors.RESET}"
session = f" | Session: {Colors.DIM}{state.session_id[:8]}...{Colors.RESET}" if state.session_id else ""
else:
status = f"{Colors.RED}DISCONNECTED{Colors.RESET}"
session = ""
lines.append(f" Status: {status}{session}")
# Processing state
if state.is_processing:
proc = f"{Colors.YELLOW}{Colors.BOLD}PROCESSING{Colors.RESET}"
else:
proc = f"{Colors.DIM}idle{Colors.RESET}"
lines.append(f" State: {proc}")
lines.append("")
# Token usage
lines.append(f" {Colors.BOLD}Tokens{Colors.RESET}")
lines.append(f" Input: {format_tokens(state.input_tokens)}")
lines.append(f" Output: {format_tokens(state.output_tokens)}")
lines.append("")
# Active tools
lines.append(f" {Colors.BOLD}Active Tools{Colors.RESET}")
if state.active_tools:
for tool_id, tool_name in state.active_tools.items():
lines.append(f" {Colors.CYAN}>{Colors.RESET} {format_tool(tool_name)}")
else:
lines.append(f" {Colors.DIM}(none){Colors.RESET}")
lines.append("")
# Recent tool completions
lines.append(f" {Colors.BOLD}Recent Tools{Colors.RESET}")
if state.tool_history:
for item in state.tool_history[-5:]:
name, success, output = item
status = "done" if success else "error"
output_preview = truncate(output.replace("\n", " "), 40)
lines.append(f" {format_tool(name, status)}: {Colors.DIM}{output_preview}{Colors.RESET}")
else:
lines.append(f" {Colors.DIM}(none){Colors.RESET}")
lines.append("")
# Current streaming text
lines.append(f" {Colors.BOLD}Streaming Text{Colors.RESET}")
if state.current_text:
# Show last few lines of streaming text
text_lines = state.current_text.split("\n")[-4:]
for tl in text_lines:
lines.append(f" {Colors.WHITE}{truncate(tl, width-6)}{Colors.RESET}")
else:
lines.append(f" {Colors.DIM}(waiting...){Colors.RESET}")
lines.append("")
# Stats
lines.append(f" {Colors.DIM}Events: {state.events_received} | Last: {time.time() - state.last_event_time:.1f}s ago{Colors.RESET}")
# Errors
if state.errors:
lines.append("")
lines.append(f" {Colors.RED}{Colors.BOLD}Errors{Colors.RESET}")
for err in state.errors[-3:]:
lines.append(f" {Colors.RED}{truncate(err, width-6)}{Colors.RESET}")
# Print with clear
print(Colors.CLEAR, end="")
print("\n".join(lines))
def process_event(event: dict, state: MonitorState):
"""Process a single event and update state"""
state.events_received += 1
state.last_event_time = time.time()
event_type = event.get("type", "")
if event_type == "ack":
state.is_processing = True
elif event_type == "text_delta":
state.current_text += event.get("text", "")
# Keep last 2000 chars
if len(state.current_text) > 2000:
state.current_text = state.current_text[-2000:]
elif event_type == "tool_start":
tool_id = event.get("id", "")
tool_name = event.get("name", "unknown")
state.active_tools[tool_id] = tool_name
elif event_type == "tool_exec":
pass # Tool is executing, still active
elif event_type == "tool_done":
tool_id = event.get("id", "")
tool_name = event.get("name", "unknown")
output = event.get("output", "")
error = event.get("error")
# Remove from active
state.active_tools.pop(tool_id, None)
# Add to history
state.tool_history.append((tool_name, error is None, output[:100] if output else "(empty)"))
# Keep last 20
state.tool_history = state.tool_history[-20:]
elif event_type == "tokens":
state.input_tokens = event.get("input", 0)
state.output_tokens = event.get("output", 0)
elif event_type == "done":
state.is_processing = False
state.current_text = "" # Clear for next turn
elif event_type == "error":
state.errors.append(event.get("message", "unknown error"))
state.errors = state.errors[-10:]
elif event_type == "pong":
pass # Health check response
elif event_type == "state":
state.session_id = event.get("session_id", "")
state.is_processing = event.get("is_processing", False)
elif event_type == "session":
state.session_id = event.get("session_id", "")
def main():
"""Main monitor loop"""
socket_path = sys.argv[1] if len(sys.argv) > 1 else get_socket_path()
print(f"Connecting to {socket_path}...")
state = MonitorState()
sock = None
request_id = 1
last_ping = 0
while True:
try:
# Connect if needed
if sock is None:
sock = connect_to_socket(socket_path)
if sock:
state.connected = True
# Subscribe to events
send_request(sock, {"type": "subscribe", "id": request_id})
request_id += 1
# Get initial state
send_request(sock, {"type": "state", "id": request_id})
request_id += 1
else:
state.connected = False
# Read events
if sock:
events = read_events(sock)
for event in events:
process_event(event, state)
# Periodic ping
if time.time() - last_ping > 5:
send_request(sock, {"type": "ping", "id": request_id})
request_id += 1
last_ping = time.time()
# Render dashboard
render_dashboard(state)
# Small delay
time.sleep(0.1)
except KeyboardInterrupt:
print(f"\n{Colors.DIM}Exiting...{Colors.RESET}")
break
except BrokenPipeError:
state.connected = False
sock = None
except Exception as e:
state.errors.append(str(e))
time.sleep(1)
if sock:
sock.close()
if __name__ == "__main__":
main()
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
# Shared helper that ensures a directory is on PATH for bash, zsh, and fish.
#
# Usage:
# source "$(dirname "$0")/lib/configure_path.sh"
# jcode_configure_path "/path/to/install/dir"
#
# This is intentionally POSIX-friendly and side-effect free until you call
# jcode_configure_path. It is kept in sync with the inline copy in install.sh,
# which must stay self-contained because it is run via `curl ... | bash`.
# Configure PATH for bash, zsh and fish.
# jcode_configure_path <install-dir> [report-fn]
# report-fn (optional) is called with a human-readable summary string.
jcode_configure_path() {
_jcp_install_dir="$1"
_jcp_report="${2:-}"
_jcp_path_line="export PATH=\"$_jcp_install_dir:\$PATH\""
_jcp_added=""
_jcp_have() { command -v "$1" >/dev/null 2>&1; }
# Append the POSIX (bash/zsh/sh) PATH line to an rc file, idempotently.
# _jcp_posix_rc <rc-file> <create:yes|no>
# With create=yes the file (and parent dir) is created if missing; with
# create=no we only touch files that already exist, so we never change how a
# login shell resolves its startup files (e.g. creating ~/.bash_profile would
# stop bash from reading ~/.profile).
_jcp_posix_rc() {
_rc="$1"; _create="$2"
if [ ! -f "$_rc" ]; then
[ "$_create" = "yes" ] || return 0
mkdir -p "$(dirname "$_rc")"
fi
if ! grep -qF "$_jcp_install_dir" "$_rc" 2>/dev/null; then
printf '\n# Added by jcode installer\n%s\n' "$_jcp_path_line" >> "$_rc"
_jcp_added="$_jcp_added $_rc"
fi
}
# fish uses its own syntax and does not read POSIX rc files.
_jcp_fish_rc() {
_create="$1"
_rc="${XDG_CONFIG_HOME:-$HOME/.config}/fish/config.fish"
if [ ! -f "$_rc" ]; then
[ "$_create" = "yes" ] || return 0
mkdir -p "$(dirname "$_rc")"
fi
if ! grep -qF "$_jcp_install_dir" "$_rc" 2>/dev/null; then
{
printf '\n# Added by jcode installer\n'
printf 'if not contains "%s" $PATH\n' "$_jcp_install_dir"
printf ' set -gx PATH "%s" $PATH\n' "$_jcp_install_dir"
printf 'end\n'
} >> "$_rc"
_jcp_added="$_jcp_added $_rc"
fi
}
# zsh: ~/.zshenv is read for every zsh invocation (login, interactive and
# scripts), so it is the most reliable single place to export PATH.
if _jcp_have zsh || [ "$(uname -s)" = "Darwin" ] || [ -f "$HOME/.zshenv" ] || [ -f "$HOME/.zshrc" ]; then
_jcp_posix_rc "$HOME/.zshenv" yes
fi
# bash: ~/.bashrc for interactive shells, ~/.profile for login shells.
if _jcp_have bash || [ -f "$HOME/.bashrc" ] || [ -f "$HOME/.bash_profile" ]; then
_jcp_posix_rc "$HOME/.bashrc" yes
fi
_jcp_posix_rc "$HOME/.profile" yes
# fish: only set it up when fish is installed or already configured.
if _jcp_have fish || [ -f "${XDG_CONFIG_HOME:-$HOME/.config}/fish/config.fish" ]; then
_jcp_fish_rc yes
fi
# Also patch other common startup files when they already exist.
for _rc in "$HOME/.zshrc" "$HOME/.zprofile" "$HOME/.bash_profile"; do
_jcp_posix_rc "$_rc" no
done
if [ -n "$_jcp_added" ]; then
if [ -n "$_jcp_report" ] && command -v "$_jcp_report" >/dev/null 2>&1; then
"$_jcp_report" "Added $_jcp_install_dir to PATH in:$_jcp_added"
else
printf 'Added %s to PATH in:%s\n' "$_jcp_install_dir" "$_jcp_added"
fi
fi
}
+339
View File
@@ -0,0 +1,339 @@
#!/usr/bin/env bash
# memory_probe.sh - Reproducible memory probe harness for jcode TUI clients.
#
# Spawns a headless tester TUI via the debug socket (tester:spawn) that resumes
# a LARGE existing session, then records RSS/PSS/rss_anon at fixed phases:
#
# fresh - right after spawn, before the event loop settles
# post_connect - first successful client debug "state" response
# post_history_loaded - display messages applied and stable
# idle - after --idle-secs (default 30) of idle time
# post_trim - after a forced allocator trim (gdb malloc_trim(0))
#
# Each phase emits exactly ONE compact, key-sorted JSON line on stdout so runs
# are diffable (all human logging goes to stderr). Memory is sampled from
# /proc/<pid>/status + /proc/<pid>/smaps_rollup, plus the client's own
# aggregate memory profile (the same handler that serves `client:memory`),
# fetched through the tester's file-based debug channel.
#
# Usage:
# scripts/memory_probe.sh [options]
#
# Options:
# --session <id> Session to resume (default: session_hog_1783086065415_4ad4ae66cd43dd5b)
# --idle-secs <n> Idle wait before the idle phase (default: 30)
# --binary <path> Client binary (default: ~/.jcode/builds/current/jcode)
# --jcode <path> jcode CLI used for `jcode debug ...` (default: ~/.local/bin/jcode)
# --cwd <path> Working directory for the tester (default: $HOME)
# --skip-trim Skip the forced-trim phase
# --keep Do not stop the tester on exit (for manual inspection)
# -h | --help Show this help
#
# Requirements: a running jcode server with the debug socket enabled, jq,
# and (for the trim phase) gdb. Trim prefers `sudo -n gdb` because
# kernel.yama.ptrace_scope=1 blocks same-uid attach to non-children.
set -euo pipefail
SESSION_ID="session_hog_1783086065415_4ad4ae66cd43dd5b"
IDLE_SECS=30
JCODE_BIN="${JCODE_BIN:-$HOME/.local/bin/jcode}"
CLIENT_BIN="$HOME/.jcode/builds/current/jcode"
TESTER_CWD="$HOME"
SKIP_TRIM=0
KEEP_TESTER=0
usage() { sed -n '2,35p' "$0" | sed 's/^# \{0,1\}//'; }
while [[ $# -gt 0 ]]; do
case "$1" in
--session) SESSION_ID="$2"; shift 2 ;;
--idle-secs) IDLE_SECS="$2"; shift 2 ;;
--binary) CLIENT_BIN="$2"; shift 2 ;;
--jcode) JCODE_BIN="$2"; shift 2 ;;
--cwd) TESTER_CWD="$2"; shift 2 ;;
--skip-trim) SKIP_TRIM=1; shift ;;
--keep) KEEP_TESTER=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;;
esac
done
log() { printf '[memory_probe] %s\n' "$*" >&2; }
die() { log "FATAL: $*"; exit 1; }
command -v jq >/dev/null || die "jq is required"
[[ -x "$JCODE_BIN" ]] || die "jcode CLI not found at $JCODE_BIN"
[[ -x "$CLIENT_BIN" ]] || CLIENT_BIN="$(command -v jcode)" || die "client binary not found"
[[ -f "$HOME/.jcode/sessions/${SESSION_ID}.json" ]] \
|| log "WARN: $HOME/.jcode/sessions/${SESSION_ID}.json not found; resume may create a new session"
RUN_ID="probe_$(date -u +%Y%m%dT%H%M%SZ)_$$"
SESSION_FILE="$HOME/.jcode/sessions/${SESSION_ID}.json"
SESSION_BYTES=$(stat -c %s "$SESSION_FILE" 2>/dev/null || echo 0)
TESTER_ID=""
TESTER_PID=""
CMD_PATH=""
RESP_PATH=""
WRAPPER=""
SPAWN_EPOCH_MS=""
cleanup() {
local rc=$?
if [[ -n "$TESTER_ID" && "$KEEP_TESTER" -ne 1 ]]; then
"$JCODE_BIN" debug "tester:${TESTER_ID}:stop" >/dev/null 2>&1 || true
log "stopped tester $TESTER_ID"
elif [[ -n "$TESTER_ID" ]]; then
log "kept tester $TESTER_ID (pid $TESTER_PID) running (--keep)"
fi
[[ -n "$WRAPPER" ]] && rm -f "$WRAPPER"
exit $rc
}
trap cleanup EXIT INT TERM
now_ms() { date +%s%3N; }
# ---------------------------------------------------------------------------
# /proc sampling: emits a JSON object with kB values from status+smaps_rollup.
# ---------------------------------------------------------------------------
proc_kb() { # file key
awk -v k="$2" '$1 == k ":" { print $2; found=1; exit } END { if (!found) print "null" }' "$1" 2>/dev/null || echo null
}
sample_proc() { # pid -> json on stdout
local pid="$1"
local status="/proc/$pid/status" rollup="/proc/$pid/smaps_rollup"
[[ -r "$status" ]] || { echo null; return 1; }
local tmp_rollup
tmp_rollup="$(mktemp)"
# snapshot smaps_rollup once so all fields come from the same read
cat "$rollup" > "$tmp_rollup" 2>/dev/null || true
jq -n -S \
--argjson rss_kb "$(proc_kb "$status" VmRSS)" \
--argjson rss_anon_kb "$(proc_kb "$status" RssAnon)" \
--argjson rss_file_kb "$(proc_kb "$status" RssFile)" \
--argjson rss_shmem_kb "$(proc_kb "$status" RssShmem)" \
--argjson vm_hwm_kb "$(proc_kb "$status" VmHWM)" \
--argjson vm_swap_kb "$(proc_kb "$status" VmSwap)" \
--argjson pss_kb "$(proc_kb "$tmp_rollup" Pss)" \
--argjson pss_anon_kb "$(proc_kb "$tmp_rollup" Pss_Anon)" \
--argjson pss_file_kb "$(proc_kb "$tmp_rollup" Pss_File)" \
--argjson private_clean_kb "$(proc_kb "$tmp_rollup" Private_Clean)" \
--argjson private_dirty_kb "$(proc_kb "$tmp_rollup" Private_Dirty)" \
--argjson shared_clean_kb "$(proc_kb "$tmp_rollup" Shared_Clean)" \
--argjson shared_dirty_kb "$(proc_kb "$tmp_rollup" Shared_Dirty)" \
--argjson swap_kb "$(proc_kb "$tmp_rollup" Swap)" \
'{rss_kb: $rss_kb, rss_anon_kb: $rss_anon_kb, rss_file_kb: $rss_file_kb,
rss_shmem_kb: $rss_shmem_kb, vm_hwm_kb: $vm_hwm_kb, vm_swap_kb: $vm_swap_kb,
pss_kb: $pss_kb, pss_anon_kb: $pss_anon_kb, pss_file_kb: $pss_file_kb,
private_clean_kb: $private_clean_kb, private_dirty_kb: $private_dirty_kb,
shared_clean_kb: $shared_clean_kb, shared_dirty_kb: $shared_dirty_kb,
swap_kb: $swap_kb}'
local rc=$?
rm -f "$tmp_rollup"
return $rc
}
# ---------------------------------------------------------------------------
# Tester file-based debug channel: write command, poll response until it is
# non-empty and stable (guards against partially written responses).
# ---------------------------------------------------------------------------
tester_cmd() { # command timeout_secs -> response on stdout, rc 1 on timeout
local cmd="$1" timeout_s="${2:-30}"
rm -f "$RESP_PATH"
printf '%s' "$cmd" > "$CMD_PATH"
local deadline=$(( $(date +%s) + timeout_s ))
local prev="" cur=""
while (( $(date +%s) < deadline )); do
if [[ -s "$RESP_PATH" ]]; then
cur="$(cat "$RESP_PATH" 2>/dev/null || true)"
if [[ -n "$cur" && "$cur" == "$prev" ]]; then
rm -f "$RESP_PATH"
printf '%s' "$cur"
return 0
fi
prev="$cur"
fi
sleep 0.2
done
return 1
}
tester_json_cmd() { # command timeout_secs -> valid JSON on stdout or rc 1
local out
out="$(tester_cmd "$1" "${2:-30}")" || return 1
jq -e . >/dev/null 2>&1 <<<"$out" || return 1
printf '%s' "$out"
}
# Client aggregate memory profile subset (same handler as `client:memory`).
client_memory_subset() { # timeout -> compact json or "null"
local raw
if ! raw="$(tester_json_cmd "memory" "${1:-90}")"; then
echo null
return 0
fi
jq -c -S '{
rss_bytes: (.process.rss_bytes // null),
pss_bytes: (.process.os.pss_bytes // null),
rss_anon_bytes: (.process.os.rss_anon_bytes // null),
allocator: (.process.allocator.name // null),
allocated_bytes: (.process.allocator.stats.allocated_bytes // null),
session_json_bytes: (.session.totals.json_bytes // null),
provider_messages_count: (.ui.provider_messages.count // null),
provider_messages_json_bytes: (.ui.provider_messages.json_bytes // null),
display_messages_count: (.ui.display_messages.count // null),
display_messages_estimate_bytes: (.ui.display_messages.estimate_bytes // null)
}' <<<"$raw" 2>/dev/null || echo null
}
# ---------------------------------------------------------------------------
# Phase emitter: one compact key-sorted JSON line on stdout.
# ---------------------------------------------------------------------------
emit_phase() { # phase proc_json client_json extras_json
local phase="$1" proc_json="$2" client_json="$3" extras_json="${4:-{\}}"
local ts elapsed_ms
ts="$(now_ms)"
elapsed_ms=$(( ts - SPAWN_EPOCH_MS ))
jq -c -S -n \
--arg probe "jcode_memory_probe" \
--arg schema "1" \
--arg run_id "$RUN_ID" \
--arg phase "$phase" \
--arg session "$SESSION_ID" \
--arg tester_id "$TESTER_ID" \
--argjson session_file_bytes "$SESSION_BYTES" \
--argjson pid "${TESTER_PID:-null}" \
--argjson ts_ms "$ts" \
--argjson elapsed_ms "$elapsed_ms" \
--argjson proc "$proc_json" \
--argjson client "$client_json" \
--argjson extras "$extras_json" \
'{probe: $probe, schema: ($schema | tonumber), run_id: $run_id, phase: $phase,
session: $session, session_file_bytes: $session_file_bytes,
tester_id: $tester_id, pid: $pid, ts_ms: $ts_ms, elapsed_ms: $elapsed_ms,
proc: $proc, client: $client} + $extras'
}
# ---------------------------------------------------------------------------
# Forced trim: call malloc_trim(0) inside the client via gdb.
# ---------------------------------------------------------------------------
force_trim() { # pid -> prints method used ("gdb_sudo" | "gdb" | "unavailable")
local pid="$1"
if ! command -v gdb >/dev/null; then
echo unavailable
return 0
fi
local gdb_args=(--batch -p "$pid" -ex 'call (int) malloc_trim(0)' -ex detach -ex quit)
if sudo -n true 2>/dev/null; then
if timeout 60 sudo -n gdb "${gdb_args[@]}" >/dev/null 2>&1; then
echo gdb_sudo
return 0
fi
fi
if timeout 60 gdb "${gdb_args[@]}" >/dev/null 2>&1; then
echo gdb
return 0
fi
echo unavailable
}
# ===========================================================================
# 1. Spawn headless tester resuming the target session.
# ===========================================================================
WRAPPER="$(mktemp /tmp/jcode_memory_probe_wrapper.XXXXXX.sh)"
cat > "$WRAPPER" <<EOF
#!/usr/bin/env bash
exec "$CLIENT_BIN" --resume "$SESSION_ID" --no-update "\$@"
EOF
chmod +x "$WRAPPER"
log "spawning headless tester (binary wrapper resumes $SESSION_ID, ${SESSION_BYTES} byte session file)"
SPAWN_OUT="$("$JCODE_BIN" debug "tester:spawn {\"binary\":\"$WRAPPER\",\"cwd\":\"$TESTER_CWD\",\"cols\":120,\"rows\":40}")" \
|| die "tester:spawn failed: $SPAWN_OUT"
SPAWN_EPOCH_MS="$(now_ms)"
TESTER_ID="$(jq -r '.id // empty' <<<"$SPAWN_OUT")"
TESTER_PID="$(jq -r '.pid // empty' <<<"$SPAWN_OUT")"
[[ -n "$TESTER_ID" && -n "$TESTER_PID" ]] || die "could not parse tester:spawn response: $SPAWN_OUT"
log "tester $TESTER_ID pid $TESTER_PID"
TESTER_INFO="$("$JCODE_BIN" debug tester:list | jq -c --arg id "$TESTER_ID" '.[] | select(.id == $id)')"
CMD_PATH="$(jq -r '.debug_cmd_path' <<<"$TESTER_INFO")"
RESP_PATH="$(jq -r '.debug_response_path' <<<"$TESTER_INFO")"
[[ -n "$CMD_PATH" && -n "$RESP_PATH" ]] || die "could not resolve tester debug paths"
# ===========================================================================
# 2. Phase: fresh (immediately after spawn, before the event loop settles).
# ===========================================================================
sleep 0.2
PROC_JSON="$(sample_proc "$TESTER_PID")" || die "tester process $TESTER_PID vanished during fresh phase"
emit_phase "fresh" "$PROC_JSON" null '{}'
# ===========================================================================
# 3. Phase: post_connect (first successful client debug state response).
# ===========================================================================
STATE_JSON=""
CONNECT_DEADLINE=$(( $(date +%s) + 60 ))
while (( $(date +%s) < CONNECT_DEADLINE )); do
if STATE_JSON="$(tester_json_cmd "state" 5)"; then
break
fi
STATE_JSON=""
done
[[ -n "$STATE_JSON" ]] || die "tester never answered a state command (stderr: $(jq -r '.stderr_path' <<<"$TESTER_INFO"))"
PROC_JSON="$(sample_proc "$TESTER_PID")"
DISPLAY_COUNT="$(jq -r '.display_messages // 0' <<<"$STATE_JSON")"
emit_phase "post_connect" "$PROC_JSON" null "{\"display_messages\": $DISPLAY_COUNT}"
log "connected; display_messages=$DISPLAY_COUNT"
# ===========================================================================
# 4. Phase: post_history_loaded (display messages present and stable).
# ===========================================================================
HISTORY_DEADLINE=$(( $(date +%s) + 180 ))
PREV_COUNT=-1
STABLE=0
while (( $(date +%s) < HISTORY_DEADLINE )); do
if STATE_JSON="$(tester_json_cmd "state" 10)"; then
DISPLAY_COUNT="$(jq -r '.display_messages // 0' <<<"$STATE_JSON")"
PROCESSING="$(jq -r '.processing // false' <<<"$STATE_JSON")"
if [[ "$DISPLAY_COUNT" -gt 0 && "$PROCESSING" == "false" && "$DISPLAY_COUNT" -eq "$PREV_COUNT" ]]; then
STABLE=$(( STABLE + 1 ))
[[ $STABLE -ge 2 ]] && break
else
STABLE=0
fi
PREV_COUNT="$DISPLAY_COUNT"
fi
sleep 1
done
[[ "$PREV_COUNT" -gt 0 ]] || die "history never loaded (display_messages=$PREV_COUNT)"
PROC_JSON="$(sample_proc "$TESTER_PID")"
CLIENT_JSON="$(client_memory_subset 90)"
emit_phase "post_history_loaded" "$PROC_JSON" "$CLIENT_JSON" "{\"display_messages\": $PREV_COUNT}"
log "history loaded; display_messages=$PREV_COUNT"
# ===========================================================================
# 5. Phase: idle (after --idle-secs of no activity).
# ===========================================================================
log "idling for ${IDLE_SECS}s"
sleep "$IDLE_SECS"
PROC_JSON="$(sample_proc "$TESTER_PID")"
CLIENT_JSON="$(client_memory_subset 90)"
emit_phase "idle" "$PROC_JSON" "$CLIENT_JSON" "{\"idle_secs\": $IDLE_SECS}"
# ===========================================================================
# 6. Phase: post_trim (forced allocator trim via gdb malloc_trim(0)).
# ===========================================================================
if [[ "$SKIP_TRIM" -eq 1 ]]; then
log "skipping trim phase (--skip-trim)"
else
TRIM_METHOD="$(force_trim "$TESTER_PID")"
log "forced trim via: $TRIM_METHOD"
sleep 1
PROC_JSON="$(sample_proc "$TESTER_PID")"
CLIENT_JSON="$(client_memory_subset 90)"
emit_phase "post_trim" "$PROC_JSON" "$CLIENT_JSON" "{\"trim_method\": \"$TRIM_METHOD\"}"
fi
log "done (run_id=$RUN_ID)"
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env bash
# memory_regression_gate.sh - Automated pass/fail gate over client memory.
#
# Wraps scripts/memory_probe.sh with thresholds so memory regressions fail
# loudly (nightly job, pre-release check) instead of being rediscovered when
# the machine starts swapping.
#
# Baseline (2026-07-04, selfdev build, 8 MB / 322-message hog session), after
# the retention + live-heap work (mmap-threshold pin, idle trim, syntect
# regex-onig backend, inline-image payload release):
#
# idle rss_anon: ~38 MB (was ~69 MB)
# idle live heap (allocated): ~30 MB (was ~60 MB)
#
# Thresholds sit roughly halfway between the fixed and regressed values, so
# normal variance passes but a real regression toward old behavior fails.
#
# Usage:
# scripts/memory_regression_gate.sh [--session <id>] [--idle-secs <n>]
#
# Env overrides:
# JCODE_MEMGATE_SESSION probe session id
# JCODE_MEMGATE_MAX_RSS_ANON_KB idle rss_anon bound (default 56320 = 55 MiB)
# JCODE_MEMGATE_MAX_LIVE_HEAP_BYTES idle allocated_bytes bound (default 47185920 = 45 MiB)
#
# Exit codes: 0 pass, 1 threshold exceeded, 2 could not measure, 3 skipped
# (probe session missing on this machine).
#
# Emits one machine-parseable line: `JCODE_MEMGATE_RESULT {json}`.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SESSION_ID="${JCODE_MEMGATE_SESSION:-session_hog_1783086065415_4ad4ae66cd43dd5b}"
IDLE_SECS=60
MAX_RSS_ANON_KB="${JCODE_MEMGATE_MAX_RSS_ANON_KB:-56320}"
MAX_LIVE_HEAP_BYTES="${JCODE_MEMGATE_MAX_LIVE_HEAP_BYTES:-47185920}"
while [[ $# -gt 0 ]]; do
case "$1" in
--session) SESSION_ID="$2"; shift 2 ;;
--idle-secs) IDLE_SECS="$2"; shift 2 ;;
-h|--help) sed -n '2,30p' "$0"; exit 0 ;;
*) echo "unknown arg: $1" >&2; exit 2 ;;
esac
done
command -v jq >/dev/null || { echo "jq required" >&2; exit 2; }
# Skip (not fail) when the pinned probe session does not exist on this
# machine: thresholds are only meaningful against a fixed workload.
if ! ls "$HOME/.jcode/sessions/${SESSION_ID}"* >/dev/null 2>&1; then
echo "JCODE_MEMGATE_RESULT {\"status\":\"skipped\",\"reason\":\"probe session ${SESSION_ID} not found\"}"
exit 3
fi
echo "[memgate] probing session ${SESSION_ID} (idle ${IDLE_SECS}s)..." >&2
PROBE_OUT="$("$SCRIPT_DIR/memory_probe.sh" --session "$SESSION_ID" --idle-secs "$IDLE_SECS" --skip-trim)" || {
echo "JCODE_MEMGATE_RESULT {\"status\":\"error\",\"reason\":\"memory_probe failed\"}"
exit 2
}
IDLE_LINE="$(printf '%s\n' "$PROBE_OUT" | jq -c 'select(.phase=="idle")' | head -1)"
[[ -n "$IDLE_LINE" ]] || {
echo "JCODE_MEMGATE_RESULT {\"status\":\"error\",\"reason\":\"no idle phase in probe output\"}"
exit 2
}
RSS_ANON_KB="$(jq -r '.proc.rss_anon_kb // 0' <<<"$IDLE_LINE")"
LIVE_HEAP_BYTES="$(jq -r '.client.allocated_bytes // 0' <<<"$IDLE_LINE")"
# Informational server-side numbers (never gate: server load varies).
SERVER_INFO="$(jq -cn \
--argjson rss "$(awk '/VmRSS/{print $2*1024}' "/proc/$(pgrep -f 'shared-server/jcode serve' | head -1)/status" 2>/dev/null || echo 0)" \
'{server_rss_bytes: $rss}')"
FAILURES=()
if (( RSS_ANON_KB > MAX_RSS_ANON_KB )); then
FAILURES+=("idle rss_anon ${RSS_ANON_KB}kB > ${MAX_RSS_ANON_KB}kB")
fi
if (( LIVE_HEAP_BYTES > MAX_LIVE_HEAP_BYTES )); then
FAILURES+=("idle live heap ${LIVE_HEAP_BYTES}B > ${MAX_LIVE_HEAP_BYTES}B")
fi
STATUS="pass"
(( ${#FAILURES[@]} > 0 )) && STATUS="fail"
jq -cn \
--arg status "$STATUS" \
--arg session "$SESSION_ID" \
--argjson rss_anon_kb "$RSS_ANON_KB" \
--argjson live_heap_bytes "$LIVE_HEAP_BYTES" \
--argjson max_rss_anon_kb "$MAX_RSS_ANON_KB" \
--argjson max_live_heap_bytes "$MAX_LIVE_HEAP_BYTES" \
--argjson server "$SERVER_INFO" \
--args '{status:$status, session:$session, idle_rss_anon_kb:$rss_anon_kb,
idle_live_heap_bytes:$live_heap_bytes,
thresholds:{rss_anon_kb:$max_rss_anon_kb, live_heap_bytes:$max_live_heap_bytes},
server:$server, failures:$ARGS.positional}' -- "${FAILURES[@]+"${FAILURES[@]}"}" \
| sed 's/^/JCODE_MEMGATE_RESULT /'
if [[ "$STATUS" == "fail" ]]; then
printf '[memgate] FAIL: %s\n' "${FAILURES[@]}" >&2
exit 1
fi
echo "[memgate] PASS: idle rss_anon ${RSS_ANON_KB}kB (<= ${MAX_RSS_ANON_KB}), live heap ${LIVE_HEAP_BYTES}B (<= ${MAX_LIVE_HEAP_BYTES})" >&2
+295
View File
@@ -0,0 +1,295 @@
#!/usr/bin/env python3
"""Probe Jcode pinned Mermaid fit planning for screenshot repros.
This intentionally mirrors the geometry math in src/tui/ui_diagram_pane.rs so a
bad crop can be reproduced without compiling the whole Rust test binary.
Defaults are the 2026-05-07 Beetle/Harbor clipped Mermaid screenshot:
inner pane 73x46 cells, font 8x16 px, PNG 1180x1470 px.
"""
from __future__ import annotations
import argparse
import json
import math
from pathlib import Path
from typing import Any
TARGET_UTILIZATION_PERCENT = 85.0
MIN_READABLE_ZOOM_PERCENT = 70
MAX_AUTO_FILL_ZOOM_PERCENT = 1000
def clamp(value: int, lo: int, hi: int) -> int:
return max(lo, min(hi, value))
def utilization_percent(used: int, total: int) -> float:
return 0.0 if total == 0 else (used * 100.0) / total
def div_ceil(value: int, divisor: int) -> int:
return 0 if divisor == 0 else (value + divisor - 1) // divisor
def axis_fill_zoom_percent(available_cells: int, image_px: int, cell_px: int) -> int:
if available_cells == 0 or image_px == 0 or cell_px == 0:
return 100
return clamp((available_cells * cell_px * 100) // max(image_px, 1), 1, MAX_AUTO_FILL_ZOOM_PERCENT)
def fit_zoom_percent_for_area(width_cells: int, height_cells: int, img_w_px: int, img_h_px: int, font_w: int, font_h: int) -> int:
if width_cells == 0 or height_cells == 0 or img_w_px == 0 or img_h_px == 0:
return 100
zoom_w = (width_cells * max(font_w, 1) * 100) // max(img_w_px, 1)
zoom_h = (height_cells * max(font_h, 1) * 100) // max(img_h_px, 1)
return clamp(min(zoom_w, zoom_h), 1, MAX_AUTO_FILL_ZOOM_PERCENT)
def vcenter_fitted(width_cells: int, height_cells: int, img_w_px: int, img_h_px: int, font_w: int, font_h: int) -> dict[str, int]:
if width_cells == 0 or height_cells == 0 or img_w_px == 0 or img_h_px == 0:
return {"x": 0, "y": 0, "width": width_cells, "height": height_cells}
area_w_px = width_cells * max(font_w, 1)
area_h_px = height_cells * max(font_h, 1)
scale = min(area_w_px / img_w_px, area_h_px / img_h_px)
fitted_w = min(math.ceil((img_w_px * scale) / max(font_w, 1)), width_cells)
fitted_h = min(math.ceil((img_h_px * scale) / max(font_h, 1)), height_cells)
return {
"x": (width_cells - fitted_w) // 2,
"y": (height_cells - fitted_h) // 2,
"width": fitted_w,
"height": fitted_h,
}
def centered_viewport_scroll_cells(image_px: int, area_cells: int, zoom_percent: int, cell_px: int) -> int:
if image_px == 0 or area_cells == 0 or zoom_percent == 0 or cell_px == 0:
return 0
view_px = area_cells * cell_px * 100 // zoom_percent
max_scroll_px = max(0, image_px - view_px)
if max_scroll_px == 0:
return 0
cell_px_at_zoom = max(div_ceil(cell_px * 100, zoom_percent), 1)
return (max_scroll_px // 2) // cell_px_at_zoom
def plan(width_cells: int, height_cells: int, img_w_px: int, img_h_px: int, font_w: int, font_h: int) -> dict[str, Any]:
contain = vcenter_fitted(width_cells, height_cells, img_w_px, img_h_px, font_w, font_h)
fit_zoom = fit_zoom_percent_for_area(width_cells, height_cells, img_w_px, img_h_px, font_w, font_h)
width_fill_zoom = axis_fill_zoom_percent(width_cells, img_w_px, font_w)
height_fill_zoom = axis_fill_zoom_percent(height_cells, img_h_px, font_h)
preferred_fill_zoom = clamp(max(width_fill_zoom, height_fill_zoom), MIN_READABLE_ZOOM_PERCENT, MAX_AUTO_FILL_ZOOM_PERCENT)
width_utilization = utilization_percent(contain["width"], width_cells)
height_utilization = utilization_percent(contain["height"], height_cells)
area_utilization = utilization_percent(contain["width"] * contain["height"], width_cells * height_cells)
underutilized = (
width_utilization < TARGET_UTILIZATION_PERCENT
or height_utilization < TARGET_UTILIZATION_PERCENT
or area_utilization < TARGET_UTILIZATION_PERCENT
)
meaningfully_larger = preferred_fill_zoom > fit_zoom + 5
old_would_fill = (fit_zoom < MIN_READABLE_ZOOM_PERCENT or underutilized) and meaningfully_larger
fixed_would_fill = underutilized and meaningfully_larger
fill_plan = {
"mode": f"fit-fill@{preferred_fill_zoom}%",
"zoom_percent": preferred_fill_zoom,
"scroll_x": centered_viewport_scroll_cells(img_w_px, width_cells, preferred_fill_zoom, font_w),
"scroll_y": centered_viewport_scroll_cells(img_h_px, height_cells, preferred_fill_zoom, font_h),
}
return {
"input": {
"inner_width_cells": width_cells,
"inner_height_cells": height_cells,
"image_width_px": img_w_px,
"image_height_px": img_h_px,
"font_width_px": font_w,
"font_height_px": font_h,
},
"contain_rect_cells": contain,
"utilization_percent": {
"width": width_utilization,
"height": height_utilization,
"area": area_utilization,
},
"fit_zoom_percent": fit_zoom,
"axis_fill_zoom_percent": {"width": width_fill_zoom, "height": height_fill_zoom},
"preferred_fill_zoom_percent": preferred_fill_zoom,
"underutilized": underutilized,
"meaningfully_larger": meaningfully_larger,
"old_buggy_plan": fill_plan if old_would_fill else {"mode": "fit", "rect": contain},
"fixed_plan": fill_plan if fixed_would_fill else {"mode": "fit", "rect": contain},
"repro_was_clipping_bug": old_would_fill and not fixed_would_fill,
}
def maybe_png_info(path: str | None) -> dict[str, Any] | None:
if not path:
return None
png = Path(path).expanduser()
info: dict[str, Any] = {"path": str(png), "exists": png.exists()}
if not png.exists():
return info
try:
from PIL import Image # type: ignore
except Exception as exc: # pragma: no cover - diagnostic fallback
info["pil_error"] = str(exc)
return info
im = Image.open(png).convert("RGBA")
bbox = im.getchannel("A").getbbox()
info["size"] = list(im.size)
info["alpha_bbox"] = list(bbox) if bbox else None
if bbox:
left, top, right, bottom = bbox
info["content_size"] = [right - left, bottom - top]
info["transparent_margins"] = {
"left": left,
"top": top,
"right": im.size[0] - right,
"bottom": im.size[1] - bottom,
}
return info
def visual_fit_metrics(
*,
width_cells: int,
height_cells: int,
img_w_px: int,
img_h_px: int,
font_w: int,
font_h: int,
contain_rect: dict[str, int],
alpha_bbox: list[int] | tuple[int, int, int, int] | None,
) -> dict[str, Any] | None:
"""Project the visible PNG alpha bbox into the TUI pane.
The layout planner works in cells and the PNG alpha bbox works in pixels.
Projecting the bbox into cells gives a camera-like metric that matches the
visual complaint: even if the full image rect is centered, is the actual
non-transparent diagram content centered in the pane?
"""
if not alpha_bbox or img_w_px <= 0 or img_h_px <= 0 or font_w <= 0 or font_h <= 0:
return None
left_px, top_px, right_px, bottom_px = [float(value) for value in alpha_bbox]
render_w_px = contain_rect["width"] * font_w
render_h_px = contain_rect["height"] * font_h
scale_x = render_w_px / img_w_px
scale_y = render_h_px / img_h_px
content_left = contain_rect["x"] + (left_px * scale_x / font_w)
content_top = contain_rect["y"] + (top_px * scale_y / font_h)
content_right = contain_rect["x"] + (right_px * scale_x / font_w)
content_bottom = contain_rect["y"] + (bottom_px * scale_y / font_h)
content_width = max(0.0, content_right - content_left)
content_height = max(0.0, content_bottom - content_top)
left_blank = content_left
right_blank = width_cells - content_right
top_blank = content_top
bottom_blank = height_cells - content_bottom
center_x = (content_left + content_right) / 2.0
center_y = (content_top + content_bottom) / 2.0
pane_center_x = width_cells / 2.0
pane_center_y = height_cells / 2.0
offset_x = center_x - pane_center_x
offset_y = center_y - pane_center_y
return {
"content_bbox_cells": {
"left": content_left,
"top": content_top,
"right": content_right,
"bottom": content_bottom,
"width": content_width,
"height": content_height,
},
"blank_cells": {
"left": left_blank,
"right": right_blank,
"top": top_blank,
"bottom": bottom_blank,
},
"blank_imbalance_cells": {
"horizontal_right_minus_left": right_blank - left_blank,
"vertical_bottom_minus_top": bottom_blank - top_blank,
},
"content_center_offset_cells": {
"x": offset_x,
"y": offset_y,
},
"content_area_utilization_percent": utilization_percent(
int(round(content_width * content_height * 1000)),
int(width_cells * height_cells * 1000),
),
"camera_centered": abs(offset_x) <= 1.0 and abs(offset_y) <= 1.0,
"note": (
"Low content_area_utilization can be normal for a wide/short or tall/narrow diagram. "
"A large content_center_offset or top/bottom blank imbalance means the camera/placement is wrong."
),
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--inner", default="73x46", help="inner pane size in cells, WIDTHxHEIGHT")
parser.add_argument("--image", default="1180x1470", help="rendered PNG size in px, WIDTHxHEIGHT")
parser.add_argument("--font", default="8x16", help="terminal cell size in px, WIDTHxHEIGHT")
parser.add_argument("--png", help="optional rendered PNG path to inspect alpha bounds")
parser.add_argument(
"--alpha-bbox",
help="optional alpha/content bbox LEFT,TOP,RIGHT,BOTTOM in PNG pixels when --png is unavailable",
)
parser.add_argument(
"--assert-camera-centered",
action="store_true",
help="fail if projected visible content is more than one cell away from pane center",
)
parser.add_argument(
"--assert-old-clipping-repro",
action="store_true",
help="fail unless the old readability-floor rule would have produced the known clipping bug",
)
args = parser.parse_args()
def parse_pair(raw: str) -> tuple[int, int]:
left, right = raw.lower().split("x", 1)
return int(left), int(right)
width_cells, height_cells = parse_pair(args.inner)
img_w_px, img_h_px = parse_pair(args.image)
font_w, font_h = parse_pair(args.font)
result = plan(width_cells, height_cells, img_w_px, img_h_px, font_w, font_h)
png_info = maybe_png_info(args.png)
if png_info is not None:
result["png"] = png_info
alpha_bbox = None
if args.alpha_bbox:
alpha_bbox = [int(part) for part in args.alpha_bbox.split(",")]
elif png_info is not None:
alpha_bbox = png_info.get("alpha_bbox")
metrics = visual_fit_metrics(
width_cells=width_cells,
height_cells=height_cells,
img_w_px=img_w_px,
img_h_px=img_h_px,
font_w=font_w,
font_h=font_h,
contain_rect=result["contain_rect_cells"],
alpha_bbox=alpha_bbox,
)
if metrics is not None:
result["visual_fit_metrics"] = metrics
print(json.dumps(result, indent=2, sort_keys=True))
if args.assert_camera_centered and metrics is not None:
return 0 if metrics["camera_centered"] else 2
if args.assert_old_clipping_repro:
return 0 if result["repro_was_clipping_bug"] else 3
return 0
if __name__ == "__main__":
raise SystemExit(main())
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""Helper script to complete Claude OAuth flow with proper PKCE."""
import base64
import hashlib
import json
import os
import secrets
import sys
import urllib.parse
import requests
CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
AUTHORIZE_URL = "https://claude.ai/oauth/authorize"
TOKEN_URL = "https://console.anthropic.com/v1/oauth/token"
REDIRECT_URI = "https://console.anthropic.com/oauth/code/callback"
SCOPES = "org:create_api_key user:profile user:inference"
def generate_pkce():
"""Generate PKCE verifier and challenge."""
verifier = secrets.token_urlsafe(48)[:64] # 64 chars
digest = hashlib.sha256(verifier.encode()).digest()
challenge = base64.urlsafe_b64encode(digest).rstrip(b'=').decode()
return verifier, challenge
def generate_state():
"""Generate random state for CSRF protection."""
return secrets.token_hex(16)
def main():
if len(sys.argv) > 1 and sys.argv[1] == "--exchange":
# Exchange mode: read code from stdin or arg
code = sys.argv[2] if len(sys.argv) > 2 else input("Enter code: ").strip()
# Load saved state
with open("/tmp/claude_oauth_state.json") as f:
saved = json.load(f)
# Exchange code for tokens
resp = requests.post(TOKEN_URL, data={
"grant_type": "authorization_code",
"client_id": CLIENT_ID,
"code": code,
"code_verifier": saved["verifier"],
"redirect_uri": REDIRECT_URI,
})
if resp.status_code != 200:
print(f"Error: {resp.text}")
sys.exit(1)
tokens = resp.json()
print(f"\nAccess token: {tokens['access_token'][:50]}...")
print(f"Refresh token: {tokens['refresh_token'][:50]}...")
# Save in Claude Code format
creds_dir = os.path.expanduser("~/.claude")
os.makedirs(creds_dir, exist_ok=True)
import time
expires_at = int(time.time() * 1000) + (tokens["expires_in"] * 1000)
creds = {
"claudeAiOauth": {
"accessToken": tokens["access_token"],
"refreshToken": tokens["refresh_token"],
"expiresAt": expires_at,
}
}
with open(os.path.join(creds_dir, ".credentials.json"), "w") as f:
json.dump(creds, f, indent=2)
print(f"\nCredentials saved to ~/.claude/.credentials.json")
else:
# Generate new auth URL
verifier, challenge = generate_pkce()
state = generate_state()
params = {
"response_type": "code",
"client_id": CLIENT_ID,
"redirect_uri": REDIRECT_URI,
"scope": SCOPES,
"code_challenge": challenge,
"code_challenge_method": "S256",
"state": state,
}
auth_url = f"{AUTHORIZE_URL}?{urllib.parse.urlencode(params)}"
with open("/tmp/claude_oauth_state.json", "w") as f:
json.dump({
"verifier": verifier,
"challenge": challenge,
"state": state,
"auth_url": auth_url
}, f)
print(f"Auth URL: {auth_url}")
print(f"\nState saved to /tmp/claude_oauth_state.json")
print(f"Verifier: {verifier}")
if __name__ == "__main__":
main()
+341
View File
@@ -0,0 +1,341 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
command=${1:-help}
if [[ $# -gt 0 ]]; then
shift
fi
sandbox_name=${JCODE_ONBOARDING_SANDBOX:-default}
sandbox_root_default="$repo_root/.tmp/onboarding/$sandbox_name"
sandbox_root=${JCODE_ONBOARDING_DIR:-$sandbox_root_default}
jcode_home="$sandbox_root/home"
runtime_dir="$sandbox_root/runtime"
ensure_dirs() {
mkdir -p "$jcode_home" "$runtime_dir"
}
run_in_sandbox() {
ensure_dirs
(
cd "$repo_root"
# Strip any inherited self-dev markers so the sandbox behaves like a real
# first-run install. `--no-selfdev` only prevents *setting*
# JCODE_CLIENT_SELFDEV_MODE; it cannot unset one inherited from a parent
# self-dev shell, which would otherwise force every sandbox session canary
# (suppressing the new-session suggestion cards we are trying to verify).
env -u JCODE_CLIENT_SELFDEV_MODE -u JCODE_SELFDEV -u JCODE_CANARY \
JCODE_HOME="$jcode_home" \
JCODE_RUNTIME_DIR="$runtime_dir" \
"$@"
)
}
print_usage() {
cat <<EOF
Usage: $(basename "$0") <command> [args...]
Commands:
env Print the sandbox environment exports
status Show sandbox paths and current contents
reset Delete the sandbox entirely
shell Open a clean shell with sandbox env vars set
jcode [args...] Run jcode inside the sandbox
auth-status Run 'jcode auth status' inside the sandbox
fresh [args...] Reset sandbox, then launch jcode with args
seed-real-logins [--with-transcripts|--transcripts-only]
Copy your REAL external logins (Codex/Claude/Gemini/
Copilot/Cursor/OpenCode/pi) into the sandbox so the
onboarding import step can import them. Add
--with-transcripts to also copy Codex/Claude
transcripts (so "continue where you left off" has data).
Originals are never modified.
fresh-real [--with-transcripts]
Reset sandbox, seed your real logins, then launch jcode
login <provider> ... Run 'jcode --provider <provider> login ...' in sandbox
fixture-list List saved local auth fixtures
fixture-save <name> Save current sandbox auth state as a local fixture
fixture-load <name> Load a saved auth fixture into this sandbox
fixture-run <name> -- [args...]
Load a fixture, then run jcode with args
help Show this help
Environment overrides:
JCODE_ONBOARDING_SANDBOX Sandbox name (default: default)
JCODE_ONBOARDING_DIR Explicit sandbox directory
JCODE_AUTH_FIXTURE_DIR Fixture store (default: .tmp/auth-fixtures)
Examples:
$(basename "$0") fresh
$(basename "$0") login openai
$(basename "$0") fixture-save normal-openai
$(basename "$0") fixture-load normal-openai
$(basename "$0") auth-status
EOF
}
print_env() {
ensure_dirs
cat <<EOF
export JCODE_HOME="$jcode_home"
export JCODE_RUNTIME_DIR="$runtime_dir"
EOF
}
status() {
ensure_dirs
echo "Sandbox name: $sandbox_name"
echo "Sandbox root: $sandbox_root"
echo "JCODE_HOME: $jcode_home"
echo "RUNTIME_DIR: $runtime_dir"
echo
if [[ -d "$jcode_home" ]]; then
echo "Home contents:"
find "$jcode_home" -maxdepth 3 \( -type f -o -type d \) | sed "s#^$sandbox_root#.#" | sort
fi
}
reset() {
rm -rf "$sandbox_root"
echo "Removed onboarding sandbox: $sandbox_root"
}
open_shell() {
ensure_dirs
echo "Opening sandbox shell"
echo " JCODE_HOME=$jcode_home"
echo " JCODE_RUNTIME_DIR=$runtime_dir"
env JCODE_HOME="$jcode_home" JCODE_RUNTIME_DIR="$runtime_dir" bash --noprofile --norc
}
run_jcode() {
# The sandbox should behave like a real standalone install, not a self-dev
# client. Because we launch from inside the repo, jcode would otherwise
# auto-detect the repository and join the shared self-dev server (remote
# mode), which both breaks isolation and skips local-only first-run behavior
# like the new-session model validation. `--no-selfdev` keeps it standalone,
# spawning its own server under the sandbox's JCODE_RUNTIME_DIR. Set
# JCODE_SANDBOX_SELFDEV=1 to opt back into the shared-server behavior.
local prefix=()
if [[ "${JCODE_SANDBOX_SELFDEV:-0}" != "1" ]]; then
prefix=(--no-selfdev)
fi
# Allow pointing the sandbox at an already-built binary (e.g. the selfdev
# profile output) without rebuilding the debug binary. Falls back to the
# debug binary, then to `cargo run`.
if [[ -n "${JCODE_SANDBOX_BIN:-}" ]]; then
if [[ -x "$JCODE_SANDBOX_BIN" ]]; then
run_in_sandbox "$JCODE_SANDBOX_BIN" "${prefix[@]}" "$@"
return
fi
echo "JCODE_SANDBOX_BIN=$JCODE_SANDBOX_BIN is not executable" >&2
return 1
fi
local binary_path="$repo_root/target/debug/jcode"
if [[ -x "$binary_path" ]]; then
run_in_sandbox "$binary_path" "${prefix[@]}" "$@"
else
run_in_sandbox cargo run --bin jcode -- "${prefix[@]}" "$@"
fi
}
run_auth_fixture() {
JCODE_ONBOARDING_SANDBOX="$sandbox_name" \
JCODE_ONBOARDING_DIR="$sandbox_root" \
"$repo_root/scripts/auth_fixture.sh" "$@"
}
# Copy one real file from $HOME into the sandbox's external/ tree, preserving its
# relative path. jcode resolves every external credential/transcript lookup to
# $JCODE_HOME/external/<same-relative-path-as-$HOME> when JCODE_HOME is set, so
# seeding here makes your real logins/transcripts visible to the onboarding
# import + continue steps. Copies (never symlinks: jcode rejects symlinked auth
# files) and never touches the originals.
seed_one_file() {
local rel=$1
local src="$HOME/$rel"
local dst="$jcode_home/external/$rel"
if [[ ! -e "$src" ]]; then
return 1
fi
mkdir -p "$(dirname "$dst")"
cp -a "$src" "$dst"
chmod -R go-rwx "$jcode_home/external" 2>/dev/null || true
return 0
}
# Copy a real directory subtree (e.g. transcript stores) into external/.
seed_one_dir() {
local rel=$1
local src="$HOME/$rel"
local dst="$jcode_home/external/$rel"
if [[ ! -d "$src" ]]; then
return 1
fi
mkdir -p "$dst"
cp -a "$src/." "$dst/"
chmod -R go-rwx "$jcode_home/external" 2>/dev/null || true
return 0
}
# Seed the sandbox with copies of your real external logins (and, with
# --with-transcripts, your Codex/Claude transcripts) so onboarding's import and
# "continue where you left off" steps act on real data.
seed_real_logins() {
ensure_dirs
local with_transcripts=0
for arg in "$@"; do
case "$arg" in
--with-transcripts) with_transcripts=1 ;;
--transcripts-only) with_transcripts=2 ;;
*) echo "Unknown seed-real-logins option: $arg" >&2; return 2 ;;
esac
done
# Auth/credential files the external-import detectors read, each relative to
# $HOME (mirrors crate::storage::user_home_path and the per-provider paths).
local auth_files=(
".codex/auth.json"
".claude/.credentials.json"
".claude.json"
".local/share/opencode/auth.json"
".pi/agent/auth.json"
".gemini/oauth_creds.json"
".config/github-copilot/hosts.json"
".config/github-copilot/apps.json"
".cursor/auth.json"
".config/cursor/auth.json"
".config/Cursor/User/globalStorage/state.vscdb"
".config/cursor/User/globalStorage/state.vscdb"
)
# Transcript stores the "continue where you left off" picker reads.
local transcript_dirs=(
".codex/sessions"
".claude/projects"
)
local seeded=()
local skipped=()
if [[ $with_transcripts -ne 2 ]]; then
for rel in "${auth_files[@]}"; do
if seed_one_file "$rel"; then
seeded+=("$rel")
else
skipped+=("$rel")
fi
done
fi
if [[ $with_transcripts -ge 1 ]]; then
for rel in "${transcript_dirs[@]}"; do
if seed_one_dir "$rel"; then
seeded+=("$rel/ (transcripts)")
else
skipped+=("$rel/ (transcripts)")
fi
done
fi
echo "Seeded real logins into sandbox external dir:"
echo " $jcode_home/external"
echo
if [[ ${#seeded[@]} -gt 0 ]]; then
echo "Copied:"
for rel in "${seeded[@]}"; do
echo " + $rel"
done
else
echo "Copied nothing (no matching real files found under \$HOME)."
fi
if [[ ${#skipped[@]} -gt 0 ]]; then
echo
echo "Not present (skipped):"
for rel in "${skipped[@]}"; do
echo " - $rel"
done
fi
echo
echo "These are copies; your real \$HOME files are untouched."
echo "Onboarding will now offer to import them. Start it with:"
echo " $(basename "$0") jcode"
}
scenario_arg() {
if [[ $# -gt 0 ]]; then
printf '%s' "$1"
else
printf 'onboarding'
fi
}
case "$command" in
env)
print_env
;;
status)
status
;;
reset)
reset
;;
shell)
open_shell
;;
jcode)
run_jcode "$@"
;;
auth-status)
run_jcode auth status
;;
fresh)
reset
run_jcode "$@"
;;
seed-real-logins)
seed_real_logins "$@"
;;
fresh-real)
reset
seed_real_logins "$@"
echo
echo "Launching sandbox jcode with your real logins available to import..."
run_jcode
;;
login)
if [[ $# -lt 1 ]]; then
echo "login requires a provider, for example: $(basename "$0") login openai" >&2
exit 1
fi
provider=$1
shift
run_jcode --provider "$provider" login "$@"
;;
fixture-list)
run_auth_fixture list
;;
fixture-save)
run_auth_fixture save "$@"
;;
fixture-load)
run_auth_fixture load "$@"
;;
fixture-run)
run_auth_fixture run "$@"
;;
help|-h|--help)
print_usage
;;
*)
echo "Unknown command: $command" >&2
echo >&2
print_usage >&2
exit 1
;;
esac
+18
View File
@@ -0,0 +1,18 @@
{
"total": 33,
"tracked_files": {
"crates/jcode-app-core/src/session_launch.rs": 1,
"crates/jcode-app-core/src/tool/computer/win.rs": 3,
"crates/jcode-base/src/auth/oauth.rs": 3,
"crates/jcode-base/src/hooks.rs": 1,
"crates/jcode-desktop/src/single_session_render/wrapping.rs": 1,
"crates/jcode-provider-doctor/src/lifecycle_driver.rs": 2,
"crates/jcode-terminal-launch/src/lib.rs": 1,
"crates/jcode-tui-core/src/stream_buffer.rs": 3,
"crates/jcode-tui/src/tui/app/helpers.rs": 1,
"crates/jcode-tui/src/tui/session_picker.rs": 2,
"src/bin/memory_recall_bench.rs": 14,
"src/cli/commands/menubar.rs": 1
},
"version": 1
}
+568
View File
@@ -0,0 +1,568 @@
# Phone-server AWS IAM least-privilege assessment
Assessment date: 2026-07-12
Account: `302154194530`
Region: `us-east-1`
This document began as a design review. The live hardening described in `README.md` was subsequently applied and verified. The remaining deliberate gap is removal of `AdministratorAccess` from `jade-deploy`, which is blocked until an independent root/MFA recovery login is verified.
## Executive assessment
`jade-deploy` should not retain `AdministratorAccess`. A compromised long-lived deployment credential can currently modify identities, disable cost guardrails, exfiltrate data, create arbitrary infrastructure, and establish persistence anywhere in the account.
Replace it with an assume-role-only principal and three distinct privilege planes:
1. **`JcodePhoneOperator`** for normal deployments and maintenance of the existing stack.
2. **`JcodePhoneProvisioner`** for infrequent rebuilds, MFA-gated, time-limited, restricted to `us-east-1`, `jcode-phone-*` resources, and bounded runtime roles.
3. **A separate emergency administrator path** protected by MFA and never used by automation. This is required to avoid lockout while removing the existing administrator attachment.
Also replace the instance's `AmazonBedrockFullAccess` with inference-only permissions. The code uses model catalog discovery and `ConverseStream`; it does not need Bedrock administration.
## Evidence and stated live resources
Repository evidence:
- `README.md` states account `302154194530`, region `us-east-1`, instance `i-08214cf66cd3f80c7`, Elastic IP `54.196.207.97`, and API ID `8c3wp4cbag`.
- `wake-lambda.py` calls `ec2:DescribeInstances` and `ec2:StartInstances` for that instance.
- `breaker-lambda.py` calls `ec2:DescribeInstances`, `ec2:StopInstances`, and `sns:Publish` to `arn:aws:sns:us-east-1:302154194530:jcode-guard-warn`.
- The Bedrock provider calls `ListFoundationModels`, `ListInferenceProfiles`, and the streaming Converse API. Converse streaming is authorized by `bedrock:InvokeModelWithResponseStream`.
- The rebuild instructions require EC2, EIP, security group, IAM instance profile, Lambda, API Gateway v2, SNS, CloudWatch alarms, and Budgets administration.
- TestFlight automation is App Store Connect only and requires no AWS permission.
The SSM-based wake implementation is now deployed. The Lambda generates pair codes through SSM Run Command, and the legacy public `:7644` path is disabled.
Stated named resources:
| Type | Resource |
|---|---|
| EC2 instance | `arn:aws:ec2:us-east-1:302154194530:instance/i-08214cf66cd3f80c7` |
| Elastic IP | `54.196.207.97`; allocation ID must be inventoried |
| Lambda | `jcode-phone-wake` |
| Lambda | `jcode-guard-breaker` |
| API Gateway v2 | `8c3wp4cbag` |
| SNS | `jcode-guard-stop` and `jcode-guard-warn` |
| CloudWatch alarms | `jcode-bedrock-tokens-warn`, `jcode-bedrock-tokens-stop`; the ineffective billing alarms were replaced by the working Budget/SNS breaker path |
| Budget | `jcode-dev-monthly-cost` |
| Bedrock model route | `us.anthropic.claude-opus-4-6-v1` |
### Live-state verification
The account was subsequently inventoried through the `jcode-bedrock` profile. Runtime role names, Lambda roles, the EC2 instance and security group, the Elastic IP, Budget subscribers, CloudWatch alarms, log retention, API Gateway stage, S3/DynamoDB resources, and access-key metadata were verified directly. The wake Lambda now uses SSM pairing, all public EC2 ingress is closed, the root EBS volume is encrypted, CloudTrail and Access Analyzer are enabled, and the old deployment key is inactive.
## Target identity design
### Human access
Preferred end state:
- Use IAM Identity Center or another federated identity for the human operator.
- Require MFA for both operator and provisioner role assumption.
- Do not create long-lived keys for human access.
- Keep `jade-deploy` only during migration, then delete it after the observation period.
Transitional end state if an IAM user must remain:
- `jade-deploy` has no console password and no service permissions.
- Its only permission is `sts:AssumeRole` for the two deployment roles.
- It cannot modify itself, policies, access keys, MFA devices, or role trust policies.
Assume-only policy for `jade-deploy`:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AssumePhoneServerRolesOnly",
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": [
"arn:aws:iam::302154194530:role/jcode-phone/JcodePhoneOperator",
"arn:aws:iam::302154194530:role/jcode-phone/JcodePhoneProvisioner"
]
}
]
}
```
Require MFA in both role trust policies for a human IAM principal:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::302154194530:user/jade-deploy"
},
"Action": "sts:AssumeRole",
"Condition": {
"Bool": { "aws:MultiFactorAuthPresent": "true" },
"NumericLessThan": { "aws:MultiFactorAuthAge": "3600" }
}
}
]
}
```
Set `JcodePhoneProvisioner` maximum session duration to one hour. Do not allow either role to change its own trust or permissions.
For unattended CI, use a separate OIDC-federated role restricted to the exact repository, branch/environment, and workflow. Do not weaken the human role's MFA trust for CI.
## Runtime policies
Use separate execution roles for the instance and each Lambda. Do not reuse the deploy role at runtime.
### EC2 instance: inference only
Replace `AmazonBedrockFullAccess` with the following customer-managed policy. `List*` actions require `Resource: "*"`. The inference resources deliberately cover only the configured Claude Opus 4.6 cross-region profile and its backing foundation model. Cross-region inference can route outside `us-east-1`, so a single-region foundation-model ARN may fail.
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DiscoverBedrockModels",
"Effect": "Allow",
"Action": [
"bedrock:ListFoundationModels",
"bedrock:ListInferenceProfiles"
],
"Resource": "*"
},
{
"Sid": "InvokeConfiguredClaudeProfile",
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream"
],
"Resource": [
"arn:aws:bedrock:us-east-1:302154194530:inference-profile/us.anthropic.claude-opus-4-6-v1",
"arn:aws:bedrock:*::foundation-model/anthropic.claude-opus-4-6-v1:0"
]
}
]
}
```
Verify the exact inference-profile ARN and backing model IDs with `bedrock list-inference-profiles` before application. If the deployed profile is AWS-owned or the backing model has a different version suffix, substitute the returned ARNs rather than widening the model-name pattern. The wildcard region is intentional because a US cross-region inference profile can route to multiple US regions. Remove `bedrock:InvokeModel` after testing if all production paths exclusively use `ConverseStream`; retaining it is a small compatibility allowance, not administrative access. The provider's optional STS identity validation does not require an added service permission for normal operation.
### Wake Lambda
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadTargetInstanceState",
"Effect": "Allow",
"Action": "ec2:DescribeInstances",
"Resource": "*"
},
{
"Sid": "StartTargetInstanceOnly",
"Effect": "Allow",
"Action": "ec2:StartInstances",
"Resource": "arn:aws:ec2:us-east-1:302154194530:instance/i-08214cf66cd3f80c7"
},
{
"Sid": "WriteOwnLogs",
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:us-east-1:302154194530:log-group:/aws/lambda/jcode-phone-wake:*"
}
]
}
```
Create the log group during provisioning with retention configured, rather than granting runtime `logs:CreateLogGroup` on `*`.
If the SSM-based wake implementation is deployed, add:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadManagedInstanceStatus",
"Effect": "Allow",
"Action": "ssm:DescribeInstanceInformation",
"Resource": "*"
},
{
"Sid": "RunPairCommandOnTargetOnly",
"Effect": "Allow",
"Action": "ssm:SendCommand",
"Resource": [
"arn:aws:ec2:us-east-1:302154194530:instance/i-08214cf66cd3f80c7",
"arn:aws:ssm:us-east-1::document/AWS-RunShellScript"
]
},
{
"Sid": "ReadPairCommandResult",
"Effect": "Allow",
"Action": "ssm:GetCommandInvocation",
"Resource": "*"
}
]
}
```
That variant also requires an SSM-managed-instance policy on the EC2 role. Keep it separate from the Bedrock policy, validate the exact Systems Manager agent calls in CloudTrail, and narrow it from `AmazonSSMManagedInstanceCore` where operationally practical. Treat `ssm:SendCommand` as privileged remote code execution and keep it scoped to the one instance and AWS-managed document.
### Breaker Lambda
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadTargetInstanceState",
"Effect": "Allow",
"Action": "ec2:DescribeInstances",
"Resource": "*"
},
{
"Sid": "StopTargetInstanceOnly",
"Effect": "Allow",
"Action": "ec2:StopInstances",
"Resource": "arn:aws:ec2:us-east-1:302154194530:instance/i-08214cf66cd3f80c7"
},
{
"Sid": "PublishGuardNotification",
"Effect": "Allow",
"Action": "sns:Publish",
"Resource": "arn:aws:sns:us-east-1:302154194530:jcode-guard-warn"
},
{
"Sid": "WriteOwnLogs",
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:us-east-1:302154194530:log-group:/aws/lambda/jcode-guard-breaker:*"
}
]
}
```
## Daily operator policy
This role updates and diagnoses existing resources but cannot create identities, create arbitrary compute, terminate the instance, release the EIP, delete guardrails, or alter role trust.
Replace the `<...>` values after read-only inventory. The API Gateway resource form is intentionally the API Gateway management ARN, which does not include the account ID.
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DescribePhoneServerInfrastructure",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstances",
"ec2:DescribeInstanceStatus",
"ec2:DescribeAddresses",
"ec2:DescribeSecurityGroups",
"ec2:DescribeVolumes",
"cloudwatch:DescribeAlarms",
"cloudwatch:GetMetricData",
"cloudwatch:GetMetricStatistics",
"cloudwatch:ListMetrics"
],
"Resource": "*"
},
{
"Sid": "ReadExistingFunctions",
"Effect": "Allow",
"Action": [
"lambda:GetFunction",
"lambda:GetFunctionConfiguration",
"lambda:GetPolicy"
],
"Resource": [
"arn:aws:lambda:us-east-1:302154194530:function:jcode-phone-wake",
"arn:aws:lambda:us-east-1:302154194530:function:jcode-phone-wake:*",
"arn:aws:lambda:us-east-1:302154194530:function:jcode-guard-breaker",
"arn:aws:lambda:us-east-1:302154194530:function:jcode-guard-breaker:*"
]
},
{
"Sid": "OperateExistingInstance",
"Effect": "Allow",
"Action": [
"ec2:StartInstances",
"ec2:StopInstances",
"ec2:RebootInstances",
"ec2:ModifyInstanceAttribute"
],
"Resource": "arn:aws:ec2:us-east-1:302154194530:instance/i-08214cf66cd3f80c7"
},
{
"Sid": "DeployExistingFunctions",
"Effect": "Allow",
"Action": [
"lambda:UpdateFunctionCode",
"lambda:UpdateFunctionConfiguration",
"lambda:PublishVersion",
"lambda:CreateAlias",
"lambda:UpdateAlias",
"lambda:DeleteAlias",
"lambda:InvokeFunction"
],
"Resource": [
"arn:aws:lambda:us-east-1:302154194530:function:jcode-phone-wake",
"arn:aws:lambda:us-east-1:302154194530:function:jcode-phone-wake:*",
"arn:aws:lambda:us-east-1:302154194530:function:jcode-guard-breaker",
"arn:aws:lambda:us-east-1:302154194530:function:jcode-guard-breaker:*"
]
},
{
"Sid": "ReadAndPatchExistingHttpApiRoot",
"Effect": "Allow",
"Action": [
"apigateway:GET",
"apigateway:PATCH"
],
"Resource": "arn:aws:apigateway:us-east-1::/apis/8c3wp4cbag"
},
{
"Sid": "MaintainExistingHttpApiChildren",
"Effect": "Allow",
"Action": [
"apigateway:GET",
"apigateway:POST",
"apigateway:PUT",
"apigateway:PATCH",
"apigateway:DELETE"
],
"Resource": "arn:aws:apigateway:us-east-1::/apis/8c3wp4cbag/*"
},
{
"Sid": "PassInstanceRoleToEc2Only",
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "arn:aws:iam::302154194530:role/jcode-phone/runtime/JcodePhoneInstance",
"Condition": {
"StringEquals": {
"iam:PassedToService": "ec2.amazonaws.com"
}
}
},
{
"Sid": "PassLambdaRolesToLambdaOnly",
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": [
"arn:aws:iam::302154194530:role/jcode-phone/runtime/JcodePhoneWakeLambda",
"arn:aws:iam::302154194530:role/jcode-phone/runtime/JcodePhoneBreakerLambda"
],
"Condition": {
"StringEquals": {
"iam:PassedToService": "lambda.amazonaws.com"
}
}
},
{
"Sid": "ReadPhoneLogs",
"Effect": "Allow",
"Action": [
"logs:DescribeLogStreams",
"logs:GetLogEvents",
"logs:FilterLogEvents"
],
"Resource": [
"arn:aws:logs:us-east-1:302154194530:log-group:/aws/lambda/jcode-phone-wake:*",
"arn:aws:logs:us-east-1:302154194530:log-group:/aws/lambda/jcode-guard-breaker:*"
]
},
{
"Sid": "MaintainGuardTopics",
"Effect": "Allow",
"Action": [
"sns:GetTopicAttributes",
"sns:ListSubscriptionsByTopic",
"sns:Publish"
],
"Resource": [
"arn:aws:sns:us-east-1:302154194530:jcode-guard-stop",
"arn:aws:sns:us-east-1:302154194530:jcode-guard-warn"
]
},
{
"Sid": "ReadNamedBudget",
"Effect": "Allow",
"Action": "budgets:ViewBudget",
"Resource": "arn:aws:budgets::302154194530:budget/jcode-dev-monthly-cost"
}
]
}
```
Recommended tightening:
- Remove `ec2:ModifyInstanceAttribute` if routine maintenance never changes shutdown behavior, instance type, source/destination check, or attached profile.
- Keep `lambda:AddPermission`, `lambda:RemovePermission`, `sns:Subscribe`, alarm mutation, and budget mutation in the MFA-gated provisioner path. Those actions can expose invocations, exfiltrate notifications, or disable cost controls.
- API Gateway `DELETE` is allowed only below `/apis/8c3wp4cbag/*`; the daily role cannot delete the API root.
- Do not grant `cloudwatch:DeleteAlarms`, `budgets:DeleteBudget`, `sns:DeleteTopic`, `ec2:TerminateInstances`, `ec2:ReleaseAddress`, or IAM write actions to the daily operator.
## Rebuild/provisioner role
A rebuild inherently needs create/delete privileges on several services. Keep those permissions out of the daily role. The safest maintainable implementation is to put the stack in CloudFormation, CDK, or Terraform and let the human invoke only a named stack deployment role.
Recommended model:
- Human `JcodePhoneProvisioner`: CloudFormation stack operations on `jcode-phone-server*`, read-only diagnostics, and `iam:PassRole` only for `JcodePhoneCloudFormationExecution` with `iam:PassedToService = cloudformation.amazonaws.com`.
- `JcodePhoneCloudFormationExecution`: service permissions below, usable only by CloudFormation.
- Every created resource is tagged `Project=jcode-phone-server` and `ManagedBy=cloudformation`.
- Every created runtime role is under path `/jcode-phone/runtime/` and must carry the `JcodePhoneRuntimeBoundary` permissions boundary.
Human provisioner policy:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ManageNamedPhoneStack",
"Effect": "Allow",
"Action": [
"cloudformation:CreateStack",
"cloudformation:UpdateStack",
"cloudformation:DeleteStack",
"cloudformation:DescribeStacks",
"cloudformation:DescribeStackEvents",
"cloudformation:DescribeStackResources",
"cloudformation:GetTemplate",
"cloudformation:GetTemplateSummary",
"cloudformation:ListStackResources",
"cloudformation:CreateChangeSet",
"cloudformation:DescribeChangeSet",
"cloudformation:ExecuteChangeSet",
"cloudformation:DeleteChangeSet",
"cloudformation:ValidateTemplate"
],
"Resource": [
"arn:aws:cloudformation:us-east-1:302154194530:stack/jcode-phone-server*/*",
"arn:aws:cloudformation:us-east-1:302154194530:changeSet/jcode-phone-server*/*"
]
},
{
"Sid": "ValidateNewTemplate",
"Effect": "Allow",
"Action": "cloudformation:ValidateTemplate",
"Resource": "*"
},
{
"Sid": "PassPhoneCloudFormationExecutionRole",
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "arn:aws:iam::302154194530:role/jcode-phone/JcodePhoneCloudFormationExecution",
"Condition": {
"StringEquals": {
"iam:PassedToService": "cloudformation.amazonaws.com"
}
}
}
]
}
```
The CloudFormation execution role should permit only this service/action envelope:
| Service | Required rebuild operations | Scope/guardrail |
|---|---|---|
| EC2 | Run/terminate the one server; create/tag volume and ENI; create/manage one SG; allocate/associate/release one EIP; modify shutdown behavior; describe AMIs/subnets/VPCs | `us-east-1`; request/resource tag `Project=jcode-phone-server`; approved instance types only; IMDSv2 required; approved VPC/subnet |
| IAM | Create/update/delete the three runtime roles and one instance profile; put/delete inline policies; pass roles | Path `/jcode-phone/runtime/` only; require boundary ARN on `CreateRole`; never allow changing deploy/provisioner/emergency roles |
| Lambda | Create/update/delete the two named functions; versions/aliases; permissions | Function ARN prefix `jcode-phone-*` and `jcode-guard-breaker*`; project tag |
| API Gateway v2 | Create/update/delete one HTTP API, integration, route, and stage | Project tag and stack ownership |
| SNS | Create/manage/delete `jcode-guard-stop` and `jcode-guard-warn`; subscriptions | Exact topic-name ARNs |
| CloudWatch | Create/update/delete the four named alarms | Exact alarm-name ARNs |
| Logs | Create/configure/delete the two Lambda log groups | Exact `/aws/lambda/...` ARNs; retention required |
| Budgets | Create/update/delete `jcode-dev-monthly-cost` | Exact budget ARN |
The execution role, not the daily operator, also owns `lambda:AddPermission`/`RemovePermission`, `sns:Subscribe`/`Unsubscribe`, CloudWatch alarm mutation, and budget mutation. This preserves integration and guardrail maintenance without making those high-impact actions part of everyday credentials.
Do not give the CloudFormation execution role general `iam:*`, `organizations:*`, `account:*`, `sts:AssumeRole`, `kms:*`, unrestricted `s3:*`, unrestricted `secretsmanager:*`, or permission to modify the provisioner, operator, emergency role, its own role, or its own boundary.
A permissions boundary is not a grant. Use it as a maximum for runtime roles and combine it with their narrow inline policies. The boundary should allow only:
- The instance's Bedrock discovery and configured-model inference permissions.
- The wake Lambda's start/describe and own-log permissions.
- The breaker Lambda's stop/describe, warning publish, and own-log permissions.
Because these permissions differ, the boundary can be their union; each role's inline policy must remain the narrower subset. Explicitly deny IAM, STS role assumption, Organizations, account administration, and access-key operations in the boundary as defense in depth.
## Read-only inventory required before migration
Run these with the existing administrator session after reauthentication. They make no changes:
```bash
aws sts get-caller-identity
aws iam list-attached-user-policies --user-name jade-deploy
aws iam list-user-policies --user-name jade-deploy
aws iam list-access-keys --user-name jade-deploy
aws iam get-account-authorization-details > phone-server-iam-before.json
aws ec2 describe-instances --instance-ids i-08214cf66cd3f80c7 --region us-east-1
aws ec2 describe-addresses --public-ips 54.196.207.97 --region us-east-1
aws lambda get-function --function-name jcode-phone-wake --region us-east-1
aws lambda get-function --function-name jcode-guard-breaker --region us-east-1
aws apigatewayv2 get-api --api-id 8c3wp4cbag --region us-east-1
aws sns list-topics --region us-east-1
aws cloudwatch describe-alarms --alarm-name-prefix jcode- --region us-east-1
aws budgets describe-budget --account-id 302154194530 --budget-name jcode-dev-monthly-cost
aws bedrock list-inference-profiles --type-equals SYSTEM_DEFINED --region us-east-1
```
Also inventory CloudTrail usage for at least 30 days. Add an action only when it corresponds to a known deploy/maintenance operation. Access Analyzer policy generation from CloudTrail is useful as a second opinion, but should not replace review because rarely used recovery operations may be absent.
## Lockout-safe migration and credential rotation
1. **Prepare recovery first.** Verify the account root email, root MFA, and recovery contacts. Create or verify a separate MFA-protected emergency administrator path. Prefer IAM Identity Center. It must be independent of `jade-deploy`, have no access key, and be tested before touching `AdministratorAccess`.
2. **Capture state.** Export IAM authorization details, user policy attachments, role trust policies, access-key metadata, resource ARNs, and tags. Record the exact `AdministratorAccess` attachment being replaced.
3. **Create runtime policies and roles.** Create the narrow instance, wake, and breaker roles. Do not switch workloads yet. Validate their documents with IAM Access Analyzer or `aws accessanalyzer validate-policy`.
4. **Create operator and provisioner roles.** Add MFA-gated trust. Ensure neither role can modify itself, the emergency path, permission boundaries, or arbitrary identities.
5. **Grant assume-only access while admin remains attached.** Attach the small `sts:AssumeRole` policy to `jade-deploy`, but leave `AdministratorAccess` temporarily attached.
6. **Test role entry.** From a distinct profile/session, assume each role and confirm `aws sts get-caller-identity` reports the role ARN. Confirm operator reads for EC2, Lambda, API Gateway, logs, SNS, alarms, and budget.
7. **Simulate every required API call.** Use `iam:SimulatePrincipalPolicy` or the policy simulator for the action/resource matrix in this document. Explicitly test denied controls such as creating users, attaching `AdministratorAccess`, terminating arbitrary instances, and deleting guardrails.
8. **Canary mutation paths without touching production.** Through the provisioner, deploy and remove a tiny tagged canary stack using the same resource classes where practical. For the operator, update a disposable Lambda alias or canary function rather than invoking `jcode-guard-breaker`, which stops production. Do not use the breaker invocation as an IAM test.
9. **Switch runtime roles one at a time.** First update Lambda execution roles and verify logs plus harmless wake status checks. Then replace the EC2 instance profile and run a real Bedrock streaming request. Keep the previous roles intact but unattached until verification completes.
10. **Rotate the credential transport.** Preferred: switch the workstation to IAM Identity Center and role profiles. Transitional IAM-user option: create a second `jade-deploy` access key, configure it under a new profile, and verify role assumption. Never overwrite the only working profile first. An IAM user can have at most two keys.
11. **Detach `AdministratorAccess` only after independent recovery succeeds.** Confirm the emergency administrator path in a separate browser/profile immediately before detachment. Then detach `AdministratorAccess` from `jade-deploy`. Do not delete the user, keys, old runtime roles, or old policies in the same change window.
12. **Run post-detachment checks.** Re-assume operator and provisioner, rerun all read checks and safe deployment checks, verify the phone-server health endpoint, verify wake behavior during an agreed maintenance window, verify pairing, and verify one Bedrock streaming completion.
13. **Disable the old key, do not delete it yet.** After the new access path has worked for at least 24 to 72 hours, mark the old key inactive. Monitor CloudTrail for attempts using its access-key ID and for `AccessDenied` on expected workflows.
14. **Delete after observation.** After another 7 to 14 days without needed rollback, delete the inactive key, remove obsolete policies/roles, and, if federation is stable, delete `jade-deploy` entirely.
15. **Review quarterly.** Use access-key last-used data, CloudTrail, Access Analyzer, credential reports, and alarm/budget tests. Remove unused actions. Test the emergency path without using it for routine work.
### Rollback rule
If a required operation fails after administrator removal, stop and use the independent emergency administrator path to correct the narrow role. Do not reattach `AdministratorAccess` to the everyday user as the default fix. Never rely on an existing STS session as the only rollback mechanism because policy changes and session expiry can invalidate that assumption.
## Additional security findings adjacent to IAM
These are not required for the IAM replacement, but they materially affect the deployment:
- The wake secret is embedded in Lambda source and placed in a query string. Query tokens can appear in browser history, logs, analytics, screenshots, and referrers. Store it in Secrets Manager or SSM Parameter Store, compare a header or signed short-lived request, and grant only the wake Lambda read access to that one secret.
- Port `7644` is publicly exposed and the Lambda calls it over plain HTTP. Prefer a private VPC path, SSM-mediated pairing, or tailnet-only access. If keeping it public, restrict the security group source and add TLS.
- The pair service runs as root and invokes `sudo -u ec2-user`; harden the systemd unit with a dedicated user, filesystem protections, `NoNewPrivileges`, and a narrowly scoped sudo rule.
- The instance role's current `AmazonBedrockFullAccess` is broader than necessary even if `jade-deploy` is fixed.
- Add CloudTrail alerts for `AttachUserPolicy`, `PutUserPolicy`, `CreateAccessKey`, `UpdateAssumeRolePolicy`, `PassRole`, and changes to the emergency/deployment roles.
## Acceptance criteria
The migration is complete when:
- `jade-deploy` has no `AdministratorAccess` and no direct AWS service permissions beyond role assumption.
- Daily deployment and maintenance succeed through `JcodePhoneOperator`.
- A full tagged rebuild can be performed through the MFA-gated provisioner/CloudFormation path.
- Runtime roles contain only the API calls documented above.
- An independent emergency administrator path is tested.
- The old access key is disabled, observed, then deleted.
- CloudTrail shows no unexpected `AccessDenied` for required operations and no use of the old key during the observation period.
+88
View File
@@ -0,0 +1,88 @@
# jcode phone server (managed cloud host)
A self-managing EC2 host that runs `jcode serve` with the WebSocket gateway so
phones (the iOS app, or SSH clients like Termius) can drive jcode sessions
without any laptop in the loop. Billing safety is layered and each layer has
been live-tested.
Live deployment (July 2026): AWS account `302154194530`, us-east-1,
instance `i-08214cf66cd3f80c7` (m7i-flex.large), Elastic IP `54.196.207.97`.
## Architecture
```
phone (jcode iOS app / Termius, connected through Tailscale)
│ WebSocket :7643 (pair token auth, tailnet-only)
EC2 jcode server ──instance role──▶ AWS Bedrock (Opus 4.6 default)
│ tap wake link (API Gateway → wake lambda, bearer token from URL fragment)
│ "Pair this phone" button (lambda → SSM Run Command → `jcode pair`)
AWS Budget $10 ──▶ SNS jcode-guard-stop ──▶ breaker lambda ──▶ stop instance
└──▶ email
```
## Files
| File | Deployed at | Purpose |
|---|---|---|
| `units/jcode-serve.service` | `~ec2-user/.config/systemd/user/` (user unit, linger on) | jcode daemon + gateway, restart always |
| `units/jcode-pair.service` | `/etc/systemd/system/` | Legacy local pairing HTTP service, now disabled after migration to SSM |
| `units/idle-autostop.{service,timer}` | `/etc/systemd/system/` | 5-min check, poweroff after 30 min idle |
| `idle-autostop.sh` | `/usr/local/bin/` | idle = no gateway/SSH clients AND jcode has no outbound :443 (not streaming) |
| `jcode-pair-service.py` | `/usr/local/bin/` | Legacy tailnet-only fallback; normal pairing now uses SSM |
| `wake-lambda.py` | Lambda `jcode-phone-wake` (behind API Gateway `8c3wp4cbag`) | wake page: starts instance, polls SSM health, generates pair codes through SSM |
| `breaker-lambda.py` | Lambda `jcode-guard-breaker` | stops the instance; subscribed to SNS `jcode-guard-stop` |
| `IAM-LEAST-PRIVILEGE.md` | repository documentation | scoped runtime/deploy policies and lockout-safe `jade-deploy` rotation plan |
## Server config (instance)
- `~/.jcode/config.toml`: `[provider]` default bedrock/Opus 4.6, `[gateway] enabled = true, port 7643, bind 0.0.0.0`
(note: `~/.jcode/config.toml`, NOT `~/.config/jcode/`)
- `~/.bashrc` env: `JCODE_BEDROCK_ENABLE=1`, `AWS_REGION=us-east-1`,
`JCODE_BEDROCK_MODEL=us.anthropic.claude-opus-4-6-v1`, `JCODE_GATEWAY_HOST=100.109.78.41`
- Helpers: `~/bin/jc` (jcode with bedrock), `~/bin/phone` (attach-or-create tmux jcode)
- `loginctl enable-linger ec2-user` so the user service runs at boot
- Instance attr `instance-initiated-shutdown-behavior=stop` so `poweroff` = stopped (not billed)
## Cost guardrails (all live-tested)
| Layer | Trigger | Action |
|---|---|---|
| idle-autostop | 30 min no clients + not streaming | instance powers itself off |
| `jcode-bedrock-tokens-warn` | >3M input tokens / 15 min | email |
| `jcode-bedrock-tokens-stop` | >10M input tokens / 15 min, 2 periods | breaker stops instance + email |
| AWS budget `jcode-dev-monthly-cost` | forecast >50% / actual >80% | email |
| AWS budget `jcode-dev-monthly-cost` | actual >100% of $10 | SNS breaker stops instance + email |
The legacy `AWS/Billing/EstimatedCharges` alarms were removed because the account was not publishing that metric. The Budget notification path is active and verified. Stopped instance cost remains approximately $6/mo for encrypted EBS plus the idle Elastic IP. The Elastic IP is retained because this existing instance needs public IPv4 for outbound Bedrock, SSM, and Tailscale connectivity when it boots; all public inbound security-group rules are closed.
## Phone flow
1. Bookmark the wake link (`https://<api-id>.execute-api.us-east-1.amazonaws.com/#t=<token>`, token stored at `~/.jcode/jcode-phone-wake-token` on the workstation). The URL fragment is not sent in HTTP requests; JavaScript exchanges it for an `Authorization: Bearer` header and keeps it in session storage.
2. Tap it: the Lambda starts the instance and polls EC2/SSM every 5 s until ready.
3. Tap "Pair this phone" → Lambda runs `jcode pair` through SSM → 6-digit code + `jcode://` deep link → opens the iOS app paired to `100.109.78.41:7643`.
4. SSH fallback: connect through Tailscale to `ec2-user@100.109.78.41`, then run `phone`.
## Security notes
- Gateway `/pair` requires a live 6-digit code (5-min TTL); WS requires the bearer token minted at pairing. Tokens are stored hashed server-side.
- Wake actions require a bearer token. The bookmark stores it in the URL fragment, which browsers do not send to API Gateway. Legacy `?t=` links redirect once to the fragment form.
- The EC2 security group has no public ingress. Gateway and SSH access are tailnet-only through `jcode-phone` (`100.109.78.41`).
- Pair generation uses SSM Run Command, so port 7644 is no longer public and the legacy pair service is disabled.
- The root EBS volume and future EBS volumes are encrypted by default.
- CloudTrail records multi-region management events to a private encrypted bucket with 90-day retention. Account-level IAM Access Analyzer and S3 Block Public Access are enabled.
- API Gateway access logs exclude query strings and authorization headers and expire after 30 days.
- IAM: the instance role has inference-only access to the configured Opus 4.6 profile plus SSM managed-instance access. Waker Lambda has start/describe EC2 plus narrowly scoped SSM command permissions. Breaker Lambda has stop/describe EC2 plus SNS publish.
- The deployment access key was rotated and the prior key is inactive. Daily maintenance can use the tested `jcode-operator` profile and `JcodeOperator` role, which cannot create IAM users or terminate instances. `jade-deploy` retains its administrator attachment only as a temporary recovery path until an independent root/MFA login is verified; see `IAM-LEAST-PRIVILEGE.md`.
## Rebuild from scratch (≈15 min)
1. Launch AL2023 x86_64 with an encrypted 30GB gp3 root volume, the Bedrock + SSM instance role, and a security group with no inbound rules. Associate an Elastic IP for outbound internet while running.
2. Install jcode, Tailscale, tmux, and git. Join the tailnet as `jcode-phone`; set the jcode gateway host to `100.109.78.41`.
3. Copy `units/jcode-serve.service` and the idle-autostop unit/script; enable linger and the required services. The legacy pair service is not required.
4. Deploy `wake-lambda.py` as `waker.py`, set `WAKE_TOKEN`, `INSTANCE_ID`, and `JCODE_GATEWAY_HOST=100.109.78.41`, and grant scoped EC2/SSM permissions. API Gateway HTTP API routes to the Lambda.
5. Create the SNS topics/subscriptions and connect the $10 Budget's 100% actual notification to `jcode-guard-stop`.
6. Enable CloudTrail, Access Analyzer, account-level S3 Block Public Access, EBS encryption by default, and bounded CloudWatch log retention.
7. Test: breaker stops the host; wake link starts it; SSM reports online; tailnet `/health` works; pair button returns a code targeting the tailnet address.
+16
View File
@@ -0,0 +1,16 @@
import boto3
INSTANCE_ID = "i-08214cf66cd3f80c7"
def handler(event, context):
ec2 = boto3.client("ec2", region_name="us-east-1")
state = ec2.describe_instances(InstanceIds=[INSTANCE_ID])["Reservations"][0]["Instances"][0]["State"]["Name"]
if state == "running":
ec2.stop_instances(InstanceIds=[INSTANCE_ID])
msg = f"Circuit breaker: stopped {INSTANCE_ID} (was {state})"
else:
msg = f"Circuit breaker: {INSTANCE_ID} already {state}"
sns = boto3.client("sns", region_name="us-east-1")
sns.publish(TopicArn="arn:aws:sns:us-east-1:302154194530:jcode-guard-warn",
Subject="jcode circuit breaker fired", Message=msg)
return {"message": msg}
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash
# Power off after 30 min with no clients AND no active agent work.
STATE_FILE=/var/tmp/idle-since
CONNS=$(ss -Htn state established "( sport = :7643 or sport = :22 )" | wc -l)
# jcode server doing outbound work (e.g. streaming from Bedrock)?
JPID=$(pgrep -f "jcode.*serve" | head -1)
BUSY=0
if [ -n "$JPID" ]; then
OUT443=$(ss -Htnp state established "( dport = :443 )" 2>/dev/null | grep -c "pid=$JPID") || true
[ "$OUT443" -gt 0 ] && BUSY=1
fi
if [ "$CONNS" -gt 0 ] || [ "$BUSY" -gt 0 ]; then
rm -f "$STATE_FILE"
exit 0
fi
NOW=$(date +%s)
if [ ! -f "$STATE_FILE" ]; then
echo "$NOW" > "$STATE_FILE"
exit 0
fi
IDLE_SECS=$((NOW - $(cat "$STATE_FILE")))
if [ "$IDLE_SECS" -ge 1800 ]; then
logger "idle-autostop: idle ${IDLE_SECS}s, powering off"
systemctl poweroff
fi
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Token-protected HTTP service that generates jcode pairing codes.
GET /pair-code?t=<token> -> {"code": "123456", "host": "100.109.78.41", "port": 7643, "uri": "jcode://pair?..."}
"""
import http.server, json, re, subprocess, os
from urllib.parse import urlparse, parse_qs
TOKEN = open('/etc/jcode-pair-token').read().strip()
HOST = '100.109.78.41'
PORT = 7643
class H(http.server.BaseHTTPRequestHandler):
def log_message(self, *a):
pass
def _send(self, code, obj):
body = json.dumps(obj).encode()
self.send_response(code)
self.send_header('Content-Type', 'application/json')
self.send_header('Cache-Control', 'no-store')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
u = urlparse(self.path)
q = parse_qs(u.query)
if q.get('t', [''])[0] != TOKEN:
return self._send(403, {'error': 'forbidden'})
if u.path != '/pair-code':
return self._send(404, {'error': 'not found'})
env = dict(os.environ)
env['PATH'] = '/home/ec2-user/.local/bin:' + env.get('PATH', '')
env['JCODE_GATEWAY_HOST'] = HOST
try:
out = subprocess.run(
['sudo', '-u', 'ec2-user', '-i', 'jcode', 'pair'],
capture_output=True, text=True, timeout=30, env=env,
)
text = re.sub(r'\x1b\[[0-9;]*m', '', out.stdout + out.stderr)
m = re.search(r'Pairing code:\s+(\d{3})\s(\d{3})', text)
if not m:
return self._send(500, {'error': 'no code in output'})
code = m.group(1) + m.group(2)
uri = f'jcode://pair?host={HOST}&port={PORT}&code={code}'
return self._send(200, {'code': code, 'host': HOST, 'port': PORT, 'uri': uri, 'expires_in': 300})
except Exception as e:
return self._send(500, {'error': str(e)})
http.server.HTTPServer(('0.0.0.0', 7644), H).serve_forever()
+98
View File
@@ -0,0 +1,98 @@
import importlib.util
import json
import os
from pathlib import Path
import sys
import types
import unittest
from unittest.mock import patch
os.environ["WAKE_TOKEN"] = "test-token"
os.environ["JCODE_GATEWAY_HOST"] = "100.64.0.10"
sys.modules.setdefault("boto3", types.ModuleType("boto3"))
sys.modules["boto3"].client = lambda *_args, **_kwargs: None
SPEC = importlib.util.spec_from_file_location(
"wake_lambda", Path(__file__).with_name("wake-lambda.py")
)
wake = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(wake)
class FakeEc2:
def __init__(self, state="running"):
self.state = state
self.started = False
def describe_instances(self, **_kwargs):
return {"Reservations": [{"Instances": [{"State": {"Name": self.state}}]}]}
def start_instances(self, **_kwargs):
self.started = True
class InvocationDoesNotExist(Exception):
pass
class FakeSsm:
class exceptions:
InvocationDoesNotExist = InvocationDoesNotExist
def describe_instance_information(self, **_kwargs):
return {"InstanceInformationList": [{"PingStatus": "Online"}]}
def send_command(self, **_kwargs):
return {"Command": {"CommandId": "command-1"}}
def get_command_invocation(self, **_kwargs):
return {
"Status": "Success",
"StandardOutputContent": "",
"StandardErrorContent": "Pairing code: \x1b[1;37m123 456\x1b[0m",
}
class WakeLambdaTests(unittest.TestCase):
def event(self, method, *, token=None, body=None, query=None):
headers = {"Authorization": f"Bearer {token}"} if token else {}
return {
"requestContext": {"http": {"method": method}},
"headers": headers,
"queryStringParameters": query,
"body": json.dumps(body) if body is not None else None,
}
def test_landing_page_never_embeds_token(self):
result = wake.handler(self.event("GET"), None)
self.assertEqual(result["statusCode"], 200)
self.assertNotIn("test-token", result["body"])
self.assertIn("Content-Security-Policy", result["headers"])
def test_legacy_query_redirects_token_into_fragment(self):
result = wake.handler(self.event("GET", query={"t": "test-token"}), None)
self.assertEqual(result["statusCode"], 302)
self.assertEqual(result["headers"]["Location"], "/#t=test-token")
def test_post_requires_bearer_token(self):
result = wake.handler(self.event("POST", body={"action": "status"}), None)
self.assertEqual(result["statusCode"], 403)
def test_status_uses_ec2_and_ssm_health(self):
ec2, ssm = FakeEc2(), FakeSsm()
with patch.object(wake.boto3, "client", side_effect=[ec2, ssm]):
result = wake.handler(
self.event("POST", token="test-token", body={"action": "status"}), None
)
self.assertEqual(result["statusCode"], 200)
self.assertTrue(json.loads(result["body"])["healthy"])
def test_pairing_parses_ansi_output_and_returns_tailnet_host(self):
result = wake.fetch_pair_code(FakeSsm())
self.assertEqual(result["code"], "123456")
self.assertEqual(result["host"], "100.64.0.10")
self.assertEqual(result["uri"], "jcode://pair?host=100.64.0.10&port=7643&code=123456")
if __name__ == "__main__":
unittest.main()
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env python3
"""Post-upload TestFlight setup for JCodeMobile.
Run after a build reaches App Store Connect (PLA must be accepted):
/tmp/ascvenv/bin/python scripts/phone-server/testflight-setup.py
Idempotent. Does three things:
1. Finds the com.jcode.mobile app and its latest build.
2. Ensures an internal beta group exists with the account holder as tester.
3. Assigns the latest build to that group so it is installable in TestFlight.
"""
import json
import sys
import time
import urllib.request
import urllib.error
KEY_ID = "XJKP4235XC"
ISSUER = "f1147f07-48fe-4850-9171-f37d4b2dee41"
KEY_PATH = "/home/jeremy/Downloads/AuthKey_XJKP4235XC.p8"
BUNDLE_ID = "com.jcode.mobile"
TESTER_EMAIL = "jeremyhuang55555@gmail.com"
GROUP_NAME = "internal"
API = "https://api.appstoreconnect.apple.com/v1"
def token():
import jwt
key = open(KEY_PATH).read()
return jwt.encode(
{
"iss": ISSUER,
"iat": int(time.time()),
"exp": int(time.time()) + 1200,
"aud": "appstoreconnect-v1",
},
key,
algorithm="ES256",
headers={"kid": KEY_ID},
)
def req(method, path, body=None):
url = path if path.startswith("http") else API + path
data = json.dumps(body).encode() if body is not None else None
r = urllib.request.Request(url, data=data, method=method)
r.add_header("Authorization", f"Bearer {token()}")
if data:
r.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(r, timeout=30) as resp:
raw = resp.read().decode()
return resp.status, json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
return e.code, json.loads(e.read().decode() or "{}")
def main():
st, apps = req("GET", f"/apps?filter[bundleId]={BUNDLE_ID}")
if st != 200 or not apps.get("data"):
print(f"app lookup failed ({st}): {json.dumps(apps)[:300]}")
return 1
app_id = apps["data"][0]["id"]
print(f"app: {app_id}")
st, builds = req(
"GET",
f"/builds?filter[app]={app_id}&sort=-uploadedDate&limit=1",
)
if st != 200 or not builds.get("data"):
print(f"no builds yet ({st}): {json.dumps(builds)[:300]}")
return 1
build = builds["data"][0]
build_id = build["id"]
attrs = build["attributes"]
print(f"latest build: {attrs.get('version')} ({attrs.get('processingState')})")
# Beta group (create if missing)
st, groups = req(
"GET", f"/betaGroups?filter[app]={app_id}&filter[name]={GROUP_NAME}"
)
if groups.get("data"):
group_id = groups["data"][0]["id"]
else:
st, g = req(
"POST",
"/betaGroups",
{
"data": {
"type": "betaGroups",
"attributes": {"name": GROUP_NAME, "isInternalGroup": True},
"relationships": {
"app": {"data": {"type": "apps", "id": app_id}}
},
}
},
)
if st not in (200, 201):
print(f"group create failed ({st}): {json.dumps(g)[:300]}")
return 1
group_id = g["data"]["id"]
print(f"group: {group_id}")
# Tester (create if missing, then add to group)
st, testers = req("GET", f"/betaTesters?filter[email]={TESTER_EMAIL}")
if testers.get("data"):
tester_id = testers["data"][0]["id"]
else:
st, t = req(
"POST",
"/betaTesters",
{
"data": {
"type": "betaTesters",
"attributes": {"email": TESTER_EMAIL, "firstName": "Jeremy"},
"relationships": {
"betaGroups": {
"data": [{"type": "betaGroups", "id": group_id}]
}
},
}
},
)
if st not in (200, 201):
print(f"tester create failed ({st}): {json.dumps(t)[:300]}")
return 1
tester_id = t["data"]["id"]
print(f"tester: {tester_id}")
# Make sure tester is in the group (no-op if already)
req(
"POST",
f"/betaGroups/{group_id}/relationships/betaTesters",
{"data": [{"type": "betaTesters", "id": tester_id}]},
)
# Assign build to group
st, r = req(
"POST",
f"/betaGroups/{group_id}/relationships/builds",
{"data": [{"type": "builds", "id": build_id}]},
)
if st not in (200, 201, 204):
print(f"build assign failed ({st}): {json.dumps(r)[:300]}")
return 1
print("build assigned to group. TestFlight install should be available.")
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,5 @@
[Unit]
Description=Stop instance when idle
[Service]
Type=oneshot
ExecStart=/usr/local/bin/idle-autostop.sh
@@ -0,0 +1,7 @@
[Unit]
Description=Idle auto-stop check every 5 min
[Timer]
OnBootSec=10min
OnUnitActiveSec=5min
[Install]
WantedBy=timers.target
@@ -0,0 +1,11 @@
[Unit]
Description=jcode pairing code service
After=network.target
[Service]
ExecStart=/usr/bin/python3 /usr/local/bin/jcode-pair-service.py
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,16 @@
[Unit]
Description=jcode server with WebSocket gateway
After=network.target
[Service]
Environment=PATH=/home/ec2-user/.local/bin:/usr/local/bin:/usr/bin:/bin
Environment=JCODE_BEDROCK_ENABLE=1
Environment=AWS_REGION=us-east-1
Environment=JCODE_BEDROCK_MODEL=us.anthropic.claude-opus-4-6-v1
ExecStartPre=/bin/rm -f /run/user/1000/jcode-daemon.lock /run/user/1000/jcode.sock /run/user/1000/jcode.sock.hash /run/user/1000/jcode-debug.sock
ExecStart=/home/ec2-user/.local/bin/jcode --provider bedrock serve
Restart=always
RestartSec=5
[Install]
WantedBy=default.target
+199
View File
@@ -0,0 +1,199 @@
import boto3
import hmac
import json
import os
import re
import secrets
import shlex
import time
from urllib.parse import quote
INSTANCE_ID = os.environ.get("INSTANCE_ID", "i-08214cf66cd3f80c7")
TOKEN = os.environ.get("WAKE_TOKEN", "REPLACE_WITH_WAKE_TOKEN")
HOST = os.environ.get("JCODE_GATEWAY_HOST", "100.109.78.41")
PORT = int(os.environ.get("JCODE_GATEWAY_PORT", "7643"))
REGION = os.environ.get("AWS_REGION", "us-east-1")
ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
PAIRING_CODE_RE = re.compile(r"Pairing code:\s+(\d{3})\s+(\d{3})")
def response(status, body="", content_type="application/json", headers=None):
result_headers = {
"Cache-Control": "no-store",
"Content-Type": content_type,
"Referrer-Policy": "no-referrer",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
}
if headers:
result_headers.update(headers)
return {"statusCode": status, "headers": result_headers, "body": body}
def json_response(status, obj):
return response(status, json.dumps(obj))
def authorized(event):
headers = {k.lower(): v for k, v in (event.get("headers") or {}).items()}
supplied = headers.get("authorization", "")
expected = f"Bearer {TOKEN}"
return hmac.compare_digest(supplied, expected)
def instance_state(ec2):
instance = ec2.describe_instances(InstanceIds=[INSTANCE_ID])["Reservations"][0]["Instances"][0]
return instance["State"]["Name"]
def ssm_online(ssm):
result = ssm.describe_instance_information(
Filters=[{"Key": "InstanceIds", "Values": [INSTANCE_ID]}],
MaxResults=5,
)
return any(item.get("PingStatus") == "Online" for item in result.get("InstanceInformationList", []))
def fetch_pair_code(ssm):
host = shlex.quote(HOST)
command = (
"sudo -iu ec2-user env "
f"JCODE_GATEWAY_HOST={host} "
"/home/ec2-user/.local/bin/jcode pair"
)
command_id = ssm.send_command(
InstanceIds=[INSTANCE_ID],
DocumentName="AWS-RunShellScript",
Parameters={"commands": [command]},
TimeoutSeconds=35,
)["Command"]["CommandId"]
deadline = time.monotonic() + 35
invocation = None
while time.monotonic() < deadline:
try:
invocation = ssm.get_command_invocation(CommandId=command_id, InstanceId=INSTANCE_ID)
except ssm.exceptions.InvocationDoesNotExist:
time.sleep(1)
continue
if invocation["Status"] in {"Success", "Failed", "Cancelled", "TimedOut"}:
break
time.sleep(1)
if not invocation or invocation.get("Status") != "Success":
status = invocation.get("Status", "TimedOut") if invocation else "TimedOut"
return {"error": f"pair command {status.lower()}"}
text = ANSI_RE.sub(
"",
invocation.get("StandardOutputContent", "") + invocation.get("StandardErrorContent", ""),
)
match = PAIRING_CODE_RE.search(text)
if not match:
return {"error": "no pairing code in command output"}
code = match.group(1) + match.group(2)
return {
"code": code,
"host": HOST,
"port": PORT,
"uri": f"jcode://pair?host={HOST}&port={PORT}&code={code}",
"expires_in": 300,
}
def landing_page():
nonce = secrets.token_urlsafe(18)
html = """<!doctype html><html><head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>jcode server</title>
<style nonce="__NONCE__">
body{font-family:-apple-system,system-ui;background:#101314;color:#eee;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0}
.card{text-align:center;padding:32px;max-width:360px}h1{color:#4DD9A6;font-size:1.6em;margin-bottom:8px}
#s{font-size:1.05em;line-height:1.5}.dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:8px;background:#e6b450}
.ok .dot{background:#4DD9A6}.ok #s b{color:#4DD9A6}.spin{opacity:.75;font-size:.9em;margin-top:14px}
#pairbtn{display:none;margin-top:22px;background:#4DD9A6;color:#0c0f10;border:0;border-radius:12px;padding:14px 22px;font-size:1.05em;font-weight:600}
#pairout{margin-top:16px;font-size:1em;line-height:1.6}#pairout .code{font-size:1.9em;letter-spacing:.18em;color:#4DD9A6;font-weight:700}#pairout a{color:#4DD9A6}
</style></head><body><div class="card" id="c">
<h1>jcode server</h1><p id="s"><span class="dot"></span>Authenticating…</p>
<p class="spin" id="hint">checking every 5s…</p><button id="pairbtn">Pair this phone</button><div id="pairout"></div>
<script nonce="__NONCE__">
const fragment = new URLSearchParams(location.hash.slice(1));
if (fragment.get('t')) sessionStorage.setItem('jcode-wake-token', fragment.get('t'));
history.replaceState(null, '', location.pathname);
const token = sessionStorage.getItem('jcode-wake-token');
const api = async action => {
if (!token) throw new Error('missing token; open the saved wake link');
const r = await fetch(location.pathname, {method:'POST', cache:'no-store',
headers:{'Authorization':'Bearer '+token,'Content-Type':'application/json'},
body:JSON.stringify({action})});
if (!r.ok) throw new Error(r.status === 403 ? 'invalid token' : 'request failed: '+r.status);
return r.json();
};
async function poll(){
try{
const j=await api('status'),s=document.getElementById('s'),c=document.getElementById('c');
if(j.healthy){c.classList.add('ok');s.innerHTML='<span class="dot"></span><b>Ready.</b> Open the jcode app now.';document.getElementById('hint').textContent='server is up';document.getElementById('pairbtn').style.display='inline-block';return;}
s.innerHTML='<span class="dot"></span>Instance: '+j.state+' · services warming up…';
}catch(e){document.getElementById('s').textContent=e.message;document.getElementById('hint').textContent='';return;}
setTimeout(poll,5000);
}
async function pair(){
const o=document.getElementById('pairout');o.textContent='generating code…';
try{const j=await api('pair');if(j.code){o.innerHTML='<div class="code">'+j.code.slice(0,3)+' '+j.code.slice(3)+'</div><div>host '+j.host+':'+j.port+' · expires in 5 min</div><div style="margin-top:10px"><a href="'+j.uri+'">Open in jcode app</a></div>';}else{o.textContent='error: '+(j.error||'unknown');}}catch(e){o.textContent='error: '+e.message;}
}
document.getElementById('pairbtn').addEventListener('click',pair);
(async()=>{try{await api('wake');poll();}catch(e){document.getElementById('s').textContent=e.message;document.getElementById('hint').textContent='';}})();
</script></div></body></html>""".replace("__NONCE__", nonce)
csp = (
"default-src 'none'; "
f"style-src 'nonce-{nonce}'; script-src 'nonce-{nonce}'; "
"connect-src 'self'; base-uri 'none'; frame-ancestors 'none'"
)
return response(200, html, "text/html; charset=utf-8", {"Content-Security-Policy": csp})
def handler(event, context):
method = (
event.get("requestContext", {}).get("http", {}).get("method")
or event.get("httpMethod")
or "GET"
).upper()
if method == "GET":
legacy_token = (event.get("queryStringParameters") or {}).get("t")
if legacy_token and hmac.compare_digest(legacy_token, TOKEN):
return response(302, headers={"Location": f"/#t={quote(TOKEN, safe='')}"})
return landing_page()
if method != "POST":
return json_response(405, {"error": "method not allowed"})
if not authorized(event):
return json_response(403, {"error": "forbidden"})
try:
payload = json.loads(event.get("body") or "{}")
except json.JSONDecodeError:
return json_response(400, {"error": "invalid JSON"})
action = payload.get("action")
ec2 = boto3.client("ec2", region_name=REGION)
ssm = boto3.client("ssm", region_name=REGION)
state = instance_state(ec2)
if action == "wake":
started = state == "stopped"
if started:
ec2.start_instances(InstanceIds=[INSTANCE_ID])
return json_response(200, {"state": state, "started": started})
if action == "status":
healthy = state == "running" and ssm_online(ssm)
return json_response(200, {"state": state, "healthy": healthy})
if action == "pair":
if state != "running":
return json_response(409, {"error": f"instance {state}, wake it first"})
if not ssm_online(ssm):
return json_response(503, {"error": "instance management agent is not ready"})
result = fetch_pair_code(ssm)
return json_response(200 if "code" in result else 500, result)
return json_response(400, {"error": "unknown action"})
+398
View File
@@ -0,0 +1,398 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import os
import pty
import select
import signal
import socket
import statistics
import subprocess
import tempfile
import time
from dataclasses import dataclass
from pathlib import Path
CLK_TCK = os.sysconf("SC_CLK_TCK")
PAGE_SIZE = os.sysconf("SC_PAGE_SIZE")
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="Profile resumed jcode PTY burst startup")
p.add_argument("--binary", default="./target/release/jcode")
p.add_argument("--burst", type=int, default=20)
p.add_argument("--timeout", type=float, default=15.0)
p.add_argument("--stagger-ms", type=float, default=0.0)
p.add_argument("--json-out", default="/tmp/jcode_remote_burst_profile.json")
return p.parse_args()
@dataclass
class ProcSample:
cpu_ticks: int
rss_kb: int
@dataclass
class ProcTracker:
start_cpu_ticks: int | None = None
last_cpu_ticks: int = 0
peak_rss_kb: int = 0
def record(self, sample: ProcSample | None) -> bool:
if sample is None:
return False
if self.start_cpu_ticks is None:
self.start_cpu_ticks = sample.cpu_ticks
self.last_cpu_ticks = sample.cpu_ticks
self.peak_rss_kb = max(self.peak_rss_kb, sample.rss_kb)
return True
def cpu_ms(self) -> float:
if self.start_cpu_ticks is None:
return 0.0
return (self.last_cpu_ticks - self.start_cpu_ticks) * 1000.0 / CLK_TCK
def read_proc_sample(pid: int) -> ProcSample | None:
try:
stat = Path(f"/proc/{pid}/stat").read_text()
except FileNotFoundError:
return None
end = stat.rfind(")")
if end == -1:
return None
fields = stat[end + 2 :].split()
if len(fields) <= 21:
return None
utime = int(fields[11])
stime = int(fields[12])
rss_pages = int(fields[21])
return ProcSample(cpu_ticks=utime + stime, rss_kb=rss_pages * PAGE_SIZE // 1024)
def wait_for_socket(path: Path, timeout_s: float = 10.0) -> None:
deadline = time.time() + timeout_s
while time.time() < deadline:
if path.exists():
try:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.settimeout(0.2)
sock.connect(str(path))
sock.close()
return
except OSError:
pass
time.sleep(0.01)
raise RuntimeError(f"socket not ready: {path}")
def create_session(debug_sock: Path, cwd: str = ".") -> str:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(str(debug_sock))
req = {"type": "debug_command", "id": 1, "command": f"create_session:{cwd}"}
sock.sendall((json.dumps(req) + "\n").encode())
buf = b""
while True:
chunk = sock.recv(65536)
if not chunk:
break
buf += chunk
while b"\n" in buf:
line, buf = buf.split(b"\n", 1)
resp = json.loads(line.decode())
if resp.get("type") == "ack":
continue
if resp.get("type") == "error":
raise RuntimeError(resp.get("message") or resp)
if resp.get("type") != "debug_response":
continue
if not resp.get("ok", True):
raise RuntimeError(resp.get("output") or resp)
output = json.loads(resp["output"])
return output["session_id"]
raise RuntimeError("missing debug response")
def reply_queries(master_fd: int, buffer: bytes) -> bytes:
replies = [
(b"\x1b[6n", b"\x1b[1;1R"),
(b"\x1b[c", b"\x1b[?62;c"),
(b"\x1b]10;?\x1b\\", b"\x1b]10;rgb:ffff/ffff/ffff\x1b\\"),
(b"\x1b]11;?\x1b\\", b"\x1b]11;rgb:0000/0000/0000\x1b\\"),
(b"\x1b]10;?\x07", b"\x1b]10;rgb:ffff/ffff/ffff\x07"),
(b"\x1b]11;?\x07", b"\x1b]11;rgb:0000/0000/0000\x07"),
(b"\x1b[14t", b"\x1b[4;600;800t"),
(b"\x1b[16t", b"\x1b[6;16;8t"),
(b"\x1b[18t", b"\x1b[8;24;80t"),
(b"\x1b[?1016$p", b"\x1b[?1016;1$y"),
(b"\x1b[?2027$p", b"\x1b[?2027;1$y"),
(b"\x1b[?2031$p", b"\x1b[?2031;1$y"),
(b"\x1b[?1004$p", b"\x1b[?1004;1$y"),
(b"\x1b[?2004$p", b"\x1b[?2004;1$y"),
(b"\x1b[?2026$p", b"\x1b[?2026;1$y"),
]
changed = True
while changed:
changed = False
for query, response in replies:
if query in buffer:
os.write(master_fd, response)
buffer = buffer.replace(query, b"")
changed = True
return buffer
@dataclass
class LiveClient:
session_id: str
proc: subprocess.Popen
master_fd: int
start: float
buffer: bytes
tracker: ProcTracker
first_output_ms: float | None = None
last_output_at: float | None = None
done: bool = False
def start_resume_client(binary: str, env: dict[str, str], session_id: str) -> LiveClient:
master_fd, slave_fd = pty.openpty()
start = time.perf_counter()
proc = subprocess.Popen(
[
binary,
"--no-update",
"--no-selfdev",
"--socket",
env["JCODE_SOCKET"],
"--fresh-spawn",
"--resume",
session_id,
],
stdin=slave_fd,
stdout=slave_fd,
stderr=slave_fd,
env=env,
preexec_fn=os.setsid,
)
os.close(slave_fd)
os.set_blocking(master_fd, False)
return LiveClient(
session_id=session_id,
proc=proc,
master_fd=master_fd,
start=start,
buffer=b"",
tracker=ProcTracker(),
)
def finish_client(client: LiveClient) -> dict:
try:
client.tracker.record(read_proc_sample(client.proc.pid))
return {
"session_id": client.session_id,
"pid": client.proc.pid,
"first_output_ms": client.first_output_ms,
"buffer_bytes": len(client.buffer),
"cpu_ms": client.tracker.cpu_ms(),
"peak_rss_kb": client.tracker.peak_rss_kb,
}
finally:
os.close(client.master_fd)
try:
os.killpg(client.proc.pid, signal.SIGTERM)
except ProcessLookupError:
pass
try:
client.proc.wait(timeout=0.05)
except Exception:
try:
os.killpg(client.proc.pid, signal.SIGKILL)
except ProcessLookupError:
pass
def run_burst(
binary: str,
env: dict[str, str],
session_ids: list[str],
timeout_s: float,
server_pid: int,
stagger_ms: float,
) -> tuple[list[dict], dict[str, float | int]]:
settle_after_output_s = 0.15
clients: list[LiveClient] = []
fd_to_index: dict[int, int] = {}
launch_index = 0
next_launch_at = time.perf_counter()
deadline = time.perf_counter() + timeout_s
server_tracker = ProcTracker()
peak_clients_rss_kb = 0
peak_live_clients = 0
def sample_processes() -> None:
nonlocal peak_clients_rss_kb, peak_live_clients
server_tracker.record(read_proc_sample(server_pid))
live_clients = 0
clients_rss_kb = 0
for client in clients:
sample = read_proc_sample(client.proc.pid)
if client.tracker.record(sample):
live_clients += 1
clients_rss_kb += sample.rss_kb if sample is not None else 0
peak_live_clients = max(peak_live_clients, live_clients)
peak_clients_rss_kb = max(peak_clients_rss_kb, clients_rss_kb)
while time.perf_counter() < deadline and (
launch_index < len(session_ids) or any(not client.done for client in clients)
):
now = time.perf_counter()
while launch_index < len(session_ids) and now >= next_launch_at:
client = start_resume_client(binary, env, session_ids[launch_index])
fd_to_index[client.master_fd] = len(clients)
clients.append(client)
launch_index += 1
next_launch_at += stagger_ms / 1000.0 if stagger_ms > 0 else 0.0
now = time.perf_counter()
sample_processes()
active_fds = [client.master_fd for client in clients if not client.done]
timeout = 0.05
if launch_index < len(session_ids):
timeout = max(0.0, min(timeout, next_launch_at - time.perf_counter()))
if not active_fds and launch_index < len(session_ids):
time.sleep(timeout)
continue
if not active_fds:
break
rlist, _, _ = select.select(active_fds, [], [], timeout)
for fd in rlist:
client = clients[fd_to_index[fd]]
try:
chunk = os.read(fd, 65536)
except BlockingIOError:
chunk = b""
if not chunk:
client.done = True
continue
if client.first_output_ms is None:
client.first_output_ms = (time.perf_counter() - client.start) * 1000.0
client.last_output_at = time.perf_counter()
client.buffer += chunk
client.buffer = reply_queries(fd, client.buffer)
lower = client.buffer.lower()
if b"loading session" in lower or b"jcode" in lower or len(client.buffer) > 2048:
client.done = True
for client in clients:
if client.done:
continue
if client.proc.poll() is not None:
client.done = True
continue
if (
client.first_output_ms is not None
and client.last_output_at is not None
and time.perf_counter() - client.last_output_at >= settle_after_output_s
):
client.done = True
for client in clients:
client.done = True
sample_processes()
results = [finish_client(client) for client in clients]
metrics = {
"server_cpu_ms": server_tracker.cpu_ms(),
"server_peak_rss_kb": server_tracker.peak_rss_kb,
"clients_cpu_ms": sum(result["cpu_ms"] for result in results),
"clients_peak_rss_kb": peak_clients_rss_kb,
"peak_live_clients": peak_live_clients,
}
return results, metrics
def main() -> None:
args = parse_args()
root = Path(tempfile.mkdtemp(prefix="jcode-remote-burst-"))
home = root / "home"
run = root / "run"
home.mkdir(parents=True, exist_ok=True)
run.mkdir(parents=True, exist_ok=True)
env = os.environ.copy()
env["JCODE_HOME"] = str(home)
env["JCODE_RUNTIME_DIR"] = str(run)
env["JCODE_SOCKET"] = str(run / "jcode.sock")
env["JCODE_NO_TELEMETRY"] = "1"
env["JCODE_DEBUG_CONTROL"] = "1"
env["JCODE_TEMP_SERVER"] = "1"
env["JCODE_SERVER_OWNER_PID"] = str(os.getpid())
debug_socket = run / "jcode-debug.sock"
server = subprocess.Popen(
[args.binary, "serve", "--socket", env["JCODE_SOCKET"], "--debug-socket"],
env=env,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
preexec_fn=os.setsid,
)
try:
wait_for_socket(Path(env["JCODE_SOCKET"]))
wait_for_socket(debug_socket)
session_ids = [create_session(debug_socket, os.getcwd()) for _ in range(args.burst)]
wall_start = time.perf_counter()
results, proc_metrics = run_burst(
args.binary,
env,
session_ids,
args.timeout,
server.pid,
args.stagger_ms,
)
wall_ms = (time.perf_counter() - wall_start) * 1000.0
firsts = [r["first_output_ms"] for r in results if r["first_output_ms"] is not None]
output = {
"burst": args.burst,
"stagger_ms": args.stagger_ms,
"wall_ms": wall_ms,
"server_cpu_ms": proc_metrics["server_cpu_ms"],
"clients_cpu_ms": proc_metrics["clients_cpu_ms"],
"total_cpu_ms": proc_metrics["server_cpu_ms"] + proc_metrics["clients_cpu_ms"],
"cpu_utilization_ratio": 0.0
if wall_ms == 0
else (proc_metrics["server_cpu_ms"] + proc_metrics["clients_cpu_ms"]) / wall_ms,
"first_output_ms": {
"min": min(firsts) if firsts else None,
"p50": statistics.median(firsts) if firsts else None,
"max": max(firsts) if firsts else None,
},
"buffer_bytes_total": sum(r["buffer_bytes"] for r in results),
"server_peak_rss_kb": proc_metrics["server_peak_rss_kb"],
"clients_peak_rss_kb": proc_metrics["clients_peak_rss_kb"],
"peak_live_clients": proc_metrics["peak_live_clients"],
"results": results,
}
Path(args.json_out).write_text(json.dumps(output, indent=2))
print(json.dumps(output, indent=2))
finally:
try:
os.killpg(server.pid, signal.SIGTERM)
except ProcessLookupError:
pass
try:
server.wait(timeout=2)
except Exception:
try:
os.killpg(server.pid, signal.SIGKILL)
except ProcessLookupError:
pass
if __name__ == "__main__":
main()
+250
View File
@@ -0,0 +1,250 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import os
import pty
import select
import signal
import socket
import subprocess
import tempfile
import time
from pathlib import Path
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Profile a single resumed jcode spawn")
parser.add_argument("--binary", default="./target/selfdev/jcode")
parser.add_argument("--timeout", type=float, default=20.0)
parser.add_argument("--cwd", default=os.getcwd())
parser.add_argument("--json", action="store_true", help="Emit JSON summary")
return parser.parse_args()
def wait_for_socket(path: Path, timeout_s: float = 10.0) -> None:
deadline = time.time() + timeout_s
while time.time() < deadline:
if path.exists():
try:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.settimeout(0.2)
sock.connect(str(path))
sock.close()
return
except OSError:
pass
time.sleep(0.01)
raise RuntimeError(f"socket not ready: {path}")
def create_session(debug_sock: Path, cwd: str) -> str:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(str(debug_sock))
req = {"type": "debug_command", "id": 1, "command": f"create_session:{cwd}"}
sock.sendall((json.dumps(req) + "\n").encode())
buf = b""
while True:
chunk = sock.recv(65536)
if not chunk:
break
buf += chunk
while b"\n" in buf:
line, buf = buf.split(b"\n", 1)
resp = json.loads(line.decode())
if resp.get("type") == "ack":
continue
if resp.get("type") == "error":
raise RuntimeError(resp.get("message") or resp)
if resp.get("type") != "debug_response":
continue
if not resp.get("ok", True):
raise RuntimeError(resp.get("output") or resp)
output = json.loads(resp["output"])
return output["session_id"]
raise RuntimeError("missing debug response")
def reply_queries(master_fd: int, buffer: bytes) -> bytes:
replies = [
(b"\x1b[6n", b"\x1b[1;1R"),
(b"\x1b[c", b"\x1b[?62;c"),
(b"\x1b]10;?\x1b\\", b"\x1b]10;rgb:ffff/ffff/ffff\x1b\\"),
(b"\x1b]11;?\x1b\\", b"\x1b]11;rgb:0000/0000/0000\x1b\\"),
(b"\x1b]10;?\x07", b"\x1b]10;rgb:ffff/ffff/ffff\x07"),
(b"\x1b]11;?\x07", b"\x1b]11;rgb:0000/0000/0000\x07"),
(b"\x1b[14t", b"\x1b[4;600;800t"),
(b"\x1b[16t", b"\x1b[6;16;8t"),
(b"\x1b[18t", b"\x1b[8;24;80t"),
(b"\x1b[?1016$p", b"\x1b[?1016;1$y"),
(b"\x1b[?2027$p", b"\x1b[?2027;1$y"),
(b"\x1b[?2031$p", b"\x1b[?2031;1$y"),
(b"\x1b[?1004$p", b"\x1b[?1004;1$y"),
(b"\x1b[?2004$p", b"\x1b[?2004;1$y"),
(b"\x1b[?2026$p", b"\x1b[?2026;1$y"),
]
changed = True
while changed:
changed = False
for query, response in replies:
if query in buffer:
os.write(master_fd, response)
buffer = buffer.replace(query, b"")
changed = True
return buffer
def latest_log_file(log_dir: Path) -> Path:
logs = sorted(log_dir.glob("jcode-*.log"), key=lambda p: p.stat().st_mtime)
if not logs:
raise RuntimeError(f"no log files found in {log_dir}")
return logs[-1]
def extract_timing_lines(log_path: Path) -> list[str]:
timing_lines = []
for line in log_path.read_text(errors="replace").splitlines():
if "[TIMING]" in line or "Startup Profile (" in line:
timing_lines.append(line)
return timing_lines
def profile_single_spawn(binary: str, cwd: str, timeout_s: float) -> dict:
root = Path(tempfile.mkdtemp(prefix="jcode-single-profile-"))
home = root / "home"
runtime_dir = root / "run"
socket_path = runtime_dir / "jcode.sock"
debug_socket_path = runtime_dir / "jcode-debug.sock"
home.mkdir(parents=True, exist_ok=True)
runtime_dir.mkdir(parents=True, exist_ok=True)
env = os.environ.copy()
env.update(
{
"JCODE_HOME": str(home),
"JCODE_RUNTIME_DIR": str(runtime_dir),
"JCODE_SOCKET": str(socket_path),
"JCODE_DEBUG_SOCKET": str(debug_socket_path),
"JCODE_SWARM_ENABLED": "0",
"JCODE_NO_TELEMETRY": "1",
"JCODE_TRACE": "1",
"JCODE_TEMP_SERVER": "1",
"JCODE_SERVER_OWNER_PID": str(os.getpid()),
}
)
server_proc = subprocess.Popen(
[binary, "--no-update", "--no-selfdev", "serve", "--socket", str(socket_path)],
env=env,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
preexec_fn=os.setsid,
)
try:
wait_for_socket(socket_path, timeout_s=min(timeout_s, 10.0))
wait_for_socket(debug_socket_path, timeout_s=min(timeout_s, 10.0))
session_id = create_session(debug_socket_path, cwd)
master_fd, slave_fd = pty.openpty()
client_start = time.perf_counter()
client_proc = subprocess.Popen(
[
binary,
"--no-update",
"--no-selfdev",
"--socket",
str(socket_path),
"--fresh-spawn",
"--resume",
session_id,
],
stdin=slave_fd,
stdout=slave_fd,
stderr=slave_fd,
env=env,
preexec_fn=os.setsid,
)
os.close(slave_fd)
os.set_blocking(master_fd, False)
buffer = b""
first_output_ms = None
last_output_at = None
deadline = time.perf_counter() + timeout_s
settle_after_output_s = 0.2
while time.perf_counter() < deadline:
if client_proc.poll() is not None:
break
rlist, _, _ = select.select([master_fd], [], [], 0.05)
if rlist:
chunk = os.read(master_fd, 65536)
if not chunk:
break
if first_output_ms is None:
first_output_ms = (time.perf_counter() - client_start) * 1000.0
last_output_at = time.perf_counter()
buffer += chunk
buffer = reply_queries(master_fd, buffer)
lower = buffer.lower()
if b"loading session" in lower or b"jcode" in lower or len(buffer) > 4096:
if time.perf_counter() - last_output_at >= settle_after_output_s:
break
elif last_output_at is not None and time.perf_counter() - last_output_at >= settle_after_output_s:
break
os.close(master_fd)
try:
os.killpg(client_proc.pid, signal.SIGTERM)
except ProcessLookupError:
pass
try:
client_proc.wait(timeout=0.2)
except Exception:
try:
os.killpg(client_proc.pid, signal.SIGKILL)
except ProcessLookupError:
pass
log_path = latest_log_file(home / "logs")
return {
"temp_root": str(root),
"log_path": str(log_path),
"session_id": session_id,
"first_output_ms": first_output_ms,
"timing_lines": extract_timing_lines(log_path),
}
finally:
try:
os.killpg(server_proc.pid, signal.SIGTERM)
except ProcessLookupError:
pass
try:
server_proc.wait(timeout=1.0)
except Exception:
try:
os.killpg(server_proc.pid, signal.SIGKILL)
except ProcessLookupError:
pass
def main() -> None:
args = parse_args()
result = profile_single_spawn(args.binary, args.cwd, args.timeout)
if args.json:
print(json.dumps(result, indent=2))
return
print(f"temp root: {result['temp_root']}")
print(f"session id: {result['session_id']}")
print(f"log path: {result['log_path']}")
if result["first_output_ms"] is not None:
print(f"first output: {result['first_output_ms']:.1f}ms")
else:
print("first output: none")
print("timings:")
for line in result["timing_lines"]:
print(f" {line}")
if __name__ == "__main__":
main()
+662
View File
@@ -0,0 +1,662 @@
#!/usr/bin/env python3
"""Benchmark + profile the time and resource utilization of a jcode spawn.
A jcode "spawn" is the full cold-launch path of the interactive client:
process start -> tokio runtime -> startup setup -> arg parse
-> (cold) spawn the background server daemon -> connect TUI client.
This profiler complements scripts/bench_startup.py (which only measures time)
by also attributing RESOURCE UTILIZATION to each spawned process:
* wall-clock time (and the built-in startup-profile phase breakdown)
* peak / final RSS, anon vs file-backed RSS, PSS, swap
* CPU time (user + system) and average %CPU over the spawn window
* thread count high-water mark
* open file descriptors high-water mark
* minor / major page faults
* voluntary / involuntary context switches
* block I/O bytes read/written
Everything runs under an isolated JCODE_HOME / JCODE_RUNTIME_DIR / JCODE_SOCKET
so it never touches the user's real shared server, logs, sessions, or creds.
Two spawn shapes are profiled:
server : the background daemon (`jcode serve`) on a private socket. This is
the heavy half of a cold spawn and owns the long-lived footprint.
client : the cold interactive client launched in a PTY, which itself spawns
the server when none is running. We sample BOTH the client and the
daemon it spawns.
Usage:
python3 scripts/profile_spawn.py [BINARY] [--runs N] [--json out.json]
"""
from __future__ import annotations
import argparse
import json
import os
import re
import select
import shutil
import socket
import statistics
import subprocess
import sys
import tempfile
import time
from dataclasses import dataclass, field, asdict
from pathlib import Path
try:
import pty
import termios
import struct
import fcntl
_HAVE_PTY = True
except ImportError: # pragma: no cover - non-unix
_HAVE_PTY = False
CLK_TCK = os.sysconf("SC_CLK_TCK") if hasattr(os, "sysconf") else 100
PAGE_SIZE = os.sysconf("SC_PAGE_SIZE") if hasattr(os, "sysconf") else 4096
PROFILE_TOTAL_RE = re.compile(r"Startup Profile \(([0-9.]+)ms total\)")
PROFILE_LINE_RE = re.compile(
r"\[INFO\]\s+([0-9.]+)ms\s+([0-9.]+)ms\s+[0-9.]+%\s+([a-zA-Z0-9_]+)"
)
REMOTE_HISTORY_RE = re.compile(r"remote bootstrap: history after ([0-9.]+)ms")
# --------------------------------------------------------------------------- #
# /proc sampling
# --------------------------------------------------------------------------- #
def _read(path: str) -> str | None:
try:
with open(path, "r") as fh:
return fh.read()
except OSError:
return None
def _status_kib(status: str, key: str) -> int | None:
m = re.search(rf"^{re.escape(key)}\s+(\d+)\s*kB", status, re.MULTILINE)
return int(m.group(1)) * 1024 if m else None
@dataclass
class ProcSample:
"""One point-in-time sample of a process from /proc/<pid>."""
rss_bytes: int | None = None
rss_anon_bytes: int | None = None
rss_file_bytes: int | None = None
peak_rss_bytes: int | None = None
pss_bytes: int | None = None
swap_bytes: int | None = None
threads: int | None = None
fds: int | None = None
# cumulative counters
utime_s: float | None = None
stime_s: float | None = None
minflt: int | None = None
majflt: int | None = None
vol_ctxt: int | None = None
nonvol_ctxt: int | None = None
read_bytes: int | None = None
write_bytes: int | None = None
def sample_proc(pid: int) -> ProcSample | None:
status = _read(f"/proc/{pid}/status")
stat = _read(f"/proc/{pid}/stat")
if status is None or stat is None:
return None
s = ProcSample()
s.rss_bytes = _status_kib(status, "VmRSS:")
s.rss_anon_bytes = _status_kib(status, "RssAnon:")
s.rss_file_bytes = _status_kib(status, "RssFile:")
s.peak_rss_bytes = _status_kib(status, "VmHWM:")
s.swap_bytes = _status_kib(status, "VmSwap:")
tm = re.search(r"^Threads:\s+(\d+)", status, re.MULTILINE)
s.threads = int(tm.group(1)) if tm else None
rollup = _read(f"/proc/{pid}/smaps_rollup")
if rollup:
pm = re.search(r"^Pss:\s+(\d+)\s*kB", rollup, re.MULTILINE)
s.pss_bytes = int(pm.group(1)) * 1024 if pm else None
# /proc/<pid>/stat fields after "comm) ": index 0 == field 3 (state),
# so field N maps to fields[N-3]. minflt=10 majflt=12 utime=14 stime=15.
rparen = stat.rfind(")")
if rparen != -1:
fields = stat[rparen + 2 :].split()
try:
s.minflt = int(fields[10 - 3]) # field 10
s.majflt = int(fields[12 - 3]) # field 12
s.utime_s = int(fields[14 - 3]) / CLK_TCK # field 14
s.stime_s = int(fields[15 - 3]) / CLK_TCK # field 15
except (IndexError, ValueError):
pass
sched = status
vm = re.search(r"^voluntary_ctxt_switches:\s+(\d+)", sched, re.MULTILINE)
nm = re.search(r"^nonvoluntary_ctxt_switches:\s+(\d+)", sched, re.MULTILINE)
s.vol_ctxt = int(vm.group(1)) if vm else None
s.nonvol_ctxt = int(nm.group(1)) if nm else None
io = _read(f"/proc/{pid}/io")
if io:
rm = re.search(r"^read_bytes:\s+(\d+)", io, re.MULTILINE)
wm = re.search(r"^write_bytes:\s+(\d+)", io, re.MULTILINE)
s.read_bytes = int(rm.group(1)) if rm else None
s.write_bytes = int(wm.group(1)) if wm else None
try:
s.fds = len(os.listdir(f"/proc/{pid}/fd"))
except OSError:
s.fds = None
return s
@dataclass
class ResourceProfile:
"""Aggregated resource utilization for one spawned process over its window."""
wall_ms: float = 0.0
peak_rss_bytes: int = 0
final_rss_bytes: int = 0
peak_rss_anon_bytes: int = 0
peak_pss_bytes: int = 0
peak_swap_bytes: int = 0
peak_threads: int = 0
peak_fds: int = 0
cpu_user_s: float = 0.0
cpu_sys_s: float = 0.0
cpu_total_s: float = 0.0
cpu_pct: float = 0.0
minflt: int = 0
majflt: int = 0
vol_ctxt: int = 0
nonvol_ctxt: int = 0
io_read_bytes: int = 0
io_write_bytes: int = 0
samples: int = 0
class ProcessMonitor:
"""High-frequency sampler that tracks one pid's resource high-water marks."""
def __init__(self, pid: int):
self.pid = pid
self.first: ProcSample | None = None
self.last: ProcSample | None = None
self.peak_rss = 0
self.peak_rss_anon = 0
self.peak_pss = 0
self.peak_swap = 0
self.peak_threads = 0
self.peak_fds = 0
self.start = time.perf_counter()
self.end = self.start
self.n = 0
def poll(self) -> bool:
s = sample_proc(self.pid)
if s is None:
return False
self.n += 1
self.end = time.perf_counter()
if self.first is None:
self.first = s
self.last = s
if s.rss_bytes:
self.peak_rss = max(self.peak_rss, s.rss_bytes)
if s.peak_rss_bytes:
self.peak_rss = max(self.peak_rss, s.peak_rss_bytes)
if s.rss_anon_bytes:
self.peak_rss_anon = max(self.peak_rss_anon, s.rss_anon_bytes)
if s.pss_bytes:
self.peak_pss = max(self.peak_pss, s.pss_bytes)
if s.swap_bytes:
self.peak_swap = max(self.peak_swap, s.swap_bytes)
if s.threads:
self.peak_threads = max(self.peak_threads, s.threads)
if s.fds:
self.peak_fds = max(self.peak_fds, s.fds)
return True
def finalize(self) -> ResourceProfile:
p = ResourceProfile()
p.wall_ms = (self.end - self.start) * 1000.0
p.samples = self.n
p.peak_rss_bytes = self.peak_rss
p.peak_rss_anon_bytes = self.peak_rss_anon
p.peak_pss_bytes = self.peak_pss
p.peak_swap_bytes = self.peak_swap
p.peak_threads = self.peak_threads
p.peak_fds = self.peak_fds
if self.last:
p.final_rss_bytes = self.last.rss_bytes or 0
l = self.last
if l:
# Cumulative counters in /proc are totals since process birth. We
# monitor from spawn, so the final absolute value IS the total
# resource cost of the spawn (more robust than last-first, which
# would miss work done before the first sample landed).
if l.utime_s is not None:
p.cpu_user_s = l.utime_s
if l.stime_s is not None:
p.cpu_sys_s = l.stime_s
p.cpu_total_s = p.cpu_user_s + p.cpu_sys_s
if p.wall_ms > 0:
p.cpu_pct = 100.0 * p.cpu_total_s / (p.wall_ms / 1000.0)
if l.minflt is not None:
p.minflt = l.minflt
if l.majflt is not None:
p.majflt = l.majflt
if l.vol_ctxt is not None:
p.vol_ctxt = l.vol_ctxt
if l.nonvol_ctxt is not None:
p.nonvol_ctxt = l.nonvol_ctxt
if l.read_bytes is not None:
p.io_read_bytes = l.read_bytes
if l.write_bytes is not None:
p.io_write_bytes = l.write_bytes
return p
# --------------------------------------------------------------------------- #
# Environment isolation
# --------------------------------------------------------------------------- #
def isolated_env(root: str) -> dict[str, str]:
env = os.environ.copy()
env["JCODE_HOME"] = os.path.join(root, "home")
env["JCODE_RUNTIME_DIR"] = os.path.join(root, "run")
env["JCODE_SOCKET"] = os.path.join(env["JCODE_RUNTIME_DIR"], "jcode.sock")
env["JCODE_NO_TELEMETRY"] = "1"
os.makedirs(env["JCODE_HOME"], exist_ok=True)
os.makedirs(env["JCODE_RUNTIME_DIR"], exist_ok=True)
return env
def socket_connectable(path: str) -> bool:
if not os.path.exists(path):
return False
try:
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect(path)
s.close()
return True
except OSError:
return False
def find_child_server(env: dict[str, str], exclude: set[int]) -> int | None:
"""Find the spawned `serve` daemon bound to our private socket."""
try:
pids = [int(p) for p in os.listdir("/proc") if p.isdigit()]
except OSError:
return None
for pid in pids:
if pid in exclude:
continue
cmd = _read(f"/proc/{pid}/cmdline")
if not cmd:
continue
argv = cmd.split("\0")
if "serve" in argv and any(env["JCODE_SOCKET"] in a or "serve" == a for a in argv):
# confirm it is our isolated daemon by socket env or socket arg
envblob = _read(f"/proc/{pid}/environ") or ""
if env["JCODE_SOCKET"] in envblob or env["JCODE_SOCKET"] in cmd:
return pid
return None
# --------------------------------------------------------------------------- #
# Startup profile parsing
# --------------------------------------------------------------------------- #
@dataclass
class StartupProfile:
total_ms: float = 0.0
deltas_ms: dict[str, float] = field(default_factory=dict)
remote_history_ms: float | None = None
def parse_startup_profile(log_path: Path) -> StartupProfile | None:
if not log_path.exists():
return None
lines = log_path.read_text(errors="replace").splitlines()
last_block: list[str] = []
remote_history_ms = None
for i, line in enumerate(lines):
if "=== Startup Profile (" in line:
last_block = lines[i : i + 40]
m = REMOTE_HISTORY_RE.search(line)
if m:
remote_history_ms = float(m.group(1))
if not last_block:
return None
prof = StartupProfile(remote_history_ms=remote_history_ms)
for line in last_block:
tm = PROFILE_TOTAL_RE.search(line)
if tm:
prof.total_ms = float(tm.group(1))
pm = PROFILE_LINE_RE.search(line)
if pm:
_from, delta, name = pm.groups()
prof.deltas_ms[name] = float(delta)
return prof
# --------------------------------------------------------------------------- #
# Server spawn profiling
# --------------------------------------------------------------------------- #
def profile_server_spawn(binary: str, sample_interval_s: float) -> dict | None:
root = tempfile.mkdtemp(prefix="jcode-spawn-server-")
env = isolated_env(root)
sock = env["JCODE_SOCKET"]
proc = None
try:
t0 = time.perf_counter()
proc = subprocess.Popen(
[binary, "--no-update", "serve"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
env=env,
)
mon = ProcessMonitor(proc.pid)
ready_ms = None
deadline = t0 + 8.0
while time.perf_counter() < deadline:
if not mon.poll():
break
if ready_ms is None and socket_connectable(sock):
ready_ms = (time.perf_counter() - t0) * 1000.0
# keep sampling briefly to capture steady-state footprint
settle_until = time.perf_counter() + 0.4
while time.perf_counter() < settle_until:
mon.poll()
time.sleep(sample_interval_s)
break
time.sleep(sample_interval_s)
res = mon.finalize()
out = asdict(res)
out["ready_ms"] = ready_ms
return out
finally:
if proc is not None:
proc.terminate()
try:
proc.wait(timeout=2)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait(timeout=2)
shutil.rmtree(root, ignore_errors=True)
# --------------------------------------------------------------------------- #
# Cold client spawn profiling (PTY) - samples both client and its server child
# --------------------------------------------------------------------------- #
def profile_client_spawn(binary: str, sample_interval_s: float, hold_s: float) -> dict | None:
if not _HAVE_PTY:
return None
root = tempfile.mkdtemp(prefix="jcode-spawn-client-")
env = isolated_env(root)
log_path = Path(env["JCODE_HOME"]) / "logs" / f"jcode-{time.strftime('%Y-%m-%d')}.log"
pid = None
master_fd = None
try:
pid, master_fd = pty.fork()
if pid == 0: # child
try:
# 120x40 window
winsize = struct.pack("HHHH", 40, 120, 0, 0)
fcntl.ioctl(0, termios.TIOCSWINSZ, winsize)
except OSError:
pass
os.execve(
binary,
[binary, "--no-update", "--socket", env["JCODE_SOCKET"]],
env,
)
os._exit(127)
# parent: monitor the client and (once spawned) the server daemon
t0 = time.perf_counter()
client_mon = ProcessMonitor(pid)
server_mon = None
server_pid = None
server_ready_ms = None
deadline = t0 + hold_s
# drain pty output so the child does not block on a full pipe
while time.perf_counter() < deadline:
r, _, _ = select.select([master_fd], [], [], 0.0)
if r:
try:
os.read(master_fd, 65536)
except OSError:
pass
if not client_mon.poll():
break
if server_pid is None:
cand = find_child_server(env, exclude={pid, os.getpid()})
if cand is not None:
server_pid = cand
server_mon = ProcessMonitor(server_pid)
if server_mon is not None:
server_mon.poll()
if server_ready_ms is None and socket_connectable(env["JCODE_SOCKET"]):
server_ready_ms = (time.perf_counter() - t0) * 1000.0
time.sleep(sample_interval_s)
client_res = asdict(client_mon.finalize())
server_res = asdict(server_mon.finalize()) if server_mon else None
profile = parse_startup_profile(log_path)
return {
"client": client_res,
"server": server_res,
"server_ready_ms": server_ready_ms,
"startup_profile": asdict(profile) if profile else None,
}
finally:
if pid:
try:
os.kill(pid, 15)
except OSError:
pass
time.sleep(0.05)
try:
os.kill(pid, 9)
except OSError:
pass
try:
os.waitpid(pid, 0)
except OSError:
pass
if master_fd is not None:
try:
os.close(master_fd)
except OSError:
pass
shutil.rmtree(root, ignore_errors=True)
# --------------------------------------------------------------------------- #
# Aggregation / reporting
# --------------------------------------------------------------------------- #
def mb(n) -> str:
if not n:
return " -"
return f"{n / (1024 * 1024):6.1f}"
def agg(values: list[float]) -> dict:
vals = [v for v in values if v is not None]
if not vals:
return {"min": None, "median": None, "max": None, "mean": None}
return {
"min": min(vals),
"median": statistics.median(vals),
"max": max(vals),
"mean": statistics.mean(vals),
}
def median_field(runs: list[dict], key: str) -> float | None:
vals = [r[key] for r in runs if r and r.get(key) is not None]
return statistics.median(vals) if vals else None
def print_resource_block(title: str, runs: list[dict]) -> None:
runs = [r for r in runs if r]
if not runs:
print(f"\n{title}: no data")
return
print(f"\n{title} (median of {len(runs)} runs)")
rows = [
("Peak RSS (MB)", "peak_rss_bytes", lambda v: f"{v/1048576:.1f}"),
("Final RSS (MB)", "final_rss_bytes", lambda v: f"{v/1048576:.1f}"),
("Peak anon RSS (MB)", "peak_rss_anon_bytes", lambda v: f"{v/1048576:.1f}"),
("Peak PSS (MB)", "peak_pss_bytes", lambda v: f"{v/1048576:.1f}"),
("Peak threads", "peak_threads", lambda v: f"{v:.0f}"),
("Peak open FDs", "peak_fds", lambda v: f"{v:.0f}"),
("CPU user (ms)", "cpu_user_s", lambda v: f"{v*1000:.1f}"),
("CPU sys (ms)", "cpu_sys_s", lambda v: f"{v*1000:.1f}"),
("CPU total (ms)", "cpu_total_s", lambda v: f"{v*1000:.1f}"),
("Avg CPU (%)", "cpu_pct", lambda v: f"{v:.0f}"),
("Minor faults", "minflt", lambda v: f"{v:.0f}"),
("Major faults", "majflt", lambda v: f"{v:.0f}"),
("Vol ctx switch", "vol_ctxt", lambda v: f"{v:.0f}"),
("Invol ctx switch", "nonvol_ctxt", lambda v: f"{v:.0f}"),
("Block read (KB)", "io_read_bytes", lambda v: f"{v/1024:.0f}"),
("Block write (KB)", "io_write_bytes", lambda v: f"{v/1024:.0f}"),
]
for label, key, fmt in rows:
med = median_field(runs, key)
if med is None:
continue
print(f" {label:<20} {fmt(med):>10}")
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
default_bin = os.environ.get("JCODE_BENCH_BIN") or shutil.which("jcode") or "./target/release/jcode"
ap.add_argument("binary", nargs="?", default=default_bin)
ap.add_argument("--runs", type=int, default=5)
ap.add_argument("--sample-interval-ms", type=float, default=2.0)
ap.add_argument("--client-hold-s", type=float, default=2.5,
help="how long to keep the cold client alive while sampling")
ap.add_argument("--json", type=str, default=None, help="write full results to a JSON file")
ap.add_argument("--skip-client", action="store_true")
ap.add_argument("--skip-server", action="store_true")
args = ap.parse_args()
binary = args.binary
if not os.path.exists(binary):
resolved = shutil.which(binary)
if resolved:
binary = resolved
else:
print(f"Binary not found: {binary}", file=sys.stderr)
return 1
binary = os.path.realpath(binary)
size = os.path.getsize(binary)
print("=" * 64)
print("jcode spawn profile: time + resource utilization")
print("=" * 64)
print(f"Binary : {binary}")
print(f"Size : {size/1048576:.1f} MB")
print(f"Runs : {args.runs} sample interval: {args.sample_interval_ms} ms")
print(f"Host : {os.cpu_count()} CPUs, isolated JCODE_HOME per run")
interval = args.sample_interval_ms / 1000.0
results: dict = {"binary": binary, "binary_size_bytes": size, "runs": args.runs}
# warm the page cache / binary load
subprocess.run([binary, "--version"], capture_output=True, check=False)
if not args.skip_server:
print(f"\n[1/2] Profiling server daemon spawn ({args.runs} runs)...")
server_runs = []
ready = []
for i in range(args.runs):
r = profile_server_spawn(binary, interval)
if r:
server_runs.append(r)
if r.get("ready_ms"):
ready.append(r["ready_ms"])
print(f" run {i+1}: ready={r.get('ready_ms', 0):.1f}ms "
f"peak_rss={mb(r.get('peak_rss_bytes')).strip()}MB "
f"threads={r.get('peak_threads')} fds={r.get('peak_fds')}")
results["server"] = server_runs
if ready:
print(f"\n Server ready (socket connectable): "
f"min={min(ready):.1f} median={statistics.median(ready):.1f} "
f"max={max(ready):.1f} ms")
print_resource_block("Server daemon resource footprint", server_runs)
if not args.skip_client and _HAVE_PTY:
print(f"\n[2/2] Profiling cold client spawn in PTY ({args.runs} runs)...")
client_runs = []
spawned_server_runs = []
totals = []
ready = []
for i in range(args.runs):
r = profile_client_spawn(binary, interval, args.client_hold_s)
if not r:
continue
client_runs.append(r["client"])
if r.get("server"):
spawned_server_runs.append(r["server"])
sp = r.get("startup_profile")
if sp and sp.get("total_ms"):
totals.append(sp["total_ms"])
if r.get("server_ready_ms"):
ready.append(r["server_ready_ms"])
results.setdefault("client_full", []).append(r)
print(f" run {i+1}: startup_profile_total={sp['total_ms'] if sp else 0:.1f}ms "
f"client_peak_rss={mb(r['client'].get('peak_rss_bytes')).strip()}MB "
f"server_ready={r.get('server_ready_ms', 0):.1f}ms")
if totals:
print(f"\n Startup-profile total (in-process marks): "
f"min={min(totals):.1f} median={statistics.median(totals):.1f} "
f"max={max(totals):.1f} ms")
if ready:
print(f" Server ready from client launch: "
f"min={min(ready):.1f} median={statistics.median(ready):.1f} "
f"max={max(ready):.1f} ms")
# phase breakdown
if client_runs:
full = results.get("client_full", [])
phase_keys = [
"args_parse", "selfdev_git_hash", "tui_client_enter",
"tui_terminal_init", "server_spawn_start", "server_ready",
"app_new_for_remote",
]
print("\n Startup phase breakdown (median ms):")
for k in phase_keys:
vals = [
f["startup_profile"]["deltas_ms"].get(k)
for f in full
if f.get("startup_profile")
and f["startup_profile"]["deltas_ms"].get(k) is not None
]
if vals:
print(f" {k:<22} {statistics.median(vals):7.2f}")
print_resource_block("Cold client process resource footprint", client_runs)
if spawned_server_runs:
print_resource_block("Server daemon (spawned by client) footprint", spawned_server_runs)
if args.json:
Path(args.json).write_text(json.dumps(results, indent=2))
print(f"\nWrote full results to {args.json}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+156
View File
@@ -0,0 +1,156 @@
#!/usr/bin/env bash
set -euo pipefail
# Quick release script - builds Linux + macOS locally and uploads to GitHub.
# Linux is built inside an Ubuntu 22.04 container for an older glibc baseline.
# macOS is cross-compiled via osxcross (~/.osxcross). Windows is built by CI.
#
# Setup (one-time):
# 1. Install osxcross at ~/.osxcross
# 2. rustup target add aarch64-apple-darwin
# 3. Add to ~/.cargo/config.toml:
# [target.aarch64-apple-darwin]
# linker = "aarch64-apple-darwin23.5-clang"
#
# Usage:
# scripts/quick-release.sh v0.5.5 # tag + build + release
# scripts/quick-release.sh v0.5.5 "Fix bug" # with custom title
# scripts/quick-release.sh --dry-run v0.5.5 # build only, don't publish
DRY_RUN=false
if [[ "${1:-}" == "--dry-run" ]]; then
DRY_RUN=true
shift
fi
VERSION="${1:?Usage: scripts/quick-release.sh [--dry-run] <version> [title]}"
TITLE="${2:-$VERSION}"
VERSION_NUM="${VERSION#v}"
if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Version must be in format v0.5.4"
exit 1
fi
cd "$(git rev-parse --show-toplevel)"
for cmd in gh cargo docker; do
command -v "$cmd" &>/dev/null || { echo "Error: $cmd not found."; exit 1; }
done
[[ -f "$HOME/.cargo/env" ]] && source "$HOME/.cargo/env"
export PATH="$HOME/.osxcross/bin:$PATH"
# Verify osxcross is available
if ! command -v aarch64-apple-darwin23.5-clang &>/dev/null; then
echo "Error: osxcross not found. Install at ~/.osxcross"
exit 1
fi
if [[ -n "$(git status --porcelain -- src/ Cargo.toml Cargo.lock)" ]]; then
echo "Warning: uncommitted changes in src/ or Cargo files."
read -rp "Continue anyway? [y/N] " confirm
[[ "$confirm" =~ ^[Yy]$ ]] || exit 1
fi
echo "=== Quick Release: $VERSION ==="
echo ""
DIST="$(mktemp -d)"
trap 'rm -rf "$DIST"' EXIT
OVERALL_START=$(date +%s)
# Build Linux + macOS in parallel
echo "▸ Building Linux x86_64 + macOS aarch64 in parallel..."
(
JCODE_RELEASE_BUILD=1 JCODE_BUILD_SEMVER="$VERSION_NUM" scripts/build_linux_compat.sh "$DIST" >/dev/null
echo " ✅ Linux done ($(( $(date +%s) - OVERALL_START ))s)"
) &
LINUX_PID=$!
(
JCODE_RELEASE_BUILD=1 JCODE_BUILD_SEMVER="$VERSION_NUM" \
CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS="${CARGO_BUILD_JOBS:-1}" \
cargo build --release --target aarch64-apple-darwin --bin jcode 2>/dev/null
cp target/aarch64-apple-darwin/release/jcode "$DIST/jcode-macos-aarch64"
chmod +x "$DIST/jcode-macos-aarch64"
(cd "$DIST" && tar czf jcode-macos-aarch64.tar.gz jcode-macos-aarch64)
echo " ✅ macOS done ($(( $(date +%s) - OVERALL_START ))s)"
) &
MACOS_PID=$!
wait $LINUX_PID || { echo "Error: Linux build failed"; exit 1; }
wait $MACOS_PID || { echo "Error: macOS build failed"; exit 1; }
BUILD_TIME=$(( $(date +%s) - OVERALL_START ))
echo ""
echo "Build time: ${BUILD_TIME}s"
ls -lh "$DIST"/*.tar.gz
# Verify binaries
file "$DIST/jcode-linux-x86_64.bin" | grep -q 'ELF 64-bit' || { echo "Error: bad Linux binary"; exit 1; }
head -1 "$DIST/jcode-linux-x86_64" | grep -q '^#!/' || { echo "Error: bad Linux wrapper"; exit 1; }
file "$DIST/jcode-macos-aarch64" | grep -q 'Mach-O 64-bit' || { echo "Error: bad macOS binary"; exit 1; }
if $DRY_RUN; then
echo ""
echo "Dry run complete. Binaries in: $DIST"
trap - EXIT
exit 0
fi
echo ""
echo "▸ Tagging $VERSION..."
if git tag -l "$VERSION" | grep -q "$VERSION"; then
echo " Tag already exists"
else
git tag "$VERSION" -m "$TITLE"
git push origin "$VERSION"
echo " Tag pushed (CI will add Windows)"
fi
echo "▸ Creating GitHub release..."
# Human-readable changelog body (issue #435): changelog/v<version>.json when
# present, otherwise grouped commit subjects, always with the compare link.
NOTES_FILE="$DIST/release_notes.md"
if scripts/generate_release_notes.sh "$VERSION" > "$NOTES_FILE" && [[ -s "$NOTES_FILE" ]]; then
gh release create "$VERSION" \
"$DIST/jcode-linux-x86_64.tar.gz" \
"$DIST/jcode-macos-aarch64.tar.gz" \
--title "$TITLE" \
--notes-file "$NOTES_FILE"
else
echo " Warning: release notes generation failed, falling back to --generate-notes"
gh release create "$VERSION" \
"$DIST/jcode-linux-x86_64.tar.gz" \
"$DIST/jcode-macos-aarch64.tar.gz" \
--title "$TITLE" \
--generate-notes
fi
# Close issues marked fixed-but-not-yet-released.
PENDING_LABEL="triage: fixed-pending-release"
echo "▸ Closing issues labeled '$PENDING_LABEL'..."
PENDING_ISSUES="$(gh issue list --label "$PENDING_LABEL" --state open --json number --jq '.[].number' 2>/dev/null || true)"
if [[ -n "$PENDING_ISSUES" ]]; then
while read -r issue; do
[[ -z "$issue" ]] && continue
gh issue close "$issue" \
--comment "Released in [$VERSION](https://github.com/$(gh repo view --json nameWithOwner --jq .nameWithOwner)/releases/tag/$VERSION). Run \`jcode update\` to get it." \
--reason completed >/dev/null 2>&1 \
&& echo " ✅ Closed #$issue"
gh issue edit "$issue" --remove-label "$PENDING_LABEL" >/dev/null 2>&1 || true
done <<< "$PENDING_ISSUES"
else
echo " (none)"
fi
TOTAL_TIME=$(( $(date +%s) - OVERALL_START ))
echo ""
echo "=== Released $VERSION in ${TOTAL_TIME}s ==="
echo " ✅ Linux + macOS: available now"
echo " ⏳ Windows: CI (~15 min)"
echo ""
echo "Users can now: jcode update"
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
provider=${JCODE_PROVIDER:-auto}
prompt=${1:-"Use the bash tool to run 'pwd', then use the ls tool to list the current directory, then respond with DONE."}
expect=${JCODE_TRACE_EXPECT:-DONE}
cargo_exec="$repo_root/scripts/cargo_exec.sh"
echo "=== Real Provider Smoke ==="
echo "Provider: ${provider}"
if [[ "${JCODE_REAL_PROVIDER_TEST_API:-1}" == "1" ]]; then
if [[ "${provider}" == "claude" && "${JCODE_USE_DIRECT_API:-0}" != "1" ]]; then
echo ""
echo "Test 1: Claude CLI smoke (test_api)"
if [[ "${JCODE_REMOTE_CARGO:-0}" == "1" ]]; then
(cd "$repo_root" && "$cargo_exec" build --bin test_api)
(cd "$repo_root" && ./target/debug/test_api)
else
(cd "$repo_root" && cargo run --bin test_api)
fi
else
echo ""
echo "Test 1: Skipping test_api (provider=${provider}, JCODE_USE_DIRECT_API=${JCODE_USE_DIRECT_API:-0})"
fi
fi
echo ""
echo "Test 2: Tool harness (network tools enabled)"
if [[ "${JCODE_REMOTE_CARGO:-0}" == "1" ]]; then
(cd "$repo_root" && "$cargo_exec" build --bin jcode-harness)
(cd "$repo_root" && ./target/debug/jcode-harness -- --include-network)
else
(cd "$repo_root" && cargo run --bin jcode-harness -- --include-network)
fi
echo ""
echo "Test 3: End-to-end trace"
if [[ ! -x "$repo_root/target/release/jcode" ]]; then
(cd "$repo_root" && "$cargo_exec" build --release)
fi
workdir=$(mktemp -d)
trap 'rm -rf "$workdir"' EXIT
set +e
output=$(JCODE_HOME="$workdir" PATH="$repo_root/target/release:$PATH" \
jcode run --no-update --trace --provider "$provider" "$prompt" 2>&1)
status=$?
set -e
printf "%s\n" "$output"
if [[ $status -ne 0 ]]; then
echo "Trace failed with exit code $status" >&2
exit $status
fi
if [[ -n "$expect" ]] && ! grep -q "$expect" <<<"$output"; then
echo "Trace output did not include expected marker: ${expect}" >&2
exit 1
fi
echo ""
echo "=== Real provider smoke OK ==="
+126
View File
@@ -0,0 +1,126 @@
#!/bin/bash
# jcode demo recording orchestrator
# Usage: ./scripts/record_demo.sh <demo_name> <prompt>
#
# This script:
# 1. Opens a fresh jcode in a new kitty window
# 2. Starts wf-recorder on that window
# 3. Sends the prompt to jcode
# 4. Waits for completion, then stops recording
set -euo pipefail
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
DEMO_NAME="${1:?Usage: record_demo.sh <name> <prompt>}"
PROMPT="${2:?Usage: record_demo.sh <name> <prompt>}"
DEMO_DIR="/tmp/jcode-demo/$DEMO_NAME"
OUTPUT_DIR="$repo_root/assets/demos"
SOCK=$(ls /tmp/kitty.sock* 2>/dev/null | head -1)
mkdir -p "$DEMO_DIR" "$OUTPUT_DIR"
echo "=== jcode Demo Recorder ==="
echo "Demo: $DEMO_NAME"
echo "Prompt: $PROMPT"
echo "Working dir: $DEMO_DIR"
echo ""
# Step 1: Launch jcode in a new kitty OS window
echo "[1/5] Launching jcode..."
kitten @ --to unix:$SOCK launch --type=os-window \
--cwd "$DEMO_DIR" \
--title "jcode-demo-$DEMO_NAME" \
"$repo_root/target/release/jcode"
sleep 3 # Let jcode fully start
# Step 2: Find the window
DEMO_WIN_ID=$(niri msg windows 2>/dev/null | grep -B5 "jcode-demo-$DEMO_NAME" | grep "Window ID" | awk '{print $3}' | tr -d ':')
if [ -z "$DEMO_WIN_ID" ]; then
echo "ERROR: Could not find demo window"
exit 1
fi
echo "[2/5] Found window ID: $DEMO_WIN_ID"
# Focus the window
niri msg action focus-window --id "$DEMO_WIN_ID"
sleep 0.5
# Step 3: Start recording
echo "[3/5] Starting recording..."
RECORDING_FILE="$OUTPUT_DIR/${DEMO_NAME}.mp4"
wf-recorder -f "$RECORDING_FILE" &
RECORDER_PID=$!
sleep 1
# Step 4: Type the prompt into jcode
echo "[4/5] Sending prompt..."
# Find the kitty window id
KITTY_WIN_ID=$(kitten @ --to unix:$SOCK ls 2>/dev/null | python3 -c "
import json, sys
data = json.load(sys.stdin)
for os_win in data:
for tab in os_win.get('tabs', []):
for win in tab.get('windows', []):
if 'jcode-demo-$DEMO_NAME' in win.get('title', ''):
print(win['id'])
sys.exit(0)
")
if [ -n "$KITTY_WIN_ID" ]; then
# Type the prompt character by character with small delay for visual effect
kitten @ --to unix:$SOCK send-text --match "id:$KITTY_WIN_ID" "$PROMPT"
sleep 0.5
# Press Enter
kitten @ --to unix:$SOCK send-text --match "id:$KITTY_WIN_ID" $'\r'
else
echo "WARNING: Could not find kitty window, trying by title match..."
kitten @ --to unix:$SOCK send-text --match "title:jcode-demo-$DEMO_NAME" "$PROMPT"
sleep 0.5
kitten @ --to unix:$SOCK send-text --match "title:jcode-demo-$DEMO_NAME" $'\r'
fi
echo "Prompt sent. Waiting for completion..."
echo "(Press Ctrl+C to stop recording early, or wait for auto-detection)"
# Step 5: Wait and then stop recording
# Poll for completion - check if jcode is still processing
# Simple approach: wait for a fixed time or manual Ctrl+C
trap 'echo "Stopping..."; kill $RECORDER_PID 2>/dev/null; wait $RECORDER_PID 2>/dev/null; echo "Recording saved: $RECORDING_FILE"' INT
# Wait for the agent to finish (poll the debug socket)
MAX_WAIT=180 # 3 minutes max
ELAPSED=0
while [ $ELAPSED -lt $MAX_WAIT ]; do
sleep 5
ELAPSED=$((ELAPSED + 5))
# Check if the kitty window title indicates idle (no streaming)
CURRENT_TITLE=$(kitten @ --to unix:$SOCK ls 2>/dev/null | python3 -c "
import json, sys
data = json.load(sys.stdin)
for os_win in data:
for tab in os_win.get('tabs', []):
for win in tab.get('windows', []):
if win['id'] == $KITTY_WIN_ID:
print(win.get('title', ''))
sys.exit(0)
" 2>/dev/null || echo "unknown")
echo " [${ELAPSED}s] Window: $CURRENT_TITLE"
done
# Add a small pause at the end so viewer can see the result
sleep 3
# Stop recording
kill $RECORDER_PID 2>/dev/null
wait $RECORDER_PID 2>/dev/null
echo ""
echo "[5/5] Recording saved: $RECORDING_FILE"
FILE_SIZE=$(du -h "$RECORDING_FILE" | cut -f1)
echo "Size: $FILE_SIZE"
echo ""
echo "To convert to GIF: ffmpeg -i $RECORDING_FILE -vf 'fps=15,scale=800:-1' ${RECORDING_FILE%.mp4}.gif"
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
cargo_exec="$repo_root/scripts/cargo_exec.sh"
run_cargo() {
(cd "$repo_root" && "$cargo_exec" "$@")
}
echo "=== Phase 1 Refactor Verification ==="
echo "[1/7] Isolated environment sanity"
"$repo_root/scripts/refactor_shadow.sh" check
echo "[2/7] Build (debug)"
"$repo_root/scripts/refactor_shadow.sh" build
echo "[3/7] Compile + budgets"
run_cargo check -q
"$repo_root/scripts/check_warning_budget.sh"
python3 "$repo_root/scripts/check_code_size_budget.py"
echo "[4/7] Security preflight"
"$repo_root/scripts/security_preflight.sh"
echo "[5/7] Full tests"
run_cargo test -q
echo "[6/7] E2E tests"
run_cargo test --test e2e -q
echo "[7/7] All-targets/all-features lint"
run_cargo check --all-targets --all-features
run_cargo clippy --all-targets --all-features -- -D warnings
echo "=== Phase 1 verification passed ==="
+229
View File
@@ -0,0 +1,229 @@
#!/usr/bin/env bash
set -euo pipefail
# Keep files created by this helper private by default.
umask 077
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
user_name="${USER:-$(id -un)}"
runtime_dir="${XDG_RUNTIME_DIR:-/tmp}"
default_home="${HOME}/.jcode-refactor"
default_socket="${runtime_dir}/jcode-refactor-${user_name}.sock"
ref_home="${JCODE_REF_HOME:-$default_home}"
ref_socket="${JCODE_REF_SOCKET:-$default_socket}"
ref_profile="${JCODE_REF_PROFILE:-debug}"
case "$ref_profile" in
debug) default_bin="$repo_root/target/debug/jcode" ;;
release) default_bin="$repo_root/target/release/jcode" ;;
*)
printf 'error: unsupported JCODE_REF_PROFILE: %s (expected debug or release)\n' "$ref_profile" >&2
exit 1
;;
esac
ref_bin="${JCODE_REF_BIN:-$default_bin}"
usage() {
cat <<'USAGE'
Usage:
scripts/refactor_shadow.sh env
scripts/refactor_shadow.sh build [--release]
scripts/refactor_shadow.sh serve [-- <jcode serve args>]
scripts/refactor_shadow.sh run [-- <jcode args>]
scripts/refactor_shadow.sh connect [-- <jcode connect args>]
scripts/refactor_shadow.sh check
What it does:
- Runs jcode in an isolated refactor environment
- Uses separate JCODE_HOME and JCODE_SOCKET
- Refuses to run against ~/.jcode to protect live sessions
Environment overrides:
JCODE_REF_HOME Isolated home dir (default: ~/.jcode-refactor)
JCODE_REF_SOCKET Isolated socket path
JCODE_REF_PROFILE debug|release (default: debug)
JCODE_REF_BIN Explicit jcode binary path
USAGE
}
die() {
printf 'error: %s\n' "$*" >&2
exit 1
}
assert_safe_paths() {
[[ -n "$ref_home" ]] || die "JCODE_REF_HOME resolved to empty path"
[[ -n "$ref_socket" ]] || die "JCODE_REF_SOCKET resolved to empty path"
[[ "$ref_home" = /* ]] || die "JCODE_REF_HOME must be an absolute path: $ref_home"
[[ "$ref_socket" = /* ]] || die "JCODE_REF_SOCKET must be an absolute path: $ref_socket"
local prod_home="${HOME}/.jcode"
if [[ "$ref_home" == "$prod_home" ]]; then
die "refusing to run with production home ($prod_home); set JCODE_REF_HOME to an isolated path"
fi
}
ensure_ref_home() {
if [[ ! -d "$ref_home" ]]; then
mkdir -p -m 700 "$ref_home"
fi
# Best-effort hardening if dir already exists.
chmod 700 "$ref_home" 2>/dev/null || true
}
ensure_socket_parent() {
local socket_parent
socket_parent=$(dirname "$ref_socket")
if [[ ! -d "$socket_parent" ]]; then
mkdir -p -m 700 "$socket_parent"
fi
}
ensure_binary() {
if [[ ! -x "$ref_bin" ]]; then
die "jcode binary not found or not executable: $ref_bin (run 'scripts/refactor_shadow.sh build')"
fi
}
remove_stale_socket() {
local debug_socket
debug_socket="${ref_socket%.sock}-debug.sock"
for path in "$ref_socket" "$debug_socket"; do
if [[ -e "$path" ]]; then
if [[ -S "$path" ]]; then
rm -f "$path"
else
die "refusing to remove non-socket path: $path"
fi
fi
done
}
run_isolated() {
JCODE_HOME="$ref_home" JCODE_SOCKET="$ref_socket" "$@"
}
normalize_args() {
if [[ "${1:-}" == "--" ]]; then
shift
fi
printf '%s\0' "$@"
}
cmd_env() {
cat <<EOF_OUT
JCODE_REF_HOME=$ref_home
JCODE_REF_SOCKET=$ref_socket
JCODE_REF_PROFILE=$ref_profile
JCODE_REF_BIN=$ref_bin
# One-off command example:
JCODE_HOME=$ref_home JCODE_SOCKET=$ref_socket $ref_bin --version
EOF_OUT
}
cmd_build() {
local profile_flag=""
if [[ "${1:-}" == "--release" ]]; then
profile_flag="--release"
elif [[ -n "${1:-}" ]]; then
die "unknown build argument: $1"
fi
(cd "$repo_root" && "$repo_root/scripts/dev_cargo.sh" build $profile_flag)
}
cmd_check() {
assert_safe_paths
ensure_ref_home
ensure_socket_parent
printf 'Refactor home: %s\n' "$ref_home"
printf 'Refactor socket: %s\n' "$ref_socket"
printf 'Refactor binary: %s\n' "$ref_bin"
if [[ -S "$ref_socket" ]]; then
printf 'Socket status: present (server likely running)\n'
elif [[ -e "$ref_socket" ]]; then
printf 'Socket status: present but not a socket (unexpected)\n'
exit 1
else
printf 'Socket status: not present\n'
fi
}
cmd_serve() {
assert_safe_paths
ensure_ref_home
ensure_socket_parent
ensure_binary
remove_stale_socket
local -a args=("$@")
run_isolated "$ref_bin" serve "${args[@]}"
}
cmd_run() {
assert_safe_paths
ensure_ref_home
ensure_socket_parent
ensure_binary
local -a args=("$@")
run_isolated "$ref_bin" "${args[@]}"
}
cmd_connect() {
assert_safe_paths
ensure_ref_home
ensure_socket_parent
ensure_binary
local -a args=("$@")
run_isolated "$ref_bin" connect "${args[@]}"
}
main() {
local cmd="${1:-help}"
shift || true
case "$cmd" in
env)
cmd_env
;;
build)
cmd_build "$@"
;;
serve)
if [[ "${1:-}" == "--" ]]; then
shift
fi
cmd_serve "$@"
;;
run)
if [[ "${1:-}" == "--" ]]; then
shift
fi
cmd_run "$@"
;;
connect)
if [[ "${1:-}" == "--" ]]; then
shift
fi
cmd_connect "$@"
;;
check)
cmd_check
;;
help|-h|--help)
usage
;;
*)
die "unknown command: $cmd (use --help)"
;;
esac
}
main "$@"
+641
View File
@@ -0,0 +1,641 @@
#!/usr/bin/env python3
"""
Audit self-dev reload recovery handoffs.
This is a read-only diagnostic helper for bugs where `selfdev reload` restarts the
server but not every interrupted session continues. It correlates three sources:
1. ~/.jcode/reload-recovery/*.json durable recovery intent records
2. ~/.jcode/logs/jcode-*.log recovery lifecycle messages
3. ~/.jcode/sessions/<session>.json coarse transcript markers
The important invariant this helps check is:
A recovery intent should not become effectively terminal unless the client
queued/dispatched the continuation or the server accepted the continuation.
Older builds could mark the server-side intent "Delivered" while merely building
a History payload, before the TUI processed that payload. Current builds keep the
intent pending until the replacement server accepts the exact hidden
continuation. This script flags old-style claimed/delivered records that have no
client queue or server acceptance evidence.
Examples:
scripts/reload_recovery_audit.py
scripts/reload_recovery_audit.py --reload-id reload_1779786108758_15766248331403251908
scripts/reload_recovery_audit.py --max-log-files 5 --show-lines
scripts/reload_recovery_audit.py --json
"""
from __future__ import annotations
import argparse
import dataclasses
import json
import os
import pathlib
import re
import sys
from collections import defaultdict
from typing import Any
PERSISTED_RE = re.compile(
r"reload recovery store: persisted intent reload_id=(reload_[^ ]+) session=([^ ]+) role=([^ ]+)"
)
CLAIMED_RE = re.compile(
r"reload recovery store: claimed intent reload_id=(reload_[^ ]+) session=([^ ]+) role=([^ ]+)"
)
ATTACHED_RE = re.compile(
r"reload recovery store: attached pending intent reload_id=(reload_[^ ]+) session=([^ ]+) role=([^ ]+)"
)
DELIVERED_RE = re.compile(
r"reload recovery store: delivered intent reload_id=(reload_[^ ]+) session=([^ ]+) role=([^ ]+) accepted_by=([^ ]+)"
)
NON_PENDING_RE = re.compile(
r"reload recovery store: skipping non-pending intent session=([^ ]+) reload_id=(reload_[^ ]+) status=([^ ]+)"
)
HISTORY_QUEUE_RE = re.compile(
r"History payload requested reload recovery continuation: session=([^ ]+) was_interrupted=([^ ]+)"
)
TUI_FLOW_RE = re.compile(
r"reload recovery flow=([^ ]+) session_id=([^ ]+) outcome=([^ ]+) detail=(.*)"
)
HISTORY_SNAPSHOT_RE = re.compile(
r"history_reload_recovery_snapshot: (?:using|attaching) server-owned recovery intent for session=([^ ]+)"
)
SEND_HISTORY_RE = re.compile(r"\[TIMING\] send_history write: session=([^, ]+)")
HIDDEN_SEND_RE = re.compile(r"Sending hidden continuation reminder \((\d+) chars\)")
MESSAGE_RUNNING_RE = re.compile(
r"new_status=running old_status=[^ ]+ phase=member_status_updated .*session_id=([^ ]+)"
)
RELOAD_ID_RE = re.compile(r"reload_\d+_\d+")
@dataclasses.dataclass
class Event:
kind: str
line: int
text: str
file: str
reload_id: str | None = None
session_id: str | None = None
role: str | None = None
detail: str | None = None
def compact(self) -> dict[str, Any]:
return {
"kind": self.kind,
"line": self.line,
"file": self.file,
"reload_id": self.reload_id,
"session_id": self.session_id,
"role": self.role,
"detail": self.detail,
"text": self.text.strip(),
}
@dataclasses.dataclass
class IntentRecord:
reload_id: str
session_id: str
role: str | None = None
persisted: Event | None = None
claimed: Event | None = None
attached: list[Event] = dataclasses.field(default_factory=list)
delivered: list[Event] = dataclasses.field(default_factory=list)
delivery_mismatch: list[Event] = dataclasses.field(default_factory=list)
non_pending: list[Event] = dataclasses.field(default_factory=list)
history_snapshot: list[Event] = dataclasses.field(default_factory=list)
history_queued: list[Event] = dataclasses.field(default_factory=list)
tui_flows: list[Event] = dataclasses.field(default_factory=list)
send_history: list[Event] = dataclasses.field(default_factory=list)
hidden_sends_nearby: list[Event] = dataclasses.field(default_factory=list)
member_running: list[Event] = dataclasses.field(default_factory=list)
durable_status: str | None = None
durable_delivered_at: str | None = None
transcript_markers: dict[str, int] = dataclasses.field(default_factory=dict)
def has_client_queue_evidence(self) -> bool:
return bool(self.history_queued) or any(
event.detail
and (
"queued" in event.detail.lower()
or "outcome=resumed" in event.detail.lower()
)
for event in self.tui_flows
)
def has_server_acceptance_evidence(self) -> bool:
return bool(self.delivered) or any(
event.detail
and (
"accepted_by=client_message_accepted" in event.detail
or "accepted_by=server_startup_headless" in event.detail
)
for event in self.tui_flows
)
def verdict(self) -> str:
if self.has_client_queue_evidence():
return "ok-client-queued"
if self.has_server_acceptance_evidence():
return "ok-server-accepted"
if self.durable_status == "pending" or self.durable_status == "Pending":
return "pending"
if self.claimed or (self.durable_status or "").lower() == "delivered":
return "suspect-delivered-without-acceptance-log"
if self.attached:
return "attached-not-accepted"
if self.persisted:
return "persisted-not-attached"
return "observed-without-persist-log"
def compact(self, show_lines: bool = False) -> dict[str, Any]:
data: dict[str, Any] = {
"reload_id": self.reload_id,
"session_id": self.session_id,
"role": self.role,
"durable_status": self.durable_status,
"durable_delivered_at": self.durable_delivered_at,
"persisted": bool(self.persisted),
"claimed": bool(self.claimed),
"attached_count": len(self.attached),
"delivered_count": len(self.delivered),
"delivery_mismatch_count": len(self.delivery_mismatch),
"history_snapshot_count": len(self.history_snapshot),
"history_queued_count": len(self.history_queued),
"tui_flow_count": len(self.tui_flows),
"send_history_count": len(self.send_history),
"member_running_after_reload_count": len(self.member_running),
"transcript_markers": self.transcript_markers,
"verdict": self.verdict(),
}
if show_lines:
data["events"] = [
*(event.compact() for event in [self.persisted, self.claimed] if event),
*(event.compact() for event in self.attached),
*(event.compact() for event in self.delivered),
*(event.compact() for event in self.delivery_mismatch),
*(event.compact() for event in self.history_snapshot),
*(event.compact() for event in self.history_queued),
*(event.compact() for event in self.tui_flows),
*(event.compact() for event in self.send_history),
*(event.compact() for event in self.member_running),
]
return data
def jcode_home() -> pathlib.Path:
return pathlib.Path(os.environ.get("JCODE_HOME", pathlib.Path.home() / ".jcode"))
def log_files(log_dir: pathlib.Path, max_log_files: int) -> list[pathlib.Path]:
files = sorted(log_dir.glob("jcode-*.log"), key=lambda p: p.stat().st_mtime, reverse=True)
return list(reversed(files[:max_log_files]))
def normalize_reload_id(value: str | None) -> str | None:
if not value:
return None
return value if value.startswith("reload_") else f"reload_{value}"
def add_event(records: dict[tuple[str, str], IntentRecord], event: Event) -> None:
if not event.reload_id or not event.session_id:
return
key = (event.reload_id, event.session_id)
record = records.setdefault(
key,
IntentRecord(
reload_id=event.reload_id,
session_id=event.session_id,
role=event.role,
),
)
if event.role and not record.role:
record.role = event.role
if event.kind == "persisted":
record.persisted = event
elif event.kind == "claimed":
record.claimed = event
elif event.kind == "attached":
record.attached.append(event)
elif event.kind == "delivered":
record.delivered.append(event)
elif event.kind == "delivery_mismatch":
record.delivery_mismatch.append(event)
elif event.kind == "non_pending":
record.non_pending.append(event)
elif event.kind == "history_snapshot":
record.history_snapshot.append(event)
elif event.kind == "history_queued":
record.history_queued.append(event)
elif event.kind == "tui_flow":
record.tui_flows.append(event)
elif event.kind == "send_history":
record.send_history.append(event)
elif event.kind == "member_running":
record.member_running.append(event)
def parse_logs(files: list[pathlib.Path]) -> tuple[dict[tuple[str, str], IntentRecord], list[Event]]:
records: dict[tuple[str, str], IntentRecord] = {}
session_latest_reload: dict[str, str] = {}
global_hidden_sends: list[Event] = []
for path in files:
try:
handle = path.open(errors="ignore")
except OSError:
continue
with handle:
for line_no, line in enumerate(handle, 1):
text = line.rstrip("\n")
if match := PERSISTED_RE.search(text):
reload_id, session_id, role = match.groups()
session_latest_reload[session_id] = reload_id
add_event(
records,
Event(
"persisted",
line_no,
text,
path.name,
reload_id,
session_id,
role,
),
)
continue
if match := CLAIMED_RE.search(text):
reload_id, session_id, role = match.groups()
session_latest_reload[session_id] = reload_id
add_event(
records,
Event("claimed", line_no, text, path.name, reload_id, session_id, role),
)
continue
if match := ATTACHED_RE.search(text):
reload_id, session_id, role = match.groups()
session_latest_reload[session_id] = reload_id
add_event(
records,
Event("attached", line_no, text, path.name, reload_id, session_id, role),
)
continue
if match := DELIVERED_RE.search(text):
reload_id, session_id, role, accepted_by = match.groups()
session_latest_reload[session_id] = reload_id
add_event(
records,
Event(
"delivered",
line_no,
text,
path.name,
reload_id,
session_id,
role,
detail=f"accepted_by={accepted_by}",
),
)
continue
if match := NON_PENDING_RE.search(text):
session_id, reload_id, status = match.groups()
session_latest_reload[session_id] = reload_id
add_event(
records,
Event(
"non_pending",
line_no,
text,
path.name,
reload_id,
session_id,
detail=status,
),
)
continue
if match := HISTORY_QUEUE_RE.search(text):
session_id, was_interrupted = match.groups()
reload_id = session_latest_reload.get(session_id)
if reload_id:
add_event(
records,
Event(
"history_queued",
line_no,
text,
path.name,
reload_id,
session_id,
detail=f"was_interrupted={was_interrupted}",
),
)
continue
if match := TUI_FLOW_RE.search(text):
flow, session_id, outcome, detail = match.groups()
reload_id = session_latest_reload.get(session_id)
if reload_id:
add_event(
records,
Event(
"tui_flow",
line_no,
text,
path.name,
reload_id,
session_id,
detail=f"flow={flow} outcome={outcome} detail={detail}",
),
)
continue
if match := HISTORY_SNAPSHOT_RE.search(text):
session_id = match.group(1)
reload_id = session_latest_reload.get(session_id)
if reload_id:
add_event(
records,
Event(
"history_snapshot",
line_no,
text,
path.name,
reload_id,
session_id,
),
)
continue
if match := SEND_HISTORY_RE.search(text):
session_id = match.group(1)
reload_id = session_latest_reload.get(session_id)
if reload_id:
add_event(
records,
Event("send_history", line_no, text, path.name, reload_id, session_id),
)
continue
if match := MESSAGE_RUNNING_RE.search(text):
session_id = match.group(1)
reload_id = session_latest_reload.get(session_id)
if reload_id:
add_event(
records,
Event("member_running", line_no, text, path.name, reload_id, session_id),
)
continue
if match := HIDDEN_SEND_RE.search(text):
global_hidden_sends.append(
Event(
"hidden_send",
line_no,
text,
path.name,
detail=f"chars={match.group(1)}",
)
)
return records, global_hidden_sends
def parse_trace_files(records: dict[tuple[str, str], IntentRecord], trace_dir: pathlib.Path) -> None:
"""Merge structured ~/.jcode/reload-traces/*.jsonl lifecycle events."""
phase_to_kind = {
"intent_attached_to_history": "attached",
"intent_delivered": "delivered",
"intent_delivery_mismatch": "delivery_mismatch",
}
for path in sorted(trace_dir.glob("*.jsonl")):
try:
handle = path.open(errors="ignore")
except OSError:
continue
with handle:
for line_no, line in enumerate(handle, 1):
text = line.rstrip("\n")
try:
data = json.loads(text)
except json.JSONDecodeError:
continue
phase = data.get("phase")
kind = phase_to_kind.get(phase)
if not kind:
continue
reload_id = data.get("reload_id") or path.stem
session_id = data.get("session_id")
if not reload_id or not session_id:
continue
detail_parts = []
if accepted_by := data.get("accepted_by"):
detail_parts.append(f"accepted_by={accepted_by}")
if status := data.get("status"):
detail_parts.append(f"status={status}")
if phase:
detail_parts.append(f"phase={phase}")
add_event(
records,
Event(
kind,
line_no,
text,
path.name,
reload_id,
session_id,
data.get("role"),
detail=" ".join(detail_parts) or None,
),
)
def merge_durable_records(records: dict[tuple[str, str], IntentRecord], recovery_dir: pathlib.Path) -> None:
for path in sorted(recovery_dir.glob("*.json")):
try:
data = json.loads(path.read_text())
except Exception:
continue
reload_id = data.get("reload_id")
session_id = data.get("session_id")
if not reload_id or not session_id:
continue
key = (reload_id, session_id)
record = records.setdefault(
key,
IntentRecord(
reload_id=reload_id,
session_id=session_id,
role=data.get("role"),
),
)
record.role = record.role or data.get("role")
record.durable_status = data.get("status")
record.durable_delivered_at = data.get("delivered_at")
def add_transcript_markers(records: list[IntentRecord], sessions_dir: pathlib.Path) -> None:
markers = [
"Reload succeeded",
"interrupted by a server reload",
"Continue immediately from where you left off",
"Reload complete",
]
session_to_records: dict[str, list[IntentRecord]] = defaultdict(list)
for record in records:
session_to_records[record.session_id].append(record)
for session_id, session_records in session_to_records.items():
path = sessions_dir / f"{session_id}.json"
if not path.exists():
continue
try:
text = path.read_text(errors="ignore")
except OSError:
continue
counts = {marker: text.count(marker) for marker in markers}
for record in session_records:
record.transcript_markers = counts
def short_session(session_id: str) -> str:
if len(session_id) <= 34:
return session_id
return session_id[:31] + "..."
def render_table(records: list[IntentRecord], hidden_sends: list[Event], show_lines: bool) -> str:
if not records:
return "No reload recovery records found for the selected filters."
rows = []
for record in records:
client = "yes" if record.has_client_queue_evidence() else "no"
accepted = "yes" if record.has_server_acceptance_evidence() else "no"
claimed = "yes" if record.claimed else "no"
persisted = "yes" if record.persisted else "no"
transcript = sum(record.transcript_markers.values()) if record.transcript_markers else 0
rows.append(
[
record.reload_id,
short_session(record.session_id),
record.role or "?",
record.durable_status or "?",
persisted,
claimed,
str(len(record.attached)),
accepted,
client,
str(record.send_history and len(record.send_history) or 0),
str(record.member_running and len(record.member_running) or 0),
str(transcript),
record.verdict(),
]
)
headers = [
"reload_id",
"session",
"role",
"store",
"persist",
"old_claim",
"attach",
"accepted",
"client_queue",
"hist_write",
"ran_after",
"markers",
"verdict",
]
widths = [len(h) for h in headers]
for row in rows:
for idx, value in enumerate(row):
widths[idx] = max(widths[idx], len(value))
def fmt(row: list[str]) -> str:
return " ".join(value.ljust(widths[idx]) for idx, value in enumerate(row))
out = [fmt(headers), fmt(["-" * width for width in widths])]
out.extend(fmt(row) for row in rows)
suspects = [record for record in records if record.verdict().startswith("suspect")]
out.append("")
out.append(f"Global hidden continuation send log lines scanned: {len(hidden_sends)}")
out.append(f"Suspect delivered-without-acceptance records: {len(suspects)}")
if suspects:
out.append("")
out.append("Suspects:")
for record in suspects:
claim = f"{record.claimed.file}:{record.claimed.line}" if record.claimed else "durable-only"
out.append(
f" - {record.session_id} ({record.role}) {record.reload_id} terminal={claim}"
)
if show_lines:
out.append("")
out.append("Event lines:")
for record in records:
out.append(f"\n[{record.reload_id} {record.session_id}]")
for event in record.compact(show_lines=True).get("events", []):
out.append(f" {event['file']}:{event['line']} {event['kind']}: {event['text']}")
return "\n".join(out)
def main() -> int:
parser = argparse.ArgumentParser(description="Audit self-dev reload recovery handoffs")
parser.add_argument("--home", type=pathlib.Path, default=jcode_home(), help="JCODE_HOME path")
parser.add_argument("--reload-id", type=str, help="Only show one reload id")
parser.add_argument("--session", type=str, help="Only show one session id")
parser.add_argument("--max-log-files", type=int, default=3, help="Newest jcode logs to scan")
parser.add_argument("--show-lines", action="store_true", help="Show matching source log lines")
parser.add_argument("--json", action="store_true", help="Emit JSON")
args = parser.parse_args()
home = args.home
records, hidden_sends = parse_logs(log_files(home / "logs", args.max_log_files))
parse_trace_files(records, home / "reload-traces")
merge_durable_records(records, home / "reload-recovery")
reload_filter = normalize_reload_id(args.reload_id)
selected = list(records.values())
if reload_filter:
selected = [record for record in selected if record.reload_id == reload_filter]
if args.session:
selected = [record for record in selected if record.session_id == args.session]
selected.sort(key=lambda record: (record.reload_id, record.session_id))
add_transcript_markers(selected, home / "sessions")
if args.json:
print(
json.dumps(
{
"home": str(home),
"records": [record.compact(show_lines=args.show_lines) for record in selected],
"global_hidden_sends": [event.compact() for event in hidden_sends],
},
indent=2,
sort_keys=True,
)
)
else:
print(render_table(selected, hidden_sends, args.show_lines))
return 0
if __name__ == "__main__":
sys.exit(main())
+288
View File
@@ -0,0 +1,288 @@
#!/usr/bin/env bash
# Remote cargo runner (build/test/check/clippy) via SSH + rsync.
#
# Defaults:
# - Config file: ~/.config/jcode/remote-build.env (override with JCODE_REMOTE_CONFIG)
# - Host: JCODE_REMOTE_HOST from env/config, or --host
# - Remote dir: .cache/remote-builds/jcode/<repo-name> (override with JCODE_REMOTE_DIR or --remote-dir)
#
# Examples:
# scripts/remote_build.sh --release
# scripts/remote_build.sh test
# scripts/remote_build.sh check --all-targets
# scripts/remote_build.sh --host mybox --remote-dir ~/src/jcode test -- --nocapture
set -euo pipefail
usage() {
cat <<'EOF'
Usage: scripts/remote_build.sh [options] [cargo-subcommand] [cargo-args...]
Options:
-r, --release Add --release to cargo invocation
--host HOST Remote SSH host (default: $JCODE_REMOTE_HOST from env/config; required if unset)
--remote-dir DIR Remote project directory (default: $JCODE_REMOTE_DIR or .cache/remote-builds/jcode/<repo-name>)
--no-sync Skip rsync upload step
--sync-back Force sync-back of built binary after command
--no-sync-back Disable sync-back of built binary after command
-h, --help Show this help
Behavior:
- Default cargo subcommand is 'build'
- Sync-back defaults to ON for 'build', OFF for other subcommands
- For build sync-back, copies target/{debug|release}/<artifact> from remote to local
(artifact defaults to 'jcode', or '--bin <name>' when provided)
- Default config file is ~/.config/jcode/remote-build.env
EOF
}
LOCAL_DIR="$(cd "$(dirname "$0")/.." && pwd)"
REPO_NAME="$(basename "$LOCAL_DIR")"
# shellcheck source=scripts/remote_config.sh
source "$LOCAL_DIR/scripts/remote_config.sh"
jcode_load_remote_config
REMOTE="${JCODE_REMOTE_HOST:-}"
REMOTE_DIR="${JCODE_REMOTE_DIR:-.cache/remote-builds/jcode/${REPO_NAME}}"
SSH_BIN="${JCODE_REMOTE_SSH_BIN:-ssh}"
RSYNC_BIN="${JCODE_REMOTE_RSYNC_BIN:-rsync}"
SYNC_SOURCE=1
SYNC_BACK_MODE="auto" # auto|always|never
RELEASE=0
SUBCOMMAND="build"
SUBCOMMAND_SET=0
POSITIONAL=()
remote_connect_timeout() {
local value="${JCODE_REMOTE_CONNECT_TIMEOUT:-5}"
if [[ ! "$value" =~ ^[0-9]+$ || "$value" -lt 1 ]]; then
value=5
fi
printf '%s\n' "$value"
}
while [[ $# -gt 0 ]]; do
case "$1" in
-r|--release)
RELEASE=1
shift
;;
--host)
[[ $# -lt 2 ]] && { echo "error: --host requires a value" >&2; exit 2; }
REMOTE="$2"
shift 2
;;
--remote-dir)
[[ $# -lt 2 ]] && { echo "error: --remote-dir requires a value" >&2; exit 2; }
REMOTE_DIR="$2"
shift 2
;;
--no-sync)
SYNC_SOURCE=0
shift
;;
--sync-back)
SYNC_BACK_MODE="always"
shift
;;
--no-sync-back)
SYNC_BACK_MODE="never"
shift
;;
-h|--help)
usage
exit 0
;;
--)
shift
POSITIONAL+=("$@")
break
;;
*)
if [[ "$SUBCOMMAND_SET" -eq 0 && "$1" != -* ]]; then
SUBCOMMAND="$1"
SUBCOMMAND_SET=1
else
POSITIONAL+=("$1")
fi
shift
;;
esac
done
if [[ "$REMOTE_DIR" == *" "* ]]; then
echo "error: remote dir cannot contain spaces: $REMOTE_DIR" >&2
exit 2
fi
if [[ -z "$REMOTE" ]]; then
echo "error: remote host not configured; set JCODE_REMOTE_HOST or pass --host HOST" >&2
exit 2
fi
for bin in "$SSH_BIN" "$RSYNC_BIN"; do
if ! command -v "$bin" >/dev/null 2>&1; then
echo "error: required binary not found: $bin" >&2
exit 2
fi
done
SSH_CONNECT_TIMEOUT="$(remote_connect_timeout)"
SSH_SERVER_ALIVE_INTERVAL="${JCODE_REMOTE_SERVER_ALIVE_INTERVAL:-10}"
SSH_SERVER_ALIVE_COUNT_MAX="${JCODE_REMOTE_SERVER_ALIVE_COUNT_MAX:-1}"
SSH_OPTS=(
-o BatchMode=yes
-o ConnectTimeout="$SSH_CONNECT_TIMEOUT"
-o ServerAliveInterval="$SSH_SERVER_ALIVE_INTERVAL"
-o ServerAliveCountMax="$SSH_SERVER_ALIVE_COUNT_MAX"
)
RSYNC_SSH_COMMAND="${JCODE_REMOTE_RSYNC_SSH:-$SSH_BIN -o BatchMode=yes -o ConnectTimeout=$SSH_CONNECT_TIMEOUT -o ServerAliveInterval=$SSH_SERVER_ALIVE_INTERVAL -o ServerAliveCountMax=$SSH_SERVER_ALIVE_COUNT_MAX}"
remote_ssh() {
"$SSH_BIN" "${SSH_OPTS[@]}" "$REMOTE" "$@"
}
CARGO_CMD=(cargo "$SUBCOMMAND")
if [[ "$RELEASE" -eq 1 ]]; then
CARGO_CMD+=(--release)
fi
if [[ "${#POSITIONAL[@]}" -gt 0 ]]; then
CARGO_CMD+=("${POSITIONAL[@]}")
fi
sync_back=0
case "$SYNC_BACK_MODE" in
always) sync_back=1 ;;
never) sync_back=0 ;;
auto)
if [[ "$SUBCOMMAND" == "build" ]]; then
sync_back=1
fi
;;
esac
profile_name=""
for ((i=0; i<${#POSITIONAL[@]}; i++)); do
case "${POSITIONAL[$i]}" in
--profile)
if [[ $((i + 1)) -lt ${#POSITIONAL[@]} ]]; then
profile_name="${POSITIONAL[$((i + 1))]}"
fi
;;
--profile=*)
profile_name="${POSITIONAL[$i]#--profile=}"
;;
esac
done
if [[ "$RELEASE" -eq 1 || "$profile_name" == "release" ]]; then
build_mode="release"
elif [[ -n "$profile_name" && "$profile_name" != "dev" ]]; then
build_mode="$profile_name"
else
build_mode="debug"
fi
artifact_name="jcode"
if [[ "$SUBCOMMAND" == "build" ]]; then
for ((i=0; i<${#POSITIONAL[@]}; i++)); do
if [[ "${POSITIONAL[$i]}" == "--bin" && $((i + 1)) -lt ${#POSITIONAL[@]} ]]; then
artifact_name="${POSITIONAL[$((i + 1))]}"
break
fi
done
fi
BINARY_PATH="target/${build_mode}/${artifact_name}"
local_git_hash=""
local_git_date=""
local_git_tag=""
local_git_dirty="0"
local_changelog_raw=""
if command -v git >/dev/null 2>&1 && git -C "$LOCAL_DIR" rev-parse --git-dir >/dev/null 2>&1; then
local_git_hash="$(git -C "$LOCAL_DIR" rev-parse --short HEAD 2>/dev/null || true)"
local_git_date="$(git -C "$LOCAL_DIR" log -1 --format=%ci 2>/dev/null || true)"
local_git_tag="$(git -C "$LOCAL_DIR" describe --tags --always 2>/dev/null || true)"
local_changelog_raw="$(git -C "$LOCAL_DIR" log -700 --format='%h|%ct|%D|%s' 2>/dev/null || true)"
if [[ -n "$(git -C "$LOCAL_DIR" status --porcelain 2>/dev/null || true)" ]]; then
local_git_dirty="1"
fi
fi
echo "=== Remote Cargo on $REMOTE ==="
echo "Local: $LOCAL_DIR"
echo "Remote: $REMOTE_DIR"
echo "Command: ${CARGO_CMD[*]}"
echo "Mode: $build_mode"
echo "SSH timeout: ${SSH_CONNECT_TIMEOUT}s"
echo ""
echo "[0/3] Checking remote SSH..."
if ! preflight_output="$(remote_ssh "printf 'jcode-remote-ok\\n'" 2>&1)"; then
echo "error: remote host '$REMOTE' is not reachable within ${SSH_CONNECT_TIMEOUT}s" >&2
echo "$preflight_output" >&2
echo "hint: set JCODE_REMOTE_CARGO=0 to force local cargo, or fix JCODE_REMOTE_HOST/JCODE_REMOTE_CONNECT_TIMEOUT." >&2
exit 75
fi
if [[ "$SYNC_SOURCE" -eq 1 ]]; then
echo ""
echo "[1/3] Syncing source files..."
remote_ssh "$(printf 'mkdir -p %q' "$REMOTE_DIR")"
"$RSYNC_BIN" -avz --delete \
-e "$RSYNC_SSH_COMMAND" \
--exclude 'target/' \
--exclude '.git/' \
--exclude '*.log' \
--exclude '.claude/' \
--exclude '.codex-socktest/' \
--exclude '.jcode/' \
--exclude '.tmp/' \
--exclude '.wrangler/' \
--exclude 'tmp/' \
--exclude 'node_modules/' \
--exclude 'assets/demos/' \
--exclude 'assets/readme/' \
"$LOCAL_DIR/" "$REMOTE:$REMOTE_DIR/"
metadata_file="$(mktemp)"
trap 'rm -f "$metadata_file"' EXIT
{
printf 'git_hash=%s\n' "$local_git_hash"
printf 'git_date=%s\n' "$local_git_date"
printf 'git_tag=%s\n' "$local_git_tag"
printf 'git_dirty=%s\n' "$local_git_dirty"
printf 'changelog_raw<<JCODE_CHANGELOG_EOF\n%s\nJCODE_CHANGELOG_EOF\n' "$local_changelog_raw"
} > "$metadata_file"
"$RSYNC_BIN" -avz -e "$RSYNC_SSH_COMMAND" "$metadata_file" "$REMOTE:$REMOTE_DIR/.jcode-build-meta"
else
echo ""
echo "[1/3] Skipping source sync (--no-sync)"
fi
printf -v REMOTE_CARGO_CMD '%q ' "${CARGO_CMD[@]}"
printf -v REMOTE_INNER_CMD 'cd %q && env JCODE_BUILD_METADATA_FILE=.jcode-build-meta %s' "$REMOTE_DIR" "$REMOTE_CARGO_CMD"
printf -v REMOTE_RUN_CMD 'sh -lc %q' "$REMOTE_INNER_CMD"
echo ""
echo "[2/3] Running on remote..."
remote_ssh "$REMOTE_RUN_CMD 2>&1"
echo ""
if [[ "$sync_back" -eq 1 ]]; then
printf -v REMOTE_TEST_CMD 'test -f %q' "$REMOTE_DIR/$BINARY_PATH"
if remote_ssh "$REMOTE_TEST_CMD"; then
echo "[3/3] Syncing built artifact back..."
mkdir -p "$(dirname "$LOCAL_DIR/$BINARY_PATH")"
"$RSYNC_BIN" -avz -e "$RSYNC_SSH_COMMAND" "$REMOTE:$REMOTE_DIR/$BINARY_PATH" "$LOCAL_DIR/$BINARY_PATH"
echo ""
echo "=== Remote cargo complete ==="
ls -la "$LOCAL_DIR/$BINARY_PATH"
else
echo "[3/3] Skipping sync-back: $BINARY_PATH not found on remote"
fi
else
echo "[3/3] Skipping binary sync-back"
fi
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env bash
# Shared loader for Jcode remote build defaults.
#
# The config file is intentionally a shell fragment so users can write either:
# JCODE_REMOTE_HOST=builder
# or:
# export JCODE_REMOTE_HOST=builder
#
# Explicit environment variables take precedence over values loaded from the
# config file. This lets callers temporarily disable remote builds with, for
# example, `JCODE_REMOTE_CARGO=0 scripts/dev_cargo.sh check`.
jcode_remote_config_path() {
if [[ -n "${JCODE_REMOTE_CONFIG:-}" ]]; then
printf '%s\n' "$JCODE_REMOTE_CONFIG"
elif [[ -n "${XDG_CONFIG_HOME:-}" ]]; then
printf '%s\n' "$XDG_CONFIG_HOME/jcode/remote-build.env"
elif [[ -n "${HOME:-}" ]]; then
printf '%s\n' "$HOME/.config/jcode/remote-build.env"
fi
}
jcode_load_remote_config() {
local config_file
config_file="$(jcode_remote_config_path)"
[[ -n "$config_file" && -f "$config_file" ]] || return 0
local had_remote_cargo=0 remote_cargo=""
local had_remote_host=0 remote_host=""
local had_remote_dir=0 remote_dir=""
local had_remote_ssh_bin=0 remote_ssh_bin=""
local had_remote_rsync_bin=0 remote_rsync_bin=""
if [[ ${JCODE_REMOTE_CARGO+x} ]]; then
had_remote_cargo=1
remote_cargo="$JCODE_REMOTE_CARGO"
fi
if [[ ${JCODE_REMOTE_HOST+x} ]]; then
had_remote_host=1
remote_host="$JCODE_REMOTE_HOST"
fi
if [[ ${JCODE_REMOTE_DIR+x} ]]; then
had_remote_dir=1
remote_dir="$JCODE_REMOTE_DIR"
fi
if [[ ${JCODE_REMOTE_SSH_BIN+x} ]]; then
had_remote_ssh_bin=1
remote_ssh_bin="$JCODE_REMOTE_SSH_BIN"
fi
if [[ ${JCODE_REMOTE_RSYNC_BIN+x} ]]; then
had_remote_rsync_bin=1
remote_rsync_bin="$JCODE_REMOTE_RSYNC_BIN"
fi
# shellcheck source=/dev/null
source "$config_file"
if [[ "$had_remote_cargo" -eq 1 ]]; then
JCODE_REMOTE_CARGO="$remote_cargo"
fi
if [[ "$had_remote_host" -eq 1 ]]; then
JCODE_REMOTE_HOST="$remote_host"
fi
if [[ "$had_remote_dir" -eq 1 ]]; then
JCODE_REMOTE_DIR="$remote_dir"
fi
if [[ "$had_remote_ssh_bin" -eq 1 ]]; then
JCODE_REMOTE_SSH_BIN="$remote_ssh_bin"
fi
if [[ "$had_remote_rsync_bin" -eq 1 ]]; then
JCODE_REMOTE_RSYNC_BIN="$remote_rsync_bin"
fi
}
+178
View File
@@ -0,0 +1,178 @@
#!/bin/bash
# Replay a jcode recording as video
#
# This script:
# 1. Starts a fresh jcode instance in a new terminal
# 2. Records the screen with wf-recorder
# 3. Replays the recorded keystrokes with proper timing
# 4. Outputs a video file
#
# Usage: ./replay_recording.sh <recording.json> [output.mp4]
set -e
RECORDING_FILE="${1:?Usage: $0 <recording.json> [output.mp4]}"
OUTPUT_FILE="${2:-${RECORDING_FILE%.json}.mp4}"
SCRIPT_DIR="$(dirname "$0")"
if [ ! -f "$RECORDING_FILE" ]; then
echo "Error: Recording file not found: $RECORDING_FILE"
exit 1
fi
echo "🎬 jcode Recording Replay"
echo " Input: $RECORDING_FILE"
echo " Output: $OUTPUT_FILE"
echo ""
# Parse the recording and generate wtype commands
generate_wtype_script() {
python3 << 'PYTHON' "$RECORDING_FILE"
import json
import sys
with open(sys.argv[1]) as f:
events = json.load(f)
prev_time = 0
for event in events:
offset = event.get('offset_ms', 0)
delay = offset - prev_time
prev_time = offset
if delay > 0:
# Sleep for the delay (in seconds)
print(f"sleep {delay / 1000:.3f}")
evt = event.get('event', {})
evt_type = evt.get('type')
if evt_type == 'Key':
data = evt.get('data', {})
code = data.get('code', '')
mods = data.get('modifiers', [])
# Convert code to wtype format
key = None
if code.startswith('Char('):
# Extract character: Char('a') -> a
char = code[6:-2] if len(code) > 7 else code[6:-1]
key = char
elif code == 'Enter':
key = 'Return'
elif code == 'Backspace':
key = 'BackSpace'
elif code == 'Tab':
key = 'Tab'
elif code == 'Esc':
key = 'Escape'
elif code == 'Up':
key = 'Up'
elif code == 'Down':
key = 'Down'
elif code == 'Left':
key = 'Left'
elif code == 'Right':
key = 'Right'
elif code == 'Home':
key = 'Home'
elif code == 'End':
key = 'End'
elif code == 'PageUp':
key = 'Page_Up'
elif code == 'PageDown':
key = 'Page_Down'
elif code == 'Delete':
key = 'Delete'
elif code == 'Insert':
key = 'Insert'
elif code.startswith('F') and code[1:].isdigit():
key = code # F1, F2, etc.
else:
# Unknown key, skip
continue
if key:
cmd = 'wtype'
for mod in mods:
if mod == 'ctrl':
cmd += ' -M ctrl'
elif mod == 'alt':
cmd += ' -M alt'
elif mod == 'shift':
cmd += ' -M shift'
if len(key) == 1 and key.isalnum():
cmd += f' "{key}"'
else:
cmd += f' -k {key}'
# Release modifiers
for mod in reversed(mods):
if mod == 'ctrl':
cmd += ' -m ctrl'
elif mod == 'alt':
cmd += ' -m alt'
elif mod == 'shift':
cmd += ' -m shift'
print(cmd)
PYTHON
}
# Get the screen geometry for recording
GEOMETRY=$(niri msg focused-output 2>/dev/null | grep -oP 'Mode: \K\d+x\d+' | head -1 || echo "1920x1080")
echo "📹 Starting screen recording..."
echo " Geometry: $GEOMETRY"
echo ""
# Start wf-recorder in background
wf-recorder -g "0,0 $GEOMETRY" -f "$OUTPUT_FILE" &
RECORDER_PID=$!
sleep 1 # Let recorder initialize
# Start jcode in a new kitty window
echo "🚀 Starting jcode..."
kitty --title "jcode-replay" -e bash -c "cd $(pwd) && ~/.cargo/bin/jcode; read -p 'Press Enter to close...'" &
KITTY_PID=$!
sleep 2 # Wait for jcode to start
# Focus the new window
sleep 0.5
# Find and focus the jcode-replay window
WINDOW_ID=$(niri msg windows 2>/dev/null | grep -B5 "jcode-replay" | grep -oP 'Window ID \K\d+' | head -1)
if [ -n "$WINDOW_ID" ]; then
echo " Focusing window $WINDOW_ID"
niri msg action focus-window --id "$WINDOW_ID"
sleep 0.3
fi
echo "⌨️ Replaying keystrokes..."
echo ""
# Generate and execute wtype script
generate_wtype_script | while read -r cmd; do
if [[ "$cmd" == sleep* ]]; then
eval "$cmd"
else
eval "$cmd" 2>/dev/null || true
fi
done
echo ""
echo "⏹️ Stopping recording..."
sleep 1 # Final pause
# Stop recorder
kill $RECORDER_PID 2>/dev/null || true
wait $RECORDER_PID 2>/dev/null || true
# Clean up kitty window
kill $KITTY_PID 2>/dev/null || true
echo ""
echo "✅ Done!"
echo " Video saved to: $OUTPUT_FILE"
ls -lh "$OUTPUT_FILE"
@@ -0,0 +1,2 @@
/target
Cargo.lock
@@ -0,0 +1,20 @@
[package]
name = "tls-bad-record-mac-repro"
version = "0.1.0"
edition = "2021"
publish = false
# Standalone: do not attach to the parent jcode workspace.
[workspace]
[dependencies]
anyhow = "1"
tokio = { version = "1", features = ["full"] }
tokio-rustls = "0.26"
rustls = "0.23"
rcgen = "0.13"
reqwest = { version = "=0.12.28", default-features = false, features = ["json","stream","blocking","charset","http2","system-proxy","rustls-tls","rustls-tls-native-roots"] }
[[bin]]
name = "tls-bad-record-mac-repro"
path = "src/main.rs"
@@ -0,0 +1,56 @@
# TLS `BadRecordMac` transport-retry reproduction
Standalone reproduction for the bug fixed in commit
`fix(providers): retry on TLS transport faults (BadRecordMac) across all providers`.
## What it reproduces
Users on flaky networks / corrupting middleboxes / some VPNs hit:
```
Stream error: IO error: received fatal alert: BadRecordMac
```
A `BadRecordMac` is a TLS record-authentication failure. It is transient: a
fresh connection on retry almost always succeeds. The bug was that jcode failed
these immediately instead of retrying, for two reasons:
1. **OpenAI** maintained its own `is_retryable_error` allowlist that did not
call the shared `is_transient_transport_error` and omitted every TLS term, so
a `BadRecordMac` on the websocket path surfaced at `attempt=1`
(`will_retry=false`).
2. **claude / copilot / openrouter / openai** classified on `e.to_string()`.
When a transport cause is wrapped behind `anyhow`'s `.context(...)`,
`to_string()` only returns the top-level context (e.g. `Failed to send
request to ... API`) and **masks** the underlying `BadRecordMac`, so the
classifier never sees it. (The anthropic provider already used
`format!("{e:#}")` and was unaffected.)
## How it works
```
reqwest client (rustls) -> corrupting TCP proxy -> real TLS server (rustls)
```
The proxy flips one byte inside the first large client->server TLS
`application_data` record. The server's AEAD check then fails and it emits a TLS
`bad_record_mac` fatal alert, which the client surfaces exactly as users see it.
The harness then:
- wraps the error like the providers do (`.context(...)`),
- runs the **verbatim** shipping `is_transient_transport_error`,
- runs the **old** vs **new** OpenAI `is_retryable_error`,
and asserts old logic misses it while the fix retries.
## Run
```bash
cd scripts/repro/tls-bad-record-mac
cargo run
# exit 0 + "SUCCESS: realistic BadRecordMac reproduced and both fixes validated."
```
It is intentionally a standalone crate (own `[workspace]` table) so its
TLS/cert dev-dependencies do not touch the main build.
@@ -0,0 +1,324 @@
// Realistic reproduction of David's `received fatal alert: BadRecordMac`.
//
// Architecture (mimics a corrupting middlebox / flaky VPN):
//
// reqwest client (rustls) -> corrupting TCP proxy -> real TLS server (rustls)
//
// The proxy flips one byte inside the first large client->server TLS
// application_data record (the HTTP request body). The server's AEAD
// authentication then fails, so it returns a TLS `bad_record_mac` fatal alert,
// which the client surfaces as: "received fatal alert: BadRecordMac".
//
// We then wrap that error exactly like jcode's anthropic provider does
// (`.context("Failed to send request to Anthropic API")`) and show how the
// cause is masked from `to_string()` but visible via the error source chain.
use std::sync::Arc;
use anyhow::Context;
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio_rustls::TlsAcceptor;
fn classify(error_str: &str) -> bool {
// VERBATIM copy of jcode's shared is_transient_transport_error
// (crates/jcode-base/src/provider/routing.rs @ v0.24.0).
let lower = error_str.to_ascii_lowercase();
lower.contains("connection reset")
|| lower.contains("connection closed")
|| lower.contains("connection refused")
|| lower.contains("connection aborted")
|| lower.contains("broken pipe")
|| lower.contains("timed out")
|| lower.contains("timeout")
|| lower.contains("operation timed out")
|| lower.contains("error decoding")
|| lower.contains("error reading")
|| lower.contains("unexpected eof")
|| lower.contains("tls handshake eof")
|| lower.contains("badrecordmac")
|| lower.contains("bad_record_mac")
|| lower.contains("fatal alert: badrecordmac")
|| lower.contains("fatal alert: bad_record_mac")
|| lower.contains("received fatal alert: badrecordmac")
|| lower.contains("received fatal alert: bad_record_mac")
|| lower.contains("decryption failed or bad record mac")
|| lower.contains("temporary failure in name resolution")
|| lower.contains("failed to lookup address information")
|| lower.contains("dns error")
|| lower.contains("name or service not known")
|| lower.contains("no route to host")
|| lower.contains("network is unreachable")
|| lower.contains("host is unreachable")
|| lower.contains("http2 error")
|| lower.contains("stream error")
|| lower.contains("protocol error")
|| lower.contains("refused_stream")
|| lower.contains("refused stream")
|| lower.contains("enhance_your_calm")
|| lower.contains("goaway")
|| lower.contains("go away")
|| lower.contains("sendrequest")
}
/// OLD OpenAI is_retryable_error (pre-fix): standalone list, NO TLS terms,
/// does NOT call the shared classifier. This is what shipped in v0.24.0 and
/// caused David's BadRecordMac to fail immediately.
fn openai_classify_old(error_str: &str) -> bool {
error_str.contains("connection reset")
|| error_str.contains("connection closed")
|| error_str.contains("connection refused")
|| error_str.contains("broken pipe")
|| error_str.contains("timed out")
|| error_str.contains("timeout")
|| error_str.contains("failed to send request to openai api")
|| error_str.contains("error decoding")
|| error_str.contains("error reading")
|| error_str.contains("unexpected eof")
|| error_str.contains("incomplete message")
|| error_str.contains("stream disconnected before completion")
|| error_str.contains("ended before message completion marker")
|| error_str.contains("falling back from websockets to https transport")
|| error_str.contains("500 internal server error")
|| error_str.contains("502 bad gateway")
|| error_str.contains("503 service unavailable")
|| error_str.contains("504 gateway timeout")
|| error_str.contains("overloaded")
|| error_str.contains("api_error")
|| error_str.contains("server_error")
|| error_str.contains("internal server error")
|| error_str.contains("an error occurred while processing your request")
|| error_str.contains("please include the request id")
}
/// NEW OpenAI is_retryable_error (post-fix): delegates to the shared classifier.
fn openai_classify_new(error_str: &str) -> bool {
classify(error_str) || openai_classify_old(error_str)
}
/// Walk the full anyhow error source chain and classify on the joined text.
fn classify_chain(err: &anyhow::Error) -> bool {
// 1) alternate Display includes the cause chain
if classify(&format!("{err:#}")) {
return true;
}
// 2) explicit chain walk (most robust)
for cause in err.chain() {
if classify(&cause.to_string()) {
return true;
}
}
false
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let _ = rustls::crypto::ring::default_provider().install_default();
// ---- 1. Self-signed cert for 127.0.0.1 ----
let cert = rcgen::generate_simple_self_signed(vec!["127.0.0.1".to_string()])?;
let cert_der = CertificateDer::from(cert.cert.der().to_vec());
let key_der = PrivateKeyDer::try_from(cert.key_pair.serialize_der())
.map_err(|e| anyhow::anyhow!("key: {e}"))?;
let server_config = rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(vec![cert_der.clone()], key_der)?;
let acceptor = TlsAcceptor::from(Arc::new(server_config));
// ---- 2. Real TLS server ----
let server_listener = TcpListener::bind("127.0.0.1:0").await?;
let server_addr = server_listener.local_addr()?;
tokio::spawn(async move {
loop {
let Ok((tcp, _)) = server_listener.accept().await else {
continue;
};
let acceptor = acceptor.clone();
tokio::spawn(async move {
match acceptor.accept(tcp).await {
Ok(mut tls) => {
// Try to read the request; AEAD failure shows up here and
// rustls automatically emits a bad_record_mac fatal alert.
let mut buf = [0u8; 4096];
let _ = tls.read(&mut buf).await;
let _ = tls
.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok")
.await;
let _ = tls.shutdown().await;
}
Err(e) => {
eprintln!("[server] handshake/accept error: {e}");
}
}
});
}
});
// ---- 3. Corrupting proxy ----
let proxy_listener = TcpListener::bind("127.0.0.1:0").await?;
let proxy_addr = proxy_listener.local_addr()?;
tokio::spawn(async move {
loop {
let Ok((client, _)) = proxy_listener.accept().await else {
continue;
};
tokio::spawn(async move {
if let Err(e) = corrupt_proxy(client, server_addr).await {
eprintln!("[proxy] {e}");
}
});
}
});
// ---- 4. Client (mirrors jcode's reqwest+rustls config) ----
let mut roots = rustls::RootCertStore::empty();
roots.add(cert_der)?;
let client = reqwest::Client::builder()
.use_preconfigured_tls(
rustls::ClientConfig::builder()
.with_root_certificates(roots)
.with_no_client_auth(),
)
.build()?;
let url = format!("https://127.0.0.1:{}/v1/messages", proxy_addr.port());
let body = serde_json_body();
let result = client
.post(&url)
.header("content-type", "application/json")
.header("x-api-key", "sk-test-1234567890")
.body(body)
.send()
.await;
let raw = match &result {
Ok(_) => "<no error: request unexpectedly succeeded>".to_string(),
Err(e) => e.to_string(),
};
println!("=== Reproduction result ===");
println!("raw reqwest to_string : {raw:?}");
// Wrap exactly like the anthropic provider does.
let wrapped: anyhow::Result<()> = result
.map(|_| ())
.context("Failed to send request to Anthropic API");
if let Err(err) = wrapped {
let masked = err.to_string();
let alt = format!("{err:#}");
println!("provider to_string() : {masked:?}");
println!("provider {{:#}} (chain) : {alt:?}");
println!();
let reproduced = alt.to_lowercase().contains("badrecordmac")
|| raw.to_lowercase().contains("badrecordmac");
println!("REPRODUCED BadRecordMac: {reproduced}");
println!();
// Anthropic-style WS/HTTP error where the cause is INLINE in the message
// (this is what David actually saw from the OpenAI websocket path).
let openai_inline = "stream error: io error: received fatal alert: badrecordmac";
println!("=== OpenAI websocket path (inline error, as David saw) ===");
println!(" error string: {openai_inline:?}");
println!(" OLD is_retryable = {} (immediate fail, will_retry=false)",
openai_classify_old(openai_inline));
println!(" NEW is_retryable = {} (retried)", openai_classify_new(openai_inline));
println!();
println!("=== Send path with anyhow .context() masking ===");
println!(" to_string (masked) -> shared classify = {}", classify(&masked));
println!(" {{:#}} chain -> shared classify = {}", classify(&alt));
println!(" chain-aware walk -> classify = {}", classify_chain(&err));
println!();
let bug_confirmed = !openai_classify_old(openai_inline)
&& openai_classify_new(openai_inline)
&& !classify(&masked)
&& classify_chain(&err);
println!("BUG CONFIRMED (old misses, fix catches; masking confirmed): {bug_confirmed}");
if reproduced && bug_confirmed {
println!("\nSUCCESS: realistic BadRecordMac reproduced and both fixes validated.");
std::process::exit(0);
} else {
println!("\nINCOMPLETE: reproduced={reproduced} bug_confirmed={bug_confirmed}");
std::process::exit(1);
}
}
Ok(())
}
fn serde_json_body() -> Vec<u8> {
// Large enough that the HTTP request becomes a sizable application_data record.
let payload = format!(
"{{\"model\":\"claude-opus-4\",\"messages\":[{{\"role\":\"user\",\"content\":\"{}\"}}]}}",
"x".repeat(800)
);
payload.into_bytes()
}
/// TCP proxy that forwards both directions but flips a byte inside the first
/// large client->server TLS application_data record, corrupting the ciphertext.
async fn corrupt_proxy(client: TcpStream, server_addr: std::net::SocketAddr) -> anyhow::Result<()> {
let server = TcpStream::connect(server_addr).await?;
let (mut cr, mut cw) = client.into_split();
let (mut sr, mut sw) = server.into_split();
// server -> client: pass through untouched
let s2c = tokio::spawn(async move {
let mut buf = [0u8; 8192];
loop {
match sr.read(&mut buf).await {
Ok(0) | Err(_) => break,
Ok(n) => {
if cw.write_all(&buf[..n]).await.is_err() {
break;
}
}
}
}
});
// client -> server: parse TLS records, corrupt first big application_data
let c2s = tokio::spawn(async move {
let mut acc: Vec<u8> = Vec::new();
let mut buf = [0u8; 8192];
let mut corrupted = false;
loop {
match cr.read(&mut buf).await {
Ok(0) | Err(_) => break,
Ok(n) => {
acc.extend_from_slice(&buf[..n]);
// Emit complete TLS records, corrupting as configured.
while acc.len() >= 5 {
let content_type = acc[0];
let len = ((acc[3] as usize) << 8) | (acc[4] as usize);
let total = 5 + len;
if acc.len() < total {
break;
}
let mut rec: Vec<u8> = acc.drain(..total).collect();
// application_data == 23; corrupt the first big one (the
// HTTP request), flipping a ciphertext byte so the server
// AEAD check fails -> bad_record_mac alert.
if !corrupted && content_type == 23 && len > 80 {
let mid = 5 + len / 2;
rec[mid] ^= 0xff;
corrupted = true;
eprintln!("[proxy] corrupted application_data record ({len} bytes)");
}
if sw.write_all(&rec).await.is_err() {
return;
}
}
}
}
}
let _ = sw.write_all(&acc).await;
});
let _ = tokio::join!(s2c, c2s);
Ok(())
}
+565
View File
@@ -0,0 +1,565 @@
#!/usr/bin/env python3
"""
End-to-end verifier for the Alt+Shift+E "expand edit diff" shortcut.
Why this exists
---------------
The handler logic for Alt+Shift+E is covered by unit tests, but those tests
inject *synthetic* `crossterm::KeyEvent`s. They never exercise:
* crossterm's real terminal escape-sequence decoder,
* the live event loop (local + remote client),
* the kitty keyboard protocol bytes a real terminal actually sends.
So a unit test can be green while the live shortcut does nothing. This harness
closes that gap by driving the **real jcode binary** under a pseudo-terminal and
feeding it the **exact bytes** a terminal emits for Alt+Shift+E, then reading the
resulting diff state back over the debug socket.
What "pass" means (100% confidence)
-----------------------------------
The harness runs three checks against one live client:
1. NEGATIVE CONTROL
Confirm the fixture starts collapsed (diff_mode == Inline), so a later
"FullInline" reading cannot be a false positive from a pre-expanded state.
2. RAW-BYTES E2E (the real test)
Write the literal Alt+Shift+E byte sequence (kitty CSI-u form, plus the
legacy ESC-prefixed form) into the PTY master. crossterm decodes it, the
live loop dispatches it, and we assert diff_mode flips to FullInline.
3. POSITIVE CONTROL (decode-independent)
Reset the fixture, then drive the *same* handler through the debug socket's
synthetic `keys:alt+shift+e`. If raw bytes fail but synthetic keys succeed,
the break is in terminal decode / byte delivery, not the handler.
Each byte encoding is tried against a freshly reset fixture so we learn exactly
which terminal encoding(s) work end to end.
Everything runs in a throwaway JCODE_HOME / runtime dir / socket. The user's
real server and sessions are never touched.
Usage
-----
python3 scripts/repro_expand_edit_shortcut.py [--binary PATH] [--keep] [-v]
Exit codes:
0 = raw-bytes Alt+Shift+E expands the diff end to end (shortcut works)
2 = raw bytes do NOT expand, but synthetic keys do (decode/delivery break)
3 = even synthetic keys fail (handler/fixture break) or setup failure
"""
from __future__ import annotations
import argparse
import json
import os
import pty
import select
import signal
import socket
import subprocess
import tempfile
import threading
import time
import uuid
from dataclasses import dataclass, field
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
# ──────────────────────────────────────────────────────────────────────────────
# The exact bytes a terminal sends for Alt+Shift+E.
#
# Captured live from kitty 0.46 with the kitty keyboard protocol enabled (the
# protocol jcode turns on at startup):
# press = ESC [ 101 ; 4 u -> b"\x1b[101;4u"
# release = ESC [ 101 ; 4 : 3 u -> b"\x1b[101;4:3u"
# (101 = 'e' codepoint, modifier mask 4 = (4-1)=3 = SHIFT|ALT.)
#
# We also include the legacy xterm encodings some terminals use when the kitty
# protocol is not negotiated, so the harness reports which encodings work.
# ──────────────────────────────────────────────────────────────────────────────
KEY_ENCODINGS: list[tuple[str, bytes]] = [
# kitty keyboard protocol (what this machine's kitty actually sends)
("kitty-csi-u (e;SHIFT|ALT)", b"\x1b[101;4u\x1b[101;4:3u"),
# kitty form some terminals send with the uppercase codepoint
("kitty-csi-u (E;SHIFT|ALT)", b"\x1b[69;4u\x1b[69;4:3u"),
# legacy: ESC then Shift+E (Alt=ESC prefix, Shift folded into uppercase)
("legacy esc+E", b"\x1bE"),
# legacy: ESC then lowercase e (some terminals drop the shift bit)
("legacy esc+e", b"\x1be"),
]
# ──────────────────────────────────────────────────────────────────────────────
# debug socket helpers (line-delimited JSON over a unix socket)
# ──────────────────────────────────────────────────────────────────────────────
def _recv_debug_response(sock: socket.socket, timeout: float) -> dict:
sock.settimeout(timeout)
buf = b""
while True:
chunk = sock.recv(65536)
if not chunk:
break
buf += chunk
while b"\n" in buf:
line, buf = buf.split(b"\n", 1)
line = line.strip()
if not line:
continue
resp = json.loads(line.decode())
if resp.get("type") in ("ack", "pong"):
continue
return resp
raise RuntimeError("debug socket closed without a response")
def dbg(debug_sock: Path, command: str, session_id: str | None = None,
timeout: float = 30.0) -> dict:
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect(str(debug_sock))
try:
req = {"type": "debug_command", "id": 1, "command": command}
if session_id:
req["session_id"] = session_id
s.sendall((json.dumps(req) + "\n").encode())
return _recv_debug_response(s, timeout)
finally:
s.close()
def dbg_output(debug_sock: Path, command: str, session_id: str | None = None,
timeout: float = 30.0) -> str:
resp = dbg(debug_sock, command, session_id=session_id, timeout=timeout)
if resp.get("type") == "error":
raise RuntimeError(f"debug error for {command!r}: {resp.get('message')}")
if resp.get("ok") is False:
raise RuntimeError(f"command {command!r} failed: {resp.get('output')}")
return resp.get("output", "")
def wait_for_socket(path: Path, timeout_s: float = 25.0) -> None:
deadline = time.time() + timeout_s
while time.time() < deadline:
if path.exists():
try:
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.settimeout(0.2)
s.connect(str(path))
s.close()
return
except OSError:
pass
time.sleep(0.02)
raise RuntimeError(f"socket not ready: {path}")
# ──────────────────────────────────────────────────────────────────────────────
# A PTY-driven jcode client. This is the real live-render input path; bytes
# written to master_fd are decoded by crossterm exactly as a terminal would.
# ──────────────────────────────────────────────────────────────────────────────
_TERM_REPLIES = [
(b"\x1b[6n", b"\x1b[1;1R"),
(b"\x1b[c", b"\x1b[?62;c"),
(b"\x1b]10;?\x1b\\", b"\x1b]10;rgb:ffff/ffff/ffff\x1b\\"),
(b"\x1b]11;?\x1b\\", b"\x1b]11;rgb:0000/0000/0000\x1b\\"),
(b"\x1b]10;?\x07", b"\x1b]10;rgb:ffff/ffff/ffff\x07"),
(b"\x1b]11;?\x07", b"\x1b]11;rgb:0000/0000/0000\x07"),
(b"\x1b[14t", b"\x1b[4;600;800t"),
(b"\x1b[16t", b"\x1b[6;16;8t"),
(b"\x1b[18t", b"\x1b[8;24;80t"),
(b"\x1b[?1016$p", b"\x1b[?1016;1$y"),
(b"\x1b[?2027$p", b"\x1b[?2027;1$y"),
(b"\x1b[?2031$p", b"\x1b[?2031;1$y"),
(b"\x1b[?1004$p", b"\x1b[?1004;1$y"),
(b"\x1b[?2004$p", b"\x1b[?2004;1$y"),
(b"\x1b[?2026$p", b"\x1b[?2026;1$y"),
# kitty keyboard protocol query: report DISAMBIGUATE|REPORT_EVENT_TYPES active
(b"\x1b[?u", b"\x1b[?3u"),
]
@dataclass
class LiveClient:
name: str
proc: subprocess.Popen
master_fd: int
stop: threading.Event = field(default_factory=threading.Event)
thread: threading.Thread | None = None
total_bytes: int = 0
def _pump(self) -> None:
buffer = b""
while not self.stop.is_set():
try:
rlist, _, _ = select.select([self.master_fd], [], [], 0.1)
except (OSError, ValueError):
break
if not rlist:
if self.proc.poll() is not None:
break
continue
try:
chunk = os.read(self.master_fd, 65536)
except (BlockingIOError, OSError):
continue
if not chunk:
break
self.total_bytes += len(chunk)
buffer = (buffer + chunk)[-8192:]
changed = True
while changed:
changed = False
for query, response in _TERM_REPLIES:
if query in buffer:
try:
os.write(self.master_fd, response)
except OSError:
pass
buffer = buffer.replace(query, b"")
changed = True
def start_pump(self) -> None:
self.thread = threading.Thread(target=self._pump, daemon=True)
self.thread.start()
def alive(self) -> bool:
return self.proc.poll() is None
def send_bytes(self, data: bytes) -> None:
os.write(self.master_fd, data)
def shutdown(self) -> None:
self.stop.set()
if self.thread:
self.thread.join(timeout=1.0)
try:
os.killpg(self.proc.pid, signal.SIGTERM)
except (ProcessLookupError, PermissionError):
pass
try:
self.proc.wait(timeout=2.0)
except Exception:
try:
os.killpg(self.proc.pid, signal.SIGKILL)
except ProcessLookupError:
pass
try:
os.close(self.master_fd)
except OSError:
pass
def launch_client(binary: str, env: dict, session_id: str, name: str,
debug_cmd: Path, debug_resp: Path) -> LiveClient:
master_fd, slave_fd = pty.openpty()
cenv = dict(env)
# Route this client's file-based debug channel to per-client paths so we can
# talk to *this* live TUI directly (fixture setup + state readback + the
# synthetic-key positive control).
cenv["JCODE_DEBUG_CMD_PATH"] = str(debug_cmd)
cenv["JCODE_DEBUG_RESPONSE_PATH"] = str(debug_resp)
proc = subprocess.Popen(
[
binary,
"--no-update",
"--no-selfdev",
"--socket", env["JCODE_SOCKET"],
"--resume", session_id,
],
stdin=slave_fd, stdout=slave_fd, stderr=slave_fd,
env=cenv, preexec_fn=os.setsid,
)
os.close(slave_fd)
os.set_blocking(master_fd, False)
client = LiveClient(name=name, proc=proc, master_fd=master_fd)
client.start_pump()
return client
# ──────────────────────────────────────────────────────────────────────────────
# per-client file-based debug channel (talks to the live TUI, not the server)
# ──────────────────────────────────────────────────────────────────────────────
def client_cmd(debug_cmd: Path, debug_resp: Path, command: str,
timeout_s: float = 6.0) -> str:
"""Send a command to a live TUI client via its file debug channel."""
try:
debug_resp.unlink()
except FileNotFoundError:
pass
debug_cmd.write_text(command)
deadline = time.time() + timeout_s
while time.time() < deadline:
if debug_resp.exists():
# small settle so the write completes
time.sleep(0.02)
return debug_resp.read_text()
time.sleep(0.02)
raise RuntimeError(f"client did not answer debug command {command!r} in {timeout_s}s")
def client_diff_mode(debug_cmd: Path, debug_resp: Path) -> dict:
raw = client_cmd(debug_cmd, debug_resp, "expand-badge-state")
return json.loads(raw)
def reset_fixture(debug_cmd: Path, debug_resp: Path) -> dict:
raw = client_cmd(debug_cmd, debug_resp, "expand-badge-fixture")
return json.loads(raw)
# ──────────────────────────────────────────────────────────────────────────────
# minimal session seed (just needs to exist so the client can resume)
# ──────────────────────────────────────────────────────────────────────────────
def now_iso() -> str:
return time.strftime("%Y-%m-%dT%H:%M:%S.000000000Z", time.gmtime())
def seed_session(home: Path, working_dir: str) -> str:
sessions_dir = home / "sessions"
sessions_dir.mkdir(parents=True, exist_ok=True)
session_id = f"session_expand_{int(time.time()*1000)}_{uuid.uuid4().hex[:12]}"
messages = [{
"id": f"message_{uuid.uuid4().hex}",
"role": "user",
"content": [{"type": "text", "text": "seed for expand-edit shortcut test"}],
"timestamp": now_iso(),
}]
session = {
"id": session_id,
"parent_id": None,
"title": "expand-edit-shortcut",
"created_at": now_iso(),
"updated_at": now_iso(),
"working_dir": working_dir,
"messages": messages,
}
(sessions_dir / f"{session_id}.json").write_text(json.dumps(session))
return session_id
# ──────────────────────────────────────────────────────────────────────────────
def settle_client(debug_cmd: Path, debug_resp: Path, timeout_s: float = 20.0,
verbose: bool = False) -> bool:
deadline = time.time() + timeout_s
last_err = None
while time.time() < deadline:
try:
client_cmd(debug_cmd, debug_resp, "expand-badge-state", timeout_s=2.0)
return True
except Exception as e: # noqa: BLE001
last_err = e
time.sleep(0.2)
if verbose and last_err:
print(f" (settle never answered: {last_err})")
return False
def try_raw_encoding(client: LiveClient, debug_cmd: Path, debug_resp: Path,
label: str, raw: bytes, verbose: bool) -> bool:
"""Reset to collapsed, feed raw bytes, return True if it expanded."""
reset_fixture(debug_cmd, debug_resp)
time.sleep(0.15)
before = client_diff_mode(debug_cmd, debug_resp)
if before.get("diff_mode") != "Inline":
# Force a clean collapsed baseline; never report a false positive.
if verbose:
print(f" [{label}] baseline not Inline ({before.get('diff_mode')}), retrying reset")
reset_fixture(debug_cmd, debug_resp)
time.sleep(0.2)
before = client_diff_mode(debug_cmd, debug_resp)
client.send_bytes(raw)
# Give the event loop time to read + dispatch the bytes.
deadline = time.time() + 2.0
after = before
while time.time() < deadline:
time.sleep(0.1)
after = client_diff_mode(debug_cmd, debug_resp)
if after.get("diff_mode") == "FullInline":
break
ok = (before.get("diff_mode") == "Inline"
and after.get("diff_mode") == "FullInline")
status = "✅ EXPANDED" if ok else "— no change"
print(f" [{label:<28}] {before.get('diff_mode')} -> {after.get('diff_mode')} {status}")
return ok
def negative_discriminator(client: LiveClient, debug_cmd: Path, debug_resp: Path,
verbose: bool) -> bool:
"""Prove the detector can tell expand from no-expand.
Feed a plain 'e' (no Alt/Shift). It must NOT expand the diff; otherwise a
later "FullInline" reading would be meaningless. Returns True if the
discriminator behaved correctly (stayed collapsed).
"""
reset_fixture(debug_cmd, debug_resp)
time.sleep(0.15)
before = client_diff_mode(debug_cmd, debug_resp)
client.send_bytes(b"e") # plain ASCII e
client.send_bytes(b"\x1b[101u") # kitty CSI-u plain 'e', no modifiers
time.sleep(0.6)
after = client_diff_mode(debug_cmd, debug_resp)
ok = (before.get("diff_mode") == "Inline"
and after.get("diff_mode") == "Inline")
status = "✅ stayed collapsed" if ok else "❌ unexpectedly expanded"
print(f" [{'plain e (no Alt/Shift)':<28}] "
f"{before.get('diff_mode')} -> {after.get('diff_mode')} {status}")
return ok
def synthetic_positive_control(client: LiveClient, debug_cmd: Path, debug_resp: Path,
verbose: bool) -> bool:
reset_fixture(debug_cmd, debug_resp)
time.sleep(0.15)
before = client_diff_mode(debug_cmd, debug_resp)
client_cmd(debug_cmd, debug_resp, "keys:alt+shift+e")
time.sleep(0.3)
after = client_diff_mode(debug_cmd, debug_resp)
ok = (before.get("diff_mode") == "Inline"
and after.get("diff_mode") == "FullInline")
status = "✅ EXPANDED" if ok else "— no change"
print(f" [{'synthetic keys:alt+shift+e':<28}] "
f"{before.get('diff_mode')} -> {after.get('diff_mode')} {status}")
return ok
def main() -> int:
ap = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
default_bin = Path.home() / ".jcode" / "builds" / "current" / "jcode"
ap.add_argument("--binary", default=str(default_bin))
ap.add_argument("--keep", action="store_true", help="keep temp home on exit")
ap.add_argument("-v", "--verbose", action="store_true")
args = ap.parse_args()
binary = str(Path(args.binary).resolve())
if not Path(binary).exists():
print(f"❌ binary not found: {binary}")
return 3
root = Path(tempfile.mkdtemp(prefix="jcode-expand-edit-"))
home = root / "home"
run = root / "run"
home.mkdir(parents=True, exist_ok=True)
run.mkdir(parents=True, exist_ok=True)
env = os.environ.copy()
env["JCODE_HOME"] = str(home)
env["JCODE_RUNTIME_DIR"] = str(run)
env["JCODE_SOCKET"] = str(run / "jcode.sock")
env["JCODE_NO_TELEMETRY"] = "1"
env["JCODE_DEBUG_CONTROL"] = "1"
env["JCODE_TEMP_SERVER"] = "1"
env["JCODE_SERVER_OWNER_PID"] = str(os.getpid())
debug_sock = run / "jcode-debug.sock"
client_cmd_path = run / "client_debug_cmd"
client_resp_path = run / "client_debug_resp"
print("╔══════════════════════════════════════════════════════════╗")
print("║ Alt+Shift+E expand-edit shortcut: end-to-end verifier ║")
print("╚══════════════════════════════════════════════════════════╝")
print(f" binary : {binary}")
print(f" home : {home}")
server = subprocess.Popen(
[binary, "serve", "--socket", env["JCODE_SOCKET"], "--debug-socket",
"--no-update", "--no-selfdev"],
env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
preexec_fn=os.setsid,
)
client: LiveClient | None = None
rc = 3
try:
wait_for_socket(Path(env["JCODE_SOCKET"]))
wait_for_socket(debug_sock)
print(" server : up")
session_id = seed_session(home, str(REPO_ROOT))
print(f" seeded : {session_id}")
client = launch_client(binary, env, session_id, "client-A",
client_cmd_path, client_resp_path)
if not settle_client(client_cmd_path, client_resp_path, verbose=args.verbose):
print(" ❌ client never came up on the file debug channel")
return 3
print(" client : up (PTY)\n")
# ── 1. negative control: fixture must start collapsed ────────────────
print("── 1. negative control (fixture starts collapsed) ──────────")
fx = reset_fixture(client_cmd_path, client_resp_path)
time.sleep(0.2)
state = client_diff_mode(client_cmd_path, client_resp_path)
collapsed_ok = state.get("diff_mode") == "Inline"
print(f" fixture diff_mode = {state.get('diff_mode')} "
f"{'' if collapsed_ok else '❌ expected Inline'}")
if not collapsed_ok:
print("\n ❌ could not establish a collapsed baseline; aborting.")
return 3
# ── 2. discriminator: prove the detector can tell expand from no-op ──
print("\n── 2. discriminator (plain 'e' must NOT expand) ────────────")
disc_ok = negative_discriminator(
client, client_cmd_path, client_resp_path, args.verbose)
if not disc_ok:
print("\n ❌ detector has no discriminating power; results meaningless.")
return 3
# ── 3. RAW-BYTES end-to-end (the real test) ──────────────────────────
print("\n── 3. raw terminal bytes -> live decode -> handler ─────────")
raw_results = {}
for label, raw in KEY_ENCODINGS:
raw_results[label] = try_raw_encoding(
client, client_cmd_path, client_resp_path, label, raw, args.verbose)
raw_any = any(raw_results.values())
# ── 4. synthetic positive control ────────────────────────────────────
print("\n── 4. positive control (synthetic key, bypasses decode) ────")
synth_ok = synthetic_positive_control(
client, client_cmd_path, client_resp_path, args.verbose)
# ── verdict ──────────────────────────────────────────────────────────
print("\n╭──────────────────────── verdict ─────────────────────────╮")
if raw_any:
working = [k for k, v in raw_results.items() if v]
print("│ ✅ Alt+Shift+E EXPANDS the diff via real terminal bytes. │")
print(f"│ working encodings: {', '.join(working)}")
rc = 0
elif synth_ok:
print("│ ⚠ Raw terminal bytes do NOT expand the diff, but the │")
print("│ handler works via synthetic keys. │")
print("│ => break is in terminal decode / byte delivery, │")
print("│ NOT the expand handler logic. │")
rc = 2
else:
print("│ ❌ Neither raw bytes nor synthetic keys expand the diff. │")
print("│ => break is in the expand handler / fixture itself. │")
rc = 3
print("╰──────────────────────────────────────────────────────────╯")
return rc
finally:
if client:
client.shutdown()
try:
os.killpg(server.pid, signal.SIGTERM)
except (ProcessLookupError, PermissionError):
pass
try:
server.wait(timeout=3.0)
except Exception:
try:
os.killpg(server.pid, signal.SIGKILL)
except ProcessLookupError:
pass
if args.keep:
print(f"\n (kept temp dir: {root})")
else:
import shutil
shutil.rmtree(root, ignore_errors=True)
if __name__ == "__main__":
raise SystemExit(main())
+639
View File
@@ -0,0 +1,639 @@
#!/usr/bin/env python3
"""
Reproduction / detection harness for GitHub issue #314:
"Live transcript duplicates assistant commentary and tool calls"
Goal
----
We have no confirmed repro of #314. This harness builds a *programmatic,
read-back* environment for the live-render layer so duplication can be
detected objectively instead of by eyeballing a transcript.
Strategy
--------
The bug is a divergence between two layers:
* server conversation history -> source of truth (one copy per message)
* client `display_messages` -> what is actually rendered live
So the detector simply diffs the two. Each seeded assistant/tool/tool-result
carries a UNIQUE marker token ("MARK_xxxx"). If a marker shows up in more
display_messages than it appears in the server's history, that is the bug,
caught with the exact trigger isolated.
Everything runs in a throwaway JCODE_HOME / runtime dir / socket, so the
user's real server and sessions are never touched.
The suspected triggers (issue "Possible trigger" + "Notes") are exercised in
stages:
1. fresh-resume single headed client resumes the seeded session
2. second-attach a second headed client attaches to the same session
3. reload client triggers /reload (server reload recovery path)
4. detach-reattach first client is killed, a new client reattaches
After each stage every live client's display history is read back and diffed
against server truth.
Usage
-----
python3 scripts/repro_live_duplicate.py [--binary PATH] [--keep] [-v]
Exit code 0 = no duplication detected, 2 = duplication reproduced.
"""
from __future__ import annotations
import argparse
import json
import os
import pty
import select
import shutil
import signal
import socket
import subprocess
import tempfile
import threading
import time
import uuid
from dataclasses import dataclass, field
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
# ──────────────────────────────────────────────────────────────────────────────
# debug socket helpers
# ──────────────────────────────────────────────────────────────────────────────
def _recv_debug_response(sock: socket.socket, timeout: float) -> dict:
sock.settimeout(timeout)
buf = b""
while True:
chunk = sock.recv(65536)
if not chunk:
break
buf += chunk
while b"\n" in buf:
line, buf = buf.split(b"\n", 1)
line = line.strip()
if not line:
continue
resp = json.loads(line.decode())
t = resp.get("type")
if t in ("ack", "pong"):
continue
return resp
raise RuntimeError("debug socket closed without a response")
def dbg(debug_sock: Path, command: str, session_id: str | None = None,
timeout: float = 30.0) -> dict:
"""Send one debug command, return parsed {ok, output, ...}."""
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect(str(debug_sock))
try:
req = {"type": "debug_command", "id": 1, "command": command}
if session_id:
req["session_id"] = session_id
s.sendall((json.dumps(req) + "\n").encode())
return _recv_debug_response(s, timeout)
finally:
s.close()
def dbg_output(debug_sock: Path, command: str, session_id: str | None = None,
timeout: float = 30.0) -> str:
resp = dbg(debug_sock, command, session_id=session_id, timeout=timeout)
if resp.get("type") == "error":
raise RuntimeError(f"debug error for {command!r}: {resp.get('message')}")
if resp.get("ok") is False:
raise RuntimeError(f"command {command!r} failed: {resp.get('output')}")
return resp.get("output", "")
def wait_for_socket(path: Path, timeout_s: float = 20.0) -> None:
deadline = time.time() + timeout_s
while time.time() < deadline:
if path.exists():
try:
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.settimeout(0.2)
s.connect(str(path))
s.close()
return
except OSError:
pass
time.sleep(0.02)
raise RuntimeError(f"socket not ready: {path}")
# ──────────────────────────────────────────────────────────────────────────────
# PTY-driven headed TUI client (this is the real live-render path)
# ──────────────────────────────────────────────────────────────────────────────
# Terminal capability queries the TUI emits at startup; we answer them so the
# client finishes initializing under a pseudo-terminal.
_TERM_REPLIES = [
(b"\x1b[6n", b"\x1b[1;1R"),
(b"\x1b[c", b"\x1b[?62;c"),
(b"\x1b]10;?\x1b\\", b"\x1b]10;rgb:ffff/ffff/ffff\x1b\\"),
(b"\x1b]11;?\x1b\\", b"\x1b]11;rgb:0000/0000/0000\x1b\\"),
(b"\x1b]10;?\x07", b"\x1b]10;rgb:ffff/ffff/ffff\x07"),
(b"\x1b]11;?\x07", b"\x1b]11;rgb:0000/0000/0000\x07"),
(b"\x1b[14t", b"\x1b[4;600;800t"),
(b"\x1b[16t", b"\x1b[6;16;8t"),
(b"\x1b[18t", b"\x1b[8;24;80t"),
(b"\x1b[?1016$p", b"\x1b[?1016;1$y"),
(b"\x1b[?2027$p", b"\x1b[?2027;1$y"),
(b"\x1b[?2031$p", b"\x1b[?2031;1$y"),
(b"\x1b[?1004$p", b"\x1b[?1004;1$y"),
(b"\x1b[?2004$p", b"\x1b[?2004;1$y"),
(b"\x1b[?2026$p", b"\x1b[?2026;1$y"),
]
@dataclass
class LiveClient:
name: str
proc: subprocess.Popen
master_fd: int
stop: threading.Event = field(default_factory=threading.Event)
thread: threading.Thread | None = None
total_bytes: int = 0
def _pump(self) -> None:
buffer = b""
while not self.stop.is_set():
try:
rlist, _, _ = select.select([self.master_fd], [], [], 0.1)
except (OSError, ValueError):
break
if not rlist:
if self.proc.poll() is not None:
break
continue
try:
chunk = os.read(self.master_fd, 65536)
except (BlockingIOError, OSError):
continue
if not chunk:
break
self.total_bytes += len(chunk)
buffer = (buffer + chunk)[-8192:]
changed = True
while changed:
changed = False
for query, response in _TERM_REPLIES:
if query in buffer:
try:
os.write(self.master_fd, response)
except OSError:
pass
buffer = buffer.replace(query, b"")
changed = True
def start_pump(self) -> None:
self.thread = threading.Thread(target=self._pump, daemon=True)
self.thread.start()
def alive(self) -> bool:
return self.proc.poll() is None
def send_keys(self, data: bytes) -> None:
os.write(self.master_fd, data)
def shutdown(self) -> None:
self.stop.set()
if self.thread:
self.thread.join(timeout=1.0)
try:
os.killpg(self.proc.pid, signal.SIGTERM)
except (ProcessLookupError, PermissionError):
pass
try:
self.proc.wait(timeout=2.0)
except Exception:
try:
os.killpg(self.proc.pid, signal.SIGKILL)
except ProcessLookupError:
pass
try:
os.close(self.master_fd)
except OSError:
pass
def launch_client(binary: str, env: dict, session_id: str, name: str) -> LiveClient:
master_fd, slave_fd = pty.openpty()
proc = subprocess.Popen(
[
binary,
"--no-update",
"--no-selfdev",
"--socket", env["JCODE_SOCKET"],
"--resume", session_id,
],
stdin=slave_fd, stdout=slave_fd, stderr=slave_fd,
env=env, preexec_fn=os.setsid,
)
os.close(slave_fd)
os.set_blocking(master_fd, False)
client = LiveClient(name=name, proc=proc, master_fd=master_fd)
client.start_pump()
return client
# ──────────────────────────────────────────────────────────────────────────────
# session seeding
# ──────────────────────────────────────────────────────────────────────────────
@dataclass
class SeededSession:
session_id: str
markers: dict[str, int] # marker -> expected occurrence count in truth
def now_iso() -> str:
return time.strftime("%Y-%m-%dT%H:%M:%S.000000000Z", time.gmtime())
def make_stored_message(role: str, blocks: list[dict],
display_role: str | None = None) -> dict:
msg = {
"id": f"message_{uuid.uuid4().hex}",
"role": role,
"content": blocks,
"timestamp": now_iso(),
}
if display_role:
msg["display_role"] = display_role
return msg
def seed_session(home: Path, working_dir: str, turns: int = 3) -> SeededSession:
"""Write a session transcript with assistant commentary + tool calls.
Every assistant text, tool_use input, and tool_result carries a unique
MARK_xxxx token so duplication can be counted unambiguously.
"""
sessions_dir = home / "sessions"
sessions_dir.mkdir(parents=True, exist_ok=True)
session_id = f"session_repro_{int(time.time()*1000)}_{uuid.uuid4().hex[:16]}"
markers: dict[str, int] = {}
def mark(tag: str) -> str:
token = f"MARK_{tag}"
markers[token] = 1
return token
messages: list[dict] = []
# opening user turn
messages.append(make_stored_message("user", [
{"type": "text", "text": f"{mark('USER_OPEN')} please run the repro tasks"}
]))
for i in range(turns):
a_text = mark(f"ASSIST_{i:02d}")
t_in = mark(f"TOOLIN_{i:02d}")
t_out = mark(f"TOOLOUT_{i:02d}")
tool_id = f"toolu_{uuid.uuid4().hex[:20]}"
# assistant commentary + a tool call (mirrors the issue's pattern)
messages.append(make_stored_message("assistant", [
{"type": "text", "text": f"{a_text} I'll inspect step {i}."},
{"type": "tool_use", "id": tool_id, "name": "bash",
"input": {"command": f"echo {t_in}", "intent": f"repro step {i}"}},
]))
# tool result comes back on the user turn
messages.append(make_stored_message("user", [
{"type": "tool_result", "tool_use_id": tool_id,
"content": f"{t_out} step {i} ok"},
]))
# final assistant summary
messages.append(make_stored_message("assistant", [
{"type": "text", "text": f"{mark('ASSIST_FINAL')} all steps complete"}
]))
session = {
"id": session_id,
"parent_id": None,
"title": "repro-314",
"created_at": now_iso(),
"updated_at": now_iso(),
"messages": messages,
"provider_key": "openai",
"model": "gpt-5.5",
"reasoning_effort": "low",
"is_canary": False,
"testing_build": None,
"working_dir": working_dir,
"short_name": "repro",
"status": "Active",
"last_pid": None,
"last_active_at": now_iso(),
"is_debug": True,
"saved": False,
"env_snapshots": [],
}
path = sessions_dir / f"{session_id}.json"
path.write_text(json.dumps(session, indent=2))
return SeededSession(session_id=session_id, markers=markers)
# ──────────────────────────────────────────────────────────────────────────────
# detection
# ──────────────────────────────────────────────────────────────────────────────
def count_markers_in_display(history_json: str, markers: dict[str, int]) -> dict[str, int]:
"""Count how many display_messages contain each marker."""
counts = {m: 0 for m in markers}
try:
msgs = json.loads(history_json)
except json.JSONDecodeError:
return counts
for m in msgs:
blob = json.dumps(m)
for marker in markers:
if marker in blob:
counts[marker] += 1
return counts
def count_markers_in_text(text: str, markers: dict[str, int]) -> dict[str, int]:
return {m: text.count(m) for m in markers}
@dataclass
class StageResult:
stage: str
client: str
display_count: int
duplicates: dict[str, int] # marker -> count, only where count > 1
error: str | None = None
def diff_stage(stage: str, client_name: str, history_json: str,
markers: dict[str, int]) -> StageResult:
try:
msgs = json.loads(history_json)
n = len(msgs)
except json.JSONDecodeError:
return StageResult(stage, client_name, 0, {}, error="non-JSON history")
counts = count_markers_in_display(history_json, markers)
dups = {m: c for m, c in counts.items() if c > markers[m]}
return StageResult(stage, client_name, n, dups)
# ──────────────────────────────────────────────────────────────────────────────
# orchestration
# ──────────────────────────────────────────────────────────────────────────────
def settle(debug_sock: Path, session_id: str, client_name_hint: str,
verbose: bool, attempts: int = 60) -> bool:
"""Poll until a client for this session answers client:history."""
for i in range(attempts):
try:
out = dbg_output(debug_sock, "client:history", session_id=session_id,
timeout=8.0)
if out.strip().startswith("["):
if verbose:
print(f" [{client_name_hint}] client:history ready after {i} polls")
return True
except Exception as e:
if verbose and i % 10 == 0:
print(f" [{client_name_hint}] waiting ({i}): {e}")
time.sleep(0.5)
return False
def read_client_history(debug_sock: Path, session_id: str) -> str:
return dbg_output(debug_sock, "client:history", session_id=session_id, timeout=10.0)
def run_self_test(debug_sock: Path, session_id: str, verbose: bool) -> bool:
"""Positive control: force a real duplicate into a live client and confirm
the detector flags it. Proves the instrument is not a no-op.
Injecting the same assistant marker twice produces two display_messages
(assistant role is never coalesced), so a working detector must report x2.
"""
marker = f"MARK_SELFTEST_{uuid.uuid4().hex[:8]}"
expected = {marker: 1}
for _ in range(2):
dbg_output(debug_sock, f"client:inject:assistant:{marker} injected duplicate",
session_id=session_id, timeout=10.0)
time.sleep(0.3)
hist = read_client_history(debug_sock, session_id)
r = diff_stage("self-test", "control", hist, expected)
if verbose:
print(f" self-test marker {marker}: display_count={r.display_count} "
f"duplicates={r.duplicates}")
# detector must have caught exactly the injected duplicate
return r.duplicates.get(marker, 0) == 2
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
default_bin = Path.home() / ".jcode" / "builds" / "current" / "jcode"
ap.add_argument("--binary", default=str(default_bin))
ap.add_argument("--turns", type=int, default=3, help="tool-call turns to seed")
ap.add_argument("--keep", action="store_true", help="keep temp home on exit")
ap.add_argument("--self-test", action=argparse.BooleanOptionalAction, default=True,
help="run positive-control: force a real duplicate and confirm "
"the detector flags it (default: on)")
ap.add_argument("-v", "--verbose", action="store_true")
args = ap.parse_args()
binary = str(Path(args.binary).resolve())
if not Path(binary).exists():
print(f"❌ binary not found: {binary}")
return 1
root = Path(tempfile.mkdtemp(prefix="jcode-repro314-"))
home = root / "home"
run = root / "run"
home.mkdir(parents=True, exist_ok=True)
run.mkdir(parents=True, exist_ok=True)
env = os.environ.copy()
env["JCODE_HOME"] = str(home)
env["JCODE_RUNTIME_DIR"] = str(run)
env["JCODE_SOCKET"] = str(run / "jcode.sock")
env["JCODE_NO_TELEMETRY"] = "1"
env["JCODE_DEBUG_CONTROL"] = "1"
env["JCODE_TEMP_SERVER"] = "1"
env["JCODE_SERVER_OWNER_PID"] = str(os.getpid())
debug_sock = run / "jcode-debug.sock"
print("╔══════════════════════════════════════════════════════════╗")
print("║ repro #314: live transcript duplicate detector ║")
print("╚══════════════════════════════════════════════════════════╝")
print(f" binary : {binary}")
print(f" home : {home}")
print(f" socket : {env['JCODE_SOCKET']}")
server = subprocess.Popen(
[binary, "serve", "--socket", env["JCODE_SOCKET"], "--debug-socket",
"--no-update", "--no-selfdev"],
env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
preexec_fn=os.setsid,
)
clients: list[LiveClient] = []
results: list[StageResult] = []
try:
wait_for_socket(Path(env["JCODE_SOCKET"]))
wait_for_socket(debug_sock)
print(" server : up")
seeded = seed_session(home, str(REPO_ROOT), turns=args.turns)
print(f" seeded : {seeded.session_id} ({len(seeded.markers)} markers, "
f"{args.turns} tool turns)")
# cross-check server truth (each marker should be present once)
try:
server_hist = dbg_output(debug_sock, "history",
session_id=seeded.session_id, timeout=10.0)
except Exception:
server_hist = ""
# ── stage 1: fresh resume ───────────────────────────────────────────
print("\n── stage 1: fresh-resume ─────────────────────────────────")
c1 = launch_client(binary, env, seeded.session_id, "client-A")
clients.append(c1)
if not settle(debug_sock, seeded.session_id, "client-A", args.verbose):
print(" ⚠ client-A never answered client:history")
else:
r = diff_stage("fresh-resume", "client-A",
read_client_history(debug_sock, seeded.session_id),
seeded.markers)
results.append(r)
report_stage(r, args.verbose)
# ── stage 2: second concurrent attach ───────────────────────────────
print("\n── stage 2: second-attach ────────────────────────────────")
c2 = launch_client(binary, env, seeded.session_id, "client-B")
clients.append(c2)
settle(debug_sock, seeded.session_id, "client-B", args.verbose)
time.sleep(1.0)
# both clients now attached to the same session; read whichever the
# server routes to (most-recent connection) plus re-read after a beat
for label in ("after-B-attach", "after-B-attach-2"):
try:
r = diff_stage(f"second-attach/{label}", "active",
read_client_history(debug_sock, seeded.session_id),
seeded.markers)
results.append(r)
report_stage(r, args.verbose)
except Exception as e:
print(f" ⚠ read failed: {e}")
time.sleep(0.8)
# ── stage 3: reload ─────────────────────────────────────────────────
print("\n── stage 3: reload ───────────────────────────────────────")
try:
dbg_output(debug_sock, "client:reload", session_id=seeded.session_id,
timeout=10.0)
print(" /reload triggered on active client")
except Exception as e:
print(f" ⚠ reload trigger failed: {e}")
time.sleep(2.0)
if settle(debug_sock, seeded.session_id, "post-reload", args.verbose):
r = diff_stage("post-reload", "active",
read_client_history(debug_sock, seeded.session_id),
seeded.markers)
results.append(r)
report_stage(r, args.verbose)
# ── stage 4: detach + reattach ──────────────────────────────────────
print("\n── stage 4: detach-reattach ──────────────────────────────")
c1.shutdown()
c2.shutdown()
time.sleep(1.0)
c3 = launch_client(binary, env, seeded.session_id, "client-C")
clients.append(c3)
if settle(debug_sock, seeded.session_id, "client-C", args.verbose):
for label in ("reattach", "reattach-2"):
r = diff_stage(f"detach-reattach/{label}", "client-C",
read_client_history(debug_sock, seeded.session_id),
seeded.markers)
results.append(r)
report_stage(r, args.verbose)
time.sleep(0.8)
# ── positive control: prove the detector can catch a real duplicate ──
self_test_ok: bool | None = None
if args.self_test:
print("\n── self-test: positive control ───────────────────────────")
try:
self_test_ok = run_self_test(debug_sock, seeded.session_id,
args.verbose)
except Exception as e:
print(f" ⚠ self-test failed to run: {e}")
self_test_ok = False
if self_test_ok:
print(" 🟢 detector caught the forced duplicate (instrument live)")
else:
print(" 🔴 detector MISSED a forced duplicate -> results untrustworthy")
# ── summary ─────────────────────────────────────────────────────────
print("\n" + "" * 60)
if args.self_test and not self_test_ok:
print(" ⛔ INSTRUMENT INVALID: positive control failed.")
print(" A clean run cannot be trusted because the detector did not")
print(" flag a known-injected duplicate. Investigate the harness.")
return 3
reproduced = [r for r in results if r.duplicates]
if reproduced:
print(" 🔴 DUPLICATION REPRODUCED")
for r in reproduced:
print(f" stage={r.stage} client={r.client} "
f"display_count={r.display_count}")
for marker, cnt in sorted(r.duplicates.items()):
print(f" {marker}: rendered x{cnt} (expected x1)")
return 2
else:
print(" 🟢 no duplication detected across all stages")
print(f" stages checked: {len(results)}")
if server_hist:
sc = count_markers_in_text(server_hist, seeded.markers)
bad = {m: c for m, c in sc.items() if c > 2}
print(f" server history marker check: "
f"{'clean' if not bad else bad}")
return 0
finally:
for c in clients:
c.shutdown()
try:
os.killpg(server.pid, signal.SIGTERM)
except ProcessLookupError:
pass
try:
server.wait(timeout=3)
except Exception:
try:
os.killpg(server.pid, signal.SIGKILL)
except ProcessLookupError:
pass
if args.keep:
print(f"\n (kept temp home: {root})")
else:
shutil.rmtree(root, ignore_errors=True)
def report_stage(r: StageResult, verbose: bool) -> None:
if r.error:
print(f" {r.stage:28s} {r.client:10s} ERROR: {r.error}")
return
status = "🔴 DUP" if r.duplicates else "🟢 ok "
print(f" {status} {r.stage:28s} {r.client:10s} display_messages={r.display_count}")
if r.duplicates:
for marker, cnt in sorted(r.duplicates.items()):
print(f"{marker} x{cnt}")
if __name__ == "__main__":
raise SystemExit(main())
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
set -euo pipefail
# Publishable Terminal-Bench 2.1 campaign for jcode + Opus 4.8 (native Anthropic
# API). Runs with effectively-uncapped agent execution time so no task is lost
# to an agent timeout, while keeping verifier/build timeouts at their default
# (deterministic grading). Captures full provenance for publication.
#
# Env knobs:
# JCODE_TB_JOBS_DIR output dir (default /tmp/jcode-tb21-pub)
# JCODE_TB_K attempts per task (default 1)
# JCODE_TB_NCONC concurrent containers (default 3)
# JCODE_TB_AGENT_MULT agent-timeout multiplier (default 1000 ~ uncapped)
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
REPO_ROOT=$(cd -- "$SCRIPT_DIR/.." && pwd)
JOBS_DIR=${JCODE_TB_JOBS_DIR:-/tmp/jcode-tb21-pub}
K=${JCODE_TB_K:-1}
NCONC=${JCODE_TB_NCONC:-3}
AGENT_MULT=${JCODE_TB_AGENT_MULT:-1000}
JOB_NAME=${JCODE_TB_JOB_NAME:-tb21-opus48-uncapped-k${K}}
TB_PATH=${JCODE_TB_PATH:-/tmp/terminal-bench-2.1}
MODEL=${JCODE_TB_MODEL:-anthropic-api/claude-opus-4-8}
mkdir -p "$JOBS_DIR"
# Provenance manifest.
{
echo "# jcode Terminal-Bench 2.1 publishable run"
echo "timestamp_utc: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "git_commit: $(git -C "$REPO_ROOT" rev-parse HEAD 2>/dev/null || echo unknown)"
echo "git_describe: $(git -C "$REPO_ROOT" describe --tags --always --dirty 2>/dev/null || echo unknown)"
echo "jcode_binary: ${JCODE_HARBOR_BINARY:-/tmp/jcode-compat-dist/jcode-linux-x86_64.bin}"
echo "jcode_version: $(/tmp/jcode-compat-dist/jcode-linux-x86_64.bin --no-update --no-selfdev version 2>/dev/null | head -1 || echo unknown)"
echo "harbor_version: $(harbor --version 2>/dev/null | head -1 || echo unknown)"
echo "dataset: terminal-bench/terminal-bench-2-1 (local: $TB_PATH)"
echo "n_tasks: $(ls "$TB_PATH" | wc -l)"
echo "model: $MODEL"
echo "reasoning_effort: ${JCODE_ANTHROPIC_REASONING_EFFORT:-high}"
echo "k_attempts: $K"
echo "n_concurrent: $NCONC"
echo "agent_timeout_multiplier: $AGENT_MULT"
echo "verifier_timeout: dataset default (unchanged)"
} > "$JOBS_DIR/RUN_MANIFEST.txt"
cat "$JOBS_DIR/RUN_MANIFEST.txt"
exec "$REPO_ROOT/scripts/run_terminal_bench_claude.sh" \
--path "$TB_PATH" \
--model "$MODEL" \
--n-concurrent "$NCONC" \
-k "$K" \
--agent-timeout-multiplier "$AGENT_MULT" \
--jobs-dir "$JOBS_DIR" \
--job-name "$JOB_NAME" \
--yes
+518
View File
@@ -0,0 +1,518 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import concurrent.futures
import datetime as dt
import hashlib
import json
import os
from pathlib import Path
import subprocess
import sys
from typing import Any
def repo_root() -> Path:
return Path(__file__).resolve().parent.parent
def run(cmd: list[str], *, env: dict[str, str] | None = None, cwd: Path | None = None) -> subprocess.CompletedProcess[str]:
return subprocess.run(cmd, check=True, text=True, cwd=cwd, env=env)
def capture(cmd: list[str], *, cwd: Path | None = None) -> str:
return subprocess.check_output(cmd, text=True, cwd=cwd).strip()
def sha256_file(path: Path) -> str:
h = hashlib.sha256()
with path.open("rb") as f:
for chunk in iter(lambda: f.read(1024 * 1024), b""):
h.update(chunk)
return h.hexdigest()
def resolve_existing_file(candidates: list[str | None]) -> Path | None:
for raw in candidates:
if not raw:
continue
p = Path(raw).expanduser()
if p.exists() and p.is_file():
return p.resolve()
return None
def load_tasks(args: argparse.Namespace) -> list[str]:
tasks: list[str] = list(args.task)
if args.tasks_file:
for line in Path(args.tasks_file).read_text().splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
tasks.append(line)
deduped: list[str] = []
seen: set[str] = set()
for task in tasks:
if task not in seen:
seen.add(task)
deduped.append(task)
if not deduped:
raise SystemExit("No tasks provided. Use --task and/or --tasks-file.")
return deduped
def ensure_binary(root: Path, env: dict[str, str]) -> Path:
binary_dir = Path(env.get("JCODE_HARBOR_BINARY_DIR", "/tmp/jcode-compat-dist")).expanduser()
binary_path = Path(env.get("JCODE_HARBOR_BINARY", str(binary_dir / "jcode-linux-x86_64"))).expanduser()
if not (binary_path.exists() and os.access(binary_path, os.X_OK)):
run([str(root / "scripts" / "build_linux_compat.sh"), str(binary_dir)], env=env, cwd=root)
return binary_path.resolve()
def current_settings(root: Path, args: argparse.Namespace) -> dict[str, Any]:
env = os.environ.copy()
binary_path = ensure_binary(root, env)
openai_auth = resolve_existing_file([
env.get("JCODE_HARBOR_OPENAI_AUTH"),
"~/.jcode/openai-auth.json",
])
if openai_auth is None:
raise SystemExit("OpenAI OAuth file not found. Set JCODE_HARBOR_OPENAI_AUTH or log in first.")
settings: dict[str, Any] = {
"schema_version": 1,
"created_at": dt.datetime.now(dt.UTC).isoformat(),
"repo_root": str(root),
"git_head": capture(["git", "rev-parse", "HEAD"], cwd=root),
"runner_script": str((root / "scripts" / "run_terminal_bench_harbor.sh").resolve()),
"model": args.model,
"reasoning_effort": os.environ.get("JCODE_OPENAI_REASONING_EFFORT", "high"),
"service_tier": os.environ.get("JCODE_OPENAI_SERVICE_TIER", "priority"),
"binary_path": str(binary_path),
"binary_sha256": sha256_file(binary_path),
"openai_auth_path": str(openai_auth),
"dataset": args.dataset,
"path": str(Path(args.path).resolve()) if args.path else None,
"attempts_per_task": args.n_attempts,
"n_concurrent": 1,
"timeout_multiplier": args.timeout_multiplier,
}
return settings
PINNED_KEYS = [
"runner_script",
"model",
"reasoning_effort",
"service_tier",
"binary_path",
"binary_sha256",
"openai_auth_path",
"dataset",
"path",
"attempts_per_task",
"n_concurrent",
"timeout_multiplier",
]
def ensure_manifest(campaign_dir: Path, settings: dict[str, Any]) -> dict[str, Any]:
manifest_path = campaign_dir / "campaign.json"
if manifest_path.exists():
manifest = json.loads(manifest_path.read_text())
mismatches: list[str] = []
for key in PINNED_KEYS:
if manifest.get(key) != settings.get(key):
mismatches.append(f"{key}: existing={manifest.get(key)!r} current={settings.get(key)!r}")
if mismatches:
raise SystemExit(
"Campaign settings drift detected. Refusing to mix incompatible runs in one campaign:\n- "
+ "\n- ".join(mismatches)
)
return manifest
manifest = dict(settings)
manifest["tasks_run"] = []
manifest["notes"] = [
"This campaign is intended to preserve coherent sequential Harbor runs for later leaderboard assembly."
]
manifest_path.write_text(json.dumps(manifest, indent=2) + "\n")
return manifest
def load_manifest(campaign_dir: Path) -> dict[str, Any]:
return json.loads((campaign_dir / "campaign.json").read_text())
def write_results_jsonl(campaign_dir: Path, records: list[dict[str, Any]]) -> None:
results_jsonl = campaign_dir / "results.jsonl"
with results_jsonl.open("w", encoding="utf-8") as f:
for record in records:
f.write(json.dumps(record) + "\n")
def append_result(campaign_dir: Path, record: dict[str, Any]) -> None:
manifest_path = campaign_dir / "campaign.json"
manifest = json.loads(manifest_path.read_text())
existing = manifest.setdefault("tasks_run", [])
replaced = False
for idx, item in enumerate(existing):
if item.get("task_name") == record.get("task_name") and item.get("job_name") == record.get("job_name"):
if item == record:
return
existing[idx] = record
replaced = True
break
if not replaced:
existing.append(record)
manifest_path.write_text(json.dumps(manifest, indent=2) + "\n")
write_results_jsonl(campaign_dir, existing)
def collect_trial_results(job_dir: Path) -> list[dict[str, Any]]:
trial_results: list[dict[str, Any]] = []
for result_path in sorted(job_dir.glob("*__*/result.json")):
payload = json.loads(result_path.read_text())
verifier_result = payload.get("verifier_result") or {}
rewards = verifier_result.get("rewards") or {}
exception_info = payload.get("exception_info") or {}
agent_result = payload.get("agent_result") or {}
metadata = agent_result.get("metadata") or {}
trial_results.append(
{
"task_name": payload["task_name"],
"trial_name": payload["trial_name"],
"reward": rewards.get("reward"),
"exception_type": exception_info.get("exception_type"),
"exception_message": exception_info.get("exception_message"),
"agent_return_code": metadata.get("return_code"),
"started_at": payload.get("started_at"),
"finished_at": payload.get("finished_at"),
"result_path": str(result_path),
}
)
return trial_results
def summarize_job(job_result_path: Path, trial_results: list[dict[str, Any]]) -> dict[str, Any]:
payload = json.loads(job_result_path.read_text())
rewards = [trial.get("reward") for trial in trial_results]
numeric_rewards = [r for r in rewards if isinstance(r, (int, float))]
return {
"job_result_path": str(job_result_path),
"n_total_trials": payload.get("n_total_trials"),
"job_started_at": payload.get("started_at"),
"job_finished_at": payload.get("finished_at"),
"trial_names": [trial["trial_name"] for trial in trial_results],
"rewards": rewards,
"mean_reward": (sum(numeric_rewards) / len(numeric_rewards)) if numeric_rewards else None,
"trial_results": trial_results,
}
def has_strict_numeric_trials(record: dict[str, Any], required: int) -> bool:
trial_results = record.get("trial_results") or []
numeric_rewards = [
trial.get("reward")
for trial in trial_results
if isinstance(trial.get("reward"), (int, float))
]
return len(numeric_rewards) >= required
def completed_recorded_jobs(campaign_dir: Path) -> dict[str, dict[str, Any]]:
manifest = load_manifest(campaign_dir)
required = int(manifest.get("attempts_per_task") or 1)
out: dict[str, dict[str, Any]] = {}
for item in manifest.get("tasks_run", []):
mean_reward = item.get("mean_reward")
if (
item.get("status") == "completed"
and item.get("task_name")
and isinstance(mean_reward, (int, float))
and has_strict_numeric_trials(item, required)
):
out[item["task_name"]] = item
return out
def adopt_existing_job(campaign_dir: Path, task: str, task_jobs_dir: Path, required_attempts: int) -> dict[str, Any] | None:
for job_dir in sorted([p for p in task_jobs_dir.iterdir() if p.is_dir()], reverse=True):
job_result_path = job_dir / "result.json"
if not job_result_path.exists():
continue
trial_results = collect_trial_results(job_dir)
if not trial_results:
continue
numeric_rewards = [t.get("reward") for t in trial_results if isinstance(t.get("reward"), (int, float))]
if len(numeric_rewards) < required_attempts:
continue
record = {
"task_name": task,
"job_name": job_dir.name,
"jobs_dir": str(task_jobs_dir),
"status": "completed",
**summarize_job(job_result_path, trial_results),
}
append_result(campaign_dir, record)
return record
return None
def build_task_command(
*,
runner: Path,
task: str,
task_jobs_dir: Path,
job_name: str,
args: argparse.Namespace,
pass_through_args: list[str],
) -> list[str]:
cmd = [
str(runner),
"--include-task-name", task,
"--n-tasks", "1",
"--n-concurrent", "1",
"--jobs-dir", str(task_jobs_dir),
"--job-name", job_name,
"--yes",
"--timeout-multiplier", str(args.timeout_multiplier),
"-k", str(args.n_attempts),
]
if args.path:
cmd.extend(["--path", str(Path(args.path).resolve())])
else:
cmd.extend(["--dataset", args.dataset])
if args.model:
cmd.extend(["--model", args.model])
cmd.extend(pass_through_args)
return cmd
def execute_task_process(
*,
runner: Path,
task: str,
task_jobs_dir: Path,
job_name: str,
args: argparse.Namespace,
pass_through_args: list[str],
) -> tuple[str, str, Path, int]:
cmd = build_task_command(
runner=runner,
task=task,
task_jobs_dir=task_jobs_dir,
job_name=job_name,
args=args,
pass_through_args=pass_through_args,
)
print(f"\n=== Running task {task} as {job_name} ===", flush=True)
env = os.environ.copy()
env["JCODE_HARBOR_CURRENT_TASK"] = task
proc = subprocess.run(cmd, text=True, env=env)
return task, job_name, task_jobs_dir, proc.returncode
def finalize_task_result(
*,
campaign_dir: Path,
task: str,
job_name: str,
task_jobs_dir: Path,
process_return_code: int,
continue_on_failure: bool,
required_attempts: int,
) -> tuple[bool, dict[str, Any]]:
job_result_path = task_jobs_dir / job_name / "result.json"
trial_results = collect_trial_results(task_jobs_dir / job_name)
if job_result_path.exists() and trial_results:
numeric_rewards = [
trial.get("reward")
for trial in trial_results
if isinstance(trial.get("reward"), (int, float))
]
task_result = {
"task_name": task,
"job_name": job_name,
"jobs_dir": str(task_jobs_dir),
"status": "completed",
"process_return_code": process_return_code,
**summarize_job(job_result_path, trial_results),
}
if isinstance(task_result.get("mean_reward"), (int, float)) and len(numeric_rewards) >= required_attempts:
append_result(campaign_dir, task_result)
print(
f"Completed {task}: mean_reward={task_result['mean_reward']} trials={len(trial_results)}",
flush=True,
)
return True, task_result
task_result["status"] = (
"completed_with_partial_numeric_reward"
if numeric_rewards
else "completed_without_numeric_reward"
)
append_result(campaign_dir, task_result)
if continue_on_failure:
print(
f"Task {task} produced {len(numeric_rewards)}/{required_attempts} numeric trial rewards; continuing.",
file=sys.stderr,
)
return False, task_result
return False, task_result
if process_return_code != 0 or not job_result_path.exists():
record = {
"task_name": task,
"job_name": job_name,
"status": "failed_to_produce_result",
"return_code": process_return_code,
"jobs_dir": str(task_jobs_dir),
}
append_result(campaign_dir, record)
if continue_on_failure:
print(f"Task {task} failed, continuing because --continue-on-failure is set.", file=sys.stderr)
return False, record
if not trial_results:
record = {
"task_name": task,
"job_name": job_name,
"status": "missing_trial_results",
"return_code": process_return_code,
"job_result_path": str(job_result_path),
"jobs_dir": str(task_jobs_dir),
}
append_result(campaign_dir, record)
if continue_on_failure:
print(f"Task {task} produced no per-trial results, continuing.", file=sys.stderr)
return False, record
raise AssertionError("unreachable")
def prepare_task(campaign_dir: Path, jobs_root: Path, task: str, required_attempts: int) -> tuple[str, Path] | None:
recorded = completed_recorded_jobs(campaign_dir)
if task in recorded:
print(f"\n=== Skipping task {task}; already recorded as {recorded[task]['job_name']} ===", flush=True)
return None
task_jobs_dir = jobs_root / task
task_jobs_dir.mkdir(parents=True, exist_ok=True)
adopted = adopt_existing_job(campaign_dir, task, task_jobs_dir, required_attempts)
if adopted is not None:
print(
f"\n=== Adopted existing job for {task}: {adopted['job_name']} mean_reward={adopted['mean_reward']} ===",
flush=True,
)
return None
return task, task_jobs_dir
def main() -> int:
parser = argparse.ArgumentParser(description="Run a sequential Terminal-Bench campaign for jcode and preserve stitchable artifacts.")
parser.add_argument("--campaign-dir", required=True, help="Persistent output directory for the campaign")
parser.add_argument("--task", action="append", default=[], help="Task name to run. Can be passed multiple times.")
parser.add_argument("--tasks-file", help="File with one task name per line")
parser.add_argument("--dataset", default="terminal-bench@2.0", help="Harbor dataset name to use")
parser.add_argument("--path", help="Local task/dataset path to use instead of --dataset")
parser.add_argument("--model", default="openai/gpt-5.4", help="Harbor model string")
parser.add_argument("-k", "--n-attempts", type=int, default=1, help="Attempts per task")
parser.add_argument("--timeout-multiplier", type=float, default=1.0)
parser.add_argument("--continue-on-failure", action="store_true", help="Continue to the next task if one task fails")
parser.add_argument("--max-parallel-tasks", type=int, default=1, help="Maximum number of separate task jobs to run at once")
parser.add_argument("harbor_args", nargs=argparse.REMAINDER, help="Extra args passed through after '--'")
args = parser.parse_args()
root = repo_root()
campaign_dir = Path(args.campaign_dir).expanduser().resolve()
campaign_dir.mkdir(parents=True, exist_ok=True)
jobs_root = campaign_dir / "harbor-jobs"
jobs_root.mkdir(parents=True, exist_ok=True)
tasks = load_tasks(args)
settings = current_settings(root, args)
ensure_manifest(campaign_dir, settings)
pass_through_args = list(args.harbor_args)
if pass_through_args and pass_through_args[0] == "--":
pass_through_args = pass_through_args[1:]
runner = root / "scripts" / "run_terminal_bench_harbor.sh"
pending: list[tuple[str, Path, str]] = []
for task in tasks:
prepared = prepare_task(campaign_dir, jobs_root, task, args.n_attempts)
if prepared is None:
continue
task_name, task_jobs_dir = prepared
existing_runs = [p for p in task_jobs_dir.iterdir() if p.is_dir()]
run_index = len(existing_runs) + 1
job_name = f"run-{run_index:03d}"
pending.append((task_name, task_jobs_dir, job_name))
if not pending:
return 0
max_workers = max(1, args.max_parallel_tasks)
if max_workers == 1:
for task, task_jobs_dir, job_name in pending:
_, _, _, return_code = execute_task_process(
runner=runner,
task=task,
task_jobs_dir=task_jobs_dir,
job_name=job_name,
args=args,
pass_through_args=pass_through_args,
)
ok, _record = finalize_task_result(
campaign_dir=campaign_dir,
task=task,
job_name=job_name,
task_jobs_dir=task_jobs_dir,
process_return_code=return_code,
continue_on_failure=args.continue_on_failure,
required_attempts=args.n_attempts,
)
if not ok and not args.continue_on_failure:
return return_code or 1
return 0
had_failure = False
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
future_map = {
executor.submit(
execute_task_process,
runner=runner,
task=task,
task_jobs_dir=task_jobs_dir,
job_name=job_name,
args=args,
pass_through_args=pass_through_args,
): (task, task_jobs_dir, job_name)
for task, task_jobs_dir, job_name in pending
}
for future in concurrent.futures.as_completed(future_map):
task, task_jobs_dir, job_name = future_map[future]
_task, _job_name, _task_jobs_dir, return_code = future.result()
ok, _record = finalize_task_result(
campaign_dir=campaign_dir,
task=task,
job_name=job_name,
task_jobs_dir=task_jobs_dir,
process_return_code=return_code,
continue_on_failure=args.continue_on_failure,
required_attempts=args.n_attempts,
)
if not ok:
had_failure = True
return 1 if had_failure and not args.continue_on_failure else 0
if __name__ == "__main__":
raise SystemExit(main())
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env bash
set -euo pipefail
# Run Terminal-Bench through Harbor with jcode using Opus 4.8.
# Default route is OpenRouter (anthropic/claude-opus-4.8) since native Claude
# OAuth may be unavailable. Override with JCODE_TB_MODEL / env vars.
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
REPO_ROOT=$(cd -- "$SCRIPT_DIR/.." && pwd)
DEFAULT_BINARY_DIR=${JCODE_HARBOR_BINARY_DIR:-/tmp/jcode-compat-dist}
DEFAULT_BINARY_PATH=${JCODE_HARBOR_BINARY:-$DEFAULT_BINARY_DIR/jcode-linux-x86_64.bin}
DEFAULT_MODEL=${JCODE_TB_MODEL:-anthropic-api/claude-opus-4-8}
DEFAULT_PATH=${JCODE_TB_PATH:-/tmp/terminal-bench-2.1}
have_model=0
have_agent_import=0
have_task_source=0
for arg in "$@"; do
case "$arg" in
--model|-m)
have_model=1
;;
--agent-import-path)
have_agent_import=1
;;
--path|-p|--dataset|-d|--task|-t)
have_task_source=1
;;
esac
done
if [[ ! -x "$DEFAULT_BINARY_PATH" ]]; then
echo "Building Linux-compatible jcode binary into $DEFAULT_BINARY_DIR" >&2
"$REPO_ROOT/scripts/build_linux_compat.sh" "$DEFAULT_BINARY_DIR"
fi
# Resolve provider keys from jcode's env files if not already set.
if [[ -z "${OPENROUTER_API_KEY:-}" ]]; then
OR_ENV=${JCODE_HARBOR_OPENROUTER_ENV:-$HOME/.config/jcode/openrouter.env}
if [[ -f "$OR_ENV" ]]; then
export JCODE_HARBOR_OPENROUTER_ENV="$OR_ENV"
fi
fi
if [[ -z "${ANTHROPIC_API_KEY:-}" ]]; then
ANT_ENV=${JCODE_HARBOR_ANTHROPIC_ENV:-$HOME/.config/jcode/anthropic.env}
if [[ -f "$ANT_ENV" ]]; then
export JCODE_HARBOR_ANTHROPIC_ENV="$ANT_ENV"
fi
fi
export PYTHONPATH="$REPO_ROOT/scripts${PYTHONPATH:+:$PYTHONPATH}"
export JCODE_HARBOR_BINARY="$DEFAULT_BINARY_PATH"
export JCODE_ANTHROPIC_REASONING_EFFORT=${JCODE_ANTHROPIC_REASONING_EFFORT:-high}
export JCODE_NO_TELEMETRY=${JCODE_NO_TELEMETRY:-1}
HARBOR_BIN=${JCODE_HARBOR_BIN:-harbor}
cmd=($HARBOR_BIN run)
if [[ $have_task_source -eq 0 ]]; then
cmd+=(--path "$DEFAULT_PATH")
fi
if [[ $have_agent_import -eq 0 ]]; then
cmd+=(--agent-import-path jcode_harbor_claude_agent:JcodeClaudeHarborAgent)
fi
if [[ $have_model -eq 0 ]]; then
cmd+=(--model "$DEFAULT_MODEL")
fi
cmd+=("$@")
{
echo "Running Harbor with jcode Opus 4.8 adapter"
echo " binary: $JCODE_HARBOR_BINARY"
echo " model: ${DEFAULT_MODEL}"
} >&2
exec "${cmd[@]}"
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
REPO_ROOT=$(cd -- "$SCRIPT_DIR/.." && pwd)
DEFAULT_BINARY_DIR=${JCODE_HARBOR_BINARY_DIR:-/tmp/jcode-compat-dist}
DEFAULT_BINARY_PATH=${JCODE_HARBOR_BINARY:-$DEFAULT_BINARY_DIR/jcode-linux-x86_64}
DEFAULT_MODEL=${JCODE_TB_MODEL:-openai/gpt-5.4}
DEFAULT_PATH=${JCODE_TB_PATH:-/tmp/terminal-bench-2}
have_model=0
have_agent_import=0
have_task_source=0
for arg in "$@"; do
case "$arg" in
--model|-m)
have_model=1
;;
--agent-import-path)
have_agent_import=1
;;
--path|-p|--dataset|-d|--task|-t)
have_task_source=1
;;
esac
done
if [[ ! -x "$DEFAULT_BINARY_PATH" ]]; then
echo "Building Linux-compatible jcode binary into $DEFAULT_BINARY_DIR" >&2
"$REPO_ROOT/scripts/build_linux_compat.sh" "$DEFAULT_BINARY_DIR"
fi
OPENAI_AUTH=${JCODE_HARBOR_OPENAI_AUTH:-$HOME/.jcode/openai-auth.json}
if [[ ! -f "$OPENAI_AUTH" ]]; then
echo "OpenAI OAuth file not found at $OPENAI_AUTH" >&2
exit 1
fi
export PYTHONPATH="$REPO_ROOT/scripts${PYTHONPATH:+:$PYTHONPATH}"
export JCODE_HARBOR_BINARY="$DEFAULT_BINARY_PATH"
export JCODE_HARBOR_OPENAI_AUTH="$OPENAI_AUTH"
export JCODE_OPENAI_REASONING_EFFORT=${JCODE_OPENAI_REASONING_EFFORT:-high}
export JCODE_OPENAI_SERVICE_TIER=${JCODE_OPENAI_SERVICE_TIER:-priority}
export JCODE_NO_TELEMETRY=${JCODE_NO_TELEMETRY:-1}
HARBOR_BIN=${JCODE_HARBOR_BIN:-}
if [[ -z "$HARBOR_BIN" ]]; then
CACHED_HARBOR="$HOME/.cache/uv/archive-v0/qtLT-I4hA5Q9ne5Zq-5cn/bin/harbor"
if [[ -x "$CACHED_HARBOR" ]]; then
HARBOR_BIN="$CACHED_HARBOR"
else
HARBOR_BIN="uvx --offline harbor"
fi
fi
cmd=($HARBOR_BIN run)
if [[ $have_task_source -eq 0 ]]; then
cmd+=(--path "$DEFAULT_PATH")
fi
if [[ $have_agent_import -eq 0 ]]; then
cmd+=(--agent-import-path jcode_harbor_agent:JcodeHarborAgent)
fi
if [[ $have_model -eq 0 ]]; then
cmd+=(--model "$DEFAULT_MODEL")
fi
cmd+=("$@")
{
echo "Running Harbor with jcode adapter"
echo " binary: $JCODE_HARBOR_BINARY"
echo " auth: $JCODE_HARBOR_OPENAI_AUTH"
echo " model: ${DEFAULT_MODEL}"
} >&2
exec "${cmd[@]}"
+88
View File
@@ -0,0 +1,88 @@
#!/bin/bash
# Screenshot Watcher - monitors for jcode screenshot signals
#
# This script watches the signal directory and captures screenshots
# when jcode signals that a specific UI state is ready.
#
# Usage: ./screenshot_watcher.sh [window_id]
#
# Start jcode with: /screenshot-mode on
set -e
SIGNAL_DIR="${XDG_RUNTIME_DIR:-/tmp}/jcode-screenshots"
OUTPUT_DIR="$(dirname "$0")/../docs/screenshots"
WINDOW_ID="${1:-}"
mkdir -p "$OUTPUT_DIR" "$SIGNAL_DIR"
# Get window ID if not provided
if [ -z "$WINDOW_ID" ]; then
echo "Tip: Pass window ID as argument, or we'll use focused window for each capture"
fi
echo "🎬 Screenshot Watcher"
echo " Signal dir: $SIGNAL_DIR"
echo " Output dir: $OUTPUT_DIR"
echo " Window ID: ${WINDOW_ID:-<focused>}"
echo ""
echo "Waiting for signals... (Ctrl+C to stop)"
echo "Enable in jcode with: /screenshot-mode on"
echo ""
capture_signal() {
local file="$1"
local state_name="${file%.ready}"
local signal_path="$SIGNAL_DIR/$file"
local output_path="$OUTPUT_DIR/${state_name}.png"
echo "📸 Signal: $state_name"
# Read metadata from signal file
if [ -f "$signal_path" ]; then
cat "$signal_path" | jq . 2>/dev/null || true
fi
# Small delay to ensure UI is fully rendered
sleep 0.15
# Focus window if ID provided, otherwise use current focus
if [ -n "$WINDOW_ID" ]; then
niri msg action focus-window --id "$WINDOW_ID"
sleep 0.1
fi
# Capture screenshot
niri msg action screenshot-window --path "$output_path"
# Clear the signal
rm -f "$signal_path"
echo " ✅ Saved: $output_path"
echo ""
}
# Try inotifywait first, fall back to polling
if command -v inotifywait &>/dev/null; then
echo "Using inotifywait for efficient watching..."
inotifywait -m -e create -e modify "$SIGNAL_DIR" 2>/dev/null | while read -r dir event file; do
if [[ "$file" == *.ready ]]; then
capture_signal "$file"
fi
done
else
echo "Using polling (install inotify-tools for better performance)..."
SEEN_FILES=""
shopt -s nullglob
while true; do
for file in "$SIGNAL_DIR"/*.ready; do
[ -e "$file" ] || continue
basename_file=$(basename "$file")
if [[ ! " $SEEN_FILES " =~ " $basename_file " ]]; then
capture_signal "$basename_file"
SEEN_FILES="$SEEN_FILES $basename_file"
fi
done
sleep 0.2
done
fi
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
strict=0
usage() {
cat <<'USAGE'
Usage:
scripts/security_preflight.sh [--strict]
Checks:
1) Secret-pattern scan in tracked source/docs/scripts
2) World-writable file check under scripts/
3) Rust dependency advisory scan via cargo-audit (when available)
Options:
--strict Fail if cargo-audit is not installed
USAGE
}
die() {
printf 'error: %s\n' "$*" >&2
exit 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
--strict)
strict=1
;;
-h|--help)
usage
exit 0
;;
*)
die "unknown option: $1"
;;
esac
shift
done
cd "$repo_root"
echo "=== Security Preflight ==="
echo "[1/3] Scanning for likely secrets"
secret_regex='(AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9]{36,}|xox[baprs]-[A-Za-z0-9-]{10,}|-----BEGIN (RSA|OPENSSH|EC|DSA|PGP) PRIVATE KEY-----|AIza[0-9A-Za-z_-]{35})'
set +e
mapfile -d '' tracked_files < <(git ls-files -z)
scan_status=1
if [[ "${#tracked_files[@]}" -gt 0 ]]; then
if command -v rg >/dev/null 2>&1; then
rg -n --color=never -e "$secret_regex" \
--glob '!Cargo.lock' --glob '!*.snap' --glob '!*.png' --glob '!*.jpg' --glob '!*.jpeg' \
--glob '!*.gif' --glob '!*.svg' --glob '!*.pdf' --glob '!*.woff' --glob '!*.woff2' --glob '!*.ttf' \
"${tracked_files[@]}" > /tmp/jcode-secret-scan.txt
scan_status=$?
else
scan_files=()
for tracked_file in "${tracked_files[@]}"; do
case "$tracked_file" in
Cargo.lock|*.snap|*.png|*.jpg|*.jpeg|*.gif|*.svg|*.pdf|*.woff|*.woff2|*.ttf)
;;
*)
scan_files+=("$tracked_file")
;;
esac
done
if [[ "${#scan_files[@]}" -gt 0 ]]; then
grep -I -n -E "$secret_regex" "${scan_files[@]}" > /tmp/jcode-secret-scan.txt
scan_status=$?
fi
fi
fi
set -e
if [[ "$scan_status" -gt 1 ]]; then
rm -f /tmp/jcode-secret-scan.txt
die "secret scan failed to execute"
fi
if [[ -s /tmp/jcode-secret-scan.txt ]]; then
cat /tmp/jcode-secret-scan.txt
rm -f /tmp/jcode-secret-scan.txt
die "potential secret material detected"
fi
rm -f /tmp/jcode-secret-scan.txt
echo "[2/3] Checking script permissions"
if find scripts -type f -perm -0002 -print -quit | grep -q .; then
find scripts -type f -perm -0002 -print
die "world-writable files detected under scripts/"
fi
echo "[3/3] Dependency advisories (cargo-audit)"
audit_ignores=(
# Documented in docs/SECURITY_DEPENDENCIES.md. These are transitive
# advisories with tracked remediation paths; keep them visible in the triage
# doc while preventing unrelated CI/release work from being blocked.
--ignore RUSTSEC-2026-0141 # lettre via notify-email, Boring TLS backend not used by jcode
--ignore RUSTSEC-2026-0099 # rustls-webpki via rustls stack, awaiting upstream upgrade
--ignore RUSTSEC-2026-0104 # rustls-webpki via rustls stack, awaiting upstream upgrade
--ignore RUSTSEC-2026-0098 # rustls-webpki via rustls stack, awaiting upstream upgrade
--ignore RUSTSEC-2026-0049 # rustls-webpki via rustls stack, awaiting upstream upgrade
--ignore RUSTSEC-2026-0187 # lopdf via pdf-extract 0.8.2 (pins lopdf 0.34); PDF text extraction only, awaiting pdf-extract upgrade to lopdf >=0.42
--ignore RUSTSEC-2026-0194 # quick-xml via wayland-scanner (proc-macro); parses trusted Wayland protocol XML at build time only, never untrusted input at runtime
--ignore RUSTSEC-2026-0195 # quick-xml via wayland-scanner (proc-macro); same build-time-only exposure as RUSTSEC-2026-0194
)
if command -v cargo-audit >/dev/null 2>&1; then
cargo audit "${audit_ignores[@]}"
elif cargo audit --version >/dev/null 2>&1; then
cargo audit "${audit_ignores[@]}"
else
if [[ "$strict" -eq 1 ]]; then
die "cargo-audit is not installed (install with: cargo install cargo-audit --locked)"
fi
echo "warning: cargo-audit not installed; skipping advisory check"
fi
echo "=== Security preflight passed ==="
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env bash
# Live end-to-end sandbox for the "current client, stale older server" fix.
#
# Server: the REAL released v0.14.6 binary (downloaded from GitHub).
# Client: the freshly built current binary (target/debug/jcode, has the fix).
# Field state: shared-server channel pinned to OLD (v0.14.6); stable -> NEW.
#
# It starts the real old daemon, then runs the NEW client's `jcode server reload`
# (which repairs the stale shared-server channel, then forces a reload). PASS iff
# the resulting daemon is running v0.22.x.
#
# Usage:
# cargo build -p jcode --bin jcode
# scripts/stale_server_upgrade_sandbox.sh
#
# Linux x86_64 only (uses the published jcode-linux-x86_64 release asset).
set -uo pipefail
REPO_ROOT="$(cd -- "$(dirname -- "$0")/.." && pwd)"
NEW_BIN="${NEW_BIN:-$REPO_ROOT/target/debug/jcode}"
OLD_VERSION="${OLD_VERSION:-v0.14.6}"
OLD_DIR="${OLD_DIR:-/tmp/jcode-sandbox}"
OLD_WRAP="$OLD_DIR/jcode-linux-x86_64"
[ -x "$NEW_BIN" ] || { echo "missing new client binary: $NEW_BIN (run: cargo build -p jcode --bin jcode)"; exit 2; }
# Fetch + extract the real old release binary if it is not already present.
if [ ! -x "$OLD_WRAP" ]; then
mkdir -p "$OLD_DIR"
url="$(curl -fsSL "https://api.github.com/repos/1jehuang/jcode/releases/tags/$OLD_VERSION" \
| grep -o 'https://[^"]*jcode-linux-x86_64.tar.gz' | head -1)"
[ -n "$url" ] || { echo "could not resolve $OLD_VERSION linux asset URL"; exit 2; }
echo "Downloading old server $OLD_VERSION ..."
curl -fsSL "$url" -o "$OLD_DIR/old.tar.gz"
tar -C "$OLD_DIR" -xzf "$OLD_DIR/old.tar.gz"
fi
[ -x "$OLD_WRAP" ] || { echo "missing old binary $OLD_WRAP after download"; exit 2; }
SANDBOX="$(mktemp -d /tmp/jcode-stale-sandbox.XXXXXX)"
export JCODE_HOME="$SANDBOX/home"
export JCODE_RUNTIME_DIR="$SANDBOX/runtime"
# Hard isolation: pin the socket explicitly so we can NEVER touch the real
# global daemon at /run/user/<uid>/jcode.sock.
export JCODE_SOCKET="$SANDBOX/runtime/jcode.sock"
# Make the new client's clean release version comparable (debug build is dirty).
export JCODE_TEST_CLIENT_VERSION_OVERRIDE="v0.22.0 (sandbox)"
mkdir -p "$JCODE_HOME" "$JCODE_RUNTIME_DIR"
BUILDS="$JCODE_HOME/builds"
mkdir -p "$BUILDS/versions/0.14.6" "$BUILDS/versions/0.22.0" \
"$BUILDS/shared-server" "$BUILDS/stable" "$BUILDS/current"
log() { printf '\n=== %s ===\n' "$*"; }
# --- Install the OLD binary (with bundled libs) as version 0.14.6 ----------
cp "$OLD_DIR/jcode-linux-x86_64.bin" "$OLD_DIR/libssl.so.10" \
"$OLD_DIR/libcrypto.so.10" "$BUILDS/versions/0.14.6/"
cat > "$BUILDS/versions/0.14.6/jcode" <<'WRAP'
#!/usr/bin/env sh
set -eu
real=$0
if command -v readlink >/dev/null 2>&1; then
resolved=$(readlink -f -- "$0" 2>/dev/null || true)
[ -n "$resolved" ] && real=$resolved
fi
self_dir=$(CDPATH= cd -- "$(dirname -- "$real")" && pwd)
export LD_LIBRARY_PATH="$self_dir:${LD_LIBRARY_PATH:-}"
exec "$self_dir/jcode-linux-x86_64.bin" "$@"
WRAP
chmod +x "$BUILDS/versions/0.14.6/jcode"
# --- Install the NEW binary as version 0.22.0 (newer mtime) ----------------
cp "$NEW_BIN" "$BUILDS/versions/0.22.0/jcode"
touch -d "+1 minute" "$BUILDS/versions/0.22.0/jcode"
# --- Field state: shared-server -> OLD, stable/current -> NEW --------------
ln -sf "../versions/0.14.6/jcode" "$BUILDS/shared-server/jcode"
echo "0.14.6" > "$BUILDS/shared-server-version"
ln -sf "../versions/0.22.0/jcode" "$BUILDS/stable/jcode"
echo "0.22.0" > "$BUILDS/stable-version"
ln -sf "../versions/0.22.0/jcode" "$BUILDS/current/jcode"
echo "0.22.0" > "$BUILDS/current-version"
log "Initial channel state (the field bug: shared-server pinned to OLD)"
echo "shared-server-version: $(cat "$BUILDS/shared-server-version")"
echo "stable-version: $(cat "$BUILDS/stable-version")"
SERVER_PID=""
cleanup() {
[ -n "$SERVER_PID" ] && kill "$SERVER_PID" 2>/dev/null || true
"$NEW_BIN" --no-update server stop >/dev/null 2>&1 || true
pkill -f "$BUILDS/versions/0.14.6/jcode-linux-x86_64.bin" 2>/dev/null || true
pkill -f "$BUILDS/versions/0.22.0/jcode" 2>/dev/null || true
rm -rf "$SANDBOX"
}
trap cleanup EXIT
server_version_via_socket() {
# Ask the running daemon (via the new client's debug surface) its version.
"$NEW_BIN" --no-update debug server:info 2>/dev/null \
| grep -oE '"version":[[:space:]]*"[^"]*"' | head -1
}
# --- 1) Start the REAL old v0.14.6 daemon ----------------------------------
log "Starting OLD v0.14.6 daemon"
"$BUILDS/shared-server/jcode" --no-update --provider antigravity serve \
>"$SANDBOX/server.log" 2>&1 &
SERVER_PID=$!
# Wait for the socket to appear.
for _ in $(seq 1 40); do
[ -S "$JCODE_SOCKET" ] && break
sleep 0.25
done
sleep 1
echo "old daemon pid=$SERVER_PID"
echo "server.log tail:"; tail -8 "$SANDBOX/server.log" 2>/dev/null || true
BEFORE="$(server_version_via_socket)"
echo "server version BEFORE (via socket): ${BEFORE:-<none>}"
# --- 2) New client: jcode server reload (repairs channel, then reloads) ----
log "Running NEW client: jcode server reload"
"$NEW_BIN" --no-update server reload 2>&1 | sed 's/^/[server reload] /' || true
echo "shared-server-version after repair: $(cat "$BUILDS/shared-server-version")"
# Give the handoff a moment.
for _ in $(seq 1 40); do
[ -S "$JCODE_SOCKET" ] && break
sleep 0.25
done
sleep 2
# --- 3) Verify the running daemon is now v0.22.x ---------------------------
AFTER="$(server_version_via_socket)"
echo "server version AFTER (via socket): ${AFTER:-<none>}"
echo "server.log tail (post-reload):"; tail -8 "$SANDBOX/server.log" 2>/dev/null || true
log "RESULT"
echo "shared-server-version: before=0.14.6 after=$(cat "$BUILDS/shared-server-version")"
echo "server version: before=${BEFORE:-?} after=${AFTER:-?}"
ok_channel=0
[ "$(cat "$BUILDS/shared-server-version")" = "0.22.0" ] && ok_channel=1
ok_server=0
echo "${AFTER:-}" | grep -q "0.22" && ok_server=1
if [ "$ok_channel" = 1 ] && [ "$ok_server" = 1 ]; then
echo "PASS: new client repaired the channel AND the stale server upgraded to v0.22"
exit 0
elif [ "$ok_channel" = 1 ]; then
echo "PARTIAL: channel repaired to 0.22.0, but server version probe inconclusive (AFTER=${AFTER:-none})"
echo " (channel repair is the load-bearing fix; server exec depends on old daemon handoff)"
exit 0
else
echo "FAIL: channel was not repaired"
exit 1
fi
+420
View File
@@ -0,0 +1,420 @@
#!/usr/bin/env python3
"""
jcode Stress Test: Spawn 40 sessions via debug socket, measure performance.
Tests:
1. Session creation throughput
2. Memory growth per session
3. FD leak detection
4. Socket responsiveness under load
5. Message handling under load (optional)
6. Session cleanup / resource recovery
"""
import socket
import json
import time
import os
import sys
import subprocess
import signal
NUM_INSTANCES = int(sys.argv[1]) if len(sys.argv) > 1 else 40
MAIN_SOCK = f"/run/user/{os.getuid()}/jcode.sock"
DEBUG_SOCK = f"/run/user/{os.getuid()}/jcode-debug.sock"
class Colors:
BOLD = "\033[1m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
RED = "\033[31m"
CYAN = "\033[36m"
DIM = "\033[2m"
RESET = "\033[0m"
def debug_cmd(cmd, timeout=10):
"""Send a debug command and return parsed response."""
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.settimeout(timeout)
try:
sock.connect(DEBUG_SOCK)
req = {"type": "debug_command", "id": 1, "command": cmd}
sock.send((json.dumps(req) + "\n").encode())
# Read until we get a complete JSON response
buf = b""
while True:
chunk = sock.recv(65536)
if not chunk:
break
buf += chunk
try:
return json.loads(buf.decode())
except json.JSONDecodeError:
continue
except Exception as e:
return {"error": str(e)}
finally:
sock.close()
def get_server_pid():
"""Get the jcode server PID."""
try:
result = subprocess.run(
["lsof", "-U", "-a", "-c", "jcode"],
capture_output=True, text=True, timeout=5
)
for line in result.stdout.splitlines():
if "jcode.sock" in line and "LISTEN" in line:
parts = line.split()
return int(parts[1])
except:
pass
# Fallback: find the oldest jcode process (likely the server)
try:
result = subprocess.run(
["pgrep", "-o", "jcode"], capture_output=True, text=True, timeout=5
)
if result.stdout.strip():
return int(result.stdout.strip().splitlines()[0])
except:
pass
return None
def proc_stat(pid):
"""Get process stats from /proc."""
stats = {}
try:
with open(f"/proc/{pid}/status") as f:
for line in f:
if line.startswith("VmRSS:"):
stats["rss_kb"] = int(line.split()[1])
elif line.startswith("VmSize:"):
stats["vms_kb"] = int(line.split()[1])
elif line.startswith("Threads:"):
stats["threads"] = int(line.split()[1])
stats["fds"] = len(os.listdir(f"/proc/{pid}/fd"))
except:
pass
return stats
def fmt_kb(kb):
if kb > 1024*1024:
return f"{kb/1024/1024:.1f}GB"
elif kb > 1024:
return f"{kb/1024:.1f}MB"
return f"{kb}KB"
def print_header(text):
print(f"\n{Colors.BOLD}{Colors.CYAN}{'='*60}")
print(f" {text}")
print(f"{'='*60}{Colors.RESET}\n")
def print_section(text):
print(f"\n{Colors.BOLD}--- {text} ---{Colors.RESET}")
def print_ok(text):
print(f" {Colors.GREEN}{text}{Colors.RESET}")
def print_warn(text):
print(f" {Colors.YELLOW}⚠️ {text}{Colors.RESET}")
def print_err(text):
print(f" {Colors.RED}{text}{Colors.RESET}")
def print_stat(label, value):
print(f" {label}: {Colors.BOLD}{value}{Colors.RESET}")
# ============================================================
# MAIN
# ============================================================
print_header(f"jcode Stress Test: {NUM_INSTANCES} sessions")
# --- Pre-flight ---
print_section("Pre-flight checks")
if not os.path.exists(MAIN_SOCK):
print_err(f"No server socket at {MAIN_SOCK}")
print(" Start a server with: jcode serve &")
sys.exit(1)
if not os.path.exists(DEBUG_SOCK):
print_err(f"No debug socket at {DEBUG_SOCK}")
print(" Enable with: touch ~/.jcode/debug_control")
sys.exit(1)
# Test connectivity
t0 = time.monotonic()
state = debug_cmd("state")
t1 = time.monotonic()
if "error" in state:
print_err(f"Debug socket error: {state['error']}")
sys.exit(1)
print_ok(f"Debug socket responding ({(t1-t0)*1000:.0f}ms)")
server_pid = get_server_pid()
if server_pid:
print_ok(f"Server PID: {server_pid}")
else:
print_warn("Could not determine server PID")
# --- Baseline ---
print_section("Baseline measurements")
baseline = proc_stat(server_pid) if server_pid else {}
baseline_procs = int(subprocess.run(["pgrep", "-c", "jcode"], capture_output=True, text=True).stdout.strip() or "0")
print_stat("Server RSS", fmt_kb(baseline.get("rss_kb", 0)))
print_stat("Server VMS", fmt_kb(baseline.get("vms_kb", 0)))
print_stat("Server FDs", baseline.get("fds", "?"))
print_stat("Server threads", baseline.get("threads", "?"))
print_stat("Total jcode procs", baseline_procs)
# List existing sessions
sessions_resp = debug_cmd("sessions")
try:
existing_sessions = json.loads(sessions_resp.get("output", "[]"))
print_stat("Existing sessions", len(existing_sessions))
except:
existing_sessions = []
print_stat("Existing sessions", "?")
# --- Phase 1: Rapid session creation ---
print_section(f"Phase 1: Creating {NUM_INSTANCES} sessions")
created_sessions = []
create_times = []
create_errors = []
per_session_stats = []
for i in range(1, NUM_INSTANCES + 1):
t0 = time.monotonic()
resp = debug_cmd(f"create_session:/tmp/jcode-stress-{i}", timeout=30)
t1 = time.monotonic()
elapsed_ms = (t1 - t0) * 1000
create_times.append(elapsed_ms)
if resp.get("ok"):
try:
session_data = json.loads(resp["output"])
session_id = session_data.get("session_id", "")
created_sessions.append(session_id)
except:
created_sessions.append(f"unknown_{i}")
create_errors.append((i, "Failed to parse session ID"))
else:
create_errors.append((i, resp.get("error", resp.get("output", "unknown"))))
# Progress + snapshot every 10
if i % 10 == 0 or i == NUM_INSTANCES:
stats = proc_stat(server_pid) if server_pid else {}
per_session_stats.append((i, stats.copy()))
rss = fmt_kb(stats.get("rss_kb", 0))
fds = stats.get("fds", "?")
threads = stats.get("threads", "?")
avg_ms = sum(create_times[-10:]) / len(create_times[-10:])
print(f" [{i:3d}/{NUM_INSTANCES}] avg_create={avg_ms:.0f}ms rss={rss} fds={fds} threads={threads} sessions_ok={len(created_sessions)}")
elif i % 5 == 0:
sys.stdout.write(".")
sys.stdout.flush()
if create_errors:
print_warn(f"{len(create_errors)} creation errors:")
for idx, err in create_errors[:5]:
print(f" Instance {idx}: {err}")
if len(create_errors) > 5:
print(f" ... and {len(create_errors) - 5} more")
else:
print_ok(f"All {NUM_INSTANCES} sessions created successfully")
# --- Phase 2: Socket responsiveness under load ---
print_section("Phase 2: Socket responsiveness with all sessions active")
socket_times = []
for probe in range(1, 11):
t0 = time.monotonic()
resp = debug_cmd("state", timeout=15)
t1 = time.monotonic()
elapsed_ms = (t1 - t0) * 1000
socket_times.append(elapsed_ms)
if socket_times:
print_stat("Debug cmd min", f"{min(socket_times):.0f}ms")
print_stat("Debug cmd max", f"{max(socket_times):.0f}ms")
print_stat("Debug cmd avg", f"{sum(socket_times)/len(socket_times):.0f}ms")
if max(socket_times) > 1000:
print_warn(f"Socket latency exceeded 1s ({max(socket_times):.0f}ms)")
# List sessions under load
t0 = time.monotonic()
sessions_resp = debug_cmd("sessions", timeout=30)
t1 = time.monotonic()
try:
all_sessions = json.loads(sessions_resp.get("output", "[]"))
print_stat("Sessions list time", f"{(t1-t0)*1000:.0f}ms ({len(all_sessions)} sessions)")
except:
print_stat("Sessions list time", f"{(t1-t0)*1000:.0f}ms (parse error)")
# --- Phase 3: Send a message to a few sessions ---
print_section("Phase 3: Message handling under load (5 sessions)")
message_times = []
for idx, sid in enumerate(created_sessions[:5]):
t0 = time.monotonic()
# Use tool execution instead of message (avoids LLM call)
resp = debug_cmd(f"tool:bash {{\"command\":\"echo stress_test_{idx}\"}}", timeout=30)
t1 = time.monotonic()
elapsed_ms = (t1 - t0) * 1000
message_times.append(elapsed_ms)
status = "ok" if resp.get("ok") else "err"
print(f" Session {idx+1}: tool exec {elapsed_ms:.0f}ms ({status})")
if message_times:
print_stat("Tool exec avg", f"{sum(message_times)/len(message_times):.0f}ms")
# --- Phase 4: Peak resource measurement ---
print_section("Phase 4: Peak resource usage")
peak = proc_stat(server_pid) if server_pid else {}
peak_procs = int(subprocess.run(["pgrep", "-c", "jcode"], capture_output=True, text=True).stdout.strip() or "0")
print_stat("Server RSS", fmt_kb(peak.get("rss_kb", 0)))
print_stat("Server VMS", fmt_kb(peak.get("vms_kb", 0)))
print_stat("Server FDs", peak.get("fds", "?"))
print_stat("Server threads", peak.get("threads", "?"))
print_stat("Total jcode procs", peak_procs)
print_stat("RSS per session (approx)", fmt_kb(
max(0, (peak.get("rss_kb", 0) - baseline.get("rss_kb", 0))) // max(1, len(created_sessions))
))
# --- Phase 5: Destroy all sessions ---
print_section(f"Phase 5: Destroying {len(created_sessions)} sessions")
destroy_times = []
destroy_errors = []
for i, sid in enumerate(created_sessions, 1):
t0 = time.monotonic()
resp = debug_cmd(f"destroy_session:{sid}", timeout=15)
t1 = time.monotonic()
elapsed_ms = (t1 - t0) * 1000
destroy_times.append(elapsed_ms)
if not resp.get("ok"):
destroy_errors.append((i, resp.get("error", resp.get("output", "unknown"))))
if i % 10 == 0 or i == len(created_sessions):
stats = proc_stat(server_pid) if server_pid else {}
rss = fmt_kb(stats.get("rss_kb", 0))
fds = stats.get("fds", "?")
avg_ms = sum(destroy_times[-10:]) / len(destroy_times[-10:])
print(f" [{i:3d}/{len(created_sessions)}] avg_destroy={avg_ms:.0f}ms rss={rss} fds={fds}")
if destroy_errors:
print_warn(f"{len(destroy_errors)} destroy errors")
for idx, err in destroy_errors[:3]:
print(f" Session {idx}: {err}")
else:
print_ok(f"All {len(created_sessions)} sessions destroyed")
# --- Phase 6: Resource recovery check ---
print_section("Phase 6: Resource recovery (waiting 10s for cleanup)")
time.sleep(10)
final = proc_stat(server_pid) if server_pid else {}
final_procs = int(subprocess.run(["pgrep", "-c", "jcode"], capture_output=True, text=True).stdout.strip() or "0")
# Final socket test
t0 = time.monotonic()
final_state = debug_cmd("state")
t1 = time.monotonic()
final_socket_ms = (t1 - t0) * 1000
print_stat("Server RSS", f"{fmt_kb(final.get('rss_kb', 0))} (was {fmt_kb(baseline.get('rss_kb', 0))})")
print_stat("Server FDs", f"{final.get('fds', '?')} (was {baseline.get('fds', '?')})")
print_stat("Server threads", f"{final.get('threads', '?')} (was {baseline.get('threads', '?')})")
print_stat("Socket latency", f"{final_socket_ms:.0f}ms")
# Check for leaks
rss_delta = final.get("rss_kb", 0) - baseline.get("rss_kb", 0)
fd_delta = (final.get("fds", 0) or 0) - (baseline.get("fds", 0) or 0)
thread_delta = (final.get("threads", 0) or 0) - (baseline.get("threads", 0) or 0)
# ============================================================
# FINAL REPORT
# ============================================================
print_header("STRESS TEST RESULTS")
print(f" {Colors.BOLD}Sessions:{Colors.RESET} {NUM_INSTANCES} created, {len(created_sessions)} successful, {len(create_errors)} failed")
print()
print(f" {Colors.BOLD}Session Creation:{Colors.RESET}")
if create_times:
print(f" Min: {min(create_times):.0f}ms")
print(f" Max: {max(create_times):.0f}ms")
print(f" Avg: {sum(create_times)/len(create_times):.0f}ms")
print(f" p95: {sorted(create_times)[int(len(create_times)*0.95)]:.0f}ms")
print(f" Total: {sum(create_times):.0f}ms")
print()
print(f" {Colors.BOLD}Session Destruction:{Colors.RESET}")
if destroy_times:
print(f" Min: {min(destroy_times):.0f}ms")
print(f" Max: {max(destroy_times):.0f}ms")
print(f" Avg: {sum(destroy_times)/len(destroy_times):.0f}ms")
print()
print(f" {Colors.BOLD}Memory:{Colors.RESET}")
print(f" Baseline RSS: {fmt_kb(baseline.get('rss_kb', 0))}")
print(f" Peak RSS: {fmt_kb(peak.get('rss_kb', 0))}")
print(f" Final RSS: {fmt_kb(final.get('rss_kb', 0))}")
print(f" RSS delta: {'+' if rss_delta >= 0 else ''}{fmt_kb(abs(rss_delta))}")
print(f" Per-session: ~{fmt_kb(max(0, peak.get('rss_kb', 0) - baseline.get('rss_kb', 0)) // max(1, len(created_sessions)))}")
print()
print(f" {Colors.BOLD}Resource Leaks:{Colors.RESET}")
fd_ok = abs(fd_delta) <= 10
thread_ok = abs(thread_delta) <= 5
rss_ok = rss_delta < (baseline.get("rss_kb", 0) or 100000) * 0.2 # <20% growth
if fd_ok:
print_ok(f"FDs: {baseline.get('fds','?')} -> {final.get('fds','?')} (delta: {fd_delta})")
else:
print_warn(f"FD leak: {baseline.get('fds','?')} -> {final.get('fds','?')} (delta: {fd_delta})")
if thread_ok:
print_ok(f"Threads: {baseline.get('threads','?')} -> {final.get('threads','?')} (delta: {thread_delta})")
else:
print_warn(f"Thread leak: {baseline.get('threads','?')} -> {final.get('threads','?')} (delta: {thread_delta})")
if rss_ok:
print_ok(f"Memory: {fmt_kb(baseline.get('rss_kb',0))} -> {fmt_kb(final.get('rss_kb',0))} (delta: {fmt_kb(abs(rss_delta))})")
else:
print_warn(f"Memory not fully recovered: {fmt_kb(baseline.get('rss_kb',0))} -> {fmt_kb(final.get('rss_kb',0))} (delta: {fmt_kb(abs(rss_delta))})")
print()
print(f" {Colors.BOLD}Socket Health:{Colors.RESET}")
if final_socket_ms < 100:
print_ok(f"Responsive after stress: {final_socket_ms:.0f}ms")
elif final_socket_ms < 1000:
print_warn(f"Slow after stress: {final_socket_ms:.0f}ms")
else:
print_err(f"Very slow after stress: {final_socket_ms:.0f}ms")
# Memory growth timeline
print()
print(f" {Colors.BOLD}Memory Growth Timeline:{Colors.RESET}")
print(f" {'Sessions':>10s} {'RSS':>10s} {'FDs':>6s} {'Threads':>8s}")
print(f" {''*10} {''*10} {''*6} {''*8}")
print(f" {'baseline':>10s} {fmt_kb(baseline.get('rss_kb',0)):>10s} {str(baseline.get('fds','?')):>6s} {str(baseline.get('threads','?')):>8s}")
for count, stats in per_session_stats:
print(f" {count:>10d} {fmt_kb(stats.get('rss_kb',0)):>10s} {str(stats.get('fds','?')):>6s} {str(stats.get('threads','?')):>8s}")
print(f" {'final':>10s} {fmt_kb(final.get('rss_kb',0)):>10s} {str(final.get('fds','?')):>6s} {str(final.get('threads','?')):>8s}")
print()
print(f"{'='*60}")
+412
View File
@@ -0,0 +1,412 @@
#!/usr/bin/env bash
#
# Stress test: spawn 40 jcode TUI client instances rapidly
# Measures startup time, memory usage, CPU, fd count, socket health
#
set -euo pipefail
NUM_INSTANCES=${1:-40}
JCODE_BIN="${JCODE_BIN:-$(which jcode)}"
LOG_DIR="/tmp/jcode-stress-test-$(date +%s)"
mkdir -p "$LOG_DIR"
MAIN_SOCK="/run/user/$(id -u)/jcode.sock"
DEBUG_SOCK="/run/user/$(id -u)/jcode-debug.sock"
echo "========================================="
echo " jcode Stress Test: $NUM_INSTANCES instances"
echo "========================================="
echo "Binary: $JCODE_BIN"
echo "Log dir: $LOG_DIR"
echo "Main socket: $MAIN_SOCK"
echo ""
# --- Helper functions ---
get_server_pid() {
# Server listens on the main socket
lsof -U 2>/dev/null | grep "$(basename $MAIN_SOCK)" | awk '{print $2}' | sort -u | head -1
}
snapshot() {
local label="$1"
local ts=$(date +%s%N)
echo "--- Snapshot: $label ---" >> "$LOG_DIR/snapshots.log"
echo "timestamp_ns: $ts" >> "$LOG_DIR/snapshots.log"
# Memory
free -m >> "$LOG_DIR/snapshots.log" 2>/dev/null
echo "" >> "$LOG_DIR/snapshots.log"
# jcode process count and total RSS
local jcode_procs=$(pgrep -c jcode 2>/dev/null || echo 0)
local total_rss=0
local total_vms=0
for pid in $(pgrep jcode 2>/dev/null); do
local rss=$(awk '/^VmRSS:/{print $2}' /proc/$pid/status 2>/dev/null || echo 0)
local vms=$(awk '/^VmSize:/{print $2}' /proc/$pid/status 2>/dev/null || echo 0)
total_rss=$((total_rss + rss))
total_vms=$((total_vms + vms))
done
echo "jcode_processes: $jcode_procs" >> "$LOG_DIR/snapshots.log"
echo "total_rss_kb: $total_rss" >> "$LOG_DIR/snapshots.log"
echo "total_vms_kb: $total_vms" >> "$LOG_DIR/snapshots.log"
# Open file descriptors for server
local server_pid=$(get_server_pid)
if [ -n "$server_pid" ]; then
local fd_count=$(ls /proc/$server_pid/fd 2>/dev/null | wc -l)
local thread_count=$(ls /proc/$server_pid/task 2>/dev/null | wc -l)
echo "server_pid: $server_pid" >> "$LOG_DIR/snapshots.log"
echo "server_fd_count: $fd_count" >> "$LOG_DIR/snapshots.log"
echo "server_threads: $thread_count" >> "$LOG_DIR/snapshots.log"
# Server RSS specifically
local server_rss=$(awk '/^VmRSS:/{print $2}' /proc/$server_pid/status 2>/dev/null || echo 0)
echo "server_rss_kb: $server_rss" >> "$LOG_DIR/snapshots.log"
fi
# CPU load
cat /proc/loadavg >> "$LOG_DIR/snapshots.log"
echo "" >> "$LOG_DIR/snapshots.log"
echo "===" >> "$LOG_DIR/snapshots.log"
# Print summary line to stdout
echo "[$label] procs=$jcode_procs rss=${total_rss}KB($(( total_rss / 1024 ))MB) server_rss=$(awk '/^VmRSS:/{print $2}' /proc/${server_pid:-0}/status 2>/dev/null || echo '?')KB fds=$(ls /proc/${server_pid:-0}/fd 2>/dev/null | wc -l) threads=$(ls /proc/${server_pid:-0}/task 2>/dev/null | wc -l)"
}
check_socket_health() {
local label="$1"
# Try a quick connect-and-disconnect on the main socket
local start_ns=$(date +%s%N)
if python3 -c "
import socket, json, sys, time
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
sock.settimeout(5)
sock.connect('$MAIN_SOCK')
# just connect and close - test socket responsiveness
sock.close()
sys.exit(0)
except Exception as e:
print(f'Socket error: {e}', file=sys.stderr)
sys.exit(1)
" 2>>"$LOG_DIR/socket_errors.log"; then
local end_ns=$(date +%s%N)
local dur_ms=$(( (end_ns - start_ns) / 1000000 ))
echo "[$label] Socket connect: ${dur_ms}ms" | tee -a "$LOG_DIR/socket_latency.log"
else
echo "[$label] Socket connect: FAILED" | tee -a "$LOG_DIR/socket_latency.log"
fi
}
debug_cmd() {
local cmd="$1"
local timeout="${2:-5}"
python3 -c "
import socket, json, sys
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
sock.settimeout($timeout)
sock.connect('$DEBUG_SOCK')
req = {'type': 'debug_command', 'id': 1, 'command': '$cmd'}
sock.send((json.dumps(req) + '\n').encode())
data = sock.recv(65536).decode()
print(data)
sock.close()
except Exception as e:
print(json.dumps({'error': str(e)}))
" 2>/dev/null
}
# --- Pre-flight ---
echo "=== Pre-flight checks ==="
# Check if server is running
if ! [ -S "$MAIN_SOCK" ]; then
echo "ERROR: No jcode server running at $MAIN_SOCK"
echo "Start one with: jcode serve &"
exit 1
fi
# Test socket
check_socket_health "pre-flight"
# Baseline snapshot
snapshot "baseline"
echo ""
# --- Record system baseline ---
BASELINE_RSS=$(pgrep jcode 2>/dev/null | while read pid; do awk '/^VmRSS:/{print $2}' /proc/$pid/status 2>/dev/null; done | paste -sd+ | bc 2>/dev/null || echo 0)
BASELINE_PROCS=$(pgrep -c jcode 2>/dev/null || echo 0)
BASELINE_SERVER_PID=$(get_server_pid)
BASELINE_FDS=$(ls /proc/${BASELINE_SERVER_PID:-0}/fd 2>/dev/null | wc -l)
echo "=== Baseline ==="
echo " Processes: $BASELINE_PROCS"
echo " Total RSS: ${BASELINE_RSS}KB ($(( BASELINE_RSS / 1024 ))MB)"
echo " Server FDs: $BASELINE_FDS"
echo ""
# --- Start background monitoring ---
echo "=== Starting background monitor ==="
(
while true; do
ts=$(date +%s)
jcode_procs=$(pgrep -c jcode 2>/dev/null || echo 0)
total_rss=0
for pid in $(pgrep jcode 2>/dev/null); do
rss=$(awk '/^VmRSS:/{print $2}' /proc/$pid/status 2>/dev/null || echo 0)
total_rss=$((total_rss + rss))
done
server_pid=$(get_server_pid)
server_rss=$(awk '/^VmRSS:/{print $2}' /proc/${server_pid:-0}/status 2>/dev/null || echo 0)
server_fds=$(ls /proc/${server_pid:-0}/fd 2>/dev/null | wc -l)
server_threads=$(ls /proc/${server_pid:-0}/task 2>/dev/null | wc -l)
cpu_load=$(awk '{print $1}' /proc/loadavg)
echo "$ts,$jcode_procs,$total_rss,$server_rss,$server_fds,$server_threads,$cpu_load"
sleep 1
done
) > "$LOG_DIR/timeseries.csv" &
MONITOR_PID=$!
echo "Monitor PID: $MONITOR_PID"
echo ""
# --- Spawn instances ---
echo "=== Spawning $NUM_INSTANCES jcode instances ==="
PIDS=()
SPAWN_TIMES=()
# Use script to give each instance a pty (jcode requires tty)
for i in $(seq 1 $NUM_INSTANCES); do
local_start=$(date +%s%N)
# Each instance gets its own pseudo-terminal via script(1)
# We connect to the existing server, which creates sessions
script -q -c "$JCODE_BIN --no-update --no-selfdev" /dev/null \
> "$LOG_DIR/instance_${i}_stdout.log" \
2> "$LOG_DIR/instance_${i}_stderr.log" &
pid=$!
PIDS+=($pid)
local_end=$(date +%s%N)
spawn_ms=$(( (local_end - local_start) / 1000000 ))
SPAWN_TIMES+=($spawn_ms)
# Log it
echo " [$i/$NUM_INSTANCES] PID=$pid spawn=${spawn_ms}ms" | tee -a "$LOG_DIR/spawn.log"
# Snapshot every 10 instances
if (( i % 10 == 0 )); then
sleep 1 # Let things settle
snapshot "after_${i}_spawns"
check_socket_health "after_${i}_spawns"
fi
# Small delay to avoid overwhelming everything at once
sleep 0.2
done
echo ""
echo "=== All $NUM_INSTANCES instances spawned ==="
echo ""
# Let them run for a bit
echo "=== Letting instances stabilize for 10 seconds ==="
sleep 10
snapshot "post_spawn_settled"
check_socket_health "post_spawn_settled"
echo ""
# --- Debug socket probe under load ---
echo "=== Debug socket responsiveness under load ==="
for probe in 1 2 3; do
start_ns=$(date +%s%N)
result=$(debug_cmd "state" 10)
end_ns=$(date +%s%N)
dur_ms=$(( (end_ns - start_ns) / 1000000 ))
echo " Probe $probe: ${dur_ms}ms" | tee -a "$LOG_DIR/debug_probe.log"
sleep 0.5
done
echo ""
# --- Session listing under load ---
echo "=== Session list under load ==="
start_ns=$(date +%s%N)
sessions_result=$(debug_cmd "sessions" 15)
end_ns=$(date +%s%N)
dur_ms=$(( (end_ns - start_ns) / 1000000 ))
session_count=$(echo "$sessions_result" | python3 -c "
import json, sys
try:
data = json.loads(sys.stdin.read())
output = data.get('output', '')
sessions = json.loads(output) if output else []
print(len(sessions))
except:
print('?')
" 2>/dev/null)
echo " Sessions: $session_count, query time: ${dur_ms}ms"
echo ""
# --- Kill all spawned instances ---
echo "=== Killing all spawned instances ==="
for pid in "${PIDS[@]}"; do
kill "$pid" 2>/dev/null || true
done
# Wait for them to die
sleep 3
# Force kill stragglers
for pid in "${PIDS[@]}"; do
kill -9 "$pid" 2>/dev/null || true
done
sleep 2
snapshot "post_kill"
echo ""
# --- Post-kill: check for leaked resources ---
echo "=== Post-kill resource check ==="
POST_PROCS=$(pgrep -c jcode 2>/dev/null || echo 0)
POST_RSS=0
for pid in $(pgrep jcode 2>/dev/null); do
rss=$(awk '/^VmRSS:/{print $2}' /proc/$pid/status 2>/dev/null || echo 0)
POST_RSS=$((POST_RSS + rss))
done
POST_SERVER_PID=$(get_server_pid)
POST_FDS=$(ls /proc/${POST_SERVER_PID:-0}/fd 2>/dev/null | wc -l)
POST_SERVER_RSS=$(awk '/^VmRSS:/{print $2}' /proc/${POST_SERVER_PID:-0}/status 2>/dev/null || echo 0)
echo " Processes: $BASELINE_PROCS -> $POST_PROCS (delta: $((POST_PROCS - BASELINE_PROCS)))"
echo " Total RSS: ${BASELINE_RSS}KB -> ${POST_RSS}KB (delta: $((POST_RSS - BASELINE_RSS))KB)"
echo " Server FDs: $BASELINE_FDS -> $POST_FDS (delta: $((POST_FDS - BASELINE_FDS)))"
echo " Server RSS: ${POST_SERVER_RSS}KB"
echo ""
# Check socket health after cleanup
echo "=== Post-cleanup socket health ==="
check_socket_health "post_cleanup_1"
sleep 1
check_socket_health "post_cleanup_2"
echo ""
# --- Wait a bit more and check for memory leak ---
echo "=== Waiting 15s for GC/cleanup ==="
sleep 15
snapshot "post_gc"
FINAL_SERVER_PID=$(get_server_pid)
FINAL_FDS=$(ls /proc/${FINAL_SERVER_PID:-0}/fd 2>/dev/null | wc -l)
FINAL_SERVER_RSS=$(awk '/^VmRSS:/{print $2}' /proc/${FINAL_SERVER_PID:-0}/status 2>/dev/null || echo 0)
check_socket_health "final"
echo ""
# --- Stop background monitor ---
kill $MONITOR_PID 2>/dev/null || true
# --- Summary report ---
echo "========================================="
echo " STRESS TEST SUMMARY"
echo "========================================="
echo ""
echo "Configuration:"
echo " Instances spawned: $NUM_INSTANCES"
echo " Binary: $JCODE_BIN"
echo ""
# Spawn time stats
if [ ${#SPAWN_TIMES[@]} -gt 0 ]; then
total=0
min=${SPAWN_TIMES[0]}
max=${SPAWN_TIMES[0]}
for t in "${SPAWN_TIMES[@]}"; do
total=$((total + t))
if (( t < min )); then min=$t; fi
if (( t > max )); then max=$t; fi
done
avg=$((total / ${#SPAWN_TIMES[@]}))
echo "Spawn Times (fork+exec overhead):"
echo " Min: ${min}ms"
echo " Max: ${max}ms"
echo " Avg: ${avg}ms"
echo " Total: ${total}ms"
echo ""
fi
echo "Memory Impact:"
echo " Baseline total RSS: ${BASELINE_RSS}KB ($(( BASELINE_RSS / 1024 ))MB)"
echo " Peak server RSS: (see timeseries)"
echo " Final server RSS: ${FINAL_SERVER_RSS}KB ($(( FINAL_SERVER_RSS / 1024 ))MB)"
echo " Server RSS delta from baseline: $((FINAL_SERVER_RSS - $(awk '/^VmRSS:/{print $2}' /proc/${BASELINE_SERVER_PID:-0}/status 2>/dev/null || echo $FINAL_SERVER_RSS)))KB"
echo ""
echo "File Descriptors (leak check):"
echo " Baseline: $BASELINE_FDS"
echo " After kill: $POST_FDS (delta: $((POST_FDS - BASELINE_FDS)))"
echo " After GC: $FINAL_FDS (delta: $((FINAL_FDS - BASELINE_FDS)))"
if (( FINAL_FDS > BASELINE_FDS + 5 )); then
echo " ⚠️ POSSIBLE FD LEAK: $((FINAL_FDS - BASELINE_FDS)) fds not cleaned up"
else
echo " ✅ FDs cleaned up properly"
fi
echo ""
echo "Socket Latency:"
if [ -f "$LOG_DIR/socket_latency.log" ]; then
cat "$LOG_DIR/socket_latency.log"
fi
echo ""
echo "Debug Socket Latency:"
if [ -f "$LOG_DIR/debug_probe.log" ]; then
cat "$LOG_DIR/debug_probe.log"
fi
echo ""
# Check for errors
echo "Errors:"
error_count=0
for f in "$LOG_DIR"/instance_*_stderr.log; do
if [ -s "$f" ]; then
instance=$(basename "$f" | sed 's/instance_\([0-9]*\)_.*/\1/')
errors=$(grep -i "error\|panic\|crash\|failed\|refused" "$f" 2>/dev/null | head -3)
if [ -n "$errors" ]; then
echo " Instance $instance:"
echo "$errors" | sed 's/^/ /'
error_count=$((error_count + 1))
fi
fi
done
if (( error_count == 0 )); then
echo " ✅ No errors detected"
else
echo ""
echo " ⚠️ $error_count instances had errors"
fi
echo ""
# Generate timeseries summary
echo "Timeseries data: $LOG_DIR/timeseries.csv"
echo " Format: timestamp,procs,total_rss_kb,server_rss_kb,server_fds,server_threads,cpu_load"
if [ -f "$LOG_DIR/timeseries.csv" ]; then
echo " Rows: $(wc -l < "$LOG_DIR/timeseries.csv")"
echo ""
echo " Peak values from timeseries:"
awk -F, '{
if ($2+0 > max_procs) max_procs=$2+0;
if ($3+0 > max_rss) max_rss=$3+0;
if ($4+0 > max_srv_rss) max_srv_rss=$4+0;
if ($5+0 > max_fds) max_fds=$5+0;
if ($6+0 > max_threads) max_threads=$6+0;
} END {
printf " Max processes: %d\n", max_procs;
printf " Max total RSS: %d KB (%d MB)\n", max_rss, max_rss/1024;
printf " Max server RSS: %d KB (%d MB)\n", max_srv_rss, max_srv_rss/1024;
printf " Max server FDs: %d\n", max_fds;
printf " Max server threads: %d\n", max_threads;
}' "$LOG_DIR/timeseries.csv"
fi
echo ""
echo "Full logs: $LOG_DIR/"
echo "========================================="
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More