aimeat 1.23.2 → 1.24.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 (239) hide show
  1. package/dist/locales/en.json +580 -21
  2. package/dist/locales/fi.json +592 -33
  3. package/dist/public/components/ContactCard.js +28 -0
  4. package/dist/public/css/views/admin.css +12 -0
  5. package/dist/public/css/views/agents-detail.css +62 -10
  6. package/dist/public/css/views/landing.css +87 -0
  7. package/dist/public/css/views/offers.css +96 -0
  8. package/dist/public/css/views/portfolio.css +18 -0
  9. package/dist/public/css/views/profile.css +153 -8
  10. package/dist/public/css/views/start.css +96 -0
  11. package/dist/public/css/views/workflows.css +111 -0
  12. package/dist/public/img/business-hero.png +0 -0
  13. package/dist/public/img/toimitus.png +0 -0
  14. package/dist/public/js/components/tag-cloud.js +46 -31
  15. package/dist/public/js/components/tag-editor.js +2 -2
  16. package/dist/public/js/recents.js +40 -0
  17. package/dist/public/js/services/admin.js +6 -0
  18. package/dist/public/js/services/agents.js +34 -0
  19. package/dist/public/js/services/offers.js +152 -0
  20. package/dist/public/js/services/packages.js +1 -0
  21. package/dist/public/js/services/workflows.js +57 -0
  22. package/dist/public/llms-template.txt +7 -0
  23. package/dist/public/spa.html +35 -3
  24. package/dist/public/views/admin/subdomains-tab.js +182 -0
  25. package/dist/public/views/admin.js +2 -0
  26. package/dist/public/views/business.js +119 -0
  27. package/dist/public/views/how-it-works.js +159 -0
  28. package/dist/public/views/landing.js +235 -0
  29. package/dist/public/views/portfolio.js +120 -23
  30. package/dist/public/views/pricing.js +75 -0
  31. package/dist/public/views/profile/access-tab.js +124 -51
  32. package/dist/public/views/profile/agents/agent-card.js +68 -37
  33. package/dist/public/views/profile/agents/shared-board.js +56 -22
  34. package/dist/public/views/profile/agents/tab-activity.js +24 -12
  35. package/dist/public/views/profile/agents/tab-agent-config.js +31 -1
  36. package/dist/public/views/profile/agents/tab-data-access.js +9 -13
  37. package/dist/public/views/profile/agents/tab-integration.js +27 -1
  38. package/dist/public/views/profile/agents/tab-messages.js +6 -11
  39. package/dist/public/views/profile/agents/tab-quality.js +45 -5
  40. package/dist/public/views/profile/agents-tab.js +41 -6
  41. package/dist/public/views/profile/apps-tab.js +7 -1
  42. package/dist/public/views/profile/knowledge-tab.js +4 -1
  43. package/dist/public/views/profile/landing-page.js +428 -123
  44. package/dist/public/views/profile/memory-tab.js +409 -211
  45. package/dist/public/views/profile/nodes-tab.js +18 -3
  46. package/dist/public/views/profile/offers-tab.js +310 -0
  47. package/dist/public/views/profile/organisms-tab.js +289 -28
  48. package/dist/public/views/profile/packages-tab.js +152 -16
  49. package/dist/public/views/profile/portfolio-tab.js +98 -31
  50. package/dist/public/views/profile/workflows-form.js +220 -0
  51. package/dist/public/views/profile/workflows-tab.js +244 -0
  52. package/dist/public/views/profile.js +4 -0
  53. package/dist/public/views/start-flows.js +281 -0
  54. package/dist/public/views/start.js +258 -0
  55. package/dist/src/cli/connect/mcp/tools/index.d.ts.map +1 -1
  56. package/dist/src/cli/connect/mcp/tools/index.js +2 -0
  57. package/dist/src/cli/connect/mcp/tools/index.js.map +1 -1
  58. package/dist/src/cli/connect/mcp/tools/workflows.d.ts +12 -0
  59. package/dist/src/cli/connect/mcp/tools/workflows.d.ts.map +1 -0
  60. package/dist/src/cli/connect/mcp/tools/workflows.js +37 -0
  61. package/dist/src/cli/connect/mcp/tools/workflows.js.map +1 -0
  62. package/dist/src/cli/connect/tool-call.d.ts.map +1 -1
  63. package/dist/src/cli/connect/tool-call.js +37 -0
  64. package/dist/src/cli/connect/tool-call.js.map +1 -1
  65. package/dist/src/generated/api-types.d.ts +1474 -66
  66. package/dist/src/generated/api-types.d.ts.map +1 -1
  67. package/dist/src/generated/prisma-postgres/edge.js +15 -3
  68. package/dist/src/generated/prisma-postgres/index-browser.js +12 -0
  69. package/dist/src/generated/prisma-postgres/index.d.ts +1476 -160
  70. package/dist/src/generated/prisma-postgres/index.js +15 -3
  71. package/dist/src/generated/prisma-postgres/package.json +1 -1
  72. package/dist/src/generated/prisma-postgres/schema.prisma +11 -0
  73. package/dist/src/generated/prisma-postgres/wasm.js +15 -3
  74. package/dist/src/mcp/annotations.d.ts.map +1 -1
  75. package/dist/src/mcp/annotations.js +3 -0
  76. package/dist/src/mcp/annotations.js.map +1 -1
  77. package/dist/src/mcp/catalog/definitions.d.ts.map +1 -1
  78. package/dist/src/mcp/catalog/definitions.js +27 -0
  79. package/dist/src/mcp/catalog/definitions.js.map +1 -1
  80. package/dist/src/mcp/catalog/scopes.d.ts.map +1 -1
  81. package/dist/src/mcp/catalog/scopes.js +5 -1
  82. package/dist/src/mcp/catalog/scopes.js.map +1 -1
  83. package/dist/src/mcp/catalog/surfaces.d.ts.map +1 -1
  84. package/dist/src/mcp/catalog/surfaces.js +1 -0
  85. package/dist/src/mcp/catalog/surfaces.js.map +1 -1
  86. package/dist/src/mcp/index.d.ts.map +1 -1
  87. package/dist/src/mcp/index.js +2 -0
  88. package/dist/src/mcp/index.js.map +1 -1
  89. package/dist/src/mcp/workflows.d.ts +18 -0
  90. package/dist/src/mcp/workflows.d.ts.map +1 -0
  91. package/dist/src/mcp/workflows.js +68 -0
  92. package/dist/src/mcp/workflows.js.map +1 -0
  93. package/dist/src/middleware/subdomain.d.ts +28 -0
  94. package/dist/src/middleware/subdomain.d.ts.map +1 -0
  95. package/dist/src/middleware/subdomain.js +32 -0
  96. package/dist/src/middleware/subdomain.js.map +1 -0
  97. package/dist/src/models/offer-schemas.d.ts +236 -0
  98. package/dist/src/models/offer-schemas.d.ts.map +1 -0
  99. package/dist/src/models/offer-schemas.js +112 -0
  100. package/dist/src/models/offer-schemas.js.map +1 -0
  101. package/dist/src/models/workflow-schemas.d.ts +220 -0
  102. package/dist/src/models/workflow-schemas.d.ts.map +1 -0
  103. package/dist/src/models/workflow-schemas.js +102 -0
  104. package/dist/src/models/workflow-schemas.js.map +1 -0
  105. package/dist/src/routes/agent-tasks.d.ts.map +1 -1
  106. package/dist/src/routes/agent-tasks.js +49 -0
  107. package/dist/src/routes/agent-tasks.js.map +1 -1
  108. package/dist/src/routes/agents.d.ts.map +1 -1
  109. package/dist/src/routes/agents.js +146 -1
  110. package/dist/src/routes/agents.js.map +1 -1
  111. package/dist/src/routes/apps-backup.d.ts +20 -0
  112. package/dist/src/routes/apps-backup.d.ts.map +1 -0
  113. package/dist/src/routes/apps-backup.js +165 -0
  114. package/dist/src/routes/apps-backup.js.map +1 -0
  115. package/dist/src/routes/ghii.d.ts.map +1 -1
  116. package/dist/src/routes/ghii.js +4 -0
  117. package/dist/src/routes/ghii.js.map +1 -1
  118. package/dist/src/routes/instances.d.ts +2 -1
  119. package/dist/src/routes/instances.d.ts.map +1 -1
  120. package/dist/src/routes/instances.js +59 -1
  121. package/dist/src/routes/instances.js.map +1 -1
  122. package/dist/src/routes/memory.d.ts.map +1 -1
  123. package/dist/src/routes/memory.js +5 -0
  124. package/dist/src/routes/memory.js.map +1 -1
  125. package/dist/src/routes/packages.d.ts.map +1 -1
  126. package/dist/src/routes/packages.js +32 -0
  127. package/dist/src/routes/packages.js.map +1 -1
  128. package/dist/src/routes/permissions.d.ts +12 -0
  129. package/dist/src/routes/permissions.d.ts.map +1 -1
  130. package/dist/src/routes/permissions.js +35 -3
  131. package/dist/src/routes/permissions.js.map +1 -1
  132. package/dist/src/routes/portal.d.ts +3 -0
  133. package/dist/src/routes/portal.d.ts.map +1 -1
  134. package/dist/src/routes/portal.js +12 -0
  135. package/dist/src/routes/portal.js.map +1 -1
  136. package/dist/src/routes/public-stats.d.ts +15 -0
  137. package/dist/src/routes/public-stats.d.ts.map +1 -0
  138. package/dist/src/routes/public-stats.js +90 -0
  139. package/dist/src/routes/public-stats.js.map +1 -0
  140. package/dist/src/routes/sse.d.ts +7 -2
  141. package/dist/src/routes/sse.d.ts.map +1 -1
  142. package/dist/src/routes/sse.js +35 -5
  143. package/dist/src/routes/sse.js.map +1 -1
  144. package/dist/src/routes/stats.d.ts.map +1 -1
  145. package/dist/src/routes/stats.js +13 -0
  146. package/dist/src/routes/stats.js.map +1 -1
  147. package/dist/src/routes/subdomains.d.ts +33 -0
  148. package/dist/src/routes/subdomains.d.ts.map +1 -0
  149. package/dist/src/routes/subdomains.js +208 -0
  150. package/dist/src/routes/subdomains.js.map +1 -0
  151. package/dist/src/routes/workflows.d.ts +30 -0
  152. package/dist/src/routes/workflows.d.ts.map +1 -0
  153. package/dist/src/routes/workflows.js +178 -0
  154. package/dist/src/routes/workflows.js.map +1 -0
  155. package/dist/src/server-bootstrap/routes-loader.d.ts +2 -0
  156. package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
  157. package/dist/src/server-bootstrap/routes-loader.js +19 -2
  158. package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
  159. package/dist/src/server-bootstrap/service-init.d.ts +2 -0
  160. package/dist/src/server-bootstrap/service-init.d.ts.map +1 -1
  161. package/dist/src/server-bootstrap/service-init.js +6 -0
  162. package/dist/src/server-bootstrap/service-init.js.map +1 -1
  163. package/dist/src/server.d.ts.map +1 -1
  164. package/dist/src/server.js +5 -0
  165. package/dist/src/server.js.map +1 -1
  166. package/dist/src/services/apps-backup-export.d.ts +22 -0
  167. package/dist/src/services/apps-backup-export.d.ts.map +1 -0
  168. package/dist/src/services/apps-backup-export.js +136 -0
  169. package/dist/src/services/apps-backup-export.js.map +1 -0
  170. package/dist/src/services/apps-backup-import.d.ts +120 -0
  171. package/dist/src/services/apps-backup-import.d.ts.map +1 -0
  172. package/dist/src/services/apps-backup-import.js +259 -0
  173. package/dist/src/services/apps-backup-import.js.map +1 -0
  174. package/dist/src/services/component-registrar.d.ts +17 -0
  175. package/dist/src/services/component-registrar.d.ts.map +1 -1
  176. package/dist/src/services/component-registrar.js +63 -5
  177. package/dist/src/services/component-registrar.js.map +1 -1
  178. package/dist/src/services/handbooks/agent.d.ts +1 -1
  179. package/dist/src/services/handbooks/agent.d.ts.map +1 -1
  180. package/dist/src/services/handbooks/agent.js +10 -0
  181. package/dist/src/services/handbooks/agent.js.map +1 -1
  182. package/dist/src/services/handbooks/appdev.d.ts +1 -1
  183. package/dist/src/services/handbooks/appdev.d.ts.map +1 -1
  184. package/dist/src/services/handbooks/appdev.js +5 -2
  185. package/dist/src/services/handbooks/appdev.js.map +1 -1
  186. package/dist/src/services/scheduler.d.ts +6 -0
  187. package/dist/src/services/scheduler.d.ts.map +1 -1
  188. package/dist/src/services/scheduler.js +22 -0
  189. package/dist/src/services/scheduler.js.map +1 -1
  190. package/dist/src/services/schema-validator.d.ts +9 -0
  191. package/dist/src/services/schema-validator.d.ts.map +1 -1
  192. package/dist/src/services/schema-validator.js +16 -0
  193. package/dist/src/services/schema-validator.js.map +1 -1
  194. package/dist/src/services/workflow/engine.d.ts +84 -0
  195. package/dist/src/services/workflow/engine.d.ts.map +1 -0
  196. package/dist/src/services/workflow/engine.js +545 -0
  197. package/dist/src/services/workflow/engine.js.map +1 -0
  198. package/dist/src/services/workflow/eval-context.d.ts +23 -0
  199. package/dist/src/services/workflow/eval-context.d.ts.map +1 -0
  200. package/dist/src/services/workflow/eval-context.js +52 -0
  201. package/dist/src/services/workflow/eval-context.js.map +1 -0
  202. package/dist/src/services/workflow/lifecycle.d.ts +38 -0
  203. package/dist/src/services/workflow/lifecycle.d.ts.map +1 -0
  204. package/dist/src/services/workflow/lifecycle.js +99 -0
  205. package/dist/src/services/workflow/lifecycle.js.map +1 -0
  206. package/dist/src/services/workflow/signal-eval.d.ts +70 -0
  207. package/dist/src/services/workflow/signal-eval.d.ts.map +1 -0
  208. package/dist/src/services/workflow/signal-eval.js +174 -0
  209. package/dist/src/services/workflow/signal-eval.js.map +1 -0
  210. package/dist/src/services/workflow/store.d.ts +89 -0
  211. package/dist/src/services/workflow/store.d.ts.map +1 -0
  212. package/dist/src/services/workflow/store.js +284 -0
  213. package/dist/src/services/workflow/store.js.map +1 -0
  214. package/dist/src/storage/interface.d.ts +13 -3
  215. package/dist/src/storage/interface.d.ts.map +1 -1
  216. package/dist/src/storage/interface.js.map +1 -1
  217. package/dist/src/storage/providers/mongodb/index.d.ts +10 -1
  218. package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
  219. package/dist/src/storage/providers/mongodb/index.js +76 -0
  220. package/dist/src/storage/providers/mongodb/index.js.map +1 -1
  221. package/dist/src/storage/providers/sqlite/index.d.ts +8 -1
  222. package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
  223. package/dist/src/storage/providers/sqlite/index.js +61 -0
  224. package/dist/src/storage/providers/sqlite/index.js.map +1 -1
  225. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  226. package/dist/src/storage/providers/sqlite/schema.js +11 -0
  227. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  228. package/dist/src/storage/repositories/index.d.ts +1 -0
  229. package/dist/src/storage/repositories/index.d.ts.map +1 -1
  230. package/dist/src/storage/repositories/package.repository.d.ts +2 -0
  231. package/dist/src/storage/repositories/package.repository.d.ts.map +1 -1
  232. package/dist/src/storage/repositories/subdomain-site.repository.d.ts +19 -0
  233. package/dist/src/storage/repositories/subdomain-site.repository.d.ts.map +1 -0
  234. package/dist/src/storage/repositories/subdomain-site.repository.js +2 -0
  235. package/dist/src/storage/repositories/subdomain-site.repository.js.map +1 -0
  236. package/dist/static/app-catalog.html +1139 -28
  237. package/package.json +1 -1
  238. package/prisma/schema.postgres.prisma +11 -0
  239. package/prisma/schema.prisma +11 -0
@@ -0,0 +1,84 @@
1
+ import type { AimeatConfig } from '../../config.js';
2
+ import type { Storage, AgentTaskRecord } from '../../storage/interface.js';
3
+ import type { createWebhookDispatcher } from '../webhook-dispatcher.js';
4
+ import type { PushService } from '../push.js';
5
+ import type { WorkflowDef, WorkflowRunStep, WorkflowStep } from '../../models/workflow-schemas.js';
6
+ type WebhookDispatcher = ReturnType<typeof createWebhookDispatcher>;
7
+ export declare function setActiveWorkflowEngine(e: WorkflowEngine): void;
8
+ export declare function getActiveWorkflowEngine(): WorkflowEngine | null;
9
+ /**
10
+ * The steps that may start now: pending, all `after` deps green, and past any retry backoff.
11
+ * Pure — operates on the def + the current run-step map.
12
+ */
13
+ export declare function computeReadySteps(def: WorkflowDef, steps: Record<string, WorkflowRunStep>, now: string): WorkflowStep[];
14
+ /** The terminal run status given the step states: done if all green, else partial; running otherwise. */
15
+ export declare function runOutcome(steps: Record<string, WorkflowRunStep>): 'done' | 'partial' | 'running';
16
+ export interface StartRunOpts {
17
+ mode: 'signals-only' | 'full-live' | 'full-sandbox';
18
+ vars?: Record<string, string>;
19
+ }
20
+ export declare class WorkflowEngine {
21
+ private config;
22
+ private storage;
23
+ private webhookDispatcher?;
24
+ private pushService?;
25
+ /** Per-run serialization: advancing the same run from two task completions is a RMW race. */
26
+ private locks;
27
+ private sweepTimer?;
28
+ constructor(config: AimeatConfig, storage: Storage);
29
+ setWebhookDispatcher(d: WebhookDispatcher): void;
30
+ setPushService(p: PushService): void;
31
+ /** Start the watchdog (timeouts + due retries). Idempotent; the timer is unref'd. */
32
+ start(): Promise<void>;
33
+ stop(): void;
34
+ /** Run a single run's mutation under its lock (serializes concurrent advances). */
35
+ private withLock;
36
+ startRun(ownerGhii: string, ownerName: string, workflowId: string, opts: StartRunOpts): Promise<{
37
+ runId: string;
38
+ } | {
39
+ error: string[];
40
+ }>;
41
+ /** The pinned per-step resolved signals (from start time) as a lookup. No mid-run re-resolution. */
42
+ private resolvedMap;
43
+ /** signals-only: evaluate every step's input + output against existing memory; no dispatch. */
44
+ private runSignalsOnly;
45
+ private tick;
46
+ onTaskTerminal(task: AgentTaskRecord, outcome: 'done' | 'failed'): Promise<void>;
47
+ sweep(): Promise<void>;
48
+ private sweepRun;
49
+ onMemoryWrite(ownerGhii: string, key: string): Promise<void>;
50
+ onOfferOrdered(ownerGhii: string, offerId: string): Promise<void>;
51
+ private fireEventTriggers;
52
+ /**
53
+ * On boot, re-sync in-flight runs: a step's task may have reached done/failed during the restart
54
+ * gap (after the HTTP response, before the fire-and-forget onTaskTerminal ran), leaving the step
55
+ * stuck `dispatched`. Re-check each dispatched step's tasks and advance any that already finished —
56
+ * otherwise the watchdog would wrongly TIME OUT a step whose task actually succeeded. Remaining
57
+ * in-flight runs are then carried by live task events + the watchdog.
58
+ */
59
+ resumeInflight(): Promise<void>;
60
+ private resolveVars;
61
+ private template;
62
+ /** Evaluate a signal (or 'none'/undefined → pass), accumulating the keys it reads. */
63
+ private evalSignal;
64
+ /** Mark every step that (transitively) depends on a failed step as skipped (partial-fail policy). */
65
+ private skipSubtree;
66
+ /** Dispatch a step's agent task(s); tag with the workflow-run scope for onTaskTerminal. */
67
+ private dispatchStep;
68
+ /**
69
+ * On a RED step: GUARANTEE the owner sees it (push — node-owned, never silent), then best-effort
70
+ * dispatch the crew `workflow-inspector` agent for diagnosis/repair. The push is the contract; the
71
+ * inspector is enrichment, so a missing/offline inspector never hides the failure.
72
+ */
73
+ private onStepFail;
74
+ /**
75
+ * Queue a task to the owner's `workflow-inspector` agent (crew-owned) with full run context: the
76
+ * run record (defSnapshot + every step's state + expected-vs-observed) is at a known memory key.
77
+ * Tagged `workflow-inspect` (NOT `workflow-run`) so completing it never advances the run. Returns
78
+ * the task id, or null when no inspector agent is installed.
79
+ */
80
+ private dispatchInspector;
81
+ private persist;
82
+ }
83
+ export {};
84
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../../src/services/workflow/engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,545 @@
1
+ /**
2
+ * @file engine.ts
3
+ * @description The Agent Workflows engine — the deterministic, node-owned run loop. A workflow run
4
+ * is an ASYNC persisted state machine (NOT a synchronous loop): the node dispatches an agent_task
5
+ * and parks; the run advances when that task reaches a terminal state (the agent-tasks route calls
6
+ * onTaskTerminal) or when a step times out (the watchdog sweep). State + recovery live in the run
7
+ * record in owner memory, so a restart resumes in-flight runs. Per-step input/output signals are
8
+ * evaluated deterministically (signal-eval.ts); the `llm` leaf uses the node OpenRouter only when
9
+ * the workflow's owner has approved it AND a key is configured. Run-fail policy = PARTIAL: a RED
10
+ * step fails its dependent subtree, independent branches finish, the run ends `partial`. See
11
+ * docs/plans/2026-06-13-agent-workflows-node-plan.md §4.
12
+ * @structure
13
+ * - WorkflowEngine — startRun / onTaskTerminal / sweep / resumeInflight / start / stop
14
+ * - setActiveWorkflowEngine / getActiveWorkflowEngine — process-wide handle (so the task route +
15
+ * scheduler reach the live engine without threading it through every signature)
16
+ * - computeReadySteps / runOutcome — pure decision helpers (exported for tests)
17
+ * @usage
18
+ * const engine = new WorkflowEngine(config, storage);
19
+ * setActiveWorkflowEngine(engine); engine.setWebhookDispatcher(d); await engine.start();
20
+ * @version-history
21
+ * v1.0.0 — 2026-06-13 — Phase 4: state machine (signals-only + full-live), dispatch, task-terminal
22
+ * advance, notBefore-backoff retry + timeout via the watchdog sweep, restart recovery.
23
+ */
24
+ import { randomUUID } from 'node:crypto';
25
+ import { buildGAII } from '../../utils/gaii.js';
26
+ import { emitChange } from '../event-bus.js';
27
+ import { logger } from '../../utils/logger.js';
28
+ import { evaluateSignal, globToRegExp } from './signal-eval.js';
29
+ import { buildEvalCtx } from './eval-context.js';
30
+ import { getWorkflow, validateWorkflow, runKey } from './store.js';
31
+ import { readEventTriggers, readActiveRuns, reconcileActiveRun } from './lifecycle.js';
32
+ let _active = null;
33
+ export function setActiveWorkflowEngine(e) { _active = e; }
34
+ export function getActiveWorkflowEngine() { return _active; }
35
+ /** Pick a display string from a localized value (prefers en_US, else fi_FI, else first). */
36
+ function loc(s) {
37
+ if (!s)
38
+ return '';
39
+ if (typeof s === 'string')
40
+ return s;
41
+ return s.en_US ?? s.fi_FI ?? Object.values(s)[0] ?? '';
42
+ }
43
+ const TERMINAL_STEP_STATES = new Set(['green', 'input-red', 'output-red', 'timed-out', 'skipped']);
44
+ /**
45
+ * The steps that may start now: pending, all `after` deps green, and past any retry backoff.
46
+ * Pure — operates on the def + the current run-step map.
47
+ */
48
+ export function computeReadySteps(def, steps, now) {
49
+ return def.steps.filter(s => {
50
+ const rs = steps[s.id];
51
+ if (!rs || rs.state !== 'pending')
52
+ return false;
53
+ if (rs.notBefore && rs.notBefore > now)
54
+ return false;
55
+ return (s.after ?? []).every(dep => steps[dep]?.state === 'green');
56
+ });
57
+ }
58
+ /** The terminal run status given the step states: done if all green, else partial; running otherwise. */
59
+ export function runOutcome(steps) {
60
+ const all = Object.values(steps);
61
+ if (all.some(s => !TERMINAL_STEP_STATES.has(s.state)))
62
+ return 'running';
63
+ return all.every(s => s.state === 'green') ? 'done' : 'partial';
64
+ }
65
+ export class WorkflowEngine {
66
+ config;
67
+ storage;
68
+ webhookDispatcher;
69
+ pushService;
70
+ /** Per-run serialization: advancing the same run from two task completions is a RMW race. */
71
+ locks = new Map();
72
+ sweepTimer;
73
+ constructor(config, storage) {
74
+ this.config = config;
75
+ this.storage = storage;
76
+ }
77
+ setWebhookDispatcher(d) { this.webhookDispatcher = d; }
78
+ setPushService(p) { this.pushService = p; }
79
+ /** Start the watchdog (timeouts + due retries). Idempotent; the timer is unref'd. */
80
+ async start() {
81
+ await this.resumeInflight();
82
+ if (!this.sweepTimer) {
83
+ this.sweepTimer = setInterval(() => { this.sweep().catch(err => logger.error('workflow sweep failed', { error: String(err) })); }, 60_000);
84
+ this.sweepTimer.unref?.();
85
+ }
86
+ }
87
+ stop() { if (this.sweepTimer) {
88
+ clearInterval(this.sweepTimer);
89
+ this.sweepTimer = undefined;
90
+ } }
91
+ /** Run a single run's mutation under its lock (serializes concurrent advances). */
92
+ async withLock(runId, fn) {
93
+ const prev = this.locks.get(runId) ?? Promise.resolve();
94
+ let release;
95
+ const gate = new Promise(r => { release = r; });
96
+ this.locks.set(runId, prev.then(() => gate));
97
+ await prev;
98
+ try {
99
+ return await fn();
100
+ }
101
+ finally {
102
+ release();
103
+ if (this.locks.get(runId) === prev.then(() => gate))
104
+ this.locks.delete(runId);
105
+ }
106
+ }
107
+ // ── start a run ──────────────────────────────────────────────────────────────
108
+ async startRun(ownerGhii, ownerName, workflowId, opts) {
109
+ const def = await getWorkflow(this.storage, ownerGhii, workflowId);
110
+ if (!def)
111
+ return { error: [`workflow "${workflowId}" not found`] };
112
+ const v = await validateWorkflow(this.storage, this.config, ownerName, def);
113
+ if (!v.ok || !v.resolved)
114
+ return { error: v.errors };
115
+ // Overlap guard: a full run that dispatches agents must not run twice at once (the steps share
116
+ // templated keys). If one is already in flight for this workflow, return it instead of starting a
117
+ // second. signals-only is read-only, so it always runs fresh.
118
+ if (opts.mode === 'full-live') {
119
+ const active = await readActiveRuns(this.storage, this.config.nodeId);
120
+ const inFlight = active.find(a => a.workflowId === workflowId && a.ownerGhii === ownerGhii);
121
+ if (inFlight) {
122
+ logger.info(`workflow "${workflowId}": a run is already in flight (${inFlight.runId}); skipping this start`);
123
+ return { runId: inFlight.runId };
124
+ }
125
+ }
126
+ const runId = randomUUID();
127
+ const vars = this.resolveVars(def, opts.vars);
128
+ const now = new Date().toISOString();
129
+ const steps = {};
130
+ for (const s of def.steps)
131
+ steps[s.id] = { state: 'pending', attempt: 0, reads: [], writes: [] };
132
+ const run = {
133
+ runId, workflowId, defSnapshot: def, resolved: v.resolved, vars,
134
+ mode: opts.mode, keyPrefix: opts.mode === 'full-sandbox' ? `wf-test.${runId}.` : '',
135
+ status: 'running', steps, startedAt: now,
136
+ };
137
+ if (opts.mode === 'signals-only') {
138
+ await this.runSignalsOnly(ownerGhii, run);
139
+ }
140
+ else {
141
+ await this.persist(ownerGhii, run);
142
+ await this.tick(ownerGhii, run);
143
+ }
144
+ return { runId };
145
+ }
146
+ /** The pinned per-step resolved signals (from start time) as a lookup. No mid-run re-resolution. */
147
+ resolvedMap(run) {
148
+ return new Map((run.resolved ?? []).map(r => [r.stepId, r]));
149
+ }
150
+ /** signals-only: evaluate every step's input + output against existing memory; no dispatch. */
151
+ async runSignalsOnly(ownerGhii, run) {
152
+ const resolved = this.resolvedMap(run);
153
+ const ctx = buildEvalCtx(this.storage, this.config, ownerGhii, run);
154
+ for (const step of run.defSnapshot.steps) {
155
+ const r = resolved.get(step.id);
156
+ const rs = run.steps[step.id];
157
+ const reads = new Set();
158
+ // input
159
+ const input = await this.evalSignal(r?.required_to_function, ctx, reads);
160
+ if (!input.ok) {
161
+ rs.state = 'input-red';
162
+ rs.inputObserved = input.observed;
163
+ rs.reads = [...reads];
164
+ continue;
165
+ }
166
+ // output
167
+ const output = await this.evalSignal(r?.success_signal, ctx, reads);
168
+ rs.outputObserved = output.observed;
169
+ rs.reads = [...reads];
170
+ rs.state = output.ok ? 'green' : 'output-red';
171
+ }
172
+ run.status = runOutcome(run.steps);
173
+ run.endedAt = new Date().toISOString();
174
+ await this.persist(ownerGhii, run);
175
+ emitChange('workflows');
176
+ }
177
+ // ── the advance loop (full-live) ──────────────────────────────────────────────
178
+ async tick(ownerGhii, run) {
179
+ const resolved = this.resolvedMap(run);
180
+ const now = new Date().toISOString();
181
+ const ctx = buildEvalCtx(this.storage, this.config, ownerGhii, run);
182
+ let dispatchedAny = false;
183
+ for (const step of computeReadySteps(run.defSnapshot, run.steps, now)) {
184
+ const r = resolved.get(step.id);
185
+ const rs = run.steps[step.id];
186
+ const reads = new Set();
187
+ const input = await this.evalSignal(r?.required_to_function, ctx, reads);
188
+ rs.reads = [...reads];
189
+ if (!input.ok) {
190
+ rs.state = 'input-red';
191
+ rs.inputObserved = input.observed;
192
+ rs.endedAt = now;
193
+ this.skipSubtree(run, step.id);
194
+ await this.onStepFail(ownerGhii, run, step.id, 'input-red');
195
+ continue;
196
+ }
197
+ // dispatch
198
+ const taskIds = await this.dispatchStep(ownerGhii, run, step, r);
199
+ rs.state = 'dispatched';
200
+ rs.taskIds = taskIds;
201
+ rs.startedAt = now;
202
+ rs.notBefore = undefined;
203
+ dispatchedAny = true;
204
+ }
205
+ const outcome = runOutcome(run.steps);
206
+ if (outcome !== 'running') {
207
+ run.status = outcome;
208
+ run.endedAt = new Date().toISOString();
209
+ }
210
+ else {
211
+ run.status = Object.values(run.steps).some(s => s.state === 'dispatched') ? 'waiting-step' : 'running';
212
+ }
213
+ await this.persist(ownerGhii, run);
214
+ if (dispatchedAny || outcome !== 'running')
215
+ emitChange('workflows');
216
+ }
217
+ // ── task terminal → advance ────────────────────────────────────────────────────
218
+ async onTaskTerminal(task, outcome) {
219
+ const scope = task.scope?.find(s => s.name === 'workflow-run');
220
+ if (!scope?.value)
221
+ return;
222
+ const [workflowId, runId] = scope.value.split('/');
223
+ const stepId = scope.description;
224
+ if (!workflowId || !runId || !stepId)
225
+ return;
226
+ const ownerGhii = task.ownerGaii;
227
+ await this.withLock(runId, async () => {
228
+ const rec = await this.storage.getMemory(ownerGhii, runKey(workflowId, runId));
229
+ if (!rec)
230
+ return;
231
+ const run = rec.value;
232
+ const rs = run.steps[stepId];
233
+ if (!rs || rs.state !== 'dispatched')
234
+ return; // already resolved / not awaiting
235
+ // Wait until every task of this step is terminal (a step may fan out to several agents).
236
+ for (const tid of rs.taskIds ?? []) {
237
+ const t = await this.storage.getAgentTask(tid);
238
+ if (t && t.status !== 'done' && t.status !== 'failed')
239
+ return; // still in flight
240
+ }
241
+ // Use the signals PINNED at start time (not current offers) — a mid-run offer edit/delete must
242
+ // not silently turn this output check into a false pass.
243
+ const r = this.resolvedMap(run).get(stepId);
244
+ const ctx = buildEvalCtx(this.storage, this.config, ownerGhii, run);
245
+ const reads = new Set(rs.reads);
246
+ const output = await this.evalSignal(r?.success_signal, ctx, reads);
247
+ const now = new Date().toISOString();
248
+ rs.outputObserved = output.observed;
249
+ rs.reads = [...reads];
250
+ if (r?.deliverableKey)
251
+ rs.writes = [...new Set([...rs.writes, this.template(r.deliverableKey, run.vars)])];
252
+ const stepDef = run.defSnapshot.steps.find(s => s.id === stepId);
253
+ if (output.ok && outcome === 'done') {
254
+ rs.state = 'green';
255
+ rs.endedAt = now;
256
+ }
257
+ else if (stepDef.retry && rs.attempt < stepDef.retry.max) {
258
+ // Deterministic retry with backoff: back to pending, gated by notBefore; the sweep re-ticks.
259
+ rs.attempt += 1;
260
+ rs.state = 'pending';
261
+ rs.taskIds = undefined;
262
+ rs.notBefore = new Date(Date.now() + stepDef.retry.backoff_min * 60_000).toISOString();
263
+ }
264
+ else {
265
+ rs.state = 'output-red';
266
+ rs.endedAt = now;
267
+ this.skipSubtree(run, stepId);
268
+ await this.onStepFail(ownerGhii, run, stepId, 'output-red');
269
+ }
270
+ await this.tick(ownerGhii, run);
271
+ });
272
+ }
273
+ // ── watchdog: timeouts + due retries ─────────────────────────────────────────────
274
+ async sweep() {
275
+ for (const e of await readActiveRuns(this.storage, this.config.nodeId)) {
276
+ const rec = await this.storage.getMemory(e.ownerGhii, runKey(e.workflowId, e.runId));
277
+ if (!rec) {
278
+ continue;
279
+ }
280
+ await this.sweepRun(e.ownerGhii, rec.value);
281
+ }
282
+ }
283
+ async sweepRun(ownerGhii, run) {
284
+ await this.withLock(run.runId, async () => {
285
+ const fresh = await this.storage.getMemory(ownerGhii, runKey(run.workflowId, run.runId));
286
+ if (!fresh)
287
+ return;
288
+ const r = fresh.value;
289
+ if (r.status !== 'running' && r.status !== 'waiting-step')
290
+ return;
291
+ const now = Date.now();
292
+ let changed = false;
293
+ // timeouts on dispatched steps
294
+ for (const step of r.defSnapshot.steps) {
295
+ const rs = r.steps[step.id];
296
+ if (rs.state === 'dispatched' && rs.startedAt) {
297
+ const deadline = new Date(rs.startedAt).getTime() + (step.timeout_min ?? 60) * 60_000;
298
+ if (now >= deadline) {
299
+ if (step.retry && rs.attempt < step.retry.max) {
300
+ rs.attempt += 1;
301
+ rs.state = 'pending';
302
+ rs.taskIds = undefined;
303
+ rs.notBefore = new Date(now + step.retry.backoff_min * 60_000).toISOString();
304
+ }
305
+ else {
306
+ rs.state = 'timed-out';
307
+ rs.endedAt = new Date().toISOString();
308
+ this.skipSubtree(r, step.id);
309
+ await this.onStepFail(ownerGhii, r, step.id, 'timed-out');
310
+ }
311
+ changed = true;
312
+ }
313
+ }
314
+ }
315
+ // re-tick (fires any now-due pending retries), using the run's pinned resolved signals.
316
+ if (changed)
317
+ await this.persist(ownerGhii, r);
318
+ await this.tick(ownerGhii, r);
319
+ });
320
+ }
321
+ // ── event triggers (Phase 8) ──────────────────────────────────────────────────
322
+ // The descriptor's `trigger.kind:'event'` is registered in a system-namespace index
323
+ // (lifecycle.ts). These hooks are called from the write/order sites; a match starts a run.
324
+ // Loop guard: skip if the workflow already has an in-flight run (so a workflow that produces a
325
+ // key it also listens on doesn't re-trigger itself).
326
+ async onMemoryWrite(ownerGhii, key) {
327
+ await this.fireEventTriggers('memory.write', ownerGhii, t => {
328
+ const pat = t.match.key;
329
+ return !!pat && globToRegExp(pat).test(key);
330
+ });
331
+ }
332
+ async onOfferOrdered(ownerGhii, offerId) {
333
+ await this.fireEventTriggers('offer.ordered', ownerGhii, t => {
334
+ const pat = t.match.offer;
335
+ return !!pat && globToRegExp(pat).test(offerId);
336
+ });
337
+ }
338
+ async fireEventTriggers(on, ownerGhii, matches) {
339
+ let triggers;
340
+ try {
341
+ triggers = await readEventTriggers(this.storage, this.config.nodeId);
342
+ }
343
+ catch (err) {
344
+ logger.error('readEventTriggers failed', { on, error: String(err) });
345
+ return;
346
+ }
347
+ const hits = triggers.filter(t => t.on === on && t.ownerGhii === ownerGhii && matches(t));
348
+ if (hits.length === 0)
349
+ return;
350
+ const active = await readActiveRuns(this.storage, this.config.nodeId);
351
+ for (const t of hits) {
352
+ if (active.some(a => a.workflowId === t.workflowId && a.ownerGhii === t.ownerGhii))
353
+ continue; // loop/overlap guard (owner-scoped)
354
+ this.startRun(ownerGhii, ownerGhii.split('@')[0], t.workflowId, { mode: 'full-live' })
355
+ .catch(err => logger.error('event-triggered workflow run failed', { workflowId: t.workflowId, error: String(err) }));
356
+ }
357
+ }
358
+ /**
359
+ * On boot, re-sync in-flight runs: a step's task may have reached done/failed during the restart
360
+ * gap (after the HTTP response, before the fire-and-forget onTaskTerminal ran), leaving the step
361
+ * stuck `dispatched`. Re-check each dispatched step's tasks and advance any that already finished —
362
+ * otherwise the watchdog would wrongly TIME OUT a step whose task actually succeeded. Remaining
363
+ * in-flight runs are then carried by live task events + the watchdog.
364
+ */
365
+ async resumeInflight() {
366
+ const active = await readActiveRuns(this.storage, this.config.nodeId);
367
+ if (!active.length)
368
+ return;
369
+ logger.info(`WorkflowEngine: re-syncing ${active.length} in-flight run(s) after restart`);
370
+ for (const a of active) {
371
+ try {
372
+ const rec = await this.storage.getMemory(a.ownerGhii, runKey(a.workflowId, a.runId));
373
+ if (!rec)
374
+ continue;
375
+ const run = rec.value;
376
+ for (const [, rs] of Object.entries(run.steps)) {
377
+ if (rs.state !== 'dispatched')
378
+ continue;
379
+ for (const tid of rs.taskIds ?? []) {
380
+ const task = await this.storage.getAgentTask(tid);
381
+ if (task && (task.status === 'done' || task.status === 'failed')) {
382
+ await this.onTaskTerminal(task, task.status); // advances the run (re-checks all step tasks)
383
+ break;
384
+ }
385
+ }
386
+ }
387
+ }
388
+ catch (err) {
389
+ logger.error('resumeInflight re-sync failed', { runId: a.runId, error: String(err) });
390
+ }
391
+ }
392
+ }
393
+ // ── helpers ──────────────────────────────────────────────────────────────────────
394
+ resolveVars(def, overrides) {
395
+ const today = new Date().toISOString().slice(0, 10);
396
+ const out = {};
397
+ for (const v of def.vars) {
398
+ const override = overrides?.[v.name];
399
+ const def0 = v.default === '<run-date>' ? today : v.default;
400
+ out[v.name] = override ?? def0 ?? '';
401
+ }
402
+ return out;
403
+ }
404
+ template(tmpl, vars) {
405
+ return tmpl.replace(/\{([a-zA-Z0-9_]+)\}/g, (_m, n) => vars[n] ?? `{${n}}`);
406
+ }
407
+ /** Evaluate a signal (or 'none'/undefined → pass), accumulating the keys it reads. */
408
+ async evalSignal(signal, ctx, reads) {
409
+ if (!signal || signal === 'none')
410
+ return { ok: true, observed: { skipped: 'none' } };
411
+ // wrap ctx.read/listGlob to record reads
412
+ const tracking = {
413
+ ...ctx,
414
+ read: async (k) => { reads.add(k); return ctx.read(k); },
415
+ listGlob: async (g) => { reads.add(g); return ctx.listGlob(g); },
416
+ };
417
+ return evaluateSignal(signal, tracking);
418
+ }
419
+ /** Mark every step that (transitively) depends on a failed step as skipped (partial-fail policy). */
420
+ skipSubtree(run, failedId) {
421
+ const dependents = (id) => run.defSnapshot.steps.filter(s => (s.after ?? []).includes(id)).map(s => s.id);
422
+ const queue = [...dependents(failedId)];
423
+ while (queue.length) {
424
+ const id = queue.shift();
425
+ const rs = run.steps[id];
426
+ if (rs && rs.state === 'pending') {
427
+ rs.state = 'skipped';
428
+ queue.push(...dependents(id));
429
+ }
430
+ }
431
+ }
432
+ /** Dispatch a step's agent task(s); tag with the workflow-run scope for onTaskTerminal. */
433
+ async dispatchStep(ownerGhii, run, step, resolved) {
434
+ const ownerName = ownerGhii.split('@')[0];
435
+ const agents = Array.isArray(step.agent) ? step.agent : [step.agent];
436
+ const now = new Date().toISOString();
437
+ const ids = [];
438
+ for (const agentName of agents) {
439
+ const agentGaii = buildGAII(agentName, ownerName, this.config.nodeId);
440
+ const scope = [
441
+ { name: 'workflow-run', value: `${run.workflowId}/${run.runId}`, type: 'text', description: step.id },
442
+ { name: 'offer', value: step.offer, type: 'text', description: loc(step.description) },
443
+ ];
444
+ // Sandbox run: tell the agent the key prefix to write under (signals read under it too), so a
445
+ // test run doesn't clobber production keys. A cooperating agent honors it; the node can't force it.
446
+ if (run.keyPrefix)
447
+ scope.push({ name: 'wf-key-prefix', value: run.keyPrefix, type: 'text', description: 'prefix all deliverable keys with this' });
448
+ const record = {
449
+ id: randomUUID(), agentGaii, ownerGaii: ownerGhii,
450
+ title: loc(step.description) || `${run.workflowId} · ${step.id}`,
451
+ description: loc(run.defSnapshot.description),
452
+ scope, rules: [], verification: { userExpects: '', technicalChecks: [] },
453
+ todos: [], status: 'active', createdAt: now, updatedAt: now, lastEventAt: now,
454
+ };
455
+ await this.storage.createAgentTask(record);
456
+ await this.storage.appendTaskEvent({ id: randomUUID(), taskId: record.id, type: 'started', message: `Dispatched by workflow "${run.workflowId}" step "${step.id}"`, timestamp: now });
457
+ this.webhookDispatcher?.dispatchWebhookEvent(agentGaii, 'task.approved', {
458
+ task_id: record.id, title: record.title, description: record.description ?? '',
459
+ has_todos: false, todo_count: 0, scope_summary: scope.map(s => `${s.type}:${s.value}`),
460
+ created_at: now, auto_activated: true, workflow_id: run.workflowId,
461
+ });
462
+ ids.push(record.id);
463
+ }
464
+ void resolved;
465
+ return ids;
466
+ }
467
+ /**
468
+ * On a RED step: GUARANTEE the owner sees it (push — node-owned, never silent), then best-effort
469
+ * dispatch the crew `workflow-inspector` agent for diagnosis/repair. The push is the contract; the
470
+ * inspector is enrichment, so a missing/offline inspector never hides the failure.
471
+ */
472
+ async onStepFail(ownerGhii, run, stepId, reason) {
473
+ const ownerName = ownerGhii.split('@')[0];
474
+ logger.warn(`workflow ${run.workflowId} run ${run.runId}: step "${stepId}" ${reason}`);
475
+ // 1. Guaranteed owner alert (deterministic, node-owned).
476
+ if (this.pushService?.enabled) {
477
+ this.pushService.sendNotification(ownerName, {
478
+ title: 'Workflow step failed',
479
+ body: `${loc(run.defSnapshot.title) || run.workflowId}: step "${stepId}" → ${reason}`,
480
+ url: '/v1/profile?tab=workflows',
481
+ tag: `workflow:${run.workflowId}`,
482
+ }).catch(() => { });
483
+ }
484
+ // 2. Best-effort inspector dispatch (crew-owned; absent ⇒ skip silently, the push already fired).
485
+ const taskId = await this.dispatchInspector(ownerGhii, ownerName, run, stepId, reason);
486
+ if (taskId) {
487
+ run.inspections = [...(run.inspections ?? []), { stepId, taskId, reason, at: new Date().toISOString() }];
488
+ }
489
+ }
490
+ /**
491
+ * Queue a task to the owner's `workflow-inspector` agent (crew-owned) with full run context: the
492
+ * run record (defSnapshot + every step's state + expected-vs-observed) is at a known memory key.
493
+ * Tagged `workflow-inspect` (NOT `workflow-run`) so completing it never advances the run. Returns
494
+ * the task id, or null when no inspector agent is installed.
495
+ */
496
+ async dispatchInspector(ownerGhii, ownerName, run, stepId, reason) {
497
+ const inspectorGaii = buildGAII('workflow-inspector', ownerName, this.config.nodeId);
498
+ const inspector = await this.storage.getAgent(inspectorGaii);
499
+ if (!inspector)
500
+ return null;
501
+ const rk = runKey(run.workflowId, run.runId);
502
+ const failing = run.steps[stepId];
503
+ const failingAgents = (run.defSnapshot.steps.find(s => s.id === stepId)?.agent) ?? '';
504
+ const now = new Date().toISOString();
505
+ const scope = [
506
+ { name: 'workflow-inspect', value: `${run.workflowId}/${run.runId}`, type: 'text', description: stepId },
507
+ ];
508
+ const record = {
509
+ id: randomUUID(), agentGaii: inspectorGaii, ownerGaii: ownerGhii,
510
+ title: `Inspect workflow "${run.workflowId}" — step "${stepId}" ${reason}`,
511
+ description: [
512
+ `A workflow step failed (${reason}).`,
513
+ `Read the full run record at owner memory key "${rk}" — it carries defSnapshot, every step's`,
514
+ `state (green / input-red / output-red / timed-out / skipped), and per-leaf expected-vs-observed.`,
515
+ `Failing step: "${stepId}" (agent: ${Array.isArray(failingAgents) ? failingAgents.join(', ') : failingAgents}).`,
516
+ `Observed: ${JSON.stringify(failing?.outputObserved ?? failing?.inputObserved ?? {}).slice(0, 1000)}.`,
517
+ `Diagnose, auto-run any safe deterministic repairs, and report recommendations.`,
518
+ ].join(' '),
519
+ scope, rules: [], verification: { userExpects: '', technicalChecks: [] },
520
+ resources: { memoryKeys: [rk] },
521
+ todos: [], status: 'active', createdAt: now, updatedAt: now, lastEventAt: now,
522
+ };
523
+ await this.storage.createAgentTask(record);
524
+ await this.storage.appendTaskEvent({ id: randomUUID(), taskId: record.id, type: 'started', message: `Workflow inspection requested for "${run.workflowId}" step "${stepId}" (${reason})`, timestamp: now });
525
+ this.webhookDispatcher?.dispatchWebhookEvent(inspectorGaii, 'task.approved', {
526
+ task_id: record.id, title: record.title, description: record.description ?? '',
527
+ has_todos: false, todo_count: 0, scope_summary: scope.map(s => `${s.name}:${s.value}`),
528
+ created_at: now, auto_activated: true, workflow_id: run.workflowId,
529
+ });
530
+ return record.id;
531
+ }
532
+ async persist(ownerGhii, run) {
533
+ const key = runKey(run.workflowId, run.runId);
534
+ const existing = await this.storage.getMemory(ownerGhii, key);
535
+ const now = new Date().toISOString();
536
+ await this.storage.setMemory({
537
+ key, ownerGaii: ownerGhii, value: run,
538
+ visibility: 'private', tags: ['workflow-run'], ttlHours: null,
539
+ version: existing ? existing.version + 1 : 1,
540
+ createdAt: existing?.createdAt ?? now, updatedAt: now,
541
+ });
542
+ await reconcileActiveRun(this.storage, this.config.nodeId, ownerGhii, run);
543
+ }
544
+ }
545
+ //# sourceMappingURL=engine.js.map