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
4 changes: 2 additions & 2 deletions agent-skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Use `Content-Type: application/json` on requests with JSON bodies.
Tasks can include coordination fields in addition to the work instructions:

- `note`: optional string or `null`. Use this as the agent result note: concise findings, implementation note, completion summary, QA handoff, or status update. When marking a task `done`, include what changed, changed files/branch/commit/PR when relevant, and check results. Dashboard users can review non-empty notes from the Notes page as well as the source task card.
- `summaryUpdatedAt`: nullable timestamp showing when the `note`/summary was last set or changed. Treat it as freshness metadata for review/read-marker decisions.
- `summaryUpdatedAt`: nullable stored timestamp showing when the `note`/summary content was last set or changed. It is `null` when `note` is `null`, remains unchanged when a note is omitted or submitted unchanged, updates when note content changes to a non-empty value, and clears when a note is blanked/cleared. Treat it as freshness metadata for review/read-marker decisions; use `taskUpdated*` fields and AuditLog entries for actor auditability.
- `readBy`: array of agent `AgentId` strings that have read the task in its **current status**. The underlying read tracking is per task, per agent, and per status. If `main` has read a task in `todo`, that does not mean `main` has read it in `done`; each status is independent.
- `blockingReason`: optional string or `null`, used when `status` is `blocked`.
- `dependencyIds`: array of dependency task database UUIDs returned on task responses.
Expand Down Expand Up @@ -603,7 +603,7 @@ Body fields:
- `name`: optional non-empty string; trimmed.
- `job`: optional non-empty string; trimmed.
- `status`: optional; one of `todo`, `inprogress`, `done`, `blocked`.
- `note`: optional string or `null`; trimmed; blank or `null` is stored as `null`. Changed notes update `summaryUpdatedAt`; unchanged or omitted notes preserve it.
- `note`: optional string or `null`; trimmed; blank or `null` is stored as `null` and clears `summaryUpdatedAt`. Changed non-empty notes update `summaryUpdatedAt`; unchanged or omitted notes preserve it.
- `readBy`: optional array of agent `AgentId` strings to replace readers for the resulting status. Omit when changing `status` or `note` unless intentionally marking the result already reviewed.
- `blockingReason`: optional string or `null`; blank or whitespace-only values are stored as `null`.
- At least one field is required.
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/api/agent/tasks/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export async function GET(request: NextRequest) {
* note:
* type: string
* nullable: true
* description: Result note or done summary. Trimmed; blank values are stored as null and leave summaryUpdatedAt null.
* description: Result note or done summary. Trimmed; non-empty values set summaryUpdatedAt and blank values are stored as null with summaryUpdatedAt null.
* readBy:
* type: array
* items:
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/api/openapi/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ const openApiDocument = swaggerJsdoc({
format: "date-time",
nullable: true,
description:
"Timestamp for the latest stored note/summary change. Null when note is null; unchanged when note is omitted or submitted unchanged; updated when note content changes; cleared when note is blanked.",
"Stored timestamp for the latest note/summary content change. Null when note is null or when no summary timestamp has been stored; unchanged when note is omitted or submitted unchanged; updated when note content changes; cleared when note is blanked. Actor auditability comes from taskUpdated* fields and AuditLog entries.",
},
readBy: {
type: "array",
Expand Down
44 changes: 44 additions & 0 deletions apps/web/lib/api/task-read-markers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import assert from "node:assert/strict"
import { describe, it } from "node:test"

import { Status } from "@/generated/prisma/enums"
import { getStoredSummaryUpdatedAt, serializeTaskReadMarkers } from "./task-read-markers"

const markerAgent = { AgentId: "main" }

describe("Agent API task serialization", () => {
it("returns stored summaryUpdatedAt for noted tasks without taskUpdatedAt fallback", () => {
const taskUpdatedAt = new Date("2026-05-15T10:00:00.000Z")

assert.equal(
getStoredSummaryUpdatedAt({ note: "summary", summaryUpdatedAt: null }),
null
)

const serialized = serializeTaskReadMarkers({
id: "task-1",
name: "Task",
job: "Do work",
status: Status.done,
note: "summary",
summaryUpdatedAt: null,
taskUpdatedAt,
readMarkers: [
{ status: Status.done, readAt: taskUpdatedAt, agent: markerAgent },
{ status: Status.todo, readAt: taskUpdatedAt, agent: { AgentId: "kaito" } },
],
})

assert.equal(serialized.summaryUpdatedAt, null)
assert.deepEqual(serialized.readBy, ["main"])
})

it("clears summaryUpdatedAt when the note is absent", () => {
const staleSummaryTimestamp = new Date("2026-05-15T10:00:00.000Z")

assert.equal(
getStoredSummaryUpdatedAt({ note: null, summaryUpdatedAt: staleSummaryTimestamp }),
null
)
})
})
9 changes: 3 additions & 6 deletions apps/web/lib/api/task-read-markers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { Status } from "@/generated/prisma/enums"
type TaskSummaryPayload = {
note?: string | null
summaryUpdatedAt?: Date | null
taskUpdatedAt?: Date
}

type ReadMarker = {
Expand All @@ -23,15 +22,13 @@ export function serializeTaskReadMarkers<

return {
...serializedTask,
summaryUpdatedAt: getSummaryUpdatedAt(serializedTask),
summaryUpdatedAt: getStoredSummaryUpdatedAt(serializedTask),
readBy: readMarkers
.filter((marker) => marker.status === task.status)
.map((marker) => marker.agent.AgentId),
}
}

function getSummaryUpdatedAt(task: TaskSummaryPayload) {
if (!task.note) return task.summaryUpdatedAt ?? null

return task.summaryUpdatedAt ?? task.taskUpdatedAt ?? null
export function getStoredSummaryUpdatedAt(task: TaskSummaryPayload) {
return task.note ? (task.summaryUpdatedAt ?? null) : null
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"prisma:studio": "prisma studio",
"typecheck": "corepack pnpm prisma:generate && corepack pnpm --filter @agentbridge/web typecheck && corepack pnpm --filter agentbridge-ai typecheck",
"typecheck:web": "corepack pnpm prisma:generate && corepack pnpm --filter @agentbridge/web typecheck",
"test:api": "tsx --tsconfig apps/web/tsconfig.json --test apps/web/lib/api/*.test.ts",
"test:task-freshness": "tsx --tsconfig apps/web/tsconfig.json --test apps/web/lib/api/task-freshness.test.ts",
"cli:dev": "corepack pnpm --filter agentbridge-ai dev",
"cli:build": "corepack pnpm --filter agentbridge-ai build",
Expand Down
Loading