fix: suppress [SILENT]-prefixed messages from reaching chat channels#1005
Closed
gorillahub wants to merge 49 commits intoqwibitai:mainfrom
Closed
fix: suppress [SILENT]-prefixed messages from reaching chat channels#1005gorillahub wants to merge 49 commits intoqwibitai:mainfrom
gorillahub wants to merge 49 commits intoqwibitai:mainfrom
Conversation
…iner model - Replace GroupState.active boolean with containers Map<string, ContainerSlot> - Each container slot tracks independent lifecycle (idleWaiting, process, groupFolder) - enqueueMessageCheck reuses idle containers before spawning, allows concurrent spawns - sendMessage finds any idle non-task container in the group - registerProcess/notifyIdle/closeStdin accept optional containerId parameter - processMessagesFn callback signature now includes containerId - pendingRegistrations map bridges containerId from runForGroup to registerProcess - Global activeCount cap enforced across all groups' container slots - drainGroup only starts new containers when under global cap
…istency - Add inline comments for all 6 edge cases from plan verification - Edge case 1: race condition — slot created synchronously, idleWaiting=false - Edge case 2: idle reuse checked before global cap (no new slot needed) - Edge case 3: finally blocks prevent orphan slots on error - Edge case 4: multiple idle containers — first picked, others time out - Edge case 5: task dedup scans all container slots, not single field - Edge case 6: drainGroup only starts work when under cap, no interference
- Add 01-01-SUMMARY.md with full execution results - Update STATE.md: plan 01-01 complete, advance to plan 02
- Replace 13 single-container tests with 17 multi-container tests - Add CONC-01: concurrent containers for same group - Add CONC-04: global cap across same-group containers - Add CONC-05: idle container reuse (no new spawn) - Add COMPAT-01: single message backward compatibility - Add multi-group mixed concurrency under global cap - Update all existing test mocks for containerId parameter - 371/371 tests pass (was 366/367 before refactor)
- 17/17 tests pass covering CONC-01, CONC-04, CONC-05, COMPAT-01 - 371/371 full suite passes (zero regressions)
…upport - processGroupMessages accepts containerId, threads to closeStdin/notifyIdle/registerProcess - runAgent passes undefined sessionId for fresh sessions per container (CONC-02) - Idle timeout targets specific container via containerId (CONC-03) - SchedulerDependencies.onProcess includes containerId parameter - Task fn callback receives containerId from GroupQueue.runTask - QueuedTask.fn signature updated to (containerId: string) => Promise<void> - startSchedulerLoop call in main() updated for new interface
- SUMMARY.md documents containerId threading, CONC-02/CONC-03 satisfaction - STATE.md updated: Phase 01 complete (3/3 plans), progress 50%
- writeActiveSessionsFile/removeActiveSession/readActiveSessionsFile exports - Atomic writes via temp file + rename pattern - 11 tests covering write, append, remove, corrupt file, missing file, idempotency
- onContainerStart/onContainerExit callbacks on GroupQueue (optional, null by default) - registerProcess fires onContainerStart when groupFolder is known - Both runForGroup and runTask finally blocks fire onContainerExit - index.ts wires callbacks to writeActiveSessionsFile/removeActiveSession - Existing 371 GroupQueue tests unaffected (callbacks are optional)
- 02-01-SUMMARY.md with self-check passed - STATE.md updated: Phase 2 in progress, 4/6 plans complete, 382 tests
…ntainer - Add readSessionAwareness() to container agent-runner reading /workspace/ipc/active_sessions.json - Format active sessions as <active-sessions> XML block prepended to initial prompt - Filter out own container from the list using containerId - Pass containerId through ContainerInput from host to container - Graceful handling of missing, corrupt, or empty awareness files
… active containers
- Add google-chat.ts channel adapter (service account auth, space resolution, message splitting) - Register google-chat import in channels/index.ts barrel file - Add @googleapis/chat and google-auth-library dependencies - Format session-awareness files (lint fixes only)
IPC tasks (Google Chat messages, GMS triggers, GitHub push notifications) were waiting up to 60s for the scheduler poll loop. Now the IPC watcher calls triggerSchedulerCheck() after createTask(), so tasks are enqueued within 1-2 seconds of arrival.
…sages When two messages arrive within 1-2 seconds, the second task may not be due yet when the first trigger fires. A 2-second follow-up check catches these near-future tasks without waiting for the 60s poll cycle.
Tasks with context_mode 'group' were sharing the group's session ID. When two tasks ran concurrently, the second blocked on the Claude API session lock, causing 60s+ response times. All task containers now get fresh sessions — they still have full group context via mounts.
Keep task containers alive after completing work (TASK_IDLE_TIMEOUT, default 10 min) so subsequent tasks can reuse them via IPC instead of cold-starting a new container (~3-5s vs ~20s). - notifyIdle marks task containers warm with timeout + MAX_WARM_PER_GROUP - enqueueTask checks for idle containers before spawning new ones - onTaskReuse callback pipes task prompt to warm container via IPC - task-scheduler uses TASK_IDLE_TIMEOUT instead of 10s close delay - Best-effort SQLite bookkeeping for reused tasks - 5 new tests (WARM-01 to WARM-05), 387 total passing
The mount security module blocks paths containing 'credentials' by default. Moving to service-accounts/ allows the key to be mounted into containers so Holly can access Google Calendar.
NanoClaw side of Google Chat threading support. Reads the most recent thread name per JID from google-chat-threads.json (written by trigger-writer on each inbound message) and passes it as requestBody.thread when sending replies. Falls back to a new message if the file is absent or the JID has no entry. Requires companion change to trigger-writer.mjs to write the thread name on each inbound webhook event.
…ix context loss Google Chat messages arrived as standalone IPC tasks, each spawning a fresh container with zero conversation history. Craig would ask Holly a question, get a reply, then follow up — and Holly had no idea what he was referring to. Three-part fix: - trigger-writer: pass senderName/senderEmail/messageText as structured fields in the task JSON (VPS change already deployed) - ipc.ts: store inbound gchat-msg-* messages in the messages DB, fetch the last 20 messages, and prepend as <conversation-history> XML block - task-scheduler.ts: store Holly's outbound responses in the messages DB so subsequent messages include full bidirectional context Also adds getRecentMessages() to db.ts — returns both user and bot messages in chronological order, unlike getMessagesSince() which filters out bot messages for the WhatsApp accumulation path. 12 new tests (416 total): 6 for getRecentMessages, 6 for conversation history injection covering storage, prepending, edge cases.
The storeMessage call was hitting a FOREIGN KEY constraint because gchat:pm-agent didn't exist in the chats table on the production DB. Add storeChatMetadata call before storeMessage to create the chat entry if it doesn't already exist.
…c pollution When multiple Google Chat threads are active simultaneously, Holly's conversation history was mixing all threads into a single context window. This caused her to confuse topics — e.g. responding to a task management question with M365 migration context. Changes: - Add thread_id column to messages table (auto-migrated) - Add thread_id column to scheduled_tasks table (auto-migrated) - Add getRecentMessagesByThread() for thread-filtered history queries - Store thread_id on inbound Google Chat messages (from trigger-writer) - Store thread_id on outbound bot messages (task-scheduler) - Use thread-scoped history in gchat-msg prompt enrichment - Falls back to all-messages when threadId is null (backward compat) - 11 new tests covering thread isolation, fallback, limits, edge cases - All 428 tests pass
feat(google-chat): thread-scoped conversation history
When a warm container is reused for a new task, the streaming callback from the original runTask still runs with the old task in its closure. This caused outbound messages to be tagged with the wrong thread_id. Fix: track current task per container in a Map. The warm-reuse callback (setContainerCurrentTask) updates it, and the streaming callback reads from it instead of the closed-over task variable.
fix(google-chat): propagate thread_id through warm container reuse
…sk re-execution Bug 1: sendMessage only accepted (jid, text) — Google Chat channel resolved the thread from a file-based map that only stored the LAST thread per JID. All replies went to whichever thread received the most recent message. Fix: thread threadId through Channel.sendMessage → GoogleChatChannel → SchedulerDependencies → task-scheduler streaming callback, using the task's stored thread_id for reply routing. Bug 2: one-shot tasks (gchat-msg-*) could be re-dispatched by a subsequent scheduler check while the first container was still running. After the container finished and went warm, the dedup in enqueueTask no longer saw the task as 'running', so it was dispatched again — producing duplicate responses. Fix: null out next_run immediately when a once task is picked up by checkDueTasks, before enqueuing.
fix(google-chat): route replies to correct thread and prevent duplicate responses
Multiple warm containers for the same group share a single IPC input directory. When two messages arrive close together, both warm containers race to read the files — the wrong container can pick up the wrong question, causing replies to appear in the wrong thread with wrong content. Capping to 1 warm container per group eliminates the race. Messages process sequentially (still fast via warm reuse, no cold start penalty). Parallel warm containers can be restored once per-container IPC input directories are implemented.
fix(google-chat): cap warm containers to 1 to prevent IPC race
…containers When multiple tasks are piped to a warm container in quick succession, setContainerCurrentTask overwrote the map entry immediately — before the previous task's output was fully consumed. The streaming callback then read the WRONG task's thread_id, sending the reply to the wrong Google Chat thread. Fix: replace the single-entry containerCurrentTask Map with a FIFO queue (containerTaskQueue). Tasks are pushed on warm-reuse and shifted off on completion (status: 'success'). The streaming callback always reads the HEAD of the queue — the task currently producing output — regardless of how many new tasks have been queued behind it.
fix(google-chat): task queue for warm container reuse prevents thread_id swap
…prompts Warm containers retain Claude's session memory from previous tasks. When a message from thread B is piped into a container that just answered thread A, Claude sees both conversations and may merge them into a single response — answering both questions together. Wraps gchat task prompts with a <new-thread-message> directive telling Claude to treat the piped message as independent and ignore prior session context from other threads.
…lation fix(google-chat): thread isolation directive for warm-reused containers
Messages prefixed with [SILENT] are used by agents for internal actions (e.g. logging to Airtable) that should not be visible to users. - Add [SILENT] detection to formatOutbound() in router.ts - Unify user-message streaming callback to use formatOutbound() instead of inline <internal> stripping (index.ts:329) - Add formatOutbound() filtering to IPC send_message path which previously had no outbound filtering at all (index.ts:675) - Add 4 test cases for [SILENT] suppression behaviour
This was referenced Mar 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
[SILENT]) were appearing in user-facing threads because no suppression logic existed anywhere in NanoClaw[SILENT]detection toformatOutbound()inrouter.ts— the single outbound formatting functionformatOutbound():index.ts:329) — was using inline regex, now usesformatOutbound()index.ts:670) — already usedformatOutbound(), now gets[SILENT]handling for freeindex.ts:675) — had no filtering at all, now filters throughformatOutbound()[SILENT]suppression (432 total tests passing)Behaviour
[SILENT]are suppressed (not sent to any channel)[SILENT]in the middle of text is NOT suppressed (only prefix-match)<internal>tag stripping happens first, then[SILENT]check — so<internal>reason</internal>[SILENT] actionis also suppressedTesting