mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-17 15:34:54 +00:00
Our history is just-every's with author emails rewritten, so the graphs are
disjoint and git finds no merge base. Supplied one by hand: d1f1f51a41, the
just-every commit matching our Oct-2025 fork point (22 files apart). 9618
upstream commits, 7004 files, +1.81M/-159k, 37 conflicts.
Resolution:
- Upstream code where our side was the Oct-2025 prototype of a feature they
have since reimplemented 3-5x larger (agent_runs, browser_sessions,
history_cell/{agent,browser}, the vt100 harness and its snapshots).
- Upstream's config.rs module split (builder/defaults/sources/validation)
kept; zen5-coder and the provider-follows-model rule re-applied on top.
- Ours where the file IS the identity: README, package.json, postinstall,
release notes pipeline, integration-zed.
- Hybrid where upstream improved a file we had branded: updates.rs (their
release-repo migration, our hanzoai/dev), release.yml (their fail-fast npm
auth check, our package name), chatwidget.rs (their turn handling, our
HANZO_USER_KEY hint), exec human output (their 237 lines, our banner).
Superseded and dropped deliberately: our resume_command_name() coder-alias
hack — upstream's derive_resume_command_name() reads the invocation basename
generically, which yields "dev" for us.
LLM.md now records the real topology and the sync point; AGENTS.md and
CLAUDE.md are symlinks to it again. Single parent on purpose: adopting
jecode/main as a second parent would re-import 13,742 commits carrying the
upstream author emails a prior rewrite deliberately removed.
Version 0.2.188 -> 0.2.189.
Co-authored-by: Hanzo Dev <[email protected]>
213 lines
5.3 KiB
Bash
213 lines
5.3 KiB
Bash
#!/usr/bin/env bash
|
|
# Capture macOS memory diagnostics for a process once its RSS exceeds a threshold.
|
|
#
|
|
# Usage examples:
|
|
# scripts/memdump.sh --pid 12345
|
|
# scripts/memdump.sh --pid 12345 --threshold 4096 --keep 5 --no-core
|
|
|
|
set -euo pipefail
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: memdump.sh [OPTIONS]
|
|
|
|
Options:
|
|
-p, --pid PID Process ID to inspect (required)
|
|
-t, --threshold MB RSS threshold in MB (default: 5120)
|
|
-k, --keep N Keep at most N dumps (default: 10)
|
|
-d, --dir PATH Base directory for dumps (default: ~/.code/memdumps)
|
|
--sample-secs N Seconds for `sample` (default: 3)
|
|
--sample-interval MS Sampling interval in milliseconds (default: 20)
|
|
--no-core Skip lldb core dump
|
|
-h, --help Show this help
|
|
|
|
Artifacts (best effort):
|
|
ps snapshot, footprint JSON, vmmap summary, sample stacks, optional lldb core
|
|
EOF
|
|
}
|
|
|
|
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') [memdump] $*" >&2; }
|
|
warn() { log "WARN: $*"; }
|
|
error() { log "ERROR: $*"; }
|
|
|
|
require_darwin() {
|
|
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
error "This helper is macOS-only"; exit 1; fi
|
|
}
|
|
|
|
require_binary() {
|
|
if ! command -v "$1" >/dev/null 2>&1; then
|
|
warn "'$1' not found; skipping"; return 1; fi
|
|
return 0
|
|
}
|
|
|
|
check_process_exists() {
|
|
local pid="$1"
|
|
if ! ps -p "$pid" >/dev/null 2>&1; then
|
|
error "Process $pid is not running"; return 1; fi
|
|
return 0
|
|
}
|
|
|
|
get_rss_mb() {
|
|
local pid="$1"
|
|
local rss_kb
|
|
rss_kb=$(ps -o rss= -p "$pid" 2>/dev/null | awk '{print $1}')
|
|
[[ -z "$rss_kb" ]] && echo "0" && return 1
|
|
echo $((rss_kb / 1024))
|
|
}
|
|
|
|
capture_ps_snapshot() {
|
|
local pid="$1" output="$2"
|
|
if ps -p "$pid" -o pid,ppid,user,%cpu,%mem,vsz,rss,etime,command >"$output" 2>&1; then
|
|
log "ps -> $output"
|
|
else
|
|
warn "ps snapshot failed"
|
|
fi
|
|
}
|
|
|
|
capture_footprint() {
|
|
local pid="$1" output="$2"
|
|
require_binary footprint || return 0
|
|
if footprint -j "$pid" >"$output" 2>&1; then
|
|
log "footprint -> $output"
|
|
else
|
|
warn "footprint failed (permissions?)"
|
|
fi
|
|
}
|
|
|
|
capture_vmmap() {
|
|
local pid="$1" output="$2"
|
|
require_binary vmmap || return 0
|
|
if vmmap -summary "$pid" >"$output" 2>&1; then
|
|
log "vmmap -> $output"
|
|
else
|
|
warn "vmmap failed (permissions?)"
|
|
fi
|
|
}
|
|
|
|
capture_sample() {
|
|
local pid="$1" output="$2" secs="$3" interval_ms="$4"
|
|
require_binary sample || return 0
|
|
if sample "$pid" "$secs" "$interval_ms" -file "$output" >"$output.log" 2>&1; then
|
|
log "sample -> $output"
|
|
else
|
|
warn "sample failed (permissions?)"
|
|
fi
|
|
}
|
|
|
|
capture_lldb_core() {
|
|
local pid="$1" dump_dir="$2"
|
|
require_binary lldb || return 0
|
|
|
|
local core_path="$dump_dir/core.$pid"
|
|
local script_path="$dump_dir/lldb_script.txt"
|
|
cat >"$script_path" <<LLDB
|
|
process attach --pid $pid
|
|
process save-core "$core_path"
|
|
detach
|
|
quit
|
|
LLDB
|
|
|
|
log "lldb core attempt (timeout 60s)"
|
|
lldb --batch --source "$script_path" >"$dump_dir/lldb.log" 2>&1 &
|
|
local lldb_pid=$!
|
|
(
|
|
sleep 60
|
|
if kill -0 "$lldb_pid" 2>/dev/null; then
|
|
warn "lldb timed out; killing"
|
|
kill "$lldb_pid" 2>/dev/null || true
|
|
fi
|
|
) &
|
|
local watchdog=$!
|
|
wait "$lldb_pid" || true
|
|
kill "$watchdog" 2>/dev/null || true
|
|
|
|
if [[ -f "$core_path" ]]; then
|
|
log "lldb core -> $core_path"
|
|
else
|
|
warn "lldb did not produce a core (see $dump_dir/lldb.log)"
|
|
fi
|
|
|
|
rm -f "$script_path"
|
|
}
|
|
|
|
rotate_dumps() {
|
|
local base_dir="$1" keep="$2"
|
|
[[ -d "$base_dir" ]] || return 0
|
|
local dumps
|
|
dumps=$(find "$base_dir" -mindepth 1 -maxdepth 1 -type d -name "*-pid*" | sort -r)
|
|
local idx=0
|
|
while IFS= read -r d; do
|
|
idx=$((idx + 1))
|
|
if [[ $idx -gt $keep ]]; then
|
|
log "prune old dump $d"
|
|
rm -rf "$d"
|
|
fi
|
|
done <<<"$dumps"
|
|
}
|
|
|
|
PID=""
|
|
THRESHOLD_MB=5120
|
|
KEEP=10
|
|
BASE_DIR="$HOME/.code/memdumps"
|
|
SKIP_CORE=false
|
|
SAMPLE_SECS=3
|
|
SAMPLE_INTERVAL_MS=20
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-p|--pid) PID="${2:-}"; shift 2 ;;
|
|
-t|--threshold) THRESHOLD_MB="${2:-}"; shift 2 ;;
|
|
-k|--keep) KEEP="${2:-}"; shift 2 ;;
|
|
-d|--dir) BASE_DIR="${2:-}"; shift 2 ;;
|
|
--sample-secs) SAMPLE_SECS="${2:-}"; shift 2 ;;
|
|
--sample-interval) SAMPLE_INTERVAL_MS="${2:-}"; shift 2 ;;
|
|
--no-core) SKIP_CORE=true; shift ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*) error "unknown option $1"; usage; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
require_darwin
|
|
|
|
if [[ -z "$PID" ]]; then error "--pid is required"; usage; exit 1; fi
|
|
check_process_exists "$PID" || exit 1
|
|
|
|
RSS_MB=$(get_rss_mb "$PID")
|
|
log "PID $PID RSS=${RSS_MB}MB (threshold ${THRESHOLD_MB}MB)"
|
|
if [[ "$RSS_MB" -lt "$THRESHOLD_MB" ]]; then
|
|
log "below threshold; nothing to do"
|
|
exit 0
|
|
fi
|
|
|
|
mkdir -p "$BASE_DIR"
|
|
TIMESTAMP=$(date '+%Y%m%d-%H%M%S')
|
|
DUMP_DIR="$BASE_DIR/$TIMESTAMP-pid$PID"
|
|
mkdir -p "$DUMP_DIR"
|
|
|
|
log "writing dump to $DUMP_DIR"
|
|
|
|
cat >"$DUMP_DIR/manifest.txt" <<MANIFEST
|
|
Memory Dump Manifest
|
|
====================
|
|
Timestamp: $(date '+%Y-%m-%d %H:%M:%S')
|
|
PID: $PID
|
|
RSS (MB): $RSS_MB
|
|
Threshold (MB): $THRESHOLD_MB
|
|
Hostname: $(hostname)
|
|
macOS: $(sw_vers -productVersion 2>/dev/null || echo "unknown")
|
|
Command: $(ps -o command= -p "$PID" 2>/dev/null)
|
|
MANIFEST
|
|
|
|
capture_ps_snapshot "$PID" "$DUMP_DIR/ps.txt"
|
|
capture_footprint "$PID" "$DUMP_DIR/footprint.json"
|
|
capture_vmmap "$PID" "$DUMP_DIR/vmmap_summary.txt"
|
|
capture_sample "$PID" "$DUMP_DIR/sample.txt" "$SAMPLE_SECS" "$SAMPLE_INTERVAL_MS"
|
|
if [[ "$SKIP_CORE" != true ]]; then
|
|
capture_lldb_core "$PID" "$DUMP_DIR"
|
|
fi
|
|
|
|
rotate_dumps "$BASE_DIR" "$KEEP"
|
|
|
|
log "done"
|