Skip to content

Commit 201553f

Browse files
authored
fix(ai): throttle github workflow sync cadence (#13832) (#13833)
1 parent fdd2913 commit 201553f

4 files changed

Lines changed: 83 additions & 5 deletions

File tree

ai/config.template.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ class Config extends ConfigProvider {
372372
pollMs : leaf(3000, 'NEO_ORCHESTRATOR_POLL_INTERVAL_MS', 'number'),
373373
summarySweepMs : leaf(10 * 60 * 1000, 'NEO_ORCHESTRATOR_SUMMARY_SWEEP_INTERVAL_MS', 'number'),
374374
kbSyncMs : leaf(30 * 60 * 1000, 'NEO_ORCHESTRATOR_KB_SYNC_INTERVAL_MS', 'number'),
375-
githubWorkflowSyncMs: leaf(30 * 60 * 1000, 'NEO_ORCHESTRATOR_GITHUB_WORKFLOW_SYNC_INTERVAL_MS', 'number'),
375+
githubWorkflowSyncMs: leaf(2 * HOUR_MS, 'NEO_ORCHESTRATOR_GITHUB_WORKFLOW_SYNC_INTERVAL_MS', 'number'),
376376
backupMs : leaf(DAY_MS, 'NEO_ORCHESTRATOR_BACKUP_INTERVAL_MS', 'number'),
377377
graphLogCompactionMs: leaf(DAY_MS, 'NEO_ORCHESTRATOR_GRAPHLOG_COMPACTION_INTERVAL_MS', 'number'),
378378
primaryDevSyncMs : leaf(10 * 60 * 1000, 'NEO_ORCHESTRATOR_PRIMARY_DEV_SYNC_INTERVAL_MS', 'number'),

ai/daemons/orchestrator/scheduling/registry.mjs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,24 @@ import {getDueTask as getSwarmHeartbeatDueTask} from './swarmHeartbe
99
import {getDueTask as getTenantRepoSyncDueTask} from './tenantRepoSync.mjs';
1010
import {getDueTask as getEmbedDrainLivenessWatchdogDueTask} from './embedDrainLivenessWatchdog.mjs';
1111

12+
function toTimestampMs(value) {
13+
if (typeof value === 'number' && Number.isFinite(value)) {
14+
return value;
15+
}
16+
17+
if (typeof value === 'string' && value) {
18+
const parsed = Date.parse(value);
19+
return Number.isFinite(parsed) ? parsed : null;
20+
}
21+
22+
return null;
23+
}
24+
25+
function getLatestTimestampMs(...values) {
26+
const timestamps = values.map(toTimestampMs).filter(Number.isFinite);
27+
return timestamps.length ? Math.max(...timestamps) : null;
28+
}
29+
1230
/**
1331
* Coordinator-descriptor registry for the Orchestrator scheduling pipeline.
1432
*
@@ -89,9 +107,11 @@ export const TASK_REGISTRY = Object.freeze([
89107
dependencies : [],
90108
getDueTask({state, now, intervals, enables}) {
91109
if (!enables.githubWorkflowSync) return null;
92-
const lastRunAt = state.githubWorkflowSync?.lastRunAt ?? 0;
93-
const intervalMs = intervals.githubWorkflowSync;
94-
if (intervalMs > 0 && now - lastRunAt >= intervalMs) {
110+
const taskState = state.githubWorkflowSync || {};
111+
const terminalAt = getLatestTimestampMs(taskState.lastSuccessAt, taskState.lastErrorAt);
112+
const cadenceAnchor = terminalAt ?? toTimestampMs(taskState.lastRunAt) ?? 0;
113+
const intervalMs = intervals.githubWorkflowSync;
114+
if (intervalMs > 0 && now - cadenceAnchor >= intervalMs) {
95115
return {
96116
taskName: 'githubWorkflowSync',
97117
source : 'periodic-sync',

test/playwright/unit/ai/config.template.spec.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ test.describe('Tier 1 Config Immutability', () => {
132132
pollMs : 3000,
133133
summarySweepMs : 10 * 60 * 1000,
134134
kbSyncMs : 30 * 60 * 1000,
135-
githubWorkflowSyncMs : 30 * 60 * 1000,
135+
githubWorkflowSyncMs : 2 * 60 * 60 * 1000,
136136
backupMs : 24 * 60 * 60 * 1000,
137137
graphLogCompactionMs : 24 * 60 * 60 * 1000,
138138
primaryDevSyncMs : 10 * 60 * 1000,

test/playwright/unit/ai/daemons/orchestrator/scheduling/registry.spec.mjs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,64 @@ test.describe('orchestrator/scheduling/registry (#11862 Sub 18)', () => {
100100
})).toBeNull();
101101
});
102102

103+
test('githubWorkflowSync cadence uses terminal timestamp before start timestamp (#13832)', () => {
104+
const descriptor = TASK_REGISTRY.find(d => d.taskName === 'githubWorkflowSync');
105+
expect(descriptor, 'githubWorkflowSync is registered').toBeTruthy();
106+
107+
expect(descriptor.getDueTask({
108+
state: {
109+
githubWorkflowSync: {
110+
lastRunAt : 0,
111+
lastSuccessAt: new Date(1200).toISOString()
112+
}
113+
},
114+
now : 1800,
115+
intervals: {githubWorkflowSync: 1000},
116+
enables : {githubWorkflowSync: true}
117+
})).toBeNull();
118+
119+
expect(descriptor.getDueTask({
120+
state: {
121+
githubWorkflowSync: {
122+
lastRunAt : 0,
123+
lastSuccessAt: new Date(1200).toISOString()
124+
}
125+
},
126+
now : 2200,
127+
intervals: {githubWorkflowSync: 1000},
128+
enables : {githubWorkflowSync: true}
129+
})).toMatchObject({taskName: 'githubWorkflowSync', source: 'periodic-sync'});
130+
});
131+
132+
test('githubWorkflowSync failed terminal attempts also cool down retries (#13832)', () => {
133+
const descriptor = TASK_REGISTRY.find(d => d.taskName === 'githubWorkflowSync');
134+
expect(descriptor, 'githubWorkflowSync is registered').toBeTruthy();
135+
136+
expect(descriptor.getDueTask({
137+
state: {
138+
githubWorkflowSync: {
139+
lastRunAt : 0,
140+
lastErrorAt: new Date(1200).toISOString()
141+
}
142+
},
143+
now : 1800,
144+
intervals: {githubWorkflowSync: 1000},
145+
enables : {githubWorkflowSync: true}
146+
})).toBeNull();
147+
148+
expect(descriptor.getDueTask({
149+
state: {
150+
githubWorkflowSync: {
151+
lastRunAt : 0,
152+
lastErrorAt: new Date(1200).toISOString()
153+
}
154+
},
155+
now : 2200,
156+
intervals: {githubWorkflowSync: 1000},
157+
enables : {githubWorkflowSync: true}
158+
})).toMatchObject({taskName: 'githubWorkflowSync', source: 'periodic-sync'});
159+
});
160+
103161
test('continuous tasks (chroma/bridgeDaemon/mlx) are intentionally NOT in registry', () => {
104162
const names = TASK_REGISTRY.map(d => d.taskName);
105163
expect(names).not.toContain('chroma');

0 commit comments

Comments
 (0)