mindwire 0.1.14 → 0.1.18

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/dist/client.d.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import { Http, type FetchLike } from "./http.js";
2
2
  import { Run } from "./run.js";
3
- import { WorkspaceApi } from "./workspace.js";
3
+ import { WorkspaceApi, type ProjectAuth } from "./workspace.js";
4
4
  import { SurfacesApi } from "./surfaces.js";
5
+ import { ServiceApi } from "./service.js";
5
6
  import { type Target } from "./target/index.js";
6
7
  import type { EnsureEvent } from "./target/host.js";
7
- import type { AgentInfo, AuthMethod, AuthState, AuthStatus, Catalog, ChatSummary, CustomProvider, DeleteResult, DoctorReport, Health, MCPServer, MemoryDoc, MemoryScope, Message, ModelInfo, Notification, NotifyChannel, NotifyChannelInput, NotifyChannelTestResult, NotifyConfigInput, NotifyConfigStatus, NotifyRule, NotifyRuleInput, ProcessFrame, PromptTemplate, ResolveOptions, SetupStatus, Stats, Subagent, TurnOptions } from "./types.js";
8
+ import type { AgentInfo, AuthMethod, AuthState, AuthStatus, Catalog, ChatSummary, CustomProvider, DeleteResult, DoctorReport, Health, MCPServer, MemoryDoc, MemoryScope, Message, ModelInfo, Notification, NotifyChannel, NotifyChannelInput, NotifyChannelTestResult, NotifyConfigInput, NotifyConfigStatus, NotifyRule, NotifyRuleInput, ProcessFrame, PromptTemplate, ResolveOptions, SetupStatus, HarnessSoftware, Stats, Subagent, TurnOptions } from "./types.js";
8
9
  export interface MindwireOptions {
9
10
  /**
10
11
  * Default agent type for agent-scoped calls (e.g. `"claude-code"`). When omitted, the daemon
@@ -72,6 +73,7 @@ export declare class Mindwire {
72
73
  /** Workspace registry: saved agent profiles, projects and chat relationships. */
73
74
  readonly workspace: WorkspaceApi;
74
75
  readonly surfaces: SurfacesApi;
76
+ readonly service: ServiceApi;
75
77
  readonly http: Http;
76
78
  /** The default agent type applied to agent-scoped calls, if set. */
77
79
  readonly defaultAgent: string | undefined;
@@ -120,6 +122,10 @@ export declare class Mindwire {
120
122
  catalog(): Promise<Catalog>;
121
123
  /** `GET /agent` — capabilities + settings schema + auth methods/status for the selected agent. */
122
124
  agent(scoped?: AgentScoped): Promise<AgentInfo>;
125
+ /** `GET /agent/software` — installed version, compatibility and approved update target. */
126
+ software(opts?: AgentScoped & {
127
+ refresh?: boolean;
128
+ }): Promise<HarnessSoftware>;
123
129
  /**
124
130
  * `GET /models` — the models the selected agent can run for the configured account. An empty array
125
131
  * is valid (no credentials yet / offline). Throws a 400 {@link ApiError} for an agent whose model is
@@ -130,7 +136,7 @@ export declare class Mindwire {
130
136
  doctor(scoped?: AgentScoped): Promise<DoctorReport>;
131
137
  /** `POST /setup` — start the agent's install toolchain (background; poll {@link setupStatus}). */
132
138
  setup(scoped?: AgentScoped): Promise<SetupStatus>;
133
- /** `POST /update` — re-run the toolchain, forcing reinstall of installable steps. */
139
+ /** `POST /update` — install the catalog's newest tested version compatible with this daemon. */
134
140
  update(scoped?: AgentScoped): Promise<SetupStatus>;
135
141
  /** `GET /setup` — current toolchain install progress. */
136
142
  setupStatus(scoped?: AgentScoped): Promise<SetupStatus>;
@@ -189,6 +195,7 @@ export declare class Mindwire {
189
195
  options?: TurnOptions;
190
196
  mode?: "turn" | "resolve";
191
197
  resolve?: ResolveOptions;
198
+ gitAuth?: ProjectAuth;
192
199
  } & AgentScoped): Promise<Run>;
193
200
  /**
194
201
  * `POST /turns {mode:"resolve"}` — start a **global-resolve** run: instead of returning after one
@@ -208,6 +215,7 @@ export declare class Mindwire {
208
215
  cwd?: string;
209
216
  options?: TurnOptions;
210
217
  resolve?: ResolveOptions;
218
+ gitAuth?: ProjectAuth;
211
219
  } & AgentScoped): Promise<Run>;
212
220
  /** `GET /runs/{id}` — fetch an existing run as a {@link Run} handle. */
213
221
  run(id: string): Promise<Run>;
@@ -253,8 +261,14 @@ export declare class AuthApi {
253
261
  begin(method: string, scoped?: AgentScoped): Promise<AuthState>;
254
262
  /** `POST /auth/step` — submit fields, or poll an interactive login to completion. */
255
263
  step(input: Record<string, string>, scoped?: AgentScoped): Promise<AuthState>;
264
+ /** Read one interactive attempt, scoped by the returned AuthState.flowId. */
265
+ poll(flowId: string, scoped?: AgentScoped): Promise<AuthState>;
266
+ /** Cancel this native login without affecting a newer sign-in attempt. */
267
+ cancel(flowId: string, scoped?: AgentScoped): Promise<AuthState>;
256
268
  /** `GET /auth/status` — is the agent authenticated, and via which method. */
257
269
  status(scoped?: AgentScoped): Promise<AuthStatus>;
270
+ /** `POST /auth/logout` — disconnect this harness once its running chats finish. */
271
+ logout(scoped?: AgentScoped): Promise<AuthStatus>;
258
272
  }
259
273
  /**
260
274
  * The persistent prompt/memory surface for a scoped agent — the three layers beneath a turn's
package/dist/index.cjs CHANGED
@@ -383,8 +383,9 @@ var Run = class _Run {
383
383
  await this.http.request("POST", `/runs/${encodeURIComponent(this.id)}/cancel`);
384
384
  }
385
385
  /**
386
- * Answer a mid-turn interaction the turn is waiting on — a permission approval, or an
387
- * AskUserQuestion / ExitPlanMode reply. Requires the agent's `respond` capability.
386
+ * Answer a pending permission, question or plan. Message-mode questions remain answerable
387
+ * after completion; the daemon steers or resumes the conversation. Read the chat's latest
388
+ * run after replying to a completed run. Requires the agent's `respond` capability.
388
389
  */
389
390
  async respond(input = {}) {
390
391
  await this.http.request("POST", `/runs/${encodeURIComponent(this.id)}/respond`, { body: input });
@@ -548,12 +549,14 @@ var WorkspaceCollection = class {
548
549
  var WorkspaceApi = class {
549
550
  constructor(mw) {
550
551
  this.mw = mw;
552
+ this.git = new GitAccessApi(mw);
551
553
  this.operations = new ProjectOperationsApi(mw);
552
554
  this.agents = new WorkspaceCollection(mw, "agents");
553
555
  this.projects = new WorkspaceCollection(mw, "projects");
554
556
  this.chats = new WorkspaceCollection(mw, "chats");
555
557
  }
556
558
  mw;
559
+ git;
557
560
  operations;
558
561
  agents;
559
562
  projects;
@@ -582,6 +585,75 @@ var WorkspaceApi = class {
582
585
  return this.mw.http.request("POST", "/workspace/import", { body: records });
583
586
  }
584
587
  };
588
+ var GitAccessApi = class {
589
+ constructor(mw) {
590
+ this.mw = mw;
591
+ }
592
+ mw;
593
+ state() {
594
+ return this.mw.http.request("GET", "/workspace/git");
595
+ }
596
+ setDefault(connection, auth) {
597
+ return this.mw.http.request("PUT", "/workspace/git", { body: { connection, auth } });
598
+ }
599
+ forget(connectionId) {
600
+ return this.mw.http.request("DELETE", `/workspace/git/connections/${encodeURIComponent(connectionId)}`);
601
+ }
602
+ project(projectId) {
603
+ return this.mw.http.request("GET", `/workspace/projects/${encodeURIComponent(projectId)}/git`);
604
+ }
605
+ setProject(projectId, connection, expectedRevision, auth) {
606
+ return this.mw.http.request("PUT", `/workspace/projects/${encodeURIComponent(projectId)}/git`, {
607
+ body: { connection, expectedRevision, auth }
608
+ });
609
+ }
610
+ /** Compatibility call. Prefer start() and operation()/watch() for reconnectable writes. */
611
+ run(projectId, operation, auth) {
612
+ return this.mw.http.request("POST", `/workspace/projects/${encodeURIComponent(projectId)}/git/${operation}`, { body: { auth } });
613
+ }
614
+ /** Requires health.gitOperationsVersion >= 1. Acceptance persists before Git runs;
615
+ * disconnecting only detaches the client. The same ID/intent never runs twice.
616
+ */
617
+ start(projectId, request) {
618
+ return this.mw.http.request("POST", `/workspace/projects/${encodeURIComponent(projectId)}/git/operations`, { body: request });
619
+ }
620
+ async operations(projectId, activeOnly = false) {
621
+ const result = await this.mw.http.request(
622
+ "GET",
623
+ `/workspace/projects/${encodeURIComponent(projectId)}/git/operations`,
624
+ { query: { active: activeOnly } }
625
+ );
626
+ return result.operations;
627
+ }
628
+ operation(id) {
629
+ return this.mw.http.request("GET", `/workspace/git/operations/${encodeURIComponent(id)}`);
630
+ }
631
+ cancel(id) {
632
+ return this.mw.http.request("POST", `/workspace/git/operations/${encodeURIComponent(id)}/cancel`);
633
+ }
634
+ /** Current snapshot followed by state changes. Aborting observation never cancels the operation. */
635
+ async *watch(id, opts = {}) {
636
+ const controller = new AbortController();
637
+ const abort = () => controller.abort();
638
+ opts.signal?.addEventListener("abort", abort, { once: true });
639
+ if (opts.signal?.aborted) controller.abort();
640
+ try {
641
+ const response = await this.mw.http.open("GET", `/workspace/git/operations/${encodeURIComponent(id)}/stream`, {
642
+ signal: controller.signal
643
+ });
644
+ let sequence = -1;
645
+ for await (const operation of readSSE(response.body, controller.signal)) {
646
+ if (operation.sequence > sequence) {
647
+ sequence = operation.sequence;
648
+ yield operation;
649
+ }
650
+ }
651
+ } finally {
652
+ controller.abort();
653
+ opts.signal?.removeEventListener("abort", abort);
654
+ }
655
+ }
656
+ };
585
657
 
586
658
  // src/surfaces.ts
587
659
  var SurfacesApi = class {
@@ -643,8 +715,27 @@ var SurfacesApi = class {
643
715
  }
644
716
  };
645
717
 
718
+ // src/service.ts
719
+ var ServiceApi = class {
720
+ constructor(client) {
721
+ this.client = client;
722
+ }
723
+ client;
724
+ updateStatus() {
725
+ return this.client.http.request("GET", "/service/update");
726
+ }
727
+ /** Download first. Acquire immediately before replacement; busy work returns 409.
728
+ * Admission stays closed until release, process exit, or the one-minute expiry. */
729
+ acquireUpdate() {
730
+ return this.client.http.request("POST", "/service/update");
731
+ }
732
+ releaseUpdate(id) {
733
+ return this.client.http.request("DELETE", `/service/update/${encodeURIComponent(id)}`);
734
+ }
735
+ };
736
+
646
737
  // src/version.ts
647
- var SDK_VERSION = "0.1.14" ;
738
+ var SDK_VERSION = "0.1.18" ;
648
739
 
649
740
  // src/daemon-binary.ts
650
741
  function supported(platform, arch) {
@@ -905,6 +996,7 @@ var Mindwire = class _Mindwire {
905
996
  /** Workspace registry: saved agent profiles, projects and chat relationships. */
906
997
  workspace;
907
998
  surfaces;
999
+ service;
908
1000
  http;
909
1001
  /** The default agent type applied to agent-scoped calls, if set. */
910
1002
  defaultAgent;
@@ -940,6 +1032,7 @@ var Mindwire = class _Mindwire {
940
1032
  this.defaultAgent = opts.agent;
941
1033
  this.workspace = new WorkspaceApi(this);
942
1034
  this.surfaces = new SurfacesApi(this);
1035
+ this.service = new ServiceApi(this);
943
1036
  this.auth = new AuthApi(this);
944
1037
  this.prompts = new PromptsApi(this);
945
1038
  this.mcp = new McpApi(this);
@@ -962,6 +1055,7 @@ var Mindwire = class _Mindwire {
962
1055
  clone.defaultAgent = agent;
963
1056
  clone.workspace = new WorkspaceApi(clone);
964
1057
  clone.surfaces = new SurfacesApi(clone);
1058
+ clone.service = new ServiceApi(clone);
965
1059
  clone.auth = new AuthApi(clone);
966
1060
  clone.prompts = new PromptsApi(clone);
967
1061
  clone.mcp = new McpApi(clone);
@@ -1010,6 +1104,12 @@ var Mindwire = class _Mindwire {
1010
1104
  agent(scoped) {
1011
1105
  return this.http.request("GET", "/agent", { query: this.agentParam(scoped) });
1012
1106
  }
1107
+ /** `GET /agent/software` — installed version, compatibility and approved update target. */
1108
+ software(opts = {}) {
1109
+ return this.http.request("GET", "/agent/software", {
1110
+ query: { ...this.agentParam(opts), ...opts.refresh ? { refresh: "true" } : {} }
1111
+ });
1112
+ }
1013
1113
  /**
1014
1114
  * `GET /models` — the models the selected agent can run for the configured account. An empty array
1015
1115
  * is valid (no credentials yet / offline). Throws a 400 {@link ApiError} for an agent whose model is
@@ -1027,7 +1127,7 @@ var Mindwire = class _Mindwire {
1027
1127
  setup(scoped) {
1028
1128
  return this.http.request("POST", "/setup", { query: this.agentParam(scoped) });
1029
1129
  }
1030
- /** `POST /update` — re-run the toolchain, forcing reinstall of installable steps. */
1130
+ /** `POST /update` — install the catalog's newest tested version compatible with this daemon. */
1031
1131
  update(scoped) {
1032
1132
  return this.http.request("POST", "/update", { query: this.agentParam(scoped) });
1033
1133
  }
@@ -1124,6 +1224,7 @@ var Mindwire = class _Mindwire {
1124
1224
  if (input.options !== void 0) body.options = input.options;
1125
1225
  if (input.mode !== void 0) body.mode = input.mode;
1126
1226
  if (input.resolve !== void 0) body.resolve = input.resolve;
1227
+ if (input.gitAuth !== void 0) body.gitAuth = input.gitAuth;
1127
1228
  const data = await this.http.request("POST", "/turns", {
1128
1229
  query: this.agentParam(input),
1129
1230
  body
@@ -1227,12 +1328,26 @@ var AuthApi = class {
1227
1328
  body: input
1228
1329
  });
1229
1330
  }
1331
+ /** Read one interactive attempt, scoped by the returned AuthState.flowId. */
1332
+ poll(flowId, scoped) {
1333
+ return this.step({ _flowId: flowId }, scoped);
1334
+ }
1335
+ /** Cancel this native login without affecting a newer sign-in attempt. */
1336
+ cancel(flowId, scoped) {
1337
+ return this.step({ _flowId: flowId, _action: "cancel" }, scoped);
1338
+ }
1230
1339
  /** `GET /auth/status` — is the agent authenticated, and via which method. */
1231
1340
  status(scoped) {
1232
1341
  return this.mw.http.request("GET", "/auth/status", {
1233
1342
  query: this.mw.agentParam(scoped)
1234
1343
  });
1235
1344
  }
1345
+ /** `POST /auth/logout` — disconnect this harness once its running chats finish. */
1346
+ logout(scoped) {
1347
+ return this.mw.http.request("POST", "/auth/logout", {
1348
+ query: this.mw.agentParam(scoped)
1349
+ });
1350
+ }
1236
1351
  };
1237
1352
  var PromptsApi = class {
1238
1353
  constructor(mw) {
@@ -1474,6 +1589,23 @@ var NotifyApi = class {
1474
1589
  };
1475
1590
 
1476
1591
  // src/target/host.ts
1592
+ function versionAtLeast(actual, desired) {
1593
+ if (actual === desired) return true;
1594
+ if (!actual || !/^\d+\.\d+\.\d+$/.test(actual) || !/^\d+\.\d+\.\d+$/.test(desired)) return false;
1595
+ const a = actual.split(".").map(Number), b = desired.split(".").map(Number);
1596
+ for (let i = 0; i < 3; i++) {
1597
+ if (a[i] !== b[i]) return a[i] > b[i];
1598
+ }
1599
+ return true;
1600
+ }
1601
+ var versionCheckScript = String.raw`version_at_least() {
1602
+ [ "$1" != "$2" ] || return 0
1603
+ [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && [[ "$2" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || return 1
1604
+ local a b c x y z
1605
+ IFS=. read -r a b c <<< "$1"
1606
+ IFS=. read -r x y z <<< "$2"
1607
+ (( 10#$a > 10#$x || (10#$a == 10#$x && 10#$b > 10#$y) || (10#$a == 10#$x && 10#$b == 10#$y && 10#$c >= 10#$z) ))
1608
+ }`;
1477
1609
  function makeEmit(cfg) {
1478
1610
  const target = cfg.target ?? "sandbox";
1479
1611
  return (e) => {
@@ -1510,19 +1642,24 @@ async function ensureDaemon(host, cfg) {
1510
1642
  message: health.version ? `daemon reachable (v${health.version})` : "daemon reachable (version unknown)",
1511
1643
  version: health.version
1512
1644
  });
1513
- const upToDate = health.version !== void 0 && health.version === desired;
1645
+ const upToDate = versionAtLeast(health.version, desired);
1514
1646
  if (!cfg.forceDeploy && (upToDate || !cfg.autoUpdate)) {
1515
1647
  emit2({
1516
1648
  phase: "skip",
1517
- message: upToDate ? `daemon already at v${desired}` : "keeping the running daemon",
1649
+ message: upToDate ? `daemon already at v${health.version}` : "keeping the running daemon",
1518
1650
  version: health.version
1519
1651
  });
1520
1652
  return token;
1521
1653
  }
1654
+ if ((health.serviceUpdateVersion ?? 0) < 1) {
1655
+ if (cfg.forceDeploy) throw new MindwireError("This legacy service cannot reserve an idle update. Upgrade it explicitly or stop it after its work finishes before deploying.");
1656
+ emit2({ phase: "skip", message: "automatic update deferred: upgrade this legacy service explicitly to enable idle updates", version: health.version });
1657
+ return token;
1658
+ }
1522
1659
  } else {
1523
1660
  emit2({ phase: "probe", message: "no daemon reachable; deploying" });
1524
1661
  }
1525
- await deploy(host, cfg, emit2, token, directory);
1662
+ await deploy(host, cfg, emit2, token, directory, health.reachable);
1526
1663
  if (!cfg.token) token = await readWorkspaceToken(host, directory) ?? token;
1527
1664
  return token;
1528
1665
  } catch (err) {
@@ -1535,7 +1672,7 @@ async function readWorkspaceToken(host, directory) {
1535
1672
  const candidate = saved.stdout?.trim();
1536
1673
  return candidate && candidate.length <= 4096 && /^[\x21-\x7e]+$/.test(candidate) ? candidate : void 0;
1537
1674
  }
1538
- async function deploy(host, cfg, emit2, token, directory) {
1675
+ async function deploy(host, cfg, emit2, token, directory, requireLease) {
1539
1676
  const newPath = directory + "/mindwired.new";
1540
1677
  const BIN = shellQuote(directory + "/mindwired"), BIN_NEW = shellQuote(newPath);
1541
1678
  const STATE = shellQuote(directory + "/agent-state.json"), LOG = shellQuote(directory + "/daemon.log");
@@ -1577,6 +1714,7 @@ async function deploy(host, cfg, emit2, token, directory) {
1577
1714
  ` latest=$(curl -fsSL https://api.github.com/repos/oblien/mindwire/releases/latest | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p')`,
1578
1715
  ' case "$latest" in v[0-9]*.[0-9]*.[0-9]*) ;; *) echo "MINDWIRE_FAIL no matching or latest release"; exit 1 ;; esac',
1579
1716
  ' release="https://github.com/oblien/mindwire/releases/download/$latest"',
1717
+ ' mw_expected_version="${latest#v}"',
1580
1718
  ` asset="mindwired-$latest-${platform}-${arch}"`,
1581
1719
  ' download || { echo "MINDWIRE_FAIL latest release download failed"; exit 1; }',
1582
1720
  "fi",
@@ -1596,43 +1734,75 @@ async function deploy(host, cfg, emit2, token, directory) {
1596
1734
  'else echo "MINDWIRE_FAIL No supported update lock is available (flock on Linux, lockf on macOS)."; exit 1; fi',
1597
1735
  `mw_token=${shellQuote(token)}`,
1598
1736
  !cfg.token ? `if [ -s ${TOKEN} ]; then mw_token=$(cat ${TOKEN}); fi` : "",
1737
+ 'mw_lease=""',
1738
+ `mw_lease_file=${shellQuote(directory)}/update-lease-$$.json`,
1739
+ "cleanup_update() {",
1740
+ ` if [ -n "$mw_lease" ]; then curl -s --connect-timeout 2 --max-time 3 -X DELETE -H "Authorization: Bearer $mw_token" "http://127.0.0.1:${cfg.port}/service/update/$mw_lease" >/dev/null 2>&1 || true; fi`,
1741
+ ` rm -f ${BIN_NEW} "$mw_lease_file"${stagedUpload ? " " + shellQuote(stagedUpload) : ""}`,
1742
+ "}",
1743
+ // Shared staging is only removed while this process owns the workspace lock.
1744
+ "trap cleanup_update EXIT",
1745
+ versionCheckScript,
1746
+ `mw_desired=${shellQuote(desired)}`,
1747
+ 'mw_expected_version="$mw_desired"',
1748
+ `mw_health=$(curl -fsS --max-time 3 -H "Authorization: Bearer $mw_token" http://127.0.0.1:${cfg.port}/healthz 2>/dev/null || true)`,
1599
1749
  !cfg.forceDeploy ? [
1600
- `mw_health=$(curl -fsS --max-time 3 -H "Authorization: Bearer $mw_token" http://127.0.0.1:${cfg.port}/healthz 2>/dev/null || true)`,
1601
1750
  `mw_version=$(printf '%s' "$mw_health" | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\\([^" ]*\\)".*/\\1/p')`,
1602
- `if [ -n "$mw_health" ] && ${cfg.autoUpdate ? `[ "$mw_version" = ${shellQuote(desired)} ]` : "true"}; then echo MINDWIRE_READY; exit 0; fi`
1751
+ `if [ -n "$mw_health" ] && ${cfg.autoUpdate ? 'version_at_least "$mw_version" "$mw_desired"' : "true"}; then echo MINDWIRE_READY; exit 0; fi`
1603
1752
  ].join("\n") : "",
1604
1753
  // macOS ships POSIX setsid in Perl, but does not include Linux's setsid executable.
1605
1754
  "if command -v setsid >/dev/null 2>&1; then mw_detach=(setsid);",
1606
1755
  `elif command -v perl >/dev/null 2>&1; then mw_detach=(perl -MPOSIX -e 'defined(my $sid = POSIX::setsid()) && $sid >= 0 or die "setsid: $!"; exec @ARGV; die "exec: $!";');`,
1607
1756
  'else echo "MINDWIRE_FAIL Cannot start the Mindwire service: setsid or Perl is required."; exit 1; fi',
1608
1757
  acquire,
1609
- // Stop a prior daemon by exact process NAME, never `pkill -f <path>`: this whole script (which
1610
- // contains `${BIN}` several times) is the argv of the `bash -lc` shell running it, so a full-cmdline
1611
- // match would SIGTERM our own deploying shell before the daemon ever launches. `-x mindwired` matches
1612
- // only the daemon's comm (`bash`/`pkill` never match), leaving this shell alive.
1613
- "if command -v pkill >/dev/null 2>&1; then",
1614
- " pkill -x mindwired 2>/dev/null || true",
1615
- "else",
1616
- " for mw_proc in /proc/[0-9]*/comm; do",
1617
- ' IFS= read -r mw_name 2>/dev/null < "$mw_proc" || continue',
1618
- ' [ "$mw_name" = mindwired ] || continue',
1619
- " mw_pid=${mw_proc#/proc/}; mw_pid=${mw_pid%/comm}",
1620
- ' kill "$mw_pid" 2>/dev/null || true',
1621
- " done",
1758
+ // The service decides idleness after transfer. Never stop arbitrary processes
1759
+ // or interrupt a chat that began while the SDK was downloading its binary.
1760
+ `mw_health=$(curl -fsS --max-time 3 -H "Authorization: Bearer $mw_token" http://127.0.0.1:${cfg.port}/healthz 2>/dev/null || true)`,
1761
+ 'if [ -n "$mw_health" ]; then',
1762
+ ` mw_status=$(curl -s --connect-timeout 2 --max-time 5 -X POST -H "Authorization: Bearer $mw_token" -o "$mw_lease_file" -w '%{http_code}' http://127.0.0.1:${cfg.port}/service/update) || mw_status=000`,
1763
+ ' case "$mw_status" in',
1764
+ " 201)",
1765
+ ` mw_lease=$(sed -n 's/.*"id"[[:space:]]*:[[:space:]]*"\\([[:alnum:]]*\\)".*/\\1/p' "$mw_lease_file")`,
1766
+ ` mw_service_pid=$(sed -n 's/.*"pid"[[:space:]]*:[[:space:]]*\\([0-9][0-9]*\\).*/\\1/p' "$mw_lease_file")`,
1767
+ ' [ -n "$mw_lease" ] && [[ "$mw_service_pid" =~ ^[0-9]+$ ]] && [ "$mw_service_pid" -gt 1 ] || { echo "MINDWIRE_FAIL invalid service update lease"; exit 1; }',
1768
+ ' kill "$mw_service_pid" ;;',
1769
+ " 409) echo MINDWIRE_UPDATE_DEFERRED; exit 0 ;;",
1770
+ ' 404) echo "MINDWIRE_FAIL this legacy service requires an explicit upgrade before idle updates"; exit 1 ;;',
1771
+ ' *) echo "MINDWIRE_FAIL could not reserve an idle service update"; exit 1 ;;',
1772
+ " esac",
1773
+ requireLease ? 'else echo "MINDWIRE_FAIL the service went offline before its idle update could be reserved"; exit 1' : "",
1622
1774
  "fi",
1623
1775
  "sleep 0.3",
1624
1776
  `mv -f ${BIN_NEW} ${BIN}`,
1625
1777
  `chmod +x ${BIN}`,
1626
1778
  // Detach so the daemon survives this exec's shell exiting. ADDR=":<port>" binds 0.0.0.0.
1627
- `"\${mw_detach[@]}" nohup env ADDR=":${cfg.port}" AGENT_TYPE=${shellQuote(cfg.agent)} AGENT_CWD=${shellQuote(cfg.agentCwd)} STATE_PATH=${STATE} DAEMON_TOKEN="$mw_token" ${BIN} > ${LOG} 2>&1 < /dev/null 9>&- &`,
1779
+ // Ignore HUP directly: macOS nohup can fail after setsid in a headless workspace.
1780
+ `"\${mw_detach[@]}" /bin/sh -c 'trap "" HUP; exec "$@"' mindwire-service env ADDR=":${cfg.port}" AGENT_TYPE=${shellQuote(cfg.agent)} AGENT_CWD=${shellQuote(cfg.agentCwd)} STATE_PATH=${STATE} DAEMON_TOKEN="$mw_token" ${BIN} > ${LOG} 2>&1 < /dev/null 9>&- &`,
1628
1781
  // Health-poll from inside the VM (loopback) and emit a marker — exit codes are unreliable here.
1629
- `for ((mw_attempt=0; mw_attempt<60; mw_attempt++)); do curl -fsS --max-time 2 -H "Authorization: Bearer $mw_token" http://127.0.0.1:${cfg.port}/healthz >/dev/null 2>&1 && { echo MINDWIRE_READY; exit 0; }; sleep 0.25; done`,
1630
- "echo MINDWIRE_FAIL",
1631
- `tail -n 40 ${LOG} 2>/dev/null || true`
1782
+ 'mw_pid=$!; mw_exit=""; mw_deadline=$((SECONDS + 30))',
1783
+ "while (( SECONDS < mw_deadline )); do",
1784
+ ` mw_health=$(curl -fsS --connect-timeout 2 --max-time 3 -H "Authorization: Bearer $mw_token" http://127.0.0.1:${cfg.port}/healthz 2>/dev/null || true)`,
1785
+ ` mw_version=$(printf '%s' "$mw_health" | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\\([^" ]*\\)".*/\\1/p')`,
1786
+ ' if version_at_least "$mw_version" "$mw_expected_version"; then echo MINDWIRE_READY; exit 0; fi',
1787
+ ' if [ -z "$mw_exit" ] && ! kill -0 "$mw_pid" 2>/dev/null; then',
1788
+ ' mw_exit=0; wait "$mw_pid" || mw_exit=$?; [ "$mw_exit" = 0 ] || break',
1789
+ " fi",
1790
+ " sleep 0.25",
1791
+ "done",
1792
+ `mw_tail=$(tail -n 40 ${LOG} 2>/dev/null || true)`,
1793
+ 'if [ -n "$mw_exit" ] && [ "$mw_exit" != 0 ]; then mw_reason="Mindwire exited during startup (status $mw_exit).";',
1794
+ 'else mw_reason="The Mindwire service did not become ready at v$mw_expected_version (reported: $mw_version)."; fi',
1795
+ '[ -z "$mw_tail" ] || mw_reason="$mw_reason $mw_tail"',
1796
+ 'printf "MINDWIRE_FAIL %s\\n" "$mw_reason"'
1632
1797
  ].join("\n");
1633
1798
  emit2({ phase: "launch", message: "launching daemon" });
1634
1799
  const res = await host.exec(["bash", "-lc", script], { timeoutSeconds: 420 });
1635
1800
  const out = res.stdout ?? "";
1801
+ if (out.includes("MINDWIRE_UPDATE_DEFERRED")) {
1802
+ if (cfg.forceDeploy) throw new MindwireError("The workspace is busy. Wait for its operations to finish before deploying the service.");
1803
+ emit2({ phase: "skip", message: "service update deferred while workspace operations are running" });
1804
+ return;
1805
+ }
1636
1806
  if (!out.includes("MINDWIRE_READY")) {
1637
1807
  throw new MindwireError(
1638
1808
  "mindwire: the in-sandbox daemon did not become healthy after deploy.\n" + (out || res.stderr || res.error || "(no output)").trim()
@@ -1647,7 +1817,11 @@ async function probeHealth(host, port, token) {
1647
1817
  if (!body) return { reachable: false };
1648
1818
  try {
1649
1819
  const j = JSON.parse(body);
1650
- return { reachable: true, version: typeof j.version === "string" ? j.version : void 0 };
1820
+ return {
1821
+ reachable: true,
1822
+ version: typeof j.version === "string" ? j.version : void 0,
1823
+ serviceUpdateVersion: typeof j.serviceUpdateVersion === "number" ? j.serviceUpdateVersion : void 0
1824
+ };
1651
1825
  } catch {
1652
1826
  return { reachable: true };
1653
1827
  }
@@ -1748,7 +1922,7 @@ var ContainerHost = class {
1748
1922
  base;
1749
1923
  containerId;
1750
1924
  exec(argv, opts) {
1751
- return this.base.exec(["docker", "exec", this.containerId, ...argv], opts);
1925
+ return this.base.exec(["docker", "exec", "--env", "MINDWIRE_ISOLATION=container", this.containerId, ...argv], opts);
1752
1926
  }
1753
1927
  async putFile(path, data, opts) {
1754
1928
  const slash = path.lastIndexOf("/");
@@ -1848,7 +2022,7 @@ async function runContainer(host, cfg, emit2) {
1848
2022
  "mindwire: running the daemon in a container needs an image (docker.image) to create one, or an existing container (docker.container) to attach to."
1849
2023
  );
1850
2024
  }
1851
- const runArgs = ["docker", "run", "-d"];
2025
+ const runArgs = ["docker", "run", "-d", "--env", "MINDWIRE_ISOLATION=container"];
1852
2026
  if (cfg.name) runArgs.push("--name", cfg.name);
1853
2027
  runArgs.push("-p", `127.0.0.1:0:${port}`);
1854
2028
  if (cfg.createArgs?.length) runArgs.push(...cfg.createArgs);
@@ -2175,7 +2349,7 @@ var DockerHost = class {
2175
2349
  container;
2176
2350
  async exec(argv) {
2177
2351
  const { Writable } = await import('stream');
2178
- const exec = await this.container.exec({ Cmd: argv, AttachStdout: true, AttachStderr: true });
2352
+ const exec = await this.container.exec({ Cmd: argv, Env: ["MINDWIRE_ISOLATION=container"], AttachStdout: true, AttachStderr: true });
2179
2353
  const stream = await exec.start({ hijack: true, stdin: false });
2180
2354
  const out = [];
2181
2355
  const err = [];
@@ -2229,7 +2403,7 @@ async function provisionDocker(docker2, config = {}, onLog) {
2229
2403
  // The runtime image starts mindwired itself. Do not replace its command: doing so turns the
2230
2404
  // image ENTRYPOINT into `mindwired sleep infinity` and prevents the runtime from starting.
2231
2405
  Image: image,
2232
- Env: [`ADDR=:${port}`, `AGENT_TYPE=${agent}`, `AGENT_CWD=${agentCwd}`],
2406
+ Env: [`ADDR=:${port}`, `AGENT_TYPE=${agent}`, `AGENT_CWD=${agentCwd}`, "MINDWIRE_ISOLATION=container"],
2233
2407
  Tty: false,
2234
2408
  ExposedPorts: { [portKey]: {} },
2235
2409
  HostConfig: { PortBindings: { [portKey]: [{ HostPort: "0" }] } },
@@ -2547,6 +2721,7 @@ function clearCatalogCache() {
2547
2721
  exports.ApiError = ApiError;
2548
2722
  exports.AuthApi = AuthApi;
2549
2723
  exports.ContainerHost = ContainerHost;
2724
+ exports.GitAccessApi = GitAccessApi;
2550
2725
  exports.Http = Http;
2551
2726
  exports.MODELS_DEV_URL = MODELS_DEV_URL;
2552
2727
  exports.McpApi = McpApi;
@@ -2559,6 +2734,7 @@ exports.ProvidersApi = ProvidersApi;
2559
2734
  exports.Run = Run;
2560
2735
  exports.RunFailedError = RunFailedError;
2561
2736
  exports.SDK_VERSION = SDK_VERSION;
2737
+ exports.ServiceApi = ServiceApi;
2562
2738
  exports.SurfacesApi = SurfacesApi;
2563
2739
  exports.TimeoutError = TimeoutError;
2564
2740
  exports.WorkspaceApi = WorkspaceApi;