@cotal-ai/runtime 0.49.0 → 0.50.0
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/host-starvation.d.ts +45 -7
- package/dist/host-starvation.d.ts.map +1 -1
- package/dist/host-starvation.js +57 -9
- package/dist/host-starvation.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/mesh-handler.d.ts +69 -1
- package/dist/mesh-handler.d.ts.map +1 -1
- package/dist/mesh-handler.js +197 -8
- package/dist/mesh-handler.js.map +1 -1
- package/dist/run-command.d.ts +22 -1
- package/dist/run-command.d.ts.map +1 -1
- package/dist/run-command.js +34 -7
- package/dist/run-command.js.map +1 -1
- package/dist/run-host.d.ts +7 -0
- package/dist/run-host.d.ts.map +1 -1
- package/dist/run-host.js +16 -1
- package/dist/run-host.js.map +1 -1
- package/package.json +7 -4
package/dist/mesh-handler.js
CHANGED
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
* again.
|
|
18
18
|
*/
|
|
19
19
|
import { createHash } from "node:crypto";
|
|
20
|
-
import {
|
|
20
|
+
import { isAbsolute } from "node:path";
|
|
21
|
+
import { realpathSync } from "node:fs";
|
|
22
|
+
import { mintCheckpoint, heartbeatCheckpoint, resumeCheckpoint, readCheckpointSettle, readCheckpointAnswer, readCheckpointStatus, readCheckpointSpec, reconcileCheckpointSchedule, handleCheckpointFire, eptStreamName, eptSubject, chatStream, waitConsumerName, waitConsumerConfig, isConcreteChannel, assertSafePattern, assertValidChannel, presenceBucket, principalKey, liveKvEntries, IncompleteKvScan, openMembersRegistry, openChannelRegistry, writeChannelConfig, readMember, commitMember, tombstoneMember, StaleMembershipWrite, runNoticeId, writeRunNotice, actionContext, invokeCommand, replyRefusedBeforeEffect, readGoalResult, readGoalStatus, resolveService, listRunNotices, listRunMigrations, markRunNoticeConsumed, listRunNoticesForRun, readRunRecord, writeRunStatus, EpEnvelopeError, assertAdmittedPublish, assertAdmittedSubscribe, assertNotRevoked, } from "@cotal-ai/core";
|
|
21
23
|
import { renderRunContext } from "./run-context.js";
|
|
22
24
|
import { migrationSeats } from "./migrate.js";
|
|
23
25
|
import { Kvm } from "@nats-io/kv";
|
|
@@ -92,7 +94,17 @@ export class MeshHandler {
|
|
|
92
94
|
* the next effect instead of poisoning every spawn for the handler's lifetime.
|
|
93
95
|
*/
|
|
94
96
|
managerService;
|
|
95
|
-
manager() {
|
|
97
|
+
manager(instanceId) {
|
|
98
|
+
// #1616 ITEM 3 — PINNED DISPATCH. An explicit placement target resolves through the EXISTING
|
|
99
|
+
// instance-dispatch API: `resolveService`'s `instanceId` opt (endpoint-invoke.ts:274-280)
|
|
100
|
+
// becomes `EpRoute { mode: "inst", instanceId }` at :113, and the handle it returns carries
|
|
101
|
+
// `pinnedInstanceId` (:315) so `invokeCommand` addresses that instance and never the class
|
|
102
|
+
// queue. It is deliberately NOT served from `managerService`: that memo holds the class-anycast
|
|
103
|
+
// resolution, and handing a pinned caller the anycast handle would reinstate the exact fallback
|
|
104
|
+
// this item removes. A wrong, unavailable or replaced instance therefore fails to resolve —
|
|
105
|
+
// before a child exists — instead of quietly succeeding somewhere else.
|
|
106
|
+
if (instanceId !== undefined)
|
|
107
|
+
return resolveService(this.nc, this.binding.space, this.binding.endpoint, this.binding.caller, { instanceId });
|
|
96
108
|
this.managerService ??= resolveService(this.nc, this.binding.space, this.binding.endpoint, this.binding.caller)
|
|
97
109
|
.catch((e) => {
|
|
98
110
|
this.managerService = undefined;
|
|
@@ -100,6 +112,99 @@ export class MeshHandler {
|
|
|
100
112
|
});
|
|
101
113
|
return this.managerService;
|
|
102
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* One manager call, with a SPEC 13.2 bind refusal REPAIRED rather than raised.
|
|
117
|
+
*
|
|
118
|
+
* A run resolves the manager on the class rail and binds the incarnation that answered its
|
|
119
|
+
* describe. The invoke is a second, independent trip through the same anycast queue, so in a
|
|
120
|
+
* space with more than one manager it routinely reaches another member, and that member refuses
|
|
121
|
+
* before dispatching. The refusal is honest for one command and destructive for a run: it says
|
|
122
|
+
* the command did not run and its remedy is to re-issue, but raised as the effect's own failure
|
|
123
|
+
* it ends the run and consumes the run id and its journal (#1638).
|
|
124
|
+
*
|
|
125
|
+
* So a refusal the responder MARKS as pre-effect is re-issued instead of returned. It is a first
|
|
126
|
+
* attempt and not a second: the marker together with `not-executed` is the responder's own
|
|
127
|
+
* statement that no effect of the command exists, which is what {@link replyRefusedBeforeEffect}
|
|
128
|
+
* checks, and it is the same licence core's `Endpoint.invokeService` re-issues on. The stale
|
|
129
|
+
* class handle is dropped first, so the re-issue re-describes rather than rebinding the
|
|
130
|
+
* incarnation that was just refused.
|
|
131
|
+
*
|
|
132
|
+
* BOUNDED, because a re-issue draws the same queue again. The describe and the invoke stay two
|
|
133
|
+
* independent trips, so a space of m managers still splits (m-1)/m of the time and the repair
|
|
134
|
+
* converges geometrically rather than deterministically; after {@link BIND_SPLIT_REISSUES} of
|
|
135
|
+
* them the refusal surfaces unchanged, still stating that the command did not run. What removes
|
|
136
|
+
* the residual is addressing one instance, and the run's caller holds no instance-rail grant for
|
|
137
|
+
* a command its program did not place (SPEC 13.9, `run-driver-grants.ts`), so that is a wider
|
|
138
|
+
* change than this one.
|
|
139
|
+
*
|
|
140
|
+
* A PINNED handle is never repaired. It addresses one instance by name, so a refusal from it is
|
|
141
|
+
* that incarnation answering about itself, and re-resolving onto the class rail would reinstate
|
|
142
|
+
* the anycast fallback #1616 removed.
|
|
143
|
+
*/
|
|
144
|
+
async invokeManager(service, command, args, opts) {
|
|
145
|
+
let handle = service;
|
|
146
|
+
for (let reissues = 0;; reissues += 1) {
|
|
147
|
+
const reply = await invokeCommand(this.nc, this.binding.space, handle, command, args, opts);
|
|
148
|
+
if (reply.reply.ok !== false || !replyRefusedBeforeEffect(reply.reply.error))
|
|
149
|
+
return reply;
|
|
150
|
+
if (handle.pinnedInstanceId !== undefined || reissues === BIND_SPLIT_REISSUES)
|
|
151
|
+
return reply;
|
|
152
|
+
this.managerService = undefined;
|
|
153
|
+
try {
|
|
154
|
+
handle = await this.manager();
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// The repair could not be attempted. The REFUSAL is what surfaces, not the resolve failure:
|
|
158
|
+
// every caller of this method already reads a refused reply as "the manager declined", and
|
|
159
|
+
// this one states that nothing ran, which is the fact a describe timeout raised in its
|
|
160
|
+
// place would lose.
|
|
161
|
+
return reply;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* #1616 item 5 — PHASE A, RESOLVE ON THE HOST THAT WILL LAUNCH. A directory is a fact about one
|
|
167
|
+
* filesystem, so the only process that can canonicalize it is the manager instance that will
|
|
168
|
+
* `chdir` into it. This asks the PINNED instance for the canonical form and returns it together
|
|
169
|
+
* with the identity that answered, so phase B dispatches the resolved path and the journal records
|
|
170
|
+
* which host resolved it. There is deliberately NO envelope `id`: a resolve binds no goal, takes
|
|
171
|
+
* no reservation and allocates nothing, so a retry or a crash between the phases costs nothing.
|
|
172
|
+
*
|
|
173
|
+
* EVERY failure direction is a REFUSAL. A path the target cannot resolve is not passed through as
|
|
174
|
+
* the raw string: the raw string would launch somewhere plausible on the manager's own root, which
|
|
175
|
+
* is the fail-open outcome this repair exists to remove.
|
|
176
|
+
*/
|
|
177
|
+
async resolveCwd(req, service, cwd, instanceId) {
|
|
178
|
+
let reply;
|
|
179
|
+
try {
|
|
180
|
+
reply = await invokeCommand(this.nc, this.binding.space, service, "resolve-cwd", { cwd }, {
|
|
181
|
+
deadlineMs: SPAWN_ACCEPT_DEADLINE_MS,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
// An older manager does not list `resolve-cwd`, and `invokeCommand` refuses an unlisted
|
|
186
|
+
// command with `not-found` before it publishes anything (endpoint-invoke.ts:349-350). That is
|
|
187
|
+
// the fail-CLOSED direction and it stays closed: a host that cannot answer the question does
|
|
188
|
+
// not get handed the caller's guess.
|
|
189
|
+
throw cwdResolutionRefusal(req.persona, instanceId, cwd, err instanceof EpEnvelopeError && err.code === "not-found"
|
|
190
|
+
? `this manager serves no resolve-cwd command, so it can state no canonical form (${err.message})`
|
|
191
|
+
: String(err?.message ?? err));
|
|
192
|
+
}
|
|
193
|
+
if (reply.reply.ok === false)
|
|
194
|
+
throw cwdResolutionRefusal(req.persona, instanceId, cwd, reply.reply.error?.message ?? "refused with no message");
|
|
195
|
+
const data = reply.reply.data;
|
|
196
|
+
if (typeof data?.cwd !== "string" || data.cwd.length === 0 || !isAbsolute(data.cwd))
|
|
197
|
+
throw cwdResolutionRefusal(req.persona, instanceId, cwd, `the reply names no absolute directory (${JSON.stringify(data?.cwd)})`);
|
|
198
|
+
// The AUTHORITATIVE identity is the one that answered, off the attributed reply's subject, not
|
|
199
|
+
// the one the caller asked for: the pinned resolve already rejects a reply from another
|
|
200
|
+
// instance, so recording the responder is recording what was checked.
|
|
201
|
+
return {
|
|
202
|
+
cwd: data.cwd,
|
|
203
|
+
endpoint: reply.responder.endpoint,
|
|
204
|
+
instanceId: reply.responder.instanceId,
|
|
205
|
+
...(typeof data.host === "string" ? { host: data.host } : {}),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
103
208
|
/** The branded goal-fact context over this handler's own connection, memoized the same way. */
|
|
104
209
|
actions;
|
|
105
210
|
actionCtx() {
|
|
@@ -484,8 +589,7 @@ export class MeshHandler {
|
|
|
484
589
|
console.error(`! discharge: the cancelled spawn goal "${goalId}" settled ${fact.state} with no readable agent identity; if its seat is up it must be despawned by hand (cotal ps)`);
|
|
485
590
|
return;
|
|
486
591
|
}
|
|
487
|
-
const
|
|
488
|
-
const reply = await invokeCommand(this.nc, this.binding.space, service, "despawn", { graceful: true }, {
|
|
592
|
+
const reply = await this.invokeManager(await this.manager(), "despawn", { graceful: true }, {
|
|
489
593
|
target: { mode: "owner", ...target },
|
|
490
594
|
deadlineMs: SPAWN_ACCEPT_DEADLINE_MS,
|
|
491
595
|
});
|
|
@@ -1025,6 +1129,28 @@ export class MeshHandler {
|
|
|
1025
1129
|
if (ctx.signal.cancelled)
|
|
1026
1130
|
throw new Cancelled(ctx.signal.reason ?? "cancelled");
|
|
1027
1131
|
const goalId = ctx.requestId;
|
|
1132
|
+
// Placement is explicit and host-local. Refuse malformed values before manager discovery or
|
|
1133
|
+
// submission, so an invalid request can never turn into an omitted `cwd` and inherit the
|
|
1134
|
+
// manager's workspace root. Existence and launch authority stay with the serving manager.
|
|
1135
|
+
if (req.cwd !== undefined && (typeof req.cwd !== "string" || req.cwd.length === 0 || !isAbsolute(req.cwd)))
|
|
1136
|
+
throw new EffectError("L4000", "spawn", `spawn(${req.persona}) cwd must be a non-empty absolute directory on the serving manager's host; refusing ${JSON.stringify(req.cwd)} rather than falling back`);
|
|
1137
|
+
// #1616 ITEM 2 — THE AFFINITY GATE. A directory is HOST-LOCAL, so a request that names one but
|
|
1138
|
+
// names no manager instance rides the class `one` queue and lands wherever the anycast fell.
|
|
1139
|
+
// The design record calls that combination not acceptable, so it REFUSES. There is deliberately
|
|
1140
|
+
// NO anycast fallback: falling back IS the defect. And the refusal is raised HERE, ahead of
|
|
1141
|
+
// `readPermits`, ahead of `this.manager()`'s describe round-trip, ahead of any submission,
|
|
1142
|
+
// acceptance or launch — so nothing is reserved, claimed or started before it. Legacy
|
|
1143
|
+
// cwd-omitted spawns never reach this line and keep their prior behaviour byte for byte.
|
|
1144
|
+
if (req.cwd !== undefined && req.placement === undefined)
|
|
1145
|
+
throw new EffectError("L4000", "spawn", `spawn(${req.persona}) names a cwd but no placement target: a host-local directory needs an explicit { endpoint, instanceId } manager instance, and this spawn refuses rather than falling back to class anycast`);
|
|
1146
|
+
if (req.placement !== undefined
|
|
1147
|
+
&& (typeof req.placement.endpoint !== "string" || req.placement.endpoint.length === 0
|
|
1148
|
+
|| typeof req.placement.instanceId !== "string" || req.placement.instanceId.length === 0))
|
|
1149
|
+
throw new EffectError("L4000", "spawn", `spawn(${req.persona}) placement must name both an endpoint and an instanceId; refusing ${JSON.stringify(req.placement)} rather than dispatching unpinned`);
|
|
1150
|
+
// Naming a target the run is not bound to is a mismatched target, not a request to go find it:
|
|
1151
|
+
// automatic manager discovery is outside this bounded repair, so it refuses here too.
|
|
1152
|
+
if (req.placement !== undefined && req.placement.endpoint !== this.binding.endpoint)
|
|
1153
|
+
throw new EffectError("L4000", "spawn", `spawn(${req.persona}) placement targets endpoint ${JSON.stringify(req.placement.endpoint)} but this run is bound to ${JSON.stringify(this.binding.endpoint)}; refusing rather than dispatching off-binding`);
|
|
1028
1154
|
// A recorded goalId is a previous attempt's ACCEPTANCE: the submission landed and its
|
|
1029
1155
|
// identity was bound before the crash. Go straight back to the terminal. An entry that says
|
|
1030
1156
|
// `adoptedFrom` names the goal the ORPHANED spawn submitted, not this step's own request id:
|
|
@@ -1073,14 +1199,31 @@ export class MeshHandler {
|
|
|
1073
1199
|
try {
|
|
1074
1200
|
if (ext === undefined) {
|
|
1075
1201
|
let reply;
|
|
1202
|
+
// #1616 item 5 — PHASE A, then phase B. A previous attempt's journalled resolution is
|
|
1203
|
+
// reused rather than re-asked, so the path a resumed spawn launches in is the one the
|
|
1204
|
+
// resolve recorded and cannot drift under a re-resolve. A spawn naming no cwd has nothing
|
|
1205
|
+
// to resolve and skips both the invoke and the bind, so its behaviour is unchanged.
|
|
1206
|
+
let resolution = readCwdResolution(recorded?.resolution);
|
|
1076
1207
|
try {
|
|
1077
|
-
const service = await this.manager();
|
|
1078
|
-
|
|
1208
|
+
const service = await this.manager(req.placement?.instanceId);
|
|
1209
|
+
if (req.cwd !== undefined && resolution === undefined) {
|
|
1210
|
+
// `req.placement` is guaranteed here: a cwd without one refused above, at :1203.
|
|
1211
|
+
resolution = await this.resolveCwd(req, service, req.cwd, req.placement?.instanceId ?? "");
|
|
1212
|
+
// PERSIST BEFORE SUBMITTING. The resolution is what phase B dispatches and what a
|
|
1213
|
+
// resume re-reads; binding it after the submission would leave a crash in between with
|
|
1214
|
+
// a launched seat whose directory no record names.
|
|
1215
|
+
await ctx.bind({ resolution });
|
|
1216
|
+
}
|
|
1217
|
+
reply = await this.invokeManager(service, "spawn", spawnArgs(resolution === undefined ? req : { ...req, cwd: resolution.cwd }), {
|
|
1079
1218
|
id: goalId,
|
|
1080
1219
|
deadlineMs: SPAWN_ACCEPT_DEADLINE_MS,
|
|
1081
1220
|
});
|
|
1082
1221
|
}
|
|
1083
1222
|
catch (err) {
|
|
1223
|
+
// A refusal this handler RAISED is never a lost reply: it was decided here, before
|
|
1224
|
+
// anything was submitted, so it is re-thrown rather than weighed against a goal record.
|
|
1225
|
+
if (err instanceof EffectError)
|
|
1226
|
+
throw err;
|
|
1084
1227
|
// The invoke did not come back — which does not prove nothing happened: the request may
|
|
1085
1228
|
// have been accepted while the reply was lost. The goal record is the arbiter: a durable
|
|
1086
1229
|
// trace under this goalId means the submission landed, so proceed to its terminal; none
|
|
@@ -1107,6 +1250,9 @@ export class MeshHandler {
|
|
|
1107
1250
|
ext = {
|
|
1108
1251
|
goalId,
|
|
1109
1252
|
...(floor !== undefined ? pickAcceptanceFloor(floor) : {}),
|
|
1253
|
+
// `bind` REPLACES the external record, so the phase-A resolution is re-stated here or the
|
|
1254
|
+
// acceptance bind would erase it and a resume would re-resolve against a moved target.
|
|
1255
|
+
...(resolution !== undefined ? { resolution } : {}),
|
|
1110
1256
|
...(req.worktree !== undefined ? { worktree: req.worktree } : {}),
|
|
1111
1257
|
...(req.onFork !== undefined ? { onFork: req.onFork } : {}),
|
|
1112
1258
|
...(req.permits !== undefined ? { permits: req.permits } : {}),
|
|
@@ -1234,7 +1380,7 @@ export class MeshHandler {
|
|
|
1234
1380
|
throw new EffectError("L4001", "permit-wall-clock", `turn(${name}#${uid}): a ${deadlineMs}ms deadline runs past the agent's remaining wall clock (${remaining}ms of its ${entry.permits.wallClockMs}ms permit)`);
|
|
1235
1381
|
}
|
|
1236
1382
|
const payload = JSON.stringify({ run: this.binding.runId, step, context, noticeIds: notices.map((n) => n.noticeId) });
|
|
1237
|
-
const submit = async () =>
|
|
1383
|
+
const submit = async () => this.invokeManager(await this.manager(), "turn", { payload, deadlineMs, ...(handoffFrom !== undefined ? { handoffFrom } : {}) }, {
|
|
1238
1384
|
id: goalId,
|
|
1239
1385
|
deadlineMs: TURN_ACCEPT_DEADLINE_MS,
|
|
1240
1386
|
target: { mode: "owner", owner, actor, lifecycleUid: uid },
|
|
@@ -1593,7 +1739,7 @@ export class MeshHandler {
|
|
|
1593
1739
|
return;
|
|
1594
1740
|
let reply;
|
|
1595
1741
|
try {
|
|
1596
|
-
reply = await
|
|
1742
|
+
reply = await this.invokeManager(await this.manager(), "turn", { payload, deadlineMs: Math.max(1_000, deadlineAt - this.now()) }, {
|
|
1597
1743
|
id: goalId,
|
|
1598
1744
|
deadlineMs: TURN_ACCEPT_DEADLINE_MS,
|
|
1599
1745
|
target: { mode: "owner", owner: seat.owner, actor: seat.actor, lifecycleUid: uid },
|
|
@@ -2205,6 +2351,14 @@ const GOAL_POLL_MS = 2_000;
|
|
|
2205
2351
|
const SPAWN_ACCEPT_DEADLINE_MS = 30_000;
|
|
2206
2352
|
/** Bound on the manager's synchronous `turn` ACCEPT reply (the relay registration, not the yield). */
|
|
2207
2353
|
const TURN_ACCEPT_DEADLINE_MS = 30_000;
|
|
2354
|
+
/** How many times {@link MeshHandler.invokeManager} re-issues one manager call after a
|
|
2355
|
+
* `not-executed` bind refusal. Every re-issue is a first attempt, so the bound is a loop guard and
|
|
2356
|
+
* not a duplication guard: it stops a class whose describe and invoke never agree from re-issuing
|
|
2357
|
+
* forever. Nine attempts leave a two-manager space a 1-in-512 residual where the unrepaired refusal
|
|
2358
|
+
* was 1-in-2 (#1638). The loop turns only on a refusal that has already been ANSWERED, so an
|
|
2359
|
+
* attempt costs a describe and an invoke round trip and never an elapsed deadline: a call nobody
|
|
2360
|
+
* answers raises its own `deadline-exceeded`, which is not a bind refusal and is not re-issued. */
|
|
2361
|
+
const BIND_SPLIT_REISSUES = 8;
|
|
2208
2362
|
/** A step key's enclosing scope: the journal's own rendering (`entry.scope`), re-derived so the
|
|
2209
2363
|
* live path and the adoption rebuild key the handoff memos identically. */
|
|
2210
2364
|
function scopeOf(key) {
|
|
@@ -2217,11 +2371,46 @@ const DISCHARGE_TERMINAL_BOUND_MS = 30_000;
|
|
|
2217
2371
|
/** The manager `spawn` args a {@link SpawnRequest} submits: persona names the persona file
|
|
2218
2372
|
* (`name`), `join` becomes the seat's channel subscriptions. `permits` stay on the run (they
|
|
2219
2373
|
* bind at `turn`); `supervise` travels because the manager is who restarts the process. */
|
|
2374
|
+
/**
|
|
2375
|
+
* #1616 proof item 5 — ALIAS NORMALIZATION POLICY. One clone reached through a symlink and through
|
|
2376
|
+
* its realpath is ONE writable directory, and the single-writer rule keys on identity, so the two
|
|
2377
|
+
* forms must collapse before any claim is taken. THE CANONICAL FORM IS THE REALPATH: it is the form
|
|
2378
|
+
* the kernel and the child's own `process.cwd()` report, so a claim keyed on it matches what any
|
|
2379
|
+
* concurrent run or imperative spawn path observes, whichever alias that caller typed. The symlink
|
|
2380
|
+
* path is NOT canonical — normalizing toward it would require resolving every other alias to it,
|
|
2381
|
+
* which has no unique answer. Resolution is done ONCE, on the serving host, before the claim and
|
|
2382
|
+
* before launch; an unresolvable path is a refusal, never a pass-through of the raw string.
|
|
2383
|
+
*/
|
|
2384
|
+
export function canonicalCwd(cwd) {
|
|
2385
|
+
return realpathSync(cwd);
|
|
2386
|
+
}
|
|
2387
|
+
/** The single named refusal every phase-A failure direction produces: the target does not serve
|
|
2388
|
+
* `resolve-cwd`, refuses the path, or answers with something that is not an absolute directory.
|
|
2389
|
+
* `L4000` is the host declining the request, and it is raised before any submission, so nothing is
|
|
2390
|
+
* bound, allocated or launched. There is no raw-path arm: a fall-back to the caller's string is
|
|
2391
|
+
* fail-OPEN, and launching somewhere plausible is the outcome being removed. */
|
|
2392
|
+
export function cwdResolutionRefusal(persona, instanceId, cwd, cause) {
|
|
2393
|
+
return new EffectError("L4000", "spawn", `spawn(${persona}) cwd ${JSON.stringify(cwd)} was not resolved by manager instance ${instanceId}: ${cause}; refusing rather than dispatching a path this host never canonicalized`);
|
|
2394
|
+
}
|
|
2395
|
+
/** A journalled phase-A resolution, read back on resume. A garbled entry is treated as absent and
|
|
2396
|
+
* phase A runs again: re-resolving is free (no goal, no reservation), trusting a broken record is
|
|
2397
|
+
* not. */
|
|
2398
|
+
function readCwdResolution(value) {
|
|
2399
|
+
const r = value;
|
|
2400
|
+
if (typeof r?.cwd !== "string" || r.cwd.length === 0 || typeof r.endpoint !== "string" || typeof r.instanceId !== "string")
|
|
2401
|
+
return undefined;
|
|
2402
|
+
return { cwd: r.cwd, endpoint: r.endpoint, instanceId: r.instanceId, ...(typeof r.host === "string" ? { host: r.host } : {}) };
|
|
2403
|
+
}
|
|
2220
2404
|
export function spawnArgs(req) {
|
|
2221
2405
|
return {
|
|
2222
2406
|
name: req.persona,
|
|
2223
2407
|
...(req.model !== undefined ? { model: req.model } : {}),
|
|
2224
2408
|
...(req.variant !== undefined ? { variant: req.variant } : {}),
|
|
2409
|
+
// #1616 item 5: the cwd dispatched here is ALREADY the canonical form, resolved by the serving
|
|
2410
|
+
// manager in phase A (see MeshEffectHandler.resolveCwd) and read back off the journalled
|
|
2411
|
+
// resolution. This projection no longer canonicalizes anything: `realpathSync` in the driver
|
|
2412
|
+
// answers about the DRIVER's filesystem, which is a different host's answer to the question.
|
|
2413
|
+
...(req.cwd !== undefined ? { cwd: req.cwd } : {}),
|
|
2225
2414
|
...(req.role !== undefined ? { role: req.role } : {}),
|
|
2226
2415
|
...(req.join !== undefined && req.join.length > 0 ? { subscribe: req.join.map((c) => c.channel) } : {}),
|
|
2227
2416
|
...(req.supervise !== undefined ? { supervise: readSupervise(req.supervise, req.persona) } : {}),
|