mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-09-11 19:46:21 +00:00
* feat(ipc): drop peer messages that arrive faster than a session can take, and tell the sender once The inbound gate decided whether a peer message may act. Nothing decided how fast messages may arrive, and everything downstream costs something per message: the hold buffer evicts its oldest entry per arrival, so a flood walks a user's real backlog out one message at a time; the input queue fills; and every outcome draws a receipt over an outbound ceiling shared with everything else the session sends, so the receipts most likely to be lost are the ones belonging to the legitimate messages arriving alongside the flood. Meter arrivals before policy runs. Thirty from one sender at once, then one every two seconds; sixty across all senders, then one a second. The second limit exists because a sender names itself on the frame, so rotating that name buys a fresh allowance from the first limit but not from the second. Neither is a security boundary — a hostile same-uid process has better options than flooding a socket — they bound the damage an ordinary bug does, such as two same-class sessions replying to each other automatically. Judge repeats on the body rather than the id: a model in a retry loop mints a fresh id every time, so the id guard cannot see it. Only for messages from another session. A process this session started and a controller the user minted a token for are exempt, because a hook that reports two builds is reporting two facts and a person who says "continue" twice means it twice; both are still rate limited. A dropped message is never held, never delivered, and leaves no tombstone, so a sender that waits out its burst and retries still lands. The sender learns through a new `dropped` receipt carrying `dropReason` and, when one receipt stands for several messages, `droppedMsgIds` — the first drop is answered immediately and the rest are folded into one receipt every few seconds, because a report about a flood must not scale with it. The receiving user is told at most once a minute per sender, with a count of what that line stands for. A full input queue is now a drop naming the queue instead of an expiry, which claimed a decision had run out when none was ever pending. Sending sessions mirror the receiver's bucket per address and refuse a send the receiver would drop, so the model is told to batch before the message is written rather than a round trip later, and the receiver never spends a connection on a message it was going to turn away. The mirror is never stricter than the real limit, and a `rate-limited` receipt empties it: the receiver is the authority on its own level. No hop chain here — cutting a relay loop properly needs the path carried on the frame and the current turn's inbound message known at send time, which is separate plumbing. The buckets bound a loop to one message every two seconds meanwhile, and adding the field later breaks no reader. Part of #10925 * fix(ipc): keep peer drop reports truthful at the meter, mirror, and close (#11277) - judge the repeat window on the larger of the wall and monotonic deltas, so a system suspend cannot turn an honest re-send into a duplicate - roll the admission's body record back when an admitted message could not be delivered, so a retry after queue-full is not dropped as a repeat of a message that never arrived - mirror the receiver's duplicate verdict in the send-side pacer instead of charging a token per send, and keep the burst count a refusal quotes this session's own - defer an over-budget drop receipt to the next window rather than discarding it, so a legitimate sender under someone else's flood still learns its message died - stop arrivals before draining the receipt coalescer at close and let the flush wait inside the cleanup budget, so receipts owed at shutdown reach their senders - skip the socket-binding drop tests on Windows * fix(ipc): keep the drop path honest about what it turned away, and about who Round two of review on the admission work. Six behaviour defects, each one a case where the reporting or the metering said something that was not true. **A full hold buffer no longer evicts.** It used to make room by discarding its oldest parked message and telling that message's sender it had `expired` — so an arrival destroyed a message the user had not read yet, and an uninvolved third party was told a decision had run out that nobody was ever making. A flood walked the backlog out one entry at a time, which is the first thing this feature exists to stop. The newcomer is turned away with `queue-full` instead, the shape the accept path already had: the cost falls on the sender that could not fit, it is told the truth, and it can retry. With no eviction left, `expired` means only what the design doc says it means. **The repeat record is rolled back wherever a message is settled unseen.** A body is recorded when it is admitted, but admission is not arrival: the gate can still refuse it, fail to queue it, park it into a full buffer, or be shutting down. Left behind, the record turned the sender's honest retry into a `duplicate` — a verdict whose whole premise is that the content is already at the far side. Worse on the refusing path, where the sender's "stop, nobody will see this" became "fold it into a later message". The rollback restores what the admission displaced rather than clearing the slot, so the body admitted before the failed one keeps its own protection, and the token stays spent. **The metering key no longer takes a peer at its word.** It is chosen by what the transport established and the self-asserted address is namespaced inside it. A peer could otherwise write `from: "own-process"` and share a bucket, and a transcript line, with the scripts this session itself started: it would spend their allowance, its flood would be announced to the user as coming from their own process, and the receipts would be addressed to a socket path that does not exist. The address is also bounded now, since it is retained as a map key in three tables and nothing that can be dialled comes near the cap. **An evicted receipt batch no longer keeps a live timer.** It left the table that `flush` and `dispose` iterate while still re-arming every few seconds, so under a rotating `from` the orphans accumulated without bound, each pinning a rejected message. **A waiting batch holds an address, not a frame.** It kept the whole untrusted `PeerUserFrame` for the trail, and longer while the receipt budget was spent, so a flood the meter turned away lived on in the heap of the session that turned it away. It keeps the three fields a receipt actually addresses. **A deferred receipt is bounded, and the close path forces one out.** Deferring an over-budget receipt made its age unbounded, and a sender turns a `rate-limited` receipt into a live throttle on itself — so a minutes-late one paced an innocent sender against a wall that was gone. Past that age it is abandoned. At the other end, `flush` used to dispatch nothing at all when the budget was spent, which is exactly the sender the deferral was added for. Also: `close()` no longer serializes an unbounded leg ahead of bounded ones, so the corrective receipts and the registry clear happen inside the exit budget rather than after it; a refund can no longer un-drain the mirror it raced, or erase a body record the receiver still holds; the mirror refuses a repeat rather than writing a frame it knows will be dropped; a drain corrects the level without re-anchoring the burst window or inventing a mirror for a target nothing was sent to; and a drop notice raised before the UI subscribes is replayed rather than swallowed after spending its sender's minute. The user-facing copy is corrected the same way: the trust category leads the receiving line, `rate-limited` no longer accuses the named peer of a rate it may not have set, the suppressed count is no longer called "similar" when it is a session-wide total, a `duplicate` is no longer told to re-send what was already accepted, the dedup window is read from the constant rather than restated, and the expiry sentence keeps its retry advice for the cases that are not a shutdown. * fix(ipc): keep peer admission mirrors consistent * fix(ipc): reconcile peer admission accounting * fix(ipc): align peer pacing with session state * fix(ipc): keep a late drop receipt useful, and bound the reply token where it is held Five follow-ups on top of the round-5 fixes, all in the same PR's own surface. A `rate-limited` receipt that waited out a spent receipt budget was abandoned on the receiving side once it aged past one bucket refill. That traded a stale throttle for a worse silence: the ids it named stayed `pending` on the sending side forever, and a sender told nothing cannot tell a drop from a delivery that was ignored. The receipt now always goes, and the part of it that can go stale — the throttle it implies — is judged where the answer is known: the ledger stamps when each frame was written, and a drop is decided the moment a frame arrives, so the age of the send is the age of the drop. Past one refill the ids still settle and the mirror is left alone. `parsePeerFrame` refused a whole frame whose `replyToken` was longer than the drop reporter retains. The message may be perfectly ordinary; the token only routes the receipt. The bound stays where the token is retained, and the frame is delivered. `DropReceiptCoalescer.note` accepted its arguments two ways for a caller that does not exist — the method is new in this change. The global burst's own reasoning was in neither document: it is half the 64-send outbound ceiling because every admitted message draws a receipt against that ceiling. Both docs say so now, the module header no longer claims the per-sender bucket is the binding one, and a test pins the relationship so a retune of either number cannot quietly break it. Three guards had no witness: the meter reset when a session id changes, and the repeat-record rollback at hold expiry and at a policy flip. Each new test fails when its guard is removed. * test(cli): give the settled-receipt stubs the age the ledger now reports `SettledPeerReceipt` carries `ageMs` so the drop path can tell a live rate limit from one the receiver has already refilled past. The injected stubs in the messaging tests still returned the old shape, which type-checks against a stale core build and fails against a fresh one — so the package build, which is what CI runs during install, was the first thing to see it. --------- Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> |
||
|---|---|---|
| .. | ||
| scripts | ||
| src | ||
| .gitignore | ||
| index.ts | ||
| package.json | ||
| test-setup.ts | ||
| tsconfig.json | ||
| vitest.config.ts | ||