Commit Graph

1341 Commits

Author SHA1 Message Date
Patrick Buckley 9f54a97cc6 fix(understone): rebalance early game and add inn turn refresh
The opening hours were a gold/HP death spiral: a fresh hero spent more gold
healing a fight than the kill paid, mid-tier foes out-damaged a starting HP
bar, the map gave no read on where harder foes spawned, and a spent day left
the player idle until the UTC rollover. This eases the on-ramp across both
shipped worlds.

Economy
- Healer drops from 2 to 1 gold per HP, so topping up no longer outruns income.
- Starting purse 20 -> 37: enough to buy the cheapest armor and one potion up
  front, a one-point DEF bump plus a heal cushion the player chooses to spend.

Combat
- Tier 2-4 common foes lose 1 ATK each, trimming the burst that could halve or
  end a fresh hero in a single bout. Rares and the boss are untouched.

Wayfinding
- The road glyph changes from "=" to a shaded path that reads as one continuous
  road in every orientation; "=" only looked right horizontally and broke into
  stacked dashes on vertical runs. Cinder's basalt path gets the same treatment
  with a crosshatch glyph free in its palette.

Rest
- Sleeping at the inn now rolls a spent adventurer into a fresh day's turns (it
  already fully heals). The top-up fires only at zero turns, so it never banks
  past the daily cap.

Verified: full suite green (+2 rest tests, ruff/mypy clean); the greedy balance
bot still clears the world 11/12 seeds at an unchanged pace on both packs.
2026-06-27 09:40:03 -07:00
Patrick Buckley 0e1972e6aa chore: bump version to 1.7.0a3 v1.7.0a3 2026-06-27 01:22:33 -07:00
Patrick Buckley b1542ad62d fix(title): reliable titles on thinking models; defer utility temperature
Auto-title generation and manual refresh stopped producing titles on
reasoning models (the cluster serves qwen3.6). The title call capped
max_tokens at 200, so the model's think pass consumed the whole budget and
content came back empty (finish_reason=length) -> the title was skipped.
Both paths share _generate_title, so both broke.

Title path:
- Raise the title completion to 2048 tokens so reasoning finishes and the
  title text actually lands.
- Recover the title from content (never reasoning): reuse the canonical
  _strip_reasoning (handles <think>/<reasoning>, paired or unclosed) plus a
  backstop for the opener-absent </think> shape some templates emit, take
  the first non-empty line, and peel a "Title:" label and wrapping
  markdown/quote decoration. Internal punctuation is preserved. Cap at 80 to
  match the manual-alias bound.

Temperature:
- _utility_completion no longer hard-codes a temperature; it defaults to the
  session/registry value the main turn uses. Title (was 0.7/0.3), web-fetch
  extraction (was 0.2), and compaction all defer. Hard-coding a constant
  fought thinking/no-temp models and silently overrode an explicit [models.*]
  temperature; the provider still gates temperature per model.

Tests: title sanitization across think/reasoning variants, truncation, and a
trailing-prose case; utility-completion temperature deferral + explicit
override.
2026-06-27 01:21:56 -07:00
Patrick Buckley b2c93ce15a fix(composer): show server error text on a rejected send
The send POST's `!r.ok` guard threw a bare `send_http_<status>`, which both
send `.catch` handlers render verbatim — so a rejected send surfaced as
"Connection error: send_http_400" instead of the server's reason. Read the
`{error}` body and throw that, falling back to the status code when a wedged
proxy answers non-JSON (502/504 HTML) so it can't become an "Unexpected
token <" error. Applied to interactive and coordinator.

Also correct the queue-controller comments: a dequeue releases no
"server-side reservation" (queued messages are text-only and dequeue_message
just pops the entry), and onAfterDequeue is wired by coordinator too — not
omitted.
2026-06-27 00:09:44 -07:00
Patrick Buckley 5f0b5bb173 fix(composer): node-proxy-correct, reliable queued-message dismiss
The queued-message dismiss DELETE hardcoded /v1/api/workstreams without
the node-proxy prefix, so cancelling a queued message on a proxied
(remote-node) interactive workstream hit the console root, 404'd, and
the message was delivered anyway -- the dismiss silently did nothing.

composer_queue:
- Prefix getBase() onto the dequeue DELETE; interactive passes getBase
  (mirrors the attachment controller). Coordinator stays at base "".
- Never remove the card before the server confirms the cancel: removed
  -> drop the card; not_found (already drained) -> promote to a sent
  bubble + "already sent" notice; 404 (reaped session) -> terminal drop;
  error/timeout -> re-enable + "couldn't remove" notice.
- Bound the DELETE with a 15s AbortController (Promise.race fallback when
  AbortController is absent) so a wedged node can't freeze the card.
- a11y: aria-disabled (not the real disabled attribute) keeps keyboard
  focus on the dismiss control; in-flight state shown via aria-busy + CSS.

consumers (interactive, coordinator):
- Bound the send POST with the same 15s timeout so a pre-bind dismiss
  can't strand the card when the POST hangs.
- r.ok guard so a rejected send (4xx/5xx error body) surfaces as an
  error instead of being promoted as "delivered".
- Coordinator wires onNotice -> appendText.
2026-06-27 00:09:44 -07:00
Patrick Buckley f2e48166f4 fix(lowering): warn when operator context would fold onto an assistant turn
Operator-context system turns must follow a user/tool input turn — producers
maintain this via the user/tool drain seams plus the synthetic wake turn, so an
assistant predecessor is unreachable today. Add a fail-loud guard so a future
producer that breaks the invariant surfaces in logs instead of silently splicing
operator markup into the model's own prior output.

Logged, not raised: it degrades to a fold, since the nonce still gates operator
trust regardless of the host turn, so the harm is out-of-distribution voice
rather than a trust breach — disproportionate to crash a turn over.
2026-06-26 19:37:01 -07:00
Patrick Buckley 2c1ec9c230 fix(fence): guard detection_pattern against an empty tag set
Address PR review feedback:

- detection_pattern(()) with an empty tag set compiled to an overly-broad regex
  (the empty alternation matches any [start ...]/[end ...] run), which would turn
  the forgery scanner into a false-positive generator. Reject an empty or
  all-empty tag set up front. Not reachable from the sole caller today, but it is
  a public, security-relevant helper.
- Clarify build_operator_instruction_declaration's docstring: the trusted region
  is delimited by both the start and end markers (each carrying the nonce), not
  just the opening marker.
2026-06-26 19:37:01 -07:00
Patrick Buckley a318265946 fix(fence): bracket trust-fence markers instead of angle-bracket XML
Swap the trust-fence marker shape from <tag_nonce>...</tag_nonce> to
[start tag_nonce]...[end tag_nonce] for both the operator fold (system-reminder)
and the output-guard judge (tool_output). Angle-bracket markup pushed some local
models out of distribution and toward emitting their own turn-structure tokens:
chat templates built around rigid <...>-style structural tokens derail once a
few folded reminders accumulate. The start/end keywords carry no slash (no </ or
[/ closing-tag shape) and read as ordinary text.

Single-source the shape in fence.py (_OPEN_KW/_CLOSE_KW + detection_pattern) so
wrap, neutralize, the forgery/leak detector, and both trust declarations track
one definition. The nonce still rides both boundaries (unforgeable close); the
leak-vs-forgery split and the forge-in / break-out defang are preserved. The
fold is wire-only, so there is no migration; the legacy persisted-envelope
readers keep the old shape.

Add regression tests pinning each trust declaration to fence.wrap's emission so
a future keyword change fails loudly instead of silently desyncing the anchors.
2026-06-26 19:37:01 -07:00
Patrick Buckley 2169559d6e feat(projects): governed project containers — memory scope, grouping, manage UI (#724)
* feat(projects): governed project containers — memory scope, grouping, manage UI

A workstream can attach to a project: a first-class, shareable resource
container that owns a `project` memory scope, groups conversations, and is
managed from the console.

Storage / migration 062: projects + project_members tables, workstreams.
project_id, and the memory type default project→general; grants
project.{create,read,write,delete} (admin-default).

Recall + writes: project memory is recalled iff the workstream is attached AND
the user has access (owner ∨ member ∨ public-for-read), resolved once at session
construction; coordinators recall it too. New saves default to the project when
attached + writable; the save and delete paths are write-gated; deleting a
project purges its scoped memory; archived projects aren't recalled.

Access = RBAC capability ∧ per-project ACL (auth.resolve_project_access, a
single-fetch resolver); visibility changes, member management, and delete are
owner-only.

API: project CRUD routes on both the server and console; project_id threaded
through workstream creation, spawn inheritance, the cluster-create proxy, the
dashboard / snapshot / coordinator row builders, and the collector deltas.

UI: a project picker with an inline "+ New project" creator in every creation
box (console launcher + standalone dialog + dashboard); group-by-project in the
rail; a project badge in the composer and on dashboard rows; a console manage
tab (list + create/edit + members shelves). The admin Memories view gains
coordinator/project scope filters and human scope labels (name, not hex). The
memory tool schema documents the project scope and the attach-aware default.

* fix(projects): client refresh hardening, creator race guard, SDK project_id

Addresses PR #724 review feedback plus two bugs found while validating it.

- projects.js refreshProjects: a non-OK status (e.g. 403 when the caller
  lacks project.read) or a network/parse error no longer blanks the cache
  or masquerades as "no projects" -- the prior cache is preserved, the
  failure is recorded (new projectsError()) and warned. Honors the
  long-standing "a transient error can't blank the rail" docstring.
- projects.js _fp: the fingerprint separators were raw control bytes,
  which made git treat the whole file as binary (no reviewable diff).
  Rewritten as escape sequences instead of raw bytes -- behavior is
  byte-identical at runtime.
- project_creator.js: createProject() could reject unhandled (authFetch
  throws on network/401; r.json() throws on a non-JSON body), leaving the
  widget stuck busy/disabled. Added a .catch, plus a generation guard so a
  create whose widget was cancelled/reopened mid-flight drops its result
  instead of selecting a project the user backed out of.
- types.ts: add project_id to CreateWorkstreamRequest / WorkstreamInfo /
  DashboardWorkstream to match the server schemas (was SDK-invisible).
- test_project_api.py: move side-effecting HTTP calls out of asserts so
  the requests run even under python -O.

* fix(projects): JSON.stringify the cache fingerprint, drop control-byte separators

_fp joined fields/rows on raw NUL/SOH bytes, which made projects.js read as binary to git. Replace with a collision-proof, escape-free JSON.stringify encoding -- same change-detection semantics, zero embedded control characters.
2026-06-26 17:24:06 -07:00
Patrick Buckley c7d8acb6a5 fix(effect-status): harden effect_status decode + fix tests for typed synth
- Turn.effect_status also catches TypeError: a corrupt non-string meta value
  (e.g. a dict that survived into the column) would otherwise crash a consumer
  on access, since EffectStatus(non-str) raises TypeError, not ValueError.
  Degrade to None, mirroring the meta decoders (Copilot review).
- test_lowering: the wire-repair synth now carries the _effect_status side
  channel (stripped before the provider wire) — assert it.
- test_session_mcp_dispatch_error: the _capture stub swallows the new status
  kwarg via **_ so it stays signature-compatible with _report_tool_result.
2026-06-26 08:38:31 -07:00
Patrick Buckley b74a5e116b feat(effect-status): type tool dispositions, not just prose
The unknown / none / committed distinction the cancel and timeout paths
carry lived only in the result's free text — a deterministic reader (a
re-issue guard, owner-side compensation) couldn't recover it without
parsing prose. Promote it to a typed EffectStatus on the canonical Turn.

- EffectStatus (committed/none/unknown/partial/rolled_back) rides
  TurnMeta.extra["effect_status"] — wire-invisible like the other meta
  side channels: the model still reads the body, deterministic code reads
  the type.
- Persisted in the role-exclusive conversations.meta column (source_meta
  rides SYSTEM turns, effect_status rides TOOL turns), routed by role in
  reconstruct_turns. No migration; survives reload for the audit trail.
- Producer seam: _report_tool_result(status=) + a _tool_status dict popped
  at the fold, mirroring _tool_error_flags.
- Populated where the disposition is already determined: UNKNOWN at the six
  unobserved sites (bash / MCP-tool timeout, bash SIGKILL-cancel, cancel
  synthesis, wire-repair) and a precise none/partial/unknown on a cancelled
  task agent (shared _cancel_ledger so the typed status and the prose
  disposition can't disagree). Ordinary results stay unset.

Only the unknown/none split is load-bearing (HYPOTHESIS.md effect-record
appendix: unknown, never none); the full per-effect reversibility list
stays deferred. Thread A of the effect-record work; Thread B (per-tool
Smart-Approval floor + reversibility surfacing) follows.
2026-06-26 08:38:31 -07:00
Patrick Buckley c1ca742b54 fix(tools): timed-out side-effecting tools read UNKNOWN, not a flat failure
A bash command SIGKILL'd at its deadline and a timed-out MCP tool call are
killed / abandoned mid-flight, so their side effects are as unobserved as a
cancelled call's. Both read as a definitive "timed out after Ns", which invites
a blind re-run (a double-send) exactly as a dropped record invites an orphan.

Route both through a shared TIMEOUT_OUTCOME_CLAUSE so they read "Outcome
UNKNOWN ... do not assume it did not run, reconcile before re-issuing" — the
same "unknown, never none" discipline cancellation already follows
(HYPOTHESIS.md effect-record appendix). bash also keeps any partial stdout
captured before the kill, mirroring the cancel path.

Read-only timeouts (search, MCP resource/prompt reads) stay a plain failure:
an idempotent read has nothing to reconcile, so the reconcile advice would be
misleading there.
2026-06-26 07:32:35 -07:00
Patrick Buckley 16ac3f19b5 docs(hypothesis): gate-placement & effect-record appendix; scope incompressibility; split the two walls (#721)
* docs(hypothesis): gate-placement & effect-record appendix; scope incompressibility; split the two walls

Refinement + expansion pass on the harness hypothesis.

Appendix (new subsections):
- Gate placement (fail-closed, in practice): γ as a pure, effect-free
  parse-and-authorize; syntactic / user-authorization / structural-intent
  validation; semantic intent as a recursive plant call (a mini-harness),
  not a predicate in γ; "before any invocation" sharpened to "before any
  effect" — reads aren't free, the parser must not act, the output is an
  action too.
- Effect records (what ρ folds back): pins down the
  e = (tool_id, action_id, status, effects, time) shape the body referenced
  twice but never defined; committed/none/unknown trichotomy + a reversibility
  bit, framed explicitly as an open interface, not a result.

Corrections:
- Scope the incompressibility conjecture: split per-step drift by coordinate
  (the shell term is a low-complexity designed descent), so the incompressible
  part is the plant's, not all of W; add the coarse-functional counter-
  possibility (V* is one scalar hitting time, sometimes cheap) and state the
  claim conditionally. Walks back the earlier "the dynamics it certifies are
  the weights" overclaim.
- Split the second wall: the tape / space-O(L) picture follows from the
  autoregressive structure alone; the per-pass TC^0 bound is separate and
  weaker; flag that chaining them is a non-sequitur.

Smaller:
- Concrete justification for the standard-Borel assumption.
- Reading-table rows for the C/Y/A/E spaces and for H_ok/B.
- Daemon note: per-cycle hazard compounds, (1-q)^h over the horizon.
- Minimax: well-posedness caveat for sup over the adversary class Π.
- Note that H_cancel refines the body's deliberately coarse H\H_ok.

Notation (consistency linter clean):
- Brace the subscript A_{⊥} in the new table row (was unbraced — GitHub
  render hazard the linter guards against).
- Daemon cycle-count N → h, freeing N for the fundamental matrix.

* docs(hypothesis): address Copilot review — plain quotes + 'none' status value

- Effect-record status enum: add `none`, which the prose already treats as a
  distinct value ("unknown ... never none"; the committed/none/unknown
  trichotomy). Resolves the enum/prose inconsistency — `none` (no effect) is
  distinct from `rolled_back` (ran, then undone).
- Drop the two backslash-escaped quotes (the incompressibility walk-back and
  the minimax well-posedness caveat) for plain quotes, matching the rest of
  the document. GFM strips the backslash, so they rendered fine; the escapes
  were just unnecessary and inconsistent.
2026-06-26 05:24:32 -07:00
Patrick Buckley c0be383f99 refactor(doctor): replace turnstone-bootstrap with turnstone-doctor (#718)
* refactor(doctor): replace turnstone-bootstrap with turnstone-doctor

turnstone-bootstrap was an LLM setup wizard for Day-0; run.sh now owns install.
Repurpose its LLM/conversation plumbing into turnstone-doctor — a diagnose-only
tool for a running cluster.

- Preflight detects the install kind (docker-compose/systemd/pip/source) from
  config.toml + TURNSTONE_* env, with secret redaction.
- Self-configuring brain resolves the cluster's own model from config/env/storage
  read-only (no migrations, no create_all), falling back to interactive
  selection; the attempt itself is the LLM-backend health check.
- Deterministic version check: installed version, cluster drift via the console's
  authoritative /health, and latest upstream stable/experimental (offline-safe).
- Read-only diagnostic tools (read_file, compose/systemd/journal, http_health,
  check_llm_backend, node_health, finish) behind one secret-scrubbing chokepoint;
  no generic shell, so read-only is structural.
- node_health reaches a node the right way for the detected install kind
  (exec-into-container for compose, direct HTTP otherwise), overridable per node
  for mixed clusters.
- mTLS-aware: forwards [database] SSL params and reports node-mesh mTLS instead of
  mislabelling healthy nodes "unreachable".

init_storage gains a backward-compatible create_tables override for read-only
opens. Entry point turnstone-bootstrap -> turnstone-doctor; README/QUICKSTART/
architecture/docker docs, the bundled compose header, run.sh, and the CI smoke
updated. CHANGELOG deferred.

* fix(doctor): address Copilot + CodeQL review findings on #718

Validated all seven review findings (none false positives) and fixed:

- check_llm_backend now applies the same scheme / metadata-host guard as
  http_health (extracted to _assert_safe_http_url), so a model-supplied
  base_url can't be steered at the cloud metadata endpoint or a file:// URL.
- node_health no longer double-appends the default port when the operator
  passes host:port (regression: 10.0.0.5:8081 -> http://10.0.0.5:8081:8080).
- node_health install_type enum uses "git-source" to match the label the
  rest of the module and the prompt/report show the model (a schema-strict
  provider would otherwise reject the value the model is told to use).
- _read_api_creds takes base_url + api_key as a unit from the first config
  source that defines either field, then env-fills, instead of splicing the
  two across different config files into a pair that exists in no real config.
- _mask_secrets masks assignment-shaped content inside comment lines, so a
  commented-out real secret can't leak through read_file / the report; prose
  comments (no KEY=value shape) still pass through untouched.
- drop the mixed import styles CodeQL flagged in doctor.py and test_doctor.py.

Adds 5 tests; ruff + mypy clean; full doctor suite passes (129).
2026-06-26 04:57:20 -07:00
Patrick Buckley 4baf6f81c3 docs(hypothesis): clarity pass, GitHub-render fixes, consistency linter (#720)
* docs(hypothesis): clarity pass, GitHub-render fixes, consistency linter

Document (HYPOTHESIS.md):
- split the dense "Formal" definition into labeled subsections
- define the load-bearing terms: certificate (proven witness vs measured
  surrogate) and the controller / plant (= M_W) / shell triad
- corrections: three-way drift split (+ r_env), scope the success/safety
  collapse to absorbing refusal, unify tau*->tau_H and drop the orphaned bare tau
- calibrations: pin the incompressibility conjecture (still conjectural),
  mark the interlingua=certificate identity as figure, soften the two-walls trade
- GitHub math rendering: brace command-subscripts (_\bot -> _{\bot}, etc.) so the
  markdown emphasis parser stops breaking inline math; replace R_\# with R_{\sharp}
  (\# unescapes to a raw # in GitHub math)

Linter (lint_hypothesis.py):
- deterministic consistency checks A-G; G adds an orphan/redundant-declaration
  scan that catches the bare-tau failure mode
- residue guards so tau^star, unbraced _\cmd subscripts, and \# cannot return

* fix(hypothesis): make lint_hypothesis.py pass ruff under py311

- precompute the inline-$ count so no backslash sits inside an f-string
  expression (backslashes in f-strings are 3.12+; the project targets 3.11)
- split the one-line import (E401/I001); open HYPOTHESIS.md via a context manager (SIM115)
2026-06-26 03:57:36 -07:00
Patrick Buckley 4aaf6feac4 chore(cancel): address Copilot review nits
- console/server.py: replace a stale hard-coded `session_routes.py:852-854`
  comment reference (already drifted to make_close_handler's signature) with
  a by-name reference to make_close_handler's not-found path.
- test_cancel.py: rename test_marks_most_recent_action_unknown ->
  test_marks_in_flight_action_unknown; the disposition marks the first
  unanswered (in-flight) call, not the most recent — they merely coincide in
  this two-call case.
2026-06-26 03:28:06 -07:00
Patrick Buckley bc93b1f748 fix(cancel): address code-review findings before PR
The multi-stage review of this branch surfaced four major + two minor issues,
three of them in the new cancellation code. All fixed here (bug-3, the stale
generated TS SDK spec, stays deferred — it regenerates out-of-band).

- sec-1: cancelling a coordinator now auto-cascades to its children, but the
  cancel route allows the service-scope bypass while the removed stop_cascade
  gated the same destructive subtree-cancel at no-bypass — a service token
  without admin.coordinator could trigger the cascade. Re-assert the
  no-service-bypass gate inside _cascade_cancel_to_children, so a plain cancel
  by an under-privileged service token still cancels the coordinator's own
  turn but no longer cascades.
- bug-1: _cancelled_agent_disposition took the LAST issued tool call as the
  in-flight one. _run_agent executes a turn's calls sequentially, so the
  in-flight call is the FIRST unanswered one — taking the last inverted
  unknown/none on a multi-call turn (a SIGKILL'd bash mislabelled "not
  started", the never-run tail mislabelled UNKNOWN, inviting a re-run of the
  destructive call). Fixed to first-unanswered.
- perf-1: the per-child cancel fan-out was awaited inline before the cancel's
  200, so a cancel could block for tens of seconds on slow/unreachable
  children. Return the fan-out as a response BackgroundTask so it runs after
  the 200 (trigger, not drain).
- bug-2: the initial-send worker (_run_initial) cleared _worker_running
  unconditionally — the same clobber the session_worker guard just fixed.
  Apply the identity guard there too.
- sec-2: restore the per-child cascade audit row (coordinator.cancel_cascaded)
  the removed stop_cascade wrote; it had become log-only.
- q-1: extract the shared UNKNOWN-outcome clause (UNOBSERVED_OUTCOME_CLAUSE)
  so the wire-repair fallback and the session-layer synthesis can't drift.
2026-06-26 03:28:06 -07:00
Patrick Buckley 03f82521d9 fix(cancel): close workstream self-cancel gaps from the completeness review
Follow-up to the cancellation review — harden how cancel interacts with a
workstream's OWN turn and tools, not just its children and agents.

- wait_for_workstream: the wait loop holds no cancel handle and blocks on the
  child-event bus, so a cancelled coordinator parked in a wait stayed pinned
  for up to WAIT_MAX_TIMEOUT (600s). Add a cooperative check to the ~2s
  progress heartbeat — it raises GenerationCancelled, which propagates out of
  the otherwise cancel-blind wait (~2s abort).
- spawn_batch: stop creating the rest of the children once cancel is observed;
  already-spawned children stay recorded (they are live, durably parent-linked
  workstreams), the remainder are marked not-spawned.
- session worker: only clear _worker_running if this thread is still the
  current worker, so a late-finishing abandoned worker (force-cancel) can't
  clobber a live successor's flag — which would let a third send spawn a
  duplicate worker on the same session.
- bash silent-cancel: a SIGKILL'd silent command now records outcome-UNKNOWN
  (is_error, partial output kept) instead of a clean "Cancelled by user." that
  read as a successful empty result on replay.
- wire-repair: the last-resort orphan disposition now reads outcome-UNKNOWN,
  matching the cooperative-cancel message (unknown, never none).

Deferred: MCP / web_fetch / web_search remain uninterruptible mid-call,
bounded by tool_timeout; only bash is truly preemptible.
2026-06-26 03:28:06 -07:00
Patrick Buckley 776430d860 feat(cancel): honest cancellation dispositions + coordinator subtree propagation
A cancelled agent previously discarded its own ledger and reported a bare
"(task interrupted by user)" — fabricating the *outcome* (read downstream
as "nothing happened"), which invites a double-send as readily as a
dropped record causes an orphan. Make the fold-back honest, and propagate
an owner's cancel down the coordinator subtree.

- task_agent (single + parallel): on cancel, fold back a deterministic
  disposition built from the agent's in-memory ledger — actions completed,
  the in-flight action flagged outcome-UNKNOWN, and not-started calls —
  instead of the opaque interrupted string.
- coordinator cancel now auto-propagates to its direct children via a
  post_cancel hook on the shared cancel handler (cooperative fan-out; no
  blocking drain).
- synthesized cancelled tool results now read outcome-UNKNOWN rather than
  implying the call never ran.
- remove the now-redundant stop_cascade operator endpoint (handler, route,
  OpenAPI spec + schema, tests, docs); a coordinator cancel supersedes it.
2026-06-26 03:28:06 -07:00
Patrick Buckley fafc2d5617 fix(mcp): prune the refresh lock alongside backoff on the missing/decrypt path
Review follow-up (#717). The bug-1 fix made the transient keep-path retain the
per-(user, server) refresh lock for serialization, so the lock entry now lingers
after a transient failure. When the token then vanishes (missing) or goes
undecryptable, _no_token_result pruned only the backoff entry and left the lock
entry stranded, so mcp_oauth_refresh_locks could grow on that path. Drop both
sibling dicts in _no_token_result (removing the now-redundant explicit
_drop_refresh_lock on the in-lock decrypt return); the regression test asserts
both are pruned on the missing-after-transient path.
2026-06-25 23:13:30 -07:00
Patrick Buckley 800b561f56 fix(mcp): classify OAuth refresh failures so neither a blip revokes consent nor a dead grant strands the user
Follow-up to #714 (Entra OBO, #682). A refresh failure deleted the user token +
emitted token_revoked regardless of cause, so a transient AS/network blip during
a forced refresh (the live 401-retry path) permanently revoked consent
cluster-wide. Fixing only that, though, opens the dual failure: a genuinely-dead
grant the AS reports in a non-standard shape would now be kept forever and the
user stranded on a retryable error with no re-consent path. This classifies the
failure three ways so each is handled correctly.

Classification (_classify_refresh_failure): MCPOAuthRefreshFailed carries a
_RefreshFailureClass instead of a bool —
- PERMANENT (revoke + re-consent): an explicit dead-grant / re-consent signal —
  invalid_grant at any 4xx (400/401/403), invalid_scope, or an OIDC
  interaction-required code (interaction_required / login_required /
  consent_required / account_selection_required) the AS surfaces.
- TRANSIENT (keep, retry, never escalate): infrastructure (network, 5xx, 429,
  malformed body) and operator-fixable codes (invalid_client, invalid_request,
  unauthorized_client, unsupported_grant_type, temporarily_unavailable) —
  re-consenting the user can't fix a bad client_secret, and an outage must not
  revoke consent however long it lasts.
- AMBIGUOUS (keep, but escalate after a run): a 400/401 we can't map to a
  standard code. A one-off can't revoke, but an uninterrupted streak past a
  threshold escalates to re-consent so a dead grant in a non-standard shape
  can't strand the user. Infra transients reset the streak, so an outage never
  escalates.

Concurrency: do NOT drop the per-(user,server) refresh lock on the keep-the-token
path. Evicting it while the token is still live let a second concurrent caller
mint a fresh lock and refresh the same token in parallel; with refresh-token
rotation the second send reuses the consumed token, gets invalid_grant, and
spuriously revokes — the exact bug this commit prevents. The async-with still
releases the lock on return; the registry entry is pruned only when the token is
actually refreshed or revoked. Bit SQLite single-node hardest, where the pg
advisory lock is a no-op.

perf: a per-(user,server) cooldown short-circuits the token-endpoint round-trip
for a brief window after a transient failure, so a down AS isn't hit once per
tool call; self-heals when the window expires. Plus the lock-free in-flight key
set that collapses concurrent session-start pool primes (single mcp-loop thread).

dispatch/FE: the transient kind maps to a retryable mcp_refresh_unavailable
structured error (not mcp_consent_required); the FE titles it "Temporarily
unavailable" under a new soft "transient" category (amber, not the red hard-error
styling) in both stylesheets, with no wrong re-consent button.

tests: invalid_client kept (pins the discriminator on the error code, not the 4xx
status), single ambiguous 400 kept, 403 invalid_grant revokes, interaction_required
revokes, ambiguous streak escalates at the threshold, sustained 5xx never escalates
(outage safety), and the cooldown skips the second AS round-trip — all through the
real AS HTTP boundary.
2026-06-25 23:13:30 -07:00
copilot-swe-agent[bot] de271dc2f9 docs: remove Ollama from run.sh local backend list 2026-06-25 21:59:54 -07:00
Patrick Buckley 090f31c5c4 docs: drop Ollama from local-model lists (README + bootstrap wizard) 2026-06-25 21:38:49 -07:00
Patrick Buckley b939919560 fix(mcp): harden Entra OBO OAuth review follow-ups for #706
Follow-up review of the #706 on-behalf-of / Entra ID MCP changes (#682).

security (PKCE downgrade): the AS-metadata "assume S256 when
code_challenge_methods_supported is absent" relaxation applied to BOTH the
RFC 8414 oauth-authorization-server document and the OIDC openid-configuration
document. Per RFC 8414 an omitted field on the oauth-authorization-server
document means the AS does NOT support PKCE, so this was fail-open. The client
always sends code_challenge_method=S256, making this discovery check the only
pre-flight that the AS enforces PKCE. Track which document won discovery and
assume S256 only for the OIDC document; the RFC 8414 document now fails closed.
Also log which discovery profile (rfc8414 vs oidc) answered, for operators
debugging an enterprise AS.

bug (consent loss): session-start pool priming called the refreshing token
lookup for every cold oauth_user server. A near-expiry token triggered a
refresh, and a transient refresh failure (network/5xx/429) deletes the token
and emits token_revoked — so a blip during a cold-pool warm (e.g. after a
reboot) silently revoked consent across servers the user wasn't even using.
Priming now reads the token directly and skips missing/near-expiry tokens;
a refresh that may fail stays on the lazy dispatch path.

perf/UX (blocking redirect): the OAuth callback awaited prime_user_server
(default 20s timeout), holding the consent redirect on a slow/unreachable MCP
server. Replaced with fire-and-forget schedule_prime_user_server that schedules
onto the mcp-loop (GC-safe, no unreferenced request-loop task) and returns at
once.

perf: prime a user's pools concurrently under a bound instead of serially, so
one slow upstream can't stall the rest.

hygiene: log (not silently swallow) prime scheduling failures at session start;
add exc_info to the prime-failure warning; guard run_coroutine_threadsafe
against a closed mcp-loop.

tests: per-document S256 + OIDC-fallback discovery cases; pool priming
(non-destructive on near-expiry, skips connected) and bound-token rotation
reconnect.
2026-06-25 20:42:22 -07:00
github-actions[bot] 0ed8d19db5 chore: download vendored JS files 2026-06-25 19:42:08 -07:00
renovate[bot] 8e362986cc chore(deps): update dependency mermaid to v11.16.0 2026-06-25 19:42:08 -07:00
metaclassing 76e9d0a4f1 Address remaining blockers to entra id on behalf of flow for user impersonation to mcp servers (#706)
* This is a collection of little snippits to resolve all the OBO flow problems required to get this talking to entra id for on behalf of user impersonating to protected mcp servers. we make sure turnstone checks these mcp servers on startup, and address some of microsofts opinionated implementations of oauth2/oidc and metadata provided by the identity provider.

* minor token timeout bugfix

---------

Co-authored-by: root <root@pow3rtools>
2026-06-25 19:26:47 -07:00
renovate[bot] 1c746af36f chore(deps): update actions/checkout action to v7 2026-06-25 19:21:07 -07:00
renovate[bot] c99d1d42ba chore(deps): update ghcr.io/astral-sh/uv docker tag to v0.11.24 2026-06-25 19:18:32 -07:00
Patrick Buckley ff5db73f97 chore(ui): refresh favicon to chevron mark across web entry points
Replace the amber gauge/needle favicon with a teal up-chevron and amber
dot on a dark-teal field. Applied identically to the console, coordinator,
and standalone UI entry points. Self-contained inline SVG data URI; no
network dependency.
2026-06-25 19:18:09 -07:00
renovate[bot] af98fe434a chore(deps): lock file maintenance 2026-06-25 19:17:12 -07:00
renovate[bot] c67b1ce9af chore(deps): update github actions 2026-06-25 19:16:47 -07:00
Claude e7e3135a37 docs(hypothesis): add "Appendix: model implementation" — cancellation as the worked pattern
Establishes the appendix pattern (locate a practical concern in the existing
formal objects; read off the discipline rather than inventing machinery) with
cancellation as the first and only worked example. Not the whole model.

Cancellation semantics, derived from objects already on the page:
- Cancel is a signal → lives in s (Markov). The gate closes on it: γ(s,y)=⊥ while
  live, which blocks pending actions and all future turns with no new machinery.
- In-flight (past γ) disposition is a trinary on the kind of Q_E: cancellable
  (propagate, true end-state), bounded (drain, real e), or opaque/unbounded
  (controller fabricates a synthetic "cancelled" e so the loop can halt).
- Load-bearing rule: ρ may fabricate the acknowledgment but not the outcome — an
  unobserved outcome is `unknown`, never `none` (double-send vs orphan, same bug
  opposite sign).
- New terminal H_cancel ⊆ H\H_ok: non-accepting but safe (outside B), postcondition
  "no action past γ after observed; in-flight drained or recorded unknown; ledger
  consistent."
- Cooperative not preemptive (observed at next γ check, not on send); recursive
  down the task-agent subtree (why task agents are the worst case).
- Compensation is the owner's job (saga, after H_cancel, reads child ledger) — the
  cancelled agent can't know if it's needed; it never observed the outcome.
- Design pressure: prefer bounded/instrumented Q_E over opaque, so cancellation and
  the ledger stay honest (a bash wrapper converts branch 3 -> branch 1).

Linter: balanced, no new collisions. Two ρ role-flags, both false positives
("authorized action" near ρ, correct usage).
2026-06-22 12:16:45 -07:00
Claude d1acded028 docs(hypothesis): round-sixteen (maxima re-run) — semantic-axis fixes the linter cannot see
The linter closed mechanical consistency; this review probes meaning, a separate
axis. Twelve findings, several real corrections, all folded.

Correctness:
- Stationarity overclaim: the supermartingale BOUND survives a nonstationary
  kernel under uniform conditional drift. Time-homogeneity is needed for V* as a
  fixed function, the resolvent/fundamental-matrix identities, and δ-calibration.
- Self-contradiction: "the certificate cannot be proven, only observed" contradicted
  the established "a proven inequality certifies" — reworded to "the architecture
  does not hand it to you; estimated unless separately certified."
- Citation: the TACL result is LOG-precision → logspace-uniform TC⁰ (verified);
  fixed/constant precision is a stronger restriction. Fixed in body and Grounding.

Modeling holes closed:
- Adversary class Π must respect rejection: γ(s,y)=⊥ ⇒ Q_E^α(s,⊥,·)=δ_e0, else the
  adversary resurrects refused side effects.
- R must be a syntactic/verified readout, not a semantic solver — otherwise the
  L-wall is void (compute could hide in R off the ≤L window).
- e must be an effect record (ledger outcome), not just API bytes, since only ρ
  writes external effects into s.
- The displayed M_W(c) freezes endpoint/version/sampler; config changes need a
  state-indexed M_{κ(s)} or K_C — the kernel can't silently depend on config in s.
- The final user-visible response/log is itself an effect: an authorized action
  through γ, or emitted only after an accepted halt.
- m_t must include a token counter and clock for the cap/timeout to be functions of it.

Residue (omissions a collision-linter can't catch):
- Another γ dropped from the K_C Dirac-special-case list.
- τ* mislabeled as "designed code" → the halt test (H) is the code; τ* is its
  emergent hitting time.
- H\H_ok relabeled "non-accepting" (safe refusals outside B; wrong/bad halts
  possibly in B), not uniformly "rejecting/fail-closed."
2026-06-22 12:16:45 -07:00
Claude d61baea8ae docs(hypothesis): round-fifteen — deterministic consistency lint; resolve G collision
Built and ran a static linter (no model): delimiter/emphasis balance, residue
regexes for everything prior rounds fixed, single-capital collision scan, a
definition check for recently-introduced symbols, and a γ/ρ role-neighborhood
scan. Result:
- All balance checks pass; all 10 residue regexes clean (no regression across
  14 rounds); all 12 introduced symbols defined; no display-only symbols.
- γ/ρ scan: one flag, a false positive (the symbol-table cell defines both).
- One real find: G was overloaded — the parser-stop update G(m_t,v) (added in
  round 14) collided with the Green/potential operator G. Renamed the stop-update
  to \mathsf{step}; the Green operator G is now unique.

This closes the consistency axis deterministically rather than by another review.
2026-06-22 12:16:45 -07:00
Claude 4ee148d9c0 docs(hypothesis): round-fourteen (maxima re-run) — round-13 residue, role/contradiction fixes, exact citation
Same prior-maxima full-tools review, re-run. Found mostly residue from round-13's
own edits plus longer-standing inconsistencies. Folded all; left the final
signature line alone (it is the author's call, and it is well-formed — see below).

Round-13 residue:
- 𝒴/𝒴_⊥ split half-committed: 𝒴 already includes ⊥, so R:𝒵→𝒴 and A_Y⊆𝒴 (drop _⊥).
- m_t was added to the inner triple with no dynamics: add m_{t+1}=G(m_t,v), define
  the stop set Stop and τ=inf{t:m_t∈Stop} in both display and prose.
- No-truncation special case had R=id, ill-typed on a triple: R(c,b,m)=c.
- Safety/success "exactly on safe refusals" overclaimed: they differ on any
  B-avoiding non-success run — also safe non-halting / endless safe retry, absent
  a.s. absorption into H∪B.

Role residue (γ does authorization/rejection; ρ does response/fold-back):
- "⊥ branch is what ρ rejects" → γ rejects it.
- "ρ validates response as well as the proposal" → ρ validates the response; γ
  gated the proposal.
- "fail-closed rejection at ρ" (falsification list) → at the gate γ.
- Symbol table still typed Q_E on authorized a → a∈𝒜_⊥ with the no-op; define e_0.

Longer-standing:
- Stochastic-controller contradiction: stochastic control falsifies the
  deterministic special case, not the broader K_C kernel model (round-9 K_C).
- Drift split r=r_shell+r_plant needs an additively separable V̂ or a declared
  attribution scheme.

Citation (verified via search, not the reviewer's say-so):
- TC⁰/log-precision → Merrill & Sabharwal, "The Parallelism Tradeoff", TACL 2023;
  caveat (added autoregressive steps escape it) → Merrill & Sabharwal, "The
  Expressive Power of Transformers with Chain of Thought", ICLR 2024.
2026-06-22 12:16:45 -07:00
Claude 7a403e5d91 docs(hypothesis): round-thirteen (prior-maxima full-tools audit) — consistency-debt cleanup
A cold reviewer given the complete prior-maxima changelog + full tools ran a
consistency audit of the file (it did not use tools for grounding — the gap was
internal). Found 15 real issues, all folded. No new design flaws; this is
accumulated editing debt from 12 rounds of surgical patches.

Half-applied fixes now propagated:
- Inner-kernel display still showed M_W(c)=Law(c_τ) and R:C→Y_⊥ despite the round-12
  triple; made z_t=(c_t,b_t,m_t) primary, R:Z→Y_⊥, M_W=Law(R(z_τ)).
- Append formula still used bare c·v; now suffix_{≤L}(c·v) in the display.
- Tuple still called B a "terminal set"; B is separate (τ_B fires mid-run).
- "halt/ready" survived at line 75 (fixed before only in tuple + table).

Collisions created by added notation:
- γ was both the authorization gate and the RL discount in (I-γP)^{-1}; discount → β.
- B was both the bad set and the dummy measurable set in the pushforward; dummy → A_Y.
- ρ over-credited as the disturbance-rejection margin; for side effects the margin
  is γ (consistent with round-12 irreversibility), ρ validates response/fold-back.

Real error in a prior round:
- The round-12 safety/success distinction collapses under absorbing refusal
  (Pr(τ_Hok<τ_B) requires reaching H_ok, so it is a success form). Split correctly:
  p_succ=Pr(τ_Hok<τ_F), F=B∪(H\Hok); p_safe=Pr(τ_B=∞); they differ on safe refusals.

Typing / hygiene:
- Q_E typed on S×A_⊥ (it is applied to ⊥); 𝒴 declared to include ⊥ (M_W, γ total).
- Controller list omitted γ and mis-listed the readout (specialization-only).
- Defined the previously-bare symbols D={s:E[τ_H]=∞}, μ, the drift r(s), and Π.
- Grounding "verify by measured drift" overstated; a proven inequality certifies,
  empirical drift only checks — reconciled with the body.
2026-06-22 12:16:45 -07:00
Claude afd66df4c0 docs(hypothesis): round-twelve (cold review, sandbox) — inner-state triple, authorization irreversibility, safety vs success
A cold no-priors review (given a local sandbox it did not use — the remaining
work is judgment, not computation). Mostly editorial/formal; its real catches
again concern round-10/11 additions. Folded the substantive ones, declined the
"extract a smaller core" restructure and the formalism padding.

Substantive:
- Inner kernel: replace round-11's awkward "read c_τ as the buffer" overload with
  a clean inner-state triple z_t=(c_t,b_t,m_t) — window, output buffer, parser/
  stop state — and M_W(c,·)=Law(R(z_τ)) from z_0=(c,∅,m_0). Strictly cleaner.
- Authorization is the irreversibility boundary: ρ can reject a bad tool RESPONSE
  but cannot undo an authorized action's side effects, so γ (not ρ) is the last
  line before irreversible effects. And the gate is bypassed if raw y reaches any
  sink (tool, logger, browser, remote) before γ.
- Safety ≠ success: p_ok=Pr(τ_Hok<τ_B) is the safety object (refusal permitted);
  the stricter success object races H_ok against all failure F=B∪(H\Hok). They
  differ exactly on safe refusals.

Precision:
- Foster–Lyapunov positive recurrence needs irreducibility/petite-set hypotheses;
  the absorbing-halt case needs only the weaker supermartingale hitting-time bound.
- Name an initial distribution s_0~μ_0. Fix residual "halt/ready" in the table
  (round 11 fixed only the tuple).
2026-06-22 12:16:45 -07:00
Claude 79cf8dae0b docs(hypothesis): round-eleven (confirmatory cold review) — fix B/terminal partition, fail-closed, output buffer
A JSON-constrained cold review largely validated round 10; its new catches
cluster in round-10's newly-added material.

Fixes (the real ones):
- Terminal-set partition was wrong (a round-10 error): B is NOT a terminal
  component — τ_B can fire mid-run. H now splits into accepting (H_ok) and
  rejecting/fail-closed (H\H_ok); B is a separate unsafe set for reach-avoid.
- Fail-closed generalized: rejection need not be terminal (reject-then-retry is
  valid) — define it as "no unauthorized side effect + land in a safe non-bad
  set," with terminal rejection one case. ρ must also validate the tool RESPONSE
  e (adversarial/malformed Q_E output), not only the model proposal at γ.
- Sliding-window truncation (round-10) loses transcript: the readout R reads a
  separate output buffer, not the truncated c_τ alone.

Precision:
- Deterministic maps are measurable transforms inside the pushforward, not
  literally "outside the integral."
- Absorbing halt H vs the separate (non-absorbing) daemon "ready" recurrence.
- "Syntactic soundness is free" qualified: relative to a formal schema and a
  correct validator.
- State-ablation falsifies Markovity but cannot establish it (necessary, not
  sufficient). Added a readout-typing diagnostic.
2026-06-22 12:16:45 -07:00
Claude 23a79a6c64 docs(hypothesis): round-ten (no-priors review) — authorization gate, minimax fix, raw-vs-correct halting
A cold no-tools external review (lower trust on world-facts, but its catches are
math-internal and correct) found two real bugs plus rigor gaps.

Bugs fixed:
- Verification-after-side-effect (the important one): the kernel ran e~Q_E(s,y)
  then ρ verified, so a tool call's side effect landed before authorization. Add
  a deterministic authorization gate γ:S×Y→A_⊥ between model and environment;
  Q_E now acts on the authorized action γ(s,y); ρ becomes ρ(s,y,a,e). Fail-closed
  is now a property (γ=⊥ ⇒ no-op env ⇒ fold to H\H_ok), not a name.
- Minimax drift display had a free y (introduced round 9): it integrated only
  over e while y~M_W(π(s)). Now integrates over both y and e, adversary as a
  policy α(s,y) over environment kernels, on the authorized action.

Reframing / rigor:
- Raw halting is cheap: a budget counter k gives V=k as a trivial halting
  certificate, so "no certificate by construction" overstated. The missing
  guarantee is correct/safe/successful halting (H_ok, B, p_ok).
- Standard Borel spaces (not merely measurable); define H, H_ok (⊆H), B (∩H_ok=∅)
  and hitting times τ_A up front; add 𝒜 to the tuple.
- Inner kernel: truncate c·v to suffix_{≤L} at the window edge; M_W is a
  probability kernel only via EOS/max-token/timeout/⊥ (else sub-probability +
  cemetery).
- Drift: weaker bound δ≤δ̄<ε gives E[τ]≤V̂/(ε-δ̄); distinguish δ_ν (distributional)
  from δ_sup (worst-case).
- Injection enters π's inputs (retrieval/pages/tool metadata), not only post-model
  Q_E; B needs a side-effect ledger in S. Architectural invariants stated
  (model sees only C; outputs are proposals; γ gates side effects; terminals
  partitioned). Complexity/LBA material marked heuristic, not definitional.
  An LLM-judge verifier is a learned kernel, not deterministic ρ.
2026-06-22 12:16:45 -07:00
Claude 2c943d2634 docs(hypothesis): round-nine (fresh review) — stochastic controller, minimax policy, reach-avoid
A cold external review (same priors, no path-dependence) surfaced three real gaps
the iterative chain missed, plus precision items. Folded in:

Substantive:
- Stochastic controller: the deterministic π,ρ,H are the Dirac special case of a
  controller kernel K_C(s,dc) (routing, sampled retries, ensembles, learned
  routers). Deterministic is the case worth wanting (localizes randomness); the
  split widens, not breaks, under stochastic control.
- Minimax type fix: the adversary chooses a POLICY/kernel, not the realized
  sample. Display is now sup over α of ∫ V(ρ(s,y,e)) Q_E^α(s,y,de), not sup over
  the post-probability e.
- Reach-avoid security: add a bad set B; injection steers toward B (wrong
  acceptance, exfiltration, unauthorized tool use, privilege escalation,
  irreversible effects), so security is reach-avoid p_ok=Pr(τ_{H_ok}<τ_B) with a
  barrier certificate for B, not liveness. B and H_ok added to the tuple.
- Unconditional V*_ok is infinite under any positive pre-acceptance failure
  probability ⇒ the workable object is p_ok (or the regenerative time on restart).

Precision / hygiene:
- Compiler claim scoped to a specific data-flow analysis (not a whole compiler);
  add integrability/optional-stopping conditions to the hitting-time bound.
- Formal hygiene: spaces measurable, τ/τ* stopping times, H absorbing.
- Mid-generation tool calls interleave the loops — clean nesting is an
  idealization needing a finer state machine.
- Soften SSM ("different", not "tighter"); demote "manifold" to informal
  shorthand in the formal section; gloss "all undefined behavior" as "no complete
  formal source-language semantics."

Not changed: V* incompressibility (already labeled conjectural in Grounding).
2026-06-22 12:16:45 -07:00
Claude 6c32b7e3cf docs(hypothesis): round-eight micro-edits — precise compiler claim, well-posedness wording
The review's verdict was "Merge." These are its two correct non-blocking nits;
its third nit (stop adding theorems/caveats) is heeded — nothing else changed.

- Grounding: "the compiler's V is free" → "a classical monotone data-flow
  analysis gets its V for free." A whole compiler does not get termination for
  free; the specific lattice-based analysis does (Kildall).
- Asserted: the Koopman/certificate co-determination "holds only under" →
  "is well-posed only under" the spectral assumptions — avoids asserting truth
  ("holds") for a claim explicitly labeled as not-a-theorem.

Deliberately NOT changed: D → D_H (prose already marks D harness-relative;
subscripting one formula while D stays bare elsewhere would add asymmetry, not
remove it), and no further theorem additions or caveats per the review's note
that more caveating now costs clarity without adding rigor.
2026-06-22 12:16:45 -07:00
Claude 480acd262b docs(hypothesis): round-seven micro-patch — harness-relative reachable, attribution, grounding
The review's verdict was "mergeable"; these are its three optional items plus the
delta-attribution nit.

- δ attribution: sampled-state coverage is an evaluation-protocol property, not a
  weights property. Attribute the noise floor / residual risk to the trained
  weights, the environment, AND the evaluation distribution.
- reachable(L) is harness-relative too (same reason U_H(L) is): rename to
  reachable_H(L) and note the divergent set D is likewise relative to H.
- Split the dense frontier paragraph in two: (1) the SR / fundamental-matrix /
  potential-operator identity with its caveats; (2) the speculative interlingua/
  certificate thesis. No content change.
- Grounding: add the absorbing-chain fundamental matrix (Kemeny & Snell 1960),
  the general-state potential/Green operator (Revuz 1984), and Koopman (Koopman
  1931; Lyapunov-from-eigenfunctions, Mauroy & Mezić 2016) to Proven; mark the
  Koopman/certificate co-determination (spectral-assumption-dependent) and the
  interlingua/certificate identification as Asserted.
2026-06-22 12:16:45 -07:00
Claude 08edb14588 docs(hypothesis): round-six fixes — harness-relative U_H(L), Neumann caveat, predicate split, Koopman hedge
- U(L) is harness-relative: tools and decompositions change membership, so rename
  to U_H(L) and note the shell's verified tools / decompositions determine what
  can be paged or outsourced.
- Countable fundamental matrix: lead with the Neumann series N=Σ Q_tr^n, scope
  countable to convergence, and write (I-Q_tr)^{-1} only when the inverse exists;
  general-state version is the same series read as the potential (Green) operator.
- Distinguish failure modes for V*_ok: infinite under a formal success predicate
  vs undefined if no predicate has been specified.
- Soften the delta "floors" line: mu(D), sampled-coverage, and Var[tau*] drive
  the empirical noise floor / residual risk, they are not literal floors of the
  drift slack.
- Hedge the Koopman bridge (the last frontier thread): the eigenbasis claim
  presumes a diagonalizable, point-spectrum operator — mixing dynamics carry
  continuous spectrum and admit no eigenbasis — and the linearizes/certificate-
  decomposes coincidence holds only for a V in the span of those eigenfunctions.
2026-06-22 12:16:45 -07:00
Claude 3f1be4f963 docs(hypothesis): round-five fixes — finite-mean hitting, potential operator, absorbing failure
Address the round-five review's three precision points (plus the adaptive-adversary
refinement).

- Absorption is finite expected hitting time, not positive recurrence: replace
  "positive-recurrent to H" with "reached in finite expected time," domain
  {s : E_s[τ_H] < ∞}. Positive recurrence stays reserved for the daemon/
  ready-state case (where it is used correctly).
- The fundamental matrix N=(I-Q_tr)^{-1}=Σ Q_tr^n is the finite/countable object;
  the formal model lives on general measurable spaces, so add the general-state
  potential (Green) operator G=Σ Q_tr^n with G·1=V* where the series converges.
  Q_tr now stated as the sub-stochastic kernel restricted to H^c.
- V*_ok is taken on the process where H\H_ok (halting wrong, refusing, failing
  closed) is absorbing failure — so a run that fails closed before acceptance
  has infinite accepting hitting time unless the spec restarts it. This is the
  mechanism by which a U(L) task sends V*_ok → ∞.
- Adaptive adversary: nonstationary Q_{E,n} → time-ordered product; an adaptive
  adversary → controlled / game-value operator (not merely time-indexed).
2026-06-22 12:16:45 -07:00
Claude 509f6e29a3 docs(hypothesis): pre-emptive round-four fixes — halting vs success, fundamental matrix
Fold in the two seams flagged after round three, before the next review pass.

- Limit section now states explicitly that its V*=E[τ*|s] certifies *halting*
  (reaching H at all), not correct halting; defers V*_ok (expected time to an
  accepting H_ok ⊆ H) to the second wall. Removes the latent inconsistency
  between the limit section (plain H) and the U(L) refinement (H_ok).
- Frontier section: the discounted successor-representation resolvent
  (I-γP)^{-1} presumes a discount γ and fixed P the stopped formulation lacks.
  Replace with the correct undiscounted/absorbing object — the fundamental
  matrix N=(I-Q_tr)^{-1}, Q_tr the sub-stochastic transient block — whose row
  sums N·1 are exactly V*. Converts analogy-dressed-as-identity into a true
  identity for the doc's own kernel.
- Mark the "one object seen twice" identity as holding only in the stationary
  regime: under the adversarial Q_{E,n} the resolvent/fundamental matrix become
  a time-ordered product, so identity in the stationary case, analogy beyond.
2026-06-22 12:16:45 -07:00
Claude 78f4b644b4 docs(hypothesis): round-three review fixes — V*_ok vs V*, pushforward readout, tool discharge
Address the round-three review. The substantive one is the V* correction.

- Successful halting vs raw halting (the real conceptual fix): a U(L) task does
  NOT make V*=E[τ_H|s] undefined — the chain can still hit H by failing closed,
  refusing, or returning a wrong answer. Split H from the accepting set H_ok and
  define V*_ok=E[τ_{H_ok}|s]; U(L) blows up V*_ok, not V*. Restate the domain as
  dom_{<∞}(V*_ok) ⊆ reachable(L)\D.
- Tools compute, not just store: the L-wall binds *model-mediated* work; work
  discharged to a verified external tool (solver, interpreter, compiler) runs
  off-context. U(L) now excludes tool-dischargeable work explicitly.
- Readout typing: use the pushforward M_W(c,·)=R_# Law(c_τ) (equivalently the
  conditional law); make R total, R: C → Y_⊥, with the ⊥ branch handled by the
  fail-closed ρ.
- Adversary/history: a history-conditioning adversary needs that history in s,
  else the object is a Markov game requiring further augmentation, not a chain.
- Hedge the LBA claim: "in the variable-L, fixed-precision idealization, the
  model-mediated inner computation behaves like a linear-bounded automaton."
2026-06-22 12:16:45 -07:00
Claude 7c84cc5353 docs(hypothesis): round-two review fixes — readout typing, S vs C, adversarial kernel
Address the three follow-up points on the first review patch.

- Reconcile the model kernel's two types: M_W(c,dy) maps into 𝒴, while the
  transformer line writes M_W(c)=Law(c_τ) over contexts. Add the readout R:
  𝒴 is either c_τ itself (𝒴=𝒞) or a deterministic readout R(c_τ), with
  M_W(c,dy)=Law(R(c_τ)∈dy).
- Separate harness state 𝒮 from model-visible context 𝒞: the L wall binds 𝒞
  (the L×d residual stream), not 𝒮. External stores (files, DBs, vector stores,
  durable memory) are shell-supplied memory that extends addressable storage but
  not the per-pass resident set — every read still routes through the ≤L window.
  Retype U(L) accordingly: not data exceeding L (pageable) but irreducible
  per-step working set exceeding L (not pageable).
- Make the time-homogeneity assumption explicit at the formal kernel: the
  displayed T is the fixed-kernel case; nonstationary/adversarial environments
  replace Q_E with a time-indexed kernel Q_{E,n} / admissible family, which the
  minimax certificate downstream quantifies over.
2026-06-22 12:16:45 -07:00
Claude 853bb27b3a docs(hypothesis): apply peer-review fixes — typing, Lyapunov status, δ as risk metric
Address the accepted points from an external peer review while preserving the
controller/plant thesis and the document's voice (layer, don't flatten).

- Claim: replace the ill-typed `T = ρ ∘ (M_W ∘ π, E)` with the integral
  transition kernel over (𝒴,ℰ); add explicit informal/formal split; demote the
  residual-stream implementation from definitional to a kept specialization
  (M_W as a general learned kernel); weaken "fixpoint searches" to hitting-time
  processes with fixpoint as one mode.
- Reading-it: note s is Markov only after state augmentation; mark controller
  determinism as conditional on versioned code/config/endpoint/interfaces.
- The limit: rephrase "carries no descent function by construction" to "supplies
  no certificate automatically" (a certificate is sufficient, not provided for
  free); label V* incompressibility as conjecture, not theorem.
- δ: "measure" → "estimate"; demote empirical δ from certificate to calibrated
  risk metric (confounds: bad V̂, coverage, sup not attained, nonstationarity,
  non-Markov); certificate only once statistically bounded.
- Cash-out: split "soundness is free" into syntactic soundness (free) vs
  semantic adequacy (empirical).
- Qualify the single-pass TC^0 claim (fixed-depth/fixed-precision; log-depth
  changes it) in both body and Grounding.
- Add an operational falsification program (state-ablation, determinism audit,
  drift calibration, adversarial-environment, boundary-control ablation).
2026-06-22 12:16:45 -07:00
Patrick Buckley a5f10b1c8c docs(readme): render the headline formula as a code block (PyPI-safe) 2026-06-22 01:10:20 -07:00