mistri 0.5.0 → 0.6.1

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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +480 -4
  3. data/CONTRIBUTING.md +52 -0
  4. data/README.md +289 -385
  5. data/SECURITY.md +40 -0
  6. data/UPGRADING.md +640 -0
  7. data/assets/logo-animated.svg +30 -0
  8. data/assets/logo-dark.svg +14 -0
  9. data/assets/logo-light.svg +14 -0
  10. data/assets/logo.svg +14 -0
  11. data/assets/social-preview.png +0 -0
  12. data/docs/README.md +87 -0
  13. data/docs/context-and-workspaces.md +378 -0
  14. data/docs/mcp.md +366 -0
  15. data/docs/reliability.md +450 -0
  16. data/docs/sessions.md +295 -0
  17. data/docs/sub-agents.md +401 -0
  18. data/docs/tool-contracts.md +324 -0
  19. data/examples/approval.rb +36 -0
  20. data/examples/browser.rb +27 -0
  21. data/examples/page_editor.rb +31 -0
  22. data/examples/quickstart.rb +21 -0
  23. data/lib/generators/mistri/install/install_generator.rb +7 -3
  24. data/lib/generators/mistri/install/templates/migration.rb.tt +2 -2
  25. data/lib/generators/mistri/mcp/templates/migration.rb.tt +1 -1
  26. data/lib/generators/mistri/mcp/templates/model.rb.tt +15 -8
  27. data/lib/mistri/agent.rb +575 -55
  28. data/lib/mistri/budget.rb +26 -1
  29. data/lib/mistri/child.rb +72 -16
  30. data/lib/mistri/compaction.rb +26 -10
  31. data/lib/mistri/compactor.rb +34 -11
  32. data/lib/mistri/console.rb +28 -7
  33. data/lib/mistri/content.rb +9 -3
  34. data/lib/mistri/dispatchers.rb +14 -12
  35. data/lib/mistri/errors.rb +83 -4
  36. data/lib/mistri/event.rb +24 -8
  37. data/lib/mistri/event_delivery.rb +60 -0
  38. data/lib/mistri/locks.rb +3 -3
  39. data/lib/mistri/mcp/client.rb +74 -19
  40. data/lib/mistri/mcp/egress.rb +216 -0
  41. data/lib/mistri/mcp/oauth.rb +476 -127
  42. data/lib/mistri/mcp/wires.rb +115 -23
  43. data/lib/mistri/mcp.rb +42 -8
  44. data/lib/mistri/message.rb +21 -11
  45. data/lib/mistri/models.rb +160 -22
  46. data/lib/mistri/providers/anthropic/assembler.rb +282 -44
  47. data/lib/mistri/providers/anthropic/serializer.rb +14 -9
  48. data/lib/mistri/providers/anthropic.rb +29 -6
  49. data/lib/mistri/providers/fake.rb +26 -10
  50. data/lib/mistri/providers/gemini/assembler.rb +148 -21
  51. data/lib/mistri/providers/gemini/serializer.rb +78 -9
  52. data/lib/mistri/providers/gemini.rb +31 -5
  53. data/lib/mistri/providers/openai/assembler.rb +337 -60
  54. data/lib/mistri/providers/openai/serializer.rb +13 -12
  55. data/lib/mistri/providers/openai.rb +29 -5
  56. data/lib/mistri/providers/schema_capabilities.rb +214 -0
  57. data/lib/mistri/result.rb +1 -1
  58. data/lib/mistri/schema.rb +893 -75
  59. data/lib/mistri/session.rb +560 -48
  60. data/lib/mistri/sinks/coalesced.rb +17 -10
  61. data/lib/mistri/spawner.rb +111 -61
  62. data/lib/mistri/sse.rb +57 -14
  63. data/lib/mistri/stores/active_record.rb +11 -4
  64. data/lib/mistri/stores/memory.rb +21 -2
  65. data/lib/mistri/sub_agent/execution.rb +81 -0
  66. data/lib/mistri/sub_agent/runtime.rb +297 -0
  67. data/lib/mistri/sub_agent.rb +124 -87
  68. data/lib/mistri/task_output.rb +24 -6
  69. data/lib/mistri/tool.rb +93 -13
  70. data/lib/mistri/tool_arguments.rb +377 -0
  71. data/lib/mistri/tool_call.rb +43 -9
  72. data/lib/mistri/tool_context.rb +4 -2
  73. data/lib/mistri/tool_executor.rb +117 -26
  74. data/lib/mistri/tool_result.rb +15 -10
  75. data/lib/mistri/tools/edit_file.rb +62 -8
  76. data/lib/mistri/tools.rb +41 -4
  77. data/lib/mistri/transport.rb +149 -44
  78. data/lib/mistri/usage.rb +65 -13
  79. data/lib/mistri/version.rb +1 -1
  80. data/lib/mistri/workspace/active_record.rb +190 -5
  81. data/lib/mistri/workspace/directory.rb +28 -8
  82. data/lib/mistri/workspace/memory.rb +34 -9
  83. data/lib/mistri/workspace/single.rb +62 -5
  84. data/lib/mistri/workspace.rb +39 -0
  85. data/lib/mistri.rb +6 -1
  86. data/mistri.gemspec +34 -0
  87. metadata +31 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d46cab51a8a899ddd2e3cd0284a7e9eb4185d1362af158303adb97c2a404a657
4
- data.tar.gz: 548bf1b8cdc9d22f304674eb5b876b624c16f6b3cc31839daba83909f78641a5
3
+ metadata.gz: 1414bffbf49921cf9e575ea1a8998cfc435b77325d80d55ed76e6564d63311c5
4
+ data.tar.gz: b707010a75d56d65a8c42b50a874807e0f529da6b5878246aa9b34239493c808
5
5
  SHA512:
6
- metadata.gz: 69b2758b3b137714bb2bffc170ff6520f1d38851230ba19acabbb50042d28234ce934d74555fd328d12d1e8002afe6d96bab8dd21a06567411252e4aeabac973
7
- data.tar.gz: 972354e7a95bebfa4f5918efa84f13cbc6e45e9e8dabdf49a60a4d210ecf7c11425866b2ad4d8b648916477568aa2ac69915c98d241311e3778c38667d570558
6
+ metadata.gz: b7781114db5365b8ce899af9ab50b8eaf2279979d8110a282d54a4fb3187a2576fc94821c3ddb550e9af3da8052ad64b14310c5136d3a064044aee57418c209b
7
+ data.tar.gz: d87fb8db281d3c6cedaea995054723f4f15d62e5268384393e16d655dfd45ba714e4b276c73c378307e8b3314ecec1108b3d320578edd659d77e6d07a8e34e2f
data/CHANGELOG.md CHANGED
@@ -3,6 +3,473 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
+ ## [Unreleased]
7
+
8
+ ## [0.6.1] - 2026-07-21
9
+
10
+ - The ActiveRecord store and workspace read past the host's query cache.
11
+ Rails keeps the cache on for a job's whole span and serves repeated
12
+ identical queries from it, so a parent polling for a child's report inside
13
+ a job never saw the report land and every wait ran to its timeout, and a
14
+ workspace read could serve a stale document. Reads bypass only Rails'
15
+ cache: an open REPEATABLE READ transaction still pins its snapshot, so do
16
+ not poll from inside one.
17
+
18
+ ## [0.6.0] - 2026-07-12
19
+
20
+ - Synchronous event subscribers now propagate the exact exception object even
21
+ when its class also names an internal failure: provider `Mistri::Error`
22
+ values, transport I/O and timeout errors, SSE `JSON::ParserError`, hook
23
+ failures, and automatic-compaction `CompactionError` can no longer be
24
+ swallowed, retried, folded into an errored assistant turn, or persisted as a
25
+ policy or tool failure. An owner-scoped private marker preserves provenance
26
+ through nested inline agents and unwraps only at the boundary that first
27
+ observed the subscriber. A mid-stream subscriber failure closes the
28
+ interrupted provider connection before propagating. SSE parse tolerance now
29
+ covers `JSON.parse` only, never its callback. Genuine provider, hook, tool,
30
+ and compaction failures keep their existing contracts; configured tool
31
+ timeouts still include progress delivery. Asynchronous background delivery
32
+ remains unable to raise into a caller that has already returned. A callback
33
+ that aborts child execution records the original failure class in its failed
34
+ terminal and dispatcher diagnostic; a post-terminal report callback failure
35
+ leaves the already-durable terminal and parent inbox intact and appears in
36
+ the diagnostic. Active subscribers add one short-lived boundary per provider
37
+ turn, tool batch, hook phase, or automatic compaction and one method/rescue
38
+ frame per delivered event, with no parsing, buffering, I/O, or string work on
39
+ the successful path.
40
+ - Approval decisions now behave as a write-once register over the Session
41
+ store's durable order. The first valid decision is authoritative; repeating
42
+ that value is idempotent, while a conflicting caller receives
43
+ `Mistri::ConfigurationError`. Concurrent stale writers may both reach append,
44
+ but every later well-formed decision for that approval request is an inert
45
+ losing occurrence, including one that lands after the approved handler has
46
+ already returned. It cannot revoke the winner, alter settlement, or
47
+ permanently poison `open_approvals`, `resume`, replay, or compaction. The first
48
+ note remains authoritative.
49
+ Malformed decisions, decisions without a matching request, duplicate
50
+ requests, and ambiguous legacy reused-call approvals still fail closed or
51
+ interrupt conservatively. A newly appended decision adds one linear
52
+ control-state re-read to tell a racing loser that it lost; already-visible
53
+ idempotent retries do not append. No provider streaming path changes.
54
+ - Anchored `edit_file` changes now compose safely with concurrent writers when
55
+ their Workspace advertises atomic conditional writes. The new optional
56
+ contract is `atomic_writes?`, `snapshot(path)`, and
57
+ `compare_and_write(path, content, expected_revision:)`, with immutable
58
+ `Mistri::Workspace::Snapshot` values and a public
59
+ `Mistri::WorkspaceConflictError`. Memory implements the contract under its
60
+ mutex; the optional Active Record adapter advertises it only for PostgreSQL
61
+ or an InnoDB table on MySQL2 or Trilogy, using exact-byte SHA-256 revisions,
62
+ unique-index create-only CAS, and short transactions without requiring
63
+ `lock_version`. It rejects atomic operations inside a host transaction rather
64
+ than joining a stale or overlong boundary, validates model identity against
65
+ the database catalog, restricts atomic scope to owned scalar equality values,
66
+ and reads the committed row before returning its revision. Single opts in
67
+ through a host `synchronize:` callback that must enforce the same complete
68
+ boundary and outer-transaction rule. Directory, MyISAM, and existing custom
69
+ four-method workspaces retain their documented legacy behavior. `edit_file`
70
+ applies its existing exact/fuzzy anchor outside any lock and retries only a
71
+ lost conditional write, up to three total attempts. An unrelated concurrent
72
+ change is preserved, while a target that no longer matches remains an
73
+ actionable typed edit failure. Callback, validation, I/O, deadlock, and other
74
+ host errors are never retried. Tool names and schemas are unchanged. A
75
+ committed storage transformation produces an explicit
76
+ read-before-continuing failure instead of a false exact-replacement claim.
77
+ Atomic editing adds owned snapshot copies, three exact-byte linear hashes,
78
+ one exact committed-byte comparison, and one conditional commit to the
79
+ ordinary edit path; Memory also returns an owned read copy. Conflicts add at
80
+ most two complete reapplications. Active Record capability adds cached schema
81
+ inspection on the first capability check, outside the edit path. No
82
+ provider streaming path changes. Dedicated MySQL 8.4/InnoDB and PostgreSQL CI
83
+ jobs prove real locking, readback, and unique-create behavior while the gem
84
+ retains zero runtime dependencies.
85
+ - Built-in document reads, searches, and edits now return an explicitly failed
86
+ `ToolResult` when the document is missing or a requested replacement cannot
87
+ be applied. The same actionable text still reaches the model, but lifecycle
88
+ events, persisted tool messages, and provider-supported error signaling no
89
+ longer misclassify an expected document-tool failure as success. Tool names,
90
+ schemas, and successful results are unchanged.
91
+ - Background dispatch now crosses a versioned, deeply owned JSON capability
92
+ boundary instead of closing over the spawn-time provider, Tool objects, Agent
93
+ options, and workspace state. Every `dispatcher:` requires a host
94
+ `runtime_factory:` that constructs a `SubAgent::Runtime` inside the worker;
95
+ the runtime provider model and unique Tool names must match the durable grant
96
+ exactly before any provider call or tool side effect. Missing, additional,
97
+ renamed, duplicated, non-Tool, reconstructed statically approval-gated, and
98
+ synthetic Skill capabilities fail closed before execution. New immutable
99
+ specs carry `spec_version`, require a model identity, reject unknown fields,
100
+ and omit the model-controlled `workspace` field. The identical canonical
101
+ spec is stored in the child Session before dispatch; `run_dispatched` compares
102
+ the complete queue copy and uses the stored grant for execution and report
103
+ routing, so a changed capability, task, model, name, or parent ID leaves the
104
+ child untouched. Legacy unversioned specs containing `workspace` remain
105
+ executable. Finished redeliveries are rejected before factory construction;
106
+ a configured child lease also rejects concurrent live redeliveries. The
107
+ compatible direct form receives the same grant checks.
108
+ Runtime factory exceptions remain retryable for queues by default; built-in
109
+ in-process runners and an explicit `retry_factory_errors: false` persist a
110
+ terminal failure before re-raising. An optional Runtime `cleanup:` hook runs
111
+ exactly once without masking a primary error. Generic child Agent options
112
+ are allowlisted and travel as one isolated Hash, so they cannot replace the
113
+ durable Session, task, signal, event sink, or other lifecycle state; Runtime
114
+ cannot add Skill capabilities. The dispatched
115
+ lease now spans runtime construction, execution, cleanup, terminal
116
+ persistence, and parent report delivery; it suppresses ordinary stop and
117
+ redelivery races while the tokenless lease remains live. Queued and
118
+ interrupted cancellations now deliver their durable stopped report while holding that
119
+ lease even when the queue never starts or retries the job; repeating stop
120
+ reconciles a transient parent-report failure. A queue copy that does not
121
+ match the persisted grant raises the public, non-retryable
122
+ `Mistri::DispatchGrantError`, which is reserved for grant binding.
123
+ Model selection records only its identity at spawn time; its provider is
124
+ constructed in the worker. `tools: []` now grants no tools, while omission
125
+ keeps the general pool or typed defaults, so an empty typed Definition can no
126
+ longer inherit the entire pool. Duplicate pool, model, or model-selected tool
127
+ names fail before a child starts, and Spawner owns copies of its policy
128
+ arrays. Dispatch adds one bounded spec ownership/comparison and one linear
129
+ capability-name check per child, outside all provider streaming paths.
130
+ - Model-originated tool calls now cross a local execution boundary rather than
131
+ relying on provider guidance alone. Completed calls deeply own immutable JSON,
132
+ canonicalize symbol keys without coercing values, retain malformed encoded
133
+ input as a durable `arguments_error`, and stop at 8 MiB, 64 levels, 10,000
134
+ nodes, or a 64 KiB numeric token. The same numeric bound applies to encoded
135
+ JSON and programmatic custom-provider arguments. The Agent requires an object
136
+ argument envelope before `before_tool`, approval predicates, `ends_turn`,
137
+ handlers, or remote MCP calls. Invalid calls receive a bounded typed result in
138
+ band, emit no execution-start event, expose no received field values in core
139
+ errors, and do not prevent valid siblings from running. Results settled in
140
+ the same phase return in model-call order.
141
+ Completed tool-call IDs, names, signatures, and provider correlation IDs must
142
+ be non-empty UTF-8 strings. Live provider turns require IDs unique for the
143
+ session, and provider correlation IDs are unique within one assistant turn.
144
+ Persisted histories recognize only the `call_N` reuse without provider
145
+ correlation IDs that earlier releases synthesized for Gemini and the Fake
146
+ provider, after the prior occurrence was answered or crash-interrupted.
147
+ Replay, provider metadata, and compaction track each occurrence independently
148
+ while live IDs remain reserved for the full session. An unsettled reused-ID
149
+ approval that cannot distinguish its generation becomes an interrupted result
150
+ rather than executable stale authorization. Every other duplicate ID,
151
+ malformed call envelope, or non-assistant provider result rejects the whole
152
+ provider attempt before persistence, policy, or execution. The normal
153
+ provider retry contract retries unchanged history; exhaustion persists a
154
+ pairable error with no tool calls. An abort during normalization, validation,
155
+ policy, or approval evaluation interrupts every uncommitted call and parks no
156
+ approval. Custom
157
+ providers that emitted missing, blank, non-string, non-UTF-8, or duplicate
158
+ identifiers must correct their completed call contract. Each run or resume
159
+ validates every persisted message and audits call identity in one control-state
160
+ preflight before doing work, so malformed legacy history fails closed. The
161
+ preceding assistant entry remains the durable provider source; a normalized
162
+ approval stores the reviewed prepared call plus an explicit provenance
163
+ marker. Resume verifies pairing metadata and revalidates the exact approved
164
+ arguments without rerunning the normalizer. Requests must follow the exact
165
+ assistant call, and decisions must follow one request and carry an exact
166
+ boolean. Duplicate requests and malformed or mismatched controls fail closed.
167
+ The first valid durable decision wins; later well-formed decisions for that
168
+ approval request are inert. Legacy requests remain valid only when they
169
+ exactly mirror the source call.
170
+ Persisted tool results require one prior call with the exact name, settle in
171
+ assistant-call order within each direct or approval phase, and cannot cross
172
+ an unresolved approval; compaction may neither hide an open approval nor
173
+ split a completed call from its result.
174
+ `ToolCall#arguments_error?` is stable; its non-nil string remains an opaque
175
+ diagnostic rather than a public enum. Fake-provider default call IDs now use
176
+ a per-provider UUID namespace plus a monotonic sequence; explicitly scripted
177
+ `id:` values remain deterministic. New approval entries no longer duplicate
178
+ the source argument payload, and generated MySQL-family session tables use
179
+ LONGTEXT so a legal parallel tool turn is not constrained by MEDIUMTEXT's
180
+ 16 MiB ceiling. Existing MySQL-family stores must migrate `payload` to
181
+ `size: :long`; generators cannot alter an installed table. `Stores::Memory`
182
+ owns durable appends while returning fresh mutable snapshots, matching stores
183
+ that decode each read.
184
+ - Tool schemas compile once into the same deeply frozen canonical UTF-8 JSON
185
+ value used by provider serializers, so mutation and Ruby encoding cannot make
186
+ local validation disagree with the wire. Definition checks cover the complete
187
+ schema document. The zero-dependency runtime subset enforces JSON types and
188
+ unions, `enum`, `required`, `properties`, `prefixItems`, one-schema `items`,
189
+ boolean schemas, and boolean or schema-valued `additionalProperties`.
190
+ `argument_validator:` adds domain rules without weakening core;
191
+ `complete_argument_validator:` is separate explicit authority for every
192
+ argument-applicable non-empty `patternProperties` contract. Tool inputs use
193
+ JSON Schema 2020-12 and require an object root; legacy array-form `items` is
194
+ rejected in favor of `prefixItems`. A schema-less Tool is now a genuinely
195
+ closed no-argument contract; hosts that accepted model fields without
196
+ declaring them must add an explicit schema. The schema-less wire shape
197
+ changed with it, so provider prompt caches re-warm once per affected tool
198
+ set after upgrading. Supplied object schemas preserve
199
+ JSON Schema's default-open semantics unless a raw schema explicitly sets
200
+ `additionalProperties: false`; handlers should extract named fields instead
201
+ of mass-assigning model input. MCP bridging enforces directly reachable
202
+ portable constraints locally and keeps unsupported applicator subtrees and
203
+ other unimplemented standard assertions as guidance for the server, which the
204
+ MCP spec obligates to validate its own tool inputs. A complete validator takes
205
+ explicit whole-contract authority when local policy depends on those
206
+ assertions. External references and
207
+ explicit `inputSchema: null` are rejected in every mode, an omitted schema
208
+ still defaults to a no-argument object, and argument-applicable non-empty
209
+ `patternProperties` still requires complete authority. Common map schemas
210
+ work in core. Task mode rejects assertions local validation cannot guarantee
211
+ before making a provider request, then shares one deeply frozen strict schema
212
+ between its prompt and compiled local validator.
213
+ Providers derive a native constraint only for schema shapes their documented
214
+ and live-verified subsets accept; incompatible shapes fall back to the prompt
215
+ and local correction loop instead of failing the request. Native constraints
216
+ are limited to catalogued matching-provider models and documented provider
217
+ complexity ceilings; unknown models keep the local path. Raw task output is
218
+ byte-bounded, then lexically
219
+ width/depth-bounded before JSON parsing so wide hostile JSON is rejected
220
+ before parser allocation. A Tool-owned `argument_normalizer:` remains the
221
+ explicit compatibility seam and runs once before validation; approved calls
222
+ persist that canonical value and revalidate against the current schema on
223
+ resume without renormalizing or reevaluating approval. `edit_file` declares
224
+ its existing alias and string-boolean tolerance through that seam and rejects
225
+ alias collisions. Provider replay substitutes `{}` only as the wire placeholder
226
+ for an already rejected call, paired with its error result. Direct `Tool#call`
227
+ remains a trusted host path and skips Agent validation. Task output now uses
228
+ the same JSON resource limits. This is minor-release behavior for hosts whose
229
+ policy or handlers previously received malformed values, whose custom
230
+ validators relied on implicit full-schema authority, or whose code mutated or
231
+ indexed a Tool's raw schema with symbol keys. `Schema.strict` results are now
232
+ deeply frozen, and `Tool.define` raises when both `input_schema:` and `schema:`
233
+ are supplied instead of silently choosing one. Definition compilation is paid
234
+ once; a completed call adds one bounded ownership pass and one compiled schema
235
+ traversal, plus ownership only when a normalizer replaces the value. Streaming
236
+ tool snapshots deep-freeze each newly refreshed bounded preview once; deltas
237
+ that reuse the cached preview are O(1), and pairing strings are copied.
238
+ Completed call envelope verification is one linear pass over that turn's
239
+ calls. Run and resume add one linear control-state read of the session log to
240
+ seed O(1) identity checks. Encoded Anthropic/OpenAI arguments and task text add
241
+ one linear lexical scan before JSON parsing; it is off the token-delta path. A
242
+ normalized approval adds only a small provenance marker. Structured
243
+ output request building adds one bounded schema ownership pass; task plans
244
+ compile their local validator once, with no added work on token streaming.
245
+ Arguments are now deeply frozen before hooks and handlers; hosts that mutated
246
+ model-supplied hashes or arrays must copy them first.
247
+ - Provider replay now keeps opaque state with its provider of origin. Gemini
248
+ sends tool schemas through `parametersJsonSchema`, preserves each signed part
249
+ boundary, and returns Google's function-call ID exactly when one was supplied;
250
+ legacy calls without a wire ID get a collision-safe internal ID without
251
+ mutating the signed provider part. On pre-3 Gemini turns, an earlier gated
252
+ same-name/no-ID call is rejected in band if a later sibling would answer
253
+ first; Gemini 3 wire IDs retain out-of-order pairing. A MAX_TOKENS turn never
254
+ executes a returned call. OpenAI treats a missing completed `arguments` field
255
+ as malformed instead of executing `{}`. Anthropic and OpenAI no longer place
256
+ another provider's signatures on their wire. These replay checks add constant
257
+ metadata work per completed block. Terminal records now fence later known
258
+ content for all three providers. Anthropic enforces
259
+ sequential and open content-block indexes plus the delta kind expected by the
260
+ open block. OpenAI enforces one open output item, its kind, any present item or
261
+ output-index correlation, and terminal event/status agreement. Unknown future
262
+ event types remain ignorable. The checks add only constant primitive
263
+ comparisons per known delta, with no parsing, copying, or I/O.
264
+ Completed foreign tool exchanges project to neutral text when a session
265
+ moves to Gemini, preserving the result without manufacturing a Gemini
266
+ function call that lacks Google's thought signature.
267
+ Real providers now emit the same initial `:start` snapshot as the Fake, and
268
+ every opened text, thinking, or tool-call block emits its matching end before
269
+ a terminal failure. Interrupted tool calls remain explicitly incomplete and
270
+ are stripped before Agent persistence; interrupted Anthropic thinking never
271
+ replays a partial signature.
272
+ - Tool execution now emits `:tool_started` when each resolved call commits to
273
+ invocation and carries a structured failure fact end to end. A handler may
274
+ return `ToolResult(error: true)`; unknown tools, policy blocks,
275
+ handler exceptions, timeouts, pre-invocation interruptions, failed result
276
+ hooks, healed dangling calls, and MCP `isError` results set it automatically.
277
+ The flag is sticky through `after_tool` rewrites, so changing error text
278
+ cannot relabel a failed operation as successful. `Event#tool_error` is always
279
+ true or false on `:tool_result`; new tool messages persist the same value, and
280
+ legacy messages without it remain unknown and replay with the historical
281
+ unmarked provider shape. Human approval denial remains a normal control
282
+ outcome rather than an execution error. Anthropic receives `is_error: true`;
283
+ Gemini receives the documented `error` function-response key; OpenAI retains
284
+ textual output because its function-call output has no
285
+ execution-error member. Error text is not classified by prefix, and an error
286
+ bit never authorizes a mechanical replay because a side effect may already
287
+ have happened; the model may still choose a new call, so hosts retain
288
+ idempotency and reconciliation policy. `ends_turn` remains the host's stronger
289
+ floor-transfer policy even when a result is errored; `handed_off?` does not
290
+ claim confirmed tool success. The new lifecycle signal adds one synchronous
291
+ sink event per committed call. Partial streaming Event construction adds two
292
+ primitive checks and one immutable nil slot; partial Message construction
293
+ adds one nil check and one immutable nil slot, with no parsing, buffering,
294
+ I/O, or string work. Tool batches add one sentinel write at commitment and one
295
+ identity check per result so a hard worker exit distinguishes started calls
296
+ from untouched queue entries; same-batch ordering adds one short-lived
297
+ identity map and a linear post-join pass, plus a second identity map only when
298
+ `after_tool` is configured;
299
+ starts arrive on serialized worker-thread callbacks, while same-batch results
300
+ still emit in model-call order after the batch joins. `:tool_started` and
301
+ handler-progress subscriber exceptions propagate and never become tool
302
+ failures.
303
+ Unknown, blocked, denied, queued, and pre-invocation interrupted calls add no
304
+ start event.
305
+ - A blockless nested object in the tool-schema DSL now declares a freeform JSON
306
+ object instead of leaking `LocalJumpError` during application boot. A bare
307
+ top-level `Schema.build` raises `ConfigurationError` with a useful message.
308
+ Constrained-output preparation now refuses this freeform shape, and directly
309
+ encountered explicitly open objects, with a schema path instead of silently
310
+ closing them and changing their meaning. Existing block-built and raw tool
311
+ schemas keep their wire shape. Tool-schema work remains definition-time;
312
+ constrained-output request building adds one openness check to its existing
313
+ property/item traversal, with no streaming or token-path work.
314
+ - Provider and MCP JSON bodies, individual SSE lines, and stdio JSON records
315
+ now default to an 8 MiB ceiling, configurable with `max_record_bytes:`.
316
+ Successful bodies are counted incrementally, declared oversized bodies fail
317
+ before reading, streaming remains unlimited across individually safe records,
318
+ and error responses retain at most a 500-byte valid UTF-8 preview. Requests
319
+ use identity encoding so compressed expansion cannot bypass the boundary.
320
+ Overflow closes the connection or child process; an oversized MCP
321
+ `tools/call` response remains explicitly ambiguous and is never replayed.
322
+ Stdio timeouts now cover the complete record instead of only its first byte;
323
+ any in-flight stdio wire failure after `tools/call` is ambiguous, reaps the
324
+ child, and requires a clean handshake before the next operation. Explicitly
325
+ closing and reusing an MCP client also clears its session, negotiated
326
+ protocol version, and server information before the fresh handshake.
327
+ Each completed SSE record also receives a bounded 20,001-token lexical
328
+ preflight, derived from the 10,000-node canonical limit, plus depth and 64 KiB
329
+ numeric-token ceilings before JSON parsing. A distinct
330
+ `ResponseTooComplexError` preserves the exact boundary and bounds Gemini
331
+ argument allocation inside its enclosing record. Either size or complexity
332
+ overflow resets an MCP wire and its negotiated session. An unconfirmed
333
+ `tools/call` outcome becomes ambiguous and is never replayed; a matching result
334
+ received before an invalid trailing SSE record remains authoritative, while
335
+ the next operation still performs a fresh handshake. Fragmented Anthropic and
336
+ OpenAI tool arguments also carry an aggregate 8 MiB cap across SSE records.
337
+ Their live partial preview stops reparsing after a bounded 64 KiB schedule
338
+ while raw delta events continue unchanged; OpenAI's completed item remains
339
+ authoritative. Fragmented Anthropic thinking
340
+ signatures stop immediately at the same aggregate ceiling rather than
341
+ growing quadratically or replaying a truncated opaque value.
342
+ The hot path adds one byte-count addition and comparison per JSON socket or SSE
343
+ line fragment, then one bounded linear lexical pass over each completed SSE
344
+ record before the provider's existing JSON parse.
345
+ - Remote MCP and server-side OAuth requests now default to public HTTPS and
346
+ direct connections. Every DNS answer is checked against the IANA
347
+ special-purpose registries, including embedded NAT64 destinations; one unsafe
348
+ answer rejects the whole set. Approved addresses are pinned without changing
349
+ Host, SNI, or certificate identity, alternates are tried only before request
350
+ transmission, and every connection cycle resolves and validates a fresh DNS
351
+ set, including Net::HTTP's internal reconnect cycles. Redirects and ambient
352
+ proxies cannot bypass the boundary. `allow_non_public:` is a narrow host
353
+ callback for approved internal HTTPS and explicit loopback development.
354
+ Client construction remains DNS-free; a reused socket adds no request-path
355
+ work. A new connection cycle pays one validated DNS lookup, with approved
356
+ candidates tried only before request transmission.
357
+ - MCP custom headers are copied at client construction and transport-owned
358
+ headers are rejected; a token callable remains the dynamic authentication
359
+ path. Tool discovery rejects repeated cursors and defaults to at most 100
360
+ pages and 10,000 tools; hosts can adjust either ceiling.
361
+ - MCP OAuth discovery now follows the required protected-resource and
362
+ authorization-server candidate order, uses exact candidate-aware resource and
363
+ issuer binding, honors challenge scope, requires S256 PKCE, and never adds
364
+ `offline_access` as gem policy. OAuth response bodies are uncompressed and
365
+ bounded to 256 KiB; token error descriptions cannot reflect credentials into
366
+ exceptions. A pre-registered client now requires its exact trusted `issuer:`,
367
+ and completion and refresh rediscover the token endpoint from that persisted
368
+ issuer instead of trusting a stored endpoint. New confidential flows persist
369
+ an explicit authentication method; legacy rows with a secret and no method
370
+ retain their previous `client_secret_post` behavior.
371
+ - The MCP Rails generator now persists `issuer` instead of `token_endpoint` and
372
+ exposes `mcp_allow_non_public` as the host's shared egress-policy hook. Existing
373
+ generated models must add and pass an issuer, restart pending OAuth flows,
374
+ and thread their non-public policy through the client and all OAuth
375
+ operations. Existing connected rows must reconnect unless the application
376
+ independently recorded their exact historical issuer. An issuer must never
377
+ be inferred from a token endpoint.
378
+ - GPT-5.6 Sol, Terra, and Luna are catalogued with their 1.05M-token context,
379
+ 128K-token output ceiling, and standard paid pricing above and below the
380
+ 272K long-context boundary. The `gpt-5.6` alias resolves to Sol. OpenAI usage
381
+ now separates billable GPT-5.6 cache writes from uncached input, keeping
382
+ compaction, reported cost, and cost budgets accurate.
383
+ - Directory workspaces reject existing symlinks in model-controlled paths and
384
+ omit symlinked files from listings. In a stable, host-controlled tree, reads,
385
+ writes, and deletes no longer follow a link outside the configured root.
386
+ - Streamable HTTP no longer replays an MCP `tools/call` when its response cannot
387
+ be confirmed. The server may already have committed the tool's side effect,
388
+ so the call now raises `AmbiguousDeliveryError` with an explicit do-not-retry
389
+ warning instead of transparently executing twice.
390
+ - Refusals and content-filter stops surface honestly on every provider
391
+ instead of reading as clean stops or retryable truncations. Gemini's
392
+ verdict finish reasons (SAFETY, RECITATION, LANGUAGE, BLOCKLIST,
393
+ PROHIBITED_CONTENT, SPII, the image verdicts) and blocked prompts
394
+ (promptFeedback.blockReason, which previously read as a retryable
395
+ truncated stream) fail fast as Mistri::InvalidRequestError with the
396
+ wire word in the message. MISSING_THOUGHT_SIGNATURE is a deterministic input
397
+ failure; its model fumbles and incomplete sentinels
398
+ (MALFORMED_FUNCTION_CALL, UNEXPECTED_TOOL_CALL, TOO_MANY_TOOL_CALLS,
399
+ MALFORMED_RESPONSE, FINISH_REASON_UNSPECIFIED, NO_IMAGE, and the catch-all
400
+ OTHER pair, documented as unknown reasons rather than rulings) error retryably.
401
+ Anthropic's refusal
402
+ stop reason fails fast carrying stop_details' category and explanation
403
+ (the API's guidance is a different model, never a same-model retry),
404
+ and model_context_window_exceeded maps to :length per its guidance.
405
+ OpenAI's incomplete_details content_filter and structured-output refusal
406
+ parts or deltas fail fast instead of reading as a completed answer; unknown
407
+ incomplete reasons retry rather than becoming clean stops. Partial content
408
+ stays on the errored message for hosts to show. Genuinely undocumented future
409
+ Gemini stop reasons still tolerate as clean stops, unchanged. Anthropic and
410
+ OpenAI top-level stream errors now classify documented authentication,
411
+ request/policy, rate-limit, timeout, overload, and server failures by their
412
+ wire code rather than retrying deterministic 4xx errors.
413
+ - Active session execution is not yet leased. Store appends from approvals,
414
+ steers, and reports remain concurrency-safe, but hosts must allow only one
415
+ `run` or `resume` at a time for a session. Even with one runner, a crash,
416
+ result-store failure, or subscriber exception after invocation but before
417
+ result persistence can leave an approved call open for possible duplicate
418
+ re-execution. Simultaneous runners widen the same risk. Hosts must use
419
+ idempotency/reconciliation across that commit gap; durable atomic claiming is
420
+ the next synchronization feature, not simulated by a local mutex.
421
+ - Sinks::Coalesced is origin-aware and thread-safe: a background worker's
422
+ deltas no longer merge into the parent's (or another worker's) event at
423
+ the same content index, and concurrent emitters serialize on an
424
+ internal mutex instead of racing the buffer. One uncontended mutex
425
+ acquire per event, at coalesced rate; nothing changes on the token
426
+ path.
427
+ - OpenAI response.failed events classify by their error code instead of
428
+ all reading as a retryable truncated stream: rate limits and server
429
+ errors stay retryable, timeouts retry, and permanent rejections
430
+ (invalid_prompt, the image family) carry the new
431
+ Mistri::InvalidRequestError shape and fail fast, so the loop no longer
432
+ burns its retry budget on input the provider already ruled out. A
433
+ failed response without an error object now errors instead of reading
434
+ as a clean stop.
435
+ - The model catalog carries each model's published standard paid direct-API
436
+ prices. Assemblers price every turn's usage from them: message.usage.cost and
437
+ Result#usage now report list-price dollar estimates for catalogued models, and the
438
+ Budget cost_usd ceiling actually stops a run. It never could before:
439
+ nothing computed cost, so the comparison always saw zero. Pricing is
440
+ selected per request, including GPT-5.4/5.5 and Gemini Pro long-context
441
+ tiers and Sonnet 5's September 2026 rate change. Unpriced usage is marked
442
+ unknown instead of free. A cost ceiling rejects an unknown model or origin
443
+ at construction and fails closed when a request's pricing is unknown at
444
+ runtime. Catalog pricing requires an explicit `catalog_pricing:` opt-in on
445
+ custom origins and observes service-tier policy without changing it;
446
+ nonstandard or unreported tiers stay unknown. A deterministic standard tier
447
+ is required up front for cost-budgeted Anthropic and OpenAI agents; Gemini's
448
+ reported standard, Flex, or Priority tier is honored rather than assuming
449
+ standard. Cost ceilings remain soft and are checked between model-visible
450
+ turns. An unmetered attempt raises Mistri::BudgetError instead of retrying
451
+ under false certainty; the error and an unpriced_attempt session entry retain
452
+ its partial accounting. Run usage includes every retry and compaction attempt,
453
+ and retry session entries now carry their own usage. Truncated streams preserve
454
+ partial token counts but mark their dollar total unknown.
455
+ - Compaction now uses the published 1M context windows for catalogued Fable,
456
+ Opus, and Sonnet models and the 1.05M windows for GPT-5.4 and GPT-5.5.
457
+ Automatic headroom protects a full next output plus framing slack when the
458
+ provider shares input and output capacity; Gemini's separately published
459
+ input limit keeps input-only headroom. An explicit `reserve:` still wins as
460
+ host policy.
461
+ `Compaction#automatic_reserve?` lets hosts distinguish the default mode from
462
+ an explicit 16,384-token policy without changing `Compaction#reserve`.
463
+ Usage reported before the latest compaction no longer makes a fresh summary
464
+ appear full. A single run can compact repeatedly across tool turns while
465
+ keeping parallel calls paired with every result. Large tool results are
466
+ bounded only on the lossy summarizer wire, with their beginning and end
467
+ retained. That shortening never mutates the stored result or an ordinary
468
+ request that still replays it. Compacted replay intentionally remains
469
+ summary plus tail. The live
470
+ provider harness now proves two compactions, an exact tool-token handoff,
471
+ and final fact recall inside one run.
472
+
6
473
  ## [0.5.0] - 2026-07-08
7
474
 
8
475
  - The children registry: Session#children lists every sub-agent a session
@@ -46,7 +513,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
46
513
  requires inline mode, enforced in band.
47
514
 
48
515
  - Report delivery: a background child's terminal outcome reports back to
49
- its parent, exactly once. The report queues in the parent's inbox as a
516
+ its parent. The report queues in the parent's inbox as a
50
517
  typed subagent_report entry and folds at the next turn boundary the way
51
518
  a steer does (the model sees `[Magpie finished] <report>`; failures
52
519
  carry the error, stops say so), and a report landing as a run finishes
@@ -54,9 +521,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
54
521
  event (agent, session_id, status, content) closes the child's lane in
55
522
  whatever UI watched the spawn. Session#pending_inbox is the
56
523
  steers-and-reports view (hosts that wake idle sessions on steers should
57
- watch it instead); Session#deliver_report is the idempotent primitive
58
- underneath.
59
- - Dispatched runs are fenced by the child's lease, taken before the
524
+ watch it instead); Session#deliver_report drops sequential duplicate
525
+ delivery by child Session ID. Concurrent callers need serialization.
526
+ - With a lock adapter, dispatched runs use the child's lease before the
60
527
  run-or-not decision: a queue that redelivers a live job leaves the
61
528
  owner alone, a retry of a finished or cancelled child is a clean no-op,
62
529
  and a retry of a child a crashed process left mid-run runs it again
@@ -415,4 +882,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
415
882
 
416
883
  - Reserved the gem name.
417
884
 
885
+ [Unreleased]: https://github.com/mcheemaa/mistri/compare/v0.6.1...HEAD
886
+ [0.6.1]: https://github.com/mcheemaa/mistri/compare/v0.6.0...v0.6.1
887
+ [0.6.0]: https://github.com/mcheemaa/mistri/compare/v0.5.0...v0.6.0
888
+ [0.5.0]: https://github.com/mcheemaa/mistri/compare/v0.4.1...v0.5.0
889
+ [0.4.1]: https://github.com/mcheemaa/mistri/compare/v0.4.0...v0.4.1
890
+ [0.4.0]: https://github.com/mcheemaa/mistri/compare/v0.3.0...v0.4.0
891
+ [0.3.0]: https://github.com/mcheemaa/mistri/compare/v0.2.1...v0.3.0
892
+ [0.2.1]: https://github.com/mcheemaa/mistri/compare/v0.2.0...v0.2.1
893
+ [0.2.0]: https://github.com/mcheemaa/mistri/compare/v0.1.0...v0.2.0
418
894
  [0.1.0]: https://github.com/mcheemaa/mistri/releases/tag/v0.1.0
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,52 @@
1
+ # Contributing
2
+
3
+ Thanks for wanting to make mistri better.
4
+
5
+ ## Setup
6
+
7
+ ```console
8
+ $ bundle install
9
+ $ bundle exec rake test # hermetic, fast, no keys needed
10
+ $ bundle exec rubocop
11
+ ```
12
+
13
+ The live suites need provider keys in a gitignored `.env.development.local`
14
+ (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`):
15
+
16
+ ```console
17
+ $ MISTRI_LIVE=1 bundle exec rake test # gated live tests
18
+ $ bundle exec rake integration # critical end-to-end scenarios, per model
19
+ ```
20
+
21
+ Missing keys skip their provider. Read the output before treating a live run as
22
+ three-provider coverage.
23
+
24
+ ## The rules that keep the gem what it is
25
+
26
+ - **Zero runtime dependencies.** Development and test dependencies are fine;
27
+ a runtime dependency needs an unusually good reason.
28
+ - **Tests ship with the change**, in the same commit. New provider-touching
29
+ behavior gets a live test.
30
+ - **Errors follow their boundary.** A tool failure becomes a tool result the
31
+ model can react to. Built-in provider failures finish as an errored Result;
32
+ direct MCP Client and configuration failures raise. A bridged MCP Client
33
+ failure crosses the ordinary tool-result boundary.
34
+ - **Sessions are append-only.** Derive state from the entry log; never
35
+ require a repair step after a crash.
36
+ - **Comments say why, not what.** Few and load-bearing.
37
+ - Run `bundle exec rubocop` before pushing; CI enforces it and a coverage
38
+ floor.
39
+
40
+ ## Pull requests
41
+
42
+ Small, focused, sentence-case titles. Describe the why in a paragraph, not
43
+ a checklist. If the change alters public behavior, add a CHANGELOG entry
44
+ under Unreleased.
45
+
46
+ ## Documentation
47
+
48
+ The README is the adoption path: category, first success, fit, core mechanism,
49
+ and honest reliability boundaries. Put detailed contracts in the task-oriented
50
+ Markdown guides under `docs/`, and put required host migrations in
51
+ `UPGRADING.md`. Link repository files relatively so links work on branches and
52
+ pull requests. Do not duplicate the changelog into a guide.