nemoris 0.1.0 → 0.1.2
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 -49
- package/LICENSE +21 -21
- package/README.md +209 -209
- package/SECURITY.md +59 -119
- package/bin/nemoris +46 -46
- package/config/agents/agent.toml.example +28 -28
- package/config/agents/content.toml +23 -0
- package/config/agents/default.toml +22 -22
- package/config/agents/heartbeat.toml +35 -0
- package/config/agents/iris.toml +23 -0
- package/config/agents/lab.toml +23 -0
- package/config/agents/main.toml +45 -0
- package/config/agents/nemo.toml +21 -0
- package/config/agents/ops.toml +38 -0
- package/config/agents/orchestrator.toml +18 -18
- package/config/agents/revenue.toml +23 -0
- package/config/agents/testyboo.toml +19 -0
- package/config/delivery.toml +73 -73
- package/config/embeddings.toml +5 -5
- package/config/identity/content-purpose.md +11 -0
- package/config/identity/content-soul.md +45 -0
- package/config/identity/default-purpose.md +1 -1
- package/config/identity/default-soul.md +3 -3
- package/config/identity/heartbeat-purpose.md +9 -0
- package/config/identity/heartbeat-soul.md +16 -0
- package/config/identity/iris-purpose.md +17 -0
- package/config/identity/iris-soul.md +68 -0
- package/config/identity/lab-purpose.md +10 -0
- package/config/identity/lab-soul.md +38 -0
- package/config/identity/main-purpose.md +17 -0
- package/config/identity/main-soul.md +66 -0
- package/config/identity/main-user.md +22 -0
- package/config/identity/ops-purpose.md +9 -0
- package/config/identity/ops-soul.md +16 -0
- package/config/identity/orchestrator-purpose.md +1 -1
- package/config/identity/orchestrator-soul.md +1 -1
- package/config/identity/revenue-purpose.md +9 -0
- package/config/identity/revenue-soul.md +41 -0
- package/config/identity/testyboo-purpose.md +13 -0
- package/config/identity/testyboo-soul.md +20 -0
- package/config/improvement-targets.toml +15 -15
- package/config/jobs/heartbeat-check.toml +30 -30
- package/config/jobs/memory-rollup.toml +46 -46
- package/config/jobs/workspace-health.toml +63 -63
- package/config/mcp.toml +16 -16
- package/config/output-contracts.toml +17 -17
- package/config/peers.toml +32 -32
- package/config/peers.toml.example +32 -32
- package/config/policies/memory-default.toml +10 -10
- package/config/policies/memory-heartbeat.toml +5 -5
- package/config/policies/memory-ops.toml +10 -10
- package/config/policies/tools-heartbeat-minimal.toml +8 -8
- package/config/policies/tools-interactive-safe.toml +8 -8
- package/config/policies/tools-ops-bounded.toml +8 -8
- package/config/policies/tools-orchestrator.toml +7 -7
- package/config/providers/anthropic.toml +15 -15
- package/config/providers/ollama.toml +5 -5
- package/config/providers/openai-codex.toml +9 -9
- package/config/providers/openrouter.toml +5 -5
- package/config/router.toml +22 -22
- package/config/runtime.toml +114 -114
- package/config/skills/self-improvement.toml +15 -15
- package/config/skills/telegram-onboarding-spec.md +240 -240
- package/config/skills/workspace-monitor.toml +15 -15
- package/config/task-router.toml +42 -42
- package/install.sh +50 -50
- package/package.json +91 -90
- package/src/auth/auth-profiles.js +169 -169
- package/src/auth/openai-codex-oauth.js +285 -285
- package/src/battle.js +449 -449
- package/src/cli/help.js +265 -265
- package/src/cli/output-filter.js +49 -49
- package/src/cli/runtime-control.js +704 -704
- package/src/cli-main.js +2763 -2763
- package/src/cli.js +78 -78
- package/src/config/loader.js +332 -332
- package/src/config/schema-validator.js +214 -214
- package/src/config/toml-lite.js +8 -8
- package/src/daemon/action-handlers.js +71 -71
- package/src/daemon/healing-tick.js +87 -87
- package/src/daemon/health-probes.js +90 -90
- package/src/daemon/notifier.js +57 -57
- package/src/daemon/nurse.js +218 -218
- package/src/daemon/repair-log.js +106 -106
- package/src/daemon/rule-staging.js +90 -90
- package/src/daemon/rules.js +29 -29
- package/src/daemon/telegram-commands.js +54 -54
- package/src/daemon/updater.js +85 -85
- package/src/jobs/job-runner.js +78 -78
- package/src/mcp/consumer.js +129 -129
- package/src/memory/active-recall.js +171 -171
- package/src/memory/backend-manager.js +97 -97
- package/src/memory/backends/file-backend.js +38 -38
- package/src/memory/backends/qmd-backend.js +219 -219
- package/src/memory/embedding-guards.js +24 -24
- package/src/memory/embedding-index.js +118 -118
- package/src/memory/embedding-service.js +179 -179
- package/src/memory/file-index.js +177 -177
- package/src/memory/memory-signature.js +5 -5
- package/src/memory/memory-store.js +648 -648
- package/src/memory/retrieval-planner.js +66 -66
- package/src/memory/scoring.js +145 -145
- package/src/memory/simhash.js +78 -78
- package/src/memory/sqlite-active-store.js +824 -824
- package/src/memory/write-policy.js +36 -36
- package/src/onboarding/aliases.js +33 -33
- package/src/onboarding/auth/api-key.js +224 -224
- package/src/onboarding/auth/ollama-detect.js +42 -42
- package/src/onboarding/clack-prompter.js +77 -77
- package/src/onboarding/doctor.js +530 -530
- package/src/onboarding/lock.js +42 -42
- package/src/onboarding/model-catalog.js +344 -344
- package/src/onboarding/phases/auth.js +576 -589
- package/src/onboarding/phases/build.js +130 -130
- package/src/onboarding/phases/choose.js +82 -82
- package/src/onboarding/phases/detect.js +98 -98
- package/src/onboarding/phases/hatch.js +216 -216
- package/src/onboarding/phases/identity.js +79 -79
- package/src/onboarding/phases/ollama.js +345 -345
- package/src/onboarding/phases/scaffold.js +99 -99
- package/src/onboarding/phases/telegram.js +377 -377
- package/src/onboarding/phases/validate.js +204 -204
- package/src/onboarding/phases/verify.js +206 -206
- package/src/onboarding/platform.js +482 -482
- package/src/onboarding/status-bar.js +95 -95
- package/src/onboarding/templates.js +794 -794
- package/src/onboarding/toml-writer.js +38 -38
- package/src/onboarding/tui.js +250 -250
- package/src/onboarding/uninstall.js +153 -153
- package/src/onboarding/wizard.js +516 -499
- package/src/providers/anthropic.js +168 -168
- package/src/providers/base.js +247 -247
- package/src/providers/circuit-breaker.js +136 -136
- package/src/providers/ollama.js +163 -163
- package/src/providers/openai-codex.js +149 -149
- package/src/providers/openrouter.js +136 -136
- package/src/providers/registry.js +36 -36
- package/src/providers/router.js +16 -16
- package/src/runtime/bootstrap-cache.js +47 -47
- package/src/runtime/capabilities-prompt.js +25 -25
- package/src/runtime/completion-ping.js +99 -99
- package/src/runtime/config-validator.js +121 -121
- package/src/runtime/context-ledger.js +360 -360
- package/src/runtime/cutover-readiness.js +42 -42
- package/src/runtime/daemon.js +729 -729
- package/src/runtime/delivery-ack.js +195 -195
- package/src/runtime/delivery-adapters/local-file.js +41 -41
- package/src/runtime/delivery-adapters/openclaw-cli.js +94 -94
- package/src/runtime/delivery-adapters/openclaw-peer.js +98 -98
- package/src/runtime/delivery-adapters/shadow.js +13 -13
- package/src/runtime/delivery-adapters/standalone-http.js +98 -98
- package/src/runtime/delivery-adapters/telegram.js +104 -104
- package/src/runtime/delivery-adapters/tui.js +128 -128
- package/src/runtime/delivery-manager.js +807 -807
- package/src/runtime/delivery-store.js +168 -168
- package/src/runtime/dependency-health.js +118 -118
- package/src/runtime/envelope.js +114 -114
- package/src/runtime/evaluation.js +1089 -1089
- package/src/runtime/exec-approvals.js +216 -216
- package/src/runtime/executor.js +500 -500
- package/src/runtime/failure-ping.js +67 -67
- package/src/runtime/flows.js +83 -83
- package/src/runtime/guards.js +45 -45
- package/src/runtime/handoff.js +51 -51
- package/src/runtime/identity-cache.js +28 -28
- package/src/runtime/improvement-engine.js +109 -109
- package/src/runtime/improvement-harness.js +581 -581
- package/src/runtime/input-sanitiser.js +72 -72
- package/src/runtime/interaction-contract.js +347 -347
- package/src/runtime/lane-readiness.js +226 -226
- package/src/runtime/migration.js +323 -323
- package/src/runtime/model-resolution.js +78 -78
- package/src/runtime/network.js +64 -64
- package/src/runtime/notification-store.js +97 -97
- package/src/runtime/notifier.js +256 -256
- package/src/runtime/orchestrator.js +53 -53
- package/src/runtime/orphan-reaper.js +41 -41
- package/src/runtime/output-contract-schema.js +139 -139
- package/src/runtime/output-contract-validator.js +439 -439
- package/src/runtime/peer-readiness.js +69 -69
- package/src/runtime/peer-registry.js +133 -133
- package/src/runtime/pilot-status.js +108 -108
- package/src/runtime/prompt-builder.js +261 -261
- package/src/runtime/provider-attempt.js +582 -582
- package/src/runtime/report-fallback.js +71 -71
- package/src/runtime/result-normalizer.js +183 -183
- package/src/runtime/retention.js +74 -74
- package/src/runtime/review.js +244 -244
- package/src/runtime/route-job.js +15 -15
- package/src/runtime/run-store.js +38 -38
- package/src/runtime/schedule.js +88 -88
- package/src/runtime/scheduler-state.js +434 -434
- package/src/runtime/scheduler.js +656 -656
- package/src/runtime/session-compactor.js +182 -182
- package/src/runtime/session-search.js +155 -155
- package/src/runtime/slack-inbound.js +249 -249
- package/src/runtime/ssrf.js +102 -102
- package/src/runtime/status-aggregator.js +330 -330
- package/src/runtime/task-contract.js +140 -140
- package/src/runtime/task-packet.js +107 -107
- package/src/runtime/task-router.js +140 -140
- package/src/runtime/telegram-inbound.js +1565 -1565
- package/src/runtime/token-counter.js +134 -134
- package/src/runtime/token-estimator.js +59 -59
- package/src/runtime/tool-loop.js +200 -200
- package/src/runtime/transport-server.js +311 -311
- package/src/runtime/tui-server.js +411 -411
- package/src/runtime/ulid.js +44 -44
- package/src/security/ssrf-check.js +197 -197
- package/src/setup.js +369 -369
- package/src/shadow/bridge.js +303 -303
- package/src/skills/loader.js +84 -84
- package/src/tools/catalog.json +49 -49
- package/src/tools/cli-delegate.js +44 -44
- package/src/tools/mcp-client.js +106 -106
- package/src/tools/micro/cancel-task.js +6 -6
- package/src/tools/micro/complete-task.js +6 -6
- package/src/tools/micro/fail-task.js +6 -6
- package/src/tools/micro/http-fetch.js +74 -74
- package/src/tools/micro/index.js +36 -36
- package/src/tools/micro/lcm-recall.js +60 -60
- package/src/tools/micro/list-dir.js +17 -17
- package/src/tools/micro/list-skills.js +46 -46
- package/src/tools/micro/load-skill.js +38 -38
- package/src/tools/micro/memory-search.js +45 -45
- package/src/tools/micro/read-file.js +11 -11
- package/src/tools/micro/session-search.js +54 -54
- package/src/tools/micro/shell-exec.js +43 -43
- package/src/tools/micro/trigger-job.js +79 -79
- package/src/tools/micro/web-search.js +58 -58
- package/src/tools/micro/workspace-paths.js +39 -39
- package/src/tools/micro/write-file.js +14 -14
- package/src/tools/micro/write-memory.js +41 -41
- package/src/tools/registry.js +348 -348
- package/src/tools/tool-result-contract.js +36 -36
- package/src/tui/chat.js +835 -835
- package/src/tui/renderer.js +175 -175
- package/src/tui/socket-client.js +217 -217
- package/src/utils/canonical-json.js +29 -29
- package/src/utils/compaction.js +30 -30
- package/src/utils/env-loader.js +5 -5
- package/src/utils/errors.js +80 -80
- package/src/utils/fs.js +101 -101
- package/src/utils/ids.js +5 -5
- package/src/utils/model-context-limits.js +30 -30
- package/src/utils/token-budget.js +74 -74
- package/src/utils/usage-cost.js +25 -25
- package/src/utils/usage-metrics.js +14 -14
|
@@ -1,168 +1,168 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { ensureDir, listFilesRecursive, readJson, 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
|
-
function deliveryKey(mode, notificationFilePath, dedupeKey = null) {
|
|
11
|
-
return dedupeKey ? `${mode}:dedupe:${dedupeKey}` : `${mode}:file:${notificationFilePath}`;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export class DeliveryStore {
|
|
15
|
-
constructor({ rootDir, retention = {} }) {
|
|
16
|
-
this.rootDir = rootDir;
|
|
17
|
-
this.indexPath = path.join(rootDir, "index.json");
|
|
18
|
-
this.setRetentionPolicy(retention);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
setRetentionPolicy(retention = {}) {
|
|
22
|
-
this.retentionPolicy = buildRetentionPolicy(retention, {
|
|
23
|
-
ttlDays: 14,
|
|
24
|
-
maxFilesPerBucket: 1000
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async loadIndex() {
|
|
29
|
-
await ensureDir(this.rootDir);
|
|
30
|
-
return readJson(this.indexPath, { delivered: {} });
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async saveIndex(index) {
|
|
34
|
-
await ensureDir(this.rootDir);
|
|
35
|
-
await writeJson(this.indexPath, index);
|
|
36
|
-
return index;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async isDelivered(mode, notificationFilePath, dedupeKey = null) {
|
|
40
|
-
const record = await this.getDeliveryRecord(mode, notificationFilePath, dedupeKey);
|
|
41
|
-
return Boolean(record && ["delivered", "uncertain"].includes(record.status || "delivered"));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async getDeliveryRecord(mode, notificationFilePath, dedupeKey = null) {
|
|
45
|
-
const index = await this.loadIndex();
|
|
46
|
-
return (
|
|
47
|
-
index.delivered[deliveryKey(mode, notificationFilePath, dedupeKey)] ||
|
|
48
|
-
index.delivered[deliveryKey(mode, notificationFilePath)] ||
|
|
49
|
-
null
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async markDelivered(mode, notificationFilePath, receiptFilePath, options = {}) {
|
|
54
|
-
const index = await this.loadIndex();
|
|
55
|
-
const current = await this.getDeliveryRecord(mode, notificationFilePath, options.dedupeKey);
|
|
56
|
-
const record = {
|
|
57
|
-
id: current?.id || createRuntimeId("delivery-record"),
|
|
58
|
-
receiptFilePath,
|
|
59
|
-
deliveredAt: new Date().toISOString(),
|
|
60
|
-
status: "delivered",
|
|
61
|
-
dedupeKey: options.dedupeKey || null,
|
|
62
|
-
retryEligible: false,
|
|
63
|
-
uncertain: false,
|
|
64
|
-
attempts: (current?.attempts || 0) + 1,
|
|
65
|
-
lastError: null
|
|
66
|
-
};
|
|
67
|
-
index.delivered[deliveryKey(mode, notificationFilePath)] = record;
|
|
68
|
-
if (options.dedupeKey) {
|
|
69
|
-
index.delivered[deliveryKey(mode, notificationFilePath, options.dedupeKey)] = record;
|
|
70
|
-
}
|
|
71
|
-
await this.saveIndex(index);
|
|
72
|
-
return record;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async markUncertain(mode, notificationFilePath, receiptFilePath, options = {}) {
|
|
76
|
-
const index = await this.loadIndex();
|
|
77
|
-
const current = await this.getDeliveryRecord(mode, notificationFilePath, options.dedupeKey);
|
|
78
|
-
const record = {
|
|
79
|
-
id: current?.id || createRuntimeId("delivery-record"),
|
|
80
|
-
receiptFilePath,
|
|
81
|
-
deliveredAt: null,
|
|
82
|
-
status: "uncertain",
|
|
83
|
-
dedupeKey: options.dedupeKey || null,
|
|
84
|
-
retryEligible: false,
|
|
85
|
-
uncertain: true,
|
|
86
|
-
attempts: (current?.attempts || 0) + 1,
|
|
87
|
-
lastError: options.error || null,
|
|
88
|
-
updatedAt: new Date().toISOString()
|
|
89
|
-
};
|
|
90
|
-
index.delivered[deliveryKey(mode, notificationFilePath)] = record;
|
|
91
|
-
if (options.dedupeKey) {
|
|
92
|
-
index.delivered[deliveryKey(mode, notificationFilePath, options.dedupeKey)] = record;
|
|
93
|
-
}
|
|
94
|
-
await this.saveIndex(index);
|
|
95
|
-
return record;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async markFailed(mode, notificationFilePath, receiptFilePath, options = {}) {
|
|
99
|
-
const index = await this.loadIndex();
|
|
100
|
-
const current = await this.getDeliveryRecord(mode, notificationFilePath, options.dedupeKey);
|
|
101
|
-
const record = {
|
|
102
|
-
id: current?.id || createRuntimeId("delivery-record"),
|
|
103
|
-
receiptFilePath,
|
|
104
|
-
deliveredAt: null,
|
|
105
|
-
status: "failed",
|
|
106
|
-
dedupeKey: options.dedupeKey || null,
|
|
107
|
-
retryEligible: options.retryEligible ?? false,
|
|
108
|
-
uncertain: false,
|
|
109
|
-
attempts: (current?.attempts || 0) + 1,
|
|
110
|
-
lastError: options.error || null,
|
|
111
|
-
updatedAt: new Date().toISOString()
|
|
112
|
-
};
|
|
113
|
-
index.delivered[deliveryKey(mode, notificationFilePath)] = record;
|
|
114
|
-
if (options.dedupeKey) {
|
|
115
|
-
index.delivered[deliveryKey(mode, notificationFilePath, options.dedupeKey)] = record;
|
|
116
|
-
}
|
|
117
|
-
await this.saveIndex(index);
|
|
118
|
-
return record;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async saveReceipt(jobId, receipt) {
|
|
122
|
-
const receiptDir = path.join(this.rootDir, jobId);
|
|
123
|
-
await ensureDir(receiptDir);
|
|
124
|
-
const filePath = path.join(receiptDir, `${stamp()}.json`);
|
|
125
|
-
await writeJson(filePath, {
|
|
126
|
-
id: receipt.id || createRuntimeId("delivery-attempt"),
|
|
127
|
-
...receipt
|
|
128
|
-
});
|
|
129
|
-
await pruneJsonBuckets(this.rootDir, this.retentionPolicy, {
|
|
130
|
-
excludePaths: [this.indexPath]
|
|
131
|
-
});
|
|
132
|
-
return filePath;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
async listAll() {
|
|
136
|
-
const files = (await listFilesRecursive(this.rootDir)).filter(
|
|
137
|
-
(filePath) => filePath.endsWith(".json") && filePath !== this.indexPath
|
|
138
|
-
);
|
|
139
|
-
const receipts = [];
|
|
140
|
-
|
|
141
|
-
for (const filePath of files) {
|
|
142
|
-
const data = await readJson(filePath, null);
|
|
143
|
-
if (!data) continue;
|
|
144
|
-
receipts.push({
|
|
145
|
-
filePath,
|
|
146
|
-
...data
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return receipts.sort((a, b) => String(b.timestamp || "").localeCompare(String(a.timestamp || "")));
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
async listRecent(limit = 10) {
|
|
154
|
-
const receipts = await this.listAll();
|
|
155
|
-
return receipts.slice(0, limit);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async prune(options = {}) {
|
|
159
|
-
const result = await pruneJsonBuckets(this.rootDir, this.retentionPolicy, {
|
|
160
|
-
...options,
|
|
161
|
-
excludePaths: [...(options.excludePaths || []), this.indexPath]
|
|
162
|
-
});
|
|
163
|
-
return {
|
|
164
|
-
...result,
|
|
165
|
-
indexFilePath: this.indexPath
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
}
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { ensureDir, listFilesRecursive, readJson, 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
|
+
function deliveryKey(mode, notificationFilePath, dedupeKey = null) {
|
|
11
|
+
return dedupeKey ? `${mode}:dedupe:${dedupeKey}` : `${mode}:file:${notificationFilePath}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class DeliveryStore {
|
|
15
|
+
constructor({ rootDir, retention = {} }) {
|
|
16
|
+
this.rootDir = rootDir;
|
|
17
|
+
this.indexPath = path.join(rootDir, "index.json");
|
|
18
|
+
this.setRetentionPolicy(retention);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
setRetentionPolicy(retention = {}) {
|
|
22
|
+
this.retentionPolicy = buildRetentionPolicy(retention, {
|
|
23
|
+
ttlDays: 14,
|
|
24
|
+
maxFilesPerBucket: 1000
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async loadIndex() {
|
|
29
|
+
await ensureDir(this.rootDir);
|
|
30
|
+
return readJson(this.indexPath, { delivered: {} });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async saveIndex(index) {
|
|
34
|
+
await ensureDir(this.rootDir);
|
|
35
|
+
await writeJson(this.indexPath, index);
|
|
36
|
+
return index;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async isDelivered(mode, notificationFilePath, dedupeKey = null) {
|
|
40
|
+
const record = await this.getDeliveryRecord(mode, notificationFilePath, dedupeKey);
|
|
41
|
+
return Boolean(record && ["delivered", "uncertain"].includes(record.status || "delivered"));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async getDeliveryRecord(mode, notificationFilePath, dedupeKey = null) {
|
|
45
|
+
const index = await this.loadIndex();
|
|
46
|
+
return (
|
|
47
|
+
index.delivered[deliveryKey(mode, notificationFilePath, dedupeKey)] ||
|
|
48
|
+
index.delivered[deliveryKey(mode, notificationFilePath)] ||
|
|
49
|
+
null
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async markDelivered(mode, notificationFilePath, receiptFilePath, options = {}) {
|
|
54
|
+
const index = await this.loadIndex();
|
|
55
|
+
const current = await this.getDeliveryRecord(mode, notificationFilePath, options.dedupeKey);
|
|
56
|
+
const record = {
|
|
57
|
+
id: current?.id || createRuntimeId("delivery-record"),
|
|
58
|
+
receiptFilePath,
|
|
59
|
+
deliveredAt: new Date().toISOString(),
|
|
60
|
+
status: "delivered",
|
|
61
|
+
dedupeKey: options.dedupeKey || null,
|
|
62
|
+
retryEligible: false,
|
|
63
|
+
uncertain: false,
|
|
64
|
+
attempts: (current?.attempts || 0) + 1,
|
|
65
|
+
lastError: null
|
|
66
|
+
};
|
|
67
|
+
index.delivered[deliveryKey(mode, notificationFilePath)] = record;
|
|
68
|
+
if (options.dedupeKey) {
|
|
69
|
+
index.delivered[deliveryKey(mode, notificationFilePath, options.dedupeKey)] = record;
|
|
70
|
+
}
|
|
71
|
+
await this.saveIndex(index);
|
|
72
|
+
return record;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async markUncertain(mode, notificationFilePath, receiptFilePath, options = {}) {
|
|
76
|
+
const index = await this.loadIndex();
|
|
77
|
+
const current = await this.getDeliveryRecord(mode, notificationFilePath, options.dedupeKey);
|
|
78
|
+
const record = {
|
|
79
|
+
id: current?.id || createRuntimeId("delivery-record"),
|
|
80
|
+
receiptFilePath,
|
|
81
|
+
deliveredAt: null,
|
|
82
|
+
status: "uncertain",
|
|
83
|
+
dedupeKey: options.dedupeKey || null,
|
|
84
|
+
retryEligible: false,
|
|
85
|
+
uncertain: true,
|
|
86
|
+
attempts: (current?.attempts || 0) + 1,
|
|
87
|
+
lastError: options.error || null,
|
|
88
|
+
updatedAt: new Date().toISOString()
|
|
89
|
+
};
|
|
90
|
+
index.delivered[deliveryKey(mode, notificationFilePath)] = record;
|
|
91
|
+
if (options.dedupeKey) {
|
|
92
|
+
index.delivered[deliveryKey(mode, notificationFilePath, options.dedupeKey)] = record;
|
|
93
|
+
}
|
|
94
|
+
await this.saveIndex(index);
|
|
95
|
+
return record;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async markFailed(mode, notificationFilePath, receiptFilePath, options = {}) {
|
|
99
|
+
const index = await this.loadIndex();
|
|
100
|
+
const current = await this.getDeliveryRecord(mode, notificationFilePath, options.dedupeKey);
|
|
101
|
+
const record = {
|
|
102
|
+
id: current?.id || createRuntimeId("delivery-record"),
|
|
103
|
+
receiptFilePath,
|
|
104
|
+
deliveredAt: null,
|
|
105
|
+
status: "failed",
|
|
106
|
+
dedupeKey: options.dedupeKey || null,
|
|
107
|
+
retryEligible: options.retryEligible ?? false,
|
|
108
|
+
uncertain: false,
|
|
109
|
+
attempts: (current?.attempts || 0) + 1,
|
|
110
|
+
lastError: options.error || null,
|
|
111
|
+
updatedAt: new Date().toISOString()
|
|
112
|
+
};
|
|
113
|
+
index.delivered[deliveryKey(mode, notificationFilePath)] = record;
|
|
114
|
+
if (options.dedupeKey) {
|
|
115
|
+
index.delivered[deliveryKey(mode, notificationFilePath, options.dedupeKey)] = record;
|
|
116
|
+
}
|
|
117
|
+
await this.saveIndex(index);
|
|
118
|
+
return record;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async saveReceipt(jobId, receipt) {
|
|
122
|
+
const receiptDir = path.join(this.rootDir, jobId);
|
|
123
|
+
await ensureDir(receiptDir);
|
|
124
|
+
const filePath = path.join(receiptDir, `${stamp()}.json`);
|
|
125
|
+
await writeJson(filePath, {
|
|
126
|
+
id: receipt.id || createRuntimeId("delivery-attempt"),
|
|
127
|
+
...receipt
|
|
128
|
+
});
|
|
129
|
+
await pruneJsonBuckets(this.rootDir, this.retentionPolicy, {
|
|
130
|
+
excludePaths: [this.indexPath]
|
|
131
|
+
});
|
|
132
|
+
return filePath;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async listAll() {
|
|
136
|
+
const files = (await listFilesRecursive(this.rootDir)).filter(
|
|
137
|
+
(filePath) => filePath.endsWith(".json") && filePath !== this.indexPath
|
|
138
|
+
);
|
|
139
|
+
const receipts = [];
|
|
140
|
+
|
|
141
|
+
for (const filePath of files) {
|
|
142
|
+
const data = await readJson(filePath, null);
|
|
143
|
+
if (!data) continue;
|
|
144
|
+
receipts.push({
|
|
145
|
+
filePath,
|
|
146
|
+
...data
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return receipts.sort((a, b) => String(b.timestamp || "").localeCompare(String(a.timestamp || "")));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async listRecent(limit = 10) {
|
|
154
|
+
const receipts = await this.listAll();
|
|
155
|
+
return receipts.slice(0, limit);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async prune(options = {}) {
|
|
159
|
+
const result = await pruneJsonBuckets(this.rootDir, this.retentionPolicy, {
|
|
160
|
+
...options,
|
|
161
|
+
excludePaths: [...(options.excludePaths || []), this.indexPath]
|
|
162
|
+
});
|
|
163
|
+
return {
|
|
164
|
+
...result,
|
|
165
|
+
indexFilePath: this.indexPath
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -1,118 +1,118 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { execFile } from "node:child_process";
|
|
3
|
-
import { promisify } from "node:util";
|
|
4
|
-
import { ConfigLoader } from "../config/loader.js";
|
|
5
|
-
import { ProviderRegistry } from "../providers/registry.js";
|
|
6
|
-
import { PeerReadinessProbe } from "./peer-readiness.js";
|
|
7
|
-
import { Scheduler } from "./scheduler.js";
|
|
8
|
-
|
|
9
|
-
const execFileAsync = promisify(execFile);
|
|
10
|
-
|
|
11
|
-
function resolveNamedProfile(profiles, profileName) {
|
|
12
|
-
if (!profileName) return null;
|
|
13
|
-
return profiles[profileName] || profiles[String(profileName).replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())] || null;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export class DependencyHealth {
|
|
17
|
-
constructor({ projectRoot, liveRoot, fetchImpl, execFileImpl } = {}) {
|
|
18
|
-
this.loader = new ConfigLoader({ rootDir: path.join(projectRoot, "config") });
|
|
19
|
-
this.registry = new ProviderRegistry({ fetchImpl });
|
|
20
|
-
this.scheduler = new Scheduler({ projectRoot, liveRoot, stateRoot: path.join(projectRoot, "state") });
|
|
21
|
-
this.peerProbe = new PeerReadinessProbe({ liveRoot });
|
|
22
|
-
this.execFileImpl = execFileImpl || execFileAsync;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async provider(providerId, runtime = null) {
|
|
26
|
-
const loaded = runtime || (await this.loader.loadAll());
|
|
27
|
-
const providerConfig = loaded.providers?.[providerId];
|
|
28
|
-
if (!providerConfig) {
|
|
29
|
-
return { providerId, configured: false, reachable: false, failureClass: "provider_missing" };
|
|
30
|
-
}
|
|
31
|
-
try {
|
|
32
|
-
const adapter = this.registry.create(providerConfig);
|
|
33
|
-
const health = typeof adapter.healthCheck === "function" ? await adapter.healthCheck() : { ok: true, status: null };
|
|
34
|
-
return {
|
|
35
|
-
providerId,
|
|
36
|
-
configured: true,
|
|
37
|
-
reachable: Boolean(health.ok),
|
|
38
|
-
status: health.status ?? null,
|
|
39
|
-
failureClass: health.ok ? null : "provider_unhealthy"
|
|
40
|
-
};
|
|
41
|
-
} catch (error) {
|
|
42
|
-
return {
|
|
43
|
-
providerId,
|
|
44
|
-
configured: true,
|
|
45
|
-
reachable: false,
|
|
46
|
-
status: null,
|
|
47
|
-
failureClass: error.failureClass || error.networkClass || "provider_unreachable",
|
|
48
|
-
error: error.message
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async probeGatewayHealth() {
|
|
54
|
-
try {
|
|
55
|
-
await this.execFileImpl("openclaw", ["--version"], { timeout: 5000, maxBuffer: 64 * 1024 });
|
|
56
|
-
return { ok: true };
|
|
57
|
-
} catch (error) {
|
|
58
|
-
const code = error.code || "";
|
|
59
|
-
if (code === "ENOENT") {
|
|
60
|
-
return { ok: false, reason: "openclaw CLI binary not found on PATH." };
|
|
61
|
-
}
|
|
62
|
-
if (code === "ETIMEDOUT" || error.killed) {
|
|
63
|
-
return { ok: false, reason: "openclaw CLI timed out; the gateway may be stalled." };
|
|
64
|
-
}
|
|
65
|
-
return { ok: false, reason: `openclaw CLI probe failed: ${error.message || code}` };
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async delivery(profileName, runtime = null) {
|
|
70
|
-
const loaded = runtime || (await this.loader.loadAll());
|
|
71
|
-
const profile = resolveNamedProfile(loaded.delivery?.profiles || {}, profileName);
|
|
72
|
-
if (!profile) {
|
|
73
|
-
return { profileName, configured: false, reachable: false, failureClass: "delivery_profile_missing" };
|
|
74
|
-
}
|
|
75
|
-
const adapter = profile.adapter || null;
|
|
76
|
-
let reachable = true;
|
|
77
|
-
let failureClass = null;
|
|
78
|
-
const findings = [];
|
|
79
|
-
if (adapter === "openclaw_cli" || adapter === "openclaw_peer") {
|
|
80
|
-
const probeResult = await this.probeGatewayHealth();
|
|
81
|
-
reachable = probeResult.ok;
|
|
82
|
-
if (!reachable) {
|
|
83
|
-
failureClass = "delivery_gateway_unreachable";
|
|
84
|
-
findings.push(probeResult.reason || "OpenClaw gateway is not reachable for delivery.");
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return {
|
|
88
|
-
profileName,
|
|
89
|
-
configured: true,
|
|
90
|
-
adapter,
|
|
91
|
-
reachable,
|
|
92
|
-
failureClass: reachable ? null : (failureClass || "delivery_not_ready"),
|
|
93
|
-
findings
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async peer(peerId, runtime = null) {
|
|
98
|
-
const loaded = runtime || (await this.loader.loadAll());
|
|
99
|
-
return this.peerProbe.probe(loaded, peerId);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async forJob(jobId) {
|
|
103
|
-
const runtime = await this.loader.loadAll();
|
|
104
|
-
const plan = await this.scheduler.hydrateJob(jobId, { shadowImport: false });
|
|
105
|
-
const primaryProviderId = String(plan.routing.selectedLane || "").startsWith("openrouter")
|
|
106
|
-
? "openrouter"
|
|
107
|
-
: null;
|
|
108
|
-
const providerId = plan.routing.selectedLane === "local_report" || plan.routing.selectedLane === "local_cheap"
|
|
109
|
-
? "ollama"
|
|
110
|
-
: primaryProviderId || "anthropic";
|
|
111
|
-
|
|
112
|
-
const fallbackProfile = plan.agent.deliveryProfile || runtime.delivery?.defaultInteractiveProfile || null;
|
|
113
|
-
return {
|
|
114
|
-
provider: await this.provider(providerId, runtime),
|
|
115
|
-
delivery: await this.delivery(fallbackProfile, runtime)
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
}
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { ConfigLoader } from "../config/loader.js";
|
|
5
|
+
import { ProviderRegistry } from "../providers/registry.js";
|
|
6
|
+
import { PeerReadinessProbe } from "./peer-readiness.js";
|
|
7
|
+
import { Scheduler } from "./scheduler.js";
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
|
|
11
|
+
function resolveNamedProfile(profiles, profileName) {
|
|
12
|
+
if (!profileName) return null;
|
|
13
|
+
return profiles[profileName] || profiles[String(profileName).replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())] || null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class DependencyHealth {
|
|
17
|
+
constructor({ projectRoot, liveRoot, fetchImpl, execFileImpl } = {}) {
|
|
18
|
+
this.loader = new ConfigLoader({ rootDir: path.join(projectRoot, "config") });
|
|
19
|
+
this.registry = new ProviderRegistry({ fetchImpl });
|
|
20
|
+
this.scheduler = new Scheduler({ projectRoot, liveRoot, stateRoot: path.join(projectRoot, "state") });
|
|
21
|
+
this.peerProbe = new PeerReadinessProbe({ liveRoot });
|
|
22
|
+
this.execFileImpl = execFileImpl || execFileAsync;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async provider(providerId, runtime = null) {
|
|
26
|
+
const loaded = runtime || (await this.loader.loadAll());
|
|
27
|
+
const providerConfig = loaded.providers?.[providerId];
|
|
28
|
+
if (!providerConfig) {
|
|
29
|
+
return { providerId, configured: false, reachable: false, failureClass: "provider_missing" };
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const adapter = this.registry.create(providerConfig);
|
|
33
|
+
const health = typeof adapter.healthCheck === "function" ? await adapter.healthCheck() : { ok: true, status: null };
|
|
34
|
+
return {
|
|
35
|
+
providerId,
|
|
36
|
+
configured: true,
|
|
37
|
+
reachable: Boolean(health.ok),
|
|
38
|
+
status: health.status ?? null,
|
|
39
|
+
failureClass: health.ok ? null : "provider_unhealthy"
|
|
40
|
+
};
|
|
41
|
+
} catch (error) {
|
|
42
|
+
return {
|
|
43
|
+
providerId,
|
|
44
|
+
configured: true,
|
|
45
|
+
reachable: false,
|
|
46
|
+
status: null,
|
|
47
|
+
failureClass: error.failureClass || error.networkClass || "provider_unreachable",
|
|
48
|
+
error: error.message
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async probeGatewayHealth() {
|
|
54
|
+
try {
|
|
55
|
+
await this.execFileImpl("openclaw", ["--version"], { timeout: 5000, maxBuffer: 64 * 1024 });
|
|
56
|
+
return { ok: true };
|
|
57
|
+
} catch (error) {
|
|
58
|
+
const code = error.code || "";
|
|
59
|
+
if (code === "ENOENT") {
|
|
60
|
+
return { ok: false, reason: "openclaw CLI binary not found on PATH." };
|
|
61
|
+
}
|
|
62
|
+
if (code === "ETIMEDOUT" || error.killed) {
|
|
63
|
+
return { ok: false, reason: "openclaw CLI timed out; the gateway may be stalled." };
|
|
64
|
+
}
|
|
65
|
+
return { ok: false, reason: `openclaw CLI probe failed: ${error.message || code}` };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async delivery(profileName, runtime = null) {
|
|
70
|
+
const loaded = runtime || (await this.loader.loadAll());
|
|
71
|
+
const profile = resolveNamedProfile(loaded.delivery?.profiles || {}, profileName);
|
|
72
|
+
if (!profile) {
|
|
73
|
+
return { profileName, configured: false, reachable: false, failureClass: "delivery_profile_missing" };
|
|
74
|
+
}
|
|
75
|
+
const adapter = profile.adapter || null;
|
|
76
|
+
let reachable = true;
|
|
77
|
+
let failureClass = null;
|
|
78
|
+
const findings = [];
|
|
79
|
+
if (adapter === "openclaw_cli" || adapter === "openclaw_peer") {
|
|
80
|
+
const probeResult = await this.probeGatewayHealth();
|
|
81
|
+
reachable = probeResult.ok;
|
|
82
|
+
if (!reachable) {
|
|
83
|
+
failureClass = "delivery_gateway_unreachable";
|
|
84
|
+
findings.push(probeResult.reason || "OpenClaw gateway is not reachable for delivery.");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
profileName,
|
|
89
|
+
configured: true,
|
|
90
|
+
adapter,
|
|
91
|
+
reachable,
|
|
92
|
+
failureClass: reachable ? null : (failureClass || "delivery_not_ready"),
|
|
93
|
+
findings
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async peer(peerId, runtime = null) {
|
|
98
|
+
const loaded = runtime || (await this.loader.loadAll());
|
|
99
|
+
return this.peerProbe.probe(loaded, peerId);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async forJob(jobId) {
|
|
103
|
+
const runtime = await this.loader.loadAll();
|
|
104
|
+
const plan = await this.scheduler.hydrateJob(jobId, { shadowImport: false });
|
|
105
|
+
const primaryProviderId = String(plan.routing.selectedLane || "").startsWith("openrouter")
|
|
106
|
+
? "openrouter"
|
|
107
|
+
: null;
|
|
108
|
+
const providerId = plan.routing.selectedLane === "local_report" || plan.routing.selectedLane === "local_cheap"
|
|
109
|
+
? "ollama"
|
|
110
|
+
: primaryProviderId || "anthropic";
|
|
111
|
+
|
|
112
|
+
const fallbackProfile = plan.agent.deliveryProfile || runtime.delivery?.defaultInteractiveProfile || null;
|
|
113
|
+
return {
|
|
114
|
+
provider: await this.provider(providerId, runtime),
|
|
115
|
+
delivery: await this.delivery(fallbackProfile, runtime)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|