Effect 4.
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.
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.
closeAppScopenow logs the complete cause throughEffect.tapCausebefore 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
sampledvalue. 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.yieldNowschedules 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
orDiedrops co-present cleanup defects when a typed error is present. Three existing fatal service boundaries now useorDiePreservingCause, 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.0pnpm 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 insrc/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, andservices/activity-reporting-service.ts. The cloud provider also usesManagedRuntime.
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]
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.CloseableScopeScope.extend
|
Scope.CloseableScope.provide
|
Preserve the chosen scope ownership and finalizer behavior; review any boot-ownership rewrite under 3.7 first. Scope guide |
Effect.asyncEffect.zipRight / zipLeftEffect.yieldNow()
|
Effect.callbackEffect.andThen / tapEffect.yieldNow (value)
|
Check callback cleanup and abort paths. Sequence combinators must retain return values. Effect declarations |
Effect.makeSemaphoreEffect.Semaphore
|
Semaphore.makeSemaphore.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
|
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
|
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 |
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
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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:114races 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.getAppPathis 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.
-
Official npm package tags
and
RC package metadata
and
publication history
— observed target
4.0.0-rc.115; published2026-09-11T17:20:13.421Z. - Effect 4.0.0-rc.115 release — release notes and tagged source.
- Effect 4 installation — RC install channel, TypeScript and strict-mode requirements.
- Pinned migration index — guide to the v3 → v4 changes.
- Runtime, forking, and Tracer source — runner and span changes.
- Cause guide and recovery implementation — flattened reasons and mixed-cause recovery.
- Fiber, Deferred, and Queue — exact RC primitive declarations.
- 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, rootpackage.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.tsandsrc/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/workflowsandapps/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.