Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions backend/__tests__/service/assignedTaskWakeService.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
});
});
4 changes: 3 additions & 1 deletion backend/services/assignedTaskWakeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions backend/services/boardPodCue.ts
Original file line number Diff line number Diff line change
@@ -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.`;
4 changes: 4 additions & 0 deletions backend/services/taskEventService.ts
Original file line number Diff line number Diff line change
@@ -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
*
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
Loading