@intentic/sandbox-contract 1.226.1 → 1.228.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 (70) hide show
  1. package/dist/agent-catalog.d.ts +2 -2
  2. package/dist/agent-catalog.d.ts.map +1 -1
  3. package/dist/agent-catalog.js +26 -1
  4. package/dist/agent-catalog.js.map +1 -1
  5. package/dist/contracts/agent.contract.d.ts +19 -2
  6. package/dist/contracts/agent.contract.d.ts.map +1 -1
  7. package/dist/contracts/agents.contract.d.ts +62 -0
  8. package/dist/contracts/agents.contract.d.ts.map +1 -1
  9. package/dist/contracts/agents.contract.js +2 -2
  10. package/dist/contracts/agents.contract.js.map +1 -1
  11. package/dist/contracts/capabilities.contract.d.ts +7 -0
  12. package/dist/contracts/capabilities.contract.d.ts.map +1 -1
  13. package/dist/contracts/cursor.contract.d.ts +63 -0
  14. package/dist/contracts/cursor.contract.d.ts.map +1 -0
  15. package/dist/contracts/cursor.contract.js +49 -0
  16. package/dist/contracts/cursor.contract.js.map +1 -0
  17. package/dist/contracts/endpoints.contract.d.ts +1 -0
  18. package/dist/contracts/endpoints.contract.d.ts.map +1 -1
  19. package/dist/contracts/extensions.contract.d.ts +1 -0
  20. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  21. package/dist/contracts/logs.contract.d.ts +17 -0
  22. package/dist/contracts/logs.contract.d.ts.map +1 -1
  23. package/dist/contracts/logs.contract.js +10 -1
  24. package/dist/contracts/logs.contract.js.map +1 -1
  25. package/dist/contracts/providers.contract.d.ts +2 -0
  26. package/dist/contracts/providers.contract.d.ts.map +1 -1
  27. package/dist/contracts/sessions.contract.d.ts +3 -0
  28. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  29. package/dist/contracts/settings.contract.d.ts +19 -0
  30. package/dist/contracts/settings.contract.d.ts.map +1 -1
  31. package/dist/contracts/system.contract.d.ts +21 -14
  32. package/dist/contracts/system.contract.d.ts.map +1 -1
  33. package/dist/events.d.ts +46 -0
  34. package/dist/events.d.ts.map +1 -1
  35. package/dist/events.js +14 -0
  36. package/dist/events.js.map +1 -1
  37. package/dist/index.d.ts +271 -70
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +5 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/prompt-complexity.d.ts +9 -1
  42. package/dist/prompt-complexity.d.ts.map +1 -1
  43. package/dist/prompt-complexity.js +13 -5
  44. package/dist/prompt-complexity.js.map +1 -1
  45. package/dist/request-id.d.ts +3 -0
  46. package/dist/request-id.d.ts.map +1 -0
  47. package/dist/request-id.js +3 -0
  48. package/dist/request-id.js.map +1 -0
  49. package/dist/schemas.d.ts +268 -1
  50. package/dist/schemas.d.ts.map +1 -1
  51. package/dist/schemas.js +79 -1
  52. package/dist/schemas.js.map +1 -1
  53. package/dist/starter.d.ts +3 -0
  54. package/dist/starter.d.ts.map +1 -0
  55. package/dist/starter.js +3 -0
  56. package/dist/starter.js.map +1 -0
  57. package/package.json +6 -5
  58. package/src/agent-catalog.test.ts +2 -2
  59. package/src/agent-catalog.ts +123 -13
  60. package/src/contract-lock.test.ts +8 -2
  61. package/src/contracts/agents.contract.ts +2 -1
  62. package/src/contracts/cursor.contract.ts +74 -0
  63. package/src/contracts/logs.contract.ts +23 -2
  64. package/src/events.ts +62 -6
  65. package/src/index.ts +5 -0
  66. package/src/prompt-complexity.test.ts +69 -0
  67. package/src/prompt-complexity.ts +102 -39
  68. package/src/request-id.ts +41 -0
  69. package/src/schemas.ts +276 -2
  70. package/src/starter.ts +13 -0
package/src/schemas.ts CHANGED
@@ -54,7 +54,7 @@ export const RepoParamSchema = z.object({
54
54
  // ACP (Agent Client Protocol).
55
55
  // Kept as a bare string on the wire (not an enum) so an unknown id is a clean error frame from the agent
56
56
  // route, the same bet RepoParamSchema makes, and adding an ACP agent needs no contract change.
57
- export const NATIVE_PROVIDERS = ["claude", "codex", "grok", "kimi", "gemini"] as const;
57
+ export const NATIVE_PROVIDERS = ["claude", "codex", "grok", "kimi", "gemini", "cursor"] as const;
58
58
  export type NativeProvider = (typeof NATIVE_PROVIDERS)[number];
59
59
  export const AgentProviderSchema = z.string().min(1);
60
60
  export type AgentProvider = z.infer<typeof AgentProviderSchema>;
@@ -378,6 +378,18 @@ export const AgentTurnSchema = z
378
378
  .describe(
379
379
  "Ask for the same work at a higher rate for a higher price. A request rather than a promise: the answer says what actually happened.",
380
380
  ),
381
+ /* KEEP THIS TURN ON THE MODEL I PICKED: the user's veto over automatic tier selection, riding the turn
382
+ * like `fast` does because it changes what the turn costs and so belongs to the turn rather than to the
383
+ * workspace. The judge still runs and the verdict is still recorded — a deny is the strongest label the
384
+ * calibration ledger ever gets (UsageTurn.tierDenied) — but nothing is substituted. The composer sends
385
+ * its conversation-level toggle here every turn, and the registry persists it beside `fast`, so the
386
+ * choice survives reopening the tab. Absent ⇒ no opinion, routing follows settings.autoTier. */
387
+ tierHold: z
388
+ .boolean()
389
+ .optional()
390
+ .describe(
391
+ "Run exactly the model that was picked, even when the turn looks simple enough for a cheaper one. The judgement is still recorded; nothing is substituted.",
392
+ ),
381
393
  // The opt-in editor context chip: what the user is looking at, folded into the prompt daemon-side.
382
394
  editorContext: EditorContextSchema.optional().describe(
383
395
  'What the user has open in their editor, folded into the prompt so that pointing words like "this" resolve.',
@@ -1080,6 +1092,21 @@ export const AgentSummarySchema = z.object({
1080
1092
  effort: z.string().optional().describe("How hard that turn was told to think."),
1081
1093
  thinking: z.boolean().optional().describe("Whether that turn showed its reasoning."),
1082
1094
  fast: z.boolean().optional().describe("Whether that turn asked for higher speed. What was asked for, not what was served."),
1095
+ /* WHAT THE COMPLEXITY JUDGE MADE OF THE LAST TURN HERE, mirrored from the persisted entry (agents-store.ts
1096
+ * `tier`) so a client opening the conversation tomorrow can seed its composer preview with the one judge
1097
+ * input a draft cannot contain (prompt-complexity.ts `afterHardTurn`). The JUDGEMENT, never what ran, for
1098
+ * the reason the store states: what ran is a fact about configuration, the next turn is asking about the
1099
+ * difficulty of the work. Absent ⇒ nothing judged yet. */
1100
+ tier: z
1101
+ .enum(["fast", "standard"])
1102
+ .optional()
1103
+ .describe("How hard its last turn looked to the complexity judge. What the next turn's preview needs, not what actually ran."),
1104
+ // The conversation's standing "keep every turn on my pick" choice, the composer's memory of it, the same
1105
+ // shape as `fast` above: what was asked for, restored into the composer on open, sent back on every turn.
1106
+ tierHold: z
1107
+ .boolean()
1108
+ .optional()
1109
+ .describe("Whether this conversation is pinned to the picked model, so a turn that looks simple is never moved to a cheaper one."),
1083
1110
  account: z.string().optional().describe("Which connected account paid for it."),
1084
1111
  // The worktree branch (agent/<id>); absent for a non-isolated (main-tree) conversation.
1085
1112
  branch: z.string().optional().describe("The branch its private copy works on. Absent for a conversation that works directly in the shared tree."),
@@ -1340,6 +1367,22 @@ export const AgentsMovedSchema = z.object({
1340
1367
  ),
1341
1368
  });
1342
1369
  export type AgentsMoved = z.infer<typeof AgentsMovedSchema>;
1370
+ /* AN ARCHIVE ALSO REPORTS WHAT IT COULD NOT DO, which is the half that used to go missing. Releasing a working
1371
+ * copy is git work, and it can fail for reasons no press can fix: the repository behind a checkout was deleted
1372
+ * from the workspace, a checkout is locked. Those agents stay on the board, so the answer carries them and the
1373
+ * sentence each failed with, otherwise the only true thing the caller could say about an archive that moved
1374
+ * nothing was "there was nothing to archive": to a user looking straight at the card it refused. */
1375
+ export const AgentsArchivedSchema = AgentsMovedSchema.extend({
1376
+ failed: z
1377
+ .array(
1378
+ z.object({
1379
+ id: z.string().describe("Which conversation stayed on the board."),
1380
+ reason: z.string().describe("Why its working copy could not be released, in the words the failure came with."),
1381
+ }),
1382
+ )
1383
+ .describe("The conversations this press could not put away, each with the reason, so the board can say it instead of reporting silence."),
1384
+ });
1385
+ export type AgentsArchived = z.infer<typeof AgentsArchivedSchema>;
1343
1386
  // What a purge actually deleted. Ids, not summaries: these agents no longer exist anywhere, there is nothing
1344
1387
  // left to show and nothing to put back, so the only thing the caller can do with the answer is drop those rows
1345
1388
  // and count them. No revision either: archived agents are already off the broadcast roster (see `list`), so a
@@ -1954,6 +1997,33 @@ export const AuthorizeChallengeSchema = z.object({
1954
1997
  verifier: z.string().describe("Keep this and send it back when finishing. It is what proves the code that comes back belongs to this handshake."),
1955
1998
  state: z.string().describe("The handshake's own id, sent back with it."),
1956
1999
  });
2000
+
2001
+ /* CURSOR'S SIGN-IN START. A third login shape, and the reason it is not one of the two above is where the
2002
+ * SECRET lives during the handshake.
2003
+ *
2004
+ * Claude's is paste-back: the browser receives a code and the caller hands it plus its verifier to `exchange`,
2005
+ * so the handshake's proof has to travel on the wire and AuthorizeChallengeSchema carries it. Cursor's PKCE
2006
+ * verifier must never leave the process that generated it, anyone holding it can redeem the login and mint a
2007
+ * durable key, so the daemon starts the whole flow, keeps the verifier in memory, polls Cursor itself, and
2008
+ * writes the account when it lands. Nothing redeemable is on this shape at all.
2009
+ *
2010
+ * Which makes it behave like a DEVICE flow from the caller's side (open the page, then watch the account list),
2011
+ * except that there is no one-time code to display: the login page is addressed to this handshake already. So
2012
+ * DeviceStartSchema's `code` would be a permanently blank field on every card, and TranslatorStartSchema's
2013
+ * `state` a value nothing sends back. `handshake` is neither, it is a cancellation handle. */
2014
+ export const CursorLoginStartSchema = z.object({
2015
+ url: z.string().describe("The page to open and sign in on. It is already addressed to this attempt, so there is no code to type."),
2016
+ handshake: z
2017
+ .string()
2018
+ .describe(
2019
+ "This attempt's id, for abandoning it. Not a credential and not redeemable: the proof that finishes the sign-in never leaves the sandbox.",
2020
+ ),
2021
+ expiresAt: z.number().describe("When this attempt stops being answerable, in milliseconds, so a card can stop waiting instead of spinning."),
2022
+ });
2023
+ export type CursorLoginStart = z.infer<typeof CursorLoginStartSchema>;
2024
+ // Abandon a sign-in nobody completed, so the daemon stops polling Cursor for it. Ordinary tidiness rather than
2025
+ // a security boundary: an unanswered attempt also times out on its own (see `expiresAt`).
2026
+ export const CursorLoginCancelSchema = z.object({ handshake: z.string().min(1).describe("Which attempt to stop waiting on.") });
1957
2027
  // xAI Grok (via OpenCode) uses subscription OAuth via the headless device-code method. `start` returns the
1958
2028
  // `url` the user opens (xAI's verification_uri_complete, which pre-fills the code) and `code`, the same
1959
2029
  // one-time code, surfaced so the card matches x.ai exactly. There is no paste-back: OpenCode polls to
@@ -2018,6 +2088,15 @@ export const ModelSchema = z.object({
2018
2088
  "What it is good for, in the provider's own words. Absent where the provider publishes only ids, which is the honest answer rather than something to paper over with a hand-written table.",
2019
2089
  ),
2020
2090
  badges: z.array(ModelBadgeSchema).optional().describe("What it is known for, where the provider says so."),
2091
+ /* HOW MUCH THE SERVER WILL ACCEPT IN ONE REQUEST, where the server says so, and the one field here that a
2092
+ * turn is refused against rather than merely rendered (agent/context-budget.ts).
2093
+ *
2094
+ * The SERVED window, never the weights' training length. An inference server takes its context size from a
2095
+ * flag and then clamps it to the memory it actually has, so a 3B model whose GGUF advertises 131k can be
2096
+ * serving 16k, and it is the 16k that refuses the request. Read from llama.cpp's /props and vLLM's
2097
+ * `max_model_len`; absent for every provider that publishes no such number, which is most of them, and
2098
+ * absent means unknown rather than unlimited: nothing gates on a window it was never told. */
2099
+ contextWindow: z.number().optional().describe("How many tokens this model will accept in one request, where the server publishes it."),
2021
2100
  });
2022
2101
  export type Model = z.infer<typeof ModelSchema>;
2023
2102
  export const ModelsSchema = z.object({
@@ -2521,6 +2600,23 @@ export const SandboxSettingsSchema = z.object({
2521
2600
  .describe(
2522
2601
  "Whether an easy-looking turn may run on a cheaper model from the same provider. Three states rather than a switch, because the middle one is the only honest road to the third: it scores every turn and routes nothing, so the guess can become a measurement before it changes anything. It can only ever route down, so the worst case is one turn's quality rather than a bill nobody asked for.",
2523
2602
  ),
2603
+ /* HOW EAGER THE JUDGE IS, the one dial this feature exposes and the answer to what the Measure mode is for:
2604
+ * the numbers say how many turns were called simple, and this is the control that acts on them.
2605
+ *
2606
+ * Three named stops rather than a number, because the number means nothing to anyone who has not read the
2607
+ * weights, while "only the unmistakable" / "the default" / "an easy question about real code too" are three
2608
+ * sentences an owner can actually hold an opinion about (FAST_CEILINGS spells out each). It moves the
2609
+ * cutoff and nothing else: the rule that a downgrade needs something POSITIVE to have been said holds at
2610
+ * every stop, so no setting of this can start downgrading short vague requests.
2611
+ *
2612
+ * `balanced` is the default and is what every verdict recorded before this existed was judged against, so
2613
+ * the shadow history stays comparable across the change rather than silently becoming two populations. */
2614
+ autoTierEagerness: z
2615
+ .enum(["cautious", "balanced", "eager"])
2616
+ .default("balanced")
2617
+ .describe(
2618
+ "How readily a turn counts as simple enough for the cheaper model. It moves only the cutoff: at every setting a turn still has to say something positively easy, so nothing here can downgrade a short vague request.",
2619
+ ),
2524
2620
  /* WHICH CHEAP MODEL A DOWNGRADED TURN LANDS ON, an ordered list of `${provider}:${model}` keys
2525
2621
  * (quickModelKey), or EMPTY for Auto.
2526
2622
  *
@@ -2843,10 +2939,45 @@ export type TurnExperiment = z.infer<typeof TurnExperimentSchema>;
2843
2939
  // `output`/`search` are absent when that experiment isn't running at all (its flag off, or no holdout set), a
2844
2940
  // section that isn't there reads as "not measured", which is the truth, while zeros would read as "measured,
2845
2941
  // worth nothing".
2942
+ /* WHAT THE COMPLEXITY JUDGE HAS BEEN SAYING, read back off the spend ledger's tier fields (UsageTurn.tierScore
2943
+ * and friends) over the requested window. The three numbers docs/model-routing-design.md §4 says the feature
2944
+ * cannot be defended without, plus the veto count, and nothing else: no counterfactual "you would have saved
2945
+ * $X", because the ledger holds what turns COST, not what they would have cost on a model they never ran.
2946
+ *
2947
+ * NOT a TurnExperiment, deliberately. The experiments compare two randomized arms of one population; this is a
2948
+ * tally of what one mechanism observed and did. Dressing it in arms and margins would claim a control group that
2949
+ * does not exist (routing follows the settings mode, which follows time, not a coin flip).
2950
+ *
2951
+ * The whole section is absent when no turn in the window was judged at all (autoTier "off" throughout), which a
2952
+ * screen renders as absence: "not measured" is the truth, zeros would read as "measured, found nothing". */
2953
+ export const TierReportSchema = z.object({
2954
+ // Turns the judge ran on in the window, the denominator under everything below.
2955
+ judged: z.number(),
2956
+ // …of which landed at or below FAST_CEILING: the turns that looked simple. fast ÷ judged is the fast share.
2957
+ fast: z.number(),
2958
+ /* What the fast-judged turns that STAYED on the user's pick actually cost, the money measure mode is
2959
+ * pointing at. An upper bound on any saving, never an estimate of one: moving those turns to the cheap rung
2960
+ * would have cost something too, and this schema refuses to guess how much. */
2961
+ atStakeUsd: z.number(),
2962
+ // Turns that actually ran the cheap rung, and what they cost there. Realized, not projected.
2963
+ routed: z.number(),
2964
+ routedUsd: z.number(),
2965
+ /* THE GUARDRAIL: fast-judged turns whose conversation's very next ledger row asked for a dearer model, the
2966
+ * user reaching for the model picker right after a turn the judge called simple. The strongest negative
2967
+ * signal the ledger can carry (§4's first calibration row). Past a few percent of `fast`, the judge is
2968
+ * costing more in retries and trust than it saves in tokens. */
2969
+ escalated: z.number(),
2970
+ // Fast-judged turns the user vetoed outright (UsageTurn.tierDenied): the same signal, said even louder.
2971
+ denied: z.number(),
2972
+ });
2973
+ export type TierReport = z.infer<typeof TierReportSchema>;
2974
+
2846
2975
  export const SavingsReportSchema = z.object({
2847
2976
  input: InputSavingsSchema,
2848
2977
  output: TurnExperimentSchema.optional(),
2849
2978
  search: TurnExperimentSchema.optional(),
2979
+ // Automatic tier selection's readout, see TierReportSchema. Absent ⇒ nothing was judged in the window.
2980
+ tier: TierReportSchema.optional(),
2850
2981
  });
2851
2982
  export type SavingsReport = z.infer<typeof SavingsReportSchema>;
2852
2983
 
@@ -4889,11 +5020,48 @@ export const EndpointConfigSchema = z.object({
4889
5020
  *
4890
5021
  * `gpu` mirrors the docker card's option and rides the same allowlisted `--gpus=all` directive: the ASK lives
4891
5022
  * here, what became of it is SANDBOX_GPU, stamped by the runner (see the docker handler's gpuState). "on"/"off"
4892
- * rather than a boolean for the manifest-wide reason DockerConfigSchema gives. */
5023
+ * rather than a boolean for the manifest-wide reason DockerConfigSchema gives.
5024
+ *
5025
+ * `context`/`contextTokens` are HOW MUCH CONVERSATION the server holds, the `model`/`url` pair's shape for the
5026
+ * same reason: a short list of rungs anyone can choose between, and one escape hatch for a person who knows the
5027
+ * exact number they want. Resolved to a single token count in exactly one place (the daemon's
5028
+ * endpoints/local-model.ts localModelWindow), because the flag llama-server is started with and the number the
5029
+ * card promises must never be two opinions. */
5030
+ export const LOCAL_MODEL_WINDOWS = ["16384", "32768", "65536", "131072"] as const;
5031
+ export type LocalModelWindow = (typeof LOCAL_MODEL_WINDOWS)[number];
5032
+
5033
+ /* THE RUNG A CARD WITH NO OPINION LANDS ON, and the one number in this block that is a product decision rather
5034
+ * than an arithmetic one.
5035
+ *
5036
+ * It is 65,536 because this sandbox runs a TOOL-CALLING AGENT LOOP, and that loop's own fixed cost, its
5037
+ * instructions plus one JSON schema per tool it can call, times every capability the owner has connected, is
5038
+ * tens of thousands of tokens before the user has typed anything (agent/context-budget.ts holds the measurement
5039
+ * and the refusal built on it). A window that cannot hold that cost is not a smaller version of the product; it
5040
+ * is a model whose every real turn is refused, which is what the previous flat 32,768 shipped: a 27B model,
5041
+ * seventeen gigabytes downloaded, and a first message that died on `36216 tokens exceeds 32768`.
5042
+ *
5043
+ * So the default is the smallest rung a full turn fits in, and the smaller rungs stay on the list because they
5044
+ * are honestly useful: pinned as the quick model (titles, commit messages) a window this size is waste, and the
5045
+ * gigabyte it gives back is the difference between running one of these models on an eight-gigabyte laptop and
5046
+ * not. What each rung costs in memory is the card's job to say (capability-catalog): roughly 2 GB of quantized
5047
+ * cache per 32k of window, on top of the weights. */
5048
+ export const LOCAL_MODEL_WINDOW_DEFAULT: LocalModelWindow = "65536";
5049
+
5050
+ /* THE BOUNDS ON THE TYPED NUMBER, and they are bounds against a TYPO rather than against a preference. Below
5051
+ * the floor there is no conversation left to have once the loop's own instructions land; above the ceiling is a
5052
+ * number no GGUF on offer was trained for, and llama-server would spend minutes reserving a cache for it before
5053
+ * failing. Everything between is the owner's call: their machine, their memory. */
5054
+ export const LOCAL_MODEL_WINDOW_MIN = 2048;
5055
+ export const LOCAL_MODEL_WINDOW_MAX = 1_048_576;
5056
+
4893
5057
  export const LocalModelConfigSchema = z.object({
4894
5058
  model: z.string().min(1),
4895
5059
  gpu: z.enum(["on", "off"]).default("off"),
4896
5060
  url: z.url().optional(),
5061
+ context: z.union([z.enum(LOCAL_MODEL_WINDOWS), z.literal("custom")]).default(LOCAL_MODEL_WINDOW_DEFAULT),
5062
+ // Coerced because it arrives from a text field as a string, the ssh card's `port` precedent, and only read
5063
+ // when `context` is "custom" (the `url`/`model` relationship exactly).
5064
+ contextTokens: z.coerce.number().int().min(LOCAL_MODEL_WINDOW_MIN).max(LOCAL_MODEL_WINDOW_MAX).optional(),
4897
5065
  });
4898
5066
  export type LocalModelConfig = z.infer<typeof LocalModelConfigSchema>;
4899
5067
  /* THE SANDBOX WALLET, a USDC balance the agent can spend on x402-payable endpoints, under owner policy.
@@ -8839,7 +9007,47 @@ export const UsageTurnSchema = z.object({
8839
9007
  .describe(
8840
9008
  "The model that actually ran, past whatever was asked for and every default. Absent only when the provider's own default served it without being named.",
8841
9009
  ),
9010
+ /* WHAT THE CLIENT ASKED FOR, beside `model` above, which is what ran. The pair is the point: a routing
9011
+ * surprise is then a diff on one row rather than an investigation through the routing code.
9012
+ *
9013
+ * The gap between the two is real and was unreadable. A pick is resolved past the tier judge's downgrade,
9014
+ * a provider's own subscription default, a catalog validity check that silently substitutes (Grok rejects
9015
+ * a retired models.dev id, so an invalid pin becomes the catalog default), and CLIProxyAPI's own choice on
9016
+ * a routed turn. Every one of those is a legitimate substitution and none of them was recorded, so "I
9017
+ * chose one model and got another's error" could only be answered by reading four resolution paths and
9018
+ * guessing which had fired.
9019
+ *
9020
+ * Absent ⇒ the client named nothing and asked for the default, which is not the same as asking for what it
9021
+ * got. Equal to `model` on the overwhelming majority of turns; the rows where they differ are the whole
9022
+ * reason this is here. */
9023
+ modelRequested: z
9024
+ .string()
9025
+ .optional()
9026
+ .describe("The model that was asked for, when one was named. Differs from `model` when something resolved it."),
8842
9027
  harness: z.string().describe("Which agentic loop it ran on."),
9028
+ /* HOW THE TURN ENDED. The field that turns the ledger from an accounting record into a diagnostic one.
9029
+ *
9030
+ * Without it a turn that died is byte-for-byte indistinguishable from one that succeeded, except that it
9031
+ * cost less, so "four sessions all broke a minute ago" had no record to read and had to be answered by
9032
+ * re-running the destructive act in a live sandbox. The failure was never nowhere: it was in the activity
9033
+ * log, which prunes to its most recent entries, so an incident survives only until the feed rolls past it.
9034
+ * This log is never pruned, which is the entire difference.
9035
+ *
9036
+ * "cancelled" is a user pressing Stop, which is not a failure and must never be read as one, the registry
9037
+ * learned that lesson already (see the abort branch in streamAgent). "error" is a turn the provider or the
9038
+ * request killed. Both still carry whatever they spent before they ended.
9039
+ *
9040
+ * Absent ⇒ the row predates this being recorded, NOT a turn that succeeded. Readers that count failures
9041
+ * must treat absent as unknown, and the experiment readers do exactly that. */
9042
+ outcome: z.enum(["ok", "error", "cancelled"]).optional().describe("How it ended: finished, failed, or was stopped by the user."),
9043
+ // The failing frame's code, when it carried one, e.g. `rate_limit`, `provider-outage`, `claude-not-entitled`.
9044
+ // Present only alongside outcome "error", and absent even then for a failure that named no code, which is
9045
+ // itself the interesting case: an unclassified failure is one nothing downstream knows how to handle.
9046
+ errorCode: z.string().optional().describe("The failure's code, when it had one."),
9047
+ // The failing frame's own sentence, capped at ERROR_MESSAGE_CHARS. Capped rather than omitted because the
9048
+ // provider's wording is routinely the only thing that distinguishes two failures sharing one code, and
9049
+ // uncapped it would let one bad provider message dominate a file that must stay cheap to read whole.
9050
+ errorMessage: z.string().optional().describe("What the failure said, trimmed."),
8843
9051
  // The conversation this turn belonged to, so spend can join to a fleet agent. Absent only for an internal
8844
9052
  // one-shot turn that has no conversation identity.
8845
9053
  conversationId: z
@@ -8942,6 +9150,24 @@ export const UsageTurnSchema = z.object({
8942
9150
  tierScore: z.number().optional(),
8943
9151
  tierRules: z.array(z.string()).optional(),
8944
9152
  tierRouted: z.boolean().optional(),
9153
+ /* THE VERDICT ITSELF, and the cutoff it was reached against.
9154
+ *
9155
+ * `tierScore` stopped being able to answer "was this called simple" the moment the cutoff became the owner's
9156
+ * to choose (settings.autoTierEagerness): 0.35 is standard on the middle stop and fast on the eager one, and
9157
+ * a fast verdict also requires a positively-easy signal that no score can express. So the answer is written
9158
+ * down rather than re-derived, and the ceiling goes with it because a refit reading a column of bare scores
9159
+ * could not otherwise tell two rows apart.
9160
+ *
9161
+ * Absent on rows written before the knob existed, which were all judged at the `balanced` cutoff — which is
9162
+ * exactly what a reader falls back to (FAST_CEILING), so old and new rows stay one population. */
9163
+ tierFast: z.boolean().optional(),
9164
+ tierCeiling: z.number().optional(),
9165
+ /* THE USER SAID NO: the turn carried AgentTurn.tierHold, so a fast verdict moved nothing. Recorded rather
9166
+ * than folded into `tierRouted: false` because it is the strongest calibration label this ledger ever gets,
9167
+ * a person looking at this very conversation deciding the cheap rung was not to be trusted with it, and the
9168
+ * refit (docs/model-routing-design.md §4) needs it kept apart from "nothing cheaper was published". Absent ⇒
9169
+ * no veto, which is every row written before the control existed and most rows after. */
9170
+ tierDenied: z.boolean().optional(),
8945
9171
  });
8946
9172
  export type UsageTurn = z.infer<typeof UsageTurnSchema>;
8947
9173
 
@@ -9043,6 +9269,54 @@ export const LogReadSchema = z.object({
9043
9269
  });
9044
9270
  export type LogRead = z.infer<typeof LogReadSchema>;
9045
9271
 
9272
+ /* ---- client diagnostics: what the BROWSER saw, landed where the daemon's own records live ----
9273
+ *
9274
+ * The one gap no daemon-side record could ever close. Everything the editor measured or caught ended at
9275
+ * `console.warn`: the perf ring buffer dies on reload, a render error reaches Vue's handler and stops there, and
9276
+ * the startup self-heal CLEARS this origin's storage and reloads, destroying the evidence for the one class of
9277
+ * bug that reproduces least often. So when a user hit a bug in their own browser the durable record was
9278
+ * nothing, and the only way to investigate was to re-drive a browser and hope. Measured over 728 sessions that
9279
+ * is 1,545 screenshots against 65 console reads, and a quarter of all prompts arriving with a picture attached
9280
+ * because there was no other channel.
9281
+ *
9282
+ * Deliberately NARROW. This is not analytics (PostHog owns product events) and not a log pipe: it carries the
9283
+ * things a person cannot describe and a screenshot cannot show, which are caught errors, self-heal wipes, and
9284
+ * spans that blew their frame budget. Everything is capped at the schema so one looping component cannot fill
9285
+ * the /history volume, and the daemon appends to logs/client.jsonl under the same prune sweep as the rest. */
9286
+ export const ClientDiagnosticSchema = z.object({
9287
+ // When the BROWSER saw it. Distinct from the `time` the daemon stamps on the record, because a batch can
9288
+ // arrive seconds late and a reader correlating a stutter to a daemon span needs the moment it happened.
9289
+ seenAt: z.number().describe("When the browser saw it, in milliseconds."),
9290
+ level: z.enum(["warn", "error"]).describe("How bad it was."),
9291
+ // A stable name, not free text: this is what a reader filters on. e.g. `vue.render`, `window.error`,
9292
+ // `unhandled.rejection`, `self-heal.wipe`, `perf.slow`.
9293
+ event: z.string().min(1).max(100).describe("What kind of thing it was, as a stable name."),
9294
+ message: z.string().max(2_000).describe("What it said."),
9295
+ // The app route the user was on, which is the single most useful field for reproducing anything.
9296
+ route: z.string().max(300).optional().describe("Which page they were on."),
9297
+ /* The id this browser put on the daemon call it was making, when there was one. The join key: without it
9298
+ * "the UI stuttered at 15:22" and "slow http.request at 15:22" can only be matched by eye. */
9299
+ requestId: z.string().max(100).optional().describe("Which daemon call it belonged to, when it belonged to one."),
9300
+ // Which build this browser was running, so a report from a tab nobody has reloaded in a week says so.
9301
+ build: z.string().max(100).optional().describe("Which build of the app was running."),
9302
+ // Bounded and primitive: a stack, an op name, a duration. Kept flat so a line stays greppable.
9303
+ fields: z
9304
+ .record(z.string().max(60), z.union([z.string().max(4_000), z.number(), z.boolean()]))
9305
+ .optional()
9306
+ .describe("Whatever else was worth keeping."),
9307
+ });
9308
+ export type ClientDiagnostic = z.infer<typeof ClientDiagnosticSchema>;
9309
+
9310
+ // A batch. Capped: the client coalesces and drops on its own side too, and a route that accepts an unbounded
9311
+ // array is a way to fill a disk with one request.
9312
+ export const ClientDiagnosticsReportSchema = z.object({
9313
+ events: z.array(ClientDiagnosticSchema).min(1).max(50).describe("What the browser has to report, oldest first."),
9314
+ });
9315
+ export type ClientDiagnosticsReport = z.infer<typeof ClientDiagnosticsReportSchema>;
9316
+ export const ClientDiagnosticsAcceptedSchema = z.object({
9317
+ recorded: z.number().describe("How many were written down."),
9318
+ });
9319
+
9046
9320
  // A tab's self-report of what it is looking at, keyed by its /events connection's clientId. Full replace,
9047
9321
  // not a merge, an absent field means "cleared", so a tab leaving a file drops the path with the same report.
9048
9322
  export const PresenceReportSchema = z.object({
package/src/starter.ts ADDED
@@ -0,0 +1,13 @@
1
+ /* THE STARTER SITE'S NAMES, declared where both sides of the wire read them.
2
+ *
3
+ * A fresh sandbox opens with one real, running thing in it: a one-page site, copied out of the image and
4
+ * started by the daemon's first boot (sandbox src/scaffold/starter-site.ts). The browser's job is the other
5
+ * half of that promise, to have it on screen when the user arrives (web shell/ShellDesktop.vue), and to do
6
+ * that it has to name the same repo and the same app the daemon seeded.
7
+ *
8
+ * Two constants rather than one because they are two different kinds of name: `site` is the repo directory a
9
+ * person would have picked, and `landing` is the template's own app name, which the apps extension, the
10
+ * preview hostname (preview-site--landing-<id>.<zone>) and the folder under `_apps/` all inherit. Renaming
11
+ * either is a product decision, not a rename: the seeded workspaces already out there keep the old names. */
12
+ export const STARTER_REPO = "site";
13
+ export const STARTER_APP = "landing";