a commander is told where to record itself, however its loop gets started
The record instruction lived in one place: the preamble the background-job dialog composes. Fine while the dialog was the only way to arm a commander, and wrong for the flow being built — the commander arming itself from the conversation, with the owner never touching the launcher. An agent starting its own background job never sees that preamble, so it would record nothing, and delete would quietly go back to leaving orphaned loops. So the paths are compiled into a commander's session preamble, beside the affordances and the skills index. THE SPLIT, which is the part worth keeping: the METHOD lives in the Commanders skill — read on demand, as long as it needs to be — and the PATHS are compiled in always, because they are a fact about this session that the agent cannot derive. That is exactly the line between a skill and an always-loaded block, and it falls in the same place here as everywhere else. The account is derived from the CWD rather than passed in, because the caller does not have it: the sidecar knows a member's osUser and home, and an owner's turn carries no email at all. A commander's cwd is DATA_PATH/<email>/commanders, so the email is in the path — and it is verified by reconstructing the directory from it, so a coincidentally-shaped path cannot pass. Keyed on the cwd rather than a database lookup, for the same reason every other surface uses isCommandersDir: a session must not be a commander in one place and not another. It also works before the row exists, which a lookup would not — and the first turn is when the row does not exist yet. Stated in the file rather than left to be found: a brand-new commander's FIRST turn gets no block, because the transcript id does not exist until that turn begins. In practice the first turn is the conversation about what to watch and the loop starts on a later one, but a commander that armed itself on its opening message would not have been told where to record. Verified on the three cases: a real commanders cwd renders the block with both paths, an ordinary chat gets null, and a session with no id yet gets null. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import { query, type Query, type SpawnOptions, type SpawnedProcess } from '@anth
|
||||
import type { ChatEvent, PromptImage } from '../../api/chat/types';
|
||||
import { affordanceBlock } from '../../affordances/load';
|
||||
import { skillsBlock } from '../../skills/load';
|
||||
import { commanderBlock } from '../../api/command-hq/commander-block';
|
||||
import type { ClaudeSpawnParams, ClaudeSpawnStreamingParams, ClaudeCodeResult, LiveClaudeSession } from '../protocol';
|
||||
import { setClaudeSession, clearClaudeSession, getClaudeSession } from './state';
|
||||
import { countsAsOutput, diagnoseExit, stderrReason } from './exit-diagnosis';
|
||||
@@ -608,7 +609,18 @@ function createSession(
|
||||
// the inverse of an affordance. Read here rather than at module load for the same reason: a skill
|
||||
// written an hour ago should be offered on the next session.
|
||||
const skills = skillsBlock();
|
||||
const preamble = [affordances, skills].filter(Boolean).join('\n\n') || null;
|
||||
/*
|
||||
And, for a commander, the two paths only this session can be told.
|
||||
|
||||
Not in the skill, which is read on demand and cannot know them; not only in the background-job dialog's
|
||||
preamble, which is where this used to live and which an agent arming itself never sees. See
|
||||
`api/command-hq/commander-block.ts`.
|
||||
*/
|
||||
const commander = commanderBlock({
|
||||
sessionId: resumeId ?? null,
|
||||
cwd: params.cwd ?? resumeCwd ?? null,
|
||||
});
|
||||
const preamble = [affordances, skills, commander].filter(Boolean).join('\n\n') || null;
|
||||
|
||||
const q = query({
|
||||
prompt: input.gen as AsyncIterable<SdkUserMessage>,
|
||||
|
||||
Reference in New Issue
Block a user