* Chat via Cascade flow (for premium models with string UIDs). * * 1. StartCascade → cascade_id * 2. SendUserCascadeMessage (with model config) * 3. Poll GetCascadeTrajectorySteps until IDLE * * @param {Array} messages * @param {number} modelEnum * @param {string} modelUid
(messages, modelEnum, modelUid, opts = {})
| 591 | * @param {object} opts - { onChunk, onEnd, onError } |
| 592 | */ |
| 593 | async cascadeChat(messages, modelEnum, modelUid, opts = {}) { |
| 594 | let onChunk, onEnd, onError, signal, reuseEntry, toolPreamble, displayModel, nativeEnvironment; |
| 595 | let nativeMode, nativeAllowlist, additionalSteps; |
| 596 | acquireLsUseOrThrow(this.port); |
| 597 | try { |
| 598 | ({ |
| 599 | onChunk, onEnd, onError, signal, reuseEntry, toolPreamble, displayModel, nativeEnvironment, |
| 600 | // v2.0.65 native tool bridge handles. When nativeMode=true the |
| 601 | // upstream cascade_config switches the planner to DEFAULT mode + a |
| 602 | // restricted CascadeToolConfig.tool_allowlist; additionalSteps[9] |
| 603 | // carries fake "completed" trajectory steps for the caller's prior |
| 604 | // tool turns so the planner reasons from post-tool state. |
| 605 | nativeMode, nativeAllowlist, additionalSteps, |
| 606 | } = opts); |
| 607 | const aborted = () => signal?.aborted; |
| 608 | const inputChars = messages.reduce((n, m) => n + contentToString(m?.content).length, 0); |
| 609 | |
| 610 | log.debug(`CascadeChat: uid=${modelUid} enum=${modelEnum} msgs=${messages.length} reuse=${!!reuseEntry}`); |
| 611 | |
| 612 | // One-shot per-LS workspace init (idempotent; typically pre-warmed at |
| 613 | // LS startup). Falls back to a local session id if the LS entry is gone. |
| 614 | const lsEntry = getLsEntryByPort(this.port); |
| 615 | await this.warmupCascade(); |
| 616 | let sessionId = reuseEntry?.sessionId || lsEntry?.sessionId || randomUUID(); |
| 617 | |
| 618 | // "panel state not found" means the LS forgot the panel for our sessionId |
| 619 | // (LS restarted, TTL expired, etc.). Re-run warmupCascade with a fresh |
| 620 | // sessionId and retry the handshake once. |
| 621 | const isPanelMissing = (e) => /panel state not found|not_found.*panel/i.test(e?.message || ''); |
| 622 | // v2.0.25 HIGH-2: a cascade we tried to resume is gone upstream (TTL |
| 623 | // expired, server flushed, replay window passed). Same recovery as |
| 624 | // panel-missing — discard the reuse entry and fresh-start with full |
| 625 | // history replay. |
| 626 | const isExpiredCascade = (e) => /not_found.*(cascade|trajectory)|(?:cascade|trajectory).*not[ _-]?found|expired.*cascade|unknown.*cascade/i.test(e?.message || ''); |
| 627 | // #107 follow-up (zhangzhang-bit): SendUserCascadeMessage occasionally |
| 628 | // returns "untrusted workspace" on a freshly-spun LS — UpdateWorkspaceTrust |
| 629 | // either silently failed during warmup (handleWarmupError swallows |
| 630 | // non-transport errors) or the trust state was reset between warmup |
| 631 | // and the first message. Same recovery: force re-warm (which retries |
| 632 | // UpdateWorkspaceTrust) and reopen the cascade. |
| 633 | const isUntrustedWorkspace = (e) => /untrusted workspace|workspace.*not.*trusted/i.test(e?.message || ''); |
| 634 | |
| 635 | // Step 1: Start cascade — with retry on panel-state-not-found |
| 636 | let cascadeId; |
| 637 | const openCascade = async () => { |
| 638 | if (reuseEntry?.cascadeId) { |
| 639 | log.debug(`Cascade resumed: ${reuseEntry.cascadeId}`); |
| 640 | return reuseEntry.cascadeId; |
| 641 | } |
| 642 | const startProto = buildStartCascadeRequest(this.apiKey, sessionId); |
| 643 | const startResp = await grpcUnary( |
| 644 | this.port, this.csrfToken, `${LS_SERVICE}/StartCascade`, grpcFrame(startProto) |
| 645 | ); |
| 646 | const id = parseStartCascadeResponse(startResp); |
| 647 | if (!id) throw new Error('StartCascade returned empty cascade_id'); |
| 648 | log.debug(`Cascade started: ${id}`); |
| 649 | return id; |
| 650 | }; |