@cotal-ai/manager 0.102.0 → 0.103.2
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/commands.js +28 -2
- package/dist/commands.js.map +1 -1
- package/dist/manager.d.ts +45 -1
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +91 -17
- package/dist/manager.js.map +1 -1
- package/package.json +12 -12
package/dist/manager.js
CHANGED
|
@@ -6,7 +6,7 @@ import { isAbsolute, join, dirname, resolve } from "node:path";
|
|
|
6
6
|
import Headless from "@xterm/headless";
|
|
7
7
|
import { CotalEndpoint, canonicalJson, MANAGED_HANDOFF_KIND, accountFromCreds, credsClaims, DEFAULT_SERVER, DEV_OWNER, MANAGER_LEASE_RENEW_MS, MANAGER_LEASE_TTL_MS, awaitLeaseExpiry, leaseRemainingMs, STANDING_RENEWABLE_TTL_SEC, assertLifecycleToken, discardLaunchArtifacts, divergentSecretStoreNotice, injectedSecretStoreIdentity, parseDaemonStoreAnswer, parseSecretStoreIdentity, sameSecretStoreIdentity, agentFilePath, clearSpaceHistory, dialerFor, connectorServers, spawnEnvAllow, deprovisionAgent, DeprovisionError, addDeprovisionAccounting, isConcreteChannel, deprovisionTargetPrincipal, firstFreeName, spawnNameError, idFromCreds, identityFromCreds, inspectCredHealth, credsRenewalDelayMs, composeWirePersona, loadAgentFile, listPersonaCatalog, personaCatalogDescription, personaCatalogReadable, loadCotalConfig, modelPolicyRefusal, mintCreds, mintGeneration, mintAcceptedToken, openIssuedStore, acceptedBucket, issuedBucket, mintLifecycleUid, managedRetirementOpId, mkSecretDir, newIdentity, oneLine, actionContext, parsePrincipalKey, principalKey, probeConnect, provisionAgent, provisionAgentDurables, registry, resolveAuthProvider, resolveReadAcl, runAgentBearer, saveAgentFile, subjectMatches, AUTH_ENDPOINT, EP_CMD_RETIRE_LIFECYCLE, controlServiceSubject, eventChannelPrincipal, resolveService, invokeCommand, digestIdToken, } from "@cotal-ai/core";
|
|
8
8
|
import { loadLaunchPersonaFile, PersonaSourceError } from "@cotal-ai/workspace";
|
|
9
|
-
import { agentAuthState, agentCredsDir, agentLifecycleSecretFilePaths, agentSecretFilePaths, agentSecretKeyForFile, authDir, claimIdentityRecord, claimManagerSiblingIdentities, connectorInstallHint, DELIVERY_CREDS_KIND, extensionConnectors, findCotalRoot, getSpaceAuth, hasUserAuthState, loadExtensionsManifest, loadManagerInstanceIdentity, loadMeshes, localTrustOfSpace, manifestExtensionNames, materializeFromManifest, materializeSecretToFile, MEMBERSHIP_RW_CREDS_KIND, mergeLaunchOptions, probeLiveness, remintDaemonCreds, resolveAgentType, resolveOnPath, createManagerInstanceIdentity, spaceKey, spaceMaterialKey, SYSTEM_CREDS_FILES, userAuthStateDir, workspaceSecretStore, writeRenewalRecord } from "@cotal-ai/workspace";
|
|
9
|
+
import { agentAuthState, agentCredsDir, agentLifecycleSecretFilePaths, agentSecretFilePaths, agentSecretKeyForFile, authDir, claimIdentityRecord, claimManagerSiblingIdentities, connectorInstallHint, DEFAULT_SEAT_CAP, DELIVERY_CREDS_KIND, extensionConnectors, findCotalRoot, getSpaceAuth, hasUserAuthState, loadExtensionsManifest, loadManagerInstanceIdentity, loadMeshes, localTrustOfSpace, manifestExtensionNames, materializeFromManifest, materializeSecretToFile, MEMBERSHIP_RW_CREDS_KIND, mergeLaunchOptions, probeLiveness, remintDaemonCreds, resolveAgentType, resolveOnPath, createManagerInstanceIdentity, SEAT_CAP_UNLIMITED, spaceKey, spaceMaterialKey, SYSTEM_CREDS_FILES, userAuthStateDir, workspaceSecretStore, writeRenewalRecord } from "@cotal-ai/workspace";
|
|
10
10
|
import { createRuntime, exitProof, isCustodialRuntime, pollProvedExit, requireRuntimeAdopt, requireRuntimeReap, RuntimeReapUnproven, watchProvedExit, } from "./runtime/index.js";
|
|
11
11
|
import { AttachEndpoint } from "./attach-endpoint.js";
|
|
12
12
|
import { DELIVERY_ADMIN_BOOT_WAIT_MS, isUnansweredDeliveryAdmin, makeManagerEndpointEvictionEvidence, makeManagerEndpointHolderEvictor, untilDeliveryAdminAnswers, withScopedEndpoint } from "./endpoint-evict.js";
|
|
@@ -32,7 +32,7 @@ import { ManagerSessionPlane, openSessionLedgerKv } from "./session/index.js";
|
|
|
32
32
|
// The manager as an ordinary v0.4 `service` endpoint — the §13.1
|
|
33
33
|
// endpoint-serve credential subsystem (gate provisioning, registration barrier, mint fence) plus
|
|
34
34
|
// the register/authorize/serve seams, all driven over a scoped one-shot executor connection.
|
|
35
|
-
import { provisionEndpointGateOpen, endpointRegistrationBarrier, serveIssuanceGateKv, commitSiblingIssuance, markLedgerRowRevoked, epcredRowKey, epgateKey, parseEndpointGate, readEndpointGateGeneration, registerServingInstance, deregisterServiceInstance, serveEndpoint, bindGoal, createGoal, transitionGoal, commitGoalResult, requestGoalCancel, goalAlreadyTerminal, settleGoalUncertain, readGoalResult, readGoalStatus, readGoalSpec, recordGoalIndex, readGoalIndex, clearGoalIndex, listGoalIndex, mintCheckpoint, resumeCheckpoint, readCheckpointSettle, readCheckpointSpec, expireCheckpoint, GOAL_TERMINAL_STATES, goalRefOf, goalProgressTopic, turnNoticeSubject, epeSubject, submissionFingerprint, EpEnvelopeError, lifecycleBlocked, lifecycleBlockedFrom, renderLifecycleBlocked, } from "@cotal-ai/core";
|
|
35
|
+
import { provisionEndpointGateOpen, endpointRegistrationBarrier, serveIssuanceGateKv, commitSiblingIssuance, markLedgerRowRevoked, epcredRowKey, epgateKey, parseEndpointGate, readEndpointGateGeneration, registerServingInstance, deregisterServiceInstance, serveEndpoint, bindGoal, createGoal, transitionGoal, commitGoalResult, requestGoalCancel, goalAlreadyTerminal, settleGoalUncertain, readGoalResult, readGoalStatus, readGoalSpec, recordGoalIndex, readGoalIndex, clearGoalIndex, listGoalIndex, mintCheckpoint, resumeCheckpoint, readCheckpointSettle, readCheckpointSpec, expireCheckpoint, GOAL_TERMINAL_STATES, goalRefOf, goalProgressTopic, turnNoticeSubject, epeSubject, submissionFingerprint, EpEnvelopeError, lifecycleBlocked, seatCapRefused, lifecycleBlockedFrom, renderLifecycleBlocked, controlReplyFromThrown, } from "@cotal-ai/core";
|
|
36
36
|
import { MANAGER_ENDPOINT, managerClusterArtifacts, managerCommandDefs, managerContractArtifactValues } from "./manager-service-contract.js";
|
|
37
37
|
import { measureManagerVitals } from "./vitals.js";
|
|
38
38
|
import { staticLifecycleTransport, activateStaticLifecycle, runStaticTerminal, readStaticSlot, casStaticSlot, recordSlotCredential, appendStaticCredentialRow, planStaticSlotResume, ownedBySibling, observeStaticSlot, listStaticSlotObservations, walkStaticSlots, renderStaticSlotObservation, StaticSlotReadError, STATIC_SLOT_READ_FAILED_DETAIL, } from "./static-lifecycle.js";
|
|
@@ -136,6 +136,12 @@ const STATIC_RECONCILE_MAX_ATTEMPTS = STATIC_RECONCILE_RETRY_DELAYS_MS.length +
|
|
|
136
136
|
function retireOpId(lifecycleUid) {
|
|
137
137
|
return managedRetirementOpId(lifecycleUid);
|
|
138
138
|
}
|
|
139
|
+
/** The owner a retained seat resumes under: its inventory identity's, which is the same value the
|
|
140
|
+
* adopted seat records as `userOwner`, so a resumed seat is counted against the owner that held
|
|
141
|
+
* it before the restart ({@link Manager.ownerSeats}) and not against the mesh's default one. */
|
|
142
|
+
function resumeOwner(entry) {
|
|
143
|
+
return entry.identity.mode === "user" ? entry.identity.owner : DEV_OWNER;
|
|
144
|
+
}
|
|
139
145
|
/** Sentinel owner-filter value that matches NO agent's `userOwner` (owner tokens never contain a
|
|
140
146
|
* dash) — what {@link Manager.psOwnerFilter} returns for an unparseable caller so a malformed
|
|
141
147
|
* principal fail-closes to an empty `ps` instead of an unbounded one. */
|
|
@@ -355,6 +361,8 @@ export class Manager {
|
|
|
355
361
|
eventsRequired;
|
|
356
362
|
/** The operator-set global live-session ceiling (see {@link ManagerOptions.maxSessions}). */
|
|
357
363
|
maxSessions;
|
|
364
|
+
/** The operator-set per-owner seat ceiling (see {@link ManagerOptions.seatCap}). */
|
|
365
|
+
seatCap;
|
|
358
366
|
/** The ONE secret store for every kind this manager touches (daemon-cred remint + agent kinds).
|
|
359
367
|
* See {@link ManagerOptions.secretStore}. */
|
|
360
368
|
secrets;
|
|
@@ -371,8 +379,11 @@ export class Manager {
|
|
|
371
379
|
agents = new Map();
|
|
372
380
|
/** Names whose spawn is in flight (reserved synchronously before the provision await) — counted
|
|
373
381
|
* toward the ceiling (once per name, see {@link Manager.occupancy}) so two concurrent same-name
|
|
374
|
-
* spawns can't both pass the gate (P4a).
|
|
375
|
-
|
|
382
|
+
* spawns can't both pass the gate (P4a). Each name carries the OWNER its launch was admitted
|
|
383
|
+
* for, so the per-owner ceiling ({@link Manager.ownerSeats}) counts a launch from the moment it
|
|
384
|
+
* is reserved; a cap read only off `agents` would let a burst of concurrent spawns overshoot it,
|
|
385
|
+
* because none of them has a seat yet when the next one checks. */
|
|
386
|
+
reserved = new Map();
|
|
376
387
|
/** Reserved names whose launch already put its seat in `agents`. The name stays reserved until
|
|
377
388
|
* the launch settles, so no other spawn can take it, but the ceiling counts that seat through
|
|
378
389
|
* `agents` and, if it frees young, its cooling stamp, never through the reservation (#906). */
|
|
@@ -687,6 +698,7 @@ export class Manager {
|
|
|
687
698
|
this.onServiceIdentity = opts.onServiceIdentity;
|
|
688
699
|
this.eventsRequired = opts.eventsRequired === true;
|
|
689
700
|
this.maxSessions = opts.maxSessions;
|
|
701
|
+
this.seatCap = opts.seatCap ?? DEFAULT_SEAT_CAP;
|
|
690
702
|
this.remoteAuthority = opts.remoteAuthority;
|
|
691
703
|
this.remoteExecutorCreds = opts.remoteAuthority?.executorCreds;
|
|
692
704
|
this.remoteSupervisorCreds = opts.remoteAuthority?.supervisorCreds;
|
|
@@ -3048,7 +3060,7 @@ export class Manager {
|
|
|
3048
3060
|
// only — the action model gives failures a typed channel.
|
|
3049
3061
|
const unwrap = (r) => {
|
|
3050
3062
|
if (!r.ok)
|
|
3051
|
-
throw new EpEnvelopeError("failed-precondition", r.error ?? "the operation failed");
|
|
3063
|
+
throw new EpEnvelopeError((r.code ?? "failed-precondition"), r.error ?? "the operation failed", r.details);
|
|
3052
3064
|
return r.data;
|
|
3053
3065
|
};
|
|
3054
3066
|
// The admin-family serve gate: every `manager.admin`-class
|
|
@@ -5531,6 +5543,16 @@ export class Manager {
|
|
|
5531
5543
|
// collision, a manifest name and a `--name` override all meet it, and before the reservation.
|
|
5532
5544
|
if (name.startsWith(CHAT_ALIAS_PREFIX))
|
|
5533
5545
|
return { ok: false, error: `the name "${name}" starts with "${CHAT_ALIAS_PREFIX}", which is reserved for chat sessions; pick another name` };
|
|
5546
|
+
// THE PER-OWNER CEILING, read in the same synchronous window as the reserve below rather than
|
|
5547
|
+
// beside the host capacity gate higher up: the count it takes must be the one the reservation
|
|
5548
|
+
// lands against, or a burst of concurrent launches for one owner each reads a count taken
|
|
5549
|
+
// before any of the others reserved, and they all pass. Nothing between here and the reserve
|
|
5550
|
+
// awaits. It refuses BEFORE `onEffect`, so the launch states `not-executed` and leaves no
|
|
5551
|
+
// name, no mint and no footprint behind.
|
|
5552
|
+
const owner = this.launchOwner(opts, spawner);
|
|
5553
|
+
const overCap = this.seatCapRefusal(owner);
|
|
5554
|
+
if (overCap)
|
|
5555
|
+
return { ok: false, code: overCap.code, error: overCap.message, ...(overCap.details ? { details: overCap.details } : {}) };
|
|
5534
5556
|
// Set once the agent's footprint (durables + creds, or the user-mode grant + secret files)
|
|
5535
5557
|
// exists; cleared when a live slot takes ownership. If it survives to `finally`, the spawn threw
|
|
5536
5558
|
// AFTER provisioning (buildLaunch / runtime.spawn) — the orphan-rollback tears it down. Carries
|
|
@@ -5547,7 +5569,7 @@ export class Manager {
|
|
|
5547
5569
|
// Taken right before the try so its finally is the only release; a refusal belongs above the
|
|
5548
5570
|
// allocation, where there is no name to give back.
|
|
5549
5571
|
hooks?.onEffect?.();
|
|
5550
|
-
this.reserved.
|
|
5572
|
+
this.reserved.set(name, owner);
|
|
5551
5573
|
try {
|
|
5552
5574
|
// A stable nkey identity assigned at spawn: the public key is the agent's card.id (threaded via
|
|
5553
5575
|
// COTAL_ID); the seed is retained to mint matching creds later.
|
|
@@ -5577,8 +5599,8 @@ export class Manager {
|
|
|
5577
5599
|
// freshly-minted nkey; user mode keys on the derived owner (opts.owner, else a u_-owner spawner)
|
|
5578
5600
|
// + the alias — derived HERE where the mode and owner source are in scope.
|
|
5579
5601
|
const agentTriple = this.userMode
|
|
5580
|
-
? { owner
|
|
5581
|
-
: { owner
|
|
5602
|
+
? { owner, actor: name, uid: lifecycleUid }
|
|
5603
|
+
: { owner, actor: identity.id, uid: lifecycleUid };
|
|
5582
5604
|
// THE EVENT GRANT, derived from the principal that was actually ALLOCATED and never from the
|
|
5583
5605
|
// display name. A display name is UI convenience: this mesh permits two live agents to carry
|
|
5584
5606
|
// one, so a name-keyed channel fuses two principals onto one subject and, in auth mode,
|
|
@@ -5661,7 +5683,7 @@ export class Manager {
|
|
|
5661
5683
|
if ("error" in prep) {
|
|
5662
5684
|
if (prep.refused !== undefined)
|
|
5663
5685
|
hooks?.onUndone?.(prep.refused);
|
|
5664
|
-
return { ok: false, error: prep.error, ...(prep.details ? { details: prep.details } : {}) };
|
|
5686
|
+
return { ok: false, error: prep.error, ...(prep.refused !== undefined ? { code: prep.refused } : {}), ...(prep.details ? { details: prep.details } : {}) };
|
|
5665
5687
|
}
|
|
5666
5688
|
// THE HOST'S UID WINS (#1972). On the local arm this is the value passed in, unchanged. On
|
|
5667
5689
|
// the hosted enrollment arm the host selected it, and from here every lifecycle-keyed thing
|
|
@@ -6000,15 +6022,15 @@ export class Manager {
|
|
|
6000
6022
|
catch (e) {
|
|
6001
6023
|
// Failure after reserve (provision / launch threw): the slot was never live, so no cold-start
|
|
6002
6024
|
// was paid — the reserved rollback (finally) is enough, no cooling stamp.
|
|
6003
|
-
//
|
|
6004
|
-
//
|
|
6025
|
+
// Keep the envelope's code and lifecycle details on the ControlReply so the serve boundary
|
|
6026
|
+
// can carry the refusal's classification and barrier to the caller. An extension can throw
|
|
6005
6027
|
// any value, and an envelope's `message` is only typed as a string, so the refusal text always
|
|
6006
6028
|
// comes from rejectionText; telling an envelope apart is guarded because a thrown Proxy can
|
|
6007
6029
|
// throw from `instanceof` or from its own fields.
|
|
6008
6030
|
const error = rejectionText(e);
|
|
6009
6031
|
try {
|
|
6010
6032
|
if (e instanceof EpEnvelopeError)
|
|
6011
|
-
return
|
|
6033
|
+
return controlReplyFromThrown(e, error);
|
|
6012
6034
|
}
|
|
6013
6035
|
catch { /* not a readable envelope */ }
|
|
6014
6036
|
return { ok: false, error };
|
|
@@ -6108,7 +6130,7 @@ export class Manager {
|
|
|
6108
6130
|
return { ok: false, agents: [], error: `retained agent "${entry.name}" is already managed or reserved` };
|
|
6109
6131
|
}
|
|
6110
6132
|
for (const entry of entries) {
|
|
6111
|
-
this.reserved.
|
|
6133
|
+
this.reserved.set(entry.name, resumeOwner(entry));
|
|
6112
6134
|
batchReservations.push(entry.name);
|
|
6113
6135
|
}
|
|
6114
6136
|
const preflight = [];
|
|
@@ -6528,7 +6550,7 @@ export class Manager {
|
|
|
6528
6550
|
}
|
|
6529
6551
|
async launchPreparedResume(entry, prepared, batchReserved, supervised) {
|
|
6530
6552
|
if (!batchReserved)
|
|
6531
|
-
this.reserved.
|
|
6553
|
+
this.reserved.set(entry.name, resumeOwner(entry));
|
|
6532
6554
|
try {
|
|
6533
6555
|
// Unit B F5(b): recover the STATIC identity's nkey seed from the adopted credential (the
|
|
6534
6556
|
// creds file embeds it) so the manager stays this incarnation's RENEWAL OWNER across a
|
|
@@ -6932,7 +6954,7 @@ export class Manager {
|
|
|
6932
6954
|
const cooling = this.coolingCount();
|
|
6933
6955
|
let reserved = 0;
|
|
6934
6956
|
let launching = 0;
|
|
6935
|
-
for (const name of this.reserved) {
|
|
6957
|
+
for (const name of this.reserved.keys()) {
|
|
6936
6958
|
const placed = this.reservedLive.has(name);
|
|
6937
6959
|
if (!placed)
|
|
6938
6960
|
reserved++;
|
|
@@ -6963,6 +6985,59 @@ export class Manager {
|
|
|
6963
6985
|
: "waiting frees no slot; despawn one";
|
|
6964
6986
|
return `at capacity (${this.occupancyText(o)}); ${remedy}`;
|
|
6965
6987
|
}
|
|
6988
|
+
/**
|
|
6989
|
+
* The owner a launch belongs to, read ONCE so the per-owner ceiling, the agent's addressing
|
|
6990
|
+
* triple and the user-mode provisioner cannot charge three different owners for one seat. A
|
|
6991
|
+
* static or open mesh has one owner by construction; a user-auth mesh takes the spec's owner,
|
|
6992
|
+
* else the owner of the principal that asked, which is what {@link provisionUserAgent} resolves
|
|
6993
|
+
* from the same two values (it refuses outright where this falls back, so the two never disagree
|
|
6994
|
+
* on an admitted launch).
|
|
6995
|
+
*/
|
|
6996
|
+
launchOwner(opts, spawner) {
|
|
6997
|
+
if (!this.userMode)
|
|
6998
|
+
return DEV_OWNER;
|
|
6999
|
+
const asked = spawner === undefined ? undefined : parsePrincipalKey(spawner)?.owner;
|
|
7000
|
+
return opts.owner ?? (asked?.startsWith("u_") ? asked : DEV_OWNER);
|
|
7001
|
+
}
|
|
7002
|
+
/** The owner of a seat this manager supervises, by the same rule {@link launchOwner} admitted it
|
|
7003
|
+
* under: a user-mode seat records its derived owner, and every other seat is the mesh's one owner. */
|
|
7004
|
+
seatOwner(a) {
|
|
7005
|
+
return a.userOwner ?? DEV_OWNER;
|
|
7006
|
+
}
|
|
7007
|
+
/**
|
|
7008
|
+
* Live seats this manager holds for one owner: the seats it supervises, plus the launches it has
|
|
7009
|
+
* reserved for that owner and not yet placed. It deliberately does NOT count cooling stamps — a
|
|
7010
|
+
* cooling slot is a seat that already freed, and the recycle floor it holds is a global
|
|
7011
|
+
* fork-bomb bound (P4c), not one tenant's occupancy. Mirrors {@link occupancy}'s reserved rule,
|
|
7012
|
+
* so a reservation stops being counted twice the moment its seat reaches `agents`.
|
|
7013
|
+
*/
|
|
7014
|
+
ownerSeats(owner) {
|
|
7015
|
+
let seats = 0;
|
|
7016
|
+
for (const a of this.agents.values())
|
|
7017
|
+
if (this.seatOwner(a) === owner)
|
|
7018
|
+
seats++;
|
|
7019
|
+
for (const [name, reservedFor] of this.reserved)
|
|
7020
|
+
if (reservedFor === owner && !this.reservedLive.has(name))
|
|
7021
|
+
seats++;
|
|
7022
|
+
return seats;
|
|
7023
|
+
}
|
|
7024
|
+
/**
|
|
7025
|
+
* The per-owner ceiling, consulted at accept: `undefined` admits the launch, and a refusal
|
|
7026
|
+
* carries the owner's count and the cap so a caller can render a remedy and a script can decide
|
|
7027
|
+
* without re-reading prose. Distinct from {@link capacityRefusal}, which bounds the HOST: that
|
|
7028
|
+
* one frees when anyone stops a seat or a cooling slot expires, this one frees only when THIS
|
|
7029
|
+
* owner stops one of its own, or the operator raises the cap, so they cannot share a sentence.
|
|
7030
|
+
*/
|
|
7031
|
+
seatCapRefusal(owner) {
|
|
7032
|
+
const cap = this.seatCap;
|
|
7033
|
+
if (cap === SEAT_CAP_UNLIMITED)
|
|
7034
|
+
return undefined;
|
|
7035
|
+
const seats = this.ownerSeats(owner);
|
|
7036
|
+
if (seats < cap)
|
|
7037
|
+
return undefined;
|
|
7038
|
+
return seatCapRefused(`owner ${owner} holds ${seats} of the ${cap} seat${cap === 1 ? "" : "s"} manager ${this.managerInstanceId} admits per owner; ` +
|
|
7039
|
+
`stop one of that owner's seats, or ask the operator who runs this manager for a higher seat cap`, { owner, seats, cap });
|
|
7040
|
+
}
|
|
6966
7041
|
async opStop(args, caller, admin) {
|
|
6967
7042
|
const name = String(args.name ?? "").trim();
|
|
6968
7043
|
const a = this.agents.get(name);
|
|
@@ -8434,8 +8509,7 @@ export class Manager {
|
|
|
8434
8509
|
// outcome unknown.
|
|
8435
8510
|
if (acceptance === undefined) {
|
|
8436
8511
|
const details = reply.details;
|
|
8437
|
-
|
|
8438
|
-
const code = undone ?? (reply.code === "already-exists" ? "already-exists" : "failed-precondition");
|
|
8512
|
+
const code = undone ?? reply.code ?? "failed-precondition";
|
|
8439
8513
|
rejectAccept(new EpEnvelopeError(code, reply.error ?? "spawn refused at accept", details, effectStarted && undone === undefined ? undefined : "not-executed"));
|
|
8440
8514
|
return;
|
|
8441
8515
|
}
|