@swirls/looms 0.1.0 → 0.3.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 (224) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/actor/host.js +4 -5
  3. package/dist/actor/index.d.ts +1 -1
  4. package/dist/actor/index.js +1 -1
  5. package/dist/actor/routing.d.ts +5 -0
  6. package/dist/actor/routing.js +14 -0
  7. package/dist/agent/agent-session.js +1 -0
  8. package/dist/agent/debugger/agent-chat.d.ts +2 -0
  9. package/dist/agent/debugger/agent-chat.js +210 -0
  10. package/dist/agent/debugger/index.d.ts +5 -0
  11. package/dist/agent/debugger/index.js +19 -0
  12. package/dist/agent/debugger/summarize.d.ts +2 -0
  13. package/dist/agent/debugger/summarize.js +53 -0
  14. package/dist/agent/debugger/token-usage.d.ts +3 -0
  15. package/dist/agent/debugger/token-usage.js +11 -0
  16. package/dist/agent/definitions.d.ts +3 -11
  17. package/dist/agent/definitions.js +5 -6
  18. package/dist/agent/effects.d.ts +30 -0
  19. package/dist/agent/events.d.ts +2 -0
  20. package/dist/agent/events.js +2 -0
  21. package/dist/agent/llm.js +1 -8
  22. package/dist/agent/module.d.ts +604 -534
  23. package/dist/agent/module.js +11 -7
  24. package/dist/agent/projections.d.ts +5 -0
  25. package/dist/agent/projections.js +43 -2
  26. package/dist/agent/scope.d.ts +2 -0
  27. package/dist/agent/signals.d.ts +7 -0
  28. package/dist/agent/signals.js +25 -16
  29. package/dist/agent/threads.js +35 -21
  30. package/dist/agent/tool-schema.d.ts +1 -17
  31. package/dist/agent/tool-schema.js +3 -42
  32. package/dist/agent/transcript.d.ts +7 -0
  33. package/dist/agent/transcript.js +19 -0
  34. package/dist/agent/types.d.ts +10 -0
  35. package/dist/agent/types.js +4 -0
  36. package/dist/ai-vercel/index.d.ts +1 -1
  37. package/dist/ai-vercel/vercel-evaluator.d.ts +21 -8
  38. package/dist/ai-vercel/vercel-evaluator.js +7 -2
  39. package/dist/approval/debugger/approvals-view.d.ts +3 -0
  40. package/dist/approval/debugger/approvals-view.js +18 -0
  41. package/dist/approval/debugger/index.d.ts +4 -0
  42. package/dist/approval/debugger/index.js +11 -0
  43. package/dist/approval/debugger/summarize.d.ts +2 -0
  44. package/dist/approval/debugger/summarize.js +17 -0
  45. package/dist/approval/effects.d.ts +12 -0
  46. package/dist/approval/module.d.ts +24 -0
  47. package/dist/client/client.d.ts +310 -0
  48. package/dist/client/client.js +20 -2
  49. package/dist/cloudflare/router.d.ts +1 -1
  50. package/dist/cloudflare/router.js +6 -8
  51. package/dist/core/compose.js +1 -0
  52. package/dist/core/debugger-host.d.ts +16 -0
  53. package/dist/core/debugger-host.js +1 -0
  54. package/dist/core/effects.d.ts +7 -0
  55. package/dist/core/effects.js +1 -0
  56. package/dist/core/fold.js +12 -6
  57. package/dist/core/fork.d.ts +35 -0
  58. package/dist/core/fork.js +129 -0
  59. package/dist/core/index.d.ts +3 -0
  60. package/dist/core/index.js +2 -0
  61. package/dist/core/module-scope.d.ts +1 -0
  62. package/dist/core/module.d.ts +4 -2
  63. package/dist/core/projection.js +1 -0
  64. package/dist/core/protocol.d.ts +10 -1
  65. package/dist/core/protocol.js +11 -0
  66. package/dist/core/published.d.ts +43 -0
  67. package/dist/core/published.js +44 -0
  68. package/dist/core/replay.d.ts +8 -0
  69. package/dist/core/schema.d.ts +3 -1
  70. package/dist/core/schema.js +38 -0
  71. package/dist/core/snapshot-store.d.ts +4 -0
  72. package/dist/core/snapshot-store.js +7 -0
  73. package/dist/core/snapshots.js +3 -17
  74. package/dist/core/sqlite-store.js +39 -0
  75. package/dist/core/state.d.ts +14 -0
  76. package/dist/core/state.js +25 -2
  77. package/dist/core/store.d.ts +1 -0
  78. package/dist/debugger/app/assets/index-BevQYa-U.js +22 -0
  79. package/dist/debugger/app/assets/index-CgDzWJg7.css +2 -0
  80. package/dist/debugger/app/assets/rolldown-runtime-CbXtAM7H.js +1 -0
  81. package/dist/debugger/app/assets/routes-B17Om93i.js +47 -0
  82. package/dist/debugger/app/index.html +14 -0
  83. package/dist/debugger/components/composer.d.ts +20 -0
  84. package/dist/debugger/components/composer.js +26 -0
  85. package/dist/debugger/components/debugger-split.d.ts +2 -1
  86. package/dist/debugger/components/debugger-split.js +2 -2
  87. package/dist/debugger/components/event-inspector.d.ts +2 -1
  88. package/dist/debugger/components/event-inspector.js +25 -16
  89. package/dist/debugger/components/event-stream.d.ts +2 -1
  90. package/dist/debugger/components/event-stream.js +12 -71
  91. package/dist/debugger/components/follow-list.d.ts +19 -0
  92. package/dist/debugger/components/follow-list.js +96 -0
  93. package/dist/debugger/components/json-tree.d.ts +11 -0
  94. package/dist/debugger/components/json-tree.js +120 -0
  95. package/dist/debugger/components/json-view.js +2 -2
  96. package/dist/debugger/components/run-tree.js +50 -20
  97. package/dist/debugger/components/status-dot.d.ts +4 -0
  98. package/dist/debugger/components/status-dot.js +6 -2
  99. package/dist/debugger/context.d.ts +14 -0
  100. package/dist/debugger/context.js +17 -0
  101. package/dist/debugger/contracts.d.ts +7 -4
  102. package/dist/debugger/events/builtin-families.d.ts +3 -0
  103. package/dist/debugger/events/builtin-families.js +9 -0
  104. package/dist/debugger/events/catalog.d.ts +3 -4
  105. package/dist/debugger/events/catalog.js +25 -17
  106. package/dist/debugger/events/protocol-summarize.d.ts +1 -2
  107. package/dist/debugger/events/protocol-summarize.js +36 -37
  108. package/dist/debugger/index.d.ts +20 -6
  109. package/dist/debugger/index.js +19 -5
  110. package/dist/debugger/lib/cn.d.ts +1 -0
  111. package/dist/debugger/lib/cn.js +3 -0
  112. package/dist/debugger/lib/event-counts.d.ts +2 -2
  113. package/dist/debugger/lib/event-counts.js +5 -9
  114. package/dist/debugger/lib/event-meta.d.ts +2 -3
  115. package/dist/debugger/lib/event-meta.js +6 -11
  116. package/dist/debugger/lib/json.d.ts +25 -0
  117. package/dist/debugger/lib/json.js +87 -0
  118. package/dist/debugger/lib/payload.d.ts +7 -0
  119. package/dist/debugger/lib/payload.js +11 -0
  120. package/dist/debugger/lib/time.d.ts +3 -0
  121. package/dist/debugger/lib/time.js +24 -0
  122. package/dist/debugger/plugin.d.ts +36 -0
  123. package/dist/debugger/plugin.js +14 -0
  124. package/dist/debugger/server/index.d.ts +14 -0
  125. package/dist/debugger/server/index.js +114 -0
  126. package/dist/debugger/shell/debugger-shell.d.ts +15 -0
  127. package/dist/debugger/shell/debugger-shell.js +43 -0
  128. package/dist/debugger/shell/definition-key.d.ts +3 -0
  129. package/dist/debugger/shell/definition-key.js +4 -0
  130. package/dist/debugger/shell/events-pane.d.ts +12 -0
  131. package/dist/debugger/shell/events-pane.js +25 -0
  132. package/dist/debugger/shell/projections-panel.d.ts +4 -0
  133. package/dist/debugger/shell/projections-panel.js +59 -0
  134. package/dist/debugger/shell/run-controls.d.ts +5 -0
  135. package/dist/debugger/shell/run-controls.js +99 -0
  136. package/dist/debugger/shell/run-search.d.ts +6 -0
  137. package/dist/debugger/shell/run-search.js +100 -0
  138. package/dist/debugger/shell/run-type-menu.d.ts +6 -0
  139. package/dist/debugger/shell/run-type-menu.js +11 -0
  140. package/dist/debugger/shell/schema-form.d.ts +31 -0
  141. package/dist/debugger/shell/schema-form.js +117 -0
  142. package/dist/debugger/shell/start-form.d.ts +3 -0
  143. package/dist/debugger/shell/start-form.js +75 -0
  144. package/dist/debugger/shell/use-start-run.d.ts +14 -0
  145. package/dist/debugger/shell/use-start-run.js +32 -0
  146. package/dist/debugger/shell/workspace.d.ts +6 -0
  147. package/dist/debugger/shell/workspace.js +15 -0
  148. package/dist/debugger/styles.css +111 -27
  149. package/dist/debugger/ui/button.d.ts +24 -0
  150. package/dist/debugger/ui/button.js +24 -0
  151. package/dist/debugger/ui/command.d.ts +9 -0
  152. package/dist/debugger/ui/command.js +24 -0
  153. package/dist/debugger/ui/dropdown-menu.d.ts +13 -0
  154. package/dist/debugger/ui/dropdown-menu.js +37 -0
  155. package/dist/debugger/ui/panel.d.ts +14 -0
  156. package/dist/debugger/ui/panel.js +16 -0
  157. package/dist/debugger/ui/popover.d.ts +7 -0
  158. package/dist/debugger/ui/popover.js +17 -0
  159. package/dist/debugger/ui/resizable.d.ts +7 -0
  160. package/dist/debugger/ui/resizable.js +14 -0
  161. package/dist/debugger/ui/scroll-area.d.ts +2 -1
  162. package/dist/debugger/ui/scroll-area.js +5 -3
  163. package/dist/debugger/ui/select.d.ts +10 -0
  164. package/dist/debugger/ui/select.js +21 -0
  165. package/dist/debugger/ui/tabs.js +2 -2
  166. package/dist/debugger/ui/textarea.d.ts +3 -0
  167. package/dist/debugger/ui/textarea.js +7 -0
  168. package/dist/evaluator/effects.d.ts +12 -0
  169. package/dist/evaluator/module.d.ts +24 -0
  170. package/dist/projectors/index-model.d.ts +13 -7
  171. package/dist/projectors/index-model.js +21 -26
  172. package/dist/projectors/memory.js +19 -11
  173. package/dist/projectors/postgres.js +20 -19
  174. package/dist/projectors/schema.js +20 -18
  175. package/dist/projectors/sqlite.js +32 -27
  176. package/dist/react/materialize.js +16 -11
  177. package/dist/react/react.d.ts +6 -2
  178. package/dist/react/react.js +5 -2
  179. package/dist/react/store.d.ts +1 -1
  180. package/dist/react/store.js +1 -1
  181. package/dist/react/tables.d.ts +3 -2
  182. package/dist/runtime/cancellation.js +14 -27
  183. package/dist/runtime/errors.d.ts +39 -0
  184. package/dist/runtime/errors.js +32 -0
  185. package/dist/runtime/helpers.d.ts +2 -0
  186. package/dist/runtime/helpers.js +23 -1
  187. package/dist/runtime/index.d.ts +1 -1
  188. package/dist/runtime/index.js +1 -1
  189. package/dist/runtime/looms.d.ts +13 -1
  190. package/dist/runtime/looms.js +5 -2
  191. package/dist/runtime/repository.js +16 -1
  192. package/dist/runtime/run-start.js +1 -3
  193. package/dist/runtime/runtime.d.ts +1 -1
  194. package/dist/runtime/runtime.js +60 -18
  195. package/dist/runtime/server/auth.d.ts +1 -0
  196. package/dist/runtime/server/auth.js +5 -0
  197. package/dist/runtime/server/debugger-host.d.ts +11 -0
  198. package/dist/runtime/server/debugger-host.js +32 -0
  199. package/dist/runtime/server/fetch-handler.d.ts +5 -1
  200. package/dist/runtime/server/fetch-handler.js +24 -13
  201. package/dist/runtime/server/http.d.ts +3 -0
  202. package/dist/runtime/server/http.js +21 -2
  203. package/dist/runtime/server/routes/definitions.d.ts +3 -0
  204. package/dist/runtime/server/routes/definitions.js +20 -0
  205. package/dist/runtime/server/routes/runs.js +43 -3
  206. package/dist/runtime/server.d.ts +3 -1
  207. package/dist/runtime/server.js +3 -1
  208. package/dist/runtime/types.d.ts +11 -1
  209. package/dist/runtime/wake-engine.js +5 -2
  210. package/dist/s2/snapshot-store.js +45 -7
  211. package/dist/workflow/debugger/index.d.ts +4 -0
  212. package/dist/workflow/debugger/index.js +11 -0
  213. package/dist/workflow/debugger/nodes-view.d.ts +3 -0
  214. package/dist/workflow/debugger/nodes-view.js +13 -0
  215. package/dist/workflow/debugger/summarize.d.ts +2 -0
  216. package/dist/workflow/debugger/summarize.js +30 -0
  217. package/dist/workflow/effects.d.ts +24 -0
  218. package/dist/workflow/effects.js +1 -0
  219. package/dist/workflow/module.d.ts +48 -0
  220. package/package.json +20 -1
  221. package/dist/debugger/events/domain-summarize.d.ts +0 -3
  222. package/dist/debugger/events/domain-summarize.js +0 -107
  223. package/dist/debugger/events/family.d.ts +0 -8
  224. package/dist/debugger/events/family.js +0 -44
@@ -1,7 +1,7 @@
1
1
  import { Data, Effect, Predicate } from 'effect';
2
2
  import { EventStoreTag, makeMemoryEventStore, snapshotStoreOf, SnapshotStoreTag, } from '@swirls/looms/core';
3
3
  import { createRuntime, } from './runtime.js';
4
- import { createFetchHandler, isLoomsApiPath, serveHttp, } from './server.js';
4
+ import { createFetchHandler, handlesPath, mountDebugger, serveHttp, } from './server.js';
5
5
  class LoomsInitializationError extends Data.TaggedError('LoomsInitializationError') {
6
6
  constructor(cause) {
7
7
  super({
@@ -13,6 +13,7 @@ class LoomsInitializationError extends Data.TaggedError('LoomsInitializationErro
13
13
  }
14
14
  export function createLooms(options = {}) {
15
15
  const modules = options.modules ?? [];
16
+ const debuggerMount = options.debugger ? mountDebugger(options.debugger) : undefined;
16
17
  let initPromise;
17
18
  let runningServer;
18
19
  const getInit = () => {
@@ -52,6 +53,7 @@ export function createLooms(options = {}) {
52
53
  runtime,
53
54
  store,
54
55
  authorize: options.authorize,
56
+ debugger: debuggerMount,
55
57
  });
56
58
  return { runtime, store, snapshotStore: snapshotStoreOf(store), fetchHandler };
57
59
  }));
@@ -96,9 +98,10 @@ export function createLooms(options = {}) {
96
98
  project: (runId, definition) => runEffect((i) => i.runtime.project(runId, definition)),
97
99
  replayTo: (runId, seq) => runEffect((i) => i.runtime.replayTo(runId, seq)),
98
100
  cancel: (runId, threadId) => runEffect((i) => i.runtime.cancel(runId, threadId)),
101
+ fork: (sourceRunId, forkOpts) => runEffect((i) => i.runtime.fork(sourceRunId, forkOpts)),
99
102
  workerCallback: (runId, input) => runEffect((i) => i.runtime.workerCallback(runId, input)),
100
103
  fetch: (req) => {
101
- if (!isLoomsApiPath(new URL(req.url).pathname)) {
104
+ if (!handlesPath(new URL(req.url).pathname, debuggerMount)) {
102
105
  return Promise.resolve(null);
103
106
  }
104
107
  return getInit().then(({ fetchHandler }) => fetchHandler(req));
@@ -1,5 +1,5 @@
1
1
  import { Clock, Data, Effect, Option } from 'effect';
2
- import { advanceCursor, assignSequences, createCoalescingAppender, emptyCursor, EventStoreTrimmedError, hashRunState, trimEventStoreSafely, } from '@swirls/looms/core';
2
+ import { advanceCursor, assignSequences, createCoalescingAppender, emptyCursor, EventStoreTrimmedError, hashRunState, summarizeRun, trimEventStoreSafely, } from '@swirls/looms/core';
3
3
  import { RuntimeExecutionError } from './errors.js';
4
4
  import { stripSeq } from './helpers.js';
5
5
  import { notifyObserver } from './observer.js';
@@ -36,7 +36,18 @@ function readRemaining(store, runId, events, expectedTail) {
36
36
  }
37
37
  export function createRuntimeRepository(options) {
38
38
  const { registry, snapshotStore, trimAfterSnapshot, keepSnapshots, runCache, executionContexts } = options;
39
+ const headerFingerprints = new Map();
39
40
  const observe = (event) => notifyObserver(options.observer, event);
41
+ const rememberHeader = (state) => {
42
+ const header = summarizeRun(state);
43
+ const fingerprint = `${header.status}\0${header.kind ?? ''}\0${header.definitionName ?? ''}`;
44
+ if (headerFingerprints.get(header.runId) === fingerprint) {
45
+ return Effect.void;
46
+ }
47
+ return snapshotStore.saveHeader(header).pipe(Effect.tap(() => Effect.sync(() => {
48
+ headerFingerprints.set(header.runId, fingerprint);
49
+ })), Effect.catch((error) => Effect.logWarning(`Run header persistence failed for "${header.runId}"`, error)));
50
+ };
40
51
  const appendObserved = (store, runId, events, appendOptions) => Effect.suspend(() => {
41
52
  const startedAt = currentTimeMillis();
42
53
  return store.append(runId, events, appendOptions).pipe(Effect.tap(() => Effect.sync(() => observe({
@@ -156,6 +167,7 @@ export function createRuntimeRepository(options) {
156
167
  return yield* loadCursorCold(store, runId);
157
168
  }
158
169
  if (tail === cached.seq) {
170
+ yield* rememberHeader(cached.state);
159
171
  return cached;
160
172
  }
161
173
  const next = yield* catchUp(store, runId, cached, tail).pipe(Effect.catchTag('EventStoreTrimmedError', () => {
@@ -163,10 +175,12 @@ export function createRuntimeRepository(options) {
163
175
  return loadCursorCold(store, runId);
164
176
  }));
165
177
  runCache.set(runId, next);
178
+ yield* rememberHeader(next.state);
166
179
  return next;
167
180
  }
168
181
  const next = yield* loadCursorCold(store, runId);
169
182
  runCache.set(runId, next);
183
+ yield* rememberHeader(next.state);
170
184
  return next;
171
185
  });
172
186
  /**
@@ -241,6 +255,7 @@ export function createRuntimeRepository(options) {
241
255
  next = yield* catchUp(store, runId, next, appendResult.tail);
242
256
  }
243
257
  runCache.set(runId, next);
258
+ yield* rememberHeader(next.state);
244
259
  return next;
245
260
  });
246
261
  /**
@@ -5,9 +5,7 @@ function canonicalJson(value) {
5
5
  return `[${value.map(canonicalJson).join(',')}]`;
6
6
  }
7
7
  if (Predicate.isObject(value)) {
8
- const keys = Object.keys(value);
9
- // oxlint-disable-next-line unicorn/no-array-sort
10
- keys.sort();
8
+ const keys = Object.keys(value).toSorted();
11
9
  return `{${keys
12
10
  .map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key] ?? null)}`)
13
11
  .join(',')}}`;
@@ -2,7 +2,7 @@ import { type AnyRuntimeModule } from '@swirls/looms/core';
2
2
  export type { RegisteredDefinition } from '@swirls/looms/core';
3
3
  export type { WakeScheduler } from './wake-scheduler';
4
4
  export { createTimeoutScheduler } from './wake-scheduler';
5
- export { DuplicateEffectDispatchError, MaxWakeIterationsError, StartRunConflictError, } from './errors';
5
+ export { DuplicateEffectDispatchError, ForkConflictError, MaxWakeIterationsError, RunNotFinishedError, RunNotFoundError, RunTerminalError, StartRunConflictError, } from './errors';
6
6
  export { RunOverloadedError } from './ingress';
7
7
  export type { CreateRuntimeOptions, EffectWorker, EffectWorkerTask, LoomsRuntime, RunOperationalStatus, StartResult, StartRunArgs, WakeError, } from './types';
8
8
  export type { WorkerCallback, WorkerCallbackInput } from '@swirls/looms/core';
@@ -1,8 +1,8 @@
1
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';
2
+ import { composeModules, createEvent, createThreadId, createRunId, DEFAULT_DEFINITION_VERSION, DEFAULT_SNAPSHOT_EVERY, definitionKey, descendantThreadIds, EventStoreTag, foldRun, isRunTerminal, planFork, project, replayTo, summarizeRun, } from '@swirls/looms/core';
3
3
  import { createCancellation } from './cancellation.js';
4
4
  import { nextRuntimeDeadline } from './deadline-transition-planner.js';
5
- import { RuntimeExecutionError } from './errors.js';
5
+ import { ForkConflictError, RunNotFinishedError, RunNotFoundError, RunTerminalError, RuntimeExecutionError, } from './errors.js';
6
6
  import { admitExternalEvents, moduleServices, resolveSnapshotStore, RunCursorCache, threadStartedEvents, UnknownDefinitionError, validateAndCreateEvents, waitSatisfiedEvents, } from './helpers.js';
7
7
  import { createRunIngress } from './ingress.js';
8
8
  import { notifyObserver } from './observer.js';
@@ -13,26 +13,21 @@ import { createWakeEngine } from './wake-engine.js';
13
13
  import { createTimeoutScheduler } from './wake-scheduler.js';
14
14
  import { createWorkerCallbacks } from './worker-callbacks.js';
15
15
  export { createTimeoutScheduler } from './wake-scheduler.js';
16
- export { DuplicateEffectDispatchError, MaxWakeIterationsError, StartRunConflictError, } from './errors.js';
16
+ export { DuplicateEffectDispatchError, ForkConflictError, MaxWakeIterationsError, RunNotFinishedError, RunNotFoundError, RunTerminalError, StartRunConflictError, } from './errors.js';
17
17
  export { RunOverloadedError } from './ingress.js';
18
18
  function currentTimeMillis() {
19
19
  return Math.floor(performance.timeOrigin + performance.now());
20
20
  }
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) {
21
+ /** Threads whose in-process effects an admitted signal interrupts: each named one and its subtree. */
22
+ function interruptedThreadIds(events, rules, state) {
23
+ const named = events.flatMap((event) => {
25
24
  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
- }
34
- }
35
- return threadIds;
25
+ const cancelled = event.type === 'runtime.thread.cancel.requested' && Predicate.isString(payload.threadId)
26
+ ? [payload.threadId]
27
+ : [];
28
+ return [...cancelled, ...rules.flatMap((rule) => rule(event))];
29
+ });
30
+ return new Set(named.flatMap((threadId) => [threadId, ...descendantThreadIds(state, threadId)]));
36
31
  }
37
32
  const DEFAULT_RUN_CACHE_SIZE = 0;
38
33
  const RESCAN_CONCURRENCY = 16;
@@ -83,6 +78,9 @@ export function createRuntime(options) {
83
78
  ...input,
84
79
  idempotencyKey: input.idempotencyKey ?? signalKey,
85
80
  })), { origin: { type: 'external' } });
81
+ if (isRunTerminal(current.state)) {
82
+ return yield* new RunTerminalError(runId, current.state.status);
83
+ }
86
84
  const folded = foldRun(batch, registry, { runId, initial: current.state });
87
85
  const satisfiedEvents = waitSatisfiedEvents(folded, batch).map((input) => createEvent(runId, input));
88
86
  return {
@@ -92,7 +90,7 @@ export function createRuntime(options) {
92
90
  };
93
91
  }));
94
92
  if (committed.append) {
95
- executionContexts.abort(runId, interruptedThreadIds(events, registry.interruptRules), 'Effect interrupted');
93
+ executionContexts.abort(runId, interruptedThreadIds(events, registry.interruptRules, committed.value), 'Effect interrupted');
96
94
  }
97
95
  return committed.value;
98
96
  });
@@ -134,6 +132,21 @@ export function createRuntime(options) {
134
132
  const store = yield* EventStoreTag;
135
133
  return yield* store.listRuns;
136
134
  }),
135
+ listRunSummaries: Effect.gen(function* () {
136
+ const store = yield* EventStoreTag;
137
+ const runIds = yield* store.listRuns;
138
+ const indexed = yield* snapshotStore.listHeaders;
139
+ const byId = new Map(indexed.map((header) => [header.runId, header]));
140
+ const missing = runIds.filter((runId) => !byId.has(runId));
141
+ const filled = yield* Effect.forEach(missing, (runId) => runtime.getRun(runId).pipe(Effect.map(summarizeRun)), { concurrency: 8 });
142
+ for (const header of filled) {
143
+ byId.set(header.runId, header);
144
+ }
145
+ return runIds.flatMap((runId) => {
146
+ const header = byId.get(runId);
147
+ return header ? [header] : [];
148
+ });
149
+ }),
137
150
  getEvents: (runId, readOptions) => Effect.gen(function* () {
138
151
  const store = yield* EventStoreTag;
139
152
  return yield* store.read(runId, readOptions);
@@ -280,6 +293,35 @@ export function createRuntime(options) {
280
293
  },
281
294
  signal: (runId, events, signalOpts) => admitExternalEvents(events).pipe(Effect.flatMap((admitted) => withRunIngress(runId, signalAdmitted(runId, admitted, signalOpts))), Effect.andThen(runtime.wake(runId))),
282
295
  cancel: cancellation,
296
+ fork: (sourceRunId, forkOptions) => {
297
+ const runId = forkOptions?.runId ?? createRunId();
298
+ return withRunIngress(runId, Effect.gen(function* () {
299
+ const store = yield* EventStoreTag;
300
+ const source = yield* loadCursor(store, sourceRunId);
301
+ if (source.seq === 0) {
302
+ return yield* new RunNotFoundError(sourceRunId);
303
+ }
304
+ if (!isRunTerminal(source.state)) {
305
+ return yield* new RunNotFinishedError(sourceRunId, source.state.status);
306
+ }
307
+ const plan = yield* planFork({
308
+ sourceRunId,
309
+ runId,
310
+ events: yield* store.read(sourceRunId),
311
+ registry,
312
+ });
313
+ // Appending at tail 0 numbers the copied events 1..n, the seqs they had in the source.
314
+ yield* commit(store, runId, (existing) => {
315
+ if (existing.seq === 0) {
316
+ return Effect.succeed({ events: plan.events, value: undefined });
317
+ }
318
+ return existing.state.forkedFrom?.runId === sourceRunId
319
+ ? Effect.succeed({ events: [], value: undefined })
320
+ : Effect.fail(new ForkConflictError(runId, sourceRunId));
321
+ });
322
+ return plan.rootThreadId;
323
+ })).pipe(Effect.flatMap((threadId) => runtime.wake(runId).pipe(Effect.map((state) => ({ runId, threadId, state })))));
324
+ },
283
325
  wake: wakeEngine,
284
326
  dispose: Effect.sync(() => scheduler.dispose?.()).pipe(Effect.andThen(services.disposeEffect)),
285
327
  };
@@ -22,6 +22,7 @@ export type AuthDecision = {
22
22
  export type Authorize = (request: Request, route: RouteInfo) => Effect.Effect<AuthDecision>;
23
23
  export declare const authAllowed: AuthDecision;
24
24
  export declare function authDenied(status: number, error: string): AuthDecision;
25
+ export declare function authResponse(decision: AuthDecision): Response | null;
25
26
  /**
26
27
  * Used when a host supplies no `authorize`: run reads and writes stay open, while worker callbacks
27
28
  * and operational endpoints are unavailable until a host opts in.
@@ -3,6 +3,11 @@ export const authAllowed = { allowed: true };
3
3
  export function authDenied(status, error) {
4
4
  return { allowed: false, status, error };
5
5
  }
6
+ export function authResponse(decision) {
7
+ return decision.allowed
8
+ ? null
9
+ : Response.json({ error: decision.error }, { status: decision.status });
10
+ }
6
11
  const unauthorized = authDenied(401, 'Unauthorized');
7
12
  function notConfigured(access) {
8
13
  return authDenied(503, access === 'worker'
@@ -0,0 +1,11 @@
1
+ import { Effect } from 'effect';
2
+ import type { DebuggerHost } from '@swirls/looms/core';
3
+ import { type Authorize } from './auth';
4
+ export interface DebuggerMount {
5
+ readonly basePath: string;
6
+ readonly host: DebuggerHost;
7
+ }
8
+ export declare function mountDebugger(host: DebuggerHost): DebuggerMount;
9
+ /** Path below the mount, or null when `pathname` is outside it. */
10
+ export declare function debuggerPath(mount: DebuggerMount, pathname: string): string | null;
11
+ export declare function serveDebugger(req: Request, mount: DebuggerMount, path: string, authorize: Authorize): Effect.Effect<Response | null>;
@@ -0,0 +1,32 @@
1
+ import { Effect } from 'effect';
2
+ import { authResponse } from './auth.js';
3
+ import { isLoomsApiPath } from './http.js';
4
+ export function mountDebugger(host) {
5
+ const path = host.basePath;
6
+ if (!path.startsWith('/') || path.includes('//') || path.split('/').includes('..')) {
7
+ throw new Error(`debugger basePath must be an absolute URL path: ${path}`);
8
+ }
9
+ const basePath = path.endsWith('/') ? path.slice(0, -1) : path;
10
+ if (basePath === '') {
11
+ throw new Error('debugger basePath must not be /');
12
+ }
13
+ if (isLoomsApiPath(basePath)) {
14
+ throw new Error(`debugger basePath overlaps the HTTP API: ${basePath}`);
15
+ }
16
+ return { basePath, host };
17
+ }
18
+ /** Path below the mount, or null when `pathname` is outside it. */
19
+ export function debuggerPath(mount, pathname) {
20
+ if (pathname === mount.basePath) {
21
+ return '';
22
+ }
23
+ return pathname.startsWith(`${mount.basePath}/`) ? pathname.slice(mount.basePath.length) : null;
24
+ }
25
+ export function serveDebugger(req, mount, path, authorize) {
26
+ return authorize(req, { access: 'read', name: 'debugger' }).pipe(Effect.flatMap((decision) => {
27
+ const denied = authResponse(decision);
28
+ return denied
29
+ ? Effect.succeed(denied)
30
+ : Effect.promise(() => mount.host.fetch(req, { basePath: mount.basePath, path }));
31
+ }));
32
+ }
@@ -1,6 +1,7 @@
1
1
  import type { EventStore } from '@swirls/looms/core';
2
2
  import type { LoomsRuntime } from '../runtime';
3
3
  import { type Authorize } from './auth';
4
+ import { type DebuggerMount } from './debugger-host';
4
5
  export interface FetchHandlerOptions {
5
6
  readonly runtime: LoomsRuntime;
6
7
  readonly store: EventStore;
@@ -9,6 +10,9 @@ export interface FetchHandlerOptions {
9
10
  * operations routes unavailable; use `bearerAuth` or your own policy to change that.
10
11
  */
11
12
  readonly authorize?: Authorize;
13
+ /** Debugger UI served beside the API and authorized as `{ access: 'read', name: 'debugger' }`. */
14
+ readonly debugger?: DebuggerMount;
12
15
  }
13
- export declare function isLoomsApiPath(path: string): boolean;
16
+ /** Whether the fetch handler answers `pathname`: the HTTP API or the debugger mount. */
17
+ export declare function handlesPath(pathname: string, mount: DebuggerMount | undefined): boolean;
14
18
  export declare function createFetchHandler(options: FetchHandlerOptions): (req: Request) => Promise<Response | null>;
@@ -1,12 +1,19 @@
1
1
  import { Effect, Predicate } from 'effect';
2
- import { defaultAuthorize } from './auth.js';
3
- import { acceptsEventStream, toErrorResponse } from './http.js';
2
+ import { authResponse, defaultAuthorize } from './auth.js';
3
+ import { debuggerPath, serveDebugger } from './debugger-host.js';
4
+ import { acceptsEventStream, isLoomsApiPath, toErrorResponse } from './http.js';
4
5
  import { matchRoute } from './route-table.js';
6
+ import { definitionRoutes } from './routes/definitions.js';
5
7
  import { effectRoutes } from './routes/effects.js';
6
8
  import { operationRoutes } from './routes/operations.js';
7
9
  import { runRoutes } from './routes/runs.js';
8
10
  function createRoutes(options) {
9
- return [...runRoutes(options), ...effectRoutes(options), ...operationRoutes(options)];
11
+ return [
12
+ ...definitionRoutes(options),
13
+ ...runRoutes(options),
14
+ ...effectRoutes(options),
15
+ ...operationRoutes(options),
16
+ ];
10
17
  }
11
18
  function authorizeRoute(req, matched, authorize) {
12
19
  const { access, name } = matched.route;
@@ -14,21 +21,22 @@ function authorizeRoute(req, matched, authorize) {
14
21
  return Effect.succeed(null);
15
22
  }
16
23
  const runId = matched.params.runId;
17
- return authorize(req, Predicate.isString(runId) ? { access, name, runId } : { access, name }).pipe(Effect.map((decision) => decision.allowed
18
- ? null
19
- : Response.json({ error: decision.error }, { status: decision.status })));
24
+ return authorize(req, Predicate.isString(runId) ? { access, name, runId } : { access, name }).pipe(Effect.map(authResponse));
20
25
  }
21
- export function isLoomsApiPath(path) {
22
- return (path === '/health' ||
23
- path === '/runs' ||
24
- path.startsWith('/runs/') ||
25
- path.startsWith('/operations/'));
26
+ /** Whether the fetch handler answers `pathname`: the HTTP API or the debugger mount. */
27
+ export function handlesPath(pathname, mount) {
28
+ return isLoomsApiPath(pathname) || (mount !== undefined && debuggerPath(mount, pathname) !== null);
26
29
  }
27
30
  export function createFetchHandler(options) {
28
31
  const routes = createRoutes(options);
29
32
  const authorize = options.authorize ?? defaultAuthorize;
33
+ const mount = options.debugger;
30
34
  return (req) => {
31
35
  const url = new URL(req.url);
36
+ const mountedPath = mount ? debuggerPath(mount, url.pathname) : null;
37
+ if (mount && mountedPath !== null) {
38
+ return respond(serveDebugger(req, mount, mountedPath, authorize));
39
+ }
32
40
  if (!isLoomsApiPath(url.pathname)) {
33
41
  return Promise.resolve(null);
34
42
  }
@@ -36,7 +44,7 @@ export function createFetchHandler(options) {
36
44
  if (req.method === 'GET' && accept.includes('text/html') && !acceptsEventStream(req)) {
37
45
  return Promise.resolve(null);
38
46
  }
39
- return Effect.runPromise(Effect.gen(function* () {
47
+ return respond(Effect.gen(function* () {
40
48
  const matched = matchRoute(routes, req.method, url.pathname);
41
49
  if (!matched) {
42
50
  return new Response('Not Found', { status: 404 });
@@ -46,6 +54,9 @@ export function createFetchHandler(options) {
46
54
  return denied;
47
55
  }
48
56
  return yield* matched.route.handle({ req, url, params: matched.params });
49
- }).pipe(Effect.catch((error) => Effect.succeed(toErrorResponse(error))), Effect.catchCause((cause) => Effect.logError('[@swirls/looms/runtime] request failed', cause).pipe(Effect.as(Response.json({ error: 'Internal server error' }, { status: 500 }))))));
57
+ }));
50
58
  };
51
59
  }
60
+ function respond(effect) {
61
+ return Effect.runPromise(effect.pipe(Effect.catch((error) => Effect.succeed(toErrorResponse(error))), Effect.catchCause((cause) => Effect.logError('[@swirls/looms/runtime] request failed', cause).pipe(Effect.as(Response.json({ error: 'Internal server error' }, { status: 500 }))))));
62
+ }
@@ -26,8 +26,11 @@ export declare const EventInputBodySchema: Schema.Struct<{
26
26
  readonly payload: Schema.Codec<Schema.Json, Schema.Json, never, never>;
27
27
  readonly threadId: Schema.optional<Schema.NullOr<Schema.String>>;
28
28
  }>;
29
+ export declare function isLoomsApiPath(path: string): boolean;
29
30
  export declare function provideStore<A, E>(effect: Effect.Effect<A, E, EventStoreTag>, store: EventStore): Effect.Effect<A, E>;
30
31
  export declare const readJson: (req: Request) => Effect.Effect<JsonValue, RequestBodyError>;
32
+ /** `readJson` for routes whose body is optional: a request without one reads as `{}`. */
33
+ export declare const readOptionalJson: (req: Request) => Effect.Effect<JsonValue, RequestBodyError>;
31
34
  export declare function extractIdempotencyKey(req: Request, bodyKey?: string): string | undefined;
32
35
  export declare function acceptsEventStream(req: Request): boolean;
33
36
  export declare function readIntegerQuery(url: URL, name: string, options: {
@@ -1,6 +1,6 @@
1
1
  import { Data, Effect, Schema } from 'effect';
2
- import { EventStoreTag, InvalidEventError, InvalidInputError, JsonValueSchema, } from '@swirls/looms/core';
3
- import { RunOverloadedError, StartRunConflictError } from '../runtime.js';
2
+ import { EventStoreTag, ForkSourceError, InvalidEventError, InvalidInputError, JsonValueSchema, } from '@swirls/looms/core';
3
+ import { ForkConflictError, RunNotFinishedError, RunNotFoundError, RunOverloadedError, RunTerminalError, StartRunConflictError, } from '../runtime.js';
4
4
  export class RequestBodyError extends Data.TaggedError('RequestBodyError') {
5
5
  constructor(cause) {
6
6
  super({ cause, message: 'Invalid JSON request body' });
@@ -14,6 +14,10 @@ export const EventInputBodySchema = Schema.Struct({
14
14
  payload: JsonValueSchema,
15
15
  threadId: Schema.optional(Schema.NullOr(Schema.String)),
16
16
  });
17
+ const LOOMS_API_ROOTS = ['/health', '/runs', '/definitions', '/operations'];
18
+ export function isLoomsApiPath(path) {
19
+ return LOOMS_API_ROOTS.some((root) => path === root || path.startsWith(`${root}/`));
20
+ }
17
21
  export function provideStore(effect, store) {
18
22
  return Effect.provideService(effect, EventStoreTag, store);
19
23
  }
@@ -21,6 +25,10 @@ export const readJson = (req) => Effect.tryPromise({
21
25
  try: () => req.json(),
22
26
  catch: (cause) => new RequestBodyError(cause),
23
27
  }).pipe(Effect.flatMap(Schema.decodeUnknownEffect(JsonValueSchema)), Effect.mapError((cause) => cause instanceof RequestBodyError ? cause : new RequestBodyError(cause)));
28
+ /** `readJson` for routes whose body is optional: a request without one reads as `{}`. */
29
+ export const readOptionalJson = (req) => req.body === null || req.headers.get('content-length') === '0'
30
+ ? Effect.succeed({})
31
+ : readJson(req);
24
32
  export function extractIdempotencyKey(req, bodyKey) {
25
33
  return (bodyKey ??
26
34
  req.headers.get('idempotency-key') ??
@@ -55,8 +63,19 @@ export function toErrorResponse(err) {
55
63
  if (err instanceof RunOverloadedError) {
56
64
  return Response.json({ error: err.message }, { status: 429 });
57
65
  }
66
+ if (err instanceof RunNotFoundError) {
67
+ return Response.json({ error: err.message }, { status: 404 });
68
+ }
69
+ if (err instanceof RunTerminalError) {
70
+ return Response.json({ error: err.message, status: err.status }, { status: 409 });
71
+ }
58
72
  if (err instanceof StartRunConflictError) {
59
73
  return Response.json({ error: err.message, mismatches: err.mismatches }, { status: 409 });
60
74
  }
75
+ if (err instanceof RunNotFinishedError ||
76
+ err instanceof ForkConflictError ||
77
+ err instanceof ForkSourceError) {
78
+ return Response.json({ error: err.message }, { status: 409 });
79
+ }
61
80
  return Response.json({ error: err.message }, { status: 500 });
62
81
  }
@@ -0,0 +1,3 @@
1
+ import type { RouteOptions } from '../http';
2
+ import { type Route } from '../route-table';
3
+ export declare function definitionRoutes({ runtime }: RouteOptions): readonly Route[];
@@ -0,0 +1,20 @@
1
+ import { Effect } from 'effect';
2
+ import { publishDefinition } from '@swirls/looms/core';
3
+ import { route } from '../route-table.js';
4
+ export function definitionRoutes({ runtime }) {
5
+ return [
6
+ route({
7
+ method: 'GET',
8
+ path: '/definitions',
9
+ name: 'definitions.list',
10
+ access: 'read',
11
+ handle: () => {
12
+ const catalog = {
13
+ definitions: runtime.registry.definitions.map(publishDefinition),
14
+ projections: [...runtime.registry.projections.keys()],
15
+ };
16
+ return Effect.succeed(Response.json(catalog));
17
+ },
18
+ }),
19
+ ];
20
+ }
@@ -1,7 +1,7 @@
1
1
  import { Effect, Schema } from 'effect';
2
2
  import { createRunId, EventStoreTag, JsonValueSchema, stringifyJson, } from '@swirls/looms/core';
3
3
  import { createEventStreamResponse } from '../../sse.js';
4
- import { acceptsEventStream, EventInputBodySchema, extractIdempotencyKey, provideStore, readIntegerQuery, readJson, RequestQueryError, } from '../http.js';
4
+ import { acceptsEventStream, EventInputBodySchema, extractIdempotencyKey, provideStore, readIntegerQuery, readJson, readOptionalJson, RequestQueryError, } from '../http.js';
5
5
  import { route } from '../route-table.js';
6
6
  const StartRunBodySchema = Schema.Struct({
7
7
  kind: Schema.optional(Schema.String),
@@ -19,6 +19,12 @@ const SignalBodySchema = Schema.Struct({
19
19
  threadId: Schema.optional(Schema.NullOr(Schema.String)),
20
20
  idempotencyKey: Schema.optional(Schema.String),
21
21
  });
22
+ const CancelBodySchema = Schema.Struct({
23
+ threadId: Schema.optional(Schema.String),
24
+ });
25
+ const ForkBodySchema = Schema.Struct({
26
+ runId: Schema.optional(Schema.String),
27
+ });
22
28
  function wantsRunStream(req, url) {
23
29
  return url.searchParams.get('stream') === 'true' || acceptsEventStream(req);
24
30
  }
@@ -72,7 +78,14 @@ export function runRoutes({ runtime, store }) {
72
78
  path: '/runs',
73
79
  name: 'runs.list',
74
80
  access: 'read',
75
- handle: () => provideStore(runtime.listRuns, store).pipe(Effect.map((runIds) => Response.json({ runIds }))),
81
+ handle: () => provideStore(runtime.listRuns.pipe(Effect.map((runIds) => Response.json({ runIds }))), store),
82
+ }),
83
+ route({
84
+ method: 'GET',
85
+ path: '/runs/summaries',
86
+ name: 'runs.summaries',
87
+ access: 'read',
88
+ handle: () => provideStore(runtime.listRunSummaries.pipe(Effect.map((runs) => Response.json({ runs }))), store),
76
89
  }),
77
90
  route({
78
91
  method: 'POST',
@@ -186,7 +199,34 @@ export function runRoutes({ runtime, store }) {
186
199
  path: '/runs/:runId/wake',
187
200
  name: 'runs.wake',
188
201
  access: 'write',
189
- handle: ({ params }) => provideStore(runtime.wake(params.runId), store).pipe(Effect.map((state) => Response.json({ runId: params.runId, state }))),
202
+ handle: ({ params }) => Effect.gen(function* () {
203
+ const before = yield* store.tail(params.runId);
204
+ const state = yield* provideStore(runtime.wake(params.runId), store);
205
+ const appended = (yield* store.tail(params.runId)) - before;
206
+ return Response.json({ runId: params.runId, state, appended });
207
+ }),
208
+ }),
209
+ route({
210
+ method: 'POST',
211
+ path: '/runs/:runId/cancel',
212
+ name: 'runs.cancel',
213
+ access: 'write',
214
+ handle: ({ req, params }) => Effect.gen(function* () {
215
+ const body = yield* readOptionalJson(req).pipe(Effect.flatMap(Schema.decodeUnknownEffect(CancelBodySchema)));
216
+ const state = yield* provideStore(runtime.cancel(params.runId, body.threadId), store);
217
+ return Response.json({ runId: params.runId, state });
218
+ }),
219
+ }),
220
+ route({
221
+ method: 'POST',
222
+ path: '/runs/:runId/fork',
223
+ name: 'runs.fork',
224
+ access: 'write',
225
+ handle: ({ req, params }) => Effect.gen(function* () {
226
+ const body = yield* readOptionalJson(req).pipe(Effect.flatMap(Schema.decodeUnknownEffect(ForkBodySchema)));
227
+ const result = yield* provideStore(runtime.fork(params.runId, { runId: body.runId }), store);
228
+ return Response.json(result);
229
+ }),
190
230
  }),
191
231
  ];
192
232
  }
@@ -1,4 +1,6 @@
1
- export { createFetchHandler, isLoomsApiPath, type FetchHandlerOptions, } from './server/fetch-handler';
1
+ export { createFetchHandler, handlesPath, type FetchHandlerOptions } from './server/fetch-handler';
2
+ export { isLoomsApiPath } from './server/http';
3
+ export { mountDebugger, type DebuggerMount } from './server/debugger-host';
2
4
  export { authAllowed, authDenied, bearerAuth, defaultAuthorize, type AuthDecision, type AuthorizedAccess, type Authorize, type BearerTokens, type RouteAccess, type RouteInfo, } from './server/auth';
3
5
  export interface RunningServer {
4
6
  port: number;
@@ -1,4 +1,6 @@
1
- export { createFetchHandler, isLoomsApiPath, } from './server/fetch-handler.js';
1
+ export { createFetchHandler, handlesPath } from './server/fetch-handler.js';
2
+ export { isLoomsApiPath } from './server/http.js';
3
+ export { mountDebugger } from './server/debugger-host.js';
2
4
  export { authAllowed, authDenied, bearerAuth, defaultAuthorize, } from './server/auth.js';
3
5
  export function serveHttp(fetchHandler, options) {
4
6
  const port = options?.port ?? 8787;
@@ -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, SnapshotStore } from '@swirls/looms/core';
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';
3
3
  import type { EventStoreTrimmedError } from '@swirls/looms/core';
4
4
  import type { DuplicateEffectDispatchError, MaxWakeIterationsError } from './errors';
5
5
  import type { RuntimeObserver } from './observer';
@@ -105,9 +105,19 @@ export interface LoomsRuntime<TModules extends readonly AnyRuntimeModule[] = rea
105
105
  inspectRun(runId: string): Effect.Effect<RunOperationalStatus, EventStoreError | EventStoreTrimmedError | EventStoreTruncationError, EventStoreTag>;
106
106
  retryEffect(runId: string, effectId: string): Effect.Effect<RunState, Error | EventStoreAppendError | EventStoreError, EventStoreTag>;
107
107
  cancel(runId: string, threadId?: string): Effect.Effect<RunState, Error | EventStoreAppendError | EventStoreError, EventStoreTag>;
108
+ /**
109
+ * Start a new run that continues where a finished run ended. The fork copies the source's
110
+ * history up to the point it finished and re-issues the effects outstanding there. Retrying
111
+ * with the same `runId` returns the existing fork.
112
+ */
113
+ fork(sourceRunId: string, options?: {
114
+ runId?: string;
115
+ }): Effect.Effect<StartResult, Error | EventStoreAppendError | EventStoreError, EventStoreTag>;
108
116
  /** Applies a worker's report about one effect attempt; stale or foreign reports are ignored. */
109
117
  workerCallback(runId: string, input: WorkerCallbackInput): Effect.Effect<RunState, Error | EventStoreAppendError | EventStoreError, EventStoreTag>;
110
118
  readonly listRuns: Effect.Effect<string[], EventStoreError, EventStoreTag>;
119
+ /** 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>;
111
121
  readonly rescanTimers: Effect.Effect<number, EventStoreTruncationError | EventStoreTrimmedError | EventStoreError, EventStoreTag>;
112
122
  /** Reschedule every durable timer and effect deadline after restart or repair. */
113
123
  readonly recoverDeadlines: Effect.Effect<number, EventStoreTruncationError | EventStoreTrimmedError | EventStoreError, EventStoreTag>;
@@ -2,7 +2,7 @@ import { Clock, Effect, Exit, Option } from 'effect';
2
2
  import { createEvent, EventStoreTag, foldRun, isRunTerminal, } from '@swirls/looms/core';
3
3
  import { nextRuntimeDeadline, planDeadlineTransitions, planTerminalCancellationTransitions, } from './deadline-transition-planner.js';
4
4
  import { DuplicateEffectDispatchError, MaxWakeIterationsError, RuntimeExecutionError, } from './errors.js';
5
- import { stripSeq, synthesizedThreadFailed, waitSatisfiedEvents } from './helpers.js';
5
+ import { cascadedCancellations, stripSeq, synthesizedThreadFailed, waitSatisfiedEvents, } from './helpers.js';
6
6
  import { executeLocalEffects } from './local-effect-executor.js';
7
7
  import { notifyObserver } from './observer.js';
8
8
  import { dispatchWorkerEffect, isWorkerEffect, planWorkerEffects } from './worker-effect-dispatch.js';
@@ -225,7 +225,10 @@ export function createWakeEngine(options) {
225
225
  yield* liveAppends.drain(runId);
226
226
  const beforeFold = cursor.state;
227
227
  const intermediateState = foldRun(produced, registry, { runId, initial: beforeFold });
228
- const synthesized = synthesizedThreadFailed(beforeFold, intermediateState, produced);
228
+ const synthesized = [
229
+ ...synthesizedThreadFailed(beforeFold, intermediateState, produced),
230
+ ...cascadedCancellations(intermediateState, produced),
231
+ ];
229
232
  const synthesizedEvents = synthesized.map((input) => createEvent(runId, input));
230
233
  const stateAfterSynth = synthesizedEvents.length > 0
231
234
  ? foldRun(synthesizedEvents, registry, { runId, initial: intermediateState })