Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/issues/subagent-session-stays-starting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Subagent session can stay in `starting` indefinitely

While implementing the macOS desktop app with Kit 0.1.86, a final review subagent remained in `starting` and never accepted work. Closing completed sibling sessions did not unblock it. Cancelling that session and creating a fresh subagent produced the same behavior.

This creates friction because `subagents` provides no failure reason or timeout for the stuck startup, so the parent cannot distinguish queueing from a failed harness launch. The parent must abandon the review or repeatedly cancel and retry.

Expected behavior: a subagent either starts within a bounded interval or transitions to a failed state with a diagnostic that explains the capacity or harness problem.
48 changes: 47 additions & 1 deletion fixtures/mock-acp.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#!/usr/bin/env python3
import json
import os
import subprocess
import sys
import threading
import time
Expand All @@ -11,6 +13,11 @@
selected_models = {}
model_ids = ["mock/default", "mock/requested"]

if os.environ.get("MOCK_CHILD_PID_FILE"):
child = subprocess.Popen([sys.executable, "-c", "import signal,time; signal.signal(signal.SIGTERM, signal.SIG_IGN); time.sleep(60)"] )
with open(os.environ["MOCK_CHILD_PID_FILE"], "w") as file:
file.write(str(child.pid))


def send(message):
with write_lock:
Expand All @@ -25,12 +32,16 @@ def respond(request_id, result):
def prompt(request):
params = request["params"]
session_id = params["sessionId"]
text = params["prompt"][0]["text"]
text = next((block.get("text", "") for block in params["prompt"] if block.get("type") == "text"), "")
if "MOCK_HANG" in text:
return
time.sleep(0.40)
if "MOCK_SELECTED_MODEL" in text:
text = selected_models.get(session_id, model_ids[0])
if "MOCK_STRUCTURED_OUTPUT" in text:
text = json.dumps({"approved": True, "reason": "mock approved"})
if "MOCK_MEDIA" in text:
text = ",".join(block.get("type", "unknown") for block in params["prompt"])
if "MOCK_RICH_OUTPUT" in text:
updates = [
{
Expand Down Expand Up @@ -69,6 +80,12 @@ def prompt(request):
"method": "session/update",
"params": {"sessionId": session_id, "update": update},
})
if update["sessionUpdate"] == "tool_call":
sys.stderr.write("\x01kit-runtime\x01" + json.dumps({"event": "child_started", "call": "call-1:compose:shell", "tool": "shell", "summary": "echo mock", "at": 1}) + "\n")
sys.stderr.flush()
elif update["sessionUpdate"] == "tool_call_update":
sys.stderr.write("\x01kit-runtime\x01" + json.dumps({"event": "child_finished", "call": "call-1:compose:shell", "tool": "shell", "ok": True, "summary": "done", "millis": 2}) + "\nmock diagnostic\n")
sys.stderr.flush()
text = "rich done"
send({
"jsonrpc": "2.0",
Expand Down Expand Up @@ -99,6 +116,10 @@ def prompt(request):
elif method == "session/new":
selected_models["base"] = model_ids[0]
result = {"sessionId": "base"}
if os.environ.get("MOCK_EXIT_TAIL"):
sys.stdout.write(json.dumps({"jsonrpc": "2.0", "id": request["id"], "result": result}))
sys.stdout.flush()
os._exit(0)
if supports_models:
result["configOptions"] = [{
"id": "model",
Expand All @@ -111,6 +132,31 @@ def prompt(request):
],
}]
respond(request["id"], result)
send({"jsonrpc": "2.0", "method": "session/update", "params": {
"sessionId": result["sessionId"],
"update": {"sessionUpdate": "available_commands_update", "availableCommands": [
{"name": "compact", "description": "Compact the session context"}
]},
}})
elif method == "session/load":
session_id = request["params"]["sessionId"]
selected_models[session_id] = model_ids[0]
for update in [
{"sessionUpdate": "user_message_chunk", "content": {"type": "text", "text": "replayed user"}},
{"sessionUpdate": "agent_message_chunk", "content": {"type": "text", "text": "replayed assistant"}},
]:
send({"jsonrpc": "2.0", "method": "session/update", "params": {"sessionId": session_id, "update": update}})
result = {}
if supports_models:
result["configOptions"] = [{
"id": "model", "name": "Model", "category": "model", "type": "select",
"currentValue": model_ids[0], "options": [{"value": value, "name": value} for value in model_ids],
}]
respond(request["id"], result)
send({"jsonrpc": "2.0", "method": "session/update", "params": {
"sessionId": session_id,
"update": {"sessionUpdate": "available_commands_update", "availableCommands": [{"name": "compact", "description": "Compact the session context"}]},
}})
elif method == "session/fork":
if not supports_fork:
send({
Expand Down
3 changes: 3 additions & 0 deletions macos/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.build/
*.xcuserstate
xcuserdata/
Loading