@intentic/sandbox-contract 1.340.0 → 1.342.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 (179) hide show
  1. package/README.md +4 -0
  2. package/dist/contracts/agent.contract.d.ts +274 -0
  3. package/dist/contracts/agent.contract.d.ts.map +1 -1
  4. package/dist/contracts/agent.contract.js +11 -1
  5. package/dist/contracts/agent.contract.js.map +1 -1
  6. package/dist/contracts/agents.contract.d.ts +87 -0
  7. package/dist/contracts/agents.contract.d.ts.map +1 -1
  8. package/dist/contracts/ci.contract.d.ts +5 -0
  9. package/dist/contracts/ci.contract.d.ts.map +1 -1
  10. package/dist/contracts/git.contract.d.ts +320 -0
  11. package/dist/contracts/git.contract.d.ts.map +1 -1
  12. package/dist/contracts/git.contract.js +28 -0
  13. package/dist/contracts/git.contract.js.map +1 -1
  14. package/dist/contracts/runner.contract.d.ts +205 -125
  15. package/dist/contracts/runner.contract.d.ts.map +1 -1
  16. package/dist/contracts/sessions.contract.d.ts +35 -0
  17. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  18. package/dist/contracts/settings.contract.d.ts +286 -6
  19. package/dist/contracts/settings.contract.d.ts.map +1 -1
  20. package/dist/contracts/system.contract.d.ts +42 -30
  21. package/dist/contracts/system.contract.d.ts.map +1 -1
  22. package/dist/contracts/workspace.contract.d.ts +9 -0
  23. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  24. package/dist/contracts/workspace.contract.js +11 -1
  25. package/dist/contracts/workspace.contract.js.map +1 -1
  26. package/dist/events/agent-events.d.ts +143 -0
  27. package/dist/events/agent-events.d.ts.map +1 -1
  28. package/dist/events/agent-events.js +3 -0
  29. package/dist/events/agent-events.js.map +1 -1
  30. package/dist/events/agent-words.d.ts +1 -0
  31. package/dist/events/agent-words.d.ts.map +1 -1
  32. package/dist/events/agent-words.js +2 -2
  33. package/dist/events/agent-words.js.map +1 -1
  34. package/dist/events/review.d.ts +70 -0
  35. package/dist/events/review.d.ts.map +1 -0
  36. package/dist/events/review.js +29 -0
  37. package/dist/events/review.js.map +1 -0
  38. package/dist/events/system-events.d.ts +8 -0
  39. package/dist/events/system-events.d.ts.map +1 -1
  40. package/dist/events/system-events.js +2 -0
  41. package/dist/events/system-events.js.map +1 -1
  42. package/dist/events/transcript.d.ts +210 -0
  43. package/dist/events/transcript.d.ts.map +1 -1
  44. package/dist/events/transcript.js +2 -0
  45. package/dist/events/transcript.js.map +1 -1
  46. package/dist/events/undelivered.d.ts +6 -0
  47. package/dist/events/undelivered.d.ts.map +1 -0
  48. package/dist/events/undelivered.js +9 -0
  49. package/dist/events/undelivered.js.map +1 -0
  50. package/dist/index.d.ts +1086 -64
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/models/agent-standing.d.ts +1 -0
  53. package/dist/models/agent-standing.d.ts.map +1 -1
  54. package/dist/models/agent-standing.js +4 -1
  55. package/dist/models/agent-standing.js.map +1 -1
  56. package/dist/models/plan-pools.d.ts.map +1 -1
  57. package/dist/models/plan-pools.js +2 -2
  58. package/dist/models/plan-pools.js.map +1 -1
  59. package/dist/netd/generated/browser-wire.d.ts +2 -0
  60. package/dist/netd/generated/browser-wire.d.ts.map +1 -1
  61. package/dist/protocol/peer-dial.d.ts +2 -1
  62. package/dist/protocol/peer-dial.d.ts.map +1 -1
  63. package/dist/protocol/peer-dial.js.map +1 -1
  64. package/dist/protocol/raw/raw-routes.d.ts +4 -0
  65. package/dist/protocol/raw/raw-routes.d.ts.map +1 -1
  66. package/dist/protocol/raw/raw-routes.js +1 -0
  67. package/dist/protocol/raw/raw-routes.js.map +1 -1
  68. package/dist/schemas/agent.d.ts +4 -0
  69. package/dist/schemas/agent.d.ts.map +1 -1
  70. package/dist/schemas/agent.js +4 -0
  71. package/dist/schemas/agent.js.map +1 -1
  72. package/dist/schemas/agents-list.d.ts +1 -0
  73. package/dist/schemas/agents-list.d.ts.map +1 -1
  74. package/dist/schemas/agents.d.ts +3 -0
  75. package/dist/schemas/agents.d.ts.map +1 -1
  76. package/dist/schemas/ci.d.ts +16 -0
  77. package/dist/schemas/ci.d.ts.map +1 -1
  78. package/dist/schemas/ci.js +9 -0
  79. package/dist/schemas/ci.js.map +1 -1
  80. package/dist/schemas/dev-rebuild-log.d.ts +11 -0
  81. package/dist/schemas/dev-rebuild-log.d.ts.map +1 -0
  82. package/dist/schemas/dev-rebuild-log.js +30 -0
  83. package/dist/schemas/dev-rebuild-log.js.map +1 -0
  84. package/dist/schemas/devices.d.ts +1 -10
  85. package/dist/schemas/devices.d.ts.map +1 -1
  86. package/dist/schemas/devices.js +1 -29
  87. package/dist/schemas/devices.js.map +1 -1
  88. package/dist/schemas/git/index-run.d.ts +201 -0
  89. package/dist/schemas/git/index-run.d.ts.map +1 -0
  90. package/dist/schemas/git/index-run.js +48 -0
  91. package/dist/schemas/git/index-run.js.map +1 -0
  92. package/dist/schemas/keep-warm.d.ts +7 -0
  93. package/dist/schemas/keep-warm.d.ts.map +1 -1
  94. package/dist/schemas/keep-warm.js +25 -0
  95. package/dist/schemas/keep-warm.js.map +1 -1
  96. package/dist/schemas/providers/plan-limits.d.ts +29 -0
  97. package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
  98. package/dist/schemas/providers/plan-limits.js +27 -0
  99. package/dist/schemas/providers/plan-limits.js.map +1 -1
  100. package/dist/schemas/providers/usage.d.ts +4 -0
  101. package/dist/schemas/providers/usage.d.ts.map +1 -1
  102. package/dist/schemas/providers/usage.js +4 -0
  103. package/dist/schemas/providers/usage.js.map +1 -1
  104. package/dist/schemas/settings-experiments.d.ts +15 -0
  105. package/dist/schemas/settings-experiments.d.ts.map +1 -0
  106. package/dist/schemas/settings-experiments.js +52 -0
  107. package/dist/schemas/settings-experiments.js.map +1 -0
  108. package/dist/schemas/settings.d.ts +295 -13
  109. package/dist/schemas/settings.d.ts.map +1 -1
  110. package/dist/schemas/settings.js +14 -40
  111. package/dist/schemas/settings.js.map +1 -1
  112. package/dist/schemas/voice.d.ts +3 -0
  113. package/dist/schemas/voice.d.ts.map +1 -1
  114. package/dist/schemas/voice.js +29 -1
  115. package/dist/schemas/voice.js.map +1 -1
  116. package/dist/schemas/workspace/workspace-tree.d.ts +10 -0
  117. package/dist/schemas/workspace/workspace-tree.d.ts.map +1 -1
  118. package/dist/schemas/workspace/workspace-tree.js +15 -0
  119. package/dist/schemas/workspace/workspace-tree.js.map +1 -1
  120. package/dist/state/definition.d.ts +44 -12
  121. package/dist/state/definition.d.ts.map +1 -1
  122. package/dist/text/packed-formats.d.ts +2 -0
  123. package/dist/text/packed-formats.d.ts.map +1 -0
  124. package/dist/text/packed-formats.js +8 -0
  125. package/dist/text/packed-formats.js.map +1 -0
  126. package/dist/text/preview-guide.d.ts +15 -0
  127. package/dist/text/preview-guide.d.ts.map +1 -0
  128. package/dist/text/preview-guide.js +138 -0
  129. package/dist/text/preview-guide.js.map +1 -0
  130. package/dist/text/transcript-fold.d.ts +1 -1
  131. package/dist/text/transcript-fold.d.ts.map +1 -1
  132. package/dist/text/transcript-fold.js +14 -25
  133. package/dist/text/transcript-fold.js.map +1 -1
  134. package/dist/text/transcript-inline.d.ts +8 -0
  135. package/dist/text/transcript-inline.d.ts.map +1 -0
  136. package/dist/text/transcript-inline.js +28 -0
  137. package/dist/text/transcript-inline.js.map +1 -0
  138. package/dist/time/zone.d.ts +1 -0
  139. package/dist/time/zone.d.ts.map +1 -1
  140. package/dist/time/zone.js +6 -0
  141. package/dist/time/zone.js.map +1 -1
  142. package/package.json +5 -5
  143. package/src/contracts/agent.contract.ts +13 -0
  144. package/src/contracts/git.contract.ts +33 -0
  145. package/src/contracts/workspace.contract.ts +15 -0
  146. package/src/events/agent-events.ts +7 -0
  147. package/src/events/agent-words.test.ts +6 -0
  148. package/src/events/agent-words.ts +4 -2
  149. package/src/events/review.ts +44 -0
  150. package/src/events/system-events.ts +6 -0
  151. package/src/events/transcript.ts +3 -0
  152. package/src/events/undelivered.ts +15 -0
  153. package/src/models/agent-standing.ts +21 -3
  154. package/src/models/plan-pools.test.ts +12 -0
  155. package/src/models/plan-pools.ts +3 -2
  156. package/src/netd/generated/browser-wire.json +13 -0
  157. package/src/netd/generated/browser-wire.ts +1 -1
  158. package/src/protocol/peer-dial.ts +4 -2
  159. package/src/protocol/raw/raw-routes.ts +3 -0
  160. package/src/schemas/agent.ts +6 -0
  161. package/src/schemas/ci.ts +15 -0
  162. package/src/schemas/dev-rebuild-log.test.ts +1 -1
  163. package/src/schemas/dev-rebuild-log.ts +58 -0
  164. package/src/schemas/devices.ts +2 -54
  165. package/src/schemas/git/index-run.ts +80 -0
  166. package/src/schemas/keep-warm.ts +34 -0
  167. package/src/schemas/providers/plan-limits.ts +37 -0
  168. package/src/schemas/providers/usage.ts +9 -0
  169. package/src/schemas/settings-experiments.ts +80 -0
  170. package/src/schemas/settings.ts +28 -53
  171. package/src/schemas/voice.ts +40 -1
  172. package/src/schemas/workspace/workspace-tree.ts +20 -0
  173. package/src/text/packed-formats.ts +17 -0
  174. package/src/text/preview-guide.ts +159 -0
  175. package/src/text/transcript-fold.ts +20 -35
  176. package/src/text/transcript-inline.test.ts +43 -0
  177. package/src/text/transcript-inline.ts +38 -0
  178. package/src/time/zone.test.ts +18 -1
  179. package/src/time/zone.ts +12 -0
@@ -0,0 +1,80 @@
1
+ import { z } from "zod";
2
+ import { CommandRunSchema } from "../ci.js";
3
+ import { RepoParamSchema } from "../shared.js";
4
+ import { CommitResultSchema, GitTargetSchema } from "./git.js";
5
+
6
+ // Stage-all and commit-all as runs the daemon keeps, not requests held open: over a tree of tens of thousands of
7
+ // untracked files `git add -A` alone outlives the browser's header deadline (seconds), and a request that dies there
8
+ // leaves git running with nobody told how it ended. Modelled on the push run (PushRunSchema): started and answered at
9
+ // once, polled for progress and verdict, one per repository, a second start of the same thing joining the first.
10
+
11
+ // The guardrail's line, past which a run stops before staging and asks: a stage that size is almost always a dependency
12
+ // or build tree nobody meant to commit. Either one trips it.
13
+ export const INDEX_RUN_FILE_LIMIT = 2_000;
14
+ export const INDEX_RUN_BYTE_LIMIT = 100 * 1024 * 1024;
15
+
16
+ export const IndexRunOpSchema = z.enum(["stage", "commit"]);
17
+ export type IndexRunOp = z.infer<typeof IndexRunOpSchema>;
18
+
19
+ // Where a running run is, in the order it passes them; a plain commit skips counting and staging.
20
+ // waiting: queued behind another write on the same repository (an agent's land, another commit).
21
+ // counting: reading what the stage would take, for the guardrail.
22
+ // staging / committing: git is running.
23
+ // reading: re-reading the repository the commit just wrote, for the answer.
24
+ export const IndexRunPhaseSchema = z.enum(["waiting", "counting", "staging", "committing", "reading"]);
25
+
26
+ export const IndexRunDirSchema = z.object({
27
+ dir: z.string().describe("A top-level directory of the repository, or an empty string for the files directly at its root."),
28
+ files: z.number().describe("How many of the files the stage would take sit under it."),
29
+ bytes: z.number().optional().describe("Their combined size on disk. Absent when the run did not measure sizes, which it skips once the file count alone is over the line."),
30
+ });
31
+ export type IndexRunDir = z.infer<typeof IndexRunDirSchema>;
32
+
33
+ // What a held run found, so the question names the directories rather than only a number.
34
+ export const IndexRunHoldSchema = z.object({
35
+ files: z.number().describe("How many files the stage would take."),
36
+ bytes: z.number().optional().describe("Their combined size on disk, when measured."),
37
+ dirs: z.array(IndexRunDirSchema).describe("The largest top-level directories among them, biggest first, at most a handful."),
38
+ });
39
+ export type IndexRunHold = z.infer<typeof IndexRunHoldSchema>;
40
+
41
+ // Flat rather than a union on `op`: the route's path parameter merges into the input, which a discriminated union cannot
42
+ // take, and the one cross-field rule (a commit needs a message) is a refine.
43
+ export const IndexRunStartSchema = RepoParamSchema.extend({
44
+ op: IndexRunOpSchema.describe("Stage, or commit (staging `target` first when given)."),
45
+ target: GitTargetSchema.optional().describe(
46
+ "What to stage. For a stage, leave it out for the whole repository. For a commit, leave it out to record the index exactly as it stands, or give an empty object to stage everything first.",
47
+ ),
48
+ message: z.string().min(1).optional().describe("The commit message. Required for a commit, refused for a stage."),
49
+ confirmed: z
50
+ .boolean()
51
+ .optional()
52
+ .describe(
53
+ `Go ahead past the guardrail. Without it, a stage that would take more than ${String(INDEX_RUN_FILE_LIMIT)} files or ${String(INDEX_RUN_BYTE_LIMIT / 1024 / 1024)} MB stops before touching the index and settles holding what it found, for a person to confirm.`,
54
+ ),
55
+ })
56
+ .refine((start) => start.target === undefined || start.target.paths === undefined || start.target.scope === undefined, {
57
+ message: "name paths or a scope, not both",
58
+ })
59
+ .refine((start) => (start.op === "commit") === (start.message !== undefined), { message: "a commit takes a message and a stage does not" });
60
+ export type IndexRunStart = z.infer<typeof IndexRunStartSchema>;
61
+
62
+ export const IndexRunSchema = CommandRunSchema.extend({
63
+ id: z.string().describe("This run's own id, new on every start that did not join one already going. Empty when idle."),
64
+ repo: z.string().describe("The repository this run is about, the same id the routes take."),
65
+ op: IndexRunOpSchema.optional().describe("What was started. Absent when idle."),
66
+ phase: IndexRunPhaseSchema.optional().describe("Where a running run is. Absent once it settles."),
67
+ files: z
68
+ .object({
69
+ total: z.number().optional().describe("How many files the stage takes, once counted."),
70
+ done: z.number().optional().describe("How many of them are staged so far. git stages a whole repository in one go, so this jumps from 0 to the total."),
71
+ })
72
+ .optional()
73
+ .describe("Progress in files, where known."),
74
+ reason: z.string().optional().describe("Why it did not go, in git's own words where git said something."),
75
+ held: IndexRunHoldSchema.optional().describe(
76
+ "Present when the run stopped at the guardrail before touching anything (status `error`): what it would have staged. Start again with `confirmed` to go ahead.",
77
+ ),
78
+ result: CommitResultSchema.optional().describe("A commit's answer, the same one the commit call gives, once it passes."),
79
+ });
80
+ export type IndexRun = z.infer<typeof IndexRunSchema>;
@@ -131,3 +131,37 @@ export type PromptFingerprint = z.infer<typeof PromptFingerprintSchema>;
131
131
  /** Which parts differ between two fingerprints, by name; empty when they match. */
132
132
  export const changedParts = (before: PromptFingerprint, after: PromptFingerprint): string[] =>
133
133
  [...new Set([...Object.keys(before.parts), ...Object.keys(after.parts)])].filter((name) => before.parts[name] !== after.parts[name]).toSorted();
134
+
135
+ // keep-warm's knobs, one object since each only means anything beside the others (cache-keepwarm.ts).
136
+ export const KeepWarmSettingsSchema = z.object({
137
+ auto: z
138
+ .boolean()
139
+ .default(false)
140
+ .describe(
141
+ "Keep a Claude conversation's prompt cache warm after each turn a person asked for. Each refresh re-reads the cached context at the cache price and adds nothing to the conversation.",
142
+ ),
143
+ hours: z
144
+ .number()
145
+ .min(1)
146
+ .max(8)
147
+ .default(4)
148
+ .describe(
149
+ "How long an idle conversation is kept warm after its last turn, in hours, and what a press on one offers first. Shortened where refreshing would cost more than the cold resume it saves, and at midnight where the agent runs, when the date in its prompt changes.",
150
+ ),
151
+ minTokens: z
152
+ .number()
153
+ .int()
154
+ .min(0)
155
+ .default(100_000)
156
+ .describe("Only conversations at least this large, in tokens, are kept warm by `auto`: a small one is cheap to re-read anyway."),
157
+ reserve: z
158
+ .number()
159
+ .int()
160
+ .min(0)
161
+ .max(90)
162
+ .default(15)
163
+ .describe(
164
+ "How much of an account's usage limit, in percent, keeping conversations warm must leave untouched for real work. Refreshing stops once any limit that account's model spends is fuller than that.",
165
+ ),
166
+ });
167
+ export type KeepWarmSettings = z.infer<typeof KeepWarmSettingsSchema>;
@@ -148,6 +148,14 @@ export const ProviderRefusalSchema = z.object({
148
148
  .describe(
149
149
  "When the provider said to try again, in epoch seconds, for a spent allowance where it named one. Until then the refusal stands whatever a reading says; after it, it is over.",
150
150
  ),
151
+ // Codex's "To get more access now, send a request to your admin": a team's credit, which no usage endpoint reads,
152
+ // so a weekly pool at 57% says nothing about it (2026-10-17).
153
+ unmetered: z
154
+ .literal(true)
155
+ .optional()
156
+ .describe(
157
+ "Set on a spent allowance no usage reading measures, such as a credit an organisation's admin grants. No reading answers it: it stands until the provider's own reset, or until a turn is answered.",
158
+ ),
151
159
  });
152
160
  export type ProviderRefusal = z.infer<typeof ProviderRefusalSchema>;
153
161
  export const ProviderRefusalsSchema = z.object({
@@ -476,6 +484,35 @@ export const QueueScheduleSchema = z
476
484
  message: "a conversation cannot wait for its own work to land",
477
485
  });
478
486
  export type QueueSchedule = z.infer<typeof QueueScheduleSchema>;
487
+ // Books the conversation to carry on from where its last turn stopped, at an instant or once another conversation's work
488
+ // lands: a Continue (`agent.run` with `continues`) that waits in the queue, where every window draws it as scheduled,
489
+ // rather than words. What a spent allowance's "Continue at 05:00" books, where it once booked the word "Continue".
490
+ export const QueueContinueSchema = z
491
+ .object({
492
+ conversationId: z.string().min(1).describe("Which conversation carries on."),
493
+ sendAt: z
494
+ .number()
495
+ .int()
496
+ .positive()
497
+ .optional()
498
+ .describe("Carry on at this instant (epoch milliseconds). At most a month ahead; an instant already past carries on now."),
499
+ sendAfter: ConversationIdSchema.optional().describe("Carry on once that conversation has finished and all of its work has landed, instead."),
500
+ routing: ResumeRoutingSchema.optional().describe(
501
+ "Who serves the turn it starts. Leave it out to carry on where the conversation runs when it goes.",
502
+ ),
503
+ messageId: z
504
+ .string()
505
+ .min(1)
506
+ .optional()
507
+ .describe("Names the booking, so the same press sent twice books it once."),
508
+ })
509
+ .refine((input) => (input.sendAt === undefined) !== (input.sendAfter === undefined), {
510
+ message: "name exactly one of sendAt and sendAfter",
511
+ })
512
+ .refine((input) => input.sendAfter !== input.conversationId, {
513
+ message: "a conversation cannot wait for its own work to land",
514
+ });
515
+ export type QueueContinue = z.infer<typeof QueueContinueSchema>;
479
516
  // Moves a conversation to another account of the provider it runs on: the one command that changes who pays, so the
480
517
  // account a conversation runs on is always the daemon's record and never a window's guess.
481
518
  export const SwitchAccountSchema = z.object({
@@ -96,6 +96,15 @@ export const UsageTurnSchema = z.object({
96
96
  // Hash of the brief that was sent, recorded on control turns too. Load-bearing rather than decorative: the file is
97
97
  // rewritten monthly, so a 30-day window holds two different treatments and pooling them would measure neither.
98
98
  notesCohort: z.string().optional(),
99
+ // Arm of the tool-search experiment, stable per conversation: true loads connected tools' schemas on demand, false
100
+ // sends them all with every request. Absent where the turn was not a native Claude one on the Claude Code loop.
101
+ toolSearchArm: z.boolean().optional(),
102
+ // Arm of the stable-prompt experiment, stable per conversation: true says the guidance that moves between turns on
103
+ // the message when it changed, false keeps it in the system block. Absent outside the Claude Code loop.
104
+ stablePromptArm: z.boolean().optional(),
105
+ // Assigned on cold-eligible follow-ups only, including controls; cleared results only when a copy was resumed.
106
+ coldTrimArm: z.boolean().optional(),
107
+ coldTrimmed: z.number().int().min(0).optional(),
99
108
  // What became of pre-turn retrieval on this turn, and how long it took. Assignment and DELIVERY are different
100
109
  // facts: the first version of this mechanism was assigned to every eligible turn and reached four in five of them,
101
110
  // which is the difference between a null result and a mechanism that never ran. Absent means the flag was off, so
@@ -0,0 +1,80 @@
1
+ import { z } from "zod";
2
+
3
+ // Agent experiments share conversation-stable arms; the sandbox settings schema owns their persisted shape.
4
+ export const claudeExperimentSettings = {
5
+ // Claude Code turns itself out of tool search whenever its base URL is not Anthropic's own host, and the privacy
6
+ // shield's gateway is a loopback address, so every shielded native Claude turn paid every MCP tool's schema on every
7
+ // call (2026-10-18: a median 83k-token prompt before the first tool ran). The gateway forwards `tool_reference`
8
+ // blocks and the beta header untouched, so this asks for it by name. Native Claude only: a translator or minted
9
+ // endpoint may not speak the beta.
10
+ toolSearch: z
11
+ .boolean()
12
+ .default(true)
13
+ .describe(
14
+ "Load the schemas of connected tools (browsers, devices, accounts, phones) only when the agent looks one up, instead of sending all of them with every request. The tools the agent leans on every turn stay loaded. Applies to Claude models on the Claude Code loop.",
15
+ ),
16
+ // Both arms set the CLI's switch explicitly, so a shielded turn and an unshielded one compare the same two things.
17
+ toolSearchHoldout: z
18
+ .number()
19
+ .min(0)
20
+ .max(1)
21
+ .default(0.5)
22
+ .describe(
23
+ "What share of conversations to run with every tool loaded up front, so the two can be compared. Whole conversations rather than individual turns, because switching mid-conversation would rewrite the prompt the cache holds.",
24
+ ),
25
+ // Until 2026-10-18 this switch was read by nothing. The guidance paragraphs whose condition moves between turns (who
26
+ // is watching, which of the owner's machines, browsers and phones are online) sat in the system block, so each move
27
+ // rewrote the whole conversation to the provider's cache, and where Claude Code records a session's system prompt
28
+ // once the move never reached the model. Now they ride the turn's message, said when they changed.
29
+ stableSystemPrompt: z
30
+ .boolean()
31
+ .default(true)
32
+ .describe(
33
+ "Keep the instructions identical between turns so the provider can cache them, saying what changes between turns (who is watching, which of your computers, browsers and phones are connected) on the message instead, and only when it changed. Applies to the Claude Code loop.",
34
+ ),
35
+ stableSystemPromptHoldout: z
36
+ .number()
37
+ .min(0)
38
+ .max(1)
39
+ .default(0.5)
40
+ .describe(
41
+ "What share of conversations to run with those changing paragraphs kept in the instructions, so the two can be compared. Whole conversations rather than individual turns, because moving them mid-conversation would itself rewrite the cached prompt.",
42
+ ),
43
+ coldTrim: z
44
+ .boolean()
45
+ .default(true)
46
+ .describe(
47
+ "Resume large Claude Code conversations with older tool output cleared once their prompt cache has expired. Keeps the conversation and tool calls, with directions to recover cleared output, and shows a trim notice.",
48
+ ),
49
+ coldTrimHoldout: z
50
+ .number()
51
+ .min(0)
52
+ .max(1)
53
+ .default(0.5)
54
+ .describe(
55
+ "What share of conversations to resume whole when their cache is cold, so opening prompt size and failed tool calls can be compared. Whole conversations keep the same arm across follow-up turns.",
56
+ ),
57
+ coldTrimMinTokens: z
58
+ .number()
59
+ .int()
60
+ .min(0)
61
+ .default(100_000)
62
+ .describe(
63
+ "How many measured context tokens a Claude Code session must exceed before a cold follow-up can clear older tool output. Smaller sessions save little from trimming.",
64
+ ),
65
+ };
66
+
67
+ export const iqSearchSettings = {
68
+ iqSearch: z
69
+ .boolean()
70
+ .default(false)
71
+ .describe("Teach the agent how to use this workspace's own search tool, rather than leaving it to grep around."),
72
+ iqSearchHoldout: z
73
+ .number()
74
+ .min(0)
75
+ .max(1)
76
+ .default(0)
77
+ .describe(
78
+ "What share of conversations to run without that teaching, so the two can be compared. Whole conversations rather than individual turns, because once the teaching is in a session, withholding it from the next request does not make the model forget it.",
79
+ ),
80
+ };
@@ -2,10 +2,12 @@
2
2
  // itself, which those settings adopt.
3
3
  import { STATE_DIR } from "@intentic/constants";
4
4
  import { z } from "zod";
5
+ import { claudeExperimentSettings, iqSearchSettings } from "./settings-experiments.js";
5
6
  import { CommandJudgeModeSchema, ProjectInstallModeSchema } from "../policy/safety-policy.js";
6
7
  import { ModelRoleSchema } from "../models/model-roles.js";
7
8
  import { MODEL_SETUP_ID, ModelSetupSchema } from "../models/model-setups.js";
8
9
  import { AdmissionPolicySchema, AdmissionRuleSchema } from "./agent.js";
10
+ import { KeepWarmSettingsSchema } from "./keep-warm.js";
9
11
  import { LimitHandoffSchema } from "./providers/handoff.js";
10
12
  import { SessionsPlacementSchema } from "./offload.js";
11
13
  import { LimitPolicySchema, RetryPolicySchema, RoomPolicySchema } from "./turn-break.js";
@@ -211,39 +213,8 @@ export type SkillSwitch = z.infer<typeof SkillSwitchSchema>;
211
213
  // picks rather than failing whole. A setting removed or reshaped here gets its conversion in settings-history.ts, which
212
214
  // also names every retired setting, none of whose names may come back.
213
215
 
214
- // keep-warm's knobs, one object since each only means anything beside the others (cache-keepwarm.ts).
215
- export const KeepWarmSettingsSchema = z.object({
216
- auto: z
217
- .boolean()
218
- .default(false)
219
- .describe(
220
- "Keep a Claude conversation's prompt cache warm after each turn a person asked for. Each refresh re-reads the cached context at the cache price and adds nothing to the conversation.",
221
- ),
222
- hours: z
223
- .number()
224
- .min(1)
225
- .max(8)
226
- .default(4)
227
- .describe(
228
- "How long an idle conversation is kept warm after its last turn, in hours, and what a press on one offers first. Shortened where refreshing would cost more than the cold resume it saves, and at midnight where the agent runs, when the date in its prompt changes.",
229
- ),
230
- minTokens: z
231
- .number()
232
- .int()
233
- .min(0)
234
- .default(100_000)
235
- .describe("Only conversations at least this large, in tokens, are kept warm by `auto`: a small one is cheap to re-read anyway."),
236
- reserve: z
237
- .number()
238
- .int()
239
- .min(0)
240
- .max(90)
241
- .default(15)
242
- .describe(
243
- "How much of an account's usage limit, in percent, keeping conversations warm must leave untouched for real work. Refreshing stops once any limit that account's model spends is fuller than that.",
244
- ),
245
- });
246
- export type KeepWarmSettings = z.infer<typeof KeepWarmSettingsSchema>;
216
+ // keep-warm's knobs live with its clock (keep-warm.ts); re-exported, as every settings reader imports them from here.
217
+ export { type KeepWarmSettings, KeepWarmSettingsSchema } from "./keep-warm.js";
247
218
 
248
219
  // Who the editor's screens are written for: `developer` reads git's own words (branch, land, commit), `maker` plain ones
249
220
  // (draft, accept, save) over the same mechanisms. The editor's vocabulary, never anything an agent does. A person's own
@@ -273,12 +244,6 @@ export const SandboxSettingsSchema = z.object({
273
244
  .describe(
274
245
  "Which clock this sandbox's schedules are set by, as a zone name like Europe/Warsaw. Automations that repeat on a clock fire by this, not by the machine's own time. Leave it empty and they fire by UTC, which is almost certainly not what you meant when you typed a time.",
275
246
  ),
276
- stableSystemPrompt: z
277
- .boolean()
278
- .default(false)
279
- .describe(
280
- "Keep the instructions identical between turns so the provider can cache them, moving anything that varies into the message instead. Cheaper, at the cost of some flexibility.",
281
- ),
282
247
  skills: z.array(z.string()).default(["lsp", "fileq"]).describe("Which built-in tools are switched on. A skill of your own is not listed here: it is on while the agent's copy of it exists."),
283
248
  // One half of the reading a new chat gets between send and its first turn (chat-router.ts): with this on, the
284
249
  // message and one line per card are put to the same model that chooses what the chat runs on, in the same call.
@@ -306,6 +271,15 @@ export const SandboxSettingsSchema = z.object({
306
271
  .describe(
307
272
  "Keep the text of every document in the workspace (pdf, Word, OpenDocument, spreadsheets, presentations, epub, and scans where OCR is installed) ready to search. In the background, one low-priority process at a time, each document is rendered to text when it appears or changes, so `rg` and iq find what documents say under their own paths, cited by page. Off, nothing renders until someone opens a document, and searches skip them.",
308
273
  ),
274
+ // Whether the daemon tells its host it is unneeded (system/quiet-watch.ts): off, the work signal carries no
275
+ // `quietSince` and says `keepAwake`, so the machine's keeper never puts it to sleep (ic's `sandbox sleep`). Only a
276
+ // sandbox on somebody's own computer sleeps that way; a hosted machine's idle stop is the plan's and ignores this.
277
+ sleepWhenUnused: z
278
+ .boolean()
279
+ .default(true)
280
+ .describe(
281
+ "Let the computer this sandbox runs on put it to sleep after a stretch nobody needs it (no editor open, no agent working, no terminal typed in, no app of it open), which frees its memory. It starts again when somebody opens it, and a few minutes before anything it has scheduled comes due, so automations still run on time. Off keeps it running. Applies only to a sandbox on your own computer.",
282
+ ),
309
283
  hashlineEdits: z
310
284
  .boolean()
311
285
  .default(false)
@@ -329,18 +303,8 @@ export const SandboxSettingsSchema = z.object({
329
303
  .describe(
330
304
  "Your own instructions, used only when the mode above says custom. Then it is the whole of them: both built-in bases go, and so does everything this product would otherwise add, including the guidance the chat's own cards are driven by. That is the price of total control.",
331
305
  ),
332
- iqSearch: z
333
- .boolean()
334
- .default(false)
335
- .describe("Teach the agent how to use this workspace's own search tool, rather than leaving it to grep around."),
336
- iqSearchHoldout: z
337
- .number()
338
- .min(0)
339
- .max(1)
340
- .default(0)
341
- .describe(
342
- "What share of conversations to run without that teaching, so the two can be compared. Whole conversations rather than individual turns, because once the teaching is in a session, withholding it from the next request does not make the model forget it.",
343
- ),
306
+ ...iqSearchSettings,
307
+ ...claudeExperimentSettings,
344
308
  // Rooted at the run's own starting folder (a persona's, an isolated conversation's worktree), not the workspace
345
309
  // root — it maps the project containing that folder.
346
310
  workspaceMap: z
@@ -452,6 +416,10 @@ export const SandboxSettingsSchema = z.object({
452
416
  outagePolicy: RetryPolicySchema.default("wait").describe(
453
417
  "What happens to a turn the model provider's own failure killed. `wait` holds it for a press. `retry` re-runs it on the shared per-provider breaker, backing off between attempts. The sandbox-wide default; any one conversation can say otherwise. Worth `retry` for a sandbox whose work mostly happens with nobody in the room.",
454
418
  ),
419
+ // A vendor loop's hard cap on one turn; half an hour ended two Gemini children mid-work as failures (2026-10-17).
420
+ turnCapMinutes: z.number().int().min(5).max(1440).default(30).describe(
421
+ "The longest one turn may run on a loop the sandbox drives itself (OpenCode, an ACP agent, Pi, Muse) before it is ended, in minutes, however busy it keeps. A subagent's turn that reaches it is carried on in a fresh turn of the same session rather than ended as a failure. The Claude Code and Codex loops have no such cap.",
422
+ ),
455
423
  // The posture that used to live only in a browser tab; moving it here is what lets it fire with nothing open.
456
424
  stopPolicy: RetryPolicySchema.default("wait").describe(
457
425
  "What happens to a turn that stopped short with nothing to repair — a hung runtime, a crashed harness. `wait` holds it for a press. `retry` re-runs the held turn on a short ladder, standing down after three tries that got nowhere rather than looping forever.",
@@ -734,10 +702,14 @@ export const SavingsArmSchema = z.object({ turns: z.number(), mean: z.number() }
734
702
  // openedWithListing: whether a turn listed directories to orient itself, as a share of turns (the project map).
735
703
  // callsBeforeTarget: how far a turn walked before touching a file it went on to edit.
736
704
  // failedCalls: tool calls that ended in error (the field notes, whose largest section is a failure taxonomy).
705
+ // openingPrompt: tokens a turn's first request sent (cache reads plus cache writes), which is what every later call of
706
+ // the turn reads again (tool search, which moves the tool schemas out of it).
707
+ // openingWrite: tokens a follow-up turn's first request wrote to the cache instead of reading them (the stable prompt).
737
708
  // Every reading is the main agent's own: a subagent's calls are counted toward the turn and read for nothing.
738
- // Never cost: each mechanism moves one small part of a turn's work, inside the noise of the rest.
709
+ // Never cost: each mechanism moves one small part of a turn's work, inside the noise of the rest. The prompt's size is
710
+ // the exception that proves it: it is the part every call pays again, so it is read directly rather than through cost.
739
711
  export const TurnMetricReadingSchema = z.object({
740
- metric: z.enum(["searchCalls", "openingSearches", "openedWithListing", "callsBeforeTarget", "failedCalls"]),
712
+ metric: z.enum(["searchCalls", "openingSearches", "openedWithListing", "callsBeforeTarget", "failedCalls", "openingPrompt", "openingWrite"]),
741
713
  on: SavingsArmSchema,
742
714
  off: SavingsArmSchema,
743
715
  // Additional control turns to reach a fixed target resolution, not today's delta (which inherits noise and always
@@ -806,6 +778,9 @@ export const SavingsReportSchema = z.object({
806
778
  // Same absence rule as `search`: not measured, never zero.
807
779
  map: TurnExperimentSchema.optional(),
808
780
  notes: TurnExperimentSchema.optional(),
781
+ toolSearch: TurnExperimentSchema.optional(),
782
+ stablePrompt: TurnExperimentSchema.optional(),
783
+ coldTrim: TurnExperimentSchema.optional(),
809
784
  });
810
785
  export type SavingsReport = z.infer<typeof SavingsReportSchema>;
811
786
 
@@ -159,7 +159,13 @@ export const VoiceServerMessageSchema = z.discriminatedUnion("type", [
159
159
  type: z.literal("audio"),
160
160
  id: z.number(),
161
161
  sampleRate: z.number(),
162
- samples: z.number().describe("How many mono s16le samples the binary frame right after this one holds."),
162
+ samples: z.number().describe("How many mono samples the binary frame right after this one holds, as s16le or once decoded."),
163
+ codec: z
164
+ .literal("opus")
165
+ .optional()
166
+ .describe(
167
+ "Absent: the frame is s16le PCM. `opus`, only for a socket opened with `codec=opus`: the frame is the chunk's Opus packets, each a big-endian 16-bit length then the packet (opusPackets), one Opus stream per utterance and sample rate.",
168
+ ),
163
169
  }),
164
170
  z.object({ type: z.literal("done"), id: z.number() }),
165
171
  z.object({
@@ -170,3 +176,36 @@ export const VoiceServerMessageSchema = z.discriminatedUnion("type", [
170
176
  z.object({ type: z.literal("error"), id: z.number().optional(), message: z.string() }),
171
177
  ]);
172
178
  export type VoiceServerMessage = z.infer<typeof VoiceServerMessageSchema>;
179
+
180
+ // Opus on the voice sockets, for a client that asks with `codec=opus` where the daemon's /events hello names it among
181
+ // `voiceCodecs`: about a tenth of PCM's bytes (24 kbit/s for 16 kHz speech against 256, 33 for 24 kHz against 384).
182
+ // `/speech/stream` takes one 20 ms packet per binary frame; `/speech/say` sends each chunk's packets in one frame, framed
183
+ // by these two. A client that asks nothing is sent PCM, and an older daemon that ignores the ask sends PCM too.
184
+
185
+ /** Packets as one frame: each a big-endian 16-bit length, then the packet. */
186
+ export const framedOpus = (packets: readonly Uint8Array[]): Uint8Array<ArrayBuffer> => {
187
+ const frame = new Uint8Array(packets.reduce((total, packet) => total + 2 + packet.byteLength, 0));
188
+ const view = new DataView(frame.buffer);
189
+ let at = 0;
190
+ for (const packet of packets) {
191
+ view.setUint16(at, packet.byteLength);
192
+ frame.set(packet, at + 2);
193
+ at += 2 + packet.byteLength;
194
+ }
195
+ return frame;
196
+ };
197
+
198
+ /** The packets of a frame framedOpus made; a frame cut short ends at its last whole packet. */
199
+ export const opusPackets = (frame: Uint8Array): Uint8Array[] => {
200
+ const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
201
+ const packets: Uint8Array[] = [];
202
+ for (let at = 0; at + 2 <= frame.byteLength;) {
203
+ const length = view.getUint16(at);
204
+ if (at + 2 + length > frame.byteLength) {
205
+ break;
206
+ }
207
+ packets.push(frame.subarray(at + 2, at + 2 + length));
208
+ at += 2 + length;
209
+ }
210
+ return packets;
211
+ };
@@ -160,6 +160,26 @@ export const WorkspaceDownloadTicketSchema = z.object({
160
160
  expiresAt: z.number().describe("When it stops working, in milliseconds. It is meant to be used at once."),
161
161
  filename: z.string().describe("What the archive is saved as, so a caller can say what is on its way."),
162
162
  });
163
+ // Credential for GET /workspace/preview/{ticket}/*, which a frame loads a web page from so it runs as it would on a
164
+ // static file server: its scripts, modules, fetches and links resolve against real addresses. The ticket is the path's
165
+ // first segment, so every relative address the page names carries it on.
166
+ export const WorkspacePreviewTicketQuerySchema = WorkspaceScopeSchema.extend({
167
+ path: z.string().min(1).describe("The web page to preview, as a workspace path."),
168
+ source: z
169
+ .string()
170
+ .max(8 * 1024 * 1024)
171
+ .optional()
172
+ .describe("The page's text as the editor holds it, when it has edits not yet saved; served in place of the file on disk."),
173
+ });
174
+ export const WorkspacePreviewTicketSchema = z.object({
175
+ page: z.string().describe("The page's address on this daemon, a path to put after its base URL; the ticket is inside it."),
176
+ root: z
177
+ .string()
178
+ .describe(
179
+ "The folder the ticket serves, as a workspace path (empty for the workspace's root): the page's own repository. An address under the ticket names the file at the same place under this folder.",
180
+ ),
181
+ expiresAt: z.number().describe("When it stops working, in milliseconds."),
182
+ });
163
183
  // A read of a window: offset negative reads that many bytes from the end (for following a growing log); limit is
164
184
  // clamped to the daemon's own cap. Coerced from query strings.
165
185
  export const WorkspaceFileReadQuerySchema = WorkspaceScopeSchema.extend({
@@ -0,0 +1,17 @@
1
+ // File formats whose bytes are already compressed (images, audio, video, archives, office documents, fonts): encoding
2
+ // them again spends CPU and saves next to nothing. The daemon stores them as they are in a ZIP it streams, and the
3
+ // browser sends them plain where it gzips the rest of an upload, so both read the one list here.
4
+
5
+ const PACKED = new Set(
6
+ (
7
+ "7z aac apk avif br bz2 cab deb docx dmg epub flac gif gz heic heif ico jar jpeg jpg jxl lz lz4 lzma m4a m4v mkv mov mp3 mp4 mpeg mpg " +
8
+ "odg odp ods odt ogg ogv opus pdf png pptx rar rpm tbz tgz txz webm webp whl woff woff2 xlsx xz zip zst"
9
+ ).split(" "),
10
+ );
11
+
12
+ /** Whether a file of this name holds bytes that are already compressed, by its extension. */
13
+ export const isPackedName = (name: string): boolean => {
14
+ const base = name.slice(name.lastIndexOf("/") + 1);
15
+ const dot = base.lastIndexOf(".");
16
+ return dot > 0 && PACKED.has(base.slice(dot + 1).toLowerCase());
17
+ };