@yemi33/minions 0.1.2424 → 0.1.2425
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 +6 -4
- 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/internal-install.md +190 -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/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/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/kb-sweep.js
CHANGED
|
@@ -24,6 +24,7 @@ const KB_SWEEP_LOG_PATH = path.join(ENGINE_DIR, 'kb-sweep.log');
|
|
|
24
24
|
const KB_SWEEP_RUNNER_PATH = path.join(__dirname, 'kb-sweep-runner.js');
|
|
25
25
|
const SWEPT_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
|
26
26
|
const AUTO_SWEEP_INTERVAL_MS = 4 * 60 * 60 * 1000;
|
|
27
|
+
const AUTO_SWEEP_FAILURE_RETRY_MS = 15 * 60 * 1000;
|
|
27
28
|
const KB_SWEPT_PATH = path.join(ENGINE_DIR, 'kb-swept.json');
|
|
28
29
|
const COMPRESS_THRESHOLD_BYTES = 5000;
|
|
29
30
|
const LLM_BATCH_SIZE = 30;
|
|
@@ -726,7 +727,19 @@ function _writeSweepState(state) {
|
|
|
726
727
|
// Phase 9.3: route through shared.mutateKbSweepState (file-locked RMW) so
|
|
727
728
|
// the dashboard process + the detached runner can't race each other.
|
|
728
729
|
const augmented = { pid: process.pid, ...state };
|
|
729
|
-
|
|
730
|
+
let accepted = false;
|
|
731
|
+
try {
|
|
732
|
+
shared.mutateKbSweepState((current) => {
|
|
733
|
+
if (state.sweepToken != null
|
|
734
|
+
&& current?.sweepToken != null
|
|
735
|
+
&& current.sweepToken !== state.sweepToken) {
|
|
736
|
+
return undefined;
|
|
737
|
+
}
|
|
738
|
+
accepted = true;
|
|
739
|
+
return augmented;
|
|
740
|
+
});
|
|
741
|
+
} catch { return false; }
|
|
742
|
+
return accepted;
|
|
730
743
|
}
|
|
731
744
|
|
|
732
745
|
/**
|
|
@@ -740,7 +753,7 @@ function _writeSweepState(state) {
|
|
|
740
753
|
* @param {number} [opts.entryCount=0] - KB entry count for staleGuardMs()
|
|
741
754
|
* @param {number} [opts.now=Date.now()] - injectable clock for tests
|
|
742
755
|
* @param {(pid:number)=>boolean} [opts.isPidAlive] - injectable for tests
|
|
743
|
-
* @returns {{ inFlight: boolean, alive?: boolean, stale?: boolean, pid?: number,
|
|
756
|
+
* @returns {{ inFlight: boolean, alive?: boolean, stale?: boolean, overdue?: boolean, pid?: number,
|
|
744
757
|
* startedAt?: number, sweepToken?: string|number|null, guardMs?: number,
|
|
745
758
|
* status?: string }}
|
|
746
759
|
*/
|
|
@@ -764,16 +777,19 @@ function readSweepLiveness(opts = {}) {
|
|
|
764
777
|
const age = startedAt ? now - startedAt : 0;
|
|
765
778
|
let alive;
|
|
766
779
|
if (state.status === 'starting') {
|
|
767
|
-
//
|
|
768
|
-
//
|
|
780
|
+
// Current claims record the caller PID so slow pre-spawn I/O cannot make a
|
|
781
|
+
// live owner stealable. Legacy pid-less claims retain the short boot grace.
|
|
769
782
|
const STARTING_GRACE_MS = 15000;
|
|
770
|
-
alive = age <= STARTING_GRACE_MS;
|
|
783
|
+
alive = pid > 0 ? !!isPidAlive(pid) : age <= STARTING_GRACE_MS;
|
|
771
784
|
} else {
|
|
772
785
|
alive = pid > 0 ? !!isPidAlive(pid) : false;
|
|
773
786
|
}
|
|
774
|
-
|
|
787
|
+
// Sweeps regularly run for 1h+; replacing an overdue live runner would leave
|
|
788
|
+
// its detached process tree active and create overlapping sweeps.
|
|
789
|
+
const overdue = startedAt > 0 && age > guardMs;
|
|
790
|
+
const stale = !alive;
|
|
775
791
|
return {
|
|
776
|
-
inFlight: true, alive, stale, pid, startedAt, guardMs,
|
|
792
|
+
inFlight: true, alive, stale, overdue, pid, startedAt, guardMs,
|
|
777
793
|
sweepToken: state.sweepToken || null, status: state.status,
|
|
778
794
|
};
|
|
779
795
|
}
|
|
@@ -833,18 +849,6 @@ function reconcileSweepStateOnBoot(opts = {}) {
|
|
|
833
849
|
|
|
834
850
|
const reason = pid > 0 ? 'pid-dead-on-startup-check' : 'no-pid-recorded-on-startup-check';
|
|
835
851
|
|
|
836
|
-
// CAS guard: re-read right before the write so we don't clobber a fresh
|
|
837
|
-
// `starting`/`in-flight` record that a concurrent dashboard POST or runner
|
|
838
|
-
// wrote between our decision and our write.
|
|
839
|
-
const current = safeJson(KB_SWEEP_STATE_PATH);
|
|
840
|
-
if (!current
|
|
841
|
-
|| current.status !== state.status
|
|
842
|
-
|| Number(current.startedAt || 0) !== Number(state.startedAt || 0)
|
|
843
|
-
|| Number(current.pid || 0) !== pid
|
|
844
|
-
|| (current.sweepToken || null) !== (state.sweepToken || null)) {
|
|
845
|
-
return { scanned: 1, released: 0, reason: 'state-changed-before-reconcile' };
|
|
846
|
-
}
|
|
847
|
-
|
|
848
852
|
const failedState = {
|
|
849
853
|
...state,
|
|
850
854
|
status: 'failed',
|
|
@@ -856,7 +860,25 @@ function reconcileSweepStateOnBoot(opts = {}) {
|
|
|
856
860
|
// Direct mutateKbSweepState (NOT _writeSweepState) so the original pid is
|
|
857
861
|
// preserved for forensics — _writeSweepState would overwrite it with this
|
|
858
862
|
// process's pid, destroying the diagnostic value of the record.
|
|
859
|
-
|
|
863
|
+
let released = false;
|
|
864
|
+
try {
|
|
865
|
+
shared.mutateKbSweepState((current) => {
|
|
866
|
+
if (!current
|
|
867
|
+
|| current.status !== state.status
|
|
868
|
+
|| Number(current.startedAt || 0) !== Number(state.startedAt || 0)
|
|
869
|
+
|| Number(current.pid || 0) !== pid
|
|
870
|
+
|| (current.sweepToken || null) !== (state.sweepToken || null)) {
|
|
871
|
+
return undefined;
|
|
872
|
+
}
|
|
873
|
+
released = true;
|
|
874
|
+
return failedState;
|
|
875
|
+
});
|
|
876
|
+
} catch {
|
|
877
|
+
return { scanned: 1, released: 0, reason: 'reconcile-write-failed' };
|
|
878
|
+
}
|
|
879
|
+
if (!released) {
|
|
880
|
+
return { scanned: 1, released: 0, reason: 'state-changed-before-reconcile' };
|
|
881
|
+
}
|
|
860
882
|
return {
|
|
861
883
|
scanned: 1, released: 1, reason,
|
|
862
884
|
prevStatus: state.status, prevPid: pid,
|
|
@@ -884,9 +906,18 @@ async function runKbSweep(opts = {}) {
|
|
|
884
906
|
// still reports terminal status and the dashboard pre-write doesn't leak
|
|
885
907
|
// a stale "in-flight"/"starting" record. The inner _runKbSweepImpl still
|
|
886
908
|
// honors dryRun for actual file mutations.
|
|
887
|
-
|
|
909
|
+
const ownsSweep = _writeSweepState({
|
|
910
|
+
status: 'in-flight', startedAt, startedAtIso: ts(), sweepToken, dryRun,
|
|
911
|
+
});
|
|
912
|
+
if (sweepToken != null && !ownsSweep) {
|
|
913
|
+
throw new Error('KB sweep claim was superseded before runner start');
|
|
914
|
+
}
|
|
888
915
|
try {
|
|
889
916
|
const result = await _runKbSweepImpl(opts);
|
|
917
|
+
if (!dryRun) {
|
|
918
|
+
try { shared.mutateKbSwept(() => ({ timestamp: ts(), summary: result.summary, detail: result })); } catch { /* ignore */ }
|
|
919
|
+
try { queries.invalidateKnowledgeBaseCache(); } catch { /* ignore */ }
|
|
920
|
+
}
|
|
890
921
|
_writeSweepState({
|
|
891
922
|
status: 'completed', startedAt, completedAt: Date.now(), completedAtIso: ts(),
|
|
892
923
|
durationMs: result.durationMs, summary: result.summary, lastResult: result,
|
|
@@ -1019,10 +1050,6 @@ async function _runKbSweepImpl(opts = {}) {
|
|
|
1019
1050
|
summary.durationMs = Date.now() - t0;
|
|
1020
1051
|
summary.summary = `${summary.hashDuplicatesArchived} hash-dup, ${summary.notesConsolidated} consolidated (${summary.digestsWritten} digests), ${summary.llmDuplicatesArchived} llm-dup, ${summary.staleRemoved} stale, ${summary.ageExpired} age-expired, ${summary.reclassified} reclassified, ${summary.rewritten} rewritten (${(summary.bytesBefore - summary.bytesAfter).toLocaleString()} bytes saved)`;
|
|
1021
1052
|
|
|
1022
|
-
if (!opts.dryRun) {
|
|
1023
|
-
try { shared.mutateKbSwept(() => ({ timestamp: ts(), summary: summary.summary, detail: summary })); } catch { /* ignore */ }
|
|
1024
|
-
try { queries.invalidateKnowledgeBaseCache(); } catch { /* ignore */ }
|
|
1025
|
-
}
|
|
1026
1053
|
return summary;
|
|
1027
1054
|
}
|
|
1028
1055
|
|
|
@@ -1032,8 +1059,8 @@ async function _runKbSweepImpl(opts = {}) {
|
|
|
1032
1059
|
* tick's auto-sweep phase. Performs the same synchronous "starting" → "in-flight"
|
|
1033
1060
|
* CAS dance the dashboard handler used to do inline.
|
|
1034
1061
|
*
|
|
1035
|
-
*
|
|
1036
|
-
*
|
|
1062
|
+
* The helper atomically claims the persistent sweep state before spawning, so
|
|
1063
|
+
* concurrent manual and automatic callers cannot launch overlapping runners.
|
|
1037
1064
|
*
|
|
1038
1065
|
* @param {object} opts
|
|
1039
1066
|
* @param {string[]} [opts.pinnedKeys] - extra pinned KB keys to skip in the sweep
|
|
@@ -1045,23 +1072,61 @@ async function _runKbSweepImpl(opts = {}) {
|
|
|
1045
1072
|
* ok=false + error on synchronous spawn failure; the "starting" claim is
|
|
1046
1073
|
* released so the caller can retry immediately.
|
|
1047
1074
|
*/
|
|
1075
|
+
function _claimSweepRunnerState({ sweepToken, startedAt, observed }) {
|
|
1076
|
+
let claimed = false;
|
|
1077
|
+
let currentState = null;
|
|
1078
|
+
shared.mutateKbSweepState((current) => {
|
|
1079
|
+
currentState = current || null;
|
|
1080
|
+
const currentInFlight = current?.status === 'starting' || current?.status === 'in-flight';
|
|
1081
|
+
if (currentInFlight) {
|
|
1082
|
+
const matchesObserved = observed?.inFlight
|
|
1083
|
+
&& current.status === observed.status
|
|
1084
|
+
&& Number(current.startedAt || 0) === Number(observed.startedAt || 0)
|
|
1085
|
+
&& Number(current.pid || 0) === Number(observed.pid || 0)
|
|
1086
|
+
&& (current.sweepToken || null) === (observed.sweepToken || null);
|
|
1087
|
+
if (!matchesObserved || observed.alive) return undefined;
|
|
1088
|
+
}
|
|
1089
|
+
claimed = true;
|
|
1090
|
+
return {
|
|
1091
|
+
status: 'starting', startedAt, startedAtIso: new Date(startedAt).toISOString(),
|
|
1092
|
+
sweepToken, pid: process.pid,
|
|
1093
|
+
};
|
|
1094
|
+
});
|
|
1095
|
+
return { claimed, current: currentState };
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1048
1098
|
function spawnSweepRunnerDetached(opts = {}) {
|
|
1049
1099
|
const fsLocal = require('fs');
|
|
1050
1100
|
const { spawn: cpSpawn } = require('child_process');
|
|
1101
|
+
const spawnFn = typeof opts.spawn === 'function' ? opts.spawn : cpSpawn;
|
|
1051
1102
|
const logFn = typeof opts.log === 'function'
|
|
1052
1103
|
? opts.log
|
|
1053
1104
|
: (level, msg) => { (level === 'error' ? console.error : console.log)(`[kb-sweep] ${msg}`); };
|
|
1054
1105
|
const cwd = opts.cwd || require('./queries').MINIONS_DIR;
|
|
1055
1106
|
const startedAt = Date.now();
|
|
1056
1107
|
const sweepToken = `${startedAt}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1108
|
+
const observed = readSweepLiveness({ now: startedAt });
|
|
1109
|
+
if (observed.inFlight && observed.alive) {
|
|
1110
|
+
return {
|
|
1111
|
+
ok: true, alreadyRunning: true, sweepToken: observed.sweepToken,
|
|
1112
|
+
pid: observed.pid || null, startedAt: observed.startedAt || null, bodyFile: null,
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1057
1115
|
|
|
1058
1116
|
try {
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1117
|
+
const claim = _claimSweepRunnerState({ sweepToken, startedAt, observed });
|
|
1118
|
+
if (!claim.claimed) {
|
|
1119
|
+
return {
|
|
1120
|
+
ok: true, alreadyRunning: true,
|
|
1121
|
+
sweepToken: claim.current?.sweepToken || null,
|
|
1122
|
+
pid: Number(claim.current?.pid) || null,
|
|
1123
|
+
startedAt: Number(claim.current?.startedAt) || null,
|
|
1124
|
+
bodyFile: null,
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1063
1127
|
} catch (e) {
|
|
1064
1128
|
logFn('error', `failed to write starting state: ${e.message}`);
|
|
1129
|
+
return { ok: false, error: `claim failed: ${e.message}`, sweepToken, pid: null, bodyFile: null };
|
|
1065
1130
|
}
|
|
1066
1131
|
|
|
1067
1132
|
let bodyFile = null;
|
|
@@ -1082,9 +1147,16 @@ function spawnSweepRunnerDetached(opts = {}) {
|
|
|
1082
1147
|
|
|
1083
1148
|
let logFdNum = null;
|
|
1084
1149
|
let stdio = ['ignore', 'ignore', 'ignore'];
|
|
1150
|
+
const closeLogFd = () => {
|
|
1151
|
+
if (Number.isInteger(logFdNum)) {
|
|
1152
|
+
try { fsLocal.closeSync(logFdNum); } catch { /* ignore */ }
|
|
1153
|
+
logFdNum = null;
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1085
1156
|
try {
|
|
1086
|
-
|
|
1087
|
-
|
|
1157
|
+
const opened = shared.openAppendLogFd(path.basename(KB_SWEEP_LOG_PATH), path.dirname(KB_SWEEP_LOG_PATH));
|
|
1158
|
+
logFdNum = opened.fd;
|
|
1159
|
+
if (logFdNum !== 'ignore') stdio = ['ignore', logFdNum, logFdNum];
|
|
1088
1160
|
} catch (e) {
|
|
1089
1161
|
logFn('error', `failed to open log ${KB_SWEEP_LOG_PATH}: ${e.message}`);
|
|
1090
1162
|
}
|
|
@@ -1092,19 +1164,61 @@ function spawnSweepRunnerDetached(opts = {}) {
|
|
|
1092
1164
|
const spawnArgs = ['--sweep-token', sweepToken];
|
|
1093
1165
|
if (bodyFile) spawnArgs.push('--body-file', bodyFile);
|
|
1094
1166
|
|
|
1167
|
+
const claimedState = safeJson(KB_SWEEP_STATE_PATH);
|
|
1168
|
+
if (!claimedState || claimedState.sweepToken !== sweepToken || claimedState.status !== 'starting') {
|
|
1169
|
+
closeLogFd();
|
|
1170
|
+
if (bodyFile) try { fsLocal.unlinkSync(bodyFile); } catch { /* ignore */ }
|
|
1171
|
+
return {
|
|
1172
|
+
ok: true, alreadyRunning: true,
|
|
1173
|
+
sweepToken: claimedState?.sweepToken || null,
|
|
1174
|
+
pid: Number(claimedState?.pid) || null,
|
|
1175
|
+
startedAt: Number(claimedState?.startedAt) || null,
|
|
1176
|
+
bodyFile: null,
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1095
1180
|
let proc;
|
|
1096
1181
|
try {
|
|
1097
|
-
proc =
|
|
1182
|
+
proc = spawnFn(process.execPath, [KB_SWEEP_RUNNER_PATH, ...spawnArgs], {
|
|
1098
1183
|
cwd, stdio, detached: true, windowsHide: true,
|
|
1099
1184
|
env: { ...process.env },
|
|
1100
1185
|
});
|
|
1101
1186
|
} catch (e) {
|
|
1102
|
-
|
|
1187
|
+
closeLogFd();
|
|
1103
1188
|
if (bodyFile) try { fsLocal.unlinkSync(bodyFile); } catch { /* ignore */ }
|
|
1104
|
-
|
|
1189
|
+
_writeSweepState({
|
|
1190
|
+
status: 'failed',
|
|
1191
|
+
startedAt,
|
|
1192
|
+
completedAt: Date.now(),
|
|
1193
|
+
completedAtIso: new Date().toISOString(),
|
|
1194
|
+
error: `spawn failed: ${e.message}`,
|
|
1195
|
+
sweepToken,
|
|
1196
|
+
});
|
|
1105
1197
|
return { ok: false, error: `spawn failed: ${e.message}`, sweepToken, pid: null, bodyFile: null };
|
|
1106
1198
|
}
|
|
1107
|
-
|
|
1199
|
+
let spawnErrorHandled = false;
|
|
1200
|
+
const handleSpawnError = (error) => {
|
|
1201
|
+
if (spawnErrorHandled) return;
|
|
1202
|
+
spawnErrorHandled = true;
|
|
1203
|
+
closeLogFd();
|
|
1204
|
+
if (bodyFile) try { fsLocal.unlinkSync(bodyFile); } catch { /* ignore */ }
|
|
1205
|
+
_writeSweepState({
|
|
1206
|
+
status: 'failed',
|
|
1207
|
+
startedAt,
|
|
1208
|
+
completedAt: Date.now(),
|
|
1209
|
+
completedAtIso: new Date().toISOString(),
|
|
1210
|
+
error: `spawn failed: ${error?.message || error}`,
|
|
1211
|
+
sweepToken,
|
|
1212
|
+
});
|
|
1213
|
+
logFn('error', `runner spawn failed: ${error?.message || error}`);
|
|
1214
|
+
};
|
|
1215
|
+
proc.once('error', handleSpawnError);
|
|
1216
|
+
if (!Number.isInteger(proc.pid) || proc.pid <= 0) {
|
|
1217
|
+
const error = new Error('runner process did not provide a pid');
|
|
1218
|
+
handleSpawnError(error);
|
|
1219
|
+
return { ok: false, error: error.message, sweepToken, pid: null, bodyFile: null };
|
|
1220
|
+
}
|
|
1221
|
+
closeLogFd();
|
|
1108
1222
|
|
|
1109
1223
|
// Phase 9.3: atomic starting→in-flight transition under the file lock
|
|
1110
1224
|
// (was previously a safeJson→safeWrite race).
|
|
@@ -1139,9 +1253,16 @@ function shouldAutoSweep(opts = {}) {
|
|
|
1139
1253
|
const now = Number(opts.now) || Date.now();
|
|
1140
1254
|
const intervalMs = Number(opts.intervalMs) || AUTO_SWEEP_INTERVAL_MS;
|
|
1141
1255
|
const liveness = opts.liveness || readSweepLiveness({ entryCount: opts.entryCount || 0, now });
|
|
1142
|
-
if (liveness.inFlight && liveness.alive
|
|
1256
|
+
if (liveness.inFlight && liveness.alive) {
|
|
1143
1257
|
return { shouldSpawn: false, reason: 'sweep-in-flight', lastCompletedAt: null };
|
|
1144
1258
|
}
|
|
1259
|
+
const sweepState = safeJson(KB_SWEEP_STATE_PATH);
|
|
1260
|
+
const failedAt = sweepState?.status === 'failed'
|
|
1261
|
+
? Number(sweepState.completedAt || 0)
|
|
1262
|
+
: 0;
|
|
1263
|
+
if (failedAt > 0 && (now - failedAt) < AUTO_SWEEP_FAILURE_RETRY_MS) {
|
|
1264
|
+
return { shouldSpawn: false, reason: 'failure-backoff', lastCompletedAt: null };
|
|
1265
|
+
}
|
|
1145
1266
|
const swept = safeJson(KB_SWEPT_PATH);
|
|
1146
1267
|
const sweptTs = swept && swept.timestamp ? Date.parse(swept.timestamp) : NaN;
|
|
1147
1268
|
const lastCompletedAt = Number.isFinite(sweptTs) ? sweptTs : null;
|
|
@@ -1165,6 +1286,7 @@ module.exports = {
|
|
|
1165
1286
|
spawnSweepRunnerDetached,
|
|
1166
1287
|
shouldAutoSweep,
|
|
1167
1288
|
AUTO_SWEEP_INTERVAL_MS,
|
|
1289
|
+
AUTO_SWEEP_FAILURE_RETRY_MS,
|
|
1168
1290
|
KB_SWEPT_PATH,
|
|
1169
1291
|
KB_SWEEP_STATE_PATH,
|
|
1170
1292
|
KB_SWEEP_LOG_PATH,
|
|
@@ -1178,6 +1300,7 @@ module.exports = {
|
|
|
1178
1300
|
_entryAgeMs,
|
|
1179
1301
|
_resolveTtlByCategory,
|
|
1180
1302
|
_archiveKbFile,
|
|
1303
|
+
_claimSweepRunnerState, _writeSweepState, // exported for testing
|
|
1181
1304
|
_expireByAge,
|
|
1182
1305
|
_entryAgeMs,
|
|
1183
1306
|
_resolveTtlByCategory,
|
|
@@ -21,11 +21,44 @@ const fs = require('fs');
|
|
|
21
21
|
const path = require('path');
|
|
22
22
|
const shared = require('./shared');
|
|
23
23
|
const queries = require('./queries');
|
|
24
|
+
const processUtils = require('./process-utils');
|
|
24
25
|
|
|
25
26
|
const { log, ENGINE_DEFAULTS } = shared;
|
|
26
27
|
|
|
27
28
|
const KEEP_PIDS_FILENAME = 'keep-pids.json';
|
|
28
29
|
const INVALID_WORKDIR_REASON_PREFIX = 'invalid-workdir: ';
|
|
30
|
+
const KEEP_PROCESS_AGENT_ID_MAX = 128;
|
|
31
|
+
const KEEP_PROCESS_PID_MAX = 0x7fffffff;
|
|
32
|
+
const KEEP_PROCESS_IDENTITY_CLOCK_SKEW_MS = 1000;
|
|
33
|
+
const KEEP_PROCESS_AGENT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
34
|
+
|
|
35
|
+
function isSafeKeepProcessAgentId(agentId) {
|
|
36
|
+
return typeof agentId === 'string'
|
|
37
|
+
&& agentId.length > 0
|
|
38
|
+
&& agentId.length <= KEEP_PROCESS_AGENT_ID_MAX
|
|
39
|
+
&& KEEP_PROCESS_AGENT_ID_RE.test(agentId);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function validateKeepProcessPid(pid) {
|
|
43
|
+
if (!Number.isInteger(pid) || pid <= 0 || pid > KEEP_PROCESS_PID_MAX) {
|
|
44
|
+
return { ok: false, reason: 'pid-invalid (' + pid + ')' };
|
|
45
|
+
}
|
|
46
|
+
return { ok: true, value: pid };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getKeepProcessIdentity(pid, opts = {}) {
|
|
50
|
+
const validation = validateKeepProcessPid(pid);
|
|
51
|
+
if (!validation.ok) return null;
|
|
52
|
+
return processUtils.getProcessIdentity(pid, opts);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isKeepProcessIdentityCompatible(identity, sidecarMtimeMs) {
|
|
56
|
+
return processUtils.isProcessIdentityCompatible(
|
|
57
|
+
identity,
|
|
58
|
+
sidecarMtimeMs,
|
|
59
|
+
KEEP_PROCESS_IDENTITY_CLOCK_SKEW_MS,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
29
62
|
|
|
30
63
|
function _agentsDir() {
|
|
31
64
|
return queries.AGENTS_DIR || path.join(shared.MINIONS_DIR, 'agents');
|
|
@@ -57,9 +90,9 @@ function validateKeepPidsRecord(parsed, opts) {
|
|
|
57
90
|
if (parsed.pids.length > maxPids) return { ok: false, reason: 'pids-too-many (>' + maxPids + ')' };
|
|
58
91
|
const pids = [];
|
|
59
92
|
for (const raw of parsed.pids) {
|
|
60
|
-
const
|
|
61
|
-
if (!
|
|
62
|
-
pids.push(
|
|
93
|
+
const validation = validateKeepProcessPid(raw);
|
|
94
|
+
if (!validation.ok) return validation;
|
|
95
|
+
pids.push(validation.value);
|
|
63
96
|
}
|
|
64
97
|
|
|
65
98
|
if (typeof parsed.expires_at !== 'string') return { ok: false, reason: 'expires_at-missing' };
|
|
@@ -565,6 +598,13 @@ function buildKeepProcessesHint(opts) {
|
|
|
565
598
|
module.exports = {
|
|
566
599
|
KEEP_PIDS_FILENAME: KEEP_PIDS_FILENAME,
|
|
567
600
|
INVALID_WORKDIR_REASON_PREFIX: INVALID_WORKDIR_REASON_PREFIX,
|
|
601
|
+
KEEP_PROCESS_AGENT_ID_MAX: KEEP_PROCESS_AGENT_ID_MAX,
|
|
602
|
+
KEEP_PROCESS_PID_MAX: KEEP_PROCESS_PID_MAX,
|
|
603
|
+
KEEP_PROCESS_IDENTITY_CLOCK_SKEW_MS: KEEP_PROCESS_IDENTITY_CLOCK_SKEW_MS,
|
|
604
|
+
isSafeKeepProcessAgentId: isSafeKeepProcessAgentId,
|
|
605
|
+
validateKeepProcessPid: validateKeepProcessPid,
|
|
606
|
+
getKeepProcessIdentity: getKeepProcessIdentity,
|
|
607
|
+
isKeepProcessIdentityCompatible: isKeepProcessIdentityCompatible,
|
|
568
608
|
validateKeepPidsRecord: validateKeepPidsRecord,
|
|
569
609
|
readKeepPidsFile: readKeepPidsFile,
|
|
570
610
|
listAllKeepPidsFiles: listAllKeepPidsFiles,
|