@beignet/cli 0.0.50 → 0.0.51

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/src/index.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  type CommandContext,
9
9
  type FlagParametersForType,
10
10
  proposeCompletions,
11
+ type RouteMap,
11
12
  run,
12
13
  type StricliDynamicCommandContext,
13
14
  type StricliProcess,
@@ -180,6 +181,7 @@ type OutboxDrainFlags = {
180
181
  json?: boolean;
181
182
  module?: string;
182
183
  batchSize?: number;
184
+ concurrency?: number;
183
185
  cwd?: string;
184
186
  };
185
187
 
@@ -486,7 +488,13 @@ const outboxDrainFlagParameters = {
486
488
  kind: "parsed",
487
489
  parse: parsePositiveInteger,
488
490
  optional: true,
489
- brief: "Maximum messages to claim in one drain pass.",
491
+ brief: "Maximum eligible messages to handle in one drain pass.",
492
+ },
493
+ concurrency: {
494
+ kind: "parsed",
495
+ parse: parsePositiveInteger,
496
+ optional: true,
497
+ brief: "Maximum concurrent deliveries; values above one are unordered.",
490
498
  },
491
499
  cwd: cwdFlag,
492
500
  } satisfies FlagParametersForType<OutboxDrainFlags, CliContext>;
@@ -1597,6 +1605,7 @@ const outboxDrainCommand = buildCommand<OutboxDrainFlags, [], CliContext>({
1597
1605
  cwd: flags.cwd,
1598
1606
  modulePath: flags.module,
1599
1607
  batchSize: flags.batchSize,
1608
+ concurrency: flags.concurrency,
1600
1609
  });
1601
1610
 
1602
1611
  writeOutput(
@@ -1605,6 +1614,9 @@ const outboxDrainCommand = buildCommand<OutboxDrainFlags, [], CliContext>({
1605
1614
  ? JSON.stringify(result, null, 2)
1606
1615
  : outboxDrainNextSteps(result),
1607
1616
  );
1617
+ if (result.result.settlementFailed > 0 || result.result.leaseLost > 0) {
1618
+ this.process.exitCode = 1;
1619
+ }
1608
1620
  };
1609
1621
  },
1610
1622
  });
@@ -2448,6 +2460,27 @@ Run npm create beignet@latest (or bun create beignet) to scaffold a new app.`,
2448
2460
  },
2449
2461
  });
2450
2462
 
2463
+ const completionProposalCommandPath = ["completion", "propose"] as const;
2464
+
2465
+ function commandPathsFromRouteMap(
2466
+ routeMap: RouteMap<CliContext>,
2467
+ prefix: readonly string[] = [],
2468
+ ): string[][] {
2469
+ return routeMap.getAllEntries().flatMap((entry) => {
2470
+ const path = [...prefix, entry.name["convert-camel-to-kebab"]];
2471
+ if ("getAllEntries" in entry.target) {
2472
+ return commandPathsFromRouteMap(entry.target, path);
2473
+ }
2474
+ return [path];
2475
+ });
2476
+ }
2477
+
2478
+ /** Every canonical CLI command path, derived from runtime routing definitions. */
2479
+ export const cliCommandPaths: readonly (readonly string[])[] = [
2480
+ ...commandPathsFromRouteMap(rootRoutes),
2481
+ completionProposalCommandPath,
2482
+ ].sort((left, right) => left.join(" ").localeCompare(right.join(" ")));
2483
+
2451
2484
  const cli = buildApplication(rootRoutes, {
2452
2485
  name: "beignet",
2453
2486
  versionInfo: {
@@ -2541,6 +2574,9 @@ type OutboxDrainNextStepsResult = {
2541
2574
  delivered: number;
2542
2575
  retried: number;
2543
2576
  deadLettered: number;
2577
+ abandonedDeadLettered: number;
2578
+ settlementFailed: number;
2579
+ leaseLost: number;
2544
2580
  };
2545
2581
  };
2546
2582
 
@@ -2608,7 +2644,10 @@ Result:
2608
2644
  claimed: ${result.result.claimed}
2609
2645
  delivered: ${result.result.delivered}
2610
2646
  retried: ${result.result.retried}
2611
- deadLettered: ${result.result.deadLettered}`;
2647
+ deadLettered: ${result.result.deadLettered}
2648
+ abandonedDeadLettered: ${result.result.abandonedDeadLettered}
2649
+ settlementFailed: ${result.result.settlementFailed}
2650
+ leaseLost: ${result.result.leaseLost}`;
2612
2651
  }
2613
2652
 
2614
2653
  function formatOutboxDate(value: Date | string | null | undefined): string {
@@ -3053,7 +3092,10 @@ export async function main(
3053
3092
  // Shell completion scripts call `beignet completion propose <words...>`.
3054
3093
  // Handle it before stricli parses the inputs: the trailing words are a
3055
3094
  // partial command line, not flags for this CLI.
3056
- if (inputs[0] === "completion" && inputs[1] === "propose") {
3095
+ if (
3096
+ inputs[0] === completionProposalCommandPath[0] &&
3097
+ inputs[1] === completionProposalCommandPath[1]
3098
+ ) {
3057
3099
  await writeCompletionProposals(inputs.slice(2), context);
3058
3100
  return;
3059
3101
  }
package/src/inspect.ts CHANGED
@@ -1576,36 +1576,57 @@ function parseRouteExports(
1576
1576
  config: ResolvedBeignetConfig,
1577
1577
  ): RouteExport[] {
1578
1578
  const exports: RouteExport[] = [];
1579
- const imports = parseNamedImports(source, config);
1580
- const exportRegex =
1581
- /export const\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*=\s*([^;\n]+)/g;
1582
- const apiRouteExportRegex =
1583
- /export const\s*\{([^}]+)\}\s*=\s*createApiRoute\s*\(/g;
1579
+ const sourceFile = ts.createSourceFile(
1580
+ handlerFile,
1581
+ source,
1582
+ ts.ScriptTarget.Latest,
1583
+ true,
1584
+ ts.ScriptKind.TS,
1585
+ );
1586
+ const imports = parseRouteNamedImports(sourceFile, config, handlerFile);
1584
1587
 
1585
- for (const match of source.matchAll(apiRouteExportRegex)) {
1586
- for (const member of match[1].split(",")) {
1587
- const parts = member.split(":");
1588
- const method = (parts[1] ?? parts[0])?.trim();
1589
- if (!method || !isHttpMethod(method)) continue;
1588
+ for (const statement of sourceFile.statements) {
1589
+ if (
1590
+ !ts.isVariableStatement(statement) ||
1591
+ !statement.modifiers?.some(
1592
+ (modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword,
1593
+ )
1594
+ ) {
1595
+ continue;
1596
+ }
1590
1597
 
1591
- exports.push({
1592
- method,
1593
- handlerFile,
1594
- contractRef: routePath,
1595
- catchAllPrefix: catchAllRoutePrefix(routePath),
1596
- source: "next-route",
1597
- });
1598
+ for (const declaration of statement.declarationList.declarations) {
1599
+ if (
1600
+ !declaration.initializer ||
1601
+ !ts.isObjectBindingPattern(declaration.name) ||
1602
+ !isNamedRouteFactoryCall(declaration.initializer, "createApiRoute")
1603
+ ) {
1604
+ continue;
1605
+ }
1606
+
1607
+ for (const element of declaration.name.elements) {
1608
+ if (!ts.isIdentifier(element.name)) continue;
1609
+ const method = element.name.text;
1610
+ if (!isHttpMethod(method)) continue;
1611
+ exports.push({
1612
+ method,
1613
+ handlerFile,
1614
+ contractRef: routePath,
1615
+ catchAllPrefix: catchAllRoutePrefix(routePath),
1616
+ source: "next-route",
1617
+ });
1618
+ }
1598
1619
  }
1599
1620
  }
1600
1621
 
1601
- for (const match of source.matchAll(exportRegex)) {
1602
- const method = match[1] as HttpMethod;
1603
- const expression = match[2];
1604
- const contractMatch =
1605
- /server\.route\(\s*([A-Za-z_$][\w$]*)\s*\)\.handle/.exec(expression);
1622
+ for (const { exportName, declaration } of exportedVariableDeclarations(
1623
+ sourceFile,
1624
+ )) {
1625
+ if (!isHttpMethod(exportName) || !declaration.initializer) continue;
1626
+ const method = exportName;
1627
+ const localName = routeLocalContractIdentifier(declaration.initializer);
1606
1628
 
1607
- if (contractMatch) {
1608
- const localName = contractMatch[1];
1629
+ if (localName) {
1609
1630
  const imported = imports.get(localName);
1610
1631
  exports.push({
1611
1632
  method,
@@ -1617,7 +1638,7 @@ function parseRouteExports(
1617
1638
  continue;
1618
1639
  }
1619
1640
 
1620
- if (/server\.api\b/.test(expression)) {
1641
+ if (isServerApiExpression(declaration.initializer)) {
1621
1642
  exports.push({
1622
1643
  method,
1623
1644
  handlerFile,
@@ -1631,6 +1652,98 @@ function parseRouteExports(
1631
1652
  return exports;
1632
1653
  }
1633
1654
 
1655
+ function parseRouteNamedImports(
1656
+ sourceFile: ts.SourceFile,
1657
+ config: ResolvedBeignetConfig,
1658
+ handlerFile: string,
1659
+ ): Map<string, { importedName: string; contractFile?: string }> {
1660
+ const imports = new Map<
1661
+ string,
1662
+ { importedName: string; contractFile?: string }
1663
+ >();
1664
+
1665
+ for (const statement of sourceFile.statements) {
1666
+ if (
1667
+ !ts.isImportDeclaration(statement) ||
1668
+ !ts.isStringLiteral(statement.moduleSpecifier) ||
1669
+ statement.importClause?.isTypeOnly ||
1670
+ !statement.importClause?.namedBindings ||
1671
+ !ts.isNamedImports(statement.importClause.namedBindings)
1672
+ ) {
1673
+ continue;
1674
+ }
1675
+
1676
+ const contractFile = contractFileFromImport(
1677
+ statement.moduleSpecifier.text,
1678
+ config,
1679
+ handlerFile,
1680
+ );
1681
+ for (const element of statement.importClause.namedBindings.elements) {
1682
+ if (element.isTypeOnly) continue;
1683
+ imports.set(element.name.text, {
1684
+ importedName: element.propertyName?.text ?? element.name.text,
1685
+ contractFile,
1686
+ });
1687
+ }
1688
+ }
1689
+
1690
+ return imports;
1691
+ }
1692
+
1693
+ function isNamedRouteFactoryCall(
1694
+ expression: ts.Expression,
1695
+ name: string,
1696
+ ): boolean {
1697
+ const unwrapped = unwrapContractExpression(expression);
1698
+ if (!ts.isCallExpression(unwrapped)) return false;
1699
+ const callee = unwrapContractExpression(unwrapped.expression);
1700
+ return ts.isIdentifier(callee) && callee.text === name;
1701
+ }
1702
+
1703
+ function routeLocalContractIdentifier(
1704
+ expression: ts.Expression,
1705
+ ): string | undefined {
1706
+ const handleCall = unwrapContractExpression(expression);
1707
+ if (!ts.isCallExpression(handleCall)) return undefined;
1708
+ const handleAccess = unwrapContractExpression(handleCall.expression);
1709
+ if (
1710
+ !ts.isPropertyAccessExpression(handleAccess) ||
1711
+ handleAccess.name.text !== "handle"
1712
+ ) {
1713
+ return undefined;
1714
+ }
1715
+
1716
+ const routeCall = unwrapContractExpression(handleAccess.expression);
1717
+ if (!ts.isCallExpression(routeCall)) return undefined;
1718
+ const routeAccess = unwrapContractExpression(routeCall.expression);
1719
+ if (
1720
+ !ts.isPropertyAccessExpression(routeAccess) ||
1721
+ routeAccess.name.text !== "route" ||
1722
+ !ts.isIdentifier(routeAccess.expression) ||
1723
+ routeAccess.expression.text !== "server"
1724
+ ) {
1725
+ return undefined;
1726
+ }
1727
+
1728
+ const contract = routeCall.arguments[0];
1729
+ const unwrappedContract = contract
1730
+ ? unwrapContractExpression(contract)
1731
+ : undefined;
1732
+ return unwrappedContract && ts.isIdentifier(unwrappedContract)
1733
+ ? unwrappedContract.text
1734
+ : undefined;
1735
+ }
1736
+
1737
+ function isServerApiExpression(expression: ts.Expression): boolean {
1738
+ const unwrapped = unwrapContractExpression(expression);
1739
+ return (
1740
+ ts.isPropertyAccessExpression(unwrapped) &&
1741
+ ts.isIdentifier(unwrapped.expression) &&
1742
+ unwrapped.expression.text === "server" &&
1743
+ unwrapped.name.text === "api"
1744
+ );
1745
+ }
1746
+
1634
1747
  function catchAllRoutePrefix(routePath: string): string | undefined {
1635
1748
  const segments = routePath.split("/").filter(Boolean);
1636
1749
  const catchAllIndex = segments.findIndex((segment) => segment.endsWith("*"));
@@ -1762,6 +1875,29 @@ function contractFileFromImport(
1762
1875
  config: ResolvedBeignetConfig,
1763
1876
  importerFile?: string,
1764
1877
  ): string | undefined {
1878
+ const resolveCandidate = (candidate: string) => {
1879
+ const extension = path.extname(candidate).toLowerCase();
1880
+ if (!extension) return `${candidate}.ts`;
1881
+
1882
+ let sourceExtension: string | undefined;
1883
+ switch (extension) {
1884
+ case ".js":
1885
+ sourceExtension = ".ts";
1886
+ break;
1887
+ case ".jsx":
1888
+ sourceExtension = ".tsx";
1889
+ break;
1890
+ case ".mjs":
1891
+ sourceExtension = ".mts";
1892
+ break;
1893
+ case ".cjs":
1894
+ sourceExtension = ".cts";
1895
+ break;
1896
+ }
1897
+ return sourceExtension
1898
+ ? `${candidate.slice(0, -extension.length)}${sourceExtension}`
1899
+ : candidate;
1900
+ };
1765
1901
  const contractsPath = directoryPath(config.paths.contracts);
1766
1902
  const aliasPrefix = `@/${contractsPath}/`;
1767
1903
  const aliasExact = `@/${contractsPath}`;
@@ -1772,18 +1908,18 @@ function contractFileFromImport(
1772
1908
  return `${contractsPath}/index.ts`;
1773
1909
  }
1774
1910
  if (sourcePath.startsWith(aliasPrefix)) {
1775
- return `${sourcePath.slice("@/".length)}.ts`;
1911
+ return resolveCandidate(sourcePath.slice("@/".length));
1776
1912
  }
1777
1913
 
1778
1914
  if (sourcePath.startsWith(relativePrefix)) {
1779
- return `${sourcePath}.ts`;
1915
+ return resolveCandidate(sourcePath);
1780
1916
  }
1781
1917
 
1782
1918
  if (importerFile && sourcePath.startsWith(".")) {
1783
1919
  const resolved = normalizePath(
1784
1920
  path.join(path.dirname(importerFile), sourcePath),
1785
1921
  );
1786
- return `${resolved}.ts`;
1922
+ return resolveCandidate(resolved);
1787
1923
  }
1788
1924
 
1789
1925
  return undefined;
package/src/make.ts CHANGED
@@ -2225,7 +2225,6 @@ async function updateOutboxDrainProvider(
2225
2225
  \t\t\tdefer: after,
2226
2226
  \t\t\tcreateContext: () => createServiceContext(undefined),
2227
2227
  \t\t\tregistry: async () => (await import("${outboxModule}")).outboxRegistry,
2228
- \t\t\tbatchSize: 100,
2229
2228
  \t\t});
2230
2229
 
2231
2230
  \t\treturn {
@@ -5921,7 +5920,6 @@ export const { GET, POST } = createOutboxDrainRoute({
5921
5920
  \tserver: getServer,
5922
5921
  \tregistry: outboxRegistry,
5923
5922
  \tsecret: env.CRON_SECRET,
5924
- \tbatchSize: 100,
5925
5923
  });
5926
5924
  `;
5927
5925
  }
package/src/mcp.ts CHANGED
@@ -285,6 +285,7 @@ const outboxRunInputSchema = z.discriminatedUnion("operation", [
285
285
  z.object({
286
286
  operation: z.literal("drain"),
287
287
  batchSize: z.number().int().positive().optional(),
288
+ concurrency: z.number().int().positive().optional(),
288
289
  module: operationalModuleSchema,
289
290
  timeoutMs: operationalTimeoutSchema,
290
291
  }),
@@ -349,17 +350,36 @@ function jsonResult(value: unknown): McpToolResult {
349
350
  };
350
351
  }
351
352
 
352
- function operationalJsonResult(value: unknown): McpToolResult {
353
+ function operationalJsonResult(
354
+ value: unknown,
355
+ options: { isError?: boolean } = {},
356
+ ): McpToolResult {
353
357
  const formatted = JSON.stringify(value, null, 2);
354
358
  const text =
355
359
  Buffer.byteLength(formatted) <= defaultOperationalResultMaxBytes
356
360
  ? formatted
357
361
  : JSON.stringify(value);
358
362
  return {
363
+ ...(options.isError ? { isError: true } : {}),
359
364
  content: [{ type: "text", text }],
360
365
  };
361
366
  }
362
367
 
368
+ function isUncertainOutboxDrainReport(value: unknown): boolean {
369
+ if (typeof value !== "object" || value === null || !("result" in value)) {
370
+ return false;
371
+ }
372
+ const result = value.result;
373
+ if (typeof result !== "object" || result === null) return false;
374
+ const settlementFailed =
375
+ "settlementFailed" in result ? result.settlementFailed : undefined;
376
+ const leaseLost = "leaseLost" in result ? result.leaseLost : undefined;
377
+ return (
378
+ (typeof settlementFailed === "number" && settlementFailed > 0) ||
379
+ (typeof leaseLost === "number" && leaseLost > 0)
380
+ );
381
+ }
382
+
363
383
  function errorResult(error: unknown): McpToolResult {
364
384
  return {
365
385
  isError: true,
@@ -972,7 +992,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
972
992
  "outbox_run",
973
993
  {
974
994
  description:
975
- "Run a state-changing outbox drain, requeue, purge, or prune operation and return exactly the matching versioned beignet outbox <operation> --json report. Purge and prune support dryRun. Runs in an isolated process with bounded structured output, cancellation, and a timeout.",
995
+ "Run a state-changing outbox drain, requeue, purge, or prune operation and return exactly the matching versioned beignet outbox <operation> --json report. Drain supports bounded unordered concurrency and returns an error-bearing report when settlement or lease state is uncertain. Purge and prune support dryRun. Runs in an isolated process with bounded structured output, cancellation, and a timeout.",
976
996
  inputSchema: outboxRunInputSchema,
977
997
  annotations: {
978
998
  readOnlyHint: false,
@@ -988,6 +1008,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
988
1008
  ? {
989
1009
  command: "outbox:drain" as const,
990
1010
  batchSize: input.batchSize,
1011
+ concurrency: input.concurrency,
991
1012
  modulePath: input.module,
992
1013
  }
993
1014
  : input.operation === "requeue"
@@ -1014,14 +1035,16 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
1014
1035
  dryRun: input.dryRun,
1015
1036
  modulePath: input.module,
1016
1037
  };
1017
- return operationalJsonResult(
1018
- await runIsolatedOperationalCommand({
1019
- cwd,
1020
- request,
1021
- signal: ctx.mcpReq.signal,
1022
- timeoutMs: input.timeoutMs ?? defaultOperationalCommandTimeoutMs,
1023
- }),
1024
- );
1038
+ const result = await runIsolatedOperationalCommand({
1039
+ cwd,
1040
+ request,
1041
+ signal: ctx.mcpReq.signal,
1042
+ timeoutMs: input.timeoutMs ?? defaultOperationalCommandTimeoutMs,
1043
+ });
1044
+ return operationalJsonResult(result, {
1045
+ isError:
1046
+ input.operation === "drain" && isUncertainOutboxDrainReport(result),
1047
+ });
1025
1048
  }),
1026
1049
  );
1027
1050
 
@@ -44,6 +44,7 @@ export type OperationalCommandRequest =
44
44
  | {
45
45
  command: "outbox:drain";
46
46
  batchSize?: number;
47
+ concurrency?: number;
47
48
  modulePath?: string;
48
49
  }
49
50
  | {
@@ -47,6 +47,7 @@ async function runOperationalRequest(input: OperationalRunnerInput) {
47
47
  return runOutboxDrain({
48
48
  cwd,
49
49
  batchSize: request.batchSize,
50
+ concurrency: request.concurrency,
50
51
  modulePath: request.modulePath,
51
52
  });
52
53
  case "outbox:list":
package/src/outbox.ts CHANGED
@@ -10,6 +10,7 @@ import type {
10
10
  OutboxMessageStatus,
11
11
  OutboxRegistry,
12
12
  } from "@beignet/core/outbox";
13
+ import type { TracingPort } from "@beignet/core/tracing";
13
14
  import { createJiti } from "jiti";
14
15
  import { loadBeignetConfig, normalizePath } from "./config.js";
15
16
  import {
@@ -26,6 +27,7 @@ export type RunOutboxDrainOptions = {
26
27
  cwd?: string;
27
28
  modulePath?: string;
28
29
  batchSize?: number;
30
+ concurrency?: number;
29
31
  };
30
32
 
31
33
  /**
@@ -175,6 +177,7 @@ type OutboxDrainPorts = {
175
177
  logger?: OutboxDrainLogger;
176
178
  devtools?: OutboxDrainInstrumentation;
177
179
  instrumentation?: OutboxDrainInstrumentation;
180
+ tracing?: TracingPort;
178
181
  errorReporter?: ErrorReporterPort;
179
182
  };
180
183
 
@@ -187,6 +190,7 @@ type OutboxDrainContext = {
187
190
  type OutboxDrainContextArgs = {
188
191
  registry: OutboxRegistry;
189
192
  batchSize?: number;
193
+ concurrency?: number;
190
194
  };
191
195
 
192
196
  type OutboxAdminOperation = "list" | "show" | "requeue" | "purge" | "prune";
@@ -219,7 +223,11 @@ export async function runOutboxDrain(
219
223
  const config = await loadBeignetConfig(cwd);
220
224
  const modulePath = normalizePath(options.modulePath ?? config.paths.outbox);
221
225
  const batchSize = options.batchSize;
226
+ const concurrency = options.concurrency;
222
227
  if (batchSize !== undefined) assertPositiveInteger("batchSize", batchSize);
228
+ if (concurrency !== undefined) {
229
+ assertPositiveInteger("concurrency", concurrency);
230
+ }
223
231
 
224
232
  const startedAt = performance.now();
225
233
  const outboxModule = await loadOutboxModule(cwd, modulePath);
@@ -227,7 +235,7 @@ export async function runOutboxDrain(
227
235
  outboxModule.outboxRegistry,
228
236
  modulePath,
229
237
  );
230
- const contextArgs = { registry, batchSize };
238
+ const contextArgs = { registry, batchSize, concurrency };
231
239
  const rawContext = outboxModule.createOutboxDrainContext
232
240
  ? await outboxModule.createOutboxDrainContext(contextArgs)
233
241
  : undefined;
@@ -247,7 +255,11 @@ export async function runOutboxDrain(
247
255
  eventBus: ctx.ports.eventBus,
248
256
  jobs: ctx.ports.jobs,
249
257
  batchSize,
250
- instrumentation: ctx.ports.instrumentation ?? ctx.ports.devtools,
258
+ concurrency,
259
+ instrumentation: {
260
+ instrumentation: ctx.ports.instrumentation ?? ctx.ports.devtools,
261
+ tracing: ctx.ports.tracing,
262
+ },
251
263
  instrumentationContext: {
252
264
  requestId: ctx.requestId,
253
265
  traceId: ctx.traceId,
@@ -288,27 +300,65 @@ export async function runOutboxDrain(
288
300
  },
289
301
  });
290
302
  },
291
- onSettlementError(settlementError, message) {
303
+ onLeaseError(failure) {
304
+ const outcome =
305
+ failure.state === "lost"
306
+ ? "leaseLost"
307
+ : failure.state === "recovered"
308
+ ? "leaseRecovered"
309
+ : "leaseDegraded";
292
310
  return reportOperationalFailure({
293
311
  ctx,
294
- error: settlementError,
312
+ error: failure.error,
313
+ reportOptions: {
314
+ level: failure.state === "lost" ? "error" : "warning",
315
+ mechanism: "beignet.outbox.cli",
316
+ handled: failure.state !== "lost",
317
+ tags: {
318
+ "beignet.kind": "outbox",
319
+ "beignet.outbox.kind": failure.message.kind,
320
+ "beignet.outbox.name": failure.message.name,
321
+ "beignet.outbox.outcome": outcome,
322
+ },
323
+ contexts: {
324
+ outbox: {
325
+ messageId: failure.message.id,
326
+ kind: failure.message.kind,
327
+ name: failure.message.name,
328
+ attempts: failure.message.attempts,
329
+ maxAttempts: failure.message.maxAttempts,
330
+ operation: failure.operation,
331
+ state: failure.state,
332
+ confirmedLost: failure.confirmedLost,
333
+ outcome,
334
+ },
335
+ },
336
+ },
337
+ });
338
+ },
339
+ onSettlementError(failure) {
340
+ return reportOperationalFailure({
341
+ ctx,
342
+ error: failure.error,
295
343
  reportOptions: {
296
344
  level: "error",
297
345
  mechanism: "beignet.outbox.cli",
298
346
  handled: false,
299
347
  tags: {
300
348
  "beignet.kind": "outbox",
301
- "beignet.outbox.kind": message.kind,
302
- "beignet.outbox.name": message.name,
349
+ "beignet.outbox.kind": failure.message.kind,
350
+ "beignet.outbox.name": failure.message.name,
303
351
  "beignet.outbox.outcome": "settlementFailed",
304
352
  },
305
353
  contexts: {
306
354
  outbox: {
307
- messageId: message.id,
308
- kind: message.kind,
309
- name: message.name,
310
- attempts: message.attempts,
311
- maxAttempts: message.maxAttempts,
355
+ messageId: failure.message.id,
356
+ kind: failure.message.kind,
357
+ name: failure.message.name,
358
+ attempts: failure.message.attempts,
359
+ maxAttempts: failure.message.maxAttempts,
360
+ operation: failure.operation,
361
+ deliverySucceeded: failure.deliverySucceeded,
312
362
  outcome: "settlementFailed",
313
363
  },
314
364
  },
@@ -620,6 +670,7 @@ function isOutboxPort(value: unknown): value is DrainOutboxOptions["outbox"] {
620
670
  isRecord(value) &&
621
671
  typeof value.enqueue === "function" &&
622
672
  typeof value.claimBatch === "function" &&
673
+ typeof value.renewClaim === "function" &&
623
674
  typeof value.markDelivered === "function" &&
624
675
  typeof value.markFailed === "function"
625
676
  );
@@ -677,7 +728,7 @@ async function recordOutboxDrain(
677
728
  watcher: "outbox",
678
729
  name: "outbox.drain",
679
730
  label: "Outbox drain",
680
- summary: `${result.delivered} delivered, ${result.retried} retried, ${result.deadLettered} dead-lettered`,
731
+ summary: `${result.delivered} delivered, ${result.retried} retried, ${result.deadLettered} dead-lettered, ${result.settlementFailed} settlement-uncertain, ${result.leaseLost} lease-lost`,
681
732
  requestId: ctx.requestId,
682
733
  traceId: ctx.traceId,
683
734
  details: result,
@@ -107,7 +107,9 @@ re-exports and \`db\` for \`generate\`, \`migrate\`, \`seed\`, or \`reset\`
107
107
  instead of falling back to a shell. Use the read-only \`db_status\` tool before
108
108
  deploying. Use \`task_run\`, \`schedule_run\`,
109
109
  \`outbox_inspect\`, and \`outbox_run\` for registered operational workflows
110
- and outbox recovery.
110
+ and outbox recovery. Treat a drain with nonzero \`settlementFailed\` or
111
+ \`leaseLost\` as an operational failure. Parallel delivery is opt-in and does
112
+ not preserve message order.
111
113
 
112
114
  ## The framework already solves these
113
115
 
@@ -242,7 +244,10 @@ and reports the script without executing it; it does not simulate SQL or data
242
244
  changes. Use \`task_run\` and \`schedule_run\` for registered operational
243
245
  workflows, \`outbox_inspect\` for read-only \`list\` and \`show\`, and
244
246
  \`outbox_run\` for \`drain\`, \`requeue\`, \`purge\`, or \`prune\`; purge and
245
- prune support \`dryRun\`. Operational commands run in isolated process trees
247
+ prune support \`dryRun\`. A drain with nonzero \`settlementFailed\` or
248
+ \`leaseLost\` remains a complete JSON report but is an error result; inspect
249
+ storage health and clock synchronization before retrying. Operational commands
250
+ run in isolated process trees
246
251
  with bounded results, cancellation, and timeouts. Optional \`module\` overrides
247
252
  must remain inside the app root where the MCP server started. Cancellation and
248
253
  timeouts cannot roll back side effects that already completed. Inspect app