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. No lease,
666
- * context restoration, replay or publication write is authorized here. */
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
- if (!Number.isSafeInteger(job.generation) || job.generation < 1 || typeof job.id !== 'string' || !job.id || bindingId !== uuidv5(`local-agent-job:${job.id}:${job.generation}`, uuidv5.URL)
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
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 run("/bin/launchctl", ["bootstrap", `gui/${this.deps.uid}`, this.plistPath]);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtech-ai-local-runner",
3
- "version": "0.1.58",
3
+ "version": "0.1.59",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "bin": {