@runuai/host 0.9.151 → 0.9.153
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/db/migrations/0029_task_shared_files_mode.sql +5 -0
- package/db/migrations/meta/_journal.json +7 -0
- package/db/schema.ts +10 -0
- package/lib/agent.ts +4 -0
- package/lib/browser-testing.ts +8 -3
- package/lib/cloud-credential-leases.ts +15 -4
- package/lib/cloud-credential-runtime.ts +142 -3
- package/lib/credential-cleanup-inventory.ts +2 -0
- package/lib/credential-consumers/engine-anchor.ts +5 -1
- package/lib/credential-consumers/files.ts +934 -0
- package/lib/credential-consumers/mcp.ts +6 -1
- package/lib/mcp-gateway.ts +576 -122
- package/lib/orchestrator.ts +99 -10
- package/lib/runtime-state.ts +88 -0
- package/lib/shared-files-s3.ts +743 -3
- package/lib/sidecar-resume.ts +9 -0
- package/lib/task-credential-cleanup.ts +36 -5
- package/lib/task-environment/machine-task-up.ts +35 -19
- package/lib/task-environment/workspace-files.ts +33 -6
- package/lib/task-files-machine-retirement.ts +88 -0
- package/lib/task-files-readiness.ts +127 -0
- package/package.json +1 -1
- package/src/cloud-credentials.ts +10 -0
- package/src/index.ts +38 -2
- package/src/main.ts +47 -2
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
-- ADR-154 Files v1: persist the cloud-issued mode for crash recovery. The
|
|
2
|
+
-- task-owned `.uai/files-mounted` marker is an observation, never authority.
|
|
3
|
+
ALTER TABLE `uai_host_tasks` ADD `shared_files_mode` text
|
|
4
|
+
CONSTRAINT `uai_host_tasks_shared_files_mode`
|
|
5
|
+
CHECK (`shared_files_mode` IS NULL OR `shared_files_mode` IN ('off', 'ro', 'rw'));
|
|
@@ -204,6 +204,13 @@
|
|
|
204
204
|
"when": 1791331200001,
|
|
205
205
|
"tag": "0028_leased_engine_runtimes",
|
|
206
206
|
"breakpoints": true
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"idx": 29,
|
|
210
|
+
"version": "6",
|
|
211
|
+
"when": 1791590400000,
|
|
212
|
+
"tag": "0029_task_shared_files_mode",
|
|
213
|
+
"breakpoints": true
|
|
207
214
|
}
|
|
208
215
|
]
|
|
209
216
|
}
|
package/db/schema.ts
CHANGED
|
@@ -39,6 +39,12 @@ export const hostTasks = sqliteTable(
|
|
|
39
39
|
// Value-free resolver metadata for rebuilding the project-env gateway
|
|
40
40
|
// after a runner restart, before the cloud re-sends task-up.
|
|
41
41
|
projectEnvSpec: text("project_env_spec"),
|
|
42
|
+
// ADR-154 Files v1: the normalized mode from the cloud's task launch.
|
|
43
|
+
// Recovery must never trust the task-writable `.uai/files-mounted`
|
|
44
|
+
// marker as authority for what may be mounted.
|
|
45
|
+
sharedFilesMode: text("shared_files_mode", {
|
|
46
|
+
enum: ["off", "ro", "rw"],
|
|
47
|
+
}),
|
|
42
48
|
statusMirror: text("status_mirror"),
|
|
43
49
|
// Owner + project slugs — received in TaskLaunchInput at task-up and
|
|
44
50
|
// persisted here so the local UI (ADR-028) can show "who/what" without a
|
|
@@ -70,6 +76,10 @@ export const hostTasks = sqliteTable(
|
|
|
70
76
|
"uai_host_tasks_environment_pair",
|
|
71
77
|
sql`(${table.environmentProvider} IS NULL) = (${table.environmentLocator} IS NULL)`,
|
|
72
78
|
),
|
|
79
|
+
sharedFilesModeCheck: check(
|
|
80
|
+
"uai_host_tasks_shared_files_mode",
|
|
81
|
+
sql`${table.sharedFilesMode} IS NULL OR ${table.sharedFilesMode} IN ('off', 'ro', 'rw')`,
|
|
82
|
+
),
|
|
73
83
|
}),
|
|
74
84
|
);
|
|
75
85
|
|
package/lib/agent.ts
CHANGED
|
@@ -68,6 +68,10 @@ export interface LeasedTaskUp {
|
|
|
68
68
|
* the start.
|
|
69
69
|
*/
|
|
70
70
|
runtimeReady: (environment: TaskWriteEnvironment, signal: AbortSignal) => Promise<void>;
|
|
71
|
+
/** This exact start carries a task-machine Files session. Machine task-up
|
|
72
|
+
* skips its legacy copy/mount path only in this case; owned leased tasks do
|
|
73
|
+
* not receive a Files session and keep their normal Files behavior. */
|
|
74
|
+
filesV1?: true;
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
/** Refuses a start on cloud credentials that is not whole, or not this
|
package/lib/browser-testing.ts
CHANGED
|
@@ -1390,11 +1390,16 @@ export async function setupBrowserTesting(
|
|
|
1390
1390
|
}
|
|
1391
1391
|
}
|
|
1392
1392
|
|
|
1393
|
-
// Grok registers
|
|
1394
|
-
//
|
|
1395
|
-
//
|
|
1393
|
+
// Grok registers through its own read-modify-write CLI (user scope,
|
|
1394
|
+
// see grokBrowserAddCommand), under the same lock as gateway reconciliation
|
|
1395
|
+
// so two successful CLI processes cannot silently lose each other's entry.
|
|
1396
|
+
// A failure leaves the task configured-false so the next ensure retries.
|
|
1396
1397
|
if (hasGrok) {
|
|
1397
1398
|
const grokArgv = [
|
|
1399
|
+
"flock",
|
|
1400
|
+
"-w",
|
|
1401
|
+
"10",
|
|
1402
|
+
MCP_CONFIG_LOCK_PATH,
|
|
1398
1403
|
"sh",
|
|
1399
1404
|
"-lc",
|
|
1400
1405
|
selectedGrokHomes.map(grokBrowserAddCommand).join(" && "),
|
|
@@ -1126,7 +1126,10 @@ export class TaskCredentialLeaseRegistry {
|
|
|
1126
1126
|
await this.#applyCredentialVersion(state, frame);
|
|
1127
1127
|
} catch (error) {
|
|
1128
1128
|
this.#report(error, state.assignment);
|
|
1129
|
-
|
|
1129
|
+
// A failed consumer install may already have completed full cleanup
|
|
1130
|
+
// and armed the bounded retry which owns what comes next. Do not cancel
|
|
1131
|
+
// that backoff by immediately requesting the same refused manifest.
|
|
1132
|
+
if (this.#isActive(state) && !state.retryTimer) this.#request(state);
|
|
1130
1133
|
}
|
|
1131
1134
|
}
|
|
1132
1135
|
|
|
@@ -1226,7 +1229,7 @@ export class TaskCredentialLeaseRegistry {
|
|
|
1226
1229
|
});
|
|
1227
1230
|
} catch (error) {
|
|
1228
1231
|
this.#report(error, state.assignment);
|
|
1229
|
-
this.#request(state);
|
|
1232
|
+
if (this.#isActive(state) && !state.retryTimer) this.#request(state);
|
|
1230
1233
|
}
|
|
1231
1234
|
} else if (frame.outcome === "needs_sign_in") {
|
|
1232
1235
|
// The authoritative stale frame supplies any floor and removals.
|
|
@@ -2452,8 +2455,16 @@ export class TaskCredentialLeaseRegistry {
|
|
|
2452
2455
|
// A permanent revocation answers every request with another denial.
|
|
2453
2456
|
// Keep asking, but do not turn that into a tight
|
|
2454
2457
|
// deny/cleanup/request loop against the cloud.
|
|
2455
|
-
|
|
2456
|
-
|
|
2458
|
+
// A denial is stable until cloud state changes, and a consumer which
|
|
2459
|
+
// refused a grant can keep refusing the same manifest (for example
|
|
2460
|
+
// while a task-owned workspace entry is being repaired). Neither is
|
|
2461
|
+
// allowed to become a cleanup/request loop which repeatedly mints STS
|
|
2462
|
+
// sessions and reads Parameter Store without delay.
|
|
2463
|
+
if (reason === "denied" || reason === "manifest-narrowed") {
|
|
2464
|
+
this.#armRetry(state);
|
|
2465
|
+
} else {
|
|
2466
|
+
this.#request(state);
|
|
2467
|
+
}
|
|
2457
2468
|
}
|
|
2458
2469
|
}
|
|
2459
2470
|
}
|
|
@@ -79,6 +79,8 @@ import {
|
|
|
79
79
|
setMcpCredentialRefresher,
|
|
80
80
|
} from "./credential-consumers/mcp";
|
|
81
81
|
import type { ProjectEnvTaskReplicaManager, ProjectEnvTaskReplicaSnapshot } from "./project-env-task-replica";
|
|
82
|
+
import type { SharedFilesMode } from "./shared-files-s3";
|
|
83
|
+
import type { TaskEnvironmentHandle } from "./task-environment/types";
|
|
82
84
|
import {
|
|
83
85
|
TaskCredentialCleanupCoordinator,
|
|
84
86
|
type TaskCredentialCleanupDeps,
|
|
@@ -95,6 +97,17 @@ const ENV_READY_POLL_MS = 50;
|
|
|
95
97
|
* way; the next process's recovery sweep finishes what is left. */
|
|
96
98
|
export const SHUTDOWN_DRAIN_MS = 30_000;
|
|
97
99
|
|
|
100
|
+
/** The only MCP state an agent config exposes. Material, provider URL,
|
|
101
|
+
* revision and availability stay behind the task gateway, so changing any of
|
|
102
|
+
* them must not churn a long-lived agent process. */
|
|
103
|
+
function mcpAgentMembershipFingerprint(view: TaskCredentialView): string {
|
|
104
|
+
return JSON.stringify(
|
|
105
|
+
view.mcp
|
|
106
|
+
.map((connection) => connection.manifest.slug)
|
|
107
|
+
.sort((left, right) => left.localeCompare(right)),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
98
111
|
/** The task's Files (ADR-154 §12): installed, opened and closed with the
|
|
99
112
|
* rest. `close` is synchronous: no new Files admission after it. */
|
|
100
113
|
export interface TaskFilesConsumer {
|
|
@@ -102,6 +115,32 @@ export interface TaskFilesConsumer {
|
|
|
102
115
|
open(assignment: Readonly<TaskAssignment>, view: TaskCredentialView): void;
|
|
103
116
|
close(assignment: Readonly<TaskAssignment>): void;
|
|
104
117
|
closeConsumers(request: TaskCredentialCleanupRequest): Promise<void>;
|
|
118
|
+
/** A trusted task-machine runtime could not establish its Files boundary.
|
|
119
|
+
* This run cannot be reopened on the same machine. */
|
|
120
|
+
retire(assignment: Readonly<TaskAssignment>, error: unknown): void;
|
|
121
|
+
/** Optional only so owned runners can use the same composer without a
|
|
122
|
+
* task-machine Files implementation. */
|
|
123
|
+
runtimeReady?(
|
|
124
|
+
assignment: Readonly<TaskAssignment>,
|
|
125
|
+
environment: TaskEnvironmentHandle<unknown>,
|
|
126
|
+
mode: SharedFilesMode,
|
|
127
|
+
signal: AbortSignal,
|
|
128
|
+
): Promise<void>;
|
|
129
|
+
/** Drop secrets at process shutdown without starting cleanup. */
|
|
130
|
+
shutdown?(): void;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A Files implementation proved that the runtime-ready operation which
|
|
134
|
+
* failed still owns the exact open run and requested terminal retirement of
|
|
135
|
+
* its one-task machine; a stale operation throws its ordinary error. */
|
|
136
|
+
export class AuthoritativeTaskFilesFailure extends Error {
|
|
137
|
+
override readonly cause: unknown;
|
|
138
|
+
|
|
139
|
+
constructor(cause: unknown) {
|
|
140
|
+
super(cause instanceof Error ? cause.message : "the task's Files boundary failed");
|
|
141
|
+
this.name = "AuthoritativeTaskFilesFailure";
|
|
142
|
+
this.cause = cause;
|
|
143
|
+
}
|
|
105
144
|
}
|
|
106
145
|
|
|
107
146
|
export interface CloudCredentialRuntimeDeps {
|
|
@@ -266,6 +305,9 @@ export class CloudCredentialRuntime {
|
|
|
266
305
|
/** Work accepted and not yet over (startups, frames, installs, holds not
|
|
267
306
|
* yet released, closes): what shutdown joins. */
|
|
268
307
|
readonly #work = new Set<Promise<unknown>>();
|
|
308
|
+
/** Last agent-facing MCP membership published for each task. Kept across a
|
|
309
|
+
* gate close/renewal because the config remains in the task runtime. */
|
|
310
|
+
readonly #publishedMcpMembership = new Map<string, string>();
|
|
269
311
|
readonly #setTimer: typeof setTimeout;
|
|
270
312
|
readonly #clearTimer: typeof clearTimeout;
|
|
271
313
|
#shutDown = false;
|
|
@@ -500,6 +542,69 @@ export class CloudCredentialRuntime {
|
|
|
500
542
|
};
|
|
501
543
|
}
|
|
502
544
|
|
|
545
|
+
/** Whether this process has the task-machine Files-v1 consumer wired. */
|
|
546
|
+
taskFilesV1(): boolean {
|
|
547
|
+
return typeof this.#deps.files?.runtimeReady === "function";
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** Runtime-ready half of Files v1. It is tracked with the composer's
|
|
551
|
+
* accepted work so shutdown joins a write/mount already admitted. */
|
|
552
|
+
prepareTaskFiles(
|
|
553
|
+
assignment: Readonly<TaskAssignment>,
|
|
554
|
+
environment: TaskEnvironmentHandle<unknown>,
|
|
555
|
+
mode: SharedFilesMode,
|
|
556
|
+
signal: AbortSignal,
|
|
557
|
+
): Promise<void> {
|
|
558
|
+
const ready = this.#deps.files?.runtimeReady;
|
|
559
|
+
if (!ready) return Promise.reject(new Error("task-machine Files v1 is unavailable"));
|
|
560
|
+
return this.#track(
|
|
561
|
+
ready.call(this.#deps.files, assignment, environment, mode, signal).catch((error: unknown) => {
|
|
562
|
+
if (error instanceof AuthoritativeTaskFilesFailure) {
|
|
563
|
+
// Never reopen this run on the same machine. The consumer already
|
|
564
|
+
// proved this is authoritative; centrally latch/request exact-run
|
|
565
|
+
// terminal retirement too, so an implementation cannot turn this
|
|
566
|
+
// into an ordinary cleanup/regrant by omission.
|
|
567
|
+
let retirementError: unknown = null;
|
|
568
|
+
try {
|
|
569
|
+
this.#deps.files!.retire(assignment, error.cause);
|
|
570
|
+
} catch (failure) {
|
|
571
|
+
retirementError = failure;
|
|
572
|
+
}
|
|
573
|
+
void this.closeAssignment({ ...assignment }, "stop").catch(() => {});
|
|
574
|
+
if (retirementError !== null) throw retirementError;
|
|
575
|
+
throw error.cause;
|
|
576
|
+
}
|
|
577
|
+
throw error;
|
|
578
|
+
}),
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** A recovered task whose trusted Files metadata is absent or invalid has
|
|
583
|
+
* failed the same boundary before the consumer can be called. Request its
|
|
584
|
+
* terminal machine retirement, close its gate synchronously, and run the
|
|
585
|
+
* run's full cleanup. */
|
|
586
|
+
failTaskFilesSetup(
|
|
587
|
+
assignment: Readonly<TaskAssignment>,
|
|
588
|
+
error: unknown = new Error("the recovered task has no trusted Files-v1 metadata"),
|
|
589
|
+
): void {
|
|
590
|
+
try {
|
|
591
|
+
this.#deps.files?.retire(assignment, error);
|
|
592
|
+
} catch (retirementError) {
|
|
593
|
+
console.warn(
|
|
594
|
+
`[credentials] task ${assignment.taskId} run ${String(assignment.runEpoch)}: Files machine retirement could not be requested: ${
|
|
595
|
+
retirementError instanceof Error ? retirementError.message : String(retirementError)
|
|
596
|
+
}`,
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
void this.closeAssignment({ ...assignment }, "stop").catch((error: unknown) => {
|
|
600
|
+
console.warn(
|
|
601
|
+
`[credentials] task ${assignment.taskId} run ${String(assignment.runEpoch)}: Files recovery cleanup failed: ${
|
|
602
|
+
error instanceof Error ? error.message : String(error)
|
|
603
|
+
}`,
|
|
604
|
+
);
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
|
|
503
608
|
/** The start is over, successful or not: its token has no authority left,
|
|
504
609
|
* and it is no longer the task's newest. */
|
|
505
610
|
endStart(start: TaskCredentialStart): void {
|
|
@@ -688,7 +793,14 @@ export class CloudCredentialRuntime {
|
|
|
688
793
|
const signal = recovered.controller.signal;
|
|
689
794
|
recovered.running = this.#track(
|
|
690
795
|
Promise.resolve()
|
|
691
|
-
.then(() =>
|
|
796
|
+
.then(() => {
|
|
797
|
+
// Stop or a newer run can end recovery after the gate schedules
|
|
798
|
+
// this microtask but before it starts. Never enter task-machine
|
|
799
|
+
// setup with already-revoked authority: Files treats a current
|
|
800
|
+
// setup failure as terminal for the whole machine.
|
|
801
|
+
if (signal.aborted) return;
|
|
802
|
+
return setup(signal);
|
|
803
|
+
})
|
|
692
804
|
.catch((error: unknown) =>
|
|
693
805
|
console.warn(
|
|
694
806
|
`[credentials] task ${assignment.taskId} run ${String(assignment.runEpoch)}: recovered runtime setup failed: ${
|
|
@@ -830,6 +942,7 @@ export class CloudCredentialRuntime {
|
|
|
830
942
|
setEngineCredentialRefresher(null);
|
|
831
943
|
setEngineLaunchFenceEscalator(null);
|
|
832
944
|
setMcpCredentialRefresher(null);
|
|
945
|
+
this.#deps.files?.shutdown?.();
|
|
833
946
|
}
|
|
834
947
|
|
|
835
948
|
// -- the registry's gate and install --------------------------------------------
|
|
@@ -860,13 +973,17 @@ export class CloudCredentialRuntime {
|
|
|
860
973
|
openEngineGate(assignment, view);
|
|
861
974
|
openMcpGate(assignment, view);
|
|
862
975
|
openPersonCredentialGate(assignment, view);
|
|
863
|
-
this.#deps.files?.open(assignment, view);
|
|
864
976
|
this.#open.set(assignment.taskId, assignment.runEpoch);
|
|
865
977
|
const generation = this.#generations.get(view);
|
|
866
978
|
if (generation === undefined) this.#served.delete(assignment.taskId);
|
|
867
979
|
else this.#served.set(assignment.taskId, generation);
|
|
868
|
-
this.#deps.mcpServedChanged?.(assignment.taskId);
|
|
869
980
|
this.#envServedChanged(assignment.taskId);
|
|
981
|
+
// Last throwing boundary: a recovered runtime's mounts stay staged and
|
|
982
|
+
// root-inaccessible until every other consumer accepted this exact view.
|
|
983
|
+
// Nothing which follows publishes work synchronously. If Files itself
|
|
984
|
+
// fails, the registry compensates by closing the earlier gates.
|
|
985
|
+
this.#deps.files?.open(assignment, view);
|
|
986
|
+
this.#publishMcpMembership(assignment.taskId, view);
|
|
870
987
|
this.#runRecoveredSetup(assignment);
|
|
871
988
|
}
|
|
872
989
|
|
|
@@ -878,6 +995,21 @@ export class CloudCredentialRuntime {
|
|
|
878
995
|
}
|
|
879
996
|
}
|
|
880
997
|
|
|
998
|
+
/** Publish only a normalized change visible to agent configuration. A
|
|
999
|
+
* renewed lease, a refreshed OAuth token, and available/unavailable
|
|
1000
|
+
* transitions all keep the same named gateway route and therefore do not
|
|
1001
|
+
* rewrite config or restart agents; an unavailable route answers 503. */
|
|
1002
|
+
#publishMcpMembership(taskId: string, view: TaskCredentialView): void {
|
|
1003
|
+
const fingerprint = mcpAgentMembershipFingerprint(view);
|
|
1004
|
+
if (this.#publishedMcpMembership.get(taskId) === fingerprint) return;
|
|
1005
|
+
this.#publishedMcpMembership.set(taskId, fingerprint);
|
|
1006
|
+
try {
|
|
1007
|
+
this.#deps.mcpServedChanged?.(taskId);
|
|
1008
|
+
} catch {
|
|
1009
|
+
// The gateway gate is authoritative even when its config follower fails.
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
881
1013
|
/**
|
|
882
1014
|
* Synchronous and idempotent: every consumer of this run (and older)
|
|
883
1015
|
* stops being served, env and Files included, and every start holding its
|
|
@@ -947,6 +1079,12 @@ export class CloudCredentialRuntime {
|
|
|
947
1079
|
installPersonCredentials(request);
|
|
948
1080
|
await this.#deps.files?.install(request);
|
|
949
1081
|
this.#assertInstallCurrent(request);
|
|
1082
|
+
// Credential changes normally affect material or availability only. Keep
|
|
1083
|
+
// this normalized comparison at the install boundary so even a future
|
|
1084
|
+
// same-run membership change publishes exactly once.
|
|
1085
|
+
if (request.reason === "credential-change") {
|
|
1086
|
+
this.#publishMcpMembership(request.assignment.taskId, request.next);
|
|
1087
|
+
}
|
|
950
1088
|
}
|
|
951
1089
|
|
|
952
1090
|
#assertInstallCurrent(request: CredentialInstallRequest): void {
|
|
@@ -1019,6 +1157,7 @@ export class CloudCredentialRuntime {
|
|
|
1019
1157
|
if (request.credentialIds !== undefined) return;
|
|
1020
1158
|
this.#deps.envReplica?.dropAssignment(request.taskId, request.runEpoch);
|
|
1021
1159
|
if (request.reason !== "teardown") return;
|
|
1160
|
+
this.#publishedMcpMembership.delete(request.taskId);
|
|
1022
1161
|
forgetLeasedEngines({ taskId: request.taskId, runEpoch: request.runEpoch });
|
|
1023
1162
|
const mcp = leasedMcpFor(request.taskId);
|
|
1024
1163
|
if (mcp && mcp.assignment.runEpoch <= request.runEpoch) forgetLeasedMcp(request.taskId);
|
|
@@ -165,7 +165,9 @@ const SSH_PATHS = new Set(
|
|
|
165
165
|
);
|
|
166
166
|
const FILES_SESSION_PATHS = new Set([
|
|
167
167
|
LEASE_CREDENTIALS_FILE,
|
|
168
|
+
`${LEASE_CREDENTIALS_FILE}.uai-tmp`,
|
|
168
169
|
LEASE_AWS_CONFIG_FILE,
|
|
170
|
+
`${LEASE_AWS_CONFIG_FILE}.uai-tmp`,
|
|
169
171
|
]);
|
|
170
172
|
|
|
171
173
|
/** Destructive cleanup accepts only lane-owned files. The executor repeats
|
|
@@ -1685,7 +1685,11 @@ export async function stopAnchoredEngines(
|
|
|
1685
1685
|
request.taskId,
|
|
1686
1686
|
String(request.runEpoch),
|
|
1687
1687
|
named === undefined ? "all" : "named",
|
|
1688
|
-
|
|
1688
|
+
// Every provider refuses an empty argv entry, and "all" names no
|
|
1689
|
+
// account: a lone newline stands for the empty list (the helper drops
|
|
1690
|
+
// blank names). Live 2026-10-11: the empty string failed every
|
|
1691
|
+
// stop-all, so no cleanup, recovery sweep or relaunch could finish.
|
|
1692
|
+
(named ?? []).map(idPathSegment).join("\n") || "\n",
|
|
1689
1693
|
String(anchorStopSeconds),
|
|
1690
1694
|
carrierRoots(),
|
|
1691
1695
|
],
|