fix(agent-runner): require explicit destination addressing, fix per-destination threading
The poll loop had a bare-text routing fallback in dispatchResultText: when the agent produced text without <message to="..."> wrapping, it would auto- route to the session's originating channel (via a frozen RoutingContext) or to the single configured destination. This caused three problems: 1. Routing drift: RoutingContext was extracted once from the initial batch and never refreshed. When the initial batch was a null-routed cron task and a real chat arrived mid-query, replies were silently dropped to scratchpad because the frozen routing had all-null fields. 2. Cross-channel thread bleed: sendToDestination applied a single routing.threadId to every outbound message regardless of destination. In agent-shared sessions (multiple channels sharing one session), one channel's thread ID was stamped onto messages to a different channel. 3. Inconsistent formatting: task, webhook, and system messages had no origin metadata in their formatted output, so the agent couldn't tell which destination they came from — even when the underlying messages_in rows carried routing fields. Changes: - Remove the bare-text routing fallbacks in dispatchResultText (both the routing-based and single-destination shortcuts). All agent output must be wrapped in <message to="name">...</message>. Bare text is scratchpad. - Update buildDestinationsSection() to require explicit wrapping for all groups, including single-destination. No more "no special wrapping needed" shortcut. - Resolve thread_id per-destination via resolveDestinationThread(), which queries messages_in for the most recent message matching the target channel+platform. Falls back to null (top-level channel message) when no prior inbound exists for that destination. - Extract originAttr() helper in formatter.ts and apply it to all message types. Tasks now render as <task from="dest" time="...">, webhooks as <webhook from="dest" source="..." event="...">, system responses as <system_response from="dest" ...>. The agent always sees where a message originated. - Add a PreCompact shell hook (compact-instructions.ts) that outputs custom compaction instructions, telling the compactor to preserve recent message XML structure and routing metadata in the summary. Wired via settings.json in the .claude-shared scaffold, with a migration path (ensurePreCompactHook) for existing groups. Relation to open PRs: - #2277 (mergeRouting) becomes unnecessary — the routing fallback it patches no longer exists. Can be closed. - #2327 (post-compaction destination reminder) is complementary — it handles the post-compaction push, this handles pre-compaction instructions. Both can merge independently. - #2328 (default routing instruction) is complementary — it adds "reply to the from= destination" guidance to the multi-destination section. Compatible with the unified instruction format here. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,18 @@ const DEFAULT_SETTINGS_JSON =
|
||||
CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD: '1',
|
||||
CLAUDE_CODE_DISABLE_AUTO_MEMORY: '0',
|
||||
},
|
||||
hooks: {
|
||||
PreCompact: [
|
||||
{
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
command: 'bun /app/src/compact-instructions.ts',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
@@ -71,10 +83,13 @@ export function initGroupFilesystem(group: AgentGroup, opts?: { instructions?: s
|
||||
if (!fs.existsSync(settingsFile)) {
|
||||
fs.writeFileSync(settingsFile, DEFAULT_SETTINGS_JSON);
|
||||
initialized.push('settings.json');
|
||||
} else {
|
||||
ensurePreCompactHook(settingsFile, initialized);
|
||||
}
|
||||
|
||||
// Skills directory — created empty here; symlinks are synced at spawn
|
||||
// time by container-runner.ts based on container.json skills selection.
|
||||
// (ensurePreCompactHook is defined after the main function.)
|
||||
const skillsDst = path.join(claudeDir, 'skills');
|
||||
if (!fs.existsSync(skillsDst)) {
|
||||
fs.mkdirSync(skillsDst, { recursive: true });
|
||||
@@ -90,3 +105,32 @@ export function initGroupFilesystem(group: AgentGroup, opts?: { instructions?: s
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const PRE_COMPACT_COMMAND = 'bun /app/src/compact-instructions.ts';
|
||||
|
||||
/**
|
||||
* Patch an existing settings.json to add the PreCompact hook if missing.
|
||||
* Runs on every group init so pre-existing groups pick up the hook.
|
||||
*/
|
||||
function ensurePreCompactHook(settingsFile: string, initialized: string[]): void {
|
||||
try {
|
||||
const raw = fs.readFileSync(settingsFile, 'utf-8');
|
||||
const settings = JSON.parse(raw);
|
||||
|
||||
// Check if there's already a PreCompact hook with our command.
|
||||
const existing = settings.hooks?.PreCompact as unknown[] | undefined;
|
||||
if (existing && JSON.stringify(existing).includes(PRE_COMPACT_COMMAND)) return;
|
||||
|
||||
// Add the hook, preserving existing hooks.
|
||||
if (!settings.hooks) settings.hooks = {};
|
||||
if (!settings.hooks.PreCompact) settings.hooks.PreCompact = [];
|
||||
settings.hooks.PreCompact.push({
|
||||
hooks: [{ type: 'command', command: PRE_COMPACT_COMMAND }],
|
||||
});
|
||||
|
||||
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
|
||||
initialized.push('settings.json (added PreCompact hook)');
|
||||
} catch {
|
||||
// Don't break init if settings.json is malformed — it'll use whatever's there.
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user