0 / Architecture & executionV3 → V4 RC

Effect 4.

Upgrade the runtime.
Preserve the app’s behavior.

Amical already uses Effect for desktop services, dictation, synchronization, and telemetry. The migration is bounded in package scope, but several paths depend on precise Effect 3 runtime behavior.

53production files import Effect
23test / type-probe files import it
1,321v3 baseline tests · 140 files
1workspace package to migrate
Recommendation

Pin Effect 4.0.0-rc.115, move the desktop compiler to TypeScript 5.9.3, and migrate in one coordinated branch. First prove immediate fiber startup, error settlement, and scope cleanup against the exact RC. Then migrate the wider service graph and package the app.

Prefer a clean Effect 4 design where the old approach no longer fits. Internal adapters and v3 workarounds may be rewritten. Surface the recommendation, tradeoffs, and behavior changes before implementing a substantial rewrite. See 3.7 Rewrite decisions.

Planning snapshot: 15 September 2026 · repository commit abdfd4d. Baseline typecheck and full desktop unit suite were run on Effect 3.22.1. Conversion is complete on feat/effect-4: 1,348 unit tests across 140 files and all workspace typechecks pass. The macOS arm64 package builds, native dependency smoke checks pass, and the freshly rebuilt package passes both macOS E2E checks (two Windows-only checks skipped). Lint has no errors; it reports 282 warnings. Boot ownership, quit policy, the FIFO lock, and session regions are retained. The static tracer and cause-aware error boundaries are rewritten. The follow-up review used three independent reviewers and covered reuse, simplicity, efficiency, and module structure. All 11 Swift helper tests also pass.

Latest pass: native Effect v4 use and simplification

Three fresh reviewers checked runtime/error handling, auth/sync, and transcription/lifecycle against the official pinned migration guides and installed RC source. Two remaining gaps were fixed:

  • Cleanup diagnostics: v4's Promise runner rejects with one error when several finalizers fail. closeAppScope now logs the complete cause through Effect.tapCause before rejection. Tests use the real ServiceManager cleanup boundary and verify both errors remain visible, all releases run, the first rejection keeps its identity, and repeated cleanup is a no-op.
  • Tracer sampling: the custom tracer now stores v4's computed sampled value. Tests cover explicit true/false decisions and child inheritance. Default production sampling stays unchanged.

Eight synchronous operations at imperative boundaries now use native Ref.makeUnsafe, Ref.getUnsafe, Deferred.makeUnsafe, or Queue.offerUnsafe. This removes unnecessary root-fiber execution. The unbounded sync event queue still delivers through its captured scheduler. Composite effects, resource ownership, and error settlement stay intact.

Context services and runners, scoped forks, callback cleanup, and flat Cause handling match v4. The explicit app scope and FIFO lock remain necessary for the existing boot and ordering contracts. Lifecycle V2 state-machine and contract files are unchanged. The full 1,348-test suite, workspace typechecks, lifecycle contract validation, and RC probes pass. Full lint reports zero errors and the same 282 warnings. A fresh macOS arm64 package, including the auth fix, passes both macOS E2E checks; two Windows-only checks are skipped on this machine.

The latest E2E setup warned that macOS System Events could not snapshot login items. Test assertions passed, but that run's login-item baseline was not captured. The existing harness treats this as a warning; it is outside the Effect conversion.

Follow-up review: three migration regressions fixed
  • FIFO lock handoff: v4 resumes Deferred waiters synchronously. A queued chunk could start before the preceding failure retired its session; a long synchronous queue could also overflow the stack. Completing the ticket with Effect.yieldNow schedules its continuation while retaining ownership and cancellation safety. Tests cover two chunks, interruption after handoff, and 2,000 queued users.
  • Fatal layer errors: v4's orDie drops co-present cleanup defects when a typed error is present. Three existing fatal service boundaries now use orDiePreservingCause, which keeps all reasons and annotations. Boot and shutdown policy stays the same.
  • Remote-config reporting: the outer defect handler again contains an exception thrown by telemetry while reporting invalid config. Refresh resolves and retains the last valid config, as it did before the migration.

Simplification removes three copies of fake timer setup and the duplicate lifecycle replay invocation in CI. The full unit suite includes that replay; the separate contract-version check remains.

Resolved follow-up: pre-existing auth shutdown race

Starting refreshTokenIfNeeded(true) and immediately calling shutdown() previously left its caller waiting when shutdown interrupted the child before its completion handler was installed. The same reproduction failed on Effect 3.22.1 and the initial v4 conversion.

The refresh fiber now starts with cancellation masked until its abort and completion handlers are installed. An inner Effect.interruptible keeps the refresh work cancellable. Both early shutdown and admission into a closed auth scope complete callers with interruption and start no HTTP request. The existing shared-refresh behavior is retained. Lifecycle V2 receives no additional changes from this fix.

Both new regressions failed before the fix and pass afterward. All 20 auth refresh tests, the full 1,345-test desktop suite, workspace typechecks, and lint/format checks for the changed auth files passed at the auth-fix checkpoint. Packaging and E2E were not rerun at that checkpoint; the latest native-v4 review is recorded above.

Validation note: notes deletion timing

The first packaged run failed at e2e/notes-markdown.spec.ts:191: the assertion expects a temporarily disabled editor after deletion, but the widget had already reached its empty state. All earlier persistence and synchronization checks passed. The notes source and test are unchanged by this migration. An isolated Effect 3.22.1 baseline passed four runs, and the pre-review v4 package passed three further runs. The package rebuilt after the follow-up fixes also passes. The initial timing failure remains a test limitation; it was not reproduced on the baseline or fixed in this conversion.

1Version & scope

Component Current Planned target
Effect ^3.22.1, locked to 3.22.1 4.0.0-rc.115, exact pin
Desktop TypeScript ~5.8.3 5.9.3, exact pin
Root TypeScript 5.8.2 Retain unless a root task needs the newer compiler. Turbo runs package checks with each package’s own compiler.
Runtime / package manager Node >=24 required; local 24.18.0
pnpm 10.34.5 · Electron 44.3.0
Retain versions; verify the packaged Electron runtime
Other Effect packages No direct @effect/* dependencies No additional packages required by this plan

The npm rc tag resolved to 4.0.0-rc.115 during research. It was published on 11 September 2026. The latest tag still points to Effect 3.22.2. Recheck the tag when implementation starts, record any new target, and then keep the exact version fixed through validation. npm dist-tags · RC release

Effect 4 requires TypeScript 5.9 or newer and strict checking. Desktop already enables strict. Version 5.9.3 is a small, compatible compiler step; a move to TypeScript 7 is a separate decision. Official installation requirements

1.1 Included

  • Existing Effect production code and its tests in apps/desktop.
  • Runtime bridges, service tags, error handling, fibers, queues, and the custom tracer.
  • Desktop compiler pin, lockfile, and the CI checks needed to protect the upgrade.

1.2 Scope boundaries

The plan assumes “convert” means upgrade current Effect 3 code. Existing Promise interfaces, Zod validation, IPC contracts, database format, native helpers, and renderer architecture stay part of the current design.

Within the Effect code, preserving the old implementation is not a goal. Compare a direct port with a design that uses v4’s native primitives; prefer the latter when it removes proven complexity. Flag changes to public interfaces or behavior before implementation.

No Effect Schema, Stream, or Schedule usage was found. apps/www is explicitly excluded from the pnpm workspace. Expanding Effect into those areas would be separate work.

1.3 Proposed dependency commands — for the implementation branch
pnpm view effect dist-tags --json
pnpm --filter @amical/desktop add --save-exact effect@4.0.0-rc.115
pnpm --filter @amical/desktop add -D --save-exact typescript@5.9.3
pnpm -r why effect

Review the lockfile for unrelated churn. Root and other workspace compiler declarations do not need a blanket upgrade; change them only if an actual dependency or validation failure requires it.

2Current footprint

Effect is concentrated in the Electron main process and its service boundaries. Counts below are direct-import files, not an estimate of every indirectly affected caller.

Area Files Migration focus
src/services 24 Auth refresh, account sync, activity reporting, live transcription, FIFO token lock, service layers.
src/main 17 App construction and cleanup, lifecycle session work, custom telemetry runtime and trace projection.
src/types 7 Tagged domain errors, Promise lifting, failure/defect settlement, error projection.
src/pipeline 5 Cloud provider state and runtime, HTTP transport, gRPC callbacks and cancellation.
tests 23 Direct Effect imports, including compile-time error probes. Broader tests also exercise these paths.
2.1 Concrete inventory and starting files
  • 22 class service tags in src/main/runtime/tags.ts:48; 2 generic tags in src/pipeline/providers/transcription/amical-cloud-provider-state.ts:22.
  • 29 tagged error classes across domain/service errors. Keep public tags, fields, and frozen error codes stable.
  • 4 semaphore constructors: auth (2), activity reporting (1), settings sync (1).
  • 4 Effect callback constructors: gRPC (2), lifecycle session work (1), settings sync (1).
  • 4 files using the Runtime module: main/runtime/telemetry-runtime.ts, services/settings-sync-service.ts, services/settings-sync-supervisor.ts, and services/activity-reporting-service.ts. The cloud provider also uses ManagedRuntime.

All paths in this plan are relative to apps/desktop, unless a root/workspace path is shown explicitly.

flowchart TD
    A[Electron app scope] --> B[Service layers and tags]
    B --> C[Auth, sync and activity]
    B --> D[Lifecycle and dictation]
    D --> E[Immediate telemetry runners]
    D --> F[Live session and FIFO token lock]
    F --> G[Cloud HTTP / gRPC provider]
    G --> H[Failure and defect settlement]
    E --> H
    H --> I[Existing Promise / IPC boundaries]
Read the migration from app ownership to runtime boundaries. The same user-visible behavior must survive at the edges.

3API change map

Use the exact RC’s declarations and source for implementation. Mechanical renames are only one part of the change; the runtime and data-model rows require behavior checks.

3.1 Mechanical changes with focused review

Current API Effect 4.0.0-rc.115 Where / check
Effect.gen(this, generator) Effect.gen({ self: this }, generator) 86 calls in 11 production files use the receiver overload, including auth, settings sync, and gRPC. Preserve generator receiver binding with the native options object; no wrapper is needed. Pinned generator declaration
Context.Tag(id)<Self, Shape>()
Context.GenericTag<T>(id)
Context.Service<Self, Shape>()(id)
Context.Service<T>(id)
Keep service IDs and shapes; migrate test tags too. Services guide
Scope.CloseableScope
Scope.extend
Scope.Closeable
Scope.provide
Preserve the chosen scope ownership and finalizer behavior; review any boot-ownership rewrite under 3.7 first. Scope guide
Effect.async
Effect.zipRight / zipLeft
Effect.yieldNow()
Effect.callback
Effect.andThen / tap
Effect.yieldNow (value)
Check callback cleanup and abort paths. Sequence combinators must retain return values. Effect declarations
Effect.makeSemaphore
Effect.Semaphore
Semaphore.make
Semaphore.Semaphore
withPermits remains available. Preserve masks and scope ownership. Semaphore declarations
Deferred.unsafeMake(FiberId.none)
Deferred.unsafeDone
Deferred.makeUnsafe()
Deferred.doneUnsafe
Remove the fiber-ID argument. Exit.void still works as a completion value; Exit remains an Effect in this RC. Deferred declarations · Exit declarations

3.2 Runtime, fiber, and tracer changes

Surface Migration Required review
Runtime

Runtime.Runtime, Effect.runtime, Runtime.run*

Capture Context.Context with Effect.context; use Effect.run*With(context). Use plain runners or an empty context for defaults. Audit telemetry, sync, and activity reporting. Root runner options no longer accept scope; attach fibers before their work starts. For eager roots, use Effect.withFiber to call Fiber.runIn before returning the work effect. Attaching after runForkWith returns is too late when the work synchronously triggers shutdown. Test finalizer completion in that case. Runtime guide · Scope attachment
Fiber

RuntimeFiber, interruption and fork APIs

Fiber.Fiber; fiber.interruptUnsafe(); numeric fiber.id. fork → forkChild; forkDaemon → forkDetach. Fiber.interrupt now returns Effect<void>; await the fiber separately if its Exit is needed. Review immediate-start requirements at each fork. Fiber declarations · Forking guide
Tracer

Custom DictationSpan and Tracer.make

The span factory receives one options object. Span.context becomes annotations. Remove the optional context: (f) => f() hook, whose v4 arguments changed. Supply Tracer.Tracer in the runner context; Layer.setTracer / Layer.toRuntime are absent. Preserve span timing, parentage, exit, and sink isolation. Tracer declarations
Queue

Settings sync control events

Queue.unbounded, offer, and take remain. Keep the existing shutdown event/acknowledgment protocol. Test pending acknowledgments at shutdown. Queue termination operations have distinct drain/discard behavior; choose only if needed. Queue declarations
3.3 Immediate-start behavior — observed in isolated RC probes

runFork, runForkWith, runPromise, and runPromiseWith executed the synchronous prefix before returning in rc.115. Root runner options have no startImmediately field.

Child and detached fork combinators defer startup by default. Their { startImmediately: true } option restores immediate startup where the app requires it. Review main/lifecycle/effect/session-work.ts:233 in particular.

Effect.forever now yields between iterations. The activity schedule keeps its explicit 301–360 second or one-hour sleep and uses { disableYield: true } to register the next timer without another scheduler handoff. Sleep still suspends the fiber for the intended interval.

// Static telemetry bridge; covered by the custom tracer tests.
const context = Context.make(Tracer.Tracer, tracer);
export const runFork = Effect.runForkWith(context);
export const runPromise = Effect.runPromiseWith(context);
export const runPromiseExit = Effect.runPromiseExitWith(context);

The isolated probe confirmed immediate startup and tracer invocation for this context pattern. It did not migrate or validate the full app. Runtime guide · Forking guide

3.4 Error handling is the first semantic gate

Observed RC difference

A failure with both a typed error and a finalizer defect retains both in its raw Exit. In rc.115, Effect.catch can recover the typed error and discard the defect; Effect.result, Effect.mapError, Effect.orDie, and typed matching can also hide it. The conversion uses catchCause plus Cause.map to transform typed errors while retaining the complete cause. Regression tests cover auth, cloud fallback, remote config, and finalizer defects.

Review catchAll → catch, catchAllCause → catchCause, catchAllDefect → catchDefect, and test either → result changes together. Audit existing catchTags and error taps where finalizers can also fail. Keep application-level defect reporting through cause-aware handling at the relevant boundaries. Pinned recovery implementation

3.5 Cause representation and test changes

V4 uses a flat collection of reasons. Replace Cause.failureOption with findErrorOption; dieOption becomes findDefect, which returns a Result, not an Option. Read defect values from cause.reasons using Cause.isDieReason. sequential becomes combine, which deduplicates equal reasons while preserving first-occurrence order; interruption checks become hasInterrupts / hasInterruptsOnly. Exit.isInterrupted becomes Exit.hasInterrupts in settings sync and its tests.

The RC has no Cause.electFailures export. Update probes that describe v3 mechanics to describe actual v4 behavior. Keep tests for the app’s typed-error priority, original error identity, null rejection handling, and exactly-once defect capture.

Cause.originalError is also removed. Isolated probes preserved raw defect identity through plain, single-span, and nested-span failures. Use the raw defect value for deduplication in the live session and retain tests for those cases. Cause source

Start in types/errors/settle.ts, services/transcription/live-transcription-session.ts, services/auth-retry.ts, and tests/effect/error-model-probes.test.ts. Cause migration guide

3.6 Packaging and lifetime

The RC package exposes ESM. Effect currently bundles into the Vite main output; verify the packaged output has no unresolved Effect load and launches in Electron. A repo-wide module-format conversion is not required by the current evidence. Pinned package exports

V4 keeps suspended fibers alive, and layer memoization changes across repeated Effect.provide calls. Verify process exit, singleton construction, test isolation, and finalizer order. Fiber lifetime guide · Layer memoization guide

3.7 Rewrite decisions

Updated direction: prefer a durable v4 implementation where the old design no longer fits. These recommendations distinguish confirmed library changes from optional architecture changes.

Area Recommendation Reason and decision gate
Runtime / tracer bridge
Rewrite now
Replace the old runtime-building shell with context-based runners and the v4 tracer interface. The old Runtime type and tracer-layer construction APIs are gone. Remove obsolete initialization machinery. Direct context runners fit the current static tracer. A fresh RC probe also showed that a cold ManagedRuntime starts immediately for a synchronous layer; do not carry forward the v3 assumption that it always defers startup. Runtime guide · ManagedRuntime source
Error settlement / recovery
Rewrite now
Rewrite cause inspection and recovery at affected boundaries around v4 reasons and raw defect identity. Mixed-cause behavior changed. Make error priority and defect reporting explicit where needed; remove old proxy normalization and revise v3-only probes. Verify null failures, interrupted finalizers, and exactly-once reports. See 3.4–3.5.
Custom FIFO token lock
Retain for ordering
Keep the explicit FIFO handoff. Complete each waiter with Effect.yieldNow to schedule its continuation; v4's synchronous Deferred wakeup otherwise changes failure ordering and can recurse through a long queue. An rc.115 probe showed a new semaphore acquirer overtaking an already queued waiter. Native Semaphore(1) does not satisfy the current FIFO contract. Removing this lock would require a resource-serialization design, such as ordered jobs with one worker. Include VAD reset, final flush, and history retry as well as streaming chunks: they share these locks. That rewrite is an option to compare, not a demonstrated simplification. Semaphore implementation
Boot scope ownership
Design review
Evaluate an outer crash-reporting scope with ordinary scoped acquisition for the inner application graph. This is an architecture option, not a new v4 requirement. Keeping telemetry alive is insufficient: inner rollback can block initialization from rejecting, so the current crash reporter never runs. Compare reporting before rollback, a defined bounded rollback policy, and current ownership. Test blocked cleanup and a pending sibling initializer before recommending a rewrite; see 4.1.
Session fiber ownership
Evaluate primitives
Evaluate FiberSet for generic running-fiber bookkeeping and native scopes for background workers. Keep cloud/provider ManagedRuntime ownership. Production closes chunk admission before draining (transcription-service.ts:912,962), so FiberSet.awaitEmpty may meet the real requirement: drain accepted work and let abort release the wait. Snapshot semantics and SessionWork.settled() are currently test/API details, not proven product constraints. Verify the chosen drain and retain explicit error settlement. FiberMap holds one fiber per key and would replace earlier work under the same session key; it cannot replace the whole lifecycle registry directly. Delivery and surviving obligations need distinct lifetimes. FiberSet source · FiberMap source
Electron quit boundary
Design review
Require asynchronous finalizer completion before normal application exit; evaluate a guarded quit sequence if needed. This is a pre-existing integration risk. The only quit hook (main/app.ts:157) returns the cleanup Promise without preventing termination. A successful exit can therefore hide incomplete cleanup. Decide repeated-quit, cleanup-error, and timeout behavior, and prove completion in a packaged app. Electron quit semantics

For each non-clean migration, report the old design, supported v4 alternative, recommended rewrite, behavior changes, and effort/test impact before implementation. Service-tag renames and retained queue operations can follow the guide directly. Any compatibility wrapper needs a concrete reason to survive the migration.

4Behavior contracts

These are the current behavior checks. Separate product requirements from tests that pin a v3 implementation detail. Update the latter for the chosen v4 design; document any proposed product-behavior change before implementation.

4.1

Startup failures reach crash reporting before cleanup can block

Preserve the original boot error, telemetry flush, and fatal dialog even when a sibling’s cleanup is pending. Register release before fallible initialization; own and await or cancel pending initialization when a sibling fails. The chosen scope design must clean up resources once in dependency order.

The essential requirement is usable crash reporting and reliable cleanup. Keeping the entire partial graph alive is the current solution; the scope review in 3.7 may propose a smaller retained graph and explicitly revised cleanup expectations.

Evidence: main/runtime/app-runtime.ts:1, main/runtime/layer-helpers.ts:16. Gate: existing tests/main/app-layers.test.ts failure and shutdown cases, extended with a pending initializer and a blocked finalizer. The existing late-failure case does not prove those scenarios. Pinned layer cleanup

4.2

The first dictation fork starts immediately

The synchronous prefix must run before the runner returns, including on the first call. The lifecycle code uses this to register work and telemetry obligations without a scheduling gap.

Evidence: main/runtime/telemetry-runtime.ts:6. Gate: keep the first test in tests/main/telemetry-runtime.test.ts:15; extend coverage to Promise/exit runners and callback-created work where needed.

4.3

Errors retain identity and defects stay visible

Promise boundaries reject with the same typed error value. In a mixed cause, settlement preserves the app’s failure priority and reports each dropped defect exactly once. Pure cancellation does not become a domain failure or a bug report.

Evidence: types/errors/settle.ts:3, services/transcription/live-transcription-session.ts:52. Gate: error-model probes, settlement tests, and live-session defect-capture tests.

4.4

Token acquisition remains FIFO and cancellation-safe

Waiters enter in arrival order. An interrupted waiter is removed; handoff cannot lose the token; release happens once. Keep the custom lock until an alternative proves all of these properties.

Evidence: services/transcription/token-lock.ts:5. Gate: tests/services/token-lock.test.ts, including interrupted handoff and mixed Promise/Effect use.

4.5

Identity changes fence old work

Logout and account switches cancel or retire old requests before new-account work can commit. Settings sync and activity reporting retain their single-flight, debounce, retry, and shutdown rules. Auth refresh remains shared by concurrent callers.

Gate: settings-sync service/client, activity-reporting service/client, auth-refresh and auth-retry suites. Add targeted RC cases for queued work during scope close only where coverage is missing.

4.6

Telemetry completes once; quit completes without a forced kill

Nested spans keep parentage; separate sessions keep distinct trace IDs. A throwing telemetry sink cannot fail dictation. Cancellation, finalization, and provider disposal leave no stuck background work when the app quits. Observe finalizer completion before process exit; an exit code alone is insufficient. Test repeated quit requests and the chosen error/timeout policy.

Gate: telemetry runtime/trace tests, lifecycle session-work tests, cloud provider tests, and an explicit packaged clean-exit check. The current E2E close helper can kill a hung app, so it is insufficient evidence by itself.

5Execution plan

Use one integration owner and one migration branch. Land compatible preparation separately if useful; merge the dependency cutover and all affected callers together.

5.1 · Baseline & target · ½ day

Freeze the comparison

Record the RC tag, dependency graph, compiler versions, test results, packaged startup behavior, and shutdown timing. Keep unrelated native submodule changes outside the migration diff. Add only missing contract tests that can run on v3. Mark which existing assertions describe product behavior and which describe v3 mechanics.

Exit gate: a reproducible v3 baseline and a named RC target. Current typecheck and unit-suite baseline already pass; packaging and live behavior still need capture.

5.2 · Design decisions & probes · 1–2 days

Choose what to migrate and what to rewrite

Run the retained characterization harness in 6.6, then extend focused probes for the chosen design. Test the exact RC’s runner startup, custom tracer, flattened causes, app-owned scope behavior, and FIFO lock primitives. Compare each rewrite candidate in 3.7 with a direct port. Present the recommended design, code removed, behavior differences, validation evidence, and effort impact before implementing a substantial rewrite or distributing wider file edits.

Exit gate: each candidate has a recorded decision with retained probe source, command, exact version, assertions, and output. A mismatch first triggers a design review; call it an upstream blocker only after checking the supported v4 approach.

5.3 · Coordinated source migration · 1–2 days

Update the graph and its consumers

Change the dependency/compiler pins. Implement the agreed runtime and error-boundary rewrites, then migrate generator receivers, service tags, fibers, queues, deferred values, callback cancellation, and tracing. Remove old adapters made obsolete by those changes. Update test fixtures and compile-time probes for the chosen contracts.

Exit gate: desktop typecheck passes; focused suites pass; searches find no obsolete APIs in executable source/tests. No new broad casts or suppressed errors hide incompatibilities.

5.4 · Full validation & review · 1–2 days

Exercise the real app boundaries

Run the full suite, lint/format checks, lifecycle contracts, desktop packaging, native dependency smoke, and packaged E2E. Verify dictation cancel, account changes, and completed finalizers before clean quit under active work. Add the full desktop unit suite to CI.

Exit gate: all required checks pass on the intended release platforms. Apply the simplify review to each major implementation pass, then rerun checks affected by any cleanup.

5.5 · Candidate & observation · ½–1 day + soak

Ship through the existing prerelease process

Review one concrete candidate with the pinned lockfile and validation evidence. Exercise repeated launch/dictation/logout/quit cycles, compare telemetry to the v3 baseline, and keep the previous artifact available.

Exit gate: stable behavior during the agreed internal observation window and a tested rollback path.

Planning estimate: approximately 4–8 engineer-days, plus CI/platform availability and an observation window. This is a provisional range, not a delivery commitment. Re-estimate after the design decisions and runtime probes. A wider boot-ownership or session redesign needs its own revised estimate before implementation.

5.6 Safe parallel work after runtime decisions
Owner Bounded assignment Dependency
Integration owner Package/lockfile, runtime bridges, service tags, shared error settlement, final integration. Owns the contract decisions first.
Agent A Auth, settings-sync, activity-reporting APIs and tests. Agreed runtime and Cause approach.
Agent B Live transcription, FIFO lock, gRPC/HTTP bridges, lifecycle session-work and tests. Agreed fiber/deferred and immediate-start approach.
Agent C Independent compatibility review, CI/unit gate, packaged checks, regression audit. Compiling integration branch.

Give each editor distinct files. Integrate shared primitives centrally before branch-wide verification.

6Validation

Observed today · Effect 3.22.1

Typecheck passed. 140 test files passed. 1,321 tests passed. The full Vitest run completed in 9.27 seconds. These results establish the old-version baseline; they do not prove RC compatibility.

6.1 Required commands

Run from the repository root. Start with focused suites during edits, then run this full gate once the integrated change is ready.

pnpm type:check
pnpm --filter @amical/desktop test
pnpm --filter @amical/desktop lint
pnpm --filter @amical/desktop format:check
pnpm --filter @amical/desktop test:lifecycle-contract
pnpm --filter @amical/desktop verify:lifecycle-contract-version BASE_SHA

Replace BASE_SHA with the migration PR’s base revision; the version-check command requires it.

6.2 Package and test using the existing CI build setup
pnpm --filter @amical/types generate:all
pnpm --filter @amical/whisper-wrapper build
pnpm --filter @amical/whisper-wrapper build:native
pnpm --filter @amical/whisper-wrapper test:load
pnpm --filter @amical/desktop download-node

# macOS shell; use equivalent environment settings on Windows.
AMICAL_E2E_PACKAGE=1 SKIP_CODESIGNING=true SKIP_NOTARIZATION=true \
  TELEMETRY_ENABLED=false pnpm --filter @amical/desktop package
pnpm --filter @amical/desktop test:native-dependencies
pnpm --filter @amical/desktop test:e2e

Reuse .github/workflows/ci.yml for native setup, API/auth build settings, and platform architecture checks. test:e2e defaults to the packaged executable. test:e2e:bundle tests bundles with a development Electron binary and is only an additional check.

The desktop has no build script. Root pnpm build is not a desktop bundle gate; use Electron Forge’s package script. During edits, use the faster pnpm --filter @amical/desktop type:check; the full gate checks the workspace. Package/E2E commands require the project’s native tooling and test configuration.

6.3 Behavior matrix

Surface Existing coverage to retain RC-specific evidence
App graph app-layers, identity-subscriptions, service layers, window bridge. Crash reporting before blocked rollback; owned pending initialization; finalizer order and exactly-once cleanup; no duplicate service construction.
Dictation lifecycle lifecycle-session-work, live-transcription-session, token-lock. Immediate first start; cancel at acquisition/handoff; late callbacks after retirement.
Error model tests/effect/error-model-probes.*, tests/types/error-model.test.ts, defect-capture tests. Mixed failure/defect order, exact error identity, each dropped defect captured once, pure interruption.
Identity and requests Auth refresh/retry, settings sync, activity reporting, cloud provider suites. Concurrent refresh; account switch during active I/O; queue and semaphore shutdown; gRPC abort cleanup.
Telemetry telemetry-runtime, dictation-trace. Tracer fields/timestamps; parentage; one completion event; a failing sink stays isolated.
Packaged app Native dependency smoke and packaged launch/onboarding E2E. Effect module loading, real dictation success/cancel, and finalizer completion before clean process exit with active background work, including repeated quit and cleanup failure.

6.4 CI gate updated

The updated CI gate typechecks and runs the complete desktop Vitest suite, including lifecycle replay, before packaging and E2E on macOS arm64 and Windows x64/arm64. The separate lifecycle contract-version check remains. Releases also ship macOS x64. This local pass validates macOS arm64; the other shipped architectures still need packaged verification before broad release.

6.5 Limits of the current evidence
  • The full application suite and typechecks ran on v4, with local macOS arm64 packaging, native smoke checks, and packaged E2E. Windows x64/arm64 and macOS x64 were not validated locally, and no remote CI run was started.
  • Live microphone dictation and graceful quit under real active provider work were not manually exercised. Native whisper binaries were smoke-tested without rebuilding the user's separately modified whisper source.
  • e2e/helpers/launch.ts:114 races shutdown against a timeout and catches errors before killing the app. A passing existing E2E result does not prove graceful shutdown.
  • Some passing v3 tests log expected failures; a ServiceManager mock also warns that app.getAppPath is unavailable. Compare new output to the captured baseline instead of treating all stderr as a regression.
  • Existing coverage should be extended only for changed or uncovered behavior. Avoid tests that merely repeat renamed API calls.
6.6 Reproduce the RC observations

Companion probe source uses Node’s built-in assertions and verifies the package version before loading it. Run from the repository root with Node 24. This downloads the exact RC into a temporary directory and leaves the application dependencies unchanged.

effect_probe_dir=$(mktemp -d "${TMPDIR:-/tmp}/amical-effect4.XXXXXX")
npm pack effect@4.0.0-rc.115 --ignore-scripts --pack-destination "$effect_probe_dir"
tar -xzf "$effect_probe_dir/effect-4.0.0-rc.115.tgz" -C "$effect_probe_dir"
node docs/plans/effect-4-probes.mjs "$effect_probe_dir/package"

Shell commands above work on macOS/Linux; on Windows, extract the tarball into a temporary directory and pass its package path to the same Node command. This RC has no runtime dependencies, so its extracted distribution runs directly.

Checks cover generator receiver binding; root/child startup; static tracer injection; cold synchronous/asynchronous ManagedRuntime initialization; mixed errors and defects; raw defect identity through spans; and semaphore overtaking under the default scheduler. The waiter probe starts its earlier waiter synchronously, then releases and reacquires in one fiber before the scheduled wake-up runs.

Effect 4.0.0-rc.115; Node v24.18.0
… individual assertions and observations …
Semaphore overtaking: new arrival -> queued earlier
All RC characterization checks passed.

Reconstructed from the exploratory cases, retained, and rerun during this review. Passing these checks proves the listed library behavior; application tests separately cover boot ownership, pending initialization, session cancellation, and cleanup. Existing packaged E2E does not prove graceful Electron quit under active provider work. If the target RC changes, review the harness’s version pin and expected semantics explicitly.

7Release & rollback

7.1 Release criteria

  • Exact RC/compiler pins and reviewed lockfile.
  • All six behavior contracts pass with no unresolved new failures.
  • Full unit suite runs in CI; packaged checks pass for release targets.
  • Internal sessions show no new dropped dictation, duplicate telemetry, wrong-account writes, or stuck quit.
  • Compare startup, stop-to-paste latency, and memory after repeated sessions with the v3 baseline. Investigate sustained regressions before promotion.

7.2 Rollback unit

Keep the dependency update, API migration, and corresponding tests as one revertible release unit. If a regression appears, pause promotion and use a corrective build from the known-good v3 code and lockfile.

Validate the updater’s version rules before choosing an older artifact directly. The plan makes no data-format or cloud-protocol changes, so rollback should not require a database downgrade.

7.3 Decisions to settle during implementation

Decision Default in this draft Evidence that could change it
RC target 4.0.0-rc.115 A newer RC at kickoff; read its release notes and rerun the same gates.
Runtime replacement Explicit synchronous context runners for telemetry; scoped context capture for sync. Exact RC probes fail to retain current startup/cancellation behavior.
Release observation Internal candidate first; use the existing prerelease channel. Team release cadence and availability of macOS/Windows testers.
Broader adoption Upgrade existing Effect boundaries. A separate request to convert more services, validation, or renderer code.

These decisions do not block drafting or research. Any confirmed upstream incompatibility should be documented with a minimal reproduction and its effect on the app’s contract.

8Evidence & sources

Primary sources were checked on 15 September 2026. The version target comes from npm; migration API decisions should use the pinned release source. General website guidance can change after this snapshot.

  1. Official npm package tags and RC package metadata and publication history — observed target 4.0.0-rc.115; published 2026-09-11T17:20:13.421Z.
  2. Effect 4.0.0-rc.115 release — release notes and tagged source.
  3. Effect 4 installation — RC install channel, TypeScript and strict-mode requirements.
  4. Pinned migration index — guide to the v3 → v4 changes.
  5. Runtime, forking, and Tracer source — runner and span changes.
  6. Cause guide and recovery implementation — flattened reasons and mixed-cause recovery.
  7. Fiber, Deferred, and Queue — exact RC primitive declarations.
  8. Pinned README — Node ≥18 and TypeScript ≥5.9. The website installation page specifies Node ≥22.18 for its direct TypeScript workflow; Amical’s Node 24 baseline meets both. The pinned migration index still uses an older beta label; npm and the release identify this version as an RC.
8.1 Repository evidence index
  • apps/desktop/package.json:108,180, root package.json:18, pnpm-lock.yaml:239 — current dependency and compiler pins.
  • pnpm-workspace.yaml:6 — website excluded from workspace.
  • apps/desktop/src/main/runtime/{tags,app-runtime,layer-helpers,telemetry-runtime}.ts — service identity, app ownership, and immediate-start runtime.
  • apps/desktop/src/types/errors/settle.ts and src/services/transcription/{live-transcription-session,token-lock}.ts — settlement and cancellation contracts.
  • apps/desktop/src/services/{auth-service,settings-sync-supervisor,activity-reporting-service}.ts — semaphores, queue ownership, and identity fencing.
  • apps/desktop/vitest.config.ts, tsconfig.json, vite.main.config.mts — test discovery, compiler, and bundled main entry.
  • .github/workflows and apps/desktop/e2e/helpers/launch.ts:114 — release gates and shutdown-test limitation.

Working-tree observation before writing: the packages/whisper-wrapper/whisper.cpp submodule already had modifications. This plan does not alter them.