@pdpp/local-collector 0.11.1 → 0.12.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.
|
@@ -10,6 +10,7 @@ const DEFAULT_QUEUE_PATH = join(dirname(fileURLToPath(import.meta.url)), "..", "
|
|
|
10
10
|
const LOCAL_COLLECTOR_PACKAGE_NAME = "@pdpp/local-collector";
|
|
11
11
|
const LOCAL_COLLECTOR_PACKAGE_VERSION_FALLBACK = "0.0.0";
|
|
12
12
|
const LOCAL_COLLECTOR_PROFILE_DIR_ENV = "PDPP_LOCAL_COLLECTOR_PROFILE_DIR";
|
|
13
|
+
const RECOVER_DEFAULT_MAX_DRAIN_PASSES = 20;
|
|
13
14
|
const LOCAL_COLLECTOR_PLACEHOLDER_VERSION = "0.0.0";
|
|
14
15
|
const REPO_ONLY_PACKAGE_SIBLINGS = ["src", "bin", "test", "scripts", "tsconfig.build.json"];
|
|
15
16
|
function resolveLocalCollectorManifest(startUrl) {
|
|
@@ -113,9 +114,10 @@ Subcommands:
|
|
|
113
114
|
[--limit <n>]
|
|
114
115
|
[--apply] Dry-run by default; --apply mutates after a DB backup.
|
|
115
116
|
recover Resolve the enrolled local profile, recover stalled outbox work,
|
|
116
|
-
and
|
|
117
|
+
and drain queued work until clear or bounded.
|
|
117
118
|
--source-instance-id <id>
|
|
118
119
|
[--profile <name>] Optional profile name under the collector profile dir.
|
|
120
|
+
[--max-drain-passes <n>] Apply mode runs up to N drain passes (default: ${RECOVER_DEFAULT_MAX_DRAIN_PASSES}).
|
|
119
121
|
[--apply] Dry-run by default; --apply requeues and runs.
|
|
120
122
|
prune-sent Delete sent (succeeded) outbox rows to reclaim disk space.
|
|
121
123
|
[--queue <path>]
|
|
@@ -387,7 +389,7 @@ export function buildLocalOutboxDoctor(status, errorSummary) {
|
|
|
387
389
|
remediation.push(`${status.outbox.counts.dead_letter} dead-letter row(s) need recovery.${causeHint} ` +
|
|
388
390
|
"Preview with `pdpp-local-collector recover --source-instance-id <id>`, then apply with " +
|
|
389
391
|
"`pdpp-local-collector recover --source-instance-id <id> --apply`. The apply step backs up the DB, " +
|
|
390
|
-
"prepares failed uploads for retry when present, and
|
|
392
|
+
"prepares failed uploads for retry when present, and drains queued work until clear or bounded.");
|
|
391
393
|
}
|
|
392
394
|
if (checks.expired_leases === "warn") {
|
|
393
395
|
remediation.push(`${status.outbox.expired_leases} lease(s) are past expiry — a previous run likely crashed mid-drain. ` +
|
|
@@ -699,27 +701,45 @@ function hasDeadLetters(status) {
|
|
|
699
701
|
}
|
|
700
702
|
function recoverDryRunNote(status) {
|
|
701
703
|
if (hasDeadLetters(status)) {
|
|
702
|
-
return (`${status.outbox.counts.dead_letter} failed upload row(s) would be prepared for retry, then the collector would
|
|
704
|
+
return (`${status.outbox.counts.dead_letter} failed upload row(s) would be prepared for retry, then the collector would drain queued work. ` +
|
|
703
705
|
"Dry run only; re-run with --apply to mutate the local outbox and upload.");
|
|
704
706
|
}
|
|
705
|
-
return ("No failed upload rows are present for this source. The recovery apply step would run the collector
|
|
706
|
-
"to refresh state and drain queued work.");
|
|
707
|
+
return ("No failed upload rows are present for this source. The recovery apply step would run the collector on this host " +
|
|
708
|
+
"to refresh state and drain queued work until clear or bounded.");
|
|
707
709
|
}
|
|
708
|
-
function
|
|
710
|
+
function outboxOpenWork(status) {
|
|
711
|
+
const counts = status.outbox.counts;
|
|
712
|
+
return counts.dead_letter + counts.leased + counts.pending + counts.retrying;
|
|
713
|
+
}
|
|
714
|
+
function recoverAppliedNote(input) {
|
|
715
|
+
const { attempts, maxPasses, retry, statusAfter, statusBefore, stoppedReason } = input;
|
|
709
716
|
const retried = retry ? `${retry.requeued} failed upload row(s) were prepared for retry. ` : "";
|
|
717
|
+
const remaining = outboxOpenWork(statusAfter);
|
|
718
|
+
if (stoppedReason === "drained") {
|
|
719
|
+
return `${retried}The collector drained queued work in ${attempts} pass(es).`;
|
|
720
|
+
}
|
|
710
721
|
if (hasDeadLetters(statusBefore)) {
|
|
711
|
-
|
|
722
|
+
if (stoppedReason === "max_passes") {
|
|
723
|
+
return `${retried}The collector ran ${attempts} drain pass(es) and ${remaining} queued row(s) remain. Re-run this command to continue; it stopped at the ${maxPasses}-pass safety bound.`;
|
|
724
|
+
}
|
|
725
|
+
return `${retried}The collector ran ${attempts} drain pass(es) and ${remaining} queued row(s) remain. It stopped because another pass did not reduce the backlog.`;
|
|
712
726
|
}
|
|
713
|
-
|
|
727
|
+
if (stoppedReason === "max_passes") {
|
|
728
|
+
return `The collector ran ${attempts} drain pass(es) and ${remaining} queued row(s) remain. Re-run this command to continue; it stopped at the ${maxPasses}-pass safety bound.`;
|
|
729
|
+
}
|
|
730
|
+
return `The collector ran ${attempts} drain pass(es) and ${remaining} queued row(s) remain. It stopped because another pass did not reduce the backlog.`;
|
|
714
731
|
}
|
|
715
|
-
export async function recoverLocalCollector(options) {
|
|
732
|
+
export async function recoverLocalCollector(options, deps = {}) {
|
|
733
|
+
const inspectStatus = deps.inspectStatus ?? inspectLocalOutboxStatus;
|
|
734
|
+
const retryDeadLetters = deps.retryDeadLetters ?? retryLocalOutboxDeadLetters;
|
|
735
|
+
const runOnce = deps.runOnce ?? runCollectorOnce;
|
|
716
736
|
const resolved = resolveRecoveryOptions(options);
|
|
717
737
|
const sourceInstanceId = resolved.options.sourceInstanceId;
|
|
718
738
|
if (!sourceInstanceId) {
|
|
719
739
|
throw new CollectorUsageError("recover requires --source-instance-id <id>");
|
|
720
740
|
}
|
|
721
|
-
const statusBefore =
|
|
722
|
-
const retryPreview = hasDeadLetters(statusBefore) ?
|
|
741
|
+
const statusBefore = inspectStatus(resolved.options);
|
|
742
|
+
const retryPreview = hasDeadLetters(statusBefore) ? retryDeadLetters({ ...resolved.options, apply: false }) : null;
|
|
723
743
|
if (!options.apply) {
|
|
724
744
|
return {
|
|
725
745
|
applied: false,
|
|
@@ -735,18 +755,52 @@ export async function recoverLocalCollector(options) {
|
|
|
735
755
|
status_before: statusBefore,
|
|
736
756
|
};
|
|
737
757
|
}
|
|
738
|
-
const retryApply = hasDeadLetters(statusBefore) ?
|
|
739
|
-
const
|
|
740
|
-
const
|
|
758
|
+
const retryApply = hasDeadLetters(statusBefore) ? retryDeadLetters({ ...resolved.options, apply: true }) : null;
|
|
759
|
+
const maxPasses = options.maxDrainPasses ?? RECOVER_DEFAULT_MAX_DRAIN_PASSES;
|
|
760
|
+
const runs = [];
|
|
761
|
+
let statusAfter = inspectStatus(resolved.options);
|
|
762
|
+
let stoppedReason = "drained";
|
|
763
|
+
let previousOpenAfterRun = null;
|
|
764
|
+
for (let attempt = 0; attempt < maxPasses; attempt += 1) {
|
|
765
|
+
const run = summarizeRunResultForCli(await runOnce(resolved.options));
|
|
766
|
+
runs.push(run);
|
|
767
|
+
statusAfter = inspectStatus(resolved.options);
|
|
768
|
+
const openWork = outboxOpenWork(statusAfter);
|
|
769
|
+
const discoveredNewWork = run.recordsQueued > 0 || run.enqueuedBatches > 0;
|
|
770
|
+
if (openWork === 0) {
|
|
771
|
+
stoppedReason = "drained";
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
774
|
+
if (previousOpenAfterRun !== null && openWork >= previousOpenAfterRun && !discoveredNewWork) {
|
|
775
|
+
stoppedReason = "no_progress";
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
previousOpenAfterRun = openWork;
|
|
779
|
+
if (attempt === maxPasses - 1) {
|
|
780
|
+
stoppedReason = "max_passes";
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
const latestRun = runs.at(-1) ?? null;
|
|
741
784
|
return {
|
|
742
785
|
applied: true,
|
|
743
786
|
db: statusAfter.db.path ? { exists: statusAfter.db.exists, path: statusAfter.db.path } : { exists: false, path: "" },
|
|
787
|
+
drain_attempts: runs.length,
|
|
788
|
+
drain_stopped_reason: stoppedReason,
|
|
744
789
|
dry_run: false,
|
|
745
|
-
|
|
790
|
+
fully_drained: outboxOpenWork(statusAfter) === 0,
|
|
791
|
+
note: recoverAppliedNote({
|
|
792
|
+
attempts: runs.length,
|
|
793
|
+
maxPasses,
|
|
794
|
+
retry: retryApply,
|
|
795
|
+
statusAfter,
|
|
796
|
+
statusBefore,
|
|
797
|
+
stoppedReason,
|
|
798
|
+
}),
|
|
746
799
|
object: "local_collector_recovery",
|
|
747
800
|
profile: { name: resolved.profileName, source: resolved.profileSource },
|
|
748
801
|
retry_dead_letters: retryApply,
|
|
749
|
-
run,
|
|
802
|
+
run: latestRun,
|
|
803
|
+
...(runs.length > 1 ? { runs } : {}),
|
|
750
804
|
source_instance_id: sourceInstanceId,
|
|
751
805
|
status_after: statusAfter,
|
|
752
806
|
status_before: statusBefore,
|
|
@@ -1106,6 +1160,9 @@ function applyOption(options, arg, value) {
|
|
|
1106
1160
|
"--keep-count": (next) => {
|
|
1107
1161
|
options.keepCount = parseNonNegativeInteger("--keep-count", next);
|
|
1108
1162
|
},
|
|
1163
|
+
"--max-drain-passes": (next) => {
|
|
1164
|
+
options.maxDrainPasses = parsePositiveInteger("--max-drain-passes", next);
|
|
1165
|
+
},
|
|
1109
1166
|
};
|
|
1110
1167
|
const set = setters[arg];
|
|
1111
1168
|
if (!set) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const COLLECTOR_BUILD_SOURCE_SENTINEL = "source";
|
|
2
2
|
const COLLECTOR_BUILD_INFO = {
|
|
3
|
-
builtAt: "2026-06-
|
|
4
|
-
revision: "
|
|
5
|
-
version: "0.
|
|
3
|
+
builtAt: "2026-06-18T01:24:53.245Z",
|
|
4
|
+
revision: "0715759501e0",
|
|
5
|
+
version: "0.12.1",
|
|
6
6
|
};
|
|
7
7
|
function buildAgentVersion(info = COLLECTOR_BUILD_INFO) {
|
|
8
8
|
return `${info.version}+${info.revision}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pdpp/local-collector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "Publishable local collector runtime for PDPP: filesystem-class connectors (Claude Code, Codex) plus the device-exporter ingest client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|