feat(FN-3606): correct chat-bound message routing and stabilize merger auto
Merger receives substantial hardening: autostash race-rescue with de-duplication, advisory observer for destructive operations, and subject-line preference for step headlines. The TUI gains a narrow log-split mode on System panel with proper back-navigation to the main pane. Chat header and mobile n Fusion-Task-Id: FN-3606
This commit is contained in:
@@ -587,11 +587,15 @@ The dashboard mailbox UI also uses the same metadata contract when users click *
|
||||
|
||||
For dashboard user messaging, agents should target the canonical user recipient ID `dashboard`.
|
||||
|
||||
When an agent is sending to the dashboard user through `fn_send_message`, the message must be stored as `agent-to-user` (agent → dashboard user), not as a user/CLI → agent mailbox message.
|
||||
|
||||
Runtime safeguards defensively normalize the legacy alias forms below to the same logical dashboard user:
|
||||
- `dashboard` (canonical)
|
||||
- `user:dashboard`
|
||||
- `User: user:dashboard`
|
||||
|
||||
If the message type is omitted but the recipient normalizes to the dashboard user alias, routing defaults to the `agent-to-user` direction to preserve correct inbox semantics.
|
||||
|
||||
This normalization applies on send and mailbox reads, so replies from agents still land in the dashboard inbox even when older alias-like recipient strings appear.
|
||||
|
||||
### How It Works
|
||||
|
||||
@@ -981,6 +981,8 @@ describe("ChatManager.sendMessage", () => {
|
||||
expect(mockAgentStore.init).toHaveBeenCalledTimes(1);
|
||||
expect(mockAgentStore.getAgent).toHaveBeenCalledWith("agent-001");
|
||||
expect(createOptions.systemPrompt).toContain("Be calm and precise.");
|
||||
expect(createOptions.systemPrompt).toContain("type: \"agent-to-user\"");
|
||||
expect(createOptions.systemPrompt).toContain("to_id: \"dashboard\"");
|
||||
});
|
||||
|
||||
it("passes enriched system prompt with agent memory when agent context is available", async () => {
|
||||
|
||||
@@ -122,6 +122,8 @@ async function ensureEngineReady(): Promise<void> {
|
||||
/** Chat system prompt for the AI agent */
|
||||
const CHAT_SYSTEM_PROMPT = `You are a helpful AI assistant integrated into the fn task board system. You help users with questions about their project, code, architecture, and tasks. You have access to project files and can read them to provide informed responses. Be concise, accurate, and helpful. When referencing files or code, provide specific paths and line numbers when possible.`;
|
||||
|
||||
const CHAT_AGENT_MESSAGE_ROUTING_GUIDANCE = `## Messaging Semantics\n\nWhen this chat is bound to an agent and you need to send a mailbox message to the dashboard user, use \`fn_send_message\` with \`type: "agent-to-user"\` and target the dashboard user alias (\`to_id: "dashboard"\` is preferred). Never route that as a user/CLI → agent message.`;
|
||||
|
||||
/** Rate limiting window in milliseconds (1 minute) */
|
||||
const RATE_LIMIT_WINDOW_MS = 60 * 1000;
|
||||
|
||||
@@ -862,6 +864,7 @@ export class ChatManager {
|
||||
basePrompt: CHAT_SYSTEM_PROMPT,
|
||||
includeProjectMemory: true,
|
||||
});
|
||||
systemPrompt = `${systemPrompt}\n\n${CHAT_AGENT_MESSAGE_ROUTING_GUIDANCE}`;
|
||||
} catch (promptBuildError) {
|
||||
const message = promptBuildError instanceof Error ? promptBuildError.message : String(promptBuildError);
|
||||
diagnostics.warn(`Failed to build enriched system prompt for ${agent.id}: ${message}`);
|
||||
|
||||
@@ -716,6 +716,23 @@ describe("createSendMessageTool", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.each(["dashboard", "user:dashboard", "User: user:dashboard"])(
|
||||
"infers agent-to-user when type is omitted for dashboard alias '%s'",
|
||||
async (dashboardAlias) => {
|
||||
const mockMessage = createMessage({ toId: "dashboard", toType: "user", type: "agent-to-user" });
|
||||
vi.mocked(messageStore.sendMessage).mockReturnValue(mockMessage);
|
||||
|
||||
await executeTool(tool, {
|
||||
to_id: dashboardAlias,
|
||||
content: "Test",
|
||||
});
|
||||
|
||||
expect(messageStore.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ toId: "dashboard", toType: "user", type: "agent-to-user" }),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("uses provided type when specified and maps recipient type for agent-to-user", async () => {
|
||||
const mockMessage = createMessage({ toType: "user", type: "agent-to-user" });
|
||||
vi.mocked(messageStore.sendMessage).mockReturnValue(mockMessage);
|
||||
|
||||
@@ -1431,7 +1431,9 @@ export function createSendMessageTool(messageStore: MessageStore, fromAgentId: s
|
||||
}
|
||||
|
||||
try {
|
||||
const messageType = params.type ?? "agent-to-agent";
|
||||
const inferredDashboardRecipient = normalizeMessageParticipant(params.to_id, "user");
|
||||
const messageType = params.type
|
||||
?? (inferredDashboardRecipient.id === DASHBOARD_USER_ID ? "agent-to-user" : "agent-to-agent");
|
||||
const recipientType: "user" | "agent" = messageType === "agent-to-user" ? "user" : "agent";
|
||||
const recipient = recipientType === "user"
|
||||
? normalizeMessageParticipant(params.to_id, recipientType)
|
||||
|
||||
Reference in New Issue
Block a user