feat(messaging): read receipts + ack convention

Closes the fire-and-forget gap the user hit: the sender now sees when a message
is read. When markMessageRead fires, the read event carries the reader, and the
watch stream renders it as a receipt:

  AgentHub: ✓ Read         MSG-0002  read by windows-claude

Plus an ack convention in the implementer guide: when you act on a message-
request, send a brief "on it …" + a result message — so the sender sees progress,
not silence. (Board-side activity feed with messages is folded into TSK-0032.)

Bump 0.8.0 -> 0.8.1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-06-29 04:04:04 +02:00
parent 07afa717ac
commit ed6c5b01d0
5 changed files with 16 additions and 3 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "agenthub", "name": "agenthub",
"version": "0.8.0", "version": "0.8.1",
"description": "Local coordination layer for AI coding agents", "description": "Local coordination layer for AI coding agents",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",

View File

@ -80,6 +80,10 @@ function describeEvent(event: AgentHubEvent): { label: string; detail: string }
case 'memory': case 'memory':
return { label: 'Memory', detail: event.title ?? '' }; return { label: 'Memory', detail: event.title ?? '' };
case 'message': case 'message':
// A read message surfaces to the SENDER as a delivery/read receipt.
if (event.action === 'updated' && event.status === 'read') {
return { label: '✓ Read', detail: event.assignedTo ? `read by ${event.assignedTo}` : 'read' };
}
return { label: 'Message', detail: event.title ?? '' }; return { label: 'Message', detail: event.title ?? '' };
default: default:
// 'agent' presence events are formatted in formatEvent() before reaching // 'agent' presence events are formatted in formatEvent() before reaching

View File

@ -117,7 +117,7 @@ async function runRemote(serverUrl: string, fn: () => Promise<void>): Promise<vo
export function createProgram(cwd: string): Command { export function createProgram(cwd: string): Command {
const program = new Command('agenthub') const program = new Command('agenthub')
.description('Local coordination layer for AI coding agents') .description('Local coordination layer for AI coding agents')
.version('0.8.0') .version('0.8.1')
.option('--server <url>', 'AgentHub server URL (env: AGENTHUB_SERVER)'); .option('--server <url>', 'AgentHub server URL (env: AGENTHUB_SERVER)');
program program

View File

@ -59,6 +59,10 @@ claims it, and returns the task + its handoff. Then:
then \`agenthub task review <id>\` + \`agenthub memory add …\`, then \`agenthub work\` again then \`agenthub task review <id>\` + \`agenthub memory add …\`, then \`agenthub work\` again
(run it in the background so the wait doesn't tie up your turn). (run it in the background so the wait doesn't tie up your turn).
**Messages:** \`agenthub_work\` surfaces messages addressed to you (or check \`agenthub_inbox\`).
When you act on a message-request, send a short ack with \`agenthub_message\` ("on it ...") and
a result message when done so the sender sees progress + outcome, not silence.
NEVER close a task yourself (no \`agenthub_task_done\` / \`agenthub task done\`) — only the NEVER close a task yourself (no \`agenthub_task_done\` / \`agenthub task done\`) — only the
architect approves and closes. You drive the tools yourself; the human doesn't type them for you. architect approves and closes. You drive the tools yourself; the human doesn't type them for you.
`; `;

View File

@ -300,7 +300,12 @@ export async function registerRoutes(app: FastifyInstance, cwd: string): Promise
const { id } = request.params as { id: string }; const { id } = request.params as { id: string };
try { try {
const message = markMessageRead(cwd, id); const message = markMessageRead(cwd, id);
emitChange({ type: 'message', action: 'updated', id: message.id, status: 'read' }, message.updatedAt); // Read receipt: carry the reader (to) so the sender's stream shows
// "✓ MSG-xxxx read by <agent>".
emitChange(
{ type: 'message', action: 'updated', id: message.id, status: 'read', title: `${message.from}${message.to}`, assignedTo: message.to },
message.updatedAt,
);
return message; return message;
} catch (err) { } catch (err) {
return badRequest(reply, err instanceof Error ? err.message : 'Message not found'); return badRequest(reply, err instanceof Error ? err.message : 'Message not found');