Skip to content

feat(flutter): port the session core to Dart (#192) - #216

Merged
V3RON merged 6 commits into
mainfrom
issue-192-port-the-session-core-to-dart
Oct 8, 2026
Merged

V3RON merged 6 commits into
mainfrom
issue-192-port-the-session-core-to-dart

Conversation

@V3RON

@V3RON V3RON commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Szymon · project thread

Part of #192 (stays open until the frame-limits.json and background-scenario replays are wired in after #201 and #215 merge)

What changed

createDartCore(DartCorePorts) in packages/flutter/lib/src/core: a port of packages/web/src/core that claims and resumes a session, reconnects with jitter, pings, owns backgrounding, keeps the tool and event registries, and answers calls with deadline, cancel and progress. It runs against memory fakes (MemoryTransport, MemorySessionStore, ManualClock, FixedRandom) and imports only the four allowed dart: libraries. ToolHost runs app handlers in a guarded zone.

Two things are not wired yet because their files are not on main:

Differences from the other cores:

  • Keepalive. The web core cannot ping. The Dart core sends a protocol-level ping per keepalive_interval_s through Socket.ping(); two consecutive failures close the socket (1011 ping_timeout) and it resumes like any loss. The fake socket counts pings apart from text frames, so the scenario runner has no ping frames to drop.
  • Close codes. dart:io accepts 1001, 1008 and 1011, so the web core's 4008/4011 mapping is gone. The core still reports the code it chose, not the echo.
  • Trust rules. The Swift core refuses non-private IPs at bootstrap (requirePrivateIp). That check belongs with pinning and trust in slice 3, so the Dart core does not do it yet.
  • PROTOCOL.md and the other cores agreed on everything else I compared (grace, 1008 terminal, event registry only after an ack with event_registry, session_suspended after the state change).

Acceptance criteria

# Criterion Test Tier
1 Claims, snapshot, call within deadline, tool_timeout, tool_cancel, progress, event session_test.dart, tools_test.dart unit
2 Resume with the stored token; rejected resume clears the store and reports the session lost session_test.dart "after the socket drops", "restoring a session" unit
3 300 KiB result never sent, call fails with the #186 error frame_limit_test.dart unit
4 DateTime result answers tool_serialization_error; unawaited future error answers tool_execution_error tools_test.dart "a result the core cannot encode", "a tool host" unit
5 Late result after timeout or cancel puts nothing on the wire tools_test.dart "a late result" unit
6 Socket closes with a call in flight: handler told session_suspended tools_test.dart "when the socket closes with a call in flight" unit
7 Clock skips past missed pings: core reconnects and resumes session_test.dart "keepalive" unit
8 Scenario fixtures replayed (#188) scenarios_test.dart, session-scenarios.json unit
9 Core owns backgrounding; replays both scenario files background_test.dart; scenarios_test.dart replays session-scenarios.json now, the background file once #215 is on main (skipped until then) unit

Criteria 3 and 9 are only partly on the shared fixtures until #201 and #215 merge; their Dart-side tests are green.

E2E evidence

not applicable: pure Dart, no app or simulator involved.

Checklist

  • CHANGELOG.md has an entry under Unreleased (writing-changelog skill), or the change is not user-visible (the Flutter package is unpublished)
  • User-facing docs updated for every surface the change touches (writing-user-docs skill), or the change is not user-visible
  • No new import past a module's index.ts; no new direct node:* I/O outside an adapter (core.dart is the only file imported from outside lib/src/core; the imports test still passes)
  • Simplification checklist from the architecture skill applied, exceptions explained above
  • docs/ARCHITECTURE.md updated if a surface it describes changed

dart format --set-exit-if-changed ., dart analyze --fatal-infos and flutter test are clean in packages/flutter.

Out of scope

Status

Implement: done (292 tests) Review: round 2, approve E2E: not applicable (pure Dart core) Ready: yes

🤖 Generated with Claude Code

https://claude.ai/code/session_01R8SdLnR6fDS751JnAvzaSE


Generated by Claude Code

claude added 3 commits October 8, 2026 07:28
109 failing: ports, fakes and API types are in, createDartCore is a stub.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8SdLnR6fDS751JnAvzaSE
109 failing -> 0 failing

Claim, resume, backoff, keepalive pings, registries, tool invoker with the 256 KiB frame bound
and backgrounding. Two test helpers skipped the claim frame; fixed with the implementation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8SdLnR6fDS751JnAvzaSE
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8SdLnR6fDS751JnAvzaSE

@V3RON V3RON left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment: 2 should-fix, 1 nit. Spec: #192 with its comment, plus the #189 design comment.
Fix first: Socket.ping() in ports.dart cannot be implemented on dart:io, so move the keepalive into the socket before slice 3 builds on this port.


Generated by Claude Code

/// Sends a WebSocket protocol-level ping (`docs/PROTOCOL.md` section 3). Completes when the pong
/// arrives and completes with an error when it does not arrive in time or the ping cannot be
/// sent.
Future<void> ping();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should-fix. No real adapter can implement Socket.ping() on dart:io. WebSocket there has no call that sends a ping and reports its pong; it only has the pingInterval setter, which pings on its own and closes the socket with 1001 when a pong is missed (sdk/lib/_http/websocket.dart, pingInterval). So the slice 3 adapter (#193) gets a port it cannot back without writing its own WebSocket framing over SecureSocket, and criterion 7 is only proven against fake behaviour (MemorySocket.failPings) no real socket produces.

Fix: hand the keepalive to the socket instead, e.g. void keepalive(Duration interval), which the dart:io adapter maps to pingInterval. Have MemorySocket model a missed pong as a non-terminal close, so the core keeps only the close handling it already has.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in afb36af. Socket.ping() is now void keepalive(Duration), called once after the ack with the ack's interval; the core keeps no ping timers. MemorySocket.missPong() models the socket closing with 1001, and a test checks the core resumes from it (C4).


Generated by Claude Code

final String? pin;

/// Override the [DeviceFields] the core was built with.
final String? deviceManufacturer;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should-fix. deviceManufacturer, deviceModel and deviceOs on the public ConnectInput have no caller. _processLink never sets them, no test does, and DartCorePorts.device already supplies the same three values. That leaves two sources for the claim's device fields on an exported type, and slice 4 has to pick one when it wires the shim's activate() device. AGENTS.md rule 3 (no option that only one value ever uses) applies.

Fix: drop the three fields and the ?? fallbacks in Connection._firstFrame, and take the device only from DartCorePorts.device.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in afb36af. The three fields and the ?? fallbacks are gone; the claim takes the device only from DartCorePorts.device.


Generated by Claude Code

}

// An error from a future the handler never awaited has no caller to reach; the zone catches it.
runZonedGuarded(() async {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit. Futures cannot carry an error across a Dart error-zone boundary, and runZonedGuarded creates one. If a handler awaits a future created before the call, and that future fails, the handler's try/catch never sees the error and neither does this zone's handler. The error goes to the app's root zone as uncaught, and the call answers tool_timeout after the deadline instead of tool_execution_error.

I checked this on the PR head: register(slow, (_, _) async => await failed) with failed = Future.error(...) made before the call sends nothing for 5 s, then sends tool_timeout, and the test fails with an uncaught Bad state.

Fix: say this on ToolHandler/ToolHost, and when the slice 4 docs describe handler errors, put it there too.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in afb36af. ToolHandler now says the handler runs in its own error zone and that futures awaited must be created inside it. I'll carry the same note into the slice 4 docs.


Generated by Claude Code

claude added 3 commits October 8, 2026 07:49
0 failing -> 1 failing

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8SdLnR6fDS751JnAvzaSE
…verrides (#192)

1 failing -> 0 failing. Socket.keepalive replaces ping(); a missed pong is a
non-terminal close the core resumes from. ConnectInput loses its device
overrides. ToolHandler documents the error-zone caveat.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8SdLnR6fDS751JnAvzaSE

@V3RON V3RON left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve on substance (posted as a comment): 0 blocker, 0 should-fix, 1 nit. Both round 1 should-fixes are fixed in afb36af, the merge of main is clean, and CI is green on afb36af, the flutter job included.
Spec: #192 with its comment.
Fix first: the stale keepalive wording in the dart_core.dart doc comment and the PR body.


Generated by Claude Code


/// The session core: a Dart port of the web core (`packages/web/src/core`), itself a port of the
/// Swift and Kotlin cores. It owns the claim and resume handshake, reconnect with full jitter, the
/// grace timer, the keepalive pings, backgrounding, the tool and event registries and their

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit. The core no longer pings, but three places still say it does: this doc comment ("the keepalive pings"), and in the PR body the Keepalive bullet (Socket.ping(), two misses close with 1011 ping_timeout) and the criterion 7 row ("Clock skips past missed pings"). The #193 implementer reads this PR to build the dart:io adapter and would look for a ping method and a 1011 close that no longer exist.

Fix: say the core hands the socket its keepalive, and in the body say a missed pong is the socket's own 1001 close, which the core resumes from; criterion 7's clock skip is now proven in the adapter's tests in #193.


Generated by Claude Code

@V3RON
V3RON marked this pull request as ready for review October 8, 2026 08:10
@V3RON
V3RON merged commit 684d6ae into main Oct 8, 2026
12 checks passed
@V3RON
V3RON deleted the issue-192-port-the-session-core-to-dart branch October 8, 2026 08:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants