@swirls/looms 0.0.2 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/CHANGELOG.md +9 -2
  2. package/dist/actor/cell.d.ts +9 -4
  3. package/dist/actor/cell.js +7 -11
  4. package/dist/actor/host.d.ts +7 -3
  5. package/dist/actor/host.js +55 -32
  6. package/dist/actor/index.d.ts +1 -1
  7. package/dist/actor/index.js +1 -1
  8. package/dist/actor/routing.d.ts +12 -1
  9. package/dist/actor/routing.js +57 -51
  10. package/dist/agent/agent-session.d.ts +5 -0
  11. package/dist/agent/agent-session.js +270 -0
  12. package/dist/agent/definitions-store.d.ts +2 -3
  13. package/dist/agent/definitions-store.js +2 -4
  14. package/dist/agent/definitions.d.ts +46 -20
  15. package/dist/agent/definitions.js +36 -8
  16. package/dist/agent/effects.d.ts +359 -2
  17. package/dist/agent/effects.js +60 -34
  18. package/dist/agent/events.d.ts +175 -19
  19. package/dist/agent/events.js +45 -10
  20. package/dist/agent/index.d.ts +1 -0
  21. package/dist/agent/index.js +1 -0
  22. package/dist/agent/llm.d.ts +11 -2
  23. package/dist/agent/llm.js +13 -3
  24. package/dist/agent/module.d.ts +812 -78
  25. package/dist/agent/module.js +16 -4
  26. package/dist/agent/scope.d.ts +174 -18
  27. package/dist/agent/signals.d.ts +24 -1
  28. package/dist/agent/signals.js +77 -0
  29. package/dist/agent/threads.d.ts +3 -33
  30. package/dist/agent/threads.js +20 -25
  31. package/dist/agent/tool-schema.js +0 -1
  32. package/dist/agent/types.d.ts +49 -4
  33. package/dist/agent/types.js +5 -5
  34. package/dist/ai-vercel/index.d.ts +1 -0
  35. package/dist/ai-vercel/index.js +1 -0
  36. package/dist/ai-vercel/messages.js +22 -7
  37. package/dist/ai-vercel/vercel-evaluator.d.ts +23 -0
  38. package/dist/ai-vercel/vercel-evaluator.js +68 -0
  39. package/dist/ai-vercel/vercel-llm.js +64 -44
  40. package/dist/approval/effects.d.ts +89 -0
  41. package/dist/approval/module.d.ts +179 -29
  42. package/dist/approval/projections.d.ts +0 -14
  43. package/dist/approval/projections.js +20 -3
  44. package/dist/approval/signals.d.ts +2 -0
  45. package/dist/approval/signals.js +18 -2
  46. package/dist/cli/cli.js +2 -0
  47. package/dist/cli/commands.js +12 -16
  48. package/dist/client/client.d.ts +1195 -18
  49. package/dist/client/client.js +118 -120
  50. package/dist/client/index.d.ts +1 -1
  51. package/dist/client/index.js +1 -1
  52. package/dist/client/sse.d.ts +13 -0
  53. package/dist/client/sse.js +38 -44
  54. package/dist/cloudflare/alarm-scheduler.d.ts +12 -2
  55. package/dist/cloudflare/alarm-scheduler.js +34 -7
  56. package/dist/cloudflare/durable-object.d.ts +10 -2
  57. package/dist/cloudflare/durable-object.js +59 -28
  58. package/dist/cloudflare/event-store.d.ts +7 -2
  59. package/dist/cloudflare/event-store.js +8 -7
  60. package/dist/cloudflare/index.d.ts +2 -2
  61. package/dist/cloudflare/index.js +2 -2
  62. package/dist/cloudflare/router.js +13 -14
  63. package/dist/core/catalog.d.ts +3 -3
  64. package/dist/core/catalog.js +2 -5
  65. package/dist/core/coalesce.js +5 -3
  66. package/dist/core/compose.d.ts +2 -1
  67. package/dist/core/compose.js +18 -2
  68. package/dist/core/definition-store.d.ts +11 -0
  69. package/dist/core/definition-store.js +6 -0
  70. package/dist/core/effects.d.ts +127 -15
  71. package/dist/core/effects.js +89 -17
  72. package/dist/core/envelope.d.ts +27 -7
  73. package/dist/core/envelope.js +20 -6
  74. package/dist/core/event-codec.d.ts +4 -2
  75. package/dist/core/event-codec.js +10 -5
  76. package/dist/core/event-index.js +1 -3
  77. package/dist/core/fold.d.ts +3 -0
  78. package/dist/core/fold.js +171 -24
  79. package/dist/core/ids.d.ts +5 -0
  80. package/dist/core/ids.js +35 -2
  81. package/dist/core/index.d.ts +5 -1
  82. package/dist/core/index.js +5 -1
  83. package/dist/core/kind.d.ts +22 -0
  84. package/dist/core/kind.js +17 -0
  85. package/dist/core/limits.d.ts +34 -0
  86. package/dist/core/limits.js +46 -0
  87. package/dist/core/match.d.ts +1 -0
  88. package/dist/core/match.js +10 -1
  89. package/dist/core/module-scope.d.ts +22 -20
  90. package/dist/core/module-scope.js +38 -28
  91. package/dist/core/module.d.ts +16 -5
  92. package/dist/core/module.js +4 -0
  93. package/dist/core/projection.d.ts +13 -13
  94. package/dist/core/projection.js +9 -5
  95. package/dist/core/protocol.d.ts +68 -11
  96. package/dist/core/protocol.js +82 -11
  97. package/dist/core/replay.d.ts +312 -3
  98. package/dist/core/replay.js +11 -1
  99. package/dist/core/schema.d.ts +8 -19
  100. package/dist/core/schema.js +25 -80
  101. package/dist/core/serialize.js +2 -4
  102. package/dist/core/snapshot-store.d.ts +2 -1
  103. package/dist/core/snapshot-store.js +8 -2
  104. package/dist/core/snapshots.d.ts +5 -1
  105. package/dist/core/snapshots.js +10 -15
  106. package/dist/core/sqlite-store.js +56 -11
  107. package/dist/core/state.d.ts +142 -4
  108. package/dist/core/state.js +78 -0
  109. package/dist/core/store.d.ts +38 -3
  110. package/dist/core/store.js +77 -22
  111. package/dist/core/thread.d.ts +10 -9
  112. package/dist/core/types.d.ts +5 -8
  113. package/dist/core/types.js +28 -24
  114. package/dist/core/validate-event.js +14 -4
  115. package/dist/core/worker-callback.d.ts +17 -0
  116. package/dist/core/worker-callback.js +1 -0
  117. package/dist/debugger/components/event-stream.js +9 -10
  118. package/dist/debugger/events/protocol-summarize.js +26 -0
  119. package/dist/debugger/lib/event-meta.js +1 -2
  120. package/dist/evaluator/definitions-store.d.ts +11 -0
  121. package/dist/evaluator/definitions-store.js +8 -0
  122. package/dist/evaluator/definitions.d.ts +18 -0
  123. package/dist/evaluator/definitions.js +4 -0
  124. package/dist/evaluator/effects.d.ts +226 -0
  125. package/dist/evaluator/effects.js +132 -0
  126. package/dist/evaluator/evaluator.d.ts +24 -0
  127. package/dist/evaluator/evaluator.js +67 -0
  128. package/dist/evaluator/events.d.ts +59 -0
  129. package/dist/evaluator/events.js +21 -0
  130. package/dist/evaluator/index.d.ts +11 -0
  131. package/dist/evaluator/index.js +11 -0
  132. package/dist/evaluator/module.d.ts +606 -0
  133. package/dist/evaluator/module.js +25 -0
  134. package/dist/evaluator/projections.d.ts +50 -0
  135. package/dist/evaluator/projections.js +20 -0
  136. package/dist/evaluator/scope.d.ts +59 -0
  137. package/dist/evaluator/scope.js +7 -0
  138. package/dist/evaluator/signals.d.ts +3 -0
  139. package/dist/evaluator/signals.js +17 -0
  140. package/dist/evaluator/threads.d.ts +62 -0
  141. package/dist/evaluator/threads.js +117 -0
  142. package/dist/evaluator/types.d.ts +125 -0
  143. package/dist/evaluator/types.js +51 -0
  144. package/dist/projectors/delivery.d.ts +83 -0
  145. package/dist/projectors/delivery.js +182 -0
  146. package/dist/projectors/index-model.d.ts +1 -1
  147. package/dist/projectors/index-model.js +4 -6
  148. package/dist/projectors/index.d.ts +2 -0
  149. package/dist/projectors/index.js +2 -0
  150. package/dist/projectors/memory.d.ts +1 -1
  151. package/dist/projectors/memory.js +10 -8
  152. package/dist/projectors/postgres.js +44 -58
  153. package/dist/projectors/projector.d.ts +17 -0
  154. package/dist/projectors/projector.js +26 -1
  155. package/dist/projectors/sqlite-delivery.d.ts +4 -0
  156. package/dist/projectors/sqlite-delivery.js +147 -0
  157. package/dist/projectors/sqlite.js +23 -23
  158. package/dist/projectors/with-projectors.d.ts +7 -2
  159. package/dist/projectors/with-projectors.js +27 -28
  160. package/dist/react/notify.js +11 -8
  161. package/dist/react/react.d.ts +2 -3
  162. package/dist/react/react.js +14 -30
  163. package/dist/react/sse.d.ts +1 -1
  164. package/dist/react/sse.js +1 -1
  165. package/dist/react/store.d.ts +2 -2
  166. package/dist/react/store.js +109 -119
  167. package/dist/runtime/cancellation.d.ts +16 -0
  168. package/dist/runtime/cancellation.js +129 -0
  169. package/dist/runtime/deadline-transition-planner.d.ts +31 -0
  170. package/dist/runtime/deadline-transition-planner.js +139 -0
  171. package/dist/runtime/dispatch-effect.d.ts +3 -2
  172. package/dist/runtime/dispatch-effect.js +7 -13
  173. package/dist/runtime/errors.d.ts +51 -0
  174. package/dist/runtime/errors.js +39 -0
  175. package/dist/runtime/{runtime-helpers.d.ts → helpers.d.ts} +24 -4
  176. package/dist/runtime/{runtime-helpers.js → helpers.js} +56 -19
  177. package/dist/runtime/index.d.ts +3 -3
  178. package/dist/runtime/index.js +3 -3
  179. package/dist/runtime/ingress.d.ts +15 -0
  180. package/dist/runtime/ingress.js +42 -0
  181. package/dist/runtime/local-effect-executor.d.ts +21 -0
  182. package/dist/runtime/local-effect-executor.js +137 -0
  183. package/dist/runtime/looms.d.ts +22 -4
  184. package/dist/runtime/looms.js +54 -36
  185. package/dist/runtime/observer.d.ts +45 -0
  186. package/dist/runtime/observer.js +11 -0
  187. package/dist/runtime/repository.d.ts +55 -0
  188. package/dist/runtime/repository.js +278 -0
  189. package/dist/runtime/retry-policy.d.ts +16 -0
  190. package/dist/runtime/retry-policy.js +29 -0
  191. package/dist/runtime/run-execution-context.d.ts +30 -0
  192. package/dist/runtime/run-execution-context.js +56 -0
  193. package/dist/runtime/run-start.d.ts +4 -0
  194. package/dist/runtime/run-start.js +44 -0
  195. package/dist/runtime/runtime.d.ts +6 -87
  196. package/dist/runtime/runtime.js +233 -470
  197. package/dist/runtime/server/auth.d.ts +40 -0
  198. package/dist/runtime/server/auth.js +40 -0
  199. package/dist/runtime/server/fetch-handler.d.ts +14 -0
  200. package/dist/runtime/server/fetch-handler.js +51 -0
  201. package/dist/runtime/server/http.d.ts +37 -0
  202. package/dist/runtime/server/http.js +62 -0
  203. package/dist/runtime/server/route-table.d.ts +55 -0
  204. package/dist/runtime/server/route-table.js +74 -0
  205. package/dist/runtime/server/routes/effects.d.ts +3 -0
  206. package/dist/runtime/server/routes/effects.js +59 -0
  207. package/dist/runtime/server/routes/operations.d.ts +3 -0
  208. package/dist/runtime/server/routes/operations.js +14 -0
  209. package/dist/runtime/server/routes/runs.d.ts +3 -0
  210. package/dist/runtime/server/routes/runs.js +192 -0
  211. package/dist/runtime/server.d.ts +2 -8
  212. package/dist/runtime/server.js +4 -211
  213. package/dist/runtime/sse.d.ts +2 -1
  214. package/dist/runtime/sse.js +24 -17
  215. package/dist/runtime/types.d.ts +115 -0
  216. package/dist/runtime/types.js +1 -0
  217. package/dist/runtime/wake-engine.d.ts +20 -0
  218. package/dist/runtime/wake-engine.js +311 -0
  219. package/dist/runtime/wake-scheduler.d.ts +3 -4
  220. package/dist/runtime/wake-scheduler.js +25 -31
  221. package/dist/runtime/worker-callbacks.d.ts +16 -0
  222. package/dist/runtime/worker-callbacks.js +167 -0
  223. package/dist/runtime/worker-effect-dispatch.d.ts +36 -0
  224. package/dist/runtime/worker-effect-dispatch.js +126 -0
  225. package/dist/s2/config.d.ts +1 -1
  226. package/dist/s2/config.js +1 -1
  227. package/dist/s2/lite.js +70 -82
  228. package/dist/s2/pagination.d.ts +15 -0
  229. package/dist/s2/pagination.js +36 -0
  230. package/dist/s2/projector.js +6 -5
  231. package/dist/s2/snapshot-store.js +148 -163
  232. package/dist/s2/store.d.ts +3 -21
  233. package/dist/s2/store.js +71 -165
  234. package/dist/testing/index.d.ts +15 -0
  235. package/dist/testing/index.js +53 -16
  236. package/dist/workflow/definitions-store.d.ts +2 -3
  237. package/dist/workflow/definitions-store.js +2 -2
  238. package/dist/workflow/definitions.d.ts +78 -22
  239. package/dist/workflow/definitions.js +80 -14
  240. package/dist/workflow/effects.d.ts +453 -107
  241. package/dist/workflow/effects.js +214 -115
  242. package/dist/workflow/events.d.ts +149 -5
  243. package/dist/workflow/events.js +27 -5
  244. package/dist/workflow/graph.d.ts +56 -0
  245. package/dist/workflow/graph.js +207 -0
  246. package/dist/workflow/index.d.ts +1 -0
  247. package/dist/workflow/index.js +1 -0
  248. package/dist/workflow/module.d.ts +1047 -266
  249. package/dist/workflow/projections.d.ts +14 -12
  250. package/dist/workflow/projections.js +30 -8
  251. package/dist/workflow/scope.d.ts +146 -4
  252. package/dist/workflow/threads.d.ts +383 -155
  253. package/dist/workflow/threads.js +308 -110
  254. package/package.json +7 -2
  255. package/dist/runtime/events-api.d.ts +0 -4
  256. package/dist/runtime/events-api.js +0 -84
@@ -1,213 +1,138 @@
1
- import { Data, Effect, Option, Predicate } from 'effect';
2
- import { advanceCursor, assignSequences, composeModules, createCoalescingAppender, createEvent, createThreadId, createRunId, DEFAULT_SNAPSHOT_EVERY, emptyCursor, EventStoreTag, EventStoreTrimmedError, foldRun, hashRunState, isRunTerminal, isWaitOnTimer, project, replayTo, validateEventInput, withdrawnError, } from '@swirls/looms/core';
3
- import { dispatchEffect } from './dispatch-effect.js';
4
- import { createEffectFailedEvent, createLiveEvent, groupOutstanding, materializeEffectOutcome, moduleServices, resolveSnapshotStore, RunCursorCache, stripSeq, synthesizedThreadFailed, threadStartedEvents, validateAndCreateEvents, waitSatisfiedEvents, } from './runtime-helpers.js';
5
- import { createTimeoutScheduler, toEffectVoid } from './wake-scheduler.js';
1
+ import { Clock, Effect, ManagedRuntime, Predicate } from 'effect';
2
+ import { composeModules, createEvent, createThreadId, createRunId, DEFAULT_DEFINITION_VERSION, DEFAULT_SNAPSHOT_EVERY, definitionKey, EventStoreTag, foldRun, project, replayTo, } from '@swirls/looms/core';
3
+ import { createCancellation } from './cancellation.js';
4
+ import { nextRuntimeDeadline } from './deadline-transition-planner.js';
5
+ import { RuntimeExecutionError } from './errors.js';
6
+ import { admitExternalEvents, moduleServices, resolveSnapshotStore, RunCursorCache, threadStartedEvents, UnknownDefinitionError, validateAndCreateEvents, waitSatisfiedEvents, } from './helpers.js';
7
+ import { createRunIngress } from './ingress.js';
8
+ import { notifyObserver } from './observer.js';
9
+ import { createRuntimeRepository } from './repository.js';
10
+ import { RunExecutionContexts } from './run-execution-context.js';
11
+ import { assertSameDurableStart } from './run-start.js';
12
+ import { createWakeEngine } from './wake-engine.js';
13
+ import { createTimeoutScheduler } from './wake-scheduler.js';
14
+ import { createWorkerCallbacks } from './worker-callbacks.js';
6
15
  export { createTimeoutScheduler } from './wake-scheduler.js';
7
- export class DuplicateEffectDispatchError extends Data.TaggedError('DuplicateEffectDispatchError') {
8
- constructor(runId, effectId, threadId) {
9
- super({
10
- runId,
11
- effectId,
12
- threadId,
13
- message: `Duplicate effect dispatch detected for effectId "${effectId}" on thread "${threadId}"; refusing to re-dispatch already executed effect`,
14
- });
15
- this.name = 'DuplicateEffectDispatchError';
16
- }
16
+ export { DuplicateEffectDispatchError, MaxWakeIterationsError, StartRunConflictError, } from './errors.js';
17
+ export { RunOverloadedError } from './ingress.js';
18
+ function currentTimeMillis() {
19
+ return Math.floor(performance.timeOrigin + performance.now());
17
20
  }
18
- export class MaxWakeIterationsError extends Data.TaggedError('MaxWakeIterationsError') {
19
- constructor(runId, maxIterations) {
20
- super({
21
- runId,
22
- maxIterations,
23
- message: `Max wake iterations exceeded (${maxIterations}) for run "${runId}"; possible infinite loop`,
24
- });
25
- this.name = 'MaxWakeIterationsError';
21
+ /** Threads whose in-process effects an admitted signal interrupts. */
22
+ function interruptedThreadIds(events, rules) {
23
+ const threadIds = new Set();
24
+ for (const event of events) {
25
+ const payload = Predicate.isObject(event.payload) ? event.payload : {};
26
+ if (event.type === 'runtime.thread.cancel.requested' && Predicate.isString(payload.threadId)) {
27
+ threadIds.add(payload.threadId);
28
+ }
29
+ for (const rule of rules) {
30
+ for (const threadId of rule(event)) {
31
+ threadIds.add(threadId);
32
+ }
33
+ }
26
34
  }
35
+ return threadIds;
27
36
  }
28
37
  const DEFAULT_RUN_CACHE_SIZE = 0;
38
+ const RESCAN_CONCURRENCY = 16;
29
39
  const DEFAULT_KEEP_SNAPSHOTS = 1;
40
+ const DEFAULT_MAX_PENDING_RUN_OPERATIONS = 256;
30
41
  export function createRuntime(options) {
31
42
  const registry = composeModules(options.modules);
32
43
  const registeredDefinitions = registry.definitions;
33
- const definitions = new Map(registeredDefinitions.map((def) => [`${def.kind}:${def.name}`, def]));
34
- const services = moduleServices(options.modules);
35
- const waking = new Set();
36
- /** Coalesce wake requests that arrive while a wake is in flight for the same run. */
37
- const wakeAgain = new Set();
44
+ const definitions = new Map(registeredDefinitions.map((def) => [definitionKey(def.kind, def.name, def.version), def]));
45
+ // Module composition erases the heterogeneous service identifier union at this host boundary.
46
+ // oxlint-disable-next-line effecttsgo/any-unknown-in-error-context
47
+ const services = ManagedRuntime.make(moduleServices(options.modules));
48
+ const executionContexts = new RunExecutionContexts();
49
+ const maxPendingRunOperations = options.maxPendingRunOperations ?? DEFAULT_MAX_PENDING_RUN_OPERATIONS;
38
50
  const snapshotEvery = options.snapshotEvery ?? DEFAULT_SNAPSHOT_EVERY;
39
51
  const maxWakeIterations = options.maxWakeIterations ?? 100;
40
52
  const snapshotStore = resolveSnapshotStore(options.snapshotStore, options.store);
41
53
  const trimAfterSnapshot = options.trimAfterSnapshot;
42
54
  const keepSnapshots = trimAfterSnapshot?.keepSnapshots ?? DEFAULT_KEEP_SNAPSHOTS;
43
55
  const runCache = new RunCursorCache(options.runCacheSize ?? DEFAULT_RUN_CACHE_SIZE);
44
- let liveStore = options.store;
45
- const liveCount = new Map();
46
- const liveAppends = createCoalescingAppender((runId, batch) => {
47
- const store = liveStore;
48
- if (!store) {
49
- return Effect.fail(new Error('Cannot append live events without a store'));
50
- }
51
- return store.append(runId, batch).pipe(Effect.tap((result) => Effect.sync(() => {
52
- liveCount.set(runId, (liveCount.get(runId) ?? 0) + result.sequences.length);
53
- })), Effect.asVoid);
54
- });
55
- const readRemaining = (store, runId, events, expectedTail) => Effect.gen(function* () {
56
- let collected = events;
57
- while (collected.length > 0) {
58
- const lastSeq = collected[collected.length - 1].seq;
59
- const target = expectedTail ?? (yield* store.tail(runId));
60
- if (lastSeq >= target) {
61
- break;
62
- }
63
- const more = yield* store.read(runId, {
64
- fromSeq: lastSeq + 1,
65
- limit: expectedTail !== undefined ? Math.max(0, target - lastSeq) : undefined,
66
- });
67
- if (more.length === 0 || more[0].seq <= lastSeq) {
68
- break;
69
- }
70
- collected = [...collected, ...more];
71
- }
72
- return collected;
56
+ const observe = (event) => notifyObserver(options.observer, event);
57
+ const withRunIngress = createRunIngress(executionContexts, maxPendingRunOperations);
58
+ const repository = createRuntimeRepository({
59
+ registry,
60
+ snapshotStore,
61
+ trimAfterSnapshot,
62
+ keepSnapshots,
63
+ runCache,
64
+ executionContexts,
65
+ observer: options.observer,
73
66
  });
74
- const catchUp = (store, runId, cursor, expectedTail) => Effect.gen(function* () {
75
- if (expectedTail !== undefined && cursor.seq === expectedTail) {
76
- return cursor;
77
- }
78
- const fromSeq = cursor.seq + 1;
79
- const limit = expectedTail !== undefined ? Math.max(0, expectedTail - cursor.seq) : undefined;
80
- let delta = yield* store.read(runId, { fromSeq, limit });
81
- if (delta.length === 0) {
82
- return cursor;
83
- }
84
- const firstSeq = delta[0].seq;
85
- if (firstSeq > fromSeq) {
86
- if (store.bounds) {
87
- const bounds = yield* store.bounds(runId);
88
- if (bounds.head > fromSeq) {
89
- return yield* Effect.fail(new EventStoreTrimmedError(runId, bounds.head, fromSeq));
90
- }
91
- }
92
- else {
93
- return yield* Effect.fail(new EventStoreTrimmedError(runId, firstSeq, fromSeq));
67
+ const { commit, loadCursor } = repository;
68
+ const signalAdmitted = (runId, events, signalOpts) => Effect.gen(function* () {
69
+ const store = yield* EventStoreTag;
70
+ const signalKey = signalOpts?.idempotencyKey;
71
+ const committed = yield* commit(store, runId, (current) => Effect.gen(function* () {
72
+ const processed = current.state.processedIdempotencyKeys;
73
+ const filteredEvents = signalKey && processed.includes(signalKey)
74
+ ? []
75
+ : events.filter((input) => {
76
+ const key = input.idempotencyKey ?? signalKey;
77
+ return !key || !processed.includes(key);
78
+ });
79
+ if (filteredEvents.length === 0) {
80
+ return { events: [], value: current.state };
94
81
  }
82
+ const batch = yield* validateAndCreateEvents(registry.catalogs, runId, filteredEvents.map((input) => ({
83
+ ...input,
84
+ idempotencyKey: input.idempotencyKey ?? signalKey,
85
+ })), { origin: { type: 'external' } });
86
+ const folded = foldRun(batch, registry, { runId, initial: current.state });
87
+ const satisfiedEvents = waitSatisfiedEvents(folded, batch).map((input) => createEvent(runId, input));
88
+ return {
89
+ events: [...batch, ...satisfiedEvents],
90
+ value: current.state,
91
+ idempotencyKey: signalKey ? `signal:${signalKey}` : undefined,
92
+ };
93
+ }));
94
+ if (committed.append) {
95
+ executionContexts.abort(runId, interruptedThreadIds(events, registry.interruptRules), 'Effect interrupted');
95
96
  }
96
- delta = yield* readRemaining(store, runId, delta, expectedTail);
97
- return advanceCursor(cursor, delta, registry);
97
+ return committed.value;
98
98
  });
99
- const loadCursorCold = (store, runId) => Effect.gen(function* () {
100
- let cursor = emptyCursor(runId);
101
- if (snapshotStore) {
102
- const snap = yield* snapshotStore
103
- .loadLatest(runId)
104
- .pipe(Effect.catch(() => Effect.succeed(Option.none())));
105
- if (Option.isSome(snap)) {
106
- cursor = {
107
- state: snap.value.state,
108
- seq: snap.value.cursor,
109
- durableSinceSnapshot: 0,
110
- };
111
- }
112
- }
113
- const fromSeq = cursor.seq + 1;
114
- let delta = yield* store.read(runId, { fromSeq });
115
- if (cursor.seq === 0 && delta[0] && delta[0].seq > 1) {
116
- if (store.bounds) {
117
- const bounds = yield* store.bounds(runId);
118
- if (bounds.head > 1) {
119
- return yield* Effect.fail(new EventStoreTrimmedError(runId, bounds.head, 1));
120
- }
121
- }
122
- else {
123
- return yield* Effect.fail(new EventStoreTrimmedError(runId, delta[0].seq, 1));
124
- }
125
- }
126
- if (delta.length > 0) {
127
- delta = yield* readRemaining(store, runId, delta);
128
- }
129
- return advanceCursor(cursor, delta, registry);
99
+ let scheduler;
100
+ let runtime;
101
+ const wakeEngine = createWakeEngine({
102
+ registry,
103
+ services,
104
+ definitions,
105
+ executionContexts,
106
+ repository,
107
+ snapshotEvery,
108
+ maxWakeIterations,
109
+ worker: options.worker,
110
+ observer: options.observer,
111
+ scheduler: () => scheduler,
130
112
  });
131
- const loadCursor = (store, runId) => Effect.gen(function* () {
132
- const cached = runCache.get(runId);
133
- if (cached) {
134
- const tail = yield* store.tail(runId);
135
- // A tail behind the cursor means the stream was recreated; the cache is poison.
136
- if (tail < cached.seq) {
137
- runCache.delete(runId);
138
- return yield* loadCursorCold(store, runId);
139
- }
140
- if (tail === cached.seq) {
141
- return cached;
142
- }
143
- const next = yield* catchUp(store, runId, cached, tail).pipe(Effect.catchTag('EventStoreTrimmedError', () => {
144
- runCache.delete(runId);
145
- return loadCursorCold(store, runId);
146
- }));
147
- runCache.set(runId, next);
148
- return next;
149
- }
150
- const next = yield* loadCursorCold(store, runId);
151
- runCache.set(runId, next);
152
- return next;
113
+ const workerCallbacks = createWorkerCallbacks({
114
+ registry,
115
+ repository,
116
+ withRunIngress,
117
+ observer: options.observer,
118
+ wake: wakeEngine,
153
119
  });
154
- /**
155
- * Save the current state externally, keep only `keepSnapshots`, and optionally
156
- * trim the event log behind the oldest kept snapshot. Failures are swallowed:
157
- * a snapshot is an optimization, the log stays authoritative.
158
- */
159
- const persistSnapshot = (store, runId, cursor) => Effect.gen(function* () {
160
- if (cursor.durableSinceSnapshot === 0) {
161
- return cursor;
162
- }
163
- yield* snapshotStore.save({
164
- runId,
165
- cursor: cursor.seq,
166
- stateHash: hashRunState(cursor.state),
167
- takenAt: Date.now(),
168
- state: cursor.state,
169
- });
170
- if (snapshotStore.prune) {
171
- yield* snapshotStore.prune(runId, keepSnapshots);
172
- }
173
- if (trimAfterSnapshot && store.trim && snapshotStore.listCursors) {
174
- const cursors = yield* snapshotStore.listCursors(runId);
175
- const oldestKept = cursors[Math.max(0, cursors.length - keepSnapshots)];
176
- if (oldestKept !== undefined) {
177
- yield* store.trim(runId, oldestKept + 1);
178
- }
179
- }
180
- return { ...cursor, durableSinceSnapshot: 0 };
181
- }).pipe(Effect.catch(() => Effect.succeed(cursor)));
182
- const applyAppend = (store, runId, cursor, batch, appendResult, commandRecords = 0) => Effect.gen(function* () {
183
- const echo = assignSequences(batch, appendResult);
184
- const first = echo[0];
185
- let next = cursor;
186
- if (first && first.seq !== cursor.seq + 1 + commandRecords) {
187
- const gap = first.seq - 1 - cursor.seq;
188
- const live = liveCount.get(runId) ?? 0;
189
- if (live === gap) {
190
- next = { ...cursor, seq: first.seq - 1 };
191
- }
192
- else {
193
- next = yield* catchUp(store, runId, cursor, first.seq - 1);
194
- }
195
- }
196
- next = advanceCursor(next, echo, registry);
197
- liveCount.set(runId, 0);
198
- if (next.seq !== appendResult.tail) {
199
- next = yield* catchUp(store, runId, next, appendResult.tail);
200
- }
201
- runCache.set(runId, next);
202
- return next;
120
+ const cancellation = createCancellation({
121
+ registry,
122
+ repository,
123
+ executionContexts,
124
+ worker: options.worker,
125
+ observer: options.observer,
126
+ wake: wakeEngine,
127
+ withRunIngress,
203
128
  });
204
- let scheduler;
205
- const runtime = {
129
+ runtime = {
206
130
  modules: options.modules,
207
131
  registry,
208
- listRuns: () => Effect.gen(function* () {
132
+ ...workerCallbacks,
133
+ listRuns: Effect.gen(function* () {
209
134
  const store = yield* EventStoreTag;
210
- return yield* store.listRuns();
135
+ return yield* store.listRuns;
211
136
  }),
212
137
  getEvents: (runId, readOptions) => Effect.gen(function* () {
213
138
  const store = yield* EventStoreTag;
@@ -226,306 +151,144 @@ export function createRuntime(options) {
226
151
  const events = yield* runtime.getEvents(runId);
227
152
  return replayTo(events, registry, seq);
228
153
  }),
229
- rescanTimers: () => Effect.gen(function* () {
154
+ inspectRun: (runId) => Effect.gen(function* () {
230
155
  const store = yield* EventStoreTag;
231
- const runIds = yield* store.listRuns();
232
- let count = 0;
233
- for (const runId of runIds) {
234
- const runState = yield* runtime.getRun(runId);
235
- if (isRunTerminal(runState)) {
236
- continue;
237
- }
238
- let earliestTimerAt = null;
239
- for (const waitRecord of Object.values(runState.waits)) {
240
- if (isWaitOnTimer(waitRecord.on)) {
241
- if (earliestTimerAt === null || waitRecord.on.timerAt < earliestTimerAt) {
242
- earliestTimerAt = waitRecord.on.timerAt;
243
- }
244
- }
245
- }
246
- if (earliestTimerAt !== null) {
247
- yield* toEffectVoid(scheduler.schedule(runId, earliestTimerAt));
248
- count += 1;
249
- }
250
- }
251
- return count;
156
+ const state = yield* runtime.getRun(runId);
157
+ const tail = yield* store.tail(runId);
158
+ const bounds = store.bounds ? yield* store.bounds(runId) : { head: 1, tail };
159
+ const effects = state.outstandingEffects.map((item) => {
160
+ const execution = state.effectExecutions[item.effectId];
161
+ return {
162
+ effectId: item.effectId,
163
+ threadId: item.threadId,
164
+ type: item.effect.type,
165
+ attempt: execution?.attempt ?? 0,
166
+ status: execution?.status ?? 'pending',
167
+ deadlineAt: execution?.deadlineAt ?? null,
168
+ lastError: execution?.lastError ?? null,
169
+ };
170
+ });
171
+ return { runId, state, head: bounds.head, tail, effects };
252
172
  }),
253
- startRun: (args) => Effect.gen(function* () {
254
- if (!definitions.get(`${args.kind}:${args.definitionName}`)) {
255
- return yield* Effect.fail(new Error(`Unknown definition ${args.kind}:${args.definitionName}`));
256
- }
173
+ retryEffect: (runId, effectId) => withRunIngress(runId, Effect.gen(function* () {
257
174
  const store = yield* EventStoreTag;
258
- const runId = args.runId ?? createRunId();
259
- const existing = yield* loadCursor(store, runId);
260
- if (existing.seq > 0) {
261
- if ((args.idempotencyKey &&
262
- existing.state.processedIdempotencyKeys?.includes(args.idempotencyKey)) ||
263
- existing.state.rootThreadId) {
264
- return {
265
- runId,
266
- threadId: existing.state.rootThreadId ?? '',
267
- state: existing.state,
268
- };
175
+ yield* commit(store, runId, (cursor) => Effect.gen(function* () {
176
+ const execution = cursor.state.effectExecutions[effectId];
177
+ const item = cursor.state.outstandingEffects.find((candidate) => candidate.effectId === effectId);
178
+ if (!execution || !item) {
179
+ return yield* new RuntimeExecutionError(`Unknown outstanding effect "${effectId}"`);
269
180
  }
270
- }
271
- const threadId = args.threadId ?? createThreadId();
272
- const startedEvents = yield* threadStartedEvents(definitions, {
273
- kind: args.kind,
274
- definitionName: args.definitionName,
275
- input: args.input ?? null,
276
- threadId,
277
- parentThreadId: null,
278
- });
279
- const started = startedEvents[0];
280
- const startedInput = started && Predicate.isObject(started.payload)
281
- ? (started.payload.input ?? null)
282
- : (args.input ?? null);
283
- const batch = yield* validateAndCreateEvents(registry.catalogs, runId, [
284
- {
285
- type: 'runtime.run.started',
181
+ if (execution.status !== 'ambiguous' &&
182
+ execution.status !== 'timed_out' &&
183
+ execution.status !== 'cancel_requested') {
184
+ return yield* new RuntimeExecutionError(`Effect "${effectId}" cannot be retried from status "${execution.status}"`);
185
+ }
186
+ const now = yield* Clock.currentTimeMillis;
187
+ const retryEvent = createEvent(runId, {
188
+ type: 'runtime.effect.retry.scheduled',
286
189
  payload: {
287
- rootThreadId: threadId,
288
- kind: args.kind,
289
- definitionName: args.definitionName,
290
- input: startedInput,
190
+ effectId,
191
+ attempt: execution.attempt,
192
+ nextAttemptAt: now,
193
+ error: `operator retry from ${execution.status}`,
291
194
  },
292
- threadId: null,
293
- idempotencyKey: args.idempotencyKey,
294
- },
295
- ...startedEvents,
296
- ], { origin: { type: 'system' } });
297
- yield* store.append(runId, stripSeq(batch));
298
- const state = yield* runtime.wake(runId);
299
- return { runId, threadId, state };
300
- }),
301
- signal: (runId, events, signalOpts) => Effect.gen(function* () {
302
- const store = yield* EventStoreTag;
303
- const current = yield* loadCursor(store, runId);
304
- const currentState = current.state;
305
- if (signalOpts?.idempotencyKey &&
306
- currentState.processedIdempotencyKeys?.includes(signalOpts.idempotencyKey)) {
307
- return currentState;
308
- }
309
- const filteredEvents = events.filter((input) => {
310
- const key = input.idempotencyKey ?? signalOpts?.idempotencyKey;
311
- return !key || !currentState.processedIdempotencyKeys?.includes(key);
312
- });
313
- if (filteredEvents.length === 0) {
314
- return currentState;
315
- }
316
- const batch = yield* validateAndCreateEvents(registry.catalogs, runId, filteredEvents.map((input) => ({
317
- ...input,
318
- idempotencyKey: input.idempotencyKey ?? signalOpts?.idempotencyKey,
319
- })), { origin: { type: 'external' } });
320
- const folded = foldRun(batch, registry, { runId, initial: currentState });
321
- const satisfied = waitSatisfiedEvents(folded, batch);
322
- const satisfiedEvents = satisfied.map((input) => createEvent(runId, input));
323
- yield* store.append(runId, stripSeq([...batch, ...satisfiedEvents]));
324
- return yield* runtime.wake(runId);
325
- }),
326
- cancel: (runId, threadId) => Effect.gen(function* () {
195
+ threadId: item.threadId,
196
+ causationId: item.causingEventId,
197
+ origin: { type: 'external', actorId: 'operator' },
198
+ });
199
+ return { events: [retryEvent], value: undefined };
200
+ }));
201
+ })).pipe(Effect.andThen(runtime.wake(runId))),
202
+ rescanTimers: Effect.gen(function* () {
327
203
  const store = yield* EventStoreTag;
328
- const targetThreadId = threadId ?? (yield* loadCursor(store, runId)).state.rootThreadId ?? '';
329
- return yield* runtime.signal(runId, [
330
- {
331
- type: 'runtime.thread.cancelled',
332
- payload: { threadId: targetThreadId, reason: 'cancelled' },
333
- threadId: targetThreadId || null,
334
- origin: { type: 'external' },
335
- },
336
- ]);
204
+ const runIds = yield* store.listRuns;
205
+ const scheduled = yield* Effect.forEach(runIds, (runId) => Effect.gen(function* () {
206
+ const deadline = nextRuntimeDeadline(yield* runtime.getRun(runId));
207
+ if (deadline === undefined) {
208
+ return false;
209
+ }
210
+ yield* scheduler.schedule(runId, deadline);
211
+ return true;
212
+ }), { concurrency: RESCAN_CONCURRENCY });
213
+ return scheduled.filter(Boolean).length;
337
214
  }),
338
- wake: (runId) => Effect.gen(function* () {
339
- // Claim synchronously before any yield so concurrent fibers cannot both pass.
340
- if (waking.has(runId)) {
341
- wakeAgain.add(runId);
342
- return yield* runtime.getRun(runId);
343
- }
344
- waking.add(runId);
345
- const store = yield* EventStoreTag;
346
- liveStore = store;
347
- try {
348
- let finalState;
349
- do {
350
- let cursor = yield* loadCursor(store, runId);
351
- if (isRunTerminal(cursor.state)) {
352
- finalState = cursor.state;
353
- break;
354
- }
355
- liveCount.set(runId, 0);
356
- const dispatchedEffectIds = new Set();
357
- let guard = 0;
358
- while (!isRunTerminal(cursor.state)) {
359
- if (guard >= maxWakeIterations) {
360
- return yield* Effect.fail(new MaxWakeIterationsError(runId, maxWakeIterations));
361
- }
362
- guard += 1;
363
- const state = cursor.state;
364
- const now = Date.now();
365
- const due = Object.values(state.waits).filter((record) => isWaitOnTimer(record.on) && record.on.timerAt <= now + 5);
366
- if (due.length > 0) {
367
- const batch = [];
368
- for (const record of due) {
369
- if (!isWaitOnTimer(record.on)) {
370
- continue;
371
- }
372
- batch.push(createEvent(runId, {
373
- type: 'runtime.timer.fired',
374
- payload: { timerId: record.waitId, waitId: record.waitId },
375
- threadId: record.threadId,
376
- origin: { type: 'system' },
377
- }));
378
- }
379
- const stateAfterFired = foldRun(batch, registry, { runId, initial: state });
380
- const satisfied = waitSatisfiedEvents(stateAfterFired, batch);
381
- const satisfiedEvents = satisfied.map((input) => createEvent(runId, input));
382
- const fullTimerBatch = [...batch, ...satisfiedEvents];
383
- const appendResult = yield* store.append(runId, stripSeq(fullTimerBatch));
384
- cursor = yield* applyAppend(store, runId, cursor, stripSeq(fullTimerBatch), appendResult);
385
- continue;
386
- }
387
- const outstanding = state.outstandingEffects;
388
- if (outstanding.length === 0) {
389
- break;
390
- }
391
- for (const item of outstanding) {
392
- if (dispatchedEffectIds.has(item.effectId)) {
393
- return yield* Effect.fail(new DuplicateEffectDispatchError(runId, item.effectId, item.threadId));
394
- }
395
- }
396
- for (const item of outstanding) {
397
- dispatchedEffectIds.add(item.effectId);
398
- }
399
- const groupKeys = groupOutstanding(outstanding);
400
- const groupResults = yield* Effect.forEach(groupKeys, (groupKey) => Effect.gen(function* () {
401
- const group = outstanding.filter((item) => `${item.threadId}:${item.causingSeq}` === groupKey);
402
- const groupProduced = [];
403
- let failedEffectId;
404
- for (const item of group) {
405
- if (failedEffectId && item.effect.type === 'runtime.wait') {
406
- groupProduced.push(createEffectFailedEvent(runId, item, withdrawnError(failedEffectId)));
407
- continue;
408
- }
409
- const appendLive = async (input) => {
410
- const validated = await Effect.runPromise(validateEventInput(registry.catalogs, input));
411
- const liveEvent = createLiveEvent(runId, item, validated);
412
- if (liveEvent) {
413
- await Effect.runPromise(liveAppends.push(runId, liveEvent));
414
- }
415
- };
416
- const outcomes = yield* dispatchEffect(registry, services, definitions, item.effect, {
417
- effectId: item.effectId,
418
- runId,
419
- threadId: item.threadId,
420
- causingEventId: item.causingEventId,
421
- emit: appendLive,
422
- });
423
- const before = groupProduced.length;
424
- if (outcomes.length === 0) {
425
- groupProduced.push(createEffectFailedEvent(runId, item, 'empty-outcome'));
426
- }
427
- for (const input of outcomes) {
428
- groupProduced.push(yield* materializeEffectOutcome(registry.catalogs, runId, item, input));
429
- }
430
- const groupOutcomes = groupProduced.slice(before);
431
- if (item.effect.type !== 'runtime.wait' &&
432
- groupOutcomes.some((event) => event.type === 'runtime.effect.failed')) {
433
- failedEffectId = item.effectId;
434
- }
435
- }
436
- return groupProduced;
437
- }), { concurrency: 'unbounded' });
438
- const produced = groupResults.flat();
439
- if (produced.length === 0) {
440
- break;
441
- }
442
- yield* liveAppends.drain(runId);
443
- const beforeFold = cursor.state;
444
- const intermediateState = foldRun(produced, registry, { runId, initial: beforeFold });
445
- const synthesized = synthesizedThreadFailed(beforeFold, intermediateState, produced);
446
- const synthesizedEvents = synthesized.map((input) => createEvent(runId, input));
447
- const stateAfterSynth = synthesizedEvents.length > 0
448
- ? foldRun(synthesizedEvents, registry, { runId, initial: intermediateState })
449
- : intermediateState;
450
- const allNewEvents = [...produced, ...synthesizedEvents];
451
- const satisfied = waitSatisfiedEvents(stateAfterSynth, allNewEvents);
452
- const satisfiedEvents = satisfied.map((input) => createEvent(runId, input));
453
- const fullBatch = [...allNewEvents, ...satisfiedEvents];
454
- const appendResult = yield* store.append(runId, stripSeq(fullBatch));
455
- cursor = yield* applyAppend(store, runId, cursor, stripSeq(fullBatch), appendResult);
456
- // Bound replay after a crash inside a long wake.
457
- if (snapshotEvery > 0 && cursor.durableSinceSnapshot >= snapshotEvery) {
458
- cursor = yield* persistSnapshot(store, runId, cursor);
459
- }
460
- }
461
- const root = cursor.state.rootThreadId
462
- ? cursor.state.threads[cursor.state.rootThreadId]
463
- : undefined;
464
- if (root &&
465
- (root.status === 'completed' ||
466
- root.status === 'failed' ||
467
- root.status === 'cancelled') &&
468
- !isRunTerminal(cursor.state)) {
469
- const completed = createEvent(runId, {
470
- type: 'runtime.run.completed',
215
+ recoverDeadlines: Effect.suspend(() => runtime.rescanTimers),
216
+ startRun: (args) => {
217
+ const runId = args.runId ?? createRunId();
218
+ return withRunIngress(runId, Effect.gen(function* () {
219
+ const definitionVersion = args.definitionVersion ?? DEFAULT_DEFINITION_VERSION;
220
+ if (!definitions.get(definitionKey(args.kind, args.definitionName, definitionVersion))) {
221
+ return yield* new UnknownDefinitionError(args.kind, args.definitionName, definitionVersion);
222
+ }
223
+ const store = yield* EventStoreTag;
224
+ observe({ type: 'run.start', runId, at: currentTimeMillis() });
225
+ const threadId = args.threadId ?? createThreadId();
226
+ const startedEvents = yield* threadStartedEvents(definitions, {
227
+ kind: args.kind,
228
+ definitionName: args.definitionName,
229
+ definitionVersion,
230
+ input: args.input ?? null,
231
+ threadId,
232
+ parentThreadId: null,
233
+ });
234
+ const started = startedEvents[0];
235
+ const startedInput = started && Predicate.isObject(started.payload)
236
+ ? (started.payload.input ?? null)
237
+ : (args.input ?? null);
238
+ const requestedIdentity = {
239
+ kind: args.kind,
240
+ definitionName: args.definitionName,
241
+ definitionVersion,
242
+ input: startedInput,
243
+ requestedThreadId: args.threadId ?? null,
244
+ idempotencyKey: args.idempotencyKey ?? null,
245
+ };
246
+ const committed = yield* commit(store, runId, (existing) => existing.seq > 0
247
+ ? assertSameDurableStart(runId, existing.state, requestedIdentity).pipe(Effect.map((rootThreadId) => ({ events: [], value: rootThreadId })))
248
+ : validateAndCreateEvents(registry.catalogs, runId, [
249
+ {
250
+ type: 'runtime.run.started',
471
251
  payload: {
472
- output: root.output,
473
- error: root.error,
252
+ rootThreadId: threadId,
253
+ kind: args.kind,
254
+ definitionName: args.definitionName,
255
+ definitionVersion,
256
+ input: startedInput,
257
+ requestedThreadId: args.threadId ?? null,
474
258
  },
475
259
  threadId: null,
476
- origin: { type: 'system' },
477
- });
478
- const appendResult = yield* store.append(runId, stripSeq([completed]));
479
- cursor = yield* applyAppend(store, runId, cursor, stripSeq([completed]), appendResult);
480
- }
481
- // Snapshot on park: the next wake (on any instance) starts from here.
482
- cursor = yield* persistSnapshot(store, runId, cursor);
483
- const pendingTimers = Object.values(cursor.state.waits).filter((record) => isWaitOnTimer(record.on) && record.on.timerAt > Date.now());
484
- if (pendingTimers.length > 0 && !isRunTerminal(cursor.state)) {
485
- // SAFETY: pendingTimers is filtered by isWaitOnTimer above.
486
- const earliestTimerAt = Math.min(...pendingTimers.map((r) => r.on.timerAt));
487
- yield* toEffectVoid(scheduler.schedule(runId, earliestTimerAt));
488
- }
489
- else {
490
- yield* toEffectVoid(scheduler.cancel(runId));
491
- }
492
- if (isRunTerminal(cursor.state)) {
493
- runCache.delete(runId);
494
- }
495
- else {
496
- runCache.set(runId, cursor);
497
- }
498
- finalState = cursor.state;
499
- } while (wakeAgain.delete(runId));
500
- // Lost race: a waiter queued after the drain loop but before unlock.
501
- if (wakeAgain.has(runId)) {
502
- waking.delete(runId);
503
- liveAppends.clear(runId);
504
- liveCount.delete(runId);
505
- return yield* runtime.wake(runId);
506
- }
507
- return finalState;
508
- }
509
- finally {
510
- if (waking.has(runId)) {
511
- waking.delete(runId);
260
+ idempotencyKey: args.idempotencyKey,
261
+ },
262
+ ...startedEvents,
263
+ ], { origin: { type: 'system' } }).pipe(Effect.map((batch) => ({
264
+ events: batch,
265
+ value: threadId,
266
+ idempotencyKey: args.idempotencyKey
267
+ ? `start:${args.idempotencyKey}`
268
+ : undefined,
269
+ }))));
270
+ if (!committed.append?.deduplicated) {
271
+ return committed.value;
512
272
  }
513
- liveAppends.clear(runId);
514
- liveCount.delete(runId);
515
- }
516
- }),
517
- dispose: () => {
518
- scheduler.dispose?.();
273
+ const durable = yield* loadCursor(store, runId);
274
+ return yield* assertSameDurableStart(runId, durable.state, requestedIdentity);
275
+ })).pipe(Effect.flatMap((threadId) => runtime.wake(runId).pipe(Effect.map((state) => ({
276
+ runId,
277
+ threadId: state.rootThreadId ?? threadId,
278
+ state,
279
+ })))));
519
280
  },
281
+ signal: (runId, events, signalOpts) => admitExternalEvents(events).pipe(Effect.flatMap((admitted) => withRunIngress(runId, signalAdmitted(runId, admitted, signalOpts))), Effect.andThen(runtime.wake(runId))),
282
+ cancel: cancellation,
283
+ wake: wakeEngine,
284
+ dispose: Effect.sync(() => scheduler.dispose?.()).pipe(Effect.andThen(services.disposeEffect)),
520
285
  };
521
286
  scheduler =
522
287
  options.scheduler ??
523
- createTimeoutScheduler((runId) => {
524
- if (options.store) {
525
- Effect.runPromise(Effect.provideService(runtime.wake(runId), EventStoreTag, options.store)).catch((err) => {
526
- console.error('[timer wake error]', err);
527
- });
528
- }
529
- });
288
+ createTimeoutScheduler((runId) => options.store
289
+ ? runtime
290
+ .wake(runId)
291
+ .pipe(Effect.provideService(EventStoreTag, options.store), Effect.tapError(Effect.logError), Effect.ignore)
292
+ : Effect.void);
530
293
  return runtime;
531
294
  }