mtech-ai-local-runner 0.1.58 → 0.1.59
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
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
## 0.1.59: completed publication acknowledgement
|
|
2
|
+
|
|
3
|
+
Runner 0.1.59 corrects post-publication completion acknowledgement. The Gateway binding primary key is independent of the client session UUID. Recovery uses the saved binding/Job/generation mapping and exact server completion receipt; it does not renew an expired lease or replay a completed publication. Unverified PRs retain `unverified` / `CI not_run` status.
|
|
4
|
+
|
|
1
5
|
## 0.1.57: answers go to the session the person is using
|
|
2
6
|
|
|
3
7
|
An answer names the asker's sessions linked to the Work Item. A named session now gets it only if a person typed in it in
|
|
@@ -662,13 +662,17 @@ export class McpGatewayClient {
|
|
|
662
662
|
throw reject();
|
|
663
663
|
return cursor;
|
|
664
664
|
}
|
|
665
|
-
/** Read-only acknowledgement of Route B's atomic server completion.
|
|
666
|
-
*
|
|
665
|
+
/** Read-only acknowledgement of Route B's atomic server completion.
|
|
666
|
+
* No lease renewal, replay or publication write is authorized here. Retained context
|
|
667
|
+
* identifies the server-issued binding; its ID is not the client session UUID. */
|
|
667
668
|
async confirmServerPublicationCompletion(bindingId, afterSequence, job, receipt) {
|
|
668
669
|
if (nested(job.taskPackage, 'local_fix_scope.verification_mode') !== 'gateway_publish_v1')
|
|
669
670
|
return false;
|
|
670
671
|
try {
|
|
671
|
-
|
|
672
|
+
const context = this.requireContext(bindingId);
|
|
673
|
+
if (context.jobFence?.job_id !== job.id || context.jobFence?.generation !== job.generation)
|
|
674
|
+
throw Error();
|
|
675
|
+
if (!Number.isSafeInteger(job.generation) || job.generation < 1 || typeof job.id !== 'string' || !job.id
|
|
672
676
|
|| !Number.isSafeInteger(afterSequence) || afterSequence < 0 || receipt.status !== 'completed' || receipt.verification_mode !== 'gateway_publish_v1' || receipt.verification_status !== 'unverified' || nested(receipt, 'ci.status') !== 'not_run'
|
|
673
677
|
|| typeof receipt.publication_id !== 'string' || !receipt.publication_id || typeof receipt.commit_sha !== 'string' || !/^[a-f0-9]{40}$/.test(receipt.commit_sha) || typeof receipt.pr_url !== 'string' || !receipt.pr_url)
|
|
674
678
|
throw Error();
|
|
@@ -712,7 +716,7 @@ export class McpGatewayClient {
|
|
|
712
716
|
}
|
|
713
717
|
async restoreLocalPublicationRecoveryContext(input, stored, terminalReplay = false) {
|
|
714
718
|
if (terminalReplay) {
|
|
715
|
-
if (stored.bindingId !== input.bindingId || typeof stored.leaseToken !== 'string' || !stored.leaseToken)
|
|
719
|
+
if (stored.bindingId !== input.bindingId || nested(stored, 'jobFence.job_id') !== input.jobId || nested(stored, 'jobFence.generation') !== input.generation || typeof stored.leaseToken !== 'string' || !stored.leaseToken)
|
|
716
720
|
throw new Error('LOCAL_PUBLICATION_RECOVERY_CONTEXT_REJECTED');
|
|
717
721
|
this.context = structuredClone(stored);
|
|
718
722
|
return;
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { access, chmod, mkdir, rename, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
4
5
|
import { promisify } from "node:util";
|
|
5
6
|
const execFileAsync = promisify(execFile);
|
|
6
7
|
export const LAUNCHD_LABEL = "com.mtec.ai-local-runner";
|
|
8
|
+
const BOOTOUT_POLLS = 40;
|
|
9
|
+
const BOOTOUT_POLL_MS = 250;
|
|
10
|
+
const BOOTSTRAP_ATTEMPTS = 3;
|
|
11
|
+
const BOOTSTRAP_RETRY_MS = 1000;
|
|
7
12
|
function xml(value) {
|
|
8
13
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
9
14
|
}
|
|
@@ -45,7 +50,38 @@ export class LaunchdService {
|
|
|
45
50
|
const run = this.deps.run ?? defaultRun;
|
|
46
51
|
await (this.deps.writeAtomic ?? defaultWriteAtomic)(this.plistPath, this.render(executablePath, stateRoot), 0o600);
|
|
47
52
|
await run("/bin/launchctl", ["bootout", `gui/${this.deps.uid}/${LAUNCHD_LABEL}`]).catch(() => undefined);
|
|
48
|
-
await
|
|
53
|
+
await this.waitUntilUnloaded();
|
|
54
|
+
await this.bootstrapAfterBootout();
|
|
55
|
+
}
|
|
56
|
+
// bootout returns before launchd has removed a service whose process is still exiting, and a
|
|
57
|
+
// bootstrap in that window fails with "5: Input/output error". The service is gone afterwards,
|
|
58
|
+
// so a setup rerun on a Mac with the runner loaded left it offline (runner 0.1.57, 2026-10-26).
|
|
59
|
+
async waitUntilUnloaded() {
|
|
60
|
+
const sleep = this.deps.sleep ?? delay;
|
|
61
|
+
for (let poll = 0; poll < BOOTOUT_POLLS; poll++) {
|
|
62
|
+
if (!(await this.status()).loaded)
|
|
63
|
+
return;
|
|
64
|
+
await sleep(BOOTOUT_POLL_MS);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async bootstrapAfterBootout() {
|
|
68
|
+
const run = this.deps.run ?? defaultRun;
|
|
69
|
+
for (let attempt = 1;; attempt++) {
|
|
70
|
+
try {
|
|
71
|
+
await run("/bin/launchctl", ["bootstrap", `gui/${this.deps.uid}`, this.plistPath]);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
76
|
+
const ioError = /Bootstrap failed: 5: Input\/output error/.exec(message);
|
|
77
|
+
if (!ioError)
|
|
78
|
+
throw error;
|
|
79
|
+
if (attempt === BOOTSTRAP_ATTEMPTS) {
|
|
80
|
+
throw new Error(`launchd did not load the runner service: ${ioError[0]} (${attempt} attempts). Rerun npx mtech-ai-local-runner setup.`);
|
|
81
|
+
}
|
|
82
|
+
await (this.deps.sleep ?? delay)(BOOTSTRAP_RETRY_MS);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
49
85
|
}
|
|
50
86
|
async uninstall() {
|
|
51
87
|
await (this.deps.run ?? defaultRun)("/bin/launchctl", ["bootout", `gui/${this.deps.uid}/${LAUNCHD_LABEL}`])
|
|
@@ -24,6 +24,10 @@ export async function recoverLocalFixPublication(input) {
|
|
|
24
24
|
throw Error('LOCAL_PUBLICATION_RECOVERY_REJECTED');
|
|
25
25
|
}
|
|
26
26
|
const savedReceipt = (s.phase === 'publication_terminal_pending' ? s.publicationTerminal?.completion.result.local_fix_publication : r.completedPublication);
|
|
27
|
+
// Rehydrate only the recorded identity before a read-only terminal check.
|
|
28
|
+
// No live fence renewal or publication effect is permitted by this branch.
|
|
29
|
+
if (savedReceipt && input.gateway.confirmServerPublicationCompletion && job.taskPackage.local_fix_scope?.verification_mode === 'gateway_publish_v1')
|
|
30
|
+
await input.gateway.restoreLocalPublicationRecoveryContext(fence, r.gatewayContext, true);
|
|
27
31
|
if (await confirmed(savedReceipt)) {
|
|
28
32
|
await input.state.clear();
|
|
29
33
|
return;
|