@swirls/looms 0.4.0 → 0.5.1

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 (243) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/actor/cell.d.ts +21 -2
  3. package/dist/actor/cell.js +15 -3
  4. package/dist/actor/host.d.ts +10 -2
  5. package/dist/actor/host.js +27 -4
  6. package/dist/actor/index.d.ts +2 -1
  7. package/dist/actor/index.js +1 -1
  8. package/dist/actor/routing.d.ts +3 -0
  9. package/dist/actor/routing.js +13 -2
  10. package/dist/agent/definitions-store.d.ts +1 -1
  11. package/dist/agent/definitions-store.js +1 -1
  12. package/dist/agent/definitions.d.ts +48 -0
  13. package/dist/agent/definitions.js +44 -0
  14. package/dist/agent/effects.d.ts +184 -0
  15. package/dist/agent/effects.js +172 -40
  16. package/dist/agent/events.d.ts +89 -0
  17. package/dist/agent/events.js +25 -0
  18. package/dist/agent/module.d.ts +412 -0
  19. package/dist/agent/module.js +5 -1
  20. package/dist/agent/scope.d.ts +88 -0
  21. package/dist/agent/threads.js +39 -3
  22. package/dist/agent/types.d.ts +4 -0
  23. package/dist/approval/effects.d.ts +40 -0
  24. package/dist/approval/module.d.ts +80 -0
  25. package/dist/client/client.d.ts +468 -119
  26. package/dist/client/client.js +214 -21
  27. package/dist/client/index.d.ts +1 -1
  28. package/dist/client/sse.d.ts +1 -1
  29. package/dist/client/sse.js +2 -2
  30. package/dist/cloudflare/durable-object.d.ts +66 -2
  31. package/dist/cloudflare/durable-object.js +96 -1
  32. package/dist/cloudflare/event-store.d.ts +7 -3
  33. package/dist/cloudflare/event-store.js +16 -3
  34. package/dist/cloudflare/index.d.ts +7 -1
  35. package/dist/cloudflare/index.js +7 -1
  36. package/dist/cloudflare/memory-queue.d.ts +18 -0
  37. package/dist/cloudflare/memory-queue.js +53 -0
  38. package/dist/cloudflare/queue-consumer.d.ts +52 -0
  39. package/dist/cloudflare/queue-consumer.js +102 -0
  40. package/dist/cloudflare/queue-message.d.ts +31 -0
  41. package/dist/cloudflare/queue-message.js +33 -0
  42. package/dist/cloudflare/queue-pull.d.ts +62 -0
  43. package/dist/cloudflare/queue-pull.js +130 -0
  44. package/dist/cloudflare/queue-worker.d.ts +39 -0
  45. package/dist/cloudflare/queue-worker.js +62 -0
  46. package/dist/cloudflare/queues.d.ts +10 -0
  47. package/dist/cloudflare/queues.js +10 -0
  48. package/dist/cloudflare/r2-blob-store.d.ts +33 -0
  49. package/dist/cloudflare/r2-blob-store.js +60 -0
  50. package/dist/cloudflare/router.d.ts +4 -1
  51. package/dist/cloudflare/router.js +8 -3
  52. package/dist/cloudflare/websocket.d.ts +27 -0
  53. package/dist/cloudflare/websocket.js +215 -0
  54. package/dist/core/blob-offload.d.ts +21 -0
  55. package/dist/core/blob-offload.js +143 -0
  56. package/dist/core/blob-store.d.ts +45 -0
  57. package/dist/core/blob-store.js +164 -0
  58. package/dist/core/blobs.d.ts +37 -0
  59. package/dist/core/blobs.js +160 -0
  60. package/dist/core/definition-resolver.d.ts +40 -0
  61. package/dist/core/definition-resolver.js +129 -0
  62. package/dist/core/definition-store.d.ts +6 -0
  63. package/dist/core/definition-store.js +37 -1
  64. package/dist/core/effects.d.ts +86 -3
  65. package/dist/core/effects.js +99 -3
  66. package/dist/core/envelope.d.ts +9 -0
  67. package/dist/core/envelope.js +4 -0
  68. package/dist/core/event-codec.js +3 -0
  69. package/dist/core/fold.js +83 -1
  70. package/dist/core/ids.d.ts +2 -0
  71. package/dist/core/ids.js +4 -0
  72. package/dist/core/index.d.ts +8 -0
  73. package/dist/core/index.js +8 -0
  74. package/dist/core/launcher.d.ts +97 -0
  75. package/dist/core/launcher.js +67 -0
  76. package/dist/core/module-scope.d.ts +7 -3
  77. package/dist/core/module-scope.js +3 -0
  78. package/dist/core/module.d.ts +10 -0
  79. package/dist/core/projection.js +4 -0
  80. package/dist/core/protocol.d.ts +36 -1
  81. package/dist/core/protocol.js +35 -0
  82. package/dist/core/published.d.ts +2 -0
  83. package/dist/core/published.js +4 -1
  84. package/dist/core/replay.d.ts +96 -0
  85. package/dist/core/run-context.d.ts +33 -0
  86. package/dist/core/run-context.js +23 -0
  87. package/dist/core/sqlite-store.js +57 -5
  88. package/dist/core/state.d.ts +60 -0
  89. package/dist/core/state.js +19 -0
  90. package/dist/core/store.d.ts +6 -0
  91. package/dist/core/store.js +7 -0
  92. package/dist/core/subscription.d.ts +97 -0
  93. package/dist/core/subscription.js +224 -0
  94. package/dist/core/thread.d.ts +7 -0
  95. package/dist/core/thread.js +1 -0
  96. package/dist/core/trace.d.ts +21 -0
  97. package/dist/core/trace.js +45 -0
  98. package/dist/core/worker-callback.d.ts +25 -1
  99. package/dist/debugger/app/assets/{index-CgDzWJg7.css → index-B7GdT1UA.css} +1 -1
  100. package/dist/debugger/app/assets/index-C31SnCuN.js +22 -0
  101. package/dist/debugger/app/assets/routes-DPA3O5AF.js +47 -0
  102. package/dist/debugger/app/index.html +2 -2
  103. package/dist/debugger/components/event-inspector.d.ts +3 -1
  104. package/dist/debugger/components/event-inspector.js +25 -4
  105. package/dist/debugger/components/event-stream.d.ts +4 -1
  106. package/dist/debugger/components/event-stream.js +2 -2
  107. package/dist/debugger/components/run-tree.d.ts +5 -2
  108. package/dist/debugger/components/run-tree.js +10 -4
  109. package/dist/debugger/context.d.ts +8 -1
  110. package/dist/debugger/context.js +15 -3
  111. package/dist/debugger/events/protocol-summarize.js +18 -3
  112. package/dist/debugger/index.d.ts +1 -1
  113. package/dist/debugger/index.js +1 -1
  114. package/dist/debugger/shell/debugger-shell.js +1 -1
  115. package/dist/debugger/shell/events-pane.js +4 -3
  116. package/dist/debugger/styles.css +1 -0
  117. package/dist/evaluator/definitions-store.d.ts +3 -4
  118. package/dist/evaluator/definitions-store.js +3 -3
  119. package/dist/evaluator/definitions.d.ts +4 -1
  120. package/dist/evaluator/definitions.js +5 -2
  121. package/dist/evaluator/effects.d.ts +41 -0
  122. package/dist/evaluator/effects.js +6 -2
  123. package/dist/evaluator/module.d.ts +84 -0
  124. package/dist/evaluator/module.js +7 -3
  125. package/dist/evaluator/threads.d.ts +2 -0
  126. package/dist/evaluator/threads.js +4 -0
  127. package/dist/projectors/event-log.d.ts +87 -0
  128. package/dist/projectors/event-log.js +396 -0
  129. package/dist/projectors/index-model.d.ts +12 -0
  130. package/dist/projectors/index-model.js +12 -3
  131. package/dist/projectors/memory.js +7 -0
  132. package/dist/projectors/postgres.d.ts +1 -0
  133. package/dist/projectors/postgres.js +32 -15
  134. package/dist/projectors/schema.d.ts +5 -0
  135. package/dist/projectors/schema.js +26 -1
  136. package/dist/projectors/sqlite.js +47 -19
  137. package/dist/projectors/with-projectors.d.ts +7 -1
  138. package/dist/projectors/with-projectors.js +13 -3
  139. package/dist/react/materialize.js +5 -1
  140. package/dist/react/react.d.ts +10 -2
  141. package/dist/react/react.js +43 -19
  142. package/dist/react/store.d.ts +11 -2
  143. package/dist/react/store.js +104 -12
  144. package/dist/react/tables.d.ts +4 -1
  145. package/dist/runtime/cancellation.d.ts +2 -1
  146. package/dist/runtime/cancellation.js +35 -2
  147. package/dist/runtime/child-run.d.ts +10 -0
  148. package/dist/runtime/child-run.js +58 -0
  149. package/dist/runtime/deadline-transition-planner.js +11 -2
  150. package/dist/runtime/dispatch-effect.d.ts +2 -2
  151. package/dist/runtime/dispatch-effect.js +43 -19
  152. package/dist/runtime/errors.d.ts +14 -1
  153. package/dist/runtime/errors.js +10 -0
  154. package/dist/runtime/filesystem-blob-store.d.ts +6 -0
  155. package/dist/runtime/filesystem-blob-store.js +59 -0
  156. package/dist/runtime/helpers.d.ts +15 -6
  157. package/dist/runtime/helpers.js +46 -6
  158. package/dist/runtime/index.d.ts +4 -2
  159. package/dist/runtime/index.js +3 -1
  160. package/dist/runtime/local-effect-executor.d.ts +11 -2
  161. package/dist/runtime/local-effect-executor.js +80 -4
  162. package/dist/runtime/looms.d.ts +38 -4
  163. package/dist/runtime/looms.js +17 -3
  164. package/dist/runtime/repository.d.ts +5 -4
  165. package/dist/runtime/repository.js +5 -2
  166. package/dist/runtime/retry-policy.d.ts +5 -0
  167. package/dist/runtime/retry-policy.js +10 -2
  168. package/dist/runtime/run-start.d.ts +1 -1
  169. package/dist/runtime/run-start.js +10 -0
  170. package/dist/runtime/runtime.d.ts +2 -2
  171. package/dist/runtime/runtime.js +191 -68
  172. package/dist/runtime/server/auth.d.ts +3 -0
  173. package/dist/runtime/server/fetch-handler.d.ts +2 -2
  174. package/dist/runtime/server/fetch-handler.js +35 -4
  175. package/dist/runtime/server/grant.d.ts +49 -0
  176. package/dist/runtime/server/grant.js +177 -0
  177. package/dist/runtime/server/routes/effects.js +47 -5
  178. package/dist/runtime/server/routes/runs.js +6 -1
  179. package/dist/runtime/server/run-route.d.ts +18 -0
  180. package/dist/runtime/server/run-route.js +26 -0
  181. package/dist/runtime/server.d.ts +3 -1
  182. package/dist/runtime/server.js +3 -1
  183. package/dist/runtime/sse.js +26 -4
  184. package/dist/runtime/subscription.d.ts +8 -0
  185. package/dist/runtime/subscription.js +23 -0
  186. package/dist/runtime/types.d.ts +87 -8
  187. package/dist/runtime/wake-engine.d.ts +4 -2
  188. package/dist/runtime/wake-engine.js +57 -6
  189. package/dist/runtime/worker-callbacks.d.ts +12 -2
  190. package/dist/runtime/worker-callbacks.js +102 -72
  191. package/dist/runtime/worker-effect-dispatch.d.ts +14 -5
  192. package/dist/runtime/worker-effect-dispatch.js +84 -18
  193. package/dist/schedule/debugger/index.d.ts +4 -0
  194. package/dist/schedule/debugger/index.js +11 -0
  195. package/dist/schedule/debugger/schedule-view.d.ts +3 -0
  196. package/dist/schedule/debugger/schedule-view.js +68 -0
  197. package/dist/schedule/debugger/summarize.d.ts +2 -0
  198. package/dist/schedule/debugger/summarize.js +43 -0
  199. package/dist/schedule/definitions-store.d.ts +10 -0
  200. package/dist/schedule/definitions-store.js +8 -0
  201. package/dist/schedule/definitions.d.ts +57 -0
  202. package/dist/schedule/definitions.js +30 -0
  203. package/dist/schedule/effects.d.ts +756 -0
  204. package/dist/schedule/effects.js +244 -0
  205. package/dist/schedule/ensure.d.ts +55 -0
  206. package/dist/schedule/ensure.js +109 -0
  207. package/dist/schedule/events.d.ts +116 -0
  208. package/dist/schedule/events.js +49 -0
  209. package/dist/schedule/index.d.ts +11 -0
  210. package/dist/schedule/index.js +11 -0
  211. package/dist/schedule/module.d.ts +1474 -0
  212. package/dist/schedule/module.js +22 -0
  213. package/dist/schedule/projections.d.ts +55 -0
  214. package/dist/schedule/projections.js +88 -0
  215. package/dist/schedule/scope.d.ts +121 -0
  216. package/dist/schedule/scope.js +7 -0
  217. package/dist/schedule/signals.d.ts +25 -0
  218. package/dist/schedule/signals.js +33 -0
  219. package/dist/schedule/specs.d.ts +59 -0
  220. package/dist/schedule/specs.js +231 -0
  221. package/dist/schedule/threads.d.ts +25 -0
  222. package/dist/schedule/threads.js +259 -0
  223. package/dist/testing/corpus-module.d.ts +34 -0
  224. package/dist/testing/corpus-module.js +87 -0
  225. package/dist/testing/index.d.ts +2 -0
  226. package/dist/testing/index.js +5 -6
  227. package/dist/testing/replay-corpus.d.ts +22 -0
  228. package/dist/testing/replay-corpus.js +50 -0
  229. package/dist/workflow/definitions-store.d.ts +1 -1
  230. package/dist/workflow/definitions-store.js +1 -1
  231. package/dist/workflow/definitions.d.ts +14 -0
  232. package/dist/workflow/effects.d.ts +122 -0
  233. package/dist/workflow/effects.js +16 -6
  234. package/dist/workflow/events.d.ts +42 -0
  235. package/dist/workflow/events.js +4 -0
  236. package/dist/workflow/module.d.ts +313 -0
  237. package/dist/workflow/module.js +7 -3
  238. package/dist/workflow/scope.d.ts +42 -0
  239. package/dist/workflow/threads.d.ts +72 -0
  240. package/dist/workflow/threads.js +106 -38
  241. package/package.json +14 -1
  242. package/dist/debugger/app/assets/index-D--9esm9.js +0 -22
  243. package/dist/debugger/app/assets/routes-Dakrdusf.js +0 -47
@@ -1,4 +1,5 @@
1
1
  import { Effect, Schema } from 'effect';
2
+ import { JsonValueSchema, } from '@swirls/looms/core';
2
3
  import { EventInputBodySchema, provideStore, readJson } from '../http.js';
3
4
  import { integer, oneOf, route } from '../route-table.js';
4
5
  const WORKER_CALLBACK_KINDS = [
@@ -11,16 +12,46 @@ const WORKER_CALLBACK_KINDS = [
11
12
  const WorkerCallbackBodySchema = Schema.Struct({
12
13
  events: Schema.optional(Schema.Array(EventInputBodySchema)),
13
14
  error: Schema.optional(Schema.String),
15
+ nonRetryable: Schema.optional(Schema.Boolean),
16
+ retryAfterMs: Schema.optional(Schema.Finite),
17
+ details: Schema.optional(JsonValueSchema),
14
18
  });
15
19
  function workerCallbackInput(params, body) {
16
20
  const callback = { effectId: params.effectId, attempt: params.attempt };
17
21
  switch (params.action) {
18
22
  case 'complete':
19
23
  return { ...callback, kind: 'complete', events: body.events ?? [] };
20
- case 'fail':
21
- return body.error ? { ...callback, kind: 'fail', error: body.error } : null;
22
- default:
24
+ case 'fail': {
25
+ if (body.error === undefined) {
26
+ return null;
27
+ }
28
+ const failure = {
29
+ ...callback,
30
+ kind: 'fail',
31
+ error: body.error,
32
+ };
33
+ if (body.nonRetryable !== undefined) {
34
+ failure.nonRetryable = body.nonRetryable;
35
+ }
36
+ if (body.retryAfterMs !== undefined) {
37
+ failure.retryAfterMs = body.retryAfterMs;
38
+ }
39
+ return failure;
40
+ }
41
+ case 'heartbeat': {
42
+ const heartbeat = { ...callback, kind: 'heartbeat' };
43
+ if (body.details !== undefined) {
44
+ heartbeat.details = body.details;
45
+ }
46
+ return heartbeat;
47
+ }
48
+ case 'started':
49
+ case 'cancelled':
23
50
  return { ...callback, kind: params.action };
51
+ default: {
52
+ const exhaustive = params.action;
53
+ return exhaustive;
54
+ }
24
55
  }
25
56
  }
26
57
  export function effectRoutes({ runtime, store }) {
@@ -39,6 +70,17 @@ export function effectRoutes({ runtime, store }) {
39
70
  access: 'operations',
40
71
  handle: ({ params }) => provideStore(runtime.retryEffect(params.runId, params.effectId), store).pipe(Effect.map((state) => Response.json({ runId: params.runId, effectId: params.effectId, state }))),
41
72
  }),
73
+ route({
74
+ method: 'POST',
75
+ path: '/runs/:runId/effects/:effectId/:attempt/secrets',
76
+ name: 'effects.workerSecrets',
77
+ access: 'worker',
78
+ params: { attempt: integer },
79
+ handle: ({ params }) => provideStore(runtime.workerSecrets(params.runId, {
80
+ effectId: params.effectId,
81
+ attempt: params.attempt,
82
+ }), store).pipe(Effect.map((result) => Response.json(result, { headers: { 'cache-control': 'no-store' } }))),
83
+ }),
42
84
  route({
43
85
  method: 'POST',
44
86
  path: '/runs/:runId/effects/:effectId/:attempt/:action',
@@ -51,8 +93,8 @@ export function effectRoutes({ runtime, store }) {
51
93
  if (!input) {
52
94
  return Response.json({ error: 'error required' }, { status: 400 });
53
95
  }
54
- const state = yield* provideStore(runtime.workerCallback(params.runId, input), store);
55
- return Response.json({ runId: params.runId, state });
96
+ const result = yield* provideStore(runtime.workerCallback(params.runId, input), store);
97
+ return Response.json(result);
56
98
  }),
57
99
  }),
58
100
  ];
@@ -1,5 +1,5 @@
1
1
  import { Effect, Schema } from 'effect';
2
- import { createRunId, EventStoreTag, JsonValueSchema, stringifyJson, } from '@swirls/looms/core';
2
+ import { createRunId, EventStoreTag, JsonValueSchema, RunAttributesSchema, readTraceHeaders, SecretContextSchema, stringifyJson, } from '@swirls/looms/core';
3
3
  import { createEventStreamResponse } from '../../sse.js';
4
4
  import { acceptsEventStream, EventInputBodySchema, extractIdempotencyKey, provideStore, readIntegerQuery, readJson, readOptionalJson, RequestQueryError, } from '../http.js';
5
5
  import { route } from '../route-table.js';
@@ -11,6 +11,8 @@ const StartRunBodySchema = Schema.Struct({
11
11
  input: Schema.optional(JsonValueSchema),
12
12
  runId: Schema.optional(Schema.String),
13
13
  idempotencyKey: Schema.optional(Schema.String),
14
+ attributes: Schema.optional(RunAttributesSchema),
15
+ secrets: Schema.optional(SecretContextSchema),
14
16
  });
15
17
  const SignalBodySchema = Schema.Struct({
16
18
  events: Schema.optional(Schema.Array(EventInputBodySchema)),
@@ -108,6 +110,9 @@ export function runRoutes({ runtime, store }) {
108
110
  input: body.input ?? null,
109
111
  runId,
110
112
  idempotencyKey,
113
+ attributes: body.attributes,
114
+ secrets: body.secrets,
115
+ trace: readTraceHeaders(req.headers),
111
116
  };
112
117
  if (wantsRunStream(req, url)) {
113
118
  return streamStartRun(req, runtime, store, args);
@@ -0,0 +1,18 @@
1
+ import { Effect } from 'effect';
2
+ import { type EventStore, type RunAttributes } from '@swirls/looms/core';
3
+ import type { LoomsRuntime } from '../runtime';
4
+ import type { AuthorizedAccess, RouteInfo } from './auth';
5
+ /**
6
+ * Attributes are immutable and recorded on `runtime.run.started`, which is seq 1 of every
7
+ * untrimmed log, forks included. Only a trimmed log needs the snapshot-backed fold.
8
+ */
9
+ export declare function readStoredAttributes(runtime: LoomsRuntime, store: EventStore, runId: string): Effect.Effect<RunAttributes, Error>;
10
+ /**
11
+ * What `authorize` sees for a request about one run: the run's attributes are included when it has
12
+ * any. Every path that serves a run, including a WebSocket upgrade, authorizes through this.
13
+ */
14
+ export declare function runRouteInfo(runtime: LoomsRuntime, store: EventStore, route: {
15
+ readonly access: AuthorizedAccess;
16
+ readonly name: string;
17
+ readonly runId: string;
18
+ }): Effect.Effect<RouteInfo, Error>;
@@ -0,0 +1,26 @@
1
+ import { Effect } from 'effect';
2
+ import { EventStoreTag, hasEntries, isJsonObject, readAttributes, } from '@swirls/looms/core';
3
+ const noAttributes = {};
4
+ /**
5
+ * Attributes are immutable and recorded on `runtime.run.started`, which is seq 1 of every
6
+ * untrimmed log, forks included. Only a trimmed log needs the snapshot-backed fold.
7
+ */
8
+ export function readStoredAttributes(runtime, store, runId) {
9
+ const folded = runtime.getRun(runId).pipe(Effect.provideService(EventStoreTag, store), Effect.map((state) => state.attributes));
10
+ return store.read(runId, { fromSeq: 1, limit: 1 }).pipe(Effect.flatMap(([first]) => {
11
+ if (first === undefined) {
12
+ return Effect.succeed(noAttributes);
13
+ }
14
+ if (first.type === 'runtime.run.started' && isJsonObject(first.payload)) {
15
+ return Effect.succeed(readAttributes(first.payload.attributes));
16
+ }
17
+ return folded;
18
+ }), Effect.catch(() => folded));
19
+ }
20
+ /**
21
+ * What `authorize` sees for a request about one run: the run's attributes are included when it has
22
+ * any. Every path that serves a run, including a WebSocket upgrade, authorizes through this.
23
+ */
24
+ export function runRouteInfo(runtime, store, route) {
25
+ return readStoredAttributes(runtime, store, route.runId).pipe(Effect.map((attributes) => (hasEntries(attributes) ? { ...route, attributes } : route)));
26
+ }
@@ -1,7 +1,9 @@
1
1
  export { createFetchHandler, handlesPath, type FetchHandlerOptions } from './server/fetch-handler';
2
2
  export { isLoomsApiPath } from './server/http';
3
3
  export { mountDebugger, type DebuggerMount } from './server/debugger-host';
4
- export { authAllowed, authDenied, bearerAuth, defaultAuthorize, type AuthDecision, type AuthorizedAccess, type Authorize, type BearerTokens, type RouteAccess, type RouteInfo, } from './server/auth';
4
+ export { runRouteInfo } from './server/run-route';
5
+ export { authAllowed, authDenied, authResponse, bearerAuth, defaultAuthorize, type AuthDecision, type AuthorizedAccess, type Authorize, type BearerTokens, type RouteAccess, type RouteInfo, } from './server/auth';
6
+ export { grantAuth, issueRunGrant, grantSubprotocol, runGrantToken, verifyRunGrant, GrantError, type GrantAuthOptions, type GrantFailure, type RunGrant, } from './server/grant';
5
7
  export interface RunningServer {
6
8
  port: number;
7
9
  stop: () => void;
@@ -1,7 +1,9 @@
1
1
  export { createFetchHandler, handlesPath } from './server/fetch-handler.js';
2
2
  export { isLoomsApiPath } from './server/http.js';
3
3
  export { mountDebugger } from './server/debugger-host.js';
4
- export { authAllowed, authDenied, bearerAuth, defaultAuthorize, } from './server/auth.js';
4
+ export { runRouteInfo } from './server/run-route.js';
5
+ export { authAllowed, authDenied, authResponse, bearerAuth, defaultAuthorize, } from './server/auth.js';
6
+ export { grantAuth, issueRunGrant, grantSubprotocol, runGrantToken, verifyRunGrant, GrantError, } from './server/grant.js';
5
7
  export function serveHttp(fetchHandler, options) {
6
8
  const port = options?.port ?? 8787;
7
9
  const hostname = options?.hostname ?? '0.0.0.0';
@@ -1,5 +1,6 @@
1
1
  import { Effect, Fiber, Schedule, Stream } from 'effect';
2
- import { encodeLoomsEvent, stringifyJson } from '@swirls/looms/core';
2
+ import { encodeLoomsEvent, encodeSubscriptionFrame, } from '@swirls/looms/core';
3
+ import { readSubscriptionBounds } from './subscription.js';
3
4
  function disableBunSocketTimeout(request) {
4
5
  if (!request) {
5
6
  return;
@@ -64,12 +65,33 @@ export function createEventStreamResponse(options) {
64
65
  close();
65
66
  }
66
67
  };
67
- subscriptionFiber = Effect.runFork(store.subscribe(runId, { fromSeq }).pipe(Stream.tap((event) => Effect.sync(() => {
68
- write(`id: ${event.seq}\ndata: ${stringifyJson({ batch: [encodeLoomsEvent(event)] })}\n\n`);
69
- })), Stream.runDrain));
70
68
  // Send an immediate keepalive so bytes and headers flush over the wire immediately,
71
69
  // preventing dev server proxies and Bun.serve from aborting quiet connections.
70
+ // It has to be enqueued before the subscription fiber, which writes hello synchronously.
72
71
  write(': keepalive\n\n');
72
+ subscriptionFiber = Effect.runFork(Effect.gen(function* () {
73
+ const bounds = yield* readSubscriptionBounds(store, runId);
74
+ if (fromSeq < bounds.head) {
75
+ write(`data: ${encodeSubscriptionFrame({
76
+ type: 'resync',
77
+ head: bounds.head,
78
+ reason: 'trimmed',
79
+ })}\n\n`);
80
+ close();
81
+ return;
82
+ }
83
+ write(`data: ${encodeSubscriptionFrame({
84
+ type: 'hello',
85
+ head: bounds.head,
86
+ tail: bounds.tail,
87
+ })}\n\n`);
88
+ yield* store.subscribe(runId, { fromSeq }).pipe(Stream.tap((event) => Effect.sync(() => {
89
+ write(`id: ${event.seq}\ndata: ${encodeSubscriptionFrame({
90
+ type: 'batch',
91
+ batch: [encodeLoomsEvent(event)],
92
+ })}\n\n`);
93
+ })), Stream.runDrain);
94
+ }));
73
95
  heartbeatFiber = Effect.runFork(Effect.repeat(Effect.sleep(heartbeatMs).pipe(Effect.andThen(Effect.sync(() => write(': keepalive\n\n')))), Schedule.forever).pipe(Effect.asVoid));
74
96
  const startResult = onStart?.(write, close);
75
97
  const startEffect = Effect.isEffect(startResult)
@@ -0,0 +1,8 @@
1
+ import { Effect } from 'effect';
2
+ import { type EventStore, type EventStoreError, type SubscriptionOpening } from '@swirls/looms/core';
3
+ export declare function readSubscriptionBounds(store: EventStore, runId: string): Effect.Effect<{
4
+ readonly head: number;
5
+ readonly tail: number;
6
+ }, EventStoreError>;
7
+ /** Hello plus the retained events, or a resync when `fromSeq` is below the log head. */
8
+ export declare function loadSubscriptionOpening(store: EventStore, runId: string, fromSeq: number): Effect.Effect<SubscriptionOpening, EventStoreError>;
@@ -0,0 +1,23 @@
1
+ import { Effect } from 'effect';
2
+ import { subscriptionReplay, } from '@swirls/looms/core';
3
+ export function readSubscriptionBounds(store, runId) {
4
+ if (store.bounds) {
5
+ return store.bounds(runId);
6
+ }
7
+ return store.tail(runId).pipe(Effect.map((tail) => ({ head: 1, tail })));
8
+ }
9
+ /** Hello plus the retained events, or a resync when `fromSeq` is below the log head. */
10
+ export function loadSubscriptionOpening(store, runId, fromSeq) {
11
+ return Effect.gen(function* () {
12
+ const bounds = yield* readSubscriptionBounds(store, runId);
13
+ const decision = subscriptionReplay(bounds.head, fromSeq);
14
+ if (decision.kind === 'resync') {
15
+ return { kind: 'resync', head: decision.head };
16
+ }
17
+ const events = yield* store.read(runId, {
18
+ fromSeq: decision.fromSeq,
19
+ });
20
+ const hello = { type: 'hello', head: bounds.head, tail: bounds.tail };
21
+ return { kind: 'open', hello, events };
22
+ });
23
+ }
@@ -1,5 +1,5 @@
1
1
  import type { Effect } from 'effect';
2
- import type { AnyRuntimeModule, ComposedRegistry, EventEnvelope, EventInput, WorkerCallbackInput, EventStore, EventStoreAppendError, EventStoreError, EventStoreFencedError, EventStoreTag, EventStoreTrimCoverage, EventStoreTruncationError, JsonValue, ProjectionDefinition, ReplayStep, RunState, RunSummary, SnapshotStore, SnapshotStoreError } from '@swirls/looms/core';
2
+ import type { AnyRuntimeModule, ComposedRegistry, DefinitionResolver, EventEnvelope, EventInput, WorkerCallback, WorkerCallbackInput, DefinitionResolveError, WorkerCallbackResult, WorkerSecretsResult, EventStore, EventStoreAppendError, EventStoreError, EventStoreFencedError, EventStoreTag, EventStoreTrimCoverage, RunLauncher, RunLaunchSource, RunAttributes, SecretContext, TraceContext, EventStoreTruncationError, JsonValue, ProjectionDefinition, ReplayStep, RunState, RunSummary, SnapshotStore, SnapshotStoreError } from '@swirls/looms/core';
3
3
  import type { EventStoreTrimmedError } from '@swirls/looms/core';
4
4
  import type { DuplicateEffectDispatchError, MaxWakeIterationsError } from './errors';
5
5
  import type { RuntimeObserver } from './observer';
@@ -7,6 +7,12 @@ import type { WakeScheduler } from './wake-scheduler';
7
7
  export type WakeError = DuplicateEffectDispatchError | MaxWakeIterationsError | EventStoreTruncationError | EventStoreTrimmedError | EventStoreFencedError | EventStoreError | Error;
8
8
  export interface CreateRuntimeOptions<TModules extends readonly AnyRuntimeModule[] = readonly AnyRuntimeModule[]> {
9
9
  readonly modules: TModules;
10
+ /**
11
+ * Loads a definition that was not registered on a module. Registered definitions are checked
12
+ * first. Lookups are cached, including misses. Start fails with `UnknownDefinitionError`
13
+ * and writes no events when the definition is missing.
14
+ */
15
+ readonly definitionResolver?: DefinitionResolver;
10
16
  readonly store?: EventStore;
11
17
  /**
12
18
  * Durable events between mid-wake snapshots. Default 200; `0` disables.
@@ -15,6 +21,8 @@ export interface CreateRuntimeOptions<TModules extends readonly AnyRuntimeModule
15
21
  */
16
22
  readonly snapshotEvery?: number;
17
23
  readonly maxWakeIterations?: number;
24
+ /** Maximum concurrent in-flight worker effects dispatched per run (fairness guard). */
25
+ readonly maxInFlightWorkerEffects?: number;
18
26
  /** Maximum active and queued mutation operations per run. Default 256. */
19
27
  readonly maxPendingRunOperations?: number;
20
28
  /** Defaults to the EventStore's attached store or an in-memory SnapshotStore. */
@@ -39,6 +47,13 @@ export interface CreateRuntimeOptions<TModules extends readonly AnyRuntimeModule
39
47
  * Defaults to an in-process Effect fiber scheduler.
40
48
  */
41
49
  readonly scheduler?: WakeScheduler;
50
+ /**
51
+ * Starts runs from effect handlers. Required for schedules on an actor or Durable Object host.
52
+ * Single-store `createLooms` installs one that launches into its own store.
53
+ */
54
+ readonly launcher?: RunLauncher;
55
+ /** Install a launcher that starts runs in this runtime's store. `createLooms` sets this. */
56
+ readonly localLauncher?: boolean;
42
57
  readonly worker?: EffectWorker;
43
58
  /** Failure-isolated, framework-neutral operational event sink. */
44
59
  readonly observer?: RuntimeObserver;
@@ -49,6 +64,24 @@ export interface EffectWorkerTask {
49
64
  readonly attempt: number;
50
65
  readonly type: string;
51
66
  readonly input: JsonValue;
67
+ /** Progress from the previous attempt's last heartbeat, when it reported any. */
68
+ readonly details?: JsonValue;
69
+ /** The run's W3C trace context, for parenting the worker's spans. */
70
+ readonly trace?: TraceContext;
71
+ /** The attempt's deadlines from the effect's retry policy, when it sets any. */
72
+ readonly deadlines?: EffectWorkerDeadlines;
73
+ /**
74
+ * Present only on the in-memory dispatch. A transport that persists the task must leave this out
75
+ * and let the worker read the secret context through an authenticated callback.
76
+ */
77
+ readonly secrets?: SecretContext;
78
+ }
79
+ /** Looms enforces these; a worker can use them to pace heartbeats or give up early. */
80
+ export interface EffectWorkerDeadlines {
81
+ /** Epoch ms. A `started` callback after this is stale. */
82
+ readonly scheduleToStartAt?: number;
83
+ readonly startToCloseTimeoutMs?: number;
84
+ readonly heartbeatTimeoutMs?: number;
52
85
  }
53
86
  export interface EffectWorker {
54
87
  /** Effect types this worker claims, even when a local handler exists. */
@@ -57,6 +90,14 @@ export interface EffectWorker {
57
90
  readonly canDispatch?: (task: EffectWorkerTask) => boolean;
58
91
  dispatch(task: EffectWorkerTask): void | Promise<void>;
59
92
  cancel?(task: EffectWorkerTask): void | Promise<void>;
93
+ /**
94
+ * What a rejected `dispatch` means. With `'unacknowledged'` (the default) the task may still have
95
+ * arrived, so the attempt waits for its schedule-to-start timeout or an operator. With
96
+ * `'acknowledged'` a rejection means the transport refused it, so the attempt fails under the
97
+ * retry policy. A transport can still lose an accepted message, so every effect such a worker
98
+ * claims must set `retry.scheduleToStartTimeoutMs`.
99
+ */
100
+ readonly dispatchSemantics?: 'unacknowledged' | 'acknowledged';
60
101
  }
61
102
  export interface StartRunArgs {
62
103
  kind: string;
@@ -66,6 +107,22 @@ export interface StartRunArgs {
66
107
  runId?: string;
67
108
  threadId?: string;
68
109
  idempotencyKey?: string;
110
+ /** The effect that launched this run. The runtime sets it on launches made from effects. */
111
+ launchedBy?: RunLaunchSource;
112
+ /** Recorded on `runtime.run.started` to group related runs. */
113
+ correlationId?: string;
114
+ /** Immutable labels stored on the start event and the run summary. */
115
+ attributes?: RunAttributes;
116
+ /**
117
+ * Bearer material stored beside the log. A retry that sends a different context conflicts.
118
+ * Omitting it on a retry keeps the context already stored.
119
+ */
120
+ secrets?: SecretContext;
121
+ /**
122
+ * The caller's W3C trace context, recorded on `runtime.run.started` when it is a valid version-00
123
+ * `traceparent`. It is not part of the start identity, so a retry from another trace is not a conflict.
124
+ */
125
+ trace?: TraceContext;
69
126
  }
70
127
  export interface StartResult {
71
128
  runId: string;
@@ -91,18 +148,32 @@ export interface LoomsRuntime<TModules extends readonly AnyRuntimeModule[] = rea
91
148
  readonly modules: TModules;
92
149
  readonly registry: ComposedRegistry;
93
150
  startRun(args: StartRunArgs): Effect.Effect<StartResult, Error | EventStoreAppendError | EventStoreError, EventStoreTag>;
151
+ /**
152
+ * Commit a run's start, then schedule a wake instead of waiting for it. A duplicate start with
153
+ * the same identity is a no-op aside from the scheduled wake.
154
+ */
155
+ launchRun(args: StartRunArgs): Effect.Effect<StartResult, Error | EventStoreAppendError | EventStoreError, EventStoreTag>;
94
156
  signal(runId: string, events: ReadonlyArray<EventInput>, options?: {
95
157
  idempotencyKey?: string;
158
+ coalesce?: boolean;
159
+ }): Effect.Effect<RunState, Error | EventStoreAppendError | EventStoreError, EventStoreTag>;
160
+ /**
161
+ * Append events the runtime itself produced for another run. Unlike `signal`, this does not
162
+ * reject reserved runtime event types or rewrite their origin.
163
+ */
164
+ accept(runId: string, events: ReadonlyArray<EventInput>, options?: {
165
+ idempotencyKey?: string;
166
+ coalesce?: boolean;
96
167
  }): Effect.Effect<RunState, Error | EventStoreAppendError | EventStoreError, EventStoreTag>;
97
168
  wake(runId: string): Effect.Effect<RunState, WakeError, EventStoreTag>;
98
- getRun(runId: string): Effect.Effect<RunState, EventStoreTruncationError | EventStoreTrimmedError | EventStoreError, EventStoreTag>;
169
+ getRun(runId: string): Effect.Effect<RunState, EventStoreTruncationError | EventStoreTrimmedError | EventStoreError | DefinitionResolveError, EventStoreTag>;
99
170
  getEvents(runId: string, options?: {
100
171
  fromSeq?: number;
101
172
  limit?: number;
102
173
  }): Effect.Effect<EventEnvelope[], EventStoreError, EventStoreTag>;
103
174
  project<S>(runId: string, definition: ProjectionDefinition<S>): Effect.Effect<S, EventStoreError, EventStoreTag>;
104
175
  replayTo(runId: string, seq: number): Effect.Effect<ReplayStep | null, EventStoreError, EventStoreTag>;
105
- inspectRun(runId: string): Effect.Effect<RunOperationalStatus, EventStoreError | EventStoreTrimmedError | EventStoreTruncationError, EventStoreTag>;
176
+ inspectRun(runId: string): Effect.Effect<RunOperationalStatus, EventStoreError | EventStoreTrimmedError | EventStoreTruncationError | DefinitionResolveError, EventStoreTag>;
106
177
  retryEffect(runId: string, effectId: string): Effect.Effect<RunState, Error | EventStoreAppendError | EventStoreError, EventStoreTag>;
107
178
  cancel(runId: string, threadId?: string): Effect.Effect<RunState, Error | EventStoreAppendError | EventStoreError, EventStoreTag>;
108
179
  /**
@@ -113,13 +184,21 @@ export interface LoomsRuntime<TModules extends readonly AnyRuntimeModule[] = rea
113
184
  fork(sourceRunId: string, options?: {
114
185
  runId?: string;
115
186
  }): Effect.Effect<StartResult, Error | EventStoreAppendError | EventStoreError, EventStoreTag>;
116
- /** Applies a worker's report about one effect attempt; stale or foreign reports are ignored. */
117
- workerCallback(runId: string, input: WorkerCallbackInput): Effect.Effect<RunState, Error | EventStoreAppendError | EventStoreError, EventStoreTag>;
187
+ /**
188
+ * Applies a worker's report about one effect attempt. A report for another attempt, or one that
189
+ * arrives after the attempt settled, is `stale` and changes nothing.
190
+ */
191
+ workerCallback(runId: string, input: WorkerCallbackInput): Effect.Effect<WorkerCallbackResult, Error | EventStoreAppendError | EventStoreError, EventStoreTag>;
192
+ /**
193
+ * The run's secret context for a worker attempt that has started and not settled. Any other
194
+ * attempt is `stale`, so a redelivered or superseded message never sees the secrets.
195
+ */
196
+ workerSecrets(runId: string, callback: WorkerCallback): Effect.Effect<WorkerSecretsResult, Error | EventStoreTruncationError | EventStoreTrimmedError | EventStoreError, EventStoreTag>;
118
197
  readonly listRuns: Effect.Effect<string[], EventStoreError, EventStoreTag>;
119
198
  /** Indexed run rows. Folds a run only when its header has not been written yet. */
120
- readonly listRunSummaries: Effect.Effect<readonly RunSummary[], EventStoreError | EventStoreTrimmedError | EventStoreTruncationError | SnapshotStoreError, EventStoreTag>;
121
- readonly rescanTimers: Effect.Effect<number, EventStoreTruncationError | EventStoreTrimmedError | EventStoreError, EventStoreTag>;
199
+ readonly listRunSummaries: Effect.Effect<readonly RunSummary[], EventStoreError | EventStoreTrimmedError | EventStoreTruncationError | SnapshotStoreError | DefinitionResolveError, EventStoreTag>;
200
+ readonly rescanTimers: Effect.Effect<number, EventStoreTruncationError | EventStoreTrimmedError | EventStoreError | DefinitionResolveError, EventStoreTag>;
122
201
  /** Reschedule every durable timer and effect deadline after restart or repair. */
123
- readonly recoverDeadlines: Effect.Effect<number, EventStoreTruncationError | EventStoreTrimmedError | EventStoreError, EventStoreTag>;
202
+ readonly recoverDeadlines: Effect.Effect<number, EventStoreTruncationError | EventStoreTrimmedError | EventStoreError | DefinitionResolveError, EventStoreTag>;
124
203
  readonly dispose: Effect.Effect<void>;
125
204
  }
@@ -1,5 +1,5 @@
1
1
  import { type Context, type ManagedRuntime } from 'effect';
2
- import { type ComposedRegistry, type RegisteredDefinition } from '@swirls/looms/core';
2
+ import { type ComposedRegistry, type DefinitionResolver, type RunLauncher } from '@swirls/looms/core';
3
3
  import { type RuntimeObserver } from './observer';
4
4
  import type { RuntimeRepository } from './repository';
5
5
  import type { RunExecutionContexts } from './run-execution-context';
@@ -8,13 +8,15 @@ import type { WakeScheduler } from './wake-scheduler';
8
8
  export interface WakeEngineOptions {
9
9
  readonly registry: ComposedRegistry;
10
10
  readonly services: ManagedRuntime.ManagedRuntime<Context.Service.Any, never>;
11
- readonly definitions: ReadonlyMap<string, RegisteredDefinition>;
11
+ readonly definitionResolver: DefinitionResolver;
12
12
  readonly executionContexts: RunExecutionContexts;
13
13
  readonly repository: RuntimeRepository;
14
14
  readonly snapshotEvery: number;
15
15
  readonly maxWakeIterations: number;
16
+ readonly maxInFlightWorkerEffects?: number;
16
17
  readonly worker?: EffectWorker;
17
18
  readonly observer?: RuntimeObserver;
18
19
  readonly scheduler: () => WakeScheduler;
20
+ readonly launcher: () => RunLauncher;
19
21
  }
20
22
  export declare function createWakeEngine(options: WakeEngineOptions): LoomsRuntime['wake'];
@@ -1,5 +1,6 @@
1
1
  import { Clock, Effect, Exit, Option } from 'effect';
2
- import { createEvent, EventStoreTag, foldRun, isRunTerminal, } from '@swirls/looms/core';
2
+ import { createEvent, deferredUntil, EventStoreTag, foldRun, isRunTerminal, } from '@swirls/looms/core';
3
+ import { childCloseCancellations, notifyLaunchingParent } from './child-run.js';
3
4
  import { nextRuntimeDeadline, planDeadlineTransitions, planTerminalCancellationTransitions, } from './deadline-transition-planner.js';
4
5
  import { DuplicateEffectDispatchError, MaxWakeIterationsError, RuntimeExecutionError, } from './errors.js';
5
6
  import { cascadedCancellations, stripSeq, synthesizedThreadFailed, waitSatisfiedEvents, } from './helpers.js';
@@ -10,13 +11,36 @@ function currentTimeMillis() {
10
11
  return Math.floor(performance.timeOrigin + performance.now());
11
12
  }
12
13
  export function createWakeEngine(options) {
13
- const { registry, services, definitions, executionContexts, repository, snapshotEvery, maxWakeIterations, } = options;
14
+ const { registry, services, definitionResolver, executionContexts, repository, snapshotEvery, maxWakeIterations, } = options;
14
15
  const { appendObserved, applyAppend, liveAppends, loadCursor, persistSnapshot } = repository;
15
16
  const runCache = {
16
17
  delete: repository.deleteCachedCursor,
17
18
  set: repository.setCachedCursor,
18
19
  };
19
20
  const observe = (event) => notifyObserver(options.observer, event);
21
+ const closeLaunchedRuns = (store, runId, cursor) => Effect.gen(function* () {
22
+ let next = cursor;
23
+ const state = next.state;
24
+ if (isRunTerminal(state) && !state.childCloseSent && state.launchedRuns.length > 0) {
25
+ const requested = createEvent(runId, {
26
+ type: 'runtime.child.close.requested',
27
+ payload: { policy: 'terminate' },
28
+ threadId: state.rootThreadId,
29
+ origin: { type: 'system' },
30
+ idempotencyKey: `child-close:${runId}`,
31
+ });
32
+ const appended = yield* appendObserved(store, runId, stripSeq([requested]));
33
+ next = yield* applyAppend(store, runId, next, stripSeq([requested]), appended);
34
+ yield* Effect.forEach(state.launchedRuns, (child) => options.launcher().cancel(child.runId), {
35
+ concurrency: 1,
36
+ discard: true,
37
+ });
38
+ }
39
+ if (isRunTerminal(next.state)) {
40
+ yield* notifyLaunchingParent(options.launcher(), runId, next.state);
41
+ }
42
+ return next;
43
+ });
20
44
  return (runId) => Effect.gen(function* () {
21
45
  observe({ type: 'run.wake', runId, at: currentTimeMillis() });
22
46
  const executionContext = executionContexts.get(runId);
@@ -64,11 +88,18 @@ export function createWakeEngine(options) {
64
88
  };
65
89
  return yield* Effect.gen(function* () {
66
90
  const store = yield* EventStoreTag;
91
+ const secrets = store.runSecrets ? yield* store.runSecrets.get(runId) : {};
67
92
  const runInContext = Effect.runPromiseWith(yield* Effect.context());
68
93
  executionContext.liveStore = store;
69
94
  let finalState;
70
95
  do {
71
96
  let cursor = yield* loadCursor(store, runId);
97
+ const closeEvents = childCloseCancellations(cursor.state);
98
+ if (closeEvents.length > 0) {
99
+ const created = closeEvents.map((input) => createEvent(runId, input));
100
+ const appended = yield* appendObserved(store, runId, stripSeq(created));
101
+ cursor = yield* applyAppend(store, runId, cursor, stripSeq(created), appended);
102
+ }
72
103
  if (isRunTerminal(cursor.state)) {
73
104
  const now = yield* Clock.currentTimeMillis;
74
105
  const events = planTerminalCancellationTransitions({
@@ -80,6 +111,7 @@ export function createWakeEngine(options) {
80
111
  const appended = yield* appendObserved(store, runId, stripSeq(events));
81
112
  cursor = yield* applyAppend(store, runId, cursor, stripSeq(events), appended);
82
113
  }
114
+ cursor = yield* closeLaunchedRuns(store, runId, cursor);
83
115
  const nextDeadline = nextRuntimeDeadline(cursor.state, now);
84
116
  if (nextDeadline !== undefined) {
85
117
  yield* options.scheduler().schedule(runId, nextDeadline);
@@ -144,6 +176,8 @@ export function createWakeEngine(options) {
144
176
  registry,
145
177
  worker: options.worker,
146
178
  now,
179
+ maxInFlightWorkerEffects: options.maxInFlightWorkerEffects,
180
+ secrets,
147
181
  });
148
182
  if (workerPlans.length > 0) {
149
183
  for (const workerPlan of workerPlans) {
@@ -151,7 +185,7 @@ export function createWakeEngine(options) {
151
185
  const queuedAppend = yield* appendObserved(store, runId, stripSeq([workerPlan.queuedEvent]));
152
186
  cursor = yield* applyAppend(store, runId, cursor, stripSeq([workerPlan.queuedEvent]), queuedAppend);
153
187
  }
154
- const dispatchEvent = yield* dispatchWorkerEffect(runId, workerPlan, options.worker);
188
+ const dispatchEvent = yield* dispatchWorkerEffect(runId, workerPlan, options.worker, now);
155
189
  const dispatchAppend = yield* appendObserved(store, runId, stripSeq([dispatchEvent]));
156
190
  cursor = yield* applyAppend(store, runId, cursor, stripSeq([dispatchEvent]), dispatchAppend);
157
191
  }
@@ -162,7 +196,9 @@ export function createWakeEngine(options) {
162
196
  : []));
163
197
  const outstanding = state.outstandingEffects.filter((item) => {
164
198
  const execution = state.effectExecutions[item.effectId];
165
- return !((cancellationTargets.has(item.threadId) && item.effect.type !== 'runtime.cancel') ||
199
+ const dueAt = deferredUntil(item.effect);
200
+ return !((dueAt !== undefined && dueAt > now + 5) ||
201
+ (cancellationTargets.has(item.threadId) && item.effect.type !== 'runtime.cancel') ||
166
202
  isWorkerEffect({ runId, item, execution, registry, worker: options.worker }) ||
167
203
  (execution?.status === 'retry_wait' &&
168
204
  execution.nextAttemptAt !== null &&
@@ -213,12 +249,24 @@ export function createWakeEngine(options) {
213
249
  const produced = yield* executeLocalEffects({
214
250
  registry,
215
251
  services,
216
- definitions,
252
+ definitionResolver,
217
253
  executionContext,
218
254
  liveAppends,
219
255
  runInContext,
220
256
  observe,
221
- }, { runId, outstanding, attempts, now });
257
+ launcher: options.launcher(),
258
+ }, {
259
+ runId,
260
+ context: {
261
+ correlationId: cursor.state.startIdentity?.correlationId ?? runId,
262
+ attributes: cursor.state.attributes ?? {},
263
+ secrets,
264
+ trace: cursor.state.startIdentity?.trace ?? null,
265
+ },
266
+ outstanding,
267
+ attempts,
268
+ now,
269
+ });
222
270
  if (produced.length === 0) {
223
271
  break;
224
272
  }
@@ -271,6 +319,9 @@ export function createWakeEngine(options) {
271
319
  const appendResult = yield* appendObserved(store, runId, stripSeq([completed]));
272
320
  cursor = yield* applyAppend(store, runId, cursor, stripSeq([completed]), appendResult);
273
321
  }
322
+ if (isRunTerminal(cursor.state)) {
323
+ cursor = yield* closeLaunchedRuns(store, runId, cursor);
324
+ }
274
325
  // Snapshot on park: the next wake (on any instance) starts from here.
275
326
  cursor = yield* persistSnapshot(store, runId, cursor);
276
327
  const nextDeadline = nextRuntimeDeadline(cursor.state, plannedAt);
@@ -1,5 +1,5 @@
1
1
  import { Effect } from 'effect';
2
- import { EventStoreTag, type ComposedRegistry, type RunState, type WorkerCallbackInput } from '@swirls/looms/core';
2
+ import { EventStoreTag, type ComposedRegistry, type WorkerCallback, type WorkerCallbackInput, type WorkerCallbackResult } from '@swirls/looms/core';
3
3
  import type { RunIngress } from './ingress';
4
4
  import { type RuntimeObserver } from './observer';
5
5
  import type { RuntimeRepository } from './repository';
@@ -12,5 +12,15 @@ export interface WorkerCallbacksOptions {
12
12
  readonly wake: LoomsRuntime['wake'];
13
13
  }
14
14
  export declare function createWorkerCallbacks(options: WorkerCallbacksOptions): {
15
- workerCallback: (runId: string, callback: WorkerCallbackInput) => Effect.Effect<RunState, import("./errors").DuplicateEffectDispatchError | Error | import("@swirls/looms/core").EventPayloadTooLargeError | import("@swirls/looms/core").EventStoreConflictError | import("@swirls/looms/core").EventStoreError | import("@swirls/looms/core").EventStoreFencedError | import("@swirls/looms/core").EventStoreTrimmedError | import("@swirls/looms/core").EventStoreTruncationError | import("./errors").MaxWakeIterationsError | import("./ingress").RunOverloadedError | import("./errors").RuntimeExecutionError, EventStoreTag>;
15
+ workerCallback: (runId: string, callback: WorkerCallbackInput) => Effect.Effect<WorkerCallbackResult, import("@swirls/looms/core").DefinitionResolveError | import("./errors").DuplicateEffectDispatchError | Error | import("@swirls/looms/core").EventPayloadTooLargeError | import("@swirls/looms/core").EventStoreConflictError | import("@swirls/looms/core").EventStoreError | import("@swirls/looms/core").EventStoreFencedError | import("@swirls/looms/core").EventStoreTrimmedError | import("@swirls/looms/core").EventStoreTruncationError | import("./errors").MaxWakeIterationsError | import("./ingress").RunOverloadedError | import("./errors").RuntimeExecutionError, EventStoreTag>;
16
+ workerSecrets: (runId: string, callback: WorkerCallback) => Effect.Effect<{
17
+ status: "stale";
18
+ secrets?: undefined;
19
+ } | {
20
+ status: "cancelled";
21
+ secrets?: undefined;
22
+ } | {
23
+ status: "accepted";
24
+ secrets: import("@swirls/looms/core").SecretContext;
25
+ }, Error | import("@swirls/looms/core").EventStoreError, EventStoreTag>;
16
26
  };