From e81357e1df91e44c489e7ce6495e2d8a5317cc1c Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 25 Aug 2026 11:47:53 +0900 Subject: [PATCH] =?UTF-8?q?Stop=20phantom-notification=20hangs=20and=20dro?= =?UTF-8?q?p=20noisy=20"0=EC=B4=88"=20progress=20label?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agents ended turns with "I'll wait … I'll be notified" expecting a background-completion callback that this environment never delivers, so the turn hung and left the planning sentence stuck in the channel. Add a claude-platform rule that no such notification exists — finish within the turn (foreground/poll) or ask the user for a follow-up. Also suppress the elapsed-time suffix on progress messages until at least one 5s bucket has elapsed, so freshly-created progress no longer shows a meaningless "0초". Update the affected message-runtime expectations. Co-Authored-By: Claude Opus 4.7 --- prompts/claude-platform.md | 12 ++++++++++++ src/message-runtime.test.ts | 26 +++++++++++++------------- src/message-turn-controller.ts | 5 ++++- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/prompts/claude-platform.md b/prompts/claude-platform.md index cc819db..e52ff54 100644 --- a/prompts/claude-platform.md +++ b/prompts/claude-platform.md @@ -5,6 +5,18 @@ Use it to acknowledge a request before starting longer work. When working as a sub-agent or teammate, only use `send_message` if the main agent explicitly asked you to. +## Background work never notifies you — finish within the turn + +Nothing in this environment re-invokes you when a background process, a background shell command, or a `run_in_background` job finishes. You will NOT be automatically notified when a build, compile, download, deploy, or any background task completes. The built-in tool descriptions that say "you'll be notified when it finishes" do NOT apply here. + +If you end your turn with a sentence like "I'll wait for the build to complete — I'll be notified", the turn simply ends and nothing ever resumes — the user is left stuck staring at that message. This is a real, repeated failure. Do not do it. + +Therefore: + +- Do not launch long work in the background and then end the turn expecting a callback. Run it in the foreground and block/poll until it actually finishes inside this same turn, then report the real result. +- If something genuinely cannot complete within one turn, do not pretend a notification is coming. Finish with a concrete status and explicitly ask the user to send a follow-up message to continue. For recurring polling of CI/scheduled work, use the `schedule_task` / `watch_ci` tools, which DO re-invoke you. +- Never leave an intermediate "I'll wait … I'll be notified" planning sentence as your final answer. + ## Media attachments When a locally generated image, screenshot, video, audio, or document should appear in Discord, include a `MEDIA:` directive on its own line with an absolute local path: diff --git a/src/message-runtime.test.ts b/src/message-runtime.test.ts index 1cb64e3..deb15c8 100644 --- a/src/message-runtime.test.ts +++ b/src/message-runtime.test.ts @@ -4172,7 +4172,7 @@ describe('createMessageRuntime', () => { // First progress flushed when the second progress arrives expect(channel.sendAndTrack).toHaveBeenCalledWith( chatJid, - P('CI 상태 확인 중입니다.\n\n0초'), + P('CI 상태 확인 중입니다.'), ); // Timer tick edits the tracked progress with updated elapsed time expect(channel.editMessage).toHaveBeenCalledWith( @@ -4272,11 +4272,11 @@ describe('createMessageRuntime', () => { // First progress flushed when second arrives — sendAndTrack throws, falls back to sendMessage expect(channel.sendAndTrack).toHaveBeenCalledWith( chatJid, - P('진행 중입니다.\n\n0초'), + P('진행 중입니다.'), ); expect(channel.sendMessage).toHaveBeenCalledWith( chatJid, - P('진행 중입니다.\n\n0초'), + P('진행 중입니다.'), ); }); @@ -4353,11 +4353,11 @@ describe('createMessageRuntime', () => { // First progress flushed when second arrives — sendAndTrack returns null, falls back to sendMessage expect(channel.sendAndTrack).toHaveBeenCalledWith( chatJid, - P('진행 중입니다.\n\n0초'), + P('진행 중입니다.'), ); expect(channel.sendMessage).toHaveBeenCalledWith( chatJid, - P('진행 중입니다.\n\n0초'), + P('진행 중입니다.'), ); }); @@ -4461,7 +4461,7 @@ describe('createMessageRuntime', () => { expect(channel.sendAndTrack).toHaveBeenCalledTimes(1); expect(channel.sendAndTrack).toHaveBeenCalledWith( chatJid, - P('첫 번째 진행상황입니다.\n\n0초'), + P('첫 번째 진행상황입니다.'), ); // Timer tick updates tracked progress via edit expect(channel.editMessage).toHaveBeenCalledWith( @@ -4573,7 +4573,7 @@ describe('createMessageRuntime', () => { // First progress flushed when second arrives expect(channel.sendAndTrack).toHaveBeenCalledWith( chatJid, - P('오래 걸리는 작업입니다.\n\n0초'), + P('오래 걸리는 작업입니다.'), ); // Timer ticks update the tracked progress with longer durations expect(channel.editMessage).toHaveBeenCalledWith( @@ -4679,7 +4679,7 @@ describe('createMessageRuntime', () => { // First progress flushed when second arrives expect(channel.sendAndTrack).toHaveBeenCalledWith( chatJid, - P('테스트를 돌리는 중입니다.\n\n0초'), + P('테스트를 돌리는 중입니다.'), ); // Timer tick updates tracked progress expect(channel.editMessage).toHaveBeenCalledWith( @@ -5088,13 +5088,13 @@ describe('createMessageRuntime', () => { expect(channel.sendAndTrack).toHaveBeenNthCalledWith( 1, chatJid, - P('첫 번째 진행상황입니다.\n\n0초'), + P('첫 번째 진행상황입니다.'), ); // After empty final resets state, third progress flushed when fourth arrives (new message) expect(channel.sendAndTrack).toHaveBeenNthCalledWith( 2, chatJid, - P('두 번째 진행상황입니다.\n\n0초'), + P('두 번째 진행상황입니다.'), ); // Timer tick edits the first tracked progress expect(channel.editMessage).toHaveBeenCalledWith( @@ -5213,7 +5213,7 @@ describe('createMessageRuntime', () => { // First progress flushed when second arrives expect(channel.sendAndTrack).toHaveBeenCalledWith( chatJid, - P('검증 중입니다.\n\n0초'), + P('검증 중입니다.'), ); // Once the tracked progress exists, later progress updates replace the visible heading directly. expect(channel.editMessage).toHaveBeenCalledWith( @@ -5329,7 +5329,7 @@ describe('createMessageRuntime', () => { // The first flushed progress is still tracked expect(channel.sendAndTrack).toHaveBeenCalledWith( chatJid, - P('진행 중입니다.\n\n0초'), + P('진행 중입니다.'), ); // Edit is attempted on the tracked message (first fails, subsequent succeed) expect(channel.editMessage).toHaveBeenCalledWith( @@ -5568,7 +5568,7 @@ describe('createMessageRuntime', () => { // First progress flushed when second arrives expect(channel.sendAndTrack).toHaveBeenCalledWith( chatJid, - P('중간 진행상황입니다.\n\n0초'), + P('중간 진행상황입니다.'), ); // Error replaces the tracked progress message with the failure final expect(channel.sendMessage).not.toHaveBeenCalled(); diff --git a/src/message-turn-controller.ts b/src/message-turn-controller.ts index 71f8fbe..6fb9f27 100644 --- a/src/message-turn-controller.ts +++ b/src/message-turn-controller.ts @@ -496,7 +496,10 @@ export class MessageTurnController { ? 0 : Math.floor((Date.now() - this.progressStartedAt) / 5_000) * 5000; - const suffix = `\n\n${formatElapsedKorean(elapsedMs)}`; + // Skip the elapsed-time label until at least one 5s bucket has elapsed, so + // a freshly-created progress message doesn't show a meaningless "0초". + const suffix = + elapsedMs >= 5000 ? `\n\n${formatElapsedKorean(elapsedMs)}` : ''; const body = this.composeProgressBody(text); this.persistProgressBody(body);