diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 50dcf8d..a6f75bf 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "throughline", "description": "Continuous, state-aware session memory for Claude Code. Captures what you did and what is (commands, file changes, decisions, and live git/PR state), then hands it off with judgment at session wrap-up. Readable, committable artifacts; binds to Claude's native memory.", - "version": "0.5.2", + "version": "0.6.0", "author": { "name": "Dynamic Agency", "email": "support@dynamicagency.com" diff --git a/CHANGELOG.md b/CHANGELOG.md index bedc4d7..6414c84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,50 @@ All notable changes to throughline are documented here. Format loosely follows [Keep a Changelog](https://keepachangelog.com/); this project uses semantic versioning. +## [0.6.0] + +Issue #9 from the v0.4.0 audit (docs/AUDIT-v0.4.0.md, P2): inline post-compaction +recovery content instead of a pointer. Feature bump - onboard's compact-path +output now carries actual buffer content, not just a filename. An independent +code-review pass on this release caught two gaps in the initial +implementation's bounded-output claim, both closed before merge. Verified by +the full 154-assertion suite (11 new) plus shellcheck under both Homebrew and +an `apt-get install shellcheck` Ubuntu container. + +### Added +- **Inlined post-compaction recovery** (`session-onboard.sh`): on + `source=compact`, the last 30 lines of the current session's action + buffer - including any trailing `compaction-boundary` marker, which lands + there naturally since no captured action can land between PreCompact + stamping it and this SessionStart firing - are now printed directly into + the SessionStart block, fenced in a code block. Previously this path only + printed a pointer ("your buffer survived at ``, read it"), which cost + the model a tool call it might not make, at exactly the point where + post-compaction recall is weakest. The pointer to the full file is kept + alongside the inlined tail for sessions that ran longer than the window. + `startup`/`resume`/`clear` sources are unaffected - only `compact` inlines + buffer content. + +### Fixed (found during review, before merge) +- **The bounded-output claim was false for two unclamped fields**: a Bash + record's `description` and an Edit/Write/NotebookEdit record's `file_path` + are never length-clamped in `session-capture.sh` (only `command` and the + other free-text fields are), so an unusually long one of either would still + inline verbatim despite the 30-line cap. Rather than clamp every + capture-side field (a change to `session-capture.sh`'s existing, separate + design, out of scope here), the inline-tail block now caps each line to 300 + characters itself, via an `awk` pass - the bound this feature's own claim + depends on is now enforced at the point the claim is made, not assumed from + upstream. +- **A stamped `trigger`/`reason` value could break the new markdown fence**: + `tl_clean_ctrl` (shared by `session-flush.sh`/`session-precompact.sh` to + sanitize those fields before stamping) only stripped control characters, not + backticks - a run of 3+ backticks in one of those fields could prematurely + close the ``` fence this release introduces. Not reachable today (both + fields are fixed enum strings from the harness), but cheap to close at the + source: `tl_clean_ctrl` now strips backticks too, matching what the jq-side + `clean` def already does for capture-side fields. + ## [0.5.2] Hot-path perf batch plus issue #15, which turned out to have a second, diff --git a/hooks/_lib.sh b/hooks/_lib.sh index b16b71b..0acc13d 100755 --- a/hooks/_lib.sh +++ b/hooks/_lib.sh @@ -152,16 +152,23 @@ tl_split_sid_line() { _tl_split_line=${1#*"$_tl_tab"} } -# Replace control characters (including newlines) with a space. Shared by -# session-flush.sh and session-precompact.sh so their reason/trigger fields -# can't break the `` marker they're embedded in. (The jq `clean` def in -# tl_jq_redact_defs below duplicates this rule rather than calling it: the -# capture-side hooks apply control-char -# AND backtick stripping together, per-field, inside one jq pipeline that also -# does redaction — pulling just the control-char half out to a separate shell -# call there would mean an extra process per field for no real safety gain.) +# Replace control characters (including newlines) AND backticks with a space. +# Shared by session-flush.sh and session-precompact.sh so their reason/trigger +# fields can't break either delimiter they get embedded in: the `` +# HTML comment (control chars), and - since session-onboard.sh's post-compact +# inline-tail feature (issue #9) started wrapping raw buffer content in a +# markdown ``` fence - a run of 3+ backticks in a stamped line could also +# prematurely close that fence. Both trigger/reason are fixed enum strings +# from the harness today (auto/manual; clear/logout/prompt_input_exit/other), +# so this is currently unreachable, but it is cheap to close at the source +# rather than assume every future caller and every future delimiter stays +# safe. (The jq `clean` def in tl_jq_redact_defs below duplicates this rule +# rather than calling it: the capture-side hooks apply control-char and +# backtick stripping together, per-field, inside one jq pipeline that also +# does redaction — pulling just this half out to a separate shell call there +# would mean an extra process per field for no real safety gain.) tl_clean_ctrl() { - printf '%s' "$1" | tr '[:cntrl:]' ' ' + printf '%s' "$1" | tr '[:cntrl:]`' ' ' } # Single source for the on-disk timestamp format, used by every record line and diff --git a/hooks/session-onboard.sh b/hooks/session-onboard.sh index de127b5..797a9f6 100755 --- a/hooks/session-onboard.sh +++ b/hooks/session-onboard.sh @@ -132,11 +132,32 @@ case "$data" in ;; esac -# Post-compaction recovery: the conversation was just summarized, but this -# session's buffer is intact on disk. Point Claude at it explicitly. +# Post-compaction recovery (issue #9): the conversation was just summarized, +# but this session's buffer is intact on disk. Inline its TAIL directly into +# this SessionStart block instead of only pointing at the file - a bare +# pointer costs the model a tool call it may not make, right where +# post-compaction recall is weakest. Bounded to the last N lines, EACH also +# capped at TL_COMPACT_TAIL_LINE_CHARS characters (via the awk pass below) - +# not just line count. A record's Bash `description` and Edit/Write/ +# NotebookEdit `file_path` fields are never length-clamped in +# session-capture.sh (only `command` and the other free-text fields are), so +# without this hook's OWN cap an unusually long one of those would still +# inline verbatim; capping here, at the point this block's own bounded-size +# claim is made, holds regardless of what any capture-side branch does or +# later stops doing. The full-file pointer is kept for anything older than +# the tail. +TL_COMPACT_TAIL_LINES=30 +TL_COMPACT_TAIL_LINE_CHARS=300 if [ "$src" = "compact" ] && [ -n "$sid" ] && [ -f "$bufdir/session-$sid.md" ]; then + buf="$bufdir/session-$sid.md" echo - echo "🧷 Context was just compacted. This session's action buffer survived at \`${bufdir#"$root"/}/session-$sid.md\` - read it to recover what you did before the compaction. The raw actions persist even though the conversation summary dropped detail." + echo "🧷 Context was just compacted. The last $TL_COMPACT_TAIL_LINES line(s) of this session's action buffer are inlined below to recover what you did before the compaction, without an extra read - the raw actions persist even though the conversation summary dropped detail. Full history (if the session ran longer than this tail) is at \`${bufdir#"$root"/}/session-$sid.md\`." + echo '```' + tail -n "$TL_COMPACT_TAIL_LINES" "$buf" 2>/dev/null | awk -v max="$TL_COMPACT_TAIL_LINE_CHARS" ' + { if (length($0) > max) print substr($0, 1, max) " …[line truncated]" + else print + }' + echo '```' fi # Surface unconsumed buffers from OTHER sessions. Exclude the current session's diff --git a/tests/run.sh b/tests/run.sh index 59bccc7..da14c1f 100644 --- a/tests/run.sh +++ b/tests/run.sh @@ -254,6 +254,13 @@ eq "tl_safe_sid rejects '.'" "$(tl_safe_sid '.')" "" eq "tl_safe_sid rejects '..'" "$(tl_safe_sid '..')" "" eq "tl_safe_sid rejects empty" "$(tl_safe_sid '')" "" +# 5a2. tl_clean_ctrl unit tests: control chars AND backticks both become a +# space (issue #9 review finding: a stamped trigger/reason containing a +# run of backticks could otherwise break the markdown fence the new +# inline-tail feature wraps buffer content in). +eq "tl_clean_ctrl replaces control chars with space" "$(tl_clean_ctrl "$(printf 'a\tb')")" "a b" +eq "tl_clean_ctrl replaces backticks with space" "$(tl_clean_ctrl 'trig```ger')" "trig ger" + # 5c. tl_data_dir honors an absolute THROUGHLINE_DATA_DIR override ABS_DIR="$WORK/abs-data" eq "tl_data_dir honors absolute THROUGHLINE_DATA_DIR" \ @@ -280,10 +287,46 @@ cap '{"session_id":"T","tool_name":"Bash","tool_input":{"description":"x","comma printf '%s' '{"session_id":"T","trigger":"manual"}' | sh "$H/session-precompact.sh" has "PreCompact writes a compaction-boundary marker" "$(cat "$BUF/session-T.md")" '