@kontextmind/kxm 0.7.123 → 0.7.125

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.123",
3
+ "version": "0.7.125",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -7,6 +7,7 @@ import { print, type Runtime } from "./types.ts";
7
7
  export interface LandOptions {
8
8
  pr?: string;
9
9
  stage?: string;
10
+ title?: string;
10
11
  bodyFile?: string;
11
12
  }
12
13
 
@@ -35,6 +36,7 @@ export function cmdLand(runtime: Runtime, options: LandOptions = {}): Promise<nu
35
36
  const args = [script];
36
37
  if (options.pr) args.push("--pr", options.pr);
37
38
  if (options.stage) args.push("--stage", options.stage);
39
+ if (options.title) args.push("--title", options.title);
38
40
  if (options.bodyFile) args.push("--body-file", options.bodyFile);
39
41
  if (runtime.json) args.push("--json");
40
42
  if (runtime.dryRun) args.push("--dry-run");
@@ -522,7 +522,12 @@ interface KxmStatusDrive {
522
522
  mode?: string;
523
523
  openedAt?: string;
524
524
  receipt?: {
525
- settlement?: { kind?: string; status?: string; reason?: string };
525
+ settlement?: {
526
+ kind?: string;
527
+ status?: string;
528
+ reason?: string;
529
+ handoff?: { reason?: string; attemptId?: string };
530
+ };
526
531
  } | null;
527
532
  verified?: boolean;
528
533
  divergence?: string;
@@ -568,7 +573,14 @@ function formatRunStatusLine(
568
573
  drive: KxmStatusDrive | undefined,
569
574
  ): string {
570
575
  const reason = typeof drive?.receipt?.settlement?.reason === "string" ? drive.receipt.settlement.reason : "";
571
- const statusLabel = run.status === "cancelled" ? formatCancelledStatus(reason) : run.status;
576
+ const handoff = drive?.receipt?.settlement?.handoff;
577
+ const attemptId = typeof handoff?.attemptId === "string" ? handoff.attemptId : "";
578
+ const handoffReason = typeof handoff?.reason === "string" ? handoff.reason : "";
579
+ const statusLabel = run.status === "cancelled"
580
+ ? formatCancelledStatus(reason)
581
+ : run.status === "cancelling" && attemptId.length > 0 && handoffReason.length > 0
582
+ ? `cancelling (attempt ${attemptId}, ${handoffReason})`
583
+ : run.status;
572
584
  return `run ${run.runId}: ${statusLabel} (workflow ${run.workflowId}, updated ${run.updatedAt})`;
573
585
  }
574
586
 
@@ -31,6 +31,7 @@ import { projectRuntimeOwnsRun } from "./runtime-store.ts";
31
31
 
32
32
  // Submodule imports
33
33
  import {
34
+ CLI_RESULT_SCHEMA,
34
35
  print,
35
36
  refuseDryRun,
36
37
  runtimeFrom,
@@ -400,7 +401,7 @@ async function dispatchAgentCliCommand(
400
401
  }
401
402
  }
402
403
 
403
- function createProgram(ctx: CliContext, result: { code: number }): Command {
404
+ function createProgram(ctx: CliContext, result: { code: number }, argv: readonly string[]): Command {
404
405
  const bind = (action: (runtime: Runtime, ...args: never[]) => Promise<number>) => {
405
406
  return async function commandAction(this: Command, ...args: unknown[]) {
406
407
  const command = args.at(-1) instanceof Command ? args.at(-1) as Command : this;
@@ -415,7 +416,11 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
415
416
  .exitOverride()
416
417
  .configureOutput({
417
418
  writeOut: (text) => ctx.io.stdout(text),
418
- writeErr: (text) => ctx.io.stderr(text),
419
+ // Subcommands inherit this object by reference (copyInheritedSettings).
420
+ writeErr: (text) => {
421
+ if (hasJsonFlag(argv)) return;
422
+ ctx.io.stderr(text);
423
+ },
419
424
  })
420
425
  .helpCommand("help", "Show help");
421
426
  addGlobalOptions(program);
@@ -496,11 +501,13 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
496
501
  addGlobalOptions(program.command("land").description("Land the current branch: verify, regenerate docs, push, pull request, rebase, unblock, squash-merge, release, and milestone"))
497
502
  .option("--pr <n>", "Existing pull request number")
498
503
  .option("--stage <name>", "Run one stage: verify, docs, push, pr, rebase, unblock, merge, release, or milestone")
504
+ .option("--title <text>", "Pull request title. Default is the subject of the first commit on the branch")
499
505
  .option("--body-file <path>", "Pull request body file used when creating a pull request")
500
- .action(async function landAction(this: Command, options: { pr?: string; stage?: string; bodyFile?: string }) {
506
+ .action(async function landAction(this: Command, options: { pr?: string; stage?: string; title?: string; bodyFile?: string }) {
501
507
  result.code = await cmdLand(runtimeFrom(ctx, this), {
502
508
  ...(options.pr !== undefined ? { pr: options.pr } : {}),
503
509
  ...(options.stage !== undefined ? { stage: options.stage } : {}),
510
+ ...(options.title !== undefined ? { title: options.title } : {}),
504
511
  ...(options.bodyFile !== undefined ? { bodyFile: options.bodyFile } : {}),
505
512
  });
506
513
  });
@@ -1434,10 +1441,57 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
1434
1441
  return program;
1435
1442
  }
1436
1443
 
1437
- function mapCommanderError(error: CommanderError): number {
1444
+ function hasJsonFlag(argv: readonly string[]): boolean {
1445
+ for (let index = 0; index < argv.length; index += 1) {
1446
+ const arg = argv[index]!;
1447
+ if (arg === "--") break;
1448
+ if (arg === "--json") return true;
1449
+ }
1450
+ return false;
1451
+ }
1452
+
1453
+ function commandWords(argv: readonly string[]): string[] {
1454
+ const words: string[] = [];
1455
+ for (let index = 0; index < argv.length; index += 1) {
1456
+ const arg = argv[index]!;
1457
+ if (arg === "--") break;
1458
+ if (!arg.startsWith("-")) {
1459
+ words.push(arg);
1460
+ continue;
1461
+ }
1462
+ if (arg.includes("=") || arg === "--json" || arg === "--dry-run" || arg === "-h" || arg === "--help" || arg === "-V" || arg === "--version") continue;
1463
+ const next = argv[index + 1];
1464
+ if (next !== undefined && !next.startsWith("-")) index += 1;
1465
+ }
1466
+ return words;
1467
+ }
1468
+
1469
+ function commandPathFromArgv(program: Command, argv: readonly string[]): string {
1470
+ const names: string[] = [];
1471
+ let current: Command = program;
1472
+ for (const word of commandWords(argv)) {
1473
+ const next = current.commands.find((command) => command.name() === word || command.aliases().includes(word));
1474
+ if (!next) break;
1475
+ names.push(next.name());
1476
+ current = next;
1477
+ }
1478
+ return names.join(" ");
1479
+ }
1480
+
1481
+ function mapCommanderError(error: CommanderError, argv: readonly string[], program: Command, io: CliIo): number {
1438
1482
  if (error.exitCode === 0) return 0;
1439
- if (USAGE_ERROR_CODES.has(error.code)) return 2;
1440
- return error.exitCode || 1;
1483
+ const code = USAGE_ERROR_CODES.has(error.code) ? 2 : (error.exitCode || 1);
1484
+ if (code === 2 && hasJsonFlag(argv)) {
1485
+ const command = commandPathFromArgv(program, argv);
1486
+ io.stdout(`${JSON.stringify({
1487
+ schema: CLI_RESULT_SCHEMA,
1488
+ ok: false,
1489
+ command: command || "kxm",
1490
+ error: "usage_error",
1491
+ detail: error.message,
1492
+ })}\n`);
1493
+ }
1494
+ return code;
1441
1495
  }
1442
1496
 
1443
1497
  const MESH_REMOVED_TEXT = "kxm mesh was removed. Use kxm init, kxm hub start|view|stop, and node scripts/smoke-multi-pi.mjs (KXM_SMOKE=1).";
@@ -1486,12 +1540,12 @@ export async function runCli(
1486
1540
  return 2;
1487
1541
  }
1488
1542
  const result = { code: 0 };
1489
- const program = createProgram({ env, io, cwd }, result);
1543
+ const program = createProgram({ env, io, cwd }, result, argv);
1490
1544
  try {
1491
1545
  await program.parseAsync(argv, { from: "user" });
1492
1546
  return result.code;
1493
1547
  } catch (error) {
1494
- if (error instanceof CommanderError) return mapCommanderError(error);
1548
+ if (error instanceof CommanderError) return mapCommanderError(error, argv, program, io);
1495
1549
  if (error instanceof DryRunRefused) return error.code;
1496
1550
  throw error;
1497
1551
  }
@@ -158,6 +158,7 @@ interface MutableAttempt {
158
158
  status: string;
159
159
  resultClass?: string | undefined;
160
160
  outcome?: string | undefined;
161
+ producerError?: string | undefined;
161
162
  }
162
163
 
163
164
  interface MutableAssignment {
@@ -317,7 +318,7 @@ function authorizedCancelStep(state: MutableState, step: KxmCompiledStep, curren
317
318
  if (status !== "cancelled" || !state.cancelRequested) return false;
318
319
  if (createdOnlySingletonCancel(state, step, current, status)) return true;
319
320
  if (!panelIssuedAttemptsTerminal(current) || !panelAssignmentsTerminal(current)) return false;
320
- const members: Array<{ resultClass: string; outcome: string | undefined }> = [];
321
+ const members: Array<{ resultClass: string; outcome: string | undefined; producerError: string | undefined }> = [];
321
322
  for (const assignmentId of current.panel.order) {
322
323
  const assignment = current.panel.assignments[assignmentId];
323
324
  if (!assignment) return false;
@@ -328,9 +329,12 @@ function authorizedCancelStep(state: MutableState, step: KxmCompiledStep, curren
328
329
  }
329
330
  const currentAttempt = currentAttemptOf(assignment);
330
331
  if (!currentAttempt?.resultClass) return false;
331
- members.push({ resultClass: currentAttempt.resultClass, outcome: currentAttempt.outcome });
332
+ members.push({ resultClass: currentAttempt.resultClass, outcome: currentAttempt.outcome, producerError: currentAttempt.producerError });
332
333
  }
333
334
  if (members.some((member) => member.resultClass === "outcome_unknown" || member.resultClass === "producer_rejected")) {
335
+ if (members.every((member) => member.resultClass === "producer_rejected" && member.producerError === "executing_unrecorded")) {
336
+ return true;
337
+ }
334
338
  return false;
335
339
  }
336
340
  const declared = members.filter((member) => member.resultClass === "outcome");
@@ -343,8 +347,8 @@ function authorizedCancelStep(state: MutableState, step: KxmCompiledStep, curren
343
347
 
344
348
  function currentAttemptOf(assignment: {
345
349
  currentAttemptId?: string | undefined;
346
- attempts: { readonly [id: string]: { resultClass?: string | undefined; outcome?: string | undefined } | undefined };
347
- }): { resultClass?: string | undefined; outcome?: string | undefined } | undefined {
350
+ attempts: { readonly [id: string]: { resultClass?: string | undefined; outcome?: string | undefined; producerError?: string | undefined } | undefined };
351
+ }): { resultClass?: string | undefined; outcome?: string | undefined; producerError?: string | undefined } | undefined {
348
352
  const attemptId = assignment.currentAttemptId;
349
353
  return attemptId ? assignment.attempts[attemptId] : undefined;
350
354
  }
@@ -735,8 +739,8 @@ function isProvenFailure(state: MutableState, plan: KxmCompiledPlan | undefined)
735
739
  }
736
740
 
737
741
  function foldDriveOpened(state: MutableState, event: KxmRunEvent): void {
738
- if (state.status !== "running" && state.status !== "blocked_uncertain") {
739
- throw runtimeError("run_events_illegal", state.runId, "run.drive_opened is only legal while running or blocked_uncertain");
742
+ if (state.status !== "running" && state.status !== "blocked_uncertain" && state.status !== "cancelling") {
743
+ throw runtimeError("run_events_illegal", state.runId, "run.drive_opened is only legal while running, blocked_uncertain, or cancelling");
740
744
  }
741
745
  const driveId = stringPayload(event, "driveId");
742
746
  const mode = stringPayload(event, "mode");
@@ -1093,9 +1097,13 @@ function foldAssignmentAdvance(state: MutableState, plan: KxmCompiledPlan, event
1093
1097
  }
1094
1098
  }
1095
1099
  if (attemptId && attempt) {
1100
+ const producerError = typeof event.payload.producerError === "string" ? event.payload.producerError : undefined;
1096
1101
  nextAssignment = {
1097
1102
  ...nextAssignment,
1098
- attempts: { ...nextAssignment.attempts, [attemptId]: { ...attempt, resultClass } },
1103
+ attempts: {
1104
+ ...nextAssignment.attempts,
1105
+ [attemptId]: { ...attempt, resultClass, ...(producerError !== undefined ? { producerError } : {}) },
1106
+ },
1099
1107
  };
1100
1108
  }
1101
1109
  }
@@ -31,7 +31,7 @@ const CONTROL_ROOT_REQUIRED = ["repositoryId", "projectKey"] as const;
31
31
  const GATE_REGISTRY_SCHEMA = "kxm.gate-registry.v1";
32
32
  const REVISION_REQUIRED = ["config", "executorPolicy", "toolPolicy", "memory"] as const;
33
33
  const PROJECT_LIMIT_REQUIRED = ["maxConcurrentRuns"] as const;
34
- const PROJECT_LIMIT_OPTIONAL = ["maxRunDurationMs", "maxAgentTimeMs"] as const;
34
+ const PROJECT_LIMIT_OPTIONAL = ["maxRunDurationMs", "maxAgentTimeMs", "agentStepTimeoutMs"] as const;
35
35
  const PLAN_REQUIRED = ["schema", "workflowId", "coordinator", "limits", "transitionBudget", "hasBackEdges", "entryStepId", "order", "steps", "requirePlanHash"] as const;
36
36
  const PLAN_OPTIONAL = ["sourcePath", "reproOracle", "planHash"] as const;
37
37
  const LIMIT_OPTIONAL = ["maxTransitions", "maxRunDurationMs", "maxAgentTimeMs", "maxModelCost", "currency"] as const;
@@ -85,6 +85,7 @@ export interface KxmRunPlanEnvelope {
85
85
  readonly maxConcurrentRuns: number;
86
86
  readonly maxRunDurationMs?: number;
87
87
  readonly maxAgentTimeMs?: number;
88
+ readonly agentStepTimeoutMs?: number;
88
89
  };
89
90
  readonly plan: KxmCompiledPlan;
90
91
  readonly gates: KxmPinnedGates;
@@ -252,6 +253,7 @@ function parseEnvelope(parsed: unknown, run: KxmRunRecord): KxmRunPlanEnvelope {
252
253
  maxConcurrentRuns: asCount(limitsValue.maxConcurrentRuns, run.runId, "projectLimits.maxConcurrentRuns"),
253
254
  ...(limitsValue.maxRunDurationMs !== undefined ? { maxRunDurationMs: asDuration(limitsValue.maxRunDurationMs, run.runId, "projectLimits.maxRunDurationMs") } : {}),
254
255
  ...(limitsValue.maxAgentTimeMs !== undefined ? { maxAgentTimeMs: asDuration(limitsValue.maxAgentTimeMs, run.runId, "projectLimits.maxAgentTimeMs") } : {}),
256
+ ...(limitsValue.agentStepTimeoutMs !== undefined ? { agentStepTimeoutMs: asAgentStepTimeout(limitsValue.agentStepTimeoutMs, run.runId, "projectLimits.agentStepTimeoutMs") } : {}),
255
257
  };
256
258
  const plan = freezeKxmCompiledPlan(parseCompiledPlan(value.plan, run.runId));
257
259
  return {
@@ -698,6 +700,13 @@ function asDuration(value: unknown, runId: string, label: string): number {
698
700
  return value;
699
701
  }
700
702
 
703
+ function asAgentStepTimeout(value: unknown, runId: string, label: string): number {
704
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 60_000) {
705
+ throw runtimeError("run_plan_corrupt", runId, `${label} is below 60000`);
706
+ }
707
+ return value;
708
+ }
709
+
701
710
  function asCost(value: unknown, runId: string, label: string): number {
702
711
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
703
712
  throw runtimeError("run_plan_corrupt", runId, `${label} is not a positive cost`);
@@ -14,6 +14,7 @@ import {
14
14
  } from "./context-packet.ts";
15
15
  import { compileKxmWorkflow, type KxmCompiledPlan, type KxmCompiledStep } from "./engine-compile.ts";
16
16
  import { BUILTIN_HARNESSES, oneShotPermissionArgs, oneShotWriterArgs, validateHarnessModelPair } from "./harness.ts";
17
+ import { readOneShotObservedChildExit } from "./oneshot-evidence.ts";
17
18
  import {
18
19
  effectiveRunDurationBudget,
19
20
  isTerminalRunStatus,
@@ -75,6 +76,7 @@ import {
75
76
  kxmIncrementMonotonicNs,
76
77
  kxmMonotonicNs,
77
78
  kxmPolicyRevisions,
79
+ KXM_DEFAULT_AGENT_STEP_TIMEOUT_MS,
78
80
  kxmProjectAdmissionLimits,
79
81
  type KxmMemoryRevisionOptions,
80
82
  type KxmRuntimeContext,
@@ -157,6 +159,8 @@ export interface KxmProducerRequest {
157
159
  readonly harness?: string | undefined;
158
160
  /** Live producers select an audited argv profile from this ceiling. */
159
161
  readonly permission?: "read-only" | "edit" | undefined;
162
+ /** Agent and moa steps only. Absent means the producer options, then the process default. */
163
+ readonly timeoutMs?: number | undefined;
160
164
  readonly contextPacket?: FormalContextPacketV2 | undefined;
161
165
  readonly handoffManifest?: HandoffManifestV1 | undefined;
162
166
  }
@@ -405,6 +409,10 @@ async function driveAdmitted(
405
409
  if (started.handoff || isTerminalRunStatus(started.state.status)) return started;
406
410
  }
407
411
  let latest: KxmRunDriveResult = { state: foldStoredKxmRun(context, requireRun(context, runId)) };
412
+ if (latest.state.status === "cancelling") {
413
+ latest = await stepLocked(context, runId, producer, token);
414
+ if (latest.handoff || isTerminalRunStatus(latest.state.status)) return latest;
415
+ }
408
416
  while (latest.state.status === "running") {
409
417
  const overBudget = cancelRunDurationIfExceeded(context, runId);
410
418
  if (overBudget) return overBudget;
@@ -487,6 +495,7 @@ function receiptHandoff(handoff: KxmRunHandoff): KxmDriveReceiptHandoff {
487
495
  detail: handoff.detail,
488
496
  ...(handoff.field !== undefined ? { field: handoff.field } : {}),
489
497
  ...(handoff.stepId !== undefined ? { stepId: handoff.stepId } : {}),
498
+ ...(handoff.attemptId !== undefined ? { attemptId: handoff.attemptId } : {}),
490
499
  };
491
500
  }
492
501
 
@@ -557,6 +566,9 @@ export function recordDriveReceipt(
557
566
  producer: { id: closeInfo.producerId, closed: closeInfo.producerClosed },
558
567
  };
559
568
  context.eventStore.insertDriveReceipt(receipt);
569
+ if (closeInfo.kind === "handoff") {
570
+ releaseKxmRun(context.eventStore.path, session.runId, session.token);
571
+ }
560
572
  } catch {
561
573
  // The drive result governs; a missing receipt is visible on poll.
562
574
  }
@@ -719,7 +731,7 @@ export class KxmRunScheduler {
719
731
  }
720
732
  try {
721
733
  const run = requireRun(this.context, runId);
722
- if (isTerminalRunStatus(run.status) || run.status === "cancelling") {
734
+ if (isTerminalRunStatus(run.status)) {
723
735
  return Promise.reject(runtimeError("run_busy", runId, `run ${runId} is already ${run.status}`));
724
736
  }
725
737
  } catch (error) {
@@ -1441,6 +1453,74 @@ function resolveProducerRoute(
1441
1453
  return { provider, model, selector };
1442
1454
  }
1443
1455
 
1456
+ function projectAgentStepTimeoutMs(context: KxmRuntimeContext, run: KxmRunRecord): number {
1457
+ return loadKxmRunPlanEnvelope(context.eventStore, run).projectLimits.agentStepTimeoutMs ?? KXM_DEFAULT_AGENT_STEP_TIMEOUT_MS;
1458
+ }
1459
+
1460
+ function unsupportedAgentStepTimeout(step: KxmCompiledStep, limitMs: number): Omit<KxmRunHandoff, "stepId"> | undefined {
1461
+ if (step.kind !== "agent" && step.kind !== "moa") return undefined;
1462
+ if (step.timeoutMs === undefined || step.timeoutMs <= limitMs) return undefined;
1463
+ return {
1464
+ reason: "step_unsupported",
1465
+ field: "timeoutMs",
1466
+ detail: "step timeoutMs is wider than project limits.agentStepTimeoutMs",
1467
+ };
1468
+ }
1469
+
1470
+ function executingSingletonAttempt(
1471
+ plan: KxmCompiledPlan,
1472
+ state: KxmRunState,
1473
+ ): { stepId: string; assignmentId: string; attemptId: string } | undefined {
1474
+ const current = state.currentStep;
1475
+ if (!current || current.panel.order.length !== 1) return undefined;
1476
+ const step = plan.steps[current.stepId];
1477
+ if (!step || (step.kind !== "agent" && step.kind !== "moa") || step.assignments.maximum > 1) return undefined;
1478
+ const attemptId = unreconciledPanelAttemptId(state);
1479
+ if (!attemptId) return undefined;
1480
+ const located = kxmFoldPanelAttempt(current, attemptId);
1481
+ if (located?.attempt.status !== "executing") return undefined;
1482
+ return { stepId: current.stepId, assignmentId: located.assignmentId, attemptId };
1483
+ }
1484
+
1485
+ function settleExitedExecutingAttempt(
1486
+ context: KxmRuntimeContext,
1487
+ run: KxmRunRecord,
1488
+ executing: { stepId: string; assignmentId: string; attemptId: string },
1489
+ ): KxmRunState {
1490
+ const now = new Date().toISOString();
1491
+ let sequence = context.eventStore.nextSequence(run.runId);
1492
+ let mono = kxmMonotonicNs();
1493
+ const events: KxmRunEvent[] = [];
1494
+ const push = (eventType: string, payload: Record<string, unknown>): void => {
1495
+ events.push({
1496
+ ...kxmEventBase(context, run, now, mono),
1497
+ eventId: newKxmEventId(),
1498
+ eventType,
1499
+ sequence: sequence++,
1500
+ payload,
1501
+ });
1502
+ mono = kxmIncrementMonotonicNs(mono);
1503
+ };
1504
+ push("attempt.status_changed", { attemptId: executing.attemptId, status: "settling" });
1505
+ push("assignment.result_recorded", {
1506
+ assignmentId: executing.assignmentId,
1507
+ resultClass: "producer_rejected",
1508
+ status: "result_recorded",
1509
+ producerError: "executing_unrecorded",
1510
+ reason: "executing_unrecorded",
1511
+ });
1512
+ push("attempt.status_changed", { attemptId: executing.attemptId, status: "terminal" });
1513
+ push("assignment.terminal", { assignmentId: executing.assignmentId, outcome: "failed", status: "terminal" });
1514
+ push("step.status_changed", { stepId: executing.stepId, status: "cancelled", previousStatus: "running" });
1515
+ push("run.status_changed", { status: "cancelled", reason: cancelReasonFromLog(context, run.runId) });
1516
+ for (const event of events) context.eventStore.appendEvent(event);
1517
+ context.eventStore.settleCapability(executing.attemptId, "settled");
1518
+ const next = foldStoredKxmRun(context, run);
1519
+ persistKxmRunState(context, run.runId, next, events[events.length - 1]!.sequence);
1520
+ context.eventStore.updateRunStatus(run.runId, "cancelled", now);
1521
+ return next;
1522
+ }
1523
+
1444
1524
  function prepareDispatch(
1445
1525
  context: KxmRuntimeContext,
1446
1526
  runId: string,
@@ -1454,6 +1534,23 @@ function prepareDispatch(
1454
1534
  return { kind: "return", state };
1455
1535
  }
1456
1536
  if (state.status === "cancelling" && kxmAttemptControllers(context.eventStore.path, runId).length === 0) {
1537
+ const executing = executingSingletonAttempt(plan, state);
1538
+ if (executing) {
1539
+ const exited = readOneShotObservedChildExit(runId, executing.attemptId);
1540
+ if (exited === true) {
1541
+ return { kind: "return", state: settleExitedExecutingAttempt(context, run, executing) };
1542
+ }
1543
+ return {
1544
+ kind: "return",
1545
+ state,
1546
+ handoff: {
1547
+ reason: "attempt_unreconciled",
1548
+ stepId: executing.stepId,
1549
+ attemptId: executing.attemptId,
1550
+ detail: "issued attempt is not held by this process",
1551
+ },
1552
+ };
1553
+ }
1457
1554
  return {
1458
1555
  kind: "return",
1459
1556
  state,
@@ -1585,6 +1682,8 @@ function prepareDispatch(
1585
1682
 
1586
1683
  const unsupported = unsupportedStep(plan, step, producerId);
1587
1684
  if (unsupported) return { kind: "return", state, handoff: { ...unsupported, stepId } };
1685
+ const wideTimeout = unsupportedAgentStepTimeout(step, projectAgentStepTimeoutMs(context, run));
1686
+ if (wideTimeout) return { kind: "return", state, handoff: { ...wideTimeout, stepId } };
1588
1687
 
1589
1688
  let resolvedRoute: { provider: string; model: string; selector: string } | undefined;
1590
1689
  if (producerId !== "driver-simulated") {
@@ -1838,6 +1937,9 @@ function birthMember(
1838
1937
  prompt: input.step.instructions ? `${input.step.instructions}\n\n${generatedPrompt}` : generatedPrompt,
1839
1938
  thinking: input.stepAttempt <= 1 ? "low" : "medium",
1840
1939
  permission: Object.values(input.step.repositories).some((access) => access === "write") ? "edit" : "read-only",
1940
+ ...((input.step.kind === "agent" || input.step.kind === "moa") && input.step.timeoutMs !== undefined
1941
+ ? { timeoutMs: input.step.timeoutMs }
1942
+ : {}),
1841
1943
  contextPacket,
1842
1944
  ...(resolvedRoute ? { provider: resolvedRoute.provider, model: resolvedRoute.model } : {}),
1843
1945
  },
@@ -2716,9 +2818,10 @@ export function kxmLiveRunPrerequisites(
2716
2818
  for (const transition of Object.values(step.transitions)) {
2717
2819
  if (transition.to === "step") pending.push(transition.target);
2718
2820
  }
2821
+ const limitMs = envelope.projectLimits.agentStepTimeoutMs ?? KXM_DEFAULT_AGENT_STEP_TIMEOUT_MS;
2719
2822
  const unsupported = step.kind === "gate"
2720
2823
  ? unsupportedGateStep(plan, step, envelope, { projectRoot }) ?? starterGatePrerequisite(step, envelope.gates, projectRoot)
2721
- : unsupportedStep(plan, step, "oneshot");
2824
+ : unsupportedAgentStepTimeout(step, limitMs) ?? unsupportedStep(plan, step, "oneshot");
2722
2825
  if (unsupported) {
2723
2826
  prerequisites.push({ ...unsupported, stepId });
2724
2827
  continue;
@@ -11,7 +11,7 @@ import { deliverInboxNotification } from "./inbox.ts";
11
11
  import type { HubEvent, MessageRecord } from "./protocol.ts";
12
12
  import { sessionTokenFixHint } from "./session-token-hint.ts";
13
13
 
14
- const VERSION = "0.7.123";
14
+ const VERSION = "0.7.125";
15
15
  const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
16
16
  const inbox = new Map<string, MessageRecord>();
17
17
  const notifiedInbox = new Set<string>();
@@ -1,6 +1,8 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
+ import { lstatSync, readdirSync, readFileSync } from "node:fs";
2
3
  import { lstat, mkdir, open, rename } from "node:fs/promises";
3
4
  import { join } from "node:path";
5
+ import { kxmUserStateRoot } from "./bindings.ts";
4
6
  import type { KxmOneShotProcessResult } from "./oneshot-process.ts";
5
7
 
6
8
  export const ONESHOT_EVIDENCE_SCHEMA = "kxm.oneshot-evidence.v2" as const;
@@ -34,6 +36,46 @@ export function parseOneShotEvidenceRecord(bytes: string): OneShotEvidenceRecord
34
36
  return parsed as OneShotEvidenceRecord;
35
37
  }
36
38
 
39
+ /** Where live one-shot spawns write `intent.json` and `result.json`. */
40
+ export function oneShotEvidenceRoot(): string {
41
+ return join(kxmUserStateRoot(), "runtime", "oneshot-evidence");
42
+ }
43
+
44
+ /**
45
+ * Whether the one-shot evidence for this attempt says the direct child exited.
46
+ * Missing, unreadable, or unfinished evidence is `undefined` (do not settle).
47
+ * A recorded `false` stays `false`.
48
+ */
49
+ export function readOneShotObservedChildExit(runId: string, attemptId: string, root = oneShotEvidenceRoot()): boolean | undefined {
50
+ let names: string[];
51
+ try {
52
+ names = readdirSync(root);
53
+ } catch {
54
+ return undefined;
55
+ }
56
+ let seenTrue = false;
57
+ let seenFalse = false;
58
+ const capped = names.slice(0, 4096);
59
+ for (const name of capped) {
60
+ if (!name.startsWith("evd_")) continue;
61
+ const dir = join(root, name);
62
+ try {
63
+ const info = lstatSync(dir);
64
+ if (!info.isDirectory() || info.isSymbolicLink()) continue;
65
+ const intent = parseOneShotEvidenceRecord(readFileSync(join(dir, "intent.json"), "utf8"));
66
+ if (intent.runId !== runId || intent.attemptId !== attemptId) continue;
67
+ const result = parseOneShotEvidenceRecord(readFileSync(join(dir, "result.json"), "utf8"));
68
+ if (result.observedChildExit === true) seenTrue = true;
69
+ else if (result.observedChildExit === false) seenFalse = true;
70
+ } catch {
71
+ // An unreadable or unfinished record is not evidence that the child exited.
72
+ }
73
+ }
74
+ if (seenFalse) return false;
75
+ if (seenTrue) return true;
76
+ return undefined;
77
+ }
78
+
37
79
  /** Private diagnostic evidence, never a peer reply or acceptance authority.
38
80
  * Retained until operator archival; no automatic deletion of failed attempts.
39
81
  * Each record is bounded; inability to reserve/write evidence fails closed.
@@ -42,6 +84,7 @@ export async function beginOneShotEvidence(root: string, intent: {
42
84
  runId: string; stepId: string; attemptId: string; assignmentId: string;
43
85
  harness: string; provider: string; model: string; cwd: string;
44
86
  command: string; args: readonly string[]; input?: string | undefined;
87
+ timeoutMs?: number | undefined;
45
88
  }, sensitive: readonly string[]): Promise<{
46
89
  id: string;
47
90
  finish(result: KxmOneShotProcessResult, observation: unknown): Promise<string>;
@@ -92,6 +135,7 @@ export async function beginOneShotEvidence(root: string, intent: {
92
135
  schema: ONESHOT_EVIDENCE_SCHEMA, id, recordedAt: new Date().toISOString(),
93
136
  runId: intent.runId, stepId: intent.stepId, attemptId: intent.attemptId, assignmentId: intent.assignmentId,
94
137
  harness: intent.harness, provider: intent.provider, requestedModel: intent.model,
138
+ ...(intent.timeoutMs !== undefined ? { timeoutMs: intent.timeoutMs } : {}),
95
139
  cwd: intent.cwd, command: redact(intent.command),
96
140
  argv: bounded(JSON.stringify(intent.args), ARGV_LIMIT), stdin: intent.input === undefined ? null : bounded(intent.input),
97
141
  env: null,
@@ -1,6 +1,4 @@
1
- import { join } from "node:path";
2
- import { beginOneShotEvidence } from "./oneshot-evidence.ts";
3
- import { kxmUserStateRoot } from "./bindings.ts";
1
+ import { beginOneShotEvidence, oneShotEvidenceRoot } from "./oneshot-evidence.ts";
4
2
  import { defaultSpawn, type KxmOneShotProcessResult, type KxmOneShotSpawn } from "./oneshot-process.ts";
5
3
  export { defaultSpawn, type KxmOneShotProcessResult, type KxmOneShotSpawn } from "./oneshot-process.ts";
6
4
  import {
@@ -260,15 +258,17 @@ export function createKxmOneShotProducer(options: KxmOneShotProducerOptions = {}
260
258
 
261
259
  const spawnFn = options.spawnProcess ?? defaultSpawn;
262
260
  const cwd = options.projectRoot ?? process.cwd();
263
- const evidence = await beginOneShotEvidence(options.evidenceRoot ?? join(kxmUserStateRoot(), "runtime", "oneshot-evidence"), {
261
+ const timeoutMs = request.timeoutMs ?? options.timeoutMs;
262
+ const evidence = await beginOneShotEvidence(options.evidenceRoot ?? oneShotEvidenceRoot(), {
264
263
  runId: request.runId, stepId: request.stepId, attemptId: request.attemptId, assignmentId: request.assignmentId,
265
264
  harness, provider: resolved.provider, model: resolved.model, cwd, command, args, input,
265
+ timeoutMs: timeoutMs ?? 120_000,
266
266
  }, [request.capability, ...Object.entries(env).filter(([key]) => /TOKEN|KEY|SECRET|PASSWORD|COOKIE|AUTH/i.test(key)).map(([, value]) => value ?? "")]);
267
267
  let procResult: KxmOneShotProcessResult;
268
268
  try {
269
269
  procResult = request.signal.aborted
270
270
  ? { stdout: "", stderr: "", code: null, started: false, observedChildExit: false, error: new Error("process_aborted") }
271
- : await spawnFn(command, args, { cwd, env, input, timeoutMs: options.timeoutMs ?? 120_000, signal: request.signal });
271
+ : await spawnFn(command, args, { cwd, env, input, timeoutMs, signal: request.signal });
272
272
  } catch (error) {
273
273
  // A rejected adapter promise is not proof that no process/effect started.
274
274
  procResult = { stdout: "", stderr: "", code: null, observedChildExit: false, error: error instanceof Error ? error : new Error("process_adapter_error") };
@@ -195,19 +195,29 @@ export function kxmDeclaredExecutorIds(bundle: KxmProjectBundle): string[] {
195
195
  ].filter((value): value is string => value !== undefined))].sort();
196
196
  }
197
197
 
198
+ /** Wall-clock bound for one live agent or moa step when the step omits `timeoutMs`. */
199
+ export const KXM_DEFAULT_AGENT_STEP_TIMEOUT_MS = 3_600_000;
200
+ export const KXM_MIN_AGENT_STEP_TIMEOUT_MS = 60_000;
201
+
198
202
  export function kxmProjectAdmissionLimits(bundle: KxmProjectBundle): {
199
203
  maxConcurrentRuns: number;
200
204
  maxRunDurationMs?: number;
201
205
  maxAgentTimeMs?: number;
206
+ agentStepTimeoutMs: number;
202
207
  } {
203
208
  const limits = objectValue(bundle.project.value.limits);
204
209
  const maxConcurrentRuns = typeof limits?.maxConcurrentRuns === "number" && Number.isInteger(limits.maxConcurrentRuns) && limits.maxConcurrentRuns >= 1
205
210
  ? limits.maxConcurrentRuns
206
211
  : 1;
212
+ const declared = limits?.agentStepTimeoutMs;
213
+ const agentStepTimeoutMs = typeof declared === "number" && Number.isInteger(declared) && declared >= KXM_MIN_AGENT_STEP_TIMEOUT_MS
214
+ ? declared
215
+ : KXM_DEFAULT_AGENT_STEP_TIMEOUT_MS;
207
216
  return {
208
217
  maxConcurrentRuns,
209
218
  ...(typeof limits?.maxRunDurationMs === "number" ? { maxRunDurationMs: limits.maxRunDurationMs } : {}),
210
219
  ...(typeof limits?.maxAgentTimeMs === "number" ? { maxAgentTimeMs: limits.maxAgentTimeMs } : {}),
220
+ agentStepTimeoutMs,
211
221
  };
212
222
  }
213
223
 
@@ -371,6 +371,7 @@ export interface KxmDriveReceiptHandoff {
371
371
  reason: string;
372
372
  field?: string;
373
373
  stepId?: string;
374
+ attemptId?: string;
374
375
  detail: string;
375
376
  }
376
377
 
@@ -23,6 +23,7 @@ import {
23
23
  closeKxmRuntimeContext,
24
24
  foldStoredKxmRun,
25
25
  openKxmRuntimeContext,
26
+ kxmProjectAdmissionLimits,
26
27
  projectKxmRunReadOnly,
27
28
  type KxmRuntimeContext,
28
29
  } from "./runtime-service.ts";
@@ -836,6 +837,7 @@ async function startKxmRuntimeSupervisorInner(
836
837
  const createProducer = () => body.mode === "live"
837
838
  ? createKxmOneShotProducer({
838
839
  projectRoot,
840
+ timeoutMs: kxmProjectAdmissionLimits(bundle).agentStepTimeoutMs,
839
841
  defaultHarness: String(bundle.project.value.defaultHarness ?? "pi"),
840
842
  resolveHarness: (agentId) => {
841
843
  const agent = bundle.agents.get(agentId);
@@ -87,6 +87,7 @@
87
87
  "reason": { "type": "string", "minLength": 1, "maxLength": 80 },
88
88
  "field": { "type": "string", "minLength": 1, "maxLength": 128 },
89
89
  "stepId": { "$ref": "common.schema.json#/$defs/identifier" },
90
+ "attemptId": { "$ref": "common.schema.json#/$defs/opaqueId" },
90
91
  "detail": { "type": "string", "minLength": 1, "maxLength": 4000 }
91
92
  },
92
93
  "additionalProperties": false
@@ -76,7 +76,8 @@
76
76
  "properties": {
77
77
  "maxConcurrentRuns": { "type": "integer", "minimum": 1, "maximum": 128 },
78
78
  "maxRunDurationMs": { "$ref": "common.schema.json#/$defs/durationMs" },
79
- "maxAgentTimeMs": { "$ref": "common.schema.json#/$defs/durationMs" }
79
+ "maxAgentTimeMs": { "$ref": "common.schema.json#/$defs/durationMs" },
80
+ "agentStepTimeoutMs": { "type": "integer", "minimum": 60000, "maximum": 31536000000 }
80
81
  },
81
82
  "additionalProperties": false
82
83
  }