@analyticscli/growth-engineer 0.1.0-preview.13 → 0.1.0-preview.14
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/dist/config.d.ts +161 -34
- package/dist/config.js +21 -3
- package/dist/config.js.map +1 -1
- package/dist/runtime/export-asc-summary.mjs +294 -3
- package/dist/runtime/export-asc-summary.mjs.map +1 -1
- package/dist/runtime/openclaw-exporters-lib.d.mts +1 -0
- package/dist/runtime/openclaw-exporters-lib.mjs +8 -6
- package/dist/runtime/openclaw-exporters-lib.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-preflight.mjs +7 -2
- package/dist/runtime/openclaw-growth-preflight.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-runner.mjs +197 -70
- package/dist/runtime/openclaw-growth-runner.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-shared.d.mts +6 -0
- package/dist/runtime/openclaw-growth-shared.mjs +54 -0
- package/dist/runtime/openclaw-growth-shared.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-start.mjs +169 -4
- package/dist/runtime/openclaw-growth-start.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-status.mjs +3 -25
- package/dist/runtime/openclaw-growth-status.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-wizard.mjs +157 -123
- package/dist/runtime/openclaw-growth-wizard.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/config.example.json +14 -0
|
@@ -5,14 +5,15 @@ import process from 'node:process';
|
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import { getActionMode, getAllSourceEntries, getGitHubArtifactModes, getGitHubRequirementText, shouldAutoCreateGitHubArtifact, } from './openclaw-growth-shared.mjs';
|
|
8
|
+
import { deriveRuntimeDirFromStatePath, deriveSchedulerProofPathFromStatePath, getActionMode, getAllSourceEntries, getGitHubArtifactModes, getGitHubRequirementText, shouldAutoCreateGitHubArtifact, } from './openclaw-growth-shared.mjs';
|
|
9
9
|
import { applyOpenClawSecretRefs, loadOpenClawGrowthSecrets } from './openclaw-growth-env.mjs';
|
|
10
10
|
const DEFAULT_CONFIG_PATH = 'data/openclaw-growth-engineer/config.json';
|
|
11
11
|
const DEFAULT_STATE_PATH = 'data/openclaw-growth-engineer/state.json';
|
|
12
|
-
const
|
|
12
|
+
const DEFAULT_SCHEDULER_PROOF_PATH = 'data/openclaw-growth-engineer/runtime/scheduler-proof.jsonl';
|
|
13
13
|
const DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES = 360;
|
|
14
14
|
const SELF_UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
15
15
|
const RUNTIME_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
let schedulerProofPath = path.resolve(DEFAULT_SCHEDULER_PROOF_PATH);
|
|
16
17
|
const DEFAULT_CADENCES = [
|
|
17
18
|
{
|
|
18
19
|
key: 'daily',
|
|
@@ -154,9 +155,14 @@ function commandShouldReceiveActiveConfig(command) {
|
|
|
154
155
|
}
|
|
155
156
|
function withActiveConfigArg(command, configPath) {
|
|
156
157
|
const trimmed = String(command || '').trim();
|
|
157
|
-
if (!trimmed || !configPath ||
|
|
158
|
+
if (!trimmed || !configPath || !commandShouldReceiveActiveConfig(trimmed)) {
|
|
158
159
|
return trimmed;
|
|
159
160
|
}
|
|
161
|
+
if (commandHasConfigArg(trimmed)) {
|
|
162
|
+
return trimmed
|
|
163
|
+
.replace(/(^|\s)--config=(?:"[^"]*"|'[^']*'|\S+)/, `$1--config ${quote(configPath)}`)
|
|
164
|
+
.replace(/(^|\s)--config\s+(?:"[^"]*"|'[^']*'|\S+)/, `$1--config ${quote(configPath)}`);
|
|
165
|
+
}
|
|
160
166
|
return `${trimmed} --config ${quote(configPath)}`;
|
|
161
167
|
}
|
|
162
168
|
async function readJson(filePath) {
|
|
@@ -174,6 +180,22 @@ async function readJsonOptional(filePath, fallback) {
|
|
|
174
180
|
async function ensureDir(dirPath) {
|
|
175
181
|
await fs.mkdir(dirPath, { recursive: true });
|
|
176
182
|
}
|
|
183
|
+
async function appendSchedulerProof(event, details = {}) {
|
|
184
|
+
const proofPath = schedulerProofPath;
|
|
185
|
+
const entry = {
|
|
186
|
+
ts: new Date().toISOString(),
|
|
187
|
+
event,
|
|
188
|
+
pid: process.pid,
|
|
189
|
+
cwd: process.cwd(),
|
|
190
|
+
...details,
|
|
191
|
+
};
|
|
192
|
+
await fs.mkdir(path.dirname(proofPath), { recursive: true });
|
|
193
|
+
await fs.appendFile(proofPath, `${JSON.stringify(entry)}\n`, 'utf8');
|
|
194
|
+
}
|
|
195
|
+
function useSchedulerProofPathForStatePath(statePath) {
|
|
196
|
+
schedulerProofPath = path.resolve(deriveSchedulerProofPathFromStatePath(statePath));
|
|
197
|
+
return schedulerProofPath;
|
|
198
|
+
}
|
|
177
199
|
function sha256(input) {
|
|
178
200
|
return createHash('sha256').update(input).digest('hex');
|
|
179
201
|
}
|
|
@@ -542,7 +564,7 @@ function buildConnectorHealthAlert(statusPayload, unhealthyConnectors) {
|
|
|
542
564
|
lines.push(` Next: ${entry.nextAction}`);
|
|
543
565
|
}
|
|
544
566
|
if (entry.key === 'appStoreConnect' && entry.status === 'partial') {
|
|
545
|
-
lines.push(' Note: ASC
|
|
567
|
+
lines.push(' Note: ASC uses API-key batch reports by default. Experimental ASC web analytics should only be requested when a needed metric is unavailable through API reports.');
|
|
546
568
|
}
|
|
547
569
|
}
|
|
548
570
|
lines.push('');
|
|
@@ -563,20 +585,43 @@ async function writeConnectorHealthAlert(runtimeDir, message, statusPayload, unh
|
|
|
563
585
|
}, null, 2), 'utf8');
|
|
564
586
|
return { markdownPath, jsonPath };
|
|
565
587
|
}
|
|
566
|
-
function
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
if (
|
|
571
|
-
return
|
|
588
|
+
function notificationChannelKey(channel) {
|
|
589
|
+
const type = String(channel?.type || 'openclaw-chat');
|
|
590
|
+
if (type === 'openclaw-chat')
|
|
591
|
+
return 'openclaw-chat';
|
|
592
|
+
if (type === 'slack')
|
|
593
|
+
return `slack:${channel?.label || channel?.webhookEnv || 'slack'}`;
|
|
594
|
+
if (type === 'webhook')
|
|
595
|
+
return `webhook:${channel?.label || channel?.urlEnv || channel?.webhookEnv || 'webhook'}`;
|
|
596
|
+
if (type === 'command')
|
|
597
|
+
return `command:${channel?.label || channel?.command || 'command'}`;
|
|
598
|
+
return `${type}:${channel?.label || type}`;
|
|
599
|
+
}
|
|
600
|
+
function mergeNotificationChannelsWithDeliveries(configuredChannels, deliveryChannels) {
|
|
601
|
+
const configured = Array.isArray(configuredChannels) ? configuredChannels : [];
|
|
602
|
+
const seen = new Set(configured.map((channel) => notificationChannelKey(channel)));
|
|
603
|
+
const channels = configured.filter((channel) => channel?.enabled !== false);
|
|
604
|
+
for (const channel of deliveryChannels) {
|
|
605
|
+
if (!seen.has(notificationChannelKey(channel))) {
|
|
606
|
+
channels.push(channel);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return channels;
|
|
610
|
+
}
|
|
611
|
+
function getDeliveryNotificationChannels(config, kind) {
|
|
572
612
|
const channels = [];
|
|
573
613
|
const deliveries = config?.deliveries || {};
|
|
574
614
|
if (deliveries.openclawChat?.enabled) {
|
|
615
|
+
const isConnectorHealth = kind === 'connectorHealth';
|
|
575
616
|
channels.push({
|
|
576
617
|
type: 'openclaw-chat',
|
|
577
618
|
label: 'openclaw_chat',
|
|
578
|
-
markdownPath:
|
|
579
|
-
|
|
619
|
+
markdownPath: isConnectorHealth
|
|
620
|
+
? deliveries.openclawChat.connectorHealthMarkdownPath || deliveries.openclawChat.markdownPath
|
|
621
|
+
: deliveries.openclawChat.growthRunMarkdownPath || '.openclaw/chat/growth-summary.md',
|
|
622
|
+
jsonPath: isConnectorHealth
|
|
623
|
+
? deliveries.openclawChat.connectorHealthJsonPath || deliveries.openclawChat.jsonPath
|
|
624
|
+
: deliveries.openclawChat.growthRunJsonPath || '.openclaw/chat/growth-summary.json',
|
|
580
625
|
});
|
|
581
626
|
}
|
|
582
627
|
if (deliveries.slack?.enabled) {
|
|
@@ -595,19 +640,37 @@ function getConnectorHealthChannels(config) {
|
|
|
595
640
|
headers: deliveries.webhook.headers || {},
|
|
596
641
|
});
|
|
597
642
|
}
|
|
643
|
+
if (deliveries.command?.enabled) {
|
|
644
|
+
channels.push({
|
|
645
|
+
type: 'command',
|
|
646
|
+
label: deliveries.command.label || 'command',
|
|
647
|
+
command: deliveries.command.command || '',
|
|
648
|
+
});
|
|
649
|
+
}
|
|
598
650
|
if (deliveries.discord?.enabled) {
|
|
599
651
|
channels.push({
|
|
600
652
|
type: 'command',
|
|
601
|
-
label: 'discord',
|
|
602
|
-
command: deliveries.discord.command || '
|
|
653
|
+
label: deliveries.discord.label || 'discord',
|
|
654
|
+
command: deliveries.discord.command || '',
|
|
603
655
|
});
|
|
604
656
|
}
|
|
605
657
|
return channels;
|
|
606
658
|
}
|
|
659
|
+
function getConnectorHealthChannels(config) {
|
|
660
|
+
const configuredChannels = Array.isArray(config?.notifications?.connectorHealth?.channels)
|
|
661
|
+
? config.notifications.connectorHealth.channels
|
|
662
|
+
: [];
|
|
663
|
+
return mergeNotificationChannelsWithDeliveries(configuredChannels, getDeliveryNotificationChannels(config, 'connectorHealth'));
|
|
664
|
+
}
|
|
665
|
+
function resolveOpenClawChatDeliveryPath(channelPath, fallbackPath) {
|
|
666
|
+
const targetPath = String(channelPath || fallbackPath || '').trim();
|
|
667
|
+
if (!targetPath)
|
|
668
|
+
return path.resolve(process.cwd(), fallbackPath);
|
|
669
|
+
return path.isAbsolute(targetPath) ? targetPath : path.resolve(process.cwd(), targetPath);
|
|
670
|
+
}
|
|
607
671
|
async function writeConfiguredOpenClawChatAlert(configPath, channel, message, statusPayload, unhealthyConnectors, fingerprint) {
|
|
608
|
-
const
|
|
609
|
-
const
|
|
610
|
-
const jsonPath = path.resolve(baseDir, channel.jsonPath || '.openclaw/chat/connector-health.json');
|
|
672
|
+
const markdownPath = resolveOpenClawChatDeliveryPath(channel.markdownPath, '.openclaw/chat/connector-health.md');
|
|
673
|
+
const jsonPath = resolveOpenClawChatDeliveryPath(channel.jsonPath, '.openclaw/chat/connector-health.json');
|
|
611
674
|
await fs.mkdir(path.dirname(markdownPath), { recursive: true });
|
|
612
675
|
await fs.mkdir(path.dirname(jsonPath), { recursive: true });
|
|
613
676
|
await fs.writeFile(markdownPath, message, 'utf8');
|
|
@@ -620,8 +683,9 @@ async function writeConfiguredOpenClawChatAlert(configPath, channel, message, st
|
|
|
620
683
|
}, null, 2), 'utf8');
|
|
621
684
|
return {
|
|
622
685
|
sent: true,
|
|
686
|
+
external: false,
|
|
623
687
|
target: channel.label || 'openclaw_chat',
|
|
624
|
-
detail: `wrote ${markdownPath} and ${jsonPath}`,
|
|
688
|
+
detail: `wrote local OpenClaw chat outbox ${markdownPath} and ${jsonPath}`,
|
|
625
689
|
};
|
|
626
690
|
}
|
|
627
691
|
async function sendSlackConnectorHealthAlert(channel, message) {
|
|
@@ -637,6 +701,7 @@ async function sendSlackConnectorHealthAlert(channel, message) {
|
|
|
637
701
|
});
|
|
638
702
|
return {
|
|
639
703
|
sent: response.ok,
|
|
704
|
+
external: true,
|
|
640
705
|
target: channel.label || 'slack',
|
|
641
706
|
detail: response.ok ? `HTTP ${response.status}` : `HTTP ${response.status}: ${await response.text()}`,
|
|
642
707
|
};
|
|
@@ -664,6 +729,7 @@ async function sendWebhookConnectorHealthAlert(channel, message, statusPayload,
|
|
|
664
729
|
});
|
|
665
730
|
return {
|
|
666
731
|
sent: response.ok,
|
|
732
|
+
external: true,
|
|
667
733
|
target: channel.label || 'webhook',
|
|
668
734
|
detail: response.ok ? `HTTP ${response.status}` : `HTTP ${response.status}: ${await response.text()}`,
|
|
669
735
|
};
|
|
@@ -675,10 +741,17 @@ async function sendCommandConnectorHealthAlert(channel, message) {
|
|
|
675
741
|
const result = await runShellCommand(String(channel.command), 60_000, { input: message });
|
|
676
742
|
return {
|
|
677
743
|
sent: result.ok,
|
|
744
|
+
external: true,
|
|
678
745
|
target: channel.label || 'command',
|
|
679
746
|
detail: result.ok ? result.stdout.trim() : result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`,
|
|
680
747
|
};
|
|
681
748
|
}
|
|
749
|
+
function hasExternalNotificationChannel(channels) {
|
|
750
|
+
return channels.some((channel) => channel?.type && channel.type !== 'openclaw-chat');
|
|
751
|
+
}
|
|
752
|
+
function hasSuccessfulExternalDelivery(results) {
|
|
753
|
+
return results.some((result) => result?.sent === true && result?.external === true);
|
|
754
|
+
}
|
|
682
755
|
async function deliverConnectorHealthAlert({ config, configPath, message, statusPayload, unhealthyConnectors, fingerprint }) {
|
|
683
756
|
const channels = getConnectorHealthChannels(config);
|
|
684
757
|
if (config?.notifications?.connectorHealth?.enabled === false) {
|
|
@@ -714,48 +787,23 @@ async function deliverConnectorHealthAlert({ config, configPath, message, status
|
|
|
714
787
|
});
|
|
715
788
|
}
|
|
716
789
|
}
|
|
790
|
+
if (!hasSuccessfulExternalDelivery(results)) {
|
|
791
|
+
results.push({
|
|
792
|
+
sent: false,
|
|
793
|
+
external: true,
|
|
794
|
+
target: 'external_notification',
|
|
795
|
+
detail: hasExternalNotificationChannel(channels)
|
|
796
|
+
? 'No external notification channel successfully sent the alert.'
|
|
797
|
+
: 'Alert written locally, but no external notification channel configured.',
|
|
798
|
+
});
|
|
799
|
+
}
|
|
717
800
|
return results;
|
|
718
801
|
}
|
|
719
802
|
function getGrowthRunChannels(config) {
|
|
720
803
|
const configuredChannels = Array.isArray(config?.notifications?.growthRun?.channels)
|
|
721
|
-
? config.notifications.growthRun.channels
|
|
804
|
+
? config.notifications.growthRun.channels
|
|
722
805
|
: [];
|
|
723
|
-
|
|
724
|
-
return configuredChannels;
|
|
725
|
-
const channels = [];
|
|
726
|
-
const deliveries = config?.deliveries || {};
|
|
727
|
-
if (deliveries.openclawChat?.enabled) {
|
|
728
|
-
channels.push({
|
|
729
|
-
type: 'openclaw-chat',
|
|
730
|
-
label: 'openclaw_chat',
|
|
731
|
-
markdownPath: deliveries.openclawChat.growthRunMarkdownPath || '.openclaw/chat/growth-summary.md',
|
|
732
|
-
jsonPath: deliveries.openclawChat.growthRunJsonPath || '.openclaw/chat/growth-summary.json',
|
|
733
|
-
});
|
|
734
|
-
}
|
|
735
|
-
if (deliveries.slack?.enabled) {
|
|
736
|
-
channels.push({
|
|
737
|
-
type: 'slack',
|
|
738
|
-
label: 'slack',
|
|
739
|
-
webhookEnv: deliveries.slack.webhookEnv || 'SLACK_WEBHOOK_URL',
|
|
740
|
-
});
|
|
741
|
-
}
|
|
742
|
-
if (deliveries.webhook?.enabled) {
|
|
743
|
-
channels.push({
|
|
744
|
-
type: 'webhook',
|
|
745
|
-
label: 'webhook',
|
|
746
|
-
urlEnv: deliveries.webhook.urlEnv || 'OPENCLAW_WEBHOOK_URL',
|
|
747
|
-
method: deliveries.webhook.method || 'POST',
|
|
748
|
-
headers: deliveries.webhook.headers || {},
|
|
749
|
-
});
|
|
750
|
-
}
|
|
751
|
-
if (deliveries.discord?.enabled) {
|
|
752
|
-
channels.push({
|
|
753
|
-
type: 'command',
|
|
754
|
-
label: 'discord',
|
|
755
|
-
command: deliveries.discord.command || 'node scripts/discord-openclaw-bridge.mjs send --stdin',
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
|
-
return channels;
|
|
806
|
+
return mergeNotificationChannelsWithDeliveries(configuredChannels, getDeliveryNotificationChannels(config, 'growthRun'));
|
|
759
807
|
}
|
|
760
808
|
async function readChartAttachments(chartManifestPath) {
|
|
761
809
|
if (!chartManifestPath)
|
|
@@ -813,9 +861,8 @@ function buildGrowthRunSummaryMessage({ issuesPayload, activeCadences, sourceFil
|
|
|
813
861
|
return `${lines.join('\n')}\n`;
|
|
814
862
|
}
|
|
815
863
|
async function writeConfiguredOpenClawChatGrowthSummary(configPath, channel, message, issuesPayload, activeCadences, fingerprint, charts) {
|
|
816
|
-
const
|
|
817
|
-
const
|
|
818
|
-
const jsonPath = path.resolve(baseDir, channel.jsonPath || '.openclaw/chat/growth-summary.json');
|
|
864
|
+
const markdownPath = resolveOpenClawChatDeliveryPath(channel.markdownPath, '.openclaw/chat/growth-summary.md');
|
|
865
|
+
const jsonPath = resolveOpenClawChatDeliveryPath(channel.jsonPath, '.openclaw/chat/growth-summary.json');
|
|
819
866
|
await fs.mkdir(path.dirname(markdownPath), { recursive: true });
|
|
820
867
|
await fs.mkdir(path.dirname(jsonPath), { recursive: true });
|
|
821
868
|
await fs.writeFile(markdownPath, message, 'utf8');
|
|
@@ -835,8 +882,9 @@ async function writeConfiguredOpenClawChatGrowthSummary(configPath, channel, mes
|
|
|
835
882
|
}, null, 2), 'utf8');
|
|
836
883
|
return {
|
|
837
884
|
sent: true,
|
|
885
|
+
external: false,
|
|
838
886
|
target: channel.label || 'openclaw_chat',
|
|
839
|
-
detail: `wrote ${markdownPath} and ${jsonPath}`,
|
|
887
|
+
detail: `wrote local OpenClaw chat outbox ${markdownPath} and ${jsonPath}`,
|
|
840
888
|
};
|
|
841
889
|
}
|
|
842
890
|
async function sendSlackGrowthSummary(channel, message) {
|
|
@@ -852,6 +900,7 @@ async function sendSlackGrowthSummary(channel, message) {
|
|
|
852
900
|
});
|
|
853
901
|
return {
|
|
854
902
|
sent: response.ok,
|
|
903
|
+
external: true,
|
|
855
904
|
target: channel.label || 'slack',
|
|
856
905
|
detail: response.ok ? `HTTP ${response.status}` : `HTTP ${response.status}: ${await response.text()}`,
|
|
857
906
|
};
|
|
@@ -886,6 +935,7 @@ async function sendWebhookGrowthSummary(channel, message, issuesPayload, activeC
|
|
|
886
935
|
});
|
|
887
936
|
return {
|
|
888
937
|
sent: response.ok,
|
|
938
|
+
external: true,
|
|
889
939
|
target: channel.label || 'webhook',
|
|
890
940
|
detail: response.ok ? `HTTP ${response.status}` : `HTTP ${response.status}: ${await response.text()}`,
|
|
891
941
|
};
|
|
@@ -897,6 +947,7 @@ async function sendCommandGrowthSummary(channel, message) {
|
|
|
897
947
|
const result = await runShellCommand(String(channel.command), 60_000, { input: message });
|
|
898
948
|
return {
|
|
899
949
|
sent: result.ok,
|
|
950
|
+
external: true,
|
|
900
951
|
target: channel.label || 'command',
|
|
901
952
|
detail: result.ok ? result.stdout.trim() : result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`,
|
|
902
953
|
};
|
|
@@ -950,6 +1001,12 @@ async function maybeRunConnectorHealthCheck({ config, configPath, state, statePa
|
|
|
950
1001
|
const healthState = state?.connectorHealth || {};
|
|
951
1002
|
const intervalMinutes = getConnectorHealthIntervalMinutes(config);
|
|
952
1003
|
if (!isDue(healthState.lastCheckedAt, intervalMinutes)) {
|
|
1004
|
+
await appendSchedulerProof('connector_health_not_due', {
|
|
1005
|
+
configPath,
|
|
1006
|
+
statePath,
|
|
1007
|
+
intervalMinutes,
|
|
1008
|
+
lastCheckedAt: healthState.lastCheckedAt || null,
|
|
1009
|
+
});
|
|
953
1010
|
return state;
|
|
954
1011
|
}
|
|
955
1012
|
await ensureDir(runtimeDir);
|
|
@@ -975,6 +1032,13 @@ async function maybeRunConnectorHealthCheck({ config, configPath, state, statePa
|
|
|
975
1032
|
};
|
|
976
1033
|
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
|
977
1034
|
await fs.writeFile(statePath, JSON.stringify(nextState, null, 2), 'utf8');
|
|
1035
|
+
await appendSchedulerProof('connector_health_check_failed', {
|
|
1036
|
+
configPath,
|
|
1037
|
+
statePath,
|
|
1038
|
+
intervalMinutes,
|
|
1039
|
+
checkedAt,
|
|
1040
|
+
error: nextState.connectorHealth.lastError,
|
|
1041
|
+
});
|
|
978
1042
|
return nextState;
|
|
979
1043
|
}
|
|
980
1044
|
const unhealthyConnectors = getUnhealthyConfiguredConnectors(statusPayload);
|
|
@@ -988,11 +1052,10 @@ async function maybeRunConnectorHealthCheck({ config, configPath, state, statePa
|
|
|
988
1052
|
connectedConnectors,
|
|
989
1053
|
lastError: null,
|
|
990
1054
|
};
|
|
991
|
-
const
|
|
992
|
-
? healthState.activeIncidentFingerprint || healthState.lastAlertedFingerprint || null
|
|
993
|
-
: null;
|
|
1055
|
+
const previousExternallyDeliveredFingerprint = healthState.lastExternalAlertedFingerprint || null;
|
|
994
1056
|
if (unhealthyConnectors.length === 0) {
|
|
995
1057
|
nextHealthState.activeIncidentFingerprint = null;
|
|
1058
|
+
nextHealthState.lastExternalAlertedFingerprint = null;
|
|
996
1059
|
if (healthState.lastStatusOk === false) {
|
|
997
1060
|
nextHealthState.lastRecoveredAt = checkedAt;
|
|
998
1061
|
}
|
|
@@ -1001,7 +1064,7 @@ async function maybeRunConnectorHealthCheck({ config, configPath, state, statePa
|
|
|
1001
1064
|
nextHealthState.activeIncidentFingerprint = fingerprint;
|
|
1002
1065
|
}
|
|
1003
1066
|
if (unhealthyConnectors.length > 0 &&
|
|
1004
|
-
|
|
1067
|
+
previousExternallyDeliveredFingerprint !== fingerprint) {
|
|
1005
1068
|
const message = buildConnectorHealthAlert(statusPayload, unhealthyConnectors);
|
|
1006
1069
|
const paths = await writeConnectorHealthAlert(runtimeDir, message, statusPayload, unhealthyConnectors, fingerprint);
|
|
1007
1070
|
const deliveries = await deliverConnectorHealthAlert({
|
|
@@ -1017,6 +1080,11 @@ async function maybeRunConnectorHealthCheck({ config, configPath, state, statePa
|
|
|
1017
1080
|
nextHealthState.lastAlertMarkdownPath = paths.markdownPath;
|
|
1018
1081
|
nextHealthState.lastAlertJsonPath = paths.jsonPath;
|
|
1019
1082
|
nextHealthState.lastAlertDeliveries = deliveries;
|
|
1083
|
+
nextHealthState.lastAlertExternalSent = hasSuccessfulExternalDelivery(deliveries);
|
|
1084
|
+
if (nextHealthState.lastAlertExternalSent) {
|
|
1085
|
+
nextHealthState.lastExternalAlertedAt = checkedAt;
|
|
1086
|
+
nextHealthState.lastExternalAlertedFingerprint = fingerprint;
|
|
1087
|
+
}
|
|
1020
1088
|
}
|
|
1021
1089
|
const nextState = {
|
|
1022
1090
|
...state,
|
|
@@ -1024,6 +1092,22 @@ async function maybeRunConnectorHealthCheck({ config, configPath, state, statePa
|
|
|
1024
1092
|
};
|
|
1025
1093
|
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
|
1026
1094
|
await fs.writeFile(statePath, JSON.stringify(nextState, null, 2), 'utf8');
|
|
1095
|
+
await appendSchedulerProof('connector_health_checked', {
|
|
1096
|
+
configPath,
|
|
1097
|
+
statePath,
|
|
1098
|
+
intervalMinutes,
|
|
1099
|
+
checkedAt,
|
|
1100
|
+
lastStatusOk: nextHealthState.lastStatusOk,
|
|
1101
|
+
connectedConnectors,
|
|
1102
|
+
unhealthyConnectors: unhealthyConnectors.map((entry) => ({
|
|
1103
|
+
key: entry.key,
|
|
1104
|
+
status: entry.status,
|
|
1105
|
+
detail: entry.detail,
|
|
1106
|
+
})),
|
|
1107
|
+
alertMarkdownPath: nextHealthState.lastAlertMarkdownPath || null,
|
|
1108
|
+
deliveryCount: Array.isArray(nextHealthState.lastAlertDeliveries) ? nextHealthState.lastAlertDeliveries.length : 0,
|
|
1109
|
+
externalDeliverySent: nextHealthState.lastAlertExternalSent === true,
|
|
1110
|
+
});
|
|
1027
1111
|
return nextState;
|
|
1028
1112
|
}
|
|
1029
1113
|
function buildIssueFingerprint(issuesPayload) {
|
|
@@ -1260,6 +1344,11 @@ function hasSourceChanges(previousHashes, currentHashes) {
|
|
|
1260
1344
|
return false;
|
|
1261
1345
|
}
|
|
1262
1346
|
async function runOnce(configPath, statePath) {
|
|
1347
|
+
await appendSchedulerProof('runner_invoked', {
|
|
1348
|
+
configPath,
|
|
1349
|
+
statePath,
|
|
1350
|
+
argv: process.argv.slice(2),
|
|
1351
|
+
});
|
|
1263
1352
|
const config = await readJson(configPath);
|
|
1264
1353
|
await applyOpenClawSecretRefs(config);
|
|
1265
1354
|
const inferredGitHubRepo = await inferGitHubRepo(config);
|
|
@@ -1276,7 +1365,7 @@ async function runOnce(configPath, statePath) {
|
|
|
1276
1365
|
lastRunAt: null,
|
|
1277
1366
|
sourceCursors: {},
|
|
1278
1367
|
});
|
|
1279
|
-
const runtimeDir = path.resolve(
|
|
1368
|
+
const runtimeDir = path.resolve(deriveRuntimeDirFromStatePath(statePath));
|
|
1280
1369
|
const stateAfterHealthCheck = await maybeRunConnectorHealthCheck({
|
|
1281
1370
|
config,
|
|
1282
1371
|
configPath,
|
|
@@ -1290,14 +1379,22 @@ async function runOnce(configPath, statePath) {
|
|
|
1290
1379
|
const changed = hasSourceChanges(stateAfterHealthCheck.sourceHashes, currentHashes);
|
|
1291
1380
|
if (!changed && config.schedule?.skipIfNoDataChange !== false) {
|
|
1292
1381
|
process.stdout.write(`[${new Date().toISOString()}] No data changes. Skip run.\n`);
|
|
1382
|
+
const completedAt = new Date().toISOString();
|
|
1293
1383
|
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
|
1294
1384
|
await fs.writeFile(statePath, JSON.stringify({
|
|
1295
1385
|
...stateAfterHealthCheck,
|
|
1296
1386
|
sourceHashes: currentHashes,
|
|
1297
1387
|
sourceCursors,
|
|
1298
|
-
lastRunAt:
|
|
1388
|
+
lastRunAt: completedAt,
|
|
1299
1389
|
skippedReason: 'no_data_change',
|
|
1300
1390
|
}, null, 2), 'utf8');
|
|
1391
|
+
await appendSchedulerProof('runner_completed', {
|
|
1392
|
+
configPath,
|
|
1393
|
+
statePath,
|
|
1394
|
+
completedAt,
|
|
1395
|
+
skippedReason: 'no_data_change',
|
|
1396
|
+
activeCadences: activeCadences.map((cadence) => cadence.key),
|
|
1397
|
+
});
|
|
1301
1398
|
return;
|
|
1302
1399
|
}
|
|
1303
1400
|
const githubArtifactModes = getGitHubArtifactModes(config).filter((mode) => shouldAutoCreateGitHubArtifact(config, mode));
|
|
@@ -1325,17 +1422,26 @@ async function runOnce(configPath, statePath) {
|
|
|
1325
1422
|
const unchangedIssueSet = issueFingerprint === stateAfterHealthCheck.lastIssueFingerprint;
|
|
1326
1423
|
if (unchangedIssueSet && config.schedule?.skipIfIssueSetUnchanged !== false) {
|
|
1327
1424
|
process.stdout.write(`[${new Date().toISOString()}] Issue set unchanged. Skip GitHub creation.\n`);
|
|
1425
|
+
const completedAt = new Date().toISOString();
|
|
1328
1426
|
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
|
1329
1427
|
await fs.writeFile(statePath, JSON.stringify({
|
|
1330
1428
|
...stateAfterHealthCheck,
|
|
1331
1429
|
sourceHashes: currentHashes,
|
|
1332
1430
|
sourceCursors,
|
|
1333
1431
|
lastIssueFingerprint: issueFingerprint,
|
|
1334
|
-
lastRunAt:
|
|
1432
|
+
lastRunAt: completedAt,
|
|
1335
1433
|
lastOutFile: dryRun.outFile,
|
|
1336
|
-
cadences: markCadencesRan(stateAfterHealthCheck, activeCadences,
|
|
1434
|
+
cadences: markCadencesRan(stateAfterHealthCheck, activeCadences, completedAt),
|
|
1337
1435
|
skippedReason: 'issue_set_unchanged',
|
|
1338
1436
|
}, null, 2), 'utf8');
|
|
1437
|
+
await appendSchedulerProof('runner_completed', {
|
|
1438
|
+
configPath,
|
|
1439
|
+
statePath,
|
|
1440
|
+
completedAt,
|
|
1441
|
+
skippedReason: 'issue_set_unchanged',
|
|
1442
|
+
activeCadences: activeCadences.map((cadence) => cadence.key),
|
|
1443
|
+
outFile: dryRun.outFile,
|
|
1444
|
+
});
|
|
1339
1445
|
return;
|
|
1340
1446
|
}
|
|
1341
1447
|
const shouldCreateGitHubArtifact = createGitHubArtifact && Number(dryRun.issuesPayload?.issue_count || 0) > 0;
|
|
@@ -1356,15 +1462,16 @@ async function runOnce(configPath, statePath) {
|
|
|
1356
1462
|
else {
|
|
1357
1463
|
process.stdout.write(`[${new Date().toISOString()}] Drafts generated only (${getActionMode(config)} auto-create disabled).\n`);
|
|
1358
1464
|
}
|
|
1465
|
+
const completedAt = new Date().toISOString();
|
|
1359
1466
|
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
|
1360
1467
|
await fs.writeFile(statePath, JSON.stringify({
|
|
1361
1468
|
...stateAfterHealthCheck,
|
|
1362
1469
|
sourceHashes: currentHashes,
|
|
1363
1470
|
sourceCursors,
|
|
1364
1471
|
lastIssueFingerprint: issueFingerprint,
|
|
1365
|
-
lastRunAt:
|
|
1472
|
+
lastRunAt: completedAt,
|
|
1366
1473
|
lastOutFile: dryRun.outFile,
|
|
1367
|
-
cadences: markCadencesRan(stateAfterHealthCheck, activeCadences,
|
|
1474
|
+
cadences: markCadencesRan(stateAfterHealthCheck, activeCadences, completedAt),
|
|
1368
1475
|
lastGrowthRunNotifications: await deliverGrowthRunSummary({
|
|
1369
1476
|
config,
|
|
1370
1477
|
configPath,
|
|
@@ -1377,6 +1484,16 @@ async function runOnce(configPath, statePath) {
|
|
|
1377
1484
|
}),
|
|
1378
1485
|
skippedReason: null,
|
|
1379
1486
|
}, null, 2), 'utf8');
|
|
1487
|
+
await appendSchedulerProof('runner_completed', {
|
|
1488
|
+
configPath,
|
|
1489
|
+
statePath,
|
|
1490
|
+
completedAt,
|
|
1491
|
+
skippedReason: null,
|
|
1492
|
+
activeCadences: activeCadences.map((cadence) => cadence.key),
|
|
1493
|
+
outFile: dryRun.outFile,
|
|
1494
|
+
issueCount: Number(dryRun.issuesPayload?.issue_count || 0),
|
|
1495
|
+
createdGitHubArtifact: shouldCreateGitHubArtifact,
|
|
1496
|
+
});
|
|
1380
1497
|
}
|
|
1381
1498
|
async function main() {
|
|
1382
1499
|
await loadOpenClawGrowthSecrets();
|
|
@@ -1384,6 +1501,7 @@ async function main() {
|
|
|
1384
1501
|
await maybeSelfUpdateFromClawHub(args);
|
|
1385
1502
|
const configPath = path.resolve(args.config);
|
|
1386
1503
|
const statePath = path.resolve(args.state);
|
|
1504
|
+
useSchedulerProofPathForStatePath(statePath);
|
|
1387
1505
|
if (!args.loop) {
|
|
1388
1506
|
await runOnce(configPath, statePath);
|
|
1389
1507
|
return;
|
|
@@ -1397,12 +1515,21 @@ async function main() {
|
|
|
1397
1515
|
await runOnce(configPath, statePath);
|
|
1398
1516
|
}
|
|
1399
1517
|
catch (error) {
|
|
1518
|
+
await appendSchedulerProof('runner_failed', {
|
|
1519
|
+
configPath,
|
|
1520
|
+
statePath,
|
|
1521
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1522
|
+
}).catch(() => { });
|
|
1400
1523
|
process.stderr.write(`[${new Date().toISOString()}] Run failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1401
1524
|
}
|
|
1402
1525
|
await sleep(intervalMinutes * 60_000);
|
|
1403
1526
|
}
|
|
1404
1527
|
}
|
|
1405
|
-
main().catch((error) => {
|
|
1528
|
+
main().catch(async (error) => {
|
|
1529
|
+
await appendSchedulerProof('runner_failed', {
|
|
1530
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1531
|
+
argv: process.argv.slice(2),
|
|
1532
|
+
}).catch(() => { });
|
|
1406
1533
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
1407
1534
|
process.exitCode = 1;
|
|
1408
1535
|
});
|