Skip to content

feat: extract @codeoid/core — framework-agnostic client core (P0, final) - #115

Merged
saucam merged 4 commits into
mainfrom
feat/extract-core-package
Jul 6, 2026
Merged

feat: extract @codeoid/core — framework-agnostic client core (P0, final)#115
saucam merged 4 commits into
mainfrom
feat/extract-core-package

Conversation

@saucam

@saucam saucam commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

The second half of the packages extraction (design doc §4) and the last prerequisite for mobile P1: everything a frontend needs except the pixels, shared by web and the upcoming mobile app so transcript semantics and transport reliability live in exactly one place.

@codeoid/core (packages/core, source-only, MIT, peer-dep on @codeoid/protocol)

CodeoidClient — the transport moved from web/src/lib/ws.ts, host-parameterized:

  • capabilities + clientName are now ConnectOptions (web declares parts/replay.chunked/replay.resume + "codeoid-web" at its construction site) — no web-isms baked into the transport.
  • All timing is injectable (heartbeat cadence/timeout, backoff floor/ceiling, request timeout) — needed by React Native backgrounding, and it let the test suite move from vitest fake timers to real tiny timers.
  • Browser resume listeners are typed via a feature-detected globalThis view, so the package compiles without the DOM lib and no-ops cleanly on native hosts (which call reconnectNow() from their own resume signal, e.g. RN AppState).

Message semanticsmergeDeltaInto + dedupeReplay kernels, plus a batteries-included MessageStore for hosts without a bespoke reactive layer. Its ingest() encodes the full broadcast routing table — live message / delta / snapshot replay / chunked replay (#84) / incremental resume (never resets!) + resume-cursor advancement — the part that's easy to get subtly wrong when reimplemented per client.

ResumeCursors (now a class; web binds a singleton) and the display helpers: formatters, identity/provenance labels, approval scanning, slash parsing (already DI'd via SlashContext), URI sanitizers, UTC day buckets. Tailwind class mappers stay web-local; ctxWindowColorClass maps core's new ctxWindowSeverity.

Web

  • Eight modules become re-export shims — zero import churn, same pattern as the protocol migration.
  • The Solid store keeps its tuned fine-grained reactivity (produce/paths/epochs) but delegates the merge + dedupe semantics to the kernels, so web and mobile provably accumulate transcripts identically.
  • Consumed via file:../packages/core (same mechanics as feat: web consumes @codeoid/protocol — retire the hand-maintained mirror #105; core's protocol peer-dep resolves against web's copy).

Tests move where the code lives

  • 113 core tests under bun:test: the ported lib suites, the CodeoidClient state machine (rewritten with injected real timings; new coverage for capability declaration, request correlation incl. waitForResult, in-flight rejection on drop, shutdown semantics), and new MessageStore/kernel/ingest suites.
  • Web keeps 78 tests: the Solid store behaviour and the colour mappers.
  • Net across repo: root suite 956 pass (was 843).

Release wiring

release.yml publishes @codeoid/core (version-guarded) after @codeoid/protocol. Like protocol, it needs a one-time manual bootstrap publish + its own npm Trusted Publisher before/with the next release.

Verification

Root: lint ✓ typecheck (root + both packages) ✓ 956 tests ✓ build ✓ · Web: tsc ✓ eslint ✓ 78 tests ✓ vite build ✓ (fresh file: installs + --frozen-lockfile path).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a shared core library powering slash commands, transcript/message store with resume/cursors, identity/provenance labeling, and consistent formatting helpers across frontends.
    • Added shared URL sanitization (links/images) and usage day bucketing utilities.
  • Bug Fixes

    • Improved connection resilience (authentication handshake, heartbeat/liveness, reconnection, and request correlation/cleanup).
    • Strengthened transcript updates (delta merging, scrollback replacement/append, deduping) and pending-approval detection.
  • Chores

    • Expanded lint/test scope to include the new core package, migrated tests to Bun’s runner, and updated CI/release publishing steps.

The second half of the packages extraction (design §4): everything a
frontend needs except the pixels, shared by web and the upcoming mobile
app so transcript semantics and transport reliability can't drift
per-client.

@codeoid/core (packages/core, source-only, MIT, peer-deps on
@codeoid/protocol):
- CodeoidClient — the transport moved from web/src/lib/ws.ts: auth
  handshake, request/response correlation, heartbeat, jittered backoff
  reconnect. Now host-parameterized: capabilities + clientName are
  ConnectOptions (web passes parts/replay.chunked/replay.resume +
  "codeoid-web"), all timing (heartbeat, backoff, request timeout) is
  injectable (React Native backgrounding; real-timer tests), and the
  browser resume listeners are typed via a feature-detected globalThis
  view so the package compiles without the DOM lib. Native hosts call
  reconnectNow() from their own resume signal.
- Message semantics: mergeDeltaInto + dedupeReplay kernels (the web
  Solid store now delegates to them inside produce(), so the merge
  semantics live in exactly one place) and a batteries-included
  MessageStore for hosts without a bespoke reactive layer — including
  ingest(), the full broadcast routing table (live/delta/snapshot/
  chunked/incremental replay + resume-cursor advancement) that is easy
  to get subtly wrong when reimplemented.
- ResumeCursors (class; web binds a singleton), display helpers
  (formatters, identity/provenance labels, approval scan, slash parser,
  URI sanitizers, UTC day buckets). Tailwind class mappers stay in web;
  ctxWindowColorClass now maps core's ctxWindowSeverity.

Web: eight modules become re-export shims (zero import churn), the
Solid store keeps its tuned fine-grained reactivity but shares the
kernels, and the client construction site declares capabilities.

Tests move where the code lives: 113 core tests under bun:test —
including the CodeoidClient suite rewritten from vitest fake timers to
injected real timings, plus new MessageStore/kernel/ingest coverage.
Web keeps its Solid-store and colour-mapper tests (78).

Release: release.yml publishes @codeoid/core (version-guarded) after
protocol; needs its own npm Trusted Publisher + one-time bootstrap
publish, same as protocol.

Root suite 956 pass; web tsc/lint/tests/build green; daemon build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a80b3bee-4b14-4b05-8e70-ad9d097e5d18

📥 Commits

Reviewing files that changed from the base of the PR and between 68ccc2d and c37f11e.

📒 Files selected for processing (6)
  • packages/core/package.json
  • packages/core/src/client.test.ts
  • packages/core/src/client.ts
  • packages/core/src/messages.test.ts
  • packages/core/src/messages.ts
  • packages/protocol/package.json
✅ Files skipped from review due to trivial changes (2)
  • packages/protocol/package.json
  • packages/core/package.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/core/src/messages.ts
  • packages/core/src/client.ts
  • packages/core/src/messages.test.ts

📝 Walkthrough

Walkthrough

A new @codeoid/core package centralizes transport, message/resume state, slash parsing, sanitization, formatting, identity, and usage helpers. Root scripts, CI, release, and web modules are updated to build, publish, and consume the shared package.

Changes

Core package extraction and web integration

Layer / File(s) Summary
Package scaffolding and release wiring
packages/core/package.json, packages/core/tsconfig.json, packages/core/README.md, .github/workflows/release.yml, package.json, packages/protocol/package.json
Adds the core package metadata, TypeScript config, documentation, root script coverage, package side-effects metadata, and a conditional publish step for @codeoid/core.
WebSocket transport and lifecycle
packages/core/src/client.ts, packages/core/src/client.test.ts
Adds CodeoidClient connection/auth, request correlation, heartbeat, reconnect, and shutdown behavior with Bun tests using a mock WebSocket.
Message store and resume cursors
packages/core/src/messages.ts, packages/core/src/messages.test.ts, packages/core/src/resume.ts, packages/core/src/resume.test.ts
Adds transcript delta/replay handling, MessageStore, ResumeCursors, and tests for message mutation, scrollback, and cursor updates.
Shared formatting and identity helpers
packages/core/src/format.ts, packages/core/src/identity.ts, and their tests
Adds shared formatters plus identity/provenance label helpers and Bun coverage for their outputs.
Approvals, sanitization, usage days, and slash commands
packages/core/src/approvals.ts, packages/core/src/sanitize-url.ts, packages/core/src/usage-days.ts, packages/core/src/slash.ts, packages/core/src/index.ts, and their tests
Adds pending-approval lookup, URL sanitization, UTC day bucketing, slash parsing/dispatch, and a barrel export surface.
Web consumption and CI wiring
web/package.json, web/src/lib/*, web/src/components/prompt/slash.ts, web/src/state/connection.ts, web/src/state/messages.ts, web/src/state/resume.ts, .github/workflows/ci.yml
Adds the workspace dependency, converts web modules into re-export shims, updates connection capabilities and shared state delegation, and installs workspace dependencies in CI.

Estimated code review effort: 4 (Complex) | ~75 minutes

Possibly related PRs

  • saucam/codeoid#25: Updates the release publish flow that this PR extends with an additional @codeoid/core publish step.
  • saucam/codeoid#75: Relates to the pending-approval and UTC usage helpers that are centralized into @codeoid/core here.
  • saucam/codeoid#91: Relates to the URL-sanitization helpers that are moved into packages/core/src/sanitize-url.ts here.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: extracting a framework-agnostic @codeoid/core package from the web app.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/extract-core-package

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.75870% with 27 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.76%. Comparing base (9858cc1) to head (c37f11e).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
packages/core/src/client.ts 96.49% 12 Missing ⚠️
packages/core/src/format.ts 86.53% 7 Missing ⚠️
packages/core/src/messages.ts 97.41% 4 Missing ⚠️
packages/core/src/slash.ts 97.60% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #115      +/-   ##
==========================================
+ Coverage   76.39%   77.76%   +1.36%     
==========================================
  Files          70       79       +9     
  Lines       11605    12438     +833     
==========================================
+ Hits         8866     9672     +806     
- Misses       2739     2766      +27     
Flag Coverage Δ
daemon 77.76% <96.75%> (+1.36%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/core/src/approvals.ts 100.00% <100.00%> (ø)
packages/core/src/identity.ts 100.00% <100.00%> (ø)
packages/core/src/resume.ts 100.00% <100.00%> (ø)
packages/core/src/sanitize-url.ts 100.00% <100.00%> (ø)
packages/core/src/usage-days.ts 100.00% <100.00%> (ø)
packages/core/src/messages.ts 97.41% <97.41%> (ø)
packages/core/src/slash.ts 97.60% <97.60%> (ø)
packages/core/src/format.ts 86.53% <86.53%> (ø)
packages/core/src/client.ts 96.49% <96.49%> (ø)
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Bun (latest, in CI) installs file: deps as symlinks, so module resolution
for @codeoid/core runs from the real packages/core/src path and up-walks
to the repo-root node_modules for its @codeoid/protocol peer — which the
web job never populated (it only installed inside web/). Locally this
passed because the root workspace was installed. The release workflow
already root-installs before build:web; CI now matches.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/core/src/client.test.ts (1)

217-224: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider exposing a synchronous status getter.

Tests rely on subscribing via onStatus just to read the current status synchronously (it's invoked immediately on subscribe). A get status() on CodeoidClient would make this and similar downstream consumers (e.g. debug tooling) simpler than the subscribe-then-read pattern.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/client.test.ts` around lines 217 - 224, Add a synchronous
status accessor on CodeoidClient so callers do not need to subscribe just to
read the current state. Expose a get status() (or similarly named getter) that
returns the client’s current ClientStatus, and have it read from the same
internal state used by onStatus; update client.test.ts expectations or usage to
prefer the getter where the status is only being read synchronously.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/client.ts`:
- Around line 417-513: The initial auth path in `#connectOnce` can hang forever
because authResolved is never time-bounded, so add a dedicated handshake timeout
around the WebSocket open/auth exchange and fail the promise if neither auth.ok
nor response.error arrives in time. Use the existing `#connectOnce` and
`#connectWithBackoff` flow to clear the timer on resolve/reject, close the socket,
and surface a clear timeout error so reconnect/backoff can proceed normally.

---

Nitpick comments:
In `@packages/core/src/client.test.ts`:
- Around line 217-224: Add a synchronous status accessor on CodeoidClient so
callers do not need to subscribe just to read the current state. Expose a get
status() (or similarly named getter) that returns the client’s current
ClientStatus, and have it read from the same internal state used by onStatus;
update client.test.ts expectations or usage to prefer the getter where the
status is only being read synchronously.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: de83c719-d908-456f-8def-ebc589ae6504

📥 Commits

Reviewing files that changed from the base of the PR and between 9858cc1 and 55a5a10.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock, !**/*.lock, !bun.lock
  • web/bun.lock is excluded by !**/*.lock, !**/*.lock
📒 Files selected for processing (38)
  • .github/workflows/release.yml
  • package.json
  • packages/core/README.md
  • packages/core/package.json
  • packages/core/src/approvals.test.ts
  • packages/core/src/approvals.ts
  • packages/core/src/client.test.ts
  • packages/core/src/client.ts
  • packages/core/src/format.test.ts
  • packages/core/src/format.ts
  • packages/core/src/identity.test.ts
  • packages/core/src/identity.ts
  • packages/core/src/index.ts
  • packages/core/src/messages.test.ts
  • packages/core/src/messages.ts
  • packages/core/src/resume.test.ts
  • packages/core/src/resume.ts
  • packages/core/src/sanitize-url.test.ts
  • packages/core/src/sanitize-url.ts
  • packages/core/src/slash.test.ts
  • packages/core/src/slash.ts
  • packages/core/src/usage-days.test.ts
  • packages/core/src/usage-days.ts
  • packages/core/tsconfig.json
  • web/package.json
  • web/src/components/prompt/slash.ts
  • web/src/lib/approvals.ts
  • web/src/lib/format.ts
  • web/src/lib/identity.test.ts
  • web/src/lib/identity.ts
  • web/src/lib/sanitize-url.ts
  • web/src/lib/usage-days.ts
  • web/src/lib/ws.test.ts
  • web/src/lib/ws.ts
  • web/src/state/connection.ts
  • web/src/state/messages.ts
  • web/src/state/resume.test.ts
  • web/src/state/resume.ts
💤 Files with no reviewable changes (2)
  • web/src/lib/ws.test.ts
  • web/src/state/resume.test.ts

Comment thread packages/core/src/client.ts
saucam and others added 2 commits July 6, 2026 01:42
Address CodeRabbit review on #115:

- Bound the initial auth handshake with requestTimeoutMs: a peer (or
  middlebox) that accepts the socket but never answers the auth frame
  previously hung connect() forever — the heartbeat only starts after
  `connected` and request timeouts don't cover the pre-auth frame. The
  deadline closes the socket and rejects into the normal backoff loop
  (double-loop safe: the post-close reconnect kick is gated on
  #connecting). This hang predates the extraction (web's ws.ts had it);
  fixed in the now-shared transport. All handshake exits clear the timer.

- Add `get status()` so hosts/tests can read the connection state
  synchronously instead of subscribe-then-read.

Tests: never-answered handshake fails with "auth handshake timed out"
and lands in `failed` under bounded attempts; reconnect test simplified
via the getter. Core suite 113 pass; root + web green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…request-id guard

Second audit pass over @codeoid/core before it becomes the mobile
foundation. Five findings, all fixed:

- connect() during a background reconnect threw "connect already in
  progress" — real API bug for mobile hosts (an AppState foreground
  handler calling connect() mid-backoff got an exception). Every entry
  point (connect, reconnectNow, close- and heartbeat-triggered kicks)
  now shares one memoized loop promise: callers JOIN the in-flight
  attempt. Rejected/finished loops clear via finally, so recovery from
  `failed` starts fresh.
- connect() after shutdown() surfaced a confusing "shutdown during
  reconnect" — now throws a clear terminal-instance error, and the
  contract (shut-down clients are not reusable) is documented.
- request() with a duplicate id silently clobbered the earlier pending
  entry, hanging its caller until a misleading timeout — duplicates now
  reject immediately; the original request is unharmed.
- Subscriber-fault isolation: a throwing onMessage/onStatus handler (or
  MessageStore listener) broke the fan-out for later subscribers from
  inside the WS event context. Each handler is now isolated with the
  error logged; transport dispatch survives consumer bugs.
- sideEffects:false on @codeoid/core and @codeoid/protocol (tree-shaking
  for Vite/Metro); MessageStore.messagesFor returns a stable frozen
  empty slice instead of allocating per miss.

Audited and left deliberately unchanged: sanitize-url (scheme
allowlist + remote-image exfil blocking is careful, correct work),
approvals (turn-bounded backward scan), slash (fully DI'd), resume,
format/identity, MessageStore kernels — reviewed line-by-line, no
defects found.

Tests: 6 new regressions (concurrent-connect joins one socket;
post-shutdown connect error; duplicate-id rejection with original
unharmed; throwing handler doesn't block dispatch, both client and
store; stable empty slice). Core 119, root 963, web 78 + build — green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@saucam
saucam merged commit a4d6370 into main Jul 6, 2026
5 checks passed
@saucam saucam mentioned this pull request Jul 6, 2026
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.

1 participant