@yemi33/minions 0.1.2424 → 0.1.2426
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/install-internal-minions.js +1209 -0
- package/bin/minions.js +75 -9
- package/dashboard/docs/typography.md +27 -12
- package/dashboard/js/command-center.js +13 -11
- package/dashboard/js/live-stream.js +1 -1
- package/dashboard/js/memory-search.js +388 -43
- package/dashboard/js/modal-qa.js +5 -5
- package/dashboard/js/qa.js +39 -26
- package/dashboard/js/refresh.js +37 -18
- package/dashboard/js/render-agents.js +14 -18
- package/dashboard/js/render-dispatch.js +14 -74
- package/dashboard/js/render-inbox.js +3 -3
- package/dashboard/js/render-meetings.js +8 -8
- package/dashboard/js/render-other.js +45 -20
- package/dashboard/js/render-pipelines.js +6 -6
- package/dashboard/js/render-plans.js +14 -14
- package/dashboard/js/render-prd.js +46 -46
- package/dashboard/js/render-prs.js +9 -65
- package/dashboard/js/render-schedules.js +5 -5
- package/dashboard/js/render-skills.js +6 -6
- package/dashboard/js/render-utils.js +4 -4
- package/dashboard/js/render-watches.js +4 -16
- package/dashboard/js/render-work-items.js +95 -107
- package/dashboard/js/settings.js +175 -74
- package/dashboard/layout.html +3 -2
- package/dashboard/pages/inbox.html +1 -1
- package/dashboard/pages/work.html +1 -1
- package/dashboard/shared/model-display.js +9 -0
- package/dashboard/shared/watches-source.js +43 -0
- package/dashboard/shared/welcome-popup.js +259 -0
- package/dashboard/slim/body.html +6 -6
- package/dashboard/slim/js/chat.js +4 -2
- package/dashboard/slim/js/history.js +10 -3
- package/dashboard/slim/js/modals-tiles.js +85 -16
- package/dashboard/slim/js/status.js +12 -16
- package/dashboard/slim/styles.css +24 -4
- package/dashboard/styles.css +62 -27
- package/dashboard-build.js +8 -2
- package/dashboard.js +3854 -2514
- package/docs/README.md +7 -5
- package/docs/api-errors.md +144 -0
- package/docs/auto-discovery.md +83 -61
- package/docs/capture-demos.js +173 -30
- package/docs/command-center.md +5 -1
- package/docs/completion-reports.md +127 -11
- package/docs/constants.md +15 -3
- package/docs/constellation-style-telemetry.md +2 -2
- package/docs/copilot-cli-schema.md +32 -22
- package/docs/cross-repo-plans.md +30 -30
- package/docs/demo/memory-system.html +1 -1
- package/docs/deprecated.json +132 -18
- package/docs/diagnostics-memory.md +8 -4
- package/docs/human-vs-automated.md +2 -2
- package/docs/index.html +9 -2
- package/docs/internal-install.md +212 -0
- package/docs/kb-pr3223-cascade-archiving.md +16 -0
- package/docs/kb-pr696-merge-conflict-docs.md +23 -0
- package/docs/kb-sweep.md +2 -2
- package/docs/keep-processes.md +8 -1
- package/docs/live-checkout-mode.md +1 -1
- package/docs/managed-spawn.md +21 -3
- package/docs/named-agents.md +3 -2
- package/docs/onboarding.md +26 -0
- package/docs/plan-lifecycle.md +32 -32
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
- package/docs/qa-runbook-lifecycle.md +67 -51
- package/docs/qa-runbooks.md +2 -2
- package/docs/runtime-adapters.md +101 -50
- package/docs/security.md +28 -0
- package/docs/self-improvement.md +47 -13
- package/docs/skills.md +29 -0
- package/docs/slim-ux/concepts.md +2 -2
- package/docs/specs/agent-configurability.md +43 -41
- package/docs/specs/agent-rename.md +13 -13
- package/docs/team-memory.md +283 -14
- package/docs/tutorials/01-install-and-connect.md +23 -0
- package/docs/tutorials/03-plan-driven-feature.md +3 -3
- package/docs/tutorials/04-runtimes-and-harness.md +18 -21
- package/docs/tutorials/08-operations-and-recovery.md +3 -2
- package/docs/tutorials/README.md +4 -3
- package/docs/visual-evidence-ci.md +103 -0
- package/docs/watches.md +35 -30
- package/docs/worktree-lifecycle.md +151 -13
- package/engine/acp-transport.js +1 -0
- package/engine/ado.js +80 -33
- package/engine/agent-api-validation.js +571 -0
- package/engine/agent-worker-pool.js +7 -6
- package/engine/api-contracts/agent-content.js +761 -0
- package/engine/api-contracts/cc-ops.js +219 -0
- package/engine/api-contracts/config-runtime.js +419 -0
- package/engine/api-contracts/core.js +138 -0
- package/engine/api-contracts/index.js +506 -0
- package/engine/api-contracts/orchestration.js +602 -0
- package/engine/api-contracts/pull-requests.js +323 -0
- package/engine/api-contracts/qa-process.js +764 -0
- package/engine/api-contracts/work-plan-prd.js +494 -0
- package/engine/api-validation.js +543 -0
- package/engine/cc-api-validation.js +586 -0
- package/engine/cc-worker-pool.js +9 -1
- package/engine/cleanup.js +393 -271
- package/engine/cli.js +65 -7
- package/engine/comment-classifier.js +70 -5
- package/engine/consolidation.js +123 -5
- package/engine/content-api-validation.js +606 -0
- package/engine/create-pr-worktree.js +22 -1
- package/engine/db/migrations/027-review-learning-lifecycle.js +162 -0
- package/engine/dispatch.js +33 -0
- package/engine/distribution.js +189 -0
- package/engine/execution-model.js +59 -0
- package/engine/features.js +12 -0
- package/engine/github.js +67 -25
- package/engine/harness-context.js +313 -0
- package/engine/inbox-store.js +1 -1
- package/engine/kb-sweep.js +161 -38
- package/engine/keep-process-sweep.js +43 -3
- package/engine/lifecycle.js +1066 -165
- package/engine/llm.js +4 -0
- package/engine/managed-spawn.js +463 -48
- package/engine/meeting.js +197 -34
- package/engine/memory-retrieval.js +280 -0
- package/engine/memory-store.js +821 -38
- package/engine/model-discovery.js +17 -3
- package/engine/pipeline.js +802 -14
- package/engine/plan-prd-validation.js +745 -0
- package/engine/playbook.js +206 -17
- package/engine/pooled-agent-process.js +6 -1
- package/engine/pr-action.js +1 -1
- package/engine/pr-issue-validation.js +653 -0
- package/engine/pr-resolve.js +75 -13
- package/engine/prd-store.js +39 -6
- package/engine/preflight.js +7 -2
- package/engine/process-utils.js +79 -31
- package/engine/projects.js +198 -12
- package/engine/promotion.js +371 -0
- package/engine/qa-from-prd.js +3 -0
- package/engine/qa-process-validation.js +572 -0
- package/engine/qa-runbooks.js +41 -37
- package/engine/qa-runners/maestro.js +11 -7
- package/engine/qa-runners/playwright.js +37 -14
- package/engine/qa-runners.js +9 -10
- package/engine/qa-runs.js +124 -11
- package/engine/qa-sessions.js +139 -40
- package/engine/quarantine-refs.js +81 -11
- package/engine/queries.js +163 -9
- package/engine/review-learning-backfill.js +482 -0
- package/engine/review-learning.js +1236 -0
- package/engine/runtimes/claude.js +60 -2
- package/engine/runtimes/codex.js +57 -16
- package/engine/runtimes/contract.js +21 -0
- package/engine/runtimes/copilot.js +51 -3
- package/engine/runtimes/index.js +1 -0
- package/engine/scheduler.js +201 -32
- package/engine/settings-validation.js +909 -0
- package/engine/shared.js +705 -208
- package/engine/spawn-agent.js +48 -21
- package/engine/steering-constraints.js +31 -0
- package/engine/steering-store.js +10 -2
- package/engine/steering.js +14 -2
- package/engine/timeout.js +404 -111
- package/engine/untrusted-fence.js +24 -10
- package/engine/watch-actions.js +7 -2
- package/engine/watches.js +353 -51
- package/engine/work-item-validation.js +561 -0
- package/engine/work-items-store.js +61 -1
- package/engine/worktree-gc.js +656 -37
- package/engine/worktree-preflight.js +154 -0
- package/engine.js +2068 -342
- package/package.json +1 -1
- package/playbooks/_pr-description-audit.md +88 -41
- package/playbooks/build-fix-complex.md +3 -3
- package/playbooks/docs.md +2 -2
- package/playbooks/fix.md +18 -18
- package/playbooks/implement.md +18 -18
- package/playbooks/qa-session-draft.md +10 -12
- package/playbooks/qa-session-execute.md +16 -12
- package/playbooks/qa-validate.md +8 -7
- package/playbooks/shared-rules.md +45 -0
- package/playbooks/test.md +4 -0
- package/prompts/cc-system.md +1 -1
- package/skills/check-self-authored-review-comment/SKILL.md +2 -2
package/engine/managed-spawn.js
CHANGED
|
@@ -24,6 +24,7 @@ const https = require('https');
|
|
|
24
24
|
const { spawn, exec } = require('child_process');
|
|
25
25
|
const shared = require('./shared');
|
|
26
26
|
const queries = require('./queries');
|
|
27
|
+
const processUtils = require('./process-utils');
|
|
27
28
|
|
|
28
29
|
const { log, ENGINE_DEFAULTS } = shared;
|
|
29
30
|
|
|
@@ -38,6 +39,24 @@ const HEALTHCHECK_TYPES = ['http', 'command'];
|
|
|
38
39
|
// no leading/trailing hyphen. Mirrors the spawn-name conventions used elsewhere
|
|
39
40
|
// in the codebase (worktree-pool, dispatch IDs).
|
|
40
41
|
const _KEBAB_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
42
|
+
const MANAGED_LOG_TAIL_DEFAULT_BYTES = 64 * 1024;
|
|
43
|
+
const MANAGED_LOG_TAIL_MAX_BYTES = 1024 * 1024;
|
|
44
|
+
const MANAGED_RESERVATION_TTL_MS = 60 * 1000;
|
|
45
|
+
|
|
46
|
+
function validateManagedSpecName(name) {
|
|
47
|
+
const limits = ENGINE_DEFAULTS.managedSpawn || {};
|
|
48
|
+
const maxLength = Math.max(8, Number(limits.maxNameLength) || 64);
|
|
49
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
50
|
+
return { ok: false, reason: 'name-missing', maxLength };
|
|
51
|
+
}
|
|
52
|
+
if (name.length > maxLength) {
|
|
53
|
+
return { ok: false, reason: 'name-too-long (>' + maxLength + ')', maxLength };
|
|
54
|
+
}
|
|
55
|
+
if (!_KEBAB_RE.test(name)) {
|
|
56
|
+
return { ok: false, reason: 'name-not-kebab-case (' + name + ')', maxLength };
|
|
57
|
+
}
|
|
58
|
+
return { ok: true, value: name, maxLength };
|
|
59
|
+
}
|
|
41
60
|
|
|
42
61
|
function _agentsDir() {
|
|
43
62
|
return queries.AGENTS_DIR || path.join(shared.MINIONS_DIR, 'agents');
|
|
@@ -260,16 +279,8 @@ function _validateSpec(spec, index, limits, opts) {
|
|
|
260
279
|
}
|
|
261
280
|
|
|
262
281
|
// name
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
const maxName = Math.max(8, Number(limits.maxNameLength) || 64);
|
|
267
|
-
if (spec.name.length > maxName) {
|
|
268
|
-
return { ok: false, reason: 'name-too-long (>' + maxName + ')' };
|
|
269
|
-
}
|
|
270
|
-
if (!_KEBAB_RE.test(spec.name)) {
|
|
271
|
-
return { ok: false, reason: 'name-not-kebab-case (' + spec.name + ')' };
|
|
272
|
-
}
|
|
282
|
+
const nameValidation = validateManagedSpecName(spec.name);
|
|
283
|
+
if (!nameValidation.ok) return nameValidation;
|
|
273
284
|
|
|
274
285
|
// cmd
|
|
275
286
|
if (typeof spec.cmd !== 'string' || spec.cmd.length === 0) {
|
|
@@ -531,22 +542,59 @@ function readManagedSpawnFile(agentId, opts) {
|
|
|
531
542
|
|
|
532
543
|
// Pure engine-side acceptance helper. Mirrors evaluateKeepPidsAcceptance:
|
|
533
544
|
// returns a structured summary the engine close handler uses to decide
|
|
534
|
-
// whether to (a)
|
|
535
|
-
// workdir-invalid
|
|
536
|
-
// (c) reject as schema-invalid → fail dispatch + inbox alert + unlink.
|
|
545
|
+
// whether to (a) accept a valid record, (b) reject a required missing file,
|
|
546
|
+
// (c) reject as workdir-invalid, or (d) reject as schema/identity-invalid.
|
|
537
547
|
// No side effects; caller owns kill + unlink + WI mutation.
|
|
538
548
|
function evaluateManagedSpawnAcceptance(agentId, opts) {
|
|
539
549
|
opts = opts || {};
|
|
540
550
|
const filePath = path.join(_agentsDir(), String(agentId || ''), MANAGED_SPAWN_FILENAME);
|
|
541
551
|
if (!fs.existsSync(filePath)) {
|
|
542
|
-
return {
|
|
552
|
+
return {
|
|
553
|
+
exists: false,
|
|
554
|
+
accepted: false,
|
|
555
|
+
isWorkdirRejection: false,
|
|
556
|
+
reason: opts.required ? 'sidecar-missing' : null,
|
|
557
|
+
record: null,
|
|
558
|
+
filePath: filePath,
|
|
559
|
+
};
|
|
543
560
|
}
|
|
544
561
|
const rec = readManagedSpawnFile(agentId, opts);
|
|
545
562
|
if (!rec) {
|
|
546
563
|
// Race: file existed in existsSync above but was unlinked before read.
|
|
547
|
-
return {
|
|
564
|
+
return {
|
|
565
|
+
exists: false,
|
|
566
|
+
accepted: false,
|
|
567
|
+
isWorkdirRejection: false,
|
|
568
|
+
reason: opts.required ? 'sidecar-missing' : null,
|
|
569
|
+
record: null,
|
|
570
|
+
filePath: filePath,
|
|
571
|
+
};
|
|
548
572
|
}
|
|
549
573
|
if (rec.valid) {
|
|
574
|
+
const expectedAgentId = typeof opts.expectedAgentId === 'string' ? opts.expectedAgentId : '';
|
|
575
|
+
if (expectedAgentId && rec.value.written_by !== expectedAgentId) {
|
|
576
|
+
return {
|
|
577
|
+
exists: true,
|
|
578
|
+
accepted: false,
|
|
579
|
+
isWorkdirRejection: false,
|
|
580
|
+
reason: `written-by-mismatch (expected ${expectedAgentId}, got ${rec.value.written_by || 'missing'})`,
|
|
581
|
+
record: null,
|
|
582
|
+
filePath: rec.filePath,
|
|
583
|
+
parsedRaw: rec.parsed || null,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
const expectedWorkItemId = typeof opts.expectedWorkItemId === 'string' ? opts.expectedWorkItemId : '';
|
|
587
|
+
if (expectedWorkItemId && rec.value.wi_id !== expectedWorkItemId) {
|
|
588
|
+
return {
|
|
589
|
+
exists: true,
|
|
590
|
+
accepted: false,
|
|
591
|
+
isWorkdirRejection: false,
|
|
592
|
+
reason: `wi-id-mismatch (expected ${expectedWorkItemId}, got ${rec.value.wi_id || 'missing'})`,
|
|
593
|
+
record: null,
|
|
594
|
+
filePath: rec.filePath,
|
|
595
|
+
parsedRaw: rec.parsed || null,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
550
598
|
return {
|
|
551
599
|
exists: true,
|
|
552
600
|
accepted: true,
|
|
@@ -805,7 +853,7 @@ function _getLogsDir() {
|
|
|
805
853
|
}
|
|
806
854
|
|
|
807
855
|
function openManagedLog(name) {
|
|
808
|
-
if (
|
|
856
|
+
if (!validateManagedSpecName(name).ok) {
|
|
809
857
|
throw new Error('openManagedLog: name must be a non-empty kebab-case string');
|
|
810
858
|
}
|
|
811
859
|
// P-8a4d6f29 — rotate-on-open is centralised in shared.openAppendLogFd so
|
|
@@ -983,43 +1031,234 @@ function recordManagedSpec(spec, runtime, ctx) {
|
|
|
983
1031
|
}
|
|
984
1032
|
const idx = data.specs.findIndex(s => s && s.name === spec.name);
|
|
985
1033
|
const rec = _toStateRecord(spec, runtime, ctx);
|
|
1034
|
+
if (idx >= 0 && (data.specs[idx]._restart_token || data.specs[idx]._spawn_token)) {
|
|
1035
|
+
throw new Error(`recordManagedSpec: reservation active for ${spec.name}`);
|
|
1036
|
+
}
|
|
986
1037
|
if (idx >= 0) data.specs[idx] = rec;
|
|
987
1038
|
else data.specs.push(rec);
|
|
988
1039
|
return data;
|
|
989
1040
|
}, { defaultValue: _initialStateShape() });
|
|
990
1041
|
}
|
|
991
1042
|
|
|
1043
|
+
function reserveManagedBatch(specs, ctx) {
|
|
1044
|
+
if (!Array.isArray(specs) || specs.length === 0) return [];
|
|
1045
|
+
const names = specs.map(spec => spec && spec.name);
|
|
1046
|
+
if (names.some(name => !validateManagedSpecName(name).ok)
|
|
1047
|
+
|| new Set(names).size !== names.length) {
|
|
1048
|
+
throw new Error('reserveManagedBatch: specs require unique valid names');
|
|
1049
|
+
}
|
|
1050
|
+
const reservations = specs.map((spec) => {
|
|
1051
|
+
const token = 'spawn-' + shared.uid();
|
|
1052
|
+
const record = _toStateRecord(spec, {
|
|
1053
|
+
pid: null,
|
|
1054
|
+
started_at: Date.now(),
|
|
1055
|
+
log_path: '',
|
|
1056
|
+
}, ctx);
|
|
1057
|
+
record.alive = false;
|
|
1058
|
+
record._spawn_token = token;
|
|
1059
|
+
record._reservation_expires_at = Date.now() + MANAGED_RESERVATION_TTL_MS;
|
|
1060
|
+
return { name: spec.name, token, record };
|
|
1061
|
+
});
|
|
1062
|
+
shared.mutateManagedProcesses((data) => {
|
|
1063
|
+
if (!data || !Array.isArray(data.specs)) data = _initialStateShape();
|
|
1064
|
+
const existingNames = new Set(data.specs.map(item => item && item.name).filter(Boolean));
|
|
1065
|
+
const conflict = reservations.find(reservation => existingNames.has(reservation.name));
|
|
1066
|
+
if (conflict) {
|
|
1067
|
+
const error = new Error(`reserveManagedBatch: name already active: ${conflict.name}`);
|
|
1068
|
+
error.statusCode = 409;
|
|
1069
|
+
error.code = 'managed-process-generation-changed';
|
|
1070
|
+
throw error;
|
|
1071
|
+
}
|
|
1072
|
+
data.specs.push(...reservations.map(reservation => reservation.record));
|
|
1073
|
+
return data;
|
|
1074
|
+
}, { defaultValue: _initialStateShape() });
|
|
1075
|
+
return reservations.map(({ name, token }) => ({ name, token }));
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function releaseManagedReservations(reservations) {
|
|
1079
|
+
if (!Array.isArray(reservations) || reservations.length === 0) return 0;
|
|
1080
|
+
const tokens = new Map(reservations.map(item => [item && item.name, item && item.token]));
|
|
1081
|
+
let released = 0;
|
|
1082
|
+
shared.mutateManagedProcesses((data) => {
|
|
1083
|
+
if (!data || !Array.isArray(data.specs)) return data;
|
|
1084
|
+
data.specs = data.specs.filter((record) => {
|
|
1085
|
+
const token = tokens.get(record && record.name);
|
|
1086
|
+
const remove = !!token && record._spawn_token === token;
|
|
1087
|
+
if (remove) released += 1;
|
|
1088
|
+
return !remove;
|
|
1089
|
+
});
|
|
1090
|
+
return data;
|
|
1091
|
+
}, { defaultValue: _initialStateShape() });
|
|
1092
|
+
return released;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
992
1095
|
function recordManagedBatch(items, ctx) {
|
|
993
1096
|
if (!Array.isArray(items) || items.length === 0) return;
|
|
994
1097
|
shared.mutateManagedProcesses((data) => {
|
|
995
1098
|
if (!data || typeof data !== 'object' || Array.isArray(data) || !Array.isArray(data.specs)) {
|
|
996
1099
|
data = _initialStateShape();
|
|
997
1100
|
}
|
|
1101
|
+
const writes = [];
|
|
998
1102
|
for (const entry of items) {
|
|
999
1103
|
const spec = entry && entry.spec;
|
|
1000
1104
|
const runtime = entry && entry.runtime;
|
|
1001
1105
|
if (!spec || !spec.name) continue;
|
|
1002
1106
|
const idx = data.specs.findIndex(s => s && s.name === spec.name);
|
|
1003
1107
|
const rec = _toStateRecord(spec, runtime, ctx);
|
|
1004
|
-
if (
|
|
1005
|
-
|
|
1108
|
+
if (entry.reservationToken) {
|
|
1109
|
+
if (idx < 0 || data.specs[idx]._spawn_token !== entry.reservationToken) {
|
|
1110
|
+
throw new Error(`recordManagedBatch: reservation changed for ${spec.name}`);
|
|
1111
|
+
}
|
|
1112
|
+
} else if (idx >= 0 && (data.specs[idx]._restart_token || data.specs[idx]._spawn_token)) {
|
|
1113
|
+
throw new Error(`recordManagedBatch: reservation active for ${spec.name}`);
|
|
1114
|
+
}
|
|
1115
|
+
writes.push({ idx, rec });
|
|
1116
|
+
}
|
|
1117
|
+
for (const write of writes) {
|
|
1118
|
+
if (write.idx >= 0) data.specs[write.idx] = write.rec;
|
|
1119
|
+
else data.specs.push(write.rec);
|
|
1006
1120
|
}
|
|
1007
1121
|
return data;
|
|
1008
1122
|
}, { defaultValue: _initialStateShape() });
|
|
1009
1123
|
}
|
|
1010
1124
|
|
|
1011
|
-
function
|
|
1012
|
-
|
|
1013
|
-
|
|
1125
|
+
function _managedStartedAtMs(record) {
|
|
1126
|
+
const numeric = Number(record && record.started_at);
|
|
1127
|
+
return Number.isFinite(numeric) ? numeric : Date.parse(record && record.started_at);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function managedGenerationMatches(record, expected) {
|
|
1131
|
+
if (!record || !expected) return false;
|
|
1132
|
+
const recordStartedAt = _managedStartedAtMs(record);
|
|
1133
|
+
const expectedStartedAt = _managedStartedAtMs(expected);
|
|
1134
|
+
return record.pid === expected.pid
|
|
1135
|
+
&& Number.isFinite(recordStartedAt)
|
|
1136
|
+
&& recordStartedAt === expectedStartedAt;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function _restoreManagedRecord(record, options = {}) {
|
|
1140
|
+
let restored = false;
|
|
1141
|
+
shared.mutateManagedProcesses((data) => {
|
|
1142
|
+
if (!data || !Array.isArray(data.specs)) data = _initialStateShape();
|
|
1143
|
+
if (!data.specs.some(item => item && item.name === record.name)) {
|
|
1144
|
+
data.specs.push(JSON.parse(JSON.stringify(record)));
|
|
1145
|
+
restored = true;
|
|
1146
|
+
} else if (options.trackCollision === true) {
|
|
1147
|
+
const cleanup = JSON.parse(JSON.stringify(record));
|
|
1148
|
+
cleanup.attrs = {
|
|
1149
|
+
...cleanup.attrs,
|
|
1150
|
+
cleanup_original_name: cleanup.name,
|
|
1151
|
+
};
|
|
1152
|
+
cleanup.name = 'cleanup-' + shared.uid();
|
|
1153
|
+
cleanup.healthy = false;
|
|
1154
|
+
cleanup.ttl_expires_at = Date.now();
|
|
1155
|
+
cleanup._cleanup_reason = options.reason || 'managed kill retry';
|
|
1156
|
+
data.specs.push(cleanup);
|
|
1157
|
+
restored = true;
|
|
1158
|
+
}
|
|
1159
|
+
return data;
|
|
1160
|
+
}, { defaultValue: _initialStateShape() });
|
|
1161
|
+
return restored;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function reapOrTrackManagedRuntime(spec, runtime, ctx, options = {}) {
|
|
1165
|
+
const pid = runtime && runtime.pid;
|
|
1166
|
+
// Base the reaped/tracked decision on killImmediate's authoritative result,
|
|
1167
|
+
// not a racy synchronous liveness probe. killImmediate returns true once the
|
|
1168
|
+
// SIGKILL / taskkill was delivered to the whole process tree; on Unix the
|
|
1169
|
+
// just-killed process can linger for a few microseconds in a dying ('R') or
|
|
1170
|
+
// unreaped zombie ('Z') state before the kernel tears it down, so a single
|
|
1171
|
+
// isPidAlive() immediately after the kill can still read it as alive (Windows
|
|
1172
|
+
// taskkill /F /T blocks until termination, which is why this only bit the
|
|
1173
|
+
// Linux CI). Mirror removeManagedSpec: trust a confirmed kill and only fall
|
|
1174
|
+
// back to a liveness re-check + sweep-retry tracking when the kill itself
|
|
1175
|
+
// FAILED (or there was no pid to kill).
|
|
1176
|
+
let killed = false;
|
|
1177
|
+
if (Number.isInteger(pid) && pid > 0) {
|
|
1178
|
+
try { killed = shared.killImmediate({ pid }) !== false; }
|
|
1179
|
+
catch (_e) { killed = false; }
|
|
1180
|
+
}
|
|
1181
|
+
if (killed || !shared.isPidAlive(pid)) return { reaped: true, tracked: false };
|
|
1182
|
+
|
|
1183
|
+
const record = _toStateRecord(spec, runtime, ctx);
|
|
1184
|
+
record.healthy = false;
|
|
1185
|
+
record.ttl_expires_at = Date.now();
|
|
1186
|
+
record._cleanup_reason = options.reason || 'managed runtime cleanup retry';
|
|
1187
|
+
let tracked = false;
|
|
1188
|
+
shared.mutateManagedProcesses((data) => {
|
|
1189
|
+
if (!data || !Array.isArray(data.specs)) data = _initialStateShape();
|
|
1190
|
+
const reservationIndex = options.reservationField && options.reservationToken
|
|
1191
|
+
? data.specs.findIndex(item => (
|
|
1192
|
+
item
|
|
1193
|
+
&& item.name === spec.name
|
|
1194
|
+
&& item[options.reservationField] === options.reservationToken
|
|
1195
|
+
))
|
|
1196
|
+
: -1;
|
|
1197
|
+
if (reservationIndex >= 0) {
|
|
1198
|
+
data.specs[reservationIndex] = record;
|
|
1199
|
+
tracked = true;
|
|
1200
|
+
return data;
|
|
1201
|
+
}
|
|
1202
|
+
if (!data.specs.some(item => item && item.name === record.name)) {
|
|
1203
|
+
data.specs.push(record);
|
|
1204
|
+
tracked = true;
|
|
1205
|
+
return data;
|
|
1206
|
+
}
|
|
1207
|
+
record.attrs = {
|
|
1208
|
+
...record.attrs,
|
|
1209
|
+
cleanup_original_name: record.name,
|
|
1210
|
+
};
|
|
1211
|
+
record.name = 'cleanup-' + shared.uid();
|
|
1212
|
+
data.specs.push(record);
|
|
1213
|
+
tracked = true;
|
|
1214
|
+
return data;
|
|
1215
|
+
}, { defaultValue: _initialStateShape() });
|
|
1216
|
+
return { reaped: false, tracked };
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
function removeManagedSpec(name, opts = {}) {
|
|
1220
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
1221
|
+
return { removed: false, reason: 'invalid-name' };
|
|
1222
|
+
}
|
|
1223
|
+
const candidate = getManagedSpecByName(name);
|
|
1224
|
+
if (!candidate) return { removed: false, reason: 'not-found' };
|
|
1225
|
+
const expected = opts.expected || candidate;
|
|
1226
|
+
if (!managedGenerationMatches(candidate, expected)) {
|
|
1227
|
+
return { removed: false, reason: 'generation-changed' };
|
|
1228
|
+
}
|
|
1229
|
+
const killPid = Number.isInteger(candidate.pid) && candidate.pid > 0 ? candidate.pid : null;
|
|
1230
|
+
const verifyIdentity = opts.verifyIdentity === true;
|
|
1231
|
+
const getIdentity = typeof opts.getProcessIdentity === 'function'
|
|
1232
|
+
? opts.getProcessIdentity
|
|
1233
|
+
: processUtils.getProcessIdentity;
|
|
1234
|
+
const wasAlive = killPid !== null && shared.isPidAlive(killPid);
|
|
1235
|
+
let identityBefore = null;
|
|
1236
|
+
if (verifyIdentity && wasAlive) {
|
|
1237
|
+
identityBefore = getIdentity(killPid);
|
|
1238
|
+
if (!identityBefore) return { removed: false, reason: 'identity-unavailable' };
|
|
1239
|
+
if (!processUtils.isProcessIdentityCompatible(identityBefore, _managedStartedAtMs(candidate))) {
|
|
1240
|
+
return { removed: false, reason: 'identity-mismatch' };
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
let removedRecord = null;
|
|
1245
|
+
let conflict = false;
|
|
1014
1246
|
shared.mutateManagedProcesses((data) => {
|
|
1015
1247
|
if (!data || !Array.isArray(data.specs)) return data;
|
|
1016
1248
|
const idx = data.specs.findIndex(s => s && s.name === name);
|
|
1017
1249
|
if (idx < 0) return data;
|
|
1018
1250
|
const rec = data.specs[idx];
|
|
1019
|
-
if (
|
|
1251
|
+
if (!managedGenerationMatches(rec, expected)) {
|
|
1252
|
+
conflict = true;
|
|
1253
|
+
return data;
|
|
1254
|
+
}
|
|
1255
|
+
removedRecord = JSON.parse(JSON.stringify(rec));
|
|
1020
1256
|
data.specs.splice(idx, 1);
|
|
1021
1257
|
return data;
|
|
1022
1258
|
}, { defaultValue: _initialStateShape() });
|
|
1259
|
+
if (conflict) return { removed: false, reason: 'generation-changed' };
|
|
1260
|
+
if (!removedRecord) return { removed: false, reason: 'not-found' };
|
|
1261
|
+
|
|
1023
1262
|
// Kill OUTSIDE the lock — never run process ops inside a lock callback
|
|
1024
1263
|
// (copilot-instructions.md "Keep lock callbacks synchronous and fast").
|
|
1025
1264
|
// Use killImmediate (full process-tree kill: `taskkill /PID <pid> /F /T` on
|
|
@@ -1028,10 +1267,28 @@ function removeManagedSpec(name) {
|
|
|
1028
1267
|
// launcher-style processes (e.g. Android emulator.exe spawning a detached
|
|
1029
1268
|
// qemu-system-x86_64 child) whose descendants survive a single-PID kill and
|
|
1030
1269
|
// leak indefinitely (opg-microsoft/minions#710).
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1270
|
+
let killed = false;
|
|
1271
|
+
if (killPid !== null && (!verifyIdentity || wasAlive)) {
|
|
1272
|
+
if (verifyIdentity) {
|
|
1273
|
+
const identityAfter = getIdentity(killPid);
|
|
1274
|
+
if (!processUtils.isSameProcessIdentity(identityBefore, identityAfter)) {
|
|
1275
|
+
_restoreManagedRecord(removedRecord);
|
|
1276
|
+
return { removed: false, reason: 'identity-changed' };
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
try { killed = shared.killImmediate({ pid: killPid }) !== false; }
|
|
1280
|
+
catch (e) {
|
|
1281
|
+
log('warn', 'managed-spawn removeManagedSpec: kill ' + killPid + ' failed: ' + e.message);
|
|
1282
|
+
}
|
|
1283
|
+
if (!killed && shared.isPidAlive(killPid)) {
|
|
1284
|
+
_restoreManagedRecord(removedRecord, {
|
|
1285
|
+
trackCollision: true,
|
|
1286
|
+
reason: 'managed kill failed',
|
|
1287
|
+
});
|
|
1288
|
+
return { removed: false, reason: 'kill-failed' };
|
|
1289
|
+
}
|
|
1034
1290
|
}
|
|
1291
|
+
return { removed: true, record: removedRecord, killed };
|
|
1035
1292
|
}
|
|
1036
1293
|
|
|
1037
1294
|
function listManagedSpecs(opts) {
|
|
@@ -1042,6 +1299,11 @@ function listManagedSpecs(opts) {
|
|
|
1042
1299
|
return specs.slice();
|
|
1043
1300
|
}
|
|
1044
1301
|
|
|
1302
|
+
function getManagedSpecByName(name) {
|
|
1303
|
+
if (typeof name !== 'string' || !name) return null;
|
|
1304
|
+
return listManagedSpecs().find(spec => spec && spec.name === name) || null;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1045
1307
|
// ─── Item 3 (P-9c1f47a6) ─────────────────────────────────────────────────────
|
|
1046
1308
|
//
|
|
1047
1309
|
// Healthcheck implementations + dispatch SUCCESS/ERROR gate.
|
|
@@ -1156,19 +1418,24 @@ async function runHealthcheck(spec) {
|
|
|
1156
1418
|
// Flip state.healthy=true + last_health_at on a single locked write. Used by
|
|
1157
1419
|
// waitForFirstHealth on the first pass. Callers MUST be outside any other
|
|
1158
1420
|
// lock — this acquires its own.
|
|
1159
|
-
function _markHealthy(name, now) {
|
|
1421
|
+
function _markHealthy(name, now, expectedGeneration) {
|
|
1422
|
+
let marked = false;
|
|
1160
1423
|
shared.mutateManagedProcesses((data) => {
|
|
1161
1424
|
if (!data || !Array.isArray(data.specs)) return data;
|
|
1162
1425
|
const rec = data.specs.find(s => s && s.name === name);
|
|
1163
|
-
if (!rec) return data;
|
|
1426
|
+
if (!rec || !managedGenerationMatches(rec, expectedGeneration)) return data;
|
|
1164
1427
|
rec.healthy = true;
|
|
1165
1428
|
rec.last_health_at = now;
|
|
1429
|
+
marked = true;
|
|
1166
1430
|
return data;
|
|
1167
1431
|
}, { defaultValue: _initialStateShape() });
|
|
1432
|
+
return marked;
|
|
1168
1433
|
}
|
|
1169
1434
|
|
|
1170
1435
|
function waitForFirstHealth(spec, opts) {
|
|
1171
1436
|
opts = opts || {};
|
|
1437
|
+
const expectedGeneration = opts.expectedGeneration
|
|
1438
|
+
|| (spec && spec.pid && spec.started_at ? spec : getManagedSpecByName(spec && spec.name));
|
|
1172
1439
|
const intervalMs = Math.max(100, (Number(spec.healthcheck && spec.healthcheck.interval_s) || 1) * 1000);
|
|
1173
1440
|
const timeoutMs = Math.max(intervalMs, (Number(spec.healthcheck && spec.healthcheck.timeout_s) || 30) * 1000);
|
|
1174
1441
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -1230,8 +1497,25 @@ function waitForFirstHealth(spec, opts) {
|
|
|
1230
1497
|
}
|
|
1231
1498
|
if (stopped) return;
|
|
1232
1499
|
if (result.healthy) {
|
|
1233
|
-
|
|
1234
|
-
|
|
1500
|
+
let marked = false;
|
|
1501
|
+
try { marked = _markHealthy(spec.name, result.lastCheckAt, expectedGeneration); }
|
|
1502
|
+
catch (e) {
|
|
1503
|
+
log('warn', 'managed-spawn waitForFirstHealth: state write failed for ' + spec.name + ': ' + e.message);
|
|
1504
|
+
if (expectedGeneration) {
|
|
1505
|
+
return finish({
|
|
1506
|
+
healthy: false,
|
|
1507
|
+
error: 'generation state write failed: ' + e.message,
|
|
1508
|
+
lastCheckAt: result.lastCheckAt,
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
if (expectedGeneration && !marked) {
|
|
1513
|
+
return finish({
|
|
1514
|
+
healthy: false,
|
|
1515
|
+
error: 'generation changed before first-health state update',
|
|
1516
|
+
lastCheckAt: result.lastCheckAt,
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1235
1519
|
return finish({ healthy: true, error: null, lastCheckAt: result.lastCheckAt });
|
|
1236
1520
|
}
|
|
1237
1521
|
lastError = result.error;
|
|
@@ -1445,8 +1729,8 @@ function validateManagedSpecExecutionContext(record, opts) {
|
|
|
1445
1729
|
return { ok: true, project, mode };
|
|
1446
1730
|
}
|
|
1447
1731
|
|
|
1448
|
-
function restartManagedSpec(name, opts) {
|
|
1449
|
-
if (
|
|
1732
|
+
function restartManagedSpec(name, opts = {}) {
|
|
1733
|
+
if (!validateManagedSpecName(name).ok) {
|
|
1450
1734
|
throw new Error('restartManagedSpec: name required');
|
|
1451
1735
|
}
|
|
1452
1736
|
const existing = listManagedSpecs().find(s => s && s.name === name);
|
|
@@ -1455,8 +1739,17 @@ function restartManagedSpec(name, opts) {
|
|
|
1455
1739
|
}
|
|
1456
1740
|
const executionContext = validateManagedSpecExecutionContext(existing, opts);
|
|
1457
1741
|
if (!executionContext.ok) {
|
|
1458
|
-
|
|
1459
|
-
|
|
1742
|
+
removeManagedSpec(name, {
|
|
1743
|
+
expected: opts.expected || existing,
|
|
1744
|
+
verifyIdentity: opts.verifyIdentity !== false,
|
|
1745
|
+
getProcessIdentity: opts.getProcessIdentity,
|
|
1746
|
+
});
|
|
1747
|
+
const error = new Error(
|
|
1748
|
+
'restartManagedSpec: unsafe persisted execution context (' + executionContext.reason + ')',
|
|
1749
|
+
);
|
|
1750
|
+
error.statusCode = 409;
|
|
1751
|
+
error.code = 'managed-process-unsafe-execution-context';
|
|
1752
|
+
throw error;
|
|
1460
1753
|
}
|
|
1461
1754
|
// Reconstruct a sidecar-shaped spec from the persisted state row. The
|
|
1462
1755
|
// state shape keeps cmd/args/cwd/env/ports/attrs/healthcheck verbatim
|
|
@@ -1472,13 +1765,16 @@ function restartManagedSpec(name, opts) {
|
|
|
1472
1765
|
attrs: existing.attrs && typeof existing.attrs === 'object' ? existing.attrs : {},
|
|
1473
1766
|
healthcheck: existing.healthcheck || null,
|
|
1474
1767
|
};
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1768
|
+
const removal = removeManagedSpec(name, {
|
|
1769
|
+
expected: opts.expected || existing,
|
|
1770
|
+
verifyIdentity: opts.verifyIdentity !== false,
|
|
1771
|
+
getProcessIdentity: opts.getProcessIdentity,
|
|
1772
|
+
});
|
|
1773
|
+
if (!removal.removed) {
|
|
1774
|
+
const error = new Error(`restartManagedSpec: ${removal.reason}: ${name}`);
|
|
1775
|
+
error.statusCode = removal.reason === 'not-found' ? 404 : 409;
|
|
1776
|
+
error.code = `managed-process-${removal.reason}`;
|
|
1777
|
+
throw error;
|
|
1482
1778
|
}
|
|
1483
1779
|
const ctx = {
|
|
1484
1780
|
owner_agent: existing.owner_agent || '',
|
|
@@ -1488,10 +1784,86 @@ function restartManagedSpec(name, opts) {
|
|
|
1488
1784
|
dispatch_root: existing.dispatch_root || '',
|
|
1489
1785
|
checkout_mode: existing.checkout_mode || '',
|
|
1490
1786
|
};
|
|
1491
|
-
const
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1787
|
+
const restartToken = 'restart-' + shared.uid();
|
|
1788
|
+
const reservation = {
|
|
1789
|
+
...existing,
|
|
1790
|
+
pid: null,
|
|
1791
|
+
alive: false,
|
|
1792
|
+
healthy: false,
|
|
1793
|
+
last_health_at: null,
|
|
1794
|
+
_restart_token: restartToken,
|
|
1795
|
+
_reservation_expires_at: Date.now() + MANAGED_RESERVATION_TTL_MS,
|
|
1796
|
+
};
|
|
1797
|
+
let reserved = false;
|
|
1798
|
+
shared.mutateManagedProcesses((data) => {
|
|
1799
|
+
if (!data || !Array.isArray(data.specs)) data = _initialStateShape();
|
|
1800
|
+
if (!data.specs.some(item => item && item.name === name)) {
|
|
1801
|
+
data.specs.push(reservation);
|
|
1802
|
+
reserved = true;
|
|
1803
|
+
}
|
|
1804
|
+
return data;
|
|
1805
|
+
}, { defaultValue: _initialStateShape() });
|
|
1806
|
+
if (!reserved) {
|
|
1807
|
+
const error = new Error(`restartManagedSpec: generation-changed: ${name}`);
|
|
1808
|
+
error.statusCode = 409;
|
|
1809
|
+
error.code = 'managed-process-generation-changed';
|
|
1810
|
+
throw error;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
const clearReservation = () => {
|
|
1814
|
+
shared.mutateManagedProcesses((data) => {
|
|
1815
|
+
if (!data || !Array.isArray(data.specs)) return data;
|
|
1816
|
+
data.specs = data.specs.filter(
|
|
1817
|
+
item => !(item && item.name === name && item._restart_token === restartToken),
|
|
1818
|
+
);
|
|
1819
|
+
return data;
|
|
1820
|
+
}, { defaultValue: _initialStateShape() });
|
|
1821
|
+
};
|
|
1822
|
+
|
|
1823
|
+
let runtime;
|
|
1824
|
+
try {
|
|
1825
|
+
runtime = spawnManagedSpec(spec, ctx);
|
|
1826
|
+
} catch (error) {
|
|
1827
|
+
clearReservation();
|
|
1828
|
+
throw error;
|
|
1829
|
+
}
|
|
1830
|
+
let recorded = false;
|
|
1831
|
+
try {
|
|
1832
|
+
shared.mutateManagedProcesses((data) => {
|
|
1833
|
+
if (!data || !Array.isArray(data.specs)) data = _initialStateShape();
|
|
1834
|
+
const index = data.specs.findIndex(
|
|
1835
|
+
item => item && item.name === name && item._restart_token === restartToken,
|
|
1836
|
+
);
|
|
1837
|
+
if (index >= 0) {
|
|
1838
|
+
data.specs[index] = _toStateRecord(spec, runtime, ctx);
|
|
1839
|
+
recorded = true;
|
|
1840
|
+
}
|
|
1841
|
+
return data;
|
|
1842
|
+
}, { defaultValue: _initialStateShape() });
|
|
1843
|
+
} catch (error) {
|
|
1844
|
+
try {
|
|
1845
|
+
const cleanup = reapOrTrackManagedRuntime(spec, runtime, ctx, {
|
|
1846
|
+
reservationField: '_restart_token',
|
|
1847
|
+
reservationToken: restartToken,
|
|
1848
|
+
reason: 'restart persistence failure',
|
|
1849
|
+
});
|
|
1850
|
+
if (cleanup.reaped) clearReservation();
|
|
1851
|
+
} catch (cleanupError) {
|
|
1852
|
+
error.cleanupError = cleanupError;
|
|
1853
|
+
}
|
|
1854
|
+
throw error;
|
|
1855
|
+
}
|
|
1856
|
+
if (!recorded) {
|
|
1857
|
+
reapOrTrackManagedRuntime(spec, runtime, ctx, {
|
|
1858
|
+
reservationField: '_restart_token',
|
|
1859
|
+
reservationToken: restartToken,
|
|
1860
|
+
reason: 'restart reservation replaced',
|
|
1861
|
+
});
|
|
1862
|
+
const error = new Error(`restartManagedSpec: generation-changed: ${name}`);
|
|
1863
|
+
error.statusCode = 409;
|
|
1864
|
+
error.code = 'managed-process-generation-changed';
|
|
1865
|
+
throw error;
|
|
1866
|
+
}
|
|
1495
1867
|
log('info', 'managed-spawn restart: name=' + name + ' old_pid=' + existing.pid + ' new_pid=' + runtime.pid);
|
|
1496
1868
|
return runtime;
|
|
1497
1869
|
}
|
|
@@ -1583,6 +1955,7 @@ function _runManagedReconcile(opts) {
|
|
|
1583
1955
|
malformed: 0,
|
|
1584
1956
|
};
|
|
1585
1957
|
const ttlPidsToKill = [];
|
|
1958
|
+
const cleanupRetries = [];
|
|
1586
1959
|
const survivors = []; // [{name, log_path}] post-mutation, used for log rotation + bootReconcile probes
|
|
1587
1960
|
shared.mutateManagedProcesses((data) => {
|
|
1588
1961
|
if (!data || typeof data !== 'object' || Array.isArray(data) || !Array.isArray(data.specs)) {
|
|
@@ -1596,8 +1969,22 @@ function _runManagedReconcile(opts) {
|
|
|
1596
1969
|
continue;
|
|
1597
1970
|
}
|
|
1598
1971
|
stats.scanned++;
|
|
1972
|
+
if (rec._spawn_token || rec._restart_token) {
|
|
1973
|
+
if (Number.isFinite(rec._reservation_expires_at)
|
|
1974
|
+
&& rec._reservation_expires_at > now) {
|
|
1975
|
+
kept.push(rec);
|
|
1976
|
+
} else {
|
|
1977
|
+
stats.deadDropped++;
|
|
1978
|
+
}
|
|
1979
|
+
continue;
|
|
1980
|
+
}
|
|
1599
1981
|
const ttlExpired = Number.isFinite(rec.ttl_expires_at) && rec.ttl_expires_at <= now;
|
|
1600
1982
|
const alive = Number.isInteger(rec.pid) && rec.pid > 0 && isAlive(rec.pid);
|
|
1983
|
+
if (rec._cleanup_reason && alive) {
|
|
1984
|
+
kept.push(rec);
|
|
1985
|
+
cleanupRetries.push(rec);
|
|
1986
|
+
continue;
|
|
1987
|
+
}
|
|
1601
1988
|
if (ttlExpired) {
|
|
1602
1989
|
stats.ttlExpired++;
|
|
1603
1990
|
if (alive) ttlPidsToKill.push(rec.pid);
|
|
@@ -1613,7 +2000,14 @@ function _runManagedReconcile(opts) {
|
|
|
1613
2000
|
continue;
|
|
1614
2001
|
}
|
|
1615
2002
|
kept.push(rec);
|
|
1616
|
-
survivors.push({
|
|
2003
|
+
survivors.push({
|
|
2004
|
+
name: rec.name,
|
|
2005
|
+
pid: rec.pid,
|
|
2006
|
+
started_at: rec.started_at,
|
|
2007
|
+
log_path: rec.log_path || '',
|
|
2008
|
+
healthy: rec.healthy === true,
|
|
2009
|
+
last_health_at: rec.last_health_at || 0,
|
|
2010
|
+
});
|
|
1617
2011
|
}
|
|
1618
2012
|
data.specs = kept;
|
|
1619
2013
|
return data;
|
|
@@ -1625,6 +2019,17 @@ function _runManagedReconcile(opts) {
|
|
|
1625
2019
|
try { stats.killedPids = killBatch(ttlPidsToKill); }
|
|
1626
2020
|
catch (e) { log('warn', 'managed-spawn sweep: kill batch failed: ' + e.message); }
|
|
1627
2021
|
}
|
|
2022
|
+
for (const rec of cleanupRetries) {
|
|
2023
|
+
try {
|
|
2024
|
+
const result = removeManagedSpec(rec.name, {
|
|
2025
|
+
expected: rec,
|
|
2026
|
+
verifyIdentity: true,
|
|
2027
|
+
});
|
|
2028
|
+
if (result.removed && result.killed) stats.killedPids += 1;
|
|
2029
|
+
} catch (e) {
|
|
2030
|
+
log('warn', 'managed-spawn cleanup retry failed for ' + rec.name + ': ' + e.message);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
1628
2033
|
for (const surv of survivors) {
|
|
1629
2034
|
if (_rotateManagedLog(surv.log_path, rotateBytes)) stats.rotatedLogs++;
|
|
1630
2035
|
}
|
|
@@ -1640,11 +2045,11 @@ function sweepManagedSpawn(opts) {
|
|
|
1640
2045
|
// healthy=true→false would mask a real degradation — keep healthy sticky and
|
|
1641
2046
|
// rely on the per-spec loop to refresh post-success, or item 7 boot reconcile
|
|
1642
2047
|
// to (re)establish initial truth on engine restart).
|
|
1643
|
-
function _markBootProbe(name, result) {
|
|
2048
|
+
function _markBootProbe(name, result, expectedGeneration) {
|
|
1644
2049
|
shared.mutateManagedProcesses((data) => {
|
|
1645
2050
|
if (!data || !Array.isArray(data.specs)) return data;
|
|
1646
2051
|
const rec = data.specs.find(s => s && s.name === name);
|
|
1647
|
-
if (!rec) return data;
|
|
2052
|
+
if (!rec || !managedGenerationMatches(rec, expectedGeneration)) return data;
|
|
1648
2053
|
if (result && result.healthy === true) {
|
|
1649
2054
|
rec.healthy = true;
|
|
1650
2055
|
rec.last_health_at = Number.isFinite(result.lastCheckAt) ? result.lastCheckAt : Date.now();
|
|
@@ -1677,7 +2082,7 @@ async function bootReconcileManagedSpawn(opts) {
|
|
|
1677
2082
|
if (!rec || !rec.healthcheck) continue;
|
|
1678
2083
|
try {
|
|
1679
2084
|
const r = await runHealthcheck({ name: rec.name, healthcheck: rec.healthcheck });
|
|
1680
|
-
try { _markBootProbe(rec.name, r); }
|
|
2085
|
+
try { _markBootProbe(rec.name, r, rec); }
|
|
1681
2086
|
catch (e) { log('warn', 'managed-spawn bootReconcile state write failed for ' + rec.name + ': ' + e.message); }
|
|
1682
2087
|
probed.push({ name: rec.name, healthy: !!r.healthy });
|
|
1683
2088
|
} catch (e) {
|
|
@@ -1729,6 +2134,9 @@ module.exports = {
|
|
|
1729
2134
|
MANAGED_LOGS_DIR: MANAGED_LOGS_DIR,
|
|
1730
2135
|
INVALID_WORKDIR_REASON_PREFIX: INVALID_WORKDIR_REASON_PREFIX,
|
|
1731
2136
|
HEALTHCHECK_TYPES: HEALTHCHECK_TYPES,
|
|
2137
|
+
MANAGED_LOG_TAIL_DEFAULT_BYTES: MANAGED_LOG_TAIL_DEFAULT_BYTES,
|
|
2138
|
+
MANAGED_LOG_TAIL_MAX_BYTES: MANAGED_LOG_TAIL_MAX_BYTES,
|
|
2139
|
+
validateManagedSpecName: validateManagedSpecName,
|
|
1732
2140
|
validateManagedSpawnRecord: validateManagedSpawnRecord,
|
|
1733
2141
|
readManagedSpawnFile: readManagedSpawnFile,
|
|
1734
2142
|
evaluateManagedSpawnAcceptance: evaluateManagedSpawnAcceptance,
|
|
@@ -1737,12 +2145,19 @@ module.exports = {
|
|
|
1737
2145
|
openManagedLog: openManagedLog,
|
|
1738
2146
|
spawnManagedSpec: spawnManagedSpec,
|
|
1739
2147
|
recordManagedSpec: recordManagedSpec,
|
|
2148
|
+
reserveManagedBatch: reserveManagedBatch,
|
|
2149
|
+
releaseManagedReservations: releaseManagedReservations,
|
|
1740
2150
|
recordManagedBatch: recordManagedBatch,
|
|
2151
|
+
managedGenerationMatches: managedGenerationMatches,
|
|
2152
|
+
reapOrTrackManagedRuntime: reapOrTrackManagedRuntime,
|
|
1741
2153
|
removeManagedSpec: removeManagedSpec,
|
|
1742
2154
|
listManagedSpecs: listManagedSpecs,
|
|
2155
|
+
getManagedSpecByName: getManagedSpecByName,
|
|
1743
2156
|
// Item 3 (P-9c1f47a6): healthcheck implementations + first-pass waiter.
|
|
1744
2157
|
runHealthcheck: runHealthcheck,
|
|
1745
2158
|
waitForFirstHealth: waitForFirstHealth,
|
|
2159
|
+
_markHealthy, // exported for testing
|
|
2160
|
+
_markBootProbe, // exported for testing
|
|
1746
2161
|
tailManagedLog: tailManagedLog,
|
|
1747
2162
|
// Item 6 (P-1f9c3a45): playbook auto-inject of live managed processes.
|
|
1748
2163
|
buildLiveManagedProcessesBlock: buildLiveManagedProcessesBlock,
|