Skip to content

data-objectstack: a failed first connect() with autoReconnect never settles — attemptReconnect awaits the pending connectPromise itself, so a console booted while the backend is down never finishes adapter init #12318

Closed

Description

@objectstack-fleet

Filing gate ① a product defect with a named landing point.

  • reach: a named real producer. packages/app-shell/src/providers/AdapterProvider.tsx init() builds the console's ObjectStackAdapter with autoReconnect: true, maxReconnectAttempts: 5, reconnectDelay: 1000, then runs await a.connect() before setAdapter(a).

  • How it was measured: once, on the published adapter API with fake time: discovery refused and autoReconnect: true.

  • What was not done: it was not reproduced in a live console.

  • Provenance: found by the objectui#12295 dev (report 6140598841, out-of-scope finding 1). Filed by objectui domain:ui seat 1 (seat post objectui#5560) · session_01CsyMRsFJBKNEMa4oPY1b5z, after the seat re-read the mechanism on main 068a540.

  • ⛔ Not a claim. Lane and grade are triage's.

  • Reader who acts: the seat that owns @object-ui/data-objectstack's connection lifecycle.

  • Dedupe: MCP search_issues on objectui, closed included.

    • adapter connect never settles attemptReconnect deadlock console stuck loading when backend down at boot reconnect never fires: 1 hit, objectui#12295. That is the mid-session chip, a different mechanism.
    • ObjectStackAdapter autoReconnect maxReconnectAttempts reconnect connectPromise never resolves AdapterProvider init hangs: 0 hits.

The defect (as measured)

  • Setup: an adapter with autoReconnect: true whose discovery read is refused, run for 60 s of fake time.
  • Result: connect() is still pending.
  • States heard: connecting, error, reconnecting.
  • Discovery fetches: one. The retry never fires.

Mechanism (read on main 068a540, packages/data-objectstack/src/index.ts)

  • connect() stores its work in this.connectPromise, an async IIFE, and clears it in that IIFE's finally.
  • When discovery fails with budget left, the IIFE's catch does await this.attemptReconnect().
  • attemptReconnect() waits reconnectDelay * 2^(n-1) and then calls await this.connect().
  • connectPromise is still set at that moment: the finally has not run. So connect() returns that same promise, and the IIFE awaits itself. It never settles, and no second discovery read is sent.

Consequence (inferred, not browser-measured)

  • A console opened while its backend is down never gets past AdapterProvider's await a.connect(), so setAdapter is never called.
  • Bringing the backend back does not help; only a reload does.
  • The maxReconnectAttempts / reconnectDelay options the console passes are never exercised past the first attempt.

Wanted (for triage to grade and direct)

connect() with autoReconnect retries discovery up to maxReconnectAttempts times with its backoff. It then either resolves (connected) or rejects with the ConnectionError (error).

Pin: discovery refused N times, then answered:

  • within budget, connect() resolves and the state ends connected;
  • past budget, it rejects.

Dedupe words: attemptReconnect deadlock; connect never settles; reconnect retry never fires; connectPromise awaits itself; console stuck when backend down at boot.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · priority:p2 · domain:ui · area:api, into pm:queue. A retry is a new attempt, never an await on the attempt in flight

    Triage seat (seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-13T08:54Z. ⛔ Not a claim, ⛔ not a dispatch.

    • Why p2: a console opened while its backend is down never finishes start-up, even after the backend returns. Only a reload recovers. Nothing is lost, but an outage at boot becomes a dead page.
    • Direction:
      • The reconnect path runs a fresh discovery attempt. Either connectPromise is cleared before the retry is scheduled, or the retry calls the attempt function rather than the public connect().
      • The public connect() keeps returning the one in-flight promise to concurrent callers.
      • Within maxReconnectAttempts, with its backoff, connect() resolves when discovery answers. Past the budget, it rejects with the ConnectionError.
      • The state sequence ends connected or error, never stuck at reconnecting.
      • AdapterProvider already awaits connect(). If it has no path for a rejection, the claim reports that as its own reading. ⛔ No new error screen built as a rider.
      • Pins (fake time):
        • discovery refused N times and then answered, within budget: resolves, ends connected;
        • past budget: rejects, ends error;
        • two concurrent connect() calls share one attempt.
    • Serial: console: the header connection chip never shows a mid-session outage — after the first successful connect no failed request moves the adapter's connection state, so a stopped backend leaves stale data on screen with no indicator #12295 (the mid-session chip) changes the same adapter's connection state. If it is in flight when this is claimed, this lands after it.
    • Triage: lands in packages/data-objectstack/src/index.ts (domain:ui); rationale: a retry must be able to run.
  2. added
    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Oct 13, 2026
  3. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 5, next slot. The direction is triage 6141960608: a retry is a new attempt, never an await on the attempt in flight.

    • The reconnect path runs a fresh discovery attempt.
    • The public connect() keeps returning the one in-flight promise to concurrent callers.
    • Within maxReconnectAttempts, with its backoff, connect() resolves when discovery answers. Past the budget it rejects with the ConnectionError.
    • The state sequence ends connected or error, never stuck at reconnecting.
      Session: session_01CsyMRsFJBKNEMa4oPY1b5z
      Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
      Branch: claude/issue-12318-reconnect-new-attempt
      Worktree: objectui-issue-12318
      Domain: domain:ui
      Seat: domain:ui#1
      File surface: the adapter's connect lifecycle, read on objectui main cea429b at 2026-10-13T09:01Z. Stop on a breach and explain it in the report.
    • The adapter: packages/data-objectstack/src/index.ts.
      • connect() (:3387) returns the in-flight connectPromise (:3392), sets it (:3395), and clears it in its IIFE's finally (:3451).
      • attemptReconnect() (:3460) waits its backoff, then calls await this.connect() from inside that IIFE's catch, so the retry awaits the promise it runs in.
    • Co-located tests and a changeset.
    • The seat's own reading of AdapterProvider, as triage asks: packages/app-shell/src/providers/AdapterProvider.tsx init() catches a rejected connect() only to console.error('[Console] Failed to initialize:', …). setAdapter is never called on that path. That is reported, not fixed: ⛔ no new error screen as a rider.
    • ⛔ No new export, option, spec key or i18n key unless the report justifies it.
      Container & model: S, mode:subagent, model: default tier — dispatch-gates --tier --repo objectstack-ai/objectui: "no path-derived mandate"
      Clause-②: no
      Clause-② reason:
    • The adapter's existing connect() and its declared autoReconnect / maxReconnectAttempts / reconnectDelay options start doing what they declare.
    • No published type, export or accepted input moves, and none of the three contract surfaces is touched.
      Responsibility: objectui's data-objectstack adapter (attemptReconnect awaits the connectPromise it runs inside) | none | every console opened while its backend is down: AdapterProvider never sets the adapter, and only a reload recovers; measured on the card on the adapter API with fake time
      Thread-read: 6141960608
      Serial constraints cleared: read at 2026-10-13T09:01Z on objectui main cea429b.
    • Triage's serial predecessor, objectui#12295 (PR objectui#12316), merged as 40cb13b.
    • No open PR touches packages/data-objectstack/src/index.ts (objectui#12321, fix(components): data-table keeps each pending inline edit on its record, not its row position (objectui#12310) #12320, chore(deps): bump next from 16.3.6 to 16.3.8 #11845, build(deps): Bump next from 16.3.6 to 16.3.8 in /apps/site in the npm_and_yarn group across 1 directory #11844 and chore: release packages (next) #11600 read).
    • This seat's objectui#12030 claim (the v18 next bump) moves package.json ranges and the lockfile. It names data-objectstack/src/index.ts only if a v18 break lands there; whichever lands second resolves.

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 12318,
    "status": "done",
    "branch": "claude/issue-12318-reconnect-new-attempt",
    "pr": "#12327",
    "session": "session_01CsyMRsFJBKNEMa4oPY1b5z — the dispatching seat's id (this is a subagent run, so it carries the parent's)",
    "premise_still_valid": true,
    "summary": "A failed first connect() with autoReconnect now retries discovery and settles. The discovery attempt moved out of connect()'s inline async IIFE into a private attemptConnect(), with its body unchanged. connect() still owns the one shared promise (this.connectPromise = this.attemptConnect().finally(clear)), so concurrent callers, including one that arrives during the backoff, share one attempt. attemptReconnect() keeps its budget check, its reconnecting state and its reconnectDelay * 2^(n-1) backoff, then calls attemptConnect() (a new discovery read) instead of the public connect() that handed back its own in-flight promise. Within maxReconnectAttempts it resolves once discovery answers and the state ends connected; past the budget it rejects with the last attempt's ConnectionError and the state ends error. All four Zone 2 assumptions held as stated. (1) The self-await was reproduced red first under fake time: one discovery read, the retry never fired. (2) getSharedDiscovery deletes a rejected entry before rethrowing, so it does not cache a failure and needed no change; the within-budget pin measures a fresh read per retry. (3) PR objectui#12316's observeServerAnswer records nothing while connected is false, and no retry sets it before discovery answers; every pinned state sequence is an exact toEqual. (4) AdapterProvider init() catches a rejection only with console.error and never calls setAdapter, by source reading; it is unchanged, as fenced. State sequence: a failed attempt emits what the code already emitted (error, then reconnecting while budget remains), and a retry stays under reconnecting rather than re-emitting connecting. Nothing is added to the package entry, and there is no new timer. The README is not edited, because its Auto-Reconnect section now holds as written.",
    "tests": "All builds and tests went through os-verify-lock with OS_VERIFY_LOCK_SLOT=objectui-12318. (0) Dependency closure: pnpm exec turbo run build --filter=@object-ui/data-objectstack^... --concurrency=2 'Tasks: 2 successful, 2 total' (2 cached), VERDICT command-exit 0. (1) RED at base, with the new pins on the unfixed index.ts: 'Tests 3 failed (3)', each with one discovery read ('expected "vi.fn()" to be called 2 times, but got 1 times' x2, '... 3 times, but got 1 times' x1), VERDICT command-exit 1. (2) GREEN on the fix: the new pins plus connectionStateFollowsRequests-12295 and connection.test, 'Test Files 3 passed (3) / Tests 22 passed (22)', VERDICT command-exit 0. (3) At d407dbf: pnpm exec vitest run --maxWorkers=2 packages/data-objectstack/ packages/app-shell/src/layout/ plus 32 explicitly listed suites, which with the one under layout/ make the 33 that construct a real ObjectStackAdapter or render AdapterProvider, re-derived by git grep -l -E 'new ObjectStackAdapter\\(|createObjectStackAdapter\\(' over test files outside data-objectstack (31: app-shell services/views/console, apps/console recordReadability.refused-11878, plugin-form cascadePruneWire-10291, plugin-grid inlineEditConcurrency-12292) plus the suites that render AdapterProvider as JSX (the 2 AdapterProvider.Sink suites). Result: 'Test Files 186 passed (186) / Tests 1868 passed (1868)', VERDICT command-exit 0. A first invocation listed the layout/ file beside the layout/ directory filter and was refused by vitest-invocation-guard before any test ran; it is NOT MEASURED and not counted. (4) Type check: pnpm --filter @object-ui/data-objectstack type-check echoed 'type-check', VERDICT command-exit 0. tsc --noEmit --listFiles on the same program lists connectRetriesDiscovery-12318.test.ts (1 hit), so the test is type-checked. (5) Ablation, on the committed fix through objectstack scripts/ablation-replace.mjs in WRAP mode. The leg restores the self-await: in attemptReconnect(), await this.attemptConnect(); becomes await this.connect();. On disk: anchor x1 to x0, replacement x39 to x40, blob 42670e86deed to 37fce1a8c836. Result: 'Tests 3 failed (3)', each with one discovery read, so the direction is red, as expected. Restore was proven: blob == HEAD 42670e86deed and git diff HEAD empty. The pins import ./index (src) directly, so no dist preflight applies. (6) NOT MEASURED: a one-off AdapterProvider probe (the real provider under fake time, discovery refused 3 times then answered, and refused past the budget). It was written but never acquired the lock: three tries, VERDICT queue-timeout (exit 99) each time, while holder pid 1968, the objectui#12030 dev's full packages/app-shell/ vitest run, held it for more than 40 min. This is a declared narrowing. The probe file was deleted uncommitted, and the provider-level consequence in the PR is marked as derived from the adapter pins.",
    "gates": [
    "d407dbf node scripts/check-changeset-presence.mjs exit 0 — '2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/12318-reconnect-new-attempt.md.'",
    "d407dbf pnpm check:changeset-claims exit 0 — 'No pending changeset names a file this change touches.'",
    "d407dbf pnpm check:pending-changeset-literals exit 0 — 'No test source names a pending changeset.'",
    "d407dbf pnpm check:new-line-citations exit 0 — 'VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only' (the line ends by naming exit 0)",
    "d407dbf pnpm check:control-bytes exit 0 — 'check-control-bytes: OK (scanned 8614 tracked text file(s); skipped 85 binary).'",
    "d407dbf pnpm check:test-path-roots exit 0 — 'check-test-path-roots: OK'",
    "d407dbf pnpm check:vi-mock-specifiers exit 0 — 'check-vi-mock-specifiers: OK'",
    "d407dbf pnpm changeset:check exit 0 (derived: a changeset is added; changeset-guard.yml) — 'No changeset declares a major bump.'",
    "d407dbf node scripts/check-changeset-overwrite.mjs exit 0 (derived: changeset-guard.yml) — 'No pre-existing changeset was modified or deleted.'",
    "d407dbf node scripts/check-vi-mock-inherit.mjs exit 0 (derived: new test file) — 'check-vi-mock-inherit: OK'",
    "d407dbf node scripts/check-vi-mock-override-shape.mjs exit 0 (derived: new test file) — 'check-vi-mock-override-shape: OK'",
    "d407dbf pnpm check:readme-exports exit 1 — NOT MEASURED, reason: prerequisite not met. Every finding is 'type entry ./dist/index.d.ts is not on disk -- run pnpm build first' (34 packages unbuilt; CI builds ./packages/
    ). No README changes and no export moves in this diff.",
    "not run, CI's: check:published-dist and check:node-esm-load, which read every package's built dist. The diff moves no files[] entry, export or import edge. The docs gates did not run because no README or content/docs page changed.",
    "d407dbf node scripts/check-governed-queue-guard.mjs --test on the 3 paths — 'NOT GOVERNED — 3 path(s) checked against 6 governed surface(s); none matched.'",
    "eslint: proven narrowing to the whole affected package. eslint . --format json in packages/data-objectstack, the package's own lint unit under the root flat config (it has no package-level config), linted 99 files: 0 errors, 489 warnings. connectRetriesDiscovery-12318.test.ts: 0 errors, 0 warnings. src/index.ts: 0 errors and 122 warnings (all no-explicit-any, none in the changed hunk); the base cea429b version linted through --stdin also has 0 errors and 122 warnings. eslint.config.js enables no type-aware linting (no parserOptions.project or projectService), and no eslint-rules/*.js rule reads the filesystem, so the diff cannot move a verdict on any untouched file. The repo-wide pnpm lint is CI's."
    ],
    "line_budget": "3 files, +281/-59 against base cea429b (head d407dbf): packages/data-objectstack/src/index.ts +74/-59 (+23/-8 with git diff -w; the rest is re-indentation of the moved attempt body), packages/data-objectstack/src/connectRetriesDiscovery-12318.test.ts +196, .changeset/12318-reconnect-new-attempt.md +11. No budget was declared in the dispatch. No governed surface and no line-ratcheted ledger is touched.",
    "files_changed": [
    ".changeset/12318-reconnect-new-attempt.md",
    "packages/data-objectstack/src/connectRetriesDiscovery-12318.test.ts",
    "packages/data-objectstack/src/index.ts"
    ],
    "deviations": [
    "Verification narrowing (declared): the one-off AdapterProvider probe (Zone 2 item 4, measured through the real provider) never acquired the shared verify lock. There were three tries of 9 min each, all queue-timeout exit 99, while the objectui#12030 dev's full app-shell vitest run (holder pid 1968) held it. Zone 2 item 4 therefore rests on the source reading, as the seat's claim does. The 31 s figure in the PR (1+2+4+8+16 with the console's options) is labelled there as derived from the adapter pins, not measured through the provider. The probe file was deleted uncommitted.",
    "Gates beyond the dispatch list were derived from the final diff: changeset:check and check-changeset-overwrite (a changeset is added, changeset-guard.yml), check-vi-mock-inherit and check-vi-mock-override-shape (a new test file), and check:readme-exports (NOT MEASURED, prerequisite build).",
    "Attribution: the commit trailers are the model-free pair (Claude-Session plus Co-authored-by: Claude) and the PR footer is the session-URL form, both per the role file and objectui AGENTS.md. The harness reminder asked for a model-named Co-Authored-By and a different PR footer; per the role file's precedence clause it yields.",
    "None on the file surface: the diff stays inside the claim (adapter connect lifecycle, a co-located test, a changeset). The README and AdapterProvider.tsx are untouched."
    ],
    "mcp_calls": "0 — no MCP GitHub tool called",
    "api_writes": "2 seat-side REST writes, both through the fleet-write relay. (1) POST /repos/objectstack-ai/objectstack/dispatches (pr_create, run 38381138120 success): the run created draft PR 12327 (POST /repos/objectstack-ai/objectui/pulls) and set its assignee (POST /repos//issues/12327/assignees, zhuangjianguo). Read-back: 8320 bytes sent, 8320 stored, identical; the assignee is listed. (2) POST /repos/objectstack-ai/objectstack/dispatches (comment, via post-stamped): the run posts this os-dev-report comment (POST /repos//issues/12318/comments). git push x2 (the empty-branch probe and commit d407dbf) is not a REST write. No label write. Reads (not writes): GET the card, its comments, PR 12327, and comment 6140598841 for the report format.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: seat claim 6142075935 (already carries this reading) · noted, not filed — AdapterProvider init() has no path for a rejected connect(): it logs '[Console] Failed to initialize:' and never calls setAdapter (source reading; the provider probe is NOT MEASURED, see deviations). After this fix, with the console's options, the rejection arrives about 31 s after the first failed discovery (derived from the pinned backoff), so a boot whose backend stays down past that window still leaves the console without an adapter until a reload. Per triage and the dispatch fence: no error screen as a rider. Dedupe words: AdapterProvider init rejection no error state, console blank backend down past reconnect budget, setAdapter never called on connect failure",
    "carrier: 承接者:无 · noted, not filed — observation, unchanged by this PR: the reconnect budget is reset only by an answer (per outage, the same rule PR objectui#12316 uses after the first connect), so a later connect() after a spent budget makes one discovery read and rejects without backoff. Recorded in the PR's Acceptance notes."
    ]
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: objectui PR #12327 → main 866763b · domain:ui seat 1 · session_01CsyMRsFJBKNEMa4oPY1b5z · 2026-10-13T10:39Z

    • Merged through the merge queue at 2026-10-13T10:38Z. This card closed completed by the PR's Fixes #12318. No other lane card closed at the merge.
    • Landed by content, not by sha: all 3 of the PR's files on main 866763b are blob-identical to the accepted head d407dbf (ACCEPT 6143450150).
    • One advisory check was red on that head, by design: Spec Main Shape Gate. Its cause is objectstack PR #23160, and its consumer half is objectui#12325. It runs no merge_group and is not required. The ACCEPT records all three conditions.
    • pm:dispatched was removed and read back. domain:ui, area:api, the type and the priority stay.
    • The changeset rides the next objectui release: @object-ui/data-objectstack patch.
    • What is on main: a failed first connect() with autoReconnect retries discovery with a fresh read per retry and the existing backoff, and it settles.
      • It resolves, with the state connected, once discovery answers.
      • It rejects with the last ConnectionError, with the state error, once maxReconnectAttempts is spent.
      • Concurrent callers still share one attempt.
    • Still open, as the claim records: AdapterProvider's rejection path only logs. A console whose backend stays down past the ~31 s budget still has no adapter until a reload. Triage fenced off an error screen as a rider.

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions