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
data/SECURITY.md ADDED
@@ -0,0 +1,40 @@
1
+ # Security policy
2
+
3
+ Report vulnerabilities privately through GitHub security advisories:
4
+ https://github.com/mcheemaa/mistri/security/advisories/new
5
+
6
+ Please do not open public issues for security reports. You will get a
7
+ response within a few days, and credit in the fix's release notes unless
8
+ you prefer otherwise.
9
+
10
+ Notes worth knowing when assessing reports:
11
+
12
+ - mistri has zero runtime dependencies; supply-chain surface is Ruby's
13
+ standard library.
14
+ - Tokens and API keys are never logged by the gem and never persisted by
15
+ the gem itself; storage is the host application's, and the generated
16
+ Rails models encrypt token columns.
17
+ - Remote MCP and OAuth URLs default to public HTTPS. Mistri rejects credentials,
18
+ fragments, private, loopback, link-local, shared, multicast, documentation,
19
+ and reserved destinations; validates every DNS answer; and pins an approved
20
+ address while preserving hostname-based TLS verification. Every connection
21
+ cycle validates a fresh DNS set, including keep-alive reconnect cycles, and
22
+ never falls back from an unsafe set to an old address. Approved candidates
23
+ from one set are tried only before request transmission. MCP and server-side
24
+ OAuth requests do not follow redirects or inherit ambient HTTP proxy settings.
25
+ The user's browser resolves the authorization endpoint independently.
26
+ - A host can explicitly approve an internal HTTPS destination with
27
+ `allow_non_public:`. Plain HTTP remains limited to an explicitly approved,
28
+ actually resolved loopback address. The callback is part of the host's
29
+ security boundary and should match both an expected hostname and IP range.
30
+ - OAuth protected-resource and issuer metadata are bound to exact identifiers.
31
+ Pre-registered clients require the issuer trusted at registration, and token
32
+ endpoints are rediscovered from that issuer before credentials are sent.
33
+ OAuth response bodies are read incrementally with a 256 KiB limit and
34
+ compression disabled; server-controlled descriptions are not copied into
35
+ token errors.
36
+ - Provider and MCP JSON bodies, individual SSE lines, and stdio records default
37
+ to an 8 MiB `max_record_bytes:` ceiling. The total size of an SSE stream is
38
+ not capped when each record remains within that boundary. Error responses
39
+ retain at most a 500-byte valid UTF-8 preview. HTTP status, header, chunk-size,
40
+ and trailer lines are parsed by Net::HTTP before this body boundary applies.
data/UPGRADING.md ADDED
@@ -0,0 +1,640 @@
1
+ # Upgrading Mistri
2
+
3
+ Mistri follows SemVer from 0.x: patches fix behavior, while a minor release may
4
+ add features and intentional contract changes. Every change belongs in the
5
+ [changelog](CHANGELOG.md); this guide extracts only the work an existing host
6
+ may need to perform.
7
+
8
+ Upgrade a representative application and persisted session set in staging
9
+ before production. Do not treat a passing bundle update as a session migration
10
+ test.
11
+
12
+ ## From 0.5.x to 0.6.x
13
+
14
+ This is a safety-focused minor upgrade. The built-in Anthropic, OpenAI, and
15
+ Gemini providers preserve ordinary 0.5 sessions, including the repeated
16
+ `call_N` IDs older Gemini and Fake-provider turns generated after the earlier
17
+ occurrence settled. There is no blanket session rewrite step.
18
+
19
+ Hosts that used implicit tool schemas, mutated arguments, supplied custom
20
+ providers or Session stores, dispatched background children, used document
21
+ workspaces, generated MCP OAuth models, stored sessions in MySQL, or configured
22
+ cost budgets need to review the sections below.
23
+
24
+ ### Checklist
25
+
26
+ - [ ] Declare every argument a model may send to a formerly schema-less tool.
27
+ - [ ] Remove handler or policy mutations of model argument hashes and arrays.
28
+ - [ ] Replace implicit coercion with an explicit per-tool normalizer where it is
29
+ genuinely part of the contract.
30
+ - [ ] Make event subscribers tolerate new event types and read `tool_error`
31
+ structurally.
32
+ - [ ] Audit synchronous event subscribers: their exact exceptions now escape
33
+ `run` or `resume` and may follow durable facts or external side effects.
34
+ - [ ] Ensure at most one `run` or `resume` is active per Session across all
35
+ processes.
36
+ - [ ] Treat each approval as single-assignment; resolve quorum or multi-reviewer
37
+ policy in the host before calling `approve` or `deny`.
38
+ - [ ] Verify every successful custom Session-store append is visible to later
39
+ loads in the same stable per-session order.
40
+ - [ ] Audit custom provider tool-call IDs, names, arguments, and metadata.
41
+ - [ ] Widen Active Record session payloads to LONGTEXT on MySQL or Trilogy.
42
+ - [ ] Update generated MCP OAuth models to persist issuer and share egress
43
+ policy across the complete flow.
44
+ - [ ] Review the new MCP and provider response limits and ambiguous-delivery
45
+ behavior.
46
+ - [ ] Configure a deterministic standard service tier for any cost budget.
47
+ - [ ] Remove intentional symlinks from model-controlled Directory workspaces.
48
+ - [ ] Add a runtime factory to every background spawner and queue worker.
49
+ - [ ] Review queued child specs and ensure their exact `tool_names` grant is
50
+ still intended before executing them under 0.6.
51
+ - [ ] Keep qualifying Active Record workspaces outside host transactions and
52
+ assert `atomic_writes?` at boot when lost-update protection is required.
53
+ - [ ] Replace mutation of values returned by `Workspace::Memory#read` with an
54
+ explicit `write`.
55
+ - [ ] Use anchored `edit_file`, not blind `write_file`, when concurrent changes
56
+ must be preserved.
57
+ - [ ] Exercise active persisted sessions, approvals, tool failures, compaction,
58
+ and one real call per configured provider in staging.
59
+
60
+ ## Background children reconstruct their runtime
61
+
62
+ A dispatcher no longer captures the spawn-time provider, Tool objects, Agent
63
+ options, or their workspace closures. Every spawner with `dispatcher:` must now
64
+ provide a host-owned `runtime_factory:`. Mistri invokes it inside the worker and
65
+ requires a `Mistri::SubAgent::Runtime`:
66
+
67
+ ```ruby
68
+ runtime_factory = lambda do |spec|
69
+ workspace = HostWorkspaces.for_child(spec.fetch("session_id"))
70
+ provider = HostModels.build(spec.fetch("model"))
71
+ tools = HostTools.build(spec.fetch("tool_names"), workspace: workspace)
72
+
73
+ Mistri::SubAgent::Runtime.new(
74
+ provider: provider,
75
+ system: HostAgents.system_for(spec),
76
+ tools: tools,
77
+ cleanup: -> { HostAgents.release(provider: provider, tools: tools) },
78
+ budget: HostAgents.child_budget,
79
+ )
80
+ end
81
+
82
+ spawn = Mistri::SubAgent.spawner(
83
+ provider: child_provider,
84
+ tools: declared_tools,
85
+ dispatcher: Mistri::Dispatchers::Thread.new,
86
+ runtime_factory: runtime_factory,
87
+ )
88
+ ```
89
+
90
+ The provider model and unique Tool names returned by the factory must match the
91
+ versioned dispatch spec exactly. Mistri rejects missing, additional, duplicate,
92
+ renamed, or reconstructed statically approval-gated tools before the provider
93
+ runs. This turns `tool_names` into a durable capability grant rather than queue
94
+ metadata. A versioned spec and its exact parent routing are also stored in the
95
+ child Session; a changed queue copy raises without failing or reporting the
96
+ legitimate child.
97
+
98
+ `cleanup:` is optional and called exactly once after Mistri accepts a Runtime,
99
+ including after validation or execution failure. Use it for per-child provider
100
+ sockets, MCP subprocesses, and other host-owned resources. It never masks the
101
+ primary error. `skills:` is rejected because it would add an
102
+ undeclared `read_skill` tool. Spawner Agent options apply only to inline
103
+ children; declare background Agent options on the Runtime.
104
+
105
+ Sub-agent Agent options are now limited to `budget`, `max_concurrency`,
106
+ `transform_context`, `compaction`, `retries`, `skills`, `before_tool`,
107
+ `after_tool`, and application `context`; background Runtime still rejects
108
+ `skills`. Lifecycle keys such as `session`, `task`, `signal`, and `emit` are
109
+ owned by the child and cannot be overridden through generic keyword options.
110
+
111
+ The model-facing `workspace` argument has been removed. It never cloned a Tool
112
+ closure or proved isolation. Existing queued specs that contain `workspace`
113
+ remain readable, but new specs omit it. Build child-scoped resources from
114
+ trusted host state and stable identifiers such as `session_id`; a retry should
115
+ reopen the same durable scope. Inline children still use the exact objects the
116
+ host supplied and may share their backends.
117
+
118
+ Queue jobs should pass the factory into the lifecycle entry point:
119
+
120
+ ```ruby
121
+ Mistri::SubAgent.run_dispatched(
122
+ spec,
123
+ store: mistri_store,
124
+ runtime_factory: HostAgents.method(:runtime_for),
125
+ )
126
+ ```
127
+
128
+ Factory exceptions are retryable by default: the child remains non-terminal
129
+ and the exception returns to the queue. Pass `retry_factory_errors: false` on a
130
+ final attempt to persist and report the failure before the exception re-raises.
131
+ The flag does not control the job system's retry or discard decision. The
132
+ built-in Thread and Inline runners use terminal behavior because they have no
133
+ durable retry owner.
134
+
135
+ If the queue copy differs from the stored grant, `run_dispatched` raises
136
+ `Mistri::DispatchGrantError` without touching the legitimate child. Treat the
137
+ unchanged job as poison: discard it and alert rather than retrying it. This
138
+ error is reserved for Mistri's stored-grant binding; do not raise it from a
139
+ runtime factory.
140
+
141
+ The compatible direct `provider:`, `system:`, and `tools:` form remains, but it
142
+ now receives the same exact model and tool-grant validation. A job that used to
143
+ pass its entire registry must instead reconstruct only the Tool objects named
144
+ by `spec.fetch("tool_names")`.
145
+ Legacy 0.5 specs keep their serialized grant, including any broad grant created
146
+ by the old empty-Definition bug. Their old child entries also have no stored
147
+ copy against which Mistri can verify capability or report-routing changes.
148
+ Drain or review unversioned queued work before relying on the new boundary.
149
+
150
+ Tool selection now distinguishes omission from an explicit empty list. Omitted
151
+ `tools` means the general pool or a typed Definition's defaults. `tools: []`
152
+ means no tools for either worker shape, and a typed Definition that declares no
153
+ tools no longer inherits the general pool.
154
+
155
+ ## Tool schemas are enforced locally
156
+
157
+ Model-originated arguments now cross ownership and schema validation before
158
+ `before_tool`, approval predicates, `ends_turn`, MCP, or handlers.
159
+
160
+ Invalid input receives a bounded error result the model can correct. Valid
161
+ sibling calls still run. Mistri does not coerce values.
162
+
163
+ ### Schema-less tools are closed
164
+
165
+ In 0.5, the no-argument wire schema did not explicitly reject extra fields. In
166
+ 0.6, a tool without `schema:` or `input_schema:` is a closed no-argument tool.
167
+
168
+ No change is needed when the tool truly accepts no arguments:
169
+
170
+ ```ruby
171
+ Mistri::Tool.define("current_time", "Returns the current time.") do
172
+ Time.now.utc.iso8601
173
+ end
174
+ ```
175
+
176
+ Declare every accepted field when the handler uses arguments:
177
+
178
+ ```ruby
179
+ Mistri::Tool.define("lookup", "Looks up one account.", schema: lambda {
180
+ string :account_id, "Account ID", required: true
181
+ }) do |args|
182
+ Accounts.fetch(args.fetch("account_id"))
183
+ end
184
+ ```
185
+
186
+ For an intentionally freeform object, say so explicitly:
187
+
188
+ ```ruby
189
+ input_schema = {
190
+ type: "object",
191
+ additionalProperties: true,
192
+ }
193
+ ```
194
+
195
+ The schema-less wire shape changes once, so provider prompt caches for affected
196
+ tool sets will re-warm after deployment.
197
+
198
+ ### Supplied objects remain open by default
199
+
200
+ JSON Schema object properties do not close the object. If extra keys must fail,
201
+ use a raw schema with `additionalProperties: false`. Otherwise extract declared
202
+ fields instead of mass-assigning the complete argument hash.
203
+
204
+ ### JSON Schema dialect and subset
205
+
206
+ Tool schemas use JSON Schema 2020-12. Replace legacy tuple-array `items: [...]`
207
+ with `prefixItems`. A supplied explicit older dialect is rejected.
208
+
209
+ Core enforces its documented portable subset. Use `argument_validator:` for
210
+ domain additions and `complete_argument_validator:` only when the host
211
+ validator implements the entire schema. Argument-applicable non-empty
212
+ `patternProperties` requires complete authority. External references are never
213
+ resolved.
214
+
215
+ Task mode now rejects an assertion Mistri cannot guarantee locally instead of
216
+ claiming validated output and leaving part of the schema to provider guidance.
217
+ Freeform objects cannot become strict task output without changing meaning.
218
+
219
+ See [Tool contracts](docs/tool-contracts.md).
220
+
221
+ ### Schema objects are canonical and immutable
222
+
223
+ `Tool#input_schema` and `Schema.strict` now expose deeply frozen canonical JSON
224
+ with String keys. Replace Symbol-key access and mutation:
225
+
226
+ ```ruby
227
+ # Before
228
+ tool.input_schema[:properties][:account_id] = rule
229
+
230
+ # After
231
+ account_schema = tool.input_schema.fetch("properties").fetch("account_id")
232
+ ```
233
+
234
+ Build a new schema when the host needs a changed contract. `Tool.define` now
235
+ rejects supplying both `schema:` and `input_schema:`; choose one. Calling
236
+ `Schema.build` without a block now raises because it declares no argument
237
+ intent.
238
+
239
+ ## Arguments are immutable and policy sees validated types
240
+
241
+ Completed arguments and the canonical schema are deeply frozen. Code that
242
+ mutated model input must copy the portion it owns:
243
+
244
+ ```ruby
245
+ # Before: mutates the value shared with policy and replay.
246
+ args["tags"] << "processed"
247
+
248
+ # After: build application-owned data.
249
+ tags = args.fetch("tags", []).dup
250
+ tags << "processed"
251
+ ```
252
+
253
+ For a fully mutable JSON copy:
254
+
255
+ ```ruby
256
+ owned = JSON.parse(JSON.generate(args))
257
+ ```
258
+
259
+ Approval predicates can now rely on declared types. Remove defensive coercion
260
+ that changes authorization meaning:
261
+
262
+ ```ruby
263
+ # Do not turn "all" into zero at an authorization boundary.
264
+ approval_policy = ->(args) { args.fetch("total_usd") > 500 }
265
+ ```
266
+
267
+ When a host intentionally accepts a legacy alias or representation, use
268
+ `argument_normalizer:`. Its output is what validation, policy, approval, and
269
+ the handler see. It runs once before validation and does not run again on
270
+ resume.
271
+
272
+ Direct `Tool#call` remains a trusted host path. It applies the tool's normalizer
273
+ but does not invoke the Agent's model-input validation boundary.
274
+
275
+ ## Tool lifecycle and event subscribers
276
+
277
+ Tool results now carry a structured error fact. A handler may return
278
+ `Mistri::ToolResult.new(content: ..., error: true)`, and Mistri sets the same
279
+ fact for invalid, blocked, unknown, interrupted, timed-out, or failed calls.
280
+
281
+ `:tool_started` is emitted only when a resolved call commits to handler
282
+ invocation. `:tool_result` always carries a boolean `event.tool_error`; new
283
+ persisted tool messages carry the same value. Legacy messages without the field
284
+ remain unknown rather than being classified from prose.
285
+
286
+ `:tool_started` and handler progress callbacks originate on tool worker threads.
287
+ Custom sinks must be thread-safe. Code that manually constructs a
288
+ `:tool_result` `Mistri::Event` must now supply a boolean `tool_error` matching
289
+ the Message.
290
+
291
+ An exception from a synchronous subscriber now propagates unchanged from
292
+ `run` or `resume`. Mistri does not retry it or reinterpret it as a provider,
293
+ transport, hook, tool, parse, or compaction failure. Delivery is not a
294
+ transaction: some events follow durable Session appends, and handler progress
295
+ can follow an external side effect. Catch subscriber failures at the host
296
+ boundary, reconcile the Session and the tool's domain idempotency state, and
297
+ retry only under host policy. See [Reliability](docs/reliability.md#events-and-sinks).
298
+
299
+ Update exhaustive event switches to ignore unknown future members:
300
+
301
+ ```ruby
302
+ case event.type
303
+ when :text_delta
304
+ stream(event.delta)
305
+ when :tool_started
306
+ mark_running(event.tool_call)
307
+ when :tool_result
308
+ settle(event.tool_call, failed: event.tool_error)
309
+ else
310
+ # Event types are an extensible union.
311
+ end
312
+ ```
313
+
314
+ Do not decide failure by matching prefixes such as `"Error:"`. Human denial is
315
+ an expected control result and is not marked as execution failure.
316
+
317
+ Missing documents in `read_file`, `find_in_file`, and `edit_file`, plus an
318
+ `edit_file` replacement that cannot be applied, now carry `tool_error: true`.
319
+ Their actionable model-facing text is unchanged.
320
+
321
+ The error fact never authorizes mechanical replay. A handler can commit a side
322
+ effect before it raises.
323
+
324
+ Public value records gained fields: `ToolCall` adds `arguments_error` and
325
+ `provider_call_id`, `ToolResult` adds `error`, and Message/Event records add
326
+ `tool_error`. Keyword construction remains compatible through defaults, but
327
+ exact positional deconstruction or pattern matching can break. Prefer named
328
+ accessors and key patterns, and treat value and event records as extensible.
329
+
330
+ ## Custom provider contract
331
+
332
+ Built-in providers already satisfy the new envelope. A custom provider must
333
+ return completed `Mistri::ToolCall` values with:
334
+
335
+ - a non-empty, valid UTF-8 String ID;
336
+ - an ID unique for new calls over the active Session;
337
+ - a non-empty, valid UTF-8 String name;
338
+ - a JSON object for valid arguments, or a durable `arguments_error` for
339
+ malformed encoded input;
340
+ - non-empty valid UTF-8 signature and provider call ID strings when present;
341
+ - provider correlation IDs unique within an assistant turn.
342
+
343
+ If any completed call makes the assistant turn unpairable, Mistri rejects the
344
+ whole provider attempt before persistence, policy, or sibling execution. The
345
+ ordinary provider retry asks for a clean turn with unchanged history.
346
+
347
+ `ToolCall#arguments_error?` is the stable predicate. Its non-nil String is an
348
+ opaque diagnostic, not a public enum.
349
+
350
+ Audit active custom-provider sessions without mutating them:
351
+
352
+ ```ruby
353
+ active_session_ids.each do |session_id|
354
+ session = Mistri::Session.new(store: store, id: session_id)
355
+ session.tool_control_state # validates persisted call and approval identity
356
+ end
357
+ ```
358
+
359
+ The legacy repeated `call_N` exception is deliberately limited to settled 0.5
360
+ Gemini and Fake-provider history without provider correlation IDs. Do not copy
361
+ that shape into a custom provider.
362
+
363
+ Fake-provider call IDs are now namespaced instead of restarting at `call_1`,
364
+ `call_2`, and so on. Script an explicit `id:` when a test or persisted fixture
365
+ needs deterministic identity.
366
+
367
+ If an old custom log contains malformed IDs that cannot be paired safely, keep
368
+ the old application version available long enough to complete or retire that
369
+ session according to host policy. Do not invent replacement IDs in replay; they
370
+ would corrupt tool-result correlation.
371
+
372
+ ## Approval decisions are single-assignment
373
+
374
+ The first valid approval decision in the Session store's durable order is
375
+ final. Repeating the same `approve` or `deny` call is idempotent and does not add
376
+ another entry once the winner is visible. The first note also wins. A later
377
+ conflicting call raises `Mistri::ConfigurationError`; it cannot revoke the
378
+ winner.
379
+
380
+ Two callers can still read the parked request before either appends. Both
381
+ attempts may therefore appear in the raw append-only log. Mistri re-reads the
382
+ ordered history after append so the winner returns normally and a conflicting
383
+ loser raises. Losing entries are validated but inert, including a stale loser
384
+ that arrives after the tool result. They no longer make the Session require a
385
+ repair step.
386
+
387
+ This is single-assignment, not quorum. If two people must agree, or a denial
388
+ must outrank an approval, implement that policy transactionally in the host and
389
+ call Session only after the host has one final decision. A custom Session store
390
+ must make a successful append visible to subsequent loads while preserving its
391
+ stable total order.
392
+
393
+ ## Session stores
394
+
395
+ ### MySQL and Trilogy payload width
396
+
397
+ The generated Active Record session table now uses LONGTEXT for `payload`. An
398
+ existing table is not changed by updating the generator:
399
+
400
+ ```ruby
401
+ class WidenAgentEntryPayload < ActiveRecord::Migration[7.2]
402
+ def change
403
+ change_column :agent_entries, :payload, :text, size: :long, null: false
404
+ end
405
+ end
406
+ ```
407
+
408
+ Use the migration version appropriate for the host application. PostgreSQL
409
+ `text` needs no width change.
410
+
411
+ ### Store value ownership
412
+
413
+ `Mistri::Stores::Memory` now owns data on append and returns fresh mutable
414
+ snapshots on load, matching stores that decode JSON on every read. A custom
415
+ store should not rely on Mistri mutating a previously returned entry object.
416
+
417
+ ### Active Session execution
418
+
419
+ Mistri does not lease top-level Session execution. The host must ensure that at
420
+ most one `run` or `resume` is active for a Session across all processes.
421
+ Concurrent-safe store appends and child lock adapters do not provide this
422
+ guarantee. Keep external writes idempotent or reconcilable across the
423
+ handler-commit/result-append gap.
424
+
425
+ `Session#entries` and serialized usage are extensible records. Raw host readers
426
+ must ignore unknown entry types and keys. This release adds lifecycle, approval
427
+ provenance, unpriced-attempt, and `cost.known` data.
428
+
429
+ ## Workspace editing and conditional writes
430
+
431
+ `edit_file` now uses conditional writes when a workspace explicitly advertises
432
+ `atomic_writes?`. Existing custom workspaces that implement only `read`,
433
+ `write`, `delete`, and `list` keep their 0.5 behavior. A custom workspace must
434
+ claim atomic support only when its `snapshot` and `compare_and_write` methods
435
+ satisfy the complete contract in
436
+ [Context and workspaces](docs/context-and-workspaces.md#workspaces-and-document-tools).
437
+
438
+ Only anchored `edit_file` uses this protocol. `write_file` remains an
439
+ intentional blind whole-document replacement, and deletion has no conditional
440
+ form. Use an anchored edit or a host tool with a stronger domain contract when
441
+ concurrent writers must preserve unrelated changes.
442
+
443
+ A qualifying `Mistri::Workspace::ActiveRecord` now takes the conditional path
444
+ on PostgreSQL, or on an InnoDB table through MySQL2 or Trilogy. It verifies the
445
+ primary key, non-null path, content, and scope columns, and an exact unique
446
+ scope/path index before advertising the capability. Atomic snapshots and edits
447
+ reject when the connection already has an open host transaction, because the
448
+ adapter cannot safely join that transaction's isolation and lock lifetime. Run
449
+ the Agent outside the host transaction. Unsupported storage retains the legacy
450
+ path; assert `workspace.atomic_writes?` at boot rather than accepting that
451
+ fallback when lost-update protection is required.
452
+
453
+ `Mistri::Workspace::Memory#read` now returns an owned copy. Call `write` to
454
+ persist a changed value; mutating a previously returned String no longer changes
455
+ workspace state. Its conditional writes protect only threads sharing that
456
+ Memory instance, not separate processes.
457
+
458
+ `Mistri::Workspace::Single` opts in only when given `synchronize:`. That
459
+ callback must cover the complete current read, revision check, write, and
460
+ committed read for every competing writer. It must also refuse an already-open
461
+ Active Record transaction when it uses an Active Record lock. Without the
462
+ callback, Single keeps its legacy four-method behavior.
463
+
464
+ ## MCP changes
465
+
466
+ ### Existing generated OAuth models
467
+
468
+ The 0.5 Rails MCP generator stored `token_endpoint`. The current flow persists
469
+ the authorization-server `issuer` and rediscovers the token endpoint from that
470
+ exact identity before code exchange and refresh.
471
+
472
+ For an existing generated model:
473
+
474
+ 1. Add an `issuer` String column.
475
+ 2. Persist `flow["issuer"]` in `connect` instead of trusting
476
+ `flow["token_endpoint"]`.
477
+ 3. Pass `issuer:` to `OAuth.complete` and `OAuth.refresh`.
478
+ 4. Keep and pass `token_auth_method`.
479
+ 5. Add one class-owned `mcp_allow_non_public` policy and pass it to
480
+ `OAuth.start`, `.complete`, `.refresh`, and `MCP::Client` when internal
481
+ destinations are allowed.
482
+ 6. Restart pending OAuth flows.
483
+ 7. Reconnect existing rows unless the application independently recorded the
484
+ exact issuer used during registration.
485
+
486
+ Mistri no longer adds `offline_access`; include it explicitly in `scope:` only
487
+ when host policy and the authorization server require it. The former implicit
488
+ localhost allowance is also gone. Loopback HTTP development now needs an
489
+ explicit `allow_non_public: ->(_uri, address) { address.loopback? }` through the
490
+ complete flow. A pre-registered client, or discovery advertising multiple
491
+ authorization servers, requires the exact trusted `issuer:`.
492
+
493
+ Never infer an issuer from a token endpoint. They are different security
494
+ identities. An unused legacy `token_endpoint` column may be removed in a later
495
+ host migration after rollback needs have passed.
496
+
497
+ Compare the generated files in
498
+ [`lib/generators/mistri/mcp/templates/`](lib/generators/mistri/mcp/templates)
499
+ with the host copy. Updating the gem never rewrites generated application code.
500
+
501
+ ### Network policy
502
+
503
+ Remote MCP and server-side OAuth now default to public HTTPS, validated DNS,
504
+ direct connections, no redirects, and no ambient proxy. An internal server must
505
+ receive the same narrow `allow_non_public:` callback through the entire flow.
506
+
507
+ Custom headers are copied when the Client is constructed, and transport-owned
508
+ framing, MCP session, and protocol names are rejected. Use the token callable for
509
+ rotating authorization. Deployments that require an ambient forward proxy or
510
+ HTTP redirects need a host-supplied client; the built-in Client deliberately
511
+ connects directly.
512
+
513
+ ### Response limits and delivery
514
+
515
+ JSON bodies, individual SSE lines, and stdio records default to 8 MiB per
516
+ record. Set a larger `max_record_bytes:` only for a trusted server after
517
+ reviewing the memory boundary.
518
+
519
+ Provider records use `provider_options: { max_record_bytes: ... }`; the MCP
520
+ Client takes `max_record_bytes:` directly. Servers exceeding the new default of
521
+ 100 discovery pages or 10,000 tools must set reviewed `max_tool_pages:` and
522
+ `max_tools:` values explicitly.
523
+
524
+ An unconfirmed `tools/call` response now raises
525
+ `Mistri::AmbiguousDeliveryError` and is never replayed automatically. Catch it
526
+ when calling the Client directly, mark the operation for reconciliation, and
527
+ verify remote state before deciding whether a new call is safe. A Client used
528
+ through `Mistri::MCP.tools` raises inside the tool executor, which returns the
529
+ same uncertainty as an error-marked tool result. The transport still does not
530
+ replay it, but the model may choose to issue a new call.
531
+
532
+ ### MCP schemas
533
+
534
+ Mistri enforces directly reachable portable assertions and lets the MCP server
535
+ enforce standard assertions outside that subset. Common map schemas with
536
+ schema-valued `additionalProperties` work in core. Use a complete validator only
537
+ when local policy depends on the whole schema.
538
+
539
+ An explicit `inputSchema: null` now rejects. Omit `inputSchema` for a closed
540
+ no-argument tool, or provide an object schema.
541
+
542
+ See [MCP](docs/mcp.md).
543
+
544
+ ## Cost budgets and pricing
545
+
546
+ 0.5 exposed a cost budget but did not calculate provider cost, so the comparison
547
+ effectively saw zero. 0.6 prices catalogued standard paid direct-API usage and
548
+ represents unknown cost as unknown.
549
+
550
+ A `cost_usd` budget now fails at construction unless the model, origin, and
551
+ service-tier policy can produce deterministic catalog pricing. Configure:
552
+
553
+ ```ruby
554
+ # Anthropic
555
+ anthropic_options = { service_tier: "standard_only" }
556
+
557
+ # OpenAI
558
+ openai_options = { service_tier: "default" }
559
+ ```
560
+
561
+ Gemini's omitted tier is standard. Flex, Priority, an unreported nonstandard
562
+ tier, an unknown model, or a custom origin remains unpriced unless the custom
563
+ provider supplies complete known cost.
564
+
565
+ Without a cost budget, unknown models and custom origins continue to work.
566
+ Inspect `result.usage.cost.known?` before presenting an estimate.
567
+
568
+ Budgets remain soft ceilings checked between model-visible turns. A final turn
569
+ and its retries may cross the configured amount.
570
+
571
+ ## Compaction behavior
572
+
573
+ Catalogued Anthropic and OpenAI windows now use their published long-context
574
+ limits. Automatic reserve protects the model's full shared output capacity plus
575
+ framing slack; Gemini keeps its separately published input-oriented reserve.
576
+
577
+ Sessions with default compaction will therefore compact at different and more
578
+ accurate boundaries. An explicit `reserve:` remains host policy and wins.
579
+
580
+ Compaction can occur more than once inside one run while preserving tool-call
581
+ pairing. The full entry log remains exact; replay after a successful compaction
582
+ intentionally becomes the visible summary plus kept tail.
583
+
584
+ Review any UI or alerting code that assumed the old early thresholds. Only the
585
+ `:compaction` event carries the summary; `:compacting` is the start signal.
586
+
587
+ ## Provider refusal and stream behavior
588
+
589
+ Documented provider refusals, content-policy stops, and invalid requests now
590
+ fail fast instead of looking like clean completion or consuming retries on the
591
+ same rejected input. Unknown transient stream failures still follow the retry
592
+ policy.
593
+
594
+ Interrupted tool calls and partial Anthropic thinking signatures are removed
595
+ before persistence. Built-in stream assemblers now enforce known lifecycle,
596
+ index, and terminal-record invariants while ignoring unknown future event types
597
+ for forward compatibility.
598
+
599
+ If host code classified provider errors by prose, move to the typed
600
+ `result.errored?`, `result.message.error`, `result.stop_reason`, and structured
601
+ event or message fields. Built-in provider refusals finish in the Result
602
+ contract; they are not raised as ordinary transport exceptions.
603
+
604
+ ## Directory workspaces
605
+
606
+ Directory workspaces no longer follow existing symlinks in model-controlled
607
+ paths and omit symlinked entries from listings. Replace intentional links with
608
+ host-controlled tools or real files inside the workspace root. This boundary is
609
+ deliberate and has no opt-out in the model-controlled file tools.
610
+
611
+ ## Verification before production
612
+
613
+ At minimum:
614
+
615
+ 1. Run the hermetic suite and application tests.
616
+ 2. Audit active persisted sessions with `tool_control_state`.
617
+ 3. Resume a 0.5 Anthropic session with tool calls and reasoning.
618
+ 4. Resume a 0.5 Gemini session containing tool calls from more than one turn.
619
+ 5. Exercise one approval grant and denial after rebuilding current policy.
620
+ 6. Run a write tool with an idempotency key and simulate failure before result
621
+ persistence in the host test harness.
622
+ 7. Reconnect and refresh one MCP OAuth connection.
623
+ 8. For a workspace that requires concurrent edit preservation, assert
624
+ `atomic_writes?` and race two anchored edits against its real backend.
625
+ 9. Run the provider-specific live tests and integration matrix with every
626
+ expected key present.
627
+
628
+ ```console
629
+ $ bundle exec rake test
630
+ $ bundle exec rubocop
631
+ $ MISTRI_LIVE=1 bundle exec rake test
632
+ $ MISTRI_INTEGRATION_STRICT=1 bundle exec rake integration
633
+ ```
634
+
635
+ Ordinary integration runs skip missing provider keys and an unreachable public
636
+ MCP fixture. Strict mode fails when a matrix key is absent or that fixture is
637
+ unavailable, so use it for release verification.
638
+
639
+ For exact behavior and latency notes, read the
640
+ [0.6.0 changelog entry](CHANGELOG.md#060---2026-07-12).