fix(desktop): stop polling unavailable collaboration authority - #1
Closed
testikun wants to merge 386 commits into
Closed
fix(desktop): stop polling unavailable collaboration authority#1testikun wants to merge 386 commits into
testikun wants to merge 386 commits into
Conversation
Keep one lazy peer endpoint for each Desktop or CLI owner so Direct profiles and reconnects reuse one Swarm. Cancel individual connection attempts without tearing down unrelated streams, and close the endpoint with its owner lifecycle. Generated-by: Codex
* docs: add DeepWiki badge * docs: fix DeepWiki badge position and mirror to zh README * docs: disclose DeepWiki documentation status * docs: shorten DeepWiki disclosure --------- Co-authored-by: M4n5ter <m4n5terrr@gmail.com>
…pache#4031) Session retirement purges run resolveArtifactRemovalEntry() for every non-target live record, and each resolution issues realpath/lstat syscalls against the artifact root. A serial loop over ~11.7k records turned session-cleanup purges into a minutes-long syscall storm (issue apache#4027). Prepare removal plans with a small shared-index worker pool (8 in flight). Workers capture per-record results and always drain the queue, so all filesystem work settles before the mutation releases the writer lock and resolver failures surface in record order rather than completion order. Case-alias validation remains sequential and deterministic in record order. Local synthetic benchmark (10k records / 100 targets, 12 warm runs): 12 serial runs 4,422,7,587,11,282,9,855,8,178,8,974,9,250,8,908,9,628, 9,927,10,093,9,230 ms (median 9,108) and 12 pooled runs 3,343,5,926, 4,449,5,779,5,118,5,732,5,928,6,197,5,348,5,915,6,012,6,314 ms (median 5,855) => ~1.56x wall-clock speedup on this loop-mount workload. Storage package suite: 989 tests / 971 pass, 18 skipped, 0 fail. Generated-by: Kimi k3-256k
* feat(runtime-host): add peer mesh membership Add durable signed Mesh membership and recoverable authenticated invitations. Share one peer endpoint across application and Mesh protocols while preserving bounded lifecycle, connection ownership, and release-package validation. Generated-by: OpenAI Codex * fix(runtime-host): allow peer mesh readmission Treat a replica roster as last-known signed state rather than proof of current membership. Let a fresh invitation reach the authority and replace local state only with a newer roster from the same authority. Exercise removal, member restart, and re-admission through the installed CLI artifact. Generated-by: Codex * test(runtime-host): accept completed peer stream close Mirror the public peer stream close contract in the native test helper. A remote EOF may complete the stream before the second endpoint sends its explicit close, which is already treated as an idempotent success by the production binding. Generated-by: Codex
Generated-by: Codex
…e#4024) * feat(ui): add font-size controls to Appearance settings Appearance only exposed theme and palette; there was no in-app way to change font size. The type scale was fixed at base 14 and both xterm.js terminals hardcoded fontSize 12, so users on large/high-DPI displays had to fall back to OS-level scaling. Add two controls to the Appearance page, mirroring Codex Desktop's UI Font / Code Font but kept as separate knobs: - UI font size: a document-root multiplier (UI_FONT_SCALES). Every --font-size-* token is rem, so one root font-size uniformly scales text, icons and rem spacing — the intended UI-zoom hook, NOT the old density hack removed in makaTheme.ts. Restored before first paint in cached-theme-bootstrap to avoid a resize flash. - Terminal font size: feeds getTerminalFontSize() into both Terminal instances; live terminals subscribe and re-fit (re-emitting geometry to the PTY) on change. Both persist under appearance (already client-owned) and fail closed to their defaults on any malformed value, so an out-of-range persisted value can't drive an arbitrary root size — avoiding the Codex issues where an extreme value made the UI unusable. Refs apache#4021 Generated-by: Claude Opus 4.8 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(ui): present font size as Codex-style numeric px steppers Replace the segmented tile pickers (紧凑/标准/大/特大 and per-size cards) with two NumberInput rows — "UI 字号" and "终端字号" — each a px stepper with min/max clamp and a units suffix, matching Codex Desktop's Appearance layout. - Settings model switches from a scale allowlist to a clamped px value: uiFontSize (default 14) and terminalFontSize (default 12). Wrong-typed values still fail closed to the default; out-of-range numbers now clamp to the nearest bound instead of resetting. - UI font size maps to the document-root font-size as 16 * px / 14 (the type-scale base), preserving the uniform-zoom behavior. - Terminal font size keeps the live subscribe + re-fit path. Note: Codex's second knob also resizes chat/diff code blocks. In Maka those are governed by the type-scale contract, so this keeps the second knob scoped to the terminal (xterm), which is outside that contract. Refs apache#4021 Generated-by: Claude Opus 4.8 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(ui): single-source the type-scale base and temper the zoom claim Review follow-up (Astro-Han on apache#4024): - The base `14` existed three times — makaTheme.ts's scale literal, theme.ts's TYPE_SCALE_BASE_PX copy, and DEFAULT_UI_FONT_SIZE. The first two now read one constant in astryx-theme/type-scale.ts (a dependency-free module so the pre-paint bootstrap path does not pull the full theme + icon registry); `astryx theme build` output verified byte-identical. DEFAULT_UI_FONT_SIZE must stay in @maka/core, so a renderer-side test (font-size-type-scale.test.ts) asserts the two agree and that the built --font-size-base token matches. - Soften "grows text, icons and rem spacing together (the deliberate UI-zoom hook)" in settings.ts and theme.ts: px-literal spacing and control widths far outnumber rem in the renderer, so the root scale moves what is rem-derived (text, Astryx's rem icon atoms) while boxes stay fixed — which is why the range is clamped tightly around the base rather than offered as a free zoom. Generated-by: Claude Fable 5 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
apache#4022) * fix(runtime): persist contextRemaining on the stored TokenUsageMessage apache#1072 emitted contextRemaining only on the live TokenUsageEvent; the durable TokenUsageMessage written via appendMessage omitted it. The TUI rebuilds usage from stored messages in replaceTranscriptWithStoredMessages, so after any transcript reload (abort refresh, resume, reconnect) contextRemaining was washed back to undefined and the status line fell back to the apache#3371 `ctx ?/<window>` degrade even for providers that report per-step usage. Compute contextRemainingForUsage once and share it between the stored message and the live event. Closes apache#4019. Generated-by: Maka * refactor(runtime): give the usage payload one definition site for both writers The durable TokenUsageMessage and the live TokenUsageEvent were built from twin per-field literals; apache#4019 happened because apache#1072 added contextRemaining to only one of them. Share one usageFields object so a field cannot drift between the two writers again. Ids and timestamps stay per-writer: the stored message and the live event are distinct facts. Generated-by: Maka
…/server into TUI (apache#4007) * fix(cli): split MCP capability provider to avoid pulling runtime-host/server into TUI TUI imported createMcpCapabilityProvider from runtime-host-capability-provider-command.ts, which statically imports runRuntimeHostProcessLifecycle from @maka/runtime-host/server. That pulled 372 runtime files and 879 total ESM modules, making runtime-host-tui-command import ~1.28s vs 0.23s for cli-core. Split the pure createMcpCapabilityProvider into mcp-capability-provider.ts (deps only on @maka/core/mcp, @maka/mcp, @maka/runtime-host/protocol). tui-mcp-control now imports from the pure module; the command file re-exports for compatibility and keeps the server import only for runRuntimeHostCapabilityProviderCli. Fixes apache#4005 Measured: - tui-command import 1280ms/879 files/233 runtime -> 563ms/287 files/3 runtime (-56%) - tui-mcp-control 1110ms/808 files/233 runtime -> 76 core/5 storage/0 runtime Generated-by: Muse Spark * style(cli): collapse mcp-capability-provider type import * test(cli): guard TUI MCP import boundary Generated-by: Codex
…errors (apache#3506) * fix(cli): surface parked /resume plans as informational notices, not errors The TUI /resume command always failed with the raw protocol reason (e.g. 'Safe-boundary resume parked: continuation_unavailable') because the host's safeBoundaryResumeEnabled flag defaults to unset, so the resume plan is parked with 'resume_feature_disabled' on every stock install. Even with the feature enabled, a completed turn parks with 'resume_candidate_missing'. Both cases are informational — there is simply nothing safe to resume — but they rendered as red errors that read like session corruption. - runtime-host-session-driver: throw SafeBoundaryResumeParkedError carrying the protocol park reason instead of a plain Error - pi-tui-runner: catch it in /resume and print plain-language copy (feature disabled / nothing to resume / session busy) as an info notice; other reasons keep the raw detail for diagnosis - tests: pin the informational rendering for continuation_unavailable and resume_candidate_missing Refs apache#3505 Generated-by: Maka Agent (claude-opus-4-8) * fix(cli): point /resume at the opt-in env flag when it is disabled The continuation_unavailable notice now names MAKA_RUNTIME_SAFE_BOUNDARY_RESUME=1 so a user who has not enabled safe-boundary resume learns how to turn it on instead of only learning that it is off. Generated-by: Maka Agent (claude-opus-4-8) * fix(runtime-host): preserve parked resume failure causes Keep feature-disabled, continuation-authority, and safety-observation outcomes distinct on the wire so only the true opt-in case receives informational CLI guidance. Generated-by: Codex * docs(runtime): explain parked resume reason boundary Document the current Host-to-CLI wire reasons, presentation severity, compatibility epoch, and unchanged ownership boundaries in both architecture counterparts. Generated-by: Codex * chore: retrigger flaky CI * docs(cli): name the resume park epoch actually shipped (54) Both language versions of the resume architecture note said epoch 52, but the wire change bumped RUNTIME_HOST_COMPATIBILITY_EPOCH to 54; epoch 52 is the unrelated steering-echo entry. * docs(runtime): correct safe-boundary resume epoch --------- Co-authored-by: me2seeks <me2seeks@users.noreply.github.com>
* refactor(cli): share verified registry update artifacts * feat(runtime-host): serialize deployment source finalization * feat(cli): coordinate npm-global Runtime Host updates * fix(cli): budget update archive expansion before extraction Registry integrity binds the downloaded .tgz to its metadata but says nothing about how far it expands: a small valid archive could exhaust the user's disk during staging or the final npm-global switch. Scan the tar headers (one pass over the compressed stream, nothing written) and refuse archives whose entries exceed a 2 GiB / 100k-entry budget before npm install consumes them, at both the verified-artifact seam and the global switch itself. Regression tests pin over-budget, over-crowded, non-gzip, and truncated archives. * style(cli): format the archive budget regression fixtures * fix(cli): close installed update recovery gaps
…bove the viewport (apache#4011) (apache#4025) * feat(cli): name stranded expansion blocks and add confirmed-collapse primitives (apache#4011) A Ctrl+O/Ctrl+T collapse can strand expanded blocks above the live viewport: their heads sit in terminal scrollback, which the apache#1097 contract forbids rewriting, so they stay expanded with no way back (apache#1134). apache#1140 made the all-stranded case explained but left the far more common partial case silent — some cards collapse, some stay stuck, and the keypress reads as broken. The toggle now appends a notice whenever a collapse strands blocks, naming the count and offering the escape hatch: a second press within EXPANSION_COLLAPSE_CONFIRM_WINDOW_MS collapses them too via one knowingly-accepted, scrollback-clearing full redraw (apache#1134 option 2, deferred by apache#1140 as an orthogonal follow-up). The confirm offer exists for collapses only — collapsed blocks above the viewport are compact and harmless, and arming on expand would make a quick expand-then- collapse pair read the second press as "expand everything". New exports for the runner wiring: hasExpandedEntriesAboveViewport (arming predicate) and applyExpansionDefaultToAll (applies the current default to every entry including above-viewport ones, without flipping the default the way a plain toggle would). The window constant lives beside the notice copy so the offer text and the runner's confirm window share one authority. Generated-by: OpenCode * feat(cli): second Ctrl+O/Ctrl+T press pays one full redraw to collapse stranded blocks (apache#4011) Wire the confirm gesture into the key handler: a collapse toggle that leaves expanded blocks above the viewport arms a 2s window; pressing the same key again inside it applies the collapsed default to every entry and forces one scrollback-clearing full redraw through pi-tui's existing public requestRender(true), which re-anchors the viewport at the tail. Transcript content is fully re-rendered into fresh scrollback, so nothing from the session is lost; only pre-session shell scrollback is cleared, and only after the explicit second press the notice announced. The integration test drives the real renderer on a 24-row terminal: an 80-line thinking block is expanded past the viewport, the first collapse press renders the offer with no ESC[3J in the stream, and the confirmed second press emits exactly the deliberate clear while collapsing the block back to its compact row. Generated-by: OpenCode * fix(cli): re-offer expired expansion confirmation
…#2645) * fix: surface and render provider reasoning summaries Generated-by: Codex * fix(ui): harden reasoning summary rendering Generated-by: Codex * fix(ui): preserve multiline reasoning math Generated-by: Codex * fix: close reasoning display review gaps Generated-by: Codex
The disclosure rendered below the badge's bottom edge: an <img> aligns to the text baseline, and <sub> drops the following text a further 0.25em, so the caption hung off the badge. GitHub strips inline styles, so no CSS fix is available in a README. Fold the disclosure into the badge itself and move it into the existing badge row. The separate <p> and the <sub> caption both go away, the badge matches the row's height and palette, and the misalignment cannot recur. Generated-by: Claude Code
Generated-by: Codex Co-authored-by: likun <kunli@alva.xyz>
Generated-by: OpenAI Codex
* docs(architecture): audit context compaction Align the paired context-compaction chapters with checkpoint-first pruning, atomic SQLite projection writes, current replay policy, summary validation, and bounded malformed retries. Refresh the code and test verification map against the current tree. Refs apache#3522 Generated-by: OpenAI Codex * docs: clarify compaction failure outcomes Distinguish automatic pre-turn budget exhaustion from manual compaction's visible fail-open note in the paired architecture chapters. Generated-by: OpenAI Codex (GPT-5.6 Luna) --------- Co-authored-by: fxl112233 <275098283+fxl112233@users.noreply.github.com>
* fix(core): add DeepSeek V4 Flash Vision to model metadata deepseek-v4-flash-vision-exp is returned by the first-party /models endpoint but was absent from STATIC_MODEL_METADATA, causing Maka to classify it as text-only and filter image attachments before the request reaches the provider. Add the model ID with vision capability to both STATIC_MODEL_METADATA and CURATED_CATALOG_FALLBACK_MODELS so the existing DeepSeek adapter passes image content through. Fixes: apache#3417 Signed-off-by: Yunare Maia <yunare@gmail.com> * style: apply biome formatting to model metadata (CI fix) * fix(core): include 'low' effort for deepseek-v4-flash-vision-exp Per review: models.dev (refreshed 2026-08-21), the official Thinking Mode guide, and the deepseek-v4-pro entry all confirm low/high/max. The pinned ['high','max'] was inherited from the stale pre-0731 sibling entry. * fix(core): declare text/image modalities for deepseek-v4-flash-vision-exp Add the explicit modalities entry suggested in review so attachment routing sees image input without relying on generated snapshots, and add a focused regression test covering vision resolution, input modalities, and fallback-catalog presence for the bare model id. Co-authored-by: Yunare Maia <yunare@gmail.com> * chore: re-trigger CI after upstream main repair (apache#3796) * fix(core): add displayName, description, lastUpdated for deepseek-v4-flash-vision-exp Addresses reviewer feedback on apache#3605: adds displayName, description, and lastUpdated fields to the STATIC_MODEL_METADATA entry so the model picker shows a friendly name instead of the bare model ID. Co-authored-by: Astro-Han <Astro-Han@users.noreply.github.com> * style(core): biome format model-metadata.ts (description line wrap) * test(core): add regression tests for DeepSeek V4 Flash Vision metadata - Added tests to verify vision support, modalities, and metadata lookup - Added catalog test for V4 vision model display metadata - Fixes reviewer feedback on apache#3605 * fix(test): remove buildConnectionModelCatalogEntries import from model-metadata.test.ts The function is exported from model-catalog.ts, not model-metadata.ts. Local vitest runs pass because it compiles differently than tsc. --------- Signed-off-by: Yunare Maia <yunare@gmail.com> Co-authored-by: Astro-Han <Astro-Han@users.noreply.github.com>
…pache#3400) * fix(ui,cli,core,runtime-host): count down provider retry wait from event timestamp (apache#3393) When a provider returns a long Retry-After (e.g. a subscription quota window reset — kimi-k3 / OpenCode Go returns ~4.5h when its 5h quota is exhausted), the retry indicator pinned the original delay for the entire sleep: Retrying in 16083s (2/10) never counted down, indistinguishable from a hung process. Both clients now render the remaining wait as a live countdown: - The scheduled event carries remainingMs — a duration, therefore free of any clock domain. Runtime sets it at scheduling; the Runtime Host projection recomputes it from the snapshot's stored schedule time, so a mid-wait reconnect no longer restarts the countdown. - Each client stamps its own local receipt time when the event lands and ticks against that single clock domain, so remote-Host clock skew cannot zero out or inflate the display. The remaining-wait computation is shared in @maka/core/provider-retry-countdown with one agreed zero floor. - The TUI strip reuses the shared duration formatter (4h 28m 3s); the desktop banner follows the running-turn indicator's accessibility pattern — the ticking text is aria-hidden and the role=status region exposes a stable label (reason + waiting attempt) instead of announcing every second. Under a reduced-motion preference the banner shows a correct static value at mount without ticking. Protocol: scheduled Turn provider-retry frames may carry an optional host-clock ts, so this bumps RUNTIME_HOST_COMPATIBILITY_EPOCH to 49. Previous epoch 48 was Session branch Side Conversation. Fixes apache#3393 Generated-by: Maka * fix(core,cli,ui): share retry display floor at 1s across surfaces (P3) Both the TUI strip and desktop banner now floor the humanized countdown at 1s via providerRetryDisplaySeconds, so an expired scheduled wait reads identically until the started event replaces it. The helper lives in @maka/core/provider-retry-countdown alongside the raw remainingMs, and both call sites import it instead of duplicating ceil/max logic. Generated-by: Maka * fix(ui): wrap LiveProviderRetry in live-turn-projection test (apache#3400) The projection now stores {event, receivedAtMs} to keep the countdown in a single clock domain. The terminalization test still constructed the old flat shape and broke the build (TS2353). Generated-by: Maka * fix: remove leftover conflict markers in session-projector test Left from rebase onto ff226af, broke tsc. Generated-by: Maka --------- Co-authored-by: likun <kunli@alva.xyz>
apache#4049) * feat(runtime-host): fence lifecycle transitions Persist transition and blocked states in the canonical managed deployment authority, and require the same State Root owner for exact commit or rollback. Generated-by: OpenAI Codex * feat(runtime-host): migrate lifecycle ownership Generated-by: OpenAI Codex * refactor(desktop): persist managed deployment bindings Generated-by: OpenAI Codex * refactor(runtime-host): unify managed lifecycle transactions Generated-by: OpenAI Codex * test(release): align managed host smoke Generated-by: OpenAI Codex * refactor(runtime-host): clarify lifecycle authority contracts Generated-by: OpenAI Codex * fix(runtime-host): validate on-demand update candidates Generated-by: OpenAI Codex
* feat(workhub): persist delegation linkage Generated-by: Codex * fix(workhub): preserve retry action identity Generated-by: Codex * fix(workhub): close durable retry gaps Generated-by: Codex * fix(workhub): close durable retry edge cases Recover accepted submissions before retrying, retry pristine create cleanup after unknown outcomes, and keep waiting responses from consuming the final coordination summary. Add focused coverage and a renderer reload E2E for the durable action identity flow. Generated-by: Codex * fix(workhub): harden durable delegation recovery Persist definitive abandonment and deterministic cleanup state, preserve retry identity across renderer drafts and Host scopes, and recover accepted steering from durable proof. Carry typed failures across Desktop IPC, avoid unnecessary Host drains, and extend regression coverage for crash and retry seams. Generated-by: Codex * refactor(workhub): make delegation assignment atomic Replace the delegation saga with one canonical assignment record committed atomically with target message admission and optional Session creation. Reuse normal pending-message recovery for post-commit consumption and keep renderer retry identity separate from draft storage. Generated-by: Codex * fix(workhub): consume durable admissions exactly once * fix(workhub): mark settings toggle unavailable
* feat(runtime-host): add WSL environment support Generated-by: OpenAI Codex * fix(runtime-host): own WSL process failure boundaries Give WSL activation a cold-start budget, keep semantic failures in the framed transport, and make bounded subprocess diagnostics non-rejecting. Classify persistent identity mismatches as terminal and separate Desktop Host ownership from target location. Generated-by: OpenAI Codex
* feat(runtime-host): discover peer mesh routes Sign short-lived route records with each libp2p identity and reconcile them between admitted peers. Feed only verified routes into the shared endpoint while preserving the target PeerId and Runtime Host credential authority. Cover route changes, removal propagation, restart recovery, native identity binding, and the installed CLI composition. Generated-by: Codex * fix(runtime-host): share peer endpoint with mesh discovery Serve application and Mesh control traffic from one peer identity, preserve verified bootstrap routes, and keep removal convergence scoped to the removed member. Generated-by: OpenAI Codex * refactor(runtime-host): centralize peer route cache Store each signed peer route once per node, filter it through active Mesh rosters, and allow expired route sequences to restart without weakening fresh-route rollback protection. Generated-by: OpenAI Codex * refactor(runtime-host): keep mesh routing in mesh node Use the route resolver only for application connections so Mesh control has one target-composition authority. Generated-by: OpenAI Codex * fix(runtime-host): serialize mesh route refresh on join Reuse the node's local route refresh while joining so background reconciliation cannot publish a different fact at the same sequence. Generated-by: OpenAI Codex * refactor(runtime-host): centralize mesh route sequencing Allocate every local route sequence from the node-level cache instead of accepting caller-supplied values. Generated-by: OpenAI Codex * fix(runtime-host): serialize connects per peer Queue application and Mesh-control dials targeting the same PeerId so the shared native endpoint never rejects a valid concurrent caller as already in progress. Generated-by: OpenAI Codex * fix(runtime-host): refresh mesh proofs during reconciliation Refresh the local signed route before each synchronization page so earlier unreachable members cannot starve later healthy peers with an expired proof. Add a deterministic long-round regression covering the real per-peer dial budget. Generated-by: OpenAI Codex
…#4073) A failed turn's banner made three promises it could not keep. It named an action that does not exist. The banner joined the error class and a derived recovery label with a CSS dot, so a rate limit rendered as 「触发模型速率限制 · 已保留部分输出,可从这里继续」. There is no "here" to continue from: `partialOutputRetained` only records that the turn produced assistant text or a tool result, and the one real resume path is `app_restarted`'s safe resume. `FailedTurnRecoveryAction`'s four values were never read by any renderer, so that layer existed solely to phrase guidance no surface could honor. Each `turnError` string now says what happened and what to do in one sentence, and what it asks for — send another message — always works. The warning that layer carried was worth keeping, and does: `describeFailedTurnExecutionState()` still tells the user a tool already ran and a resend may repeat its effects. It does not rank that against the error class the way the retired chain did, where `auth` plus one errored tool dropped "sign in again". Both facts are true at once and `Banner` has a slot for each — error class in `title`, execution state in `description`. It painted its own error surface. `Marker variant="failed-banner"` plus hand-rolled `oklch(from var(--destructive) …)` borders reimplemented Astryx's `Banner`, already used for the provider retry indicator in the same file. Three `MarkerVariant`s, the bespoke `AlertOctagon`, and the shell CSS go with it. Severity follows the app's existing destructive/warning grading. It rendered above the work it described, at the head of the assistant content rather than after the timeline it concludes. The steering-order test now asserts that placement, verified by moving the banner back and watching it fail. Safe resume survives throughout, renamed 「安全恢复」 → 「继续这一轮」 across its button, pending state, park description, and toasts. Generated-by: Claude Code
* refactor(mcp): share credential retirement classification Generated-by: Codex * feat(cli): serialize TUI MCP management actions Generated-by: Codex * feat(tui): manage MCP servers from /mcp Generated-by: Codex * fix(mcp): serialize config mutations across processes Generated-by: Codex * fix(tui): keep MCP selection visible Generated-by: Codex * fix(mcp): recover config lock after process exit Generated-by: Codex
Generated-by: Codex
…objects (apache#4402) `HOST_OPERATION_SPECS` is assembled by merging per-domain spec objects, so the domain of every operation is already established by the object that declares it. `operation-dispatcher.ts` restated that grouping by hand as literal `Extract`/`Exclude` lists, while three host-core groups already derived their keys from the spec objects. Two of the hand-written groups had drifted, and the `Exclude` subtraction meant a spec object no coordinator claimed was silently swept into Session Catalog instead of failing to build. Derive every group key from `keyof typeof <DOMAIN>_OPERATION_SPECS`, the form the file already used. Session Continuity is now named as continuity plus transcript, and Session Catalog as catalog plus turns — session-turns previously reached its coordinator only through the subtraction and had no group of its own. Deleting the subtraction makes an unclaimed spec object a compile error. The 16-handler unavailable access/collaboration map becomes a loop over its two spec objects, and both `HostCoreOperationKey` and the runtime host-core partition are driven from one `HOST_CORE_SPEC_OBJECTS` declaration. Turn is the one group split across three coordinators, each `Pick`-ing a subset. Because `composeRuntimeHostDomainHandlers` seeds every domain operation with the `operation_unavailable` fallback, a Turn operation none of the three claimed compiled and silently kept the fallback. A `satisfies TurnOperationHandlerMap` assertion where the three are merged makes that a typecheck error instead. Protocol domain and coordinator ownership stay free to differ; the difference is now declared once, by naming the spec objects each coordinator serves. Routing is unchanged: every `session.*` key is declared by one of the seven named spec objects, so the old subtraction residue and the new explicit union are the same set. Fixes apache#4395 Generated-by: Claude Code
…ionTodoPanel (apache#4396) apache#4351 replaced the nested Task Ledger demand chain with the Host-owned, flat SessionTodo document, but `task-ledger.css` still described the retired four-column, depth-indented tree row. Beyond selectors that could no longer match anything, the row `padding` used a `calc()` referencing `--task-depth`, which is defined nowhere — so the whole declaration was invalid at computed-value time and the rows rendered with `padding: 0`. Reduce the sheet to what the flat `<li>` actually renders and rename it to `session-todo-panel.css` after the component it styles. The row becomes a two-column `[icon] [text]` grid that wraps within the panel width, dropping the phantom empty column and the depth indent. Removing the invalid `calc()` restores the intended `var(--space-1) var(--space-2)` row padding, which is the one visible change in this commit. Also removes rules that could not match: the `-group` wrapper, the `[data-status]` icon colours (`data-status` is never set; the three `SessionTodoStatus` values are distinguished by glyph), `-key`/`-subject`/ `-meta`/`-detail`, `-terminal`/`-terminal-trigger`, and the row `:focus-visible` on a non-focusable element. The `max-width: 620px` block is deleted rather than ported, since the flat row wraps at any width; a comment records that future width-responsive styling belongs in an `@container` query. The `--foreground-secondary` alias kept alive only by this sheet's five call sites goes with it, and `.maka-task-ledger-empty` is renamed alongside. Generated-by: Claude Code
* ci: merge planning and validation into the required test job
Splitting path planning from validation made every pull request queue for a
scarce runner three times to reach one verdict. `plan` executed for 19
seconds and the `test` aggregation job for four, yet each allocation waited
on its own: across 32 runs with complete timing the three queues averaged
5m57s, 4m46s and 7m57s. The distribution is bimodal, about a minute per
stage while the pool is free and about twenty while it is starved, so the
two extra allocations cost roughly 12m44s of pure waiting per run.
Planning is now the first step of one job and every later step gates on
`steps.plan.outputs`, which is what those steps already did through the job
boundary. Nothing is validated that was not validated before, and a
documentation-only change costs one short allocation rather than the two it
previously paid for the planning and aggregation jobs.
The job keeps the name `test` because that is the required context in
`.asf.yaml`. Renaming it would leave that check unreported on every open
pull request until the rename merged, and nothing could merge while it was
unreported.
The longest validation observed is 25m37s with every lane selected, so a
single job stays well inside the 45-minute limit.
Generated-by: Claude Code
* ci: gate app icon drift on the artwork it verifies
The two app-icon drift tests regenerate the shipped PNGs through Python and
take about 52 seconds, but they were selected by `code`, so every change
anywhere in the tree paid for them. Replayed over the last 200 commits on
main, `code` is true for 193 of them while the surface those tests actually
read is true for 41.
That surface is now its own planner selection, derived from what the tests
open: the committed artwork, the generator that must still reproduce it, the
`APP_ICONS` catalog they check it against, and the packaged-resource list
that has to keep naming every file. Coverage is unchanged — every input that
could make these tests fail still selects them.
Each of those inputs lives in a workspace or under `scripts/`, so an app-icon
selection still implies `code` and the build the tests need has already run.
A test asserts that implication rather than leaving it to be rediscovered.
Generated-by: Claude Code
* ci: fold redundant CLI package validation jobs
This lane opened fourteen runners per pull request to produce evidence that
four of them were not needed to hold.
`release-predecessor` resolved one npm version in fifteen seconds and then
released its runner. It now runs on `build`, which already waits about
sixteen minutes on the addon builds, so the resolution costs nothing it was
not already waiting through. The exported `release_predecessor_*` names are
unchanged, so `npm-publication`, `release-cli-stage` and `asf-npm-candidate`
consume exactly what they consumed before.
`state-root-qualification` ran three matrix jobs. Two of them qualify a
transition between tarballs that were published and frozen, so nothing in a
pull request can change their outcome except the qualifier itself, and the
third reads the candidate this run built. They are three invocations of one
script against one sandbox, so they now share a runner. The scenario table
survives as three calls with the same digests, epoch relations and identity
flags; each writes its own report and all three are uploaded together.
The Linux x64 smoke ran twice on two runners for two Node versions. Same
machine, same tarball, same architecture assertion — it now installs the
second version and repeats the smoke in place.
Pull requests allocate ten runners instead of fourteen. Nothing that was
verified before is unverified now.
Generated-by: Claude Code
* ci: scope the packaged Windows gate to its own inputs
This lane packages, installs and updates Maka on Windows. It takes about
25 minutes on a runner class that is scarce on shared infrastructure, and
it was being allocated for two reasons that its own steps cannot justify.
`apps/desktop/src/main/runtime-host-boot.ts` was in the path filter
because the packaged updater is driven through `MAKA_UPDATE_TEST_FEED`,
which that file hands to the update service. The wiring is a handful of
lines; the module is 1900 of them, and over the last 200 commits on main
it was the sole reason this lane ran 13 times. It is now asserted by
`scripts/update-test-feed-wiring.test.mjs`, which runs on every change in
the required job before any toolchain is installed and costs
milliseconds. The filter matches 41 of those 200 commits, down from 55.
The pinned-baseline steps qualify a transition out of an already-released
installer, so a pull request's diff is not their input and cannot change
their outcome. Downloading that baseline is also this lane's most common
failure: 14 of 36 failures across the last 300 runs, every one of them on
a branch that could not have caused it, each costing another 25-minute
Windows allocation. Those three steps move to a nightly schedule, which
is still far ahead of release day, the moment they exist to precede.
Packaging, release verification and the end-to-end autoupdate check still
run on every matching pull request.
Generated-by: Claude Code
* ci: leave the Windows recovery suites to the required test lane
This lane's pull-request filter named every source directory in the
workspace closure of the crash and owner-death tests it runs, so it took
a Windows runner on 118 of the last 200 merges. The filter was accurate —
those suites really do import most of `runtime`, `runtime-host`,
`storage` and `core`, and a generated import closure measured at 106 of
the same 200, because they boot a real Runtime Host. The list was not the
problem.
What the run history shows is that the pull-request trigger was. Across
300 runs, 241 of them pull requests, this lane produced 12 reds and not
one of them was unique: every pull-request failure sat on a commit whose
`test` run had already failed, usually at the very same step — six at
`Install dependencies`, one at the workspace tests that run the same
owner-death suite on Linux. Its single unique catch in that window was a
Windows-only NTFS alternate stream regression found by the unfiltered
main push, which is the trigger that exists for exactly that.
So the filter now names only what a Windows runner can prove and `test`
cannot: how `npm ci` resolves and what the dependency patches and the
Electron installer produce there, what the clean step removes there, and
the Local IPC trust boundary, a PowerShell script with no other caller.
That is 14 of 200 merges instead of 118. The install and build steps stay
unconditional, because proving those suites still build and run on
Windows is what the lane is for once it does run.
The recovery authorities are now covered after merge rather than before
it, by the unfiltered main push minutes later and by the nightly.
`windows_recovery` is deliberately not a required context in `.asf.yaml`,
so it was never what stood between a regression and `main` in any case.
Generated-by: Claude Code
* ci: keep the candidate installer's own upgrade and rollback on pull requests
Both steps were moved to the schedule on the grounds that they qualify a
transition out of a pinned historical release, so a pull request could not
change their outcome. That is only true of the baseline. The other input
to `verify:windows-installer` is the installer this run just built, which
it installs over the pinned one, and `verify:windows-installer-rollback`
does not read the baseline at all: it takes the candidate installer and
the version-bumped one, and it is the only place the `installer.nsh`
Abort path is exercised. `installer.nsh` and both verifiers stayed in the
path filter the whole time, so a change to them scheduled this lane and
then skipped the steps that read them.
Both run on pull requests again. The flake that motivated the move —
14 of 36 failures across 300 runs, always on branches that could not have
caused it — is handled where it belongs: the baseline is pinned by
version, tag, asset name and SHA-256 in a committed manifest, so it is
immutable and cacheable on that manifest's hash. A hit skips the network
and a corrupt entry still fails, because the download step verifies the
checksum either way.
The schedule stays. Its reason is now the one the other two Windows lanes
give for theirs: the path list is a pre-filter, not this lane's import
closure, so a transitive edit it cannot match would otherwise first be
observed on release day.
Generated-by: Claude Code
* ci: keep the Windows-only recovery surface on the recovery filter
Dropping the workspace source directories dropped something with them.
Part of these suites is portable TypeScript that Linux fails first, which
is why removing them cost no observed pull-request signal — but part is
guarded by `process.platform === 'win32'` and is skipped off Windows by
construction, so `test` cannot go red on it however carefully it runs.
`assertNoWindowsAlternateStreams` and its three NTFS alternate stream
regressions are exactly that, and they are the one thing this lane has
caught that no other lane could have.
The Windows-branching modules under the executed suites are named again,
individually: seventeen files, which the last 200 merges touch twice more
than the filter already matched. That buys back the whole Windows-only
half for the price of two runs, where readmitting the closure would cost
118 of 200.
The rule is now checkable rather than remembered, so a stale entry cannot
sit here: every workspace path on this filter must be one file, not a
glob, and must contain a Windows branch. A module that stops forking on
the platform leaves the list, and a portable file cannot enter it.
Generated-by: Claude Code
* test(release): compare durable state paths the way the platform spells them
`durableStateLocations` builds its paths with `join`, so on Windows the
State Root is `\qualification-scope\state-root`. The assertion compared it
to a literal `'/qualification-scope/state-root'` and failed there, taking
`npm run check:release` and the packaged Windows lane with it — the
account-local assertion beside it already used `join` and passed.
The nesting assertion below had the same blind spot without failing:
`golden.startsWith(`${live}/`)` cannot match a Windows path, so it would
have accepted a golden copy nested inside its live directory, which is the
one thing it exists to reject. Both now spell the separator the way the
platform does.
Generated-by: Claude Code
* ci: derive every narrowed gate from its authority instead of a reading of it
Three reviewers found three holes in this branch, and they are one hole.
Every gate it narrowed took its new input set from my reading of what the
gated work consumes, and every contract test I wrote to defend that set
proved only that what I kept belonged — never that what I dropped was
unnecessary. Containment in the easy direction. The other direction is an
absence, and an absence has no line number to check.
So each list was short by whatever my reading could not see. The recovery
filter missed `stable-storage.ts`, reached transitively through the two
lock authorities it did name, along with six test files. `APP_ICON_FILES`
missed `electron-builder.config.mjs`, which the drift test opens by path
rather than imports. And the baseline cache missed that
`prepareWindowsUpgradeBaseline` deletes its output directory before
downloading unconditionally, so a restored entry was never once consulted.
Each is now computed from the authority and asserted as set equality:
- The recovery filter is the import closure of the dist suites its steps
execute, restricted to files that branch on `win32` — 56 files, checked
with `deepEqual` so an omission and a stale entry both fail. That is 40
of the last 200 merges rather than 17, and unlike 17 it can be shown to
be complete. `collectWindowsPackageSourceClosure` is now a caller of a
general `collectWorkspaceSourceClosure`, which the packaged Windows lane
already used for exactly this purpose.
- The app icon surface is read off the step: whatever `App icon artwork
drift` runs is scanned for the repository paths it opens, and each must
select the gate.
- `prepareWindowsUpgradeBaseline` reuses a copy that hashes to the pinned
digest and downloads only otherwise. A cache entry is never an authority
on what the run installs: a mismatch or an unreadable file goes to the
network, and only a fresh download that fails the digest fails the run.
Applying the same rule to the gate none of the three reviews reached found
a fourth instance, in the required job. `heavy` decides whether the
toolchain is installed, and nothing asserted that a selection gating a
later step is one of its disjuncts — so `app_icons`, added by this branch,
gated a step that imports `@maka/core/settings` without selecting the
install it needs. It reached green only because every icon input happens
to select `code` as well. That disjunct is now present, and the workflow
is scanned for the rest.
Generated-by: Claude Code
* test(ci): give the install-free lane a name and a contract
`ci-test-plan.test.mjs` was the only suite running before `npm ci`, so
every assertion that needed a bare checkout accreted there regardless of
subject: 66 tests, of which 35 read `.github/workflows/*.yml` and had
nothing to do with the test planner. The file name carried no hint that
its one hard rule is "import nothing that is not installed yet", which is
how a closure assertion needing esbuild was very nearly added to it. That
import would have thrown `ERR_MODULE_NOT_FOUND` in the sole required
context and frozen every merge in the repository.
Split by what a test reads. `ci-test-plan.test.mjs` keeps the 30 tests
that exercise the planner against an in-memory graph; the 36 that read a
workflow move to `ci-workflow-policy.test.mjs`, beside the two policy
suites that already follow that name.
Naming the constraint is not enforcing it, so derive it: the new suite
reads which files the steps above `setup-node` run and walks their
transitive local imports, failing on any specifier that is neither a
`node:` builtin nor a repository module holding the same rule. Moving a
step below the install lifts the constraint and adding one above imposes
it, with no list to maintain.
The closure assertion itself now lives in
`windows-package-source-closure.test.mjs`, which `check:release` runs
after installing, and `windows-recovery.yml` joined the release-contract
inputs so editing that filter selects the gate that checks it.
Two app-icon tests iterated a hand-written `APP_ICON_INPUTS`; the derived
test computes that same set from the suites the step runs, so they folded
into it and the list is gone. `prepare-windows-upgrade-baseline.test.mjs`
joined `check:release` without joining the planner's release set — the
guardrail that exists for exactly that caught it.
Generated-by: Claude Code
* test(ci): assert both directions of every narrowed filter
Two filters in this branch were proved only in the easy direction, which
is the direction that cannot see an omission.
`release-windows-check.yml` asserted that its import closure is covered by
the filter. Nothing asserted the converse, so a `packages/` entry backed by
nothing stayed in the list forever and booked a 25-minute Windows runner
every time it matched, with no test able to report it. Five of its twenty
five entries turn out to be underived — peer dependency manifests, Runtime
Host candidate election, the script that builds the worker the closure
starts from. Each is legitimate, and each is now declared with its reason,
so the set is exact in both directions and a stale exception fails as
loudly as a missing entry.
Three hand-written tests asserted that a lane pairs its path filter with a
nightly run, one lane each. The rule they encode is not lane-specific: a
filter is a pre-filter, not an import closure, so something has to run the
lane without consulting it. Replaced by one test enumerating the workflow
directory and requiring a schedule, an unfiltered push, or a `workflow_call`
caller. The three pairings covered three lanes; the rule covers eight and
would have caught `release-windows-check.yml`, whose schedule this branch
added without an assertion to hold it there.
That enumeration also surfaces three lanes that have no escape at all:
`gitoxide-helper-admission` and `runtime-host-peer-admission` pair the pull
request with a `push: main` carrying the same filter, and
`runtime-host-owner-platform` pairs it with `workflow_dispatch`, which
nothing fires on its own. They predate the gates narrowed here, so they are
declared rather than changed — the point is that the gap is now countable
instead of invisible.
Generated-by: Claude Code
* ci: restore what folding jobs and lanes took away
Four leftovers from the consolidations earlier in this branch, each one a
guarantee the old shape supplied and the new shape does not.
`release-windows-check.yml` keyed its concurrency group on `github.ref`,
which is `refs/heads/main` for the nightly and for a dispatch alike, so a
dispatch could queue behind the nightly and be cancelled while still
pending. Keyed on the pull request number like the other two Windows lanes.
The three State Root transitions used to be three matrix jobs, so one
failing left the others to upload their reports. Folded into one step they
share a `set -e`, and the reports are wanted most on the run that failed,
so the upload now runs with `if: always()`. `tee` has already written the
failing transition's own output by then, and `if-no-files-found` stays
`error` so a broken path is still caught on a green run.
Moving predecessor resolution onto `build` rewrote the assertions that
bound it, and `PREDECESSOR_TARBALL_URL` lost its only one — the third
transition would still be spelled correctly while pointing at nothing.
Both env bindings and the `workflow_call` output are asserted again.
`.asf.yaml` still described the required context as an aggregation job
propagating failures from a plan lane and a heavy job, none of which exist
since the three merged into one. It now describes what is there, and names
splitting the work back across jobs as the third way to freeze the queue.
Also bounds `update-test-feed-wiring.test.mjs` to the argument object it
means: an unbounded span would have accepted a `testFeedUrl` from any later
call in a 1900-line module.
Generated-by: Claude Code
* test(ci): drop the filter assertions the closure now proves
Nine assertions restated `packages/` entries of
`release-windows-check.yml` as literals. Every one could only ever catch
"this exact line was deleted", and the previous commit made that case fail
already: the filter's `packages/` half is computed from the import closure
and compared as a set, so removing an entry fails there whether or not
anyone remembered to also name it here. Verified by deleting
`connect-or-spawn.ts` from the filter and watching the closure test fail
with these gone.
Two of the nine carried a reason for being in the filter without being in
the closure; that reason now lives on the declared exception beside the
path it explains, which is where someone auditing the entry will look.
`copy-runtime-filesystem-worker.mjs` stays asserted by hand. It is the one
entry outside the derived half: the desktop app copies the built worker in
rather than importing it, so no closure reaches it and only naming it keeps
it on the lane.
Kept as well are the assertions that encode a rule rather than a copy —
that the gate triggers on `release.yml` and on itself, and that Windows
recovery still runs the three regression suites it exists for. Those state
something no derivation produces.
Generated-by: Claude Code
A Turn larger than the Host's own range limits made its Session unopenable. `readRangeEdges` trims a selection back to whole-Turn boundaries, and when the target Turn alone exceeded `SESSION_TRANSCRIPT_RANGE_MAX_MESSAGES` (256) or `SESSION_TRANSCRIPT_RANGE_MAX_BYTES` (16 MiB) it threw `RangeError`. `createSessionTranscriptBootstrap` maps anything that is not a `TranscriptOverlayCapacityError` to `persistence_failed`, so the reader saw "Session transcript is unavailable" and no amount of retrying helped. The limits are the Host's own, so nothing on the client side could avoid them, and a 257-message tool loop is an ordinary size. The throw was added in apache#4244 as the fallback for "not even one Turn fits". Degrade instead of refusing: when a single Turn cannot be bracketed, return the selection unchanged with null range and protected-Turn boundaries. Nothing else moves. The page stays bounded because the bound never came from this function — bootstrap reads through `readDurablePage` with the range message limit and continuations through `continuationMessageLimit`, with `requirePageByteLimit` capping page bytes at 512 KB. Continuation still works because `pageFromSelection` signs its cursor from the reader's `selected.next`, and a null boundary is already a value both consumers handle: `session-subscription.ts` reads it as "boundary reached", and `desktop-transcript-replica.ts` was already written with `?? fallback` at all three sites. Ordinary Turns and partial-edge Turns take the same branches as before — the conditions are untouched. An oversized Turn now pages: 286 messages arrive as 256 + 30, with no repeats and no gap. One consequence worth recording: with a null protected-Turn boundary, Desktop groups the session under a single resident Turn key and cannot evict it, so its resident set is bounded by that Turn rather than by the Host's 256 / 16 MiB. That is a smaller problem than being unable to open the Session at all, and it belongs to the bounded-transcript work in apache#2913. Fixes apache#4428 Generated-by: OpenAI Codex
…apache#4377) * feat(desktop): route standard API key onboarding through host * feat(desktop): add host-backed API key enrollment flow * refactor(desktop): move connection settings behind feature adapter * docs(desktop): refresh Astryx surface inventory * refactor(desktop): narrow connection settings exports * fix(desktop): make connection settings entry ESM-safe * chore(desktop): refresh renderer architecture ledger * fix(desktop): observe onboarding save uncertainty * fix(desktop): fail closed on onboarding response errors * fix(desktop): rebase onboarding on host model catalog
Attachments were routed by filename extension and renderer-supplied MIME, so a PDF renamed `photo.png` reached the image resizer and preview decoder, and a real image named `report.pdf` rendered no thumbnail and raised no vision-capability notice. Sniff the leading bytes instead and let the detected type win over both extension and metadata. PNG, JPEG, GIF, WebP and PDF are recognised; a PDF header behind a short preamble is found within a bounded window so it is refused rather than decoded as text. The signatures live in one place. `@maka/core`'s `sniffAttachmentMimeType` is now the only magic-byte table, replacing the copies that had grown in `packages/runtime/src/image-file.ts` and `packages/storage/src/artifact-store.ts` — which had already drifted, since storage counted SVG as an image and core did not. Storage keeps only its own SVG text scan, which is not a magic-byte question. That consolidation is a net deletion in both files. BMP is deliberately not sniffed: no downstream reader accepts it, and its 2-byte `BM` marker matches arbitrary files, so routing those bytes to an image decoder only produced `unsupported_mime` further along. Resolution moved to pick/drop time rather than send time, which made the ownership of an in-flight staging batch load-bearing: `fileToPending` became async to read each file's leading bytes, and on a network volume or a spun-down drive that I/O takes seconds. Both staging paths now bind the draft owner after the reads resolve, from the live ref rather than the render-time value, so files land in the composer the user is looking at instead of one they have since left — where they would be invisible but still sendable. No migration and no protocol change: sniffing happens before anything is persisted, and the stored shape is unchanged. Fixes apache#4441 Generated-by: Claude Code Generated-by: OpenAI Codex
…board (apache#4359) The TUI had no way to get a reply out of the terminal: selecting text by mouse fights the alternate screen and line wrapping, and there was no command for it. Add `/copy`, which puts either the last reply or the whole transcript on the system clipboard through OSC 52, so it works over SSH and inside a multiplexer without a helper binary on the remote side. OSC 52 asks the terminal emulator to set the clipboard, and payload size is the sharp edge. Past a terminal's OSC-string buffer the write is dropped, not truncated to a usable prefix — kitty logs `OSC sequence too long, truncating` and sets nothing, and the escape sequence itself never reaches the screen, so the failure is silent. Buffers vary widely, so the only honest option is to refuse: `MAX_CLIPBOARD_TEXT_BYTES` is 4 KiB, sized so the emitted sequence (base64 at ~4/3 plus 8 bytes of framing, ~5.5 KB) clears the ~8 KB buffer of mainstream terminals. An oversized copy becomes a readable refusal carrying the byte count and the limit. The bare sequence is the right primitive under tmux, but it does not land on a default one: `set-clipboard` has defaulted to `external` since tmux 2.6, which ignores an application setting a tmux buffer, and forwarding also needs an `Ms` capability that nested tmux and the inner side of GNU screen lack. Wrapping in tmux's DCS passthrough is avoided rather than unsupported — it needs `allow-passthrough on`, off by default in modern tmux, and it bypasses tmux's own clipboard policy. The header comment records this so the next reader does not rediscover it. `serializeTranscriptText` labels `goal_continuation` and `legacy_automation` turns by their provenance instead of as `You:`, matching what the TUI displays, and keeps each as its own block so the assistant turns around them still do not merge. Generated-by: Claude Code
…tream (apache#4460) Nothing ran `refresh:model-metadata` on a schedule and nothing ever compared the committed snapshot against models.dev, so the snapshot could sit weeks behind upstream with every check green. That is how `glm-5.3`, listed upstream from 2026-08-14, was still absent from a snapshot regenerated on 2026-08-29. Add the missing upkeep and then use it: a weekly job that refreshes the snapshot into a draft pull request for review, a snapshot-versus-upstream drift check, and a refresh of the snapshot itself against a live models.dev response. One thing had to come first. models.dev now declares `video` on either side of a model and `pdf` as an output. The projector named four input and three output values and threw on anything else, so 324 of the 1906 models in the selected providers took the whole refresh down with them — `npm run refresh:model-metadata` could not write a byte. Admitting both values is a Client-Host wire change: the decoder rejects any value it does not name, so a newer Host describing a video model would fail an older client's catalog decode outright. `RUNTIME_HOST_COMPATIBILITY_EPOCH` moves 87 → 88 to keep that pairing from forming. That modality set had five hand-written copies and the widening reached four of them. `ModelModality` in `llm-connections.ts` owns it now, and the decoder, the model-facts overlay normalizer and `model-fetcher`'s live-fetch reader all read that one predicate. The projector keeps its own copy because it runs before `packages/core` is built; a value that reaches the projection but not the type fails to compile, so the build is what holds those two together. `ModelMetadata.docsUrl` goes with it. It had no reader anywhere in `packages` or `apps` and duplicated, once per model, the provider `doc` the generated provider facts already carry once per provider. The drift check and the refresh share one projector. `buildProjection` takes its failure policy as an argument: a refresh aborts on the first shape it cannot project, and the report turns that shape into its own finding so a single surprising model cannot hide every other difference. Both walk the projection's four sections through one table rather than naming them, which is what keeps `providerFacts` and `providerOverrides` in the comparison. The scheduled job passes `--accept-upstream-removals`. The guard's remediation is "inspect the change and rerun with the flag", which only a person at a terminal can do; upstream retires paths every day, so refusing there would have made the job red every week and it would never have opened a pull request at all. The review seat is the draft pull request instead: every removal is in its diff, the drift report is in its body, and a committer still approves before anything merges. This refresh ran with that flag for 31 models models.dev has retired. `deepseek-chat` and `deepseek-reasoner` are among them and stay selectable — `provider-registry` lists them under `fallbackModels` and `builtin-pricing` carries their rates, so only bundled display metadata goes. 133 models now declare an output modality without text; `model-catalog` already refused those as chat defaults, and its own comment documented that it could not see the video ones. It now can, and the test that asserted the old blind spot asserts the guard instead. Not run: the scheduled workflow itself, since `schedule` cannot fire from a pull request. It needs a `workflow_dispatch` run on main after merge. Refs apache#4398 Generated-by: Claude Code
Audit the Bots architecture document against current Desktop onboarding and runtime behavior, including QQ onboarding, bridge warnings, timeout behavior, renderer-safe secret handling, and the actual Playwright coverage boundary. Refs apache#3522 Generated-by: Cursor Grok 4.6 Generated-by: OpenAI Codex
…apache#4488) Replace the leftover independent per-recv socket timeout with the remaining explicit TLS handshake deadline, while restoring the caller's timeout after each receive. Add deterministic coverage for the deadline and timeout-restoration contract. Fixes apache#4240 Generated-by: GLM-5.3-Flash (ZCode)
…pache#4481) Use native path-relative semantics to abbreviate Windows profile descendants in the TUI status line while keeping siblings, parent traversal, and other drives absolute. Restrict the traversal guard to the native separator so legal POSIX names containing a backslash still shorten, and refresh the Windows skip inventory. Fixes apache#3825 Generated-by: GLM-5.3-Flash (ZCode) Generated-by: OpenAI Codex
Asking for `ubuntu-latest` and asking for `ubuntu-24.04` gets the same machine. Both labels reported `Image: ubuntu-24.04`, `Version: 20260823.283.1`, provisioner `20260819.586`, runner `2.336.0` in their `Set up job` logs on the same day. So this changes which queue a job waits in, and nothing else. The queues are not the same. Across 1183 runner-backed jobs in eleven ASF repositories, `ubuntu-latest` has a median wait of 0.05 minutes and a p90 of 19.07; `ubuntu-24.04` has a median of 0.03 and a p90 of 0.53, while carrying 3.6x the jobs (455 against 126). The alias is not slower on average — it is unpredictable, and a required context is paid at the tail. `apache/flink` sees a p90 of 105.8 minutes on the alias. The sharpest control comes from this repository. One push to apache#4482 created nine first-layer jobs within the same second, none of them declaring `needs`. The three on `ubuntu-latest` waited 7.5, 7.68 and 7.7 minutes. The six on `ubuntu-24.04`, `ubuntu-24.04-arm`, `windows-2025`, `macos-15`, `windows-latest` and `macos-latest` waited between 3 and 24 seconds. It is not the `-latest` alias as such, since two of the fast six are aliases. It is not our own `concurrency` groups, since every workflow's median `run_started_at - created_at` is 0.0. It is not a self-hosted split, since `runner_group_name` reads `GitHub Actions` on all nine. `ci.yml` also has a reason of its own. Its bubblewrap step disables `apparmor_restrict_unprivileged_userns` specifically because Ubuntu 24.04 gates user namespaces that way. The required context already assumes this image; the alias only left that assumption free to drift without a commit. `windows-latest` and `macos-latest` stay as they are. Across 84 same-instant groups containing both `windows-latest` and `windows-2025`, the median paired difference in wait is 0.00 minutes, so pinning them would buy nothing measurable. Nine substitutions across eight workflows; thirteen jobs in the release and packaging lanes were already pinned. Two costs, both accepted deliberately. The automatic image upgrade becomes a manual commit — `ci-workflow-policy.test.mjs` holds the rule and says how to take an exemption. And `ubuntu-latest` carries 79.1% of this repository's Ubuntu job-minutes (3141 of 3970), so this raises our demand on the pinned label 6.2x, onto a pool whose wait was measured while it carried one sixth of that. The pool absorbs 3.6x more jobs than the alias today at a p90 of 0.53 minutes, and the worst pinned figure anywhere in the sample is `apache/iceberg` at 0.43 median against 18.78 on its own alias. If the p90 on `ubuntu-24.04` passes 2 minutes after this lands, that is the signal to revisit, with both sides finally measurable. Refs apache#4480
apache#4460 added this lane and apache#4483 banned `ubuntu-latest`, 35 minutes apart. Both were green when they ran; main is red now that they sit together, which blocks every merge because `test` is the only required context. One-line fix, and exactly the drift the rule exists to catch: `ubuntu-latest` is what every tutorial writes, so it comes back one new workflow at a time. The rule caught it on the first one. Refs apache#4480
…nux (apache#4485) Render raw shortcut labels with the platform-correct modifier names in the Desktop sidebar and command palette. Keep locale catalogs free of browser capability reads, preserve both variants for search, share the renderer across surfaces, and refresh the Astryx inventory. Fixes apache#3876 Generated-by: GLM-5.3-Flash (ZCode) Generated-by: OpenAI Codex
Module pages — Extensions, Scheduled tasks, and the rest — opened with a tall empty band under the frameless titlebar. The titlebar inset was being applied twice: `.mainColumn` already reserves `--maka-plate-titlebar-clearance`, and `.maka-module-main[data-page-shell="layout"]` added another `var(--space-16)` (64px) on top of it. Drop the module-page top padding. Titlebar clearance stays solely on `.mainColumn`, which is the one place that owns it, and page content aligns to the top of the content plate again. A comment records the rule so the padding does not come back. CSS only.
…pache#4467) A Maka installed months ago described every model by the models.dev snapshot compiled into that build. Since the Host became the catalog's authority, that staleness reached every attached client at once — the Desktop and the TUI both resolve what a model is from the Host, so a Host behind upstream is the only copy that matters. The Host now asks models.dev once at startup, through the same admission as the WebFetch tool: privacy mode refuses it, a configured proxy carries it, which is why that operation is named for what it admits rather than for its first caller. A whole catalog it can project becomes the metadata every connection resolves against — no reinstall, and no client needing a matching version. Any failure at all, offline through an upstream shape the projection refuses, leaves the committed snapshot in place; there is no partial install. The swap is announced as a fifth `HostChangeFeed` frame, `connection.catalog.changed`, rather than by reusing `configuration.changed`: the user's settings did not move, and a client showing a settings-changed-elsewhere notice must not show it here. Mid-pagination it is safe, because which models a connection has comes from its stored rows, the registry's fallback list and the ids the user enabled — never from models.dev — so `catalogEntryCount` and every cursor survive; only field values can differ across pages, and the frame makes the client re-read. One refresh is now one shared unit. `@maka/core/models-dev-projection` owns provider selection and the per-model projection the build-time generator already performed, and `@maka/core/models-dev-refresh` owns the three steps both callers owe: fetch it under a byte bound, project it, account for what upstream stopped carrying. The generator keeps only what a build has — pricing, provider facts, model-provider overrides, snapshot IO, code generation — deliberately outside the runtime parser, so a malformed `cost` field the Host never reads cannot veto a metadata refresh. The two differ in exactly one step, and the difference is intended: the generator refuses a shrinking refresh until a human acknowledges it, while the Host records the removals and adopts them, because nothing it installs is committed or redistributed and the next process start asks again. Replacing the whole metadata table rather than merging model by model follows from the same reading — the snapshot is what we fall back to when we have no answer, not a second opinion about a model upstream still publishes. Four defects the shared path exposed are fixed here rather than left for the seam to hit again. Bounded response reading has one owner instead of a WebFetch copy and a refresh copy that counted `String.length`, a UTF-16 unit count that lets a multi-byte body pass a limit it exceeds by up to three times. The catalog producer now names and enforces the display-name and description limits the codec decodes with, so one over-long refreshed row can no longer make a whole page undecodable. The refresh is unwound with the other resources when composing the Host fails later. And the TUI derives the selected model's context window and thinking levels from the current choice instead of mirroring them at three sites, a mirror that a mid-session catalog republish would have made a fourth. Compatibility impact: the epoch moves to 89. An older client's strict frame decoder rejects `connection.catalog.changed` as an unknown kind, so that pairing is refused at the handshake rather than left to drop the announcement. Two hand-written model facts models.dev now states itself are deleted, verified by resolving each id through the committed snapshot and the live catalog both ways. No stored document changes shape, so no data migration is required. Refs apache#4398 Generated-by: Claude Code
Project and "No project" headers carried a neutral Badge with the number of tasks in the group. That number was already in the row's hover card and focus details, so the badge was a second place to read one fact — and it wore the shape this product reserves for unread or pending counts, so a plain group size read as something waiting for the user. Remove it. `ProjectItemMeta` no longer takes `sessionCount`, and the header renders no trailing slot at all unless it has something to put there: an availability warning, or the reserved space a project's row actions need. Task totals stay available in the contextual details, and project actions, availability warnings and expand/collapse are unchanged. The `sidebar.css` note that explained why the type tier is bound on the header rather than the section root cited a live measurement of this badge going 12px → 14px. The rule still holds for the task content below, so the note keeps the rule and drops the evidence that no longer exists. Closes apache#4448 Generated-by: OpenAI Codex GPT 5.6 sol Signed-off-by: Jiawei Zhao <Phoenix500526@163.com>
…pache#4482) * ci: give the full-suite fallback's biggest buckets their real owners A path the classifier does not recognise sets `unknownCode`, which returns the full suite. That fallback is correct as a default and is the largest single source of full-suite runs, so the fix is to give its biggest occupants an owner rather than to weaken it. `native/gitoxide-helper/` and `native/runtime-host-peer/` each have an admission lane that owns `cargo fmt` and `cargo test` for them, and the direct-peer crate additionally reaches CLI packaging, which builds it into the tarball and runs `cargo deny` against `deny.toml`. Nothing under either is read by lint, typecheck, Storybook, or a real window. They are named one crate at a time rather than by the `native/` prefix. What earns the exemption is having a lane, not being written in Rust: a crate added under `native/` has neither a lane nor a JavaScript consumer on the commit that introduces it, and this fallback is the only thing that would build it at all. A regression asserts that an unowned native root still selects full. `.asf.yaml` goes to `RELEASE_CONTRACT_FILES`. It names the required contexts and the release-environment admission rules, and `product-release.test.mjs` is the suite that parses it and asserts them — so selecting the release contract is what proves a change to the merge gate still passes its own policy test. Routing it anywhere else lets that gate change while the test that guards it is skipped. `patches/` deliberately keeps the fallback. A patch rewrites a dependency's behaviour, and which suite proves that behaviour is a property of the patch rather than of the directory: the node-pty patch is regressed by `packages/runtime/src/__tests__/node-pty-write-lifecycle.test.ts`, while the packaging smoke a build-shaped selection would run only asserts that a method name still appears in the tarball. Narrowing this bucket needs an explicit patch-to-consumer mapping; until one exists the cost here is runner minutes rather than a silent regression. Refs apache#4480 * ci: cancel superseded installed-package validation runs `cli-package-validation` had a concurrency group but no `cancel-in-progress`, which is the setting that makes a group cancel rather than queue. A run that a newer push had already invalidated therefore ran to completion at full price while its replacement waited behind it. It fans out to about fourteen jobs per run across four build targets and four install environments, so it holds more runner slots than any other workflow here — 736 jobs against CI's 523 over one measured day. In that window 14 of its 60 pull request runs were superseded while still executing, spending 275 of 1672 slot-minutes on work that was already obsolete. Scoped to `pull_request` the way `ci.yml` scopes its own group: release callers arrive through `workflow_call`, where `github.event_name` is the caller's, so a publication run is never cancelled. Refs apache#4480 * ci: stop selecting the helper admission lane on an unrelated manifest `gitoxide-helper-admission.yml` filtered on `packages/runtime/package.json`, which selected three runners across three operating systems on 17 of the last 300 first-parent commits. Dropping it takes that to 5. The coupling that entry was added for in apache#3561 is real and still exists: `gitoxide-helper-invocation-internal.ts` imports `@maka/runtime/child-process-lifecycle`, a subpath that resolves only through the exports map in that manifest, added by the same commit. Editing the map can break the Gitoxide owner. What changed is not the coupling but the question of who covers it. Core CI does. A change to `packages/runtime/package.json` selects `packages/runtime-host` through the reverse dependency closure, so the import is typechecked and tested on the required context whether or not this lane runs. What the three-runner fan-out proves that core CI cannot is the Rust side and the cross-platform invocation contract, and a manifest edit changes neither. The `pull_request` and `push` lists are edited together, and a rule now holds them that way: any lane that filters both triggers must filter them on the same paths. A lane that looks at one set before a merge and another after it reports a verdict about a tree nobody validated, and that mistake is invisible in a diff showing only the list being edited. `dependency-audit` was already drifting this way — its `push` list omitted the workflow's own file — and is corrected here. Leaving one trigger unfiltered stays available, since that is a visible and sometimes deliberate choice: `windows-recovery.yml` does exactly that, so the merged result is checked even when the pull request was green against a stale base. This replaces a test that asserted every filter entry could reach Gitoxide. Every entry names the subject, so it skipped all of them and proved nothing. Refs apache#4480
`#stopTurn` forwarded its caller's object straight to `connection.request('turn.stop', …)`.
Its parameter type names exactly `sessionId`, `turnId` and `runId`, but excess-property
checking only applies to object literals at the call site, so the active Turn's local
`outcome` classifier travelled with it onto the wire.
The Host decodes that operation with
`requireExactRecord(value, 'turn.stop input', ['sessionId', 'turnId', 'runId'])`, so the
unknown field failed the whole request. A `--timeout` expiry or SIGINT therefore never
reached the Host and never aborted the foreground tool: the CLI asked to stop and nothing
stopped.
Build the payload explicitly from the three protocol fields, and give the stop-before-start
fixture a strict mode that rejects any other key. The type could not catch this, so the
fixture asserts the wire shape instead.
Fixes apache#4304
Generated-by: pi (gpt-5.6-sol)
`pathWithinRoot`, `samePath` and `trimTrailingPathSeparators` are exported path helpers whose behavior was only ever observed through the permission matrix that consumes them. That suite pins shared prefixes, dot segments, ADS and UNC boundaries, and touches none of the case rules — so the fact that Windows paths compare case-insensitively while POSIX paths preserve case had no test anywhere, and would have broken silently on one platform. `trimTrailingPathSeparators` and `samePath` had no direct coverage at all. Add one focused file for exactly that gap: the Windows/POSIX case pair, trailing separator trimming that preserves `/` and `C:\` as themselves, and one representative `canonicalWindowsPath` rejection to pin the wrapper. The permission suite stays responsible for everything it already covers; this does not re-derive it. Tests only. Production behavior is unchanged. Fixes apache#4002 Generated-by: OpenAI Codex
Every Bot conversation — Feishu, Telegram, WeCom — answered `Maka 暂时无法处理这条消息:机器人对话处理失败` and never created a session, while the platform connection itself stayed healthy. `explore` is a boundary a product mode confers, not one a caller may request directly (`create-session-input.ts`). The Bot adapter asked for it directly: `permissionMode: 'explore'` with no `mode`. `prepareCreate` rejected that, and the rejection matched no category in `generalizedErrorMessage`, so what the user saw was a generic fallback that read like a platform or credentials fault. Desktop chat was never affected — it starts at `ask`. A Bot session is exactly such a product intent, so `bot` joins `SESSION_START_MODE_SPECS` and the adapter names the mode instead of asking for the boundary. Unlike Deep Research it carries no name of its own: `prepareCreate` resolves `mode?.name ?? input.name`, and a fixed spec name would flatten `飞书 任务` and `Telegram 任务` into one label, so `SessionStartModeSpec.name` becomes optional. The `mode:bot` label is reserved for free, since `prepareCreate` already refuses caller-supplied mode labels. `session.create.mode` therefore accepts a value it did not before, and a Host that predates it answers `Invalid Session start mode`, so `RUNTIME_HOST_COMPATIBILITY_EPOCH` moves 89 → 90. The mode registry moves from `deep-research.ts` to `session-start-mode.ts`. It was born there when Deep Research was its only member; `bot` is a sibling, not a Deep Research detail, and nobody looks for Bot permissions in a file named for Deep Research. Pure move. A mode without a name of its own also exposed a sibling defect: `sessions:create` dropped the requested name whenever a mode was present, which held only while every mode carried one. It now forwards the name either way and leaves the precedence to the Host. The Bot adapter talks to the Host directly and never took that path, so that is a contract repair rather than a second user-visible bug. Both regressions are guarded where they were rejected — at the coordinator and at the IPC handler; the Bot adapter's own test reached neither, because its fake client returns a session without entering `prepareCreate`, which is why this shipped. Deliberately not covered: `bot-incoming-main.ts` still swallows `invalid_request` into `机器人对话处理失败`, which is what made this take a packaged-app patch to diagnose, and `updateSessionConfiguration` still admits `explore` with no mode — the path `prepareSession` uses to re-arm a bound Bot session. Fixes apache#4193 Generated-by: Claude Code
…er onto Astryx's token seams (apache#4465) * refactor(design-system): converge status surfaces onto Astryx's muted tokens Tinted status surfaces had two vocabularies and no agreement between them. Maka shipped `--{status}-wash` / `--{status}-wash-border` in two tiers over four hues; Astryx ships one `--color-{success,warning,error}-muted` rung per status and paints `info` with `--color-accent-muted`. Neither side was authoritative, so call sites drifted back to hand-rolled alphas — twenty-four of them across the product, spanning 0.04 to 0.46 for what is one ladder rung, including four sibling stat-tile borders at 0.24/0.24/0.28/0.3. The Astryx bridge in `maka-tokens.css` now remaps the three status `-muted` tokens to `oklch(from var(--{status}) l c h / 0.24)`, the alpha the neutral theme's own pastels already sit at. That bridge is the seam the accent remap already used, and for the same reason: these values reference runtime tokens that only exist once a palette attribute lands on `<html>`, which a compiled theme artifact cannot do. The whole `--{status}-wash*` family is deleted along with every call site that hand-rolled around it, and a border around a tinted surface is `--border` — the fill already states the status, and a second tinted edge is DESIGN.md §4's One Means Rule. This also fixes 124 `<Banner>` instances that were palette-blind: only `--color-accent-muted` was remapped, so `success`/`warning`/`error` banners rendered the neutral theme's fixed pastels no matter which of the 11 palettes the user picked. `--info` and `--info-text` are deleted with them. "Info" is a semantic slot, not a hue: seven of the eleven palettes put `--info` in `--warning`'s hue family (50-90 against warning's fixed 55), onedark's info and warning washes measured indistinguishable, and `mono` set it to zero chroma. Informational surfaces now paint with the accent, which is what upstream Banner does and what makes them follow the user's palette instead of competing with it. The `info` semantic stays everywhere it is a slot — `StatTileTone`, Banner `status="info"`, 29 call sites — and the five dead `var(--warning-text, var(--info-text))` fallbacks go, since `--warning-text` has existed since the status block was regrouped. `--color-accent-muted` keeps its `--accent-wash` value rather than moving to 0.24: it also backs fourteen upstream consumers, eight selection backgrounds and two focus rings among them, which a status-weight fill would overwhelm. That makes it a fill token and not an edge token, which is why the stat tile's `info` tone drops its border class instead of gaining an accent one — measured live, `--color-accent-muted` as a border reads as no border at all on a white card. The tone is carried by the value colour, which is the stronger signal. The solid tier is deliberately left un-converged and DESIGN.md now says so: `--success` / `--warning` / `--destructive` remain Maka's names for status ink, dots and solid fills. Moving those means moving status text, status dots and status buttons at once. Verified against live computed styles in both colour schemes: the four `-muted` tokens resolve palette-derived at every consumer, and the Storybook render smoke passes 486/486 across 243 stories. Generated-by: Claude Code (Opus 5) * refactor(design-system): alias Astryx's radius rungs to the product's px ladder One radius ladder carried two independent sets of literals in two different units. `--radius-control` / `-surface` / `-modal` / `-pill` are absolute px in `maka-tokens.css`; Astryx's `--radius-inner` / `-element` / `-container` / `-full` were rem values from the compiled theme. Both vocabularies are live in product CSS — `--radius-element` alone has nine consumers — and they agreed only by coincidence, which DESIGN.md §6 recorded as a real failure mode rather than fixing. The theme now emits each Astryx rung as `var(--radius-<maka tier>)`, making the px side the single authority. An upstream rung change can no longer move one name out from under the other, and the ladder cannot end up half in px and half in rem — which matters here because the root font-size is deliberately left at the browser default, so a rem rung is an implicit multiplier the px rungs do not carry. §6's table also said `card` and `container` where the tokens say `surface` and `modal`, so the Two-Name Rule named tiers that do not exist; both are corrected against the source. `--radius-page` is untouched: it is Astryx's own token with no Maka tier and no product consumer. Generated-by: Claude Code (Opus 5) * docs(design-system): state the control-height ladder's offset against Astryx The scale's own header claimed six rungs (20/24/28/32/36/40) and an `xl` and `2xl` tier; there are four rungs and no such tokens, and the two references to `--h-control-xl` deriving from `--size-element-lg` described a derivation that does not exist. The live trap is the letters. `--h-control-sm` is 24px, and `--size-element-sm` three lines below it is 28px — which maka calls `md`. The ladder starts two rungs lower than Astryx's because xs/sm have no Astryx element token at all, so every letter above them is offset by one. That is now stated where the rungs are declared, next to the same rule the radius ladder carries: resolve a rung from the box, never from the letter that sounds right. No token value changes. Generated-by: Claude Code (Opus 5) * refactor(desktop): drop the sidenav hairline the shell already cancels The theme drew a 1px inline-end border on `app-shell-sidenav` and `shell-layout.css` immediately set it to `border-inline-end-width: 0` with a transparent colour. The rule and its cancellation shipped together, so the edge has never rendered: the sidebar and the canvas share one floor colour and the content plate's edge is the only seam. Removing the mechanism and its cancellation is one change. This empties the theme's `components` section, so the Maka theme is now tokens and typography only. Generated-by: Claude Code (Opus 5) * test(storybook): run the render smoke in both colour schemes The sweep pinned `globals=colorScheme:light` and never rendered dark, so half the theme was uncovered. Light and dark are separate token blocks: a rule that resolves in one can be undefined, inverted, or invisible in the other, and a light-only sweep reports nothing. Every story now runs twice, with `emulateMedia` set so `prefers-color-scheme` and the Storybook global agree. 243 stories, 486 renders, all passing. Generated-by: Claude Code (Opus 5) * docs(design-system): record --color-text-disabled as a deliberate AA exemption Astryx's `light-dark(#a3a3a3, #525252)` measures 2.52:1 and 2.29:1, under the floor §3 holds every prose tier to, and it keeps turning up as a finding. It is a decision, not a miss: a disabled control read at prose contrast stops reading as disabled, and the values that clear 4.5:1 looked wrong beside the enabled rows they sit in. Written where the contrast rule is stated so the next audit finds the exemption instead of the bug, and scoped so it cannot be cited as precedent for another sub-AA value. Generated-by: Claude Code (Opus 5) * fix(ui): give the informational tint its siblings' construction Review found the four -muted tokens shared a weight but not a construction: the three status rungs were alpha over the surface while --color-accent-muted was bound to --accent-wash, an opaque L0.96 tint. An opaque fill does not compose, so a tinted chip on a tinted panel resolved to its parent's exact pixel: deep-research's "3/8" count pill was invisible for the whole run, and plan-mode's in-progress marker had neither a distinct fill nor, since it also dropped to --border, a distinct edge. Bind it to oklch(from var(--accent) l c h / 0.24) like the other three. That is also what upstream ships the token as (#0082FB33); theme-neutral is what flattens it to an opaque #f1f1f1. --accent-wash had no other consumer and is deleted rather than left as a single-use indirection. The stat tile's info tone gets its edge class back. It was dropped because an opaque near-white reads as no line on a white card — a symptom of the wrong construction, not a property of the info tone. Four tones, one rung, no exception left to explain. Also moves the three status tints out of the CSS bridge and into makaTheme.ts, next to --color-border. The bridge's stated reason — that a compiled theme cannot reference runtime tokens — was contradicted four lines away by the radius aliases doing exactly that. The real constraint is the cascade, and it binds only the accent pair, which maka.css re-declares at component level. Refs apache#3446 * fix(ui): restore AA on the five surfaces the heavier tint moved Moving these call sites onto the 0.24 rung darkened the fill under text that had not moved with it. Measured across 11 palettes x 2 schemes, five 12px supporting-text surfaces fell under 4.5:1 — worst 4.15 (skills governance notice, onedark light). DESIGN.md 3 treats AA as a contract; this PR adds an explicit exemption for --color-text-disabled, so a silent second exemption is not available. All five take --foreground instead of a secondary or accent ink. That is also the One Means Rule reading: the tint already states the role, so the ink is only text. Also corrects three notes the same review found stale: DESIGN.md claimed an info Banner follows the palette, when .astryx-banner.info re-declares both the tint and the text colour on the banner ELEMENT, which no ancestor bridge can outrank; a completed backlog line still named the deleted --*-wash family; and the app-shell comment still credited a hairline this branch removed. Refs apache#3446
Unexpected renderer and startup failures showed raw exception text as product copy: the crash surface rendered the exception and component stack into the window, and the native startup dialogs interpolated `error.message` into their detail text — unlocalized, unstable, and occasionally carrying content that belongs only in a diagnostic report. Raw exceptions are diagnostic data, not product copy. The crash surface and the native diagnostic dialogs now show fixed localized recovery guidance from typed catalogs (the reshaped `errorBoundary` group in `shell-copy.ts`, and a new main-process `native-diagnostic-dialog-copy.ts`). The raw details keep flowing through the existing diagnostics channel: the copyable report still carries the full redacted stack and component stack, and the startup report already carried `startupError.stack`. The last `locale ===` ternary block in `runtime-host-boot.ts` moves into the catalog. Dead copy groups, the stack CSS, and the storybook fixture for the old layout are removed. The renderer copy stays in `shell-copy.ts` on purpose: the renderer architecture ratchet forbids new files in the AppShell closure, so the renderer debt footprint is identical to `main`. Part of apache#2672
apache#4422 centralized the CLI's TUI copy into the typed catalog, but nothing stopped the next hardcoded string from regressing it. `scripts/check-tui-copy.mjs` (wired into the CI code gate) parses the 13 covered TUI boundary files with the babel AST and rejects three patterns: CJK string literals anywhere, locale-comparison branches that select copy, and bare literals reaching user-visible sinks (`notice`/`title`/`hint`/`new Text`/…). Legacy English literals are ratcheted through an exact per-file, per-occurrence inventory that also fails when an allowance goes stale, so the list can only shrink. Every `*tui*` file under `packages/cli/src` must be classified as covered or infrastructure, so a new TUI file cannot skip the gate silently. Also lands the apache#4422 review follow-ups: the hand-copied `editorCopy` test fixture is replaced by reading the catalog, the en/zh test loops collapse to English-only (en/zh parity stays owned by the catalog sweep in `tui-copy-catalog.test.ts`), the unreachable `?? code` fallback in `resultCopy` is removed, the `/new` failure notice names the stop failure instead of a generic retry hint, and the pending-queue renderers lose their `= 'en'` locale defaults so callers must thread the real locale — the only production caller already did. Part of apache#2672
Generated-by: Codex
* fix(desktop): issue live peer routes for shared sessions Generated-by: Codex * fix(peer): expose live relay routes to shared sessions * fix(desktop): surface pending turn requests * fix(peer): recover idle shared-session connections * fix(desktop): keep collaboration hook scope observable * fix(desktop): streamline collaboration invitation transfer * fix(desktop): preserve available collaboration inboxes * fix(peer): retain authenticated guest routes * fix(desktop): compact turn approval prompt * docs(pr): refresh turn approval screenshot * chore: remove PR screenshot asset * fix(desktop): preserve Session rail render isolation * docs(ui): refresh Astryx surface inventory * test(peer): make route refresh liveness deterministic
* feat(desktop): replace native recovery dialogs Generated-by: OpenAI Codex * fix(desktop): harden recovery window lifecycle Generated-by: OpenAI Codex * test(desktop): keep dialog assertions behavioral Generated-by: OpenAI Codex * fix(desktop): close remaining recovery gaps * fix(desktop): converge recovery dialog behavior * fix(desktop): keep recovery decisions conservative Generated-by: OpenAI Codex * fix(runtime-host): fence silent replacement connections Generated-by: OpenAI Codex * fix(desktop): default Host prompts to cancellation Generated-by: OpenAI Codex * fix(desktop): keep recovery transitions safe * fix(desktop): harden recovery decision boundaries * fix(desktop): bind recovery readiness to reload frame * refactor(desktop): keep renderer frame identity canonical * refactor(desktop): unify dialog fallback path * chore(runtime-host): declare idle helper compatible * chore(desktop): approve dialog document loader * fix(desktop): issue live peer routes for shared sessions Generated-by: Codex * fix(peer): expose live relay routes to shared sessions * fix(desktop): surface pending turn requests * fix(peer): recover idle shared-session connections * fix(desktop): keep collaboration hook scope observable * fix(desktop): streamline collaboration invitation transfer * fix(desktop): preserve available collaboration inboxes * fix(peer): retain authenticated guest routes * fix(desktop): compact turn approval prompt * docs(pr): refresh turn approval screenshot * chore: remove PR screenshot asset * fix(desktop): preserve Session rail render isolation * docs(ui): refresh Astryx surface inventory
Generated-by: Codex
Owner
Author
|
Superseded by the upstream PR apache#4527. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes apache#4522
Desktop now learns whether each Runtime Host exposes collaboration authority from the authenticated
host.statusboundary. The global turn-request inbox skips Owner Hosts that explicitly report no collaboration authority, while legacy/unknown Hosts remain eligible and transient reconnect failures retain the existing retry behavior. Expectedoperation_unavailablequery results are projected as an empty inbox so Electron does not log repeated rejected IPC handlers.Implementation
collaborationAuthoritycapability to Runtime Host status/diagnostics and propagated it through Desktop target identities.Verification
npm --workspace @maka/runtime-host run buildnpm --workspace @maka/desktop run build:testnpx biome checkandnpx biome lintpassed for all changed files.apps/desktop/src/preload/runtime-host-session-catalog.tsand unrelated@maka/uitype drift under the available Node/npm environment.Platform limitations
Older Runtime Hosts that do not send the optional capability remain queryable for compatibility; unavailable responses are safely treated as an empty inbox.
AI use
Tool(s) and scope: OpenAI Codex authored the implementation and regression tests.
Checklist
Does this PR entail a change in behavior?