@intentic/sandbox-contract 1.223.0 → 1.224.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chores/chores.js +1 -1
- package/dist/chores/chores.js.map +1 -1
- package/dist/chores/prompt.d.ts.map +1 -1
- package/dist/chores/prompt.js +3 -3
- package/dist/chores/prompt.js.map +1 -1
- package/dist/contracts/ports.contract.d.ts +11 -0
- package/dist/contracts/ports.contract.d.ts.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/schemas.d.ts +22 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +3 -0
- package/dist/schemas.js.map +1 -1
- package/package.json +4 -4
- package/src/agent-catalog.ts +78 -78
- package/src/agent-run-model.ts +8 -8
- package/src/capability-env.ts +3 -3
- package/src/capability-secrets.ts +5 -5
- package/src/chores/chores.ts +67 -67
- package/src/chores/digest.ts +3 -3
- package/src/chores/extension-update.ts +3 -3
- package/src/chores/fix-deps.ts +3 -3
- package/src/chores/probes.ts +16 -16
- package/src/chores/prompt.ts +9 -9
- package/src/chores/stack.ts +17 -17
- package/src/chores/verdict.ts +23 -23
- package/src/contract-lock.ts +7 -7
- package/src/contracts/activity.contract.ts +1 -1
- package/src/contracts/agent.contract.ts +2 -2
- package/src/contracts/agents.contract.ts +11 -11
- package/src/contracts/automations.contract.ts +5 -5
- package/src/contracts/capabilities.contract.ts +7 -7
- package/src/contracts/chores.contract.ts +3 -3
- package/src/contracts/ci.contract.ts +2 -2
- package/src/contracts/claude.contract.ts +3 -3
- package/src/contracts/drafts.contract.ts +1 -1
- package/src/contracts/endpoints.contract.ts +2 -2
- package/src/contracts/extensions.contract.ts +8 -8
- package/src/contracts/git.contract.ts +6 -6
- package/src/contracts/grok.contract.ts +4 -4
- package/src/contracts/host.contract.ts +5 -5
- package/src/contracts/intentic.contract.ts +2 -2
- package/src/contracts/logs.contract.ts +1 -1
- package/src/contracts/loops.contract.ts +8 -8
- package/src/contracts/personas.contract.ts +8 -8
- package/src/contracts/ports.contract.ts +1 -1
- package/src/contracts/prepush.contract.ts +2 -2
- package/src/contracts/providers.contract.ts +4 -4
- package/src/contracts/public.contract.ts +1 -1
- package/src/contracts/push.contract.ts +2 -2
- package/src/contracts/secrets.contract.ts +2 -2
- package/src/contracts/sessions.contract.ts +2 -2
- package/src/contracts/settings.contract.ts +3 -3
- package/src/contracts/share.contract.ts +1 -1
- package/src/contracts/skills.contract.ts +2 -2
- package/src/contracts/system.contract.ts +9 -9
- package/src/contracts/usage.contract.ts +1 -1
- package/src/contracts/vpn.contract.ts +5 -5
- package/src/contracts/workflows.contract.ts +12 -12
- package/src/contracts/workspace.contract.ts +13 -13
- package/src/conversation-ids.ts +7 -7
- package/src/events.ts +136 -136
- package/src/history-state.ts +12 -12
- package/src/host-protocol.ts +7 -7
- package/src/hostnames.ts +15 -15
- package/src/index.ts +8 -8
- package/src/listener-protocol.ts +12 -12
- package/src/model-order.ts +30 -30
- package/src/output-fields.ts +9 -9
- package/src/path-refs.ts +6 -6
- package/src/publish-drafts.ts +6 -6
- package/src/quick-model.ts +15 -15
- package/src/routes.ts +19 -19
- package/src/runtime-state.ts +22 -22
- package/src/schemas.ts +930 -918
- package/src/search-globs.ts +6 -6
- package/src/session-names.ts +5 -5
- package/src/share-paths.ts +7 -7
- package/src/sse.ts +1 -1
- package/src/state-portability.ts +8 -8
- package/src/terminal-protocol.ts +3 -3
- package/src/title.ts +20 -20
- package/src/tunnel-ids.ts +7 -7
- package/src/versions.ts +5 -5
- package/src/workflow-faults.ts +11 -11
- package/src/workspace-state.ts +119 -119
package/src/events.ts
CHANGED
|
@@ -37,16 +37,16 @@ export type AskQuestion = z.infer<typeof AskQuestionSchema>;
|
|
|
37
37
|
|
|
38
38
|
// One per-tool permission prompt (the SDK's canUseTool callback, surfaced as a card). The daemon passes the
|
|
39
39
|
// bridge's own rendered strings through rather than re-deriving them, so the prompt reads exactly as Claude
|
|
40
|
-
// Code words it. `alwaysLabel` is present only when the SDK offered rules to persist
|
|
40
|
+
// Code words it. `alwaysLabel` is present only when the SDK offered rules to persist, without it the card
|
|
41
41
|
// shows allow-once / deny alone, because there is nothing an "always" answer could remember.
|
|
42
42
|
export const PermissionAskSchema = z.object({
|
|
43
43
|
toolName: z.string(),
|
|
44
|
-
// "Claude wants to read foo.txt"
|
|
44
|
+
// "Claude wants to read foo.txt", the full prompt sentence, when the bridge rendered one.
|
|
45
45
|
title: z.string().optional(),
|
|
46
46
|
// Short noun phrase for the allow button ("Read file").
|
|
47
47
|
displayName: z.string().optional(),
|
|
48
48
|
description: z.string().optional(),
|
|
49
|
-
// Why the prompt fired ('rule' | 'mode' | 'classifier' | …)
|
|
49
|
+
// Why the prompt fired ('rule' | 'mode' | 'classifier' | …), shown as the card's muted subline.
|
|
50
50
|
reason: z.string().optional(),
|
|
51
51
|
// The file the request is about, when it is about one (workspace-root-relative).
|
|
52
52
|
path: z.string().optional(),
|
|
@@ -54,10 +54,10 @@ export const PermissionAskSchema = z.object({
|
|
|
54
54
|
});
|
|
55
55
|
export type PermissionAsk = z.infer<typeof PermissionAskSchema>;
|
|
56
56
|
|
|
57
|
-
/* ONE PRICED SERVICE RUN, OFFERED
|
|
57
|
+
/* ONE PRICED SERVICE RUN, OFFERED, the card the daemon raises when the agent asks to run a premium service
|
|
58
58
|
* (platform/service-offer.ts). Everything with a number on it is the PLATFORM's answer, relayed verbatim from
|
|
59
59
|
* the catalog it serves the daemon: the model that asked contributes `request` (the JSON it wants sent) and
|
|
60
|
-
* `why` (its one line of rationale), and nothing else
|
|
60
|
+
* `why` (its one line of rationale), and nothing else, which is what makes the price on the card impossible
|
|
61
61
|
* to misquote, and the click on it the only way the run can happen. */
|
|
62
62
|
export const ServiceOfferSchema = z.object({
|
|
63
63
|
// The service, as the platform lists it: `<slug>` is what the run names, the rest is the catalog row.
|
|
@@ -66,26 +66,26 @@ export const ServiceOfferSchema = z.object({
|
|
|
66
66
|
publisher: z.string(),
|
|
67
67
|
description: z.string(),
|
|
68
68
|
creditsPerRun: z.number(),
|
|
69
|
-
/* Whether the platform still has this listing on probation
|
|
69
|
+
/* Whether the platform still has this listing on probation, a new provider that passed admission's
|
|
70
70
|
* mechanical gates but has not yet served enough runs cleanly to graduate. It rides the card because
|
|
71
71
|
* probation is the honest form of "listed automatically, not vouched for": the member approving the
|
|
72
72
|
* spend is the person who should know that, and the platform is the only party that can say it. */
|
|
73
73
|
probation: z.boolean().optional(),
|
|
74
|
-
// The owner's meter as the platform stated it with the catalog
|
|
74
|
+
// The owner's meter as the platform stated it with the catalog, what "N left today" renders from. Absent
|
|
75
75
|
// when the platform sent none (it answers a meter only to a member, and membership was already checked
|
|
76
76
|
// before this card went up, so in practice it is present; the field stays honest about the wire).
|
|
77
77
|
credits: z.object({ allowance: z.number(), remaining: z.number(), resetsAt: z.string() }).optional(),
|
|
78
|
-
// The request body the agent wants forwarded, verbatim
|
|
78
|
+
// The request body the agent wants forwarded, verbatim, shown so the owner can see what leaves.
|
|
79
79
|
request: z.string(),
|
|
80
|
-
// The agent's one-line case for spending
|
|
80
|
+
// The agent's one-line case for spending, the only prose on the card that is the model's.
|
|
81
81
|
why: z.string().optional(),
|
|
82
82
|
});
|
|
83
83
|
export type ServiceOffer = z.infer<typeof ServiceOfferSchema>;
|
|
84
84
|
|
|
85
|
-
/* WHAT A SERVICE STREAMS
|
|
85
|
+
/* WHAT A SERVICE STREAMS, the provider's event vocabulary, stated once here and imported by everyone who
|
|
86
86
|
* touches it: the platform validates each line of a provider's NDJSON against this before relaying it, the
|
|
87
87
|
* daemon turns `status` events into transcript frames, and the editor renders them under the offer card.
|
|
88
|
-
* A run is `status` lines (each replaces the last
|
|
88
|
+
* A run is `status` lines (each replaces the last, a spinner label, not a log) ending in exactly one
|
|
89
89
|
* `result`, whose `data` is the answer the agent acts on. The union is where future event kinds land when
|
|
90
90
|
* services start streaming richer transcript elements; today's two are the smallest honest set. */
|
|
91
91
|
export const ServiceStreamEventSchema = z.discriminatedUnion(`event`, [
|
|
@@ -94,21 +94,21 @@ export const ServiceStreamEventSchema = z.discriminatedUnion(`event`, [
|
|
|
94
94
|
]);
|
|
95
95
|
export type ServiceStreamEvent = z.infer<typeof ServiceStreamEventSchema>;
|
|
96
96
|
|
|
97
|
-
/* ONE MISSING CAPABILITY, ASKED FOR
|
|
97
|
+
/* ONE MISSING CAPABILITY, ASKED FOR, the card the daemon raises when the agent hits something this sandbox
|
|
98
98
|
* is not connected to (capabilities/capability-offer.ts). `card` names the catalog card and `name` is that
|
|
99
|
-
* card's own title, both resolved by the daemon from the catalog it validates the ask against
|
|
99
|
+
* card's own title, both resolved by the daemon from the catalog it validates the ask against, the model
|
|
100
100
|
* that asked contributes `why` (its one line of rationale) and nothing else, which is what makes the card
|
|
101
101
|
* impossible to misrepresent, and the click on it the only way anything gets connected. */
|
|
102
102
|
export const CapabilityOfferSchema = z.object({
|
|
103
103
|
// The catalog card being asked for, and how the catalog itself titles it ("Notion", "GitHub", "Docker").
|
|
104
104
|
card: z.string(),
|
|
105
105
|
name: z.string(),
|
|
106
|
-
// The agent's one-line case for connecting it
|
|
106
|
+
// The agent's one-line case for connecting it, the only prose on the card that is the model's.
|
|
107
107
|
why: z.string().optional(),
|
|
108
108
|
});
|
|
109
109
|
export type CapabilityOffer = z.infer<typeof CapabilityOfferSchema>;
|
|
110
110
|
|
|
111
|
-
/* The trailer the PLATFORM appends to every relayed run stream
|
|
111
|
+
/* The trailer the PLATFORM appends to every relayed run stream, never provider-authored: it is the ledger
|
|
112
112
|
* speaking after the stream settled. `ok` means the run served and was charged (`remaining` is the meter
|
|
113
113
|
* after); `refunded` means the provider's stream died before its `result` and the charge was reversed. */
|
|
114
114
|
export const ServiceRunReceiptSchema = z.object({
|
|
@@ -119,9 +119,9 @@ export const ServiceRunReceiptSchema = z.object({
|
|
|
119
119
|
});
|
|
120
120
|
export type ServiceRunReceipt = z.infer<typeof ServiceRunReceiptSchema>;
|
|
121
121
|
|
|
122
|
-
/* ONE OUTBOUND USDC PAYMENT, OFFERED
|
|
122
|
+
/* ONE OUTBOUND USDC PAYMENT, OFFERED, the card the daemon raises when the agent asks to pay an x402
|
|
123
123
|
* endpoint out of the sandbox wallet (wallet/payment-offer.ts). Every number on it is the daemon's own
|
|
124
|
-
* arithmetic over the ENDPOINT's parsed challenge and the wallet's own ledger
|
|
124
|
+
* arithmetic over the ENDPOINT's parsed challenge and the wallet's own ledger, the model that asked
|
|
125
125
|
* contributes `why` (its one line of rationale) and nothing else, which is what makes the price on the card
|
|
126
126
|
* impossible to misquote, and the click on it the only way the money can move. */
|
|
127
127
|
export const PaymentOfferSchema = z.object({
|
|
@@ -132,20 +132,20 @@ export const PaymentOfferSchema = z.object({
|
|
|
132
132
|
payTo: z.string(),
|
|
133
133
|
network: z.string(),
|
|
134
134
|
asset: z.string(),
|
|
135
|
-
// The token's display name ("USDC")
|
|
135
|
+
// The token's display name ("USDC"), dollar-pegged, which is what lets every amount below read as USD.
|
|
136
136
|
assetName: z.string(),
|
|
137
|
-
// The exact price in display units ("0.10")
|
|
137
|
+
// The exact price in display units ("0.10"), the x402 exact scheme has no ranges, so this is the whole
|
|
138
138
|
// spend, not a ceiling.
|
|
139
139
|
amountUsd: z.string(),
|
|
140
|
-
// The wallet's meter as the daemon's ledger states it
|
|
140
|
+
// The wallet's meter as the daemon's ledger states it, what "spent today / cap" renders from.
|
|
141
141
|
spentTodayUsd: z.string(),
|
|
142
142
|
dailyCapUsd: z.string(),
|
|
143
|
-
// The agent's one-line case for paying
|
|
143
|
+
// The agent's one-line case for paying, the only prose on the card that is the model's.
|
|
144
144
|
why: z.string().optional(),
|
|
145
145
|
});
|
|
146
146
|
export type PaymentOffer = z.infer<typeof PaymentOfferSchema>;
|
|
147
147
|
|
|
148
|
-
// One provider-advertised slash command
|
|
148
|
+
// One provider-advertised slash command, an ACP agent's available_commands entry, or a Claude Code session's
|
|
149
149
|
// supportedCommands() (its built-ins plus the workspace's own .claude/commands and any plugin/skill commands).
|
|
150
150
|
// `hint` is the argument placeholder the popover shows after the name.
|
|
151
151
|
export const AgentCommandSchema = z.object({
|
|
@@ -155,7 +155,7 @@ export const AgentCommandSchema = z.object({
|
|
|
155
155
|
});
|
|
156
156
|
export type AgentCommand = z.infer<typeof AgentCommandSchema>;
|
|
157
157
|
|
|
158
|
-
// GET /agent/commands
|
|
158
|
+
// GET /agent/commands, which provider's last-published list to read; absent = claude, matching AgentTurn.
|
|
159
159
|
export const AgentCommandsQuerySchema = z.object({ agent: AgentProviderSchema.optional() });
|
|
160
160
|
export const AgentCommandsSchema = z.object({ commands: z.array(AgentCommandSchema) });
|
|
161
161
|
|
|
@@ -183,7 +183,7 @@ export type ToolKind = z.infer<typeof ToolKindSchema>;
|
|
|
183
183
|
export const ToolCallStatusSchema = z.enum(["pending", "in_progress", "completed", "failed"]);
|
|
184
184
|
export type ToolCallStatus = z.infer<typeof ToolCallStatusSchema>;
|
|
185
185
|
|
|
186
|
-
// A file a tool call touches. Workspace-root-relative, forward-slash (the tree/file route space)
|
|
186
|
+
// A file a tool call touches. Workspace-root-relative, forward-slash (the tree/file route space), adapters
|
|
187
187
|
// normalize from the turn's cwd. `line` is 1-based.
|
|
188
188
|
export const ToolCallLocationSchema = z.object({
|
|
189
189
|
path: z.string(),
|
|
@@ -217,13 +217,13 @@ export type ToolCallContent = z.infer<typeof ToolCallContentSchema>;
|
|
|
217
217
|
// ---- restored transcripts ----
|
|
218
218
|
// What /sessions/{id} replays into a reopened tab, and what the daemon's own conversation record stores. It has
|
|
219
219
|
// to REDRAW the transcript the user was looking at rather than merely paraphrase it, so it keeps the assistant's
|
|
220
|
-
// thinking and the tool cards its turn ran
|
|
220
|
+
// thinking and the tool cards its turn ran, which is also what lets a runtime handoff carry more than bare
|
|
221
221
|
// prose across to the replacement session (see runtime-history.ts). Reconstructed from the stored
|
|
222
222
|
// tool_use/tool_result blocks, so a restored card carries everything the live `tool_call` frame did except
|
|
223
223
|
// the streaming-only correlation fields.
|
|
224
224
|
//
|
|
225
225
|
// One restored tool card. A subagent's own calls and its thinking nest under the Agent card that spawned them,
|
|
226
|
-
// the same two fields (and the same recursion) the live ChatTool carries
|
|
226
|
+
// the same two fields (and the same recursion) the live ChatTool carries, so a reopened chat redraws the
|
|
227
227
|
// delegation it was showing instead of a leaf card with the whole child collapsed into its result text.
|
|
228
228
|
// z.lazy because the shape refers to itself: a subagent that delegates nests one level deeper.
|
|
229
229
|
export const RestoredToolCallSchema: z.ZodType<RestoredToolCall> = z.lazy(() =>
|
|
@@ -260,49 +260,49 @@ export const TurnNoteSchema = z.object({ title: z.string(), text: z.string() });
|
|
|
260
260
|
export type TurnNote = z.infer<typeof TurnNoteSchema>;
|
|
261
261
|
|
|
262
262
|
// One restored bubble. Each stored assistant message becomes its own, which is what reproduces the live
|
|
263
|
-
// interleaving
|
|
263
|
+
// interleaving, prose, the tool cards that prose introduced, then the next block of prose, rather than
|
|
264
264
|
// collapsing a turn's whole narration into a single bubble with its tools hanging off the end.
|
|
265
265
|
export const RestoredMessageSchema = z.object({
|
|
266
266
|
/* `notice` is neither side of the conversation: it is something that HAPPENED to the turn, recorded so a
|
|
267
|
-
* reopened session can say it. The one that matters is a refused turn
|
|
267
|
+
* reopened session can say it. The one that matters is a refused turn, a provider that answers "your
|
|
268
268
|
* organization has disabled Claude subscription access" produced no assistant text, so a transcript of the
|
|
269
269
|
* two speakers alone ends on the user's message and the session reads as broken. It is the same muted line
|
|
270
270
|
* the live client draws for the codes it does not turn red (ChatRole's `notice`). */
|
|
271
271
|
role: z.enum(["user", "assistant", "notice"]),
|
|
272
272
|
text: z.string(),
|
|
273
|
-
/* WHEN THIS TURN WAS SENT, in epoch milliseconds (user rows only)
|
|
273
|
+
/* WHEN THIS TURN WAS SENT, in epoch milliseconds (user rows only), what the chat shows on the bubble it
|
|
274
274
|
* belongs to. The turn's START, not the moment the record was written: a turn that ran for twenty minutes
|
|
275
275
|
* was still sent when the user pressed send, and a stamp taken at settlement would say the conversation
|
|
276
276
|
* happened at the times its answers finished.
|
|
277
277
|
*
|
|
278
278
|
* Only the user's row carries one, because it is the only row whose moment the daemon actually knows. A
|
|
279
279
|
* turn's frames arrive with no clock of their own, so an assistant bubble could only ever be stamped with
|
|
280
|
-
* the whole turn's start or end
|
|
280
|
+
* the whole turn's start or end, a number that says nothing about when that particular block was written.
|
|
281
281
|
* Rows recorded before this existed simply have none, and the chat draws nothing for them. */
|
|
282
282
|
sentAt: z.number().optional(),
|
|
283
283
|
// Files the user attached to this turn (user bubbles only) as workspace-relative paths, recovered from
|
|
284
|
-
// the stored prompt's attachment note
|
|
284
|
+
// the stored prompt's attachment note, so a reopened tab redraws chips, not the injected protocol text.
|
|
285
285
|
attachments: z.array(z.string()).optional(),
|
|
286
286
|
/* The checkpoint this message can be rewound to (user bubbles only), filled in when the transcript is read
|
|
287
|
-
* back. Not stored in the record itself
|
|
288
|
-
* a rewind rewrites
|
|
287
|
+
* back. Not stored in the record itself, it is looked up per read from the daemon's rewind points, which
|
|
288
|
+
* a rewind rewrites, so a reopened tab offers exactly the turns that are still there to go back to. */
|
|
289
289
|
checkpointId: z.string().optional(),
|
|
290
290
|
thinking: z.string().optional(),
|
|
291
291
|
tools: z.array(RestoredToolCallSchema).optional(),
|
|
292
|
-
/* What the daemon added to this turn's message (user rows only)
|
|
292
|
+
/* What the daemon added to this turn's message (user rows only), the same notes the live `preamble` frame
|
|
293
293
|
* carries, recovered from the stored prompt when the transcript is read back. The reader that strips them out
|
|
294
294
|
* of the user's words is the one that hands them over here instead of dropping them on the floor.
|
|
295
295
|
*
|
|
296
|
-
* On the message rather than as a row of its own, and that
|
|
296
|
+
* On the message rather than as a row of its own, and that matters twice: they ARE part of what was
|
|
297
297
|
* sent, and a record row per turn preamble would break the one-row-per-bubble correspondence a branch counts
|
|
298
|
-
* with (see the client's recordedRows
|
|
298
|
+
* with (see the client's recordedRows, notices are drawn locally and never recorded). */
|
|
299
299
|
notes: z.array(TurnNoteSchema).optional(),
|
|
300
|
-
/* THE USER WROTE THIS ROW WEARING THE AGENT'S VOICE (assistant rows only)
|
|
300
|
+
/* THE USER WROTE THIS ROW WEARING THE AGENT'S VOICE (assistant rows only), the composer's "as agent" mode
|
|
301
301
|
* appending straight into the record, with no turn behind it (agents.place).
|
|
302
302
|
*
|
|
303
303
|
* The flag exists for exactly one audience: the HUMAN re-reading the transcript, whose bubble carries a
|
|
304
304
|
* quiet mark so that months later their own words don't pass as the agent's. The one reader that must
|
|
305
|
-
* never see it is the agent itself
|
|
305
|
+
* never see it is the agent itself, a placed line reaches the model only through the handoff that seeds a
|
|
306
306
|
* fresh runtime session (agent/runtime-history.ts), which renders role and text alone, so there the line is
|
|
307
307
|
* indistinguishable from anything the agent genuinely said. Keep it that way: rendering this flag into any
|
|
308
308
|
* agent-facing text would break the feature's whole contract. */
|
|
@@ -313,11 +313,11 @@ export type RestoredMessage = z.infer<typeof RestoredMessageSchema>;
|
|
|
313
313
|
export const SessionTranscriptSchema = z.object({ messages: z.array(RestoredMessageSchema) });
|
|
314
314
|
export const AgentTranscriptSchema = SessionTranscriptSchema.extend({ sessionId: z.string().optional() });
|
|
315
315
|
|
|
316
|
-
/* WHAT A PUBLISHED CONVERSATION'S PAGE IS HANDED
|
|
316
|
+
/* WHAT A PUBLISHED CONVERSATION'S PAGE IS HANDED, the whole of it, baked into the page as one JSON block.
|
|
317
317
|
*
|
|
318
318
|
* A share has to keep working with nothing behind it: no daemon, no session, no sandbox that has to still be
|
|
319
319
|
* running when the recipient finally opens the link. So the page carries its conversation rather than fetching
|
|
320
|
-
* it, which also settles the security question by construction
|
|
320
|
+
* it, which also settles the security question by construction, a page with nothing to ask has no way to ask
|
|
321
321
|
* for something it was not given.
|
|
322
322
|
*
|
|
323
323
|
* The messages are the SAME RestoredMessage rows the app replays a reopened tab from, already filtered to the
|
|
@@ -326,7 +326,7 @@ export const AgentTranscriptSchema = SessionTranscriptSchema.extend({ sessionId:
|
|
|
326
326
|
* is what the owner saw. */
|
|
327
327
|
export const SharePayloadSchema = z.object({
|
|
328
328
|
title: z.string(),
|
|
329
|
-
// When the snapshot was taken, not when the conversation happened
|
|
329
|
+
// When the snapshot was taken, not when the conversation happened, see SharedConversation.sharedAt.
|
|
330
330
|
sharedAt: z.number(),
|
|
331
331
|
detail: ShareDetailSchema,
|
|
332
332
|
messages: z.array(RestoredMessageSchema),
|
|
@@ -335,7 +335,7 @@ export type SharePayload = z.infer<typeof SharePayloadSchema>;
|
|
|
335
335
|
|
|
336
336
|
/* THE THREE RESTORABLE CARDS, named so the turn journal can hold them verbatim: a parked turn's raised cards
|
|
337
337
|
* are written down beside its prompt (sandbox turn-journal.ts), and a daemon death under the park restores the
|
|
338
|
-
* very same frames instead of ending the turn `interrupted
|
|
338
|
+
* very same frames instead of ending the turn `interrupted`, the card the user was about to answer survives
|
|
339
339
|
* the restart that killed the process holding it. The two handover cards are deliberately not among them:
|
|
340
340
|
* `browser_help`'s Chromium and `terminal_help`'s waiting command both die with the container, so those parks
|
|
341
341
|
* cannot be restored, only reported. */
|
|
@@ -355,7 +355,7 @@ export type ParkedCard = z.infer<typeof ParkedCardSchema>;
|
|
|
355
355
|
// subagent itself, keyed by the same tool_use id those tagged frames carry.
|
|
356
356
|
export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
357
357
|
z.object({ kind: z.literal("session"), sessionId: z.string() }),
|
|
358
|
-
/* WHERE AN ISOLATED TURN IS STANDING: the conversation's worktree identity
|
|
358
|
+
/* WHERE AN ISOLATED TURN IS STANDING: the conversation's worktree identity, its branch (agent/<id>) and
|
|
359
359
|
* the ROOT repo's short base sha. First frame of the turn, before any provider frames, and again each time
|
|
360
360
|
* the branch MOVES underneath it, which is why `base` names where the branch sits now rather than the
|
|
361
361
|
* moment it was checked out.
|
|
@@ -363,15 +363,15 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
363
363
|
* `unenforced` marks the degraded container: no CAP_SYS_ADMIN, so the turn's worktree could not be
|
|
364
364
|
* bind-mounted over the workspace root and the harness is rewriting tool paths into it instead. That
|
|
365
365
|
* fallback covers what arrives as tool input and not what a subprocess computes for itself, so the
|
|
366
|
-
* operator needs to know
|
|
366
|
+
* operator needs to know, this state used to be one line in the daemon log at boot, and the way it got
|
|
367
367
|
* noticed was files appearing in the main tree from agents that were supposed to be on branches. Repeated
|
|
368
368
|
* on every emission, because it describes the turn and a client rebuilds its standing from the last frame.
|
|
369
369
|
*
|
|
370
370
|
* `sync` reports a rebase (agents/sync.ts) and rides here because this frame is already the turn's "where
|
|
371
|
-
* you are standing" announcement. Present only when the branch was BEHIND the main line
|
|
371
|
+
* you are standing" announcement. Present only when the branch was BEHIND the main line, `commits` is how
|
|
372
372
|
* many main-line commits it gained, `blocked` names the repos whose rebase would not apply and was rolled
|
|
373
373
|
* back. Both can be non-empty at once in a multi-repo composition. Two moments produce it: before the turn
|
|
374
|
-
* starts, and after a card the turn parked on is answered
|
|
374
|
+
* starts, and after a card the turn parked on is answered, a question or a plan approval waits minutes
|
|
375
375
|
* for a person, and the main line does not stop moving meanwhile. It is a notice and never a question: the
|
|
376
376
|
* user is answering their agent, and the alternative to rebasing is not "stay safe" but "conflict at land
|
|
377
377
|
* time", which interrupts them harder. */
|
|
@@ -386,9 +386,9 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
386
386
|
// UNCOMMITTED changes in the main tree (the Changes panel is the review); conflicts ⇒ it stayed safely in
|
|
387
387
|
// the worktree, and each named path carries WHY it would not apply (see LandConflictSchema) so the report
|
|
388
388
|
// can say whether the user's own copy is at risk or the main line simply moved on underneath the agent.
|
|
389
|
-
// held ⇒ auto-land is off for this agent: nothing was applied and nothing failed
|
|
389
|
+
// held ⇒ auto-land is off for this agent: nothing was applied and nothing failed, the delta is waiting
|
|
390
390
|
// on the branch for a deliberate Land (landed is false, conflicts absent).
|
|
391
|
-
// `deps` rides along when the landed delta left the main tree declaring dependencies it does not have
|
|
391
|
+
// `deps` rides along when the landed delta left the main tree declaring dependencies it does not have,
|
|
392
392
|
// the residue of an agent adding one without installing it, which every LATER turn would inherit through
|
|
393
393
|
// the overlay it mounts over the main checkout. The daemon reconciles it rather than asking anyone to
|
|
394
394
|
// (workspace/reconcile-deps.ts); this is the receipt, and `deferred` is the honest answer while other turns
|
|
@@ -400,23 +400,23 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
400
400
|
held: z.boolean().optional(),
|
|
401
401
|
deps: z.object({ missing: z.number(), started: z.array(z.string()), deferred: z.boolean() }).optional(),
|
|
402
402
|
}),
|
|
403
|
-
/* WHAT THE DAEMON ADDED TO THE USER'S MESSAGE before the model read it
|
|
403
|
+
/* WHAT THE DAEMON ADDED TO THE USER'S MESSAGE before the model read it, the exact words, not a summary of
|
|
404
404
|
* them.
|
|
405
405
|
*
|
|
406
406
|
* A turn's prompt is not only what was typed: the daemon prepends notes the model needs and the user did not
|
|
407
|
-
* write (agent/turn-preamble.ts owns the list
|
|
407
|
+
* write (agent/turn-preamble.ts owns the list, a rebase that moved the branch, dependencies that are behind,
|
|
408
408
|
* workspace context retrieved for this very message, where an unenforced runtime's files really live). Those
|
|
409
409
|
* notes change what the agent does, and for a long time the chat's only trace of any of them was one muted
|
|
410
|
-
* line paraphrasing the rebase
|
|
410
|
+
* line paraphrasing the rebase, so a user watching an agent act on instructions they could not see had no
|
|
411
411
|
* way to find out what those instructions said. This frame is the fix: the note text verbatim, one entry per
|
|
412
412
|
* note, rendered collapsed so it costs a click rather than a scroll.
|
|
413
413
|
*
|
|
414
|
-
* `title` is the note's own opening header, which is what the stripper already anchors on
|
|
414
|
+
* `title` is the note's own opening header, which is what the stripper already anchors on, so the two
|
|
415
415
|
* cannot drift, and a note nobody thought to title cannot reach the wire unlabelled.
|
|
416
416
|
*
|
|
417
417
|
* ONE MOMENT, always: the notes went in front of the user's own message before the turn started, so they hang
|
|
418
418
|
* off that message and are stored on it, which is how a reopened tab still has them. Nothing is injected into
|
|
419
|
-
* a RUNNING turn
|
|
419
|
+
* a RUNNING turn, the rebase taken while a card sat waiting was the only thing that ever was, and it no
|
|
420
420
|
* longer says anything to the model at all (agent/turn-preamble.ts). */
|
|
421
421
|
z.object({ kind: z.literal("preamble"), notes: z.array(TurnNoteSchema) }),
|
|
422
422
|
// The SDK's init handshake; carries the model it actually resolved for the turn.
|
|
@@ -424,44 +424,44 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
424
424
|
// The pre-turn workspace snapshot's id (the attribution-fence "user" capture), emitted once before the
|
|
425
425
|
// provider stream so the client can offer "restore to before this message" on the turn's user bubble.
|
|
426
426
|
// Absent on isolated turns (they snapshot nothing) and when the tree was already clean at turn start.
|
|
427
|
-
/* The workspace checkpoint capturing the state as this turn FOUND it
|
|
427
|
+
/* The workspace checkpoint capturing the state as this turn FOUND it, what "go back to before this
|
|
428
428
|
* message" restores. `index` is the message's position in the conversation's transcript, which the rewind
|
|
429
429
|
* route addresses it by; absent on a turn with no conversation behind it (the bench, a one-shot), where
|
|
430
430
|
* the id still powers a plain restore but there is no message to rewind to. */
|
|
431
431
|
z.object({ kind: z.literal("checkpoint"), id: z.string(), index: z.number().int().nonnegative().optional() }),
|
|
432
|
-
/* A MESSAGE THE USER SENT INTO THE TURN WHILE IT RAN
|
|
432
|
+
/* A MESSAGE THE USER SENT INTO THE TURN WHILE IT RAN, the mid-turn steer, at the point in the stream where
|
|
433
433
|
* the daemon accepted it (agent/agent-steering.ts).
|
|
434
434
|
*
|
|
435
435
|
* A frame rather than a client-local write, because all three things that were wrong about the steer are the
|
|
436
436
|
* same missing fact: nothing in the run's log said WHEN it arrived.
|
|
437
437
|
* - POSITION. The harness injects a steer between tool calls and the model simply keeps writing, with no
|
|
438
|
-
* `result` in between
|
|
438
|
+
* `result` in between, so there is no `usage` boundary to retire the open bubble. The sending window
|
|
439
439
|
* appended the user's words at the END of its transcript while the turn kept typing into the bubble
|
|
440
440
|
* ABOVE them, and the answer to a question landed over the question.
|
|
441
441
|
* - EVERY OTHER WINDOW. A run is rendered by any number of attached clients; only the one that posted the
|
|
442
442
|
* steer knew about it, so the same conversation read differently in two places.
|
|
443
443
|
* - THE RECORD. The settled turn is written down from this log (sessions/turn-transcript.ts), and one that
|
|
444
|
-
* never held the steer wrote a transcript the message was missing from entirely
|
|
444
|
+
* never held the steer wrote a transcript the message was missing from entirely, which also put the
|
|
445
445
|
* client's row count one ahead of the daemon's for the rest of the conversation, and those counts are
|
|
446
446
|
* what a fork copies a prefix of and a rewind addresses.
|
|
447
447
|
*
|
|
448
448
|
* `text` is what the user typed, never the composed prompt: the editor-context and attachment notes the
|
|
449
449
|
* route wraps around it are protocol, and redrawing them as the user's words is the same lie the stored
|
|
450
450
|
* prompt is unwrapped to avoid. `attachments` are workspace-relative, like the turn's own. `sentAt` is the
|
|
451
|
-
* instant the turn took the message, carried so the bubble wears the same clock live and after a reopen
|
|
451
|
+
* instant the turn took the message, carried so the bubble wears the same clock live and after a reopen,
|
|
452
452
|
* a turn's own user row is stamped from the daemon's clock too, and a live bubble stamped from the
|
|
453
453
|
* browser's would visibly jump when the record replaced it. */
|
|
454
454
|
z.object({ kind: z.literal("steer"), text: z.string(), sentAt: z.number(), attachments: z.array(z.string()).optional() }),
|
|
455
455
|
z.object({ kind: z.literal("delta"), text: z.string(), parentToolUseId: z.string().optional() }),
|
|
456
456
|
// The prose block the `delta` frames were writing is finished. A turn emits several: the model says what
|
|
457
|
-
// it is about to do, runs tools, reports what it found, runs more, then summarizes
|
|
457
|
+
// it is about to do, runs tools, reports what it found, runs more, then summarizes, each a separate text
|
|
458
458
|
// block in the SDK stream. Without this boundary the client has no way to tell them apart and glues the
|
|
459
459
|
// whole turn's narration into one paragraph run, so the client retires its current bubble here and lets
|
|
460
460
|
// what follows (the tool calls this block introduced, or the next block of prose) open a fresh one.
|
|
461
461
|
z.object({ kind: z.literal("text_end"), parentToolUseId: z.string().optional() }),
|
|
462
462
|
z.object({ kind: z.literal("thinking"), text: z.string(), parentToolUseId: z.string().optional() }),
|
|
463
463
|
// A tool call starting (or, for backends that only report completions, arriving whole). `content` carries
|
|
464
|
-
// structured output known at call time
|
|
464
|
+
// structured output known at call time, an Edit's diff is derived from its input, no result needed.
|
|
465
465
|
z.object({
|
|
466
466
|
kind: z.literal("tool_call"),
|
|
467
467
|
id: z.string(),
|
|
@@ -474,7 +474,7 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
474
474
|
parentToolUseId: z.string().optional(),
|
|
475
475
|
}),
|
|
476
476
|
// A later state of a tool call, correlated by `id`. N updates per call: status transitions and/or fresh
|
|
477
|
-
// content/locations
|
|
477
|
+
// content/locations, both REPLACE the prior value (snapshot semantics, not append); absent ⇒ unchanged.
|
|
478
478
|
z.object({
|
|
479
479
|
kind: z.literal("tool_call_update"),
|
|
480
480
|
id: z.string(),
|
|
@@ -482,20 +482,20 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
482
482
|
content: z.array(ToolCallContentSchema).optional(),
|
|
483
483
|
locations: z.array(ToolCallLocationSchema).optional(),
|
|
484
484
|
}),
|
|
485
|
-
// The agent just started running Bash in its live `agent-<id>` tmux session
|
|
485
|
+
// The agent just started running Bash in its live `agent-<id>` tmux session, the client surfaces that
|
|
486
486
|
// terminal in the global panel. One per turn (the session is reused across a turn's commands, incl. subagents').
|
|
487
487
|
z.object({ kind: z.literal("terminal"), session: z.string() }),
|
|
488
|
-
// The agent just used a browser tool
|
|
488
|
+
// The agent just used a browser tool, its Chromium is coming up (or already is) behind a watchable
|
|
489
489
|
// `browser-<id>` session, and the client surfaces it in the same panel as the terminals. One per turn, for
|
|
490
490
|
// the same reason: one browser serves every browser call the turn makes.
|
|
491
491
|
z.object({ kind: z.literal("browser"), session: z.string() }),
|
|
492
|
-
/* THE AGENT STARTED ANOTHER AGENT
|
|
492
|
+
/* THE AGENT STARTED ANOTHER AGENT, an Agent/Task subagent, or a Codex/Grok CLI it drove from its own Bash
|
|
493
493
|
* (see SubagentSessionSchema). One `subagent` frame per child, then `subagent_update` as it works: the same
|
|
494
|
-
* call/update pair `tool_call`/`tool_call_update` uses, and for the same reason
|
|
494
|
+
* call/update pair `tool_call`/`tool_call_update` uses, and for the same reason, the fields that move
|
|
495
495
|
* (status, spend, what it is doing) arrive many times and must REPLACE, while the fields that identify it are
|
|
496
496
|
* said once.
|
|
497
497
|
*
|
|
498
|
-
* `id` is the SPAWNING TOOL CALL's id
|
|
498
|
+
* `id` is the SPAWNING TOOL CALL's id, the same id the client already nests the child's inner frames under
|
|
499
499
|
* (`parentToolUseId`), so both frames land on the card that spawned the child by the lookup that is already
|
|
500
500
|
* there (mapToolAnywhere). No second correlation, and nothing to get wrong.
|
|
501
501
|
*
|
|
@@ -510,7 +510,7 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
510
510
|
description: z.string().optional(),
|
|
511
511
|
model: z.string().optional(),
|
|
512
512
|
background: z.boolean().optional(),
|
|
513
|
-
// A delegation's tmux session
|
|
513
|
+
// A delegation's tmux session, the one live view a subagent doesn't have (SubagentSessionSchema).
|
|
514
514
|
terminal: z.string().optional(),
|
|
515
515
|
}),
|
|
516
516
|
z.object({
|
|
@@ -524,18 +524,18 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
524
524
|
error: z.string().optional(),
|
|
525
525
|
}),
|
|
526
526
|
z.object({ kind: z.literal("todos"), items: z.array(TodoItemSchema) }),
|
|
527
|
-
// The provider's own slash commands (ACP available_commands_update), replaced whole each time
|
|
527
|
+
// The provider's own slash commands (ACP available_commands_update), replaced whole each time, the
|
|
528
528
|
// composer's `/` popover lists them; invoking one is plain `/name …` prompt text (the ACP convention).
|
|
529
529
|
z.object({ kind: z.literal("commands"), items: z.array(AgentCommandSchema) }),
|
|
530
530
|
z.object({
|
|
531
531
|
kind: z.literal("usage"),
|
|
532
|
-
// The account that served this turn
|
|
532
|
+
// The account that served this turn, the client attributes the totals to it (tagged by streamAgent).
|
|
533
533
|
account: z.string().optional(),
|
|
534
534
|
costUsd: z.number().optional(),
|
|
535
535
|
inputTokens: z.number().optional(),
|
|
536
536
|
outputTokens: z.number().optional(),
|
|
537
537
|
// Provider prompt-cache buckets for the turn: tokens served from cache (read) and written to cache
|
|
538
|
-
// (creation). Optional per provider
|
|
538
|
+
// (creation). Optional per provider. Codex reports only cached input (read); runtimes/turns that
|
|
539
539
|
// don't report a bucket omit it. Lets the client show cache hit rate = read / (read + input).
|
|
540
540
|
cacheReadTokens: z.number().optional(),
|
|
541
541
|
cacheCreationTokens: z.number().optional(),
|
|
@@ -543,14 +543,14 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
543
543
|
numTurns: z.number().optional(),
|
|
544
544
|
}),
|
|
545
545
|
// The live gate: the provider's answer to "may this turn run", pushed mid-turn. Drives the rate-limited
|
|
546
|
-
// notice, not the headroom readouts
|
|
546
|
+
// notice, not the headroom readouts, see RateLimitInfoSchema.
|
|
547
547
|
RateLimitInfoSchema.extend({ kind: z.literal("rate_limit_info"), account: z.string().optional() }),
|
|
548
548
|
/* WHAT SPEED THIS TURN ACTUALLY RAN AT, and when it isn't the one asked for, why. Emitted only when the
|
|
549
549
|
* answer CHANGES within a turn, so the ordinary case is one frame at init and nothing after it; a turn that
|
|
550
550
|
* enters cooldown mid-flight (fast mode has its own rate-limit pool, separate from the model's) emits a
|
|
551
551
|
* second.
|
|
552
552
|
*
|
|
553
|
-
* This frame exists because fast mode fails SILENTLY and for a lot of different reasons
|
|
553
|
+
* This frame exists because fast mode fails SILENTLY and for a lot of different reasons, the plan is free,
|
|
554
554
|
* extra usage is off, the model doesn't offer it, the turn is routed through the translator and so isn't
|
|
555
555
|
* first-party, an env var disables it, the pool is in cooldown. Asking for it and getting standard speed is
|
|
556
556
|
* indistinguishable, from the outside, from asking for it and getting it: same frames, same text, a bill
|
|
@@ -564,19 +564,19 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
564
564
|
z.object({
|
|
565
565
|
kind: z.literal("fast_mode"),
|
|
566
566
|
state: FastModeStateSchema,
|
|
567
|
-
// Absent when nothing is blocking fast mode
|
|
567
|
+
// Absent when nothing is blocking fast mode, including on `state: "on"`, and on an `off` that simply
|
|
568
568
|
// wasn't asked for.
|
|
569
569
|
reason: z.string().optional(),
|
|
570
570
|
}),
|
|
571
571
|
/* The turn is alive but WAITING on the provider: a request failed transiently (5xx, 529, a dropped socket)
|
|
572
|
-
* and the harness is retrying it inside this same turn. A status, not a failure
|
|
572
|
+
* and the harness is retrying it inside this same turn. A status, not a failure, nothing has been lost and
|
|
573
573
|
* the turn may still finish normally, so the client renders it where "thinking" goes rather than in the
|
|
574
574
|
* transcript.
|
|
575
575
|
*
|
|
576
576
|
* It exists because the retry budget is deliberately long (see CLAUDE_CODE_RETRY_WATCHDOG in
|
|
577
577
|
* harness-credentials.ts): a turn can now sit silent for minutes riding out an outage, and silence reads as
|
|
578
578
|
* a hang. The one action a user takes against an apparent hang is Stop, which is the only action that
|
|
579
|
-
* actually loses the work
|
|
579
|
+
* actually loses the work, so the wait has to be visible, with its own next-attempt clock.
|
|
580
580
|
*
|
|
581
581
|
* `attempt`/`maxAttempts` are the harness's own counters; `nextAttemptAt` (epoch ms) is when it will try
|
|
582
582
|
* again, so the readout counts down instead of freezing on a number nobody can interpret. BOTH are optional
|
|
@@ -590,7 +590,7 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
590
590
|
maxAttempts: z.number().optional(),
|
|
591
591
|
nextAttemptAt: z.number().optional(),
|
|
592
592
|
// The HTTP status behind it when there was one (529 reads as capacity, 429 as a rate limit, 500 as a
|
|
593
|
-
// fault
|
|
593
|
+
// fault, the client says which). Absent for a transport failure that never got a response, and for a
|
|
594
594
|
// runtime that reports the refusal as prose rather than a code (grok-agent.ts reads it back off that).
|
|
595
595
|
status: z.number().optional(),
|
|
596
596
|
}),
|
|
@@ -606,7 +606,7 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
606
606
|
QuestionCardSchema,
|
|
607
607
|
PermissionCardSchema,
|
|
608
608
|
// The agent's browser needs a person: it parked mid-sign-in on something it cannot clear itself (a captcha,
|
|
609
|
-
// a password it does not hold, a phone check). `session` names the browser session on /browsers
|
|
609
|
+
// a password it does not hold, a phone check). `session` names the browser session on /browsers, the card's
|
|
610
610
|
// one action is going THERE, where the live stage and Take control already are; the Browsers banner and this
|
|
611
611
|
// card resolve the same requestId. `account` is the capability the sign-in is for, so the card can say whose
|
|
612
612
|
// login is stuck even after the browser has navigated somewhere unrecognizable.
|
|
@@ -619,7 +619,7 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
619
619
|
}),
|
|
620
620
|
// The agent's TERMINAL needs a person: a command it started is sitting at a prompt it cannot answer (a
|
|
621
621
|
// one-time password, a security-key touch, a confirm). `session` names the tmux session on the terminal
|
|
622
|
-
// panel
|
|
622
|
+
// panel, the card's one action is going THERE, where the live pane and its prompt already are, which is
|
|
623
623
|
// the same division of labour the browser card has with /browsers. Not journalled for restore, and for the
|
|
624
624
|
// browser card's reason one door along: the pane holding the prompt belongs to a process the restart kills.
|
|
625
625
|
z.object({
|
|
@@ -628,21 +628,21 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
628
628
|
session: z.string(),
|
|
629
629
|
message: z.string(),
|
|
630
630
|
}),
|
|
631
|
-
/* A premium service run awaiting the owner's click. Raised OUTSIDE the turn generator
|
|
631
|
+
/* A premium service run awaiting the owner's click. Raised OUTSIDE the turn generator, the daemon's
|
|
632
632
|
* services route parks the agent's own `services run` call and pushes this frame into the live run
|
|
633
|
-
* (platform/service-offer.ts)
|
|
633
|
+
* (platform/service-offer.ts), so unlike the four cards above it is not journalled for restore: its
|
|
634
634
|
* waiter is the CLI's held connection, which dies with the daemon, and a restored card would offer
|
|
635
635
|
* buttons nothing is waiting behind. Settles through the same `POST /agent/reply` as every other card. */
|
|
636
636
|
z.object({ kind: z.literal("service_offer"), requestId: z.string(), offer: ServiceOfferSchema }),
|
|
637
637
|
/* One event off an approved run's stream, pushed as the provider emits it so the settled card shows the
|
|
638
638
|
* run living rather than a spinner of unknowable length. Today that is `status` lines; `result` stays off
|
|
639
|
-
* the transcript on purpose (it is the agent's answer to act on, not the card's to duplicate)
|
|
639
|
+
* the transcript on purpose (it is the agent's answer to act on, not the card's to duplicate), the frame
|
|
640
640
|
* carries the whole union so richer event kinds land here without a contract break. */
|
|
641
641
|
z.object({ kind: z.literal("service_event"), requestId: z.string(), event: ServiceStreamEventSchema }),
|
|
642
642
|
/* How an approved run ended, pushed after the platform answered so the card can settle as a receipt
|
|
643
643
|
* rather than a promise: `ok` served and charged, `refunded` failed to answer and charged nothing,
|
|
644
644
|
* `refused` the platform said no after the click (a raced-out allowance). `remaining` is the meter after,
|
|
645
|
-
* when the platform stated one. Skip needs no receipt
|
|
645
|
+
* when the platform stated one. Skip needs no receipt, nothing happened, and `resolved` already says so. */
|
|
646
646
|
z.object({
|
|
647
647
|
kind: z.literal("service_receipt"),
|
|
648
648
|
requestId: z.string(),
|
|
@@ -650,17 +650,17 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
650
650
|
credits: z.number(),
|
|
651
651
|
remaining: z.number().optional(),
|
|
652
652
|
}),
|
|
653
|
-
/* A missing capability asking for the owner's setup
|
|
653
|
+
/* A missing capability asking for the owner's setup, the agent hit something this sandbox is not
|
|
654
654
|
* connected to and raised the card instead of describing manual steps. Raised OUTSIDE the turn generator
|
|
655
655
|
* exactly like the service offer above (the daemon's ask route parks the agent's `capabilities request`
|
|
656
656
|
* call and pushes this frame into the live run; capabilities/capability-offer.ts), so it is not
|
|
657
657
|
* journalled for restore either: its waiter is the CLI's held connection, which dies with the daemon.
|
|
658
658
|
* Settles through the same `POST /agent/reply` as every other card. */
|
|
659
659
|
z.object({ kind: z.literal("capability_offer"), requestId: z.string(), offer: CapabilityOfferSchema }),
|
|
660
|
-
/* How an accepted ask ended, pushed once the daemon stops watching for the connection: `connected
|
|
660
|
+
/* How an accepted ask ended, pushed once the daemon stops watching for the connection: `connected`, the
|
|
661
661
|
* capability came live while the agent waited (`id` is the connected instance, the agent's handle for it)
|
|
662
|
-
|
|
663
|
-
* asking command died). A skip needs no outcome frame
|
|
662
|
+
*, or `unfinished`, the setup did not complete while anyone was waiting (the deadline passed, or the
|
|
663
|
+
* asking command died). A skip needs no outcome frame, nothing was set up, and `resolved` already says
|
|
664
664
|
* so. It is what settles the card's "waiting for you to finish setup" state on every surface. */
|
|
665
665
|
z.object({
|
|
666
666
|
kind: z.literal("capability_outcome"),
|
|
@@ -674,9 +674,9 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
674
674
|
* CLI's held connection, which dies with the daemon. Settles through the same `POST /agent/reply`. */
|
|
675
675
|
z.object({ kind: z.literal("payment_offer"), requestId: z.string(), offer: PaymentOfferSchema }),
|
|
676
676
|
/* How an approved (or auto-approved) payment ended, pushed after the endpoint answered so the card can
|
|
677
|
-
* settle as a receipt rather than a promise: `paid
|
|
678
|
-
* the onchain hash when it stated one); `failed
|
|
679
|
-
* case the signed authorization expires unused and NOTHING left the wallet. A skip needs no receipt
|
|
677
|
+
* settle as a receipt rather than a promise: `paid`, the endpoint confirmed settlement (`transaction` is
|
|
678
|
+
* the onchain hash when it stated one); `failed`, the payment was refused or settlement failed, in which
|
|
679
|
+
* case the signed authorization expires unused and NOTHING left the wallet. A skip needs no receipt,
|
|
680
680
|
* nothing moved, and `resolved` already says so. */
|
|
681
681
|
z.object({
|
|
682
682
|
kind: z.literal("payment_receipt"),
|
|
@@ -686,22 +686,22 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
686
686
|
transaction: z.string().optional(),
|
|
687
687
|
network: z.string().optional(),
|
|
688
688
|
}),
|
|
689
|
-
// The card above named by `requestId` is released
|
|
689
|
+
// The card above named by `requestId` is released, the user answered (or dismissed it, or the turn was
|
|
690
690
|
// stopped out from under it), so the turn is executing again. Emitted by whoever parked, the moment its
|
|
691
691
|
// waiter settles, because the park's END is otherwise invisible on this stream: nothing else here says
|
|
692
692
|
// "that card is done", and it cannot be inferred from the next frame that happens along. Frames DO arrive
|
|
693
|
-
// while a turn is parked
|
|
693
|
+
// while a turn is parked, the pausing tool's own `tool_call` regularly trails its card (the SDK queues
|
|
694
694
|
// stream messages while dispatching an in-process MCP tool straight off the transport), and a card raised
|
|
695
695
|
// beside a parallel tool call sits through that tool's whole life. See agents-registry.ts, which reads
|
|
696
696
|
// this pair as the fleet's "needs you" state.
|
|
697
697
|
//
|
|
698
698
|
// `reply` says HOW it settled, and is what a transcript rebuilt from this log freezes the card with: a
|
|
699
699
|
// reload replays the run from seq 0 and a second window renders it live, so both would otherwise restore
|
|
700
|
-
// the card pending
|
|
700
|
+
// the card pending, offering buttons on a requestId nothing holds any more, under a transcript that has
|
|
701
701
|
// already moved on. It rides verbatim, exactly as the client POSTed it; absent, nobody answered (the turn
|
|
702
702
|
// was stopped, or died under the card), which is not a decision and must not replay as one.
|
|
703
703
|
z.object({ kind: z.literal("resolved"), requestId: z.string(), reply: AgentReplySchema.optional() }),
|
|
704
|
-
// The turn's permission mode, whenever it changes
|
|
704
|
+
// The turn's permission mode, whenever it changes, the user's pick at turn start, then every move the
|
|
705
705
|
// AGENT makes on its own (EnterPlanMode on a request that needs thinking through, ExitPlanMode once the
|
|
706
706
|
// user approves). The composer's mode selector follows this, so the UI never lies about the live posture.
|
|
707
707
|
z.object({ kind: z.literal("mode"), mode: PermissionModeSchema }),
|
|
@@ -714,31 +714,31 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
714
714
|
.enum([
|
|
715
715
|
"session-not-found",
|
|
716
716
|
"rate_limit",
|
|
717
|
-
// Codex ran the turn but warned about it (fallback model metadata)
|
|
717
|
+
// Codex ran the turn but warned about it (fallback model metadata), a notice, not a failure.
|
|
718
718
|
"codex-advisory",
|
|
719
719
|
"codex-reauth",
|
|
720
720
|
// The Claude subscription credential is dead (revoked, or its refresh token rejected) and only a
|
|
721
721
|
// reconnect fixes it. Distinct from "no account connected": the account IS there, so the UI can
|
|
722
722
|
// offer reconnect where the user already is and replay the message that bounced.
|
|
723
723
|
"claude-reauth",
|
|
724
|
-
// The API refused this turn's token MID-FLIGHT
|
|
724
|
+
// The API refused this turn's token MID-FLIGHT, nearly always one superseded by a rotation,
|
|
725
725
|
// which Anthropic retires the moment its successor is minted. Distinct from claude-reauth: the
|
|
726
726
|
// account is fine and the daemon re-mints on the spot, so this is usually a notice about a turn
|
|
727
727
|
// that is coming back rather than a request for the user to do anything. `autoResume` says
|
|
728
728
|
// which of the two: "scheduled" means the re-mint-and-re-run is armed, and its absence means
|
|
729
729
|
// nothing is coming (the turn was already a resume, or it ran on a credential with nothing to
|
|
730
|
-
// re-mint from)
|
|
730
|
+
// re-mint from), that is the case where reconnecting really is the fix.
|
|
731
731
|
"claude-token-refused",
|
|
732
|
-
/* THE ACCOUNT IS FINE AND STILL NOT ALLOWED TO RUN
|
|
732
|
+
/* THE ACCOUNT IS FINE AND STILL NOT ALLOWED TO RUN, an Anthropic organization that has turned
|
|
733
733
|
* Claude Code off for this seat. The token authenticates, the plan's own usage endpoint answers
|
|
734
734
|
* with real pools, and every turn is refused anyway, which is why it is its own code rather than
|
|
735
735
|
* a member of either neighbour: a spent allowance comes back on a clock and a refused credential
|
|
736
736
|
* comes back on a re-mint, and NEITHER of those is true here. Only an admin re-enabling access
|
|
737
|
-
* is, so nothing is re-run and nothing asks the user to reconnect
|
|
737
|
+
* is, so nothing is re-run and nothing asks the user to reconnect, the one recovery that looks
|
|
738
738
|
* plausible and is guaranteed to waste their time. */
|
|
739
739
|
"claude-not-entitled",
|
|
740
740
|
// The model provider itself failed transiently — 500/502/503, a 529 at capacity, a dropped
|
|
741
|
-
// socket
|
|
741
|
+
// socket, and the harness's own in-turn retries did not outlast it. Nothing about the workspace
|
|
742
742
|
// or the request is wrong, so the daemon remembers the turn and re-runs it on an escalating
|
|
743
743
|
// backoff (provider-health.ts): the frame is a notice about a turn that is coming back, and
|
|
744
744
|
// reaches the client as a plain failure only once the attempts are spent.
|
|
@@ -751,7 +751,7 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
751
751
|
// This account's platform-owned daily trial allowance is spent until its UTC reset.
|
|
752
752
|
"trial-exhausted",
|
|
753
753
|
// The harness read the message as a slash command it doesn't have, and discarded everything
|
|
754
|
-
// after the name
|
|
754
|
+
// after the name, the model never saw the message. Nothing was processed, so the client holds
|
|
755
755
|
// the text back instead of leaving the user to retype it (same treatment as claude-reauth).
|
|
756
756
|
"unknown-command",
|
|
757
757
|
"grok-model-invalid",
|
|
@@ -768,14 +768,14 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
768
768
|
// claude-token-refused). "scheduled" = the resume is armed and this turn comes back by itself;
|
|
769
769
|
// "available" = the daemon remembered the failed turn and arming THIS conversation
|
|
770
770
|
// (AgentSummarySchema.resumeAfterOutage) picks up that same resume, which is what the chat's offer
|
|
771
|
-
// banner hangs off
|
|
771
|
+
// banner hangs off, outage only, since a renewal is never gated on a posture at all. The two words
|
|
772
772
|
// are read against the effective posture (the conversation's override, else the sandbox default), so a
|
|
773
773
|
// chat armed on its own says "scheduled" while the unarmed board around it says "available".
|
|
774
774
|
// Absent means there is nothing automatic to resume: a spent usage limit never has one,
|
|
775
775
|
// and a refused credential has none once re-minting it has already been tried and failed.
|
|
776
776
|
autoResume: z.enum(["scheduled", "available"]).optional(),
|
|
777
777
|
/* provider-outage only: the shape of the wait. `retryAt` (epoch seconds) is when the next attempt is
|
|
778
|
-
* due
|
|
778
|
+
* due, not a fixed cadence, because an outage has no reset instant to aim at and hammering a provider
|
|
779
779
|
* that is down only spends tokens on refusals, so each attempt waits longer than the last
|
|
780
780
|
* (provider-health.ts owns the schedule).
|
|
781
781
|
*
|
|
@@ -789,11 +789,11 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
789
789
|
export type AgentEvent = z.infer<typeof AgentEventSchema>;
|
|
790
790
|
|
|
791
791
|
// The /agent/attach stream: a head frame identifying the run, then its AgentEvents stamped with their 1-based
|
|
792
|
-
// seq (the client's resume cursor), then `end` when the run is over
|
|
792
|
+
// seq (the client's resume cursor), then `end` when the run is over, every frame delivered, nothing more
|
|
793
793
|
// coming. A stream that closes WITHOUT `end` was dropped mid-run; the client re-attaches with `after` = the
|
|
794
794
|
// last seq it holds. The head's `prompt`/`startedAt` let a window that didn't initiate the turn (a reload, a
|
|
795
795
|
// second window, another device) synthesize the user bubble and the elapsed readout; its `seq` is the log
|
|
796
|
-
// length at attach time
|
|
796
|
+
// length at attach time, the replay/live boundary.
|
|
797
797
|
export const AttachFrameSchema = z.discriminatedUnion("kind", [
|
|
798
798
|
z.object({ kind: z.literal("attached"), run: z.string(), prompt: z.string(), startedAt: z.number(), seq: z.number() }),
|
|
799
799
|
z.object({ kind: z.literal("frame"), seq: z.number(), event: AgentEventSchema }),
|
|
@@ -806,10 +806,10 @@ export type AttachFrame = z.infer<typeof AttachFrameSchema>;
|
|
|
806
806
|
*
|
|
807
807
|
* They live on the wire rather than in the daemon because the CLIENT has to recognise them too: an attach head
|
|
808
808
|
* carries the run's prompt verbatim, and a window joining a resumed run would otherwise render the note as a
|
|
809
|
-
* message the USER wrote
|
|
809
|
+
* message the USER wrote, the same words the user already said one run up, with a machine's preamble on them.
|
|
810
810
|
* Recognising the prefix is what lets that window reuse the bubble that is already there instead. */
|
|
811
811
|
// The instruction the three whole-turn re-runs share: what follows the note is the original request, repeated.
|
|
812
|
-
// `answered` deliberately does not carry it
|
|
812
|
+
// `answered` deliberately does not carry it, what follows THAT note is not a repetition but the user's answer,
|
|
813
813
|
// and telling the model to "continue from that point instead of starting over" about words it has never seen
|
|
814
814
|
// is how a resume reads as the user contradicting themselves.
|
|
815
815
|
const REPEATED =
|
|
@@ -818,7 +818,7 @@ export const RESUME_NOTES = {
|
|
|
818
818
|
auth: `The Claude credential that interrupted this conversation has been renewed, and this turn resumed automatically. ${REPEATED}`,
|
|
819
819
|
outage: `The model provider was briefly unavailable and interrupted this conversation; this turn resumed automatically. ${REPEATED}`,
|
|
820
820
|
restart: `The sandbox restarted while this turn was running, which stopped it, and this turn resumed automatically once it came back. ${REPEATED}`,
|
|
821
|
-
// A turn that was PARKED on the user when the daemon died: nothing re-runs at boot
|
|
821
|
+
// A turn that was PARKED on the user when the daemon died: nothing re-runs at boot, the card is restored
|
|
822
822
|
// instead, and this is the turn their answer starts (turn-resume.ts). What rides below the note is the
|
|
823
823
|
// answer itself, so the model picks the session back up at exactly the decision it had handed over.
|
|
824
824
|
answered:
|
|
@@ -830,7 +830,7 @@ export const RESUME_NOTES = {
|
|
|
830
830
|
export const withResumeNote = (prompt: string, note: string): string =>
|
|
831
831
|
Object.values(RESUME_NOTES).some((known) => prompt.startsWith(known)) ? prompt : `${note}\n\n${prompt}`;
|
|
832
832
|
|
|
833
|
-
// The user's own words inside a resumed prompt
|
|
833
|
+
// The user's own words inside a resumed prompt, the note and its explanation stripped back off. Returns the
|
|
834
834
|
// prompt unchanged when it is not a resume, so a caller can hand every attach head through it.
|
|
835
835
|
export const withoutResumeNote = (prompt: string): string => {
|
|
836
836
|
const note = Object.values(RESUME_NOTES).find((known) => prompt.startsWith(known));
|
|
@@ -839,22 +839,22 @@ export const withoutResumeNote = (prompt: string): string => {
|
|
|
839
839
|
|
|
840
840
|
export type ResumeReason = keyof typeof RESUME_NOTES;
|
|
841
841
|
|
|
842
|
-
/* HOW A RESUMED TURN READS TO THE PERSON
|
|
842
|
+
/* HOW A RESUMED TURN READS TO THE PERSON, the same interruption the note above tells the model, said in the
|
|
843
843
|
* transcript's own voice instead.
|
|
844
844
|
*
|
|
845
845
|
* Stripping the note out of the user's words is only half the job, and for years it was the only half anyone
|
|
846
846
|
* did: what a reopened conversation showed was a paragraph of machine prose stapled to the front of a message
|
|
847
|
-
* the user had already sent once, directly under their own copy of it. Both halves of that are wrong
|
|
847
|
+
* the user had already sent once, directly under their own copy of it. Both halves of that are wrong, it was
|
|
848
848
|
* never their sentence, and the words under it are a REPEAT rather than something new they said.
|
|
849
849
|
*
|
|
850
850
|
* So the two shapes below, which is the whole of what a reader has to be told:
|
|
851
851
|
*
|
|
852
|
-
* `notice
|
|
852
|
+
* `notice`, the three whole-turn re-runs. The words under the note are already in the transcript one turn up,
|
|
853
853
|
* so the repeat is dropped entirely and the interruption takes its place as a muted line, sitting with the
|
|
854
854
|
* failure line it resolves ("Failed to authenticate…") and reading like every other thing that HAPPENED to a
|
|
855
855
|
* turn rather than like something anybody typed.
|
|
856
856
|
*
|
|
857
|
-
* `note
|
|
857
|
+
* `note`, the answered case, where what rides under the note is the user's actual answer to a card and belongs
|
|
858
858
|
* in the transcript as their words. Nothing is dropped; the explanation rides that message as a collapsed row,
|
|
859
859
|
* the same disclosure every other daemon-written note gets (TurnNote). */
|
|
860
860
|
export type ResumeDisclosure = { readonly kind: "notice"; readonly text: string } | { readonly kind: "note"; readonly note: TurnNote };
|
|
@@ -874,7 +874,7 @@ export const resumeDisclosure = (prompt: string): ResumeDisclosure | undefined =
|
|
|
874
874
|
};
|
|
875
875
|
|
|
876
876
|
// One parsed line from `intentic … --output ndjson` (engine events, provider `log`, the terminal `result`).
|
|
877
|
-
// Open-ended by design
|
|
877
|
+
// Open-ended by design, the sandbox consumes the wire shape, not @intentic/engine's types, so a string
|
|
878
878
|
// `kind` plus arbitrary extra fields pass through. The apply-events tail (intentic.contract `applyEvents`) rides
|
|
879
879
|
// this same loose shape with three daemon/CLI-minted sentinel kinds alongside the engine ones: {kind:"start"}
|
|
880
880
|
// (first line, written when the run's file is reset), {kind:"exit",code} (last line, on the CLI process exit),
|
|
@@ -888,7 +888,7 @@ export const HeartbeatSchema = z.object({ kind: z.literal("heartbeat") });
|
|
|
888
888
|
export type Heartbeat = z.infer<typeof HeartbeatSchema>;
|
|
889
889
|
|
|
890
890
|
// One step of the daemon's boot chain. `key` is the stable id the daemon declares it under, `label` the words
|
|
891
|
-
// the browser shows. A step that FAILED is still a step that finished
|
|
891
|
+
// the browser shows. A step that FAILED is still a step that finished, the boot chain is log-and-continue by
|
|
892
892
|
// design (see main.ts), so a failure degrades one subsystem rather than holding the gate closed forever.
|
|
893
893
|
export const BootStepSchema = z.object({
|
|
894
894
|
key: z.string(),
|
|
@@ -900,13 +900,13 @@ export const BootStepSchema = z.object({
|
|
|
900
900
|
export type BootStep = z.infer<typeof BootStepSchema>;
|
|
901
901
|
|
|
902
902
|
/* WHERE THE DAEMON IS IN ITS BOOT. The listeners come up before the state they serve has converged (main.ts:
|
|
903
|
-
* "listen first, converge behind the gate"), which is what stops a restart from reading as an outage
|
|
903
|
+
* "listen first, converge behind the gate"), which is what stops a restart from reading as an outage, but it
|
|
904
904
|
* also means the daemon spends the first seconds of every boot both reachable and unable to answer, and until
|
|
905
905
|
* this frame existed the browser had no way to tell that apart from a healthy sandbox. It painted an operable
|
|
906
906
|
* workspace off its persisted cache and then parked every request the user made against the readiness gate.
|
|
907
907
|
*
|
|
908
908
|
* The step list is declared UP FRONT and sent whole, pending entries included, so the browser can say "4 of 11,
|
|
909
|
-
* loading the conversation registry" rather than "something is happening"
|
|
909
|
+
* loading the conversation registry" rather than "something is happening", a boot that takes minutes has one
|
|
910
910
|
* slow step, and naming it is the whole point. Snapshot-not-diff, like every other roster on this stream. */
|
|
911
911
|
export const BootProgressSchema = z.object({
|
|
912
912
|
// False only while the chain is still converging. The browser holds every daemon read until this is true.
|
|
@@ -923,26 +923,26 @@ export const BootSchema = z.object({ kind: z.literal("boot"), ...BootProgressSch
|
|
|
923
923
|
export type Boot = z.infer<typeof BootSchema>;
|
|
924
924
|
|
|
925
925
|
// The stream's first frame: the workspace's stable identity, minted at the first boot of an empty /work. The
|
|
926
|
-
// browser remembers it per sandbox id and drops that sandbox's persisted query cache when it changes
|
|
926
|
+
// browser remembers it per sandbox id and drops that sandbox's persisted query cache when it changes, a wiped
|
|
927
927
|
// and recreated workspace (cleanup.sh + reconnect keeps the same sandbox id) must not be painted from the
|
|
928
928
|
// previous workspace's cache. `build` is the same guard against a different axis: the daemon's own compiled
|
|
929
929
|
// tree, so an image update (or a `pnpm build:sandbox` swap in dev) drops what the browser cached from the
|
|
930
930
|
// PREVIOUS build instead of hydrating payloads the new one no longer shapes that way.
|
|
931
931
|
//
|
|
932
|
-
// It also advertises `routes
|
|
932
|
+
// It also advertises `routes`, the contract route names (`vpn.list`, `kimi.models`) this daemon actually
|
|
933
933
|
// implements, from ITS build of the contract. A browser is routinely newer than the daemon it talks to (a
|
|
934
934
|
// released app plane serves whatever image each user last pulled; in local dev the web app is always ahead of
|
|
935
|
-
// the last `pnpm build:sandbox`), and that stays fully supported
|
|
935
|
+
// the last `pnpm build:sandbox`), and that stays fully supported, the browser just compares the two sets so a
|
|
936
936
|
// route the daemon predates surfaces as a named, explained gap instead of a bare 404 nobody can attribute.
|
|
937
937
|
//
|
|
938
938
|
// `shapes` answers the half `routes` structurally cannot: a route BOTH builds have, whose payload changed
|
|
939
|
-
// between them. Names match, so nothing 404s
|
|
939
|
+
// between them. Names match, so nothing 404s, the call goes out and a field the browser expects is simply
|
|
940
940
|
// missing from the answer. It is a map of route name → a fingerprint of that route's input and output schema
|
|
941
941
|
// (see routes.ts), so a difference is a named route rather than "something, somewhere, moved". Beside `routes`
|
|
942
942
|
// rather than folded into it: existence covers every route, shape covers only the ones that can be expressed.
|
|
943
943
|
//
|
|
944
944
|
// Every added field is optional: a daemon built before one simply says nothing, and the browser's fallback is
|
|
945
|
-
// the pre-existing behaviour
|
|
945
|
+
// the pre-existing behaviour, routes all assumed present, shapes all assumed to agree, the daemon assumed
|
|
946
946
|
// ready, the cache left alone. That is also why `routes` keeps its bare-string-array shape: an image already in
|
|
947
947
|
// the wild sends exactly that, and a breaking change here would fail the hello frame's own parse and take the
|
|
948
948
|
// whole event stream down for precisely the skew this frame exists to describe.
|
|
@@ -956,25 +956,25 @@ export const HelloSchema = z.object({
|
|
|
956
956
|
});
|
|
957
957
|
export type Hello = z.infer<typeof HelloSchema>;
|
|
958
958
|
|
|
959
|
-
// The FULL discovered repo set (sorted root-relative ids), pushed whenever it changes
|
|
959
|
+
// The FULL discovered repo set (sorted root-relative ids), pushed whenever it changes, a clone, a scaffold,
|
|
960
960
|
// or a deleted repo re-frames it. The watcher descent-ignores .git, so no workspaceChanged path pattern can
|
|
961
961
|
// detect a repo appearing; the daemon diffs its own discovery instead. Snapshot-not-diff, last frame wins.
|
|
962
962
|
export const ReposChangedSchema = z.object({ kind: z.literal("reposChanged"), repos: z.array(z.string()) });
|
|
963
963
|
export type ReposChanged = z.infer<typeof ReposChangedSchema>;
|
|
964
964
|
|
|
965
965
|
// A batch of workspace paths that just changed on disk (created/edited/deleted), pushed on the same /events
|
|
966
|
-
// stream as the heartbeat so the browser refreshes the tree + any open file live
|
|
966
|
+
// stream as the heartbeat so the browser refreshes the tree + any open file live, the agent edits files
|
|
967
967
|
// out-of-band (its own Write/Edit/Bash tools), so there's no HTTP mutation to hang an invalidate on. Paths are
|
|
968
968
|
// root-relative, forward-slash (the tree/file route space). An empty array means "something changed, refetch the
|
|
969
|
-
// tree"
|
|
969
|
+
// tree", a burst too large to enumerate, or a reconnect recovery where we don't know what was missed.
|
|
970
970
|
export const WorkspaceChangedSchema = z.object({ kind: z.literal("workspaceChanged"), paths: z.array(z.string()) });
|
|
971
971
|
export type WorkspaceChanged = z.infer<typeof WorkspaceChangedSchema>;
|
|
972
972
|
|
|
973
|
-
/* THE REPOS WHOSE REFS JUST MOVED
|
|
973
|
+
/* THE REPOS WHOSE REFS JUST MOVED, a commit, a checkout, a branch or tag, a rebase started or aborted.
|
|
974
974
|
*
|
|
975
975
|
* A third push for the same reason as the two above, and the reason is structural: a repo's git dir does not
|
|
976
976
|
* live under /work at all (it is relocated onto /history so an isolated turn's worktree can stand in for the
|
|
977
|
-
* workspace root
|
|
977
|
+
* workspace root, see git/repo-git-dirs.ts), and the file watcher descent-ignores `.git` besides. So no
|
|
978
978
|
* `workspaceChanged` path can ever say "a ref moved", and a surface built on the commit graph would otherwise
|
|
979
979
|
* be exactly as fresh as the last thing the user clicked.
|
|
980
980
|
*
|
|
@@ -985,32 +985,32 @@ export type WorkspaceChanged = z.infer<typeof WorkspaceChangedSchema>;
|
|
|
985
985
|
export const RefsChangedSchema = z.object({ kind: z.literal("refsChanged"), repos: z.array(z.string()) });
|
|
986
986
|
export type RefsChanged = z.infer<typeof RefsChangedSchema>;
|
|
987
987
|
|
|
988
|
-
/* WHICH RUNNING THINGS JUST MOVED
|
|
988
|
+
/* WHICH RUNNING THINGS JUST MOVED, a session opened or exited, a dev server bound its port, a browser closed,
|
|
989
989
|
* a subagent reported in.
|
|
990
990
|
*
|
|
991
991
|
* The fourth push, and the one that covers what the other three structurally cannot: none of this state is on
|
|
992
992
|
* disk, so no `workspaceChanged` path can name it, and none of it is a ref or a repo. Before it, every view of a
|
|
993
|
-
* running thing polled on its own timer
|
|
993
|
+
* running thing polled on its own timer, which is to say each browser asked, forever, a question only the
|
|
994
994
|
* daemon could answer and almost always answered "no change".
|
|
995
995
|
*
|
|
996
996
|
* Diff-not-snapshot, and deliberately thin: the frame carries the DOMAIN that moved, never the roster itself.
|
|
997
997
|
* Invalidation only reaches a query something is observing, so a tab showing none of these pays a frame and no
|
|
998
|
-
* request
|
|
998
|
+
* request, whereas a roster on the wire would bill every connected browser the full list whether or not
|
|
999
999
|
* anything on screen renders it. Which query keys a domain stands for is runtime-state.ts's table. */
|
|
1000
1000
|
export const RuntimeChangedSchema = z.object({ kind: z.literal("runtimeChanged"), domains: z.array(z.string()) });
|
|
1001
1001
|
export type RuntimeChanged = z.infer<typeof RuntimeChangedSchema>;
|
|
1002
1002
|
|
|
1003
1003
|
// One connected browser tab of a sandbox member. Identity fields come from the caller's verified Google ID
|
|
1004
|
-
// token; activity fields from the tab's own /system/presence reports. No timestamps on the wire
|
|
1004
|
+
// token; activity fields from the tab's own /system/presence reports. No timestamps on the wire, an entry's
|
|
1005
1005
|
// lifetime IS its /events connection's lifetime, so there is nothing to age out or compare clocks over.
|
|
1006
1006
|
export const PresenceUserSchema = z.object({
|
|
1007
|
-
// Per-CONNECTION id, minted by the browser for each /events attempt
|
|
1007
|
+
// Per-CONNECTION id, minted by the browser for each /events attempt, never reused across reconnects.
|
|
1008
1008
|
clientId: z.string(),
|
|
1009
1009
|
email: z.string(),
|
|
1010
1010
|
name: z.string().optional(),
|
|
1011
1011
|
picture: z.string().optional(),
|
|
1012
1012
|
// The caller's trust tier, resolved by the authorizer at connection time. On the roster so every member
|
|
1013
|
-
// can see who may do what
|
|
1013
|
+
// can see who may do what, and so a tab knows its OWN role without an owner-only lookup.
|
|
1014
1014
|
role: MemberRoleSchema,
|
|
1015
1015
|
idle: z.boolean(),
|
|
1016
1016
|
// Route/view name the tab is on ("workspace", "automations", "ext:<id>/<key>", …).
|
|
@@ -1022,16 +1022,16 @@ export const PresenceUserSchema = z.object({
|
|
|
1022
1022
|
});
|
|
1023
1023
|
export type PresenceUser = z.infer<typeof PresenceUserSchema>;
|
|
1024
1024
|
|
|
1025
|
-
// The FULL roster of connected members, broadcast on every change
|
|
1025
|
+
// The FULL roster of connected members, broadcast on every change, snapshots, not diffs, so a reconnecting
|
|
1026
1026
|
// browser is consistent from its first frame and ordering never matters (last frame wins).
|
|
1027
1027
|
export const PresenceSchema = z.object({ kind: z.literal("presence"), users: z.array(PresenceUserSchema) });
|
|
1028
1028
|
export type Presence = z.infer<typeof PresenceSchema>;
|
|
1029
1029
|
|
|
1030
|
-
// The FULL fleet roster, broadcast on every registry change
|
|
1030
|
+
// The FULL fleet roster, broadcast on every registry change, same snapshot-not-diff contract as presence:
|
|
1031
1031
|
// a reconnecting browser is consistent from its first frame. NOT simply "last frame wins", though: `rev` is the
|
|
1032
1032
|
// registry revision the snapshot was taken at, and the browser applies a frame only if it is newer than the one
|
|
1033
|
-
// it already holds. Snapshots race two other sources of the same fact
|
|
1034
|
-
// browser's own optimistic writes
|
|
1033
|
+
// it already holds. Snapshots race two other sources of the same fact, an explicit GET /agents and the
|
|
1034
|
+
// browser's own optimistic writes, and an unordered full replace lets the slowest of them win, which is how an
|
|
1035
1035
|
// archived card came back. See AgentsListSchema and useAgents.ts.
|
|
1036
1036
|
export const AgentsSchema = z.object({ kind: z.literal("agents"), agents: z.array(AgentSummarySchema), rev: z.number() });
|
|
1037
1037
|
export type Agents = z.infer<typeof AgentsSchema>;
|