nemoris 0.1.0
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/.env.example +49 -0
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/SECURITY.md +119 -0
- package/bin/nemoris +46 -0
- package/config/agents/agent.toml.example +28 -0
- package/config/agents/default.toml +22 -0
- package/config/agents/orchestrator.toml +18 -0
- package/config/delivery.toml +73 -0
- package/config/embeddings.toml +5 -0
- package/config/identity/default-purpose.md +1 -0
- package/config/identity/default-soul.md +3 -0
- package/config/identity/orchestrator-purpose.md +1 -0
- package/config/identity/orchestrator-soul.md +1 -0
- package/config/improvement-targets.toml +15 -0
- package/config/jobs/heartbeat-check.toml +30 -0
- package/config/jobs/memory-rollup.toml +46 -0
- package/config/jobs/workspace-health.toml +63 -0
- package/config/mcp.toml +16 -0
- package/config/output-contracts.toml +17 -0
- package/config/peers.toml +32 -0
- package/config/peers.toml.example +32 -0
- package/config/policies/memory-default.toml +10 -0
- package/config/policies/memory-heartbeat.toml +5 -0
- package/config/policies/memory-ops.toml +10 -0
- package/config/policies/tools-heartbeat-minimal.toml +8 -0
- package/config/policies/tools-interactive-safe.toml +8 -0
- package/config/policies/tools-ops-bounded.toml +8 -0
- package/config/policies/tools-orchestrator.toml +7 -0
- package/config/providers/anthropic.toml +15 -0
- package/config/providers/ollama.toml +5 -0
- package/config/providers/openai-codex.toml +9 -0
- package/config/providers/openrouter.toml +5 -0
- package/config/router.toml +22 -0
- package/config/runtime.toml +114 -0
- package/config/skills/self-improvement.toml +15 -0
- package/config/skills/telegram-onboarding-spec.md +240 -0
- package/config/skills/workspace-monitor.toml +15 -0
- package/config/task-router.toml +42 -0
- package/install.sh +50 -0
- package/package.json +90 -0
- package/src/auth/auth-profiles.js +169 -0
- package/src/auth/openai-codex-oauth.js +285 -0
- package/src/battle.js +449 -0
- package/src/cli/help.js +265 -0
- package/src/cli/output-filter.js +49 -0
- package/src/cli/runtime-control.js +704 -0
- package/src/cli-main.js +2763 -0
- package/src/cli.js +78 -0
- package/src/config/loader.js +332 -0
- package/src/config/schema-validator.js +214 -0
- package/src/config/toml-lite.js +8 -0
- package/src/daemon/action-handlers.js +71 -0
- package/src/daemon/healing-tick.js +87 -0
- package/src/daemon/health-probes.js +90 -0
- package/src/daemon/notifier.js +57 -0
- package/src/daemon/nurse.js +218 -0
- package/src/daemon/repair-log.js +106 -0
- package/src/daemon/rule-staging.js +90 -0
- package/src/daemon/rules.js +29 -0
- package/src/daemon/telegram-commands.js +54 -0
- package/src/daemon/updater.js +85 -0
- package/src/jobs/job-runner.js +78 -0
- package/src/mcp/consumer.js +129 -0
- package/src/memory/active-recall.js +171 -0
- package/src/memory/backend-manager.js +97 -0
- package/src/memory/backends/file-backend.js +38 -0
- package/src/memory/backends/qmd-backend.js +219 -0
- package/src/memory/embedding-guards.js +24 -0
- package/src/memory/embedding-index.js +118 -0
- package/src/memory/embedding-service.js +179 -0
- package/src/memory/file-index.js +177 -0
- package/src/memory/memory-signature.js +5 -0
- package/src/memory/memory-store.js +648 -0
- package/src/memory/retrieval-planner.js +66 -0
- package/src/memory/scoring.js +145 -0
- package/src/memory/simhash.js +78 -0
- package/src/memory/sqlite-active-store.js +824 -0
- package/src/memory/write-policy.js +36 -0
- package/src/onboarding/aliases.js +33 -0
- package/src/onboarding/auth/api-key.js +224 -0
- package/src/onboarding/auth/ollama-detect.js +42 -0
- package/src/onboarding/clack-prompter.js +77 -0
- package/src/onboarding/doctor.js +530 -0
- package/src/onboarding/lock.js +42 -0
- package/src/onboarding/model-catalog.js +344 -0
- package/src/onboarding/phases/auth.js +589 -0
- package/src/onboarding/phases/build.js +130 -0
- package/src/onboarding/phases/choose.js +82 -0
- package/src/onboarding/phases/detect.js +98 -0
- package/src/onboarding/phases/hatch.js +216 -0
- package/src/onboarding/phases/identity.js +79 -0
- package/src/onboarding/phases/ollama.js +345 -0
- package/src/onboarding/phases/scaffold.js +99 -0
- package/src/onboarding/phases/telegram.js +377 -0
- package/src/onboarding/phases/validate.js +204 -0
- package/src/onboarding/phases/verify.js +206 -0
- package/src/onboarding/platform.js +482 -0
- package/src/onboarding/status-bar.js +95 -0
- package/src/onboarding/templates.js +794 -0
- package/src/onboarding/toml-writer.js +38 -0
- package/src/onboarding/tui.js +250 -0
- package/src/onboarding/uninstall.js +153 -0
- package/src/onboarding/wizard.js +499 -0
- package/src/providers/anthropic.js +168 -0
- package/src/providers/base.js +247 -0
- package/src/providers/circuit-breaker.js +136 -0
- package/src/providers/ollama.js +163 -0
- package/src/providers/openai-codex.js +149 -0
- package/src/providers/openrouter.js +136 -0
- package/src/providers/registry.js +36 -0
- package/src/providers/router.js +16 -0
- package/src/runtime/bootstrap-cache.js +47 -0
- package/src/runtime/capabilities-prompt.js +25 -0
- package/src/runtime/completion-ping.js +99 -0
- package/src/runtime/config-validator.js +121 -0
- package/src/runtime/context-ledger.js +360 -0
- package/src/runtime/cutover-readiness.js +42 -0
- package/src/runtime/daemon.js +729 -0
- package/src/runtime/delivery-ack.js +195 -0
- package/src/runtime/delivery-adapters/local-file.js +41 -0
- package/src/runtime/delivery-adapters/openclaw-cli.js +94 -0
- package/src/runtime/delivery-adapters/openclaw-peer.js +98 -0
- package/src/runtime/delivery-adapters/shadow.js +13 -0
- package/src/runtime/delivery-adapters/standalone-http.js +98 -0
- package/src/runtime/delivery-adapters/telegram.js +104 -0
- package/src/runtime/delivery-adapters/tui.js +128 -0
- package/src/runtime/delivery-manager.js +807 -0
- package/src/runtime/delivery-store.js +168 -0
- package/src/runtime/dependency-health.js +118 -0
- package/src/runtime/envelope.js +114 -0
- package/src/runtime/evaluation.js +1089 -0
- package/src/runtime/exec-approvals.js +216 -0
- package/src/runtime/executor.js +500 -0
- package/src/runtime/failure-ping.js +67 -0
- package/src/runtime/flows.js +83 -0
- package/src/runtime/guards.js +45 -0
- package/src/runtime/handoff.js +51 -0
- package/src/runtime/identity-cache.js +28 -0
- package/src/runtime/improvement-engine.js +109 -0
- package/src/runtime/improvement-harness.js +581 -0
- package/src/runtime/input-sanitiser.js +72 -0
- package/src/runtime/interaction-contract.js +347 -0
- package/src/runtime/lane-readiness.js +226 -0
- package/src/runtime/migration.js +323 -0
- package/src/runtime/model-resolution.js +78 -0
- package/src/runtime/network.js +64 -0
- package/src/runtime/notification-store.js +97 -0
- package/src/runtime/notifier.js +256 -0
- package/src/runtime/orchestrator.js +53 -0
- package/src/runtime/orphan-reaper.js +41 -0
- package/src/runtime/output-contract-schema.js +139 -0
- package/src/runtime/output-contract-validator.js +439 -0
- package/src/runtime/peer-readiness.js +69 -0
- package/src/runtime/peer-registry.js +133 -0
- package/src/runtime/pilot-status.js +108 -0
- package/src/runtime/prompt-builder.js +261 -0
- package/src/runtime/provider-attempt.js +582 -0
- package/src/runtime/report-fallback.js +71 -0
- package/src/runtime/result-normalizer.js +183 -0
- package/src/runtime/retention.js +74 -0
- package/src/runtime/review.js +244 -0
- package/src/runtime/route-job.js +15 -0
- package/src/runtime/run-store.js +38 -0
- package/src/runtime/schedule.js +88 -0
- package/src/runtime/scheduler-state.js +434 -0
- package/src/runtime/scheduler.js +656 -0
- package/src/runtime/session-compactor.js +182 -0
- package/src/runtime/session-search.js +155 -0
- package/src/runtime/slack-inbound.js +249 -0
- package/src/runtime/ssrf.js +102 -0
- package/src/runtime/status-aggregator.js +330 -0
- package/src/runtime/task-contract.js +140 -0
- package/src/runtime/task-packet.js +107 -0
- package/src/runtime/task-router.js +140 -0
- package/src/runtime/telegram-inbound.js +1565 -0
- package/src/runtime/token-counter.js +134 -0
- package/src/runtime/token-estimator.js +59 -0
- package/src/runtime/tool-loop.js +200 -0
- package/src/runtime/transport-server.js +311 -0
- package/src/runtime/tui-server.js +411 -0
- package/src/runtime/ulid.js +44 -0
- package/src/security/ssrf-check.js +197 -0
- package/src/setup.js +369 -0
- package/src/shadow/bridge.js +303 -0
- package/src/skills/loader.js +84 -0
- package/src/tools/catalog.json +49 -0
- package/src/tools/cli-delegate.js +44 -0
- package/src/tools/mcp-client.js +106 -0
- package/src/tools/micro/cancel-task.js +6 -0
- package/src/tools/micro/complete-task.js +6 -0
- package/src/tools/micro/fail-task.js +6 -0
- package/src/tools/micro/http-fetch.js +74 -0
- package/src/tools/micro/index.js +36 -0
- package/src/tools/micro/lcm-recall.js +60 -0
- package/src/tools/micro/list-dir.js +17 -0
- package/src/tools/micro/list-skills.js +46 -0
- package/src/tools/micro/load-skill.js +38 -0
- package/src/tools/micro/memory-search.js +45 -0
- package/src/tools/micro/read-file.js +11 -0
- package/src/tools/micro/session-search.js +54 -0
- package/src/tools/micro/shell-exec.js +43 -0
- package/src/tools/micro/trigger-job.js +79 -0
- package/src/tools/micro/web-search.js +58 -0
- package/src/tools/micro/workspace-paths.js +39 -0
- package/src/tools/micro/write-file.js +14 -0
- package/src/tools/micro/write-memory.js +41 -0
- package/src/tools/registry.js +348 -0
- package/src/tools/tool-result-contract.js +36 -0
- package/src/tui/chat.js +835 -0
- package/src/tui/renderer.js +175 -0
- package/src/tui/socket-client.js +217 -0
- package/src/utils/canonical-json.js +29 -0
- package/src/utils/compaction.js +30 -0
- package/src/utils/env-loader.js +5 -0
- package/src/utils/errors.js +80 -0
- package/src/utils/fs.js +101 -0
- package/src/utils/ids.js +5 -0
- package/src/utils/model-context-limits.js +30 -0
- package/src/utils/token-budget.js +74 -0
- package/src/utils/usage-cost.js +25 -0
- package/src/utils/usage-metrics.js +14 -0
- package/vendor/smol-toml-1.5.2.tgz +0 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { renderContractOutput } from "./output-contract-validator.js";
|
|
2
|
+
import { estimateCost } from "../utils/usage-cost.js";
|
|
3
|
+
import { extractUsageMetrics } from "../utils/usage-metrics.js";
|
|
4
|
+
|
|
5
|
+
export function synthesizeDryRun(plan, modelId) {
|
|
6
|
+
const memoryCount = plan.packet.layers.memory.length;
|
|
7
|
+
const summary = `Dry-run shadow execution for ${plan.job.id} prepared on ${modelId} with ${memoryCount} relevant memory items.`;
|
|
8
|
+
const output = [
|
|
9
|
+
`Job ${plan.job.id} would run on lane ${plan.job.modelLane} using model ${modelId}.`,
|
|
10
|
+
`Workspace: ${plan.agent.workspaceRoot || "unknown"}.`,
|
|
11
|
+
`Tools: ${plan.packet.layers.tools.join(", ") || "none"}.`,
|
|
12
|
+
`Memory items included: ${memoryCount}.`
|
|
13
|
+
].join(" ");
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
summary,
|
|
17
|
+
output,
|
|
18
|
+
nextActions: ["switch to provider mode for a real shadow run", "compare with live cron output"]
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function formatOutputContract(contract) {
|
|
23
|
+
const lines = [];
|
|
24
|
+
if (contract.format) lines.push(`- format: ${contract.format}`);
|
|
25
|
+
if (contract.requiredSections?.length) {
|
|
26
|
+
lines.push(`- required sections: ${contract.requiredSections.join(", ")}`);
|
|
27
|
+
}
|
|
28
|
+
if (contract.styleHints?.length) {
|
|
29
|
+
lines.push(`- style hints: ${contract.styleHints.join("; ")}`);
|
|
30
|
+
}
|
|
31
|
+
if (contract.profile?.sectionStyle) {
|
|
32
|
+
lines.push(`- section style: ${contract.profile.sectionStyle}`);
|
|
33
|
+
}
|
|
34
|
+
if (contract.profile?.requireStatus) {
|
|
35
|
+
lines.push("- requires status: yes");
|
|
36
|
+
}
|
|
37
|
+
return lines.join("\n");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function formatReportGuidance(reportGuidance) {
|
|
41
|
+
const lines = [];
|
|
42
|
+
if (reportGuidance.focus?.length) {
|
|
43
|
+
lines.push(`- focus: ${reportGuidance.focus.join("; ")}`);
|
|
44
|
+
}
|
|
45
|
+
if (reportGuidance.qualityChecks?.length) {
|
|
46
|
+
lines.push(`- quality checks: ${reportGuidance.qualityChecks.join("; ")}`);
|
|
47
|
+
}
|
|
48
|
+
if (reportGuidance.avoid?.length) {
|
|
49
|
+
lines.push(`- avoid: ${reportGuidance.avoid.join("; ")}`);
|
|
50
|
+
}
|
|
51
|
+
return lines.join("\n");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function normalizeNextActions(nextActions = []) {
|
|
55
|
+
return (nextActions || [])
|
|
56
|
+
.map((item) => {
|
|
57
|
+
if (typeof item === "string") return item.trim();
|
|
58
|
+
if (item && typeof item === "object") {
|
|
59
|
+
const action = String(item.action || "").trim();
|
|
60
|
+
const details = String(item.details || item.summary || "").trim();
|
|
61
|
+
return [action, details].filter(Boolean).join(": ");
|
|
62
|
+
}
|
|
63
|
+
return String(item || "").trim();
|
|
64
|
+
})
|
|
65
|
+
.filter(Boolean);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function hasOnlyShadowImportEvidence(plan) {
|
|
69
|
+
const memory = plan?.packet?.layers?.memory || [];
|
|
70
|
+
if (!memory.length) return false;
|
|
71
|
+
return memory.every((item) => ["shadow_import", "artifact_summary"].includes(item.category));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function normalizeResultForContract(plan, result, options = {}) {
|
|
75
|
+
const contract = plan?.packet?.layers?.outputContract || null;
|
|
76
|
+
if (!contract || options.mode !== "provider") {
|
|
77
|
+
return {
|
|
78
|
+
...result,
|
|
79
|
+
nextActions: normalizeNextActions(result.nextActions || [])
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const originalOutput = result.output;
|
|
84
|
+
const canonicalOutput = renderContractOutput(contract, originalOutput, {
|
|
85
|
+
shadowOnlyEvidence: contract.format === "structured_rollup" && hasOnlyShadowImportEvidence(plan),
|
|
86
|
+
status: result.summary
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
...result,
|
|
91
|
+
output: canonicalOutput,
|
|
92
|
+
outputStructured: originalOutput,
|
|
93
|
+
nextActions: normalizeNextActions(result.nextActions || [])
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function buildOutputTemplate(contract) {
|
|
98
|
+
if (!contract) return null;
|
|
99
|
+
if (contract.profile?.templateLines?.length) {
|
|
100
|
+
return contract.profile.templateLines.join("\n");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (contract.format === "bulleted_briefing") {
|
|
104
|
+
return [
|
|
105
|
+
"Status: <one-line status>",
|
|
106
|
+
"- Calendar: <brief update or None>",
|
|
107
|
+
"- Issues: <brief update or None>",
|
|
108
|
+
"- Weather: <brief update or None>"
|
|
109
|
+
].join("\n");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (contract.format === "structured_rollup") {
|
|
113
|
+
return [
|
|
114
|
+
"## Inbox",
|
|
115
|
+
"- <brief update or None>",
|
|
116
|
+
"",
|
|
117
|
+
"## Projects",
|
|
118
|
+
"- <brief update or None>",
|
|
119
|
+
"",
|
|
120
|
+
"## Backlog",
|
|
121
|
+
"- <brief update or None>",
|
|
122
|
+
"",
|
|
123
|
+
"## Update",
|
|
124
|
+
"- <brief update or None>"
|
|
125
|
+
].join("\n");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function normalizeProviderResponse(rawResponse, adapter) {
|
|
132
|
+
if (typeof adapter?.normalizeResponse === "function") {
|
|
133
|
+
return adapter.normalizeResponse(rawResponse);
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
summary: "Provider returned unnormalized data.",
|
|
137
|
+
output: rawResponse,
|
|
138
|
+
nextActions: [],
|
|
139
|
+
reasoning: null,
|
|
140
|
+
raw: rawResponse
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function normalizeResult({ plan, rawResponse, toolResults = [], usage, options = {} }) {
|
|
145
|
+
const mode = options.mode || "dry-run";
|
|
146
|
+
let result = null;
|
|
147
|
+
|
|
148
|
+
if (options.pendingApprovalResult) {
|
|
149
|
+
result = options.pendingApprovalResult;
|
|
150
|
+
} else if (mode === "dry-run") {
|
|
151
|
+
result = synthesizeDryRun(plan, options.modelId);
|
|
152
|
+
} else {
|
|
153
|
+
result = normalizeProviderResponse(rawResponse, options.adapter);
|
|
154
|
+
result = normalizeResultForContract(plan, result, { mode });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const usageMetrics = usage || extractUsageMetrics(rawResponse || {});
|
|
158
|
+
const costUsd = options.modelId ? estimateCost(options.modelId, usageMetrics.tokensIn, usageMetrics.tokensOut) : null;
|
|
159
|
+
|
|
160
|
+
if (!options.pendingApprovalResult && mode === "provider" && rawResponse && typeof options.logUsage === "function") {
|
|
161
|
+
try {
|
|
162
|
+
await options.logUsage({
|
|
163
|
+
plan,
|
|
164
|
+
providerId: options.providerId,
|
|
165
|
+
modelId: options.modelId,
|
|
166
|
+
usage: usageMetrics,
|
|
167
|
+
costUsd,
|
|
168
|
+
rawResponse,
|
|
169
|
+
toolResults,
|
|
170
|
+
sessionId: options.sessionId || plan.job.chat_id || "unknown"
|
|
171
|
+
});
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.error(JSON.stringify({ service: "usage_logging", error: err.message, jobId: plan.job.id }));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
result,
|
|
179
|
+
usage: usageMetrics,
|
|
180
|
+
costUsd,
|
|
181
|
+
cached: Boolean(usageMetrics.cacheIn || usageMetrics.cacheCreation)
|
|
182
|
+
};
|
|
183
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { listFilesRecursive, readJson, removePath } from "../utils/fs.js";
|
|
3
|
+
|
|
4
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
5
|
+
|
|
6
|
+
function inferTimestampFromPath(filePath) {
|
|
7
|
+
const base = path.basename(filePath, ".json");
|
|
8
|
+
const candidate = base.replace(/-/g, ":").replace(/(\d{4}):(\d{2}):(\d{2})T/, "$1-$2-$3T").replace(/:(\d{3})$/, ".$1");
|
|
9
|
+
const parsed = new Date(candidate);
|
|
10
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizeTimestamp(item) {
|
|
14
|
+
return item.timestamp || item.receivedAt || inferTimestampFromPath(item.filePath) || "1970-01-01T00:00:00.000Z";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isJsonFile(filePath) {
|
|
18
|
+
return filePath.endsWith(".json");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildRetentionPolicy(config = {}, defaults = {}) {
|
|
22
|
+
return {
|
|
23
|
+
ttlDays: Number(config.ttlDays ?? defaults.ttlDays ?? 30),
|
|
24
|
+
maxFilesPerBucket: Number(config.maxFilesPerBucket ?? defaults.maxFilesPerBucket ?? 1000)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function pruneJsonBuckets(rootDir, policy, options = {}) {
|
|
29
|
+
const now = options.now ? new Date(options.now) : new Date();
|
|
30
|
+
const ttlCutoff = now.getTime() - policy.ttlDays * DAY_MS;
|
|
31
|
+
const excluded = new Set((options.excludePaths || []).map((filePath) => path.resolve(filePath)));
|
|
32
|
+
const files = (await listFilesRecursive(rootDir)).filter((filePath) => isJsonFile(filePath) && !excluded.has(path.resolve(filePath)));
|
|
33
|
+
const buckets = new Map();
|
|
34
|
+
|
|
35
|
+
for (const filePath of files) {
|
|
36
|
+
const bucket = path.dirname(filePath);
|
|
37
|
+
if (!buckets.has(bucket)) buckets.set(bucket, []);
|
|
38
|
+
const data = await readJson(filePath, null);
|
|
39
|
+
buckets.get(bucket).push({
|
|
40
|
+
filePath,
|
|
41
|
+
timestamp: normalizeTimestamp({
|
|
42
|
+
...(data || {}),
|
|
43
|
+
filePath
|
|
44
|
+
})
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const removed = [];
|
|
49
|
+
for (const entries of buckets.values()) {
|
|
50
|
+
entries.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
51
|
+
const keep = new Set();
|
|
52
|
+
|
|
53
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
54
|
+
const item = entries[index];
|
|
55
|
+
const expired = new Date(item.timestamp).getTime() < ttlCutoff;
|
|
56
|
+
const overflow = index >= policy.maxFilesPerBucket;
|
|
57
|
+
if (!expired && !overflow) {
|
|
58
|
+
keep.add(item.filePath);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const item of entries) {
|
|
63
|
+
if (keep.has(item.filePath)) continue;
|
|
64
|
+
await removePath(item.filePath);
|
|
65
|
+
removed.push(item.filePath);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
rootDir,
|
|
71
|
+
removedCount: removed.length,
|
|
72
|
+
removed
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { listFilesRecursive, readJson } from "../utils/fs.js";
|
|
3
|
+
import { SchedulerStateStore } from "./scheduler-state.js";
|
|
4
|
+
import { NotificationStore } from "./notification-store.js";
|
|
5
|
+
import { DeliveryStore } from "./delivery-store.js";
|
|
6
|
+
|
|
7
|
+
function sortDescending(items) {
|
|
8
|
+
return [...items].sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function inferTimestampFromPath(filePath) {
|
|
12
|
+
const base = path.basename(filePath, ".json");
|
|
13
|
+
const candidate = base.replace(/-/g, ":").replace(/(\d{4}):(\d{2}):(\d{2})T/, "$1-$2-$3T").replace(/:(\d{3})$/, ".$1");
|
|
14
|
+
const parsed = new Date(candidate);
|
|
15
|
+
return Number.isNaN(parsed.getTime()) ? "1970-01-01T00:00:00.000Z" : parsed.toISOString();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function expandRelatedNotificationFiles(baseFiles, notifications) {
|
|
19
|
+
const related = new Set(baseFiles);
|
|
20
|
+
let changed = true;
|
|
21
|
+
while (changed) {
|
|
22
|
+
changed = false;
|
|
23
|
+
for (const item of notifications) {
|
|
24
|
+
if (!related.has(item.filePath)) continue;
|
|
25
|
+
for (const generated of item.generatedNotificationFiles || []) {
|
|
26
|
+
if (related.has(generated)) continue;
|
|
27
|
+
related.add(generated);
|
|
28
|
+
changed = true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return related;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function summarizeInteractionState(runData, notifications = [], deliveries = []) {
|
|
36
|
+
const interaction = runData?.interaction || null;
|
|
37
|
+
const ackNotification = notifications.find((item) => item.stage === "ack") || null;
|
|
38
|
+
const completionNotification = notifications.find((item) => item.stage === "completion") || null;
|
|
39
|
+
const handoffNotification = notifications.find((item) => item.stage === "handoff") || null;
|
|
40
|
+
const followUpNotification = notifications.find((item) => item.stage === "follow_up") || null;
|
|
41
|
+
const handoffDelivery = handoffNotification
|
|
42
|
+
? deliveries.find((item) => item.notificationFilePath === handoffNotification.filePath) || null
|
|
43
|
+
: null;
|
|
44
|
+
const dedupeStatuses = deliveries.map((item) => item.delivery?.status || null);
|
|
45
|
+
const generatedNotificationFiles = new Set(followUpNotification?.generatedNotificationFiles || []);
|
|
46
|
+
const followUpDeliveries = deliveries.filter((item) => generatedNotificationFiles.has(item.notificationFilePath));
|
|
47
|
+
const ackQueued = Boolean(ackNotification);
|
|
48
|
+
const completionQueued = Boolean(completionNotification);
|
|
49
|
+
const handoffQueued = Boolean(handoffNotification);
|
|
50
|
+
const deliveryEvidenceRequired = !interaction?.yield?.required && (ackQueued || completionQueued || handoffQueued);
|
|
51
|
+
const deliveryReceiptCount = deliveries.length;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
ackRequired: interaction?.ack?.required ?? false,
|
|
55
|
+
ackQueued,
|
|
56
|
+
completionRequired: interaction?.completion?.required ?? false,
|
|
57
|
+
completionQueued,
|
|
58
|
+
handoffRequired: interaction?.handoff?.required ?? false,
|
|
59
|
+
handoffQueued,
|
|
60
|
+
yielded: interaction?.yield?.required ?? false,
|
|
61
|
+
yieldSignal: interaction?.yield?.signal || null,
|
|
62
|
+
followUpQueued: Boolean(followUpNotification),
|
|
63
|
+
followUpState: followUpNotification?.followUpState || followUpNotification?.yieldState || null,
|
|
64
|
+
followUpConsumed: followUpNotification?.status === "consumed" || followUpNotification?.yieldState === "consumed" || followUpNotification?.followUpState === "consumed",
|
|
65
|
+
followUpTarget: followUpNotification?.targetSurface || interaction?.yield?.targetSurface || null,
|
|
66
|
+
followUpCompleted: Boolean(followUpDeliveries.length),
|
|
67
|
+
followUpExpired: followUpNotification?.followUpState === "expired",
|
|
68
|
+
followUpEscalated: followUpNotification?.followUpState === "escalated",
|
|
69
|
+
followUpEscalationFilePath: followUpNotification?.escalationNotificationFilePath || null,
|
|
70
|
+
handoffPendingChoice: handoffNotification?.handoffState === "pending" || handoffNotification?.status === "awaiting_choice",
|
|
71
|
+
handoffState: handoffNotification?.handoffState || null,
|
|
72
|
+
handoffExpired: handoffNotification?.handoffState === "expired",
|
|
73
|
+
handoffEscalated: handoffNotification?.handoffState === "escalated",
|
|
74
|
+
handoffBlocked: handoffNotification?.handoffState === "blocked" || handoffNotification?.status === "blocked",
|
|
75
|
+
handoffChosen: Boolean(handoffNotification?.chosenPeer?.peerId),
|
|
76
|
+
handoffChosenPeerId: handoffNotification?.chosenPeer?.peerId || null,
|
|
77
|
+
handoffChosenBy: handoffNotification?.chosenBy || null,
|
|
78
|
+
handoffDelivered: Boolean(handoffDelivery),
|
|
79
|
+
handoffDeliveryState: handoffDelivery?.delivery?.status || null,
|
|
80
|
+
handoffReceiptFilePath: handoffDelivery?.filePath || null,
|
|
81
|
+
handoffBlockedReason: handoffNotification?.blockedReason || null,
|
|
82
|
+
handoffEscalationFilePath: handoffNotification?.escalationNotificationFilePath || null,
|
|
83
|
+
deliveryEvidenceRequired,
|
|
84
|
+
deliveryEvidenceHealthy: !deliveryEvidenceRequired || deliveryReceiptCount > 0,
|
|
85
|
+
deliveryReceiptCount,
|
|
86
|
+
deliveryDeduped: dedupeStatuses.includes("duplicate_prevented"),
|
|
87
|
+
deliveryRetried: deliveries.some((item) => Number(item.attempt) > 1),
|
|
88
|
+
deliveryUncertain: dedupeStatuses.includes("delivery_uncertain") || dedupeStatuses.includes("uncertain")
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export class RunReviewer {
|
|
93
|
+
constructor({ stateRoot }) {
|
|
94
|
+
this.stateRoot = stateRoot;
|
|
95
|
+
this.runRoot = path.join(stateRoot, "runs");
|
|
96
|
+
this.schedulerState = new SchedulerStateStore({ rootDir: path.join(stateRoot, "scheduler") });
|
|
97
|
+
this.notificationStore = new NotificationStore({ rootDir: path.join(stateRoot, "notifications") });
|
|
98
|
+
this.deliveryStore = new DeliveryStore({ rootDir: path.join(stateRoot, "deliveries") });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async listRecentRuns(limit = 10) {
|
|
102
|
+
const files = (await listFilesRecursive(this.runRoot)).filter((filePath) => filePath.endsWith(".json"));
|
|
103
|
+
const runs = [];
|
|
104
|
+
|
|
105
|
+
const [notifications, deliveries] = await Promise.all([
|
|
106
|
+
this.notificationStore.listAll(),
|
|
107
|
+
this.deliveryStore.listAll()
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
for (const filePath of files) {
|
|
111
|
+
const data = await readJson(filePath, null);
|
|
112
|
+
if (!data) continue;
|
|
113
|
+
const relatedNotificationFiles = expandRelatedNotificationFiles(data.notificationFiles || [], notifications);
|
|
114
|
+
const runNotifications = notifications.filter((item) => relatedNotificationFiles.has(item.filePath));
|
|
115
|
+
const runDeliveries = deliveries.filter((item) => relatedNotificationFiles.has(item.notificationFilePath));
|
|
116
|
+
const interactionState = summarizeInteractionState(data, runNotifications, runDeliveries);
|
|
117
|
+
runs.push({
|
|
118
|
+
filePath,
|
|
119
|
+
jobId: path.basename(path.dirname(filePath)),
|
|
120
|
+
timestamp: data.timestamp || inferTimestampFromPath(filePath),
|
|
121
|
+
mode: data.mode || null,
|
|
122
|
+
providerId: data.providerId || null,
|
|
123
|
+
modelId: data.modelId || null,
|
|
124
|
+
summary: data.result?.summary || data.summary || null,
|
|
125
|
+
output: data.result?.output || data.output || null,
|
|
126
|
+
result: data.result || null,
|
|
127
|
+
plan: data.plan || null,
|
|
128
|
+
fallback: data.fallback || null,
|
|
129
|
+
notificationFiles: data.notificationFiles || [],
|
|
130
|
+
retrievalMeta: data.plan?.packet?.layers?.retrievalMeta || null,
|
|
131
|
+
retrievedMemory: data.plan?.packet?.layers?.memory || [],
|
|
132
|
+
interaction: data.interaction || null,
|
|
133
|
+
completionSignal: data.interaction?.completion?.signal || null,
|
|
134
|
+
completionTarget: data.interaction?.completion?.target || null,
|
|
135
|
+
completionMessage: data.interaction?.completion?.message || null,
|
|
136
|
+
ackQueued: interactionState.ackQueued,
|
|
137
|
+
completionQueued: interactionState.completionQueued,
|
|
138
|
+
handoffQueued: interactionState.handoffQueued,
|
|
139
|
+
handoffSignal: data.interaction?.handoff?.signal || null,
|
|
140
|
+
handoffTarget: data.interaction?.handoff?.target || null,
|
|
141
|
+
handoffSuggestionCount: data.interaction?.handoff?.suggestions?.length || 0,
|
|
142
|
+
yielded: interactionState.yielded,
|
|
143
|
+
yieldSignal: interactionState.yieldSignal,
|
|
144
|
+
followUpState: interactionState.followUpState,
|
|
145
|
+
followUpQueued: interactionState.followUpQueued,
|
|
146
|
+
followUpConsumed: interactionState.followUpConsumed,
|
|
147
|
+
followUpTarget: interactionState.followUpTarget,
|
|
148
|
+
followUpCompleted: interactionState.followUpCompleted,
|
|
149
|
+
followUpExpired: interactionState.followUpExpired,
|
|
150
|
+
followUpEscalated: interactionState.followUpEscalated,
|
|
151
|
+
followUpEscalationFilePath: interactionState.followUpEscalationFilePath,
|
|
152
|
+
handoffChosenPeerId: interactionState.handoffChosenPeerId,
|
|
153
|
+
handoffChosenBy: interactionState.handoffChosenBy,
|
|
154
|
+
handoffDelivered: interactionState.handoffDelivered,
|
|
155
|
+
handoffDeliveryState: interactionState.handoffDeliveryState,
|
|
156
|
+
handoffState: interactionState.handoffState,
|
|
157
|
+
handoffBlockedReason: interactionState.handoffBlockedReason,
|
|
158
|
+
deliveryEvidenceRequired: interactionState.deliveryEvidenceRequired,
|
|
159
|
+
deliveryEvidenceHealthy: interactionState.deliveryEvidenceHealthy,
|
|
160
|
+
deliveryReceiptCount: interactionState.deliveryReceiptCount,
|
|
161
|
+
deliveryDeduped: interactionState.deliveryDeduped,
|
|
162
|
+
deliveryRetried: interactionState.deliveryRetried,
|
|
163
|
+
deliveryUncertain: interactionState.deliveryUncertain,
|
|
164
|
+
fallbackAllowed: Boolean(data.fallback?.allowed),
|
|
165
|
+
fallbackAttempted: Boolean(data.fallback?.attempted),
|
|
166
|
+
fallbackSucceeded: Boolean(data.fallback?.success),
|
|
167
|
+
fallbackTrigger: data.fallback?.trigger || null,
|
|
168
|
+
fallbackSourceLane: data.fallback?.sourceLane || null,
|
|
169
|
+
fallbackFinalSourceLane: data.fallback?.finalSourceLane || null,
|
|
170
|
+
fallbackFinalProviderId: data.fallback?.finalProviderId || null,
|
|
171
|
+
fallbackFinalModelId: data.fallback?.finalModelId || null,
|
|
172
|
+
fallbackBlockedReason: data.fallback?.blockedReason || null,
|
|
173
|
+
retrievalHealth: data.plan?.packet?.layers?.retrievalMeta?.embeddingHealth || null,
|
|
174
|
+
retrievalDegradedMode: data.plan?.packet?.layers?.retrievalMeta?.embeddingQueryMode === "lexical_fallback",
|
|
175
|
+
status: data.result ? "ok" : data.lastStatus || null
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return sortDescending(runs).slice(0, limit);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async schedulerOverview() {
|
|
183
|
+
const state = await this.schedulerState.load();
|
|
184
|
+
return Object.entries(state.jobs || {}).map(([jobId, jobState]) => ({
|
|
185
|
+
jobId,
|
|
186
|
+
...jobState
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async review(limit = 10) {
|
|
191
|
+
const [recentRuns, scheduler, recentNotifications, recentDeliveries] = await Promise.all([
|
|
192
|
+
this.listRecentRuns(limit),
|
|
193
|
+
this.schedulerOverview(),
|
|
194
|
+
this.notificationStore.listRecent(limit),
|
|
195
|
+
this.deliveryStore.listRecent(limit)
|
|
196
|
+
]);
|
|
197
|
+
const maintenance = await this.schedulerState.getMeta("maintenance", null);
|
|
198
|
+
const handoffs = recentNotifications
|
|
199
|
+
.filter((item) => item.stage === "handoff")
|
|
200
|
+
.map((item) => {
|
|
201
|
+
const receipt = recentDeliveries.find((delivery) => delivery.notificationFilePath === item.filePath) || null;
|
|
202
|
+
return {
|
|
203
|
+
filePath: item.filePath,
|
|
204
|
+
jobId: item.jobId,
|
|
205
|
+
timestamp: item.timestamp,
|
|
206
|
+
status: item.status,
|
|
207
|
+
handoffState: item.handoffState || null,
|
|
208
|
+
signal: item.signal,
|
|
209
|
+
suggestedPeerCount: item.suggestedPeers?.length || 0,
|
|
210
|
+
chosenPeerId: item.chosenPeer?.peerId || null,
|
|
211
|
+
chosenBy: item.chosenBy || null,
|
|
212
|
+
deliveryStatus: receipt?.delivery?.status || null,
|
|
213
|
+
receiptFilePath: receipt?.filePath || null,
|
|
214
|
+
blockedReason: item.blockedReason || null,
|
|
215
|
+
escalationNotificationFilePath: item.escalationNotificationFilePath || null
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
const followUps = recentNotifications
|
|
219
|
+
.filter((item) => item.stage === "follow_up")
|
|
220
|
+
.map((item) => ({
|
|
221
|
+
filePath: item.filePath,
|
|
222
|
+
jobId: item.jobId,
|
|
223
|
+
timestamp: item.timestamp,
|
|
224
|
+
status: item.status,
|
|
225
|
+
yieldState: item.yieldState || null,
|
|
226
|
+
followUpState: item.followUpState || null,
|
|
227
|
+
signal: item.signal || null,
|
|
228
|
+
targetSurface: item.targetSurface || null,
|
|
229
|
+
consumedAt: item.consumedAt || null,
|
|
230
|
+
expiredAt: item.expiredAt || null,
|
|
231
|
+
escalationNotificationFilePath: item.escalationNotificationFilePath || null,
|
|
232
|
+
generatedNotificationFiles: item.generatedNotificationFiles || []
|
|
233
|
+
}));
|
|
234
|
+
return {
|
|
235
|
+
scheduler,
|
|
236
|
+
maintenance,
|
|
237
|
+
recentRuns,
|
|
238
|
+
recentNotifications,
|
|
239
|
+
recentDeliveries,
|
|
240
|
+
recentHandoffs: handoffs,
|
|
241
|
+
recentFollowUps: followUps
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export async function routeJob({ jobId, mode, orchestrator, executor, skipOrchestrator = false, shadowImport = true, idempotencyKey = null }) {
|
|
2
|
+
if (orchestrator && !skipOrchestrator) {
|
|
3
|
+
const route = await orchestrator.route(jobId);
|
|
4
|
+
if (route.agentId) {
|
|
5
|
+
const result = await executor.executeJob(jobId, { mode, shadowImport, targetAgent: route.agentId, idempotencyKey });
|
|
6
|
+
return { ...result, routedVia: "orchestrator", route };
|
|
7
|
+
}
|
|
8
|
+
// Escalation — no agent could handle it
|
|
9
|
+
return { routedVia: "escalation", route, filePath: null, notificationFiles: [] };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Direct execution (backwards-compatible)
|
|
13
|
+
const result = await executor.executeJob(jobId, { mode, shadowImport, idempotencyKey });
|
|
14
|
+
return { ...result, routedVia: "direct" };
|
|
15
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { ensureDir, writeJson } from "../utils/fs.js";
|
|
3
|
+
import { buildRetentionPolicy, pruneJsonBuckets } from "./retention.js";
|
|
4
|
+
import { createRuntimeId } from "../utils/ids.js";
|
|
5
|
+
|
|
6
|
+
function stamp() {
|
|
7
|
+
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class RunStore {
|
|
11
|
+
constructor({ rootDir, retention = {} }) {
|
|
12
|
+
this.rootDir = rootDir;
|
|
13
|
+
this.setRetentionPolicy(retention);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
setRetentionPolicy(retention = {}) {
|
|
17
|
+
this.retentionPolicy = buildRetentionPolicy(retention, {
|
|
18
|
+
ttlDays: 30,
|
|
19
|
+
maxFilesPerBucket: 2000
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async saveRun(jobId, run) {
|
|
24
|
+
const runDir = path.join(this.rootDir, jobId);
|
|
25
|
+
await ensureDir(runDir);
|
|
26
|
+
const filePath = path.join(runDir, `${stamp()}.json`);
|
|
27
|
+
await writeJson(filePath, {
|
|
28
|
+
runId: run.runId || createRuntimeId("run"),
|
|
29
|
+
...run
|
|
30
|
+
});
|
|
31
|
+
await pruneJsonBuckets(this.rootDir, this.retentionPolicy);
|
|
32
|
+
return filePath;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async prune(options = {}) {
|
|
36
|
+
return pruneJsonBuckets(this.rootDir, this.retentionPolicy, options);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
function pad(value) {
|
|
2
|
+
return String(value).padStart(2, "0");
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function cloneDate(date) {
|
|
6
|
+
return new Date(date.getTime());
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function parseEveryMinutes(trigger) {
|
|
10
|
+
const match = /^every:(\d+)(m|h)$/.exec(String(trigger || ""));
|
|
11
|
+
if (!match) return null;
|
|
12
|
+
const amount = Number(match[1]);
|
|
13
|
+
const unit = match[2];
|
|
14
|
+
return unit === "h" ? amount * 60 : amount;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function computeNextRun(trigger, now = new Date()) {
|
|
18
|
+
const everyMinutes = parseEveryMinutes(trigger);
|
|
19
|
+
if (everyMinutes != null) {
|
|
20
|
+
return new Date(now.getTime() + everyMinutes * 60 * 1000);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (trigger === "hourly") {
|
|
24
|
+
const next = cloneDate(now);
|
|
25
|
+
next.setUTCMinutes(0, 0, 0);
|
|
26
|
+
next.setUTCHours(next.getUTCHours() + 1);
|
|
27
|
+
return next;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const dailyMatch = /^daily:(\d{2}):(\d{2})$/.exec(String(trigger || ""));
|
|
31
|
+
if (dailyMatch) {
|
|
32
|
+
const [, hh, mm] = dailyMatch;
|
|
33
|
+
const next = cloneDate(now);
|
|
34
|
+
next.setUTCHours(Number(hh), Number(mm), 0, 0);
|
|
35
|
+
if (next.getTime() <= now.getTime()) {
|
|
36
|
+
next.setUTCDate(next.getUTCDate() + 1);
|
|
37
|
+
}
|
|
38
|
+
return next;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
throw new Error(`Unsupported trigger: ${trigger}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function computeRunSlot(trigger, now = new Date()) {
|
|
45
|
+
const everyMinutes = parseEveryMinutes(trigger);
|
|
46
|
+
if (everyMinutes != null) {
|
|
47
|
+
const slotMs = everyMinutes * 60 * 1000;
|
|
48
|
+
return new Date(Math.floor(now.getTime() / slotMs) * slotMs);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (trigger === "hourly") {
|
|
52
|
+
const slot = cloneDate(now);
|
|
53
|
+
slot.setUTCMinutes(0, 0, 0);
|
|
54
|
+
return slot;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const dailyMatch = /^daily:(\d{2}):(\d{2})$/.exec(String(trigger || ""));
|
|
58
|
+
if (dailyMatch) {
|
|
59
|
+
const slot = cloneDate(now);
|
|
60
|
+
slot.setUTCHours(0, 0, 0, 0);
|
|
61
|
+
return slot;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
throw new Error(`Unsupported trigger: ${trigger}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function isDue(trigger, lastRunAt, nextRunAt, now = new Date()) {
|
|
68
|
+
if (nextRunAt) {
|
|
69
|
+
return new Date(nextRunAt).getTime() <= now.getTime();
|
|
70
|
+
}
|
|
71
|
+
if (!lastRunAt) return true;
|
|
72
|
+
return new Date(lastRunAt).getTime() < computeNextRun(trigger, new Date(lastRunAt)).getTime() &&
|
|
73
|
+
computeNextRun(trigger, new Date(lastRunAt)).getTime() <= now.getTime();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function formatRunState(state) {
|
|
77
|
+
return {
|
|
78
|
+
lastRunAt: state.lastRunAt || null,
|
|
79
|
+
nextRunAt: state.nextRunAt || null,
|
|
80
|
+
lastStatus: state.lastStatus || null,
|
|
81
|
+
lastRunFile: state.lastRunFile || null,
|
|
82
|
+
consecutiveFailures: state.consecutiveFailures || 0
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function iso(date) {
|
|
87
|
+
return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}T${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())}.000Z`;
|
|
88
|
+
}
|