diff --git a/apps/worker/src/mcp/roomote-mcp-server/chat-reply-satisfaction.ts b/apps/worker/src/mcp/roomote-mcp-server/chat-reply-satisfaction.ts index 9ffc4e827..95c4528f5 100644 --- a/apps/worker/src/mcp/roomote-mcp-server/chat-reply-satisfaction.ts +++ b/apps/worker/src/mcp/roomote-mcp-server/chat-reply-satisfaction.ts @@ -20,6 +20,8 @@ export type ChatReplyPurpose = interface ChatReplySatisfactionState { startedAtMs?: number; + /** Blocks opening acknowledgements and progress from automation-started tasks. */ + suppressNonTerminalRepliesWithoutTurn?: boolean; /** * Set at launch for late-bound automation execution tasks: the Stop hook * blocks silent completion, and the silence hook stamps post-closeout work diff --git a/apps/worker/src/run-task/__tests__/run-task.test.ts b/apps/worker/src/run-task/__tests__/run-task.test.ts index 04193b12b..b6a58529c 100644 --- a/apps/worker/src/run-task/__tests__/run-task.test.ts +++ b/apps/worker/src/run-task/__tests__/run-task.test.ts @@ -1245,6 +1245,7 @@ describe('runTask', () => { JSON.stringify({ startedAtMs: 345_678, currentTurnRequiresInitialAck: false, + suppressNonTerminalRepliesWithoutTurn: true, }), 'utf8', ); @@ -1309,6 +1310,7 @@ describe('runTask', () => { JSON.stringify({ startedAtMs: 456_000, currentTurnRequiresInitialAck: false, + suppressNonTerminalRepliesWithoutTurn: true, }), 'utf8', ); @@ -1367,6 +1369,7 @@ describe('runTask', () => { JSON.stringify({ startedAtMs: 456_789, currentTurnRequiresInitialAck: false, + suppressNonTerminalRepliesWithoutTurn: true, }), 'utf8', ); @@ -1425,6 +1428,7 @@ describe('runTask', () => { JSON.stringify({ startedAtMs: 567_891, currentTurnRequiresInitialAck: false, + suppressNonTerminalRepliesWithoutTurn: true, }), 'utf8', ); diff --git a/apps/worker/src/run-task/__tests__/slack-silence-hook-script.test.ts b/apps/worker/src/run-task/__tests__/slack-silence-hook-script.test.ts index c3773b729..28804f8e9 100644 --- a/apps/worker/src/run-task/__tests__/slack-silence-hook-script.test.ts +++ b/apps/worker/src/run-task/__tests__/slack-silence-hook-script.test.ts @@ -358,7 +358,7 @@ describe('SLACK_SILENCE_HOOK_SCRIPT', () => { const stateFilePath = writeState({ startedAtMs: Date.now(), currentTurnRequiresInitialAck: false, - requiresTerminalCloseoutWithoutTurn: true, + suppressNonTerminalRepliesWithoutTurn: true, }); const result = runHook({ @@ -384,7 +384,7 @@ describe('SLACK_SILENCE_HOOK_SCRIPT', () => { const stateFilePath = writeState({ startedAtMs: Date.now(), currentTurnRequiresInitialAck: false, - requiresTerminalCloseoutWithoutTurn: true, + suppressNonTerminalRepliesWithoutTurn: true, }); const result = runHook({ @@ -406,7 +406,7 @@ describe('SLACK_SILENCE_HOOK_SCRIPT', () => { const stateFilePath = writeState({ startedAtMs: Date.now(), currentTurnRequiresInitialAck: false, - requiresTerminalCloseoutWithoutTurn: true, + suppressNonTerminalRepliesWithoutTurn: true, }); const result = runHook({ @@ -424,6 +424,34 @@ describe('SLACK_SILENCE_HOOK_SCRIPT', () => { expect(result.stdout).toBe(''); }); + it.each([ + 'roomote_post_to_channel', + 'roomote_reply_to_slack_thread', + 'roomote_send_chat_reaction_emoji', + ])('rejects %s from late-bound automation tasks', (toolName) => { + const stateFilePath = writeState({ + startedAtMs: Date.now(), + currentTurnRequiresInitialAck: false, + suppressNonTerminalRepliesWithoutTurn: true, + }); + + const result = runHook({ + input: { + hook_event_name: 'PreToolUse', + tool_name: toolName, + }, + env: { + ROOMOTE_SLACK_REPLY_SATISFACTION_STATE_FILE: stateFilePath, + }, + }); + + expect(result.status).toBe(0); + expect(JSON.parse(result.stdout)).toMatchObject({ + decision: 'block', + permissionDecision: 'deny', + }); + }); + it('allows tool_search before the current Slack turn has been acknowledged', () => { const stateFilePath = writeState({ currentTurnMessageTs: 'user-111.222', diff --git a/apps/worker/src/run-task/run-task.ts b/apps/worker/src/run-task/run-task.ts index 6bb3cb871..1ebb598a6 100644 --- a/apps/worker/src/run-task/run-task.ts +++ b/apps/worker/src/run-task/run-task.ts @@ -1037,6 +1037,9 @@ export const runTask = async ({ startedAtMs, currentTurnRequiresInitialAck: shouldRequireInitialAckOnInitialTurn(taskRun), + ...(isSilentChannelAutomationLaunch(taskRun) + ? { suppressNonTerminalRepliesWithoutTurn: true } + : {}), ...(initialTurnMessageTs ? { currentTurnMessageTs: initialTurnMessageTs, diff --git a/apps/worker/src/run-task/slack-silence-hook-script.ts b/apps/worker/src/run-task/slack-silence-hook-script.ts index ffb753378..4c923d91c 100644 --- a/apps/worker/src/run-task/slack-silence-hook-script.ts +++ b/apps/worker/src/run-task/slack-silence-hook-script.ts @@ -240,7 +240,7 @@ function getSendChatReplyPurpose(input) { function isPrematureAutomationReply(input, state) { if ( - state?.requiresTerminalCloseoutWithoutTurn !== true || + state?.suppressNonTerminalRepliesWithoutTurn !== true || trimString(state.currentTurnMessageTs) ) { return false;