@quolu/lattice 0.60.6 → 0.60.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.60.6",
3
+ "version": "0.60.7",
4
4
  "description": "Schedulability compiler for multi-agent development: observe real code boundaries, refactor the conflicting seam, recompile the plan for parallel execution",
5
5
  "author": {
6
6
  "name": "Quo / クオ at kitepon.dev",
@@ -431,8 +431,11 @@ export async function runBridgeCli({ argv, stdout, stderr, env = process.env,
431
431
  } else stdout.write(`${JSON.stringify(result(command, config, recovery, liveness, diagnostics, warnings))}\n`);
432
432
  return 0;
433
433
  } catch (error) {
434
- stderr.write(`${JSON.stringify({ schema: 'lattice.cli_error.v2',
435
- code: error?.code ?? 'BRIDGE_FAILED', message: error?.message ?? 'bridge command failed' })}\n`);
434
+ const payload = { schema: 'lattice.cli_error.v2',
435
+ code: error?.code ?? 'BRIDGE_FAILED', message: error?.message ?? 'bridge command failed' };
436
+ if (error?.detail && typeof error.detail === 'object' && !Array.isArray(error.detail)
437
+ && Object.keys(error.detail).length > 0) payload.detail = error.detail;
438
+ stderr.write(`${JSON.stringify(payload)}\n`);
436
439
  return error?.code === 'USAGE' ? 2 : 1;
437
440
  }
438
441
  }
@@ -15,10 +15,13 @@ import { bridgeRegistrarSettings } from './bridge-registrar.mjs';
15
15
  export const BRIDGE_LAUNCH_AGENT_LABEL = 'dev.kitepon.lattice.bridge';
16
16
  const START_TIMEOUT_MS = 5_000;
17
17
  const STOP_TIMEOUT_MS = 3_000;
18
+ // launchd の default ExitTimeOut は 5 秒。bootout の exit 0 はその前に返り、
19
+ // label が domain から消えるのは process が落ちた後である。
20
+ const UNLOAD_TIMEOUT_MS = 8_000;
18
21
  const execFileAsync = promisify(execFile);
19
22
 
20
- function fail(code, message, cause = undefined) {
21
- return new BridgeConfigError(code, message, undefined, cause);
23
+ function fail(code, message, cause = undefined, detail = undefined) {
24
+ return new BridgeConfigError(code, message, detail, cause);
22
25
  }
23
26
 
24
27
  function userId() {
@@ -182,6 +185,14 @@ export async function defaultLaunchctlRunner(args) {
182
185
  function domain(uid) { return `gui/${uid}`; }
183
186
  function service(uid) { return `${domain(uid)}/${BRIDGE_LAUNCH_AGENT_LABEL}`; }
184
187
 
188
+ function launchctlDetail(result) {
189
+ const stderr = typeof result?.stderr === 'string' ? result.stderr.trim() : '';
190
+ return {
191
+ launchctl_code: Number.isInteger(result?.code) ? result.code : null,
192
+ stderr: stderr.length > 0 ? stderr.slice(0, 1_024) : null,
193
+ };
194
+ }
195
+
185
196
  async function launchctl(runner, args, code, message, accepted = [0]) {
186
197
  let result;
187
198
  try { result = await runner(args); } catch (error) {
@@ -189,7 +200,7 @@ async function launchctl(runner, args, code, message, accepted = [0]) {
189
200
  throw fail(code, message, error);
190
201
  }
191
202
  if (!result || !Number.isInteger(result.code) || !accepted.includes(result.code)) {
192
- throw fail(code, message);
203
+ throw fail(code, message, undefined, launchctlDetail(result));
193
204
  }
194
205
  return result;
195
206
  }
@@ -285,10 +296,25 @@ export async function describeBridgeLaunchAgent({ snapshot } = {}) {
285
296
  };
286
297
  }
287
298
 
299
+ async function waitUnloaded({ runner, uid, timeoutMs = UNLOAD_TIMEOUT_MS }) {
300
+ const deadline = Date.now() + timeoutMs;
301
+ while (true) {
302
+ if (!await loadedState(runner, uid)) return;
303
+ if (Date.now() >= deadline) {
304
+ throw fail('BRIDGE_LAUNCHCTL_BOOTOUT_FAILED', 'bridge LaunchAgent did not unload');
305
+ }
306
+ await new Promise((resolve) => setTimeout(resolve, 50));
307
+ }
308
+ }
309
+
288
310
  async function bootoutIfLoaded({ runner, uid }) {
289
311
  if (!await loadedState(runner, uid)) return false;
290
312
  await launchctl(runner, ['bootout', service(uid)], 'BRIDGE_LAUNCHCTL_BOOTOUT_FAILED',
291
313
  'could not stop bridge LaunchAgent');
314
+ // bootout の exit 0 は unload 受付だけである。label が残ったまま bootstrap すると
315
+ // launchd は 5 Input/output error を返す(2026-08-19 実測。print が 113 になって
316
+ // から bootstrap すれば通る)。
317
+ await waitUnloaded({ runner, uid });
292
318
  return true;
293
319
  }
294
320
 
@@ -325,8 +351,7 @@ export async function disableBridgeLaunchAgent({ snapshot, listen, env = process
325
351
  }
326
352
  const refs = bridgeLaunchAgentPaths(env);
327
353
  if (snapshot.loaded) {
328
- await launchctl(runner, ['bootout', service(uid)], 'BRIDGE_LAUNCHCTL_BOOTOUT_FAILED',
329
- 'could not stop bridge LaunchAgent');
354
+ await bootoutIfLoaded({ runner, uid });
330
355
  await waitStopped({ listen, env });
331
356
  }
332
357
  await rm(refs.plist, { force: true });