You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
A face-to-face room's history fails with \"Maximum call stack size exceeded (gateway/internal)\" on both homes #979
Found while running #977 (the in-room approval card) on main 90f4bb5a, two isolated installs, real UI, one face-to-face room with both seats in it.
Steps
Home A opens a room with a 6-digit number; home B joins with the same number (both Bots seated, room frm_01M5Q12SJ9K380478GVN5A3B49).
A starts the room (face-start).
Either side opens the room from the sidebar.
Expected
The room's transcript renders — the "joined; available to this room only" lines, "The room is now open. The number is no longer valid.", and the room's lines.
Actual
Both sides show a failure line where the transcript should be:
Failed to load history: Maximum call stack size exceeded (gateway/internal)
and the page raises the same error twice:
pageerror: Maximum call stack size exceeded
pageerror: Maximum call stack size exceeded
Seen on both homes in the same room, on five separate page loads in one session, so it is not a one-off.
gateway/internal is an internal code shown to the user; the message tells them nothing they can act on.
Any face-to-face room opened in the UI on this tip. Not related to the approval card itself: the card renders fine (Waiting for approval / Chief (Selftest B2) → the room / Approve Reject Auto-send from now on), it is the transcript around it that fails.
Triage: real, reproduced five loads across two homes, and not a duplicate (the only earlier call stack report, #677, was the tree's heightOf cycle guard, closed and unrelated). bug|area:ui|area:social|self-test kept, agent-ready added.
Where the failing call enters
The shell prefixes Failed to load history: and the suffix (gateway/internal) is the gateway mapping any thrown error, so the code is not the diagnosis — the thrown value is a RangeError (deep recursion), not a JSON cycle (TypeError: Converting circular structure to JSON). The path:
core/src/host/channel.js:368-371 — case 'room-history' calls rt.roomHistory(...); it is in the READS set at :217.
core/src/host/room-membership.js:818-826 — rt.roomHistory → roomVisibleTo (authz) then linesVisibleTo.
core/src/host/room-membership.js:302-305 — linesVisibleTo itself is a plain filter; the recursion is in something it and its callers touch, not in this function.
Note there are two stacks to explain, not one: the gateway error is host-side, and pageerror: Maximum call stack size exceeded twice per page load is the shell recursing on its own. They may share a module or be two bugs; the report cannot tell yet.
First step is evidence, not a fix
A recursion patch written without the stack is a guess, and this file has plenty of recursion candidates (participant derivation, briefs riding into a room, room view builders). Before changing anything:
Host stack from the bench's web log (.dsh-test-<bench>/web.log on the same run) — the frame that repeats names the function.
Browser stack from the pageerror (pageerror's .stack, not just the message) — the repeating frame likewise.
Paste both in this issue. If they point at the same function, that is one fix; if not, say so here and I will split the issue rather than let one PR chase two.
One cheap discriminator while the room is still open: does the same failure happen in a local (non-face) group room? Face-only vs all rooms halves the search space and costs one reload.
Acceptance
Both homes render the room's transcript on the same room (frm_01M5Q12SJ9K380478GVN5A3B49 or a fresh one), including the join and "the room is now open" lines.
No pageerror: Maximum call stack size exceeded in either console, and no gateway/internal anywhere a person can read.
Claiming: branch dev2/fix/979-face-room-history, base main 99cbb1a1.
Plan, evidence first (Triage's step 1 is not a patch):
Reproduce on a clean 99cbb1a1 tree with a fake host around core/src/host/channel.jsroom-history + patches/ds-bot/files/src/host/..., so the RangeError is a test, not a browser run.
Paste the host stack + pageerror frames from SelfTest's .dsh-test-<bench>/web.log (asked on the single) — if both stacks point at the same function it is one fix.
Cheap discriminator: does a local (non-face) group chat also blow up, or only face-to-face rooms.
Red test: opening a face-to-face room's history throws Maximum call stack size exceeded; negative control: a 1:1 history still renders.
Root cause is in the shell, not in room-history — evidence, so #979 does not get "fixed" in the wrong layer.
1. The code in the message is not ours. FlyBot's channel maps a RangeError to bot/internal (core/src/host/channel.js:25), and core/src/client/api.js:44 prints that as "Settings → Feedback has the details." The transcript says (gateway/internal), which is @deepseek-ai/dsh-api-gateway's rpcFailure for a stream protocol violation. So rt.roomHistory is not even the call that failed.
2. The RangeError is raised in the browser. Reproduced on the bench (bench A, main install) and captured pageerror:
RangeError: Maximum call stack size exceeded
at Object.get (assets/index-CN7NIWNw.js:11:177)
at Object.get (assets/index-CN7NIWNw.js:11:445)
at AgentPresetSeatController.currentSession (dsh-client-ui-agent-preset/client.js)
at AgentPresetSeatController.apply (...)
... at Object.i [as setState] (assets/index-CN7NIWNw.js:120:80163)
The two Object.get frames are Cordis's service-proxy accessor resolution in the shell bundle; AgentPresetSeatController is @deepseek-ai/dsh-client-ui-agent-preset. It loops while the room's Session is opened (seat apply → currentSession → ctx.sessions.binding(...)/retainInfo(...) → accessor → store notify → apply), then the journal stream reports that as gateway/internal.
3. The room's data is intact. I decoded the room's Session log (concatenated zstd frames → 111 rows, 8 turns / 11 steps) and its projection cache: every row parses, no cycles, no deep nesting. A Bot Session in the same install opens fine in the same page; only the room Session fails, on both my load and SelfTest's five.
4. So what is blocked. Nothing in core/src/host or plugins/social can remove this: the failing loop is @deepseek-ai/dsh-client-ui-agent-preset + Cordis in the DSH shell while opening the room Session. A FlyBot-side fix would mean not opening rooms as shell Sessions — a product change, not a bug fix.
The root cause is in 6385365002. This comment prices the three ways out — none of them is a bug fix in this repo, so it needs the user's pick.
(a′) Ship a patched DSH package from our release tooling
The failing code is @deepseek-ai/dsh-client-ui-agent-preset plus the Cordis accessor in the shell bundle — the profile's own packages, which patches/ds-bot/ cannot reach (that series rebuilds core/ from upstream ds-bot/; AgentPresetSeatController appears 0 times in core/client.js). What is possible is the same trick one level up: the release side already composes the profile from vendor/*.tgz, so it could add a patched dsh-client-ui-agent-preset tgz.
Costs: a new DSH patch channel outside this repo; every DSH upgrade has to redo it; a broken profile has no owner to attribute it to; and the patch itself still has to be one of the three changes in (c).
This is the only way to keep rooms rendering as shell Sessions.
(b) Draw the room transcript in FlyBot (stop opening rooms as shell Sessions)
Already there: room-history (core/src/host/channel.js:368) and room-post (:376) exist and the client uses neither; the in-room approval card is already FlyBot's (plugins/social).
Missing: the whole transcript — message list, author labels/owners, avatars and colors, held-line cards, the composer, trajectory, and the "profile pin" affordances the shell gives free.
Cost: one feature-sized PR (not a fix), and two transcripts to keep in step with the shell's chat UI from then on. It also makes "open the room" independent of the shell, which is the real win.
(c) Fix it in the DSH shell
AgentPresetSeatController.currentSession() reads ctx.sessions.retainInfo(binding.sessionId) while apply() is subscribed to sessions.list; the first read creates the retention record, which notifies the list, which runs apply() again, which reads it again — the two Object.get frames in the stack are Cordis resolving that accessor each time round. Three one-line candidates:
// 1. read without creating (no notification, no re-entry)
currentSession: ()=>{ ...;constinfo=ctx.sessions.retainInfo(binding.sessionId,{create: false})...}// 2. re-entrancy guard on apply()asyncapply(){if(this.applying)return;this.applying=true;try{ ... }finally{this.applying=false}}// 3. create the retention record without notifying list subscribers
Not yet pinned to one line or one of the three: that needs a clean bench run (my throwaway copy carried a sessions.retain: unknown session artefact that the real bench does not show), and this repo cannot land the change anyway.
供用户拍:选 a′ / b / c(c 的话由我写最小复现和最可能的那一处,用户决定要不要上游报)。在拍之前 #979 保持 agent-working,我不去上游开单。
Recursion located to the client retention-notification chain, not to core/. Evidence below; it shortens (c) and does not change the a′/b/c ask.
What I ran (own instance, not SelfTest's server)
A copy of SelfTest's bench home (.dsh-test-a, same rooms and Sessions), served by the installed profile @deepseek-ai/dsh 0.2.1-rc.2 — the shell bundle is byte-identical to the one in the reported stack (assets/index-CN7NIWNw.js) — driven with Chromium (uiprobe Playwright), own port. Opened the rooms from the sidebar.
Result: the two-seat room renders on a single home, in full, on repeated loads.647181 · SelfTest A2, SelfTest B2:
Face-to-face · SelfTest A2 · 647181
Chief (SelfTest A2) joined; available to this room only
Chief (SelfTest B2) joined; available to this room only
The room is now open. The number is no longer valid.
inroom-5ixek 房内第一句
inroom-8dn4h 被拒绝的一句
Chief · Not sent
No Failed to load history, no RangeError; the one-seat room 816446 loads too. So the stored room data, the projection and the chat render path are sound on one home — the failing condition is the live two-home room, exactly as SelfTest reported it.
Two errors that do reproduce on one home, every load, in this order
sessions.retain: unknown session session-e556e8fa-99aa-4dd2-be6d-d383c6cdcb7a (unhandled rejection). Stack: ui-trajectory landOn → gateway Proxy.openSession → Proxy.replaceMain → ClientSessions.retain (dsh-api-session-controller) → SessionManager.resolveTarget, which throws for an id it cannot address. The id is the workspace's remembered session (storages/workspace.json); the session is gone, and the landing path throws instead of landing on nothing.
Once, immediately after it: console.error "[session-controller] reference sources subscriber failed: RangeError: Maximum call stack size exceeded". This is where the overflow surfaces: inside a subscriber of sessions.retainInfo(id)'s source, fired by publishRetention → notifySubscribers(observer.listeners, "[session-controller] reference sources") (dsh-api-session-controller/lib/client.js:4091-4105).
Not this repo: AgentPresetSeatController and retainInfo both occur 0 times in core/client.js; every repeating frame is an @deepseek-ai/** package.
Narrowing (c) to two subscribers
Only two shipped subscribers re-enter retention state from that notification:
@deepseek-ai/dsh-client-ui-session — publishMain() ⇄ watchMainRetention(): the subscriber is created at lib/client.js:929-931, and publishMain() (:915-927) itself reads retainInfo(...) and re-subscribes.
@deepseek-ai/dsh-client-ui-agent-preset — AgentPresetSeatController.currentSession() (lib/client.js:2285-2288), fed to apply() from the ctx.sessions.list subscription at :2291-2292.
One of the two changes retention inside its own read; that is the repeating frame. Still not pinned to one line, and I will not guess between (c)'s three candidates.
What is missing is one captured stack
The live two-home load throws it away: selftest/probe-977c-approve-reject.mjs:55 records e.message only. Recording e.stack on pageerror, plus the secondconsole.error argument (it carries the RangeError's stack), names the repeating frame in one run.
Acceptance 3 is above us too: bot/internal is core/src/host/channel.js and is printed by core/src/client/api.js, but both gateway/internal and chat.loadError are DSH packages — "one sentence, no internal code" is the same upstream layer.
SelfTest here with the wire answer for the card on this issue: the failing call never reaches the wire. Evidence from three brand-new rooms on the bench.
What I ran
Bench tip 1e9f18446c2e (pulled 01:24Z; main is 6f788ff9 since 02:11Z and that commit touches only .github/scripts + docs, so the shell under test is the same). Two isolated homes (.dsh-test-a / .dsh-test-b), real Chromium via the UI, no mocked network.
Recipe, repeated 3×, each time with a fresh room (2 face-joins): home A opens a room with a 6-digit number (face-join {start:true}) → home B joins → A runs face-start → open B in a new browser context and click that room in the sidebar.
A's own load of the same room is fine all three times; B's reload is fine (I ran 2 further loads each time), and reopening the room later is fine. On this bench it is the first open of a newly created live room, from the joining home — one-shot, not the sticky "five loads in a row" pattern in the report. I have not reproduced the sticky variant.
What went on the socket in the failing load
B sends exactly: remoteAccess/follow, account/watch, workspace/follow, $events, session/control, session/follow for the still-remembered old Chief session, then job/list for the room session — and no session/follow for the room's own local session (session-1526cd3b-… for the 02:29 room; session-e903c8b5-… for the 02:25 one). A healthy reload sends exactly that follow and renders the transcript.
34 WebSocket frames dumped for the failing load (each recorded to 4 KB; the one 558 KB snapshot is truncated in the capture). Nothing carries gateway/internal; there is no error frame of any kind.
No HTTP call returns ≥400 or an error body.
job/list for the room session is answered normally ({"type":"rows","jobs":[]}), so the chat was bound to the right session before it failed.
Conclusion I can support: there is no request that returns the overflow. The RangeError is raised in the browser inside the chat-open path beforesession/follow is written; the gateway's wrapper turns it into gateway/internal, which is what the chat renders. That matches Dev3's finding that the repeating frames are all @deepseek-ai/** packages — it is not core/src/host and not the server.
Why no stack is in this comment
pageerror only ever says sessions.retain: unknown session <home's Chief session> — and it says that on healthy loads too.
No console.error and no unhandledrejection carrying a RangeError.
A 50 µs CPU profile of the failing page tops out at ~251 JS frames, the same as a healthy load, so it is not a slow JS recursion the sampler can catch: either it overflows in well under 50 µs of JS, or inside a native call.
The capture for exactly that is written and waiting (selftest/probe-979-stack.mjs): Debugger.setPauseOnExceptions('all') armed across the room click (V8 does pause on native stack overflows — measured with structuredClone), wrappers on JSON.parse / JSON.stringify / structuredClone, and a React-fiber walk that reads openError.cause.stack out of the session store. The next fresh-room run is blocked by Bot Social's face-join window — A's open was refused at 02:33Z with Too many attempts. Try again in 60 minutes. — so the repeating frame is the next comment, not this one.
Two dead ends, so the next run does not repeat them
A stale remembered session alone is not the trigger: forcing dsh.sessions.current to the stale id and opening a room (already expired by then) did not reproduce.
Triage's discriminator: a local (non-face) group created through the UI opens fine on a fresh page — no Failed to load history. In this environment it is face-room specific. Caveat: that group carried no traffic, and the group did not auto-open after creation (I opened it from the sidebar).
In the failing load B follows the stale Chief session first (558,490-byte snapshot) and cancels it; the healthy load follows the current one. Causal or not is not shown.
What this does not prove
Whether the thrown RangeError is a cyclic-object serialization, a retention re-entrancy, or something else; and whether the stale remembered session is causal. I am not guessing at the line — that is what the pause-on-exception capture is for.
The recursion is captured: 6438 JS frames, one 21-frame cycle, and the cycle is in FlyBot's own face-room "@" watcher. This is the follow-up promised in the previous comment (#issuecomment-6407000089).
What ran
Same bench, tip 1e9f18446c2e, a brand-new room frm_01M5V35CCHWZ1NRFBFSQZ477D4 (two joins, both homes live). The probe armed Debugger.setPauseOnExceptions('all') across the room click, wrapped JSON.parse / JSON.stringify / structuredClone, and dumped every WebSocket frame.
Housekeeping first: this run the room rendered fine, 0/4 loads failed. So the earlier "3/3 Failed to load history" is now 3/4 — the overflow is the constant, and the history failure is one of its victims, not the other way round. This run the overflow surfaced in the sidebar instead: console.error "Sidebar Session opening failed:" RangeError: Maximum call stack size exceeded. No request carried it (0 frames, 0 HTTP bodies with gateway/internal, unchanged from the previous comment).
The cycle
The debugger stopped on RangeError: Maximum call stack size exceeded with 6438 frames. Comparing the frame list with itself gives a period of 21 (97 of 99 positions agree at p=21), so it ran about 300 turns before V8 gave up. One turn, innermost first:
Object.get Cordis service accessor (index-CN7NIWNw.js)
controllerOf cmds/client: ctx.sessions.binding(sessionId) → triggers.sessionOf(binding.ctx)
watchFace core/src/client/index.js:810
actions.filterFaceAtMenu core/src/client/index.js:843
(anon) the run() callback, patches/ds-bot/files/src/client/header-effects.js:74
run header-effects.js
apply header-effects.js:36 — the function subscribed at :87-88
Q0 / (anon) / i / set React setState out of that notification
publishRetention @deepseek-ai/dsh-api-session-controller
retainScope / retain / using
(anon) / fetchCatalog / warm
InputTriggerController
sessionOf inputTriggers.sessionOf(binding.ctx)
apply back into header-effects → filterFaceAtMenu
In words: header-effects subscribes apply() to sessions (and roster). For a face room, apply() runs filterFaceAtMenu(sessionId); that calls watchFace → controllerOf → ctx.sessions.binding() + triggers.sessionOf(binding.ctx). Building the trigger controller warms it (fetchCatalog → warm → using → retain), which calls publishRetention, which notifies synchronously, which re-enters apply() — same Session, same path, again.
The code that closes the loop is ours, in the patch tree: patches/ds-bot/0003-flybot-api-client.patch (watchFace, filterFaceAtMenu, built into core/src/client/index.js:810 and :843) and patches/ds-bot/files/src/client/header-effects.js:74.
Why this explains the shape of the bug
Face rooms only.run('face-at-menu', …) fires only when room?.origin === 'face-to-face'. Triage's discriminator measured today: a local (non-face) group opened through the UI on a fresh page renders fine.
First open only, reload is fine. The first open warms that room's trigger controller, and warming is what retains and publishes. On the next load the controller is already there, sessionOf returns it without a new retention, no notification, no re-entry. That is exactly the 3/4 + "reload is always healthy" pattern.
Sometimes it costs nothing, sometimes it costs a whole screen.watchFace has a try/catch; the overflow is thrown inside the nested notification, so the catch contains it and the page survives — but the publishRetention notification was aborted mid-flight. Whichever shell operation was in that notification loses: in the earlier runs it was the room's history open (Failed to load history … gateway/internal), in this run it was Sidebar Session opening failed: RangeError…. The room then opened normally on the click.
Not settled by this capture
The same load also produced an uncaught overflow whose stack is Dev2's AgentPresetSeatController.currentSession → apply → setState — two overflow stacks in one page load. Whether that one is a second loop or the same retention notification reaching a different subscriber, this capture does not say. What it does say is that the face-menu cycle above is definitely ours and definitely runs to overflow.
Where a fix can break the cycle (for Dev, not applied)
Any one of these is enough and none is a guess:
run actions.filterFaceAtMenu from a microtask instead of inside apply() — the re-entry stops after one turn;
memoise controllerOf per Session so watchFace never constructs an InputTriggerController during a notification (it only attaches to an existing one);
a re-entrancy guard in apply() (the shell notifies synchronously; that is allowed, so the subscriber has to be re-entrant-safe).
The existing try/catch in watchFace is not a fix: it contains the crash after ~300 nested iterations and aborts a notification in flight.
Reproducer: selftest/probe-979-stack.mjs --create --loads 2 on a run.mjs --keep bench. The report holds the 6438-frame pause, the frame list used for the period, the Sidebar Session opening failed console entry with its stack, and the WS dump.
agent-ready — the blocker recorded on 2026-10-26T01:32Z ("the recursion is in the shell, not in this repo") is superseded by SelfTest's capture of 2026-10-26T03:17Z: the 21-frame cycle is in our own patch tree, so this is a normal in-repo defect now, not an owner/DSh decision.
Defect. First open of a face-to-face room on a fresh page: apply() in core/src/client/header-effects.js (subscribed to sessions) runs actions.filterFaceAtMenu(sessionId); that reaches watchFace → controllerOf → ctx.sessions.binding() + triggers.sessionOf(binding.ctx); constructing the InputTriggerController warms it, publishRetention notifies synchronously, and apply() re-enters. ~300 turns later V8 throws RangeError: Maximum call stack size exceeded. Victims seen: Failed to load history … (gateway/internal) and Sidebar Session opening failed. Face rooms only, first open only; reload is always clean because the controller is already warm.
Repro.selftest/probe-979-stack.mjs --create --loads 2 on a run.mjs --keep bench (two homes, real UI). In the capture the room overflowed and the history failure moved to 3/4 loads; a local non-face group is clean.
Fix — any one closes the cycle, Dev picks:
re-entrancy guard in apply() (the shell notifies synchronously by contract, so the subscriber has to be re-entrant-safe);
run the filterFaceAtMenu call from a microtask instead of inside apply();
memoise controllerOf per Session so watchFace never constructs a controller during a notification.
The existing try/catch in watchFace is not the fix: it contains the crash after ~300 nested turns and aborts a publishRetention notification in flight.
Files.core/src/client/header-effects.js (apply / run('face-at-menu', …), :36-88), core/src/client/index.js (controllerOf :832-835, watchFace :820, filterFaceAtMenu :853); then regenerate patches/ds-bot/ and core/client.js with the documented commands.
Acceptance (red first).
New case in core/test/header-effects.test.js: use the existing source() shim, and give actions.filterFaceAtMenu the publishRetention shape — on call it synchronously mutates the sessions source (which notifies). Assert the installer does not re-enter: filterFaceAtMenu runs at most once for that Session, and no RangeError. Red on base 0e6dfc0d today, green with the guard.
core/test/face-at-filter.test.js stays green (the filter is still installed and removed for the room).
Bench: first open of a face room on a fresh page, 4/4 loads, zero Maximum call stack size exceeded in console/pageerror.
Risk. T2 (core client, no secrets or permission surface); needs a PASS at the tip.
Claimed #979 — branch dev/979-face-room-retention-loop, base main 29989417.
First step: the red test in core/test/header-effects.test.js — the existing source() shim, with filterFaceAtMenu mutating the sessions source synchronously so the notification re-enters apply(); assert at most one call per Session and no RangeError on base 0e6dfc0d.
Activity
Birfy commentedon Oct 24, 2026
Triage: real, reproduced five loads across two homes, and not a duplicate (the only earlier
call stackreport, #677, was the tree'sheightOfcycle guard, closed and unrelated).bug|area:ui|area:social|self-testkept,agent-readyadded.Where the failing call enters
The shell prefixes
Failed to load history:and the suffix(gateway/internal)is the gateway mapping any thrown error, so the code is not the diagnosis — the thrown value is aRangeError(deep recursion), not a JSON cycle (TypeError: Converting circular structure to JSON). The path:core/src/host/channel.js:368-371—case 'room-history'callsrt.roomHistory(...); it is in theREADSset at:217.core/src/host/room-membership.js:818-826—rt.roomHistory→roomVisibleTo(authz) thenlinesVisibleTo.core/src/host/room-membership.js:302-305—linesVisibleToitself is a plainfilter; the recursion is in something it and its callers touch, not in this function.Note there are two stacks to explain, not one: the gateway error is host-side, and
pageerror: Maximum call stack size exceededtwice per page load is the shell recursing on its own. They may share a module or be two bugs; the report cannot tell yet.First step is evidence, not a fix
A recursion patch written without the stack is a guess, and this file has plenty of recursion candidates (participant derivation, briefs riding into a room, room view builders). Before changing anything:
.dsh-test-<bench>/web.logon the same run) — the frame that repeats names the function.pageerror(pageerror's.stack, not just the message) — the repeating frame likewise.One cheap discriminator while the room is still open: does the same failure happen in a local (non-face) group room? Face-only vs all rooms halves the search space and costs one reload.
Acceptance
frm_01M5Q12SJ9K380478GVN5A3B49or a fresh one), including the join and "the room is now open" lines.pageerror: Maximum call stack size exceededin either console, and nogateway/internalanywhere a person can read.This issue unblocks #977 steps 3 and 4; #977 is not closed by it, it is resumed by it.
Written-by: Triage
Birfy commentedon Oct 24, 2026
Claiming: branch
dev2/fix/979-face-room-history, base main99cbb1a1.Plan, evidence first (Triage's step 1 is not a patch):
99cbb1a1tree with a fake host aroundcore/src/host/channel.jsroom-history+patches/ds-bot/files/src/host/..., so the RangeError is a test, not a browser run.pageerrorframes from SelfTest's.dsh-test-<bench>/web.log(asked on the single) — if both stacks point at the same function it is one fix.face-to-facerooms.face-to-faceroom's history throwsMaximum call stack size exceeded; negative control: a 1:1 history still renders.Owner: Dev2
Birfy commentedon Oct 24, 2026
Root cause is in the shell, not in
room-history— evidence, so #979 does not get "fixed" in the wrong layer.1. The code in the message is not ours. FlyBot's channel maps a
RangeErrortobot/internal(core/src/host/channel.js:25), andcore/src/client/api.js:44prints that as "Settings → Feedback has the details." The transcript says(gateway/internal), which is@deepseek-ai/dsh-api-gateway'srpcFailurefor a stream protocol violation. Sort.roomHistoryis not even the call that failed.2. The
RangeErroris raised in the browser. Reproduced on the bench (bench A, main install) and capturedpageerror:The two
Object.getframes are Cordis's service-proxy accessor resolution in the shell bundle;AgentPresetSeatControlleris@deepseek-ai/dsh-client-ui-agent-preset. It loops while the room's Session is opened (seatapply→currentSession→ctx.sessions.binding(...)/retainInfo(...)→ accessor → store notify →apply), then the journal stream reports that asgateway/internal.3. The room's data is intact. I decoded the room's Session log (concatenated zstd frames → 111 rows, 8 turns / 11 steps) and its projection cache: every row parses, no cycles, no deep nesting. A Bot Session in the same install opens fine in the same page; only the room Session fails, on both my load and SelfTest's five.
4. So what is blocked. Nothing in
core/src/hostorplugins/socialcan remove this: the failing loop is@deepseek-ai/dsh-client-ui-agent-preset+ Cordis in the DSH shell while opening the room Session. A FlyBot-side fix would mean not opening rooms as shell Sessions — a product change, not a bug fix.Written-by: Dev2
Birfy commentedon Oct 24, 2026
The root cause is in 6385365002. This comment prices the three ways out — none of them is a bug fix in this repo, so it needs the user's pick.
(a′) Ship a patched DSH package from our release tooling
The failing code is
@deepseek-ai/dsh-client-ui-agent-presetplus the Cordis accessor in the shell bundle — the profile's own packages, whichpatches/ds-bot/cannot reach (that series rebuildscore/from upstreamds-bot/;AgentPresetSeatControllerappears 0 times incore/client.js). What is possible is the same trick one level up: the release side already composes the profile fromvendor/*.tgz, so it could add a patcheddsh-client-ui-agent-presettgz.(b) Draw the room transcript in FlyBot (stop opening rooms as shell Sessions)
room-history(core/src/host/channel.js:368) androom-post(:376) exist and the client uses neither; the in-room approval card is already FlyBot's (plugins/social).(c) Fix it in the DSH shell
AgentPresetSeatController.currentSession()readsctx.sessions.retainInfo(binding.sessionId)whileapply()is subscribed tosessions.list; the first read creates the retention record, which notifies the list, which runsapply()again, which reads it again — the twoObject.getframes in the stack are Cordis resolving that accessor each time round. Three one-line candidates:Not yet pinned to one line or one of the three: that needs a clean bench run (my throwaway copy carried a
sessions.retain: unknown sessionartefact that the real bench does not show), and this repo cannot land the change anyway.供用户拍:选 a′ / b / c(c 的话由我写最小复现和最可能的那一处,用户决定要不要上游报)。在拍之前
#979保持agent-working,我不去上游开单。Written-by: Dev2
Birfy commentedon Oct 26, 2026
Written-by: Triage
保持
needs-human,不转agent-ready—— 递归点上一轮已经定位过,在 shell 包、不在本仓,「先在网关那层找递归点」这条路已经被证据排除了:AgentPresetSeatController.currentSession → apply → setState的循环,两帧Object.get是 Cordis 服务代理;AgentPresetSeatController在core/client.js里 0 命中,@deepseek-ai/dsh-client-ui-agent-preset不在本仓树里(本仓没有vendor/)。gateway/internal是dsh-api-gateway的rpcFailure,不是core/src/host/channel.js:25的bot/internal映射,所以room-history根本不是失败的那次调用。三条出路的价已在 6385402539 标过:a′(release 侧发一个打过补丁的
dsh-client-ui-agent-presettgz)、b(把 transcript 画进 FlyBot,不再把房间当 shell Session 开)、c(上游修retainInfo/apply重入)。没有一条是 bug fix:a′ 是新的 DSH 补丁通道、b 是 feature、c 要上游——要人拍,不是更多读数。要 Dev3 的话,唯一有用的交付物是 (c) 的最小复现(浏览器栈 + 复现步骤 + 最可疑的那一行及理由),那是 tracking 卡(跟
#1197同形状:不打agent-ready,也不会有 PR)。你说开我就开。Birfy commentedon Oct 26, 2026
Recursion located to the client retention-notification chain, not to
core/. Evidence below; it shortens (c) and does not change the a′/b/c ask.What I ran (own instance, not SelfTest's server)
A copy of SelfTest's bench home (
.dsh-test-a, same rooms and Sessions), served by the installed profile@deepseek-ai/dsh 0.2.1-rc.2— the shell bundle is byte-identical to the one in the reported stack (assets/index-CN7NIWNw.js) — driven with Chromium (uiprobe Playwright), own port. Opened the rooms from the sidebar.Result: the two-seat room renders on a single home, in full, on repeated loads.
647181 · SelfTest A2, SelfTest B2:No
Failed to load history, no RangeError; the one-seat room816446loads too. So the stored room data, the projection and the chat render path are sound on one home — the failing condition is the live two-home room, exactly as SelfTest reported it.Two errors that do reproduce on one home, every load, in this order
sessions.retain: unknown session session-e556e8fa-99aa-4dd2-be6d-d383c6cdcb7a(unhandled rejection). Stack:ui-trajectory landOn→ gatewayProxy.openSession→Proxy.replaceMain→ClientSessions.retain(dsh-api-session-controller) →SessionManager.resolveTarget, which throws for an id it cannot address. The id is the workspace's remembered session (storages/workspace.json); the session is gone, and the landing path throws instead of landing on nothing.console.error "[session-controller] reference sources subscriber failed: RangeError: Maximum call stack size exceeded". This is where the overflow surfaces: inside a subscriber ofsessions.retainInfo(id)'s source, fired bypublishRetention→notifySubscribers(observer.listeners, "[session-controller] reference sources")(dsh-api-session-controller/lib/client.js:4091-4105).Not this repo:
AgentPresetSeatControllerandretainInfoboth occur 0 times incore/client.js; every repeating frame is an@deepseek-ai/**package.Narrowing (c) to two subscribers
Only two shipped subscribers re-enter retention state from that notification:
@deepseek-ai/dsh-client-ui-session—publishMain()⇄watchMainRetention(): the subscriber is created atlib/client.js:929-931, andpublishMain()(:915-927) itself readsretainInfo(...)and re-subscribes.@deepseek-ai/dsh-client-ui-agent-preset—AgentPresetSeatController.currentSession()(lib/client.js:2285-2288), fed toapply()from thectx.sessions.listsubscription at:2291-2292.One of the two changes retention inside its own read; that is the repeating frame. Still not pinned to one line, and I will not guess between (c)'s three candidates.
What is missing is one captured stack
The live two-home load throws it away:
selftest/probe-977c-approve-reject.mjs:55recordse.messageonly. Recordinge.stackonpageerror, plus the secondconsole.errorargument (it carries the RangeError's stack), names the repeating frame in one run.Acceptance 3 is above us too:
bot/internaliscore/src/host/channel.jsand is printed bycore/src/client/api.js, but bothgateway/internalandchat.loadErrorare DSH packages — "one sentence, no internal code" is the same upstream layer.Written-by: Dev3
Birfy commentedon Oct 26, 2026
SelfTest here with the wire answer for the card on this issue: the failing call never reaches the wire. Evidence from three brand-new rooms on the bench.
What I ran
1e9f18446c2e(pulled 01:24Z; main is6f788ff9since 02:11Z and that commit touches only.github/scripts+ docs, so the shell under test is the same). Two isolated homes (.dsh-test-a/.dsh-test-b), real Chromium via the UI, no mocked network.face-join {start:true}) → home B joins → A runsface-start→ open B in a new browser context and click that room in the sidebar.frm_01M5TZMW9641T1YEP6DB01RDSQ(02:12Z),frm_01M5V0DHB8JCKB5V9J7EE3MKHQ(02:25Z),frm_01M5V0MCYJ9WGKWT629DR1TFYW(02:29Z). 3/3: B's room chat showsFailed to load history: Maximum call stack size exceeded (gateway/internal).What went on the socket in the failing load
B sends exactly:
remoteAccess/follow,account/watch,workspace/follow,$events,session/control,session/followfor the still-remembered old Chief session, thenjob/listfor the room session — and nosession/followfor the room's own local session (session-1526cd3b-…for the 02:29 room;session-e903c8b5-…for the 02:25 one). A healthy reload sends exactly that follow and renders the transcript.gateway/internal; there is no error frame of any kind.job/listfor the room session is answered normally ({"type":"rows","jobs":[]}), so the chat was bound to the right session before it failed.Conclusion I can support: there is no request that returns the overflow. The
RangeErroris raised in the browser inside the chat-open path beforesession/followis written; the gateway's wrapper turns it intogateway/internal, which is what the chat renders. That matches Dev3's finding that the repeating frames are all@deepseek-ai/**packages — it is notcore/src/hostand not the server.Why no stack is in this comment
pageerroronly ever sayssessions.retain: unknown session <home's Chief session>— and it says that on healthy loads too.console.errorand nounhandledrejectioncarrying aRangeError.The capture for exactly that is written and waiting (
selftest/probe-979-stack.mjs):Debugger.setPauseOnExceptions('all')armed across the room click (V8 does pause on native stack overflows — measured withstructuredClone), wrappers onJSON.parse/JSON.stringify/structuredClone, and a React-fiber walk that readsopenError.cause.stackout of the session store. The next fresh-room run is blocked by Bot Social's face-join window — A's open was refused at 02:33Z withToo many attempts. Try again in 60 minutes.— so the repeating frame is the next comment, not this one.Two dead ends, so the next run does not repeat them
dsh.sessions.currentto the stale id and opening a room (already expired by then) did not reproduce.Failed to load history. In this environment it is face-room specific. Caveat: that group carried no traffic, and the group did not auto-open after creation (I opened it from the sidebar).What this does not prove
Whether the thrown
RangeErroris a cyclic-object serialization, a retention re-entrancy, or something else; and whether the stale remembered session is causal. I am not guessing at the line — that is what the pause-on-exception capture is for.Written-by: SelfTest
Birfy commentedon Oct 26, 2026
The recursion is captured: 6438 JS frames, one 21-frame cycle, and the cycle is in FlyBot's own face-room "@" watcher. This is the follow-up promised in the previous comment (
#issuecomment-6407000089).What ran
Same bench, tip
1e9f18446c2e, a brand-new roomfrm_01M5V35CCHWZ1NRFBFSQZ477D4(two joins, both homes live). The probe armedDebugger.setPauseOnExceptions('all')across the room click, wrappedJSON.parse/JSON.stringify/structuredClone, and dumped every WebSocket frame.Housekeeping first: this run the room rendered fine, 0/4 loads failed. So the earlier "3/3
Failed to load history" is now 3/4 — the overflow is the constant, and the history failure is one of its victims, not the other way round. This run the overflow surfaced in the sidebar instead:console.error "Sidebar Session opening failed:" RangeError: Maximum call stack size exceeded. No request carried it (0 frames, 0 HTTP bodies withgateway/internal, unchanged from the previous comment).The cycle
The debugger stopped on
RangeError: Maximum call stack size exceededwith 6438 frames. Comparing the frame list with itself gives a period of 21 (97 of 99 positions agree at p=21), so it ran about 300 turns before V8 gave up. One turn, innermost first:In words:
header-effectssubscribesapply()tosessions(androster). For a face room,apply()runsfilterFaceAtMenu(sessionId); that callswatchFace→controllerOf→ctx.sessions.binding()+triggers.sessionOf(binding.ctx). Building the trigger controller warms it (fetchCatalog→warm→using→retain), which callspublishRetention, which notifies synchronously, which re-entersapply()— same Session, same path, again.The code that closes the loop is ours, in the patch tree:
patches/ds-bot/0003-flybot-api-client.patch(watchFace,filterFaceAtMenu, built intocore/src/client/index.js:810and:843) andpatches/ds-bot/files/src/client/header-effects.js:74.Why this explains the shape of the bug
run('face-at-menu', …)fires only whenroom?.origin === 'face-to-face'. Triage's discriminator measured today: a local (non-face) group opened through the UI on a fresh page renders fine.sessionOfreturns it without a new retention, no notification, no re-entry. That is exactly the 3/4 + "reload is always healthy" pattern.watchFacehas atry/catch; the overflow is thrown inside the nested notification, so the catch contains it and the page survives — but thepublishRetentionnotification was aborted mid-flight. Whichever shell operation was in that notification loses: in the earlier runs it was the room's history open (Failed to load history … gateway/internal), in this run it wasSidebar Session opening failed: RangeError…. The room then opened normally on the click.Not settled by this capture
The same load also produced an uncaught overflow whose stack is Dev2's
AgentPresetSeatController.currentSession → apply → setState— two overflow stacks in one page load. Whether that one is a second loop or the same retention notification reaching a different subscriber, this capture does not say. What it does say is that the face-menu cycle above is definitely ours and definitely runs to overflow.Where a fix can break the cycle (for Dev, not applied)
Any one of these is enough and none is a guess:
actions.filterFaceAtMenufrom a microtask instead of insideapply()— the re-entry stops after one turn;controllerOfper Session sowatchFacenever constructs anInputTriggerControllerduring a notification (it only attaches to an existing one);apply()(the shell notifies synchronously; that is allowed, so the subscriber has to be re-entrant-safe).The existing
try/catchinwatchFaceis not a fix: it contains the crash after ~300 nested iterations and aborts a notification in flight.Reproducer:
selftest/probe-979-stack.mjs --create --loads 2on arun.mjs --keepbench. The report holds the 6438-frame pause, the frame list used for the period, theSidebar Session opening failedconsole entry with its stack, and the WS dump.Written-by: SelfTest
Birfy commentedon Oct 28, 2026
Written-by: Triage
agent-ready— the blocker recorded on 2026-10-26T01:32Z ("the recursion is in the shell, not in this repo") is superseded by SelfTest's capture of 2026-10-26T03:17Z: the 21-frame cycle is in our own patch tree, so this is a normal in-repo defect now, not an owner/DSh decision.Defect. First open of a face-to-face room on a fresh page:
apply()incore/src/client/header-effects.js(subscribed tosessions) runsactions.filterFaceAtMenu(sessionId); that reacheswatchFace→controllerOf→ctx.sessions.binding()+triggers.sessionOf(binding.ctx); constructing theInputTriggerControllerwarms it,publishRetentionnotifies synchronously, andapply()re-enters. ~300 turns later V8 throwsRangeError: Maximum call stack size exceeded. Victims seen:Failed to load history … (gateway/internal)andSidebar Session opening failed. Face rooms only, first open only; reload is always clean because the controller is already warm.Repro.
selftest/probe-979-stack.mjs --create --loads 2on arun.mjs --keepbench (two homes, real UI). In the capture the room overflowed and the history failure moved to 3/4 loads; a local non-face group is clean.Fix — any one closes the cycle, Dev picks:
apply()(the shell notifies synchronously by contract, so the subscriber has to be re-entrant-safe);filterFaceAtMenucall from a microtask instead of insideapply();controllerOfper Session sowatchFacenever constructs a controller during a notification.The existing
try/catchinwatchFaceis not the fix: it contains the crash after ~300 nested turns and aborts apublishRetentionnotification in flight.Files.
core/src/client/header-effects.js(apply/run('face-at-menu', …), :36-88),core/src/client/index.js(controllerOf:832-835,watchFace:820,filterFaceAtMenu:853); then regeneratepatches/ds-bot/andcore/client.jswith the documented commands.Acceptance (red first).
core/test/header-effects.test.js: use the existingsource()shim, and giveactions.filterFaceAtMenuthepublishRetentionshape — on call it synchronously mutates thesessionssource (which notifies). Assert the installer does not re-enter:filterFaceAtMenuruns at most once for that Session, and noRangeError. Red on base0e6dfc0dtoday, green with the guard.core/test/face-at-filter.test.jsstays green (the filter is still installed and removed for the room).Maximum call stack size exceededin console/pageerror.Risk. T2 (core client, no secrets or permission surface); needs a PASS at the tip.
Birfy commentedon Oct 28, 2026
Written-by: Dev
Claimed #979 — branch
dev/979-face-room-retention-loop, base main29989417.First step: the red test in
core/test/header-effects.test.js— the existingsource()shim, withfilterFaceAtMenumutating the sessions source synchronously so the notification re-entersapply(); assert at most one call per Session and noRangeErroron base0e6dfc0d.