authorgravatar for 71498452+StiensWout@users.noreply.github.comWout Stiens <71498452+StiensWout@users.noreply.github.com> 2026-10-09 07:45:58+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-09 00:45:58-05:00
log3b6af0bd1600f034b0466e1b8ff017fbabeba910
tree040d631e5f69a01196f32669eac674e9c17758f3
parent563645cf6e104ad930f4c9b209678a77eef07e65
signature Signed by PGP key B5690EEEBB952194

fix(server): Pi loads every selected skill without losing prompt text (#17194)


14 files changed, 185 insertions(+), 173 deletions(-)

apps/server/src/orchestration-v2/testkit/fixtures/message_steering/pi_transcript.ndjson-2
......@@ -1,10 +1,8 @@
11{"type":"transcript_start","provider":"pi","protocol":"pi.rpc-jsonl","version":"1.0.0","scenario":"message_steering","metadata":{"generatedBy":"live-pi-recorder","piVersion":"1.0.0","model":"openrouter/deepseek/deepseek-v4-flash","launchArgs":"--provider openrouter --model deepseek/deepseek-v4-flash --no-extensions --no-skills --no-prompt-templates --no-context-files --approve"}}
22{"type":"expect_outbound","label":"process_start","frame":{"type":"process_start","args":["--mode","rpc","--provider","openrouter","--model","deepseek/deepseek-v4-flash","--no-extensions","--no-skills","--no-prompt-templates","--no-context-files","--approve","--extension","<any>"]}}
33{"type":"expect_outbound","label":"get_state","frame":{"type":"get_state","id":"t3-0"}}
4{"type":"expect_outbound","label":"get_commands","frame":{"type":"get_commands","id":"t3-1"}}
54{"type":"emit_inbound","label":"extension_ui_request","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000001","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}}
65{"type":"emit_inbound","label":"response:get_state","frame":{"id":"t3-0","type":"response","command":"get_state","success":true,"data":{"model":{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}},"thinkingLevel":"high","isStreaming":false,"isCompacting":false,"steeringMode":"one-at-a-time","followUpMode":"one-at-a-time","sessionFile":"/pi-sessions/session-1.jsonl","sessionId":"00000000-0000-4000-8000-000000000002","autoCompactionEnabled":true,"messageCount":0,"pendingMessageCount":0}}}
7{"type":"emit_inbound","label":"response:get_commands","frame":{"id":"t3-1","type":"response","command":"get_commands","success":true,"data":{"commands":[]}}}
86{"type":"expect_outbound","label":"get_available_models","frame":{"type":"get_available_models","id":"t3-2"}}
97{"type":"emit_inbound","label":"response:get_available_models","frame":{"id":"t3-2","type":"response","command":"get_available_models","success":true,"data":{"models":[{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}}]}}}
108{"type":"expect_outbound","label":"get_entries","frame":{"type":"get_entries","id":"t3-3"}}
apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/pi_transcript.ndjson-2
......@@ -1,10 +1,8 @@
11{"type":"transcript_start","provider":"pi","protocol":"pi.rpc-jsonl","version":"1.0.0","scenario":"multi_turn","metadata":{"generatedBy":"live-pi-recorder","piVersion":"1.0.0","model":"openrouter/deepseek/deepseek-v4-flash","launchArgs":"--provider openrouter --model deepseek/deepseek-v4-flash --no-extensions --no-skills --no-prompt-templates --no-context-files --approve"}}
22{"type":"expect_outbound","label":"process_start","frame":{"type":"process_start","args":["--mode","rpc","--provider","openrouter","--model","deepseek/deepseek-v4-flash","--no-extensions","--no-skills","--no-prompt-templates","--no-context-files","--approve","--extension","<any>"]}}
33{"type":"expect_outbound","label":"get_state","frame":{"type":"get_state","id":"t3-0"}}
4{"type":"expect_outbound","label":"get_commands","frame":{"type":"get_commands","id":"t3-1"}}
54{"type":"emit_inbound","label":"extension_ui_request","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000001","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}}
65{"type":"emit_inbound","label":"response:get_state","frame":{"id":"t3-0","type":"response","command":"get_state","success":true,"data":{"model":{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}},"thinkingLevel":"high","isStreaming":false,"isCompacting":false,"steeringMode":"one-at-a-time","followUpMode":"one-at-a-time","sessionFile":"/pi-sessions/session-1.jsonl","sessionId":"00000000-0000-4000-8000-000000000002","autoCompactionEnabled":true,"messageCount":0,"pendingMessageCount":0}}}
7{"type":"emit_inbound","label":"response:get_commands","frame":{"id":"t3-1","type":"response","command":"get_commands","success":true,"data":{"commands":[]}}}
86{"type":"expect_outbound","label":"get_available_models","frame":{"type":"get_available_models","id":"t3-2"}}
97{"type":"emit_inbound","label":"response:get_available_models","frame":{"id":"t3-2","type":"response","command":"get_available_models","success":true,"data":{"models":[{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}}]}}}
108{"type":"expect_outbound","label":"get_entries","frame":{"type":"get_entries","id":"t3-3"}}
apps/server/src/orchestration-v2/testkit/fixtures/pi_compaction/pi_transcript.ndjson-2
......@@ -1,10 +1,8 @@
11{"type":"transcript_start","provider":"pi","protocol":"pi.rpc-jsonl","version":"1.0.0","scenario":"pi_compaction","metadata":{"generatedBy":"live-pi-recorder","piVersion":"1.0.0","model":"openrouter/deepseek/deepseek-v4-flash","launchArgs":"--provider openrouter --model deepseek/deepseek-v4-flash --no-extensions --no-skills --no-prompt-templates --no-context-files --approve"}}
22{"type":"expect_outbound","label":"process_start","frame":{"type":"process_start","args":["--mode","rpc","--provider","openrouter","--model","deepseek/deepseek-v4-flash","--no-extensions","--no-skills","--no-prompt-templates","--no-context-files","--approve","--extension","<any>"]}}
33{"type":"expect_outbound","label":"get_state","frame":{"type":"get_state","id":"t3-0"}}
4{"type":"expect_outbound","label":"get_commands","frame":{"type":"get_commands","id":"t3-1"}}
54{"type":"emit_inbound","label":"extension_ui_request","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000001","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}}
65{"type":"emit_inbound","label":"response:get_state","frame":{"id":"t3-0","type":"response","command":"get_state","success":true,"data":{"model":{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}},"thinkingLevel":"high","isStreaming":false,"isCompacting":false,"steeringMode":"one-at-a-time","followUpMode":"one-at-a-time","sessionFile":"/pi-sessions/session-1.jsonl","sessionId":"00000000-0000-4000-8000-000000000002","autoCompactionEnabled":true,"messageCount":0,"pendingMessageCount":0}}}
7{"type":"emit_inbound","label":"response:get_commands","frame":{"id":"t3-1","type":"response","command":"get_commands","success":true,"data":{"commands":[]}}}
86{"type":"expect_outbound","label":"get_available_models","frame":{"type":"get_available_models","id":"t3-2"}}
97{"type":"emit_inbound","label":"response:get_available_models","frame":{"id":"t3-2","type":"response","command":"get_available_models","success":true,"data":{"models":[{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}}]}}}
108{"type":"expect_outbound","label":"get_entries","frame":{"type":"get_entries","id":"t3-3"}}
apps/server/src/orchestration-v2/testkit/fixtures/provider_thread_resume/pi_transcript.ndjson-4
......@@ -1,10 +1,8 @@
11{"type":"transcript_start","provider":"pi","protocol":"pi.rpc-jsonl","version":"1.0.0","scenario":"provider_thread_resume","metadata":{"generatedBy":"live-pi-recorder","piVersion":"1.0.0","model":"openrouter/deepseek/deepseek-v4-flash","launchArgs":"--provider openrouter --model deepseek/deepseek-v4-flash --no-extensions --no-skills --no-prompt-templates --no-context-files --approve"}}
22{"type":"expect_outbound","label":"process_start","frame":{"type":"process_start","args":["--mode","rpc","--provider","openrouter","--model","deepseek/deepseek-v4-flash","--no-extensions","--no-skills","--no-prompt-templates","--no-context-files","--approve","--extension","<any>"]}}
33{"type":"expect_outbound","label":"get_state","frame":{"type":"get_state","id":"t3-0"}}
4{"type":"expect_outbound","label":"get_commands","frame":{"type":"get_commands","id":"t3-1"}}
54{"type":"emit_inbound","label":"extension_ui_request","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000001","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}}
65{"type":"emit_inbound","label":"response:get_state","frame":{"id":"t3-0","type":"response","command":"get_state","success":true,"data":{"model":{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}},"thinkingLevel":"high","isStreaming":false,"isCompacting":false,"steeringMode":"one-at-a-time","followUpMode":"one-at-a-time","sessionFile":"/pi-sessions/session-1.jsonl","sessionId":"00000000-0000-4000-8000-000000000002","autoCompactionEnabled":true,"messageCount":0,"pendingMessageCount":0}}}
7{"type":"emit_inbound","label":"response:get_commands","frame":{"id":"t3-1","type":"response","command":"get_commands","success":true,"data":{"commands":[]}}}
86{"type":"expect_outbound","label":"get_available_models","frame":{"type":"get_available_models","id":"t3-2"}}
97{"type":"emit_inbound","label":"response:get_available_models","frame":{"id":"t3-2","type":"response","command":"get_available_models","success":true,"data":{"models":[{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}}]}}}
108{"type":"expect_outbound","label":"get_entries","frame":{"type":"get_entries","id":"t3-3"}}
......@@ -61,9 +59,7 @@
6159{"type":"emit_inbound","label":"response:get_session_stats","frame":{"id":"t3-8","type":"response","command":"get_session_stats","success":true,"data":{"sessionFile":"/pi-sessions/session-1.jsonl","sessionId":"00000000-0000-4000-8000-000000000002","userMessages":1,"assistantMessages":1,"toolCalls":0,"toolResults":0,"totalMessages":3,"tokens":{"input":1792,"output":85,"cacheRead":0,"cacheWrite":0,"total":1877},"cost":0.00008212932000000001,"contextUsage":{"tokens":1877,"contextWindow":1048576,"percent":0.17900466918945312}}}}
6260{"type":"expect_outbound","label":"process_start@p2","frame":{"type":"process_start","args":["--mode","rpc","--provider","openrouter","--model","deepseek/deepseek-v4-flash","--no-extensions","--no-skills","--no-prompt-templates","--no-context-files","--approve","--extension","<any>"]}}
6361{"type":"expect_outbound","label":"switch_session@p2","frame":{"type":"switch_session","sessionPath":"/pi-sessions/session-1.jsonl","id":"t3-0"}}
64{"type":"expect_outbound","label":"get_commands@p2","frame":{"type":"get_commands","id":"t3-1"}}
6562{"type":"emit_inbound","label":"extension_ui_request@p2","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000003","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}}
66{"type":"emit_inbound","label":"response:get_commands@p2","frame":{"id":"t3-1","type":"response","command":"get_commands","success":true,"data":{"commands":[]}}}
6763{"type":"emit_inbound","label":"extension_ui_request@p2","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000004","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}}
6864{"type":"emit_inbound","label":"extension_ui_request@p2","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000005","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}}
6965{"type":"emit_inbound","label":"response:switch_session@p2","frame":{"id":"t3-0","type":"response","command":"switch_session","success":true,"data":{"cancelled":false}}}
apps/server/src/orchestration-v2/testkit/fixtures/simple/pi_transcript.ndjson-2
......@@ -1,10 +1,8 @@
11{"type":"transcript_start","provider":"pi","protocol":"pi.rpc-jsonl","version":"1.0.0","scenario":"simple","metadata":{"generatedBy":"live-pi-recorder","piVersion":"1.0.0","model":"openrouter/deepseek/deepseek-v4-flash","launchArgs":"--provider openrouter --model deepseek/deepseek-v4-flash --no-extensions --no-skills --no-prompt-templates --no-context-files --approve"}}
22{"type":"expect_outbound","label":"process_start","frame":{"type":"process_start","args":["--mode","rpc","--provider","openrouter","--model","deepseek/deepseek-v4-flash","--no-extensions","--no-skills","--no-prompt-templates","--no-context-files","--approve","--extension","<any>"]}}
33{"type":"expect_outbound","label":"get_state","frame":{"type":"get_state","id":"t3-0"}}
4{"type":"expect_outbound","label":"get_commands","frame":{"type":"get_commands","id":"t3-1"}}
54{"type":"emit_inbound","label":"extension_ui_request","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000001","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}}
65{"type":"emit_inbound","label":"response:get_state","frame":{"id":"t3-0","type":"response","command":"get_state","success":true,"data":{"model":{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}},"thinkingLevel":"high","isStreaming":false,"isCompacting":false,"steeringMode":"one-at-a-time","followUpMode":"one-at-a-time","sessionFile":"/pi-sessions/session-1.jsonl","sessionId":"00000000-0000-4000-8000-000000000002","autoCompactionEnabled":true,"messageCount":0,"pendingMessageCount":0}}}
7{"type":"emit_inbound","label":"response:get_commands","frame":{"id":"t3-1","type":"response","command":"get_commands","success":true,"data":{"commands":[]}}}
86{"type":"expect_outbound","label":"get_available_models","frame":{"type":"get_available_models","id":"t3-2"}}
97{"type":"emit_inbound","label":"response:get_available_models","frame":{"id":"t3-2","type":"response","command":"get_available_models","success":true,"data":{"models":[{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}}]}}}
108{"type":"expect_outbound","label":"get_entries","frame":{"type":"get_entries","id":"t3-3"}}
apps/server/src/orchestration-v2/testkit/fixtures/thread_rollback/pi_transcript.ndjson-2
......@@ -1,11 +1,9 @@
11{"type":"transcript_start","provider":"pi","protocol":"pi.rpc-jsonl","version":"1.0.0","scenario":"thread_rollback","metadata":{"generatedBy":"live-pi-recorder","piVersion":"1.0.0","model":"openrouter/deepseek/deepseek-v4-flash","launchArgs":"--provider openrouter --model deepseek/deepseek-v4-flash --no-extensions --no-skills --no-prompt-templates --no-context-files --approve"}}
22{"type":"expect_outbound","label":"process_start","frame":{"type":"process_start","args":["--mode","rpc","--provider","openrouter","--model","deepseek/deepseek-v4-flash","--no-extensions","--no-skills","--no-prompt-templates","--no-context-files","--approve","--extension","<any>"]}}
33{"type":"expect_outbound","label":"get_state","frame":{"type":"get_state","id":"t3-0"}}
4{"type":"expect_outbound","label":"get_commands","frame":{"type":"get_commands","id":"t3-1"}}
54{"type":"emit_inbound","label":"extension_ui_request","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000001","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}}
65{"type":"emit_inbound","label":"response:get_state","frame":{"id":"t3-0","type":"response","command":"get_state","success":true,"data":{"model":{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}},"thinkingLevel":"high","isStreaming":false,"isCompacting":false,"steeringMode":"one-at-a-time","followUpMode":"one-at-a-time","sessionFile":"/pi-sessions/session-1.jsonl","sessionId":"00000000-0000-4000-8000-000000000002","autoCompactionEnabled":true,"messageCount":0,"pendingMessageCount":0}}}
76{"type":"expect_outbound","label":"get_available_models","frame":{"type":"get_available_models","id":"t3-2"}}
8{"type":"emit_inbound","label":"response:get_commands","frame":{"id":"t3-1","type":"response","command":"get_commands","success":true,"data":{"commands":[]}}}
97{"type":"emit_inbound","label":"response:get_available_models","frame":{"id":"t3-2","type":"response","command":"get_available_models","success":true,"data":{"models":[{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}}]}}}
108{"type":"expect_outbound","label":"get_entries","frame":{"type":"get_entries","id":"t3-3"}}
119{"type":"emit_inbound","label":"response:get_entries","frame":{"id":"t3-3","type":"response","command":"get_entries","success":true,"data":{"entries":[{"type":"model_change","id":"648fe89a","parentId":null,"timestamp":"2026-01-01T00:00:00.000Z","provider":"openrouter","modelId":"deepseek/deepseek-v4-flash"},{"type":"thinking_level_change","id":"00c31b78","parentId":"648fe89a","timestamp":"2026-01-01T00:00:00.000Z","thinkingLevel":"high"}],"leafId":"00c31b78"}}}
apps/server/src/orchestration-v2/testkit/fixtures/thread_rollback_after_stop/pi_transcript.ndjson-4
......@@ -1,10 +1,8 @@
11{"type":"transcript_start","provider":"pi","protocol":"pi.rpc-jsonl","version":"1.0.0","scenario":"thread_rollback_after_stop","metadata":{"generatedBy":"live-pi-recorder","piVersion":"1.0.0","model":"openrouter/deepseek/deepseek-v4-flash","launchArgs":"--provider openrouter --model deepseek/deepseek-v4-flash --no-extensions --no-skills --no-prompt-templates --no-context-files --approve"}}
22{"type":"expect_outbound","label":"process_start","frame":{"type":"process_start","args":["--mode","rpc","--provider","openrouter","--model","deepseek/deepseek-v4-flash","--no-extensions","--no-skills","--no-prompt-templates","--no-context-files","--approve","--extension","<any>"]}}
33{"type":"expect_outbound","label":"get_state","frame":{"type":"get_state","id":"t3-0"}}
4{"type":"expect_outbound","label":"get_commands","frame":{"type":"get_commands","id":"t3-1"}}
54{"type":"emit_inbound","label":"extension_ui_request","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000001","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}}
65{"type":"emit_inbound","label":"response:get_state","frame":{"id":"t3-0","type":"response","command":"get_state","success":true,"data":{"model":{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}},"thinkingLevel":"high","isStreaming":false,"isCompacting":false,"steeringMode":"one-at-a-time","followUpMode":"one-at-a-time","sessionFile":"/pi-sessions/session-1.jsonl","sessionId":"00000000-0000-4000-8000-000000000002","autoCompactionEnabled":true,"messageCount":0,"pendingMessageCount":0}}}
7{"type":"emit_inbound","label":"response:get_commands","frame":{"id":"t3-1","type":"response","command":"get_commands","success":true,"data":{"commands":[]}}}
86{"type":"expect_outbound","label":"get_available_models","frame":{"type":"get_available_models","id":"t3-2"}}
97{"type":"emit_inbound","label":"response:get_available_models","frame":{"id":"t3-2","type":"response","command":"get_available_models","success":true,"data":{"models":[{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}}]}}}
108{"type":"expect_outbound","label":"get_entries","frame":{"type":"get_entries","id":"t3-3"}}
......@@ -151,9 +149,7 @@
151149{"type":"emit_inbound","label":"response:get_entries","frame":{"id":"t3-10","type":"response","command":"get_entries","success":true,"data":{"entries":[{"type":"message","id":"ee73daae","parentId":"b184b5a8","timestamp":"2026-01-01T00:00:01.971Z","message":{"role":"user","content":[{"type":"text","text":"Run this exact local command: `node -e \"console.log('interrupt fixture tool started'); setTimeout(() => {}, 30000)\"`. Do not answer until it completes, then respond exactly: interrupt fixture should not finish naturally."}],"timestamp":1767225601969}},{"type":"message","id":"c3915df7","parentId":"ee73daae","timestamp":"2026-01-01T00:00:06.406Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"The user wants me to run a command that will wait for 30 seconds, and then after it completes, respond with a specific message. But they also say \"Do not answer until it completes\" - but this command will run for 30 seconds and then naturally finish. However, the response they want is \"interrupt fixture should not finish naturally\" which contradicts the fact that it did finish naturally.\n\nWait, let me re-read. They want me to run the command and not answer until it completes. The command is a node script that logs \"interrupt fixture tool started\" and then waits 30 seconds. If I wait for it to complete, it will finish naturally after 30 seconds. But the response they want is \"interrupt fixture should not finish naturally\" - implying that the fixture should have been interrupted before finishing naturally.\n\nThis is a test scenario. Let me just run it and see what happens. Actually, maybe they want me to just run it and respond accordingly. Let me execute the command.\n\nActually, I think this is part of a test where the system might interrupt/kill the process. Let me just run the command with a sufficient timeout.","thinkingSignature":"<signature>"},{"type":"toolCall","id":"call_8ac3cb4177994abdb9aa1c57","name":"bash","arguments":{"command":"node -e \"console.log('interrupt fixture tool started'); setTimeout(() => {}, 30000)\"","timeout":35}}],"api":"openai-completions","provider":"openrouter","model":"deepseek/deepseek-v4-flash","usage":{"input":77,"output":317,"cacheRead":1792,"cacheWrite":0,"reasoning":284,"totalTokens":2186,"cost":{"input":0.00000322322,"output":0.000026539239999999998,"cacheRead":0.000015002623999999998,"cacheWrite":0,"total":0.000044765084}},"stopReason":"toolUse","timestamp":1767225601972,"responseId":"gen-1790898593-E5hfYfRp6aKBI57pUKss","rawStopReason":"tool_calls","thinkingLevel":"high"}},{"type":"message","id":"641f3d3f","parentId":"c3915df7","timestamp":"2026-01-01T00:00:06.463Z","message":{"role":"toolResult","toolCallId":"call_8ac3cb4177994abdb9aa1c57","toolName":"bash","content":[{"type":"text","text":"interrupt fixture tool started\n\n\nCommand aborted"}],"details":{},"isError":true,"timestamp":1767225606463}},{"type":"message","id":"e3ab1495","parentId":"641f3d3f","timestamp":"2026-01-01T00:00:06.465Z","message":{"role":"assistant","content":[],"api":"openai-completions","provider":"openrouter","model":"deepseek/deepseek-v4-flash","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"error","errorMessage":"This operation was aborted","timestamp":1767225606465,"thinkingLevel":"high"}}],"leafId":"e3ab1495"}}}
152150{"type":"expect_outbound","label":"process_start@p2","frame":{"type":"process_start","args":["--mode","rpc","--provider","openrouter","--model","deepseek/deepseek-v4-flash","--no-extensions","--no-skills","--no-prompt-templates","--no-context-files","--approve","--extension","<any>"]}}
153151{"type":"expect_outbound","label":"switch_session@p2","frame":{"type":"switch_session","sessionPath":"/pi-sessions/session-1.jsonl","id":"t3-0"}}
154{"type":"expect_outbound","label":"get_commands@p2","frame":{"type":"get_commands","id":"t3-1"}}
155152{"type":"emit_inbound","label":"extension_ui_request@p2","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000003","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}}
156{"type":"emit_inbound","label":"response:get_commands@p2","frame":{"id":"t3-1","type":"response","command":"get_commands","success":true,"data":{"commands":[]}}}
157153{"type":"emit_inbound","label":"extension_ui_request@p2","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000004","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}}
158154{"type":"emit_inbound","label":"extension_ui_request@p2","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000005","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}}
159155{"type":"emit_inbound","label":"response:switch_session@p2","frame":{"id":"t3-0","type":"response","command":"switch_session","success":true,"data":{"cancelled":false}}}
apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/pi_transcript.ndjson-2
......@@ -1,10 +1,8 @@
11{"type":"transcript_start","provider":"pi","protocol":"pi.rpc-jsonl","version":"1.0.0","scenario":"turn_interrupt_mid_tool","metadata":{"generatedBy":"live-pi-recorder","piVersion":"1.0.0","model":"openrouter/deepseek/deepseek-v4-flash","launchArgs":"--provider openrouter --model deepseek/deepseek-v4-flash --no-extensions --no-skills --no-prompt-templates --no-context-files --approve"}}
22{"type":"expect_outbound","label":"process_start","frame":{"type":"process_start","args":["--mode","rpc","--provider","openrouter","--model","deepseek/deepseek-v4-flash","--no-extensions","--no-skills","--no-prompt-templates","--no-context-files","--approve","--extension","<any>"]}}
33{"type":"expect_outbound","label":"get_state","frame":{"type":"get_state","id":"t3-0"}}
4{"type":"expect_outbound","label":"get_commands","frame":{"type":"get_commands","id":"t3-1"}}
54{"type":"emit_inbound","label":"extension_ui_request","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000001","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}}
65{"type":"emit_inbound","label":"response:get_state","frame":{"id":"t3-0","type":"response","command":"get_state","success":true,"data":{"model":{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}},"thinkingLevel":"high","isStreaming":false,"isCompacting":false,"steeringMode":"one-at-a-time","followUpMode":"one-at-a-time","sessionFile":"/pi-sessions/session-1.jsonl","sessionId":"00000000-0000-4000-8000-000000000002","autoCompactionEnabled":true,"messageCount":0,"pendingMessageCount":0}}}
7{"type":"emit_inbound","label":"response:get_commands","frame":{"id":"t3-1","type":"response","command":"get_commands","success":true,"data":{"commands":[]}}}
86{"type":"expect_outbound","label":"get_available_models","frame":{"type":"get_available_models","id":"t3-2"}}
97{"type":"emit_inbound","label":"response:get_available_models","frame":{"id":"t3-2","type":"response","command":"get_available_models","success":true,"data":{"models":[{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}}]}}}
108{"type":"expect_outbound","label":"get_entries","frame":{"type":"get_entries","id":"t3-3"}}
packages/provider-pi/src/server/adapter.test.ts+2-60
......@@ -94,10 +94,6 @@ interface FakePi {
9494 readonly allRequests: () => ReadonlyArray<PiRpcRecord>;
9595 /** Data returned by the next `get_session_stats` acks, consumed in order. */
9696 readonly queueStats: (data: unknown) => void;
97 /** Data returned by the next `get_commands` acks, consumed in order. */
98 readonly queueCommands: (data: unknown) => void;
99 /** Make the next `get_commands` ack fail. */
100 readonly failNextCommands: () => void;
10197 /** Close the fake process stdout stream. */
10298 readonly closeStdout: Effect.Effect<void>;
10399 readonly lastSpawn: () => {
......@@ -137,7 +133,6 @@ const makeFakePi: Effect.Effect<FakePi> = Effect.gen(function* () {
137133 const messagesQueue: Array<unknown> = [];
138134 const stateQueue: Array<Record<string, unknown>> = [];
139135 const statsQueue: Array<unknown> = [];
140 const commandsQueue: Array<{ readonly success: boolean; readonly data?: unknown }> = [];
141136 const allRequests: Array<PiRpcRecord> = [];
142137 let deferState = false;
143138 let deferredStateRequest: PiRpcRecord | undefined;
......@@ -191,8 +186,6 @@ const makeFakePi: Effect.Effect<FakePi> = Effect.gen(function* () {
191186 return { ...base, data: messagesQueue.shift() ?? { messages: [] } };
192187 case "get_session_stats":
193188 return { ...base, data: statsQueue.shift() ?? {} };
194 case "get_commands":
195 return { ...base, ...(commandsQueue.shift() ?? { data: { commands: [] } }) };
196189 case "fork":
197190 return { ...base, data: { text: "Hello pi", cancelled: false } };
198191 default:
......@@ -305,8 +298,6 @@ const makeFakePi: Effect.Effect<FakePi> = Effect.gen(function* () {
305298 },
306299 queueState: (data) => stateQueue.push(data),
307300 queueStats: (data) => statsQueue.push(data),
308 queueCommands: (data) => commandsQueue.push({ success: true, data }),
309 failNextCommands: () => commandsQueue.push({ success: false }),
310301 closeStdout: Queue.end(stdout),
311302 lastSpawn: () => lastSpawn,
312303 } satisfies FakePi;
......@@ -1697,22 +1688,9 @@ describe("PiAdapterV2", () => {
16971688 }).pipe(Effect.scoped, Effect.provide(layerTest)),
16981689 );
16991690
1700 it.effect("expands a selected $ skill through Pi's native skill command", () =>
1691 it.effect("preserves selected skill references for the Pi extension to expand", () =>
17011692 Effect.gen(function* () {
17021693 const fake = yield* makeFakePi;
1703 fake.queueCommands({
1704 commands: [
1705 {
1706 name: "skill:repo-review",
1707 description: "Review this repository.",
1708 source: "skill",
1709 sourceInfo: {
1710 path: "/workspace/.agents/skills/repo-review/SKILL.md",
1711 scope: "project",
1712 },
1713 },
1714 ],
1715 });
17161694 const { runtime } = yield* openRuntime(fake);
17171695 const providerThread = yield* runtime.ensureThread({
17181696 threadId: THREAD_ID,
......@@ -1728,43 +1706,7 @@ describe("PiAdapterV2", () => {
17281706 "Review this change please $repo-review",
17291707 );
17301708 const prompt = yield* fake.takeRequest("prompt");
1731 assert.equal(prompt["message"], "/skill:repo-review Review this change please");
1732 }).pipe(Effect.scoped, Effect.provide(layerTest)),
1733 );
1734
1735 it.effect("expands every selected $ skill through Pi native skill commands", () =>
1736 Effect.gen(function* () {
1737 const fake = yield* makeFakePi;
1738 fake.queueCommands({
1739 commands: [
1740 {
1741 name: "skill:repo-review",
1742 source: "skill",
1743 sourceInfo: {
1744 path: "/workspace/.agents/skills/repo-review/SKILL.md",
1745 scope: "project",
1746 },
1747 },
1748 {
1749 name: "skill:deploy",
1750 source: "skill",
1751 sourceInfo: {
1752 path: "/workspace/.agents/skills/deploy/SKILL.md",
1753 scope: "project",
1754 },
1755 },
1756 ],
1757 });
1758 const { runtime } = yield* openRuntime(fake);
1759 const providerThread = yield* runtime.ensureThread({
1760 threadId: THREAD_ID,
1761 modelSelection: modelSelection("default"),
1762 runtimePolicy,
1763 });
1764
1765 yield* startTurn(runtime, providerThread, "default", [], "use $repo-review and $deploy");
1766 const prompt = yield* fake.takeRequest("prompt");
1767 assert.equal(prompt["message"], "/skill:repo-review /skill:deploy use and");
1709 assert.equal(prompt["message"], "Review this change please $repo-review");
17681710 }).pipe(Effect.scoped, Effect.provide(layerTest)),
17691711 );
17701712
packages/provider-pi/src/server/adapter.ts+2-37
......@@ -63,12 +63,7 @@ import { ChildProcessSpawner } from "effect/process";
6363import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
6464import { mcpToolPresentation } from "@t3tools/provider-core/server/mcpToolPresentation";
6565import * as McpProviderSession from "@t3tools/provider-core/server/mcpSession";
66import {
67 expandPiSkillReference,
68 parsePiCompactCommand,
69 parsePiDiscoveredCommands,
70 type PiCompactCommand,
71} from "./commands.ts";
66import { parsePiCompactCommand, type PiCompactCommand } from "./commands.ts";
7267import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment";
7368import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
7469import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
......@@ -114,7 +109,6 @@ const STREAM_FLUSH_MS = 50;
114109const PI_REQUEST_TIMEOUT_MS = 15_000;
115110// Session lifecycle hooks reload extensions, MCP servers and language servers.
116111const PI_SESSION_TIMEOUT_MS = 60_000;
117const PI_SKILL_DISCOVERY_TIMEOUT_MS = 4_000;
118112const PI_UNSOLICITED_ACTIVITY_ERROR =
119113 "Pi started agent work outside an active T3 turn. The session was stopped to prevent invisible tool execution.";
120114const SETTLE_PROBE_MAX_ATTEMPTS = 3;
......@@ -458,15 +452,6 @@ export const makePiAdapterV2 = Effect.fn("makePiAdapterV2")(function* (
458452 }),
459453 ),
460454 );
461 const discoverSkillNames = connection
462 .request({ type: "get_commands" }, PI_SKILL_DISCOVERY_TIMEOUT_MS)
463 .pipe(
464 Effect.map(
465 (data) => new Set(parsePiDiscoveredCommands(data).skills.map((skill) => skill.name)),
466 ),
467 );
468 let skillNames: Set<string> | null = null;
469
470455 const now = yield* DateTime.now;
471456 let sessionEntity: OrchestrationV2ProviderSession = {
472457 id: input.providerSessionId,
......@@ -2117,16 +2102,6 @@ export const makePiAdapterV2 = Effect.fn("makePiAdapterV2")(function* (
21172102 Effect.forkIn(scope),
21182103 );
21192104
2120 // Discovery can invoke extension code and therefore raise a blocking
2121 // UI request. Start it only after the event pump exists, and never hold
2122 // session opening on it; startup requests are persisted at session
2123 // scope and can be answered before a turn begins.
2124 yield* discoverSkillNames.pipe(
2125 Effect.tap((discovered) => Effect.sync(() => (skillNames = discovered))),
2126 Effect.ignore,
2127 Effect.forkIn(scope),
2128 );
2129
21302105 // ── session runtime ───────────────────────────────────
21312106
21322107 const registerThread = Effect.fnUntraced(function* (
......@@ -2329,15 +2304,6 @@ export const makePiAdapterV2 = Effect.fn("makePiAdapterV2")(function* (
23292304 text: string,
23302305 attachments: ReadonlyArray<ChatAttachment>,
23312306 ) {
2332 // Provider discovery and the live session are separate Pi processes.
2333 // Retry a failed session-local lookup once at first use so a transient
2334 // startup failure cannot leave a visible $ skill inert for this session.
2335 if (skillNames === null && text.includes("$")) {
2336 skillNames = yield* discoverSkillNames.pipe(
2337 Effect.orElseSucceed(() => new Set<string>()),
2338 );
2339 }
2340 const expandedText = skillNames === null ? text : expandPiSkillReference(text, skillNames);
23412307 const images: Array<{ type: "image"; data: string; mimeType: string }> = [];
23422308 const extraLines: Array<string> = [];
23432309 for (const attachment of attachments) {
......@@ -2354,8 +2320,7 @@ export const makePiAdapterV2 = Effect.fn("makePiAdapterV2")(function* (
23542320 extraLines.push(`[Attachment saved at ${path}]`);
23552321 }
23562322 }
2357 const message =
2358 extraLines.length === 0 ? expandedText : `${expandedText}\n\n${extraLines.join("\n")}`;
2323 const message = extraLines.length === 0 ? text : `${text}\n\n${extraLines.join("\n")}`;
23592324 return { message, images };
23602325 });
23612326
packages/provider-pi/src/server/commands.test.ts-17
......@@ -1,7 +1,6 @@
11import { expect, it } from "@effect/vitest";
22
33import {
4 expandPiSkillReference,
54 parsePiCompactCommand,
65 parsePiDiscoveredCommands,
76 PI_COMPACT_SLASH_COMMAND,
......@@ -109,19 +108,3 @@ it("prepends the builtin compact command without duplicating a discovered one",
109108 ]),
110109 ).toEqual([PI_COMPACT_SLASH_COMMAND, { name: "hello" }]);
111110});
112
113it("leaves unrelated dollar-prefixed text unchanged", () => {
114 expect(expandPiSkillReference("Explain $HOME", new Set(["global-review"]))).toBe("Explain $HOME");
115});
116
117it("hoists every known $ skill and keeps the rest of the prompt", () => {
118 expect(expandPiSkillReference("use $alpha then $beta please", new Set(["alpha", "beta"]))).toBe(
119 "/skill:alpha /skill:beta use then please",
120 );
121});
122
123it("preserves code indentation and line breaks when expanding a skill", () => {
124 expect(expandPiSkillReference("$review\n```ts\n const x = 1;\n```", new Set(["review"]))).toBe(
125 "/skill:review ```ts\n const x = 1;\n```",
126 );
127});
packages/provider-pi/src/server/commands.ts-35
......@@ -91,41 +91,6 @@ export function parsePiDiscoveredCommands(data: unknown): PiDiscoveredCommands {
9191 return { slashCommands, skills };
9292}
9393
94/**
95 * Pi expands skills only through leading `/skill:name` commands. T3 stores
96 * skill chips as `$name`, so hoist every known `$skill` to that native
97 * command position while preserving the rest of the user's prompt.
98 */
99export function expandPiSkillReference(text: string, skillNames: ReadonlySet<string>): string {
100 const references = /(^|\s)\$([^\s]+)(?=\s|$)/g;
101 const found: Array<{ name: string; start: number; end: number }> = [];
102 for (const match of text.matchAll(references)) {
103 const name = match[2];
104 if (name === undefined || !skillNames.has(name) || match.index === undefined) continue;
105 const tokenStart = match.index + (match[1]?.length ?? 0);
106 found.push({ name, start: tokenStart, end: tokenStart + name.length + 1 });
107 }
108 if (found.length === 0) return text;
109
110 const orderedNames: string[] = [];
111 const seen = new Set<string>();
112 for (const token of found) {
113 if (seen.has(token.name)) continue;
114 seen.add(token.name);
115 orderedNames.push(token.name);
116 }
117
118 let body = text;
119 for (let index = found.length - 1; index >= 0; index -= 1) {
120 const token = found[index];
121 if (token === undefined) continue;
122 body = `${body.slice(0, token.start)}${body.slice(token.end)}`;
123 }
124 body = body.trim();
125 const prefix = orderedNames.map((name) => `/skill:${name}`).join(" ");
126 return body.length === 0 ? prefix : `${prefix} ${body}`;
127}
128
12994function recordField(input: unknown, key: string): unknown {
13095 return Predicate.isObject(input) ? input[key] : undefined;
13196}
packages/provider-pi/src/server/mcpExtensionSource.test.ts+146-3
......@@ -1,3 +1,7 @@
1// @effect-diagnostics nodeBuiltinImport:off -- Pi extensions run outside Effect; these tests exercise their native filesystem boundary.
2import * as NodeFSP from "node:fs/promises";
3import * as NodePath from "node:path";
4import * as NodeOS from "node:os";
15import * as NodeModule from "node:module";
26import * as NodeVM from "node:vm";
37import { assert, describe, it } from "@effect/vitest";
......@@ -9,6 +13,40 @@ type RequestHook = (
913 ctx: { model: { provider: string } },
1014) => Record<string, unknown> | undefined;
1115
16interface SkillCommand {
17 readonly name: string;
18 readonly source: string;
19 readonly sourceInfo: { readonly path: string };
20}
21
22type InputHook = (
23 event: { text: string; images?: ReadonlyArray<unknown> },
24 ctx: { ui: { notify: (message: string, level: string) => void } },
25) => Promise<{ action: string; text: string; images?: ReadonlyArray<unknown> } | undefined>;
26
27async function loadHooks(commands: ReadonlyArray<SkillCommand> = []) {
28 const handlers = new Map<string, unknown>();
29 // Execute the shipped extension with MCP disabled; this path needs no Typebox.
30 const source = NodeModule.stripTypeScriptTypes(
31 PI_T3_MCP_EXTENSION_SOURCE.replace(/^import .*;$/gm, "").replace(
32 "export default async function",
33 "async function",
34 ),
35 );
36 await NodeVM.runInNewContext(`${source}\nt3McpExtension(pi)`, {
37 process: { env: {} },
38 NodeFSP,
39 NodePath,
40 // Frontmatter parsing belongs to Pi. These fixtures contain only skill bodies.
41 stripFrontmatter: (content: string) => content,
42 pi: {
43 on: (name: string, handler: unknown) => handlers.set(name, handler),
44 getCommands: () => commands,
45 },
46 });
47 return handlers;
48}
49
1250interface RegisteredTool {
1351 readonly name: string;
1452 readonly description: string;
......@@ -55,7 +93,7 @@ async function loadMcpBridge(
5593 { name: "preview_snapshot", description: "Inspect the collaborative browser." },
5694 ].map((tool) => ({ ...tool, inputSchema: { type: "object", properties: {} } }));
5795 const source = NodeModule.stripTypeScriptTypes(
58 PI_T3_MCP_EXTENSION_SOURCE.replace('import { Type } from "typebox";', "").replace(
96 PI_T3_MCP_EXTENSION_SOURCE.replace(/^import .*;$/gm, "").replace(
5997 "export default async function",
6098 "async function",
6199 ),
......@@ -271,7 +309,7 @@ describe("Pi tool discovery permissions", () => {
271309 let toolCall: ToolCallHook | undefined;
272310 let searchPath = "builtin:tool-search";
273311 const source = NodeModule.stripTypeScriptTypes(
274 PI_T3_MCP_EXTENSION_SOURCE.replace('import { Type } from "typebox";', "").replace(
312 PI_T3_MCP_EXTENSION_SOURCE.replace(/^import .*;$/gm, "").replace(
275313 "export default async function",
276314 "async function",
277315 ),
......@@ -315,7 +353,7 @@ async function loadRequestHook(): Promise<RequestHook> {
315353 const handlers = new Map<string, RequestHook>();
316354 // Execute the shipped extension with MCP disabled; this path needs no Typebox.
317355 const source = NodeModule.stripTypeScriptTypes(
318 PI_T3_MCP_EXTENSION_SOURCE.replace('import { Type } from "typebox";', "").replace(
356 PI_T3_MCP_EXTENSION_SOURCE.replace(/^import .*;$/gm, "").replace(
319357 "export default async function",
320358 "async function",
321359 ),
......@@ -359,3 +397,108 @@ describe("Pi upstream output-budget workaround", () => {
359397 );
360398 });
361399});
400
401async function loadInputHook(commands: ReadonlyArray<SkillCommand>) {
402 const handlers = await loadHooks(commands);
403 const hook = handlers.get("input");
404 assert.isDefined(hook);
405 return hook as InputHook;
406}
407
408describe("Pi skill references", () => {
409 it("loads every selected skill once while preserving inline prose, whitespace, and images", async () => {
410 const directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "pi-skill-mentions-"));
411 try {
412 const alpha = NodePath.join(directory, "alpha.md");
413 const beta = NodePath.join(directory, "beta.md");
414 await NodeFSP.writeFile(alpha, "ALPHA_INSTRUCTIONS");
415 await NodeFSP.writeFile(beta, "BETA_INSTRUCTIONS");
416 const hook = await loadInputHook([
417 { name: "skill:alpha", source: "skill", sourceInfo: { path: alpha } },
418 { name: "skill:beta", source: "skill", sourceInfo: { path: beta } },
419 ]);
420 const text =
421 "Please use the $alpha philosophy, then $beta and $alpha\n```ts\n const x = 1;\n```";
422 const images = [{ type: "image", data: "fixture" }];
423 const result = await hook({ text, images }, { ui: { notify: assert.fail } });
424 assert.equal(result?.action, "transform");
425 assert.isTrue(result?.text.startsWith(text + "\n\n"));
426 assert.equal(result?.text.split("ALPHA_INSTRUCTIONS").length, 2);
427 assert.equal(result?.text.split("BETA_INSTRUCTIONS").length, 2);
428 assert.include(result?.text ?? "", `References are relative to ${directory}.`);
429 assert.strictEqual(result?.images, images);
430 } finally {
431 await NodeFSP.rm(directory, { recursive: true });
432 }
433 });
434
435 it("leaves unknown references and native skill commands for Pi", async () => {
436 const hook = await loadInputHook([
437 { name: "skill:alpha", source: "skill", sourceInfo: { path: "/unused" } },
438 ]);
439 // Punctuation-adjacent references are plain text in the composer, not selected chips.
440 for (const text of [
441 "Explain $HOME",
442 "/skill:alpha use $alpha",
443 "Hello",
444 "$missing",
445 "Use $alpha, then continue",
446 "Use ($alpha)",
447 ]) {
448 assert.isUndefined(await hook({ text }, { ui: { notify: assert.fail } }));
449 }
450 });
451
452 it("loads additional chips without duplicating a leading native skill", async () => {
453 const directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "pi-skill-mentions-"));
454 try {
455 const path = NodePath.join(directory, "beta.md");
456 await NodeFSP.writeFile(path, "BETA_INSTRUCTIONS");
457 const hook = await loadInputHook([
458 { name: "skill:alpha", source: "skill", sourceInfo: { path: "/unused" } },
459 { name: "skill:beta", source: "skill", sourceInfo: { path } },
460 ]);
461 const text = "/skill:alpha use $alpha and $beta";
462 const result = await hook({ text }, { ui: { notify: assert.fail } });
463 assert.isTrue(result?.text.startsWith(text + "\n\n"));
464 assert.notInclude(result?.text ?? "", '<skill name="alpha"');
465 assert.include(result?.text ?? "", "BETA_INSTRUCTIONS");
466 } finally {
467 await NodeFSP.rm(directory, { recursive: true });
468 }
469 });
470
471 it("reports an unreadable skill without deleting its reference or other selected instructions", async () => {
472 const directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "pi-skill-mentions-"));
473 try {
474 const path = NodePath.join(directory, "readable.md");
475 await NodeFSP.writeFile(path, "READABLE_INSTRUCTIONS");
476 const hook = await loadInputHook([
477 {
478 name: "skill:missing",
479 source: "skill",
480 sourceInfo: { path: NodePath.join(directory, "missing.md") },
481 },
482 { name: "skill:readable", source: "skill", sourceInfo: { path } },
483 ]);
484 const notices: string[] = [];
485 const text = "Use $missing and $readable";
486 const result = await hook(
487 { text },
488 {
489 ui: {
490 notify: (message) => {
491 notices.push(message);
492 },
493 },
494 },
495 );
496 assert.isTrue(result?.text.startsWith(text + "\n\n"));
497 assert.include(result?.text ?? "", "READABLE_INSTRUCTIONS");
498 assert.equal(notices.length, 1);
499 assert.include(notices[0] ?? "", "Could not load skill missing");
500 } finally {
501 await NodeFSP.rm(directory, { recursive: true });
502 }
503 });
504});
packages/provider-pi/src/server/mcpExtensionSource.ts+35-1
......@@ -23,7 +23,9 @@ export const T3_PI_RUNTIME_MODE_ENV = "T3_PI_RUNTIME_MODE";
2323export const PI_FILE_CHANGE_TOOLS = ["edit", "write"] as const;
2424
2525export const PI_T3_MCP_EXTENSION_SOURCE = `\
26import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
26import { stripFrontmatter, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
27import * as NodeFSP from "node:fs/promises";
28import * as NodePath from "node:path";
2729import { Type } from "typebox";
2830
2931const URL_ENV = ${JSON.stringify(T3_MCP_URL_ENV)};
......@@ -210,6 +212,38 @@ function createMcpClient(endpoint: string, token: string) {
210212}
211213
212214export default async function t3McpExtension(pi: ExtensionAPI) {
215 // Keep skill chips in the request. Pi expands only one leading /skill:
216 // command, so use its own frontmatter loader for every selected chip here.
217 pi.on("input", async (event, ctx) => {
218 if (!event.text.includes("$")) return;
219 const commands = pi.getCommands().filter((command) => command.source === "skill");
220 const nativeSkill = /^\\/skill:([^ ]+)(?: |$)/.exec(event.text)?.[1];
221 const seen = new Set<string>(nativeSkill === undefined ? [] : [nativeSkill]);
222 const blocks: string[] = [];
223 for (const match of event.text.matchAll(/(^|\\s)\\$([^\\s]+)(?=\\s|$)/g)) {
224 const name = match[2];
225 if (name === undefined || seen.has(name)) continue;
226 const command = commands.find((candidate) => candidate.name === "skill:" + name);
227 if (command === undefined) continue;
228 seen.add(name);
229 const path = command.sourceInfo.path;
230 try {
231 const body = stripFrontmatter(await NodeFSP.readFile(path, "utf8")).trim();
232 blocks.push(
233 \`<skill name="\${name}" location="\${path}">\\nReferences are relative to \${NodePath.dirname(path)}.\\n\\n\${body}\\n</skill>\`,
234 );
235 } catch {
236 ctx.ui.notify(\`Could not load skill \${name} from \${path}.\`, "error");
237 }
238 }
239 if (blocks.length === 0) return;
240 return {
241 action: "transform",
242 text: event.text + "\\n\\n" + blocks.join("\\n\\n"),
243 images: event.images,
244 };
245 });
246
213247 // Workaround for an upstream Pi context-budgeting bug: pi-ai reuses the
214248 // previous response's usage even when a fork's instructions/tools differ,
215249 // then reserves almost all remaining context for output. OpenRouter can