@kontextmind/kxm 0.7.123 → 0.7.124
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/.claude-plugin/marketplace.json +1 -1
- package/.kxm/workflows/implement-only.yaml +20 -0
- package/.kxm/workflows/review-arch-only.yaml +20 -0
- package/.kxm/workflows/review-cli-only.yaml +20 -0
- package/CHANGELOG.md +20 -3
- package/docs/contributing/assignment-runner.md +15 -4
- package/docs/reference/cli-reference.md +1 -1
- package/docs/reference/config-reference.md +2 -1
- package/docs/reference/workflow-catalog.md +10 -0
- package/docs/reference/workflow-definitions.md +1 -1
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +24 -5
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +209 -72
- package/plugins/kxm/dist/runtime.js +233 -94
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/cli/project.ts +14 -2
- package/plugins/kxm/src/engine-fold.ts +15 -7
- package/plugins/kxm/src/engine-plan.ts +10 -1
- package/plugins/kxm/src/engine.ts +105 -2
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/oneshot-evidence.ts +44 -0
- package/plugins/kxm/src/oneshot-producer.ts +5 -5
- package/plugins/kxm/src/runtime-service.ts +10 -0
- package/plugins/kxm/src/runtime-store.ts +1 -0
- package/plugins/kxm/src/runtime-supervisor.ts +2 -0
- package/schemas/drive-receipt.schema.json +1 -0
- package/schemas/project.schema.json +2 -1
package/plugins/kxm/package.json
CHANGED
|
@@ -522,7 +522,12 @@ interface KxmStatusDrive {
|
|
|
522
522
|
mode?: string;
|
|
523
523
|
openedAt?: string;
|
|
524
524
|
receipt?: {
|
|
525
|
-
settlement?: {
|
|
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
|
|
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
|
|
|
@@ -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
|
|
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: {
|
|
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)
|
|
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.
|
|
14
|
+
const VERSION = "0.7.124";
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
|
@@ -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
|
}
|