@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 +1 -1
- package/src/bridge-cli.mjs +5 -2
- package/src/bridge-launch-agent.mjs +30 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quolu/lattice",
|
|
3
|
-
"version": "0.60.
|
|
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",
|
package/src/bridge-cli.mjs
CHANGED
|
@@ -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
|
-
|
|
435
|
-
code: error?.code ?? 'BRIDGE_FAILED', message: error?.message ?? 'bridge command failed' }
|
|
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,
|
|
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
|
|
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 });
|