Agent: Change tokens
Section titled “Agent: Change tokens”Change tokens are opaque per-topic hints: a short, opaque string per work list that moves whenever that list could have changed. Use them to avoid polling. The recommended client loop reads the tokens, lists the work lists, then later re-reads the tokens and re-lists only the topics whose token moved, and every topic when scope changes. The lists are authoritative; tokens are just hints.
The first endpoint below returns one snapshot as JSON. The second opens a Server-Sent Events stream that emits a snapshot frame at start, changed frames for moved topics, and a fresh snapshot when scope changes. The SSE stream sends no id: lines, so after a reconnect, start again from the new snapshot and re-list the topics that differ from what you hold.
Read change tokens
Section titled “Read change tokens”GET /api/v1/agent/changes
Section titled “GET /api/v1/agent/changes”Returns one change token per work list topic: sessions, todos, workflow_runs, loops, tasks, gates, agents, and rules.
The X-Hoody-Realm header (or ?realm= query alias) selects the realm: "global" (not tied to a realm) or a 24-hex realm id; omitted, the agent’s current realm. A realm this login does not serve is 404 not_found. An agent pinned to one realm acts only in that realm: any other value is 400 realm_scope_unsupported. A container header is unconditionally 400 realm_scope_unsupported. The X-Hoody-Cwd and X-Hoody-Config-Dir headers scope sessions and todos as they scope sessions.list and todos.list, and with them tasks, gates, and rules, which are read over those visible sessions. rules also covers the files hooks.getRules reads for the same headers.
One read is shared for 1 s by the requests with the same realm and scope headers, so an answer can be up to 1 s old. After the active realm or account switches, the new scope arrives within that second.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Hoody-Cwd | header | string | No | Per-request working-directory scope: the .hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; todos.create also accepts a body cwd). |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves against. |
X-Hoody-Realm | header | string | No | Per-request realm selector: "global" (not tied to a realm) or a 24-hex realm id (also accepted as ?realm=). On a session route it names the realm the session is looked up in: a session in another realm, or in a realm this login does not serve, is 404 not_found. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes, and for any other realm on an agent pinned to one realm. |
realm | query | string | No | Per-request realm selector, the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" (not tied to a realm) or a 24-hex realm id. On a session route it names the realm the session is looked up in: a session in another realm, or in a realm this login does not serve, is 404 not_found. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes, and for any other realm on an agent pinned to one realm. |
Response
Section titled “Response”{ "scope": "active", "tokens": { "sessions": "01HQZ3K4N5P6R7S8T9V0W1X2Y3", "todos": "01HQZ3K4N5P6R7S8T9V0W1X2Y4", "workflow_runs": "01HQZ3K4N5P6R7S8T9V0W1X2Y5", "loops": "01HQZ3K4N5P6R7S8T9V0W1X2Y6", "tasks": "01HQZ3K4N5P6R7S8T9V0W1X2Y7", "gates": "01HQZ3K4N5P6R7S8T9V0W1X2Y8", "agents": "01HQZ3K4N5P6R7S8T9V0W1X2Y9", "rules": "01HQZ3K4N5P6R7S8T9V0W1X2ZA" }}Fields:
scope(string, required): names the realm and account the tokens belong to (it never carries the account). When it changes, re-list every topic.tokens(object, required): one opaque token per topic. Compare a token with the one you last saw; when it differs, re-list that topic. Never parse a token.tokens.sessions(string): moves when a visible session is created, closed, renamed or re-persisted (model, agent, cost), when it attaches or detaches, and when a turn starts, parks on a gate or ends. Re-list:sessions.list.tokens.todos(string): moves on any change to a visible todo. Re-list:todos.list(andgetTodofor an open one).tokens.workflow_runs(string): moves when a visible run starts, steps, finishes, is cancelled or is evicted. Re-list:workflows.listRuns(workflows.getRunfor one).tokens.loops(string): moves when a visible loop is created, deleted, paused, resumed, fires, finishes or spends. Countdowns (next_in_ms,expires_in_ms) do not move it. Re-list:loops.list.tokens.tasks(string): moves when a helper task of a visible live session starts, makes progress or ends. Re-list:tasks.listfor the sessions you show.tokens.gates(string): moves when a gate of a visible live session parks or resolves: the session’s own confirm or question, and on ahelper_gatessession each helper’s. Re-list:gates.list(orsessions.getSnapshotfor one session:pending_gate,pending_helper_gates).tokens.agents(string): moves when the realm’s agent list changes: a create, delete, rename, model, tools or turns edit, or a source edit. Re-list:definitions.list.tokens.rules(string): moves when a rules layer of a visible live session changes (hooks.setRules), or a settings filehooks.getRulesreads for the same scope headers, with or without a live session. Re-list:hooks.getRules/hooks.list/sessions.listApplicableRules.
unavailable(object): present only when a topic could not be read; maps topic to reason. That topic has no token in this answer.
{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |
realm_scope_unsupported | Realm scope unsupported | A per-request realm header was supplied to an active-only or global-no-realm RPC, which has no realm dimension to scope. | Omit the realm header on this route, or open a session to scope by realm. |
invalid_realm | Invalid realm selector | The realm selector is malformed (not ""/"global"/a 24-hex id). | Pass a valid realm selector. |
{ "code": "forbidden", "message": "request must arrive through the Hoody proxy"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
forbidden | Forbidden (Source IP Guard) | The request did not come through the program’s URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with hoody agent …. |
{ "code": "not_found", "message": "resource not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | The requested resource does not exist. | Verify the path and identifier. |
{ "code": "rate_limited", "message": "request rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Too many requests | The per-client request rate limit was exceeded; the gateway throttled the request before dispatch. | Honor the Retry-After header and retry; reduce the request rate. |
{ "code": "internal_error", "message": "internal server error"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
internal_error | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. |
{ "code": "restriction_unknown", "message": "the login's realm restriction could not be read \u2014 retry"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction "unknown"), so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
SDK and cURL
Section titled “SDK and cURL”import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const result = await client.agent.changes.get();With the optional X-Hoody-Cwd header:
const result = await client.agent.changes.get({ XHoodyCwd: '/home/user/work'});curl -X GET "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/changes" \ -H "Authorization: Bearer $HOODY_TOKEN"With the optional X-Hoody-Cwd header:
curl -X GET "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/changes" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "X-Hoody-Cwd: /home/user/work"Stream change tokens
Section titled “Stream change tokens”GET /api/v1/agent/changes/stream
Section titled “GET /api/v1/agent/changes/stream”Server-Sent Events stream over changes.get. The first frame is a snapshot carrying {scope, tokens}. After it, a changed frame with {topic, token} for each topic whose token moved, and a new snapshot frame when scope changes. Tokens are re-read every 2 s (one read per scope per second is shared by every stream). There are no id: lines, so after a reconnect, start again from the new snapshot and re-list the topics that differ from what you hold. Heartbeat comments arrive every 25 s. The stream ends with an end frame carrying {reason} when the tokens cannot be read.
The stream counts toward the per-IP stream cap (--http-max-streams-per-ip, default 16): a Frame tab holds this stream, the focused session’s /stream, and a prompt:stream while sending.
The same scoping rules apply as for changes.get: the X-Hoody-Realm header (or ?realm= query alias) selects the realm, with a 400 realm_scope_unsupported only when an agent is pinned to a different realm, and a container header is unconditionally 400 realm_scope_unsupported. X-Hoody-Cwd and X-Hoody-Config-Dir scope sessions and todos, and through them tasks, gates, and rules. rules also covers the files hooks.getRules reads for the same headers.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Hoody-Cwd | header | string | No | Per-request working-directory scope: the .hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; todos.create also accepts a body cwd). |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves against. |
X-Hoody-Realm | header | string | No | Per-request realm selector: "global" (not tied to a realm) or a 24-hex realm id (also accepted as ?realm=). On a session route it names the realm the session is looked up in: a session in another realm, or in a realm this login does not serve, is 404 not_found. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes, and for any other realm on an agent pinned to one realm. |
realm | query | string | No | Per-request realm selector, the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" (not tied to a realm) or a 24-hex realm id. On a session route it names the realm the session is looked up in: a session in another realm, or in a realm this login does not serve, is 404 not_found. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes, and for any other realm on an agent pinned to one realm. |
Response
Section titled “Response”The body is an SSE event stream (text/event-stream). Each frame is one or more event: and data: lines followed by a blank line. Heartbeats are SSE comment lines (: heartbeat) every 25 s.
event: snapshotdata: {"scope":"active","tokens":{"sessions":"01HQZ3K4N5P6R7S8T9V0W1X2Y3","todos":"01HQZ3K4N5P6R7S8T9V0W1X2Y4","workflow_runs":"01HQZ3K4N5P6R7S8T9V0W1X2Y5","loops":"01HQZ3K4N5P6R7S8T9V0W1X2Y6","tasks":"01HQZ3K4N5P6R7S8T9V0W1X2Y7","gates":"01HQZ3K4N5P6R7S8T9V0W1X2Y8","agents":"01HQZ3K4N5P6R7S8T9V0W1X2Y9","rules":"01HQZ3K4N5P6R7S8T9V0W1X2ZA"}}
event: changeddata: {"topic":"sessions","token":"01HQZ3K4N5P6R7S8T9V0W1X2Y4"}
: heartbeat
event: enddata: {"reason":"tokens_unreadable"}Frame shapes:
snapshot: carries{scope, tokens}shaped like thechanges.get200 body. Sent at start and again wheneverscopechanges.changed: carries{topic, token}for one moved topic.end: carries{reason}when the tokens cannot be read.- Heartbeat: an SSE comment line
: heartbeatevery 25 s.
{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |
realm_scope_unsupported | Realm scope unsupported | A per-request realm header was supplied to an active-only or global-no-realm RPC, which has no realm dimension to scope. | Omit the realm header on this route, or open a session to scope by realm. |
invalid_realm | Invalid realm selector | The realm selector is malformed (not ""/"global"/a 24-hex id). | Pass a valid realm selector. |
{ "code": "forbidden", "message": "request must arrive through the Hoody proxy"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
forbidden | Forbidden (Source IP Guard) | The request did not come through the program’s URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with hoody agent …. |
{ "code": "not_found", "message": "resource not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | The requested resource does not exist. | Verify the path and identifier. |
{ "code": "rate_limited", "message": "request rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Too many requests | The per-client request rate limit was exceeded; the gateway throttled the request before dispatch. | Honor the Retry-After header and retry; reduce the request rate. |
{ "code": "internal_error", "message": "internal server error"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
internal_error | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. |
{ "code": "service_unavailable", "message": "service unavailable"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
service_unavailable | Service unavailable | The daemon could not service the request (too busy, or a per-client stream concurrency cap was hit). | Honor Retry-After and retry. |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction "unknown"), so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
SDK and cURL
Section titled “SDK and cURL”import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const stream = await client.agent.changes.stream();With the optional X-Hoody-Cwd header:
const stream = await client.agent.changes.stream({ XHoodyCwd: '/home/user/work'});curl -N -X GET "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/changes/stream" \ -H "Authorization: Bearer $HOODY_TOKEN"With the optional X-Hoody-Cwd header:
curl -N -X GET "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/changes/stream" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "X-Hoody-Cwd: /home/user/work"