flowviant 0.43.0 → 0.44.1
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/bin/lib/claude.mjs +9 -603
- package/bin/lib/fleet.mjs +57 -311
- package/bin/lib/prompts.mjs +610 -0
- package/bin/lib/work.mjs +875 -0
- package/package.json +1 -1
package/bin/lib/fleet.mjs
CHANGED
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
* MCP token, and only spawns Claude when the server says an agent has work.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
mkdirSync,
|
|
10
|
+
existsSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
readdirSync,
|
|
13
|
+
statSync,
|
|
14
|
+
} from 'node:fs';
|
|
9
15
|
import { execFileSync } from 'node:child_process';
|
|
10
16
|
import { createHash } from 'node:crypto';
|
|
11
17
|
import { homedir } from 'node:os';
|
|
@@ -59,8 +65,6 @@ import {
|
|
|
59
65
|
REGROUND_KICKOFF,
|
|
60
66
|
SYSTEM_PLAN,
|
|
61
67
|
PLAN_TURN_KICKOFF,
|
|
62
|
-
SYSTEM_WORK,
|
|
63
|
-
WORK_TURN_KICKOFF,
|
|
64
68
|
SYSTEM_QUICK_EDIT,
|
|
65
69
|
QUICK_EDIT_KICKOFF,
|
|
66
70
|
} from './claude.mjs';
|
|
@@ -79,6 +83,7 @@ import {
|
|
|
79
83
|
import { processDeployJobs, reportDeployConfig } from './deploy.mjs';
|
|
80
84
|
import { machineSnapshot } from './resources.mjs';
|
|
81
85
|
import { detectRuntimes, pickRuntimeFor, RUNTIMES } from './runtimes.mjs';
|
|
86
|
+
import { createWorkManager } from './work.mjs';
|
|
82
87
|
|
|
83
88
|
async function fetchRoster(haveIds) {
|
|
84
89
|
const url = new URL(FLEET_URL);
|
|
@@ -453,6 +458,7 @@ export async function runFleetDaemon() {
|
|
|
453
458
|
const workers = new Map(); // agentId -> { state, promise, wt, label }
|
|
454
459
|
let daemonAlive = true; // flipped false on shutdown so the stream stops reconnecting
|
|
455
460
|
let stream = null; // push channel handle (set once the loop is set up)
|
|
461
|
+
let workShutdown = null; // kills live session-turn CLIs (set with the work manager below)
|
|
456
462
|
|
|
457
463
|
// Shutdown KEEPS the worktrees: in-flight local work survives Ctrl+C and
|
|
458
464
|
// resumes in place on the next run (the task marker matches). Worktrees are
|
|
@@ -473,6 +479,14 @@ export async function runFleetDaemon() {
|
|
|
473
479
|
} catch {
|
|
474
480
|
/* best-effort */
|
|
475
481
|
}
|
|
482
|
+
// Session-turn CLIs die with the daemon too: an orphan keeps editing the
|
|
483
|
+
// session worktree and burning quota, and its live-pid lock would make the
|
|
484
|
+
// restarted daemon skip that tab's turns for as long as it survived.
|
|
485
|
+
try {
|
|
486
|
+
workShutdown?.();
|
|
487
|
+
} catch {
|
|
488
|
+
/* best-effort */
|
|
489
|
+
}
|
|
476
490
|
for (const [, w] of workers) {
|
|
477
491
|
w.state.alive = false;
|
|
478
492
|
try {
|
|
@@ -496,6 +510,15 @@ export async function runFleetDaemon() {
|
|
|
496
510
|
teardown();
|
|
497
511
|
process.exit(130);
|
|
498
512
|
});
|
|
513
|
+
// A service manager stops the daemon with SIGTERM, not Ctrl+C. Without this
|
|
514
|
+
// handler every child survived a `systemctl stop` — the exact orphaning the
|
|
515
|
+
// teardown exists to prevent.
|
|
516
|
+
process.on('SIGTERM', () => {
|
|
517
|
+
console.log('');
|
|
518
|
+
note('shutting down (SIGTERM) — stopping workers. Worktrees are kept: in-flight work resumes next run.');
|
|
519
|
+
teardown();
|
|
520
|
+
process.exit(143);
|
|
521
|
+
});
|
|
499
522
|
// Keep the daemon alive on a stray rejection. Many loops here are fire-and-
|
|
500
523
|
// forget (`void drainWiki()`, dispatch, sync) and rely on their callees never
|
|
501
524
|
// rejecting; Node ≥15 terminates the process on an unhandled rejection, which
|
|
@@ -1091,7 +1114,9 @@ export async function runFleetDaemon() {
|
|
|
1091
1114
|
await reportMergeOutcome(CONSULT_DONE_URL, {
|
|
1092
1115
|
consultId: job.id,
|
|
1093
1116
|
ok: false,
|
|
1094
|
-
|
|
1117
|
+
// Scrub, like the success path: an exception routinely quotes
|
|
1118
|
+
// command output, and command output can quote a synced secret.
|
|
1119
|
+
answer: envScrub(String(e?.message ?? 'the planning turn failed')).slice(0, 2000),
|
|
1095
1120
|
});
|
|
1096
1121
|
warn(`planning turn failed: ${e?.message ?? e}`);
|
|
1097
1122
|
} finally {
|
|
@@ -1103,312 +1128,23 @@ export async function runFleetDaemon() {
|
|
|
1103
1128
|
|
|
1104
1129
|
// ── Work sessions — the Workbench tabs ─────────────────────────────────────
|
|
1105
1130
|
//
|
|
1106
|
-
//
|
|
1107
|
-
//
|
|
1108
|
-
//
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
if (workToken && !force) return workToken;
|
|
1124
|
-
try {
|
|
1125
|
-
const res = await fetch(WORK_TOKEN_URL, {
|
|
1126
|
-
method: 'POST',
|
|
1127
|
-
headers: { Authorization: `Bearer ${FLEET_TOKEN}`, 'User-Agent': USER_AGENT },
|
|
1128
|
-
});
|
|
1129
|
-
if (!res.ok) return null;
|
|
1130
|
-
const data = await res.json().catch(() => null);
|
|
1131
|
-
workToken = data?.data?.token ?? null;
|
|
1132
|
-
return workToken;
|
|
1133
|
-
} catch {
|
|
1134
|
-
return null;
|
|
1135
|
-
}
|
|
1136
|
-
};
|
|
1137
|
-
|
|
1138
|
-
/**
|
|
1139
|
-
* This tab's worktree — its held context, expressed as a place, ON A BRANCH.
|
|
1140
|
-
*
|
|
1141
|
-
* Fresh: branch `session/<id>` off the current base. Existing: touched not at
|
|
1142
|
-
* all — no fetch-reset-clean like a plan directory, because the dirty state
|
|
1143
|
-
* is the point. If the directory was retired but the branch survives, the
|
|
1144
|
-
* worktree re-attaches to the branch and the committed work is still there.
|
|
1145
|
-
*/
|
|
1146
|
-
const sessionWtFor = (sessionId) => {
|
|
1147
|
-
if (!isSafePathSegment(sessionId)) return null;
|
|
1148
|
-
const wt = join(baseDir, 'sessions', sessionId);
|
|
1149
|
-
const fresh = !existsSync(wt);
|
|
1150
|
-
if (fresh) {
|
|
1151
|
-
const branch = `session/${sessionId}`;
|
|
1152
|
-
try {
|
|
1153
|
-
git(['worktree', 'add', '-b', branch, wt, baseRef], repoRoot);
|
|
1154
|
-
} catch {
|
|
1155
|
-
git(['worktree', 'prune'], repoRoot);
|
|
1156
|
-
try {
|
|
1157
|
-
// The branch may already exist (a retired directory's work) — attach.
|
|
1158
|
-
git(['worktree', 'add', wt, branch], repoRoot);
|
|
1159
|
-
} catch {
|
|
1160
|
-
try {
|
|
1161
|
-
git(['worktree', 'add', '-b', branch, wt, baseRef], repoRoot);
|
|
1162
|
-
} catch {
|
|
1163
|
-
return null;
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
return { wt, fresh };
|
|
1169
|
-
};
|
|
1170
|
-
|
|
1171
|
-
/**
|
|
1172
|
-
* Retire the least-recently-touched CLEAN session directories past the cap.
|
|
1173
|
-
* A dirty worktree is never touched — uncommitted work is the human's, and a
|
|
1174
|
-
* resource bound does not outrank it. Committed work survives retirement on
|
|
1175
|
-
* the session branch either way.
|
|
1176
|
-
*/
|
|
1177
|
-
const MAX_WORK_DIRS = 12;
|
|
1178
|
-
const workTouched = new Map(); // sessionId -> ms
|
|
1179
|
-
const retireIdleWorkSessions = () => {
|
|
1180
|
-
const dir = join(baseDir, 'sessions');
|
|
1181
|
-
if (!existsSync(dir)) return;
|
|
1182
|
-
let ids;
|
|
1183
|
-
try {
|
|
1184
|
-
ids = readdirSync(dir);
|
|
1185
|
-
} catch {
|
|
1186
|
-
return;
|
|
1187
|
-
}
|
|
1188
|
-
if (ids.length <= MAX_WORK_DIRS) return;
|
|
1189
|
-
const oldestFirst = ids.sort(
|
|
1190
|
-
(a, b) => (workTouched.get(a) ?? 0) - (workTouched.get(b) ?? 0)
|
|
1191
|
-
);
|
|
1192
|
-
let excess = ids.length - MAX_WORK_DIRS;
|
|
1193
|
-
for (const id of oldestFirst) {
|
|
1194
|
-
if (excess <= 0) break;
|
|
1195
|
-
const wt = join(dir, id);
|
|
1196
|
-
try {
|
|
1197
|
-
if (git(['status', '--porcelain'], wt).trim() !== '') continue; // dirty — skip
|
|
1198
|
-
git(['worktree', 'remove', wt], repoRoot);
|
|
1199
|
-
workTouched.delete(id);
|
|
1200
|
-
excess--;
|
|
1201
|
-
} catch {
|
|
1202
|
-
/* leave it; a directory we can't cleanly remove is not worth a turn */
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
try {
|
|
1206
|
-
git(['worktree', 'prune'], repoRoot);
|
|
1207
|
-
} catch {
|
|
1208
|
-
/* best effort */
|
|
1209
|
-
}
|
|
1210
|
-
};
|
|
1211
|
-
|
|
1212
|
-
const processWorkTurns = (jobs) => {
|
|
1213
|
-
for (const job of jobs ?? []) {
|
|
1214
|
-
if (!job || typeof job.id !== 'string' || !job.body || !job.sessionId) continue;
|
|
1215
|
-
if (workAnswering.has(job.id)) continue;
|
|
1216
|
-
const tries = (workAttempts.get(job.id) ?? 0) + 1;
|
|
1217
|
-
if (tries > MAX_WORK_TRIES) continue;
|
|
1218
|
-
workAttempts.set(job.id, tries);
|
|
1219
|
-
workAnswering.add(job.id);
|
|
1220
|
-
const chain = workChains.get(job.sessionId) ?? Promise.resolve();
|
|
1221
|
-
workChains.set(
|
|
1222
|
-
job.sessionId,
|
|
1223
|
-
chain.then(async () => {
|
|
1224
|
-
try {
|
|
1225
|
-
note(
|
|
1226
|
-
`${c.cyan('tab')} ${c.dim(`— ${job.askedByName || 'the owner'} in "${job.sessionName || 'a session'}"`)}`
|
|
1227
|
-
);
|
|
1228
|
-
const workRt = pickRuntimeFor('build');
|
|
1229
|
-
if (!workRt) {
|
|
1230
|
-
warn('a session turn is waiting, but no installed CLI can build here');
|
|
1231
|
-
return;
|
|
1232
|
-
}
|
|
1233
|
-
const token = await mintWorkToken();
|
|
1234
|
-
if (!token) {
|
|
1235
|
-
warn('a session turn is waiting, but the work credential could not be minted');
|
|
1236
|
-
return;
|
|
1237
|
-
}
|
|
1238
|
-
const dir = sessionWtFor(job.sessionId);
|
|
1239
|
-
if (!dir) {
|
|
1240
|
-
warn('a session turn is waiting, but its worktree could not be opened');
|
|
1241
|
-
return;
|
|
1242
|
-
}
|
|
1243
|
-
workTouched.set(job.sessionId, Date.now());
|
|
1244
|
-
const resume = !dir.fresh && Boolean(job.sessionRef);
|
|
1245
|
-
const mcp = mcpFor(workRt, token, mcpUrl);
|
|
1246
|
-
let out;
|
|
1247
|
-
try {
|
|
1248
|
-
out = await runTurn({
|
|
1249
|
-
prompt: WORK_TURN_KICKOFF({
|
|
1250
|
-
sessionId: job.sessionId,
|
|
1251
|
-
sessionName: job.sessionName,
|
|
1252
|
-
message: job.body,
|
|
1253
|
-
askedByName: job.askedByName,
|
|
1254
|
-
}),
|
|
1255
|
-
resume,
|
|
1256
|
-
system: SYSTEM_WORK,
|
|
1257
|
-
cwd: dir.wt,
|
|
1258
|
-
mcpArgs: mcp.args,
|
|
1259
|
-
mcpEnv: mcp.env,
|
|
1260
|
-
runtime: workRt,
|
|
1261
|
-
label: c.cyan('[tab]'),
|
|
1262
|
-
});
|
|
1263
|
-
} finally {
|
|
1264
|
-
if (mcp.dir) rmSync(mcp.dir, { recursive: true, force: true });
|
|
1265
|
-
}
|
|
1266
|
-
const answer = (out || '').trim();
|
|
1267
|
-
const posted = await reportMergeOutcome(WORK_DONE_URL, {
|
|
1268
|
-
turnId: job.id,
|
|
1269
|
-
ok: answer.length > 0,
|
|
1270
|
-
// Scrub: a reply can quote config or env-adjacent code.
|
|
1271
|
-
answer: envScrub(answer).slice(0, 16000),
|
|
1272
|
-
sessionRef: dir.wt,
|
|
1273
|
-
});
|
|
1274
|
-
if (posted) workAttempts.delete(job.id);
|
|
1275
|
-
ok(`${c.cyan('tab')} ${c.dim('— replied in the session')}`);
|
|
1276
|
-
retireIdleWorkSessions();
|
|
1277
|
-
} catch (e) {
|
|
1278
|
-
await reportMergeOutcome(WORK_DONE_URL, {
|
|
1279
|
-
turnId: job.id,
|
|
1280
|
-
ok: false,
|
|
1281
|
-
answer: e?.message ?? 'the session turn failed',
|
|
1282
|
-
});
|
|
1283
|
-
warn(`session turn failed: ${e?.message ?? e}`);
|
|
1284
|
-
} finally {
|
|
1285
|
-
workAnswering.delete(job.id);
|
|
1286
|
-
}
|
|
1287
|
-
})
|
|
1288
|
-
);
|
|
1289
|
-
}
|
|
1290
|
-
};
|
|
1291
|
-
|
|
1292
|
-
// Ship — a session's branch merging to main, on the human's word.
|
|
1293
|
-
//
|
|
1294
|
-
// --no-ff, NEVER squash: every delivered card carries commit shas as its
|
|
1295
|
-
// receipts, and a squash would point them all at commits that no longer
|
|
1296
|
-
// exist on main. Sequence: refuse a dirty worktree (auto-committing someone's
|
|
1297
|
-
// mid-thought state is not shipping, it is guessing), fold main INTO the
|
|
1298
|
-
// branch first so conflicts surface in the worktree where the session can
|
|
1299
|
-
// resolve them, collect the branch's own commits (the server's
|
|
1300
|
-
// reconciliation input), then merge outward through a throwaway worktree so
|
|
1301
|
-
// nobody's checkout moves. Failures report INTO the tab — a ship that failed
|
|
1302
|
-
// silently leaves the human believing their work is on main.
|
|
1303
|
-
const SHIP_DONE_URL = FLEET_URL.replace(/\/agents\/?$/, '/ship-done');
|
|
1304
|
-
const shipping = new Set();
|
|
1305
|
-
let shipChain = Promise.resolve();
|
|
1306
|
-
|
|
1307
|
-
const processShipJobs = (jobs) => {
|
|
1308
|
-
for (const job of jobs ?? []) {
|
|
1309
|
-
if (!job || typeof job.sessionId !== 'string') continue;
|
|
1310
|
-
if (shipping.has(job.sessionId)) continue;
|
|
1311
|
-
shipping.add(job.sessionId);
|
|
1312
|
-
shipChain = shipChain.then(async () => {
|
|
1313
|
-
const done = (payload) =>
|
|
1314
|
-
reportMergeOutcome(SHIP_DONE_URL, { sessionId: job.sessionId, ...payload }).catch(
|
|
1315
|
-
() => {}
|
|
1316
|
-
);
|
|
1317
|
-
try {
|
|
1318
|
-
if (!isSafePathSegment(job.sessionId)) {
|
|
1319
|
-
await done({ ok: false, error: 'invalid session id' });
|
|
1320
|
-
return;
|
|
1321
|
-
}
|
|
1322
|
-
note(`${c.cyan('ship')} ${c.dim(`— "${job.sessionName || job.sessionId}"`)}`);
|
|
1323
|
-
const wt = join(baseDir, 'sessions', job.sessionId);
|
|
1324
|
-
if (!existsSync(wt)) {
|
|
1325
|
-
await done({ ok: false, error: 'no session worktree on this machine' });
|
|
1326
|
-
return;
|
|
1327
|
-
}
|
|
1328
|
-
if (git(['status', '--porcelain'], wt) !== '') {
|
|
1329
|
-
await done({
|
|
1330
|
-
ok: false,
|
|
1331
|
-
error:
|
|
1332
|
-
'the session has uncommitted changes — ask it to commit or discard them first',
|
|
1333
|
-
});
|
|
1334
|
-
return;
|
|
1335
|
-
}
|
|
1336
|
-
try {
|
|
1337
|
-
git(['fetch', 'origin', '--quiet'], repoRoot);
|
|
1338
|
-
} catch {
|
|
1339
|
-
/* offline fetch — merge against what we have */
|
|
1340
|
-
}
|
|
1341
|
-
// Fold main into the branch FIRST: conflicts land here, in the
|
|
1342
|
-
// session's own worktree, where the next turn can resolve them.
|
|
1343
|
-
try {
|
|
1344
|
-
git(['merge', '--no-edit', baseRef], wt);
|
|
1345
|
-
} catch {
|
|
1346
|
-
try {
|
|
1347
|
-
git(['merge', '--abort'], wt);
|
|
1348
|
-
} catch {
|
|
1349
|
-
/* nothing in progress */
|
|
1350
|
-
}
|
|
1351
|
-
await done({
|
|
1352
|
-
ok: false,
|
|
1353
|
-
error: 'conflicts with main — ask the session to resolve them, then ship again',
|
|
1354
|
-
});
|
|
1355
|
-
return;
|
|
1356
|
-
}
|
|
1357
|
-
// The branch's own commits — the server's reconciliation input.
|
|
1358
|
-
// --no-merges: fold-commits describe plumbing, not work.
|
|
1359
|
-
const commits = git([
|
|
1360
|
-
'log',
|
|
1361
|
-
`${baseRef}..HEAD`,
|
|
1362
|
-
'--no-merges',
|
|
1363
|
-
'--format=%H%x09%s',
|
|
1364
|
-
], wt)
|
|
1365
|
-
.split('\n')
|
|
1366
|
-
.filter(Boolean)
|
|
1367
|
-
.map((l) => {
|
|
1368
|
-
const [sha, ...rest] = l.split('\t');
|
|
1369
|
-
return { sha, subject: envScrub(rest.join('\t')).slice(0, 200) };
|
|
1370
|
-
});
|
|
1371
|
-
if (commits.length === 0) {
|
|
1372
|
-
await done({ ok: false, error: 'nothing to ship — no commits on the session branch' });
|
|
1373
|
-
return;
|
|
1374
|
-
}
|
|
1375
|
-
// Merge outward through a throwaway worktree so no checkout moves.
|
|
1376
|
-
const branch = `session/${job.sessionId}`;
|
|
1377
|
-
const tmp = join(baseDir, 'ship', job.sessionId);
|
|
1378
|
-
try {
|
|
1379
|
-
try {
|
|
1380
|
-
git(['worktree', 'remove', '--force', tmp], repoRoot);
|
|
1381
|
-
} catch {
|
|
1382
|
-
/* not there — fine */
|
|
1383
|
-
}
|
|
1384
|
-
git(['worktree', 'add', '--detach', tmp, baseRef], repoRoot);
|
|
1385
|
-
git([
|
|
1386
|
-
'merge',
|
|
1387
|
-
'--no-ff',
|
|
1388
|
-
branch,
|
|
1389
|
-
'-m',
|
|
1390
|
-
`ship(${job.sessionName || job.sessionId.slice(0, 8)}): ${commits.length} commit${commits.length === 1 ? '' : 's'}`,
|
|
1391
|
-
], tmp);
|
|
1392
|
-
git(['push', 'origin', `HEAD:${baseBranchName(baseRef)}`], tmp);
|
|
1393
|
-
} finally {
|
|
1394
|
-
try {
|
|
1395
|
-
git(['worktree', 'remove', '--force', tmp], repoRoot);
|
|
1396
|
-
git(['worktree', 'prune'], repoRoot);
|
|
1397
|
-
} catch {
|
|
1398
|
-
/* best effort */
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1401
|
-
await done({ ok: true, commits });
|
|
1402
|
-
ok(`${c.cyan('ship')} ${c.dim(`— ${commits.length} commit${commits.length === 1 ? '' : 's'} on main`)}`);
|
|
1403
|
-
} catch (e) {
|
|
1404
|
-
await done({ ok: false, error: envScrub(e?.message ?? 'the merge failed').slice(0, 500) });
|
|
1405
|
-
warn(`ship failed: ${e?.message ?? e}`);
|
|
1406
|
-
} finally {
|
|
1407
|
-
shipping.delete(job.sessionId);
|
|
1408
|
-
}
|
|
1409
|
-
});
|
|
1410
|
-
}
|
|
1411
|
-
};
|
|
1131
|
+
// The whole machinery — per-session turn/ship chains, per-session work
|
|
1132
|
+
// tokens, the settle-every-turn contract, the ship executor, worktree
|
|
1133
|
+
// retirement — lives in work.mjs; this hands it the loop's mutable state.
|
|
1134
|
+
const {
|
|
1135
|
+
flushWorkReports,
|
|
1136
|
+
processWorkTurns,
|
|
1137
|
+
processShipJobs,
|
|
1138
|
+
retireWorkSessions,
|
|
1139
|
+
shutdownWork,
|
|
1140
|
+
} = createWorkManager({
|
|
1141
|
+
repoRoot,
|
|
1142
|
+
baseDir,
|
|
1143
|
+
baseRef,
|
|
1144
|
+
getMcpUrl: () => mcpUrl,
|
|
1145
|
+
getLeaseTtl: () => leaseTtlSeconds,
|
|
1146
|
+
});
|
|
1147
|
+
workShutdown = shutdownWork; // teardown can now reach the live session CLIs
|
|
1412
1148
|
|
|
1413
1149
|
const processMergeJobs = (jobs) => {
|
|
1414
1150
|
for (const job of jobs ?? []) {
|
|
@@ -2002,12 +1738,22 @@ export async function runFleetDaemon() {
|
|
|
2002
1738
|
});
|
|
2003
1739
|
if (updating) return;
|
|
2004
1740
|
}
|
|
1741
|
+
// Settle any turn/ship answers whose earlier report POST failed BEFORE
|
|
1742
|
+
// taking new work — the skip-if-pending guards make the ordering safe, but
|
|
1743
|
+
// delivering first keeps the tab honest a poll sooner.
|
|
1744
|
+
void flushWorkReports();
|
|
2005
1745
|
processMergeJobs(roster.mergeJobs);
|
|
2006
1746
|
processPatchRevertJobs(roster.patchRevertJobs);
|
|
2007
1747
|
processPlanCheckJobs(roster.planCheckJobs);
|
|
2008
1748
|
processConsultJobs(roster.consultJobs);
|
|
2009
1749
|
processWorkTurns(roster.workTurnJobs);
|
|
2010
|
-
|
|
1750
|
+
// The roster's live-session list rides along: an ENDED session's ship
|
|
1751
|
+
// must not be refused by checks whose remedies need a live tab.
|
|
1752
|
+
processShipJobs(roster.shipJobs, roster.activeWorkSessions);
|
|
1753
|
+
// AFTER the work/ship intake: retirement is the server saying which
|
|
1754
|
+
// sessions are LIVE, and the guards above (chains, shipping) are populated
|
|
1755
|
+
// by the intake this same tick.
|
|
1756
|
+
retireWorkSessions(roster.activeWorkSessions);
|
|
2011
1757
|
processJoinJobs(roster.joinJobs);
|
|
2012
1758
|
processCleanupJobs(roster.cleanupJobs);
|
|
2013
1759
|
const rosterIds = new Set(roster.agents.map((a) => a.agentId));
|