Repository navigation
Wait for GOA's agent to start before a switch fails - #53
Merged
Merged
Conversation
A switch watched GOA's restarted session for its agent ending, and took a wrapper running alone as the agent gone. On Windows the wrapper is PowerShell staying on (-NoExit), which takes over a second to start the agent, so every switch there failed within a second and GOA was put back, to Claude Code and to Codex alike. Now a switch fails when the session ends, or when the agent was seen under its wrapper and then is gone. An agent not seen yet is waited for until the settle is up, then fails if it still isn't running, so one that ends between two looks is still caught. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ahimsa Jenar Bramsaifstyo Kusuma <tio.rmx@gmail.com>
hostconn embeds prepare.sh and linger.sh and runs them on servers with bash. Built on Windows from a checkout with core.autocrlf=true (Git for Windows' default), they get CRLF and adding a server fails with "syntax error near unexpected token `$'do\r''". Release builds run on Ubuntu and are not affected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ahimsa Jenar Bramsaifstyo Kusuma <tio.rmx@gmail.com>
The chat finds a Claude Code session's conversation by the id its SessionStart hook names, else by a file begun after the session started. Claude Code runs no hooks on Windows, so a session whose agent went on with a conversation begun before it (claude typed by hand in its shell, or /resume) showed nothing: that file reads as another session's. That is what GOD of Agents' chat showed after its switch to Claude Code. Claude Code records, for each of its processes, the conversation it is on (~/.claude/sessions/<pid>.json). Where an agent gets no hooks, the session's processes are looked at from its root down and the first such record names its conversation, which is kept as the session's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ahimsa Jenar Bramsaifstyo Kusuma <tio.rmx@gmail.com>
On Windows, ConPTY writes a line as wide as the screen with no line break after it, so the terminal keeps the next row as that line's soft-wrapped rest and its text joins them. Claude Code's composer then reads as a rule followed by its prompt mark, which no bare mark matches, so PromptEmpty never saw an empty composer. With no hooks on Windows, lines for GOD of Agents are typed in only into an empty composer, so a worker's report waited until GOA was asked to look, and was dropped once it settled the task. tailLines now cuts a rule drawn across the screen from what was written right after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ahimsa Jenar Bramsaifstyo Kusuma <tio.rmx@gmail.com>
Tiyouw
marked this pull request as ready for review
November 9, 2026 09:21
Owner
|
Thanks — this is careful work, and each commit stands on its own. I checked it on Linux: the changed packages pass, and each new test fails with its fix reverted (the composer one at |
Once Claude Code names its conversation, the rule above its composer holds
the name ("──── name ──"). On Windows that rule joins the composer row as
before, and the title between its runs kept the line from reading as an
empty composer: every line for GOD of Agents waited from then on, workers'
reports included, until the task was settled by hand and they went as moot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Ahimsa Jenar Bramsaifstyo Kusuma <tio.rmx@gmail.com>
The docs said a report waiting for GOA isn't dropped when its task is settled, but every line of a settled task was: a report GOA hadn't seen went without a word once the task was settled by hand. A report now goes all the same, saying the task is settled already; only lines saying to look (a stall, a turn's end, a question) are dropped as moot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ahimsa Jenar Bramsaifstyo Kusuma <tio.rmx@gmail.com>
This was referenced Nov 10, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
Fixes so that changing GOD of Agents' agent works on Windows and workers' lines reach it there, plus one for reports that went without a word. Each commit is one change.
1. A switch fails before the agent has even started (
daemon/goa_switch.go)On Windows, changing GOA's agent or config in Settings (
goa.switch) always failed with:This happens with any agent: switching to Codex failed the same way.
Trigger.
goaStaysUpwatches the restarted session forgoaSwitchSettle(5 s) and fails the switch as soon asagentGonesays the agent is gone. On Windows the agent runs underpwsh -NoExit -Command "& '<agent>' …", andprogramGonereads "PowerShell has no child" as the agent having ended. PowerShell starts its child late. Measured on a Windows 11 machine with PowerShell 7 (pwsh.exethrough its WindowsApps alias,-NoLogo -NoExit):claude.exeabout 1.4 s after it started (1401 ms and 1389 ms in two runs).Before. On Windows, a wrapper running alone in the first second after start reads as the agent ended. Every switch is rolled back.
After. A switch fails only when:
The decision is a small pure function,
agentTakes(settle, every, look).2. Shell scripts get CRLF in a Windows checkout (
.gitattributes)hostconnembedsprepare.shandlinger.shand runs them with bash on servers. Git for Windows defaults tocore.autocrlf=true. A build made on Windows from such a checkout therefore embeds the scripts with CRLF, and adding a server fails:git ls-files --eol hostconn/prepare.shshowedi/lf w/crlf. The script embedded in the binary is CRLF when it is built from such a checkout, and LF in the release build. Release builds run on Ubuntu and are not affected.*.sh text eol=lfkeeps every checkout LF.3. The chat shows nothing when the agent goes on with an earlier conversation (
daemon/transcript.go)The chat finds a Claude Code session's conversation in one of two ways:
SessionStarthook names; orClaude Code runs no hooks on Windows (
hooksSupported()is false). So a session whose agent went on with a conversation begun before the session showed nothing in its chat, because that file reads as another session's. Such a conversation comes fromclaudetyped by hand in the session's shell, or from/resume. That is what GOA's chat showed after the switch: the terminal ran Claude Code on a conversation begun the night before, and the chat stayed on "Nothing yet".Claude Code records, for each of its processes, the conversation it is on:
~/.claude/sessions/<pid>.jsonwithpidandsessionId. Where an agent gets no hooks,ownTranscriptnow takes that record as the conversation's id, after a hook's word and beforeMeta.Conversation. The session's processes are looked at from its root down, so the agent comes before anything it started. The id found is kept asMeta.Conversation, so a restart goes on with it. Where hooks run, nothing changes.docs/chat-view.mdsays so.4. Workers' reports wait for GOA instead of reaching it (
core/agent/prompts.go)On Windows, Claude Code runs no hooks. A line for GOA (
[enowX GOA] Task … reports done) is therefore only typed in once GOA is idle andagent.PromptEmptyis sure its composer is empty (goaCanType). ConPTY writes a rule as wide as the screen with no line break after it. The terminal keeps the next row as that rule's soft-wrapped rest, andText()joins them. Claude Code's composer then reads as────…❯, whichpromptAlonedoesn't match, so the composer always read as holding a draft. Reports reached the daemon, then waited until GOA was asked to look; once it settled the task they were dropped as moot. It was intermittent, because ConPTY doesn't always join the rows.tailLinesnow cuts a rule drawn across the screen (8 or more─━═) from what was written right after it. A line that only starts with a dash or two, such as─ Worked for 1m 03s ─, stays whole.5. The same, once GOA's conversation has a name (
core/agent/prompts.go)Once Claude Code names its conversation, the rule above its composer holds the name:
──── <name> ──. On Windows that rule joins the composer row as in change 4, and reads────… <name> ─❯. The title between the rule's runs kept that from reading as a rule then a composer, so every line for GOA waited again from the moment its conversation got a name. Seen on the machine running this branch: no[enowX GOA]line reached GOA after its conversation was named, while reports arrived on the board and lines piled up ingoa-tell.jsonwith GOA idle.splitWrappedRulenow takes a title set off by spaces between a rule's runs as part of the rule. What follows a rule's run with no space, as the composer's mark does, is never a title, so a draft's own dashes stay in the draft.6. A report waiting for GOA went without a word once its task was settled (
daemon/goa.go)docs/goa.mdsaid that of a settled task only a stall line is dropped, and a report isn't. The code dropped every line of a settled task, so a report GOA hadn't been given yet was gone once the task was settled by hand. That is how the reports in change 5 ended: GOA settled their tasks after being asked, and the reports were dropped. A line holding a report (goaLine.Report, kept when a report and its turn's end join) now goes all the same, saying the task is settled already. Lines that only say to look (a stall, a turn's end, a question) are still dropped as moot. The docs now say which.Used on Windows
A Windows build of this branch, with each commit added as it landed, has been the one running on a Windows 11 machine since 9 Nov:
preparefailed on$'do\r'.goa reportlines reach GOA by themselves when it is idle, without GOA being asked to look. They stopped again once GOA's conversation got a name, which change 5 fixes.CI
On
96547ea6, before changes 5 and 6:windows,test (macos-latest)andcloud-postgrespass.test (ubuntu-latest)fails only incli. All six of its failures aresession: load terminal emulator: downloading libghostty-vt.so: … 504 Gateway Timeoutfrom the MyGo release download, before any of this PR's code runs.daemon, including the end-to-end switch tests ingoa_switch_test.go, passes there.main's own CI is also red ontest (ubuntu-latest)ate698f39c(TestLongSessionListsStayQuick). A re-run of the ubuntu job should clear the download error.How it was tested
Switch, platform-neutral (
daemon/goa_switch_settle_test.go):Switch, Windows (
TestGOASwitchWaitsForPowerShellToStartTheAgent): a process table goes through the real WindowsprogramGone. Withpwsh -NoExitalone, then withclaude.exeas its child, the switch takes. With the child seen and then gone, it fails at once.Transcript (
core/transcript/claude_running_test.go):ClaudeRunningreads the first of a session's processes that has a record. It ignores a record naming another pid, a half-written record, and missing ones.Transcript (
daemon/transcript_running_test.go): a conversation begun 12 h before the session is followed, and kept asMeta.Conversation, once its agent's process says it is on it. Before that, it isn't followed. This test fails without the change.Prompt (
core/agent/prompts_wrap_test.go), using the end of a real GOA screen on Windows:This test fails without the change.
TestSplitWrappedRulechecks what is cut and what isn't.Named rule (
TestPromptEmptyUnderANamedRuleWrappedOntoIt): an empty composer under a rule holding a name reads as empty, and one with a draft doesn't. The screen GOA showed on Windows while its lines waited read as holding a draft before the change, and as empty after it.Settled report (
daemon/goa_settled_report_test.go): of a settled task, a report still goes with the note, while a line saying to look doesn't. A report joined to its turn's end stays a report.CRLF, by hand: in a binary built on Windows before
.gitattributes, the embeddedprepare.sh/linger.shmatched the CRLF copy. After a fresh checkout with it, they matched the LF copy.Lint:
go vetandstaticcheck@v0.8.2are clean for./daemon,./core/transcriptand./core/agent(on the head with changes 5 and 6 too).Full suite on Windows: the new tests pass. 29 daemon tests fail on Windows both with and without these changes, checked against unmodified
main, for example the transcript tests that set onlyHOME. They are outside the Windows CI subset. The end-to-end switch tests (goa_switch_test.go) useshand skip on Windows; I had no Linux machine to run them locally.By hand, on Windows: with change 1, GOA's switch to Claude Code took.
make testandmake lintpass (vetandstaticcheckpass for the changed packages;make testnot run on Linux locally)Works the same for a session on this device and on a server (on Linux and macOS the agent is seen at the first look, and hooks still name the conversation)
Docs updated where behavior changed
Commits signed off (
git commit -s)🤖 Generated with Claude Code