@intentic/sandbox-contract 1.226.0 → 1.227.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 (194) hide show
  1. package/dist/contracts/activity.contract.d.ts.map +1 -1
  2. package/dist/contracts/activity.contract.js +17 -2
  3. package/dist/contracts/activity.contract.js.map +1 -1
  4. package/dist/contracts/agent.contract.d.ts +19 -2
  5. package/dist/contracts/agent.contract.d.ts.map +1 -1
  6. package/dist/contracts/agent.contract.js +71 -8
  7. package/dist/contracts/agent.contract.js.map +1 -1
  8. package/dist/contracts/agents.contract.d.ts +59 -0
  9. package/dist/contracts/agents.contract.d.ts.map +1 -1
  10. package/dist/contracts/agents.contract.js +164 -19
  11. package/dist/contracts/agents.contract.js.map +1 -1
  12. package/dist/contracts/automations.contract.d.ts.map +1 -1
  13. package/dist/contracts/automations.contract.js +78 -9
  14. package/dist/contracts/automations.contract.js.map +1 -1
  15. package/dist/contracts/capabilities.contract.d.ts.map +1 -1
  16. package/dist/contracts/capabilities.contract.js +98 -11
  17. package/dist/contracts/capabilities.contract.js.map +1 -1
  18. package/dist/contracts/chores.contract.d.ts.map +1 -1
  19. package/dist/contracts/chores.contract.js +26 -3
  20. package/dist/contracts/chores.contract.js.map +1 -1
  21. package/dist/contracts/ci.contract.d.ts.map +1 -1
  22. package/dist/contracts/ci.contract.js +52 -6
  23. package/dist/contracts/ci.contract.js.map +1 -1
  24. package/dist/contracts/claude.contract.d.ts.map +1 -1
  25. package/dist/contracts/claude.contract.js +44 -5
  26. package/dist/contracts/claude.contract.js.map +1 -1
  27. package/dist/contracts/drafts.contract.d.ts.map +1 -1
  28. package/dist/contracts/drafts.contract.js +26 -3
  29. package/dist/contracts/drafts.contract.js.map +1 -1
  30. package/dist/contracts/endpoints.contract.d.ts +1 -0
  31. package/dist/contracts/endpoints.contract.d.ts.map +1 -1
  32. package/dist/contracts/endpoints.contract.js +17 -2
  33. package/dist/contracts/endpoints.contract.js.map +1 -1
  34. package/dist/contracts/exit.contract.d.ts.map +1 -1
  35. package/dist/contracts/exit.contract.js +62 -7
  36. package/dist/contracts/exit.contract.js.map +1 -1
  37. package/dist/contracts/extensions.contract.d.ts +1 -0
  38. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  39. package/dist/contracts/extensions.contract.js +124 -15
  40. package/dist/contracts/extensions.contract.js.map +1 -1
  41. package/dist/contracts/git.contract.d.ts.map +1 -1
  42. package/dist/contracts/git.contract.js +384 -43
  43. package/dist/contracts/git.contract.js.map +1 -1
  44. package/dist/contracts/grok.contract.d.ts.map +1 -1
  45. package/dist/contracts/grok.contract.js +25 -3
  46. package/dist/contracts/grok.contract.js.map +1 -1
  47. package/dist/contracts/history.contract.d.ts.map +1 -1
  48. package/dist/contracts/history.contract.js +35 -4
  49. package/dist/contracts/history.contract.js.map +1 -1
  50. package/dist/contracts/intentic.contract.d.ts.map +1 -1
  51. package/dist/contracts/intentic.contract.js +25 -3
  52. package/dist/contracts/intentic.contract.js.map +1 -1
  53. package/dist/contracts/inventory.contract.d.ts.map +1 -1
  54. package/dist/contracts/inventory.contract.js +26 -3
  55. package/dist/contracts/inventory.contract.js.map +1 -1
  56. package/dist/contracts/logs.contract.d.ts +17 -0
  57. package/dist/contracts/logs.contract.d.ts.map +1 -1
  58. package/dist/contracts/logs.contract.js +27 -3
  59. package/dist/contracts/logs.contract.js.map +1 -1
  60. package/dist/contracts/loops.contract.d.ts.map +1 -1
  61. package/dist/contracts/loops.contract.js +52 -6
  62. package/dist/contracts/loops.contract.js.map +1 -1
  63. package/dist/contracts/panels.contract.d.ts.map +1 -1
  64. package/dist/contracts/panels.contract.js +26 -3
  65. package/dist/contracts/panels.contract.js.map +1 -1
  66. package/dist/contracts/personas.contract.d.ts.map +1 -1
  67. package/dist/contracts/personas.contract.js +71 -8
  68. package/dist/contracts/personas.contract.js.map +1 -1
  69. package/dist/contracts/ports.contract.d.ts.map +1 -1
  70. package/dist/contracts/ports.contract.js +26 -3
  71. package/dist/contracts/ports.contract.js.map +1 -1
  72. package/dist/contracts/prepush.contract.d.ts.map +1 -1
  73. package/dist/contracts/prepush.contract.js +24 -3
  74. package/dist/contracts/prepush.contract.js.map +1 -1
  75. package/dist/contracts/providers.contract.d.ts +1 -0
  76. package/dist/contracts/providers.contract.d.ts.map +1 -1
  77. package/dist/contracts/providers.contract.js +9 -1
  78. package/dist/contracts/providers.contract.js.map +1 -1
  79. package/dist/contracts/public.contract.d.ts.map +1 -1
  80. package/dist/contracts/public.contract.js +26 -3
  81. package/dist/contracts/public.contract.js.map +1 -1
  82. package/dist/contracts/push.contract.d.ts.map +1 -1
  83. package/dist/contracts/push.contract.js +35 -4
  84. package/dist/contracts/push.contract.js.map +1 -1
  85. package/dist/contracts/secrets.contract.d.ts.map +1 -1
  86. package/dist/contracts/secrets.contract.js +43 -5
  87. package/dist/contracts/secrets.contract.js.map +1 -1
  88. package/dist/contracts/sessions.contract.d.ts +3 -0
  89. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  90. package/dist/contracts/sessions.contract.js +15 -2
  91. package/dist/contracts/sessions.contract.js.map +1 -1
  92. package/dist/contracts/settings.contract.d.ts +19 -0
  93. package/dist/contracts/settings.contract.d.ts.map +1 -1
  94. package/dist/contracts/settings.contract.js +43 -5
  95. package/dist/contracts/settings.contract.js.map +1 -1
  96. package/dist/contracts/share.contract.d.ts.map +1 -1
  97. package/dist/contracts/share.contract.js +35 -4
  98. package/dist/contracts/share.contract.js.map +1 -1
  99. package/dist/contracts/skills.contract.d.ts.map +1 -1
  100. package/dist/contracts/skills.contract.js +35 -4
  101. package/dist/contracts/skills.contract.js.map +1 -1
  102. package/dist/contracts/system.contract.d.ts +7 -0
  103. package/dist/contracts/system.contract.d.ts.map +1 -1
  104. package/dist/contracts/system.contract.js +107 -14
  105. package/dist/contracts/system.contract.js.map +1 -1
  106. package/dist/contracts/translator.contract.d.ts.map +1 -1
  107. package/dist/contracts/translator.contract.js +29 -4
  108. package/dist/contracts/translator.contract.js.map +1 -1
  109. package/dist/contracts/usage.contract.d.ts.map +1 -1
  110. package/dist/contracts/usage.contract.js +9 -1
  111. package/dist/contracts/usage.contract.js.map +1 -1
  112. package/dist/contracts/vpn.contract.d.ts.map +1 -1
  113. package/dist/contracts/vpn.contract.js +32 -4
  114. package/dist/contracts/vpn.contract.js.map +1 -1
  115. package/dist/contracts/workflows.contract.d.ts.map +1 -1
  116. package/dist/contracts/workflows.contract.js +70 -8
  117. package/dist/contracts/workflows.contract.js.map +1 -1
  118. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  119. package/dist/contracts/workspace.contract.js +216 -25
  120. package/dist/contracts/workspace.contract.js.map +1 -1
  121. package/dist/events.d.ts +46 -0
  122. package/dist/events.d.ts.map +1 -1
  123. package/dist/events.js +193 -88
  124. package/dist/events.js.map +1 -1
  125. package/dist/history-state.d.ts.map +1 -1
  126. package/dist/history-state.js +1 -0
  127. package/dist/history-state.js.map +1 -1
  128. package/dist/index.d.ts +197 -70
  129. package/dist/index.d.ts.map +1 -1
  130. package/dist/index.js +2 -0
  131. package/dist/index.js.map +1 -1
  132. package/dist/prompt-complexity.d.ts +9 -1
  133. package/dist/prompt-complexity.d.ts.map +1 -1
  134. package/dist/prompt-complexity.js +13 -5
  135. package/dist/prompt-complexity.js.map +1 -1
  136. package/dist/request-id.d.ts +3 -0
  137. package/dist/request-id.d.ts.map +1 -0
  138. package/dist/request-id.js +3 -0
  139. package/dist/request-id.js.map +1 -0
  140. package/dist/schemas.d.ts +89 -0
  141. package/dist/schemas.d.ts.map +1 -1
  142. package/dist/schemas.js +2912 -1276
  143. package/dist/schemas.js.map +1 -1
  144. package/dist/starter.d.ts +3 -0
  145. package/dist/starter.d.ts.map +1 -0
  146. package/dist/starter.js +3 -0
  147. package/dist/starter.js.map +1 -0
  148. package/package.json +5 -4
  149. package/src/contract-lock.test.ts +8 -2
  150. package/src/contracts/activity.contract.ts +18 -2
  151. package/src/contracts/agent.contract.ts +78 -8
  152. package/src/contracts/agents.contract.ts +180 -19
  153. package/src/contracts/automations.contract.ts +80 -9
  154. package/src/contracts/capabilities.contract.ts +106 -11
  155. package/src/contracts/chores.contract.ts +29 -3
  156. package/src/contracts/ci.contract.ts +55 -6
  157. package/src/contracts/claude.contract.ts +47 -5
  158. package/src/contracts/drafts.contract.ts +26 -3
  159. package/src/contracts/endpoints.contract.ts +19 -2
  160. package/src/contracts/exit.contract.ts +69 -7
  161. package/src/contracts/extensions.contract.ts +134 -15
  162. package/src/contracts/git.contract.ts +421 -43
  163. package/src/contracts/grok.contract.ts +26 -3
  164. package/src/contracts/history.contract.ts +37 -4
  165. package/src/contracts/intentic.contract.ts +28 -3
  166. package/src/contracts/inventory.contract.ts +27 -3
  167. package/src/contracts/logs.contract.ts +41 -4
  168. package/src/contracts/loops.contract.ts +58 -6
  169. package/src/contracts/panels.contract.ts +26 -3
  170. package/src/contracts/personas.contract.ts +77 -8
  171. package/src/contracts/ports.contract.ts +27 -3
  172. package/src/contracts/prepush.contract.ts +26 -3
  173. package/src/contracts/providers.contract.ts +10 -1
  174. package/src/contracts/public.contract.ts +29 -3
  175. package/src/contracts/push.contract.ts +37 -4
  176. package/src/contracts/secrets.contract.ts +46 -5
  177. package/src/contracts/sessions.contract.ts +16 -2
  178. package/src/contracts/settings.contract.ts +46 -5
  179. package/src/contracts/share.contract.ts +37 -4
  180. package/src/contracts/skills.contract.ts +39 -4
  181. package/src/contracts/system.contract.ts +119 -14
  182. package/src/contracts/translator.contract.ts +32 -4
  183. package/src/contracts/usage.contract.ts +10 -1
  184. package/src/contracts/vpn.contract.ts +35 -4
  185. package/src/contracts/workflows.contract.ts +77 -8
  186. package/src/contracts/workspace.contract.ts +231 -25
  187. package/src/events.ts +259 -89
  188. package/src/history-state.ts +6 -0
  189. package/src/index.ts +2 -0
  190. package/src/prompt-complexity.test.ts +69 -0
  191. package/src/prompt-complexity.ts +102 -39
  192. package/src/request-id.ts +41 -0
  193. package/src/schemas.ts +3700 -1260
  194. package/src/starter.ts +13 -0
@@ -158,3 +158,72 @@ test("the score never leaves 0..1, so a stored row is always comparable against
158
158
  expect(floor.score).toBeGreaterThanOrEqual(0);
159
159
  expect(ceiling.score).toBeLessThanOrEqual(1);
160
160
  });
161
+
162
+ // --- the one dial, and the property it may not move ------------------------------------------------------
163
+
164
+ /* The owner can move the cutoff (settings.autoTierEagerness) because "err toward my model or toward the cheap
165
+ * one" is a preference nobody else can hold for them. What these pin is that the dial moves the cutoff and
166
+ * NOTHING else, in particular not the rule that a downgrade needs something positively easy to have been said,
167
+ * which was the property the old ceiling held only by arithmetic coincidence. */
168
+
169
+ test("the dial widens what counts as simple, in the direction it says", () => {
170
+ // A question about a real file: eased by its words, held back by naming a path. The middle stop keeps it on
171
+ // the user's pick, and the eager stop is precisely the choice to let it through.
172
+ const aboutAFile = `explain what src/app.ts does`;
173
+
174
+ expect(tierOf(aboutAFile, { eagerness: `balanced` })).toBe(`standard`);
175
+ expect(tierOf(aboutAFile, { eagerness: `eager` })).toBe(`fast`);
176
+ });
177
+
178
+ test("the cautious stop wants every easing signal at once, not merely an easy word", () => {
179
+ // Its whole content: leave no room for doubt. A statement in easy words still qualifies at the default,
180
+ // and does not here; only a short bare question naming no file survives every stop.
181
+ expect(tierOf(`explain closures`, { eagerness: `balanced` })).toBe(`fast`);
182
+ expect(tierOf(`explain closures`, { eagerness: `cautious` })).toBe(`standard`);
183
+ expect(tierOf(`what is a closure?`, { eagerness: `cautious` })).toBe(`fast`);
184
+ });
185
+
186
+ test("an absent dial is the balanced stop, so every row recorded before it existed still compares", () => {
187
+ const bare = judgeComplexity(turn(`what is this?`));
188
+
189
+ expect(bare.ceiling).toBe(FAST_CEILING);
190
+ expect(bare.tier).toBe(judgeComplexity(turn(`what is this?`, { eagerness: `balanced` })).tier);
191
+ });
192
+
193
+ test("no setting of the dial can downgrade a short vague request", () => {
194
+ // The whole safety argument for offering an eager stop at all. Absence of complexity is not evidence of
195
+ // simplicity at ANY cutoff, so this is a rule in the judge rather than a sum that happens to clear it.
196
+ for (const eagerness of [`cautious`, `balanced`, `eager`] as const) {
197
+ expect(tierOf(`fix the bug`, { eagerness })).toBe(`standard`);
198
+ expect(tierOf(`have a look at the thing we discussed`, { eagerness })).toBe(`standard`);
199
+ }
200
+ });
201
+
202
+ test("the deceptive follow-up is standard at every stop, because it never says anything easy", () => {
203
+ /* "now do the same for the other file" is the case the whole afterHardTurn signal was built for, and the
204
+ * easing rule turns out to answer it more strongly than any cutoff can: those words make no positive claim
205
+ * of ease, so no setting of the dial reaches them. The weight still does its own job one test down. */
206
+ for (const eagerness of [`cautious`, `balanced`, `eager`] as const) {
207
+ expect(tierOf(`now do the same for the other file`, { eagerness, afterHardTurn: true })).toBe(`standard`);
208
+ }
209
+ });
210
+
211
+ test("a turn following hard work has to clear a higher bar, and at the default an eased one no longer does", () => {
212
+ // It raises the bar rather than locking the door (see ComplexityInput.afterHardTurn), so this is a shift of
213
+ // one stop's worth, not a gate: the same words that route in a fresh conversation stay put in a hard one.
214
+ expect(tierOf(`explain closures`)).toBe(`fast`);
215
+ expect(tierOf(`explain closures`, { afterHardTurn: true })).toBe(`standard`);
216
+ });
217
+
218
+ test("the verdict carries the cutoff it was judged against, because a score alone stopped being an answer", () => {
219
+ // With the cutoff an owner setting, the same 0.35 is standard on one stop and fast on another. A ledger of
220
+ // bare scores could not tell those two rows apart; the ceiling beside each is what keeps a refit honest.
221
+ const cautious = judgeComplexity(turn(`explain closures`, { eagerness: `cautious` }));
222
+ const eager = judgeComplexity(turn(`explain closures`, { eagerness: `eager` }));
223
+
224
+ expect(cautious.ceiling).toBeLessThan(FAST_CEILING);
225
+ expect(eager.ceiling).toBeGreaterThan(FAST_CEILING);
226
+ // Same words, same score, opposite verdicts: the pair that a column of bare scores could not have told apart.
227
+ expect(cautious.score).toBe(eager.score);
228
+ expect([cautious.tier, eager.tier]).toEqual([`standard`, `fast`]);
229
+ });
@@ -89,6 +89,10 @@ export interface ComplexityInput {
89
89
  * or the feature switched off) says nothing about the difficulty of the work, and reading it as escalation
90
90
  * would make the sandbox's configuration leak into its opinion about a sentence. */
91
91
  readonly afterHardTurn: boolean;
92
+ /* HOW EAGER THE OWNER ASKED THIS TO BE (settings.autoTierEagerness), the one preference the judge takes.
93
+ * Absent ⇒ `balanced`, which is the stop every verdict recorded before the knob existed was judged
94
+ * against, so an absent value and an old row mean the same thing. See FAST_CEILINGS. */
95
+ readonly eagerness?: TierEagerness;
92
96
  }
93
97
 
94
98
  export interface ComplexityVerdict {
@@ -99,6 +103,10 @@ export interface ComplexityVerdict {
99
103
  // Every rule that fired, in declaration order. Empty is legal and means "nothing distinctive": the score is
100
104
  // the base, which sits above the fast ceiling, so an unremarkable turn stays on the user's own pick.
101
105
  readonly rules: readonly ComplexityRule[];
106
+ /* The cutoff this score was judged against, carried out so the caller can record it beside the score.
107
+ * A score is only half a verdict once the ceiling is a setting: 0.35 was standard yesterday and is fast
108
+ * today, and a refit reading a column of bare scores could not tell those rows apart. */
109
+ readonly ceiling: number;
102
110
  }
103
111
 
104
112
  /* WHERE AN UNREMARKABLE TURN STARTS, and it starts ABOVE the fast ceiling on purpose: a prompt that matches no
@@ -107,10 +115,35 @@ export interface ComplexityVerdict {
107
115
  * escalation, and a user who stops trusting the feature. */
108
116
  const BASE_SCORE = 0.5;
109
117
 
110
- // Fast iff the score lands at or below this. Exported because the shadow ledger's whole purpose is to let this
111
- // be re-fitted against real traffic rather than argued about, and a reader of a stored score needs the ceiling
112
- // that score was judged against.
113
- export const FAST_CEILING = 0.25;
118
+ /* HOW EAGER THE JUDGE IS, the one knob this feature exposes, and deliberately the only one.
119
+ *
120
+ * The routing literature's own answer to "how do you tune a router in public" is a single aggressiveness
121
+ * threshold (RouteLLM ships it in the model name, `router-mf-0.3` against `router-mf-0.7`); everything else it
122
+ * learns stays inside. Same here: the weights below are a hypothesis with a ledger under them and are nobody's
123
+ * business, while "should this err toward my model or toward the cheap one" is a preference only the owner can
124
+ * hold, and the shadow numbers are useless without a way to act on what they say.
125
+ *
126
+ * THREE NAMED STOPS, not a slider, because the page has no slider idiom and, more to the point, a continuous
127
+ * control here invites fiddling with a number whose meaning nobody can feel. Each stop is a sentence about
128
+ * which turns move:
129
+ * cautious — every easing signal at once and nothing pulling the other way: a short bare question, in easy
130
+ * words, naming no file. "what is a closure?" and very little else. The zero is not a disabled
131
+ * state, it is the floor the score clamps to, so it means exactly "leave no room for doubt".
132
+ * balanced — the shipped default, and what every stored verdict before this knob existed was judged against.
133
+ * Easy words carry a turn on their own; naming a file still holds it back.
134
+ * eager — an easy-worded question about real code goes too ("explain what this file does").
135
+ *
136
+ * NONE OF THEM CAN REACH THE ABSENCE FEATURES, at any setting, because that property is enforced structurally
137
+ * now rather than by the weights happening to sum above the ceiling (see `easing` below). That is what makes an
138
+ * eager stop safe to offer at all: raising a bare number would, at 0.3, have started downgrading every short
139
+ * vague request in the product, which is the single worst population to be wrong about. */
140
+ export const FAST_CEILINGS = { cautious: 0, balanced: 0.25, eager: 0.4 } as const;
141
+ export type TierEagerness = keyof typeof FAST_CEILINGS;
142
+
143
+ // The stop a turn is judged against when nobody has chosen one, and the one every verdict recorded before the
144
+ // knob existed was judged against. Exported because a reader of a stored score needs the ceiling behind it, and
145
+ // a row written before ceilings were recorded was written against exactly this.
146
+ export const FAST_CEILING = FAST_CEILINGS.balanced;
114
147
 
115
148
  // Characters, not tokens: nothing here can tokenize, and for a threshold the constant cancels. ~600 chars is
116
149
  // where a request stops being a sentence and starts being a brief; ~2400 is where it is carrying pasted
@@ -215,38 +248,56 @@ const forcing = (input: ComplexityInput, text: string): ComplexityRule[] => {
215
248
  * nothing, which is exactly why the mechanism ships in shadow first — see docs/model-routing-design.md §4. They
216
249
  * are not a claim, they are a hypothesis with a ledger under it.
217
250
  *
218
- * ONE PROPERTY IS NOT A HYPOTHESIS AND MUST SURVIVE ANY REFIT: the two ABSENCE features (`short-prompt`,
219
- * `no-workspace-reference`) are too light to reach the ceiling together. Absence of complexity is not evidence
220
- * of simplicity "fix the bug" is four words naming no file and is not a cheap turn so a downgrade always
221
- * requires something POSITIVE to have been said, which in practice means `easy-words` or `bare-question`
222
- * carrying it. Weighted the obvious way instead, the judge downgraded every short vague request in the
223
- * product, which is the single worst population to be wrong about. */
224
- const GRADED: readonly { readonly rule: ComplexityRule; readonly weight: number; readonly of: (input: ComplexityInput, text: string) => boolean }[] =
225
- [
226
- { rule: "medium-prompt", weight: +0.2, of: (_input, text) => text.length > MEDIUM_PROMPT_CHARS },
227
- { rule: "attachment", weight: +0.15, of: (input) => input.attachments > 0 },
228
- { rule: "editor-context", weight: +0.1, of: (input) => input.editorContext },
229
- // Enough on its own to hold an easy-worded question at standard: "explain what this file does" is a
230
- // question about real code in this repo, and the cheap rung's failures on real code are the silent kind.
231
- { rule: "paths", weight: +0.15, of: (_input, text) => PATH_LIKE.test(text) },
232
- {
233
- rule: "many-verbs",
234
- weight: +0.15,
235
- of: (_input, text) => new Set((text.match(VERBS) ?? []).map((verb) => verb.toLowerCase())).size >= MANY_VERBS,
236
- },
237
- // The heaviest single weight, because it is the only feature that can see past the words. See
238
- // ComplexityInput.afterHardTurn for why it is a weight rather than the gate it was designed as.
239
- { rule: "after-hard-turn", weight: +0.25, of: (input) => input.afterHardTurn },
240
- // The two absence features, deliberately light; see the note above the list.
241
- { rule: "short-prompt", weight: -0.1, of: (_input, text) => text.length <= SHORT_PROMPT_CHARS },
242
- { rule: "easy-words", weight: -0.25, of: (_input, text) => EASY_WORDS.test(text) },
243
- { rule: "bare-question", weight: -0.15, of: (_input, text) => BARE_QUESTION.test(text) },
244
- {
245
- rule: "no-workspace-reference",
246
- weight: -0.1,
247
- of: (input, text) => input.attachments === 0 && !input.editorContext && !PATH_LIKE.test(text),
248
- },
249
- ];
251
+ * ONE PROPERTY IS NOT A HYPOTHESIS AND MUST SURVIVE ANY REFIT, and it is a RULE here rather than an accident of
252
+ * arithmetic: absence of complexity is not evidence of simplicity. "fix the bug" is four words naming no file
253
+ * and is not a cheap turn, so a downgrade always requires something POSITIVE to have been saidan easy word,
254
+ * or a bare question. Those two are marked `easing`, and `judgeComplexity` refuses a fast verdict without one
255
+ * whatever the score says.
256
+ *
257
+ * It used to hold only because the two ABSENCE features (`short-prompt`, `no-workspace-reference`) summed to
258
+ * 0.3 against a ceiling of 0.25, which is a coincidence of two numbers rather than a property, and the moment
259
+ * the ceiling became a setting it was one click from being false. Weighted the obvious way, the judge
260
+ * downgraded every short vague request in the product, which is the single worst population to be wrong
261
+ * about. */
262
+ interface GradedFeature {
263
+ readonly rule: ComplexityRule;
264
+ readonly weight: number;
265
+ readonly of: (input: ComplexityInput, text: string) => boolean;
266
+ // A POSITIVE reason to think this is easy, as opposed to the mere absence of reasons to think it is hard.
267
+ // At least one has to fire before any turn is called fast; see the note above.
268
+ readonly easing?: true;
269
+ }
270
+
271
+ const GRADED: readonly GradedFeature[] = [
272
+ { rule: "medium-prompt", weight: +0.2, of: (_input, text) => text.length > MEDIUM_PROMPT_CHARS },
273
+ { rule: "attachment", weight: +0.15, of: (input) => input.attachments > 0 },
274
+ { rule: "editor-context", weight: +0.1, of: (input) => input.editorContext },
275
+ // Enough on its own to hold an easy-worded question at standard on the middle stop: "explain what this file
276
+ // does" is a question about real code in this repo, and the cheap rung's failures on real code are the
277
+ // silent kind. The `eager` stop is precisely the choice to let that one through.
278
+ { rule: "paths", weight: +0.15, of: (_input, text) => PATH_LIKE.test(text) },
279
+ {
280
+ rule: "many-verbs",
281
+ weight: +0.15,
282
+ of: (_input, text) => new Set((text.match(VERBS) ?? []).map((verb) => verb.toLowerCase())).size >= MANY_VERBS,
283
+ },
284
+ // The heaviest single weight, because it is the only feature that can see past the words. See
285
+ // ComplexityInput.afterHardTurn for why it is a weight rather than the gate it was designed as. Heavy enough
286
+ // that even the eager stop cannot route a follow-up to hard work on easy words alone.
287
+ { rule: "after-hard-turn", weight: +0.25, of: (input) => input.afterHardTurn },
288
+ /* The easing half, in the order it has always been declared, because that order is the order a verdict
289
+ * lists its rules in and the ledger has rows written against it. `easing` marks the two POSITIVE ones, one
290
+ * of which every fast verdict must carry; the other two are the absence features, deliberately light and,
291
+ * by the rule above, never enough by themselves. */
292
+ { rule: "short-prompt", weight: -0.1, of: (_input, text) => text.length <= SHORT_PROMPT_CHARS },
293
+ { rule: "easy-words", weight: -0.25, easing: true, of: (_input, text) => EASY_WORDS.test(text) },
294
+ { rule: "bare-question", weight: -0.15, easing: true, of: (_input, text) => BARE_QUESTION.test(text) },
295
+ {
296
+ rule: "no-workspace-reference",
297
+ weight: -0.1,
298
+ of: (input, text) => input.attachments === 0 && !input.editorContext && !PATH_LIKE.test(text),
299
+ },
300
+ ];
250
301
 
251
302
  // Three places, so a stored score is a value rather than a float artefact and two rows written by the same
252
303
  // rules compare equal.
@@ -258,14 +309,26 @@ const round3 = (value: number): number => Math.round(value * 1000) / 1000;
258
309
  * up a tier, never silently move a different set of turns down.
259
310
  *
260
311
  * Only what survives all of that gets scored, which keeps the graded layer doing the one job it is good at:
261
- * separating "explain this" from "wire this up" among requests that look alike. */
312
+ * separating "explain this" from "wire this up" among requests that look alike.
313
+ *
314
+ * TWO CONDITIONS FOR FAST, not one, and the second is the one that does not move: the score has to clear the
315
+ * owner's chosen ceiling AND something POSITIVE has to have been said (a `easing` feature). The ceiling is a
316
+ * preference and belongs to whoever pays the bill; "we downgraded it because you didn't say much" is not a
317
+ * preference, it is a bug, and it stays impossible at every stop of the knob. */
262
318
  export const judgeComplexity = (input: ComplexityInput): ComplexityVerdict => {
319
+ const ceiling = FAST_CEILINGS[input.eagerness ?? "balanced"];
263
320
  const text = input.prompt.trim();
264
321
  const forced = forcing(input, text);
265
322
  if (forced.length > 0) {
266
- return { tier: "standard", score: 1, rules: forced };
323
+ return { tier: "standard", score: 1, rules: forced, ceiling };
267
324
  }
268
325
  const hits = GRADED.filter((feature) => feature.of(input, text));
269
326
  const score = Math.min(1, Math.max(0, BASE_SCORE + hits.reduce((total, feature) => total + feature.weight, 0)));
270
- return { tier: score <= FAST_CEILING ? "fast" : "standard", score: round3(score), rules: hits.map((feature) => feature.rule) };
327
+ const eased = hits.some((feature) => feature.easing === true);
328
+ return {
329
+ tier: eased && score <= ceiling ? "fast" : "standard",
330
+ score: round3(score),
331
+ rules: hits.map((feature) => feature.rule),
332
+ ceiling,
333
+ };
271
334
  };
@@ -0,0 +1,41 @@
1
+ /* THE ONE HEADER THAT JOINS A BROWSER CALL TO THE DAEMON LINE THAT SERVED IT.
2
+ *
3
+ * Both halves of a slow interaction were already measured and could not be put together. The browser times
4
+ * `rpc.request`, what the user actually waited for; the daemon times `http.request`, what it served; and on a
5
+ * sandbox answering several calls a second the only way to pair them was by timestamp and hope. So "the panel
6
+ * stuttered" stayed unattributable even with both numbers in hand, which is the gap the daemon's own perf module
7
+ * says it exists to close and could only close on its own side of the wire.
8
+ *
9
+ * Here rather than as a literal on each side, because a header name that disagrees across the wire fails
10
+ * SILENTLY: the browser sends a field nobody reads and the daemon logs nothing, which looks exactly like a quiet
11
+ * system. Both sides import this.
12
+ *
13
+ * The NAME only. The browser mints the value with the app's own `composables/uuid.ts`, which already solves the
14
+ * one hard part (`crypto.randomUUID` is a secure-context api and a self-hosted instance on a LAN address does
15
+ * not have it), and the daemon only ever reads what it is given. A generator here would be a second answer to a
16
+ * question that already has a better one. The value is a correlation token and never a security boundary: the
17
+ * daemon records it and decides nothing on it, so a caller repeating one confuses a log and nothing else. */
18
+ export const REQUEST_ID_HEADER = "x-intentic-request-id";
19
+
20
+ /* WHEN A BROWSER MAY SEND IT, and this is not the additive change it looks like.
21
+ *
22
+ * A custom request header is the one wire addition that is NOT backward compatible, because the browser does not
23
+ * get to decide it: a header outside the CORS safelist forces a preflight, and a daemon whose `allowHeaders`
24
+ * predates this name answers a preflight that omits it. The browser then fails the whole request rather than
25
+ * dropping the header. Sent unconditionally, this turns "one field the daemon never logs" into EVERY typed call
26
+ * to that daemon failing at the transport, `system.events` included, so the stream never opens, the connection
27
+ * never reaches `online`, and the app settles on "Busy, catching up" forever against a sandbox that is up and
28
+ * healthy and answering `/health` in a millisecond.
29
+ *
30
+ * That state is not exotic. A browser newer than its daemon is the NORMAL case and a supported one, both in
31
+ * production (every user's sandbox runs whatever image they last chose to pull, and COMPATIBILITY.md's second
32
+ * promise is that no update is ever forced) and in development (the web app runs from the working tree, the
33
+ * daemon is baked into the last `pnpm build:sandbox`). See the web app's useDaemonRoutes.ts, which exists to say
34
+ * exactly this.
35
+ *
36
+ * So the header ships gated on POSITIVE evidence, and this names the evidence: `logs.report` is the route that
37
+ * landed in the same commit as the `allowHeaders` entry, so a daemon advertising it on its hello frame is a
38
+ * daemon whose CORS accepts the header. Note the polarity is the opposite of `supportsRoute`'s: there, no
39
+ * evidence means assume-supported, because hiding a working feature is the greater harm; here, no evidence must
40
+ * mean DO NOT SEND, because sending costs the entire connection and the header buys a log field. */
41
+ export const REQUEST_ID_EVIDENCE_ROUTE = "logs.report";