@intentic/sandbox-contract 1.166.0 → 1.167.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.
Files changed (43) hide show
  1. package/dist/agent-catalog.d.ts +0 -1
  2. package/dist/agent-catalog.d.ts.map +1 -1
  3. package/dist/agent-catalog.js +1 -2
  4. package/dist/agent-catalog.js.map +1 -1
  5. package/dist/contracts/agents.contract.d.ts +18 -0
  6. package/dist/contracts/agents.contract.d.ts.map +1 -1
  7. package/dist/contracts/agents.contract.js.map +1 -1
  8. package/dist/contracts/automations.contract.d.ts +1 -1
  9. package/dist/contracts/gate.contract.d.ts +1 -1
  10. package/dist/contracts/kimi.contract.d.ts +0 -70
  11. package/dist/contracts/kimi.contract.d.ts.map +1 -1
  12. package/dist/contracts/kimi.contract.js +1 -5
  13. package/dist/contracts/kimi.contract.js.map +1 -1
  14. package/dist/contracts/settings.contract.d.ts +29 -3
  15. package/dist/contracts/settings.contract.d.ts.map +1 -1
  16. package/dist/contracts/system.contract.d.ts +22 -4
  17. package/dist/contracts/system.contract.d.ts.map +1 -1
  18. package/dist/contracts/system.contract.js +3 -1
  19. package/dist/contracts/system.contract.js.map +1 -1
  20. package/dist/contracts/translator.contract.d.ts +11 -0
  21. package/dist/contracts/translator.contract.d.ts.map +1 -1
  22. package/dist/contracts/translator.contract.js.map +1 -1
  23. package/dist/events.d.ts +4 -0
  24. package/dist/events.d.ts.map +1 -1
  25. package/dist/events.js.map +1 -1
  26. package/dist/index.d.ts +82 -79
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/schemas.d.ts +102 -22
  29. package/dist/schemas.d.ts.map +1 -1
  30. package/dist/schemas.js +56 -13
  31. package/dist/schemas.js.map +1 -1
  32. package/package.json +2 -2
  33. package/src/agent-catalog.ts +6 -11
  34. package/src/contracts/agents.contract.ts +8 -6
  35. package/src/contracts/gate.contract.ts +2 -2
  36. package/src/contracts/kimi.contract.ts +4 -21
  37. package/src/contracts/system.contract.ts +9 -0
  38. package/src/contracts/translator.contract.ts +4 -6
  39. package/src/events.ts +2 -1
  40. package/src/model-order.ts +1 -1
  41. package/src/quick-model.test.ts +1 -1
  42. package/src/schemas.test.ts +10 -0
  43. package/src/schemas.ts +173 -67
package/src/schemas.ts CHANGED
@@ -50,8 +50,8 @@ export const EditorContextSchema = z.object({
50
50
  });
51
51
  export type EditorContext = z.infer<typeof EditorContextSchema>;
52
52
 
53
- // The client-minted stable conversation identity. Constrained because it lands in branch names (agent/<id>)
54
- // and filesystem paths — the regex is the injection guard. Shared by the turn input and the attach input.
53
+ // The client-minted stable conversation identity. Constrained because isolated conversations also use it in
54
+ // branch names (agent/<id>) and filesystem paths — the regex is the injection guard. Shared by turn + attach.
55
55
  const ConversationIdSchema = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/);
56
56
 
57
57
  // Where a conversation came from when nobody typed it into the browser: an automation wake carrying a message
@@ -102,7 +102,7 @@ export const AgentTurnSchema = z
102
102
  account: z.string().optional(),
103
103
  sessionId: z.string().optional(),
104
104
  // The client-minted stable conversation identity (survives provider/account/harness switches, which
105
- // retire sessions). Keys the fleet registry entry, the conversation's worktree, and the turn run.
105
+ // retire sessions). Keys the fleet registry entry and turn run, plus the worktree when isolated.
106
106
  conversationId: ConversationIdSchema.optional(),
107
107
  // When true, the turn runs in the conversation's isolated git worktree (created lazily on first use)
108
108
  // instead of the shared /work tree — the parallel-agents mode. Requires conversationId.
@@ -157,10 +157,10 @@ export const AttachTurnSchema = z.object({
157
157
  });
158
158
  export type AttachTurn = z.infer<typeof AttachTurnSchema>;
159
159
 
160
- // ---- agents: the parallel-conversation fleet ----
161
- // A "fleet agent" is a conversation with a registry entry every isolated conversation, keyed by its
162
- // conversationId. Isolated ones own a git worktree (branch agent/<id> in every workspace repo); the fleet
163
- // surface shows all of them with live status/activity/cost so the user can drive N agents in parallel.
160
+ // ---- agents: the conversation fleet ----
161
+ // A "fleet agent" is any conversation with a registry entry, keyed by its conversationId. Isolated ones own a
162
+ // git worktree (branch agent/<id> in every workspace repo); workspace conversations have no branch. The fleet
163
+ // surface shows both through the same status/activity/cost lifecycle.
164
164
 
165
165
  // idle/running/awaiting are the turn lifecycle (awaiting = paused on a plan approval or question); ready /
166
166
  // landed / conflict are outcomes of the land flow — `ready` is a clean completion whose delta stayed on the
@@ -173,7 +173,31 @@ export type AttachTurn = z.infer<typeof AttachTurnSchema>;
173
173
  // unlabelled: without it such a turn rehydrates as `idle`, which is the resting status of a turn that finished
174
174
  // CLEANLY, so the board files a killed agent under Finished and the question it was holding disappears with the
175
175
  // process that asked it. See agents-store.ts — this is the status a live turn leaves on disk.
176
- export const AgentStatusSchema = z.enum(["idle", "running", "awaiting", "ready", "landed", "conflict", "error", "interrupted"]);
176
+ //
177
+ /* `stopping` and `stopped` are the two halves of a user's Stop, and they exist because a hard-cancel is NOT
178
+ * instant: /agent/stop aborts the provider and then waits for the turn's generator to unwind (worktree and
179
+ * registry cleanup), which is seconds of real time. For that whole window the runtime half still said
180
+ * `running`, so every surface kept its spinner turning on a turn the user had already killed — and then the
181
+ * card jumped to a settled state out of nowhere. `stopping` is what the daemon knows the instant the abort
182
+ * lands, published immediately so the press has a visible result; `stopped` is where the turn comes to rest.
183
+ *
184
+ * `stopped` is deliberately its own value rather than `interrupted` or `error`. Not `error`, which is what a
185
+ * stopped turn used to report (every provider adapter surfaces the abort's unwind as an error frame) — a card
186
+ * accusing the user's own deliberate press of being a failure. Not `interrupted` either: that one means the
187
+ * daemon died under the turn, and a boot pass may re-run it, which is precisely what must never happen to a
188
+ * turn a person chose to end. */
189
+ export const AgentStatusSchema = z.enum([
190
+ "idle",
191
+ "running",
192
+ "awaiting",
193
+ "stopping",
194
+ "stopped",
195
+ "ready",
196
+ "landed",
197
+ "conflict",
198
+ "error",
199
+ "interrupted",
200
+ ]);
177
201
  export type AgentStatus = z.infer<typeof AgentStatusSchema>;
178
202
  // The card's live activity snippet: the last tool the agent used (with its target) and the in-progress todo.
179
203
  export const AgentActivitySchema = z.object({
@@ -355,9 +379,8 @@ export const AgentLandSchema = z.object({ id: z.string().min(1), mode: LandModeS
355
379
  // The providers whose model can run UNDER the Claude Code harness through the bundled translator (CLIProxyAPI),
356
380
  // which holds their SUBSCRIPTION OAuth and re-serves it behind an Anthropic endpoint. The `claude` provider is
357
381
  // absent — native Anthropic OAuth serves it directly, without the translator. Codex and Grok also have a native
358
- // runtime and so carry the harness axis; `gemini` is routed-only (Google publishes no Anthropic-protocol
359
- // endpoint and this sandbox bakes no Gemini runtime), so a Gemini turn is always a Claude Code turn.
360
- export const KeyedProviderSchema = z.enum(["codex", "grok", "gemini"]);
382
+ // runtime and so carry the harness axis; Kimi and Gemini are routed-only, so their turns always use Claude Code.
383
+ export const KeyedProviderSchema = z.enum(["codex", "grok", "kimi", "gemini"]);
361
384
  export type KeyedProvider = z.infer<typeof KeyedProviderSchema>;
362
385
 
363
386
  // One connected subscription in the translator. `name` is CLIProxyAPI's auth-file name — the stable store key a
@@ -371,6 +394,7 @@ export type TranslatorAccount = z.infer<typeof TranslatorAccountSchema>;
371
394
  export const TranslatorAccountsSchema = z.object({
372
395
  codex: z.array(TranslatorAccountSchema),
373
396
  grok: z.array(TranslatorAccountSchema),
397
+ kimi: z.array(TranslatorAccountSchema),
374
398
  gemini: z.array(TranslatorAccountSchema),
375
399
  });
376
400
  export type TranslatorAccounts = z.infer<typeof TranslatorAccountsSchema>;
@@ -525,11 +549,6 @@ export const OauthExchangeSchema = z.object({
525
549
  label: z.string().optional(),
526
550
  });
527
551
  export const AuthorizeChallengeSchema = z.object({ authorizeUrl: z.string(), verifier: z.string(), state: z.string() });
528
- // Kimi (Moonshot) authenticates with an API key, not OAuth: the user pastes a key from their Moonshot account
529
- // and the sandbox stores it as an account (one key per account, several accounts side by side). `label` is the
530
- // user's display name (blank ⇒ the daemon derives a default). The key never rides back out — connection status
531
- // is existence in `/kimi/accounts`.
532
- export const KimiConnectSchema = z.object({ apiKey: z.string().min(1), label: z.string().optional() });
533
552
  // xAI Grok (via OpenCode) uses subscription OAuth via the headless device-code method. `start` returns the
534
553
  // `url` the user opens (xAI's verification_uri_complete, which pre-fills the code) and `code` — the same
535
554
  // one-time code, surfaced so the card matches x.ai exactly. There is no paste-back: OpenCode polls to
@@ -539,13 +558,15 @@ export const KimiConnectSchema = z.object({ apiKey: z.string().min(1), label: z.
539
558
  // A device-code login start: the verification URL + the one-time code the user enters there. The native Grok
540
559
  // flow (via OpenCode) — see TranslatorStartSchema for the routed-provider connect, which adds `state`.
541
560
  export const DeviceStartSchema = z.object({ url: z.string(), code: z.string() });
542
- // A routed-provider subscription login start (codex/grok/gemini via CLIProxyAPI). Codex and Grok mint a
543
- // one-time device `code` the user enters at the provider's site, and CLIProxyAPI polls to completion on its
544
- // own the card only waits. Google publishes no device flow: the user approves in a browser and is redirected
545
- // to a loopback URL this sandbox never receives, so `code` is empty and the card asks them to paste that URL
546
- // back (see TranslatorCompleteSchema). Which half a provider uses is READ from the response rather than
547
- // hardcoded per provider, so the card needs no provider table.
548
- export const TranslatorStartSchema = z.object({ url: z.string(), code: z.string(), state: z.string() });
561
+ // A routed-provider subscription login start (codex/grok/kimi/gemini via CLIProxyAPI). Device flows poll to
562
+ // completion after the user approves upstream; redirect flows need the browser's landing URL pasted back. The
563
+ // explicit flow discriminator matters even when a provider's verification URL already embeds its optional code.
564
+ export const TranslatorStartSchema = z.object({
565
+ url: z.string(),
566
+ code: z.string(),
567
+ state: z.string(),
568
+ flow: z.enum(["device", "redirect"]),
569
+ });
549
570
  // The paste-back half of a redirect login: the URL the provider sent the browser to, carrying the grant as
550
571
  // ?code=&state=. `state` ties it to the handshake that issued it — the translator rejects a mismatch.
551
572
  export const TranslatorCompleteSchema = z.object({
@@ -555,7 +576,7 @@ export const TranslatorCompleteSchema = z.object({
555
576
  });
556
577
  // A provider's model catalog, resolved daemon-side from live discovery with a persisted last-known-good list and
557
578
  // a seed floor (Grok via opencode.ts xaiModels, Codex via codex-models.ts, Claude via the Agent SDK's
558
- // supportedModels) — never empty, so the picker is never blank. `label` is the humanized display name; `default`
579
+ // supportedModels) — never empty, so the picker is never blank. `label` is the provider's display name; `default`
559
580
  // is the model a fresh chat on that provider seeds (always present). Shared by /grok/models, /codex/models,
560
581
  // /claude/models. `efforts` is the reasoning-effort tiers the model accepts (Claude reports them per model);
561
582
  // empty ⇒ the client's default tiers.
@@ -563,8 +584,8 @@ export const TranslatorCompleteSchema = z.object({
563
584
  // EVERY field here is provider-reported — nothing about a model is curated in this repo, so a new release or a
564
585
  // renamed family flows to the UI with no code change. Providers differ in how much they publish: the Claude
565
586
  // Agent SDK reports a display name, a capability description, effort tiers, and capability flags, while the
566
- // OpenAI-compatible /v1/models endpoints (codex/grok/kimi) report ids only — those rows render label-only, and
567
- // that absence is the honest answer rather than something to paper over with a hand-written table.
587
+ // Some OpenAI-compatible /v1/models endpoints report ids only — those rows render label-only, and that absence
588
+ // is the honest answer rather than something to paper over with a hand-written table.
568
589
  //
569
590
  // ORDER IS MEANINGFUL: `models` arrives in the provider's own preference order, which is what the picker sorts
570
591
  // by, and `default` is the provider's own default. Neither is re-ranked locally.
@@ -624,6 +645,11 @@ export const BuiltinPromptSchema = z.object({ base: z.enum(["intentic", "claude"
624
645
  // iqSearch — loads the image-baked iq Claude Code plugin (skill + SessionStart nudge) so the agent
625
646
  // prefers the iq CLI over grep/find/Glob; off ⇒ plugin not loaded, native search tools
626
647
  // only. Opt-in (default off); the browser Search box uses iq regardless.
648
+ // iqContext — retrieves for the user's message BEFORE the turn starts and prepends the ranked answer
649
+ // to it, so the model opens with the anchors instead of paying a search round-trip to
650
+ // find them. Independent of iqSearch: that one teaches the agent to search, this one
651
+ // answers ahead of it.
652
+ // iqContextHoldout — measurement control for iqContext, same shape as terseHoldout (UsageTurn.iqContext).
627
653
  // outputCleaners — the Bash output-cleaner spec (agent-output-filter): "off" = filter disabled (default),
628
654
  // "" = all cleaners on, else an iq-style allow-list / default-minus
629
655
  // spec ("git,pnpm" = only those; "-cap" = all except). Threaded to the filter via env.
@@ -643,6 +669,12 @@ export const BuiltinPromptSchema = z.object({ base: z.enum(["intentic", "claude"
643
669
  // surface fail to parse the moment a toggle is added — which reaches the user as a page of switches that are
644
670
  // silently dead, not as an error. It also means an older on-disk manifest keeps the owner's other picks rather
645
671
  // than being discarded whole.
672
+ // Build-wide kill switch for usage-limit auto-resume. Keep the setting and implementation in place while the
673
+ // feature is disabled, but clamp every daemon response and settings write to OFF so a persisted `true` from an
674
+ // older build cannot keep spending a newly reset allowance. The web also reads this constant to render the
675
+ // control unavailable and to distrust a stale daemon that still reports a scheduled resume.
676
+ export const USAGE_LIMIT_AUTO_RESUME_ENABLED: boolean = false;
677
+
646
678
  export const SandboxSettingsSchema = z.object({
647
679
  stableSystemPrompt: z.boolean().default(false),
648
680
  skills: z.array(z.string()).default([]),
@@ -678,6 +710,21 @@ export const SandboxSettingsSchema = z.object({
678
710
  * Cap is roomy — the bases it stands in for are ~6.8k characters — but finite, because every turn pays it. */
679
711
  systemPrompt: z.string().max(20000).default(""),
680
712
  iqSearch: z.boolean().default(false),
713
+ /* RETRIEVE BEFORE THE TURN, don't wait to be asked. The daemon runs the user's message through the resident
714
+ * iq engine and prepends the ranked answer to it, so a turn that would have opened with two or three search
715
+ * calls opens with the anchors already in hand. Independent of `iqSearch`, which only teaches the agent to
716
+ * reach for the CLI once it decides to search — this one answers ahead of that decision, and the two
717
+ * compose: the injected capsule names the anchors, the CLI is there for what it missed.
718
+ *
719
+ * It rides the USER message (turn-context.ts), never the system prompt, for the same reason the setup
720
+ * notice does: it changes every turn, and the system prefix is kept byte-stable for the prompt cache.
721
+ * Off by default — it spends input tokens on every eligible turn, and whether that trade pays is exactly
722
+ * what the holdout below is for. */
723
+ iqContext: z.boolean().default(false),
724
+ // Measurement control for the pre-injection, identical in shape to `terseHoldout`: a fraction [0,1] of
725
+ // otherwise-eligible turns run WITHOUT the retrieved context and stamp their arm onto the ledger
726
+ // (UsageTurn.iqContext), so the report compares two real populations of turns instead of asserting a saving.
727
+ iqContextHoldout: z.number().min(0).max(1).default(0),
681
728
  outputCleaners: z.string().default("off"),
682
729
  outputHoldout: z.number().min(0).max(1).default(0),
683
730
  filterBackend: z.enum(["native", "rtk"]).default("native"),
@@ -699,12 +746,13 @@ export const SandboxSettingsSchema = z.object({
699
746
  * automation-opened agents (Discord, webhooks, email) finish turns with no browser in the room — a
700
747
  * browser-held preference could not govern them. Per-agent override: AgentSummarySchema.autoLand. */
701
748
  autoLand: z.boolean().default(true),
702
- // When a turn dies on the Claude subscription's usage limit, re-run it automatically once the limit
703
- // window resets (a minute after, so a skewed clock can't retry into the same closed window). Off by
704
- // default: an unattended retry spends the fresh window without the user in the room, so the daemon
705
- // records every limit-hit either way and the chat OFFERS the toggle at the moment it would have helped —
706
- // enabling it then still resumes the turn that just bounced.
707
- autoResumeOnLimit: z.boolean().default(false),
749
+ // Latent opt-in for re-running a turn after the Claude subscription's usage window resets. It defaults off
750
+ // because an unattended retry spends the fresh allowance without the user in the room, and the build-wide
751
+ // gate above currently clamps even an older saved opt-in off while the feature is unavailable.
752
+ autoResumeOnLimit: z
753
+ .boolean()
754
+ .default(false)
755
+ .overwrite((enabled) => USAGE_LIMIT_AUTO_RESUME_ENABLED && enabled),
708
756
  /* When a turn dies because the MODEL PROVIDER was failing (500/502/503, a 529 at capacity, a dropped
709
757
  * socket), re-run it on an escalating backoff until it goes through or the attempts are spent.
710
758
  *
@@ -726,7 +774,7 @@ export const SandboxSettingsSchema = z.object({
726
774
  * OFF still records the interruption: the fleet card reads `interrupted` (see AgentStatusSchema) and an
727
775
  * automation's row shows an `interrupted` run — nothing is re-run, but nothing is silently lost either. */
728
776
  autoResumeOnRestart: z.boolean().default(true),
729
- /* THE LANDING GATE — the check command run over the COMPOSITE of landed work, once the fleet goes quiet.
777
+ /* THE LANDING GATE — the check command run over the COMPOSITE of landed work after a landing debounce.
730
778
  * Empty ⇒ no gate at all, which is the default: only the owner knows what verifies this workspace, and a
731
779
  * guessed command that fails on a fresh clone would read as the gate finding a bug on its first run.
732
780
  *
@@ -821,13 +869,21 @@ export const InputSavingsSchema = z.object({
821
869
  });
822
870
  export type InputSavings = z.infer<typeof InputSavingsSchema>;
823
871
 
824
- // One arm of the turn-level experiment: the turns that ran with the steer, and the turns the holdout ran
825
- // without it. Mean output tokens PER TURN, because the arms never hold the same number of turns.
826
- export const SavingsArmSchema = z.object({ turns: z.number(), meanOutputTokens: z.number() });
872
+ // One arm of a turn-level experiment: the turns that ran with the mechanism, and the turns the holdout ran
873
+ // without it. A mean PER TURN, because the arms never hold the same number of turns.
874
+ export const SavingsArmSchema = z.object({ turns: z.number(), mean: z.number() });
827
875
 
828
- // The terse steer, as measured. Only turns where the steer was ELIGIBLE are counted a turn under a custom
829
- // system prompt drops the steer along with everything else the daemon appends, so it belongs to neither arm.
830
- export const OutputSavingsSchema = z.object({
876
+ /* A turn-level A/B the one shape both of this sandbox's turn experiments report in, because they differ in
877
+ * nothing but which flag flips and what the turns are judged on. Only turns the mechanism was ELIGIBLE for are
878
+ * counted: a turn under a custom system prompt drops the terse steer along with everything else the daemon
879
+ * appends, so it belongs to neither arm.
880
+ *
881
+ * `metric` says what `mean` counts and what `deltaPct` is a delta in. The terse steer is judged on the model's
882
+ * OWN output tokens, which is the thing it steers. Pre-injection is judged on COST, because it spends input
883
+ * tokens deliberately to buy back search turns — scored on output tokens it would look like a pure expense,
884
+ * and scored on input tokens like a pure loss; the trade only nets out in money. */
885
+ export const TurnExperimentSchema = z.object({
886
+ metric: z.enum(["outputTokens", "costUsd"]),
831
887
  on: SavingsArmSchema,
832
888
  off: SavingsArmSchema,
833
889
  // Turns per arm before a delta is reported at all. Carried on the wire so the screen's "measuring…" state
@@ -835,18 +891,24 @@ export const OutputSavingsSchema = z.object({
835
891
  minTurns: z.number(),
836
892
  /* The three below are present TOGETHER, and only once both arms clear `minTurns` — a schema that can't
837
893
  * express a half-measured experiment is how a 34%-that-becomes-8%-tomorrow never reaches the screen.
838
- * deltaPct — change in mean output tokens per turn under the steer; negative is a saving.
839
- * marginPct — ± percentage points, 95% (Welch, unequal variances and unequal arms).
840
- * savedTokens — what the delta is worth over the turns that actually ran with the steer, in this window. */
894
+ * deltaPct — change in the metric's mean per turn under the mechanism; negative is a saving.
895
+ * marginPct — ± percentage points, 95% (Welch, unequal variances and unequal arms).
896
+ * saved — what the delta is worth over the turns that actually ran with it, in this window, in the
897
+ * metric's own unit (tokens, or dollars). */
841
898
  deltaPct: z.number().optional(),
842
899
  marginPct: z.number().optional(),
843
- savedTokens: z.number().optional(),
900
+ saved: z.number().optional(),
844
901
  });
845
- export type OutputSavings = z.infer<typeof OutputSavingsSchema>;
902
+ export type TurnExperiment = z.infer<typeof TurnExperimentSchema>;
846
903
 
847
- // `output` is absent when the experiment isn't running at all (terse off, or no holdout set) — a section that
848
- // isn't there reads as "not measured", which is the truth, while zeros would read as "measured, worth nothing".
849
- export const SavingsReportSchema = z.object({ input: InputSavingsSchema, output: OutputSavingsSchema.optional() });
904
+ // `output`/`context` are absent when that experiment isn't running at all (its flag off, or no holdout set) — a
905
+ // section that isn't there reads as "not measured", which is the truth, while zeros would read as "measured,
906
+ // worth nothing".
907
+ export const SavingsReportSchema = z.object({
908
+ input: InputSavingsSchema,
909
+ output: TurnExperimentSchema.optional(),
910
+ context: TurnExperimentSchema.optional(),
911
+ });
850
912
  export type SavingsReport = z.infer<typeof SavingsReportSchema>;
851
913
 
852
914
  // ---- intentic CLI ----
@@ -999,9 +1061,9 @@ export const RepoChangesSchema = z.object({
999
1061
  remote: GitRemoteStateSchema.optional(),
1000
1062
  // WHICH AGENT PUT IT THERE: repo-relative path → the agent ids that landed it, newest land first. Keyed by
1001
1063
  // PATH rather than carried on each GitChange because a path can be listed on two sides at once (staged and
1002
- // edited again) and its origin is the same fact for both. Only agents can appear here — a main-tree turn,
1003
- // a terminal edit and your own typing never pass through land, so they are simply absent (see
1004
- // agents/origins.ts), which is why the panel badges an agent and says nothing at all for anyone else.
1064
+ // edited again) and its origin is the same fact for both. Only branch-backed agents whose work passed
1065
+ // through land can appear here; workspace conversations, terminal edits and the user's typing are absent
1066
+ // (see agents/origins.ts), so the panel badges an attributable agent and says nothing for anyone else.
1005
1067
  // Ids, not titles: the identity for every id named here rides the response once, in `originAgents`.
1006
1068
  origins: z.record(z.string(), z.array(z.string())).optional(),
1007
1069
  // Why the repo could not be scanned at all, condensed to git's own one-line reason ("fatal: bad object HEAD").
@@ -2052,9 +2114,9 @@ export const AutomationRunSchema = z.object({
2052
2114
  // as "it never fired" — the one reading a 3 a.m. automation must not be given.
2053
2115
  outcome: z.enum(["completed", "skipped", "error", "interrupted"]),
2054
2116
  detail: z.string().optional(),
2055
- // The runtime session the wake ran in, so the row can open the transcript. Absent for a run that never
2056
- // reached a provider (skipped by its guard) or whose provider minted no session before it died.
2057
- sessionId: z.string().optional(),
2117
+ // The stable conversation opened by the wake, so the row can open the provider-neutral agent transcript.
2118
+ // Absent only for a run skipped before a conversation was needed.
2119
+ conversationId: z.string().optional(),
2058
2120
  });
2059
2121
  export type AutomationRun = z.infer<typeof AutomationRunSchema>;
2060
2122
 
@@ -2227,13 +2289,12 @@ export const GateAgentSchema = z.object({
2227
2289
  });
2228
2290
  export type GateAgent = z.infer<typeof GateAgentSchema>;
2229
2291
 
2230
- // The fix turn one red verdict got. A MAIN-TREE turn, not an isolated conversation, so there is no
2231
- // conversationId and no fleet card to open the composite it must reproduce lives in the main working tree and
2232
- // a fresh worktree branches from HEAD without it. `sessionId` is what makes the run readable after the fact,
2233
- // the same thing an automation's run record carries for the same reason.
2292
+ // The fix turn one red verdict got. It is a workspace conversation because the composite it must reproduce
2293
+ // lives as uncommitted content in the main working tree; it still has the same stable registry identity as any
2294
+ // other conversation.
2234
2295
  export const GateFixSchema = z.object({
2235
2296
  startedAt: z.number(),
2236
- sessionId: z.string().optional(),
2297
+ conversationId: z.string(),
2237
2298
  // `running` while the turn streams; `done` when it ended cleanly, whatever the re-check then said;
2238
2299
  // `error` when the turn itself failed (a provider outage, no credential), which is worth distinguishing
2239
2300
  // because it is the one case where re-running the fix could still help.
@@ -2406,11 +2467,9 @@ export type PortForwardResult = z.infer<typeof PortForwardResultSchema>;
2406
2467
  // process (a lingering shell after a crash reads false). A process row that maps to an installed extension's
2407
2468
  // declared process carries extensionId+processName, the address for its /extensions start/stop routes. The
2408
2469
  // `{name}` kill-route param is a bare string validated in the handler (a bad name is a BAD_REQUEST) since the
2409
- // same charset gates a `tmux kill-session -t` shell-out. `browser` = a `browser-<sdk session>` Chromium the
2410
- // agent is driving through its @playwright/mcp tools (browser/browser-sessions.ts) watchable live over the
2411
- // /system/browser-view WebSocket, `running` while that Chromium is connected, and hidden from the strip by
2412
- // the same rule as `agent`: it is a record of work, not a place. Its `label` is the page's own title and
2413
- // `url` the page it is on, which is the one thing a browser pill has to say that a terminal pill does not.
2470
+ // same charset gates a `tmux kill-session -t` shell-out. The agent's BROWSER is deliberately NOT one of these
2471
+ // kinds: a Chromium with its own tab strip is a surface in its own right, not a pane in the terminal panel, so
2472
+ // it lists from /system/browsers with the pages it has open (BrowserSessionSchema below).
2414
2473
  //
2415
2474
  // `activityAt` (epoch ms of the session's last output) and `exitCode` (the LAST window's exit status, absent
2416
2475
  // while that pane still lives) are what let a finished session be READ rather than merely listed: the panel's
@@ -2420,19 +2479,56 @@ export type PortForwardResult = z.infer<typeof PortForwardResultSchema>;
2420
2479
  export const TerminalSessionSchema = z.object({
2421
2480
  name: z.string(),
2422
2481
  label: z.string().optional(),
2423
- kind: z.enum(["shell", "panel", "agent", "job", "process", "browser"]),
2482
+ kind: z.enum(["shell", "panel", "agent", "job", "process"]),
2424
2483
  running: z.boolean(),
2425
2484
  activityAt: z.number(),
2426
2485
  exitCode: z.number().optional(),
2427
2486
  extensionId: z.string().optional(),
2428
2487
  processName: z.string().optional(),
2429
- // Browser sessions only: the page the agent is on right now.
2430
- url: z.string().optional(),
2431
2488
  });
2432
2489
  export const TerminalsListSchema = z.object({ sessions: z.array(TerminalSessionSchema) });
2433
2490
  export type TerminalsList = z.infer<typeof TerminalsListSchema>;
2434
2491
  export const TerminalNameParamSchema = z.object({ name: z.string() });
2435
2492
 
2493
+ /* ---- browsers: the Chromium the agent drives through its @playwright/mcp tools ----
2494
+ *
2495
+ * A `browser-<sdk session>` Chromium (browser/browser-sessions.ts), watchable live over the
2496
+ * /system/browser-view WebSocket. It lists apart from the terminals because it is shaped differently in the one
2497
+ * way that decides a UI: a terminal is ONE stream of bytes, while a browser holds SEVERAL pages at once and the
2498
+ * question "what is the agent looking at?" only has an answer if the wire carries all of them. So `pages` is the
2499
+ * point of this schema — the view renders them as a tab strip and binds the screencast to whichever the user
2500
+ * picks, and `active` is the one the agent itself last touched (what the view follows until the user says
2501
+ * otherwise).
2502
+ *
2503
+ * `id` is opaque and minted per session, and it is what makes a tab survive a relist: it is stable for the life
2504
+ * of the page, unlike its url (the agent navigates away) or its position (a closed tab renumbers the rest). */
2505
+ export const BrowserPageSchema = z.object({
2506
+ id: z.string(),
2507
+ // The page's own title. Absent mid-navigation, which is exactly when a tab still needs to render.
2508
+ title: z.string().optional(),
2509
+ url: z.string(),
2510
+ // The page the agent last drove. Exactly one page per running session has it.
2511
+ active: z.boolean(),
2512
+ });
2513
+ export const BrowserSessionSchema = z.object({
2514
+ name: z.string(),
2515
+ // The pill's text: the active page's title, else its host, else which browser this is.
2516
+ label: z.string(),
2517
+ // Which MCP server drives it: `web` (the credential-free browser) or a logged-in capability's id — the
2518
+ // difference between a throwaway page and one signed in as the user, which is worth saying out loud.
2519
+ server: z.string(),
2520
+ // False once that Chromium is gone (the turn ended, the agent closed it, it crashed). A finished session
2521
+ // still lists for a while, with the pages it had — the record of where the agent went.
2522
+ running: z.boolean(),
2523
+ activityAt: z.number(),
2524
+ pages: z.array(BrowserPageSchema),
2525
+ });
2526
+ export type BrowserPage = z.infer<typeof BrowserPageSchema>;
2527
+ export type BrowserSession = z.infer<typeof BrowserSessionSchema>;
2528
+ export const BrowsersListSchema = z.object({ sessions: z.array(BrowserSessionSchema) });
2529
+ export type BrowsersList = z.infer<typeof BrowsersListSchema>;
2530
+ export const BrowserNameParamSchema = z.object({ name: z.string() });
2531
+
2436
2532
  // ---- environment: the overlay Dockerfile extending the sandbox image ----
2437
2533
  // The approved file is DAEMON-COMPOSED: pinned FROM + capability fragments + the owner-approved custom section.
2438
2534
  // The agent writes the proposal file (.intentic/environment.Dockerfile — custom-section content only, no FROM)
@@ -2598,7 +2694,8 @@ export const UsageTurnSchema = z.object({
2598
2694
  // when the provider's own subscription default served it without the daemon naming one.
2599
2695
  model: z.string().optional(),
2600
2696
  harness: z.string(),
2601
- // The conversation this turn belonged to, so spend can join to a fleet agent. Absent on a main-tree turn.
2697
+ // The conversation this turn belonged to, so spend can join to a fleet agent. Absent only for an internal
2698
+ // one-shot turn that has no conversation identity.
2602
2699
  conversationId: z.string().optional(),
2603
2700
  // The provider's own turn count for the request (a Claude "turn" can be several under the hood), so turns
2604
2701
  // and cost stay comparable across providers. 1 when the provider reported none.
@@ -2617,6 +2714,15 @@ export const UsageTurnSchema = z.object({
2617
2714
  * no control to be compared against. Pooling those into the off-arm would compare steered turns against a
2618
2715
  * population selected by something other than the coin flip, which is not a control at all. */
2619
2716
  terse: z.boolean().optional(),
2717
+ /* Which arm of the pre-injection experiment this turn ran on (settings.iqContextHoldout), on the same terms
2718
+ * as `terse` above: absent ⇒ outside the experiment.
2719
+ *
2720
+ * TRUE means the turn was ASSIGNED the retrieved context, not that a note was necessarily prepended — a
2721
+ * treatment turn whose retrieval came back empty or unconfident injects nothing. That is deliberate: the
2722
+ * arms have to be the coin flip's populations, and re-labelling a turn by what retrieval happened to find
2723
+ * would sort turns by how searchable their question was, which is a property of the question. The control
2724
+ * arm contains the same unsearchable questions in the same proportion, so they cancel. */
2725
+ iqContext: z.boolean().optional(),
2620
2726
  });
2621
2727
  export type UsageTurn = z.infer<typeof UsageTurnSchema>;
2622
2728