@martinloop/mcp 0.8.10 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/server-validation.js +2 -3
- package/dist/server.d.ts +1 -0
- package/dist/server.js +181 -24
- package/dist/tools/get-status.d.ts +3 -0
- package/dist/tools/get-status.js +11 -2
- package/dist/tools/logs.d.ts +2 -0
- package/dist/tools/logs.js +25 -3
- package/dist/tools/run-controls.d.ts +51 -6
- package/dist/tools/run-controls.js +905 -35
- package/dist/tools/run-loop.js +7 -0
- package/dist/vendor/contracts/index.d.ts +8 -0
- package/dist/vendor/contracts/index.js +1 -0
- package/dist/vendor/contracts/swarm-timing.d.ts +7 -0
- package/dist/vendor/contracts/swarm-timing.js +22 -0
- package/dist/vendor/core/index.d.ts +35 -2
- package/dist/vendor/core/index.js +119 -14
- package/dist/vendor/core/persistence/index.d.ts +1 -1
- package/dist/vendor/core/persistence/index.js +1 -1
- package/dist/vendor/core/persistence/integrity.d.ts +1 -0
- package/dist/vendor/core/persistence/integrity.js +1 -1
- package/dist/vendor/core/resume.d.ts +25 -1
- package/dist/vendor/core/resume.js +187 -13
- package/dist/vendor/core/rollback.d.ts +14 -0
- package/dist/vendor/core/rollback.js +91 -34
- package/dist/vendor/core/swarm/live-runtime.js +2 -0
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/tools/run-loop.js
CHANGED
|
@@ -4,6 +4,7 @@ import { normalizeSafePathPatterns, resolveSafeRepoRoot } from "../server-valida
|
|
|
4
4
|
import { MartinToolError } from "./tool-errors.js";
|
|
5
5
|
import { buildArtifactSummary, buildVerificationSummary, buildLoopPreview, buildRunRecordPaths, getEngineAvailability, resolveExecutionMode } from "./tool-support.js";
|
|
6
6
|
import { normalizeLoopBudget } from "./workflow-governance.js";
|
|
7
|
+
import { createFileRunControlSource } from "./run-controls.js";
|
|
7
8
|
let proofModeVerifierSpawnImpl;
|
|
8
9
|
let runStoreOverrideForTests;
|
|
9
10
|
export function __setProofModeVerifierSpawnImplForTests(spawnImpl) {
|
|
@@ -210,6 +211,12 @@ export async function runLoopTool(input) {
|
|
|
210
211
|
workspaceId: input.workspaceId ?? "ws_mcp",
|
|
211
212
|
projectId: input.projectId ?? "proj_mcp",
|
|
212
213
|
store: runStoreOverrideForTests ?? createFileRunStore({ runsRoot }),
|
|
214
|
+
// The in-memory store override is a verification-only composition with no
|
|
215
|
+
// durable run directory to serialize. Production always couples the file
|
|
216
|
+
// store and file control source over the same runs root.
|
|
217
|
+
runControlSource: runStoreOverrideForTests
|
|
218
|
+
? undefined
|
|
219
|
+
: createFileRunControlSource(runsRoot),
|
|
213
220
|
receiptScope,
|
|
214
221
|
task: {
|
|
215
222
|
title: input.objective.slice(0, 100),
|
|
@@ -497,6 +497,12 @@ export interface RollbackBoundaryArtifact {
|
|
|
497
497
|
trackedDirtyFiles: string[];
|
|
498
498
|
untrackedFiles: string[];
|
|
499
499
|
snapshots: RollbackFileSnapshot[];
|
|
500
|
+
/**
|
|
501
|
+
* Exact repo-relative roots owned by MartinLoop persistence for this run.
|
|
502
|
+
* These roots are not agent workspace mutations and must remain excluded
|
|
503
|
+
* from observation and rollback after the boundary is persisted/resumed.
|
|
504
|
+
*/
|
|
505
|
+
managedPathExclusions?: string[];
|
|
500
506
|
/**
|
|
501
507
|
* Git-ignored files present at the boundary. Dependency and build output
|
|
502
508
|
* directories are recorded as a single collapsed `dir/` entry.
|
|
@@ -557,4 +563,6 @@ export type { ChainIntegrityState, ContextCircuitBreakResult, ContextExclusionDe
|
|
|
557
563
|
export { MISSION_SCHEMA_VERSION, MISSION_STATUSES, ALLOWED_MISSION_TRANSITIONS, createMissionRecord, isMissionTransitionAllowed } from './mission.js';
|
|
558
564
|
export type { MissionStatus, MissionDecision, MissionBudget, MissionCost, MissionRunLink, MissionRunRole, MissionApproval, MissionOutcome, MissionEvent, MissionEventKind, MissionRecord, MissionDraft } from './mission.js';
|
|
559
565
|
export { SWARM_LIVE_ENGINES, SWARM_ORCHESTRATION_STRATEGIES, SWARM_EVENT_TYPES, SWARM_PARENT_RECEIPT_SCHEMA_VERSION, SWARM_SCHEMA_VERSION, computeSwarmLivePlanHash, createSwarmLivePlan, createSwarmRunRecord, normalizeSwarmPathPattern, swarmConcretePathMatchesPattern, swarmPathPatternContains, swarmPathPatternsOverlap, validateSwarmConcretePath, validateSwarmChildReceiptLink, validateSwarmDeterministicDemoReceiptEvidence, validateSwarmChildContract, validateSwarmLiveEvent, validateSwarmLivePlan, validateSwarmLiveRevision, validateSwarmParentContract, validateSwarmRunRecord } from "./swarm.js";
|
|
566
|
+
export { SWARM_CHILD_MAX_WALL_CLOCK_MS, SWARM_PARENT_VERIFIER_ALLOWANCE_MS, resolveSwarmDeadlineMs, } from "./swarm-timing.js";
|
|
567
|
+
export type { SwarmExecutionShape } from "./swarm-timing.js";
|
|
560
568
|
export type { SwarmAgentRecord, SwarmAgentStatus, SwarmBudgetLedger, SwarmBudgetLease, SwarmBudgetUsage, SwarmCandidate, SwarmChildContract, SwarmChildReceiptLink, SwarmChildWorkspaceRecord, SwarmCleanupRecord, SwarmCleanupState, SwarmConcretePathResult, SwarmConflictRecord, SwarmDependencyWaiver, SwarmDemoReferentialBindingState, SwarmDeterministicDemoReceiptEvidence, SwarmEvent, SwarmEventType, SwarmGlobalVerification, SwarmGlobalVerificationCommandState, SwarmIntegrationWorkspaceRecord, SwarmIntegrationStrategy, SwarmLeaseStatus, SwarmLiveEngine, SwarmLiveEngineProfile, SwarmLiveEvent, SwarmLivePlan, SwarmLivePlanDraft, SwarmLiveRevision, SwarmOrchestrationStrategy, SwarmOutcome, SwarmOutcomeState, SwarmParentContract, SwarmParentReceipt, SwarmPatchAdmission, SwarmPatchAdmissionReasonCode, SwarmPatchAdmissionState, SwarmPathPatternResult, SwarmPermissions, SwarmProcessCloseState, SwarmRecoveryPolicy, SwarmReceiptBlockedAction, SwarmReceiptReassignment, SwarmRunDraft, SwarmRunRecord, SwarmScope, SwarmStopDisposition, SwarmStopPolicy, SwarmTaskNode, SwarmTaskStatus, SwarmValidationError, SwarmValidationResult, SwarmVerifierMutationState, SwarmVerifierSubprocessResult, SwarmVerificationRecord, SwarmVerifierAuthority, SwarmVerifierWorkspaceRecord, SwarmWorkspaceKind, SwarmWorkspaceRecord, SwarmWorkspaceRemovalState, SwarmWorkspaceState } from "./swarm.js";
|
|
@@ -283,3 +283,4 @@ export { EVIDENCE_STATUSES, EXECUTION_MODES, TEST_INTEGRITY_STATUSES, TEST_INTEG
|
|
|
283
283
|
export { HANDOFF_SCHEMA_VERSION } from "./context-handoff.js";
|
|
284
284
|
export { MISSION_SCHEMA_VERSION, MISSION_STATUSES, ALLOWED_MISSION_TRANSITIONS, createMissionRecord, isMissionTransitionAllowed } from './mission.js';
|
|
285
285
|
export { SWARM_LIVE_ENGINES, SWARM_ORCHESTRATION_STRATEGIES, SWARM_EVENT_TYPES, SWARM_PARENT_RECEIPT_SCHEMA_VERSION, SWARM_SCHEMA_VERSION, computeSwarmLivePlanHash, createSwarmLivePlan, createSwarmRunRecord, normalizeSwarmPathPattern, swarmConcretePathMatchesPattern, swarmPathPatternContains, swarmPathPatternsOverlap, validateSwarmConcretePath, validateSwarmChildReceiptLink, validateSwarmDeterministicDemoReceiptEvidence, validateSwarmChildContract, validateSwarmLiveEvent, validateSwarmLivePlan, validateSwarmLiveRevision, validateSwarmParentContract, validateSwarmRunRecord } from "./swarm.js";
|
|
286
|
+
export { SWARM_CHILD_MAX_WALL_CLOCK_MS, SWARM_PARENT_VERIFIER_ALLOWANCE_MS, resolveSwarmDeadlineMs, } from "./swarm-timing.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const SWARM_CHILD_MAX_WALL_CLOCK_MS = 240000;
|
|
2
|
+
export declare const SWARM_PARENT_VERIFIER_ALLOWANCE_MS = 120000;
|
|
3
|
+
export interface SwarmExecutionShape {
|
|
4
|
+
agents: number;
|
|
5
|
+
maxConcurrency: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function resolveSwarmDeadlineMs(shape: SwarmExecutionShape): number;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const SWARM_CHILD_MAX_WALL_CLOCK_MS = 240_000;
|
|
2
|
+
export const SWARM_PARENT_VERIFIER_ALLOWANCE_MS = 120_000;
|
|
3
|
+
const NODE_MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
4
|
+
export function resolveSwarmDeadlineMs(shape) {
|
|
5
|
+
assertPositiveSafeInteger(shape.agents, "agents");
|
|
6
|
+
assertPositiveSafeInteger(shape.maxConcurrency, "maxConcurrency");
|
|
7
|
+
if (shape.maxConcurrency > shape.agents) {
|
|
8
|
+
throw new Error("maxConcurrency cannot exceed agents");
|
|
9
|
+
}
|
|
10
|
+
const executionWaves = Math.ceil(shape.agents / shape.maxConcurrency);
|
|
11
|
+
const deadlineMs = (executionWaves * SWARM_CHILD_MAX_WALL_CLOCK_MS
|
|
12
|
+
+ SWARM_PARENT_VERIFIER_ALLOWANCE_MS);
|
|
13
|
+
if (!Number.isSafeInteger(deadlineMs) || deadlineMs > NODE_MAX_TIMER_DELAY_MS) {
|
|
14
|
+
throw new Error("derived deadline exceeds the Node timer maximum");
|
|
15
|
+
}
|
|
16
|
+
return deadlineMs;
|
|
17
|
+
}
|
|
18
|
+
function assertPositiveSafeInteger(value, label) {
|
|
19
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
20
|
+
throw new Error(`${label} must be a positive safe integer`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -7,7 +7,7 @@ import { type RunStore } from "./persistence/index.js";
|
|
|
7
7
|
import { type ExitPolicyOverrides } from "./exits.js";
|
|
8
8
|
import { type ExitSignalSource } from "./exit-signal.js";
|
|
9
9
|
import { type VerifierExecutionBinding } from "./verified-handoff.js";
|
|
10
|
-
import { acquireRunExecutionLease, evaluateRunResumability, loadResumeCandidateBoundary, loadResumeVerifierFailure } from "./resume.js";
|
|
10
|
+
import { acquireRunExecutionLease, readActiveRunExecutionLease, evaluateRunResumability, loadResumeCandidateBoundary, loadResumeVerifierFailure } from "./resume.js";
|
|
11
11
|
export type { AgentExecutionIntent, ApprovalPolicy, BudgetPreflightEstimate, BudgetSettlement, CostProvenance, EvidenceVector, ExecutionProfile, FailureClass, InterventionType, PatchDecision, PatchDecisionArtifact, PatchDecisionReasonCode, PatchScore, MutationMode, RollbackBoundaryArtifact, RollbackBoundaryStrategy, RollbackFileSnapshot, RollbackOutcomeArtifact, RollbackOutcomeStatus, PolicyPhase, CallStage, AgentRole, FirstDelta, RoutingEconomics } from "../contracts/index.js";
|
|
12
12
|
export { DEFAULT_AGENT_EXECUTION_INTENT, DEFAULT_PROVIDER_EXECUTION_TIMEOUT_MS, GOVERNED_AUTONOMOUS_BOUNDARY, normalizeProviderExecutionTimeoutMs } from "../contracts/index.js";
|
|
13
13
|
export { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe, evaluateVerificationLeash, evaluateFilesystemLeash, evaluateChangeApprovalLeash, evaluateSecretLeash, resolveExecutionProfile, redactSecretsFromText, buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations, captureRollbackBoundary, listAttemptChangedFilesSinceBoundary, listIgnoredFileChangesSinceBoundary, restoreRollbackBoundary };
|
|
@@ -23,7 +23,7 @@ export type { LocalIdentityAuthority } from "./identity.js";
|
|
|
23
23
|
export { compileExecutionPolicy } from "./policy-compiler.js";
|
|
24
24
|
export { assessTrajectory, decideCircuitBreak } from "./trajectory.js";
|
|
25
25
|
export { calculateAvoidedUsd, calculateLoopAvoidedUsd } from "./savings.js";
|
|
26
|
-
export { acquireRunExecutionLease, evaluateRunResumability, loadResumeCandidateBoundary, loadResumeVerifierFailure };
|
|
26
|
+
export { acquireRunExecutionLease, readActiveRunExecutionLease, evaluateRunResumability, loadResumeCandidateBoundary, loadResumeVerifierFailure };
|
|
27
27
|
export type { AvoidedUsdInput } from "./savings.js";
|
|
28
28
|
export { classifyRoute, evaluatePreworkBurnPolicy } from "./routing.js";
|
|
29
29
|
export type { RouteDecision, RouteClassificationInput } from "./routing.js";
|
|
@@ -293,6 +293,39 @@ export interface RunMartinInput {
|
|
|
293
293
|
observedUsageGovernor?: MartinObservedUsageGovernor;
|
|
294
294
|
/** Optional parent cancellation signal. Relayed into the active governed attempt. */
|
|
295
295
|
signal?: AbortSignal;
|
|
296
|
+
/** Optional durable cooperative pause/continue source. */
|
|
297
|
+
runControlSource?: RunControlSource;
|
|
298
|
+
/** Poll interval while a cooperative pause is applied (default 250 ms). */
|
|
299
|
+
runControlPollIntervalMs?: number;
|
|
300
|
+
}
|
|
301
|
+
export interface RunControlCommand {
|
|
302
|
+
controlId: string;
|
|
303
|
+
loopId: string;
|
|
304
|
+
action: "pause" | "continue";
|
|
305
|
+
requestedAt: string;
|
|
306
|
+
/** Explicit local-process boundary; not an authenticated human identity. */
|
|
307
|
+
principal: "local_mcp_client";
|
|
308
|
+
receiptHash: string;
|
|
309
|
+
reason?: string;
|
|
310
|
+
expectedPreviousControlId?: string;
|
|
311
|
+
}
|
|
312
|
+
export interface RunControlSnapshot {
|
|
313
|
+
pause?: RunControlCommand;
|
|
314
|
+
continuedBy?: RunControlCommand;
|
|
315
|
+
}
|
|
316
|
+
export interface RunControlApplication {
|
|
317
|
+
loopId: string;
|
|
318
|
+
controlId: string;
|
|
319
|
+
action: "pause_applied" | "continue_applied";
|
|
320
|
+
appliedAt: string;
|
|
321
|
+
relatedControlId?: string;
|
|
322
|
+
receiptHash: string;
|
|
323
|
+
}
|
|
324
|
+
export interface RunControlSource {
|
|
325
|
+
poll(runId: string): Promise<RunControlSnapshot>;
|
|
326
|
+
recordApplication(application: RunControlApplication): Promise<void>;
|
|
327
|
+
/** Serialize terminal intent against new control receipts before persistence. */
|
|
328
|
+
recordTerminalIntent?(runId: string): Promise<void>;
|
|
296
329
|
}
|
|
297
330
|
export interface RunMartinResult {
|
|
298
331
|
loop: LoopRecord;
|
|
@@ -2,10 +2,10 @@ import { DEFAULT_AGENT_EXECUTION_INTENT, normalizeProviderExecutionTimeoutMs, ap
|
|
|
2
2
|
import { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, isSafeUnverifiedDiscard, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe } from "./policy.js";
|
|
3
3
|
import { evaluateChangeApprovalLeash, evaluateFilesystemLeash, evaluateSecretLeash, redactSecretsFromText, resolveExecutionProfile, evaluateVerificationLeash } from "./leash.js";
|
|
4
4
|
import { buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations } from "./grounding.js";
|
|
5
|
-
import { captureRollbackBoundary, findEscapingLinks, listAttemptChangedFilesSinceBoundary, listIgnoredFileChangesSinceBoundary, restoreRollbackBoundary } from "./rollback.js";
|
|
5
|
+
import { authenticateManagedPathExclusions, captureRollbackBoundary, filterManagedRollbackPaths, findEscapingLinks, listAttemptChangedFilesSinceBoundary, listIgnoredFileChangesSinceBoundary, restoreRollbackBoundary } from "./rollback.js";
|
|
6
6
|
import { findWeakenedTestFiles } from "./test-integrity.js";
|
|
7
7
|
import { compilePromptPacket } from "./compiler.js";
|
|
8
|
-
import { makeLedgerEvent, resolveRunsRoot, runDir } from "./persistence/index.js";
|
|
8
|
+
import { makeLedgerEvent, resolveReceiptIntegrityRoot, resolveRunsRoot, runDir } from "./persistence/index.js";
|
|
9
9
|
import { createDefaultExitPolicy, evaluateExitPolicy, hashProgressState, toLegacyExitDecision } from "./exits.js";
|
|
10
10
|
import { createFileExitSignalSource, isTerminalExitSignal, startExitSignalMonitor } from "./exit-signal.js";
|
|
11
11
|
import { persistTerminationEnvelope } from "./termination-store.js";
|
|
@@ -14,7 +14,7 @@ import { runContextIntegrityPrecheck } from "./context-integrity.js";
|
|
|
14
14
|
import { decideCircuitBreak } from "./trajectory.js";
|
|
15
15
|
import { verifyContextHandoff, decideContextCircuitBreak } from "./context-handoff.js";
|
|
16
16
|
import { verifierActuallyPassed, } from "./verified-handoff.js";
|
|
17
|
-
import { acquireRunExecutionLease, evaluateRunResumability, loadResumeCandidateBoundary, loadResumeVerifierFailure } from "./resume.js";
|
|
17
|
+
import { acquireRunExecutionLease, readActiveRunExecutionLease, evaluateRunResumability, loadResumeCandidateBoundary, loadResumeVerifierFailure } from "./resume.js";
|
|
18
18
|
export { DEFAULT_AGENT_EXECUTION_INTENT, DEFAULT_PROVIDER_EXECUTION_TIMEOUT_MS, GOVERNED_AUTONOMOUS_BOUNDARY, normalizeProviderExecutionTimeoutMs } from "../contracts/index.js";
|
|
19
19
|
export { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe, evaluateVerificationLeash, evaluateFilesystemLeash, evaluateChangeApprovalLeash, evaluateSecretLeash, resolveExecutionProfile, redactSecretsFromText, buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations, captureRollbackBoundary, listAttemptChangedFilesSinceBoundary, listIgnoredFileChangesSinceBoundary, restoreRollbackBoundary };
|
|
20
20
|
export { describeMalformedVerifier } from "./verifier-input.js";
|
|
@@ -24,7 +24,7 @@ export { createLocalIdentityAuthority, hasIdentityScope, issueIdentityToken, ver
|
|
|
24
24
|
export { compileExecutionPolicy } from "./policy-compiler.js";
|
|
25
25
|
export { assessTrajectory, decideCircuitBreak } from "./trajectory.js";
|
|
26
26
|
export { calculateAvoidedUsd, calculateLoopAvoidedUsd } from "./savings.js";
|
|
27
|
-
export { acquireRunExecutionLease, evaluateRunResumability, loadResumeCandidateBoundary, loadResumeVerifierFailure };
|
|
27
|
+
export { acquireRunExecutionLease, readActiveRunExecutionLease, evaluateRunResumability, loadResumeCandidateBoundary, loadResumeVerifierFailure };
|
|
28
28
|
// ─── Routing economics ─────────────────────────────────────────────────────
|
|
29
29
|
export { classifyRoute, evaluatePreworkBurnPolicy } from "./routing.js";
|
|
30
30
|
// ─── Test Integrity Validation ──────────────────────────────────────────────
|
|
@@ -217,6 +217,7 @@ export async function runMartin(input) {
|
|
|
217
217
|
runsRoot: input.store.runsRoot,
|
|
218
218
|
runId: loop.loopId,
|
|
219
219
|
owner: resumeLoop ? "runMartin:resume" : "runMartin",
|
|
220
|
+
runControlCapable: input.runControlSource !== undefined,
|
|
220
221
|
now: () => loop.updatedAt
|
|
221
222
|
})
|
|
222
223
|
: undefined;
|
|
@@ -236,6 +237,9 @@ export async function runMartin(input) {
|
|
|
236
237
|
}, { now: now(), idFactory });
|
|
237
238
|
const adapterChain = [input.adapter, ...(input.fallbackAdapters ?? [])];
|
|
238
239
|
const runsRoot = input.store?.runsRoot ?? resolveRunsRoot();
|
|
240
|
+
const managedRollbackRoots = input.store?.runsRoot
|
|
241
|
+
? [input.store.runsRoot, resolveReceiptIntegrityRoot()]
|
|
242
|
+
: [];
|
|
239
243
|
const observesWorkspaceMutations = task.repoRoot !== undefined &&
|
|
240
244
|
adapterChain.some((adapter) => adapter.metadata.capabilities?.workspaceMutations !== false);
|
|
241
245
|
const touchedDuringExecution = new Set();
|
|
@@ -247,7 +251,8 @@ export async function runMartin(input) {
|
|
|
247
251
|
try {
|
|
248
252
|
completionBoundary = await captureRollbackBoundary({
|
|
249
253
|
repoRoot: task.repoRoot,
|
|
250
|
-
capturedAt: loop.createdAt
|
|
254
|
+
capturedAt: loop.createdAt,
|
|
255
|
+
managedRoots: managedRollbackRoots
|
|
251
256
|
});
|
|
252
257
|
if (!completionBoundary) {
|
|
253
258
|
completionBoundaryReason = "A run-wide file-effect observation boundary was not available.";
|
|
@@ -265,6 +270,7 @@ export async function runMartin(input) {
|
|
|
265
270
|
const fileEffects = observeRunFileEffects({
|
|
266
271
|
repoRoot: task.repoRoot,
|
|
267
272
|
boundary: completionBoundary,
|
|
273
|
+
managedRoots: managedRollbackRoots,
|
|
268
274
|
touchedDuringExecution,
|
|
269
275
|
unavailableReason: completionBoundaryReason
|
|
270
276
|
});
|
|
@@ -675,7 +681,84 @@ export async function runMartin(input) {
|
|
|
675
681
|
const evaluation = evaluateExitPolicy(exitPolicy, currentExitSnapshot(phase, result));
|
|
676
682
|
if (!evaluation.shouldExit)
|
|
677
683
|
return undefined;
|
|
678
|
-
|
|
684
|
+
const terminal = await selectTerminalEvaluation(evaluation);
|
|
685
|
+
await input.runControlSource?.recordTerminalIntent?.(loop.loopId);
|
|
686
|
+
return terminal;
|
|
687
|
+
}
|
|
688
|
+
async function waitForControlPoll(intervalMs) {
|
|
689
|
+
if (runController.signal.aborted)
|
|
690
|
+
return;
|
|
691
|
+
await new Promise((resolve) => {
|
|
692
|
+
let settled = false;
|
|
693
|
+
const finish = () => {
|
|
694
|
+
if (settled)
|
|
695
|
+
return;
|
|
696
|
+
settled = true;
|
|
697
|
+
clearTimeout(timer);
|
|
698
|
+
runController.signal.removeEventListener("abort", finish);
|
|
699
|
+
resolve();
|
|
700
|
+
};
|
|
701
|
+
const timer = setTimeout(finish, intervalMs);
|
|
702
|
+
timer.unref?.();
|
|
703
|
+
runController.signal.addEventListener("abort", finish, { once: true });
|
|
704
|
+
if (runController.signal.aborted)
|
|
705
|
+
finish();
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
async function awaitCooperativeRunControl() {
|
|
709
|
+
const source = input.runControlSource;
|
|
710
|
+
if (!source)
|
|
711
|
+
return undefined;
|
|
712
|
+
const initial = await source.poll(loop.loopId);
|
|
713
|
+
const pause = initial.pause;
|
|
714
|
+
if (!pause)
|
|
715
|
+
return undefined;
|
|
716
|
+
if (pause.loopId !== loop.loopId || pause.action !== "pause") {
|
|
717
|
+
throw new Error("Run control integrity check failed before pause application.");
|
|
718
|
+
}
|
|
719
|
+
await source.recordApplication({
|
|
720
|
+
loopId: loop.loopId,
|
|
721
|
+
controlId: pause.controlId,
|
|
722
|
+
action: "pause_applied",
|
|
723
|
+
appliedAt: now(),
|
|
724
|
+
receiptHash: pause.receiptHash
|
|
725
|
+
});
|
|
726
|
+
const pollIntervalMs = input.runControlPollIntervalMs ?? 250;
|
|
727
|
+
while (true) {
|
|
728
|
+
const terminal = await evaluateCheckpoint("pre_attempt");
|
|
729
|
+
if (terminal)
|
|
730
|
+
return terminal;
|
|
731
|
+
await waitForControlPoll(pollIntervalMs);
|
|
732
|
+
// Cancellation aborts the wait immediately. Reconcile terminal state
|
|
733
|
+
// before polling cooperative receipts because the production control
|
|
734
|
+
// source intentionally clears pause/continue after a cancel receipt.
|
|
735
|
+
const awakenedTerminal = await evaluateCheckpoint("pre_attempt");
|
|
736
|
+
if (awakenedTerminal)
|
|
737
|
+
return awakenedTerminal;
|
|
738
|
+
const snapshot = await source.poll(loop.loopId);
|
|
739
|
+
if (snapshot.pause) {
|
|
740
|
+
if (snapshot.pause.controlId !== pause.controlId || snapshot.pause.loopId !== loop.loopId) {
|
|
741
|
+
throw new Error("Run control integrity check failed while pause was applied.");
|
|
742
|
+
}
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
const continuedBy = snapshot.continuedBy;
|
|
746
|
+
if (!continuedBy ||
|
|
747
|
+
continuedBy.loopId !== loop.loopId ||
|
|
748
|
+
continuedBy.action !== "continue" ||
|
|
749
|
+
continuedBy.expectedPreviousControlId !== pause.controlId) {
|
|
750
|
+
throw new Error("Run control integrity check failed while releasing pause.");
|
|
751
|
+
}
|
|
752
|
+
await source.recordApplication({
|
|
753
|
+
loopId: loop.loopId,
|
|
754
|
+
controlId: continuedBy.controlId,
|
|
755
|
+
action: "continue_applied",
|
|
756
|
+
relatedControlId: pause.controlId,
|
|
757
|
+
appliedAt: now(),
|
|
758
|
+
receiptHash: continuedBy.receiptHash
|
|
759
|
+
});
|
|
760
|
+
return await evaluateCheckpoint("pre_attempt");
|
|
761
|
+
}
|
|
679
762
|
}
|
|
680
763
|
/**
|
|
681
764
|
* Blocker 1 / Blocker 2 — Authoritative retained-candidate terminal restoration.
|
|
@@ -711,6 +794,7 @@ export async function runMartin(input) {
|
|
|
711
794
|
const rollbackOutcome = await restoreRollbackBoundary({
|
|
712
795
|
repoRoot: task.repoRoot,
|
|
713
796
|
boundary: capturedBoundary,
|
|
797
|
+
managedRoots: managedRollbackRoots,
|
|
714
798
|
restoredAt,
|
|
715
799
|
decision: "DISCARD"
|
|
716
800
|
});
|
|
@@ -867,6 +951,9 @@ export async function runMartin(input) {
|
|
|
867
951
|
if (preRunTerminal !== undefined)
|
|
868
952
|
return await terminateRun(preRunTerminal);
|
|
869
953
|
while (loop.attempts.length < loop.budget.maxIterations) {
|
|
954
|
+
const controlTerminal = await awaitCooperativeRunControl();
|
|
955
|
+
if (controlTerminal !== undefined)
|
|
956
|
+
return await terminateRun(controlTerminal);
|
|
870
957
|
const distilled = distillContext(loop, {
|
|
871
958
|
maxRecentAttempts: useCompressedContext ? 1 : (input.maxRecentAttempts ?? 3)
|
|
872
959
|
});
|
|
@@ -1118,7 +1205,8 @@ export async function runMartin(input) {
|
|
|
1118
1205
|
const rollbackBoundary = tracksWorkspaceMutations
|
|
1119
1206
|
? await captureRollbackBoundary({
|
|
1120
1207
|
repoRoot: request.context.repoRoot,
|
|
1121
|
-
capturedAt: attemptStartedAt
|
|
1208
|
+
capturedAt: attemptStartedAt,
|
|
1209
|
+
managedRoots: managedRollbackRoots
|
|
1122
1210
|
})
|
|
1123
1211
|
: undefined;
|
|
1124
1212
|
// Persist attempt.started state before blocking on provider execution.
|
|
@@ -1353,10 +1441,14 @@ export async function runMartin(input) {
|
|
|
1353
1441
|
let weakenedTestFiles = [];
|
|
1354
1442
|
try {
|
|
1355
1443
|
changedFiles = tracksWorkspaceMutations
|
|
1356
|
-
? resolveChangedFiles(result, request.context.repoRoot, rollbackBoundary)
|
|
1444
|
+
? resolveChangedFiles(result, request.context.repoRoot, rollbackBoundary, managedRollbackRoots)
|
|
1357
1445
|
: [];
|
|
1358
1446
|
ignoredChanges = tracksWorkspaceMutations
|
|
1359
|
-
? listIgnoredFileChangesSinceBoundary({
|
|
1447
|
+
? listIgnoredFileChangesSinceBoundary({
|
|
1448
|
+
repoRoot: request.context.repoRoot,
|
|
1449
|
+
boundary: rollbackBoundary,
|
|
1450
|
+
managedRoots: managedRollbackRoots
|
|
1451
|
+
})
|
|
1360
1452
|
: ignoredChanges;
|
|
1361
1453
|
escapingLinks =
|
|
1362
1454
|
tracksWorkspaceMutations && request.context.repoRoot
|
|
@@ -1383,6 +1475,7 @@ export async function runMartin(input) {
|
|
|
1383
1475
|
rollbackOutcome = (await restoreRollbackBoundary({
|
|
1384
1476
|
repoRoot: request.context.repoRoot,
|
|
1385
1477
|
boundary: rollbackBoundary,
|
|
1478
|
+
managedRoots: managedRollbackRoots,
|
|
1386
1479
|
restoredAt: attemptCompletedAt,
|
|
1387
1480
|
decision: "DISCARD",
|
|
1388
1481
|
removePaths: ignoredChanges.created,
|
|
@@ -1496,6 +1589,7 @@ export async function runMartin(input) {
|
|
|
1496
1589
|
rollbackOutcome = await restoreRollbackBoundary({
|
|
1497
1590
|
repoRoot: request.context.repoRoot,
|
|
1498
1591
|
boundary: rollbackBoundary,
|
|
1592
|
+
managedRoots: managedRollbackRoots,
|
|
1499
1593
|
restoredAt: attemptCompletedAt,
|
|
1500
1594
|
decision: patchDecision.decision,
|
|
1501
1595
|
removePaths: violatingIgnoredPaths(filesystemDecision.violations, ignoredChanges.created),
|
|
@@ -1593,6 +1687,7 @@ export async function runMartin(input) {
|
|
|
1593
1687
|
rollbackOutcome = await restoreRollbackBoundary({
|
|
1594
1688
|
repoRoot: request.context.repoRoot,
|
|
1595
1689
|
boundary: rollbackBoundary,
|
|
1690
|
+
managedRoots: managedRollbackRoots,
|
|
1596
1691
|
restoredAt: attemptCompletedAt,
|
|
1597
1692
|
decision: patchDecision.decision
|
|
1598
1693
|
}) ?? undefined;
|
|
@@ -1725,6 +1820,7 @@ export async function runMartin(input) {
|
|
|
1725
1820
|
rollbackOutcome = await restoreRollbackBoundary({
|
|
1726
1821
|
repoRoot: request.context.repoRoot,
|
|
1727
1822
|
boundary: rollbackBoundary,
|
|
1823
|
+
managedRoots: managedRollbackRoots,
|
|
1728
1824
|
restoredAt: attemptCompletedAt,
|
|
1729
1825
|
decision: patchDecision?.decision ?? "DISCARD"
|
|
1730
1826
|
});
|
|
@@ -1734,6 +1830,7 @@ export async function runMartin(input) {
|
|
|
1734
1830
|
rollbackOutcome = await restoreRollbackBoundary({
|
|
1735
1831
|
repoRoot: request.context.repoRoot,
|
|
1736
1832
|
boundary: rollbackBoundary,
|
|
1833
|
+
managedRoots: managedRollbackRoots,
|
|
1737
1834
|
restoredAt: attemptCompletedAt,
|
|
1738
1835
|
decision: "DISCARD"
|
|
1739
1836
|
});
|
|
@@ -2002,6 +2099,7 @@ export async function runMartin(input) {
|
|
|
2002
2099
|
await restoreRollbackBoundary({
|
|
2003
2100
|
repoRoot: task.repoRoot,
|
|
2004
2101
|
boundary: preCandidateBoundary,
|
|
2102
|
+
managedRoots: managedRollbackRoots,
|
|
2005
2103
|
restoredAt: now(),
|
|
2006
2104
|
decision: "DISCARD"
|
|
2007
2105
|
});
|
|
@@ -2092,11 +2190,11 @@ function finalizeLoop(loop, decision, timestamp, idFactory, routingEconomicsInpu
|
|
|
2092
2190
|
};
|
|
2093
2191
|
}
|
|
2094
2192
|
function observeRunFileEffects(input) {
|
|
2095
|
-
const
|
|
2193
|
+
const observedTouchedDuringExecution = normalizeFileEffectPaths(input.touchedDuringExecution);
|
|
2096
2194
|
if (!input.repoRoot || !input.boundary) {
|
|
2097
2195
|
return {
|
|
2098
2196
|
observationStatus: "unavailable",
|
|
2099
|
-
touchedDuringExecution,
|
|
2197
|
+
touchedDuringExecution: observedTouchedDuringExecution,
|
|
2100
2198
|
netChangedAtCompletion: [],
|
|
2101
2199
|
revertedBeforeCompletion: [],
|
|
2102
2200
|
observationReason: input.unavailableReason ??
|
|
@@ -2104,9 +2202,12 @@ function observeRunFileEffects(input) {
|
|
|
2104
2202
|
};
|
|
2105
2203
|
}
|
|
2106
2204
|
try {
|
|
2205
|
+
const exclusions = authenticateManagedPathExclusions(input.repoRoot, input.boundary, input.managedRoots);
|
|
2206
|
+
const touchedDuringExecution = filterManagedRollbackPaths(observedTouchedDuringExecution, exclusions);
|
|
2107
2207
|
const netChangedAtCompletion = normalizeFileEffectPaths(listAttemptChangedFilesSinceBoundary({
|
|
2108
2208
|
repoRoot: input.repoRoot,
|
|
2109
2209
|
boundary: input.boundary,
|
|
2210
|
+
managedRoots: input.managedRoots,
|
|
2110
2211
|
includeBaselineSnapshotChanges: true
|
|
2111
2212
|
}));
|
|
2112
2213
|
const netChanged = new Set(netChangedAtCompletion);
|
|
@@ -2120,7 +2221,7 @@ function observeRunFileEffects(input) {
|
|
|
2120
2221
|
catch (error) {
|
|
2121
2222
|
return {
|
|
2122
2223
|
observationStatus: "unavailable",
|
|
2123
|
-
touchedDuringExecution,
|
|
2224
|
+
touchedDuringExecution: observedTouchedDuringExecution,
|
|
2124
2225
|
netChangedAtCompletion: [],
|
|
2125
2226
|
revertedBeforeCompletion: [],
|
|
2126
2227
|
observationReason: `Final file-effect observation was unavailable: ${fileEffectErrorMessage(error)}`
|
|
@@ -2235,11 +2336,15 @@ function violatingIgnoredPaths(violations, ignoredPaths) {
|
|
|
2235
2336
|
const violating = new Set(violations.map((violation) => violation.file?.replace(/\/$/u, "")).filter(Boolean));
|
|
2236
2337
|
return ignoredPaths.filter((filePath) => violating.has(filePath.replace(/\/$/u, "")));
|
|
2237
2338
|
}
|
|
2238
|
-
function resolveChangedFiles(result, repoRoot, rollbackBoundary) {
|
|
2239
|
-
const
|
|
2339
|
+
function resolveChangedFiles(result, repoRoot, rollbackBoundary, managedRoots = []) {
|
|
2340
|
+
const exclusions = repoRoot && rollbackBoundary
|
|
2341
|
+
? authenticateManagedPathExclusions(repoRoot, rollbackBoundary, managedRoots)
|
|
2342
|
+
: [];
|
|
2343
|
+
const adapterReported = filterManagedRollbackPaths(normalizeFileEffectPaths(result.execution?.changedFiles ?? []), exclusions);
|
|
2240
2344
|
const gitObserved = listAttemptChangedFilesSinceBoundary({
|
|
2241
2345
|
repoRoot,
|
|
2242
2346
|
boundary: rollbackBoundary,
|
|
2347
|
+
managedRoots,
|
|
2243
2348
|
includeBaselineSnapshotChanges: true
|
|
2244
2349
|
});
|
|
2245
2350
|
return normalizeFileEffectPaths([...adapterReported, ...gitObserved]);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { makeLedgerEvent } from "./ledger.js";
|
|
2
2
|
export type { LedgerEvent, LedgerEventDraft, LedgerEventKind } from "./ledger.js";
|
|
3
|
-
export { resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./integrity.js";
|
|
3
|
+
export { resolveReceiptIntegrityRoot, resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./integrity.js";
|
|
4
4
|
export type { ReceiptIntegrityChainEntry, StoredReceiptIntegrityMaterial } from "./integrity.js";
|
|
5
5
|
export { artifactDir, createFileRunStore, resolveRunsRoot, runDir } from "./store.js";
|
|
6
6
|
export type { AttemptArtifacts, RunContract, RunStore } from "./store.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { makeLedgerEvent } from "./ledger.js";
|
|
2
|
-
export { resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./integrity.js";
|
|
2
|
+
export { resolveReceiptIntegrityRoot, resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./integrity.js";
|
|
3
3
|
export { artifactDir, createFileRunStore, resolveRunsRoot, runDir } from "./store.js";
|
|
4
4
|
export { readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile } from "./runs-reader.js";
|
|
5
5
|
export { compileAndPersistContext } from "./compiler.js";
|
|
@@ -35,6 +35,7 @@ export declare function verifyReceiptIntegrityFromFiles(input: {
|
|
|
35
35
|
runsRoot: string;
|
|
36
36
|
loopRecordPath: string;
|
|
37
37
|
ledgerPath: string;
|
|
38
|
+
integrityPath?: string;
|
|
38
39
|
}): Promise<ReceiptIntegritySummary>;
|
|
39
40
|
export declare function resolveReceiptIntegrityPath(runsRoot: string, runId: string): string;
|
|
40
41
|
export declare function ensurePrivateIntegrityKey(runsRoot: string, keyFileName: string): Promise<[string, string] | undefined>;
|
|
@@ -40,7 +40,7 @@ export async function writeReceiptIntegrityMaterial(input) {
|
|
|
40
40
|
return material;
|
|
41
41
|
}
|
|
42
42
|
export async function verifyReceiptIntegrityFromFiles(input) {
|
|
43
|
-
const integrityPath = resolveReceiptIntegrityPath(input.runsRoot, input.runId);
|
|
43
|
+
const integrityPath = input.integrityPath ?? resolveReceiptIntegrityPath(input.runsRoot, input.runId);
|
|
44
44
|
const [rawMaterial, rawLoopRecord, rawLedger, key] = await Promise.all([
|
|
45
45
|
readFile(integrityPath, "utf8").catch(() => null),
|
|
46
46
|
readFile(input.loopRecordPath, "utf8").catch(() => null),
|
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
import type { LoopRecord, RollbackBoundaryArtifact } from "../contracts/index.js";
|
|
2
2
|
export interface RunExecutionLease {
|
|
3
|
-
schemaVersion: "martin.execution-lease.
|
|
3
|
+
schemaVersion: "martin.execution-lease.v2";
|
|
4
4
|
runId: string;
|
|
5
5
|
token: string;
|
|
6
6
|
owner: string;
|
|
7
7
|
pid: number;
|
|
8
8
|
hostname: string;
|
|
9
9
|
acquiredAt: string;
|
|
10
|
+
capabilities: {
|
|
11
|
+
runControl: boolean;
|
|
12
|
+
};
|
|
13
|
+
processInstanceId: string;
|
|
14
|
+
heartbeatAt: string;
|
|
15
|
+
heartbeatSequence: number;
|
|
10
16
|
}
|
|
11
17
|
export interface AcquiredRunExecutionLease {
|
|
12
18
|
lease: RunExecutionLease;
|
|
13
19
|
path: string;
|
|
14
20
|
release(): Promise<void>;
|
|
15
21
|
}
|
|
22
|
+
export interface ActiveRunExecutionLease {
|
|
23
|
+
lease: RunExecutionLease;
|
|
24
|
+
path: string;
|
|
25
|
+
}
|
|
16
26
|
export interface ResumeVerifierFailure {
|
|
17
27
|
command: string;
|
|
18
28
|
exitCode?: number;
|
|
@@ -36,4 +46,18 @@ export declare function acquireRunExecutionLease(input: {
|
|
|
36
46
|
now?: () => string;
|
|
37
47
|
host?: string;
|
|
38
48
|
pid?: number;
|
|
49
|
+
runControlCapable?: boolean;
|
|
50
|
+
}, hooks?: {
|
|
51
|
+
beforeStaleReclaim?: () => Promise<void>;
|
|
39
52
|
}): Promise<AcquiredRunExecutionLease>;
|
|
53
|
+
/**
|
|
54
|
+
* Returns the persisted execution lease only when its schema, run identity,
|
|
55
|
+
* supported owner and local process liveness are all authoritative. Control
|
|
56
|
+
* surfaces use this to avoid accepting commands for an abandoned or incapable
|
|
57
|
+
* executor.
|
|
58
|
+
*/
|
|
59
|
+
export declare function readActiveRunExecutionLease(input: {
|
|
60
|
+
runsRoot: string;
|
|
61
|
+
runId: string;
|
|
62
|
+
host?: string;
|
|
63
|
+
}): Promise<ActiveRunExecutionLease | undefined>;
|