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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ new version heading in the same commit.

## [Unreleased]

## [0.457.0] - 2026-10-09
### Added
- **A proposed task names who should work it, and assigning it on the board accepts it.** Agents filed proposals with no owner a quarter of the time, and to propose work for ANOTHER agent they had to fight `task_create`'s agent→agent default (poke-on-done implies auto-dispatch, so naming `agent:qa` handed it off instead of proposing it). `task_create` gains `suggest` (`agent:<id>` / `me` / a member id), required whenever the task lands `proposed` (refused with the roster otherwise). The pick is stored as `tasks.suggested_assignee`, not the assignee — advice the reviewer weighs; the agent re-pointing it with `task_update` only revises it. Accepting a proposal with no assignee adopts the suggestion (Inbox Accept, Accept & run, and moving it to `todo` on the board all agree); dismissing doesn't. On the board, assigning someone to a proposal moves it straight to `todo`, gated like the accept button and audited `task.proposal.accepted` (`via: 'assign'`); a board accept/dismiss is now audited at all. The Inbox card and the board's proposals strip show the pick marked "suggested", and the task page offers **Assign & accept**. Pinned by `scripts/task-proposals-test.cjs` §9.
**For users:** Agents now suggest who should take each task they propose. Accept it as-is, or just assign someone on the task page — that moves it straight to To do. [Open Tasks](#/tasks)

## [0.456.2] - 2026-10-08
### Fixed
- **A resumed session was cut again within minutes, mid-work.** The interactive LIFETIME ceiling (`interactiveMaxHours`, default 168 h) measured age from `created_at`, and nothing a human did reset it — so any session older than a week was "over lifetime" forever, and every resume was reaped on the next sweep the moment no browser terminal happened to be attached. Live instawp: a `qa` session created 13 days earlier was resumed by its owner five times in two days and reaped 1–30 minutes after each one, once with its agent mid-turn. A deliberate re-open (attach-resume `markResumed`, unpause, a crashed pane restored under an attached human) now stamps a new `term_sessions.opened_at`, and the ceiling measures from `COALESCE(opened_at, created_at)`. Sessions that were never re-opened — the 42-day zombies the ceiling exists for — age out exactly as before; restore-on-agent-activity deliberately does not restart the clock. The `session.reaped` audit on a lifetime reap also named the wrong knob (`idleHours: 72`, the idle setting); it now records `lifetimeHours` + `openedAt`. Pinned by `scripts/idle-reaper-test.cjs`.
Expand Down
2 changes: 1 addition & 1 deletion docs/agent-mcp-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ scope/parity properties, but not the cost.
| `policy_check` | `POST /api/agent/policy/check` | policy preview | R | dry-run, no side effects |
| `directory_lookup` | `GET /api/agent/directory` | `TeamStore` | R | people + their chat identities |
| `list_agents` | `GET /api/agent/roster` | `os.agents` (peer claude-code agents) | R | the fleet roster — every OTHER agent you can hand work to (id + description + category), so you pick the right specialist instead of guessing an id (a task/ask to a non-existent agent never runs). The agent-side companion to `directory_lookup` (people); pair it with `ask_agent` (sync Q&A) or `task_create({ assignee:"agent:<id>" })` (durable hand-off) |
| `task_create` | `POST /api/tasks/create` | `TaskStore.create` | W | files a unit of work; author `agent:<id>`; owner = run-as (delegation passthrough); `mode` headless/interactive for the dispatched run; optional `model` + `effort` (low…max) that PIN the dispatched session's runtime tuning — highest-priority override over the assignee agent's manifest + the workspace default (`resolveRuntimeTuning(agent, defaults, taskOverride)`), validated at the edge with `sanitizeRuntimeTuning`; useful to delegate background work at a cheaper/stronger tier than your own; optional `due` (ISO date) soft deadline; optional `goalId` (link to a strategic goal; a sub-task inherits its parent's `goalId` when omitted) + single-line `goal`/`criteria` (synonyms — the objective that drives a headless dispatch under a `/goal` convergence condition — Slice 2) + `dependsOn` (task ids this is blocked by — won't dispatch until they're done; cycle/self/missing rejected). **Async poke-back**: `poke_on_done:true` (agent→agent hand-off only) stamps the caller's agent id + pinned claude transcript on the task (`caller_agent`/`caller_claude_id`); on the delegate closing the loop (done/blocked) the task notifier hands the outcome to the **wake queue** (`Automations.pokeCaller` → `edge/wakeups.ts`), which delivers it into the caller's own live pane, else another live pane of that agent, else a `--resume` run (provenance `poke:<task>`; audited `agent.poked` with the lane in `via`) and retries anything undeliverable — the fire-and-forget counterpart to `wait` (which blocks). **The resume lane is priced by what the wake is FOR** (`kind`, docs/tasks-plan.md §3.8): a `done` completion injects into a live caller but is DROPPED at a cold one (audited `agent.poke.skipped`, `reason: 'done-cold-caller'`) — nobody is stuck, the result is on the task and the owner already has the card; a `blocked` hand-back or a delegate whose run died still resumes, since only the caller can move those. Implies `autoDispatch`. **Self-dispatch is refused**: assigning a task to YOURSELF with `autoDispatch` ends your turn and respawns you with an empty context to do work the session you are in already has loaded — the server rejects it (audited `task.self_dispatch.refused`) and tells you to do it in this turn, `schedule` it for genuinely later, or file it `autoDispatch:false` for the board. Measured on the live fleet before the guard: 104 such tasks in 7 days, 70 dispatched within 2 minutes, $1,330 of pure context reload. A goal-plan run is exempt. **A task filed WITHOUT dispatching it is a PROPOSAL** (`status: proposed`, response `proposed:true`): it sits on one Inbox card per run until the run-as human or an admin accepts it onto the board — the agent isn't blocked, and can't accept its own (docs/tasks-plan.md §Proposed tasks; off via the `task_proposals` setting) |
| `task_create` | `POST /api/tasks/create` | `TaskStore.create` | W | files a unit of work; author `agent:<id>`; owner = run-as (delegation passthrough); `mode` headless/interactive for the dispatched run; optional `model` + `effort` (low…max) that PIN the dispatched session's runtime tuning — highest-priority override over the assignee agent's manifest + the workspace default (`resolveRuntimeTuning(agent, defaults, taskOverride)`), validated at the edge with `sanitizeRuntimeTuning`; useful to delegate background work at a cheaper/stronger tier than your own; optional `due` (ISO date) soft deadline; optional `goalId` (link to a strategic goal; a sub-task inherits its parent's `goalId` when omitted) + single-line `goal`/`criteria` (synonyms — the objective that drives a headless dispatch under a `/goal` convergence condition — Slice 2) + `dependsOn` (task ids this is blocked by — won't dispatch until they're done; cycle/self/missing rejected). **Async poke-back**: `poke_on_done:true` (agent→agent hand-off only) stamps the caller's agent id + pinned claude transcript on the task (`caller_agent`/`caller_claude_id`); on the delegate closing the loop (done/blocked) the task notifier hands the outcome to the **wake queue** (`Automations.pokeCaller` → `edge/wakeups.ts`), which delivers it into the caller's own live pane, else another live pane of that agent, else a `--resume` run (provenance `poke:<task>`; audited `agent.poked` with the lane in `via`) and retries anything undeliverable — the fire-and-forget counterpart to `wait` (which blocks). **The resume lane is priced by what the wake is FOR** (`kind`, docs/tasks-plan.md §3.8): a `done` completion injects into a live caller but is DROPPED at a cold one (audited `agent.poke.skipped`, `reason: 'done-cold-caller'`) — nobody is stuck, the result is on the task and the owner already has the card; a `blocked` hand-back or a delegate whose run died still resumes, since only the caller can move those. Implies `autoDispatch`. **Self-dispatch is refused**: assigning a task to YOURSELF with `autoDispatch` ends your turn and respawns you with an empty context to do work the session you are in already has loaded — the server rejects it (audited `task.self_dispatch.refused`) and tells you to do it in this turn, `schedule` it for genuinely later, or file it `autoDispatch:false` for the board. Measured on the live fleet before the guard: 104 such tasks in 7 days, 70 dispatched within 2 minutes, $1,330 of pure context reload. A goal-plan run is exempt. **A task filed WITHOUT dispatching it is a PROPOSAL** (`status: proposed`, response `proposed:true`): it sits on one Inbox card per run until the run-as human or an admin accepts it onto the board — the agent isn't blocked, and can't accept its own (docs/tasks-plan.md §Proposed tasks; off via the `task_proposals` setting). A proposal must name who should work it via `suggest` (`agent:<id>` / `me` / a member id; an `assignee` on a proposal is read the same way) — stored as `suggested_assignee`, not an assignment, refused when missing; accepting adopts it, and the agent's later `task_update({ assignee })` on a proposal only revises it |
| `task_list` | `GET /api/tasks/list` | `TaskStore.list` | R | `assignee:"me"` → self; board query/FTS |
| `task_get` | `GET /api/tasks/get` | `TaskStore.withEvents` | R | task + full activity timeline |
| `task_claim` | `POST /api/tasks/claim` | `TaskStore.claim` | W | atomic take (→ doing); loses if already claimed |
Expand Down
10 changes: 10 additions & 0 deletions docs/tasks-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,16 @@ status puts a human's accept between "an agent thought this is work" and "it is
owner/admin; the board PATCH out of `proposed` is gated identically, and nothing can move INTO it. An
agent may withdraw its own (→ `cancelled`) or refine the text, never accept it. Audited
`task.proposed` / `task.proposal.accepted` / `task.proposal.dismissed`.
- **Suggested assignee.** Every proposal names who the agent thinks should work it — `task_create({ suggest })`
(an `assignee` on a proposal is read the same way), refused without one so the reviewer never has to
go find an owner after accepting. Stored in `tasks.suggested_assignee`, deliberately NOT `assignee`:
nobody is told "assigned to you" for work no human agreed to, and the agent's `task_update({ assignee })`
on its proposal only revises the pick. Accepting with no assignee set adopts it (`TaskStore.update`, so
the Inbox Accept, Accept & run, and the board status picker all agree); dismissing doesn't.
- **Assigning IS accepting on the board.** A board `PATCH` that sets an assignee on a proposal (no status
in the same edit) moves it to `todo` — picking who works it is the reviewer saying it is work. Same
gate as the accept button, audited `task.proposal.accepted` with `via: 'assign'`. The Inbox card's
re-point (`action: 'assign'`) still leaves it proposed: there Accept / Accept & run sit right beside it.
- **Not held:** an auto-dispatch hand-off (a `task_wait` caller would hang on a click — bound those with
a delegation budget instead), a `goal:`-provenance run (a human asked for that plan), a human's task.
- **Bounds:** 25 open proposals per agent (`MAX_OPEN_TASK_PROPOSALS`), then `task_create` refuses. The
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agent-os",
"version": "0.456.2",
"version": "0.457.0",
"description": "A generic, governed operating system for running autonomous agents safely across brands. Ships with a local web console.",
"license": "MIT",
"type": "commonjs",
Expand Down
Loading
Loading