@adhdev/daemon-core 1.0.18-rc.8 → 1.0.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/cli-adapters/provider-cli-shared.d.ts +33 -0
- package/dist/commands/cli-manager.d.ts +9 -0
- package/dist/commands/upgrade-helper.d.ts +1 -0
- package/dist/commands/windows-atomic-upgrade.d.ts +5 -0
- package/dist/config/mesh-json-config.d.ts +62 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2309 -689
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2277 -666
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-active-work.d.ts +1 -1
- package/dist/mesh/mesh-ledger.d.ts +1 -1
- package/dist/mesh/mesh-node-identity.d.ts +23 -0
- package/dist/mesh/mesh-refine-gates.d.ts +89 -0
- package/dist/mesh/mesh-remote-event-pull.d.ts +3 -0
- package/dist/mesh/mesh-work-queue.d.ts +24 -1
- package/dist/providers/auto-approve-modes.d.ts +14 -0
- package/dist/providers/chat-message-normalization.d.ts +22 -0
- package/dist/providers/cli-provider-instance-types.d.ts +2 -0
- package/dist/providers/cli-provider-instance.d.ts +76 -0
- package/dist/providers/contracts.d.ts +17 -0
- package/dist/providers/provider-schema.d.ts +1 -0
- package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +14 -0
- package/dist/providers/types/interactive-prompt.d.ts +18 -0
- package/dist/repo-mesh-types.d.ts +38 -6
- package/dist/shared-types.d.ts +4 -2
- package/package.json +3 -3
- package/src/cli-adapters/cli-state-engine.ts +17 -1
- package/src/cli-adapters/provider-cli-adapter.ts +2 -6
- package/src/cli-adapters/provider-cli-shared.ts +41 -0
- package/src/commands/cli-manager.ts +72 -8
- package/src/commands/high-family/mesh-events.ts +13 -2
- package/src/commands/med-family/mesh-crud.ts +155 -0
- package/src/commands/med-family/mesh-queue.ts +74 -1
- package/src/commands/router-refine.ts +71 -4
- package/src/commands/router.ts +8 -0
- package/src/commands/upgrade-helper.ts +78 -15
- package/src/commands/windows-atomic-upgrade.ts +146 -38
- package/src/config/mesh-json-config.ts +103 -0
- package/src/index.ts +21 -1
- package/src/mesh/coordinator-prompt.ts +2 -1
- package/src/mesh/mesh-active-work.ts +11 -1
- package/src/mesh/mesh-completion-synthesis.ts +12 -1
- package/src/mesh/mesh-event-classify.ts +19 -0
- package/src/mesh/mesh-event-forwarding.ts +64 -6
- package/src/mesh/mesh-events-utils.ts +42 -0
- package/src/mesh/mesh-ledger.ts +5 -0
- package/src/mesh/mesh-node-identity.ts +49 -0
- package/src/mesh/mesh-queue-assignment.ts +210 -11
- package/src/mesh/mesh-reconcile-loop.ts +257 -3
- package/src/mesh/mesh-refine-gates.ts +300 -0
- package/src/mesh/mesh-remote-event-pull.ts +68 -47
- package/src/mesh/mesh-work-queue.ts +85 -2
- package/src/providers/auto-approve-modes.ts +97 -0
- package/src/providers/chat-message-normalization.ts +53 -0
- package/src/providers/cli-provider-instance-types.ts +28 -0
- package/src/providers/cli-provider-instance.ts +394 -28
- package/src/providers/contracts.ts +25 -1
- package/src/providers/provider-schema.ts +83 -0
- package/src/providers/sdk/v1/builders/cli/detect-status.ts +23 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +20 -0
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +44 -0
- package/src/providers/types/interactive-prompt.ts +77 -0
- package/src/repo-mesh-types.ts +112 -12
- package/src/shared-types.ts +9 -1
- package/src/status/reporter.ts +1 -0
- package/src/status/snapshot.ts +2 -0
|
@@ -144,19 +144,68 @@ function resolveInstallPrefixFromPackageRoot(packageRoot: string, packageName: s
|
|
|
144
144
|
return maybeLibDir;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
// True when `prefix` is the bin dir of a portable Node 22 the installer manages
|
|
148
|
+
// under ~/.adhdev/tools/node22/<node-vX>/. A `npm i -g adhdev` run while that
|
|
149
|
+
// portable node is the active `node` installs adhdev into node's own default
|
|
150
|
+
// global prefix (= that dir) — a "legacy node22-prefix" install that lives at
|
|
151
|
+
// the FRONT of PATH (Enable-NodePath prepends node22) and shadows the canonical
|
|
152
|
+
// dispatcher shims in ~/.adhdev/npm-global. Because self-upgrade reuses the
|
|
153
|
+
// running prefix, that install then re-installs into the same node22 dir forever
|
|
154
|
+
// and never converts to the dispatcher. Detecting it lets us force convergence.
|
|
155
|
+
function isPortableNode22Prefix(prefix: string | null, homeDir: string): boolean {
|
|
156
|
+
if (!prefix) return false;
|
|
157
|
+
const portableRoot = path.join(homeDir, '.adhdev', 'tools', 'node22');
|
|
158
|
+
const normalizedPrefix = path.resolve(prefix).replace(/[\\/]+$/, '').toLowerCase();
|
|
159
|
+
const normalizedRoot = path.resolve(portableRoot).replace(/[\\/]+$/, '').toLowerCase();
|
|
160
|
+
return normalizedPrefix === normalizedRoot || normalizedPrefix.startsWith(`${normalizedRoot}${path.sep.toLowerCase()}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Redirect a legacy node22-prefix install to the canonical dispatcher install
|
|
164
|
+
// root so resolveWindowsInstallerLayout accepts it and the atomic-upgrade
|
|
165
|
+
// publishes the ~/.adhdev/npm-global pointer + shims. Prefer the version the
|
|
166
|
+
// dispatcher pointer already names (so we sit on the real active dispatcher
|
|
167
|
+
// prefix); otherwise synthesize a stable migration sentinel under npm-installs.
|
|
168
|
+
// resolveWindowsInstallerLayout only requires a `npm-installs/version-*` path —
|
|
169
|
+
// performWindowsAtomicUpgrade stages a fresh version- prefix of its own and uses
|
|
170
|
+
// this only as the "old prefix" to stop/clean, so a non-existent path is a no-op.
|
|
171
|
+
function canonicalDispatcherInstallPrefix(homeDir: string): string {
|
|
172
|
+
const installRoot = path.join(homeDir, '.adhdev', 'npm-installs');
|
|
173
|
+
const pointerPath = path.join(homeDir, '.adhdev', 'npm-global', '.adhdev-current');
|
|
174
|
+
try {
|
|
175
|
+
const activeVersion = fs.readFileSync(pointerPath, 'utf8').trim();
|
|
176
|
+
if (activeVersion.startsWith('version-')) return path.join(installRoot, activeVersion);
|
|
177
|
+
} catch {
|
|
178
|
+
// No dispatcher pointer yet (first migration off the legacy layout).
|
|
179
|
+
}
|
|
180
|
+
return path.join(installRoot, 'version-legacy-migrate');
|
|
181
|
+
}
|
|
182
|
+
|
|
147
183
|
export function resolveCurrentGlobalInstallSurface(options: {
|
|
148
184
|
packageName: string;
|
|
149
185
|
currentCliPath?: string;
|
|
150
186
|
nodeExecutable?: string;
|
|
151
187
|
platform?: NodeJS.Platform;
|
|
188
|
+
homeDir?: string;
|
|
152
189
|
}): CurrentGlobalInstallSurface {
|
|
153
190
|
const packageRoot = findCurrentPackageRoot(options.currentCliPath || process.argv[1], options.packageName);
|
|
154
191
|
const npmInvocation = resolveSiblingNpmInvocation(options.nodeExecutable || process.execPath, options.platform);
|
|
192
|
+
const platform = options.platform || process.platform;
|
|
193
|
+
const homeDir = options.homeDir || os.homedir();
|
|
194
|
+
let installPrefix = packageRoot ? resolveInstallPrefixFromPackageRoot(packageRoot, options.packageName) : null;
|
|
195
|
+
// FIX C: on Windows, never let a self-upgrade perpetuate the legacy
|
|
196
|
+
// node22-prefix install. If the running adhdev lives under ~/.adhdev/tools/
|
|
197
|
+
// node22, force the install onto the canonical dispatcher prefix so the update
|
|
198
|
+
// converges to the ~/.adhdev/npm-global pointer + shims. Scoped to win32 AND a
|
|
199
|
+
// tools/node22 prefix so npm-linked dev / standalone / real dispatcher installs
|
|
200
|
+
// are untouched.
|
|
201
|
+
if (platform === 'win32' && isPortableNode22Prefix(installPrefix, homeDir)) {
|
|
202
|
+
installPrefix = canonicalDispatcherInstallPrefix(homeDir);
|
|
203
|
+
}
|
|
155
204
|
return {
|
|
156
205
|
npmExecutable: npmInvocation.executable,
|
|
157
206
|
npmArgsPrefix: npmInvocation.argsPrefix,
|
|
158
207
|
packageRoot,
|
|
159
|
-
installPrefix
|
|
208
|
+
installPrefix,
|
|
160
209
|
execOptions: npmInvocation.execOptions,
|
|
161
210
|
};
|
|
162
211
|
}
|
|
@@ -560,22 +609,36 @@ async function runDaemonUpgradeHelper(payload: DaemonUpgradeHelperPayload): Prom
|
|
|
560
609
|
);
|
|
561
610
|
}
|
|
562
611
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
targetVersion: payload.targetVersion,
|
|
567
|
-
portableNode,
|
|
568
|
-
excludePids: upgradePids,
|
|
569
|
-
hooks: createDefaultWindowsAtomicHooks({
|
|
612
|
+
try {
|
|
613
|
+
await performWindowsAtomicUpgrade({
|
|
614
|
+
layout: windowsInstallerLayout,
|
|
570
615
|
packageName: payload.packageName,
|
|
571
616
|
targetVersion: payload.targetVersion,
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
617
|
+
portableNode,
|
|
618
|
+
excludePids: upgradePids,
|
|
619
|
+
hooks: createDefaultWindowsAtomicHooks({
|
|
620
|
+
packageName: payload.packageName,
|
|
621
|
+
targetVersion: payload.targetVersion,
|
|
622
|
+
npmCliPath,
|
|
623
|
+
restartArgv,
|
|
624
|
+
cwd: payload.cwd || process.cwd(),
|
|
625
|
+
env: Object.fromEntries(Object.entries(process.env).filter(([key]) => key !== UPGRADE_HELPER_ENV)),
|
|
626
|
+
log: appendUpgradeLog,
|
|
627
|
+
}),
|
|
628
|
+
});
|
|
629
|
+
} catch (error: any) {
|
|
630
|
+
// performWindowsAtomicUpgrade already rolled the pointer/shims back to the
|
|
631
|
+
// prior version before rethrowing. Without a durable notice that rollback
|
|
632
|
+
// was silent — the daemon simply kept running the old version (the rc.6
|
|
633
|
+
// stuck-upgrade defect). Leave an actionable last-error file naming the
|
|
634
|
+
// target that failed its health/version gate.
|
|
635
|
+
emitUpgradeFailureNotice([
|
|
636
|
+
`adhdev ${payload.packageName}@${payload.targetVersion} upgrade failed and was rolled back: ${error?.message || String(error)}`,
|
|
637
|
+
`Previous version preserved (active prefix: ${windowsInstallerLayout.activePrefix}).`,
|
|
638
|
+
'See daemon-upgrade.log for the full install/health trace. The next daemon start will retry.',
|
|
639
|
+
]);
|
|
640
|
+
throw error;
|
|
641
|
+
}
|
|
579
642
|
try { fs.unlinkSync(getUpgradeFailureNoticePath()); } catch { /* no previous failure notice */ }
|
|
580
643
|
appendUpgradeLog('Installer-managed Windows atomic upgrade completed');
|
|
581
644
|
return;
|
|
@@ -8,6 +8,53 @@ const POINTER_NAME = '.adhdev-current';
|
|
|
8
8
|
const STABLE_FILES = [POINTER_NAME, 'adhdev.cmd', 'adhdev.ps1', 'adhdev'] as const;
|
|
9
9
|
const DEFAULT_HEALTH_PORT = 19222;
|
|
10
10
|
|
|
11
|
+
// How long to wait for the replacement daemon to report the target version
|
|
12
|
+
// before deterministically rolling back. status.version only appears AFTER the
|
|
13
|
+
// daemon fully boots its components — a separate session-host process, node-pty/
|
|
14
|
+
// conpty, CDP, and providers — which on a Windows cold self-upgrade routinely
|
|
15
|
+
// exceeds the old 30s ceiling. Live Windows logs showed every rollback clustering
|
|
16
|
+
// at 33–35s (30s timeout + poll overhead), so the gate was tripping on slow-boot,
|
|
17
|
+
// not on genuine failure. 120s reflects real Windows self-upgrade cold-start time.
|
|
18
|
+
export const DEFAULT_HEALTH_TIMEOUT_MS = 120_000;
|
|
19
|
+
|
|
20
|
+
function fetchLocalJson(port: number, pathname: string): Promise<{ ok: boolean; body: string }> {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const req = http.get(`http://127.0.0.1:${port}${pathname}`, { timeout: 1500 }, (res) => {
|
|
23
|
+
let body = '';
|
|
24
|
+
res.setEncoding('utf8');
|
|
25
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
26
|
+
res.on('end', () => resolve({ ok: res.statusCode === 200, body }));
|
|
27
|
+
});
|
|
28
|
+
req.on('timeout', () => req.destroy());
|
|
29
|
+
req.on('error', () => resolve({ ok: false, body: '' }));
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Liveness + pid identity: GET /health returns {ok, pid, wsPath, port}.
|
|
34
|
+
async function fetchLocalHealth(port: number): Promise<{ ok: boolean; pid?: number }> {
|
|
35
|
+
const { ok, body } = await fetchLocalJson(port, '/health');
|
|
36
|
+
if (!ok) return { ok: false };
|
|
37
|
+
try {
|
|
38
|
+
const pid = Number(JSON.parse(body)?.pid);
|
|
39
|
+
return { ok: true, pid: Number.isFinite(pid) ? pid : undefined };
|
|
40
|
+
} catch {
|
|
41
|
+
return { ok: false };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Running version: GET /api/v1/status exposes it at payload.status.version.
|
|
46
|
+
// /health carries no version, so the upgrade version gate must read this.
|
|
47
|
+
async function fetchLocalStatusVersion(port: number): Promise<string | undefined> {
|
|
48
|
+
const { ok, body } = await fetchLocalJson(port, '/api/v1/status');
|
|
49
|
+
if (!ok) return undefined;
|
|
50
|
+
try {
|
|
51
|
+
const version = (JSON.parse(body) as { status?: { version?: unknown } })?.status?.version;
|
|
52
|
+
return typeof version === 'string' ? version : undefined;
|
|
53
|
+
} catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
11
58
|
// Markers that identify a Node process as ADHDev-owned when its command line
|
|
12
59
|
// also lives under a versioned install prefix. This deliberately excludes
|
|
13
60
|
// arbitrary user scripts that happen to be located under ~/.adhdev.
|
|
@@ -272,10 +319,39 @@ function snapshotStableFiles(stablePrefix: string): Map<string, FileSnapshot> {
|
|
|
272
319
|
return snapshots;
|
|
273
320
|
}
|
|
274
321
|
|
|
275
|
-
|
|
322
|
+
// Rollback must never leave PATH `adhdev` broken. Two failure modes made the old
|
|
323
|
+
// "restore-or-delete" logic dangerous:
|
|
324
|
+
// 1. If a stable shim (adhdev.cmd/.ps1/no-ext) did not exist at snapshot time —
|
|
325
|
+
// a first/partial install, or a stable tree that never had the launcher —
|
|
326
|
+
// deleting it leaves `where.exe adhdev` / `spawnSync('adhdev')` resolving to
|
|
327
|
+
// nothing (ENOENT). `adhdev doctor` then reports the runtime surface broken.
|
|
328
|
+
// 2. If the pointer (.adhdev-current) was absent at snapshot time, deleting it
|
|
329
|
+
// strands the re-published shims with no version to redirect to.
|
|
330
|
+
// So rollback (re)guarantees a valid launcher surface: existing snapshots restore
|
|
331
|
+
// their original bytes atomically; missing shims are re-issued from the canonical
|
|
332
|
+
// pointer-redirect launcher contents; and the pointer, when it has no snapshot,
|
|
333
|
+
// is re-written to the last-known-good active version instead of removed.
|
|
334
|
+
function restoreStableFiles(snapshots: Map<string, FileSnapshot>, layout: WindowsInstallerLayout): void {
|
|
335
|
+
const shims = stableShimContents();
|
|
276
336
|
for (const [target, snapshot] of snapshots) {
|
|
277
|
-
if (snapshot.exists && snapshot.data)
|
|
278
|
-
|
|
337
|
+
if (snapshot.exists && snapshot.data) {
|
|
338
|
+
atomicWrite(target, snapshot.data.toString('binary'), 'binary');
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const name = path.basename(target);
|
|
342
|
+
if (name === 'adhdev.cmd' || name === 'adhdev.ps1' || name === 'adhdev') {
|
|
343
|
+
// Re-issue a valid pointer-redirect launcher rather than deleting it, so
|
|
344
|
+
// PATH `adhdev` always resolves after a rollback.
|
|
345
|
+
atomicWrite(target, shims[name].content, shims[name].encoding);
|
|
346
|
+
} else if (name === POINTER_NAME) {
|
|
347
|
+
// Preserve the last-successful (currently active) version so the redirect
|
|
348
|
+
// launchers still reach a real prefix. Only fall back to deleting when we
|
|
349
|
+
// have no active version to point at.
|
|
350
|
+
if (layout.activeVersionName) atomicWrite(target, layout.activeVersionName, 'ascii');
|
|
351
|
+
else try { fs.unlinkSync(target); } catch { /* noop */ }
|
|
352
|
+
} else {
|
|
353
|
+
try { fs.unlinkSync(target); } catch { /* noop */ }
|
|
354
|
+
}
|
|
279
355
|
}
|
|
280
356
|
}
|
|
281
357
|
|
|
@@ -339,7 +415,7 @@ export async function performWindowsAtomicUpgrade(options: WindowsAtomicUpgradeO
|
|
|
339
415
|
if (restarted?.pid) hooks.stopProcess(restarted.pid);
|
|
340
416
|
if (activated) {
|
|
341
417
|
try {
|
|
342
|
-
restoreStableFiles(snapshots);
|
|
418
|
+
restoreStableFiles(snapshots, layout);
|
|
343
419
|
hooks.log(`Rolled back activation to ${layout.activeVersionName}`);
|
|
344
420
|
} catch (rollbackError: any) {
|
|
345
421
|
hooks.log(`Stable-file rollback failed: ${rollbackError?.message || String(rollbackError)}`);
|
|
@@ -359,7 +435,13 @@ export function createDefaultWindowsAtomicHooks(options: {
|
|
|
359
435
|
cwd: string;
|
|
360
436
|
env: NodeJS.ProcessEnv;
|
|
361
437
|
log: (message: string) => void;
|
|
438
|
+
/** Loopback IPC port to probe for health/version. Defaults to the daemon's local IPC port. */
|
|
439
|
+
healthPort?: number;
|
|
440
|
+
/** How long to poll for the replacement daemon to report the target version. */
|
|
441
|
+
healthTimeoutMs?: number;
|
|
362
442
|
}): WindowsAtomicUpgradeHooks {
|
|
443
|
+
const healthPort = options.healthPort ?? DEFAULT_HEALTH_PORT;
|
|
444
|
+
const healthTimeoutMs = options.healthTimeoutMs ?? DEFAULT_HEALTH_TIMEOUT_MS;
|
|
363
445
|
return {
|
|
364
446
|
install: (stagedPrefix, portableNode) => {
|
|
365
447
|
const env: NodeJS.ProcessEnv = {
|
|
@@ -370,7 +452,7 @@ export function createDefaultWindowsAtomicHooks(options: {
|
|
|
370
452
|
};
|
|
371
453
|
const pathKey = Object.keys(env).find((key) => key.toLowerCase() === 'path') || 'Path';
|
|
372
454
|
env[pathKey] = `${path.dirname(portableNode)};${env[pathKey] || ''}`;
|
|
373
|
-
execFileSync(portableNode, [
|
|
455
|
+
const installOutput = String(execFileSync(portableNode, [
|
|
374
456
|
options.npmCliPath,
|
|
375
457
|
'install', '-g', `${options.packageName}@${options.targetVersion}`, '--force', '--prefer-online', '--prefix', stagedPrefix,
|
|
376
458
|
], {
|
|
@@ -379,7 +461,11 @@ export function createDefaultWindowsAtomicHooks(options: {
|
|
|
379
461
|
maxBuffer: 20 * 1024 * 1024,
|
|
380
462
|
windowsHide: true,
|
|
381
463
|
env,
|
|
382
|
-
});
|
|
464
|
+
}));
|
|
465
|
+
// On failure execFileSync throws with stdout attached to the Error; on
|
|
466
|
+
// success it was previously discarded. Surface the npm output either way so
|
|
467
|
+
// a silently-succeeding-then-rolled-back upgrade leaves a diagnosable trail.
|
|
468
|
+
if (installOutput.trim()) options.log(installOutput.trim());
|
|
383
469
|
},
|
|
384
470
|
restart: (portableNode, stagedCliEntry) => {
|
|
385
471
|
const restartArgv = options.restartArgv.map((arg, index) => index === 0 ? stagedCliEntry : arg);
|
|
@@ -406,25 +492,42 @@ export function createDefaultWindowsAtomicHooks(options: {
|
|
|
406
492
|
child.unref();
|
|
407
493
|
},
|
|
408
494
|
waitForHealth: async (pid, targetVersion) => {
|
|
409
|
-
const
|
|
495
|
+
const startedAt = Date.now();
|
|
496
|
+
const deadline = startedAt + healthTimeoutMs;
|
|
497
|
+
let attempt = 0;
|
|
498
|
+
// Log the transition milestones exactly once so the daemon log shows how far
|
|
499
|
+
// the replacement got before the gate resolved: not-yet-alive → alive but
|
|
500
|
+
// version-not-yet-ready (components still booting) → version matched. This is
|
|
501
|
+
// what pins down which boot stage the full-boot budget is being spent in.
|
|
502
|
+
let loggedAlive = false;
|
|
503
|
+
let loggedVersionPending = false;
|
|
410
504
|
while (Date.now() < deadline) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
505
|
+
attempt += 1;
|
|
506
|
+
// Liveness + pid identity come from GET /health, whose body is
|
|
507
|
+
// {ok, pid, wsPath, port} — it carries NO version. The running version
|
|
508
|
+
// lives only in GET /api/v1/status → payload.status.version, so the
|
|
509
|
+
// version gate must fetch that endpoint separately. Requiring the raw
|
|
510
|
+
// /health body to include targetVersion is unsatisfiable and silently
|
|
511
|
+
// rolls every upgrade back.
|
|
512
|
+
const liveness = await fetchLocalHealth(healthPort);
|
|
513
|
+
const alive = liveness.ok && liveness.pid === pid;
|
|
514
|
+
const version = alive ? await fetchLocalStatusVersion(healthPort) : undefined;
|
|
515
|
+
const elapsedMs = Date.now() - startedAt;
|
|
516
|
+
if (alive && version === targetVersion) {
|
|
517
|
+
options.log(`Health gate passed after ${elapsedMs}ms (${attempt} probe(s)): pid ${pid} reports ${targetVersion}`);
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
if (alive && !loggedAlive) {
|
|
521
|
+
loggedAlive = true;
|
|
522
|
+
options.log(`Health gate: replacement pid ${pid} is alive at ${elapsedMs}ms; awaiting status.version (components still booting)`);
|
|
523
|
+
}
|
|
524
|
+
if (alive && version && version !== targetVersion && !loggedVersionPending) {
|
|
525
|
+
loggedVersionPending = true;
|
|
526
|
+
options.log(`Health gate: replacement reports version ${version} (want ${targetVersion}) at ${elapsedMs}ms`);
|
|
527
|
+
}
|
|
426
528
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
427
529
|
}
|
|
530
|
+
options.log(`Health gate timed out after ${Date.now() - startedAt}ms (${attempt} probe(s), budget ${healthTimeoutMs}ms) waiting for pid ${pid} to report ${targetVersion}`);
|
|
428
531
|
return false;
|
|
429
532
|
},
|
|
430
533
|
stopProcess: (pid) => {
|
|
@@ -459,13 +562,19 @@ export function boundedCleanupInactivePrefixes(
|
|
|
459
562
|
return;
|
|
460
563
|
}
|
|
461
564
|
if (candidates.length === 0) return;
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
565
|
+
// Delete in-process (see removeInactivePrefix) rather than via a powershell.exe
|
|
566
|
+
// Remove-Item batch under a 5000ms spawnSync timeout, which ETIMEDOUT'd on
|
|
567
|
+
// Windows and left orphan version-* prefixes behind. best-effort: a failure
|
|
568
|
+
// here never blocks the upgrade success/failure signal.
|
|
569
|
+
let incomplete = false;
|
|
570
|
+
for (const candidate of candidates) {
|
|
571
|
+
try {
|
|
572
|
+
fs.rmSync(candidate, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
|
|
573
|
+
} catch {
|
|
574
|
+
incomplete = true;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (incomplete) log('Bounded inactive-prefix cleanup was incomplete; future updates will retry');
|
|
469
578
|
}
|
|
470
579
|
|
|
471
580
|
export async function cleanupInactivePrefixesWithGuard(options: {
|
|
@@ -507,16 +616,15 @@ export async function cleanupInactivePrefixesWithGuard(options: {
|
|
|
507
616
|
}
|
|
508
617
|
|
|
509
618
|
function removeInactivePrefix(target: string, log?: (message: string) => void): void {
|
|
619
|
+
// Delete in-process with fs.rmSync instead of shelling out to powershell.exe.
|
|
620
|
+
// A version prefix holds thousands of small files (node_modules, node-pty
|
|
621
|
+
// prebuilds); Remove-Item -Recurse -Force over that, plus PowerShell 5.1's
|
|
622
|
+
// cold-start, routinely blew past the old 5000ms spawnSync timeout on Windows
|
|
623
|
+
// — every candidate then failed with ETIMEDOUT and orphan version-* dirs
|
|
624
|
+
// accumulated. fs.rmSync has no process spawn, no timeout, and retries the
|
|
625
|
+
// transient EBUSY/EPERM/ENOTEMPTY that a just-stopped process can leave behind.
|
|
510
626
|
try {
|
|
511
|
-
|
|
512
|
-
const script = `if (Test-Path -LiteralPath ${escaped}) { Remove-Item -LiteralPath ${escaped} -Recurse -Force }`;
|
|
513
|
-
const encoded = Buffer.from(script, 'utf16le').toString('base64');
|
|
514
|
-
const result = spawnSync('powershell.exe', [
|
|
515
|
-
'-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', encoded,
|
|
516
|
-
], { timeout: 5000, windowsHide: true, stdio: 'ignore' });
|
|
517
|
-
if (result.error || result.status !== 0) {
|
|
518
|
-
log?.(`Failed to remove inactive prefix ${target}: ${result.error?.message || `exit ${result.status}`}`);
|
|
519
|
-
}
|
|
627
|
+
fs.rmSync(target, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
|
|
520
628
|
} catch (error: any) {
|
|
521
629
|
log?.(`Failed to remove inactive prefix ${target}: ${error?.message || String(error)}`);
|
|
522
630
|
}
|
|
@@ -19,6 +19,18 @@
|
|
|
19
19
|
* machine-local policy directly. The repo file shapes the coordinator prompt and
|
|
20
20
|
* operating notes, nothing else.
|
|
21
21
|
*
|
|
22
|
+
* `providerDefaults` IS NOT POLICY EITHER. It is a repo-shared DECLARATIVE
|
|
23
|
+
* "requested auto-approve mode" per providerType — the answer to "WHEN a delegated
|
|
24
|
+
* worker of type X is auto-approved, WHICH mode should it use by default". It has
|
|
25
|
+
* NO say over WHETHER auto-approve is enabled: the ENABLE decision (and the
|
|
26
|
+
* dangerous-mode opt-in) remains 100% machine-local (meshes.json /
|
|
27
|
+
* RepoMeshPolicy). A repo can declare `providerDefaults` freely; a machine that
|
|
28
|
+
* has delegatedWorkerAutoApprove=false still gets no auto-approve, and a dangerous
|
|
29
|
+
* requested mode is still downgraded to PTY parsing unless the machine opts in.
|
|
30
|
+
* The requested mode ID is validated at runtime against the provider spec — an
|
|
31
|
+
* unknown mode ID is IGNORED (fall back to the provider's own default), never
|
|
32
|
+
* silently coerced into a dangerous mode.
|
|
33
|
+
*
|
|
22
34
|
* The coordinator/operatingNotes merge is **in-memory only**: the on-disk
|
|
23
35
|
* machine-local `meshes.json` is never mutated by this module.
|
|
24
36
|
*
|
|
@@ -60,6 +72,19 @@ export interface RepoMeshDeclarativeLimits {
|
|
|
60
72
|
maxNotes?: number;
|
|
61
73
|
}
|
|
62
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Repo-shared declarative per-provider defaults. NOT policy — see the file header.
|
|
77
|
+
* `autoApproveModes` maps a providerType (e.g. "claude-cli") to the auto-approve
|
|
78
|
+
* mode ID that a delegated worker of that type should REQUEST when it is
|
|
79
|
+
* auto-approved. The map only influences WHICH mode is used, never WHETHER
|
|
80
|
+
* auto-approve is enabled (that stays machine-local). Mode IDs are validated
|
|
81
|
+
* against the live provider spec at resolve time; an unknown ID is ignored.
|
|
82
|
+
*/
|
|
83
|
+
export interface RepoMeshDeclarativeProviderDefaults {
|
|
84
|
+
/** providerType → requested auto-approve mode ID. */
|
|
85
|
+
autoApproveModes?: Record<string, string>;
|
|
86
|
+
}
|
|
87
|
+
|
|
63
88
|
/**
|
|
64
89
|
* Parsed + normalized `.adhdev/mesh.json` shape. Every field is optional except
|
|
65
90
|
* version so a repo can declare only the zone(s) it cares about. Policy is NOT a
|
|
@@ -70,6 +95,8 @@ export interface RepoMeshDeclarativeConfig {
|
|
|
70
95
|
coordinator?: RepoMeshDeclarativeCoordinatorConfig;
|
|
71
96
|
operatingNotes?: CoordinatorOperatingNote[];
|
|
72
97
|
limits?: RepoMeshDeclarativeLimits;
|
|
98
|
+
/** Repo-shared per-provider defaults (requested auto-approve mode). NOT policy. */
|
|
99
|
+
providerDefaults?: RepoMeshDeclarativeProviderDefaults;
|
|
73
100
|
}
|
|
74
101
|
|
|
75
102
|
export interface RepoMeshJsonConfigLoadResult {
|
|
@@ -132,6 +159,20 @@ export const MESH_JSON_CONFIG_SCHEMA = {
|
|
|
132
159
|
maxNotes: { type: 'number', minimum: 1 },
|
|
133
160
|
},
|
|
134
161
|
},
|
|
162
|
+
providerDefaults: {
|
|
163
|
+
type: 'object',
|
|
164
|
+
additionalProperties: false,
|
|
165
|
+
properties: {
|
|
166
|
+
// providerType → requested auto-approve mode ID. Mode IDs are
|
|
167
|
+
// validated against the live provider spec at resolve time; an
|
|
168
|
+
// unknown ID is ignored (provider default is used), so the schema
|
|
169
|
+
// only constrains the shape (string→string), not the ID values.
|
|
170
|
+
autoApproveModes: {
|
|
171
|
+
type: 'object',
|
|
172
|
+
additionalProperties: { type: 'string' },
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
135
176
|
},
|
|
136
177
|
} as const;
|
|
137
178
|
|
|
@@ -226,6 +267,38 @@ export function normalizeRepoMeshDeclarativeConfig(parsed: unknown): {
|
|
|
226
267
|
}
|
|
227
268
|
}
|
|
228
269
|
|
|
270
|
+
// providerDefaults: shape-only normalization. We keep every string→string
|
|
271
|
+
// (providerType → requested mode ID) entry after trimming; we deliberately do
|
|
272
|
+
// NOT validate the mode IDs against any provider spec here — the normalizer has
|
|
273
|
+
// no provider registry, and (crucially) validity depends on the LIVE provider
|
|
274
|
+
// at resolve time. The runtime resolver (resolveDelegatedWorkerAutoApprove)
|
|
275
|
+
// checks the requested ID against provider.autoApproveModes.modes and falls
|
|
276
|
+
// back to the provider's own default when the ID is unknown. This keeps the
|
|
277
|
+
// config fail-closed: a stale/typo'd mode ID never coerces into a dangerous
|
|
278
|
+
// mode, it just means "no repo override → provider default".
|
|
279
|
+
if (parsed.providerDefaults !== undefined) {
|
|
280
|
+
if (isRecord(parsed.providerDefaults)) {
|
|
281
|
+
const pd: RepoMeshDeclarativeProviderDefaults = {};
|
|
282
|
+
const rawModes = (parsed.providerDefaults as Record<string, unknown>).autoApproveModes;
|
|
283
|
+
if (rawModes !== undefined) {
|
|
284
|
+
if (isRecord(rawModes)) {
|
|
285
|
+
const modes: Record<string, string> = {};
|
|
286
|
+
for (const [providerType, modeId] of Object.entries(rawModes)) {
|
|
287
|
+
const type = typeof providerType === 'string' ? providerType.trim() : '';
|
|
288
|
+
const id = typeof modeId === 'string' ? modeId.trim() : '';
|
|
289
|
+
if (type && id) modes[type] = id;
|
|
290
|
+
}
|
|
291
|
+
if (Object.keys(modes).length) pd.autoApproveModes = modes;
|
|
292
|
+
} else {
|
|
293
|
+
errors.push('providerDefaults.autoApproveModes must be an object when provided');
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (Object.keys(pd).length) config.providerDefaults = pd;
|
|
297
|
+
} else {
|
|
298
|
+
errors.push('providerDefaults must be an object when provided');
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
229
302
|
return { valid: true, config, errors };
|
|
230
303
|
}
|
|
231
304
|
|
|
@@ -364,6 +437,21 @@ export function applyRepoMeshConfig<T extends Pick<LocalMeshEntry, 'coordinator'
|
|
|
364
437
|
* exported — it is machine-local only and has no place in mesh.json. Operating
|
|
365
438
|
* notes are intentionally NOT exported either: those are runtime ledger lessons,
|
|
366
439
|
* and a repo should declare baseline notes deliberately.
|
|
440
|
+
*
|
|
441
|
+
* `providerDefaults` is NOT auto-exported from the machine-local mesh (there is no
|
|
442
|
+
* machine-local source for it — it is a repo-authored declaration). To help an
|
|
443
|
+
* operator hand-author it, the scaffold carries a commented example shape:
|
|
444
|
+
*
|
|
445
|
+
* "providerDefaults": {
|
|
446
|
+
* "autoApproveModes": {
|
|
447
|
+
* "claude-cli": "accept-edits", // requested mode WHEN auto-approve is on
|
|
448
|
+
* "codex-cli": "auto" // (enable/dangerous opt-in stays machine-local)
|
|
449
|
+
* }
|
|
450
|
+
* }
|
|
451
|
+
*
|
|
452
|
+
* The example is surfaced via the scaffold's `_providerDefaultsExample` hint (JSON
|
|
453
|
+
* has no comments) rather than a live `providerDefaults` value, so serializing the
|
|
454
|
+
* scaffold never writes an unwanted requested-mode map into the repo file.
|
|
367
455
|
*/
|
|
368
456
|
export function buildMeshJsonConfigScaffold(
|
|
369
457
|
mesh: Pick<LocalMeshEntry, 'coordinator'>,
|
|
@@ -378,6 +466,21 @@ export function buildMeshJsonConfigScaffold(
|
|
|
378
466
|
return scaffold;
|
|
379
467
|
}
|
|
380
468
|
|
|
469
|
+
/**
|
|
470
|
+
* A copy-paste example of the `providerDefaults` zone for operators hand-authoring
|
|
471
|
+
* a repo `.adhdev/mesh.json`. Returned by the export/write commands as a separate
|
|
472
|
+
* hint field (never merged into the serialized scaffold) so the repo file stays
|
|
473
|
+
* clean. The mode IDs shown are illustrative — a repo should use IDs that exist in
|
|
474
|
+
* its own providers' specs; an unknown ID is ignored at resolve time and the
|
|
475
|
+
* provider's own default is used.
|
|
476
|
+
*/
|
|
477
|
+
export const MESH_JSON_PROVIDER_DEFAULTS_EXAMPLE: RepoMeshDeclarativeProviderDefaults = {
|
|
478
|
+
autoApproveModes: {
|
|
479
|
+
'claude-cli': 'accept-edits',
|
|
480
|
+
'codex-cli': 'auto',
|
|
481
|
+
},
|
|
482
|
+
};
|
|
483
|
+
|
|
381
484
|
/** Serialize a scaffold to the canonical 2-space JSON draft text. */
|
|
382
485
|
export function serializeMeshJsonConfigScaffold(config: RepoMeshDeclarativeConfig): string {
|
|
383
486
|
return JSON.stringify(config, null, 2);
|
package/src/index.ts
CHANGED
|
@@ -143,6 +143,8 @@ export type {
|
|
|
143
143
|
export {
|
|
144
144
|
DEFAULT_MESH_POLICY,
|
|
145
145
|
resolveDelegatedWorkerAutoApprove,
|
|
146
|
+
delegatedWorkerAutoApproveSettings,
|
|
147
|
+
resolveDelegatedWorkerDangerousModeAllow,
|
|
146
148
|
resolveAllowSendKeysDestructive,
|
|
147
149
|
resolveMagiSessionCleanupMode,
|
|
148
150
|
magiAutoLaunchedSessionCleanupDecision,
|
|
@@ -161,6 +163,24 @@ export {
|
|
|
161
163
|
resolveAutoConvergeCodeChange,
|
|
162
164
|
} from './repo-mesh-types.js';
|
|
163
165
|
|
|
166
|
+
// ── Repo-shared declarative mesh config (.adhdev/mesh.json) ──
|
|
167
|
+
export {
|
|
168
|
+
loadRepoMeshJsonConfig,
|
|
169
|
+
normalizeRepoMeshDeclarativeConfig,
|
|
170
|
+
buildMeshJsonConfigScaffold,
|
|
171
|
+
serializeMeshJsonConfigScaffold,
|
|
172
|
+
MESH_JSON_CONFIG_LOCATIONS,
|
|
173
|
+
MESH_JSON_CONFIG_SCHEMA,
|
|
174
|
+
MESH_JSON_PROVIDER_DEFAULTS_EXAMPLE,
|
|
175
|
+
} from './config/mesh-json-config.js';
|
|
176
|
+
export type {
|
|
177
|
+
RepoMeshDeclarativeConfig,
|
|
178
|
+
RepoMeshDeclarativeCoordinatorConfig,
|
|
179
|
+
RepoMeshDeclarativeLimits,
|
|
180
|
+
RepoMeshDeclarativeProviderDefaults,
|
|
181
|
+
RepoMeshJsonConfigLoadResult,
|
|
182
|
+
} from './config/mesh-json-config.js';
|
|
183
|
+
|
|
164
184
|
// ── Git Surface ──
|
|
165
185
|
export * from './git/index.js';
|
|
166
186
|
|
|
@@ -489,7 +509,7 @@ export { ProviderInstanceManager } from './providers/provider-instance-manager.j
|
|
|
489
509
|
export { IdeProviderInstance } from './providers/ide-provider-instance.js';
|
|
490
510
|
export { CliProviderInstance } from './providers/cli-provider-instance.js';
|
|
491
511
|
export { AcpProviderInstance } from './providers/acp-provider-instance.js';
|
|
492
|
-
export type { ProviderModule, CdpTargetFilter, ProviderResumeCapability, InputEnvelope, InputPart, MessagePart, ReadChatTurnStatus, ControlListResult, ControlSetResult, ControlInvokeResult } from './providers/contracts.js';
|
|
512
|
+
export type { ProviderModule, AutoApproveMode, AutoApproveModesConfig, AutoApproveModeRisk, AutoApproveModeStrategy, CdpTargetFilter, ProviderResumeCapability, InputEnvelope, InputPart, MessagePart, ReadChatTurnStatus, ControlListResult, ControlSetResult, ControlInvokeResult } from './providers/contracts.js';
|
|
493
513
|
export type { ProviderSourceConfigSnapshot, ProviderSourceConfigUpdate } from './config/provider-source-config.js';
|
|
494
514
|
export { parseProviderSourceConfigUpdate } from './config/provider-source-config.js';
|
|
495
515
|
export { normalizeInputEnvelope, normalizeMessageParts, flattenMessageParts } from './providers/io-contracts.js';
|
|
@@ -1007,7 +1007,8 @@ const TOOLS_SECTION = `## Available Tools
|
|
|
1007
1007
|
| \`mesh_fast_forward_node\` | Safely dry-run or explicitly execute an obvious clean fast-forward without launching an agent session |
|
|
1008
1008
|
| \`mesh_restart_daemon\` | Update a node's daemon to the latest published version on its channel and restart it (the dashboard "preview update" path, as a mesh command) |
|
|
1009
1009
|
| \`mesh_checkpoint\` | Create a git checkpoint on a node |
|
|
1010
|
-
| \`mesh_approve\` | Approve/reject a pending agent action |
|
|
1010
|
+
| \`mesh_approve\` | Approve/reject a pending agent action (a yes/no tool-consent modal) |
|
|
1011
|
+
| \`mesh_answer_question\` | Answer a delegated session's multi-choice QUESTION (AskUserQuestion / status awaiting_choice). NOT an approval — it offers labelled options (possibly multi-select or freeform). Pass the promptId from the agent:waiting_choice event + one answer per question (select by option label or 1-based index). Use this, never mesh_approve, for a question |
|
|
1011
1012
|
| \`mesh_list_pending_approvals\` | List every session across the mesh awaiting an approval decision (the approval inbox) — read-only; enumerate all blocked sessions at once, then drive a mesh_approve for each |
|
|
1012
1013
|
| \`mesh_clone_node\` | Create a worktree node for isolated parallel branch work |
|
|
1013
1014
|
| \`mesh_refine_node\` | Validate and merge a completed worktree node back into its base branch |
|
|
@@ -5,7 +5,7 @@ import { deleteDirectDispatchesByTaskId } from './mesh-work-queue.js';
|
|
|
5
5
|
import { meshNodeIdMatches, daemonIdsEquivalent, sessionIdsEquivalent } from '@adhdev/mesh-shared';
|
|
6
6
|
|
|
7
7
|
export type MeshActiveWorkSource = 'queue' | 'direct';
|
|
8
|
-
export type MeshActiveWorkStatus = 'pending' | 'assigned' | 'generating' | 'idle' | 'failed' | 'awaiting_approval';
|
|
8
|
+
export type MeshActiveWorkStatus = 'pending' | 'assigned' | 'generating' | 'idle' | 'failed' | 'awaiting_approval' | 'awaiting_choice';
|
|
9
9
|
|
|
10
10
|
export interface MeshActiveWorkRecord {
|
|
11
11
|
taskId: string;
|
|
@@ -135,6 +135,10 @@ export function sessionStatusFromNodes(nodes: any[] | undefined, nodeId?: string
|
|
|
135
135
|
if (!session) return { staleReason: 'direct task session is not present in live session records' };
|
|
136
136
|
if (typeof session === 'string') return {};
|
|
137
137
|
const raw = `${readString(session.status) || ''} ${readString(session.lifecycle) || ''} ${readString(session.state) || ''} ${readString(session.activeChat?.status) || ''}`.toLowerCase();
|
|
138
|
+
// A question picker surfaces as waiting_choice — distinct from an approval modal.
|
|
139
|
+
// Check it first so a question worker is not mislabeled awaiting_approval and
|
|
140
|
+
// pulled into the approval inbox (mission f1d25e11).
|
|
141
|
+
if (raw.includes('waiting_choice') || raw.includes('choice')) return { status: 'awaiting_choice' };
|
|
138
142
|
if (raw.includes('approval')) return { status: 'awaiting_approval' };
|
|
139
143
|
if (raw.includes('generating') || raw.includes('running') || raw.includes('busy')) return { status: 'generating' };
|
|
140
144
|
if (raw.includes('failed') || raw.includes('stopped') || raw.includes('terminated') || raw.includes('exited')) return { status: 'failed' };
|
|
@@ -166,6 +170,10 @@ function terminalMatchesDispatch(terminal: MeshLedgerEntry, dispatch: MeshLedger
|
|
|
166
170
|
|
|
167
171
|
function statusFromTerminal(entry: MeshLedgerEntry): MeshActiveWorkStatus {
|
|
168
172
|
if (entry.kind === 'task_approval_needed') return 'awaiting_approval';
|
|
173
|
+
// A question (waiting_choice) is a distinct blocked state — kept OUT of
|
|
174
|
+
// awaiting_approval so it is not surfaced in the approval inbox / mesh_approve
|
|
175
|
+
// flow (mission f1d25e11). Answered via mesh_answer_question.
|
|
176
|
+
if (entry.kind === 'task_question_pending') return 'awaiting_choice';
|
|
169
177
|
if (entry.kind === 'task_completed') return 'idle';
|
|
170
178
|
return 'failed';
|
|
171
179
|
}
|
|
@@ -331,6 +339,7 @@ export function buildMeshActiveWorkSummary(activeWork: MeshActiveWorkRecord[]):
|
|
|
331
339
|
idle: 0,
|
|
332
340
|
failed: 0,
|
|
333
341
|
awaiting_approval: 0,
|
|
342
|
+
awaiting_choice: 0,
|
|
334
343
|
};
|
|
335
344
|
const sourceCounts: Record<MeshActiveWorkSource, number> = { queue: 0, direct: 0 };
|
|
336
345
|
for (const item of activeWork) {
|
|
@@ -380,6 +389,7 @@ export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeW
|
|
|
380
389
|
? sessionStatusFromNodes(opts.nodes, queueNodeId ?? undefined, queueSessionId ?? undefined)
|
|
381
390
|
: {};
|
|
382
391
|
const queueStatus: MeshActiveWorkStatus = queueLive.status === 'awaiting_approval'
|
|
392
|
+
|| queueLive.status === 'awaiting_choice'
|
|
383
393
|
|| queueLive.status === 'generating'
|
|
384
394
|
? queueLive.status
|
|
385
395
|
: task.status;
|