Skip to content

Commit af9bf78

Browse files
authored
Merge pull request #47 from Digital-Threads/feat/session-end-clear-catchup
feat: SessionEnd(clear) catch-up so /clear keeps the last segment (0.26.0)
2 parents 0bb52af + c472d58 commit af9bf78

8 files changed

Lines changed: 115 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.26.0] - 2026-06-14
11+
12+
### Added
13+
- **`/clear` no longer drops the last segment.** A new `SessionEnd` hook (wired
14+
by `install-hooks --auto-capture`) runs the same transcript catch-up as `Stop`
15+
when the session ends with reason `clear` — the last chance to capture the
16+
final segment before `/clear` orphans the transcript. Gated to `clear` so it
17+
doesn't re-process what `Stop` already handled on other exits.
18+
- **`recall --json`**`task-journal recall "<context>" --json` emits the
19+
cross-project memory hits as a JSON array (task_id, project_hash, event_type,
20+
text, score) for machine consumers like the Loom host; empty/missing memory
21+
yields `[]`.
22+
1023
## [0.25.1] - 2026-06-14
1124

1225
### Fixed

Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ members = [
77
]
88

99
[workspace.package]
10-
version = "0.25.1"
10+
version = "0.26.0"
1111
edition = "2021"
1212
rust-version = "1.88"
1313
license = "MIT"

crates/tj-cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ default = ["embed"]
2323
embed = ["tj-core/embed"]
2424

2525
[dependencies]
26-
tj-core = { package = "task-journal-core", version = "0.25.1", path = "../tj-core", default-features = false }
26+
tj-core = { package = "task-journal-core", version = "0.26.0", path = "../tj-core", default-features = false }
2727
anyhow = { workspace = true }
2828
clap = { workspace = true }
2929
tracing = { workspace = true }

crates/tj-cli/src/main.rs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1809,7 +1809,7 @@ fn main() -> Result<()> {
18091809
{ "type": "command", "command": cmd },
18101810
]}]),
18111811
);
1812-
for ev in ["PostToolUse", "Stop", "PreCompact"] {
1812+
for ev in ["PostToolUse", "Stop", "PreCompact", "SessionEnd"] {
18131813
obj.insert(
18141814
ev.to_string(),
18151815
serde_json::json!([{ "matcher": "", "hooks": [{ "type": "command", "command": cmd }] }]),
@@ -2493,6 +2493,21 @@ runs in the background and won't block you; it only fills gaps and never closes
24932493
return Ok(());
24942494
}
24952495

2496+
// SessionEnd with reason "clear": /clear discards the conversation
2497+
// and the transcript orphans, so this is the LAST chance to capture
2498+
// the final segment. Extracted to its own function so its locals do
2499+
// NOT bloat `main`'s already-huge stack frame — inlining it here
2500+
// overflowed the 1 MiB Windows main-thread stack on every command.
2501+
if kind == "SessionEnd" {
2502+
return run_session_end_catchup(
2503+
&payload,
2504+
&events_path,
2505+
&project_hash,
2506+
&backend,
2507+
live_session_id.as_deref(),
2508+
);
2509+
}
2510+
24962511
// Drain any pending entries first (Task 10 fills the real-classifier branch).
24972512
drain_pending(
24982513
&events_path,
@@ -3924,6 +3939,58 @@ fn run_nudge() -> anyhow::Result<()> {
39243939
}
39253940

39263941
/// Proactive recall injector (opt-in hook). Reads the UserPromptSubmit payload
3942+
/// SessionEnd(reason=clear) catch-up: enqueue transcript chunks newer than the
3943+
/// active task's last event, then spawn the classify-worker. Kept OUT of `main`
3944+
/// so its locals don't grow `main`'s already-huge stack frame — inlining it
3945+
/// overflowed the 1 MiB Windows main-thread stack on every command.
3946+
fn run_session_end_catchup(
3947+
payload: &serde_json::Value,
3948+
events_path: &std::path::Path,
3949+
project_hash: &str,
3950+
backend: &str,
3951+
live_session_id: Option<&str>,
3952+
) -> anyhow::Result<()> {
3953+
let reason = payload.get("reason").and_then(|x| x.as_str()).unwrap_or("");
3954+
if reason != "clear" || !events_path.exists() {
3955+
return Ok(());
3956+
}
3957+
let state_path = tj_core::paths::state_dir()?.join(format!("{project_hash}.sqlite"));
3958+
let conn = tj_core::db::open(&state_path)?;
3959+
tj_core::db::ingest_new_events(&conn, events_path, project_hash)?;
3960+
let Some(tc) = recent_task_contexts(&conn, 1)?.into_iter().next() else {
3961+
return Ok(());
3962+
};
3963+
let last_event_ts: Option<String> = conn
3964+
.query_row(
3965+
"SELECT timestamp FROM events_index WHERE task_id=?1 ORDER BY timestamp DESC LIMIT 1",
3966+
rusqlite::params![&tc.task_id],
3967+
|r| r.get::<_, String>(0),
3968+
)
3969+
.ok();
3970+
let transcript_path = payload
3971+
.get("transcript_path")
3972+
.and_then(|x| x.as_str())
3973+
.map(std::path::PathBuf::from);
3974+
if let Some(tp) = transcript_path.as_ref() {
3975+
if tp.exists() {
3976+
let enq = enqueue_transcript_chunks_since_last_event(
3977+
tp,
3978+
events_path,
3979+
project_hash,
3980+
backend,
3981+
last_event_ts.as_deref(),
3982+
"SessionEndChunk",
3983+
live_session_id,
3984+
)
3985+
.unwrap_or(0);
3986+
if enq > 0 && std::env::var("TJ_DISABLE_CLASSIFY_SPAWN").is_err() {
3987+
let _ = spawn_classify_worker(backend);
3988+
}
3989+
}
3990+
}
3991+
Ok(())
3992+
}
3993+
39273994
/// from stdin, keyword-searches the global index for relevant prior
39283995
/// decisions/rejections/constraints across all projects, and emits a budgeted
39293996
/// `additionalContext` block. Never blocks the prompt: any miss, empty result,

crates/tj-cli/tests/cli.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,11 +884,38 @@ fn install_hooks_auto_capture_wires_all_events() {
884884
"PostToolUse",
885885
"Stop",
886886
"PreCompact",
887+
"SessionEnd",
887888
] {
888889
assert!(content.contains(ev), "--auto-capture must wire {ev}");
889890
}
890891
}
891892

893+
#[test]
894+
fn session_end_hook_is_clean_noop_without_journal() {
895+
// SessionEnd(clear) with no journal yet must exit cleanly (it's the
896+
// last-chance catch-up; nothing to catch when there's no project journal).
897+
let dir = assert_fs::TempDir::new().unwrap();
898+
let proj = assert_fs::TempDir::new().unwrap();
899+
for reason in ["clear", "logout"] {
900+
let payload = serde_json::json!({
901+
"hook_event_name": "SessionEnd",
902+
"reason": reason,
903+
"session_id": "s-end",
904+
"transcript_path": "/nonexistent/x.jsonl",
905+
"cwd": proj.path().to_string_lossy(),
906+
})
907+
.to_string();
908+
Command::cargo_bin("task-journal")
909+
.unwrap()
910+
.current_dir(proj.path())
911+
.env("XDG_DATA_HOME", dir.path())
912+
.args(["ingest-hook", "--backend", "hybrid"])
913+
.write_stdin(payload)
914+
.assert()
915+
.success();
916+
}
917+
}
918+
892919
#[test]
893920
fn install_hooks_merges_and_preserves_third_party_hooks() {
894921
let dir = assert_fs::TempDir::new().unwrap();

crates/tj-mcp/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ path = "src/main.rs"
1717

1818
[dependencies]
1919
# Lean: the MCP server doesn't embed yet, so it skips the model2vec backend.
20-
tj-core = { package = "task-journal-core", version = "0.25.1", path = "../tj-core", default-features = false }
20+
tj-core = { package = "task-journal-core", version = "0.26.0", path = "../tj-core", default-features = false }
2121
anyhow = { workspace = true }
2222
tokio = { workspace = true }
2323
tracing = { workspace = true }

plugin/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "task-journal",
3-
"version": "0.25.1",
3+
"version": "0.26.0",
44
"description": "Append-only journal of AI-coding task reasoning chains: hypotheses, decisions, rejections, evidence. Renders compact resume packs so an agent can pick up a 2-week-old task with full context.",
55
"author": {
66
"name": "Mher Shahinyan"

0 commit comments

Comments
 (0)