diff --git a/Cargo.lock b/Cargo.lock index cd82559..83317fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,7 +91,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -102,7 +102,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -890,7 +890,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -966,7 +966,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2160,7 +2160,6 @@ version = "0.0.0-dev" dependencies = [ "anyhow", "clap", - "lash-internal-core", "lash-internal-protocol-rlm", "lash-internal-rlm-types", "lash-internal-sqlite-store", @@ -2187,7 +2186,7 @@ dependencies = [ [[package]] name = "lash-internal-core" version = "0.0.0-dev" -source = "git+https://github.com/Ascending-AI/lash.git?rev=8dac97fa5#8dac97fa5dab0aeae14733ff3ddd860e53cabad1" +source = "git+https://github.com/Ascending-AI/lash.git?rev=9debf5aab75baa3074c27cf42563e169a79ec396#9debf5aab75baa3074c27cf42563e169a79ec396" dependencies = [ "arc-swap", "async-trait", @@ -2214,7 +2213,7 @@ dependencies = [ [[package]] name = "lash-internal-http-transport" version = "0.0.0-dev" -source = "git+https://github.com/Ascending-AI/lash.git?rev=8dac97fa5#8dac97fa5dab0aeae14733ff3ddd860e53cabad1" +source = "git+https://github.com/Ascending-AI/lash.git?rev=9debf5aab75baa3074c27cf42563e169a79ec396#9debf5aab75baa3074c27cf42563e169a79ec396" dependencies = [ "async-trait", "bytes", @@ -2228,7 +2227,7 @@ dependencies = [ [[package]] name = "lash-internal-lashlang" version = "0.0.0-dev" -source = "git+https://github.com/Ascending-AI/lash.git?rev=8dac97fa5#8dac97fa5dab0aeae14733ff3ddd860e53cabad1" +source = "git+https://github.com/Ascending-AI/lash.git?rev=9debf5aab75baa3074c27cf42563e169a79ec396#9debf5aab75baa3074c27cf42563e169a79ec396" dependencies = [ "async-trait", "compact_str 0.9.1", @@ -2252,7 +2251,7 @@ dependencies = [ [[package]] name = "lash-internal-lashlang-runtime" version = "0.0.0-dev" -source = "git+https://github.com/Ascending-AI/lash.git?rev=8dac97fa5#8dac97fa5dab0aeae14733ff3ddd860e53cabad1" +source = "git+https://github.com/Ascending-AI/lash.git?rev=9debf5aab75baa3074c27cf42563e169a79ec396#9debf5aab75baa3074c27cf42563e169a79ec396" dependencies = [ "async-trait", "chrono", @@ -2263,7 +2262,6 @@ dependencies = [ "lash-internal-trace", "serde", "serde_json", - "sha2", "thiserror 2.0.19", "tokio", "tokio-util", @@ -2274,7 +2272,7 @@ dependencies = [ [[package]] name = "lash-internal-llm-tools" version = "0.0.0-dev" -source = "git+https://github.com/Ascending-AI/lash.git?rev=8dac97fa5#8dac97fa5dab0aeae14733ff3ddd860e53cabad1" +source = "git+https://github.com/Ascending-AI/lash.git?rev=9debf5aab75baa3074c27cf42563e169a79ec396#9debf5aab75baa3074c27cf42563e169a79ec396" dependencies = [ "async-trait", "jsonschema", @@ -2288,7 +2286,7 @@ dependencies = [ [[package]] name = "lash-internal-llm-transport" version = "0.0.0-dev" -source = "git+https://github.com/Ascending-AI/lash.git?rev=8dac97fa5#8dac97fa5dab0aeae14733ff3ddd860e53cabad1" +source = "git+https://github.com/Ascending-AI/lash.git?rev=9debf5aab75baa3074c27cf42563e169a79ec396#9debf5aab75baa3074c27cf42563e169a79ec396" dependencies = [ "async-trait", "bytes", @@ -2302,7 +2300,7 @@ dependencies = [ [[package]] name = "lash-internal-plugin-mcp" version = "0.0.0-dev" -source = "git+https://github.com/Ascending-AI/lash.git?rev=8dac97fa5#8dac97fa5dab0aeae14733ff3ddd860e53cabad1" +source = "git+https://github.com/Ascending-AI/lash.git?rev=9debf5aab75baa3074c27cf42563e169a79ec396#9debf5aab75baa3074c27cf42563e169a79ec396" dependencies = [ "async-trait", "base64 0.22.1", @@ -2326,7 +2324,7 @@ dependencies = [ [[package]] name = "lash-internal-plugin-process-controls" version = "0.0.0-dev" -source = "git+https://github.com/Ascending-AI/lash.git?rev=8dac97fa5#8dac97fa5dab0aeae14733ff3ddd860e53cabad1" +source = "git+https://github.com/Ascending-AI/lash.git?rev=9debf5aab75baa3074c27cf42563e169a79ec396#9debf5aab75baa3074c27cf42563e169a79ec396" dependencies = [ "async-trait", "lash-internal-core", @@ -2338,19 +2336,19 @@ dependencies = [ [[package]] name = "lash-internal-plugin-tool-output-budget" version = "0.0.0-dev" -source = "git+https://github.com/Ascending-AI/lash.git?rev=8dac97fa5#8dac97fa5dab0aeae14733ff3ddd860e53cabad1" +source = "git+https://github.com/Ascending-AI/lash.git?rev=9debf5aab75baa3074c27cf42563e169a79ec396#9debf5aab75baa3074c27cf42563e169a79ec396" dependencies = [ "lash-internal-core", "lash-internal-protocol-standard", + "lash-internal-sansio", "serde", "serde_json", - "sha2", ] [[package]] name = "lash-internal-protocol-rlm" version = "0.0.0-dev" -source = "git+https://github.com/Ascending-AI/lash.git?rev=8dac97fa5#8dac97fa5dab0aeae14733ff3ddd860e53cabad1" +source = "git+https://github.com/Ascending-AI/lash.git?rev=9debf5aab75baa3074c27cf42563e169a79ec396#9debf5aab75baa3074c27cf42563e169a79ec396" dependencies = [ "async-trait", "jsonschema", @@ -2364,7 +2362,6 @@ dependencies = [ "serde", "serde_bytes", "serde_json", - "sha2", "syn 2.0.119", "thiserror 2.0.19", "tokio", @@ -2374,7 +2371,7 @@ dependencies = [ [[package]] name = "lash-internal-protocol-standard" version = "0.0.0-dev" -source = "git+https://github.com/Ascending-AI/lash.git?rev=8dac97fa5#8dac97fa5dab0aeae14733ff3ddd860e53cabad1" +source = "git+https://github.com/Ascending-AI/lash.git?rev=9debf5aab75baa3074c27cf42563e169a79ec396#9debf5aab75baa3074c27cf42563e169a79ec396" dependencies = [ "async-trait", "lash-internal-core", @@ -2386,7 +2383,7 @@ dependencies = [ [[package]] name = "lash-internal-provider-anthropic" version = "0.0.0-dev" -source = "git+https://github.com/Ascending-AI/lash.git?rev=8dac97fa5#8dac97fa5dab0aeae14733ff3ddd860e53cabad1" +source = "git+https://github.com/Ascending-AI/lash.git?rev=9debf5aab75baa3074c27cf42563e169a79ec396#9debf5aab75baa3074c27cf42563e169a79ec396" dependencies = [ "async-trait", "base64 0.22.1", @@ -2402,7 +2399,7 @@ dependencies = [ [[package]] name = "lash-internal-provider-auth" version = "0.0.0-dev" -source = "git+https://github.com/Ascending-AI/lash.git?rev=8dac97fa5#8dac97fa5dab0aeae14733ff3ddd860e53cabad1" +source = "git+https://github.com/Ascending-AI/lash.git?rev=9debf5aab75baa3074c27cf42563e169a79ec396#9debf5aab75baa3074c27cf42563e169a79ec396" dependencies = [ "async-trait", "base64 0.22.1", @@ -2420,7 +2417,7 @@ dependencies = [ [[package]] name = "lash-internal-provider-google" version = "0.0.0-dev" -source = "git+https://github.com/Ascending-AI/lash.git?rev=8dac97fa5#8dac97fa5dab0aeae14733ff3ddd860e53cabad1" +source = "git+https://github.com/Ascending-AI/lash.git?rev=9debf5aab75baa3074c27cf42563e169a79ec396#9debf5aab75baa3074c27cf42563e169a79ec396" dependencies = [ "async-trait", "base64 0.22.1", @@ -2431,7 +2428,6 @@ dependencies = [ "reqwest", "serde", "serde_json", - "sha2", "tokio", "uuid", ] @@ -2439,7 +2435,7 @@ dependencies = [ [[package]] name = "lash-internal-provider-openai" version = "0.0.0-dev" -source = "git+https://github.com/Ascending-AI/lash.git?rev=8dac97fa5#8dac97fa5dab0aeae14733ff3ddd860e53cabad1" +source = "git+https://github.com/Ascending-AI/lash.git?rev=9debf5aab75baa3074c27cf42563e169a79ec396#9debf5aab75baa3074c27cf42563e169a79ec396" dependencies = [ "async-trait", "base64 0.22.1", @@ -2452,7 +2448,6 @@ dependencies = [ "reqwest", "serde", "serde_json", - "sha2", "tokio", "tokio-tungstenite", "tracing", @@ -2462,7 +2457,7 @@ dependencies = [ [[package]] name = "lash-internal-remote-protocol" version = "0.0.0-dev" -source = "git+https://github.com/Ascending-AI/lash.git?rev=8dac97fa5#8dac97fa5dab0aeae14733ff3ddd860e53cabad1" +source = "git+https://github.com/Ascending-AI/lash.git?rev=9debf5aab75baa3074c27cf42563e169a79ec396#9debf5aab75baa3074c27cf42563e169a79ec396" dependencies = [ "base64 0.22.1", "lash-internal-core", @@ -2476,7 +2471,7 @@ dependencies = [ [[package]] name = "lash-internal-rlm-types" version = "0.0.0-dev" -source = "git+https://github.com/Ascending-AI/lash.git?rev=8dac97fa5#8dac97fa5dab0aeae14733ff3ddd860e53cabad1" +source = "git+https://github.com/Ascending-AI/lash.git?rev=9debf5aab75baa3074c27cf42563e169a79ec396#9debf5aab75baa3074c27cf42563e169a79ec396" dependencies = [ "lash-internal-sansio", "serde", @@ -2486,19 +2481,19 @@ dependencies = [ [[package]] name = "lash-internal-sansio" version = "0.0.0-dev" -source = "git+https://github.com/Ascending-AI/lash.git?rev=8dac97fa5#8dac97fa5dab0aeae14733ff3ddd860e53cabad1" +source = "git+https://github.com/Ascending-AI/lash.git?rev=9debf5aab75baa3074c27cf42563e169a79ec396#9debf5aab75baa3074c27cf42563e169a79ec396" dependencies = [ + "blake3", "jsonschema", "schemars", "serde", "serde_json", - "sha2", ] [[package]] name = "lash-internal-sqlite-store" version = "0.0.0-dev" -source = "git+https://github.com/Ascending-AI/lash.git?rev=8dac97fa5#8dac97fa5dab0aeae14733ff3ddd860e53cabad1" +source = "git+https://github.com/Ascending-AI/lash.git?rev=9debf5aab75baa3074c27cf42563e169a79ec396#9debf5aab75baa3074c27cf42563e169a79ec396" dependencies = [ "async-trait", "flate2", @@ -2511,7 +2506,6 @@ dependencies = [ "serde", "serde_bytes", "serde_json", - "sha2", "tokio", "tokio-rusqlite", "tokio-util", @@ -2522,7 +2516,7 @@ dependencies = [ [[package]] name = "lash-internal-standard-plugins" version = "0.0.0-dev" -source = "git+https://github.com/Ascending-AI/lash.git?rev=8dac97fa5#8dac97fa5dab0aeae14733ff3ddd860e53cabad1" +source = "git+https://github.com/Ascending-AI/lash.git?rev=9debf5aab75baa3074c27cf42563e169a79ec396#9debf5aab75baa3074c27cf42563e169a79ec396" dependencies = [ "async-trait", "lash-internal-core", @@ -2536,7 +2530,7 @@ dependencies = [ [[package]] name = "lash-internal-subagents" version = "0.0.0-dev" -source = "git+https://github.com/Ascending-AI/lash.git?rev=8dac97fa5#8dac97fa5dab0aeae14733ff3ddd860e53cabad1" +source = "git+https://github.com/Ascending-AI/lash.git?rev=9debf5aab75baa3074c27cf42563e169a79ec396#9debf5aab75baa3074c27cf42563e169a79ec396" dependencies = [ "async-trait", "lash-internal-core", @@ -2553,7 +2547,7 @@ dependencies = [ [[package]] name = "lash-internal-tool-support" version = "0.0.0-dev" -source = "git+https://github.com/Ascending-AI/lash.git?rev=8dac97fa5#8dac97fa5dab0aeae14733ff3ddd860e53cabad1" +source = "git+https://github.com/Ascending-AI/lash.git?rev=9debf5aab75baa3074c27cf42563e169a79ec396#9debf5aab75baa3074c27cf42563e169a79ec396" dependencies = [ "async-trait", "lash-internal-core", @@ -2566,7 +2560,7 @@ dependencies = [ [[package]] name = "lash-internal-tools" version = "0.0.0-dev" -source = "git+https://github.com/Ascending-AI/lash.git?rev=8dac97fa5#8dac97fa5dab0aeae14733ff3ddd860e53cabad1" +source = "git+https://github.com/Ascending-AI/lash.git?rev=9debf5aab75baa3074c27cf42563e169a79ec396#9debf5aab75baa3074c27cf42563e169a79ec396" dependencies = [ "async-trait", "lash-internal-core", @@ -2586,7 +2580,7 @@ dependencies = [ [[package]] name = "lash-internal-trace" version = "0.0.0-dev" -source = "git+https://github.com/Ascending-AI/lash.git?rev=8dac97fa5#8dac97fa5dab0aeae14733ff3ddd860e53cabad1" +source = "git+https://github.com/Ascending-AI/lash.git?rev=9debf5aab75baa3074c27cf42563e169a79ec396#9debf5aab75baa3074c27cf42563e169a79ec396" dependencies = [ "chrono", "lash-internal-sansio", @@ -2600,7 +2594,7 @@ dependencies = [ [[package]] name = "lash-internal-typescript" version = "0.0.0-dev" -source = "git+https://github.com/Ascending-AI/lash.git?rev=8dac97fa5#8dac97fa5dab0aeae14733ff3ddd860e53cabad1" +source = "git+https://github.com/Ascending-AI/lash.git?rev=9debf5aab75baa3074c27cf42563e169a79ec396#9debf5aab75baa3074c27cf42563e169a79ec396" dependencies = [ "lash-internal-lashlang", "ryu-js", @@ -2613,7 +2607,7 @@ dependencies = [ [[package]] name = "lash-regress" version = "0.0.0-dev" -source = "git+https://github.com/Ascending-AI/lash.git?rev=8dac97fa5#8dac97fa5dab0aeae14733ff3ddd860e53cabad1" +source = "git+https://github.com/Ascending-AI/lash.git?rev=9debf5aab75baa3074c27cf42563e169a79ec396#9debf5aab75baa3074c27cf42563e169a79ec396" dependencies = [ "hashbrown 0.16.1", "memchr", @@ -2622,7 +2616,7 @@ dependencies = [ [[package]] name = "lash-runtime" version = "0.0.0-dev" -source = "git+https://github.com/Ascending-AI/lash.git?rev=8dac97fa5#8dac97fa5dab0aeae14733ff3ddd860e53cabad1" +source = "git+https://github.com/Ascending-AI/lash.git?rev=9debf5aab75baa3074c27cf42563e169a79ec396#9debf5aab75baa3074c27cf42563e169a79ec396" dependencies = [ "async-trait", "futures-util", @@ -2652,7 +2646,6 @@ version = "0.0.0-dev" dependencies = [ "async-trait", "fff-search", - "lash-internal-core", "lash-internal-lashlang-runtime", "lash-internal-tool-support", "lash-runtime", @@ -3039,7 +3032,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3299,7 +3292,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -3634,7 +3627,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4006,7 +3999,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4065,7 +4058,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4477,7 +4470,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4725,7 +4718,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5607,7 +5600,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 793a28c..42a428a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,27 +49,26 @@ uuid = "1" # The CLI tracks one reviewed Lash release. Dependency updates are explicit, # testable compatibility changes rather than an implicit lockstep workspace. -lash = { package = "lash-runtime", git = "https://github.com/Ascending-AI/lash.git", rev = "8dac97fa5" } -lash-core = { package = "lash-internal-core", git = "https://github.com/Ascending-AI/lash.git", rev = "8dac97fa5" } -lash-lashlang-runtime = { package = "lash-internal-lashlang-runtime", git = "https://github.com/Ascending-AI/lash.git", rev = "8dac97fa5" } -lash-llm-tools = { package = "lash-internal-llm-tools", git = "https://github.com/Ascending-AI/lash.git", rev = "8dac97fa5" } -lash-plugin-mcp = { package = "lash-internal-plugin-mcp", git = "https://github.com/Ascending-AI/lash.git", rev = "8dac97fa5" } +lash = { package = "lash-runtime", git = "https://github.com/Ascending-AI/lash.git", rev = "9debf5aab75baa3074c27cf42563e169a79ec396" } +lash-lashlang-runtime = { package = "lash-internal-lashlang-runtime", git = "https://github.com/Ascending-AI/lash.git", rev = "9debf5aab75baa3074c27cf42563e169a79ec396" } +lash-llm-tools = { package = "lash-internal-llm-tools", git = "https://github.com/Ascending-AI/lash.git", rev = "9debf5aab75baa3074c27cf42563e169a79ec396" } +lash-plugin-mcp = { package = "lash-internal-plugin-mcp", git = "https://github.com/Ascending-AI/lash.git", rev = "9debf5aab75baa3074c27cf42563e169a79ec396" } # lash-plugin-plan-mode removed upstream -lash-plugin-process-controls = { package = "lash-internal-plugin-process-controls", git = "https://github.com/Ascending-AI/lash.git", rev = "8dac97fa5" } -lash-protocol-rlm = { package = "lash-internal-protocol-rlm", git = "https://github.com/Ascending-AI/lash.git", rev = "8dac97fa5" } -lash-protocol-standard = { package = "lash-internal-protocol-standard", git = "https://github.com/Ascending-AI/lash.git", rev = "8dac97fa5" } -lash-provider-anthropic = { package = "lash-internal-provider-anthropic", git = "https://github.com/Ascending-AI/lash.git", rev = "8dac97fa5" } -lash-provider-auth = { package = "lash-internal-provider-auth", git = "https://github.com/Ascending-AI/lash.git", rev = "8dac97fa5" } -lash-provider-google = { package = "lash-internal-provider-google", git = "https://github.com/Ascending-AI/lash.git", rev = "8dac97fa5" } -lash-provider-openai = { package = "lash-internal-provider-openai", git = "https://github.com/Ascending-AI/lash.git", rev = "8dac97fa5" } -lash-rlm-types = { package = "lash-internal-rlm-types", git = "https://github.com/Ascending-AI/lash.git", rev = "8dac97fa5" } -lash-sqlite-store = { package = "lash-internal-sqlite-store", git = "https://github.com/Ascending-AI/lash.git", rev = "8dac97fa5" } -lash-standard-plugins = { package = "lash-internal-standard-plugins", git = "https://github.com/Ascending-AI/lash.git", rev = "8dac97fa5" } -lash-subagents = { package = "lash-internal-subagents", git = "https://github.com/Ascending-AI/lash.git", rev = "8dac97fa5" } -lash-tool-support = { package = "lash-internal-tool-support", git = "https://github.com/Ascending-AI/lash.git", rev = "8dac97fa5" } -lash-trace = { package = "lash-internal-trace", git = "https://github.com/Ascending-AI/lash.git", rev = "8dac97fa5" } -lash-typescript = { package = "lash-internal-typescript", git = "https://github.com/Ascending-AI/lash.git", rev = "8dac97fa5" } -lashlang = { package = "lash-internal-lashlang", git = "https://github.com/Ascending-AI/lash.git", rev = "8dac97fa5" } +lash-plugin-process-controls = { package = "lash-internal-plugin-process-controls", git = "https://github.com/Ascending-AI/lash.git", rev = "9debf5aab75baa3074c27cf42563e169a79ec396" } +lash-protocol-rlm = { package = "lash-internal-protocol-rlm", git = "https://github.com/Ascending-AI/lash.git", rev = "9debf5aab75baa3074c27cf42563e169a79ec396" } +lash-protocol-standard = { package = "lash-internal-protocol-standard", git = "https://github.com/Ascending-AI/lash.git", rev = "9debf5aab75baa3074c27cf42563e169a79ec396" } +lash-provider-anthropic = { package = "lash-internal-provider-anthropic", git = "https://github.com/Ascending-AI/lash.git", rev = "9debf5aab75baa3074c27cf42563e169a79ec396" } +lash-provider-auth = { package = "lash-internal-provider-auth", git = "https://github.com/Ascending-AI/lash.git", rev = "9debf5aab75baa3074c27cf42563e169a79ec396" } +lash-provider-google = { package = "lash-internal-provider-google", git = "https://github.com/Ascending-AI/lash.git", rev = "9debf5aab75baa3074c27cf42563e169a79ec396" } +lash-provider-openai = { package = "lash-internal-provider-openai", git = "https://github.com/Ascending-AI/lash.git", rev = "9debf5aab75baa3074c27cf42563e169a79ec396" } +lash-rlm-types = { package = "lash-internal-rlm-types", git = "https://github.com/Ascending-AI/lash.git", rev = "9debf5aab75baa3074c27cf42563e169a79ec396" } +lash-sqlite-store = { package = "lash-internal-sqlite-store", git = "https://github.com/Ascending-AI/lash.git", rev = "9debf5aab75baa3074c27cf42563e169a79ec396" } +lash-standard-plugins = { package = "lash-internal-standard-plugins", git = "https://github.com/Ascending-AI/lash.git", rev = "9debf5aab75baa3074c27cf42563e169a79ec396" } +lash-subagents = { package = "lash-internal-subagents", git = "https://github.com/Ascending-AI/lash.git", rev = "9debf5aab75baa3074c27cf42563e169a79ec396" } +lash-tool-support = { package = "lash-internal-tool-support", git = "https://github.com/Ascending-AI/lash.git", rev = "9debf5aab75baa3074c27cf42563e169a79ec396" } +lash-trace = { package = "lash-internal-trace", git = "https://github.com/Ascending-AI/lash.git", rev = "9debf5aab75baa3074c27cf42563e169a79ec396" } +lash-typescript = { package = "lash-internal-typescript", git = "https://github.com/Ascending-AI/lash.git", rev = "9debf5aab75baa3074c27cf42563e169a79ec396" } +lashlang = { package = "lash-internal-lashlang", git = "https://github.com/Ascending-AI/lash.git", rev = "9debf5aab75baa3074c27cf42563e169a79ec396" } lash-autoresearch = { path = "crates/lash-autoresearch" } lash-cli-perf = { path = "crates/lash-cli-perf" } diff --git a/crates/lash-cli/src/app/mod.rs b/crates/lash-cli/src/app/mod.rs index 4e9b9fd..4df56a4 100644 --- a/crates/lash-cli/src/app/mod.rs +++ b/crates/lash-cli/src/app/mod.rs @@ -43,6 +43,19 @@ pub(crate) use self::projection::{ }; pub(crate) use self::queues::turn_input_display_text; +const PROCESS_RETENTION: std::time::Duration = std::time::Duration::from_secs(10); + +pub(crate) struct ProcessSnapshot { + pub view: ProcessHandleView, + pub updated_at_ms: Option, +} + +fn process_updated_within_retention(updated_at_ms: u64, now_ms: u64) -> bool { + now_ms + .checked_sub(updated_at_ms) + .is_some_and(|age_ms| age_ms <= PROCESS_RETENTION.as_millis() as u64) +} + fn user_turn_start_indices(blocks: &[UiTimelineItem]) -> Vec { blocks .iter() @@ -816,6 +829,14 @@ impl App { let before_tasks = self.processes.len(); self.processes.retain(ProcessView::is_visible); if self.processes.len() != before_tasks { + if self.selected_process_id.as_deref().is_some_and(|selected| { + !self + .processes + .iter() + .any(|process| process.process_id == selected) + }) { + self.selected_process_id = None; + } self.invalidate_height_cache(); self.dirty = true; } @@ -1046,8 +1067,11 @@ impl App { self.dirty = true; } - pub fn update_processes(&mut self, tasks: Vec) { + pub fn update_processes(&mut self, snapshots: Vec) { let now = std::time::Instant::now(); + let now_ms = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_or(0, |duration| duration.as_millis() as u64); let previous: HashMap = self .processes .iter() @@ -1055,29 +1079,33 @@ impl App { .map(|task| (task.process_id.clone(), task)) .collect(); let mut next = Vec::new(); - for task in tasks { - let old_status = previous - .get(&task.process_id) - .and_then(|item| item.status.is_terminal().then_some(item.status)); + for snapshot in snapshots { + let ProcessSnapshot { + view: task, + updated_at_ms, + } = snapshot; + let previous_process = previous.get(&task.process_id); let status = task.status.is_terminal().then_some(task.status); - let first_seen = previous - .get(&task.process_id) - .map(|item| item.first_seen) - .unwrap_or(now); + let first_seen = previous_process.map(|item| item.first_seen).unwrap_or(now); let status_duration = if status.is_some() { - previous - .get(&task.process_id) + previous_process .and_then(|item| item.status_duration) .or_else(|| Some(first_seen.elapsed())) } else { None }; - let transient_until = if status.is_some() && old_status != status { - Some(now + std::time::Duration::from_secs(10)) + let transient_until = if status.is_none() { + None + } else if previous_process.is_some_and(|item| !item.status.is_terminal()) { + Some(now + PROCESS_RETENTION) + } else if let Some(previous_process) = previous_process { + previous_process.transient_until + } else if updated_at_ms.is_some_and(|updated_at_ms| { + process_updated_within_retention(updated_at_ms, now_ms) + }) { + Some(now + PROCESS_RETENTION) } else { - previous - .get(&task.process_id) - .and_then(|item| item.transient_until) + None }; next.push(ProcessView::from_summary( task, @@ -1212,3 +1240,90 @@ impl App { } pub use lash_export::transcript::format_tokens; + +#[cfg(test)] +mod process_retention_tests { + use super::*; + + fn process( + process_id: &str, + status: ProcessStatus, + updated_at_ms: Option, + ) -> ProcessSnapshot { + ProcessSnapshot { + view: ProcessHandleView::new( + process_id, + lash::process::ProcessIdentity::new("subagent").with_label(Some("spawn")), + status, + ), + updated_at_ms, + } + } + + fn now_ms() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("current time follows Unix epoch") + .as_millis() as u64 + } + + #[test] + fn running_process_stays_visible_during_terminal_retention_then_prunes() { + let mut app = App::new("test-model".into(), "test".into(), "session".into()); + + app.update_processes(vec![process("process-1", ProcessStatus::Running, None)]); + app.update_processes(vec![process("process-1", ProcessStatus::Completed, None)]); + + assert_eq!(app.processes.len(), 1); + assert_eq!(app.processes[0].status, ProcessStatus::Completed); + assert!(app.processes[0].transient_until.is_some()); + + app.processes[0].transient_until = Some(std::time::Instant::now()); + app.on_tick(); + + assert!(app.processes.is_empty()); + } + + #[test] + fn first_seen_terminal_process_after_initial_snapshot_is_visible_transiently() { + let mut app = App::new("test-model".into(), "test".into(), "session".into()); + + app.update_processes(Vec::new()); + app.update_processes(vec![process( + "process-1", + ProcessStatus::Completed, + Some(now_ms()), + )]); + + assert_eq!(app.processes.len(), 1); + assert!(app.processes[0].transient_until.is_some()); + } + + #[test] + fn first_seen_terminal_process_in_initial_snapshot_is_not_resurrected() { + let mut app = App::new("test-model".into(), "test".into(), "session".into()); + + app.update_processes(vec![process( + "process-1", + ProcessStatus::Completed, + Some(now_ms() - PROCESS_RETENTION.as_millis() as u64 - 1), + )]); + + assert!(app.processes.is_empty()); + } + + #[test] + fn selected_process_clears_when_terminal_row_prunes() { + let mut app = App::new("test-model".into(), "test".into(), "session".into()); + + app.update_processes(vec![process("process-1", ProcessStatus::Running, None)]); + app.update_processes(vec![process("process-1", ProcessStatus::Completed, None)]); + assert!(app.select_next_process()); + app.processes[0].transient_until = Some(std::time::Instant::now()); + + app.on_tick(); + + assert!(app.processes.is_empty()); + assert!(app.selected_process_id.is_none()); + } +} diff --git a/crates/lash-cli/src/app/runtime_events.rs b/crates/lash-cli/src/app/runtime_events.rs index ccd1893..902b3ab 100644 --- a/crates/lash-cli/src/app/runtime_events.rs +++ b/crates/lash-cli/src/app/runtime_events.rs @@ -235,6 +235,8 @@ impl App { /// Process a semantic turn activity, updating display blocks. pub fn handle_turn_activity(&mut self, activity: TurnActivity) { match activity.event { + // Active-turn targeting is owned by `ActiveTurnTargetSink`. + TurnEvent::TurnStarted { .. } => {} TurnEvent::ReasoningDelta { text } => { self.live.model_output_chunks.push(ModelOutputChunk { correlation_id: activity.correlation_id, diff --git a/crates/lash-cli/src/autonomous.rs b/crates/lash-cli/src/autonomous.rs index 056d4e0..f93b7a8 100644 --- a/crates/lash-cli/src/autonomous.rs +++ b/crates/lash-cli/src/autonomous.rs @@ -75,6 +75,7 @@ impl AutonomousRenderer { pub(crate) fn handle(&mut self, activity: TurnActivity) -> Result<(), String> { match activity.event { + TurnEvent::TurnStarted { .. } => {} TurnEvent::AssistantProseDelta { text } => { if !text.is_empty() { self.streamed_text = true; diff --git a/crates/lash-cli/src/event.rs b/crates/lash-cli/src/event.rs index 5cc7ebf..218670d 100644 --- a/crates/lash-cli/src/event.rs +++ b/crates/lash-cli/src/event.rs @@ -1,7 +1,6 @@ use crossterm::event::Event as TermEvent; use lash::TurnActivity; use lash::observe::SessionProcessEventKind; -use lash::process::ProcessHandleView; use lash_tui_extensions::TuiHostEffect; use tokio::sync::mpsc; @@ -60,7 +59,7 @@ pub enum AppEvent { pub struct UiSnapshotResult { pub effects: Vec, - pub processes: Option>, + pub processes: Option>, pub duration: std::time::Duration, pub timed_out: bool, pub diagnostics: Vec, diff --git a/crates/lash-cli/src/interactive/input_handling/modals.rs b/crates/lash-cli/src/interactive/input_handling/modals.rs index f0837ad..a51f963 100644 --- a/crates/lash-cli/src/interactive/input_handling/modals.rs +++ b/crates/lash-cli/src/interactive/input_handling/modals.rs @@ -163,11 +163,11 @@ pub(super) async fn cancel_selected_process(app: &mut App, runtime: &Option app.update_processes( processes .into_iter() - .map(crate::ui_effects::observed_to_handle_summary) + .map(crate::ui_effects::observed_to_process_snapshot) .collect(), ), Err(err) => { diff --git a/crates/lash-cli/src/interactive/mod.rs b/crates/lash-cli/src/interactive/mod.rs index 67208ad..bbaca0f 100644 --- a/crates/lash-cli/src/interactive/mod.rs +++ b/crates/lash-cli/src/interactive/mod.rs @@ -672,8 +672,8 @@ pub(crate) async fn run_app( timed_out = result.timed_out, "applying UI snapshot" ); - if let Some(tasks) = result.processes { - app.update_processes(tasks); + if let Some(process_snapshots) = result.processes { + app.update_processes(process_snapshots); } for diagnostic in result.diagnostics { tracing::warn!(%diagnostic, "UI snapshot diagnostic"); diff --git a/crates/lash-cli/src/render/compositor/tests.rs b/crates/lash-cli/src/render/compositor/tests.rs index a9e649f..2e59713 100644 --- a/crates/lash-cli/src/render/compositor/tests.rs +++ b/crates/lash-cli/src/render/compositor/tests.rs @@ -381,11 +381,14 @@ mod tests { Arc::new(TuiExtensions::new(vec![chrome_ext]).expect("chrome extension")); app.set_ui_extensions(ui_extensions); app.set_chrome_state(chrome_state); - app.update_processes(vec![lash::process::ProcessHandleView::new( - "process-1", - lash::process::ProcessIdentity::new("lashlang").with_label(Some("responder")), - lash::process::ProcessStatus::Running, - )]); + app.update_processes(vec![crate::app::ProcessSnapshot { + view: lash::process::ProcessHandleView::new( + "process-1", + lash::process::ProcessIdentity::new("lashlang").with_label(Some("responder")), + lash::process::ProcessStatus::Running, + ), + updated_at_ms: None, + }]); sync_chrome_turn_status(&app); let snapshot = lash_tui::render_snapshot(80, 10, |frame| draw(frame, &mut app)); @@ -421,8 +424,8 @@ mod tests { #[test] fn process_dock_renders_below_input_and_overview_as_overlay() { let mut app = App::new("gpt-5.4".into(), "test".into(), "test-session-id".into()); - app.update_processes(vec![ - lash::process::ProcessHandleView::new( + app.update_processes(vec![crate::app::ProcessSnapshot { + view: lash::process::ProcessHandleView::new( "process-1", lash::process::ProcessIdentity::new("lashlang") .with_label(Some("responder")) @@ -437,7 +440,8 @@ mod tests { )), lash::process::ProcessStatus::Running, ), - ]); + updated_at_ms: None, + }]); app.select_next_process(); let areas = render::chrome_areas(&app, 80, 16); diff --git a/crates/lash-cli/src/startup/session.rs b/crates/lash-cli/src/startup/session.rs index 9ccc6b9..605c9d7 100644 --- a/crates/lash-cli/src/startup/session.rs +++ b/crates/lash-cli/src/startup/session.rs @@ -292,9 +292,11 @@ impl CliSessionOpener { .process_registry(process_registry) .trigger_store(trigger_store) .trace_level(self.trace_level); - if self.owns_visible_queued_turns { - builder = builder.disable_queued_work_driver(); - } + builder = if self.owns_visible_queued_turns { + builder.without_queued_work() + } else { + builder.with_native_queued_work() + }; if let Some(trace_path) = self.trace_jsonl_path.clone() { builder = builder.trace_jsonl_path(trace_path); } diff --git a/crates/lash-cli/src/turn_runner.rs b/crates/lash-cli/src/turn_runner.rs index 5535bce..9e91f9a 100644 --- a/crates/lash-cli/src/turn_runner.rs +++ b/crates/lash-cli/src/turn_runner.rs @@ -38,18 +38,38 @@ impl lash::TurnActivitySink for ActiveTurnTargetSink { async fn emit(&self, _activity: lash::TurnActivity) {} - async fn emit_for_turn(&self, turn_id: &str, _activity: lash::TurnActivity) { + async fn emit_for_turn(&self, emitted_turn_id: &str, activity: lash::TurnActivity) { + let (turn_id, is_turn_started) = match activity.event { + lash::TurnEvent::TurnStarted { turn_id } => { + debug_assert_eq!(emitted_turn_id, turn_id); + (turn_id, true) + } + _ => (emitted_turn_id.to_string(), false), + }; { let mut last_turn_id = self.last_turn_id.lock().expect("turn target lock"); - if last_turn_id.as_deref() == Some(turn_id) { + if !is_turn_started { + if let Some(announced_turn_id) = last_turn_id.as_deref() { + debug_assert_eq!( + announced_turn_id, emitted_turn_id, + "physical-turn activity must match the announced turn" + ); + } else { + tracing::warn!( + turn_id = emitted_turn_id, + "physical-turn activity arrived before TurnStarted; using emitted turn id" + ); + } + } + if last_turn_id.as_deref() == Some(turn_id.as_str()) { return; } - *last_turn_id = Some(turn_id.to_string()); + *last_turn_id = Some(turn_id.clone()); } if let Some(app_tx) = &self.app_tx { let _ = app_tx.send(AppEvent::ActiveTurnObserved { stream_id: self.stream_id, - turn_id: turn_id.to_string(), + turn_id, }); } } @@ -230,18 +250,25 @@ mod tests { use std::time::Duration; use lash::TurnActivitySink as _; + use lash::persistence::QueuedWorkStore as _; use super::*; use crate::app::App; use crate::event::{AppEvent, AppEventPump}; use crate::interactive::restore_cancelled_input_texts; - fn queued_cancel_test_core(provider: lash::provider::ProviderHandle) -> lash::LashCore { - lash::LashCore::standard_builder(lash::TurnBudget::Unbounded) - .commit_budget(lash::CommitBudget::bounded(1024 * 1024, 512)) - .queued_work_batching(lash::QueuedWorkBatchingConfig::new(1)) + fn queued_cancel_test_core( + provider: lash::provider::ProviderHandle, + ) -> ( + lash::LashCore, + Arc, + ) { + let store_factory = Arc::new(lash::persistence::InMemorySessionStoreFactory::new()); + let core = lash::LashCore::standard_builder(lash::TurnBudget::Unbounded) + .commit_budget(crate::host_policy::commit_budget()) + .queued_work_batching(crate::host_policy::queued_work_batching()) .effect_host(Arc::new( - lash::durability::InlineEffectHost::default() + lash::durability::NativeEffectHost::default() .allow_process_lifetime_completion_keys(), )) .attachment_store(Arc::new(lash::persistence::InMemoryAttachmentStore::new())) @@ -251,19 +278,110 @@ mod tests { .provider(provider) .model( lash::ModelSpec::builder("queued-cancel-test") - .context_window_tokens(16_384) + .context_window_tokens(1_000_000) .build() .expect("test model"), ) - .store_factory(Arc::new( - lash::persistence::InMemorySessionStoreFactory::new(), - )) - .disable_queued_work_driver() + .store_factory(store_factory.clone()) + .without_queued_work() .build(lash::persistence::LeaseOwnerIdentity::opaque( "lash-cli-test", "queued-cancel", )) - .expect("test core") + .expect("test core"); + (core, store_factory) + } + + async fn observed_turn_target(event_pump: &mut AppEventPump, stream_id: u64) -> String { + let observed = tokio::time::timeout(Duration::from_secs(2), event_pump.recv()) + .await + .expect("turn target event") + .expect("event pump remains open"); + let AppEvent::ActiveTurnObserved { + stream_id: observed_stream_id, + turn_id, + } = observed.event + else { + panic!("expected active-turn target event"); + }; + assert_eq!(observed_stream_id, stream_id); + turn_id + } + + async fn enqueue_selected_task( + store_factory: &lash::persistence::InMemorySessionStoreFactory, + session_id: &str, + source_key: &str, + task: String, + ) -> lash::persistence::QueuedWorkBatch { + let store = store_factory + .raw_store_for_testing(session_id) + .expect("opened session retains its in-memory store"); + store + .enqueue_queued_work( + lash::persistence::QueuedWorkBatchDraft::new( + session_id, + lash::persistence::DeliveryPolicy::EarliestSafeBoundary, + vec![lash::persistence::QueuedWorkPayload::agent_frame_task( + lash::testing::frame_node_id(session_id, source_key), + task, + None, + )], + ) + .with_source_key(source_key), + ) + .await + .expect("enqueue selected queued work") + } + + #[tokio::test] + async fn direct_turn_started_immediately_targets_exact_cancellation() { + let provider = lash::testing::TestProvider::builder() + .kind("direct-turn-started-cancel") + .complete(|_request| async { + std::future::pending::<()>().await; + unreachable!("provider future is cancelled with the turn") + }) + .build() + .into_handle(); + let (core, _) = queued_cancel_test_core(provider); + let session = core + .session("direct-turn-started-cancel") + .open() + .await + .expect("open test session"); + let mut event_pump = AppEventPump::new(); + let (_process_cancel, return_rx) = spawn_session_turn( + session.clone(), + TurnInput::text("wait for cancellation"), + 5, + Some(event_pump.sender()), + ); + + let turn_id = observed_turn_target(&mut event_pump, 5).await; + assert_eq!(turn_id, "cli-turn:5"); + let receipt = session + .request_turn_cancel( + &turn_id, + "direct-turn-started-cancel-request", + Some("lash-cli-test".to_string()), + Some("cancel from TurnStarted target".to_string()), + ) + .await + .expect("request exact direct-turn cancellation"); + assert!(matches!( + receipt.outcome, + lash::TurnCancelOutcome::Requested(_) + )); + + let done = tokio::time::timeout(Duration::from_secs(2), return_rx) + .await + .expect("direct turn settles") + .expect("direct turn returns report"); + assert!(matches!( + done.result.outcome, + lash::TurnOutcome::Stopped(lash::TurnStop::Cancelled { .. }) + )); } #[tokio::test] @@ -284,7 +402,7 @@ mod tests { }) .build() .into_handle(); - let core = queued_cancel_test_core(provider); + let (core, _) = queued_cancel_test_core(provider); let session = core .session("queued-cancel-test") .open() @@ -304,14 +422,7 @@ mod tests { .await .expect("queued drain reaches provider") .expect("provider-start signal"); - let observed = tokio::time::timeout(Duration::from_secs(2), event_pump.recv()) - .await - .expect("turn target event") - .expect("event pump remains open"); - let AppEvent::ActiveTurnObserved { stream_id, turn_id } = observed.event else { - panic!("expected active-turn target event"); - }; - assert_eq!(stream_id, 7); + let turn_id = observed_turn_target(&mut event_pump, 7).await; let dropped_steer = session .enqueue(TurnInput::text("dropped queued steer")) .id("queued-cancel-steer") @@ -338,7 +449,7 @@ mod tests { receipt.outcome, lash::TurnCancelOutcome::Requested(_) )); - process_cancel.cancel(); + drop(process_cancel); let done = tokio::time::timeout(Duration::from_secs(2), return_rx) .await @@ -368,9 +479,130 @@ mod tests { } #[tokio::test] - async fn active_turn_target_sink_tracks_each_distinct_physical_turn_id() { + async fn selected_batch_turn_started_immediately_targets_exact_cancellation() { + let provider = lash::testing::TestProvider::builder() + .kind("selected-turn-started-cancel") + .complete(|_request| async { + std::future::pending::<()>().await; + unreachable!("provider future is cancelled with the turn") + }) + .build() + .into_handle(); + let (core, store_factory) = queued_cancel_test_core(provider); + let session_id = "selected-turn-started-cancel"; + let session = core + .session(session_id) + .open() + .await + .expect("open test session"); + let selected = enqueue_selected_task( + &store_factory, + session_id, + "selected-cancel-source", + "selected queued input".to_string(), + ) + .await; + let mut event_pump = AppEventPump::new(); + let target_sink = ActiveTurnTargetSink::new(13, Some(event_pump.sender())); + let run_session = session.clone(); + let run = tokio::spawn(async move { + run_session + .queued_turn() + .batch_ids([selected.batch_id]) + .turn_id("selected-turn-started-cancel-id") + .stream_to(&target_sink) + .await + }); + + let turn_id = observed_turn_target(&mut event_pump, 13).await; + assert_eq!(turn_id, "selected-turn-started-cancel-id"); + let receipt = session + .request_turn_cancel( + &turn_id, + "selected-turn-started-cancel-request", + Some("lash-cli-test".to_string()), + Some("cancel selected drain from TurnStarted target".to_string()), + ) + .await + .expect("request exact selected-turn cancellation"); + assert!(matches!( + receipt.outcome, + lash::TurnCancelOutcome::Requested(_) + )); + + let outcome = tokio::time::timeout(Duration::from_secs(2), run) + .await + .expect("selected turn settles") + .expect("selected turn task joins") + .expect("selected drain succeeds"); + let report = outcome.turn.expect("selected drain ran a turn"); + assert!(matches!( + report.outcome, + lash::TurnOutcome::Stopped(lash::TurnStop::Cancelled { .. }) + )); + } + + #[tokio::test] + async fn large_selected_queued_input_commits_with_cli_budget() { + let provider = lash::testing::TestProvider::builder() + .kind("selected-commit-budget") + .complete(|_request| async { + Ok(lash::provider::LlmResponse { + parts: vec![lash::direct::LlmOutputPart::Text { + text: "large selected input committed".to_string(), + response_meta: None, + }], + response_metadata: Default::default(), + ..Default::default() + }) + }) + .build() + .into_handle(); + let (core, store_factory) = queued_cancel_test_core(provider); + let session_id = "selected-commit-budget"; + let session = core + .session(session_id) + .open() + .await + .expect("open test session"); + let selected = enqueue_selected_task( + &store_factory, + session_id, + "large-selected-source", + "x".repeat(96 * 1024), + ) + .await; + + let outcome = session + .queued_turn() + .batch_ids([selected.batch_id]) + .turn_id("large-selected-turn") + .run() + .await + .expect("large selected drain remains within CLI commit budget"); + let turn = outcome.turn.expect("large selected drain ran a turn"); + assert!(matches!( + turn.result.outcome, + lash::TurnOutcome::Finished(_) + )); + assert_eq!( + turn.result.assistant_output.safe_text, + "large selected input committed" + ); + } + + #[tokio::test] + async fn active_turn_target_sink_prefers_turn_started_and_tracks_successive_turns() { let mut event_pump = AppEventPump::new(); let sink = ActiveTurnTargetSink::new(11, Some(event_pump.sender())); + sink.emit_for_turn( + "physical-turn-a", + lash::TurnActivity::independent(lash::TurnEvent::TurnStarted { + turn_id: "physical-turn-a".to_string(), + }), + ) + .await; + assert_eq!(event_pump.lane_depths(), (1, 0, 0)); sink.emit_for_turn( "physical-turn-a", lash::TurnActivity::independent(lash::TurnEvent::ModelRequestStarted { @@ -378,10 +610,11 @@ mod tests { }), ) .await; + assert_eq!(event_pump.lane_depths(), (1, 0, 0)); sink.emit_for_turn( "physical-turn-b", - lash::TurnActivity::independent(lash::TurnEvent::ModelRequestStarted { - protocol_iteration: 1, + lash::TurnActivity::independent(lash::TurnEvent::TurnStarted { + turn_id: "physical-turn-b".to_string(), }), ) .await; @@ -400,4 +633,26 @@ mod tests { )); assert_eq!(event_pump.lane_depths(), (0, 0, 0)); } + + #[tokio::test] + async fn active_turn_target_sink_falls_back_to_first_activity_turn_id() { + let mut event_pump = AppEventPump::new(); + let sink = ActiveTurnTargetSink::new(12, Some(event_pump.sender())); + + sink.emit_for_turn( + "physical-turn-fallback", + lash::TurnActivity::independent(lash::TurnEvent::ModelRequestStarted { + protocol_iteration: 0, + }), + ) + .await; + + let observed = event_pump.recv().await.expect("fallback target event"); + assert!(matches!( + observed.event, + AppEvent::ActiveTurnObserved { stream_id: 12, ref turn_id } + if turn_id == "physical-turn-fallback" + )); + assert_eq!(event_pump.lane_depths(), (0, 0, 0)); + } } diff --git a/crates/lash-cli/src/ui_effects.rs b/crates/lash-cli/src/ui_effects.rs index 7806f56..bd8e1a9 100644 --- a/crates/lash-cli/src/ui_effects.rs +++ b/crates/lash-cli/src/ui_effects.rs @@ -217,10 +217,38 @@ fn replay_plugin_operation_receipt( /// onto the model/handle row the process dock renders. The dock is keyed on the /// grant-entry handle shape, so this is the one boundary that maps the unified /// facade vocabulary back to it. -pub(crate) fn observed_to_handle_summary( +pub(crate) fn observed_to_process_snapshot( process: lash::process::ObservedProcess, -) -> lash::process::ProcessHandleView { - lash::process::ProcessHandleView::new(process.process_id, process.identity, process.lifecycle) +) -> crate::app::ProcessSnapshot { + let lash::process::ObservedProcess { + process_id, + graph_key: _, + kind: _, + lifecycle, + identity, + status_label: _, + terminal: _, + disposition: _, + error: _, + created_at_ms: _, + updated_at_ms, + first_started: _, + lease_holder: _, + lease_expires_at_ms: _, + abandon_request: _, + input: _, + originator: _, + env_ref: _, + caused_by: _, + external_ref: _, + wait: _, + child_session_id: _, + label: _, + } = process; + crate::app::ProcessSnapshot { + view: lash::process::ProcessHandleView::new(process_id, identity, lifecycle), + updated_at_ms: Some(updated_at_ms), + } } fn push_plugin_operation_message(app: &mut App, output: &serde_json::Value) { @@ -234,8 +262,13 @@ pub(crate) async fn collect_ui_snapshot( ) -> crate::event::UiSnapshotResult { let started = std::time::Instant::now(); let mut diagnostics = Vec::new(); - let processes = match session.processes().list().await { - Ok(tasks) => Some(tasks.into_iter().map(observed_to_handle_summary).collect()), + let processes = match session.processes().list_all().await { + Ok(tasks) => Some( + tasks + .into_iter() + .map(observed_to_process_snapshot) + .collect(), + ), Err(err) => { diagnostics.push(format!("process snapshot failed: {err}")); None diff --git a/crates/lash-export/Cargo.toml b/crates/lash-export/Cargo.toml index 8138aa6..990fb27 100644 --- a/crates/lash-export/Cargo.toml +++ b/crates/lash-export/Cargo.toml @@ -7,7 +7,6 @@ publish = false [dependencies] lash = { workspace = true } -lash-core = { workspace = true } lash-protocol-rlm = { workspace = true } lash-sqlite-store = { workspace = true } lash-rlm-types = { workspace = true } diff --git a/crates/lash-export/src/html/entries.rs b/crates/lash-export/src/html/entries.rs index 5e88651..ec2d911 100644 --- a/crates/lash-export/src/html/entries.rs +++ b/crates/lash-export/src/html/entries.rs @@ -1,9 +1,9 @@ use std::collections::HashMap; use std::fmt::Write as _; +use lash::messages::PruneState; use lash::messages::{Message, MessageRole, Part, PartKind}; use lash::persistence::ChronologicalPayload; -use lash_core::session_model::PruneState; use super::escaping::{escape, escape_attr, json_highlight}; use super::prompt::{ diff --git a/crates/lash-export/src/html/stats.rs b/crates/lash-export/src/html/stats.rs index 1ca4fcf..9cbdd0d 100644 --- a/crates/lash-export/src/html/stats.rs +++ b/crates/lash-export/src/html/stats.rs @@ -1,6 +1,6 @@ use lash::messages::MessageRole; +use lash::messages::PruneState; use lash::persistence::ChronologicalPayload; -use lash_core::session_model::PruneState; use crate::LoadedSession; use crate::transcript::lashlang_transcript_step_from_event; diff --git a/crates/lash-export/src/lib.rs b/crates/lash-export/src/lib.rs index c504bd1..637d252 100644 --- a/crates/lash-export/src/lib.rs +++ b/crates/lash-export/src/lib.rs @@ -322,13 +322,14 @@ mod tests { .expect("valid fixture model"), ) .store_factory(Arc::new(SqliteSessionStoreFactory::new(store_root))) - .effect_host(Arc::new(lash::durability::InlineEffectHost::default())) + .effect_host(Arc::new(lash::durability::NativeEffectHost::default())) .attachment_store(Arc::new(lash::persistence::InMemoryAttachmentStore::new())) .process_env_store(Arc::new( lash::persistence::InMemoryProcessExecutionEnvStore::new(), )) .commit_budget(lash::CommitBudget::bounded(1024 * 1024, 512)) .queued_work_batching(lash::QueuedWorkBatchingConfig::new(1)) + .without_queued_work() .trace_jsonl_path(trace_path) } @@ -416,8 +417,10 @@ mod tests { std::fs::create_dir_all(&store_root).expect("create store root"); let database_path = store_root.join("durable-core.db"); let connection = rusqlite::Connection::open(&database_path).expect("open empty database"); + let expected_schema = i64::from(lash_sqlite_store::SESSION_SCHEMA_VERSION); + let previous_schema = expected_schema - 1; connection - .pragma_update(None, "user_version", 11_i64) + .pragma_update(None, "user_version", previous_schema) .expect("stamp old schema"); drop(connection); @@ -440,10 +443,9 @@ mod tests { assert_eq!(refused_root, store_root); assert_eq!(stamps.len(), 1); assert_eq!(stamps[0].database, "durable core"); - assert_eq!(stamps[0].found, 11); - let expected_schema = i64::from(lash_sqlite_store::SESSION_SCHEMA_VERSION); + assert_eq!(stamps[0].found, previous_schema); assert_eq!(stamps[0].expected, expected_schema); - assert!(message.contains("11")); + assert!(message.contains(&previous_schema.to_string())); assert!(message.contains(&expected_schema.to_string())); } other => panic!("expected typed schema refusal, got {other}"), diff --git a/crates/lash-search-tools/Cargo.toml b/crates/lash-search-tools/Cargo.toml index 34990cd..1701296 100644 --- a/crates/lash-search-tools/Cargo.toml +++ b/crates/lash-search-tools/Cargo.toml @@ -33,5 +33,5 @@ tokio = { workspace = true, features = ["rt", "time"] } tokio-util = { workspace = true, features = ["rt"] } [dev-dependencies] -lash-core = { workspace = true, features = ["testing"] } +lash = { workspace = true, features = ["testing"] } tempfile = { workspace = true } diff --git a/crates/lash-search-tools/src/tests.rs b/crates/lash-search-tools/src/tests.rs index 4df99d8..d150875 100644 --- a/crates/lash-search-tools/src/tests.rs +++ b/crates/lash-search-tools/src/tests.rs @@ -74,7 +74,7 @@ mod tests { .unwrap(); let tool = grep_provider_with_base_path(dir.path().to_path_buf()); - let result = lash_core::testing::run_tool(&tool, "grep", &json!({"query": "hello"})).await; + let result = lash::testing::run_tool(&tool, "grep", &json!({"query": "hello"})).await; assert!(result.is_success()); assert_eq!(result.value_for_projection()["count"], 2); assert_eq!( @@ -97,7 +97,7 @@ mod tests { std::fs::write(dir.path().join("alpha.rs"), "fn thing() {}\n").unwrap(); let tool = grep_provider_with_base_path(dir.path().to_path_buf()); - let result = lash_core::testing::run_tool(&tool, "grep", &json!({"query": "thing"})).await; + let result = lash::testing::run_tool(&tool, "grep", &json!({"query": "thing"})).await; assert!(result.is_success()); assert_eq!( result.value_for_projection()["files"][0]["path"], @@ -113,7 +113,7 @@ mod tests { std::fs::write(dir.path().join("alpha.rs"), "ctx\nctx\n").unwrap(); let tool = grep_provider_with_base_path(dir.path().to_path_buf()); - let result = lash_core::testing::run_tool(&tool, "grep", &json!({"query": "ctx"})).await; + let result = lash::testing::run_tool(&tool, "grep", &json!({"query": "ctx"})).await; assert!(result.is_success()); assert_eq!(result.value_for_projection()["count"], 2); assert_eq!(result.value_for_projection()["files"][0]["count"], 2); @@ -126,7 +126,7 @@ mod tests { let tool = grep_provider_with_base_path(dir.path().to_path_buf()); let result = - lash_core::testing::run_tool(&tool, "grep", &json!({"query": "missing"})).await; + lash::testing::run_tool(&tool, "grep", &json!({"query": "missing"})).await; assert!(result.is_success()); assert_eq!( result.value_for_projection()["matches"] @@ -146,7 +146,7 @@ mod tests { let query = "definitely missing ".repeat(20); let tool = grep_provider_with_base_path(dir.path().to_path_buf()); - let result = lash_core::testing::run_tool(&tool, "grep", &json!({"query": query})).await; + let result = lash::testing::run_tool(&tool, "grep", &json!({"query": query})).await; assert!( result.is_success(), @@ -172,7 +172,7 @@ mod tests { let tool = grep_provider_with_base_path(dir.path().to_path_buf()); assert!(tool.executor().backend.get().is_none()); - let result = lash_core::testing::run_tool(&tool, "grep", &json!({"query": "ctx"})).await; + let result = lash::testing::run_tool(&tool, "grep", &json!({"query": "ctx"})).await; assert!(result.is_success()); assert!(tool.executor().backend.get().is_some()); } @@ -185,7 +185,7 @@ mod tests { std::fs::write(dir.path().join("inner/inner.txt"), "banana in inner\n").unwrap(); let tool = grep_provider_with_base_path(dir.path().to_path_buf()); - let result = lash_core::testing::run_tool( + let result = lash::testing::run_tool( &tool, "grep", &json!({"query": "banana", "path": "inner"}), @@ -219,7 +219,7 @@ mod tests { std::fs::write(dir.path().join("other.txt"), "banana\n").unwrap(); let tool = grep_provider_with_base_path(dir.path().to_path_buf()); - let result = lash_core::testing::run_tool( + let result = lash::testing::run_tool( &tool, "grep", &json!({"query": "banana", "path": "notes.txt"}), @@ -269,7 +269,7 @@ mod tests { .unwrap(); let tool = grep_provider_with_base_path(dir.path().to_path_buf()); - let result = lash_core::testing::run_tool( + let result = lash::testing::run_tool( &tool, "grep", &json!({ @@ -313,7 +313,7 @@ mod tests { std::fs::write(outside.path().join("external.txt"), "banana\n").unwrap(); let tool = grep_provider_with_base_path(workspace.path().to_path_buf()); - let result = lash_core::testing::run_tool( + let result = lash::testing::run_tool( &tool, "grep", &json!({ @@ -345,7 +345,7 @@ mod tests { std::fs::write(outside.path().join("external.txt"), "banana\n").unwrap(); let tool = grep_provider_with_base_path(workspace.path().to_path_buf()); - let result = lash_core::testing::run_tool( + let result = lash::testing::run_tool( &tool, "grep", &json!({"query": format!("{} banana", outside.path().display())}), @@ -371,7 +371,7 @@ mod tests { std::fs::write(&file, "banana split\n").unwrap(); let tool = grep_provider_with_base_path(workspace.path().to_path_buf()); - let result = lash_core::testing::run_tool( + let result = lash::testing::run_tool( &tool, "grep", &json!({"query": format!("{} banana", file.display())}), @@ -410,7 +410,7 @@ mod tests { async fn test_grep_path_missing_returns_clear_error() { let workspace = TempDir::new().unwrap(); let tool = grep_provider_with_base_path(workspace.path().to_path_buf()); - let result = lash_core::testing::run_tool( + let result = lash::testing::run_tool( &tool, "grep", &json!({"query": "banana", "path": "/nonexistent/totally/fake"}), diff --git a/docs/architecture.html b/docs/architecture.html index 1b3559d..9af9e6f 100644 --- a/docs/architecture.html +++ b/docs/architecture.html @@ -64,7 +64,7 @@

Ownership boundary

┌──────┴────────┐ ▼ ▼ interactive TUI --print / JSON / RPC -

Execution modes such as standard and rlm are Lash protocol plugins. The CLI chooses and configures one for a session; it does not implement protocol semantics itself. Every session lives in one Lash catalog under ~/.lash/store/; listing, resume, startup, and tree discovery take durable identity from that catalog, while the CLI host roster supplies display metadata only. Catalog-only sessions remain addressable with fallback labels. Store preflight runs before provider or session startup, and shutdown flushes the trace sink before closing each core. This pin reads session schema 44, trace schema 10, and remote protocol 47.

+

Execution modes such as standard and rlm are Lash protocol plugins. The CLI chooses and configures one for a session; it does not implement protocol semantics itself. Every session lives in one Lash catalog under ~/.lash/store/; listing, resume, startup, and tree discovery take durable identity from that catalog, while the CLI host roster supplies display metadata only. Catalog-only sessions remain addressable with fallback labels. Store preflight runs before provider or session startup, and shutdown flushes the trace sink before closing each core. This pin reads session schema 45, trace schema 10, and remote protocol 49.

Dependency rule

diff --git a/docs/index.html b/docs/index.html index e40836a..8280045 100644 --- a/docs/index.html +++ b/docs/index.html @@ -243,7 +243,10 @@

Config File

"api_key": "sk-ant-...", "options": { "cache_retention": "long", - "max_output_tokens": 16384 + "max_output_tokens": 16384, + "reliability": { + "response_start_timeout": 30000 + } } } }, @@ -254,7 +257,9 @@

Config File

"docs": { "transport": "stdio", "command": "uvx", - "args": ["mcp-server-docs"] + "args": ["mcp-server-docs"], + "graceful_period_ms": 3000, + "post_kill_wait_ms": 1000 } }, "agent_models": {}, @@ -267,11 +272,11 @@

Config File

FieldTypePurpose active_providerstringKey of the currently active provider; must exist in providers. - providersmapPer-provider credentials and metadata, keyed by provider kind. Shared provider tuning lives under options, including cache_retention (none | short | long) and max_output_tokens. + providersmapPer-provider credentials and metadata, keyed by provider kind. Shared provider tuning lives under options, including cache_retention (none | short | long), max_output_tokens, and reliability.response_start_timeout in milliseconds. Omit the response-start timeout or set it to 0 to retain the legacy min(request, chunk) bound. execution_modestringDefault mode for fresh sessions: standard or rlm. rlm_dialectstringDefault language pinned to fresh RLM sessions: lashlang or typescript. auxiliary_secretsobjectSecrets for non-LLM services. Currently tavily_api_key. - mcp_serversmapMCP server configs by name. Discriminated by transport: stdio, streamable_http, or sse. + mcp_serversmapMCP server configs by name. Discriminated by transport: stdio, streamable_http, or sse. Each server may set graceful_period_ms before forced termination and post_kill_wait_ms after it. agent_modelsmapUser-overridable model names per subagent capability. model_defaultsmapFresh-session model + variant default per provider kind. Session resumes ignore this and use the persisted head. @@ -311,7 +316,7 @@

Environment Variables

Resume & Fork

Every session has a UUID and normally a generated human-readable name. --resume <id_or_name> accepts either; legacy .db filenames are refused as incompatible. Without an argument, /resume opens an interactive picker showing the 50 most recent live root sessions from Lash's unified catalog, sorted by recency. The host roster contributes display metadata only, so catalog-only sessions remain visible with fallback labels. The picker hides zero-turn sessions when any non-empty session exists; direct /resume <id-or-name> may still target any session.

-

The current durable compatibility markers are session schema 44, trace schema 10, and remote protocol 47. lash --version and /info report them.

+

The current durable compatibility markers are session schema 45, trace schema 10, and remote protocol 49. lash --version and /info report them.

/fork branches the current session at the current message and opens the fork in a new terminal window. Lash resolves the launcher from the running executable and falls back to finding lash on $PATH if that binary isn't launchable. The launched fork runs lash --resume <new-session-id> in the new terminal.

diff --git a/runbooks/process-dock-lifecycle/runbook.md b/runbooks/process-dock-lifecycle/runbook.md index 7a53bc8..1f8d55a 100644 --- a/runbooks/process-dock-lifecycle/runbook.md +++ b/runbooks/process-dock-lifecycle/runbook.md @@ -8,47 +8,59 @@ (header `Background`), the focused process can be cancelled (`Delete`), and — the invariant — **ending or deleting a session never ends a process by itself**. The dock uniformly reflects the session's durable process registry (`session.processes().list()`). A subagent -spawn is inline tool activity only and does not appear in the dock. +spawn is a durable Runtime Process and therefore renders in two places: inline tool +activity (`◆ spawn subagent · …`, footer `Running tool · spawn_agent`) and a transient dock +row (`Background`, `◆ running · subagent · spawn`). Its durable record in +`store/processes.db` moves from `process.first_started` to `process.completed`, carries an +observer edge to the parent session, and the settled dock row is pruned after the short +retention window. **Why this matters.** The Lash repository's CONTEXT.md → "Runtime Process" says its lifecycle is independent of any session and only runtime processes appear in the CLI process dock. That independence is the whole point of a durable process; if session teardown silently killed it, the durability guarantee would be a lie. -## Pre-flight harness gap (read before scoring) +## Pre-flight harness limits (read before scoring) -**A Runtime Process visible in the dock is not reachable under `--provider test`.** Verified -by dry-run against every deterministic scenario: +**A subagent Runtime Process is reachable under `--provider test`, but not long enough to +drive every lifecycle control.** At the pinned Lash revision, the deterministic +`rlm-subagent-smoke` scenario starts a durable process: - There is **no `/process` slash command** — the builtin set is `/clear /compact /controls /fork /tree /version /info /model /variant /mode /provider /logout /retry /resume /skills /help /exit` (`crates/lash-cli/src/command.rs`). The dock is a render surface fed by `session.processes().list()`, not a command. -- The RLM `rlm-subagent-smoke` scenario **does** spawn a subagent. It renders only as - inline tool activity — `◆ spawn subagent · …` with footer `Running tool · - spawn_agent`. It never raises a `Background` dock row and is not a durable Runtime - Process, so it cannot witness the deletion invariant. +- The RLM `rlm-subagent-smoke` scenario spawns a subagent registered as + `ProcessIdentity("subagent", label "spawn")`. It renders both inline tool activity and a + `Background` dock row, and writes its lifecycle plus observer edge to + `store/processes.db`. +- That deterministic subagent completes in under a second. The transient row is therefore + not reliably focusable before completion, so `Tab`/`Delete` and the session-deletion + invariant remain un-drivable with this scenario. - The deterministic test provider only returns canned responses, so no scenario leaves a - durable Runtime Process running; the dock stays empty (`app.processes.is_empty()` → - `crates/lash-cli/src/render/sections/docks.rs` draws nothing). A **Process Engine** that - produces dock processes is contributed by an installed plugin, not wired by the test - provider. + durable Runtime Process running long enough to focus, inspect, cancel, or test survival + across session deletion. A **Process Engine** contributed by an installed plugin can + provide such a long-running process for the positive procedure. Per [../RULES.md](../RULES.md)'s "missing capability → note as harness gap": this runbook -runs the **drivable negative-space gates** below under `--provider test`, and specifies the -**full positive procedure** for `--provider real` with a process-engine plugin. Do not -fabricate a dock process that the test provider cannot produce. +runs the drivable subagent admission/lifecycle gates and the empty-dock gate under +`--provider test`, then specifies the remaining positive procedure for `--provider real` +with a process-engine plugin. Do not pretend the sub-second process offers a focus/cancel +window that it does not. ## Scenario-specific golden rules -1. **Subagent ≠ dock process.** A spawned subagent is an inline tool activity; it must - **not** raise the `Background` dock. Confirming that separation is a real gate, not a - consolation prize. +1. **Subagent = inline activity + dock process.** A spawned subagent must render both the + inline `spawn_agent` activity and the transient `Background` process row. Either surface + missing is a contract failure. 2. **The deletion invariant is the crown jewel.** In the positive (real-provider) run, the process must **survive** its originating session being ended/deleted. A process that dies with its session is a hard fail. -3. **Don't invent a process.** If the dock is empty, gate the emptiness honestly; don't - read a tool activity as a dock entry. +3. **Prove admission durably.** The dock row must correspond to a `subagent / spawn` row in + `store/processes.db`, the `process.first_started` and `process.completed` lifecycle + events, and an observer edge to the parent session. +4. **Don't invent a control window.** The deterministic subagent is a real process, but its + sub-second lifetime does not make `Tab`/`Delete` or session-deletion survival drivable. ## Phase 0 — Pre-flight @@ -56,23 +68,55 @@ Per [../RULES.md](../RULES.md). For the negative-space gates, launch `scripts/lash-operator.py --provider test --scenario rlm-subagent-smoke -- -em rlm` and confirm the deterministic provider and idle prompt. -## Phase 1 — Drivable gates under `--provider test` (negative space) +## Phase 1 — Drivable gates under `--provider test` -**Subagent renders as a tool activity, not a dock process.** +**Subagent renders as both tool activity and a durable dock process.** Use a fresh fixed +`--lash-home $LH` so its process store can be checked immediately after settlement. ``` type Does your subagent tool work key enter -expect 15 Running tool +expect 15 Running tool · spawn_agent +expect 15 Background +expect 15 ◆ running · subagent · spawn clear expect 45 ■ subagent-ok screen 40 ``` -Gate: the spawn shows as `◆ spawn subagent · …` and the footer passes through `Running tool -· spawn_agent`; the settled value is `■ subagent-ok`. The `Background` dock header is -**absent** the entire time (inspect the `screen`). A `Background` dock appearing here would -itself be a contract surprise → investigate/report. Then `lash-exit 10`. +Gate: the spawn shows as `◆ spawn subagent · …`, the footer passes through `Running tool · +spawn_agent`, and the live process projection renders `Background` with `◆ running · +subagent · spawn`. Keep the `clear` after the running-tool gates so `■ subagent-ok` must +come from a fresh settled frame. In that settled `screen`, the transient `Background` row +is absent after its retention window. + +Before `lash-exit`, cross-check the fixed home's process store from a second shell: + +``` +sqlite3 -header -column "$LH/store/processes.db" " +SELECT process_id, identity_kind, identity_label, status +FROM processes +WHERE identity_kind = 'subagent' AND identity_label = 'spawn'; +SELECT sequence, event_type +FROM process_events +WHERE process_id = ( + SELECT process_id FROM processes + WHERE identity_kind = 'subagent' AND identity_label = 'spawn' +) +AND event_type IN ('process.first_started', 'process.completed') +ORDER BY sequence; +SELECT session_id, process_id +FROM process_observers +WHERE process_id = ( + SELECT process_id FROM processes + WHERE identity_kind = 'subagent' AND identity_label = 'spawn' +);" +``` + +Gate: exactly one process row reports `subagent`, `spawn`, and `completed`; exactly two +selected lifecycle rows appear in order, `process.first_started` then `process.completed`; +and exactly one observer row links that process id to the parent session. Then +`lash-exit 10`. **Empty dock has nothing to focus.** Relaunch `--scenario standard-echo`. With an idle empty prompt and no processes, the dock-focus keys have no target: @@ -91,11 +135,13 @@ to the plan-mode toggle, further evidence there is no dock to cycle). The proces (`Delete`) and **overview** (`Enter`) rungs cannot be exercised — no process exists to focus. Record this as the harness gap. Then `lash-exit 10`. -## Phase 2 — Full positive procedure (`--provider real`, needs a process-engine plugin) +## Phase 2 — Remaining positive procedure (`--provider real`, needs a process-engine plugin) -Not runnable under the deterministic provider; specified so a real-provider run (RLM mode -plus a plugin whose tool starts a **durable Runtime Process**, e.g. a long-running -background job that yields a process handle) can execute it verbatim. Launch +The deterministic subagent already proves process admission, dock visibility, completion, +and durable observation, but it is too short-lived for focus/cancel or the deletion +invariant. A real-provider run (RLM mode plus a plugin whose tool starts a **long-running +durable Runtime Process**, e.g. a background job that yields a process handle) can execute +the remaining controls verbatim. Launch `scripts/lash-operator.py --provider real -- --model -em rlm` (spends tokens — deliberate). @@ -122,17 +168,20 @@ tokens — deliberate). | Item | Objective gate | Verdict | Notes | |------|----------------|---------|-------| -| Subagent is a tool activity, not a dock process | `◆ spawn subagent` + `Running tool`, then post-`clear` `■ subagent-ok`; no `Background` dock | | | +| Subagent renders on both surfaces | `◆ spawn subagent` + `Running tool · spawn_agent` + `Background` / `◆ running · subagent · spawn`, then post-`clear` `■ subagent-ok` | | | +| Durable subagent lifecycle | one `subagent / spawn / completed` process row; `process.first_started → process.completed`; one parent-session observer edge | | | +| Transient row is pruned | settled post-`clear` screen has no `Background` row | | | | Empty dock has no focus target | `Tab` opens no overview; no `Background` header | | | | Dock process visible *(real only)* | `Background` row `◆ running · … `; process store row | | | | Cancel path *(real only)* | `running → cancelled`; terminal state in store | | | | Deletion invariant *(real only)* | process survives its session being ended | | | -| Harness gap recorded | dock-process rungs un-drivable under `--provider test` | | | +| Harness gap recorded | sub-second process leaves focus/cancel/deletion rungs un-drivable under `--provider test` | | | -**Aggregate (under `--provider test`):** does the subagent stay a tool activity while the -`Background` dock stays absent, and is the harness gap for dock-process lifecycle recorded -honestly rather than faked. **Full aggregate (real):** does a Runtime Process show, cancel, -and survive session deletion. +**Aggregate (under `--provider test`):** does the subagent render as both inline tool +activity and a transient, durably backed Runtime Process; does it settle and leave the +dock; does an unrelated empty session keep an empty dock; and is the missing sub-second +focus/cancel/deletion window recorded honestly. **Full aggregate (real):** does a +long-running Runtime Process show, focus, cancel, and survive session deletion. --- diff --git a/runbooks/resume-picker/runbook.md b/runbooks/resume-picker/runbook.md index 693144c..1877c4c 100644 --- a/runbooks/resume-picker/runbook.md +++ b/runbooks/resume-picker/runbook.md @@ -64,18 +64,22 @@ key enter expect 10 Session expect 10 id screen 40 -key esc -``` - -Then roll once more so B becomes a non-current, hidden empty: - -``` wait 2 +key esc +clear type /clear +expect 6 ❯ /clear key enter expect 10 Started new session ``` +The post-`Esc` `clear` drops stale modal frames, then typing `/clear` must repaint the +editor draft as `❯ /clear`. If `/info` were still open, the typed bytes would not render in +the editor. This action-driven gate therefore proves dismissal and correct byte routing; +the subsequent `Started new session` gate proves submission. The explicit wait only spaces +session-file writes so their timestamp filenames cannot collide. B is now a non-current, +hidden empty. + State: A non-empty (non-current), B empty (non-current, hideable), C empty (current). ## Phase 2 — Hiding gate (mixed store) @@ -87,26 +91,26 @@ key enter expect 10 Resume Session screen 22 key esc -expect 6 Message · / for commands +clear +type /resume +expect 6 ❯ /resume +key enter +expect 10 Resumed: ``` Gate: the picker header is `Resume Session (1/1)` and the single row is A's preview — `just now 1 hello from pty …`. The empty session B is **absent**, and `No messages yet` does **not** appear. A picker that lists B (or shows `No messages yet` here) is a hiding-rule -violation → Abort/RCA. +violation → Abort/RCA. After `Esc`, typing the direct-target command must repaint the editor +draft as `❯ /resume`; if the picker were still open the bytes would instead route to its +`Search:` field. This action-driven draft-row gate proves picker dismissal and correct byte +routing without depending on an unchanged idle-placeholder row. ## Phase 3 — Direct-target escape hatch -Target the hidden empty session B directly by its catalog id (from Phase 1): - -``` -clear -type /resume -key enter -expect 10 Resumed: -``` - -Gate: `Resumed: ` — the picker suppressed B, but the direct form reached it. +The command typed as the Phase 2 dismissal gate targets the hidden empty session B directly +by its catalog id (from Phase 1). Gate: `Resumed: ` — the picker suppressed B, but the +direct form reached it. **Rigor (contract check on the identifier surface).** CONTEXT.md → "Operator UI" and `docs/index.html` present `/resume ` as accepting the session **id or @@ -150,9 +154,9 @@ thing to resume. Then `kill`. |------|----------------|---------|-------| | Hides empty when non-empty exists | `Resume Session (1/1)`, only `hello from pty`, no `No messages yet` | | | | Current session excluded | current session absent from the list | | | -| Direct target reaches hidden session | `Resumed: ` | | | +| Direct target reaches hidden session | post-picker `❯ /resume` draft-row repaint, then `Resumed: ` | | | | Only-empty shows all | every row `No messages yet` | | | -| Identifier surface (rigor) | `/info` renders `Session`, then separate `name ` and `id ` rows | | | +| Identifier surface (rigor) | `/info` renders `Session`, then separate `name ` and `id ` rows; post-`Esc` `❯ /clear` proves dismissal | | | **Aggregate:** does the picker hide zero-turn sessions only when a non-empty exists, show `No messages yet` when only empties exist, exclude the current session, and does the direct