@martinloop/mcp 0.8.8 → 0.8.9

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.
@@ -0,0 +1,39 @@
1
+ import type { LoopRecord, RollbackBoundaryArtifact } from "../contracts/index.js";
2
+ export interface RunExecutionLease {
3
+ schemaVersion: "martin.execution-lease.v1";
4
+ runId: string;
5
+ token: string;
6
+ owner: string;
7
+ pid: number;
8
+ hostname: string;
9
+ acquiredAt: string;
10
+ }
11
+ export interface AcquiredRunExecutionLease {
12
+ lease: RunExecutionLease;
13
+ path: string;
14
+ release(): Promise<void>;
15
+ }
16
+ export interface ResumeVerifierFailure {
17
+ command: string;
18
+ exitCode?: number;
19
+ crashed?: boolean;
20
+ timedOut?: boolean;
21
+ failureExcerpt: string;
22
+ attemptId: string;
23
+ attemptIndex: number;
24
+ }
25
+ export interface ResumabilityDecision {
26
+ allowed: boolean;
27
+ reason: string;
28
+ }
29
+ export declare function evaluateRunResumability(loop: LoopRecord): ResumabilityDecision;
30
+ export declare function loadResumeCandidateBoundary(runsRoot: string, loop: LoopRecord): Promise<RollbackBoundaryArtifact | undefined>;
31
+ export declare function loadResumeVerifierFailure(runsRoot: string, loop: LoopRecord): Promise<ResumeVerifierFailure | undefined>;
32
+ export declare function acquireRunExecutionLease(input: {
33
+ runsRoot: string;
34
+ runId: string;
35
+ owner: string;
36
+ now?: () => string;
37
+ host?: string;
38
+ pid?: number;
39
+ }): Promise<AcquiredRunExecutionLease>;
@@ -0,0 +1,196 @@
1
+ /// <reference types="node" />
2
+ import { createHash, randomBytes } from "node:crypto";
3
+ import { mkdir, open, readFile, unlink } from "node:fs/promises";
4
+ import { hostname } from "node:os";
5
+ import { join } from "node:path";
6
+ import { artifactDir } from "./persistence/index.js";
7
+ export function evaluateRunResumability(loop) {
8
+ if (loop.terminationEnvelope) {
9
+ return { allowed: false, reason: "The run already has authoritative terminal evidence." };
10
+ }
11
+ if (!["queued", "running", "verifying"].includes(loop.status)) {
12
+ return { allowed: false, reason: `Run status ${loop.status} is terminal and cannot be continued.` };
13
+ }
14
+ if (!["created", "running", "verifying"].includes(loop.lifecycleState)) {
15
+ return {
16
+ allowed: false,
17
+ reason: `Run lifecycle ${loop.lifecycleState} is terminal and cannot be continued.`
18
+ };
19
+ }
20
+ if (loop.task.taskClass === "irreversible" || loop.task.approvalPolicy?.externalWrites === true) {
21
+ return {
22
+ allowed: false,
23
+ reason: "Automatic execution continuation is disabled for irreversible/external-write tasks."
24
+ };
25
+ }
26
+ if (loop.attempts.length >= loop.budget.maxIterations) {
27
+ return { allowed: false, reason: "The run has no remaining iteration authority." };
28
+ }
29
+ if (loop.cost.actualUsd >= loop.budget.maxUsd) {
30
+ return { allowed: false, reason: "The run has no remaining USD budget authority." };
31
+ }
32
+ if (loop.budget.maxTokens !== undefined &&
33
+ loop.cost.tokensIn + loop.cost.tokensOut >= loop.budget.maxTokens) {
34
+ return { allowed: false, reason: "The run has no remaining token budget authority." };
35
+ }
36
+ const started = new Set(loop.events
37
+ .filter((event) => event.type === "attempt.started")
38
+ .map((event) => String(event.payload["attemptId"] ?? ""))
39
+ .filter(Boolean));
40
+ for (const event of loop.events) {
41
+ if (event.type === "attempt.completed") {
42
+ const attemptId = String(event.payload["attemptId"] ?? "");
43
+ if (attemptId)
44
+ started.delete(attemptId);
45
+ }
46
+ }
47
+ if (started.size > 0) {
48
+ return {
49
+ allowed: false,
50
+ reason: "A persisted attempt is still marked active; provider-process ownership is uncertain."
51
+ };
52
+ }
53
+ return { allowed: true, reason: "Persisted run has remaining safe execution authority." };
54
+ }
55
+ export async function loadResumeCandidateBoundary(runsRoot, loop) {
56
+ const latestAttempt = loop.attempts.at(-1);
57
+ if (latestAttempt?.candidateRetained !== true)
58
+ return undefined;
59
+ // Recover the first rollback boundary in the current contiguous retained-candidate
60
+ // suffix. Earlier retained candidates that were later discarded must not become
61
+ // authority for a new resume.
62
+ let firstRetainedAttempt = latestAttempt;
63
+ for (let index = loop.attempts.length - 2; index >= 0; index -= 1) {
64
+ const candidate = loop.attempts[index];
65
+ if (candidate?.candidateRetained !== true)
66
+ break;
67
+ firstRetainedAttempt = candidate;
68
+ }
69
+ const path = join(artifactDir(runsRoot, loop.loopId, firstRetainedAttempt.index), "rollback-boundary.json");
70
+ const raw = await readFile(path, "utf8").catch((error) => {
71
+ if (error.code === "ENOENT") {
72
+ throw new Error(`Cannot continue ${loop.loopId}: retained candidate boundary evidence is missing for attempt ${firstRetainedAttempt.index}.`);
73
+ }
74
+ throw error;
75
+ });
76
+ return JSON.parse(raw);
77
+ }
78
+ export async function loadResumeVerifierFailure(runsRoot, loop) {
79
+ const attempt = loop.attempts.at(-1);
80
+ if (!attempt || attempt.verifierScore === 1)
81
+ return undefined;
82
+ const path = join(artifactDir(runsRoot, loop.loopId, attempt.index), "verification.json");
83
+ const raw = await readFile(path, "utf8").catch((error) => {
84
+ if (error.code === "ENOENT")
85
+ return undefined;
86
+ throw error;
87
+ });
88
+ if (!raw)
89
+ return undefined;
90
+ const verification = JSON.parse(raw);
91
+ const step = verification.steps?.find((candidate) => candidate.launched === true &&
92
+ (candidate.crashed === true ||
93
+ candidate.timedOut === true ||
94
+ (candidate.exitCode !== undefined && candidate.exitCode !== 0)));
95
+ const excerpt = (step?.detail ??
96
+ [verification.summary, ...(verification.warnings ?? [])].filter(Boolean).join("\n")).trim();
97
+ return {
98
+ command: step?.command ?? loop.task.verificationPlan[0] ?? "unknown",
99
+ ...(!step?.crashed && !step?.timedOut && step?.exitCode !== undefined
100
+ ? { exitCode: step.exitCode }
101
+ : {}),
102
+ ...(step?.crashed ? { crashed: true } : {}),
103
+ ...(step?.timedOut ? { timedOut: true } : {}),
104
+ failureExcerpt: excerpt.slice(-1500),
105
+ attemptId: attempt.attemptId,
106
+ attemptIndex: attempt.index
107
+ };
108
+ }
109
+ export async function acquireRunExecutionLease(input) {
110
+ const now = input.now ?? (() => new Date().toISOString());
111
+ const host = input.host ?? hostname();
112
+ const pid = input.pid ?? process.pid;
113
+ const directory = join(input.runsRoot, ".execution-leases");
114
+ const leaseFile = `${createHash("sha256").update(input.runId).digest("hex")}.json`;
115
+ const path = join(directory, leaseFile);
116
+ await mkdir(directory, { recursive: true });
117
+ const token = `${pid}-${randomBytes(16).toString("hex")}`;
118
+ const lease = {
119
+ schemaVersion: "martin.execution-lease.v1",
120
+ runId: input.runId,
121
+ token,
122
+ owner: input.owner,
123
+ pid,
124
+ hostname: host,
125
+ acquiredAt: now()
126
+ };
127
+ for (let attempt = 0; attempt < 2; attempt += 1) {
128
+ try {
129
+ const handle = await open(path, "wx", 0o600);
130
+ try {
131
+ await handle.writeFile(`${JSON.stringify(lease, null, 2)}\n`, "utf8");
132
+ await handle.sync();
133
+ }
134
+ finally {
135
+ await handle.close();
136
+ }
137
+ return {
138
+ lease,
139
+ path,
140
+ async release() {
141
+ try {
142
+ const existing = JSON.parse(await readFile(path, "utf8"));
143
+ if (existing.token === token)
144
+ await unlink(path);
145
+ }
146
+ catch (error) {
147
+ if (error.code !== "ENOENT")
148
+ throw error;
149
+ }
150
+ }
151
+ };
152
+ }
153
+ catch (error) {
154
+ if (error.code !== "EEXIST")
155
+ throw error;
156
+ const existing = await readLease(path);
157
+ if (!existing)
158
+ continue;
159
+ if (existing.hostname !== host) {
160
+ throw new Error(`Run ${input.runId} is leased by ${existing.owner} on ${existing.hostname}; refusing cross-host takeover.`);
161
+ }
162
+ if (isProcessAlive(existing.pid)) {
163
+ throw new Error(`Run ${input.runId} is already executing under ${existing.owner} (pid ${existing.pid}).`);
164
+ }
165
+ await unlink(path).catch((unlinkError) => {
166
+ if (unlinkError.code !== "ENOENT")
167
+ throw unlinkError;
168
+ });
169
+ }
170
+ }
171
+ throw new Error(`Unable to acquire execution lease for run ${input.runId}.`);
172
+ }
173
+ async function readLease(path) {
174
+ try {
175
+ return JSON.parse(await readFile(path, "utf8"));
176
+ }
177
+ catch (error) {
178
+ if (error.code === "ENOENT")
179
+ return undefined;
180
+ throw new Error(`Existing execution lease is unreadable: ${error instanceof Error ? error.message : String(error)}`);
181
+ }
182
+ }
183
+ function isProcessAlive(pid) {
184
+ if (!Number.isInteger(pid) || pid <= 0)
185
+ return true;
186
+ try {
187
+ process.kill(pid, 0);
188
+ return true;
189
+ }
190
+ catch (error) {
191
+ const code = error.code;
192
+ if (code === "ESRCH")
193
+ return false;
194
+ return true;
195
+ }
196
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@martinloop/mcp",
3
- "version": "0.8.8",
3
+ "version": "0.8.9",
4
4
  "mcpName": "io.github.Keesan12/martin-loop",
5
5
  "private": false,
6
6
  "type": "module",
package/server.json CHANGED
@@ -7,12 +7,12 @@
7
7
  "url": "https://github.com/Keesan12/martin-loop",
8
8
  "source": "github"
9
9
  },
10
- "version": "0.8.8",
10
+ "version": "0.8.9",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "@martinloop/mcp",
15
- "version": "0.8.8",
15
+ "version": "0.8.9",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }