From bb1ce430bce35bc44db5d1545f66d59d865548ec Mon Sep 17 00:00:00 2001 From: Kyle Mistele Date: Fri, 28 Aug 2026 20:12:40 -0700 Subject: [PATCH 1/2] fix: preserve services across actor start HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a04b66-3e22-7b86-8b68-d04324d9a696 --- .changeset/capture-actor-service-context.md | 5 +++++ src/actor.ts | 13 +++++++++++-- src/internal/runtime.ts | 8 ++++---- test/services.test.ts | 15 +++++++++++++++ 4 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 .changeset/capture-actor-service-context.md diff --git a/.changeset/capture-actor-service-context.md b/.changeset/capture-actor-service-context.md new file mode 100644 index 0000000..7ba61c0 --- /dev/null +++ b/.changeset/capture-actor-service-context.md @@ -0,0 +1,5 @@ +--- +"@humanlayer/effect-machine": patch +--- + +Preserve Effect service dependencies supplied while allocating a local actor, so state effects run correctly when `actor.start` is called later. diff --git a/src/actor.ts b/src/actor.ts index 75cfc49..17614f3 100644 --- a/src/actor.ts +++ b/src/actor.ts @@ -772,6 +772,7 @@ export const createActor = Effect.fn("effect-machine.actor.spawn")(function* < }, ) { const lifecycle: Lifecycle | undefined = options?.lifecycle; + const serviceContext = yield* Effect.context(); // Spawn is cold — initial state from hydrate or machine.initial. // Recovery runs during start, not allocate. @@ -998,7 +999,11 @@ export const createActor = Effect.fn("effect-machine.actor.spawn")(function* < if (implicitSystemScope !== undefined) { yield* Scope.close(implicitSystemScope, Exit.void); } - }).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid); + }).pipe( + Effect.provide(serviceContext), + Effect.withSpan("effect-machine.actor.stop"), + Effect.asVoid, + ); // Track whether hydrate was provided — skip recovery when hydrated const isHydrated = options?.initialState !== undefined; @@ -1078,7 +1083,11 @@ export const createActor = Effect.fn("effect-machine.actor.spawn")(function* < if (currentRuntime !== undefined) { yield* currentRuntime.start; } - }).pipe(Effect.withSpan("effect-machine.actor.start"), Effect.asVoid); + }).pipe( + Effect.provide(serviceContext), + Effect.withSpan("effect-machine.actor.start"), + Effect.asVoid, + ); return buildActorRefCore( id, diff --git a/src/internal/runtime.ts b/src/internal/runtime.ts index 2e61a17..fe8be0e 100644 --- a/src/internal/runtime.ts +++ b/src/internal/runtime.ts @@ -222,8 +222,8 @@ export const createRuntime = Effect.fn("effect-machine.runtime.create")(function ) { const { actorId, hooks, lifecycle } = config; - // Capture context for fire-and-forget Deferred settlement (runForkWith) - const services = yield* Effect.context(); + // Capture services at allocation so start, stop, and deferred settlement retain them. + const services = yield* Effect.context(); const fork = Effect.runForkWith(services); // Resources: use cell-provided or allocate fresh @@ -494,8 +494,8 @@ export const createRuntime = Effect.fn("effect-machine.runtime.create")(function return { ...makeHandle(stateRef, stoppedRef, eventQueue, exitDeferred, actorScope), - stop, - start, + stop: stop.pipe(Effect.provide(services)), + start: start.pipe(Effect.provide(services)), }; }); diff --git a/test/services.test.ts b/test/services.test.ts index bd505d2..29ff0b9 100644 --- a/test/services.test.ts +++ b/test/services.test.ts @@ -70,6 +70,10 @@ const streamMachine = Machine.make({ ) .final(StreamState.Done); +const GreetingLive = Layer.succeed(GreetingService, { + greet: (name) => Effect.succeed(`Hello, ${name}!`), +}); + describe("service requirements", () => { it.scopedLive("runs state tasks with services supplied by a Layer", () => Effect.gen(function* () { @@ -105,6 +109,17 @@ describe("service requirements", () => { ), ); + it.scopedLive("preserves services provided around actor allocation for start", () => + Effect.gen(function* () { + const actor = yield* Machine.spawn(streamMachine).pipe(Effect.provide(GreetingLive)); + yield* actor.start; + yield* actor.send(StreamEvent.Start); + + const state = yield* actor.awaitFinal; + expect(state).toEqual(StreamState.Done({ message: "Hello, Grace!" })); + }), + ); + it.scopedLive("propagates service requirements through ActorSystem.spawn", () => Effect.gen(function* () { const system = yield* ActorSystemService; From 2a64c6c91d8ef759dcc23ac64b36acb1b6b90a8a Mon Sep 17 00:00:00 2001 From: Kyle Mistele Date: Fri, 28 Aug 2026 20:12:46 -0700 Subject: [PATCH 2/2] chore: validate format before pushing HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a04b66-3e22-7b86-8b68-d04324d9a696 --- lefthook.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lefthook.yml b/lefthook.yml index a99166b..ba6c6fe 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -11,3 +11,13 @@ pre-commit: run: bun run typecheck - name: test run: bun run test + +pre-push: + parallel: true + jobs: + - name: fmt + run: bun run fmt:check + - name: lint + run: bun run lint + - name: typecheck + run: bun run typecheck