From f3c2c247f42d08f848a20a390794871b60f7a33d Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 27 Oct 2026 00:33:38 -0700 Subject: [PATCH] fix(board): board wakes name their pod on a line of their own (TASK-581) A wake reaches a seat beside other 24-hex ids and never said which one is the board's pod. Fresh Haiku sessions then called commonly_*_task without podId: 12 of 20 calls made before the session had used any podId omitted it, against 10 of 344 afterwards. The board-moved, found-work, lapsed-lease and assigned-row wakes now end with 'Board pod: ; pass it as podId to every commonly_*_task call.', from one helper, in the pod-context frame's inline pattern. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MDs9DRK3nqxjeAV7xL8uhg --- .../service/assignedTaskWakeService.test.js | 8 ++++ .../taskEventService.notifyPodAgents.test.js | 38 ++++++++++++++++++- backend/services/assignedTaskWakeService.ts | 4 +- backend/services/boardPodCue.ts | 11 ++++++ backend/services/taskEventService.ts | 4 ++ 5 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 backend/services/boardPodCue.ts diff --git a/backend/__tests__/service/assignedTaskWakeService.test.js b/backend/__tests__/service/assignedTaskWakeService.test.js index e3f5b2e4df..7d10e3d359 100644 --- a/backend/__tests__/service/assignedTaskWakeService.test.js +++ b/backend/__tests__/service/assignedTaskWakeService.test.js @@ -59,6 +59,14 @@ it('wakes an assigned seat after five minutes, even with ambient board wake disa expect(await AgentTaskWakeState.countDocuments()).toBe(1); }); +it('names the board pod on a line of its own, so a fresh seat passes podId (TASK-581)', async () => { + await completed({}, 5); + await dispatchAssignedTaskWakes(NOW); + const lines = AgentEventService.enqueue.mock.calls[0][0].payload.content.split('\n'); + expect(lines.filter((line) => line.startsWith('Board pod: '))) + .toEqual([`Board pod: ${String(podId)}; pass it as podId to every commonly_*_task call.`]); +}); + it('waits five minutes after the latest completed turn', async () => { await completed({}, 15); await completed({}, 4); diff --git a/backend/__tests__/unit/services/taskEventService.notifyPodAgents.test.js b/backend/__tests__/unit/services/taskEventService.notifyPodAgents.test.js index 598b34d9ea..850afff5b0 100644 --- a/backend/__tests__/unit/services/taskEventService.notifyPodAgents.test.js +++ b/backend/__tests__/unit/services/taskEventService.notifyPodAgents.test.js @@ -41,7 +41,7 @@ const AgentEventService = require('../../../services/agentEventService'); const AgentEvent = require('../../../models/AgentEvent'); const Task = require('../../../models/Task'); const User = require('../../../models/User'); -const { notifyPodAgents, notifyFoundWork } = require('../../../services/taskEventService'); +const { notifyPodAgents, notifyFoundWork, notifyLeaseWarning } = require('../../../services/taskEventService'); const POD_ID = new mongoose.Types.ObjectId().toString(); const HUMAN_ID = new mongoose.Types.ObjectId().toString(); @@ -635,3 +635,39 @@ describe('notifyFoundWork', () => { expect(mockTaskUpdateOne).not.toHaveBeenCalled(); }); }); + +// TASK-581: a wake carries other 24-hex ids (owner, event, delivery). Fresh +// seat sessions called commonly_*_task without podId until the board's pod was +// stated on a line of its own, naming that id and no other. +describe('board wakes name their pod', () => { + const offerRows = (count) => Array.from({ length: count }, (_, index) => ({ + _id: `row-${index + 1}`, + taskId: `TASK-${index + 1}`, + title: `Task ${index + 1}`, + updatedAt: new Date(`2026-08-20T20:00:${String(index).padStart(2, '0')}.000Z`), + })); + const boardLine = (content) => content.split('\n').filter((line) => line.startsWith('Board pod: ')); + const expectBoardLine = (content) => { + expect(boardLine(content)).toEqual([`Board pod: ${POD_ID}; pass it as podId to every commonly_*_task call.`]); + for (const other of [HUMAN_ID, AGENT_ID]) expect(boardLine(content)[0]).not.toContain(other); + }; + + it('in the board-moved wake', async () => { + mockInstalls([install('scout', HUMAN_ID)]); + await notifyPodAgents(POD_ID, task(), 'created', { userId: HUMAN_ID, isAgent: false }); + expectBoardLine(AgentEventService.enqueue.mock.calls[0][0].payload.content); + }); + + it('in the found-work wake', async () => { + mockInstalls([install('scout', HUMAN_ID)]); + const rows = offerRows(2); + await notifyFoundWork(POD_ID, rows, rows.length, new Date('2026-08-20T20:00:00.000Z')); + expectBoardLine(AgentEventService.enqueue.mock.calls[0][0].payload.content); + }); + + it('in the lapsed-lease warning', async () => { + AgentEvent.findOneAndUpdate.mockResolvedValueOnce(null); + await notifyLeaseWarning(POD_ID, { agentName: 'scout', instanceId: 'default', label: 'scout' }, { taskId: 'TASK-042', title: 't' }, 0, 2); + expectBoardLine(AgentEventService.enqueue.mock.calls[0][0].payload.content); + }); +}); diff --git a/backend/services/assignedTaskWakeService.ts b/backend/services/assignedTaskWakeService.ts index 85d6bab74e..11b81963e7 100644 --- a/backend/services/assignedTaskWakeService.ts +++ b/backend/services/assignedTaskWakeService.ts @@ -8,6 +8,7 @@ import User from '../models/User'; import Pod from '../models/Pod'; import { AgentInstallation } from '../models/AgentRegistry'; import { carriesAgentMarkers, isReservedAgentEmail } from './agentIdentityService'; +import { formatBoardPodLine } from './boardPodCue'; const DELAY_MS = 5 * 60 * 1000; const INTERVAL_MS = 30 * 60 * 1000; @@ -396,7 +397,8 @@ export async function dispatchAssignedTaskWakes(now = new Date()): Promise<{ eli + shown.map((row) => `- ${row.taskId}: ${row.title.slice(0, 220)}`).join('\n') + '\n\nContinue the next actionable row. Recheck its current state and claim it before starting. ' + 'If you are waiting on a dependency or operator gate, mark the row blocked with the reason. ' - + 'Post only an action or a materially useful result.', + + 'Post only an action or a materially useful result.\n' + + formatBoardPodLine(String(install.podId)), }, }); woken += 1; diff --git a/backend/services/boardPodCue.ts b/backend/services/boardPodCue.ts new file mode 100644 index 0000000000..0ec300417b --- /dev/null +++ b/backend/services/boardPodCue.ts @@ -0,0 +1,11 @@ +// Board wakes name the pod their tasks live in, on a line of its own. +// +// A wake reaches the seat beside other 24-hex ids (the pod owner, the event, +// the delivery), and nothing said which one the board's podId is. Fresh seat +// sessions then called commonly_*_task without podId: measured 2026-10-27 across +// five Haiku seats, 12 of 20 task calls made before the session had used any +// podId omitted it, against 10 of 344 afterwards (TASK-581). Same pattern as +// agentMentionService's pod-context frame: the model reads an inline literal +// it is told to pass, and skips a structured payload field. +export const formatBoardPodLine = (podId: string): string => + `Board pod: ${podId}; pass it as podId to every commonly_*_task call.`; diff --git a/backend/services/taskEventService.ts b/backend/services/taskEventService.ts index 1303c95494..4b4baed7a1 100644 --- a/backend/services/taskEventService.ts +++ b/backend/services/taskEventService.ts @@ -1,6 +1,7 @@ import { resolveTokenAgentIdentity } from '../utils/agentTokenIdentity'; import { Types } from 'mongoose'; import { compareTaskPriority, TASK_PRIORITY_ORDER } from '../utils/taskPriority'; +import { formatBoardPodLine } from './boardPodCue'; /** * taskEventService * @@ -328,6 +329,7 @@ export async function notifyPodAgents( '', 'Do not narrate the board back to the pod. Post only if you are taking', 'work, or a human needs a decision from you.', + formatBoardPodLine(normalizedPodId), ].join('\n'); // Self-skip keyed on IDENTITY, never on `installedBy`. @@ -608,6 +610,7 @@ export async function notifyFoundWork( 'do: claim it first (a 409 means a peer got there — move on), then start.', 'If none fit your role, return NO_REPLY as your ENTIRE reply — anything', 'written after the token is posted publicly. Do not narrate the list back.', + formatBoardPodLine(normalizedPodId), ].join('\n'); let woken = 0; @@ -756,6 +759,7 @@ export async function notifyLeaseWarning( 'rescue, not the board. To keep it, post a task update or re-claim — either', 'renews the lease. If you have finished, complete it with the PR link. If it', 'is no longer yours, do nothing and it returns to the board named as yours.', + formatBoardPodLine(normalizedPodId), ].join('\n'); try {