@analyticscli/growth-engineer 0.1.1-preview.0 → 0.1.1-preview.10
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/index.js +52 -3
- package/dist/index.js.map +1 -1
- package/dist/runtime/discord-openclaw-bridge.mjs +309 -0
- package/dist/runtime/export-paddle-summary.mjs +73 -19
- package/dist/runtime/export-paddle-summary.mjs.map +1 -1
- package/dist/runtime/openclaw-exporters-lib.d.mts +17 -0
- package/dist/runtime/openclaw-exporters-lib.mjs +65 -1
- package/dist/runtime/openclaw-exporters-lib.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-preflight.mjs +48 -15
- package/dist/runtime/openclaw-growth-preflight.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-runner.mjs +617 -28
- package/dist/runtime/openclaw-growth-runner.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-shared.mjs +3 -0
- package/dist/runtime/openclaw-growth-shared.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-start.mjs +115 -92
- package/dist/runtime/openclaw-growth-start.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-status.mjs +56 -36
- package/dist/runtime/openclaw-growth-status.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-wizard.mjs +421 -64
- package/dist/runtime/openclaw-growth-wizard.mjs.map +1 -1
- package/package.json +3 -2
|
@@ -11,7 +11,11 @@ const DEFAULT_CONFIG_PATH = 'data/openclaw-growth-engineer/config.json';
|
|
|
11
11
|
const DEFAULT_STATE_PATH = 'data/openclaw-growth-engineer/state.json';
|
|
12
12
|
const DEFAULT_SCHEDULER_PROOF_PATH = 'data/openclaw-growth-engineer/runtime/scheduler-proof.jsonl';
|
|
13
13
|
const DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES = 360;
|
|
14
|
+
const DEFAULT_DAILY_ISSUE_EVENT_GROWTH_MULTIPLIER = 2;
|
|
15
|
+
const DEFAULT_DAILY_ISSUE_EVENT_GROWTH_MIN_DELTA = 10;
|
|
16
|
+
const DEFAULT_DAILY_RUNNER_FAILURE_RETENTION_DAYS = 14;
|
|
14
17
|
const SELF_UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
18
|
+
const SELF_UPDATE_SKILL_SLUG_CANDIDATES = ['growth-engineer', 'openclaw-growth-engineer'];
|
|
15
19
|
const RUNTIME_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
16
20
|
let schedulerProofPath = path.resolve(DEFAULT_SCHEDULER_PROOF_PATH);
|
|
17
21
|
const DEFAULT_CADENCES = [
|
|
@@ -144,6 +148,7 @@ function resolveRuntimeScriptPath(scriptName) {
|
|
|
144
148
|
const candidates = [
|
|
145
149
|
path.join(RUNTIME_DIR, scriptName),
|
|
146
150
|
path.resolve('scripts', scriptName),
|
|
151
|
+
path.resolve('skills/growth-engineer/scripts', scriptName),
|
|
147
152
|
path.resolve('skills/openclaw-growth-engineer/scripts', scriptName),
|
|
148
153
|
];
|
|
149
154
|
return candidates.find((candidate) => existsSync(candidate)) || path.join(RUNTIME_DIR, scriptName);
|
|
@@ -164,7 +169,7 @@ function commandIsBuiltinExporter(command) {
|
|
|
164
169
|
return /(?:^|\s)(?:node\s+)?(?:\S*\/)?(?:export-analytics-summary|export-revenuecat-summary|export-paddle-summary|export-seo-summary|export-sentry-summary|export-coolify-summary|export-asc-summary)\.mjs(?:\s|$)/.test(String(command || ''));
|
|
165
170
|
}
|
|
166
171
|
function commandSupportsActiveConfig(command) {
|
|
167
|
-
return /(?:^|\s)(?:node\s+)?(?:\S*\/)?(?:export-sentry-summary|export-coolify-summary)\.mjs(?:\s|$)/.test(String(command || ''));
|
|
172
|
+
return /(?:^|\s)(?:node\s+)?(?:\S*\/)?(?:export-paddle-summary|export-sentry-summary|export-coolify-summary)\.mjs(?:\s|$)/.test(String(command || ''));
|
|
168
173
|
}
|
|
169
174
|
function withActiveConfigArg(command, configPath) {
|
|
170
175
|
const trimmed = String(command || '').trim();
|
|
@@ -247,9 +252,13 @@ function isSentryCompatibleSource(sourceConfig, sourceName) {
|
|
|
247
252
|
function shouldDegradeTransientSourceFailure(sourceConfig, sourceName, retried) {
|
|
248
253
|
if (!retried)
|
|
249
254
|
return false;
|
|
255
|
+
if (sourceConfig?.degradeTransientFailures === false)
|
|
256
|
+
return false;
|
|
250
257
|
if (!isRequiredSource(sourceConfig, sourceName))
|
|
251
258
|
return true;
|
|
252
|
-
|
|
259
|
+
if (isSentryCompatibleSource(sourceConfig, sourceName))
|
|
260
|
+
return true;
|
|
261
|
+
return sourceConfig?.degradeRequiredTransientFailures !== false;
|
|
253
262
|
}
|
|
254
263
|
function isTruthyEnv(value) {
|
|
255
264
|
return ['1', 'true', 'yes', 'y', 'on'].includes(String(value || '').trim().toLowerCase());
|
|
@@ -327,6 +336,23 @@ async function rerunCurrentProcessWithoutSelfUpdate() {
|
|
|
327
336
|
child.on('close', (code) => resolve(code));
|
|
328
337
|
});
|
|
329
338
|
}
|
|
339
|
+
function getSelfUpdateSkillCandidates(workspaceRoot) {
|
|
340
|
+
const explicit = String(process.env.OPENCLAW_GROWTH_SKILL_SLUG || '').trim();
|
|
341
|
+
const uniqueSlugs = [...new Set([explicit, ...SELF_UPDATE_SKILL_SLUG_CANDIDATES].filter(Boolean))];
|
|
342
|
+
return uniqueSlugs.map((slug) => {
|
|
343
|
+
const skillRoot = path.join(workspaceRoot, 'skills', slug);
|
|
344
|
+
return {
|
|
345
|
+
slug,
|
|
346
|
+
skillRoot,
|
|
347
|
+
originPath: path.join(skillRoot, '.clawhub/origin.json'),
|
|
348
|
+
runnerPath: path.join(skillRoot, 'scripts/openclaw-growth-runner.mjs'),
|
|
349
|
+
bootstrapPath: path.join(skillRoot, 'scripts/bootstrap-openclaw-workspace.sh'),
|
|
350
|
+
};
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
function resolveInstalledSelfUpdateSkill(workspaceRoot) {
|
|
354
|
+
return getSelfUpdateSkillCandidates(workspaceRoot).find((candidate) => existsSync(candidate.originPath)) || null;
|
|
355
|
+
}
|
|
330
356
|
async function maybeSelfUpdateFromClawHub(args) {
|
|
331
357
|
if (args.noSelfUpdate)
|
|
332
358
|
return false;
|
|
@@ -337,26 +363,27 @@ async function maybeSelfUpdateFromClawHub(args) {
|
|
|
337
363
|
if (isFalseyEnv(process.env.OPENCLAW_GROWTH_SELF_UPDATE))
|
|
338
364
|
return false;
|
|
339
365
|
const workspaceRoot = process.cwd();
|
|
340
|
-
const
|
|
341
|
-
if (!
|
|
366
|
+
const installedSkill = resolveInstalledSelfUpdateSkill(workspaceRoot);
|
|
367
|
+
if (!installedSkill)
|
|
342
368
|
return false;
|
|
343
369
|
if (!(await commandExists('npx')))
|
|
344
370
|
return false;
|
|
345
371
|
const force = String(process.env.OPENCLAW_GROWTH_SELF_UPDATE || '').trim().toLowerCase() === 'always';
|
|
346
372
|
if (!(await shouldRunSelfUpdate(workspaceRoot, force)))
|
|
347
373
|
return false;
|
|
348
|
-
const beforeOrigin = await readJsonOptional(
|
|
374
|
+
const beforeOrigin = await readJsonOptional(installedSkill.originPath, null);
|
|
349
375
|
const beforeVersion = String(beforeOrigin?.installedVersion || '');
|
|
350
|
-
process.stdout.write(
|
|
351
|
-
const updateResult = await runShellCommand(
|
|
352
|
-
const afterOrigin = await readJsonOptional(
|
|
376
|
+
process.stdout.write(`Checking for Growth Engineer skill updates (${installedSkill.slug})...\n`);
|
|
377
|
+
const updateResult = await runShellCommand(`npx -y clawhub --no-input --dir skills update ${quote(installedSkill.slug)} --force`, 120_000);
|
|
378
|
+
const afterOrigin = await readJsonOptional(installedSkill.originPath, null);
|
|
353
379
|
const afterVersion = String(afterOrigin?.installedVersion || beforeVersion || '');
|
|
354
380
|
const workspaceRunnerPath = path.resolve(process.argv[1] || 'scripts/openclaw-growth-runner.mjs');
|
|
355
|
-
const
|
|
356
|
-
const runtimeOutdated = !(await filesHaveSameContent(workspaceRunnerPath, skillRunnerPath));
|
|
381
|
+
const runtimeOutdated = !(await filesHaveSameContent(workspaceRunnerPath, installedSkill.runnerPath));
|
|
357
382
|
await writeSelfUpdateState(workspaceRoot, {
|
|
358
383
|
lastCheckedAt: new Date().toISOString(),
|
|
359
384
|
ok: updateResult.ok,
|
|
385
|
+
skillSlug: installedSkill.slug,
|
|
386
|
+
skillRoot: installedSkill.skillRoot,
|
|
360
387
|
previousVersion: beforeVersion || null,
|
|
361
388
|
installedVersion: afterVersion || null,
|
|
362
389
|
}).catch(() => { });
|
|
@@ -370,7 +397,7 @@ async function maybeSelfUpdateFromClawHub(args) {
|
|
|
370
397
|
process.stdout.write(afterVersion && afterVersion !== beforeVersion
|
|
371
398
|
? `Updated OpenClaw Growth Engineer skill ${beforeVersion || 'unknown'} -> ${afterVersion}. Refreshing workspace runtime...\n`
|
|
372
399
|
: 'Refreshing workspace runtime from the installed OpenClaw Growth Engineer skill...\n');
|
|
373
|
-
const bootstrapResult = await runShellCommand(
|
|
400
|
+
const bootstrapResult = await runShellCommand(`bash ${quote(installedSkill.bootstrapPath)}`, 60_000);
|
|
374
401
|
if (!bootstrapResult.ok) {
|
|
375
402
|
process.stdout.write('Workspace runtime refresh failed; continuing with current process.\n');
|
|
376
403
|
return false;
|
|
@@ -399,6 +426,27 @@ function resolveShellCommand() {
|
|
|
399
426
|
function hardenUnattendedShellCommand(command) {
|
|
400
427
|
return String(command || '').replace(/(^|[;&|]\s*)sudo(?!\s+-n(?:\s|$))(?=\s|$)/g, '$1sudo -n');
|
|
401
428
|
}
|
|
429
|
+
function redactCommandForDiagnostics(command) {
|
|
430
|
+
const raw = String(command || '').trim();
|
|
431
|
+
if (!raw)
|
|
432
|
+
return '';
|
|
433
|
+
return raw
|
|
434
|
+
.replace(/((?:^|\s)(?:[A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|PASS|KEY|AUTH|CREDENTIAL)[A-Z0-9_]*)=)(?:"[^"]*"|'[^']*'|\S+)/gi, '$1[redacted]')
|
|
435
|
+
.replace(/((?:^|\s)--(?:token|access-token|api-key|key|secret|password|pass|authorization|auth|bearer)(?:=|\s+))(?:"[^"]*"|'[^']*'|\S+)/gi, '$1[redacted]');
|
|
436
|
+
}
|
|
437
|
+
function truncateDiagnosticText(value, maxLength = 2000) {
|
|
438
|
+
const text = String(value || '').trim();
|
|
439
|
+
if (text.length <= maxLength)
|
|
440
|
+
return text;
|
|
441
|
+
return `${text.slice(0, maxLength - 1)}…`;
|
|
442
|
+
}
|
|
443
|
+
function buildSourceCommandFailureMessage(sourceName, resolvedCommand, detail) {
|
|
444
|
+
const safeCommand = redactCommandForDiagnostics(resolvedCommand);
|
|
445
|
+
const safeDetail = truncateDiagnosticText(detail);
|
|
446
|
+
return safeCommand
|
|
447
|
+
? `Source "${sourceName}" command failed: command \`${safeCommand}\`; ${safeDetail}`
|
|
448
|
+
: `Source "${sourceName}" command failed: ${safeDetail}`;
|
|
449
|
+
}
|
|
402
450
|
function isSudoPasswordPrompt(stderr) {
|
|
403
451
|
return /sudo: (?:a password is required|a terminal is required to read the password|no tty present)/i.test(String(stderr || ''));
|
|
404
452
|
}
|
|
@@ -410,6 +458,7 @@ function runShellCommand(command, timeoutMs = 120_000, options = {}) {
|
|
|
410
458
|
cwd: options.cwd,
|
|
411
459
|
env: {
|
|
412
460
|
...process.env,
|
|
461
|
+
...(options.env || {}),
|
|
413
462
|
DEBIAN_FRONTEND: 'noninteractive',
|
|
414
463
|
SUDO_ASKPASS: '/bin/false',
|
|
415
464
|
SUDO_PROMPT: '',
|
|
@@ -644,7 +693,8 @@ function buildConnectorWizardCommand(configPath, entry) {
|
|
|
644
693
|
const connector = connectorWizardKey(entry.key);
|
|
645
694
|
if (!connector)
|
|
646
695
|
return null;
|
|
647
|
-
|
|
696
|
+
const configArg = configPath ? ` --config ${quote(configPath)}` : '';
|
|
697
|
+
return `npx -y @analyticscli/growth-engineer@preview wizard${configArg} --connectors ${quote(connector)}`;
|
|
648
698
|
}
|
|
649
699
|
function conciseConnectorDetail(entry) {
|
|
650
700
|
const detail = String(entry?.detail || '').replace(/\s+/g, ' ').trim();
|
|
@@ -851,6 +901,8 @@ function notificationChannelKey(channel) {
|
|
|
851
901
|
return `slack:${channel?.label || channel?.webhookEnv || 'slack'}`;
|
|
852
902
|
if (type === 'webhook')
|
|
853
903
|
return `webhook:${channel?.label || channel?.urlEnv || channel?.webhookEnv || 'webhook'}`;
|
|
904
|
+
if (type === 'discord')
|
|
905
|
+
return `discord:${channel?.label || channel?.command || 'discord'}`;
|
|
854
906
|
if (type === 'command')
|
|
855
907
|
return `command:${channel?.label || channel?.command || 'command'}`;
|
|
856
908
|
return `${type}:${channel?.label || type}`;
|
|
@@ -907,7 +959,7 @@ function getDeliveryNotificationChannels(config, kind) {
|
|
|
907
959
|
}
|
|
908
960
|
if (deliveries.discord?.enabled) {
|
|
909
961
|
channels.push({
|
|
910
|
-
type: '
|
|
962
|
+
type: 'discord',
|
|
911
963
|
label: deliveries.discord.label || 'discord',
|
|
912
964
|
command: deliveries.discord.command || '',
|
|
913
965
|
});
|
|
@@ -1001,7 +1053,23 @@ async function sendCommandConnectorHealthAlert(channel, message) {
|
|
|
1001
1053
|
sent: result.ok,
|
|
1002
1054
|
external: true,
|
|
1003
1055
|
target: channel.label || 'command',
|
|
1004
|
-
detail: result.ok ?
|
|
1056
|
+
detail: result.ok ? 'sent' : result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`,
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
async function sendDiscordConnectorHealthAlert(channel, message, statusPayload, unhealthyConnectors, fingerprint) {
|
|
1060
|
+
if (!channel.command) {
|
|
1061
|
+
return { sent: false, target: channel.label || 'discord', detail: 'discord command not configured' };
|
|
1062
|
+
}
|
|
1063
|
+
const payload = buildDiscordConnectorHealthPayload(message, statusPayload, unhealthyConnectors, fingerprint);
|
|
1064
|
+
const result = await runShellCommand(String(channel.command), 60_000, {
|
|
1065
|
+
input: JSON.stringify(payload),
|
|
1066
|
+
env: { OPENCLAW_DISCORD_DELIVERY_FORMAT: 'embed' },
|
|
1067
|
+
});
|
|
1068
|
+
return {
|
|
1069
|
+
sent: result.ok,
|
|
1070
|
+
external: true,
|
|
1071
|
+
target: channel.label || 'discord',
|
|
1072
|
+
detail: result.ok ? 'sent' : result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`,
|
|
1005
1073
|
};
|
|
1006
1074
|
}
|
|
1007
1075
|
function hasExternalNotificationChannel(channels) {
|
|
@@ -1010,6 +1078,57 @@ function hasExternalNotificationChannel(channels) {
|
|
|
1010
1078
|
function hasSuccessfulExternalDelivery(results) {
|
|
1011
1079
|
return results.some((result) => result?.sent === true && result?.external === true);
|
|
1012
1080
|
}
|
|
1081
|
+
function discordTruncate(value, maxLength) {
|
|
1082
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
1083
|
+
if (text.length <= maxLength)
|
|
1084
|
+
return text;
|
|
1085
|
+
return `${text.slice(0, Math.max(0, maxLength - 1)).trim()}…`;
|
|
1086
|
+
}
|
|
1087
|
+
function discordField(name, value, inline = false) {
|
|
1088
|
+
return {
|
|
1089
|
+
name: discordTruncate(name, 256) || 'Detail',
|
|
1090
|
+
value: discordTruncate(value, 1024) || '-',
|
|
1091
|
+
inline,
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
function connectorStatusColor(unhealthyConnectors) {
|
|
1095
|
+
return unhealthyConnectors.some((entry) => String(entry?.status || '').toLowerCase() === 'blocked')
|
|
1096
|
+
? 0xd92d20
|
|
1097
|
+
: 0xf79009;
|
|
1098
|
+
}
|
|
1099
|
+
function buildDiscordConnectorHealthPayload(message, statusPayload, unhealthyConnectors, fingerprint) {
|
|
1100
|
+
const fields = unhealthyConnectors.slice(0, 10).map((entry) => {
|
|
1101
|
+
const command = buildConnectorWizardCommand(statusPayload?.configPath || DEFAULT_CONFIG_PATH, entry);
|
|
1102
|
+
const parts = [
|
|
1103
|
+
`Status: ${entry.status || 'blocked'}`,
|
|
1104
|
+
conciseConnectorDetail(entry),
|
|
1105
|
+
command ? `Fix: \`${command}\`` : null,
|
|
1106
|
+
isAscWebAuthIssue(entry)
|
|
1107
|
+
? 'ASC web-auth only: `ASC_WEB_APPLE_ID="<apple-id>" asc web auth login --apple-id "$ASC_WEB_APPLE_ID"`'
|
|
1108
|
+
: null,
|
|
1109
|
+
].filter(Boolean);
|
|
1110
|
+
return discordField(humanConnectorName(entry.key), parts.join('\n'));
|
|
1111
|
+
});
|
|
1112
|
+
if (unhealthyConnectors.length > 10) {
|
|
1113
|
+
fields.push(discordField('More issues', `${unhealthyConnectors.length - 10} additional connector(s) need attention.`));
|
|
1114
|
+
}
|
|
1115
|
+
return {
|
|
1116
|
+
content: '',
|
|
1117
|
+
embeds: [
|
|
1118
|
+
{
|
|
1119
|
+
title: `OpenClaw connector health: ${unhealthyConnectors.length} issue(s)`,
|
|
1120
|
+
description: 'Secrets stay in the host terminal or secret store.',
|
|
1121
|
+
color: connectorStatusColor(unhealthyConnectors),
|
|
1122
|
+
fields,
|
|
1123
|
+
footer: {
|
|
1124
|
+
text: `CONNECTOR_HEALTH_ALERT • ${String(fingerprint || '').slice(0, 12)}`,
|
|
1125
|
+
},
|
|
1126
|
+
timestamp: statusPayload?.generatedAt || new Date().toISOString(),
|
|
1127
|
+
},
|
|
1128
|
+
],
|
|
1129
|
+
fallbackText: message,
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1013
1132
|
function truncateMessageText(value, maxLength = 96) {
|
|
1014
1133
|
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
1015
1134
|
if (text.length <= maxLength)
|
|
@@ -1042,6 +1161,323 @@ function groupIssuesByProject(issues, maxIssues = 4) {
|
|
|
1042
1161
|
}
|
|
1043
1162
|
return [...grouped.entries()];
|
|
1044
1163
|
}
|
|
1164
|
+
function getDailyIssueDedupeConfig(config) {
|
|
1165
|
+
const raw = config?.schedule?.dailyIssueDedupe || config?.notifications?.growthRun?.dailyIssueDedupe || {};
|
|
1166
|
+
const multiplier = Number(raw.eventGrowthMultiplier ?? raw.multiplier);
|
|
1167
|
+
const minDelta = Number(raw.eventGrowthMinDelta ?? raw.minDelta);
|
|
1168
|
+
return {
|
|
1169
|
+
enabled: raw.enabled !== false,
|
|
1170
|
+
eventGrowthMultiplier: Number.isFinite(multiplier) && multiplier > 1
|
|
1171
|
+
? multiplier
|
|
1172
|
+
: DEFAULT_DAILY_ISSUE_EVENT_GROWTH_MULTIPLIER,
|
|
1173
|
+
eventGrowthMinDelta: Number.isFinite(minDelta) && minDelta > 0
|
|
1174
|
+
? minDelta
|
|
1175
|
+
: DEFAULT_DAILY_ISSUE_EVENT_GROWTH_MIN_DELTA,
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
function getDailyRunnerFailureDedupeConfig(config) {
|
|
1179
|
+
const raw = config?.schedule?.dailyRunnerFailureDedupe || config?.notifications?.growthRun?.dailyRunnerFailureDedupe || {};
|
|
1180
|
+
const retentionDays = Number(raw.retentionDays);
|
|
1181
|
+
return {
|
|
1182
|
+
enabled: raw.enabled !== false,
|
|
1183
|
+
retentionDays: Number.isFinite(retentionDays) && retentionDays > 0
|
|
1184
|
+
? retentionDays
|
|
1185
|
+
: DEFAULT_DAILY_RUNNER_FAILURE_RETENTION_DAYS,
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
function resolveDailyIssueDedupeTimeZone(config) {
|
|
1189
|
+
return String(config?.schedule?.timezone ||
|
|
1190
|
+
config?.automation?.openclawCron?.timezone ||
|
|
1191
|
+
process.env.TZ ||
|
|
1192
|
+
'UTC').trim() || 'UTC';
|
|
1193
|
+
}
|
|
1194
|
+
function normalizeRunnerFailureForFingerprint(errorMessage) {
|
|
1195
|
+
return redactCommandForDiagnostics(errorMessage)
|
|
1196
|
+
.replace(/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\b/g, '[timestamp]')
|
|
1197
|
+
.replace(/--since(?:=|\s+)(?:"[^"]*"|'[^']*'|\S+)/g, '--since [timestamp]')
|
|
1198
|
+
.replace(/--until(?:=|\s+)(?:"[^"]*"|'[^']*'|\S+)/g, '--until [timestamp]')
|
|
1199
|
+
.replace(/\bpid\s+\d+\b/gi, 'pid [pid]')
|
|
1200
|
+
.replace(/\s+/g, ' ')
|
|
1201
|
+
.trim();
|
|
1202
|
+
}
|
|
1203
|
+
function buildRunnerFailureFingerprint(errorMessage) {
|
|
1204
|
+
return sha256(normalizeRunnerFailureForFingerprint(errorMessage));
|
|
1205
|
+
}
|
|
1206
|
+
function pruneDailyRunnerFailures(failures, now, retentionDays) {
|
|
1207
|
+
const cutoffMs = now.getTime() - retentionDays * 24 * 60 * 60 * 1000;
|
|
1208
|
+
return Object.fromEntries(Object.entries(failures || {}).filter(([, entry]) => {
|
|
1209
|
+
const lastSeenMs = Date.parse(String(entry?.lastSeenAt || entry?.firstSeenAt || ''));
|
|
1210
|
+
return !Number.isFinite(lastSeenMs) || lastSeenMs >= cutoffMs;
|
|
1211
|
+
}));
|
|
1212
|
+
}
|
|
1213
|
+
function parseFailureArgs(argv) {
|
|
1214
|
+
const args = {
|
|
1215
|
+
config: DEFAULT_CONFIG_PATH,
|
|
1216
|
+
state: DEFAULT_STATE_PATH,
|
|
1217
|
+
};
|
|
1218
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
1219
|
+
const token = argv[i];
|
|
1220
|
+
const next = argv[i + 1];
|
|
1221
|
+
if (token === '--config' && next) {
|
|
1222
|
+
args.config = next;
|
|
1223
|
+
i += 1;
|
|
1224
|
+
}
|
|
1225
|
+
else if (token === '--state' && next) {
|
|
1226
|
+
args.state = next;
|
|
1227
|
+
i += 1;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
return args;
|
|
1231
|
+
}
|
|
1232
|
+
async function recordRunnerFailure({ configPath, statePath, error, argv = [], now = new Date() }) {
|
|
1233
|
+
const errorMessage = truncateDiagnosticText(error instanceof Error ? error.message : String(error));
|
|
1234
|
+
const config = await readJsonOptional(configPath, {});
|
|
1235
|
+
const state = await readJsonOptional(statePath, {
|
|
1236
|
+
sourceHashes: {},
|
|
1237
|
+
lastIssueFingerprint: null,
|
|
1238
|
+
lastRunAt: null,
|
|
1239
|
+
sourceCursors: {},
|
|
1240
|
+
});
|
|
1241
|
+
const dedupeConfig = getDailyRunnerFailureDedupeConfig(config);
|
|
1242
|
+
const timeZone = resolveDailyIssueDedupeTimeZone(config);
|
|
1243
|
+
const date = formatDateInTimeZone(now, timeZone);
|
|
1244
|
+
const fingerprint = buildRunnerFailureFingerprint(errorMessage);
|
|
1245
|
+
const nowIso = now.toISOString();
|
|
1246
|
+
const previousDailyFailures = state?.dailyRunnerFailures?.date === date
|
|
1247
|
+
? state.dailyRunnerFailures
|
|
1248
|
+
: null;
|
|
1249
|
+
const previousFailures = previousDailyFailures?.failures && typeof previousDailyFailures.failures === 'object'
|
|
1250
|
+
? previousDailyFailures.failures
|
|
1251
|
+
: {};
|
|
1252
|
+
const failures = pruneDailyRunnerFailures(previousFailures, now, dedupeConfig.retentionDays);
|
|
1253
|
+
const previousEntry = failures[fingerprint] || null;
|
|
1254
|
+
const suppressed = dedupeConfig.enabled && Boolean(previousEntry);
|
|
1255
|
+
const nextEntry = {
|
|
1256
|
+
...(previousEntry || {}),
|
|
1257
|
+
fingerprint,
|
|
1258
|
+
error: errorMessage,
|
|
1259
|
+
normalizedError: normalizeRunnerFailureForFingerprint(errorMessage),
|
|
1260
|
+
firstSeenAt: previousEntry?.firstSeenAt || nowIso,
|
|
1261
|
+
lastSeenAt: nowIso,
|
|
1262
|
+
};
|
|
1263
|
+
if (suppressed) {
|
|
1264
|
+
nextEntry.suppressedCount = Number(previousEntry?.suppressedCount || 0) + 1;
|
|
1265
|
+
}
|
|
1266
|
+
else {
|
|
1267
|
+
nextEntry.lastReportedAt = nowIso;
|
|
1268
|
+
nextEntry.reportCount = Number(previousEntry?.reportCount || 0) + 1;
|
|
1269
|
+
}
|
|
1270
|
+
failures[fingerprint] = nextEntry;
|
|
1271
|
+
const nextState = {
|
|
1272
|
+
...state,
|
|
1273
|
+
dailyRunnerFailures: {
|
|
1274
|
+
date,
|
|
1275
|
+
timeZone,
|
|
1276
|
+
failures,
|
|
1277
|
+
updatedAt: nowIso,
|
|
1278
|
+
},
|
|
1279
|
+
lastRunnerFailure: {
|
|
1280
|
+
fingerprint,
|
|
1281
|
+
error: errorMessage,
|
|
1282
|
+
failedAt: nowIso,
|
|
1283
|
+
suppressed,
|
|
1284
|
+
},
|
|
1285
|
+
};
|
|
1286
|
+
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
|
1287
|
+
await fs.writeFile(statePath, JSON.stringify(nextState, null, 2), 'utf8');
|
|
1288
|
+
await appendSchedulerProof(suppressed ? 'runner_failed_suppressed' : 'runner_failed', {
|
|
1289
|
+
configPath,
|
|
1290
|
+
statePath,
|
|
1291
|
+
error: errorMessage,
|
|
1292
|
+
errorFingerprint: fingerprint,
|
|
1293
|
+
date,
|
|
1294
|
+
timeZone,
|
|
1295
|
+
argv,
|
|
1296
|
+
suppressed,
|
|
1297
|
+
reportCount: nextEntry.reportCount || 0,
|
|
1298
|
+
suppressedCount: nextEntry.suppressedCount || 0,
|
|
1299
|
+
socialOutput: suppressed ? 'HEARTBEAT_OK' : 'RUNNER_FAILED',
|
|
1300
|
+
socialReason: suppressed
|
|
1301
|
+
? 'runner failure unchanged and already reported today'
|
|
1302
|
+
: 'new runner failure for current day',
|
|
1303
|
+
});
|
|
1304
|
+
return {
|
|
1305
|
+
suppressed,
|
|
1306
|
+
exitCode: suppressed ? 0 : 1,
|
|
1307
|
+
fingerprint,
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
function formatDateInTimeZone(date, timeZone) {
|
|
1311
|
+
try {
|
|
1312
|
+
const parts = new Intl.DateTimeFormat('en-CA', {
|
|
1313
|
+
timeZone,
|
|
1314
|
+
year: 'numeric',
|
|
1315
|
+
month: '2-digit',
|
|
1316
|
+
day: '2-digit',
|
|
1317
|
+
}).formatToParts(date);
|
|
1318
|
+
const byType = Object.fromEntries(parts.map((part) => [part.type, part.value]));
|
|
1319
|
+
if (byType.year && byType.month && byType.day) {
|
|
1320
|
+
return `${byType.year}-${byType.month}-${byType.day}`;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
catch {
|
|
1324
|
+
// Fall back to UTC below for invalid host timezone settings.
|
|
1325
|
+
}
|
|
1326
|
+
return date.toISOString().slice(0, 10);
|
|
1327
|
+
}
|
|
1328
|
+
function normalizeIssueIdentityPart(value) {
|
|
1329
|
+
return String(value || '')
|
|
1330
|
+
.trim()
|
|
1331
|
+
.toLowerCase()
|
|
1332
|
+
.replace(/\s+/g, ' ');
|
|
1333
|
+
}
|
|
1334
|
+
function buildDailyIssueKey(issue) {
|
|
1335
|
+
const stableIdentity = [
|
|
1336
|
+
issueSourceUrl(issue),
|
|
1337
|
+
issue?.source_url,
|
|
1338
|
+
issue?.sourceUrl,
|
|
1339
|
+
issue?.issue_url,
|
|
1340
|
+
issue?.issueUrl,
|
|
1341
|
+
issue?.signal_id,
|
|
1342
|
+
issue?.signalId,
|
|
1343
|
+
issue?.id,
|
|
1344
|
+
]
|
|
1345
|
+
.map((value) => String(value || '').trim())
|
|
1346
|
+
.find(Boolean);
|
|
1347
|
+
const fallbackIdentity = [
|
|
1348
|
+
issueProjectLabel(issue),
|
|
1349
|
+
issue?.source,
|
|
1350
|
+
issue?.area,
|
|
1351
|
+
issue?.title,
|
|
1352
|
+
]
|
|
1353
|
+
.map(normalizeIssueIdentityPart)
|
|
1354
|
+
.filter(Boolean)
|
|
1355
|
+
.join('|');
|
|
1356
|
+
return sha256(stableIdentity || fallbackIdentity || stableStringify(issue));
|
|
1357
|
+
}
|
|
1358
|
+
function coerceIssueNumber(value) {
|
|
1359
|
+
if (value === null || value === undefined || value === '')
|
|
1360
|
+
return null;
|
|
1361
|
+
if (typeof value === 'number')
|
|
1362
|
+
return Number.isFinite(value) ? value : null;
|
|
1363
|
+
const normalized = String(value).replace(/,/g, '').trim();
|
|
1364
|
+
if (!normalized)
|
|
1365
|
+
return null;
|
|
1366
|
+
const number = Number(normalized);
|
|
1367
|
+
return Number.isFinite(number) ? number : null;
|
|
1368
|
+
}
|
|
1369
|
+
function issueEventCount(issue) {
|
|
1370
|
+
const direct = [
|
|
1371
|
+
issue?.events,
|
|
1372
|
+
issue?.eventCount,
|
|
1373
|
+
issue?.event_count,
|
|
1374
|
+
issue?.current_value,
|
|
1375
|
+
issue?.currentValue,
|
|
1376
|
+
issue?.count,
|
|
1377
|
+
];
|
|
1378
|
+
for (const value of direct) {
|
|
1379
|
+
const number = coerceIssueNumber(value);
|
|
1380
|
+
if (number !== null)
|
|
1381
|
+
return number;
|
|
1382
|
+
}
|
|
1383
|
+
const text = [
|
|
1384
|
+
issue?.impact,
|
|
1385
|
+
issue?.summary,
|
|
1386
|
+
...(Array.isArray(issue?.evidence) ? issue.evidence : []),
|
|
1387
|
+
issue?.body,
|
|
1388
|
+
]
|
|
1389
|
+
.filter(Boolean)
|
|
1390
|
+
.join('\n');
|
|
1391
|
+
const match = text.match(/\bEvents?:\s*([0-9][0-9,]*(?:\.[0-9]+)?)/i);
|
|
1392
|
+
return match ? coerceIssueNumber(match[1]) : null;
|
|
1393
|
+
}
|
|
1394
|
+
function isDrasticDailyIssueEventGrowth(currentEvents, previousEntry, dedupeConfig) {
|
|
1395
|
+
if (currentEvents === null || currentEvents === undefined)
|
|
1396
|
+
return false;
|
|
1397
|
+
const previousEvents = coerceIssueNumber(previousEntry?.lastReportedEvents ?? previousEntry?.lastSeenEvents);
|
|
1398
|
+
if (previousEvents === null || previousEvents < 0)
|
|
1399
|
+
return false;
|
|
1400
|
+
const requiredEvents = Math.max(previousEvents * dedupeConfig.eventGrowthMultiplier, previousEvents + dedupeConfig.eventGrowthMinDelta);
|
|
1401
|
+
return currentEvents >= requiredEvents;
|
|
1402
|
+
}
|
|
1403
|
+
function applyDailyIssueDedupe(issuesPayload, state, config, activeCadences, now = new Date()) {
|
|
1404
|
+
const issues = Array.isArray(issuesPayload?.issues) ? issuesPayload.issues : [];
|
|
1405
|
+
const dedupeConfig = getDailyIssueDedupeConfig(config);
|
|
1406
|
+
if (!dedupeConfig.enabled || !isShortOperationalCadence(activeCadences) || issues.length === 0) {
|
|
1407
|
+
return {
|
|
1408
|
+
issuesPayload,
|
|
1409
|
+
dailyIssueReports: state?.dailyIssueReports || null,
|
|
1410
|
+
suppressedCount: 0,
|
|
1411
|
+
reportedCount: issues.length,
|
|
1412
|
+
hasDrasticEventGrowth: false,
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
const timeZone = resolveDailyIssueDedupeTimeZone(config);
|
|
1416
|
+
const date = formatDateInTimeZone(now, timeZone);
|
|
1417
|
+
const nowIso = now.toISOString();
|
|
1418
|
+
const previousState = state?.dailyIssueReports?.date === date ? state.dailyIssueReports : null;
|
|
1419
|
+
const previousIssues = previousState?.issues && typeof previousState.issues === 'object'
|
|
1420
|
+
? previousState.issues
|
|
1421
|
+
: {};
|
|
1422
|
+
const nextIssues = { ...previousIssues };
|
|
1423
|
+
const reportableIssues = [];
|
|
1424
|
+
let suppressedCount = 0;
|
|
1425
|
+
let hasDrasticEventGrowth = false;
|
|
1426
|
+
for (const issue of issues) {
|
|
1427
|
+
const key = buildDailyIssueKey(issue);
|
|
1428
|
+
const previousEntry = previousIssues[key] || null;
|
|
1429
|
+
const events = issueEventCount(issue);
|
|
1430
|
+
const shouldReport = !previousEntry || isDrasticDailyIssueEventGrowth(events, previousEntry, dedupeConfig);
|
|
1431
|
+
const reportReason = !previousEntry ? 'new_daily_issue' : 'event_growth';
|
|
1432
|
+
const nextEntry = {
|
|
1433
|
+
...(previousEntry || {}),
|
|
1434
|
+
title: String(issue?.title || previousEntry?.title || '').slice(0, 240),
|
|
1435
|
+
app: issueProjectLabel(issue),
|
|
1436
|
+
sourceUrl: issueSourceUrl(issue) || previousEntry?.sourceUrl || null,
|
|
1437
|
+
lastSeenAt: nowIso,
|
|
1438
|
+
lastSeenEvents: events ?? previousEntry?.lastSeenEvents ?? null,
|
|
1439
|
+
};
|
|
1440
|
+
if (shouldReport) {
|
|
1441
|
+
reportableIssues.push(issue);
|
|
1442
|
+
if (previousEntry)
|
|
1443
|
+
hasDrasticEventGrowth = true;
|
|
1444
|
+
nextEntry.lastReportedAt = nowIso;
|
|
1445
|
+
nextEntry.lastReportedEvents = events ?? previousEntry?.lastReportedEvents ?? null;
|
|
1446
|
+
nextEntry.lastReportReason = reportReason;
|
|
1447
|
+
nextEntry.reportCount = Number(previousEntry?.reportCount || 0) + 1;
|
|
1448
|
+
}
|
|
1449
|
+
else {
|
|
1450
|
+
suppressedCount += 1;
|
|
1451
|
+
nextEntry.suppressedCount = Number(previousEntry?.suppressedCount || 0) + 1;
|
|
1452
|
+
}
|
|
1453
|
+
nextIssues[key] = nextEntry;
|
|
1454
|
+
}
|
|
1455
|
+
return {
|
|
1456
|
+
issuesPayload: {
|
|
1457
|
+
...issuesPayload,
|
|
1458
|
+
issue_count: reportableIssues.length,
|
|
1459
|
+
issues: reportableIssues,
|
|
1460
|
+
suppressed_issue_count: suppressedCount,
|
|
1461
|
+
daily_issue_dedupe: {
|
|
1462
|
+
date,
|
|
1463
|
+
timeZone,
|
|
1464
|
+
suppressedCount,
|
|
1465
|
+
reportedCount: reportableIssues.length,
|
|
1466
|
+
eventGrowthMultiplier: dedupeConfig.eventGrowthMultiplier,
|
|
1467
|
+
eventGrowthMinDelta: dedupeConfig.eventGrowthMinDelta,
|
|
1468
|
+
},
|
|
1469
|
+
},
|
|
1470
|
+
dailyIssueReports: {
|
|
1471
|
+
date,
|
|
1472
|
+
timeZone,
|
|
1473
|
+
issues: nextIssues,
|
|
1474
|
+
updatedAt: nowIso,
|
|
1475
|
+
},
|
|
1476
|
+
suppressedCount,
|
|
1477
|
+
reportedCount: reportableIssues.length,
|
|
1478
|
+
hasDrasticEventGrowth,
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1045
1481
|
async function deliverConnectorHealthAlert({ config, configPath, message, statusPayload, unhealthyConnectors, fingerprint }) {
|
|
1046
1482
|
const channels = getConnectorHealthChannels(config);
|
|
1047
1483
|
if (config?.notifications?.connectorHealth?.enabled === false) {
|
|
@@ -1062,6 +1498,9 @@ async function deliverConnectorHealthAlert({ config, configPath, message, status
|
|
|
1062
1498
|
else if (channel.type === 'webhook') {
|
|
1063
1499
|
results.push(await sendWebhookConnectorHealthAlert(channel, message, statusPayload, unhealthyConnectors, fingerprint));
|
|
1064
1500
|
}
|
|
1501
|
+
else if (channel.type === 'discord') {
|
|
1502
|
+
results.push(await sendDiscordConnectorHealthAlert(channel, message, statusPayload, unhealthyConnectors, fingerprint));
|
|
1503
|
+
}
|
|
1065
1504
|
else if (channel.type === 'command') {
|
|
1066
1505
|
results.push(await sendCommandConnectorHealthAlert(channel, message));
|
|
1067
1506
|
}
|
|
@@ -1142,6 +1581,10 @@ function buildGrowthRunSummaryMessage({ issuesPayload, activeCadences, sourceFil
|
|
|
1142
1581
|
? 'Action: GitHub artifact attempted.'
|
|
1143
1582
|
: 'Action: external alert only.');
|
|
1144
1583
|
}
|
|
1584
|
+
const suppressedIssueCount = Number(issuesPayload?.suppressed_issue_count || 0);
|
|
1585
|
+
if (suppressedIssueCount > 0) {
|
|
1586
|
+
lines.push(`Suppressed today: ${suppressedIssueCount} previously reported finding(s).`);
|
|
1587
|
+
}
|
|
1145
1588
|
if (charts.length > 0) {
|
|
1146
1589
|
lines.push(`Charts: ${charts.length}`);
|
|
1147
1590
|
}
|
|
@@ -1187,6 +1630,67 @@ function buildGrowthRunSummaryMessage({ issuesPayload, activeCadences, sourceFil
|
|
|
1187
1630
|
: 'No secrets were included.');
|
|
1188
1631
|
return `${lines.join('\n')}\n`;
|
|
1189
1632
|
}
|
|
1633
|
+
function growthRunTitle(activeCadences) {
|
|
1634
|
+
if (isShortOperationalCadence(activeCadences)) {
|
|
1635
|
+
return activeCadences.some((cadence) => String(cadence?.key) === 'healthcheck')
|
|
1636
|
+
? 'OpenClaw healthcheck'
|
|
1637
|
+
: 'OpenClaw daily';
|
|
1638
|
+
}
|
|
1639
|
+
if (isDeepAnalysisCadence(activeCadences))
|
|
1640
|
+
return 'OpenClaw growth review';
|
|
1641
|
+
return 'OpenClaw growth run';
|
|
1642
|
+
}
|
|
1643
|
+
function buildDiscordGrowthRunPayload(message, issuesPayload, activeCadences, sourceFiles, fingerprint, createdGitHubArtifact, charts = []) {
|
|
1644
|
+
const issues = Array.isArray(issuesPayload?.issues) ? issuesPayload.issues : [];
|
|
1645
|
+
const issueCount = Number(issuesPayload?.issue_count || 0);
|
|
1646
|
+
const fields = [
|
|
1647
|
+
discordField('Cadence', activeCadences.length > 0
|
|
1648
|
+
? activeCadences.map((cadence) => cadence.title || cadence.key).join(', ')
|
|
1649
|
+
: 'ad-hoc growth pass', false),
|
|
1650
|
+
discordField('Sources', Object.keys(sourceFiles || {}).sort().join(', ') || 'none', true),
|
|
1651
|
+
discordField('Findings', String(issueCount), true),
|
|
1652
|
+
];
|
|
1653
|
+
if (createdGitHubArtifact) {
|
|
1654
|
+
fields.push(discordField('Action', 'GitHub artifact creation was attempted.', true));
|
|
1655
|
+
}
|
|
1656
|
+
const suppressedIssueCount = Number(issuesPayload?.suppressed_issue_count || 0);
|
|
1657
|
+
if (suppressedIssueCount > 0) {
|
|
1658
|
+
fields.push(discordField('Suppressed today', `${suppressedIssueCount} previously reported finding(s).`, true));
|
|
1659
|
+
}
|
|
1660
|
+
if (charts.length > 0) {
|
|
1661
|
+
fields.push(discordField('Charts', String(charts.length), true));
|
|
1662
|
+
}
|
|
1663
|
+
const groupedIssues = isShortOperationalCadence(activeCadences)
|
|
1664
|
+
? groupIssuesByProject(issues, 4).map(([project, projectIssues]) => ({
|
|
1665
|
+
name: project,
|
|
1666
|
+
value: projectIssues.map((issue) => formatIssueSummaryLine(issue, 84)).filter(Boolean).join('\n'),
|
|
1667
|
+
}))
|
|
1668
|
+
: issues.slice(0, isDeepAnalysisCadence(activeCadences) ? 5 : 3).map((issue) => ({
|
|
1669
|
+
name: `${issue.priority || 'medium'} • ${issue.area || 'general'}`,
|
|
1670
|
+
value: formatIssueSummaryLine(issue, 96),
|
|
1671
|
+
}));
|
|
1672
|
+
for (const entry of groupedIssues) {
|
|
1673
|
+
if (entry.value)
|
|
1674
|
+
fields.push(discordField(entry.name, entry.value));
|
|
1675
|
+
}
|
|
1676
|
+
const summary = String(issuesPayload?.summary || '').trim();
|
|
1677
|
+
return {
|
|
1678
|
+
content: '',
|
|
1679
|
+
embeds: [
|
|
1680
|
+
{
|
|
1681
|
+
title: `${growthRunTitle(activeCadences)}: ${issueCount > 0 ? `${issueCount} finding(s)` : 'OK'}`,
|
|
1682
|
+
description: discordTruncate(summary || 'No secrets were included.', 500),
|
|
1683
|
+
color: issueCount > 0 ? 0xf79009 : 0x12b76a,
|
|
1684
|
+
fields: fields.slice(0, 20),
|
|
1685
|
+
footer: {
|
|
1686
|
+
text: `GROWTH_RUN • ${String(fingerprint || '').slice(0, 12)}`,
|
|
1687
|
+
},
|
|
1688
|
+
timestamp: new Date().toISOString(),
|
|
1689
|
+
},
|
|
1690
|
+
],
|
|
1691
|
+
fallbackText: message,
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1190
1694
|
async function writeConfiguredOpenClawChatGrowthSummary(configPath, channel, message, issuesPayload, activeCadences, fingerprint, charts) {
|
|
1191
1695
|
const markdownPath = resolveOpenClawChatDeliveryPath(channel.markdownPath, '.openclaw/chat/growth-summary.md');
|
|
1192
1696
|
const jsonPath = resolveOpenClawChatDeliveryPath(channel.jsonPath, '.openclaw/chat/growth-summary.json');
|
|
@@ -1276,7 +1780,23 @@ async function sendCommandGrowthSummary(channel, message) {
|
|
|
1276
1780
|
sent: result.ok,
|
|
1277
1781
|
external: true,
|
|
1278
1782
|
target: channel.label || 'command',
|
|
1279
|
-
detail: result.ok ?
|
|
1783
|
+
detail: result.ok ? 'sent' : result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`,
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
async function sendDiscordGrowthSummary(channel, message, issuesPayload, activeCadences, sourceFiles, fingerprint, createdGitHubArtifact, charts) {
|
|
1787
|
+
if (!channel.command) {
|
|
1788
|
+
return { sent: false, target: channel.label || 'discord', detail: 'discord command not configured' };
|
|
1789
|
+
}
|
|
1790
|
+
const payload = buildDiscordGrowthRunPayload(message, issuesPayload, activeCadences, sourceFiles, fingerprint, createdGitHubArtifact, charts);
|
|
1791
|
+
const result = await runShellCommand(String(channel.command), 60_000, {
|
|
1792
|
+
input: JSON.stringify(payload),
|
|
1793
|
+
env: { OPENCLAW_DISCORD_DELIVERY_FORMAT: 'embed' },
|
|
1794
|
+
});
|
|
1795
|
+
return {
|
|
1796
|
+
sent: result.ok,
|
|
1797
|
+
external: true,
|
|
1798
|
+
target: channel.label || 'discord',
|
|
1799
|
+
detail: result.ok ? 'sent' : result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`,
|
|
1280
1800
|
};
|
|
1281
1801
|
}
|
|
1282
1802
|
async function deliverGrowthRunSummary({ config, configPath, issuesPayload, activeCadences, sourceFiles, fingerprint, createdGitHubArtifact, chartManifestPath, }) {
|
|
@@ -1307,6 +1827,9 @@ async function deliverGrowthRunSummary({ config, configPath, issuesPayload, acti
|
|
|
1307
1827
|
else if (channel.type === 'webhook') {
|
|
1308
1828
|
results.push(await sendWebhookGrowthSummary(channel, message, issuesPayload, activeCadences, fingerprint, charts));
|
|
1309
1829
|
}
|
|
1830
|
+
else if (channel.type === 'discord') {
|
|
1831
|
+
results.push(await sendDiscordGrowthSummary(channel, message, issuesPayload, activeCadences, sourceFiles, fingerprint, createdGitHubArtifact, charts));
|
|
1832
|
+
}
|
|
1310
1833
|
else if (channel.type === 'command') {
|
|
1311
1834
|
results.push(await sendCommandGrowthSummary(channel, message));
|
|
1312
1835
|
}
|
|
@@ -1686,7 +2209,7 @@ async function resolveSourcePayloadWithCursor(sourceConfig, sourceName, cursorSt
|
|
|
1686
2209
|
},
|
|
1687
2210
|
};
|
|
1688
2211
|
}
|
|
1689
|
-
throw new Error(
|
|
2212
|
+
throw new Error(buildSourceCommandFailureMessage(sourceName, resolvedCommand, detail));
|
|
1690
2213
|
}
|
|
1691
2214
|
const fetchedAt = new Date().toISOString();
|
|
1692
2215
|
try {
|
|
@@ -1893,8 +2416,52 @@ async function runOnce(configPath, statePath) {
|
|
|
1893
2416
|
chartManifestPath,
|
|
1894
2417
|
cadencePlanPath,
|
|
1895
2418
|
});
|
|
1896
|
-
const
|
|
1897
|
-
const
|
|
2419
|
+
const dailyIssueDedupe = applyDailyIssueDedupe(dryRun.issuesPayload, stateAfterSourceCollection, config, activeCadences);
|
|
2420
|
+
const deliverableIssuesPayload = dailyIssueDedupe.issuesPayload;
|
|
2421
|
+
const issueFingerprint = buildIssueFingerprint(deliverableIssuesPayload);
|
|
2422
|
+
const unchangedIssueSet = issueFingerprint === stateAfterSourceCollection.lastIssueFingerprint &&
|
|
2423
|
+
!dailyIssueDedupe.hasDrasticEventGrowth;
|
|
2424
|
+
if (Number(dryRun.issuesPayload?.issue_count || 0) > 0 &&
|
|
2425
|
+
Number(deliverableIssuesPayload?.issue_count || 0) === 0 &&
|
|
2426
|
+
dailyIssueDedupe.suppressedCount > 0) {
|
|
2427
|
+
process.stdout.write(`[${new Date().toISOString()}] All findings were already reported today. Skip GitHub creation and external growth notification.\n`);
|
|
2428
|
+
const completedAt = new Date().toISOString();
|
|
2429
|
+
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
|
2430
|
+
await fs.writeFile(statePath, JSON.stringify({
|
|
2431
|
+
...stateAfterHealthCheck,
|
|
2432
|
+
...stateAfterSourceCollection,
|
|
2433
|
+
sourceHashes: currentHashes,
|
|
2434
|
+
sourceCursors,
|
|
2435
|
+
lastSourceFailures: sourceFailures,
|
|
2436
|
+
dailyIssueReports: dailyIssueDedupe.dailyIssueReports,
|
|
2437
|
+
lastIssueFingerprint: issueFingerprint,
|
|
2438
|
+
lastRunAt: completedAt,
|
|
2439
|
+
lastOutFile: dryRun.outFile,
|
|
2440
|
+
cadences: markCadencesRan(stateAfterSourceCollection, activeCadences, completedAt),
|
|
2441
|
+
lastGrowthRunNotifications: [
|
|
2442
|
+
{
|
|
2443
|
+
sent: false,
|
|
2444
|
+
target: 'growth_run',
|
|
2445
|
+
detail: `all ${dailyIssueDedupe.suppressedCount} finding(s) already reported today; external growth notification suppressed`,
|
|
2446
|
+
},
|
|
2447
|
+
],
|
|
2448
|
+
skippedReason: 'daily_issue_dedupe',
|
|
2449
|
+
}, null, 2), 'utf8');
|
|
2450
|
+
await appendSchedulerProof('runner_completed', {
|
|
2451
|
+
configPath,
|
|
2452
|
+
statePath,
|
|
2453
|
+
completedAt,
|
|
2454
|
+
skippedReason: 'daily_issue_dedupe',
|
|
2455
|
+
activeCadences: activeCadences.map((cadence) => cadence.key),
|
|
2456
|
+
outFile: dryRun.outFile,
|
|
2457
|
+
issueCount: Number(dryRun.issuesPayload?.issue_count || 0),
|
|
2458
|
+
suppressedIssueCount: dailyIssueDedupe.suppressedCount,
|
|
2459
|
+
sourceFailures,
|
|
2460
|
+
externalGrowthNotification: 'suppressed_daily_issue_dedupe',
|
|
2461
|
+
socialOutput: 'HEARTBEAT_OK',
|
|
2462
|
+
});
|
|
2463
|
+
return;
|
|
2464
|
+
}
|
|
1898
2465
|
if (unchangedIssueSet &&
|
|
1899
2466
|
config.schedule?.skipIfIssueSetUnchanged !== false) {
|
|
1900
2467
|
process.stdout.write(`[${new Date().toISOString()}] Issue set unchanged. Skip GitHub creation and external growth notification.\n`);
|
|
@@ -1906,6 +2473,7 @@ async function runOnce(configPath, statePath) {
|
|
|
1906
2473
|
sourceHashes: currentHashes,
|
|
1907
2474
|
sourceCursors,
|
|
1908
2475
|
lastSourceFailures: sourceFailures,
|
|
2476
|
+
dailyIssueReports: dailyIssueDedupe.dailyIssueReports,
|
|
1909
2477
|
lastIssueFingerprint: issueFingerprint,
|
|
1910
2478
|
lastRunAt: completedAt,
|
|
1911
2479
|
lastOutFile: dryRun.outFile,
|
|
@@ -1935,7 +2503,7 @@ async function runOnce(configPath, statePath) {
|
|
|
1935
2503
|
}
|
|
1936
2504
|
const issueSetChangedOrExplicitlyAllowed = !unchangedIssueSet || config.schedule?.skipIfIssueSetUnchanged === false;
|
|
1937
2505
|
const shouldCreateGitHubArtifact = createGitHubArtifact &&
|
|
1938
|
-
Number(
|
|
2506
|
+
Number(deliverableIssuesPayload?.issue_count || 0) > 0 &&
|
|
1939
2507
|
issueSetChangedOrExplicitlyAllowed;
|
|
1940
2508
|
if (shouldCreateGitHubArtifact) {
|
|
1941
2509
|
for (const githubArtifactMode of githubArtifactModes) {
|
|
@@ -1962,6 +2530,7 @@ async function runOnce(configPath, statePath) {
|
|
|
1962
2530
|
sourceHashes: currentHashes,
|
|
1963
2531
|
sourceCursors,
|
|
1964
2532
|
lastSourceFailures: sourceFailures,
|
|
2533
|
+
dailyIssueReports: dailyIssueDedupe.dailyIssueReports,
|
|
1965
2534
|
lastIssueFingerprint: issueFingerprint,
|
|
1966
2535
|
lastRunAt: completedAt,
|
|
1967
2536
|
lastOutFile: dryRun.outFile,
|
|
@@ -1969,7 +2538,7 @@ async function runOnce(configPath, statePath) {
|
|
|
1969
2538
|
lastGrowthRunNotifications: await deliverGrowthRunSummary({
|
|
1970
2539
|
config,
|
|
1971
2540
|
configPath,
|
|
1972
|
-
issuesPayload:
|
|
2541
|
+
issuesPayload: deliverableIssuesPayload,
|
|
1973
2542
|
activeCadences,
|
|
1974
2543
|
sourceFiles,
|
|
1975
2544
|
fingerprint: issueFingerprint,
|
|
@@ -2010,22 +2579,42 @@ async function main() {
|
|
|
2010
2579
|
await runOnce(configPath, statePath);
|
|
2011
2580
|
}
|
|
2012
2581
|
catch (error) {
|
|
2013
|
-
await
|
|
2582
|
+
const failureDecision = await recordRunnerFailure({
|
|
2014
2583
|
configPath,
|
|
2015
2584
|
statePath,
|
|
2016
|
-
error
|
|
2017
|
-
|
|
2018
|
-
|
|
2585
|
+
error,
|
|
2586
|
+
argv: process.argv.slice(2),
|
|
2587
|
+
}).catch(async () => {
|
|
2588
|
+
await appendSchedulerProof('runner_failed', {
|
|
2589
|
+
configPath,
|
|
2590
|
+
statePath,
|
|
2591
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2592
|
+
}).catch(() => { });
|
|
2593
|
+
return null;
|
|
2594
|
+
});
|
|
2595
|
+
process.stderr.write(`[${new Date().toISOString()}] Run failed${failureDecision?.suppressed ? ' (already reported today)' : ''}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2019
2596
|
}
|
|
2020
2597
|
await sleep(intervalMinutes * 60_000);
|
|
2021
2598
|
}
|
|
2022
2599
|
}
|
|
2023
2600
|
main().catch(async (error) => {
|
|
2024
|
-
|
|
2025
|
-
|
|
2601
|
+
const fallbackArgs = parseFailureArgs(process.argv.slice(2));
|
|
2602
|
+
const configPath = path.resolve(fallbackArgs.config);
|
|
2603
|
+
const statePath = path.resolve(fallbackArgs.state);
|
|
2604
|
+
useSchedulerProofPathForStatePath(statePath);
|
|
2605
|
+
const failureDecision = await recordRunnerFailure({
|
|
2606
|
+
configPath,
|
|
2607
|
+
statePath,
|
|
2608
|
+
error,
|
|
2026
2609
|
argv: process.argv.slice(2),
|
|
2027
|
-
}).catch(() => {
|
|
2610
|
+
}).catch(async () => {
|
|
2611
|
+
await appendSchedulerProof('runner_failed', {
|
|
2612
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2613
|
+
argv: process.argv.slice(2),
|
|
2614
|
+
}).catch(() => { });
|
|
2615
|
+
return null;
|
|
2616
|
+
});
|
|
2028
2617
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
2029
|
-
process.exitCode = 1;
|
|
2618
|
+
process.exitCode = failureDecision?.exitCode ?? 1;
|
|
2030
2619
|
});
|
|
2031
2620
|
//# sourceMappingURL=openclaw-growth-runner.mjs.map
|