Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
5ac3a11
docs(plan): OpenCode agents start under load (#1355)
Juliusolsson05 Sep 27, 2026
ce6482d
fix(opencode): wait out a slow but healthy serve start instead of kil…
Juliusolsson05 Sep 27, 2026
9751817
Merge remote-tracking branch 'origin/main' into fix/opencode-serve-st…
Juliusolsson05 Sep 27, 2026
8f0bdbe
Merge remote-tracking branch 'origin/main' into fix/opencode-serve-st…
Juliusolsson05 Sep 27, 2026
bdfa24b
test(opencode): pin the package's startup-wait forwarding and the con…
Juliusolsson05 Sep 27, 2026
36f270c
Merge remote-tracking branch 'origin/main' into fix/opencode-serve-st…
Juliusolsson05 Sep 27, 2026
38ef620
test(opencode): the slow-serve tests wait as long as the app does
Juliusolsson05 Sep 27, 2026
8f7a5ec
test(opencode): pin the startup wait's ceiling and the rejected-start…
Juliusolsson05 Sep 27, 2026
8e7f949
Merge remote-tracking branch 'origin/main' into fix/opencode-serve-st…
Juliusolsson05 Sep 27, 2026
ccedbaf
Merge remote-tracking branch 'origin/main' into fix/opencode-serve-st…
Juliusolsson05 Sep 27, 2026
90f2a4d
fix(feed-debug): a forget that races a queued append leaves no per-se…
Juliusolsson05 Sep 27, 2026
e886842
fix(feed-debug): cap remembered sessions so late appends after forget…
Juliusolsson05 Sep 27, 2026
1cb7de0
fix(codex): keep wrapped exec_command results in resumed history (#1321)
Juliusolsson05 Sep 27, 2026
49b7f5b
fix(feed-debug): release a session's state when its runtime is gone, …
Juliusolsson05 Sep 27, 2026
c053f52
fix(codex): running exec chunks are not successes; keep one terminal …
Juliusolsson05 Sep 27, 2026
bd76b4b
fix(feed-debug): write through a forget during the first size check; …
Juliusolsson05 Sep 27, 2026
84ef2f1
fix(feed-debug): keep a capped file's drop count across a rebuilt cap…
Juliusolsson05 Sep 27, 2026
99ac38a
fix(feed-debug): persist a capped session's unmarked drops before for…
Juliusolsson05 Sep 27, 2026
d14fb42
fix(feed-debug): harden the tombstone tail reader (#1392 review a rou…
Juliusolsson05 Sep 27, 2026
63553e5
fix(codex): unparsed wrappers are unknown; the fuller wrapper wins ov…
Juliusolsson05 Sep 27, 2026
0c435f0
fix(conversations): read Codex 0.157 UserMessage items when the index…
Juliusolsson05 Sep 27, 2026
d419803
test(setup): install a working DOM Storage when Node 25's placeholder…
Juliusolsson05 Sep 27, 2026
25b1a2f
build: list the renderer storage setup in the web tsconfig (#1212)
Juliusolsson05 Sep 27, 2026
02d6f4e
Merge origin/main into #1392 (conflict: useFeedDebugPersist tests; ke…
Juliusolsson05 Sep 27, 2026
b2f2238
fix(conversations): merge both prompt carriers, tail activity, image-…
Juliusolsson05 Sep 27, 2026
da06c8c
docs(plan): AI Workspace write whose status save fails (#1285)
Juliusolsson05 Sep 27, 2026
57ffbb6
docs(commands): plan reporting a refused clipboard write (#1250 row 9)
Juliusolsson05 Sep 27, 2026
ba94fef
test(ai-workspace): a write whose status save fails must report the w…
Juliusolsson05 Sep 27, 2026
462a2e2
fix(ai-workspace): a write whose status save fails is reported as don…
Juliusolsson05 Sep 27, 2026
589d42a
fix(ai-workspace): show the storage warning, guard change emits, caus…
Juliusolsson05 Sep 27, 2026
690e634
fix(commands): copy commands say when the clipboard refused (#1250 ro…
Juliusolsson05 Sep 27, 2026
a1d5b8b
fix(feed-debug): keep release bookkeeping across persistence toggles …
Juliusolsson05 Sep 27, 2026
47b7fb3
test(setup): replace any storage that is not happy-dom's, including N…
Juliusolsson05 Sep 27, 2026
d8f1eae
fix(ai-workspace): report blocked storage on every load, correct ENOT…
Juliusolsson05 Sep 27, 2026
d3f1a6b
fix(clipboard): Browser Pocket and the pickers say a refused copy; on…
Juliusolsson05 Sep 27, 2026
bb18309
fix(conversations): backward chunked scan for newest prompt, pair car…
Juliusolsson05 Sep 27, 2026
456e6e1
fix(conversations): pair carriers by records and time; carry a chunk-…
Juliusolsson05 Sep 27, 2026
b781ed5
docs(composer): plan saying an image paste the agent cannot take (#12…
Juliusolsson05 Sep 27, 2026
d584eb6
fix(conversations): pair carriers only with the immediately previous …
Juliusolsson05 Sep 27, 2026
2bb9d19
docs/test: quote the refusal as the code writes it; clearer test titl…
Juliusolsson05 Sep 27, 2026
a64baf9
fix(composer): say when a pasted image cannot go to this agent (#1250…
Juliusolsson05 Sep 27, 2026
3d5b768
fix(composer): say a dropped image for file+text, HTML-only and async…
Juliusolsson05 Sep 27, 2026
a4a0f19
fix(feed-debug): a release sent while persistence is off writes no dr…
Juliusolsson05 Sep 27, 2026
5b03070
test(composer): pin non-image items, null clipboard and the Claude ta…
Juliusolsson05 Sep 27, 2026
c32d4ba
fix(composer): paste-to-focus hands an empty paste to the image handl…
Juliusolsson05 Sep 27, 2026
5ea05e1
fix(ai-workspace): keep reporting an unsaved status after a failed st…
Juliusolsson05 Sep 27, 2026
0c696af
Merge #1367 into batch J
Juliusolsson05 Sep 27, 2026
37d9cb1
Merge #1392 into batch J
Juliusolsson05 Sep 27, 2026
e246623
Merge #1395 into batch J
Juliusolsson05 Sep 27, 2026
97be31b
Merge #1407 into batch J
Juliusolsson05 Sep 27, 2026
81ce5bb
Merge #1408 into batch J
Juliusolsson05 Sep 27, 2026
3c30c11
Merge #1416 into batch J
Juliusolsson05 Sep 27, 2026
94c6a29
Merge #1421 into batch J
Juliusolsson05 Sep 27, 2026
9895a9e
Merge #1426 into batch J
Juliusolsson05 Sep 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions docs/plans/2026-09-27-ai-workspace-write-status-warning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# An AI Workspace write whose status save fails is reported as done (#1285)

## Problem
`AiWorkspaceRegistry.writeFile` first replaces the user's file on disk (`atomicWriteTextFile`), then refreshes the entry status. The refresh saves registry state, and a save must first make any preservation copy it owes (#1260: rows the load could not read are copied aside before a save drops them).
- **When the copy is blocked** (for example, a directory sits on `ai-workspaces.json.invalid-<digest>.json`), the save throws. `writeFile` then returned `{ ok: false, error: "is a directory" }` although the file was written.
- **What that caused.** An agent or the editor told "failed" retries (converging through the version conflict) or reports a failure that did not happen.
- **Cosmetic.** Refused saves relayed the raw errno text, with no hint that clearing the copy path unblocks them.

## Evidence
- **The sequence** was reproduced and confirmed by #1260 review B (round 2), and recorded as residual #1285 (steering q24). The owed-copy state itself comes from the real registry file: `testing/fixtures/ai-workspace/real-workspaces-2026-09-25.json`, two real workspaces from the owner's `ai-workspaces.json`. The existing #1260 tests make that state owe a copy by marking one real workspace malformed, and block the copy with a directory.
- **The only consumer** of the write result is the renderer editor over IPC (`src/main/ipc/aiWorkspace.ts` → `AiWorkspaceEditor.tsx`). There is no MCP tool on this path.

## Decisions (defaults)
1. **A failed status refresh after a successful write does not fail the write.** It returns `ok: true` with an optional `warning` on the success branch of `AiWorkspaceWriteFileResult`, documented as "the file IS written; do not retry". It logs the error, and still emits `file-written` to every workspace holding the file.
2. **The owed-copy invariant is unchanged.** The state file is still never saved while a copy is owed.
3. **A refused save explains itself.** `preserveOwedCopy` rewrites a copy failure: "AI Workspace storage needs attention: N unreadable row(s) must be copied aside before saving, and the copy could not be written (<code>). <advice for that code>" (the advice names the state file's folder) Only the errno code is kept, not the raw text.

## Tests (fail-first, `AiWorkspaceRegistry.test.ts`)
The input is the real recorded workspace state. One real entry is pointed at a temp file (the recorded paths are redacted), and one real workspace is made malformed so a copy is owed. The copy path is blocked with a directory.
- `writeFile` puts the new text on disk and returns `ok: true` with a status warning. The state file is unchanged. Red without the fix: `{ ok: false, error: 'is a directory' }`.
- A following `create` is refused with the actionable message.
- Mutations: dropping the warning, and misreporting the write as failed, each fail the first test.

## Review a (round 1)
- **The editor showed no warning.** It now keeps a separate `storageWarning`, set from the write result on both Save and Overwrite and cleared by the next write that has no warning. It is shown through the file list's existing alert (`error ?? storageWarning`). It cannot ride `error`, because `loadWorkspace` clears that after every save.
- **The warning is fixed text.** `AI_WORKSPACE_STATUS_NOT_SAVED` is main's own sentence; the raw cause goes only to the log.
- **A throwing `changed` listener no longer fails a landed write.** Each emit is guarded, so every other workspace still hears about the write.
- **Advice matches the cause.** An occupied copy path (EISDIR/EEXIST/ENOTDIR), a missing folder (ENOENT), a permission or read-only refusal, and a full disk each get their own advice.
- **Tests added:** the file reads back after the warning; an ordinary write carries no warning; a throwing listener still gives `ok: true`, and the listener is still called; ENOENT advice. The always-warn, unguarded-emit and wrong-advice mutations each fail.
- **Not tested:** the editor's display of the notice is not covered at the component level (there is no AiWorkspaceEditor renderer harness). The change there is three lines, and it is stated in the body.

## Review b (round 1)
- **The notice was lost on a workspace switch, and never shown if the switch happened mid-write.** The editor held it only in React state. It is now durable in main: `get` returns a runtime-only `storageWarning` (`AI_WORKSPACE_STORAGE_BLOCKED`) while a copy is owed and its last attempt failed. The flag is set when the copy fails and cleared when it succeeds, and it is never persisted. `loadWorkspace` sets the editor's notice from it on every load, so a remount shows it again. The write-result warning still sets it immediately.
- **Wrong advice for ENOTDIR and EROFS.** ENOTDIR now gets the generic advice, since it means a component of the folder path is a file. EROFS gets its own read-only-volume advice.
- **Surviving mutations, now killed:**
- appending the raw error text to the refusal (asserted absent);
- stopping the fan-out after the first workspace (two-workspace test);
- dropping `get`'s notice.

## Review c (at 589d42a9; MERGE-READY)
Test-strength notes, not defects:
- The emit on the warning path and the fan-out are now pinned. The two-workspace listener test landed in `d8f1eae6`.
- Four of the six advice branches (EACCES/EPERM, EROFS, ENOSPC, generic) have no test. They are not reachable portably from a unit test without mocking the filesystem module.
- The quoted refusal sentence in this plan and the body is corrected to the code's wording, and a test title is fixed.

## Verification b
- **The owed copy succeeded, then the state write failed.** `copyBlocked` was already false, so `get` reported nothing, and the editor's next load cleared the notice although the status was unsaved. The registry now also tracks `lastSaveFailed`, set by any failed save step and cleared by a successful save. `get` reports `AI_WORKSPACE_STORAGE_BLOCKED` while the copy is blocked, else `AI_WORKSPACE_STATUS_NOT_SAVED` while the last save failed.
- **Test:** the reviewer's exact sequence on the real recorded state (blocked copy, unblocked, the state path turned into a directory, write, `get`, then a successful save clears it). Red on `2bb9d19c`.
39 changes: 39 additions & 0 deletions docs/plans/2026-09-27-codex-conversations-usermessage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Codex 0.157 prompts survive the conversation catalog's no-index fallback (#1363)

## Evidence
- **The fallback reader.** Without a usable `state_N.sqlite` (missing, failing schema validation, or a rollout the index does not cover), `CodexConversationSource` reads each rollout's head with `readRolloutHead`. That took prompt text ONLY from `event_msg:{type:"user_message"}`.
- **What 0.157 writes instead.** 120 recent local 0.15x rollouts all carry `event_msg:item_completed` with `item.type: 'UserMessage'`: 149 items, each `content: [{type:'text', text, text_elements}]`. None carries a legacy `user_message`. The issue's census found 233 of 233 0.157.x files without the legacy event.
- **Effect.** Every 0.157 row on the degraded path had `userTexts: []`. It was classified `empty` and hidden from the default listing, or lost its label and user activity.

## Change
`readRolloutHead` also reads `UserMessage` items: the joined text parts, with their record timestamp as user activity.
- **Why the item, not the role-user `response_item`.** Codex builds the item only for what the user sent. The injected AGENTS.md and environment context are role-user response items too, and the item leaves them out, as the index does.
- **Why a file uses one carrier.** Legacy events and items are collected apart. A file with any legacy event uses those alone, so a rollout carrying both shapes never lists its prompt twice.

## Tests (`codex.userMessage0157.test.ts`)
The fixture `testing/fixtures/conversations/codex-0157/typed-prompt-head.json` is a real 0.157.1 rollout head: `session_meta`, three role-user response items (AGENTS.md, context, the prompt) and the first typed prompt's `UserMessage` item. Text is redacted to the same length. It runs through the real `CodexConversationSource`, with no index beside it.
1. The row's `userTexts` is exactly the typed prompt, not the injected context, and user activity is set. Red on main (`[]`).
2. The same head plus a legacy `user_message` for the same prompt lists it once. Listing both carriers fails this test.

## Reviews a and b (round 1)
- **Carriers merged, not "legacy wins".** A file with both carriers can hold a prompt only the items have. Both are read in file order, and the same prompt written by both counts once: each carrier consumes a pending match from the other. These are the two carriers Codex's own index reads (`rust-v0.157.1 state/src/extract.rs`).
- **Activity from the tail.** When the 200-record head is truncated, a bounded 512 KiB tail pass takes the newest user timestamp. In 33 of 61 local 0.157.0 files a later prompt lay past the head, one 46.8 h later. `headTruncated` is set as for Claude and Pi.
- **Text and images.** Parts are joined with no separator, as Codex's `UserMessageItem::message()` does. An image-only message reads `[Image]`, Codex's preview text.
- **Comment narrowed.** Older CLIs' UserMessage items can hold injected context or command wrappers. The index lists those too, and `firstUnwrappedPrompt` and classify decide what labels a row.
- **Fixture rebuilt as a CONTIGUOUS real head.** It holds all 10 records from `session_meta` through the first UserMessage of the one local 0.157 file whose first prompt is typed. The expected length and timestamp are recorded independently in the fixture. Composed cases say they are composed.
- **Filed, not fixed here (pre-existing):** #1418 (0.149–0.151 rollouts with no prompt event) and #1419 (search matches injected context).

## Verification a (round 2)
- **The tail pass missed prompts far back.** In 10 of 46 local 0.157 files with a prompt past the head, that prompt lay wholly before the last 512 KiB, and a record straddling the window's start was dropped. The reader now scans BACKWARD in 512 KiB chunks, carrying each cut line into the next (earlier) chunk, until it finds a user record. It is bounded at 32 MiB, runs only for truncated heads, and is cached by mtime.
- **De-duplication collapsed a prompt repeated in a later turn.** A cross-carrier pair is now matched only within 4 records, since Codex writes the two carriers of one prompt back to back. No local file has both carriers, so there was no recorded distance to calibrate against.
- **Fixture label.** The fixture's `session_meta.cli_version` is 0.157.0; the label is corrected.
- **Tests (red on the previous commit):** a prompt followed by 4 MiB of output; a prompt whose line straddles the last chunk boundary, laid out deterministically; a prompt repeated in a later turn. Dropping the carried partial line fails the straddle test.

## Verification a (round 3)
- **The exact reported repeat (two records on, 48 h later) still paired.** The pair window is now records AND time: at most 4 records apart and at most 5 s apart. One prompt's two carriers share its instant.
- **A user line longer than two read chunks was lost.** A window with no newline is all one line, so all of it is carried to the next, earlier chunk, and nothing is parsed or dropped.
- **One of 452 files has its newest prompt 55.8 MB from the end, past the 32 MiB bound. Kept by design.** This is the degraded no-index path, rollouts reach gigabytes, and an unbounded scan per discovery is exactly the store-sized cost the head limit prevents. The WHY comment says so.

## Verification b (at bb183091)
- **A pair crossed an intervening prompt** (`repeat`, `different`, `repeat`, seconds apart). Fixed: a carrier pairs only with the IMMEDIATELY previous user record, which must be the other carrier, within 4 records and 5 s. Test: `start, repeat, different, repeat` keeps all four. It is red on `456e6e1b`.
- **The 32 MiB residual** (the same one file) is the deliberate bound documented at `456e6e1b`.
34 changes: 34 additions & 0 deletions docs/plans/2026-09-27-codex-exec-wrapped-output.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Codex exec_command results survive in resumed history (#1321)

## Evidence
- **Census of 2,541 local rollouts.** It found 85,355 `function_call_output` lines whose output is wrapped: `Chunk ID:`, `Wall time:`, `Process exited with code N`, `Original token count:`, then `Output:`. It found **0** `exec_command_end` events in any file.
- **Current Codex never writes that event.** codex-rs `rollout/src/policy.rs` puts `EventMsg::ExecCommandEnd` under "Transient, non-durable events". rust-v0.107.0 through v0.136.0 did persist it in extended-history mode (review a), and none of the local rollouts use that mode.
- **The drop rule.** `mapCodexRolloutToFeedEntries` drops every wrapped output that has an exit line (`isCodexExecWrapperOutput`), on the stated belief that "the correlated `exec_command_end` event carries the same result". That belief is false, so the dropped line was the only copy. Every `exec_command` card in resumed history showed no output and no exit status. That covers Codex through 0.144: `exec_command` wrapped outputs occur in 0.1xx through 0.144, and 0.15x uses the `exec` tool.
- **A second bug.** The predicate searched the WHOLE string for "Process exited with code". A still-running chunk whose command output contains those words was dropped too.

## Change
- **Header-only parsing.** `codexExecWrapperExitCode(output)` reads the exit code from the header only, the part before `\nOutput:\n`. It returns null for unwrapped output and for a still-running chunk.
- **Keep the result.** A wrapped, finished output now maps to a tool result with:
- the stripped output (possibly empty);
- `is_error = exitCode !== 0`;
- the same `codex` metadata the event branch produces (`kind: 'exec_command_end'`, `exitCode`, empty `parsedCmd` and `command`, null `cwd`).

So the command adapter reads it as the native transport it is: exit-proven, the output owned, and an empty success absorbed by the row dispatcher as before.
- **Comments corrected.** The stale comments that say the wrapper comes from, or is duplicated by, `exec_command_end` are fixed.

## Tests
`execWrappedOutput.test.ts` runs three real 0.132.0 pairs (exit 0 with output, exit 1, exit 0 with no output) through the real mapper and the real `fromCodexCommandOperation`, plus a still-running chunk whose body contains the exit words. All four are red on main.

## Not in scope
The 0.15x `exec` tool's `custom_tool_call_output` results are a different carrier, already handled by the code-mode envelope path.

## Review a (round 1)
- **P1: a running chunk read as a success.** A wrapper whose header says "Process running with session ID N" is a partial chunk. The command's exit arrives on a later `write_stdin` result with another call_id. The adapter's native branch claimed `exitProven: true` for every result, so the card said success. The mapper now marks such a chunk `codex.kind: 'exec_command_running'`, and the adapter does not claim a proven exit for it, so it shows `unknown`. A first attempt, "no exit code means unproven", also flipped synthetic Git-formatter test results that carry no metadata. The explicit mark keeps the change to the one shape the review found. Correlating the later `write_stdin` exit back to the card by session id is a separate feature, not done here.
- **P2: two carriers.** In extended-history rollouts, one call can have both the event and the wrapper. `createCodexTranscriptEntryMapper` keeps the first exec terminal result per call_id, remembering the last 512 per stream. A pair split across a history page boundary is not caught.
- **P3: no `Output:` marker.** Without the LF `\nOutput:\n` marker the wrapper is not parsed, so no exit claim and no stripping. None of the 85,355 local wrappers lacks it.
- **Unpinned metadata.** The exact `codex` metadata is now asserted, which kills the kind/parsedCmd/command/cwd mutations.

## Review b (round 2)
- **P2: a wrapper with no LF `Output:` marker still painted success.** The mapper returned a plain, metadata-less result, and the native adapter proves every unmarked result. Any `Chunk ID:` wrapper whose header cannot be parsed is now marked `exec_command_unparsed` and kept whole; the adapter shows `unknown` for it, as for `exec_command_running`. Test: that wrapper, through the adapter, shows `unknown` with a null exit.
- **P2: first-carrier dedupe kept the truncated event.** The persisted event's `aggregated_output` is sanitized to 10,000 bytes, and extended mode persisted it through v0.136.0, not v0.131.0. The wrapper now wins. An event that arrives after its wrapper is dropped. A wrapper that follows its event is kept, and later-wins `buildToolResultIndex` hands the card the wrapper. Codex emits the event before the function result (`ToolEventEmitter::finish`).
- **P2: page and burst boundaries bypass the per-mapper memory.** Accepted and documented. Across a boundary both results are kept and later-wins decides, which again favours the wrapper in Codex's emit order. No local rollout has both carriers.
36 changes: 36 additions & 0 deletions docs/plans/2026-09-27-copy-last-response-failure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Copy commands say when the clipboard refused (#1250 row 9)

Short plan: a bug with a known root cause. The row comes from `temp/quality-loop/hunt-c3.md` (row 9, P3). #1250 is a batch issue, so this PR is `Refs #1250`.

## Outcome
**Copy Last Response** shows "Copied to clipboard" only when the copy happened. When `navigator.clipboard.writeText` rejects (the document is not focused, or permission is denied), it says so in fixed words. The sibling **Copy Resume Command** failure shows fixed words, not the raw DOMException text.

## Root cause (verified in source, origin/main)
- `paneCommands.ts` `copy-last-assistant.run`: `void navigator.clipboard.writeText(text)`, then an unconditional "Copied to clipboard" toast. A rejection is an unhandled promise, and the toast lies.
- `sessionCommands.ts` `copy-resume-command.run`: catches, but renders `copy failed: ${err.message}` (raw browser text, q22).

## Design (contract)
- `copy-last-assistant.run` awaits the write. On success: "Copied to clipboard". On rejection: "Couldn't copy to the clipboard. Click into the app and try again."
- Clipboard writes need a focused document; the palette or context menu can leave focus elsewhere.
- `copy-resume-command` uses the same failure sentence. The success toast is unchanged.
- The sentence is a constant: `CLIPBOARD_WRITE_FAILED` in `features/workspace/commands/clipboardFailure.ts`.

## Tests
New `paneCommands.copy.renderer.test.ts`. The runtime entries are the recorded bundle `testing/fixtures/rendering-bundles/2026-05-20T19-11-51-193-d4a44a16.json`; the clipboard is stubbed (the one edge).
- A rejecting write shows the failure sentence, never "Copied".
- A resolving write shows "Copied to clipboard".
- The resume command's rejection shows the fixed sentence, without the raw text.
- Red on main.

## Out of scope
- #1250's other rows.
- Debug-panel copy buttons (developer surfaces).

## Review round 1 (a, b: FIX-BEFORE-MERGE; c: MERGE-READY)
- **a1 (Major): a refused copy reports `ran` to `commands.run` callers.** Declined, with reasons:
- Throwing would make `runGuarded` add its own "Command failed: Copy Last Response" toast, so every human path would get two messages for one failure.
- The `commands.run` contract already says `ran` means only that the dispatcher returned, and tells the caller to observe the app afterwards.
- **a2 (Minor): nothing pinned that the command stays pending until the write settles.** Test added with a deferred write: the command is not settled and silent before the write, then says "Copied". It fails with a fire-and-forget `.then`.
- **b1 (Major): Browser Pocket's pick (no composer) swallowed a refused write and said "Element copied".** It now says the refusal. Test in `pick.renderer.test.ts`, red on the old pick.
- **b2 / c1 (Minor): the wording was compared against the constant only.** It is now asserted literally.
- **c2 (Minor): the assistant-message picker and the code-block picker said "Clipboard write failed".** Both now use the shared sentence. The constant moved to `lib/clipboardFailure.ts`, since four features share it.
Loading
Loading