pulseed 0.4.14 → 0.4.15
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/assets/seedy.png +0 -0
- package/dist/adapters/types/mcp.d.ts +6 -6
- package/dist/base/config/global-config.d.ts.map +1 -1
- package/dist/base/config/global-config.js +2 -2
- package/dist/base/config/global-config.js.map +1 -1
- package/dist/base/llm/provider-config.d.ts +6 -1
- package/dist/base/llm/provider-config.d.ts.map +1 -1
- package/dist/base/llm/provider-config.js +148 -94
- package/dist/base/llm/provider-config.js.map +1 -1
- package/dist/base/utils/json-io.d.ts +6 -1
- package/dist/base/utils/json-io.d.ts.map +1 -1
- package/dist/base/utils/json-io.js +16 -3
- package/dist/base/utils/json-io.js.map +1 -1
- package/dist/interface/chat/chat-history.d.ts +6 -1
- package/dist/interface/chat/chat-history.d.ts.map +1 -1
- package/dist/interface/chat/chat-history.js +16 -2
- package/dist/interface/chat/chat-history.js.map +1 -1
- package/dist/interface/chat/chat-runner.d.ts +27 -1
- package/dist/interface/chat/chat-runner.d.ts.map +1 -1
- package/dist/interface/chat/chat-runner.js +412 -5
- package/dist/interface/chat/chat-runner.js.map +1 -1
- package/dist/interface/chat/grounding.d.ts.map +1 -1
- package/dist/interface/chat/grounding.js +2 -1
- package/dist/interface/chat/grounding.js.map +1 -1
- package/dist/interface/chat/mutation-tool-defs.d.ts +0 -21
- package/dist/interface/chat/mutation-tool-defs.d.ts.map +1 -1
- package/dist/interface/chat/mutation-tool-defs.js +1 -164
- package/dist/interface/chat/mutation-tool-defs.js.map +1 -1
- package/dist/interface/cli/commands/daemon.d.ts.map +1 -1
- package/dist/interface/cli/commands/daemon.js +4 -14
- package/dist/interface/cli/commands/daemon.js.map +1 -1
- package/dist/interface/cli/commands/setup/import/apply.d.ts.map +1 -1
- package/dist/interface/cli/commands/setup/import/apply.js +4 -0
- package/dist/interface/cli/commands/setup/import/apply.js.map +1 -1
- package/dist/interface/cli/commands/setup/import/discovery.d.ts.map +1 -1
- package/dist/interface/cli/commands/setup/import/discovery.js +10 -2
- package/dist/interface/cli/commands/setup/import/discovery.js.map +1 -1
- package/dist/interface/cli/commands/setup/import/flow.d.ts.map +1 -1
- package/dist/interface/cli/commands/setup/import/flow.js +11 -1
- package/dist/interface/cli/commands/setup/import/flow.js.map +1 -1
- package/dist/interface/cli/commands/setup/import/types.d.ts +3 -0
- package/dist/interface/cli/commands/setup/import/types.d.ts.map +1 -1
- package/dist/interface/cli/commands/setup-wizard.d.ts.map +1 -1
- package/dist/interface/cli/commands/setup-wizard.js +14 -1
- package/dist/interface/cli/commands/setup-wizard.js.map +1 -1
- package/dist/interface/cli/setup.d.ts.map +1 -1
- package/dist/interface/cli/setup.js +6 -5
- package/dist/interface/cli/setup.js.map +1 -1
- package/dist/interface/mcp-server/index.d.ts.map +1 -1
- package/dist/interface/mcp-server/index.js +2 -1
- package/dist/interface/mcp-server/index.js.map +1 -1
- package/dist/platform/observation/observation-tools.d.ts.map +1 -1
- package/dist/platform/observation/observation-tools.js +4 -1
- package/dist/platform/observation/observation-tools.js.map +1 -1
- package/dist/platform/soil/config.d.ts.map +1 -1
- package/dist/platform/soil/config.js +1 -2
- package/dist/platform/soil/config.js.map +1 -1
- package/dist/platform/soil/display/index.d.ts +4 -0
- package/dist/platform/soil/display/index.d.ts.map +1 -0
- package/dist/platform/soil/display/index.js +4 -0
- package/dist/platform/soil/display/index.js.map +1 -0
- package/dist/platform/soil/display/materialize.d.ts +3 -0
- package/dist/platform/soil/display/materialize.d.ts.map +1 -0
- package/dist/platform/soil/display/materialize.js +381 -0
- package/dist/platform/soil/display/materialize.js.map +1 -0
- package/dist/platform/soil/display/registry.d.ts +4 -0
- package/dist/platform/soil/display/registry.d.ts.map +1 -0
- package/dist/platform/soil/display/registry.js +19 -0
- package/dist/platform/soil/display/registry.js.map +1 -0
- package/dist/platform/soil/display/types.d.ts +28 -0
- package/dist/platform/soil/display/types.d.ts.map +1 -0
- package/dist/platform/soil/display/types.js +2 -0
- package/dist/platform/soil/display/types.js.map +1 -0
- package/dist/platform/soil/index.d.ts +1 -0
- package/dist/platform/soil/index.d.ts.map +1 -1
- package/dist/platform/soil/index.js +1 -0
- package/dist/platform/soil/index.js.map +1 -1
- package/dist/platform/soil/open.d.ts.map +1 -1
- package/dist/platform/soil/open.js +2 -0
- package/dist/platform/soil/open.js.map +1 -1
- package/dist/platform/soil/publish/publisher.d.ts.map +1 -1
- package/dist/platform/soil/publish/publisher.js +2 -0
- package/dist/platform/soil/publish/publisher.js.map +1 -1
- package/dist/reflection/evening-catchup.d.ts.map +1 -1
- package/dist/reflection/evening-catchup.js +6 -41
- package/dist/reflection/evening-catchup.js.map +1 -1
- package/dist/reflection/morning-planning.d.ts.map +1 -1
- package/dist/reflection/morning-planning.js +6 -46
- package/dist/reflection/morning-planning.js.map +1 -1
- package/dist/reflection/reflection-utils.d.ts +16 -0
- package/dist/reflection/reflection-utils.d.ts.map +1 -0
- package/dist/reflection/reflection-utils.js +56 -0
- package/dist/reflection/reflection-utils.js.map +1 -0
- package/dist/reflection/weekly-review.d.ts.map +1 -1
- package/dist/reflection/weekly-review.js +3 -12
- package/dist/reflection/weekly-review.js.map +1 -1
- package/dist/reporting/report-formatters.d.ts +7 -0
- package/dist/reporting/report-formatters.d.ts.map +1 -1
- package/dist/reporting/report-formatters.js +22 -33
- package/dist/reporting/report-formatters.js.map +1 -1
- package/dist/reporting/reporting-engine.d.ts.map +1 -1
- package/dist/reporting/reporting-engine.js +34 -49
- package/dist/reporting/reporting-engine.js.map +1 -1
- package/dist/runtime/builtin-integrations.d.ts +4 -0
- package/dist/runtime/builtin-integrations.d.ts.map +1 -0
- package/dist/runtime/builtin-integrations.js +45 -0
- package/dist/runtime/builtin-integrations.js.map +1 -0
- package/dist/runtime/event/server.d.ts +1 -1
- package/dist/runtime/event/server.d.ts.map +1 -1
- package/dist/runtime/event/server.js +5 -27
- package/dist/runtime/event/server.js.map +1 -1
- package/dist/runtime/executor/loop-supervisor.d.ts.map +1 -1
- package/dist/runtime/executor/loop-supervisor.js +2 -2
- package/dist/runtime/executor/loop-supervisor.js.map +1 -1
- package/dist/runtime/foreign-plugins/compatibility.d.ts +7 -0
- package/dist/runtime/foreign-plugins/compatibility.d.ts.map +1 -0
- package/dist/runtime/foreign-plugins/compatibility.js +175 -0
- package/dist/runtime/foreign-plugins/compatibility.js.map +1 -0
- package/dist/runtime/foreign-plugins/index.d.ts +3 -0
- package/dist/runtime/foreign-plugins/index.d.ts.map +1 -0
- package/dist/runtime/foreign-plugins/index.js +2 -0
- package/dist/runtime/foreign-plugins/index.js.map +1 -0
- package/dist/runtime/foreign-plugins/types.d.ts +25 -0
- package/dist/runtime/foreign-plugins/types.d.ts.map +1 -0
- package/dist/runtime/foreign-plugins/types.js +2 -0
- package/dist/runtime/foreign-plugins/types.js.map +1 -0
- package/dist/runtime/schedule/engine.d.ts +12 -0
- package/dist/runtime/schedule/engine.d.ts.map +1 -1
- package/dist/runtime/schedule/engine.js +402 -243
- package/dist/runtime/schedule/engine.js.map +1 -1
- package/dist/runtime/types/builtin-integration.d.ts +13 -0
- package/dist/runtime/types/builtin-integration.d.ts.map +1 -0
- package/dist/runtime/types/builtin-integration.js +2 -0
- package/dist/runtime/types/builtin-integration.js.map +1 -0
- package/dist/tools/executor.js +2 -2
- package/dist/tools/executor.js.map +1 -1
- package/dist/tools/fs/ReadPulseedFileTool/ReadPulseedFileTool.d.ts.map +1 -1
- package/dist/tools/fs/ReadPulseedFileTool/ReadPulseedFileTool.js +21 -5
- package/dist/tools/fs/ReadPulseedFileTool/ReadPulseedFileTool.js.map +1 -1
- package/dist/tools/fs/WritePulseedFileTool/WritePulseedFileTool.d.ts.map +1 -1
- package/dist/tools/fs/WritePulseedFileTool/WritePulseedFileTool.js +61 -6
- package/dist/tools/fs/WritePulseedFileTool/WritePulseedFileTool.js.map +1 -1
- package/dist/tools/interaction/plan-utils.js +2 -2
- package/dist/tools/interaction/plan-utils.js.map +1 -1
- package/dist/tools/network/McpStdioTool/McpStdioTool.d.ts +8 -8
- package/dist/tools/query/ConfigTool/ConfigTool.d.ts.map +1 -1
- package/dist/tools/query/ConfigTool/ConfigTool.js +4 -3
- package/dist/tools/query/ConfigTool/ConfigTool.js.map +1 -1
- package/dist/tools/query/PluginStateTool/PluginStateTool.d.ts.map +1 -1
- package/dist/tools/query/PluginStateTool/PluginStateTool.js +13 -2
- package/dist/tools/query/PluginStateTool/PluginStateTool.js.map +1 -1
- package/dist/tools/system/ProcessSessionTool/ProcessSessionTool.d.ts +4 -4
- package/package.json +2 -1
- package/dist/interface/chat/self-knowledge-mutation-tools.d.ts +0 -12
- package/dist/interface/chat/self-knowledge-mutation-tools.d.ts.map +0 -1
- package/dist/interface/chat/self-knowledge-mutation-tools.js +0 -261
- package/dist/interface/chat/self-knowledge-mutation-tools.js.map +0 -1
- package/dist/interface/chat/self-knowledge-tools.d.ts +0 -30
- package/dist/interface/chat/self-knowledge-tools.d.ts.map +0 -1
- package/dist/interface/chat/self-knowledge-tools.js +0 -248
- package/dist/interface/chat/self-knowledge-tools.js.map +0 -1
- package/dist/prompt/index.d.ts +0 -12
- package/dist/prompt/index.d.ts.map +0 -1
- package/dist/prompt/index.js +0 -9
- package/dist/prompt/index.js.map +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CronExpressionParser } from "cron-parser";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import * as net from "node:net";
|
|
4
|
+
import * as fsp from "node:fs/promises";
|
|
4
5
|
import { randomUUID } from "node:crypto";
|
|
5
6
|
import { exec } from "node:child_process";
|
|
6
7
|
import { writeJsonFileAtomic, readJsonFileOrNull } from "../../base/utils/json-io.js";
|
|
@@ -13,6 +14,10 @@ import { hasConfiguredSoilPublishProvider } from "../../platform/soil/publish/in
|
|
|
13
14
|
import { buildSchedulePresetEntry } from "./presets.js";
|
|
14
15
|
import { ExternalScheduleEntrySchema } from "./source.js";
|
|
15
16
|
const SCHEDULES_FILE = "schedules.json";
|
|
17
|
+
const SCHEDULE_LOCK_DIR = `${SCHEDULES_FILE}.lock`;
|
|
18
|
+
const SCHEDULE_LOCK_TIMEOUT_MS = 5000;
|
|
19
|
+
const SCHEDULE_LOCK_STALE_MS = 30_000;
|
|
20
|
+
const SCHEDULE_LOCK_RETRY_MS = 25;
|
|
16
21
|
const DEFAULT_RETRY_POLICY = {
|
|
17
22
|
enabled: true,
|
|
18
23
|
initial_delay_ms: 30_000,
|
|
@@ -43,9 +48,12 @@ export class ScheduleEngine {
|
|
|
43
48
|
memoryLifecycle;
|
|
44
49
|
knowledgeManager;
|
|
45
50
|
historyStore;
|
|
51
|
+
schedulesLockPath;
|
|
52
|
+
scheduleLockDepth = 0;
|
|
46
53
|
constructor(deps) {
|
|
47
54
|
this.baseDir = deps.baseDir;
|
|
48
55
|
this.schedulesPath = path.join(deps.baseDir, SCHEDULES_FILE);
|
|
56
|
+
this.schedulesLockPath = path.join(deps.baseDir, SCHEDULE_LOCK_DIR);
|
|
49
57
|
this.logger = deps.logger ?? noopLogger;
|
|
50
58
|
this.dataSourceRegistry = deps.dataSourceRegistry;
|
|
51
59
|
this.llmClient = deps.llmClient;
|
|
@@ -60,31 +68,136 @@ export class ScheduleEngine {
|
|
|
60
68
|
}
|
|
61
69
|
// ─── Persistence ───
|
|
62
70
|
async loadEntries() {
|
|
71
|
+
this.entries = await this.readEntriesFromDisk();
|
|
72
|
+
await this.projectCurrentSchedulesToSoil();
|
|
73
|
+
return this.entries;
|
|
74
|
+
}
|
|
75
|
+
async readEntriesFromDisk() {
|
|
63
76
|
const raw = await readJsonFileOrNull(this.schedulesPath);
|
|
64
77
|
if (raw === null) {
|
|
65
|
-
this.entries = [];
|
|
66
78
|
return [];
|
|
67
79
|
}
|
|
68
80
|
const result = ScheduleEntryListSchema.safeParse(raw);
|
|
69
|
-
|
|
70
|
-
await this.projectCurrentSchedulesToSoil();
|
|
71
|
-
return this.entries;
|
|
81
|
+
return result.success ? result.data : [];
|
|
72
82
|
}
|
|
73
83
|
async saveEntries() {
|
|
84
|
+
await this.withScheduleFileLock(async () => {
|
|
85
|
+
await this.writeEntriesAndProject();
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async writeEntriesAndProject() {
|
|
74
89
|
await writeJsonFileAtomic(this.schedulesPath, this.entries);
|
|
75
90
|
await this.projectCurrentSchedulesToSoil();
|
|
76
91
|
}
|
|
77
|
-
async
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
92
|
+
async refreshEntriesForMutation() {
|
|
93
|
+
this.entries = await this.readEntriesFromDisk();
|
|
94
|
+
}
|
|
95
|
+
captureExecutionSideEffects(entryId) {
|
|
96
|
+
const entry = this.entries.find((candidate) => candidate.id === entryId);
|
|
97
|
+
return entry ? { baseline_results: entry.baseline_results } : null;
|
|
98
|
+
}
|
|
99
|
+
applyExecutionSideEffects(entryId, sideEffects) {
|
|
100
|
+
if (!sideEffects)
|
|
101
|
+
return;
|
|
102
|
+
const idx = this.entries.findIndex((candidate) => candidate.id === entryId);
|
|
103
|
+
if (idx === -1)
|
|
104
|
+
return;
|
|
105
|
+
this.entries[idx] = {
|
|
106
|
+
...this.entries[idx],
|
|
107
|
+
baseline_results: sideEffects.baseline_results,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async withScheduleMutation(mutate) {
|
|
111
|
+
return this.withScheduleFileLock(async () => {
|
|
112
|
+
const previousEntries = this.entries;
|
|
113
|
+
await this.refreshEntriesForMutation();
|
|
114
|
+
try {
|
|
115
|
+
const result = await mutate();
|
|
116
|
+
await this.saveEntries();
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
this.entries = previousEntries;
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
async withScheduleFileLock(work) {
|
|
126
|
+
if (this.scheduleLockDepth > 0) {
|
|
127
|
+
return work();
|
|
128
|
+
}
|
|
129
|
+
const release = await this.acquireScheduleFileLock();
|
|
130
|
+
this.scheduleLockDepth++;
|
|
131
|
+
try {
|
|
132
|
+
return await work();
|
|
81
133
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return existing;
|
|
134
|
+
finally {
|
|
135
|
+
this.scheduleLockDepth--;
|
|
136
|
+
await release();
|
|
86
137
|
}
|
|
87
|
-
|
|
138
|
+
}
|
|
139
|
+
async acquireScheduleFileLock() {
|
|
140
|
+
await fsp.mkdir(this.baseDir, { recursive: true });
|
|
141
|
+
const startedAt = Date.now();
|
|
142
|
+
while (true) {
|
|
143
|
+
try {
|
|
144
|
+
await fsp.mkdir(this.schedulesLockPath);
|
|
145
|
+
await fsp.writeFile(path.join(this.schedulesLockPath, "owner.json"), JSON.stringify({ pid: process.pid, created_at: new Date().toISOString() }), "utf-8");
|
|
146
|
+
return async () => {
|
|
147
|
+
await fsp.rm(this.schedulesLockPath, { recursive: true, force: true });
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
const code = error.code;
|
|
152
|
+
if (code !== "EEXIST") {
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
await this.removeStaleScheduleLock().catch(() => undefined);
|
|
156
|
+
if (Date.now() - startedAt >= SCHEDULE_LOCK_TIMEOUT_MS) {
|
|
157
|
+
throw new Error(`Timed out waiting for schedule file lock at ${this.schedulesLockPath}`);
|
|
158
|
+
}
|
|
159
|
+
await new Promise((resolve) => setTimeout(resolve, SCHEDULE_LOCK_RETRY_MS));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async removeStaleScheduleLock() {
|
|
164
|
+
const stat = await fsp.stat(this.schedulesLockPath);
|
|
165
|
+
if (Date.now() - stat.mtimeMs <= SCHEDULE_LOCK_STALE_MS) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const raw = await fsp.readFile(path.join(this.schedulesLockPath, "owner.json"), "utf-8");
|
|
170
|
+
const owner = JSON.parse(raw);
|
|
171
|
+
if (typeof owner.pid === "number") {
|
|
172
|
+
try {
|
|
173
|
+
process.kill(owner.pid, 0);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// Owner is gone; stale lock can be removed.
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Missing or malformed owner data is treated as stale after the age threshold.
|
|
183
|
+
}
|
|
184
|
+
if (Date.now() - stat.mtimeMs > SCHEDULE_LOCK_STALE_MS) {
|
|
185
|
+
await fsp.rm(this.schedulesLockPath, { recursive: true, force: true });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async ensureSoilPublishSchedule() {
|
|
189
|
+
return this.withScheduleMutation(async () => {
|
|
190
|
+
const configured = await hasConfiguredSoilPublishProvider({ baseDir: this.baseDir });
|
|
191
|
+
if (!configured) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
const existing = this.entries.find((entry) => entry.layer === "cron" &&
|
|
195
|
+
(entry.cron?.job_kind === "soil_publish" || entry.metadata?.preset_key === "soil_publish"));
|
|
196
|
+
if (existing) {
|
|
197
|
+
return existing;
|
|
198
|
+
}
|
|
199
|
+
return this.addEntryInMemory(buildSchedulePresetEntry({ preset: "soil_publish" }));
|
|
200
|
+
});
|
|
88
201
|
}
|
|
89
202
|
async projectCurrentSchedulesToSoil() {
|
|
90
203
|
try {
|
|
@@ -104,116 +217,121 @@ export class ScheduleEngine {
|
|
|
104
217
|
return this.baseDir;
|
|
105
218
|
}
|
|
106
219
|
async syncExternalSources(sources) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
const rawEntries = await source.fetchEntries();
|
|
121
|
-
const sourceIdsFromFetchedEntries = new Set([source.id]);
|
|
122
|
-
let sourceHadEntryErrors = false;
|
|
123
|
-
for (const raw of rawEntries) {
|
|
124
|
-
const parsed = ExternalScheduleEntrySchema.safeParse(raw);
|
|
125
|
-
if (!parsed.success) {
|
|
126
|
-
sourceHadEntryErrors = true;
|
|
127
|
-
skipped++;
|
|
128
|
-
errors.push({ source_id: source.id, message: parsed.error.message });
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
const external = parsed.data;
|
|
132
|
-
sourceIdsFromFetchedEntries.add(external.source_id);
|
|
133
|
-
const entryInput = this.buildExternalScheduleEntryInput(external);
|
|
134
|
-
if (!entryInput) {
|
|
135
|
-
sourceHadEntryErrors = true;
|
|
136
|
-
skipped++;
|
|
137
|
-
errors.push({ source_id: external.source_id, message: `missing ${external.layer} config for ${external.external_id}` });
|
|
220
|
+
return this.withScheduleMutation(async () => {
|
|
221
|
+
const seenKeys = new Set();
|
|
222
|
+
const reconciledSourceIds = new Set();
|
|
223
|
+
const errors = [];
|
|
224
|
+
let added = 0;
|
|
225
|
+
let updated = 0;
|
|
226
|
+
let skipped = 0;
|
|
227
|
+
for (const source of sources) {
|
|
228
|
+
try {
|
|
229
|
+
const health = await source.healthCheck();
|
|
230
|
+
if (!health.healthy) {
|
|
231
|
+
errors.push({ source_id: source.id, message: health.error ?? "source is unhealthy" });
|
|
138
232
|
continue;
|
|
139
233
|
}
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
234
|
+
const rawEntries = await source.fetchEntries();
|
|
235
|
+
const sourceIdsFromFetchedEntries = new Set([source.id]);
|
|
236
|
+
let sourceHadEntryErrors = false;
|
|
237
|
+
for (const raw of rawEntries) {
|
|
238
|
+
const parsed = ExternalScheduleEntrySchema.safeParse(raw);
|
|
239
|
+
if (!parsed.success) {
|
|
240
|
+
sourceHadEntryErrors = true;
|
|
241
|
+
skipped++;
|
|
242
|
+
errors.push({ source_id: source.id, message: parsed.error.message });
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const external = parsed.data;
|
|
246
|
+
sourceIdsFromFetchedEntries.add(external.source_id);
|
|
247
|
+
const entryInput = this.buildExternalScheduleEntryInput(external);
|
|
248
|
+
if (!entryInput) {
|
|
249
|
+
sourceHadEntryErrors = true;
|
|
250
|
+
skipped++;
|
|
251
|
+
errors.push({ source_id: external.source_id, message: `missing ${external.layer} config for ${external.external_id}` });
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const key = this.externalEntryKey(external.source_id, external.external_id);
|
|
255
|
+
seenKeys.add(key);
|
|
256
|
+
const existingIndex = this.entries.findIndex((entry) => entry.metadata?.source === "external" &&
|
|
257
|
+
entry.metadata.external_source_id === external.source_id &&
|
|
258
|
+
entry.metadata.external_id === external.external_id);
|
|
259
|
+
if (existingIndex === -1) {
|
|
260
|
+
this.entries.push(ScheduleEntrySchema.parse({
|
|
261
|
+
...entryInput,
|
|
262
|
+
id: randomUUID(),
|
|
263
|
+
created_at: new Date().toISOString(),
|
|
264
|
+
updated_at: new Date().toISOString(),
|
|
265
|
+
last_fired_at: null,
|
|
266
|
+
next_fire_at: this.computeNextFireAt(entryInput.trigger),
|
|
267
|
+
consecutive_failures: 0,
|
|
268
|
+
last_escalation_at: null,
|
|
269
|
+
baseline_results: [],
|
|
270
|
+
total_executions: 0,
|
|
271
|
+
total_tokens_used: 0,
|
|
272
|
+
}));
|
|
273
|
+
added++;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
const existing = this.entries[existingIndex];
|
|
277
|
+
const candidate = ScheduleEntrySchema.parse({
|
|
278
|
+
...existing,
|
|
147
279
|
...entryInput,
|
|
148
|
-
id:
|
|
149
|
-
created_at:
|
|
150
|
-
updated_at:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
last_escalation_at: null,
|
|
155
|
-
baseline_results: [],
|
|
156
|
-
total_executions: 0,
|
|
157
|
-
total_tokens_used: 0,
|
|
158
|
-
}));
|
|
159
|
-
added++;
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
const existing = this.entries[existingIndex];
|
|
163
|
-
const candidate = ScheduleEntrySchema.parse({
|
|
164
|
-
...existing,
|
|
165
|
-
...entryInput,
|
|
166
|
-
id: existing.id,
|
|
167
|
-
created_at: existing.created_at,
|
|
168
|
-
updated_at: existing.updated_at,
|
|
169
|
-
next_fire_at: JSON.stringify(existing.trigger) === JSON.stringify(entryInput.trigger)
|
|
170
|
-
? existing.next_fire_at
|
|
171
|
-
: this.computeNextFireAt(entryInput.trigger),
|
|
172
|
-
});
|
|
173
|
-
if (JSON.stringify(existing) !== JSON.stringify(candidate)) {
|
|
174
|
-
this.entries[existingIndex] = ScheduleEntrySchema.parse({
|
|
175
|
-
...candidate,
|
|
176
|
-
updated_at: new Date().toISOString(),
|
|
280
|
+
id: existing.id,
|
|
281
|
+
created_at: existing.created_at,
|
|
282
|
+
updated_at: existing.updated_at,
|
|
283
|
+
next_fire_at: JSON.stringify(existing.trigger) === JSON.stringify(entryInput.trigger)
|
|
284
|
+
? existing.next_fire_at
|
|
285
|
+
: this.computeNextFireAt(entryInput.trigger),
|
|
177
286
|
});
|
|
178
|
-
|
|
287
|
+
if (JSON.stringify(existing) !== JSON.stringify(candidate)) {
|
|
288
|
+
this.entries[existingIndex] = ScheduleEntrySchema.parse({
|
|
289
|
+
...candidate,
|
|
290
|
+
updated_at: new Date().toISOString(),
|
|
291
|
+
});
|
|
292
|
+
updated++;
|
|
293
|
+
}
|
|
179
294
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
295
|
+
if (!sourceHadEntryErrors) {
|
|
296
|
+
for (const sourceId of sourceIdsFromFetchedEntries) {
|
|
297
|
+
reconciledSourceIds.add(sourceId);
|
|
298
|
+
}
|
|
184
299
|
}
|
|
185
300
|
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
errors.push({ source_id: source.id, message: error instanceof Error ? error.message : String(error) });
|
|
303
|
+
}
|
|
186
304
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
...entry,
|
|
206
|
-
enabled: false,
|
|
207
|
-
updated_at: new Date().toISOString(),
|
|
305
|
+
let disabled = 0;
|
|
306
|
+
this.entries = this.entries.map((entry) => {
|
|
307
|
+
if (entry.metadata?.source !== "external" ||
|
|
308
|
+
!entry.enabled ||
|
|
309
|
+
!entry.metadata.external_source_id ||
|
|
310
|
+
!reconciledSourceIds.has(entry.metadata.external_source_id)) {
|
|
311
|
+
return entry;
|
|
312
|
+
}
|
|
313
|
+
const key = this.externalEntryKey(entry.metadata.external_source_id, entry.metadata.external_id ?? "");
|
|
314
|
+
if (seenKeys.has(key)) {
|
|
315
|
+
return entry;
|
|
316
|
+
}
|
|
317
|
+
disabled++;
|
|
318
|
+
return ScheduleEntrySchema.parse({
|
|
319
|
+
...entry,
|
|
320
|
+
enabled: false,
|
|
321
|
+
updated_at: new Date().toISOString(),
|
|
322
|
+
});
|
|
208
323
|
});
|
|
324
|
+
if (added > 0 || updated > 0 || disabled > 0) {
|
|
325
|
+
await this.saveEntries();
|
|
326
|
+
}
|
|
327
|
+
return { added, updated, disabled, skipped, errors };
|
|
209
328
|
});
|
|
210
|
-
if (added > 0 || updated > 0 || disabled > 0) {
|
|
211
|
-
await this.saveEntries();
|
|
212
|
-
}
|
|
213
|
-
return { added, updated, disabled, skipped, errors };
|
|
214
329
|
}
|
|
215
330
|
// ─── Entry management ───
|
|
216
331
|
async addEntry(input) {
|
|
332
|
+
return this.withScheduleMutation(async () => this.addEntryInMemory(input));
|
|
333
|
+
}
|
|
334
|
+
addEntryInMemory(input) {
|
|
217
335
|
const now = new Date().toISOString();
|
|
218
336
|
const entry = ScheduleEntrySchema.parse({
|
|
219
337
|
...input,
|
|
@@ -229,90 +347,84 @@ export class ScheduleEngine {
|
|
|
229
347
|
total_tokens_used: 0,
|
|
230
348
|
});
|
|
231
349
|
this.entries.push(entry);
|
|
232
|
-
await this.saveEntries();
|
|
233
350
|
return entry;
|
|
234
351
|
}
|
|
235
352
|
async removeEntry(id) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
353
|
+
return this.withScheduleMutation(async () => {
|
|
354
|
+
const before = this.entries.length;
|
|
355
|
+
this.entries = this.entries.filter((e) => e.id !== id);
|
|
356
|
+
if (this.entries.length === before)
|
|
357
|
+
return false;
|
|
358
|
+
return true;
|
|
359
|
+
});
|
|
242
360
|
}
|
|
243
361
|
async updateEntry(id, patch) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
patch.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const current = this.entries[idx];
|
|
260
|
-
const layerConfigFields = [
|
|
261
|
-
["heartbeat", patch.heartbeat],
|
|
262
|
-
["probe", patch.probe],
|
|
263
|
-
["cron", patch.cron],
|
|
264
|
-
["goal_trigger", patch.goal_trigger],
|
|
265
|
-
];
|
|
266
|
-
for (const [field, value] of layerConfigFields) {
|
|
267
|
-
if (value === undefined)
|
|
268
|
-
continue;
|
|
269
|
-
if (current.layer !== field) {
|
|
270
|
-
throw new Error(`Cannot update ${field} config for ${current.layer} entry`);
|
|
362
|
+
return this.withScheduleMutation(async () => {
|
|
363
|
+
const idx = this.entries.findIndex((entry) => entry.id === id);
|
|
364
|
+
if (idx === -1)
|
|
365
|
+
return null;
|
|
366
|
+
const hasUpdatableFields = patch.name !== undefined ||
|
|
367
|
+
patch.enabled !== undefined ||
|
|
368
|
+
patch.trigger !== undefined ||
|
|
369
|
+
patch.heartbeat !== undefined ||
|
|
370
|
+
patch.probe !== undefined ||
|
|
371
|
+
patch.cron !== undefined ||
|
|
372
|
+
patch.goal_trigger !== undefined ||
|
|
373
|
+
patch.escalation !== undefined ||
|
|
374
|
+
patch.retry_policy !== undefined;
|
|
375
|
+
if (!hasUpdatableFields) {
|
|
376
|
+
throw new Error("No updatable fields provided");
|
|
271
377
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (patch.
|
|
292
|
-
|
|
378
|
+
const current = this.entries[idx];
|
|
379
|
+
const layerConfigFields = [
|
|
380
|
+
["heartbeat", patch.heartbeat],
|
|
381
|
+
["probe", patch.probe],
|
|
382
|
+
["cron", patch.cron],
|
|
383
|
+
["goal_trigger", patch.goal_trigger],
|
|
384
|
+
];
|
|
385
|
+
for (const [field, value] of layerConfigFields) {
|
|
386
|
+
if (value === undefined)
|
|
387
|
+
continue;
|
|
388
|
+
if (current.layer !== field) {
|
|
389
|
+
throw new Error(`Cannot update ${field} config for ${current.layer} entry`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const nextEntry = { ...current };
|
|
393
|
+
if (patch.name !== undefined)
|
|
394
|
+
nextEntry.name = patch.name;
|
|
395
|
+
if (patch.enabled !== undefined)
|
|
396
|
+
nextEntry.enabled = patch.enabled;
|
|
397
|
+
if (patch.trigger !== undefined)
|
|
398
|
+
nextEntry.trigger = patch.trigger;
|
|
399
|
+
if (patch.heartbeat !== undefined)
|
|
400
|
+
nextEntry.heartbeat = patch.heartbeat;
|
|
401
|
+
if (patch.probe !== undefined)
|
|
402
|
+
nextEntry.probe = patch.probe;
|
|
403
|
+
if (patch.cron !== undefined)
|
|
404
|
+
nextEntry.cron = patch.cron;
|
|
405
|
+
if (patch.goal_trigger !== undefined)
|
|
406
|
+
nextEntry.goal_trigger = patch.goal_trigger;
|
|
407
|
+
if (patch.retry_policy !== undefined)
|
|
408
|
+
nextEntry.retry_policy = patch.retry_policy;
|
|
409
|
+
if (patch.escalation !== undefined) {
|
|
410
|
+
if (patch.escalation === null) {
|
|
411
|
+
delete nextEntry.escalation;
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
nextEntry.escalation = patch.escalation;
|
|
415
|
+
}
|
|
293
416
|
}
|
|
294
|
-
|
|
295
|
-
nextEntry.
|
|
417
|
+
if (patch.trigger !== undefined || (current.enabled === false && patch.enabled === true)) {
|
|
418
|
+
nextEntry.next_fire_at = this.computeNextFireAt(nextEntry.trigger);
|
|
419
|
+
nextEntry.retry_state = null;
|
|
296
420
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const previousEntries = this.entries;
|
|
305
|
-
const nextEntries = [...this.entries];
|
|
306
|
-
nextEntries[idx] = parsedEntry;
|
|
307
|
-
this.entries = nextEntries;
|
|
308
|
-
try {
|
|
309
|
-
await this.saveEntries();
|
|
310
|
-
}
|
|
311
|
-
catch (error) {
|
|
312
|
-
this.entries = previousEntries;
|
|
313
|
-
throw error;
|
|
314
|
-
}
|
|
315
|
-
return parsedEntry;
|
|
421
|
+
nextEntry.updated_at = new Date().toISOString();
|
|
422
|
+
const parsedEntry = ScheduleEntrySchema.parse(nextEntry);
|
|
423
|
+
const nextEntries = [...this.entries];
|
|
424
|
+
nextEntries[idx] = parsedEntry;
|
|
425
|
+
this.entries = nextEntries;
|
|
426
|
+
return parsedEntry;
|
|
427
|
+
});
|
|
316
428
|
}
|
|
317
429
|
// ─── Scheduling ───
|
|
318
430
|
async getDueEntries() {
|
|
@@ -324,7 +436,10 @@ export class ScheduleEngine {
|
|
|
324
436
|
return filtered.slice(-limit);
|
|
325
437
|
}
|
|
326
438
|
async runEntryNow(entryId, options = {}) {
|
|
327
|
-
const entry = this.
|
|
439
|
+
const entry = await this.withScheduleFileLock(async () => {
|
|
440
|
+
await this.refreshEntriesForMutation();
|
|
441
|
+
return this.entries.find((candidate) => candidate.id === entryId) ?? null;
|
|
442
|
+
});
|
|
328
443
|
if (!entry) {
|
|
329
444
|
return null;
|
|
330
445
|
}
|
|
@@ -335,7 +450,18 @@ export class ScheduleEngine {
|
|
|
335
450
|
next_fire_at: scheduledFor,
|
|
336
451
|
};
|
|
337
452
|
const executedResult = await this.executeEntry(immediateEntry);
|
|
338
|
-
const
|
|
453
|
+
const sideEffects = entry.layer === "probe"
|
|
454
|
+
? this.captureExecutionSideEffects(entry.id)
|
|
455
|
+
: null;
|
|
456
|
+
const applied = await this.withScheduleFileLock(async () => {
|
|
457
|
+
await this.refreshEntriesForMutation();
|
|
458
|
+
const outcome = await this.applyExecutionOutcome(entry.id, executedResult, "manual_run", scheduledFor, { preserveEnabled: options.preserveEnabled ?? true });
|
|
459
|
+
if (outcome) {
|
|
460
|
+
this.applyExecutionSideEffects(entry.id, sideEffects);
|
|
461
|
+
await this.writeEntriesAndProject();
|
|
462
|
+
}
|
|
463
|
+
return outcome;
|
|
464
|
+
});
|
|
339
465
|
let finalResult = executedResult;
|
|
340
466
|
if (options.allowEscalation && applied?.entry) {
|
|
341
467
|
const escalationResult = await this.checkEscalation(applied.entry, executedResult);
|
|
@@ -344,7 +470,6 @@ export class ScheduleEngine {
|
|
|
344
470
|
}
|
|
345
471
|
}
|
|
346
472
|
if (applied) {
|
|
347
|
-
await this.saveEntries();
|
|
348
473
|
await this.recordHistory({
|
|
349
474
|
entry_id: applied.entry?.id ?? entry.id,
|
|
350
475
|
entry_name: applied.entry?.name ?? entry.name,
|
|
@@ -386,28 +511,44 @@ export class ScheduleEngine {
|
|
|
386
511
|
});
|
|
387
512
|
}
|
|
388
513
|
async tick() {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
this.entries[i]
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
514
|
+
const due = await this.withScheduleFileLock(async () => {
|
|
515
|
+
await this.refreshEntriesForMutation();
|
|
516
|
+
// Reset daily budget for entries whose budget_reset_at is null or in the past
|
|
517
|
+
const nowMs = Date.now();
|
|
518
|
+
let budgetReset = false;
|
|
519
|
+
for (let i = 0; i < this.entries.length; i++) {
|
|
520
|
+
const e = this.entries[i];
|
|
521
|
+
if (!e.budget_reset_at || new Date(e.budget_reset_at).getTime() <= nowMs) {
|
|
522
|
+
this.entries[i] = {
|
|
523
|
+
...e,
|
|
524
|
+
tokens_used_today: 0,
|
|
525
|
+
budget_reset_at: new Date(nowMs + 24 * 60 * 60 * 1000).toISOString(),
|
|
526
|
+
};
|
|
527
|
+
budgetReset = true;
|
|
528
|
+
}
|
|
401
529
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
530
|
+
if (budgetReset) {
|
|
531
|
+
await this.writeEntriesAndProject();
|
|
532
|
+
}
|
|
533
|
+
return this.getDueEntryDescriptors();
|
|
534
|
+
});
|
|
407
535
|
const results = [];
|
|
408
536
|
for (const descriptor of due) {
|
|
409
537
|
const executedResult = await this.executeEntry(descriptor.entry);
|
|
410
|
-
const
|
|
538
|
+
const sideEffects = descriptor.entry.layer === "probe"
|
|
539
|
+
? this.captureExecutionSideEffects(descriptor.entry.id)
|
|
540
|
+
: null;
|
|
541
|
+
const applied = await this.withScheduleFileLock(async () => {
|
|
542
|
+
await this.refreshEntriesForMutation();
|
|
543
|
+
const outcome = await this.applyExecutionOutcome(descriptor.entry.id, executedResult, descriptor.reason, descriptor.scheduledFor);
|
|
544
|
+
if (outcome) {
|
|
545
|
+
this.applyExecutionSideEffects(descriptor.entry.id, sideEffects);
|
|
546
|
+
// Persist cadence/retry advancement before history side effects so a crash
|
|
547
|
+
// cannot replay an already-fired entry from stale schedule state.
|
|
548
|
+
await this.writeEntriesAndProject();
|
|
549
|
+
}
|
|
550
|
+
return outcome;
|
|
551
|
+
});
|
|
411
552
|
let finalResult = executedResult;
|
|
412
553
|
if (applied?.entry) {
|
|
413
554
|
const escalationResult = await this.checkEscalation(applied.entry, executedResult);
|
|
@@ -416,9 +557,6 @@ export class ScheduleEngine {
|
|
|
416
557
|
}
|
|
417
558
|
}
|
|
418
559
|
if (applied) {
|
|
419
|
-
// Persist cadence/retry advancement before history side effects so a crash
|
|
420
|
-
// cannot replay an already-fired entry from stale schedule state.
|
|
421
|
-
await this.saveEntries();
|
|
422
560
|
await this.recordHistory({
|
|
423
561
|
entry_id: applied.entry?.id ?? descriptor.entry.id,
|
|
424
562
|
entry_name: applied.entry?.name ?? descriptor.entry.name,
|
|
@@ -681,7 +819,10 @@ export class ScheduleEngine {
|
|
|
681
819
|
});
|
|
682
820
|
}
|
|
683
821
|
async executeEscalationTargetEntry(targetEntryId) {
|
|
684
|
-
const targetEntry = this.
|
|
822
|
+
const targetEntry = await this.withScheduleFileLock(async () => {
|
|
823
|
+
await this.refreshEntriesForMutation();
|
|
824
|
+
return this.entries.find((candidate) => candidate.id === targetEntryId) ?? null;
|
|
825
|
+
});
|
|
685
826
|
if (!targetEntry) {
|
|
686
827
|
this.logger.warn(`Escalation target entry not found: ${targetEntryId}`);
|
|
687
828
|
return null;
|
|
@@ -692,9 +833,19 @@ export class ScheduleEngine {
|
|
|
692
833
|
next_fire_at: new Date().toISOString(),
|
|
693
834
|
};
|
|
694
835
|
const result = await this.executeEntry(immediateEntry);
|
|
695
|
-
const
|
|
836
|
+
const sideEffects = targetEntry.layer === "probe"
|
|
837
|
+
? this.captureExecutionSideEffects(targetEntry.id)
|
|
838
|
+
: null;
|
|
839
|
+
const applied = await this.withScheduleFileLock(async () => {
|
|
840
|
+
await this.refreshEntriesForMutation();
|
|
841
|
+
const outcome = await this.applyExecutionOutcome(targetEntryId, result, "escalation_target", immediateEntry.next_fire_at);
|
|
842
|
+
if (outcome) {
|
|
843
|
+
this.applyExecutionSideEffects(targetEntryId, sideEffects);
|
|
844
|
+
await this.writeEntriesAndProject();
|
|
845
|
+
}
|
|
846
|
+
return outcome;
|
|
847
|
+
});
|
|
696
848
|
if (applied) {
|
|
697
|
-
await this.saveEntries();
|
|
698
849
|
await this.recordHistory({
|
|
699
850
|
entry_id: targetEntry.id,
|
|
700
851
|
entry_name: targetEntry.name,
|
|
@@ -755,55 +906,63 @@ export class ScheduleEngine {
|
|
|
755
906
|
}
|
|
756
907
|
// ─── Escalation logic ───
|
|
757
908
|
async checkEscalation(entry, result) {
|
|
758
|
-
const esc = entry.escalation;
|
|
759
|
-
if (!esc?.enabled)
|
|
760
|
-
return null;
|
|
761
909
|
const isFailure = result.status === "error" || result.status === "down";
|
|
762
910
|
if (!isFailure)
|
|
763
911
|
return null;
|
|
764
|
-
const
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
if (now - lastEsc < esc.cooldown_minutes * 60 * 1000) {
|
|
769
|
-
this.logger.info(`Escalation for "${entry.name}" suppressed (cooldown)`);
|
|
912
|
+
const escalationEntry = await this.withScheduleFileLock(async () => {
|
|
913
|
+
await this.refreshEntriesForMutation();
|
|
914
|
+
const idx = this.entries.findIndex((e) => e.id === entry.id);
|
|
915
|
+
if (idx === -1) {
|
|
770
916
|
return null;
|
|
771
917
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
918
|
+
const current = this.entries[idx];
|
|
919
|
+
const esc = current.escalation;
|
|
920
|
+
if (!esc?.enabled)
|
|
921
|
+
return null;
|
|
922
|
+
const now = Date.now();
|
|
923
|
+
// Check cooldown
|
|
924
|
+
if (current.last_escalation_at) {
|
|
925
|
+
const lastEsc = new Date(current.last_escalation_at).getTime();
|
|
926
|
+
if (now - lastEsc < esc.cooldown_minutes * 60 * 1000) {
|
|
927
|
+
this.logger.info(`Escalation for "${current.name}" suppressed (cooldown)`);
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
// Rolling-window rate-limit: check escalation_timestamps within the last hour
|
|
932
|
+
const hourAgo = now - 60 * 60 * 1000;
|
|
933
|
+
const recentTimestamps = (current.escalation_timestamps ?? []).filter((ts) => new Date(ts).getTime() > hourAgo);
|
|
934
|
+
if (recentTimestamps.length >= esc.max_per_hour) {
|
|
935
|
+
this.logger.info(`Escalation for "${current.name}" suppressed (max_per_hour=${esc.max_per_hour} reached)`);
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
// Update last_escalation_at and rolling-window escalation_timestamps.
|
|
939
|
+
const nowIso = new Date(now).toISOString();
|
|
785
940
|
const prunedTimestamps = [
|
|
786
|
-
...(
|
|
941
|
+
...(current.escalation_timestamps ?? []).filter((ts) => new Date(ts).getTime() > hourAgo),
|
|
787
942
|
nowIso,
|
|
788
943
|
];
|
|
789
944
|
this.entries[idx] = {
|
|
790
|
-
...
|
|
945
|
+
...current,
|
|
791
946
|
last_escalation_at: nowIso,
|
|
792
947
|
escalation_timestamps: prunedTimestamps,
|
|
793
948
|
};
|
|
794
|
-
await this.
|
|
795
|
-
|
|
949
|
+
await this.writeEntriesAndProject();
|
|
950
|
+
return this.entries[idx];
|
|
951
|
+
});
|
|
952
|
+
if (!escalationEntry?.escalation)
|
|
953
|
+
return null;
|
|
954
|
+
const esc = escalationEntry.escalation;
|
|
796
955
|
// Dispatch escalation notification
|
|
797
956
|
await this.dispatchNotification({
|
|
798
957
|
report_type: "schedule_escalation",
|
|
799
|
-
entry_id:
|
|
800
|
-
entry_name:
|
|
958
|
+
entry_id: escalationEntry.id,
|
|
959
|
+
entry_name: escalationEntry.name,
|
|
801
960
|
target_layer: esc.target_layer,
|
|
802
961
|
target_entry_id: esc.target_entry_id,
|
|
803
962
|
target_goal_id: esc.target_goal_id,
|
|
804
|
-
consecutive_failures:
|
|
963
|
+
consecutive_failures: escalationEntry.consecutive_failures,
|
|
805
964
|
});
|
|
806
|
-
this.logger.warn(`Escalating "${
|
|
965
|
+
this.logger.warn(`Escalating "${escalationEntry.name}" to ${esc.target_layer ?? "unknown"} (failures=${escalationEntry.consecutive_failures})`);
|
|
807
966
|
// Execute target goal or target entry immediately so escalations take effect in the same tick.
|
|
808
967
|
if (esc.target_goal_id) {
|
|
809
968
|
await this.executeEscalationTargetGoal(esc.target_goal_id);
|