mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 00:02:29 +00:00
debug-p0-plus
10914
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cc99be19dc | docs(plans): STA-7948 — async reads on both sides, unknown reset verdicts stay unknown | ||
|
|
35130efac6 | docs(plans): STA-7948 — verdict application owns the toast takedown and the open scope | ||
|
|
986bb36120 | docs(plans): STA-7948 — Fix keeps the toast up; null verdict clears the open scope | ||
|
|
1f02a1029a | docs(plans): scope the folder-access notice per folder class, gate reset, prompt deadline | ||
|
|
883e2f27b5 | docs(macos): STA-7948 — verdict is a named enum after the elegance review | ||
|
|
da59536de7 | docs(macos): STA-7948 — the group a restart cannot help is ~12%, not a third; split later denials by the denying daemon's version | ||
|
|
4386375424 | docs(macos): STA-7948 dialog as built — footer actions, reset-permission step, what is known about the stuck third | ||
|
|
bda8e6b9fd | docs(macos): final STA-7948 UX — one-line toast, guided fix dialog, fresh-daemon probe | ||
|
|
27dabcbe90 |
docs(macos): fold PostHog field data into the STA-7948 plan and drop the helper probe
daemon_pty_cwd_denied fired for 1,438 users in 21 days (2.8% of users adopting a different-version daemon, 96% on Documents/Desktop/Downloads), so the daemon's existing verdict already observes the failure and the helper probe for old daemons has no population left to cover. A fresh same-version daemon ended the denial for about 69% of observable users; the notice copy now tells the rest to re-allow the folder. |
||
|
|
f7e129ffe6 |
docs(macos): finalize the STA-7948 daemon folder-access plan on live evidence
Replace the helper-first recovery design with a two-layer plan: an in-daemon opendir verdict plus a notice on the existing attribution poll (ship now), and the helper probe for already-deployed old daemons (evidence-gated). Record the 2026-09-21 measurements: on Documents, access(2) passes while opendir gets EPERM, so the current accessSync verdict is blind to the incident's folder class; daemon-spawned login shells carry the daemon's grant; the login wrapper is unconditional in production. |
||
|
|
56f816936e | docs: prioritize macOS daemon recovery | ||
|
|
2ce252f471 |
fix(grok): announce a completion once, when Grok is actually finished (#20523)
* fix(grok): announce a completion once, when Grok is actually finished Orca pinged on every Grok turn-end. Grok runs turns the user never asked for: when a background task finishes it wakes itself, does a little work, and ends another turn. One request produced several pings. Grok already reports, on every turn-end, whether it still has work outstanding. Read that instead of trying to classify which turns are "real": backgroundTasks absent -> silent, this is the session-end tail StopFailure / StopCancelled -> announce, a failure is never hidden stopHookActive -> silent, a Stop hook is keeping it working a shell task or subagent running -> silent, the work is not done otherwise -> announce Nothing here knows what an auto-wake turn is. A turn that ends with work outstanding stays quiet; the later turn where that work is finally done is the one that announces. That is also why this survives the case where Grok completes a user's goal inside one of those turns — prefix-based suppression would have silenced it. Monitors and scheduled entries are deliberately not counted as outstanding work. They can run indefinitely, so counting them would suppress a user's completion permanently, and a lost ping is worse than an extra one. Also registers StopCancelled, which Grok fires instead of Stop on a user interrupt, a declined permission, --max-turns, or a no-progress bail-out. Orca never subscribed to it, so those turns were reported as successes. Also removes a stale notification matcher that searched for prose the shipping binary never sends; the typed notification kind is matched instead, and neither idle_prompt nor task_complete is treated as a completion. Needs-input behaviour (permission prompts and ask_user_question waits) is unchanged and stays ungated by background work. * fix(grok): never hide a failed or cancelled turn behind the background-work gate The announce predicate checked field-absence before terminal outcome. Grok's StopFailure and StopCancelled payloads carry no background inventory at all, so the absent-field branch — added so the session-end tail stays silent — fired first and silenced every failure and every cancellation. That inverted the rule it was meant to serve. Before this series a cancelled turn at least surfaced as a (wrong) success; gated this way it surfaced as nothing. Terminal outcome is now checked first, so a failure or cancellation announces regardless of what other fields the payload happens to carry. The existing tests passed straight through the bug because they built failure payloads with a backgroundTasks field Grok never sends for those events. They now model the real payload shapes, verified against the provider's payload definitions and the captured envelopes. * fix(grok): settle completion from provider lifecycle state * fix(grok): fence stale turn ends without prompt ids |
||
|
|
9cf0a6c37f |
perf(remote): avoid repeated capability probes during file imports (#14555)
* perf: avoid repeated remote import capability probes * test: cover cold remote import compatibility probe * fix(remote): fence imports across runtime reconnects * fix(remote): bind import proof to connection * fix(remote): fence import routing by runtime identity * test(remote): remove unsafe import fixture assertions - type remote RPC mocks at declaration so call arguments stay checked - narrow upload params before reusing generated temp paths --------- Co-authored-by: Neil <neil@stably.ai> |
||
|
|
96b450fae8 |
fix(ssh): bound relay incumbent lsof probe (#18304)
* fix ssh relay incumbent probe timeout
* test ssh relay unconfirmed probe termination
* fix(ssh): preserve connect evidence while bounding lsof
* fix: preserve uncertainty when relay holder enumeration fails
* fix(ssh): supervise lsof helpers and preserve partial holder evidence
* test(ssh): prevent GC racing unconfirmed probe cleanup
* refactor(process): keep POSIX lsof supervision in process owner
* fix(ssh): confirm census cleanup and handle probe startup signals
* fix(ssh): keep lsof holder evidence usable on hosts with unstat-able mounts
lsof warns to stderr about mounts it cannot stat, and any stderr byte forced
the holder enumeration to 'unavailable' — making the 'exited' verdict, and so
husk reaping, unreachable on those hosts. Pass -w to suppress the warnings.
* Revert "fix(ssh): keep lsof holder evidence usable on hosts with unstat-able mounts"
This reverts commit
|
||
|
|
3ab2a1b91c |
refactor(orchestration): derive delivery eligibility from messages (#19837)
* fix(orchestration): retire read deliveries and clarify mailbox recovery * fix(orchestration): simplify delivery recovery and update nudge contracts * test: align orchestration check help expectation * refactor(orchestration): derive delivery eligibility from messages * fix(orchestration): validate live consumers and simplify batch revocation * refactor(orchestration): keep deliveries.status and derive eligibility without a column drop The outstanding_deliveries view now reads status = 'outstanding' plus unread membership, so v41 only drops uniqueness from idx_deliveries_one_outstanding and adds the view and trigger. Older binaries can still open the database. Removes the column-drop migration, the v40 test fixture and hasColumn guards, the fenced skew probe, and the unrelated nudge-text change. * docs(orchestration): drop delivery storage reference The compatibility caveat it existed to explain no longer applies; the view and index comments carry the remaining rationale. * docs: revert unrelated formatter churn * test(orchestration): verify historical database downgrade round trip |
||
|
|
31db2774f8 |
fix(git): skip upstream remote probes when the remote is absent (#18455)
* fix(git): skip upstream remote probes when the remote is absent Issue and PR resolvers listed remotes by probing `git remote get-url upstream` on every poll, including origin-only clones where that remote cannot exist. List remotes once, cache against git config, and skip the probe unless `upstream` is present. * fix(git): avoid stale remote probe cache entries * fix(github): observe origin repository probe failures * fix(github): observe verified origin probe failures * fix(github): skip missing upstream probe for PR lists * test(github): scope the #9171 lazy-resolution guard to default-branch commands The guard asserted that no git command runs for an open PR, using "no git at all" as a proxy for "no default-branch resolution". Remote-name listing is a separate concern, so allow it and keep every other command forbidden; the symbolic-ref/rev-parse resolution this issue is about stays unreachable. --------- Co-authored-by: Neil <neil@stably.ai> |
||
|
|
16d1ab81d3 |
perf(skills): bound WSL installed skill discovery (#12314)
* perf(skills): bound WSL installed skill discovery * fix(skills): preserve bounded discovery correctness * fix(skills): bound WSL metadata prefilter reads * fix(skills): isolate absent discovery cwd cache keys * test(skills): adapt WSL discovery mocks to runner * fix(skills): preserve filtered discovery fallbacks * fix(skills): share filtered scans and preserve WSL inventory * fix(skills): share WSL scans without losing skill aliases * fix: preserve skill metadata and retire filtered peer caches --------- Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
c21c083224 |
fix(auth): report callback failures instead of cancellation (#20535)
* fix(auth): distinguish failed sign-ins from user cancellation * test(mobile): fix conditional registration lint and refresh recorder fingerprints |
||
|
|
4784fa0087 | fix(grok): defer managed hook pane guard expansion to shell (#20534) | ||
|
|
e9db1642d1 |
refactor(attention): move the agent attention boundary off terminal panes (#20525)
* refactor(attention): move the agent attention boundary off terminal panes The completion-attention pipeline asked PTY questions inline, so a non-terminal agent surface was structurally excluded from unread, delivery and acknowledgement. Extract a provider-neutral policy under `src/renderer/src/attention/` that reaches every surface fact through an adapter, and move the PTY-shaped predicates (`hasLivePtyForNotification`, `isCurrentLivePaneKey`, `isCurrentKnownPaneKey`, `isVisibleForegroundPaneKey`, plus the leaf/foreground resolution the auto-ack scan did inline) into `terminal-attention-surface.ts`, now their only call site. Unread markers carry the reason that wrote them (`agent-completion`, `terminal-bell`, `manual-mark-unread`, `legacy`); an unclassified boolean still on live state reads as `legacy` rather than being guessed or migrated. Main's `notifications:dispatch` closure becomes a delivery service with injected collaborators; the IPC handler is a thin adapter. Behaviour is unchanged: unread is still written before main's desktop gate, the tray dot before the cooldown/focus gates, and mobile fan-out before the desktop early returns. * fix(attention): recognize classified tab unread markers |
||
|
|
9f435df713 | Update README downloads badge | ||
|
|
7ce8e18d07 |
test(mobile): consolidate the RPC migration's verification infrastructure (#20521)
* test(mobile): record main RPC hooks and regression schedules Add scripted sender recordings, guarded main goldens, reply matrices, lifecycle schedules, settings caller fixtures, and targeted B-seed mutants. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): flush recording user actions through React act Keep lifecycle updates in separate act boundaries while wrapping direct stateful user actions. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): compile recorded modules with the Node VM API Use the same trusted-source execution boundary as existing mobile VM test harnesses. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pool golden values and hoist pre-divergence checkpoints Golden format version 2 stores each distinct observation field value once in a `values` map keyed by a 12-hex sha256 of its sorted-key JSON, and a checkpoint references five hashes. Output stays pretty-printed; the reader rejects any other format version, resolves hashes back to values, and reports the scenario, checkpoint, field and JSON path on a mismatch. Generated variants now declare where their distinguishing input lands, so checkpoints observed before that point are recorded once in a `.prelude` scenario instead of once per reply partition. Reply matrices, interruption schedules and lifecycle schedules share the primitive, which asserts each variant's pre-divergence prefix matches the base. Equal-but-differently-reached checkpoints are untouched. 17.71 MB / 3,599 checkpoints / 58.4% intra-file duplicates becomes 4.21 MB / 1,961 checkpoints / 23.6%, with every file's set of distinct observations unchanged. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): make the recordings sense deadlines, the recorder, and every family The goldens carried no temporal information, so a request deadline could be cut to a third and all 61 files stayed byte-identical. Every threshold is now straddled by two advances with a checkpoint between them: the 30 s request deadline in both schedule drivers, the 120 ms search debounce in b1, and the 60 s repo-metadata cache TTL. Shortening any of them moves an observation. The record fence pinned product sources but excluded the whole recorder, so --record could rewrite every golden from a modified runner and report the baseline intact. Goldens now pin recorderSha256 over every non-markdown file in the runner plus pilot-scenarios.json, and the fence exemption shrinks to the one directory that digest covers. Mutation evidence covered 3 of 13 mounted operations. There is now one anchored mutant per adapter family, covering 11 operations and 51 of the 61 goldens; the two omitted are the pure async loaders whose entire output is their settlement. Anchors are asserted to match exactly one site, which caught the acceptance mutant silently half-applying against three identical guards. The archived-tree assertion pins each seed's visible state instead of merely differing from main, and error observations carry code and cause when present. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): record settlement times instead of straddling deadlines The previous commit made the reviewer's divide-by-three deadline mutant fail by placing checkpoints on each side of the 30 s deadline. That is a patch: a timing change that does not cross a hand-placed boundary stays invisible. Those scenario edits are reverted, and pilot-scenarios.json and schedule-driver.ts are byte-identical to what they were before them. The real defect was that the projection had no temporal dimension, so every settlement now carries startedAt and settledAt in virtual milliseconds on the pinned fake clock. Any transition the product schedules for itself is recorded at the time it actually fires, so a deadline or debounce change of any size, in either direction, moves a recorded number. A checkpoint's own clock is not recorded. It is always the sum of the scripted advances, so it is a function of the scenario rather than of the code under test; run-recording.ts asserts that equality at every checkpoint instead, which costs no bytes and fails loudly if it ever drifts. projectionVersion is 2 and all 61 goldens are re-recorded. With the added timestamps stripped, the distinct-observation set is identical to the previous recording, so the change is purely additive. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): probe the repo-metadata cache inside its TTL window Recorded settlement times cover thresholds the product schedules for itself, but not one it only consults when something else makes it act. The repo-metadata TTL is the single such case: with probes only at 0 s and 60 s, a 20 s TTL and a 60 s TTL are both expired at 60 s and record identically, so a 3x cache-lifetime regression was invisible. settings-repo-cache-expiry now probes the cache at 59 s as well. This is coverage, not a substitute for recorded time: it bounds how small a TTL reduction is visible rather than making the reduction itself observable, and the README says so. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): record the reply shapes a host can send, not a cross product The reply matrix froze ~26 malformed envelopes crossed against every consumed field and three boundary kinds, which is 163,925 lines of JSON pinning accidents on inputs no desktop produces. `successResponse` always sets `result`, so a JSON wire has no explicit-undefined slot, and no mounted handler returns a number, a string, an array, a bare `{}` or a boolean: `settings.get` returns `{settings: ...}`, and the seed methods return an object or nothing. Each family now runs nine witnessed partitions once, with no field cross: a normal result, an absent result, `null`, an inner `{ok: false}` envelope with a string or an object error, an inner envelope missing `ok`, an outer refusal, `method_not_found`, and a transport rejection. `null` stays because `linear.getIssue` returns it for a missing issue and b2 is a shipped null-result bug; it is also what carries the one named delta these goldens record. `run-step1-exit.ts` had zero callers and shelled out to the same two Vitest files as `rpc-recording.mts`, so it and its README paragraph go, along with `MUTATION_NAMES`, which only it read. In the module loader, the `rpc-delivery-ambiguity` escape is measured dead: over every scenario, mutant and reference run it was taken once, by the test that existed to take it. Golden comparison already fails loudly if a mounted module ever imports the marker, so both go. The history-panel exposure moves into a declarative table beside the mutation anchors, leaving the loader with one source-text mechanism and no per-file branch. The VM stays. Mount adapters load product sources from an arbitrary `root`, and the archived |
||
|
|
d138c3278d | fix: show unexpected signout notice only once across versions (#20526) | ||
|
|
5e70014da8 |
feat(native-chat): support file drag and drop (#20494)
* feat(native-chat): support workspace file drops * fix(native-chat): report OS file drops that attach nothing #15782 is a silent failure on the Finder route, and that route still swallowed every way it could fail: - the preload handler returned with no feedback when the OS handed us file items `webUtils.getPathForFile` could read no path from (promised or virtual files). It now sends the existing `rejected` payload with a new `unresolved-paths` reason, which the global drop toast names. - the composer's external-attach path dropped the batch with no notice when every path failed authorization, when an upload came back empty, and (new in this branch) when the owner changed mid-flight. Each exit now sets a notice; only a disabled composer stays quiet, because it has no notice surface. Also stops `resolveNativeChatAttachmentOwnerForWorktree` throwing out of a drop/IME handler when an SSH connection's generation is gone mid-attach — that is an unknown owner, which the resolver already models as `not-ready`. * refactor(native-chat): one owner-identity check for composer attachments The branch had two near-identical "is this still the same owner" helpers, one per attach route, and they disagreed: the workspace-drop copy ignored the SSH connection generation, so a reconnect between the drop and the IME flush read as the same owner and the path landed on a new connection. Collapses both onto one predicate in the pure ownership module (the store/toast-free seam both routes already depend on), which compares the full SSH expectation and never treats `not-ready` as a match. * perf(file-explorer): resolve drag ownership at dragstart, not per render The virtualized row list resolved the selection's source execution host on every render — the virtualizer re-renders on every scroll frame, so a large multi-selection paid a full projection scan plus a route allocation per selected path per frame, and per visible row on top of that. Only `onDragStart` ever read the result. Rows now receive a resolver they call with the paths they are about to drag. The three copies of the "stamp only if both halves resolve" guard (explorer row, both combined-diff row shapes) collapse into one helper next to the writer. * fix(native-chat): refuse a guarded composer drop visibly The drop handlers claimed the drag (preventDefault + stopPropagation) before checking `disabled`, so a guarded composer told the browser it accepted the drop, left the copy cursor up, and then did nothing — the same silent swallow this branch exists to remove. Dragover now answers `none` when the composer is guarded, so the cursor refuses and no drop event follows. It still claims the event either way: the composer sits inside the terminal surface, which accepts the same drag and would paste the paths into the shell instead. Drops `stopImmediatePropagation`. The capture-phase `stopPropagation` already keeps the event off the editor below, so the stronger form only risked suppressing unrelated listeners on the React root. The fake DataTransfer in the test now starts at a dropEffect we never write, so asserting `none` or `copy` proves the handler set it. * fix(native-chat): decide attachment ownership per path, not per batch A queued batch can mix sources — a workspace drop the target host owns and a client-local paste it cannot read — because IME composition holds both until it settles. Collapsing the batch to one verdict refused the whole thing on a remote target, including the drop the user was entitled to make. The verdict now follows the path it belongs to: owned paths attach, client-local ones are refused, and the refusal is reported rather than dropped. A stale owner still refuses everything, since that means the target moved under all of them. Also guards the empty-batch case, which previously read as "every path owned". * refactor(combined-diff): resolve drag ownership from the live workspace The combined diff captured an execution host into the open-file record at tab open and drilled it through three components to reach the row. That host was never persisted, so after a restart every drag from a restored diff was refused until the tab was reopened, and the capture failure was swallowed into an undefined source with no trace. Rows now resolve the owner the same way the source-control rows already do, from the workspace the diff belongs to at the moment of the drag. That deletes the prop drilling, the store capture and its bare catch, and leaves one way to answer "who owns these paths" for every live listing. The file explorer keeps its per-node owner: its tree is a cache that can still be showing a previous host's listing, which is exactly what that field records. * revert(file-explorer): drop the workspace-id tree reset Resetting and reloading the tree when the workspace id changes at an unchanged path is not needed for the drag source to be correct. The tree already records the workspace whose root listing it committed, so a cache left over from a previous workspace stamps that workspace and the composer refuses the drop — the intended answer, reached without touching the reset rule. That rule clears selection, the name filter and undo history, which is more file-explorer behaviour change than this feature asked for. * test(native-chat): stop the external-attach mock hiding new notices The hook's test replaced the whole attachment-owner module with a hand-written stub, so the two notices added alongside the owner-change guards resolved to undefined. Calling them threw inside the async attach loop — an unhandled rejection, which leaves every test in the file reported as passing while the run as a whole fails. CI caught it; a local run reporting only pass/fail counts does not. The mock now spreads the real module, so a notice added later cannot go missing from it, and both owner-change tests assert the string a user would read instead of only asserting that nothing attached. * test(native-chat): guard the last-path owner change on a one-file drop The owner flipping while the final path is authorizing has no next loop iteration to catch it, so the post-loop check is all that stands between a single-file drop and a path attached to a host that no longer owns it — and a one-file drop is the ordinary shape. No test covered that exit. Removing the post-loop check now turns this red; before it, only the multi-path exit was guarded. * fix(native-chat): keep a mixed attachment batch in attach order applyResolvedPaths partitioned a queued batch into a target-owned half and a client-local half and concatenated them. An IME-delayed batch that mixed a workspace drop with a paste made earlier in the same composition was therefore inserted owned-first, so the dropped reference jumped ahead of the pasted one in the draft. Filter against the two verdicts in place instead. Membership is unchanged, the order the user attached in survives, and the two intermediate arrays go away. * fix(file-explorer): name the owner of a dragged path whose row is hidden A multi-selection outlives the rows that showed it. Nothing prunes selectedPaths when a directory collapses, when the name filter narrows, or when dotfiles are hidden, and the drag still carries every selected path. Drag-source resolution read those owners from the row projection, which is built from visible rows only, so one hidden path collapsed the whole drag to an unstamped one and the composer refused it as coming from another workspace. The owner was never unknowable — the dir cache the projection is built from still records which host listed that path. Fall back to it when the path has no visible row. A path in neither (a name-filter synthetic node for a directory that was never listed) still fails closed. * fix(native-chat): ask which workspace the composer serves now The IME-flush ownership check compared the workspace id captured when the drop happened against the same captured value, so for a structured pane the comparison could only ever hold. The live protection came from the host and owner checks beside it; this one asked nothing. Read the id through a ref so the check means what it reads as. A pane whose structured target moves between the drop and the composition settling now refuses the queued path instead of attaching it. * fix(native-chat): ask which workspace an external attach lands on The post-await ownership gate resolved the owner through the render closure, so it re-asked the workspace the attach started in and compared the answer with itself. A tab moved to another workspace mid-authorization passed the gate, and the paths landed in a composer that no longer served that workspace. Read the pane through a ref and compare the workspace identity as well as the owner: two workspaces can both report a local owner, so the owner alone cannot tell them apart. * test(native-chat): read the real notice on a workspace drop The drop tests hand-built their attachment-upload mock and hand-copied the not-ready wording into it, so the assertion tracked the copy rather than the string a user reads: rewording the real notice left all 15 tests green. Spread the real module and override only the owner resolver, matching the two sibling test files in this directory. Rewording the notice now fails the test. * docs(native-chat): restore the hook's doc comment to the hook The workspace comparison landed between the doc block and the function it describes, leaving the comment attached to a type alias. * test(native-chat): cover the upload window for a moved pane The workspace-currency gate guards two windows and only the authorize loop was covered. The upload window is the longer one: the paths go to the worktree the attach captured, so a pane that moved workspaces meanwhile must not receive remote paths living under the workspace it left. * test(native-chat): pin the two untested attachment refusals Refusing an already-blocked target at the drop rather than queueing it had no test: queued paths that can never attach still spend the pending budget, and the next legitimate drop is then turned away for being one too many. Also pins the immediate already-false ownership verdict. Today's only caller settles ownership synchronously so it cannot arrive false, but the hook exports this entry point and the fallback is not a refusal — a false verdict is not "owned", so a remote target blames client-local attachments for an ownership failure. Verified: removing the branch reports the wrong notice. * docs(native-chat): say which rule the ownership refusal follows The per-path comment sat directly above the batch-wide ownership refusal while describing the blocked-target logic below it, so the refusal read as a contradiction of the line under it rather than as the file's stated rule. Name the rule at the refusal: a failed ownership verdict refuses the whole completion, the same way the pending-limit rejection does. --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
241fb9ed9d |
perf(terminal): batch file-link checks on their owning host (#20463)
* perf(terminal): batch file-link existence checks on their owning host * test(relay): allow additive filesystem capabilities * fix(web): keep terminal file links working under batched existence checks createShellApi omitted pathsExist, so withFallback answered the new batch call with a truthy proxy resolving to undefined and the whole hover batch rejected — dropping every link on lines with an out-of-worktree path. * test(web): assert the shim without type assertions --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: Neil <neil@stably.ai> |
||
|
|
c853e10e0c |
fix(rpc): validate provider-specific fields in TaskProviderIdentity (#20284)
* fix(rpc): validate task provider identity fields Validate provider-specific field types while preserving nullable scopes and unknown identity fields. Record the producer census and pin validation with regression tests. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(rpc): reject a blank GitHub owner or repo normalizeTaskProviderIdentity treats a blank owner or repo as no identity at all, but the schema accepted '' and whitespace-only, so the two disagreed about the same payload. Refined rather than trimmed: trimming would rewrite the parsed value and change what the handler receives. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(rpc): re-measure the identity evidence counts The blank-field commit added seven tests, so the recorded 74/16/58 described the commit before it. Re-ran both: 81 tests, and the discriminant-only mutation now gives 17 failures / 64 passes. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(rpc): correct the remaining stale gate count The blank-field commit moved the full-RPC total too; 2,463 was the count before it. Re-ran: 278 files, 2,470 passed, one skipped. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
a28cd9eae5 |
fix(browser): press keys through a US-layout CDP key table instead of a subprocess per keystroke (#15310)
Typing in the remote browser pane spawned an agent-browser process per keystroke -- ~160ms each, so a 17-character password took seconds -- and some keys arrived half-formed: F-keys, Insert and ContextMenu dispatched windowsVirtualKeyCode 0, Shift+1 typed '1' instead of '!', and non-ASCII printables reported success while typing nothing at all. keypress now resolves the key name through a US-layout table and dispatches the Input.dispatchKeyEvent pair over the electron debugger, the same transport mouseClick already uses. Two fallbacks keep the old behavior reachable: - a single printable BMP character outside the table dispatches in process as an IME-style event (keyCode 229 with the character as text, the shape composed input already has when it reaches pages) - anything else -- media keys, surrogate pairs, unrecognized names -- goes to the helper exactly as before, and only that path pays for creating the helper session Virtual key codes come from the table, never from the character's own char code: charCodeAt puts '&' on 38 (VK_UP) and '.' on 46 (VK_DELETE), which Blink runs as caret commands that swallow the character. Dispatch failures normalize the way evaluate's already do -- a gone page becomes browser_tab_not_found, anything else browser_error -- because attach and sendCommand reject with plain Errors that the RPC layer would report as runtime_error, and the pane only reclaims a dead page when it sees a browser_* code. Result shape is unchanged and no wire, schema or RPC surface moves, so mixed-version client/server pairs see no difference. Pages can observe the fidelity fixes: Shift+a now types 'A', Shift+1 now types '!', Alt+<char> no longer carries text, and editing keys arrive as rawKeyDown. Each matches what a real US keyboard produces. Verified against the shipped agent-browser 0.27 binary on the same browser: every difference is a fix, nothing regressed. macOS editing shortcuts (Cmd+A) do not fire through either path -- Blink runs those off the native responder chain and neither sends CDP `commands` -- so that gap is unchanged, not introduced. Co-authored-by: Neil <neil@stably.ai> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e944e76537 |
fix(grok): stop replayed Claude/Cursor hooks reporting Grok panes as Claude (#20507)
* fix(grok): stop replayed Claude/Cursor hooks reporting Grok panes as Claude Grok's hook discovery reads ~/.claude/settings.json (and the Cursor equivalent) for vendor compatibility, and that is on by default. So inside every Grok pane Orca's managed Claude hook fires in addition to Orca's managed Grok hook, and both POST the same Grok envelope. The Claude-routed copy lands last and wins, so the pane's agent type is resolved from the POST route as "claude" and no Grok-specific normalization runs for it. Guard the managed Claude and Cursor scripts on GROK_HOOK_EVENT, which Grok's hook runner stamps into every hook subprocess it spawns — including replayed vendor configs — after any user-supplied environment, so a hook cannot spoof it. This mirrors the existing DEVIN_PROJECT_DIR guard in the same script, which solves the identical problem for another agent that imports Claude hooks. Placement is load-bearing: the guard sits after the stdin capture, so Grok's writer never blocks, and before both the spool write and the HTTP POST, so a replayed event cannot leave a spool entry that replays later. The Windows variants jump to the stdin-drain label rather than exiting, because abandoning stdin there hangs the writer. The guard is scoped to agent === 'claude'; OpenClaude reuses ClaudeHookService with its own settings file, which Grok does not replay, so it is unaffected. Verified live against Grok 1.0.25 in a dev instance: the pane's reported agent type goes from "claude" to "grok" on every turn-end, including the hidden follow-up turns Grok runs when background work finishes. The guard pushed hook-service.ts past the 300-line cap, so the script builder moves to a sibling hook-script.ts. That mirrors the existing split under src/main/cursor/, where the service owns install/status and the script module owns script text. * fix(agent-hooks): preserve Windows background worker stdin contract |
||
|
|
09187fcad8 |
fix(ai-vault): stream oversized remote session transcripts (#20455)
* fix(ai-vault): stream oversized remote session transcripts * fix(build): bundle streamed JSON parser in desktop main --------- Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
149164b74f |
fix(tasks): preserve repository results under GitHub search quota (#20460)
* fix(tasks): preserve repository results under GitHub search quota * fix(github): preserve search budget on count fallback --------- Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
22f56f7c2a |
fix(runtime): reject malformed file Base64 padding (#20283)
Require padded file-write payloads to end on a Base64 quartet boundary. Cover both RPC methods and padded final upload chunks, and document client compatibility evidence. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
85d7cf3cc1 |
fix(mobile): preserve delivery ambiguity across transport cutover (#20280)
* fix(mobile): preserve delivery ambiguity across transport cutover Let physical close settle requests and retain its error as the cutover cause, copying only an existing delivery-unknown mark. Pin sent and unsent caller outcomes and both cutover predicate carriers. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): pin the RpcClient.close() settlement contract close() was declared `() => void` with no stated obligation. That was harmless while migrateTo rejected pendings itself; now that it does not, close() is the retiring generation's only settlement path, so a type-compatible implementation that leaves a request pending strands its caller for good. States the obligation on the declaration and pins it for both trackers the real implementations reject through. Dropping the delivery-unknown flag, dropping the relay mark, or leaving pendings in the map each fail a test. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): read the cutover cause without a type assertion main's new casting gate rejects `(error as Error).cause`; narrow instead so the assertion still distinguishes a missing cause from an unmarked one. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
2fc84cb492 |
fix(mobile): give native chat one tail-follow owner so streaming stops jumping (#20493)
* fix(mobile): stabilize native chat tail following * refactor(mobile): give native chat one tail-follow owner Extract the streaming scroll contract into use-mobile-native-chat-tail-follow, so intent and geometry have a single writer instead of a state/ref pair hand-synced at five call sites. No behaviour change: the existing guards pass untouched. * fix(mobile): fence native chat tail follow through momentum * fix(mobile): repin chat at measured tail --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
b0070e3720 |
refactor(mobile): migrate settings reads to RpcOperation (#20499)
* refactor(mobile): migrate settings reads to RpcOperation Replay the settings slice on the landed RPC foundation after rebasing onto main. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): refresh task parity snapshots after main rebase Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): correct rebased declaration parity hash Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): account for main task declaration Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): preserve raw RPC rejection timing Return the transport promise directly and interpret replies separately so sibling Promise.all rejection order cannot change. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): refresh parity hashes after timing fix Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): use operation interpreter after raw request * test(mobile): refresh settings migration parity hashes Refresh hook and statement parity hashes for the two task declarations whose settings reads now use RpcOperation request and interpretation. Changed declarations: - useMobileTasksRuntimeHydration: settings.get replaced by settingsRead request/interpret. - useMobileTasksWorkspaceCreateActions: settings.get response handling replaced by settingsRead request/interpret. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
1f7655f3e3 |
feat(ai-vault-search): public session search contract and transports (#20277)
* feat(ai-vault-search): define public contract and service seam * feat(ai-vault-search): add IPC runtime relay and web transports * fix(ai-vault-search): register search IPC at the core handler site ai-vault.ts was two lines over the 300-line max-lines limit; the search handlers belong with the other register*Handlers calls anyway. * fix(ai-vault-search): withhold degraded-root paths from relay status Status carried local filesystem paths over the relay while hits redact theirs. redactStatusForTransport applies the same policy at the same boundary: relay callers keep each root's reason and the array length as the count, so the type only makes root optional. * fix(ai-vault-search): close diagnostic path leak and remove test casts * feat(ai-vault-search): carry an execution host id and per-host outcomes on hits * feat(ai-vault-search): route desktop search by execution host scope, including runtimes * feat(preload): accept an execution host scope on session search * feat(web): answer only for the paired runtime on session search * docs(ai-vault-search): describe execution-host routing and the all-hosts merge * test(ai-vault-search): cover every host scope, the all-hosts merge and wire compat * fix(ai-vault-search): resume every host mid-page so a merged page never drops a hit * fix(ai-vault-search): decode the merged cursor with a schema instead of casts CI's type-aware audit refuses type assertions; a zod record validates the per-host entries and yields the typed map without one. * refactor(ai-vault-search): defer cross-host merged search |
||
|
|
974af8c0fb |
fix(worktrees): retire the chat tab of a chat with no child when its workspace goes (#19970)
* fix(worktrees): retire the chat tab of a chat with no child when its workspace goes Deleting a workspace left a chat tab behind for every structured session that had no attached provider child at the time, and that tab came back at the next launch pointing at a workspace that no longer exists. A provider child is scoped to a VISIBLE pane, not to a tab: the hold that keeps one is `enabled: isVisible && isWorktreeActive`, and dropping the last hold evicts the child after the release grace. So the sweep's liveness predicate selected only "the chat that is the visible pane in the active workspace, or was moments ago" — which means deleting a workspace from the sidebar while a different one is active left every chat in the target invisible to the sweep, and the `live.length === 0` early return did nothing at all. The durable reference is `visibleSessionIds` in the agent-session record store. Both purges a removal already performs miss it: the renderer drops `unifiedTabsByWorktree` and the main process drops the workspace metadata, and neither touches that index. Startup replays it, restores the session from it and republishes the tab. Worktree ids are path-derived, so a later workspace created at the same path inherits the old chat. Splits the two concerns the sweep conflated in one list. Liveness still decides what to CLOSE and what to refuse over, unchanged. Membership — the same fenced record filter minus the liveness clause — decides what to RETIRE, and covers exactly the complement of the close list so each session's tab is handled once. Retirement runs from `killAllProcessesForWorktree`, past every point that can refuse, not from the structured sweep itself: that sweep is joined BEFORE the unstopped-PTY verdict so a structured refusal can outrank a terminal one, and a tab retired there would still be ahead of a gate that can refuse the whole removal — leaving the workspace in place with its chats gone. * fix(worktrees): retire tabs across all teardown outcomes * test(worktrees): type teardown fixtures --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
cf20e089d2 |
fix(native-chat): wait for the runtime capability probe before resolving the creation launch route (#19819)
* fix(native-chat): wait for the runtime capability probe before resolving the launch route
A worktree created before the renderer's hydration-gated capability refresh
runs read the local capability set as null, which
resolveStructuredNativeChatSupport treats as a blocker, silently degrading
structured native chat to the legacy terminal-backed route. Creation submits
now await ensureLocalRuntimeCapabilities(), which probes the local runtime
when no answer has landed yet, so the route resolves on an actual answer.
Fixes #19154
* fix(native-chat): await the capability probe in the work-item direct launch route too
prepareDirectWorkItemAgentLaunch is the fourth creation-flow route owner and
already async; a pre-hydration submit-after-ready launch (fix-checks) read the
unprobed cache as unsupported and silently degraded to legacy. Draft-delivery
launches were unaffected (draft-prompt blocks structured before the capability
check). Same shape as the three creation-submit sites.
* fix(native-chat): keep the capability probe starting synchronously
The broken-bridge hardening wrapped the probe in Promise.resolve().then(...),
which deferred window.api.runtime.getStatus() by a microtask. The session-tabs
restore deliberately overlaps its inventory RPC with this refresh and relies on
the probe already being in flight when refresh returns, so the deferral broke it.
The bridge call is synchronous again; a synchronous throw becomes a rejection
instead, which is what the wrapper was actually for.
* fix(native-chat): hydrate local runtime capabilities at renderer boot
The capability cache's only writer was `useLocalStructuredSessionTabsSync`,
gated on workspaceSessionReady + terminalStartupRestorationReady + the
experimental flag. Every `resolveAgentLaunchRoute` reader treats an
unanswered cache as "unsupported", so the answer arriving seconds late is
what produces the bare-terminal create in #19154 — awaiting the probe at a
route decision guards four call sites but leaves the window open for the
three readers that are synchronous and cannot await.
Start the probe from the renderer boot chain, ungated, so the answer is
cached before any launch route is resolved. The per-call-site awaits stay
as the backstop for the residual window and for re-probing after a failed
probe.
Also: hoist the full-creation probe above its cancel gate so the gate stays
adjacent to createWorktree; pin the retry-after-failure, concurrent-ensure
and missing-bridge contracts; drop a stale microtask tick and correct two
comments that no longer described the code.
* test(native-chat): pin the cancel gate around the capability probe
The probe added an await to two composer creation paths. Full creation had
no gate between the route decision and createWorktree, so the earlier
revision opened a window where a dismissed composer still created a
worktree; the hoist that closed it was unpinned. Quick creation already
gated immediately before runBackgroundWorktreeCreation, so its inline
await is safe — pin that too, since nothing asserted it.
Both tests fail against origin/main (no probe) and the full-creation one
fails against the pre-hoist revision.
* fix(native-chat): close the folder-create cancel window the probe opened
The probe added the first `await` inside `submitFolderWorkspaceCreate`. On
`main` that function ran straight through to `createFolderWorkspace` with no
suspension of its own, so its caller's `isSubmissionCancelled()` gate and the
create call sat in the same turn. With the probe inline, a composer dismissed
while the probe is in flight still creates the folder workspace and launches
an agent — the same defect the full-creation hoist fixed on the git path.
Resolve capabilities in `folder-submit-orchestration` above its existing gate
and hand them down, so the create path's prefix is synchronous again. The
parameter stays optional: a caller without a cancel gate keeps the probe.
Both new tests fail against `origin/main` and against this branch's previous
head; the cancel-window one still fails with its probe-pending assertion
removed, so it pins the create, not just the probe.
* refactor(native-chat): require pre-resolved capabilities on the folder create path
The cancel-window fix in
|
||
|
|
ca2356c194 |
feat(native-chat): decide a restart-stranded send against provider history (#20139)
* feat(native-chat): decide a restart-stranded send against provider history `markPendingSubmissionsUnknown` flips every surviving `pending` submission to `unknown` on attach and stops there. The module written to finish the job describes the intended two-step in its own header -- "Every surviving `pending` becomes `unknown` and is then matched against provider history" -- and only the first step ever shipped. `reconcileSubmissions` has been imported by exactly one test file and nothing else. So a message stranded by a dead child or a host restart had no recourse but retyping: Retry correctly refuses to redeliver something that may already be with the model, the outbox entry drops, and a transient error line is all that remains. This wires the second step, so those are decided on evidence instead of refused. Caller placement is the design decision, because where it runs determines what a consistent history boundary can mean. It runs in `attachJournal`, immediately after the sweep: attach happens after the record store's CAS hands this host the lease and before a provider child starts, so nothing can append to provider history while it is read, and the window stays valid until the resume consumes it. The three other settlement sites can all be overtaken by a newly started child before the read is acted on. The history source is the Claude project JSONL for the handle chain's provider session id -- definitionally what a resume replays, which is what makes absence meaningful. Boundary consistency reuses `proveClaudeTranscriptBranchFromJsonl` rather than inventing a check: a fork, a compacted log and a truncated tail each already throw there, and each maps onto `boundaryConsistent: false`. A null leaf uuid is also false, because there is no anchor to prove a start from. Two guards were needed that the reconciler cannot enforce itself, because Claude echoes no client message id and only the fingerprint pass can fire: - A transcript records a pasted image as base64, and the block decoder drops it silently for want of a url or path. Such a record would enter the window advertising a text-only fingerprint, where an unrelated text-only submission with identical text could claim it. The window now inspects raw content parts before decoding and excludes any record a part would be dropped from. - A submission carrying an image-ref path can never match a transcript that keeps only base64. Without a guard it matches nothing by construction rather than by absence and falls straight through to `not_delivered`, and a Retry would then redeliver an image already sent. Only text-only bodies are handed to the reconciler. Both guards fail a named test when removed. Limits, stated rather than implied. The exact-match tier needs the provider to echo our id, which Codex does and Claude does not, so Claude resolves by fingerprint alone -- and two identical prompts deliberately reach `ambiguous_match` instead of guessing. Repeated one-word prompts therefore stay unknown by construction. This decides what it can prove and refuses the rest, which is the intended contract, not a shortfall in the wiring. Found while doing this and not fixed here: the block decoder silently dropping base64 images has a blast radius beyond reconciliation and deserves its own change. * fix(native-chat): harden restart history reconciliation * fix(native-chat): keep Claude adapter within lint budget --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
8999a00281 |
refactor(native-chat): give each structured dispatch state exactly one meaning (#20133)
* refactor(native-chat): give each structured dispatch state exactly one meaning
`unknown` meant five different things. Only one of them was genuine
ambiguity.
A transport write that the provider's input pump never took is provably
undelivered -- which is what `rejected` already means. It was recorded as
`unknown` anyway, and a one-entry allowlist then existed solely to teach
Retry that this particular `unknown` was safe to re-deliver.
Collapsing that case into `rejected` deletes the allowlist and turns a
predicate into an invariant: Retry never re-delivers an `unknown`, with no
exception to reason about. The four states now each assert one thing --
`pending` written and awaiting, `accepted` the provider has it, `rejected`
provably did not happen, `unknown` genuinely cannot tell.
A fail-closed guard is the right default here because the asymmetry is
severe: refusing a legitimate retry costs the user a retype, while allowing
an illegitimate one sends the model a second copy of their message.
Also fixed, found while auditing every reader of `rejected`:
- The renderer printed `submission.reason` verbatim, so a broken pipe put
the internal token `provider_write_failed: broken pipe` on screen in
destructive red. The journal reason is unchanged -- it is the durable
evidence and the transport-versus-content discriminator -- but the screen
now gets copy that names the cause and says the message is safe to
resend. Content rejections still show the provider's own words.
- The fallback copy "Message was not accepted" read as a content refusal.
A null reason now yields "Message was not sent.", which asserts only what
every rejection shares.
- A refused worker-start preamble threw a plain Error out of the dispatch
path. It now throws `OrchestrationError('dispatch_preamble_undelivered')`
so a coordinator can tell "we could not send it" from "we sent it and
something else broke" without parsing prose. Retain/discard behaviour is
unchanged; only the verdict's legibility improves.
Two behaviours improve as a consequence rather than by design: a provably
undelivered message no longer blocks conversation commands, and no longer
leaves the session reading as "working" in chat and in every session list.
Not addressed here, and named rather than implied: a message left `unknown`
by a dead child or a host restart still has no recourse but retyping. The
restart reconciler that would decide those on evidence is written and has
never had a production caller. Parking the refused entry instead would
reintroduce the head-of-queue wedge removed in #19863, so it is not an
option.
Note for whoever edits `journal-reducer.ts` next: it sits at 297 of its 300
counted lines. The next statement added there needs a split, not a shave.
* fix(native-chat): close two gaps review found in the rejection taxonomy
Both are narrow and both were real.
A journal written before a refused write became `rejected` still holds that
submission as `unknown` with the transport marker. The predicate this change
replaced excluded exactly that shape from provider-echo matching; the
state-only check that replaced it does not, so on replay such a row could
claim the echo of a later, genuinely delivered send of the same text and
attach the delivery to the wrong message. Fail-closed still prevented any
re-delivery, so nothing duplicated — but the wrong submission was credited.
Replay now excludes the legacy shape too.
And the content-versus-transport split had a third case neither side covers:
a local capacity refusal is neither the provider explaining itself nor a
frame that failed to leave. It fell through to the verbatim branch, so
`claude structured dispatch queue is full` reached the screen — the same
class of leak this change set out to fix, one reason short of being caught.
Internal reasons now get copy; only a provider's own words are shown as
written.
Each is pinned by a test that fails with its guard reverted and passes with
it restored.
* fix(native-chat): preserve dispatch refusal across clients
* fix(native-chat): rotate immediately rejected retries
* docs(native-chat): correct rejection taxonomy reference
* docs(native-chat): align mobile retry comment
* docs(native-chat): clarify unknown replay semantics
* fix(native-chat): keep a mobile send's operation id when delivery is unknown
Mobile released the retained operation id whenever a send came back
`unknown`, so the user's next send of the same text went out under a fresh
id. A fresh id has no ledger row, so the host treats it as a first delivery
and dispatches it -- even though `unknown` is the one answer that says the
provider may already have the message. That is the duplicate this branch
exists to remove, reintroduced on the client that has no outbox.
Which case that was matters. Mobile only ever sees `unknown` from ack-loss
(`isRpcDeliveryUnknown`: "the host may have processed it and only the ack
was lost"), because the mapper reported every `ok` result as `accepted`
without reading `dispatchState`. So the rotation fired exclusively where
delivery was ambiguous and never where it was provably refused, which is
the inverse of the rule this branch establishes.
Retaining the id is what makes a retry safe, and it costs no liveness:
`performSend` answers a second request under a recorded id from the journal
and never puts it back on the wire, so a reused id delivers when nothing
landed and replays when something did. Rotating can only ever add a second
copy. The retention stays bounded by the host's admission window, which
`retainStructuredSessionOperationId` already enforces.
`retryUnknown` goes with it: the host ignores it for delivery, and all it
does is skip the cached answer to re-read the same row.
Keeping the id exposes what the rotation was hiding, so fix that too: a
replayed `unknown` comes back `ok`, and mobile called it `accepted` and
cleared the composer as if the message had landed. `dispatchState` now
decides, in one pure function:
accepted/pending sent, and the id is spent
rejected provably did not happen and terminal in the reducer, so
reusing the id could only replay that rejection: spent,
and the next attempt is a first delivery under a new id
unknown keeps its id
Reading `dispatchState` at all is a pre-existing defect, fixed here because
the false "sent" cannot be removed without it, and scoped to the send path.
`mutate`'s rotation for prompt/option/cancel plans is untouched. The
rejection copy is the desktop's notice, so an internal reason
(`provider_write_failed: ...`) still never reaches a person.
Tests: the hook test that was flipped to assert a rotated id now pins the
opposite -- one id across an ack-loss and two `unknown` replays, each
reported `unknown` rather than `accepted`. The send fixture grew the durable
submission row a real host returns; without it every send test asserted
against a shape that cannot express the bug.
* fix(native-chat): enforce fail-closed structured send replay
* fix(native-chat): align retry and mobile RPC contracts
* fix(native-chat): keep transient admissions retryable
* test(tab-bar): expand nested create menu in harness
---------
Co-authored-by: Merge Sim <sim@local>
|
||
|
|
c6548b98f4 |
test(scripts): widen the Windows shim ratchet to catch package bin spawns (#20285)
* Widen Windows shim ratchet to detect package bin spawns Follow local program expressions into node_modules/.bin while preserving the existing literal check, roots, and allow-list. Document static-analysis limits and cover unsafe and resolver-based invocations. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(scripts): fold dot segments before matching node_modules/.bin The predicate joins call arguments textually, so a literal '..' segment hid a path that resolves into node_modules/.bin at runtime. Folds '.' and '..' (and Windows separators) first. A '..' that genuinely escapes .bin still does not match. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * style(scripts): use .at(-1) in the dot-segment fold oxlint's prefer-at rule; the repo-wide lint gate is an error, not a warning. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
bc5e67606f |
test(rpc): add a compile-time params catalog parity gate (#20281)
* Add compile-time RPC params catalog parity gate Check each registered handler against its catalog params type in both directions, with explicit exceptions for the three uncatalogued schemas. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(rpc): keep the params generator off its own output The parity gate imports the generated catalog for types, and it lives under RPC_DIR, which indexableModules() scans for shared imports. That re-added OUTPUT_PATH after line 46 removed it, so the generator bundled and require()d the committed catalog. A catalog referencing a renamed or deleted shared export then crashed regeneration — in exactly the state that requires regenerating. Reproduced before and after: with a dangling reference injected into the catalog, `generate:rpc-params-catalog` threw; it now rewrites the file. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
fdf16fff70 |
fix(sidebar): show agent activity before workspace activation (#20398)
* fix(sidebar): observe agent titles before workspace activation Reuse parked terminal watchers for eligible live tabs in never-mounted workspaces, with initial title catch-up and capability-driven admission. Preserve existing watcher cleanup and avoid allocating watcher sets for empty workspaces. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix: reconcile background watchers when remote coverage changes Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test: type-check terminal watcher fixtures without casts Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
a1d135e233 | fix(ai-vault): re-read a transcript rewritten to its previous size (#20261) | ||
|
|
131d5ab07e |
fix(mobile): reuse current workspace on notification taps (#20310)
* fix(mobile): reuse the current workspace on notification taps * revert(mobile): restore notification setting hint |
||
|
|
fe4237cd41 |
fix(agent-hooks): let the provider, not a keystroke, end these turns (#20149)
Escape is ambiguous at the source for Claude, OMP, Pi and Prime Agent: the same key closes an overlay and cancels a turn, and which one it meant is focus state only the TUI holds. Nothing downstream can recover it, so for these agents a plain Escape is never evidence a turn ended — the provider's own hook decides. Ctrl+C is untouched, and no other agent type changes. The renderer skips the round-trip and main re-checks the same rule, so a stale or direct inference request cannot route around it. A navigation Escape does not clear a Ctrl+C already waiting to settle: Escape is not a retraction. Fixes #13547 Fixes #9208 Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Co-authored-by: Rod Boev <rod.boev@gmail.com> |
||
|
|
9a12ccd19d | Update README downloads badge | ||
|
|
e9065ddd16 |
fix(runtime): rank tui-idle evidence instead of inferring idle from silence (#20155)
`terminal wait --for tui-idle` returned satisfied in ~0s while an agent was mid-turn. The shared title detector defaults a name-only agent title to `idle` so the sidebar can clear a stale spinner, and the wait accepted that stored value as completion. Rank the evidence instead. An explicit idle marker in the agent's own title or a known ready prompt settles the wait; a fresh first-party OSC 9999 status saying working/blocked/waiting vetoes it; a name-only title is a last resort that settles only once the stream has also gone quiet. The rank is derived at read time from `lastOscTitle` rather than stamped onto the record, because `syncWindowGraph` rebuilds leaves from an explicit field list and would drop a bespoke provenance field on any renderer publish. Two things the ranking alone gets wrong are handled here too. A quiet non-shell foreground process no longer proves idle on a pane where Orca launched a known agent — that is an agent still booting, and resolving on it is what let `dispatch --inject` lose the prompt (#9976). And the idle poll re-reads the live leaf each tick, because a record captured at registration stops advancing and its frozen `lastOutputAt` makes the quiescence gate pass while the pane streams. The demotion is scoped to agents that go on to announce rest explicitly. Grok, Copilot, Aider, Mimo, agy and OpenCode emit their name and nothing more at rest: a real idle Grok pane repaints its banner about four times a second forever, so demanding quiescence from it left no settle signal at all and the wait ran to timeout. The design is Brennan Benson's, from #14642, which won a cross-review against #6012, #6555 and this branch's earlier approach; it is ported here only because that branch shares no git history with main and cannot be merged. Neil's #6555 first drew the explicit-vs-ambiguous line the ranking rests on, and Revofusion's #6012 first identified that a single title sample cannot prove completion. Fixes #6011 Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> Co-authored-by: Revofusion <syed@moonai.org> |
||
|
|
8759b25e07 |
fix(automations): isolate the scheduler tick and refuse oversized cron steps (#20152)
Two defects that change nothing about when an existing schedule fires. #16303: evaluateDueRuns awaited each row with no catch, so one unreadable schedule skipped every later due automation in that tick. Each row is isolated now; a poison record writes one folded skipped_unavailable run explaining itself and the tick continues. A renderer send that throws is closed out as dispatch_failed rather than mislabelled as an unreadable schedule. #15895: step validation only checked integer >= 1, so a step wider than its field degraded silently to a single value and still passed validation. Oversized steps are refused at input time only, bounded by the count of distinct values a field holds, so day of week rejects */8 while */7 stays legal. Runtime parsing stays lenient so rows saved before the gate keep running the cadence they have. isValidAutomationSchedule now answers only 'acceptable as new input'; a new isRunnableAutomationSchedule answers 'can Orca still run this', and the editor uses it so a legacy row opens intact and can be renamed without re-authoring a schedule that is still firing. Verified: 34/34 corpus expressions fire identically to main. Fixes #16303 Fixes #15895 |
||
|
|
599e669375 |
fix(skills): evict removed runtime discovery cache (#11489)
* fix(skills): evict removed runtime discovery cache * fix(skills): retire removed runtime cache entries using pending scan identity * fix: rescan mounted skill consumers when a runtime re-pairs under the same id - Fold the pairing revision into useActiveSkillDiscoveryRuntimeTarget's selector so a same-id re-pair yields a new runtime target and every mounted useInstalledAgentSkillNames effect re-runs instead of holding the retired peer's installed list after the module cache is evicted. - Reset hook-local result/loading state on runtime target identity change, which also bumps the refresh generation so an in-flight scan issued to the retired peer can no longer commit its result into React state. - Add mounted-hook regression tests covering the re-pair rescan and the in-flight stale-scan fence. * fix(skills): reset discovery state via render-adjusted state, not a ref write React Doctor flagged the render-phase write to stateResetInputRef. React can discard a render after the write, in which case the next render sees "already reset" and keeps painting the previous target's skill list until a rescan. Track the reset inputs in useState and adjust it during render instead, which React replays safely. Also drop the `as never` / `as GlobalSettings` casts from the tests this PR added, since main now enforces consistent-type-assertions on changed lines. --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Neil <neil@stably.ai> |
||
|
|
6c1d95b0da |
perf(tooling): reuse directory entry types in source scans (#20212)
* perf(tooling): reuse directory entry types in source scans
* fix(source-scan): stat DT_UNKNOWN dirents so untyped directories are still walked
`readdirSync(..., { withFileTypes: true })` can hand back a Dirent whose
type the filesystem did not report. For that entry every predicate is
false, so the readdir-type fast path treated a real directory as a file
and silently dropped its subtree from every ratchet guard. Fall back to
`statSync` whenever the entry is neither conclusively a file nor a
directory, keeping the no-stat fast path for ordinary entries.
Also make the two readdir-order assertions in the walk test
order-independent; `scanSourceTree` returns raw readdir order, which
differs on tmpfs.
* test(source-scan): unit-test the stat fallback via an extracted helper
The fabricated-Dirent readdir mock could not satisfy both gates at once:
vi.mocked(readdirSync) resolves to Node's Dirent<NonSharedBuffer> overload, so
the mock needed a type assertion, and #19462's casting gate rejects new ones on
changed lines. Removing the cast then failed tsc.
Extract directoryEntryNeedsStat and test it directly with a structural probe.
No mock, no cast, no top-level await, and the DT_UNKNOWN case is pinned:
removing the fallback fails 'stats an entry whose type readdir could not report'.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
|