fix: Tell each codespace agent-worker turn that a turn is atomic (no-changelog) (#36304)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Declan Carroll
2026-08-14 11:50:32 +00:00
committed by GitHub
parent 51aa6fb519
commit 51b0de1875
2 changed files with 61 additions and 4 deletions
+17 -2
View File
@@ -71,6 +71,19 @@ A turn stops after about 25 minutes (`TURN_TIMEOUT_MS`). This limit is below the
n8n Wait limit. So the worker reports a clear message before n8n reports a
generic timeout. Keep the worker limit below the n8n limit if you change either.
**A turn is atomic, and the worker tells the session so.** The turn ends on the
session's final message, and its children end with it: a background `Bash` task
is killed, `Monitor` events never arrive, `PushNotification` has nowhere to go,
and `ScheduleWakeup` never fires. The session also gets no turn of its own to
report back in — the turn's resume URL continues one waiting n8n execution and is
then spent, so nothing on the box can post to the thread unprompted. A session
that backgrounds a build and signs off with "I'll verify once it finishes" is
therefore describing something that cannot happen. The worker states this in
`--append-system-prompt` on every turn (`turnContract`), together with a pointer
to this file for the box-specific parts. This is only the n8n/Slack path: an
interactive session (`pnpm session`, tmux) is long-lived, so background work,
monitors and notifications behave normally there.
### Build and run the app in a session
The prebuild already installed the dependencies and warmed the build. So a
@@ -100,8 +113,10 @@ session rarely needs a cold `pnpm install` or a full `pnpm build`. Both are slow
turbo cache and is fast when warm.
- To clear stale build outputs after a branch switch, run `pnpm reset`. Add
`--full` if that does not clear it.
- Give a long build its own turn. Do not chain an install and a full build
behind other work in one turn.
- Run a long build in the foreground and give it its own turn. Backgrounding it
does not help: it is killed when the turn ends (see above). Do not chain an
install and a full build behind other work in one turn either — that is what
runs into the 25-minute limit.
## Flaky tools (MCP)
+44 -2
View File
@@ -67,11 +67,53 @@ const TURN_ENV = { ...process.env };
if (BOX_ID) TURN_ENV.CODESPACE_NAME = BOX_ID;
if (GITHUB_USER) TURN_ENV.GITHUB_USER = GITHUB_USER;
function runClaude({ message, sessionId, cwd }) {
const CODESPACE_DOCS = '.devcontainer/codespaces/README.md';
// A session cannot be told any of this after its final message, and a system
// prompt is not part of the resumed transcript, so send it on every turn. State
// the turn's hard limits inline: a session that has to read a file to learn them
// can reply before it gets there. Point at the box docs for the rest — they
// already cover dev:up, ports, and build cost, and AGENTS.md does not.
function turnContract(author) {
return [
'# Your runtime',
'You are one turn of a Slack thread, driven by an n8n workflow that runs you as a headless',
'`claude -p` on a GitHub codespace. Your final message is the reply that reaches Slack, so keep',
'it short and skip heavy markdown.',
author ? `You are replying to ${author}.` : '',
'',
'# A turn is atomic',
'The turn ends when you emit your final message, and everything you started ends with it:',
'background Bash tasks are killed, Monitor events never arrive, PushNotification has nowhere to',
'go, and ScheduleWakeup never fires. You get no turn of your own afterwards — you cannot speak',
'again until a human writes again. So run long work (builds, test suites, restarts) in the',
'foreground of this turn and wait for it, or do not start it at all. Never end a turn promising',
`to verify, check back, or follow up. Work that will not fit the turn limit of ~${Math.round(
TURN_TIMEOUT_MS / 60_000,
)} minutes`,
'should be split: do the part that fits, then say what to ask for next.',
'',
'# This box',
`You are on codespace ${BOX_ID ?? '(unknown)'}, not a laptop. Before you build, start, or expose`,
`the app, read ${CODESPACE_DOCS} ("Build and run the app in a session"). It is box-specific and`,
'the repo AGENTS.md does not cover it.',
]
.filter(Boolean)
.join('\n');
}
function runClaude({ message, sessionId, cwd, author }) {
const safeCwd = resolvePath(typeof cwd === 'string' && cwd ? cwd : `${ROOT}/n8n`);
if (safeCwd !== ROOT && !safeCwd.startsWith(ROOT + sep))
throw new Error(`cwd must be under ${ROOT}`);
const args = ['-p', '--output-format', 'json', '--dangerously-skip-permissions'];
const args = [
'-p',
'--output-format',
'json',
'--dangerously-skip-permissions',
'--append-system-prompt',
turnContract(typeof author === 'string' ? author : ''),
];
if (sessionId) args.push('--resume', sessionId);
args.push(message);
return new Promise((res, rej) => {