@hasna/loops 0.4.27 → 0.4.28

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.
package/dist/api/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@hasna/loops",
6
- version: "0.4.27",
6
+ version: "0.4.28",
7
7
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
8
8
  type: "module",
9
9
  main: "dist/index.js",
@@ -113,6 +113,7 @@ var package_default = {
113
113
  bun: ">=1.0.0"
114
114
  },
115
115
  dependencies: {
116
+ "@hasna/contracts": "^0.5.1",
116
117
  "@hasna/events": "^0.1.9",
117
118
  "@modelcontextprotocol/sdk": "^1.29.0",
118
119
  "@openrouter/ai-sdk-provider": "2.9.1",
@@ -122,8 +123,7 @@ var package_default = {
122
123
  zod: "4.4.3"
123
124
  },
124
125
  optionalDependencies: {
125
- "@hasna/machines": "0.0.49",
126
- "@hasna/contracts": "^0.4.2"
126
+ "@hasna/machines": "0.0.49"
127
127
  },
128
128
  devDependencies: {
129
129
  "@types/bun": "latest",
@@ -935,6 +935,14 @@ var loops_default = {
935
935
  type: "integer"
936
936
  }
937
937
  },
938
+ {
939
+ name: "offset",
940
+ in: "query",
941
+ required: false,
942
+ schema: {
943
+ type: "integer"
944
+ }
945
+ },
938
946
  {
939
947
  name: "includeArchived",
940
948
  in: "query",
@@ -1210,6 +1218,22 @@ var loops_default = {
1210
1218
  schema: {
1211
1219
  type: "integer"
1212
1220
  }
1221
+ },
1222
+ {
1223
+ name: "offset",
1224
+ in: "query",
1225
+ required: false,
1226
+ schema: {
1227
+ type: "integer"
1228
+ }
1229
+ },
1230
+ {
1231
+ name: "showOutput",
1232
+ in: "query",
1233
+ required: false,
1234
+ schema: {
1235
+ type: "boolean"
1236
+ }
1213
1237
  }
1214
1238
  ],
1215
1239
  responses: {
@@ -1266,6 +1290,14 @@ var loops_default = {
1266
1290
  schema: {
1267
1291
  type: "string"
1268
1292
  }
1293
+ },
1294
+ {
1295
+ name: "showOutput",
1296
+ in: "query",
1297
+ required: false,
1298
+ schema: {
1299
+ type: "boolean"
1300
+ }
1269
1301
  }
1270
1302
  ],
1271
1303
  responses: {
@@ -1286,7 +1318,7 @@ var loops_default = {
1286
1318
  post: {
1287
1319
  operationId: "importRows",
1288
1320
  summary: "Bulk id-preserving import (self-hosted backfill)",
1289
- description: "Upsert full workflow/loop/run rows by id (idempotent; ON CONFLICT(id) DO UPDATE). Preserves id, status, archived state, and timestamps. Applied FK-safe: workflows, then loops, then runs. Volatile running runs are skipped and reported.",
1321
+ description: "Upsert full workflow/loop/run rows by id (idempotent; ON CONFLICT(id) DO UPDATE). Preserves ids, definitions, and timestamps while applying safe defaults: imported workflows are archived, imported loops are paused, and nextRunAt/retryScheduledFor are cleared by default so backfills never schedule immediately. Set preserveWorkflowActivation=true or preserveLoopScheduling=true only for intentional activation. Applied FK-safe: workflows, then loops, then runs. Volatile running runs are skipped and reported.",
1290
1322
  requestBody: {
1291
1323
  required: true,
1292
1324
  content: {
@@ -1311,6 +1343,260 @@ var loops_default = {
1311
1343
  }
1312
1344
  }
1313
1345
  },
1346
+ "/v1/workflows": {
1347
+ get: {
1348
+ operationId: "listWorkflows",
1349
+ summary: "List workflow specs",
1350
+ parameters: [
1351
+ {
1352
+ name: "status",
1353
+ in: "query",
1354
+ required: false,
1355
+ schema: {
1356
+ type: "string",
1357
+ enum: [
1358
+ "active",
1359
+ "archived"
1360
+ ]
1361
+ }
1362
+ },
1363
+ {
1364
+ name: "limit",
1365
+ in: "query",
1366
+ required: false,
1367
+ schema: {
1368
+ type: "integer"
1369
+ }
1370
+ },
1371
+ {
1372
+ name: "offset",
1373
+ in: "query",
1374
+ required: false,
1375
+ schema: {
1376
+ type: "integer"
1377
+ }
1378
+ }
1379
+ ],
1380
+ responses: {
1381
+ "200": {
1382
+ description: "workflow specs",
1383
+ content: {
1384
+ "application/json": {
1385
+ schema: {
1386
+ $ref: "#/components/schemas/WorkflowListResponse"
1387
+ }
1388
+ }
1389
+ }
1390
+ }
1391
+ }
1392
+ },
1393
+ post: {
1394
+ operationId: "createWorkflow",
1395
+ summary: "Create a workflow spec",
1396
+ requestBody: {
1397
+ required: true,
1398
+ content: {
1399
+ "application/json": {
1400
+ schema: {
1401
+ $ref: "#/components/schemas/CreateWorkflowInput"
1402
+ }
1403
+ }
1404
+ }
1405
+ },
1406
+ responses: {
1407
+ "201": {
1408
+ description: "workflow",
1409
+ content: {
1410
+ "application/json": {
1411
+ schema: {
1412
+ $ref: "#/components/schemas/WorkflowResponse"
1413
+ }
1414
+ }
1415
+ }
1416
+ }
1417
+ }
1418
+ }
1419
+ },
1420
+ "/v1/workflows/count": {
1421
+ get: {
1422
+ operationId: "countWorkflows",
1423
+ summary: "Count workflow specs",
1424
+ parameters: [
1425
+ {
1426
+ name: "status",
1427
+ in: "query",
1428
+ required: false,
1429
+ schema: {
1430
+ type: "string"
1431
+ }
1432
+ }
1433
+ ],
1434
+ responses: {
1435
+ "200": {
1436
+ description: "count",
1437
+ content: {
1438
+ "application/json": {
1439
+ schema: {
1440
+ $ref: "#/components/schemas/CountResponse"
1441
+ }
1442
+ }
1443
+ }
1444
+ }
1445
+ }
1446
+ }
1447
+ },
1448
+ "/v1/workflows/{id}": {
1449
+ get: {
1450
+ operationId: "getWorkflow",
1451
+ summary: "Get a workflow spec by id",
1452
+ parameters: [
1453
+ {
1454
+ name: "id",
1455
+ in: "path",
1456
+ required: true,
1457
+ schema: {
1458
+ type: "string"
1459
+ }
1460
+ }
1461
+ ],
1462
+ responses: {
1463
+ "200": {
1464
+ description: "workflow",
1465
+ content: {
1466
+ "application/json": {
1467
+ schema: {
1468
+ $ref: "#/components/schemas/WorkflowResponse"
1469
+ }
1470
+ }
1471
+ }
1472
+ }
1473
+ }
1474
+ }
1475
+ },
1476
+ "/v1/invocations": {
1477
+ get: {
1478
+ operationId: "listWorkflowInvocations",
1479
+ summary: "List workflow route invocations",
1480
+ parameters: [
1481
+ {
1482
+ name: "limit",
1483
+ in: "query",
1484
+ required: false,
1485
+ schema: {
1486
+ type: "integer"
1487
+ }
1488
+ }
1489
+ ],
1490
+ responses: {
1491
+ "200": {
1492
+ description: "workflow invocations",
1493
+ content: {
1494
+ "application/json": {
1495
+ schema: {
1496
+ $ref: "#/components/schemas/WorkflowInvocationListResponse"
1497
+ }
1498
+ }
1499
+ }
1500
+ }
1501
+ }
1502
+ },
1503
+ post: {
1504
+ operationId: "createWorkflowInvocation",
1505
+ summary: "Create an id-preserving workflow route invocation",
1506
+ requestBody: {
1507
+ required: true,
1508
+ content: {
1509
+ "application/json": {
1510
+ schema: {
1511
+ $ref: "#/components/schemas/CreateWorkflowInvocationInput"
1512
+ }
1513
+ }
1514
+ }
1515
+ },
1516
+ responses: {
1517
+ "201": {
1518
+ description: "workflow invocation",
1519
+ content: {
1520
+ "application/json": {
1521
+ schema: {
1522
+ $ref: "#/components/schemas/WorkflowInvocationResponse"
1523
+ }
1524
+ }
1525
+ }
1526
+ }
1527
+ }
1528
+ }
1529
+ },
1530
+ "/v1/work-items": {
1531
+ get: {
1532
+ operationId: "listWorkflowWorkItems",
1533
+ summary: "List workflow route work items",
1534
+ parameters: [
1535
+ {
1536
+ name: "status",
1537
+ in: "query",
1538
+ required: false,
1539
+ schema: {
1540
+ type: "string"
1541
+ }
1542
+ },
1543
+ {
1544
+ name: "routeKey",
1545
+ in: "query",
1546
+ required: false,
1547
+ schema: {
1548
+ type: "string"
1549
+ }
1550
+ },
1551
+ {
1552
+ name: "limit",
1553
+ in: "query",
1554
+ required: false,
1555
+ schema: {
1556
+ type: "integer"
1557
+ }
1558
+ }
1559
+ ],
1560
+ responses: {
1561
+ "200": {
1562
+ description: "workflow work items",
1563
+ content: {
1564
+ "application/json": {
1565
+ schema: {
1566
+ $ref: "#/components/schemas/WorkflowWorkItemListResponse"
1567
+ }
1568
+ }
1569
+ }
1570
+ }
1571
+ }
1572
+ },
1573
+ post: {
1574
+ operationId: "upsertWorkflowWorkItem",
1575
+ summary: "Upsert an id-preserving workflow route work item",
1576
+ requestBody: {
1577
+ required: true,
1578
+ content: {
1579
+ "application/json": {
1580
+ schema: {
1581
+ $ref: "#/components/schemas/UpsertWorkflowWorkItemInput"
1582
+ }
1583
+ }
1584
+ }
1585
+ },
1586
+ responses: {
1587
+ "201": {
1588
+ description: "workflow work item",
1589
+ content: {
1590
+ "application/json": {
1591
+ schema: {
1592
+ $ref: "#/components/schemas/WorkflowWorkItemResponse"
1593
+ }
1594
+ }
1595
+ }
1596
+ }
1597
+ }
1598
+ }
1599
+ },
1314
1600
  "/v1/receipts": {
1315
1601
  get: {
1316
1602
  operationId: "listRunReceipts",
@@ -1913,22 +2199,368 @@ var loops_default = {
1913
2199
  "receipts"
1914
2200
  ]
1915
2201
  },
1916
- ImportInput: {
2202
+ Workflow: {
1917
2203
  type: "object",
1918
- description: "Batches of full rows to upsert by id. Any array may be omitted.",
1919
2204
  properties: {
1920
- workflows: {
1921
- type: "array",
1922
- items: {
1923
- type: "object",
1924
- additionalProperties: true
1925
- }
2205
+ id: {
2206
+ type: "string"
1926
2207
  },
1927
- loops: {
1928
- type: "array",
1929
- items: {
1930
- type: "object",
1931
- additionalProperties: true
2208
+ name: {
2209
+ type: "string"
2210
+ },
2211
+ description: {
2212
+ type: "string",
2213
+ nullable: true
2214
+ },
2215
+ version: {
2216
+ type: "integer"
2217
+ },
2218
+ status: {
2219
+ type: "string"
2220
+ },
2221
+ steps: {
2222
+ type: "array",
2223
+ items: {
2224
+ type: "object",
2225
+ additionalProperties: true
2226
+ }
2227
+ },
2228
+ goal: {
2229
+ type: "object",
2230
+ additionalProperties: true
2231
+ },
2232
+ createdAt: {
2233
+ type: "string"
2234
+ },
2235
+ updatedAt: {
2236
+ type: "string"
2237
+ }
2238
+ },
2239
+ required: [
2240
+ "id",
2241
+ "name",
2242
+ "version",
2243
+ "status",
2244
+ "steps"
2245
+ ]
2246
+ },
2247
+ CreateWorkflowInput: {
2248
+ type: "object",
2249
+ properties: {
2250
+ name: {
2251
+ type: "string"
2252
+ },
2253
+ description: {
2254
+ type: "string"
2255
+ },
2256
+ steps: {
2257
+ type: "array",
2258
+ items: {
2259
+ type: "object",
2260
+ additionalProperties: true
2261
+ }
2262
+ },
2263
+ goal: {
2264
+ type: "object",
2265
+ additionalProperties: true
2266
+ }
2267
+ },
2268
+ required: [
2269
+ "name",
2270
+ "steps"
2271
+ ]
2272
+ },
2273
+ WorkflowResponse: {
2274
+ type: "object",
2275
+ properties: {
2276
+ ok: {
2277
+ type: "boolean"
2278
+ },
2279
+ workflow: {
2280
+ $ref: "#/components/schemas/Workflow"
2281
+ }
2282
+ },
2283
+ required: [
2284
+ "ok",
2285
+ "workflow"
2286
+ ]
2287
+ },
2288
+ WorkflowListResponse: {
2289
+ type: "object",
2290
+ properties: {
2291
+ ok: {
2292
+ type: "boolean"
2293
+ },
2294
+ workflows: {
2295
+ type: "array",
2296
+ items: {
2297
+ $ref: "#/components/schemas/Workflow"
2298
+ }
2299
+ }
2300
+ },
2301
+ required: [
2302
+ "ok",
2303
+ "workflows"
2304
+ ]
2305
+ },
2306
+ CreateWorkflowInvocationInput: {
2307
+ type: "object",
2308
+ properties: {
2309
+ id: {
2310
+ type: "string"
2311
+ },
2312
+ workflowId: {
2313
+ type: "string"
2314
+ },
2315
+ templateId: {
2316
+ type: "string"
2317
+ },
2318
+ sourceRef: {
2319
+ type: "object",
2320
+ additionalProperties: true
2321
+ },
2322
+ subjectRef: {
2323
+ type: "object",
2324
+ additionalProperties: true
2325
+ },
2326
+ intent: {
2327
+ type: "string"
2328
+ },
2329
+ scope: {
2330
+ type: "object",
2331
+ additionalProperties: true
2332
+ },
2333
+ outputPolicy: {
2334
+ type: "object",
2335
+ additionalProperties: true
2336
+ }
2337
+ },
2338
+ required: [
2339
+ "sourceRef",
2340
+ "subjectRef",
2341
+ "intent"
2342
+ ]
2343
+ },
2344
+ WorkflowInvocation: {
2345
+ allOf: [
2346
+ {
2347
+ $ref: "#/components/schemas/CreateWorkflowInvocationInput"
2348
+ },
2349
+ {
2350
+ type: "object",
2351
+ properties: {
2352
+ id: {
2353
+ type: "string"
2354
+ },
2355
+ createdAt: {
2356
+ type: "string"
2357
+ },
2358
+ updatedAt: {
2359
+ type: "string"
2360
+ }
2361
+ },
2362
+ required: [
2363
+ "id",
2364
+ "createdAt",
2365
+ "updatedAt"
2366
+ ]
2367
+ }
2368
+ ]
2369
+ },
2370
+ WorkflowInvocationResponse: {
2371
+ type: "object",
2372
+ properties: {
2373
+ ok: {
2374
+ type: "boolean"
2375
+ },
2376
+ invocation: {
2377
+ $ref: "#/components/schemas/WorkflowInvocation"
2378
+ }
2379
+ },
2380
+ required: [
2381
+ "ok",
2382
+ "invocation"
2383
+ ]
2384
+ },
2385
+ WorkflowInvocationListResponse: {
2386
+ type: "object",
2387
+ properties: {
2388
+ ok: {
2389
+ type: "boolean"
2390
+ },
2391
+ invocations: {
2392
+ type: "array",
2393
+ items: {
2394
+ $ref: "#/components/schemas/WorkflowInvocation"
2395
+ }
2396
+ }
2397
+ },
2398
+ required: [
2399
+ "ok",
2400
+ "invocations"
2401
+ ]
2402
+ },
2403
+ WorkflowWorkItem: {
2404
+ type: "object",
2405
+ additionalProperties: true,
2406
+ properties: {
2407
+ id: {
2408
+ type: "string"
2409
+ },
2410
+ routeKey: {
2411
+ type: "string"
2412
+ },
2413
+ idempotencyKey: {
2414
+ type: "string"
2415
+ },
2416
+ invocationId: {
2417
+ type: "string"
2418
+ },
2419
+ sourceType: {
2420
+ type: "string"
2421
+ },
2422
+ sourceRef: {
2423
+ type: "string"
2424
+ },
2425
+ subjectRef: {
2426
+ type: "string"
2427
+ },
2428
+ status: {
2429
+ type: "string"
2430
+ },
2431
+ priority: {
2432
+ type: "integer"
2433
+ },
2434
+ createdAt: {
2435
+ type: "string"
2436
+ },
2437
+ updatedAt: {
2438
+ type: "string"
2439
+ }
2440
+ },
2441
+ required: [
2442
+ "id",
2443
+ "routeKey",
2444
+ "idempotencyKey",
2445
+ "invocationId",
2446
+ "sourceType",
2447
+ "sourceRef",
2448
+ "subjectRef",
2449
+ "status",
2450
+ "priority"
2451
+ ]
2452
+ },
2453
+ UpsertWorkflowWorkItemInput: {
2454
+ type: "object",
2455
+ properties: {
2456
+ id: {
2457
+ type: "string"
2458
+ },
2459
+ routeKey: {
2460
+ type: "string"
2461
+ },
2462
+ idempotencyKey: {
2463
+ type: "string"
2464
+ },
2465
+ invocationId: {
2466
+ type: "string"
2467
+ },
2468
+ sourceType: {
2469
+ type: "string"
2470
+ },
2471
+ sourceRef: {
2472
+ type: "string"
2473
+ },
2474
+ subjectRef: {
2475
+ type: "string"
2476
+ },
2477
+ projectKey: {
2478
+ type: "string"
2479
+ },
2480
+ projectGroup: {
2481
+ type: "string"
2482
+ },
2483
+ machineId: {
2484
+ type: "string"
2485
+ },
2486
+ routeScope: {
2487
+ type: "string"
2488
+ },
2489
+ priority: {
2490
+ type: "integer"
2491
+ },
2492
+ status: {
2493
+ type: "string",
2494
+ enum: [
2495
+ "queued",
2496
+ "deferred"
2497
+ ]
2498
+ },
2499
+ nextAttemptAt: {
2500
+ type: "string"
2501
+ },
2502
+ lastReason: {
2503
+ type: "string"
2504
+ }
2505
+ },
2506
+ required: [
2507
+ "routeKey",
2508
+ "idempotencyKey",
2509
+ "invocationId",
2510
+ "sourceType",
2511
+ "sourceRef",
2512
+ "subjectRef"
2513
+ ]
2514
+ },
2515
+ WorkflowWorkItemResponse: {
2516
+ type: "object",
2517
+ properties: {
2518
+ ok: {
2519
+ type: "boolean"
2520
+ },
2521
+ workItem: {
2522
+ $ref: "#/components/schemas/WorkflowWorkItem"
2523
+ }
2524
+ },
2525
+ required: [
2526
+ "ok",
2527
+ "workItem"
2528
+ ]
2529
+ },
2530
+ WorkflowWorkItemListResponse: {
2531
+ type: "object",
2532
+ properties: {
2533
+ ok: {
2534
+ type: "boolean"
2535
+ },
2536
+ workItems: {
2537
+ type: "array",
2538
+ items: {
2539
+ $ref: "#/components/schemas/WorkflowWorkItem"
2540
+ }
2541
+ }
2542
+ },
2543
+ required: [
2544
+ "ok",
2545
+ "workItems"
2546
+ ]
2547
+ },
2548
+ ImportInput: {
2549
+ type: "object",
2550
+ description: "Batches of full rows to upsert by id. Any array may be omitted.",
2551
+ properties: {
2552
+ workflows: {
2553
+ type: "array",
2554
+ items: {
2555
+ type: "object",
2556
+ additionalProperties: true
2557
+ }
2558
+ },
2559
+ loops: {
2560
+ type: "array",
2561
+ items: {
2562
+ type: "object",
2563
+ additionalProperties: true
1932
2564
  }
1933
2565
  },
1934
2566
  runs: {
@@ -1940,7 +2572,15 @@ var loops_default = {
1940
2572
  },
1941
2573
  replace: {
1942
2574
  type: "boolean",
1943
- description: "Update existing rows whose id matches (default false: existing rows are left unchanged)."
2575
+ description: "Update existing rows whose id matches. Default safe imports may still archive/pause same-id workflows/loops when preserve flags are false."
2576
+ },
2577
+ preserveLoopScheduling: {
2578
+ type: "boolean",
2579
+ description: "Default false. When false, imported loops are written paused with nextRunAt and retryScheduledFor cleared so backfills never schedule immediately."
2580
+ },
2581
+ preserveWorkflowActivation: {
2582
+ type: "boolean",
2583
+ description: "Default false. When false, imported workflows are written archived, including same-id active rows, so backfills cannot activate workflow execution unless explicitly requested."
1944
2584
  }
1945
2585
  }
1946
2586
  },
@@ -2175,6 +2815,21 @@ async function handleV1Request(ctx) {
2175
2815
  return errorResponse(error);
2176
2816
  }
2177
2817
  }
2818
+ function safeImportedWorkflow(workflow, opts) {
2819
+ if (opts.preserveWorkflowActivation)
2820
+ return workflow;
2821
+ return { ...workflow, status: "archived" };
2822
+ }
2823
+ function safeImportedLoop(loop, opts) {
2824
+ if (opts.preserveLoopScheduling)
2825
+ return loop;
2826
+ return {
2827
+ ...loop,
2828
+ status: "paused",
2829
+ nextRunAt: undefined,
2830
+ retryScheduledFor: undefined
2831
+ };
2832
+ }
2178
2833
  async function handleImportRequest(ctx, segments) {
2179
2834
  if (segments.length !== 0 || ctx.request.method !== "POST")
2180
2835
  return fail("not_found", 404);
@@ -2184,14 +2839,20 @@ async function handleImportRequest(ctx, segments) {
2184
2839
  const workflows = Array.isArray(body.workflows) ? body.workflows : [];
2185
2840
  const loops = Array.isArray(body.loops) ? body.loops : [];
2186
2841
  const runs = Array.isArray(body.runs) ? body.runs : [];
2842
+ const preserveWorkflowActivation = body.preserveWorkflowActivation === true;
2843
+ const preserveLoopScheduling = body.preserveLoopScheduling === true;
2187
2844
  const imported = { workflows: 0, loops: 0, runs: 0 };
2188
2845
  let skippedRunning = 0;
2189
2846
  for (const workflow of workflows) {
2190
- await storage.upsertMigrationWorkflow(workflow, { replace });
2847
+ await storage.upsertMigrationWorkflow(safeImportedWorkflow(workflow, { preserveWorkflowActivation }), {
2848
+ replace: replace || !preserveWorkflowActivation
2849
+ });
2191
2850
  imported.workflows += 1;
2192
2851
  }
2193
2852
  for (const loop of loops) {
2194
- await storage.upsertMigrationLoop(loop, { replace });
2853
+ await storage.upsertMigrationLoop(safeImportedLoop(loop, { preserveLoopScheduling }), {
2854
+ replace: replace || !preserveLoopScheduling
2855
+ });
2195
2856
  imported.loops += 1;
2196
2857
  }
2197
2858
  for (const run of runs) {
@@ -2350,6 +3011,10 @@ async function handleWorkItemsRequest(ctx, segments) {
2350
3011
  });
2351
3012
  return ok({ workItems: items.map(publicWorkflowWorkItem) });
2352
3013
  }
3014
+ if (segments.length === 0 && ctx.request.method === "POST") {
3015
+ const body = await readJsonBody(ctx.request, ctx.bodyLimitBytes);
3016
+ return ok({ workItem: publicWorkflowWorkItem(await storage.upsertWorkflowWorkItem(body)) }, { status: 201 });
3017
+ }
2353
3018
  const id = segments[0];
2354
3019
  if (!id)
2355
3020
  return fail("not_found", 404);
@@ -2367,6 +3032,10 @@ async function handleInvocationsRequest(ctx, segments) {
2367
3032
  const invocations = await storage.listWorkflowInvocations({ limit: optionalLimit(ctx.url.searchParams.get("limit")) });
2368
3033
  return ok({ invocations: invocations.map(publicWorkflowInvocation) });
2369
3034
  }
3035
+ if (segments.length === 0 && ctx.request.method === "POST") {
3036
+ const body = await readJsonBody(ctx.request, ctx.bodyLimitBytes);
3037
+ return ok({ invocation: publicWorkflowInvocation(await storage.createWorkflowInvocation(body)) }, { status: 201 });
3038
+ }
2370
3039
  const id = segments[0];
2371
3040
  if (!id)
2372
3041
  return fail("not_found", 404);