@orboto/mcp 0.193.0 → 0.194.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 (131) hide show
  1. package/dist/event-bridge.js +1 -47
  2. package/dist/event-bridge.test.js +0 -19
  3. package/dist/http-transport-binding.test.js +1 -4
  4. package/dist/http-transport.js +1 -213
  5. package/dist/http-transport.test.js +0 -56
  6. package/dist/index.js +1 -48
  7. package/dist/input-schema.js +2 -76
  8. package/dist/input-schema.test.js +1 -15
  9. package/dist/instructions-budget.test.js +0 -2
  10. package/dist/manifest-size.test.js +3 -72
  11. package/dist/oauth-bootstrap.js +0 -47
  12. package/dist/oauth-bootstrap.test.js +4 -16
  13. package/dist/orboto-client.js +0 -30
  14. package/dist/orboto-client.test.js +0 -12
  15. package/dist/prompts.js +0 -39
  16. package/dist/protect-text-usage.test.js +1 -9
  17. package/dist/required-rules-wrapper.test.js +0 -5
  18. package/dist/required-rules.js +0 -2
  19. package/dist/resources.js +0 -62
  20. package/dist/resources.test.js +0 -16
  21. package/dist/response-budget.js +2 -150
  22. package/dist/response-budget.test.js +1 -32
  23. package/dist/server.js +4 -180
  24. package/dist/session-nudge.js +0 -9
  25. package/dist/session-nudge.test.js +2 -10
  26. package/dist/session-nudge.transport.test.js +0 -23
  27. package/dist/tool-annotations.test.js +0 -44
  28. package/dist/tool-docs.js +1 -12
  29. package/dist/tool-docs.test.js +2 -8
  30. package/dist/tool-parameters.test.js +0 -19
  31. package/dist/tools/absence-writes.js +1 -17
  32. package/dist/tools/admin-translations.js +0 -11
  33. package/dist/tools/admin-writes.js +0 -28
  34. package/dist/tools/admin-writes.test.js +0 -1
  35. package/dist/tools/agent-coordination.js +2 -24
  36. package/dist/tools/agent-coordination.test.js +0 -2
  37. package/dist/tools/agent-instructions.js +0 -10
  38. package/dist/tools/agent-instructions.test.js +0 -2
  39. package/dist/tools/agent-messages.js +0 -1
  40. package/dist/tools/ai-status.js +0 -14
  41. package/dist/tools/ai-usage.js +0 -10
  42. package/dist/tools/analytics.js +0 -2
  43. package/dist/tools/api-call.js +0 -12
  44. package/dist/tools/api-call.test.js +1 -2
  45. package/dist/tools/api-search.js +1 -6
  46. package/dist/tools/approvals.js +0 -6
  47. package/dist/tools/attach.js +0 -25
  48. package/dist/tools/backup.js +0 -13
  49. package/dist/tools/backup.test.js +1 -2
  50. package/dist/tools/bulk-create.js +1 -25
  51. package/dist/tools/bulk-create.test.js +0 -4
  52. package/dist/tools/bulk-writes.js +69 -61
  53. package/dist/tools/bulk-writes.test.js +34 -4
  54. package/dist/tools/check-similar.js +0 -17
  55. package/dist/tools/checklist-writes.js +82 -27
  56. package/dist/tools/checklist-writes.test.js +69 -1
  57. package/dist/tools/claim.js +1 -71
  58. package/dist/tools/claim.test.js +0 -2
  59. package/dist/tools/critical-path.js +0 -5
  60. package/dist/tools/critical-path.test.js +0 -3
  61. package/dist/tools/cross-project-links.ee.js +1 -24
  62. package/dist/tools/doc-attachments.js +0 -28
  63. package/dist/tools/doc-attachments.test.js +1 -4
  64. package/dist/tools/doc-comments.js +1 -37
  65. package/dist/tools/doc-comments.test.js +1 -2
  66. package/dist/tools/doc-edits.js +1 -31
  67. package/dist/tools/doc-edits.test.js +0 -9
  68. package/dist/tools/doc-export.js +0 -24
  69. package/dist/tools/doc-export.test.js +0 -12
  70. package/dist/tools/doc-revisions.js +0 -23
  71. package/dist/tools/docs-ai.js +0 -34
  72. package/dist/tools/docs-ai.test.js +0 -11
  73. package/dist/tools/docs.js +1 -75
  74. package/dist/tools/docs.test.js +1 -3
  75. package/dist/tools/embedding-status.js +0 -13
  76. package/dist/tools/embedding-status.test.js +0 -1
  77. package/dist/tools/feedback.js +1 -15
  78. package/dist/tools/feedback.test.js +0 -1
  79. package/dist/tools/get-checklists.js +6 -14
  80. package/dist/tools/get-project-primer.js +1 -25
  81. package/dist/tools/get-project.js +0 -17
  82. package/dist/tools/get-ticket.js +1 -61
  83. package/dist/tools/get-timer.js +0 -11
  84. package/dist/tools/git-health-reasons.js +0 -1
  85. package/dist/tools/identity.js +0 -13
  86. package/dist/tools/list-projects.js +0 -20
  87. package/dist/tools/list-projects.test.js +0 -1
  88. package/dist/tools/list-tickets.js +10 -9
  89. package/dist/tools/milestones-crud.test.js +0 -4
  90. package/dist/tools/milestones.js +0 -43
  91. package/dist/tools/my-tickets.js +0 -7
  92. package/dist/tools/phase-b-tools.test.js +14 -21
  93. package/dist/tools/primer-facts.js +1 -49
  94. package/dist/tools/primer-facts.test.js +0 -20
  95. package/dist/tools/project-listings.js +0 -25
  96. package/dist/tools/query.js +1 -12
  97. package/dist/tools/query.test.js +0 -8
  98. package/dist/tools/raci.js +0 -11
  99. package/dist/tools/requirements-spec.js +1 -12
  100. package/dist/tools/response-expand.js +0 -19
  101. package/dist/tools/review-policy.js +0 -24
  102. package/dist/tools/review-policy.test.js +0 -2
  103. package/dist/tools/search.js +1 -13
  104. package/dist/tools/session-start.js +1 -136
  105. package/dist/tools/session-start.test.js +1 -44
  106. package/dist/tools/set-parent.js +1 -19
  107. package/dist/tools/shared.js +0 -41
  108. package/dist/tools/shared.test.js +0 -4
  109. package/dist/tools/similar-projection.js +0 -15
  110. package/dist/tools/ticket-attachments.js +1 -25
  111. package/dist/tools/ticket-schedules.js +0 -9
  112. package/dist/tools/ticket-schedules.test.js +0 -1
  113. package/dist/tools/ticket-writes.js +1 -110
  114. package/dist/tools/ticket-writes.test.js +1 -15
  115. package/dist/tools/time-writes.js +26 -46
  116. package/dist/tools/time-writes.test.js +18 -0
  117. package/dist/tools/update-project.js +1 -27
  118. package/dist/tools/update-project.test.js +1 -1
  119. package/dist/tools/wiki.js +0 -8
  120. package/dist/tools/work-next.js +0 -5
  121. package/dist/tools/work-sessions-start.test.js +0 -2
  122. package/dist/tools/work-sessions.js +0 -48
  123. package/dist/tools/work-sessions.test.js +0 -5
  124. package/dist/toolset.js +2 -81
  125. package/dist/toolset.test.js +0 -20
  126. package/dist/version-guard.test.js +0 -4
  127. package/dist/version.js +0 -12
  128. package/dist/with-metrics-output-schema.test.js +1 -14
  129. package/dist/with-metrics.js +2 -74
  130. package/dist/with-metrics.test.js +7 -29
  131. package/package.json +1 -1
@@ -1,44 +1,5 @@
1
1
  /**
2
2
  * ORB-1697 - the central MCP response budget.
3
- *
4
- * WHY THIS EXISTS
5
- * ---------------
6
- * An MCP tool result is not paid once. The client re-sends the whole
7
- * conversation on every subsequent request, so a result costs its size
8
- * TIMES the number of turns that follow it. The 2026-08-09 transcript
9
- * audit (6.453 real calls, 32 transcripts - see doc ORB-D20) measured
10
- * `list_agent_instructions` at 238 Mtok of carry cost from only THREE
11
- * calls, because 37k characters landed 15 % into a session and rode
12
- * along for the remaining 85 %.
13
- *
14
- * Two facts from that audit shape this module:
15
- *
16
- * 1. In Claude Code, only `structuredContent` reaches the model - the
17
- * Markdown `content` block the handlers also build is dropped. So
18
- * the budget MUST measure and cut the structured payload; cutting
19
- * only the text would look like a fix and change nothing. Other
20
- * clients do the opposite, so both are measured and both are cut.
21
- * 2. Per-tool discipline does not hold across 168 registered tools.
22
- * The cap is applied once, in `with-metrics.ts`, which every tool
23
- * is already registered through.
24
- *
25
- * CONTRACT
26
- * --------
27
- * Truncation is never silent. An over-budget result comes back with a
28
- * `__truncation` block naming every cut path, plus a handle, and the
29
- * omitted remainder stays fetchable via `orboto_response_expand` for
30
- * as long as the handle lives (in-process, 15 min, 16 payloads).
31
- *
32
- * Shrinking is shape-preserving: keys never disappear, arrays stay
33
- * arrays, and strings stay strings. That matters because 7 tools
34
- * declare an `outputSchema` the SDK validates the payload against.
35
- * Adding `__truncation` is safe (no schema uses `.strict()`, and Zod v3
36
- * accepts unknown keys), and identifier-shaped strings are protected by
37
- * MIN_STRING_CUT - a uuid (36) or a ticket key is far below it, so
38
- * `.uuid()` / `.url()` fields are never cut in half.
39
- *
40
- * This module never throws. A malformed payload returns unbudgeted
41
- * rather than failing the tool call.
42
3
  */
43
4
  import { randomUUID } from 'node:crypto';
44
5
  import { z } from 'zod';
@@ -47,21 +8,7 @@ export const DEFAULT_BUDGET_CHARS = 4000;
47
8
  /**
48
9
  * Per-tool overrides.
49
10
  *
50
- * The default (4k) suits cards and lists: a response that answers
51
- * "which of these do I open?" does not need more. Two categories
52
- * legitimately need more and are raised deliberately:
53
- *
54
- * - CONTENT READS. The caller explicitly asked for a document, a
55
- * generated report or a primer. Cutting those to 4k would break the
56
- * tool's purpose rather than remove waste.
57
- *
58
- * `orboto_session_start` used to be the third category at 48.000
59
- * characters, and ORB-1818 removed that exemption: the digest now
60
- * answers with a rules INDEX plus a hash instead of the full rule text,
61
- * so the default answer fits the 4k default like every other tool. The
62
- * two answers that DO carry the full rules (`forceRules: true`,
63
- * `rulesOnly: true`) protect them instead of buying size - see
64
- * PROTECTED_PATHS and PROTECT_TEXT_META below.
11
+ * @see ORB-1818
65
12
  */
66
13
  export const TOOL_BUDGET_CHARS = {
67
14
  orboto_get_project_primer: 16_000,
@@ -73,57 +20,18 @@ export const TOOL_BUDGET_CHARS = {
73
20
  orboto_customer_report: 16_000,
74
21
  orboto_requirements_spec: 16_000,
75
22
  orboto_get_attachment: 16_000,
76
- // The continuation tool itself: a chunk is served AT the budget, so a
77
- // cap equal to the default would truncate the chunk it just sized.
78
23
  orboto_response_expand: 8_000,
79
- // ORB-1518 - detail mode returns a full endpoint schema (parameters +
80
- // request body + responses), which is the content the caller asked
81
- // for; 4k would cut most real schemas in half.
82
24
  orboto_api_search: 8_000,
83
- // ORB-1519 - the proxy envelope IS the content the caller asked for
84
- // (an arbitrary endpoint's response); the proxy already caps it, this
85
- // budget keeps the MCP-side carry cost bounded on top.
86
25
  orboto_api_call: 8_000,
87
26
  };
88
27
  /**
89
28
  * Paths the shrinker must never touch, per tool.
90
- *
91
- * The shrinker cuts the LARGEST leaf first, which is the right default and
92
- * exactly wrong for one case: in `session_start` the largest string is the
93
- * workspace's binding rules. Truncating a mandatory rule to save context is
94
- * worse than paying for it - an agent that never sees the second half of a
95
- * rule breaks it silently. Measured on 2026-08-09: a cold
96
- * `session_start({ ticketKey })` is 47.694 characters, and without this
97
- * list the budget cut 2.917 characters out of the rules block.
98
- *
99
- * A protected path is not exempt from the budget - the shrinker moves on to
100
- * the next-largest cuttable leaf (for that call: the project primer, which
101
- * is a digest with its own server-side budget and is safe to cut
102
- * explicitly). If nothing else can be cut, the response reports
103
- * `atFloor: true` and stays whole.
104
29
  */
105
30
  export const PROTECTED_PATHS = {
106
31
  orboto_session_start: ['rules'],
107
32
  };
108
33
  /**
109
34
  * ORB-1818 - per-call protection of the TEXT half.
110
- *
111
- * PROTECTED_PATHS covers the structured half, which is what Claude Code
112
- * pays for; a text-only client pays the Markdown block instead, and that
113
- * half is cut on a line boundary with no notion of protected paths. For
114
- * an answer whose whole payload IS the binding rules (`session_start`
115
- * with `forceRules` or `rulesOnly`), cutting either half truncates a
116
- * mandatory rule - the exact thing the protection above exists to
117
- * prevent, just for the other client class.
118
- *
119
- * A handler marks such a result by setting `result._meta[PROTECT_TEXT_META]
120
- * = true`. The flag is READ AND REMOVED here, so it never reaches the
121
- * wire. It protects; it does not exempt: unprotected structured leaves in
122
- * the same result are still cut normally.
123
- *
124
- * This is deliberately not a general "skip the budget" escape hatch -
125
- * `protect-text-usage.test.ts` fails the build if a file other than the
126
- * session-start tool sets it.
127
35
  */
128
36
  export const PROTECT_TEXT_META = 'orboto/protectText';
129
37
  function takeProtectTextFlag(result) {
@@ -151,9 +59,6 @@ const MIN_ARRAY_CUT = 3;
151
59
  const MAX_CUT_PASSES = 64;
152
60
  export const HANDLE_TTL_MS = 15 * 60 * 1000;
153
61
  export const MAX_HANDLES = 16;
154
- // ---------------------------------------------------------------------------
155
- // Configuration
156
- // ---------------------------------------------------------------------------
157
62
  /**
158
63
  * `ORBOTO_MCP_RESPONSE_BUDGET=off` disables the cap entirely (escape
159
64
  * hatch for a client that genuinely wants everything);
@@ -170,9 +75,6 @@ export function budgetFor(toolName, env = process.env) {
170
75
  const raw = Number(env.ORBOTO_MCP_RESPONSE_BUDGET_CHARS);
171
76
  return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : DEFAULT_BUDGET_CHARS;
172
77
  }
173
- // ---------------------------------------------------------------------------
174
- // Measurement - what the client actually pays for
175
- // ---------------------------------------------------------------------------
176
78
  function textOf(part) {
177
79
  if (part && typeof part === 'object' && 'text' in part) {
178
80
  const t = part.text;
@@ -183,14 +85,6 @@ function textOf(part) {
183
85
  }
184
86
  /**
185
87
  * The two halves a result is made of, measured separately.
186
- *
187
- * A handler returns the SAME data twice: `structuredContent` for
188
- * schema-aware clients and a Markdown text block for the rest. No known
189
- * client charges for both - Claude Code stores only the structured JSON
190
- * in its transcript (verified 2026-08-09), text-only clients see just the
191
- * text - so summing them would double-count and cut twice as hard as the
192
- * cost justifies. The budget is therefore enforced on each half, and the
193
- * reported cost is the larger of the two: what one client actually pays.
194
88
  */
195
89
  export function measureHalves(result) {
196
90
  let textChars = 0;
@@ -222,7 +116,6 @@ function pruneHandles(now) {
222
116
  if (now - value.storedAt > HANDLE_TTL_MS)
223
117
  handles.delete(key);
224
118
  }
225
- // Map iteration order is insertion order, so the oldest entries go first.
226
119
  while (handles.size > MAX_HANDLES) {
227
120
  const oldest = handles.keys().next();
228
121
  if (oldest.done)
@@ -250,14 +143,10 @@ export function annotatePayload(handle, omitted) {
250
143
  export function resetPayloadStore() {
251
144
  handles.clear();
252
145
  }
253
- // ---------------------------------------------------------------------------
254
- // Path addressing - `ticketBundle.primer.markdown`, `comments[3].body`
255
- // ---------------------------------------------------------------------------
256
146
  export function resolvePath(root, path) {
257
147
  if (path === '' || path === '$')
258
148
  return root;
259
149
  let current = root;
260
- // Split on `.` and `[i]` in one pass so both forms address the same tree.
261
150
  for (const token of path.split(/\.|(?=\[)/)) {
262
151
  if (current === null || current === undefined)
263
152
  return undefined;
@@ -290,7 +179,6 @@ function collectCandidates(root, protectedPaths = []) {
290
179
  if (!node || typeof node !== 'object')
291
180
  return;
292
181
  for (const [key, value] of Object.entries(node)) {
293
- // Never cut the truncation metadata itself - it is the way back.
294
182
  if (key === '__truncation')
295
183
  continue;
296
184
  const childPath = path ? `${path}.${key}` : key;
@@ -321,11 +209,6 @@ function collectCandidates(root, protectedPaths = []) {
321
209
  walk(root, '');
322
210
  return out.sort((a, b) => b.size - a.size);
323
211
  }
324
- // ORB-1738 - the ADVERTISED zod shape of the marker, consumed centrally in
325
- // registerWithMetrics: every declared outputSchema is extended with an
326
- // optional `__truncation` so strict clients (which validate structured
327
- // content against the advertised JSON schema, additionalProperties:false)
328
- // accept over-budget responses. Keep in lockstep with TruncationBlock.
329
212
  export const TruncationBlockSchema = z.object({
330
213
  handle: z.string(),
331
214
  budgetChars: z.number(),
@@ -344,17 +227,7 @@ export const TruncationBlockSchema = z.object({
344
227
  /**
345
228
  * ORB-1805 - what registerWithMetrics actually advertises.
346
229
  *
347
- * `TruncationBlockSchema` above is the exact runtime shape (and stays the
348
- * validator the tests assert against), but serialising all eight of its
349
- * fields costs ~620 characters in EVERY tool that declares an
350
- * outputSchema - 14 tools in the full manifest, 5 in the curated one -
351
- * for a block a caller only ever reads. The advertised form declares the
352
- * one field a caller acts on (`handle`) and stays open for the rest, so
353
- * a strict client still accepts an over-budget payload (ORB-1738, the
354
- * reason the marker is advertised at all) at a third of the bytes.
355
- *
356
- * Strictly more permissive than TruncationBlockSchema, so nothing that
357
- * validated before can fail now.
230
+ * @see ORB-1738
358
231
  */
359
232
  export const TruncationBlockAdvertisedSchema = z.object({ handle: z.string() })
360
233
  .passthrough()
@@ -369,9 +242,6 @@ function stringMarker(omitted, handle, path) {
369
242
  * admin MCP-usage panel instead of needing another transcript audit.
370
243
  */
371
244
  export function applyResponseBudget(toolName, result, env = process.env) {
372
- // ORB-1818 - read (and strip) the per-call text protection before
373
- // anything else, so the flag never rides the wire even on the
374
- // under-budget path.
375
245
  const protectText = takeProtectTextFlag(result);
376
246
  const originalChars = measureResult(result);
377
247
  if (!budgetEnabled(env)) {
@@ -385,8 +255,6 @@ export function applyResponseBudget(toolName, result, env = process.env) {
385
255
  return shrink(toolName, result, budget, originalChars, protectText);
386
256
  }
387
257
  catch {
388
- // A budget must never break a tool call. Report the real size so the
389
- // panel still shows the pressure, and hand back the untouched result.
390
258
  return { result, responseChars: originalChars, originalChars, truncatedChars: 0 };
391
259
  }
392
260
  }
@@ -402,14 +270,9 @@ function shrink(toolName, result, budget, originalChars, protectText = false) {
402
270
  const structured = result.structuredContent === undefined
403
271
  ? undefined
404
272
  : JSON.parse(safeStringify(result.structuredContent));
405
- // Reserve room for the truncation block and the text notice so the
406
- // final measurement lands under budget, not just near it.
407
273
  const RESERVE = 700;
408
274
  const target = Math.max(MIN_STRING_KEEP, budget - RESERVE);
409
- // Each half is capped against the same target - see measureHalves().
410
275
  const measure = () => (structured === undefined ? 0 : safeStringify(structured).length);
411
- // The text half: cut on a line boundary so a reader never gets half a
412
- // sentence, and record how much was dropped.
413
276
  let currentTextLength = fullText.length;
414
277
  let textOmitted = 0;
415
278
  if (!protectText && fullText.length > target) {
@@ -438,8 +301,6 @@ function shrink(toolName, result, budget, originalChars, protectText = false) {
438
301
  continue;
439
302
  }
440
303
  const items = largest.value;
441
- // Drop from the tail: list tools order by relevance, so the head is
442
- // the part a caller reads first.
443
304
  let keptItems = items.length;
444
305
  while (keptItems > MIN_ARRAY_KEEP) {
445
306
  const trial = items.slice(0, keptItems - 1);
@@ -462,10 +323,6 @@ function shrink(toolName, result, budget, originalChars, protectText = false) {
462
323
  : omitted;
463
324
  annotatePayload(handle, allOmitted);
464
325
  const atFloor = measure() > target;
465
- // ORB-1818 - the floor case gets its OWN notice. Telling an agent its
466
- // response was truncated when nothing was cut sends it hunting for a
467
- // remainder that does not exist - and that is exactly what a protected
468
- // answer (the binding rules) produces every time.
469
326
  const notice = allOmitted.length > 0
470
327
  ? `[Response truncated to the MCP response budget (${budget} chars) - it would otherwise cost `
471
328
  + `${originalChars} chars on EVERY later request in this session. Omitted content is not lost: `
@@ -484,20 +341,15 @@ function shrink(toolName, result, budget, originalChars, protectText = false) {
484
341
  ...(atFloor ? { atFloor: true } : {}),
485
342
  };
486
343
  structured.__truncation = block;
487
- // The cost model is per-half (see measureHalves), so what was omitted
488
- // is the shrinkage of the half that dominates the price.
489
344
  const finalChars = Math.max(safeStringify(structured).length, currentTextLength + notice.length + 2);
490
345
  block.omittedChars = Math.max(0, originalChars - finalChars);
491
346
  }
492
- // Rebuild the content parts: the (possibly shortened) text plus the notice.
493
347
  const shortenedText = currentTextLength >= fullText.length
494
348
  ? fullText
495
349
  : fullText.slice(0, currentTextLength);
496
350
  const content = fullText.length > 0
497
351
  ? [{ type: 'text', text: `${shortenedText}\n\n${notice}` }]
498
352
  : [{ type: 'text', text: notice }];
499
- // Non-text parts (images, resources) are never cut - they are not the
500
- // bloat this budget is about, and slicing their payload would corrupt them.
501
353
  const nonText = (result.content ?? []).filter((part) => textOf(part) === null);
502
354
  const shrunk = {
503
355
  ...result,
@@ -1,15 +1,5 @@
1
1
  /**
2
2
  * ORB-1697 - the central response budget.
3
- *
4
- * The contract under test, in the order it matters:
5
- * 1. a result inside its budget is returned byte-identical;
6
- * 2. an over-budget result is CUT, and the cut is explicit;
7
- * 3. the cut preserves the payload SHAPE (keys stay, arrays stay
8
- * arrays, identifier-shaped strings stay whole) - 7 tools declare an
9
- * outputSchema the SDK validates the payload against;
10
- * 4. nothing is lost: the omitted remainder is fetchable via the handle;
11
- * 5. text and structured content are budgeted as separate halves, since
12
- * no known client charges for both.
13
3
  */
14
4
  import { describe, it, expect, beforeEach } from 'vitest';
15
5
  import { applyResponseBudget, budgetFor, budgetEnabled, measureHalves, measureResult, readPayload, resetPayloadStore, resolvePath, DEFAULT_BUDGET_CHARS, MAX_HANDLES, HANDLE_TTL_MS, storePayload, PROTECT_TEXT_META, } from './response-budget.js';
@@ -34,11 +24,7 @@ beforeEach(() => {
34
24
  describe('budget configuration', () => {
35
25
  it('defaults to 4k and honours per-tool overrides', () => {
36
26
  expect(budgetFor('orboto_whatever', {})).toBe(DEFAULT_BUDGET_CHARS);
37
- // A content read is deliberately higher.
38
27
  expect(budgetFor('orboto_get_doc', {})).toBeGreaterThan(DEFAULT_BUDGET_CHARS);
39
- // ORB-1818 - session_start lost its 48k exemption: its default answer
40
- // carries a rules INDEX plus a hash, not the rule text, so it lives on
41
- // the plain default like every other tool.
42
28
  expect(budgetFor('orboto_session_start', {})).toBe(DEFAULT_BUDGET_CHARS);
43
29
  });
44
30
  it('reads a custom default from the env, per-tool overrides still win', () => {
@@ -65,7 +51,6 @@ describe('measurement', () => {
65
51
  const { textChars, structuredChars } = measureHalves(r);
66
52
  expect(textChars).toBe(300);
67
53
  expect(structuredChars).toBeGreaterThan(1000);
68
- // Not the sum: no known client pays for both halves.
69
54
  expect(measureResult(r)).toBe(structuredChars);
70
55
  });
71
56
  });
@@ -95,8 +80,6 @@ describe('over budget', () => {
95
80
  expect(block.omittedChars).toBeGreaterThan(0);
96
81
  expect(String(block.howToGetTheRest)).toContain('orboto_response_expand');
97
82
  expect(block.omitted).toEqual(expect.arrayContaining([expect.objectContaining({ path: 'description', kind: 'string' })]));
98
- // The text half carries the notice too, for clients that ignore
99
- // structuredContent entirely.
100
83
  const text = out.result.content[0].text;
101
84
  expect(text).toContain('orboto_response_expand');
102
85
  expect(text).toContain(out.handle);
@@ -144,7 +127,6 @@ describe('shape preservation (outputSchema tools must still validate)', () => {
144
127
  const sc = out.result.structuredContent;
145
128
  expect(sc.items.length).toBeLessThan(60);
146
129
  expect(sc.items.length).toBeGreaterThan(0);
147
- // Head-first: the first row survives, the tail is what goes.
148
130
  expect(sc.items[0].key).toBe('ORB-0');
149
131
  const entry = truncationOf(out.result).omitted
150
132
  .find((e) => e.path === 'items');
@@ -181,17 +163,13 @@ describe('shape preservation (outputSchema tools must still validate)', () => {
181
163
  });
182
164
  describe('the floor case - a payload made only of uncuttable leaves', () => {
183
165
  it('reports atFloor instead of looping or mangling identifiers', () => {
184
- // 500 short strings: every one is below MIN_STRING_CUT, so there is
185
- // nothing safe to cut. The budget must give up loudly, not spin.
186
166
  const structured = Object.fromEntries(Array.from({ length: 500 }, (_, i) => [`field_${i}`, `v${i}`.padEnd(90, 'x')]));
187
167
  const out = applyResponseBudget(TOOL, result(structured));
188
168
  expect(out.originalChars).toBeGreaterThan(budgetFor(TOOL));
189
169
  const block = truncationOf(out.result);
190
170
  expect(block.atFloor).toBe(true);
191
- // Every field survived intact - a cut that cannot be made safely is
192
- // not made at all.
193
171
  const sc = out.result.structuredContent;
194
- expect(Object.keys(sc)).toHaveLength(501); // 500 fields + __truncation
172
+ expect(Object.keys(sc)).toHaveLength(501);
195
173
  expect(sc.field_0).toBe(structured.field_0);
196
174
  });
197
175
  });
@@ -262,10 +240,6 @@ describe('the handle store is bounded', () => {
262
240
  });
263
241
  describe('protected paths - a mandatory rule is never cut (ORB-1697)', () => {
264
242
  it('cuts the primer, not the rules, on an over-budget session_start', () => {
265
- // The real shape: rules are the LARGEST string, so the default
266
- // largest-first rule would have cut exactly the wrong thing. Measured
267
- // on 2026-08-09 before this guard existed: 2.917 characters gone from
268
- // the binding rules of a cold session_start({ ticketKey }).
269
243
  const rules = 'R'.repeat(24_000);
270
244
  const out = applyResponseBudget('orboto_session_start', result({
271
245
  rules,
@@ -289,12 +263,9 @@ describe('protected paths - a mandatory rule is never cut (ORB-1697)', () => {
289
263
  it('protects the subtree, not just the exact key', () => {
290
264
  const out = applyResponseBudget('orboto_session_start', result({
291
265
  rules: 'short rules',
292
- // A hypothetical nested shape under a protected root stays protected.
293
266
  ticketBundle: { note: 'N'.repeat(80_000) },
294
267
  }));
295
268
  const sc = out.result.structuredContent;
296
- // ticketBundle is NOT protected, so this one does get cut - the guard is
297
- // path-scoped, not a blanket exemption.
298
269
  expect(sc.ticketBundle.note.length).toBeLessThan(80_000);
299
270
  });
300
271
  });
@@ -315,10 +286,8 @@ describe('per-call text protection (ORB-1818)', () => {
315
286
  expect(out.result.structuredContent.rules).toBe(rules);
316
287
  const text = out.result.content[0].text;
317
288
  expect(text).toContain(rules);
318
- // The floor notice must not claim a truncation that did not happen.
319
289
  expect(text).not.toContain('Response truncated');
320
290
  expect(text).toContain('NOTHING was omitted');
321
- // The marker is transport-internal - it never reaches the client.
322
291
  expect(out.result._meta).toBeUndefined();
323
292
  });
324
293
  it('protects the text half only - unprotected structured leaves are still cut', () => {