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.
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.
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.
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.
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.
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.
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.
* 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.
- 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.
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.
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.
* 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.
* 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).
* 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)
- 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.
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.
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.
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.
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.
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.
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.
* 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>
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.
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).
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."
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.
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.
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.
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).
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.
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 ρ.
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).
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.
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.
- 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.
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).
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.
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."
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.
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).