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>
35 lines
1.5 KiB
TypeScript
35 lines
1.5 KiB
TypeScript
/**
|
|
* PreCompact hook script — outputs custom compaction instructions to stdout.
|
|
*
|
|
* Claude Code captures the stdout of PreCompact shell hooks and passes it
|
|
* as `customInstructions` to the compaction prompt. This ensures the
|
|
* compaction summary preserves message routing context that the agent needs
|
|
* to correctly address responses.
|
|
*
|
|
* Invoked by the PreCompact hook in .claude-shared/settings.json:
|
|
* "command": "bun /app/src/compact-instructions.ts"
|
|
*/
|
|
import { getAllDestinations } from './destinations.js';
|
|
|
|
const destinations = getAllDestinations();
|
|
const names = destinations.map((d) => d.name);
|
|
|
|
const instructions = [
|
|
'Preserve the following in the compaction summary:',
|
|
'',
|
|
'1. For recent messages, keep the full XML structure including all attributes:',
|
|
' - <message from="..." sender="..." time="..."> for chat messages',
|
|
' - <task from="..." time="..."> for scheduled tasks',
|
|
' - <webhook from="..." source="..." event="..."> for webhooks',
|
|
' The message content can be summarized if long, but the XML tags and attributes must remain.',
|
|
'',
|
|
'2. Preserve the chronological message/reply sequence of recent exchanges.',
|
|
' The agent needs to see: who said what, in what order, and from which destination.',
|
|
'',
|
|
'3. The `from` attribute identifies which destination sent the message.',
|
|
' The agent MUST wrap all responses in <message to="name">...</message> blocks.',
|
|
` Available destinations: ${names.length > 0 ? names.map((n) => `\`${n}\``).join(', ') : '(none)'}`,
|
|
];
|
|
|
|
console.log(instructions.join('\n'));
|