mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
* feat(web-shell): show a Channel event timeline Channels reported what they did only through stderr, the publication audit, and the inbound task store, none of which the daemon could read. An operator wondering why a GitHub comment got no reply had to read the daemon log. Every Channel now appends one bounded journal per instance: the inbound messages it accepted, the ones it skipped and the machine reason for each skip, publication outcomes, inbound task transitions, and poll results. The journal holds routing decisions and identifiers only, never message text; it rotates at 1 MiB to a single backup, and a write failure is swallowed into one stderr line so diagnostics cannot break message handling. The daemon serves the journal through GET /workspace/channels/events and its workspace-qualified form, with limit, type, channel, and since filters, a 32-file and 2 MiB read bound, and the existing trust and client checks. The channel parameter repeats once per instance name because a Channel name may legally contain a comma. The Web Shell gains a Channel events page and sidebar entry that render the cross-Channel timeline for the resolved workspace, filtered by Channel and event type and refreshed every five seconds, gated by channel_management. Docs: the design in English and Chinese, the serve protocol, and the Channel user guide. Channel tests isolate QWEN_HOME so journals never land in a real home directory. * fix(web-shell): scope Channel events to the selected workspace Review found four correctness gaps in the event timeline. The page read the provider's daemon-wide primary cwd, so a Channel started in a registered secondary workspace could never be inspected. It now offers the same workspace selection as the management page and passes the selected cwd to both hooks; changing the workspace resets the Channel filter. The poll loop captured the adapter's detail before the poll ran, so the first success had no counters and every later one carried the previous poll's counters against the current timestamp. The field is now cleared before the poll and consumed after it settles, on both the success and failure paths. The 32-file read cap was applied before the Channel filter, which made an explicitly requested, quieter Channel unreachable once busier peers had newer files. Candidate files are now narrowed by the requested identities first. Channel identities were truncated to 80 code points while the management API accepts 255 UTF-8 bytes, so longer legal names never matched their own filter and names sharing a prefix became indistinguishable. The identity now keeps its full legal length; only the UI shortens it for display. Each fix has a regression test that was mutation-checked against the previous behavior. Design (both languages) and the Channel user guide now describe the workspace selection, the poll-detail timing, the filter-before-cap ordering, and the identity limit. * fix(web-shell): mask Channel events cached for another query The workspace selector changes the query without remounting the hook, and the underlying resource keeps its previous data while a request is in flight or after it fails. Selecting a second workspace therefore showed the first workspace's rows under the new selector until the new request succeeded, and kept them when it failed — including their Channel names in the filter list. Each snapshot is now tagged with the query that produced it (workspace, Channel, types, limit) and mismatched data is masked, the same way the Channel management hook already does. Two hook-level regressions drive the real hook and resource with a pending and a rejected second-workspace request; both fail without the tag. * fix(web-shell): keep Channel event reads off the daemon's critical path A verification round measured the journal read at the documented cap: one page poll took ~860 ms of synchronous I/O, an unrelated /health request waited ~790 ms behind it, and four concurrent reads serialized to 3.4 s. The route runs on the daemon that also serves sessions, so a diagnostic page must not be able to stall it. The read is now asynchronous and chunked (64 KiB awaits) under tighter bounds: 256 KiB per journal file, 4 MiB per request, 32 files, freshest first. On the same 32-file / 32 MiB fixture the read is ~175 ms, a bystander /health stays at 6 ms p50 against a 2 ms baseline, and four concurrent reads finish in ~630 ms. Two reporting gaps go with it. Anything the bounds clip — dropped files or an exhausted byte budget — now sets `truncated`, so the page cannot show a timeline that silently omits whole Channels. And a scope directory that cannot be read propagates instead of returning an empty list, so the route answers 5xx rather than claiming the workspace's Channels did nothing; a missing directory still means "nothing written yet". The page also stops polling while its tab is hidden and reloads when it becomes visible, so a forgotten tab no longer reads journals every 5 s. Tests pin the two behaviors the wire depended on but the suite did not: reading events that survive only in the rotated file, and clamping a limit above the maximum. Both new mutations are killed, as are the truncation, unreadable-directory, and hidden-tab guards; a bystander-timer test pins that the read yields to the event loop. * fix(web-shell): report a clipped journal tail as truncated The per-file read window introduced with the asynchronous read is 256 KiB, but a journal grows to 1 MiB before it rotates. `readTail` skipped the head of a taller file without telling anyone, so a single busy Channel lost its own older events while the response still claimed `truncated: false` — the exact completeness contract the previous commit set out to fix. A 1 MiB journal of 1020 records answered 255 events with `truncated: false` at `limit=500`. `readTail` now reports whether it started mid-file and the reader folds that into `truncated`. The request budget is also spent in whole tail windows instead of a final partial one, so its flag keeps the precise meaning "whole journals were left out". Three tests close the gaps this exposed: a journal taller than the tail window must report truncation even when the returned rows fit the limit; the budget fixture now uses twenty full-window journals whose combined rows stay under the record limit, so only the budget can explain the flag; and a journal file that cannot be read must fail loudly, like the directory case already does. All four previously surviving mutants — tail flag, budget flag, budget debit, swallowed file-read errors — are now killed. * fix(web-shell): bound Channel event read retention The events read parsed and retained every matching record across all journals before applying `limit`, so an all-Channels poll on a busy workspace held far more than the rows it returns. Trim the accumulator to the newest `limit` once it passes four times that. A dropped record is older than every kept one and the sort is stable, so the answer does not change: a 60-scenario differential run against the previous behaviour produced byte-identical output. Trimming counts as hitting the record limit, so it sets `truncated` too. Closes the remaining non-blocking review items as well. The provider workspace action declares the full query type it already forwards, so the interface no longer suggests that path cannot filter by Channel. The new UI strings use the established 频道 label rather than introducing a second word for Channel beside the existing sidebar entry; the design docs keep 渠道, which is what every other zh-CN design uses. The docs now state the page's own request size (200) apart from the route default (100), and the `truncated` comments describe every reason it can be set instead of only the record limit. The NUL query-key separator is documented as collision-free: instance names reject control characters, journal names are sanitized, and a path cannot contain NUL. * fix(channels): read a journal that fits the budget and keep aligned records Reserving a full 256 KiB window per journal left a small journal unread when the remainder was under one window, even when it covered that journal many times over — coverage lost in exchange for a flag that was already truthful. Reserve what the journal actually needs instead, using the size the enumeration already stats, so the budget flag still means "whole journals were left out" and a 200-byte journal is no longer skipped beside seventeen full ones. A tail window that landed exactly on a line boundary also dropped one complete record, because the first line was discarded whenever the window started mid-file. Read one leading byte and drop that line only when the byte is not a newline. Folding the byte into the first chunk keeps the read to one awaited call per chunk, which is what lets the daemon serve other requests between them — now pinned by a test that compares the event-loop turns a tall read takes against a short one over the same six journals. A synchronous chunk read fails it. Also records why an unparseable instant cannot satisfy `since` while an unfiltered read still returns it, and pins both halves. * fix(channels): collapse repeated reason-filter skips into a counted row A real end-to-end run — the actual GithubAdapter over HTTP against a local GitHub-compatible API, a real agent turn, the page in a browser — exposed an amplification the seeded-journal runs could not. The poll cursor does not advance while an inbound task is recoverable, so a task that failed and is still retryable makes the provider return the same notifications on every poll, and filtered notifications are skipped before any dedupe. Each poll therefore re-journaled the whole batch: with a 2 s poll and 300 filtered notifications the journal rotated and pushed the accepted/posted rows out of the page's 200-row window. Journal at most 20 reason_filter rows per poll and collapse the remainder into one row carrying detail.filtered, which the page renders as a count in both languages. The budget is per poll, so a genuinely new batch is still described in full. Measured against the real adapter: 15 polls served 4,500 notifications and wrote 315 rows instead of 4,500, without rotating. Against a stub that honours both `since` and GitHub's read-state semantics, the redelivery stops once the batch is marked read and a new batch gets a fresh budget. The design docs record the mechanism precisely, including that an in-flight turn cannot cause it because the poll loop awaits pollOnce. * fix(web-shell): keep one Channel event polling loop across visibility restores Restoring tab visibility while a journal request was still pending started a second run. Clearing the timer handle cannot cancel an in-flight run, so both runs settled, both called schedule(), and only the last timer handle was kept — the orphan kept polling forever. Ordinary tab switches on a slow response therefore multiplied journal reads permanently: one overlapping hide/show cycle left two requests per interval, three cycles left four. Guard the cycle with an in-flight flag so a restore during a pending request lets that request finish and reschedule instead of starting a second loop. Both read paths are time-bounded (the SDK's 30 s fetch default and the action timeout), so the guard cannot stall polling. The regression test holds every request pending, overlaps three hide/show cycles, and asserts one request per interval afterwards; removing the guard reproduces the reported four calls.