@ouro.bot/cli 0.1.0-alpha.691 → 0.1.0-alpha.693
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/changelog.json +14 -0
- package/dist/heart/daemon/cli-exec.js +5 -0
- package/dist/heart/daemon/doctor.js +166 -64
- package/dist/heart/daemon/sense-manager.js +64 -3
- package/dist/nerves/index.js +11 -1
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.693",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Load vault-backed provider credentials before manual context-loss Sentinel refreshes so a fresh CLI process cannot report false not-loaded provider warnings.",
|
|
8
|
+
"Have doctor lifecycle checks read the machine-local daemon log alongside bundle-local logs so stale historical bundle logs cannot mask live daemon activity."
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"version": "0.1.0-alpha.692",
|
|
13
|
+
"changes": [
|
|
14
|
+
"Retry transient managed-sense runtime credential reads with bounded backoff so enabled senses recover from vault timeouts without a daemon restart.",
|
|
15
|
+
"Rotate NDJSON logs immediately after a single oversized final event and make doctor ignore launchd plists for paused or unscheduled habits while avoiding prune warnings for already-rotated compressed history."
|
|
16
|
+
]
|
|
17
|
+
},
|
|
4
18
|
{
|
|
5
19
|
"version": "0.1.0-alpha.691",
|
|
6
20
|
"changes": [
|
|
@@ -7647,6 +7647,9 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
7647
7647
|
throw new Error("work sentinel requires --agent <name>");
|
|
7648
7648
|
const bundlesRoot = deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
|
|
7649
7649
|
const agentRoot = deps.agentBundleRoot ?? path.join(bundlesRoot, `${command.agent}.ouro`);
|
|
7650
|
+
if (command.kind === "work.sentinel.refresh") {
|
|
7651
|
+
await (0, provider_credentials_1.refreshProviderCredentialPool)(command.agent, { preserveCachedOnFailure: true });
|
|
7652
|
+
}
|
|
7650
7653
|
const sentinel = command.kind === "work.sentinel.refresh"
|
|
7651
7654
|
? await refreshContextLossSentinel(command.agent, agentRoot, { trigger: "manual_cli", homeDir: deps.homeDir })
|
|
7652
7655
|
: readContextLossSentinelView(agentRoot, { limit: 20 });
|
|
@@ -7885,8 +7888,10 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
7885
7888
|
fetchImpl: deps.fetchImpl ?? fetch,
|
|
7886
7889
|
socketPath: deps.socketPath,
|
|
7887
7890
|
bundlesRoot: deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)(),
|
|
7891
|
+
daemonLogsDir: path.join((0, ouro_version_manager_1.getOuroCliHome)(deps.homeDir), "daemon", "logs"),
|
|
7888
7892
|
homedir: os.homedir(),
|
|
7889
7893
|
envPath: process.env.PATH ?? "",
|
|
7894
|
+
platform: process.platform,
|
|
7890
7895
|
};
|
|
7891
7896
|
const doctorResult = command.category
|
|
7892
7897
|
? await (0, doctor_1.runDoctorChecks)(doctorDeps, { category: command.category })
|
|
@@ -25,6 +25,9 @@ const bluebubbles_health_diagnostics_1 = require("./bluebubbles-health-diagnosti
|
|
|
25
25
|
const ouro_path_installer_1 = require("../versioning/ouro-path-installer");
|
|
26
26
|
const runtime_credentials_1 = require("../runtime-credentials");
|
|
27
27
|
const machine_identity_1 = require("../machine-identity");
|
|
28
|
+
const habit_parser_1 = require("../habits/habit-parser");
|
|
29
|
+
const cadence_1 = require("./cadence");
|
|
30
|
+
const nerves_1 = require("../../nerves");
|
|
28
31
|
const DEFAULT_BLUEBUBBLES_REQUEST_TIMEOUT_MS = 30_000;
|
|
29
32
|
// ── Category checkers ──
|
|
30
33
|
function checkCliPath(deps) {
|
|
@@ -321,17 +324,73 @@ function checkHabits(deps) {
|
|
|
321
324
|
continue;
|
|
322
325
|
}
|
|
323
326
|
checks.push({ label: `${agentDir} habits dir`, status: "pass", detail: habitsDir });
|
|
327
|
+
const activeScheduledHabits = [];
|
|
328
|
+
let unreadableHabits = 0;
|
|
329
|
+
for (const file of deps.readdirSync(habitsDir).filter((entry) => entry.endsWith(".md"))) {
|
|
330
|
+
const filePath = `${habitsDir}/${file}`;
|
|
331
|
+
try {
|
|
332
|
+
const habit = (0, habit_parser_1.parseHabitFile)(deps.readFileSync(filePath), filePath);
|
|
333
|
+
if (habit.status === "active" && habit.cadence && (0, cadence_1.parseCadenceToCron)(habit.cadence) !== null) {
|
|
334
|
+
activeScheduledHabits.push(habit.name);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
unreadableHabits += 1;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (unreadableHabits > 0) {
|
|
342
|
+
checks.push({
|
|
343
|
+
label: `${agentDir} habit files`,
|
|
344
|
+
status: "warn",
|
|
345
|
+
detail: `${unreadableHabits} unreadable habit file(s)`,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
if (activeScheduledHabits.length === 0) {
|
|
349
|
+
checks.push({
|
|
350
|
+
label: `${agentDir} launchd plists`,
|
|
351
|
+
status: "pass",
|
|
352
|
+
detail: "no active scheduled habits require launchd",
|
|
353
|
+
});
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
324
356
|
// Check for launchd plists on macOS
|
|
357
|
+
const platform = deps.platform;
|
|
358
|
+
if (platform !== "darwin") {
|
|
359
|
+
checks.push({
|
|
360
|
+
label: `${agentDir} launchd plists`,
|
|
361
|
+
status: "pass",
|
|
362
|
+
detail: `launchd not applicable on ${platform}; scheduled habits use the platform cron manager`,
|
|
363
|
+
});
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
325
366
|
const launchAgentsDir = `${deps.homedir}/Library/LaunchAgents`;
|
|
326
367
|
if (deps.existsSync(launchAgentsDir)) {
|
|
327
|
-
const plists = deps.readdirSync(launchAgentsDir)
|
|
328
|
-
|
|
329
|
-
|
|
368
|
+
const plists = new Set(deps.readdirSync(launchAgentsDir));
|
|
369
|
+
const missing = activeScheduledHabits
|
|
370
|
+
.map((habitName) => `bot.ouro.${agentName}.${habitName}.plist`)
|
|
371
|
+
.filter((plistName) => !plists.has(plistName));
|
|
372
|
+
if (missing.length === 0) {
|
|
373
|
+
checks.push({
|
|
374
|
+
label: `${agentDir} launchd plists`,
|
|
375
|
+
status: "pass",
|
|
376
|
+
detail: `${activeScheduledHabits.length} active scheduled habit(s) registered`,
|
|
377
|
+
});
|
|
330
378
|
}
|
|
331
379
|
else {
|
|
332
|
-
checks.push({
|
|
380
|
+
checks.push({
|
|
381
|
+
label: `${agentDir} launchd plists`,
|
|
382
|
+
status: "fail",
|
|
383
|
+
detail: `missing ${missing.join(", ")}`,
|
|
384
|
+
});
|
|
333
385
|
}
|
|
334
386
|
}
|
|
387
|
+
else {
|
|
388
|
+
checks.push({
|
|
389
|
+
label: `${agentDir} launchd plists`,
|
|
390
|
+
status: "fail",
|
|
391
|
+
detail: `${launchAgentsDir} not found for ${activeScheduledHabits.length} active scheduled habit(s)`,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
335
394
|
}
|
|
336
395
|
if (checks.length === 0) {
|
|
337
396
|
checks.push({ label: "habits", status: "warn", detail: "no agents found" });
|
|
@@ -630,14 +689,29 @@ function checkFriends(deps) {
|
|
|
630
689
|
}
|
|
631
690
|
function checkDisk(deps) {
|
|
632
691
|
const checks = [];
|
|
692
|
+
const isActiveLogStream = (name) => {
|
|
693
|
+
if (name.endsWith(".ndjson"))
|
|
694
|
+
return !/\.\d+\.ndjson$/.test(name);
|
|
695
|
+
if (name.endsWith(".log"))
|
|
696
|
+
return !/\.\d+\.log$/.test(name);
|
|
697
|
+
return false;
|
|
698
|
+
};
|
|
633
699
|
const addLogSizeCheck = (labelPrefix, logsDir) => {
|
|
634
700
|
let totalSize = 0;
|
|
701
|
+
let activeSize = 0;
|
|
702
|
+
const oversizedActive = [];
|
|
635
703
|
try {
|
|
636
704
|
const files = deps.readdirSync(logsDir);
|
|
637
705
|
for (const file of files) {
|
|
638
706
|
try {
|
|
639
707
|
const stat = deps.statSync(`${logsDir}/${file}`);
|
|
640
708
|
totalSize += stat.size;
|
|
709
|
+
if (isActiveLogStream(file)) {
|
|
710
|
+
activeSize += stat.size;
|
|
711
|
+
if (stat.size >= nerves_1.DEFAULT_MAX_LOG_SIZE_BYTES) {
|
|
712
|
+
oversizedActive.push(file);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
641
715
|
}
|
|
642
716
|
catch {
|
|
643
717
|
// skip unreadable files
|
|
@@ -648,14 +722,19 @@ function checkDisk(deps) {
|
|
|
648
722
|
// readdirSync failure handled below
|
|
649
723
|
}
|
|
650
724
|
const sizeMB = totalSize / (1024 * 1024);
|
|
651
|
-
|
|
652
|
-
|
|
725
|
+
const activeSizeMB = activeSize / (1024 * 1024);
|
|
726
|
+
if (activeSizeMB > 500) {
|
|
727
|
+
checks.push({ label: `${labelPrefix} daemon log size`, status: "fail", detail: `${activeSizeMB.toFixed(1)}MB active / ${sizeMB.toFixed(1)}MB total — active logs exceed 500MB limit` });
|
|
653
728
|
}
|
|
654
|
-
else if (
|
|
655
|
-
checks.push({
|
|
729
|
+
else if (oversizedActive.length > 0) {
|
|
730
|
+
checks.push({
|
|
731
|
+
label: `${labelPrefix} daemon log size`,
|
|
732
|
+
status: "warn",
|
|
733
|
+
detail: `${activeSizeMB.toFixed(1)}MB active / ${sizeMB.toFixed(1)}MB total; active stream(s) over ${Math.round(nerves_1.DEFAULT_MAX_LOG_SIZE_BYTES / (1024 * 1024))}MB: ${oversizedActive.join(", ")} — run \`ouro logs prune\``,
|
|
734
|
+
});
|
|
656
735
|
}
|
|
657
736
|
else {
|
|
658
|
-
checks.push({ label: `${labelPrefix} daemon log size`, status: "pass", detail: `${sizeMB.toFixed(1)}MB` });
|
|
737
|
+
checks.push({ label: `${labelPrefix} daemon log size`, status: "pass", detail: `${activeSizeMB.toFixed(1)}MB active / ${sizeMB.toFixed(1)}MB total` });
|
|
659
738
|
}
|
|
660
739
|
};
|
|
661
740
|
const agents = discoverAgents(deps);
|
|
@@ -704,83 +783,106 @@ function computeSummary(categories) {
|
|
|
704
783
|
* to answer the operator's question after the daemon has gone silent: "did
|
|
705
784
|
* it crash? when did it last do anything? did it just upgrade?"
|
|
706
785
|
*
|
|
707
|
-
* Reads daemon.ndjson
|
|
708
|
-
*
|
|
786
|
+
* Reads the machine-local daemon.ndjson when available, plus any
|
|
787
|
+
* bundle-local copies left by older runtimes. The newest parseable event
|
|
788
|
+
* is the liveness signal; stale bundle logs cannot mask a live daemon.
|
|
709
789
|
*/
|
|
710
790
|
function checkLifecycle(deps) {
|
|
711
791
|
const checks = [];
|
|
712
792
|
const HOUR_MS = 60 * 60 * 1000;
|
|
713
793
|
const STALE_THRESHOLD_MS = 5 * 60 * 1000;
|
|
714
|
-
const
|
|
794
|
+
const now = Date.now();
|
|
795
|
+
const cutoff = now - HOUR_MS;
|
|
796
|
+
const logPaths = [];
|
|
797
|
+
const seenLogPaths = new Set();
|
|
798
|
+
const addLogPath = (candidate) => {
|
|
799
|
+
if (seenLogPaths.has(candidate) || !deps.existsSync(candidate))
|
|
800
|
+
return;
|
|
801
|
+
seenLogPaths.add(candidate);
|
|
802
|
+
logPaths.push(candidate);
|
|
803
|
+
};
|
|
804
|
+
if (deps.daemonLogsDir) {
|
|
805
|
+
addLogPath(`${deps.daemonLogsDir}/daemon.ndjson`);
|
|
806
|
+
}
|
|
715
807
|
const agents = discoverAgents(deps);
|
|
716
|
-
let logPath = null;
|
|
717
808
|
for (const agentDir of agents) {
|
|
718
|
-
|
|
719
|
-
if (deps.existsSync(candidate)) {
|
|
720
|
-
logPath = candidate;
|
|
721
|
-
break;
|
|
722
|
-
}
|
|
809
|
+
addLogPath(`${deps.bundlesRoot}/${agentDir}/state/daemon/logs/daemon.ndjson`);
|
|
723
810
|
}
|
|
724
|
-
if (
|
|
725
|
-
checks.push({ label: "daemon log readable", status: "warn", detail: "no daemon.ndjson found in
|
|
811
|
+
if (logPaths.length === 0) {
|
|
812
|
+
checks.push({ label: "daemon log readable", status: "warn", detail: "no daemon.ndjson found in daemon or agent bundle logs" });
|
|
726
813
|
return { name: "Lifecycle", checks };
|
|
727
814
|
}
|
|
728
815
|
let lastTs = null;
|
|
729
816
|
let lastEvent = null;
|
|
817
|
+
let lastEventTimeMs = Number.NEGATIVE_INFINITY;
|
|
730
818
|
let startCount = 0;
|
|
731
819
|
let installCount = 0;
|
|
732
820
|
let installVersions = [];
|
|
733
821
|
let processErrors = [];
|
|
734
822
|
let lastEntryAgeMs = Number.POSITIVE_INFINITY;
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
823
|
+
let readCount = 0;
|
|
824
|
+
const readErrors = [];
|
|
825
|
+
for (const logPath of logPaths) {
|
|
826
|
+
try {
|
|
827
|
+
// Read the whole log via deps.readFileSync, then take the tail. For a
|
|
828
|
+
// chatty daemon this can be a few MB; we only inspect the last 5000
|
|
829
|
+
// lines which is enough for the last hour of activity. If the file is
|
|
830
|
+
// small (typical case), reading it all is cheap.
|
|
831
|
+
const raw = deps.readFileSync(logPath);
|
|
832
|
+
readCount++;
|
|
833
|
+
const allLines = raw.split("\n").filter((l) => l.trim());
|
|
834
|
+
const usable = allLines.length > 5000 ? allLines.slice(-5000) : allLines;
|
|
835
|
+
for (const line of usable) {
|
|
836
|
+
let parsed;
|
|
837
|
+
try {
|
|
838
|
+
parsed = JSON.parse(line);
|
|
839
|
+
}
|
|
840
|
+
catch {
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
const ts = typeof parsed.ts === "string" ? parsed.ts : null;
|
|
844
|
+
const event = typeof parsed.event === "string" ? parsed.event : null;
|
|
845
|
+
if (!ts || !event)
|
|
846
|
+
continue;
|
|
847
|
+
const tsMs = Date.parse(ts);
|
|
848
|
+
if (Number.isNaN(tsMs))
|
|
849
|
+
continue;
|
|
850
|
+
if (tsMs > lastEventTimeMs) {
|
|
851
|
+
lastEventTimeMs = tsMs;
|
|
852
|
+
lastTs = ts;
|
|
853
|
+
lastEvent = event;
|
|
854
|
+
lastEntryAgeMs = Math.max(0, now - tsMs);
|
|
855
|
+
}
|
|
856
|
+
if (tsMs < cutoff)
|
|
857
|
+
continue;
|
|
858
|
+
if (event === "daemon.daemon_started")
|
|
859
|
+
startCount++;
|
|
860
|
+
if (event === "daemon.cli_version_install_end") {
|
|
861
|
+
installCount++;
|
|
862
|
+
const meta = parsed.meta;
|
|
863
|
+
const ver = typeof meta?.version === "string" ? meta.version : null;
|
|
864
|
+
if (ver)
|
|
865
|
+
installVersions.push(ver);
|
|
866
|
+
}
|
|
867
|
+
if (event === "daemon.agent_process_error") {
|
|
868
|
+
const meta = parsed.meta;
|
|
869
|
+
const reason = typeof meta?.reason === "string" ? meta.reason : "unknown";
|
|
870
|
+
const agent = typeof meta?.agent === "string" ? meta.agent : "unknown";
|
|
871
|
+
processErrors.push(`${agent}: ${reason}`);
|
|
872
|
+
}
|
|
777
873
|
}
|
|
778
874
|
}
|
|
875
|
+
catch (error) {
|
|
876
|
+
readErrors.push(`${logPath}: ${error instanceof Error ? error.message : /* v8 ignore next -- non-Error throw is unreachable from deps.readFileSync (always Error) @preserve */ String(error)}`);
|
|
877
|
+
}
|
|
779
878
|
}
|
|
780
|
-
|
|
781
|
-
checks.push({ label: "daemon log readable", status: "fail", detail: `read failed: ${
|
|
879
|
+
if (readCount === 0) {
|
|
880
|
+
checks.push({ label: "daemon log readable", status: "fail", detail: `read failed: ${readErrors.join("; ")}` });
|
|
782
881
|
return { name: "Lifecycle", checks };
|
|
783
882
|
}
|
|
883
|
+
if (readErrors.length > 0) {
|
|
884
|
+
checks.push({ label: "daemon log readable", status: "warn", detail: `some daemon logs were unreadable: ${readErrors.join("; ")}` });
|
|
885
|
+
}
|
|
784
886
|
if (lastTs === null) {
|
|
785
887
|
checks.push({ label: "recent daemon activity", status: "warn", detail: "no parseable events in tail of daemon.ndjson" });
|
|
786
888
|
}
|
|
@@ -49,6 +49,7 @@ const DEFAULT_TEAMS_PORT = 3978;
|
|
|
49
49
|
const DEFAULT_BLUEBUBBLES_PORT = 18790;
|
|
50
50
|
const DEFAULT_BLUEBUBBLES_WEBHOOK_PATH = "/bluebubbles-webhook";
|
|
51
51
|
const BLUEBUBBLES_RUNTIME_FRESHNESS_WINDOW_MS = 90_000;
|
|
52
|
+
const SENSE_CONFIG_RETRY_DELAYS_MS = [1_000, 2_000, 5_000, 10_000, 30_000, 60_000];
|
|
52
53
|
function defaultSenses() {
|
|
53
54
|
return {
|
|
54
55
|
cli: { ...identity_1.DEFAULT_AGENT_SENSES.cli },
|
|
@@ -519,6 +520,8 @@ class DaemonSenseManager {
|
|
|
519
520
|
processManager;
|
|
520
521
|
contexts;
|
|
521
522
|
pendingConfigRefreshes = new Set();
|
|
523
|
+
configRefreshRetryTimers = new Map();
|
|
524
|
+
configRefreshRetryAttempts = new Map();
|
|
522
525
|
bundlesRoot;
|
|
523
526
|
constructor(options) {
|
|
524
527
|
const bundlesRoot = options.bundlesRoot ?? path.join(os.homedir(), "AgentBundles");
|
|
@@ -580,13 +583,55 @@ class DaemonSenseManager {
|
|
|
580
583
|
},
|
|
581
584
|
});
|
|
582
585
|
}
|
|
583
|
-
scheduleSenseConfigRefresh(name, parsed) {
|
|
584
|
-
if (this.pendingConfigRefreshes.has(name))
|
|
586
|
+
scheduleSenseConfigRefresh(name, parsed, delayMs = 0) {
|
|
587
|
+
if (this.pendingConfigRefreshes.has(name) || this.configRefreshRetryTimers.has(name))
|
|
585
588
|
return;
|
|
589
|
+
if (delayMs > 0) {
|
|
590
|
+
const timer = setTimeout(() => {
|
|
591
|
+
this.configRefreshRetryTimers.delete(name);
|
|
592
|
+
this.pendingConfigRefreshes.add(name);
|
|
593
|
+
void this.refreshSenseConfigAndRetry(name, parsed);
|
|
594
|
+
}, delayMs);
|
|
595
|
+
this.configRefreshRetryTimers.set(name, timer);
|
|
596
|
+
(0, runtime_1.emitNervesEvent)({
|
|
597
|
+
level: "warn",
|
|
598
|
+
component: "channels",
|
|
599
|
+
event: "channel.daemon_sense_config_retry_scheduled",
|
|
600
|
+
message: "scheduled managed sense config retry",
|
|
601
|
+
meta: {
|
|
602
|
+
agent: parsed.agent,
|
|
603
|
+
sense: parsed.sense,
|
|
604
|
+
managedName: name,
|
|
605
|
+
delayMs,
|
|
606
|
+
attempt: this.configRefreshRetryAttempts.get(name),
|
|
607
|
+
},
|
|
608
|
+
});
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
586
611
|
this.pendingConfigRefreshes.add(name);
|
|
587
612
|
void this.refreshSenseConfigAndRetry(name, parsed);
|
|
588
613
|
}
|
|
614
|
+
nextConfigRetryDelayMs(name) {
|
|
615
|
+
const attempt = (this.configRefreshRetryAttempts.get(name) ?? 0) + 1;
|
|
616
|
+
this.configRefreshRetryAttempts.set(name, attempt);
|
|
617
|
+
return SENSE_CONFIG_RETRY_DELAYS_MS[Math.min(attempt - 1, SENSE_CONFIG_RETRY_DELAYS_MS.length - 1)];
|
|
618
|
+
}
|
|
619
|
+
clearConfigRetryState(name) {
|
|
620
|
+
clearTimeout(this.configRefreshRetryTimers.get(name));
|
|
621
|
+
this.configRefreshRetryTimers.delete(name);
|
|
622
|
+
this.configRefreshRetryAttempts.delete(name);
|
|
623
|
+
}
|
|
624
|
+
shouldRetryConfigRefresh(sense, runtimeConfig, machineRuntimeConfig) {
|
|
625
|
+
if (!runtimeConfig.ok && runtimeConfig.reason === "unavailable")
|
|
626
|
+
return true;
|
|
627
|
+
if ((sense === "bluebubbles" || sense === "voice" || sense === "a2a") &&
|
|
628
|
+
!machineRuntimeConfig.ok &&
|
|
629
|
+
machineRuntimeConfig.reason === "unavailable")
|
|
630
|
+
return true;
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
589
633
|
async refreshSenseConfigAndRetry(name, parsed) {
|
|
634
|
+
let retryAfterMs = null;
|
|
590
635
|
try {
|
|
591
636
|
const refreshed = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(parsed.agent, { preserveCachedOnFailure: true });
|
|
592
637
|
const machineRefreshed = parsed.sense === "bluebubbles" || parsed.sense === "voice" || parsed.sense === "a2a"
|
|
@@ -597,8 +642,16 @@ class DaemonSenseManager {
|
|
|
597
642
|
if (!context)
|
|
598
643
|
return;
|
|
599
644
|
context.facts = senseFactsFromRuntimeConfig(parsed.agent, context.senses, refreshed, machineRefreshed);
|
|
600
|
-
if (!context.facts[parsed.sense].configured)
|
|
645
|
+
if (!context.facts[parsed.sense].configured) {
|
|
646
|
+
if (this.shouldRetryConfigRefresh(parsed.sense, refreshed, machineRefreshed)) {
|
|
647
|
+
retryAfterMs = this.nextConfigRetryDelayMs(name);
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
this.clearConfigRetryState(name);
|
|
651
|
+
}
|
|
601
652
|
return;
|
|
653
|
+
}
|
|
654
|
+
this.clearConfigRetryState(name);
|
|
602
655
|
setTimeout(() => {
|
|
603
656
|
void this.processManager.startAgent?.(name).catch((error) => {
|
|
604
657
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -624,6 +677,9 @@ class DaemonSenseManager {
|
|
|
624
677
|
}
|
|
625
678
|
finally {
|
|
626
679
|
this.pendingConfigRefreshes.delete(name);
|
|
680
|
+
if (retryAfterMs !== null) {
|
|
681
|
+
this.scheduleSenseConfigRefresh(name, parsed, retryAfterMs);
|
|
682
|
+
}
|
|
627
683
|
}
|
|
628
684
|
}
|
|
629
685
|
async refreshEnabledSenseConfigs() {
|
|
@@ -676,6 +732,11 @@ class DaemonSenseManager {
|
|
|
676
732
|
});
|
|
677
733
|
}
|
|
678
734
|
async stopAll() {
|
|
735
|
+
for (const timer of this.configRefreshRetryTimers.values()) {
|
|
736
|
+
clearTimeout(timer);
|
|
737
|
+
}
|
|
738
|
+
this.configRefreshRetryTimers.clear();
|
|
739
|
+
this.configRefreshRetryAttempts.clear();
|
|
679
740
|
await this.processManager.stopAll();
|
|
680
741
|
}
|
|
681
742
|
/* v8 ignore start -- pid collection for orphan cleanup pidfile @preserve */
|
package/dist/nerves/index.js
CHANGED
|
@@ -303,7 +303,9 @@ function createNdjsonFileSink(filePath, optionsOrMaxSize) {
|
|
|
303
303
|
return;
|
|
304
304
|
flushing = true;
|
|
305
305
|
const line = queue.shift();
|
|
306
|
-
|
|
306
|
+
const checkBeforeAppend = bytesSinceCheck >= rotationCheckInterval;
|
|
307
|
+
const checkAfterAppend = checkBeforeAppend || line.length >= rotationCheckInterval;
|
|
308
|
+
if (checkBeforeAppend) {
|
|
307
309
|
bytesSinceCheck = 0;
|
|
308
310
|
try {
|
|
309
311
|
rotateIfNeeded(filePath, options);
|
|
@@ -313,6 +315,14 @@ function createNdjsonFileSink(filePath, optionsOrMaxSize) {
|
|
|
313
315
|
}
|
|
314
316
|
}
|
|
315
317
|
(0, fs_1.appendFile)(filePath, line, "utf8", () => {
|
|
318
|
+
if (checkAfterAppend) {
|
|
319
|
+
try {
|
|
320
|
+
rotateIfNeeded(filePath, options);
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
// Rotation errors are surfaced via nerves events; never block writes.
|
|
324
|
+
}
|
|
325
|
+
}
|
|
316
326
|
flushing = false;
|
|
317
327
|
flush();
|
|
318
328
|
});
|