message.new
Someone said something, with any attachments as signed URLs the harness can fetch.
Direction: Chips → Harness
Sent by: Chips only; refused from a client
Requires a run: no
Schema: message.new.json
Fields
Section titled “Fields”Every event also carries the envelope.
| Field | Required | Type | Description |
|---|---|---|---|
v |
yes | 1 |
Protocol major version. |
type |
yes | "message.new" |
|
id |
yes | string |
The event’s own id. Stable across a retry of the same event. |
ts |
yes | string |
When it happened, by Chips’ clock. Stamped on the way in; a value the sender writes is replaced. |
workspace_id |
yes | string |
A workspace identifier: ws_ followed by a ULID. |
space_id |
no | string |
The space, when the event belongs to one. |
run_id |
no | string |
The run, when the event belongs to one. |
seq |
no | number |
Assigned by the server on append, monotonic per workspace. Absent on the way in. |
idempotency_key |
no | string |
Required on every client-originated write. Sending the same key twice is a no-op. |
actor |
yes | object | The participant an event is attributed to. |
actor.kind |
yes | "person" | "harness" | "agent" | "system" |
Whether a person, a harness, one of a harness’s agents or Chips itself acted. |
actor.id |
yes | string |
The participant id, or system for Chips. |
actor.harness_id |
no | string |
For an agent: the harness it speaks through, whose budget and runs are the agent’s (ADR 0087). Filled in by Chips. |
message_id |
yes | string |
A message identifier: msg_ followed by a ULID. |
from |
yes | object | Who sent it, and whether they are the owner, a member or a guest. |
from.participant_id |
yes | string |
The stable id of the sender. |
from.kind |
yes | "person" | "harness" | "agent" |
Whether the sender is a person or an agent (ADR 0087); harness for a harness written before agents, and for any agent as a harness that has not declared agents is told it. |
from.display_name |
yes | string |
The name to show. |
from.role |
yes | "owner" | "member" | "guest" |
The sender’s standing in this space. Multi-user is Chips’ job, not the harness’s. |
from.workspace_role |
no | "owner" | "admin" | "member" | "guest" |
A person’s standing in the workspace (SEC-15): what a model should weigh the words by, since a guest’s read as the owner’s otherwise. promptFor says it after the name. Absent for an agent or a harness, and from a Chips before it said. |
text |
yes | string |
Markdown, with mentions resolved to participant ids. |
attachments |
no | object[] | URLs expire in ten minutes. |
attachments[].attachment_id |
yes | string |
A attachment identifier: att_ followed by a ULID. |
attachments[].filename |
yes | string |
The name to show. Never trusted for its type. |
attachments[].mime_type |
yes | string |
Sniffed from the bytes on upload, not taken from the client. |
attachments[].byte_size |
yes | number |
Size in bytes. |
attachments[].sha256 |
yes | string |
Lowercase hex digest. The store is content-addressed. |
attachments[].width |
no | number |
Pixels, for images and video. |
attachments[].height |
no | number |
Pixels, for images and video. |
attachments[].duration_ms |
no | number |
Milliseconds, for audio and video. |
attachments[].transcript |
no | string |
What was said, for a voice note the server transcribed when it arrived (ADR 0054). Machine-heard: the person’s words as best the server could make them out. |
attachments[].replaces |
no | string |
The version of the room’s file of this name that this one was made from (ADR 0099), from the post’s replaces. Filled in by Chips; a value the sender writes is replaced. |
attachments[].url |
yes | string |
A signed URL, bound to the requesting token: fetch it with that token as the bearer, since the URL alone is refused. |
attachments[].url_expires_at |
yes | string |
Ten minutes from issue, by default (decision 10). |
mentions |
no | string[] |
Participant ids mentioned in the text. |
reply_to |
no | string |
The message this answers. |
replying_to |
no | object | The message reply_to names, as the room reads it now (ADR 0219): who said it, when, its words and its files, and with a turn, when it is older than context, the lines said just before it. Put it in the turn ahead of the words; the SDK’s promptFor does. Absent on a message that answers none, or one no longer in the room. |
replying_to.seq |
yes | number |
Where in the log it happened. |
replying_to.at |
yes | string |
When. |
replying_to.kind |
yes | "said" |
|
replying_to.from |
yes | object | Who: the one who said it, came, went or took the new name; for the room’s own lines, who retitled, archived or unarchived it. |
replying_to.from.id |
yes | string |
Their participant id, or system. |
replying_to.from.kind |
yes | "person" | "harness" | "agent" | "system" |
|
replying_to.from.name |
yes | string |
What they go by now. |
replying_to.from.role |
no | "owner" | "member" | "guest" |
Their standing in this room, when they are in it (SEC-15). |
replying_to.from.workspace_role |
no | "owner" | "admin" | "member" | "guest" |
A person’s standing in the workspace (SEC-15): the plain form says it after the name, so a guest’s line reads as a guest’s. Absent for an agent or a harness. |
replying_to.message_id |
yes | string |
The message the reply answers: its reply_to. |
replying_to.text |
no | string |
On said: the words as they read now, so an edited message reads edited; a deleted one is not a line. Cut with an ellipsis when the one line is longer than the whole budget. |
replying_to.transcript |
no | string |
On said: what a voice note said, as the server heard it (ADR 0054). |
replying_to.files |
no | string[] |
On said: the names of the files it carried. |
replying_to.addressed_to |
no | string[] |
On said: the agents it was for (ADR 0081, ADR 0087), so a harness can leave out a line one of its agents already took a turn on. Absent on a message from before Chips said so. |
replying_to.held |
no | true |
On said: the guard held it (ADR 0081), so it was for nobody. |
replying_to.name |
no | string |
On renamed, for a participant: the name they took. |
replying_to.previous |
no | string |
On renamed, for a participant: the name they went by until then. |
replying_to.title |
no | string |
On renamed, for the room: its new title. |
replying_to.edited |
no | true |
Its author changed its words since: text is how it reads now. |
replying_to.deleted |
no | true |
Taken back since it was answered: no words, no files, only who and when. |
replying_to.before |
no | object[] | Up to three lines said just before it, oldest first, so the turn has what it was about. Only with a turn, and only when the message is older than the lines context hands over. |
replying_to.before[].seq |
yes | number |
Where in the log it happened. |
replying_to.before[].at |
yes | string |
When. |
replying_to.before[].kind |
yes | "said" | "joined" | "left" | "renamed" | "archived" | "unarchived" |
said: a message, addressed to this harness or not. joined and left: somebody came into the room or went. renamed: somebody took a new name, or the room a new title. archived and unarchived: the room was put away or brought back. |
replying_to.before[].from |
yes | object | Who: the one who said it, came, went or took the new name; for the room’s own lines, who retitled, archived or unarchived it. |
replying_to.before[].from.id |
yes | string |
Their participant id, or system. |
replying_to.before[].from.kind |
yes | "person" | "harness" | "agent" | "system" |
|
replying_to.before[].from.name |
yes | string |
What they go by now. |
replying_to.before[].from.role |
no | "owner" | "member" | "guest" |
Their standing in this room, when they are in it (SEC-15). |
replying_to.before[].from.workspace_role |
no | "owner" | "admin" | "member" | "guest" |
A person’s standing in the workspace (SEC-15): the plain form says it after the name, so a guest’s line reads as a guest’s. Absent for an agent or a harness. |
replying_to.before[].message_id |
no | string |
On said: the message. |
replying_to.before[].text |
no | string |
On said: the words as they read now, so an edited message reads edited; a deleted one is not a line. Cut with an ellipsis when the one line is longer than the whole budget. |
replying_to.before[].transcript |
no | string |
On said: what a voice note said, as the server heard it (ADR 0054). |
replying_to.before[].files |
no | string[] |
On said: the names of the files it carried. |
replying_to.before[].addressed_to |
no | string[] |
On said: the agents it was for (ADR 0081, ADR 0087), so a harness can leave out a line one of its agents already took a turn on. Absent on a message from before Chips said so. |
replying_to.before[].held |
no | true |
On said: the guard held it (ADR 0081), so it was for nobody. |
replying_to.before[].name |
no | string |
On renamed, for a participant: the name they took. |
replying_to.before[].previous |
no | string |
On renamed, for a participant: the name they went by until then. |
replying_to.before[].title |
no | string |
On renamed, for the room: its new title. |
replying_to.attachments |
no | object[] | Its files, as the message’s own are sent: URLs expire in ten minutes. |
replying_to.attachments[].attachment_id |
yes | string |
A attachment identifier: att_ followed by a ULID. |
replying_to.attachments[].filename |
yes | string |
The name to show. Never trusted for its type. |
replying_to.attachments[].mime_type |
yes | string |
Sniffed from the bytes on upload, not taken from the client. |
replying_to.attachments[].byte_size |
yes | number |
Size in bytes. |
replying_to.attachments[].sha256 |
yes | string |
Lowercase hex digest. The store is content-addressed. |
replying_to.attachments[].width |
no | number |
Pixels, for images and video. |
replying_to.attachments[].height |
no | number |
Pixels, for images and video. |
replying_to.attachments[].duration_ms |
no | number |
Milliseconds, for audio and video. |
replying_to.attachments[].transcript |
no | string |
What was said, for a voice note the server transcribed when it arrived (ADR 0054). Machine-heard: the person’s words as best the server could make them out. |
replying_to.attachments[].replaces |
no | string |
The version of the room’s file of this name that this one was made from (ADR 0099), from the post’s replaces. Filled in by Chips; a value the sender writes is replaced. |
replying_to.attachments[].url |
yes | string |
A signed URL, bound to the requesting token: fetch it with that token as the bearer, since the URL alone is refused. |
replying_to.attachments[].url_expires_at |
yes | string |
Ten minutes from issue, by default (decision 10). |
session_ref |
no | string |
The visit the sender meant, when they named one (ADR 0040). A harness that works by visits delivers to it rather than choosing, and says so rather than delivering elsewhere when it cannot. |
addressed_to |
no | string[] |
The agents the message is for, as Chips decided them (ADR 0081, ADR 0087). Absent on a message from before Chips said so, which was for every agent in the space. |
for |
no | string[] |
Who of this connection takes a turn: its own among addressed_to (ADR 0087). Each is one of its agents, or the harness’s own id where the harness itself is the member addressed: a room asked of it from the +, until it claims the room. Never empty on a message.new, and always empty on a message.overheard. Sent only to a harness that declares agents; one that has not is told its agents are itself. |
working_turns |
no | object[] | This connection’s agents with a turn going in the room, other than the one who said it and those it is for (ADR 0106). Hand the words to each of those turns at its next step, as said to somebody else, and start no turn on them; post message.heard on the run once a turn has them, and the room shows it heard. A turn that ends first hears nothing, and is handed the words with its agent’s next turn (ADR 0105). Absent when there are none. |
working_turns[].agent_id |
yes | string |
One of this connection’s agents. |
working_turns[].run_id |
yes | string |
Its turn going in the room. |
context |
no | object | What was said in the room since this harness last spoke there, or since it joined when it has not, up to this message and without it (ADR 0088). From when the turn that said its last words began, so what was said while it worked is here, and its own words are not (ADR 0105). Worked out from the log, so a replay hands over the same lines. As many as the budget the harness gave in its hello: 40 lines and 16,000 characters when it gave none, and none when it asked for 0. When older lines were left out, the taker’s room note says so. |
context.lines |
yes | object[] | Oldest first, the newest that fit the budget. |
context.lines[].seq |
yes | number |
Where in the log it happened. |
context.lines[].at |
yes | string |
When. |
context.lines[].kind |
yes | "said" | "joined" | "left" | "renamed" | "archived" | "unarchived" |
said: a message, addressed to this harness or not. joined and left: somebody came into the room or went. renamed: somebody took a new name, or the room a new title. archived and unarchived: the room was put away or brought back. |
context.lines[].from |
yes | object | Who: the one who said it, came, went or took the new name; for the room’s own lines, who retitled, archived or unarchived it. |
context.lines[].from.id |
yes | string |
Their participant id, or system. |
context.lines[].from.kind |
yes | "person" | "harness" | "agent" | "system" |
|
context.lines[].from.name |
yes | string |
What they go by now. |
context.lines[].from.role |
no | "owner" | "member" | "guest" |
Their standing in this room, when they are in it (SEC-15). |
context.lines[].from.workspace_role |
no | "owner" | "admin" | "member" | "guest" |
A person’s standing in the workspace (SEC-15): the plain form says it after the name, so a guest’s line reads as a guest’s. Absent for an agent or a harness. |
context.lines[].message_id |
no | string |
On said: the message. |
context.lines[].text |
no | string |
On said: the words as they read now, so an edited message reads edited; a deleted one is not a line. Cut with an ellipsis when the one line is longer than the whole budget. |
context.lines[].transcript |
no | string |
On said: what a voice note said, as the server heard it (ADR 0054). |
context.lines[].files |
no | string[] |
On said: the names of the files it carried. |
context.lines[].addressed_to |
no | string[] |
On said: the agents it was for (ADR 0081, ADR 0087), so a harness can leave out a line one of its agents already took a turn on. Absent on a message from before Chips said so. |
context.lines[].held |
no | true |
On said: the guard held it (ADR 0081), so it was for nobody. |
context.lines[].name |
no | string |
On renamed, for a participant: the name they took. |
context.lines[].previous |
no | string |
On renamed, for a participant: the name they went by until then. |
context.lines[].title |
no | string |
On renamed, for the room: its new title. |
context.since_seq |
yes | number |
Where they start: after this seq. With a message, where the turn that said the harness’s own last message in the room began (ADR 0105), else its joining; from the pull route, 0. |
context.truncated |
yes | boolean |
Whether older lines after since_seq were left out to fit the budget. |
context.before |
yes | number |
A cursor: GET /v1/spaces/:id/context?before= this reads what came before these lines. |
context.files |
no | object[] | The room’s files at their current version, newest first, the 20 newest (ADR 0099): with a message, as of it, its own files included; from the pull route, as of now. Each with a URL for this harness. Absent from a Chips before files, and when the room has none. |
context.files[].name |
yes | string |
The file’s name, which its versions share. |
context.files[].version |
yes | number |
Which version this is: 1 for the first, counting each change of its bytes. |
context.files[].attachment_id |
yes | string |
The attachment that is this version: what a post names in replaces when it changes it. |
context.files[].sha256 |
yes | string |
Lowercase hex digest of this version’s bytes. |
context.files[].byte_size |
yes | number |
Size in bytes. |
context.files[].mime_type |
yes | string |
Sniffed from the bytes on upload. |
context.files[].by |
yes | object | Who posted this version, as a line’s from says it. |
context.files[].by.id |
yes | string |
Their participant id. |
context.files[].by.kind |
yes | "person" | "harness" | "agent" | "system" |
|
context.files[].by.name |
yes | string |
What they go by now. |
context.files[].at |
yes | string |
When this version was posted. |
context.files[].message_id |
no | string |
The message that posted this version. |
context.files[].width |
no | number |
Pixels, for a photo or a video. |
context.files[].height |
no | number |
Pixels, for a photo or a video. |
context.files[].duration_ms |
no | number |
Milliseconds, for a sound or a video. |
context.files[].replaces |
no | string |
In one name’s versions (?name=): the version this one was made from, as its post said (message.post.replaces). Absent where the post said nothing, or named no version of this file. |
context.files[].url |
no | string |
For a harness: a signed URL for this version, bound to its token, as a message’s attachments are. Absent for a person, who fetches it by id. |
context.files[].url_expires_at |
no | string |
When url stops working. |
context.spend |
no | object | What the room’s job has spent, as of the message (for one Chips held, as of its hand-over, when a new job began), against what the room allows a job (ADR 0117). With a message only; the room note says it in words. Absent from a Chips before it. |
context.spend.ceiling_usd |
yes | number |
What the room allows a job, in US dollars: its own ceiling, or the default (3) when nobody has set one. |
context.spend.spent_usd |
yes | number |
What the room’s runs reported costing since the job began, in US dollars, every harness’s together. |
context.spend.since_seq |
yes | number |
Where the job began: the seq of the last thing a person did in the room, or of the last brief handed into it, whichever is later; 0 when neither has happened. |
context.spend.uncounted_runs |
yes | number |
How many of the job’s runs reported no cost, and so count nothing. |
room |
no | object | Who is in the room, with the handle that reaches each, and a note for each who takes the turn saying so and what an @ does (ADR 0092). A harness puts notes[<taker>] at the top of that turn. Absent on a message in no room. |
room.members |
yes | object[] | The room’s people, then its agents, each by name, anybody removed left out. |
room.members[].participant_id |
yes | string |
|
room.members[].kind |
yes | "person" | "agent" | "harness" |
A person, or an agent: one of a harness’s, or a harness in the room in its own right, as in a room asked of it it has not claimed. A harness that has not declared agents is shown each agent as harness, as everywhere else. |
room.members[].name |
yes | string |
|
room.members[].harness |
no | string |
For an agent, the name of the harness it runs on: Scout; for a harness waiting in a room asked of it, listed by the first name the room holds for its agent (ADR 0150), its own name. Absent for a person, any other harness in the room in its own right, and an agent named as its harness is. |
room.members[].handle |
no | string |
What reaches them, with its @: @Scout2. Their whole name with its spaces left out, or their first word where only that is a handle. Absent for a name no handle reaches, such as one letter. |
room.members[].lead |
no | true |
The room’s lead (ADR 0104): it hands out the work, and Chips hands it what was nobody’s turn once the room goes still. |
room.members[].time_zone |
no | string |
For a person, the IANA zone of the phone they registered last (ADR 0157): what “at 10am” means to them, which the note says as their time now. Absent when Chips knows none. |
room.notes |
yes | { } |
The note for each who takes the turn (for), by their id: who is here, with handles, and that only those @-mentioned act. A harness puts it at the top of the turn. |
room.rules |
no | { } |
How the space works, for each who takes the turn, by their id: the words its note ends with (ADR 0250), that everyone here reads what it writes and, beside other agents, what an @ does and when to answer NO_REPLY. They stay the same while the space’s agents do, so a harness whose session keeps its earlier turns may say them once, and the note without them after. |
room.working |
no | object[] | The room’s agents with a turn going as Chips sends this one (ADR 0104), the takers’ own included. A turn nothing has been heard from for ten minutes is not counted. |
room.working[].participant_id |
yes | string |
|
room.working[].minutes |
yes | number |
How long its turn has been going, in whole minutes. |
room.working[].step |
no | string |
The last step its turn said it was on, when its harness says its steps. |
room.idle |
no | object[] | The room’s other agents that have had a turn here: how long since the last one ended (ADR 0104). |
room.idle[].participant_id |
yes | string |
|
room.idle[].minutes |
yes | number |
How long since its last turn here ended, in whole minutes. |
exchange |
no | object | How the agents’ talk in the room stands, once it is worth a word (ADR 0097): from eight messages between agents since a person last acted or an agent last made something new. A harness puts note at the top of the turn, as Chips’s, ahead of the room’s note. Absent below that, and worked out from the log, so a replay hands over the same facts. |
exchange.count |
yes | number |
Messages agents have sent agents in the room since the reset, this one included. |
exchange.since |
yes | object | The last reset of the count. |
exchange.since.kind |
yes | "person" | "work" | "start" |
person: somebody did something in the room. work: an agent made something new — a file new to the room, a question to a person, an approval request. start: nothing since the room began. |
exchange.since.seq |
yes | number |
Where the count starts from, after this. |
exchange.since.at |
no | string |
When. Absent at the start. |
exchange.since.by |
no | string |
Who: the person, or the agent that made it. |
exchange.since.made |
no | "file" | "question" | "approval" |
For work: what the agent made. |
exchange.since.file |
no | string |
For a file: its name. |
exchange.minutes |
yes | number |
Since the reset. |
exchange.person_looking |
yes | unknown |
Whether somebody has the room open: a person not behind it by a message older than a minute. null when Chips cannot say. |
exchange.person |
no | string |
Who to tell what is done and what is blocked: the person who last acted here. |
exchange.repeats |
yes | object[] | Which of the messages repeat, newest last. |
exchange.repeats[].message_id |
yes | string |
|
exchange.repeats[].by |
yes | string |
|
exchange.repeats[].kind |
yes | "file" | "words" |
file: it carried a file already in the room. words: its words are much the same as words its sender already sent since the reset. |
exchange.repeats[].file |
no | string |
|
exchange.circling |
yes | boolean |
The same two or three agents taking turns with nothing new: two rounds with a repeat among them. |
exchange.also_for |
yes | string[] |
Who else this message is for, besides whoever it is delivered to. |
exchange.stage |
yes | "quiet" | "nudge" | "warning" | "held" |
quiet under the nudge; nudge from it (half the workspace’s stop, or 8); warning on a circle or from the stop; held at the hold or while a person has paused the agents. The stop, the hold and whether circles count are the workspace’s (ADR 0187): 16, 48 and yes unless an owner or an admin changed them. |
exchange.allowance |
no | number |
After how many messages between agents Chips holds them: the workspace’s hold (ADR 0187). Absent when it holds them only on a pause. |
exchange.paused |
yes | boolean |
A person paused the agents, or Chips did at the room’s spend ceiling (ADR 0118), and nobody has acted since. |
exchange.note |
yes | string |
The facts in plain words, for the top of the turn. |
handed |
no | true |
Chips held this message when it was said and hands it over now a person has acted (ADR 0097): the newest of an agent’s held messages, once; or, with lead, hands it to the room’s lead. Its seq and id are the hand-over’s, its message_id the message’s own. |
lead |
no | true |
Handed to the agent taking the turn as the room’s lead (ADR 0104): the message was nobody’s turn, and the room went still. Its note says so at the top, and handed is set too. |
Example
Section titled “Example”{ "v": 1, "type": "message.new", "id": "evt_01K4ZQPX000000000000000074", "ts": "2026-09-02T14:22:16.004Z", "workspace_id": "ws_01K4ZQPX000000000000000001", "space_id": "spc_01K4ZQPX000000000000000002", "actor": { "kind": "system", "id": "system" }, "message_id": "msg_01K4ZQPX000000000000000007", "from": { "participant_id": "usr_01K4ZQPX000000000000000003", "kind": "person", "display_name": "Arjun", "role": "owner" }, "text": "Here's the signed PO. Get it confirmed with Northwind and update the tracker.", "attachments": [ { "attachment_id": "att_01K4ZQPX000000000000000008", "filename": "PO-4471.pdf", "mime_type": "application/pdf", "byte_size": 184302, "sha256": "92efa547f5840a2e7257bab9e3529a0410cce0d865fdbc40ebb2895e76de010a", "url": "https://chips.local/v1/attachments/att_01K4ZQPX000000000000000008?sig=9f2c7d0184a05c3b", "url_expires_at": "2026-09-02T14:32:31.004Z" } ], "addressed_to": ["hrn_01K4ZQPX000000000000000005"], "context": { "lines": [ { "seq": 61, "at": "2026-09-02T14:19:40.004Z", "kind": "joined", "from": { "id": "usr_01K4ZQPX00000000000000000A", "kind": "person", "name": "Priya" } }, { "seq": 62, "at": "2026-09-02T14:20:02.004Z", "kind": "said", "from": { "id": "usr_01K4ZQPX00000000000000000A", "kind": "person", "name": "Priya" }, "message_id": "msg_01K4ZQPX00000000000000000B", "text": "Northwind want the PO before they hold the delivery slot.", "addressed_to": ["hrn_01K4ZQPX000000000000000004"] }, { "seq": 64, "at": "2026-09-02T14:20:31.004Z", "kind": "said", "from": { "id": "hrn_01K4ZQPX000000000000000004", "kind": "harness", "name": "Ledger" }, "message_id": "msg_01K4ZQPX00000000000000000C", "text": "Their slot holds until Friday. PO-4471 is still unsigned in the tracker.", "addressed_to": [] }, { "seq": 65, "at": "2026-09-02T14:21:12.004Z", "kind": "said", "from": { "id": "usr_01K4ZQPX000000000000000003", "kind": "person", "name": "Arjun" }, "message_id": "msg_01K4ZQPX00000000000000000D", "text": "", "transcript": "I'm signing it now, give me a minute.", "files": ["voice-note.m4a"], "addressed_to": ["hrn_01K4ZQPX000000000000000004"] } ], "since_seq": 58, "truncated": false, "before": 61, "spend": { "ceiling_usd": 3, "spent_usd": 0.42, "since_seq": 62, "uncounted_runs": 0 } }}