Repository navigation
feat(flutter): port the session core to Dart (#192) - #216
Conversation
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
left a comment
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
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
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
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)inpackages/flutter/lib/src/core: a port ofpackages/web/src/corethat 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 alloweddart:libraries.ToolHostruns app handlers in a guarded zone.Two things are not wired yet because their files are not on main:
frame-limits.json(A tool result over 256 KiB drops the session with session_suspended #186, PR fix: refuse tool results and events over 256 KiB without dropping the session (#186) #201). The 256 KiB bound is implemented with the same message and semantics as fix: refuse tool results and events over 256 KiB without dropping the session (#186) #201 (tool_serialization_errorfor a result, error listener for an event or snapshot, session stays active), andframe_limit_test.dartcarries the same six vectors inline. Replay of the file is wired in once fix: refuse tool results and events over 256 KiB without dropping the session (#186) #201 merges.session-scenarios-background.json(Add backgrounding scenarios to the session fixtures #206, PR test: add backgrounding scenarios to the session fixtures (#206) #215). The runner already handlesbackground,foregroundandsuspend; that file's test is skipped until it is on main. I ran it locally against the PR test: add backgrounding scenarios to the session fixtures (#206) #215 copy, and the tool-call scenarios of PR test: add tool call scenarios to the session fixtures (#205) #214 against its copy: all pass.Differences from the other cores:
keepalive_interval_sthroughSocket.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.dart:ioaccepts 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.requirePrivateIp). That check belongs with pinning and trust in slice 3, so the Dart core does not do it yet.event_registry,session_suspendedafter the state change).Acceptance criteria
tool_timeout,tool_cancel, progress, eventsession_test.dart,tools_test.dartsession_test.dart"after the socket drops", "restoring a session"frame_limit_test.dartDateTimeresult answerstool_serialization_error; unawaited future error answerstool_execution_errortools_test.dart"a result the core cannot encode", "a tool host"tools_test.dart"a late result"session_suspendedtools_test.dart"when the socket closes with a call in flight"session_test.dart"keepalive"scenarios_test.dart,session-scenarios.jsonbackground_test.dart;scenarios_test.dartreplayssession-scenarios.jsonnow, the background file once #215 is on main (skipped until then)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.mdhas an entry underUnreleased(writing-changelogskill), or the change is not user-visible (the Flutter package is unpublished)writing-user-docsskill), or the change is not user-visibleindex.ts; no new directnode:*I/O outside an adapter (core.dartis the only file imported from outsidelib/src/core; the imports test still passes)architectureskill applied, exceptions explained abovedocs/ARCHITECTURE.mdupdated if a surface it describes changeddart format --set-exit-if-changed .,dart analyze --fatal-infosandflutter testare clean inpackages/flutter.Out of scope
frame-limits.jsonand background fixture replay, as above.packages/native/fixtures/README.mdwill conflict with test: replay session scenarios in the Kotlin core (#204) #213 and test: add backgrounding scenarios to the session fixtures (#206) #215 on merge; the resolution is to keep both edits.dart:iotransport: slice 3 (Add the pinned dart:io transport and trust resolution #193). Binding and lifecycle observer: slice 4 (Add the Flutter binding and public Dart API #194).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