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,330 +1,330 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { ConfigLoader } from "../config/loader.js";
|
|
3
|
-
import { MemoryStore } from "../memory/memory-store.js";
|
|
4
|
-
import { NotificationStore } from "./notification-store.js";
|
|
5
|
-
import { DeliveryStore } from "./delivery-store.js";
|
|
6
|
-
import { PeerRegistry } from "./peer-registry.js";
|
|
7
|
-
import { SchedulerStateStore } from "./scheduler-state.js";
|
|
8
|
-
import { computeNextRun } from "./schedule.js";
|
|
9
|
-
import { ProviderRegistry } from "../providers/registry.js";
|
|
10
|
-
|
|
11
|
-
function _safeCount(arr) {
|
|
12
|
-
return Array.isArray(arr) ? arr.length : 0;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function _safeSlice(arr, n) {
|
|
16
|
-
return Array.isArray(arr) ? arr.slice(0, n) : [];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function resolveEmbeddingsStatus(enabled) {
|
|
20
|
-
if (enabled === false) return "disabled";
|
|
21
|
-
if (enabled === true) return "enabled";
|
|
22
|
-
return "unknown";
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function shouldHideJobFromUserStatus(job) {
|
|
26
|
-
return job?.id === "memory-rollup" && job?.lastStatus === "error";
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function collectAgents(runtime, memoryStore) {
|
|
30
|
-
const agents = [];
|
|
31
|
-
for (const [agentId, agent] of Object.entries(runtime.agents || {})) {
|
|
32
|
-
let memoryHealth;
|
|
33
|
-
try {
|
|
34
|
-
const paths = await memoryStore.initAgent(agentId);
|
|
35
|
-
const sqlite = await memoryStore.ensureSqliteStore(paths);
|
|
36
|
-
memoryHealth = {
|
|
37
|
-
totalEntries: sqlite.count(),
|
|
38
|
-
embeddingHealth: sqlite.getEmbeddingHealth()
|
|
39
|
-
};
|
|
40
|
-
} catch {
|
|
41
|
-
memoryHealth = { totalEntries: 0, embeddingHealth: null, error: "unable_to_read" };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
agents.push({
|
|
45
|
-
id: agentId,
|
|
46
|
-
primaryLane: agent.primaryLane || null,
|
|
47
|
-
fallbackLane: agent.fallbackLane || null,
|
|
48
|
-
memoryPolicy: agent.memoryPolicy || null,
|
|
49
|
-
toolPolicy: agent.toolPolicy || null,
|
|
50
|
-
deliveryProfile: agent.delivery?.profile || null,
|
|
51
|
-
memoryHealth
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
return agents;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function collectJobs(runtime, schedulerState) {
|
|
58
|
-
const jobs = [];
|
|
59
|
-
const jobStates = schedulerState?.jobs || {};
|
|
60
|
-
for (const [jobId, jobConfig] of Object.entries(runtime.jobs || {})) {
|
|
61
|
-
const state = jobStates[jobId] || {};
|
|
62
|
-
let nextDue = null;
|
|
63
|
-
try {
|
|
64
|
-
if (jobConfig.trigger) {
|
|
65
|
-
nextDue = computeNextRun(jobConfig.trigger, new Date()).toISOString();
|
|
66
|
-
}
|
|
67
|
-
} catch {
|
|
68
|
-
// trigger may not be parseable without context
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
jobs.push({
|
|
72
|
-
id: jobId,
|
|
73
|
-
trigger: jobConfig.trigger || null,
|
|
74
|
-
taskType: jobConfig.taskType || null,
|
|
75
|
-
agentId: jobConfig.agentId || null,
|
|
76
|
-
lastRunAt: state.lastRunAt || null,
|
|
77
|
-
lastStatus: state.lastStatus || null,
|
|
78
|
-
nextDue
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
return jobs;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function collectProviders(runtime, providerRegistry) {
|
|
85
|
-
const providers = [];
|
|
86
|
-
for (const [providerId, providerConfig] of Object.entries(runtime.providers || {})) {
|
|
87
|
-
let capabilities;
|
|
88
|
-
try {
|
|
89
|
-
const desc = providerRegistry.describe(providerConfig);
|
|
90
|
-
capabilities = desc.capabilities || null;
|
|
91
|
-
} catch {
|
|
92
|
-
capabilities = null;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
providers.push({
|
|
96
|
-
id: providerId,
|
|
97
|
-
adapter: providerConfig.adapter || providerId,
|
|
98
|
-
baseUrl: providerConfig.baseUrl || null,
|
|
99
|
-
capabilities
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
return providers;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function collectDelivery(deliveryStore, runtime) {
|
|
106
|
-
const deliveryConfig = runtime.delivery || {};
|
|
107
|
-
const profiles = Object.keys(deliveryConfig.profiles || {});
|
|
108
|
-
let recentFailureCount = 0;
|
|
109
|
-
let totalRecords = 0;
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
const index = await deliveryStore.loadIndex();
|
|
113
|
-
const records = Object.values(index.delivered || {});
|
|
114
|
-
totalRecords = records.length;
|
|
115
|
-
recentFailureCount = records.filter(
|
|
116
|
-
(record) => record.status === "failed" || record.status === "uncertain"
|
|
117
|
-
).length;
|
|
118
|
-
} catch {
|
|
119
|
-
// delivery store may not exist yet
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
profiles,
|
|
124
|
-
defaultInteractiveProfile: deliveryConfig.defaultInteractiveProfile || null,
|
|
125
|
-
defaultSchedulerProfile: deliveryConfig.defaultSchedulerProfile || null,
|
|
126
|
-
standaloneMode: Boolean(
|
|
127
|
-
process.env.NEMORIS_STANDALONE === "1" ||
|
|
128
|
-
process.env.NEMORIS_STANDALONE === "true" ||
|
|
129
|
-
deliveryConfig.standaloneMode
|
|
130
|
-
),
|
|
131
|
-
totalRecords,
|
|
132
|
-
recentFailureCount
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function collectPeers(runtime) {
|
|
137
|
-
const peerRegistry = new PeerRegistry(runtime.peers || {});
|
|
138
|
-
const peers = peerRegistry.list();
|
|
139
|
-
return peers.map((peer) => ({
|
|
140
|
-
peerId: peer.peerId,
|
|
141
|
-
label: peer.label || peer.peerId,
|
|
142
|
-
agentId: peer.agentId || null,
|
|
143
|
-
deliveryProfile: peer.deliveryProfile || null,
|
|
144
|
-
sessionKeys: peer.sessionKeys || [],
|
|
145
|
-
hasCard: Boolean(peer.card)
|
|
146
|
-
}));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function collectRuntime(runtime, notifications) {
|
|
150
|
-
const retention = runtime.runtime?.retention || {};
|
|
151
|
-
const concurrency = runtime.runtime?.concurrency || {};
|
|
152
|
-
const retrieval = runtime.runtime?.retrieval || {};
|
|
153
|
-
|
|
154
|
-
const followUps = (notifications || []).filter((n) => n.stage === "follow_up");
|
|
155
|
-
const pendingFollowUps = followUps.filter((n) => n.status === "pending");
|
|
156
|
-
const handoffs = (notifications || []).filter((n) => n.stage === "handoff");
|
|
157
|
-
const pendingHandoffs = handoffs.filter(
|
|
158
|
-
(n) => n.status === "awaiting_choice" || n.handoffState === "pending"
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
return {
|
|
162
|
-
concurrency: {
|
|
163
|
-
maxJobsPerTick: concurrency.maxJobsPerTick ?? concurrency.maxConcurrentJobs ?? 2
|
|
164
|
-
},
|
|
165
|
-
embeddings: {
|
|
166
|
-
enabled: runtime.embeddings?.enabled ?? null
|
|
167
|
-
},
|
|
168
|
-
retention: {
|
|
169
|
-
runs: retention.runs || {},
|
|
170
|
-
notifications: retention.notifications || {},
|
|
171
|
-
deliveries: retention.deliveries || {}
|
|
172
|
-
},
|
|
173
|
-
retrieval: {
|
|
174
|
-
lexicalWeight: retrieval.lexicalWeight ?? null,
|
|
175
|
-
embeddingWeight: retrieval.embeddingWeight ?? null,
|
|
176
|
-
recencyWeight: retrieval.recencyWeight ?? null
|
|
177
|
-
},
|
|
178
|
-
followUpStats: {
|
|
179
|
-
total: followUps.length,
|
|
180
|
-
pending: pendingFollowUps.length
|
|
181
|
-
},
|
|
182
|
-
handoffStats: {
|
|
183
|
-
total: handoffs.length,
|
|
184
|
-
pending: pendingHandoffs.length
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export async function buildRuntimeStatus(options = {}) {
|
|
190
|
-
const {
|
|
191
|
-
projectRoot,
|
|
192
|
-
stateRoot,
|
|
193
|
-
liveRoot: _liveRoot,
|
|
194
|
-
// Allow injecting dependencies for testing
|
|
195
|
-
configLoader,
|
|
196
|
-
memoryStore,
|
|
197
|
-
notificationStore,
|
|
198
|
-
deliveryStore,
|
|
199
|
-
schedulerStateStore,
|
|
200
|
-
providerRegistry
|
|
201
|
-
} = options;
|
|
202
|
-
|
|
203
|
-
const loader = configLoader || new ConfigLoader({ rootDir: path.join(projectRoot, "config") });
|
|
204
|
-
const memory = memoryStore || new MemoryStore({ rootDir: path.join(stateRoot, "memory") });
|
|
205
|
-
const notifications = notificationStore || new NotificationStore({ rootDir: path.join(stateRoot, "notifications") });
|
|
206
|
-
const deliveries = deliveryStore || new DeliveryStore({ rootDir: path.join(stateRoot, "deliveries") });
|
|
207
|
-
const stateStore = schedulerStateStore || new SchedulerStateStore({ rootDir: path.join(stateRoot, "scheduler") });
|
|
208
|
-
const registry = providerRegistry || new ProviderRegistry();
|
|
209
|
-
|
|
210
|
-
let runtime;
|
|
211
|
-
let configErrors = [];
|
|
212
|
-
try {
|
|
213
|
-
runtime = await loader.loadAll();
|
|
214
|
-
} catch (err) {
|
|
215
|
-
// Hard validation error or config parse failure — return degraded status
|
|
216
|
-
configErrors = (err.validationErrors?.map((e) => e.details || e.code) || [err.message || "Unknown config error"]).filter(Boolean);
|
|
217
|
-
try {
|
|
218
|
-
runtime = await loader.loadAll({ skipValidation: true });
|
|
219
|
-
} catch {
|
|
220
|
-
return {
|
|
221
|
-
status: "degraded",
|
|
222
|
-
configErrors,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const [schedulerState, allNotifications, agents] = await Promise.all([
|
|
228
|
-
stateStore.load().catch(() => ({ jobs: {} })),
|
|
229
|
-
notifications.listAll().catch(() => []),
|
|
230
|
-
collectAgents(runtime, memory)
|
|
231
|
-
]);
|
|
232
|
-
|
|
233
|
-
const [deliveryStatus, jobs, providers, peers, runtimeStats] = await Promise.all([
|
|
234
|
-
collectDelivery(deliveries, runtime),
|
|
235
|
-
Promise.resolve(collectJobs(runtime, schedulerState)),
|
|
236
|
-
Promise.resolve(collectProviders(runtime, registry)),
|
|
237
|
-
Promise.resolve(collectPeers(runtime)),
|
|
238
|
-
Promise.resolve(collectRuntime(runtime, allNotifications))
|
|
239
|
-
]);
|
|
240
|
-
|
|
241
|
-
return {
|
|
242
|
-
timestamp: new Date().toISOString(),
|
|
243
|
-
agents,
|
|
244
|
-
jobs,
|
|
245
|
-
providers,
|
|
246
|
-
delivery: deliveryStatus,
|
|
247
|
-
peers,
|
|
248
|
-
runtime: runtimeStats
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
export function formatRuntimeStatus(status) {
|
|
253
|
-
// Early exit for degraded status — avoid shape mismatches with partial config
|
|
254
|
-
if (status.status === "degraded") {
|
|
255
|
-
const lines = [
|
|
256
|
-
"=== Nemoris V2 Runtime Status ===",
|
|
257
|
-
`Timestamp: ${status.timestamp || new Date().toISOString()}`,
|
|
258
|
-
"",
|
|
259
|
-
"⚠ Status: degraded — run `nemoris doctor` for details",
|
|
260
|
-
];
|
|
261
|
-
for (const err of (status.configErrors || [])) {
|
|
262
|
-
lines.push(` • ${err}`);
|
|
263
|
-
}
|
|
264
|
-
return lines.join("\n");
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const lines = [];
|
|
268
|
-
const embeddingsStatus = resolveEmbeddingsStatus(status.runtime?.embeddings?.enabled);
|
|
269
|
-
const visibleJobs = (status.jobs || []).filter((job) => !shouldHideJobFromUserStatus(job));
|
|
270
|
-
|
|
271
|
-
lines.push("=== Nemoris V2 Runtime Status ===");
|
|
272
|
-
lines.push(`Timestamp: ${status.timestamp}`);
|
|
273
|
-
if (status.configErrors?.length) {
|
|
274
|
-
lines.push("");
|
|
275
|
-
lines.push(`⚠ Config validation: ${status.configErrors.length} issue(s)`);
|
|
276
|
-
for (const err of status.configErrors) {
|
|
277
|
-
lines.push(` • ${err}`);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
lines.push("");
|
|
281
|
-
|
|
282
|
-
// Agents
|
|
283
|
-
lines.push(`--- Agents (${status.agents.length}) ---`);
|
|
284
|
-
for (const agent of status.agents) {
|
|
285
|
-
const memEntries = agent.memoryHealth?.totalEntries ?? "?";
|
|
286
|
-
lines.push(` ${agent.id}`);
|
|
287
|
-
lines.push(` lane: ${agent.primaryLane || "(none)"} memory entries: ${memEntries} embeddings: ${embeddingsStatus}`);
|
|
288
|
-
}
|
|
289
|
-
lines.push("");
|
|
290
|
-
|
|
291
|
-
// Jobs
|
|
292
|
-
lines.push(`--- Jobs (${visibleJobs.length}) ---`);
|
|
293
|
-
for (const job of visibleJobs) {
|
|
294
|
-
const lastRun = job.lastRunAt ? `last: ${job.lastRunAt}` : "last: never";
|
|
295
|
-
const lastStatus = job.lastStatus ? `status: ${job.lastStatus}` : "status: (none)";
|
|
296
|
-
const next = job.nextDue ? `next: ${job.nextDue}` : "next: (unknown)";
|
|
297
|
-
lines.push(` ${job.id} [${job.trigger || "?"}]`);
|
|
298
|
-
lines.push(` ${lastRun} ${lastStatus} ${next}`);
|
|
299
|
-
}
|
|
300
|
-
lines.push("");
|
|
301
|
-
|
|
302
|
-
// Providers
|
|
303
|
-
lines.push(`--- Providers (${status.providers.length}) ---`);
|
|
304
|
-
for (const provider of status.providers) {
|
|
305
|
-
lines.push(` ${provider.id} (${provider.adapter}) ${provider.baseUrl || "(no url)"}`);
|
|
306
|
-
}
|
|
307
|
-
lines.push("");
|
|
308
|
-
|
|
309
|
-
// Delivery
|
|
310
|
-
lines.push("--- Delivery ---");
|
|
311
|
-
lines.push(` profiles: ${status.delivery.profiles.join(", ") || "(none)"}`);
|
|
312
|
-
lines.push(` standalone: ${status.delivery.standaloneMode}`);
|
|
313
|
-
lines.push(` records: ${status.delivery.totalRecords} failures: ${status.delivery.recentFailureCount}`);
|
|
314
|
-
lines.push("");
|
|
315
|
-
|
|
316
|
-
// Peers
|
|
317
|
-
lines.push(`--- Peers (${status.peers.length}) ---`);
|
|
318
|
-
for (const peer of status.peers) {
|
|
319
|
-
lines.push(` ${peer.peerId} label: ${peer.label} card: ${peer.hasCard ? "yes" : "no"}`);
|
|
320
|
-
}
|
|
321
|
-
lines.push("");
|
|
322
|
-
|
|
323
|
-
// Runtime
|
|
324
|
-
lines.push("--- Runtime ---");
|
|
325
|
-
lines.push(` max jobs/tick: ${status.runtime.concurrency.maxJobsPerTick}`);
|
|
326
|
-
lines.push(` follow-ups: ${status.runtime.followUpStats.total} total, ${status.runtime.followUpStats.pending} pending`);
|
|
327
|
-
lines.push(` handoffs: ${status.runtime.handoffStats.total} total, ${status.runtime.handoffStats.pending} pending`);
|
|
328
|
-
|
|
329
|
-
return lines.join("\n");
|
|
330
|
-
}
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { ConfigLoader } from "../config/loader.js";
|
|
3
|
+
import { MemoryStore } from "../memory/memory-store.js";
|
|
4
|
+
import { NotificationStore } from "./notification-store.js";
|
|
5
|
+
import { DeliveryStore } from "./delivery-store.js";
|
|
6
|
+
import { PeerRegistry } from "./peer-registry.js";
|
|
7
|
+
import { SchedulerStateStore } from "./scheduler-state.js";
|
|
8
|
+
import { computeNextRun } from "./schedule.js";
|
|
9
|
+
import { ProviderRegistry } from "../providers/registry.js";
|
|
10
|
+
|
|
11
|
+
function _safeCount(arr) {
|
|
12
|
+
return Array.isArray(arr) ? arr.length : 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function _safeSlice(arr, n) {
|
|
16
|
+
return Array.isArray(arr) ? arr.slice(0, n) : [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveEmbeddingsStatus(enabled) {
|
|
20
|
+
if (enabled === false) return "disabled";
|
|
21
|
+
if (enabled === true) return "enabled";
|
|
22
|
+
return "unknown";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function shouldHideJobFromUserStatus(job) {
|
|
26
|
+
return job?.id === "memory-rollup" && job?.lastStatus === "error";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function collectAgents(runtime, memoryStore) {
|
|
30
|
+
const agents = [];
|
|
31
|
+
for (const [agentId, agent] of Object.entries(runtime.agents || {})) {
|
|
32
|
+
let memoryHealth;
|
|
33
|
+
try {
|
|
34
|
+
const paths = await memoryStore.initAgent(agentId);
|
|
35
|
+
const sqlite = await memoryStore.ensureSqliteStore(paths);
|
|
36
|
+
memoryHealth = {
|
|
37
|
+
totalEntries: sqlite.count(),
|
|
38
|
+
embeddingHealth: sqlite.getEmbeddingHealth()
|
|
39
|
+
};
|
|
40
|
+
} catch {
|
|
41
|
+
memoryHealth = { totalEntries: 0, embeddingHealth: null, error: "unable_to_read" };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
agents.push({
|
|
45
|
+
id: agentId,
|
|
46
|
+
primaryLane: agent.primaryLane || null,
|
|
47
|
+
fallbackLane: agent.fallbackLane || null,
|
|
48
|
+
memoryPolicy: agent.memoryPolicy || null,
|
|
49
|
+
toolPolicy: agent.toolPolicy || null,
|
|
50
|
+
deliveryProfile: agent.delivery?.profile || null,
|
|
51
|
+
memoryHealth
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return agents;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function collectJobs(runtime, schedulerState) {
|
|
58
|
+
const jobs = [];
|
|
59
|
+
const jobStates = schedulerState?.jobs || {};
|
|
60
|
+
for (const [jobId, jobConfig] of Object.entries(runtime.jobs || {})) {
|
|
61
|
+
const state = jobStates[jobId] || {};
|
|
62
|
+
let nextDue = null;
|
|
63
|
+
try {
|
|
64
|
+
if (jobConfig.trigger) {
|
|
65
|
+
nextDue = computeNextRun(jobConfig.trigger, new Date()).toISOString();
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// trigger may not be parseable without context
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
jobs.push({
|
|
72
|
+
id: jobId,
|
|
73
|
+
trigger: jobConfig.trigger || null,
|
|
74
|
+
taskType: jobConfig.taskType || null,
|
|
75
|
+
agentId: jobConfig.agentId || null,
|
|
76
|
+
lastRunAt: state.lastRunAt || null,
|
|
77
|
+
lastStatus: state.lastStatus || null,
|
|
78
|
+
nextDue
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return jobs;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function collectProviders(runtime, providerRegistry) {
|
|
85
|
+
const providers = [];
|
|
86
|
+
for (const [providerId, providerConfig] of Object.entries(runtime.providers || {})) {
|
|
87
|
+
let capabilities;
|
|
88
|
+
try {
|
|
89
|
+
const desc = providerRegistry.describe(providerConfig);
|
|
90
|
+
capabilities = desc.capabilities || null;
|
|
91
|
+
} catch {
|
|
92
|
+
capabilities = null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
providers.push({
|
|
96
|
+
id: providerId,
|
|
97
|
+
adapter: providerConfig.adapter || providerId,
|
|
98
|
+
baseUrl: providerConfig.baseUrl || null,
|
|
99
|
+
capabilities
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return providers;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function collectDelivery(deliveryStore, runtime) {
|
|
106
|
+
const deliveryConfig = runtime.delivery || {};
|
|
107
|
+
const profiles = Object.keys(deliveryConfig.profiles || {});
|
|
108
|
+
let recentFailureCount = 0;
|
|
109
|
+
let totalRecords = 0;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const index = await deliveryStore.loadIndex();
|
|
113
|
+
const records = Object.values(index.delivered || {});
|
|
114
|
+
totalRecords = records.length;
|
|
115
|
+
recentFailureCount = records.filter(
|
|
116
|
+
(record) => record.status === "failed" || record.status === "uncertain"
|
|
117
|
+
).length;
|
|
118
|
+
} catch {
|
|
119
|
+
// delivery store may not exist yet
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
profiles,
|
|
124
|
+
defaultInteractiveProfile: deliveryConfig.defaultInteractiveProfile || null,
|
|
125
|
+
defaultSchedulerProfile: deliveryConfig.defaultSchedulerProfile || null,
|
|
126
|
+
standaloneMode: Boolean(
|
|
127
|
+
process.env.NEMORIS_STANDALONE === "1" ||
|
|
128
|
+
process.env.NEMORIS_STANDALONE === "true" ||
|
|
129
|
+
deliveryConfig.standaloneMode
|
|
130
|
+
),
|
|
131
|
+
totalRecords,
|
|
132
|
+
recentFailureCount
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function collectPeers(runtime) {
|
|
137
|
+
const peerRegistry = new PeerRegistry(runtime.peers || {});
|
|
138
|
+
const peers = peerRegistry.list();
|
|
139
|
+
return peers.map((peer) => ({
|
|
140
|
+
peerId: peer.peerId,
|
|
141
|
+
label: peer.label || peer.peerId,
|
|
142
|
+
agentId: peer.agentId || null,
|
|
143
|
+
deliveryProfile: peer.deliveryProfile || null,
|
|
144
|
+
sessionKeys: peer.sessionKeys || [],
|
|
145
|
+
hasCard: Boolean(peer.card)
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function collectRuntime(runtime, notifications) {
|
|
150
|
+
const retention = runtime.runtime?.retention || {};
|
|
151
|
+
const concurrency = runtime.runtime?.concurrency || {};
|
|
152
|
+
const retrieval = runtime.runtime?.retrieval || {};
|
|
153
|
+
|
|
154
|
+
const followUps = (notifications || []).filter((n) => n.stage === "follow_up");
|
|
155
|
+
const pendingFollowUps = followUps.filter((n) => n.status === "pending");
|
|
156
|
+
const handoffs = (notifications || []).filter((n) => n.stage === "handoff");
|
|
157
|
+
const pendingHandoffs = handoffs.filter(
|
|
158
|
+
(n) => n.status === "awaiting_choice" || n.handoffState === "pending"
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
concurrency: {
|
|
163
|
+
maxJobsPerTick: concurrency.maxJobsPerTick ?? concurrency.maxConcurrentJobs ?? 2
|
|
164
|
+
},
|
|
165
|
+
embeddings: {
|
|
166
|
+
enabled: runtime.embeddings?.enabled ?? null
|
|
167
|
+
},
|
|
168
|
+
retention: {
|
|
169
|
+
runs: retention.runs || {},
|
|
170
|
+
notifications: retention.notifications || {},
|
|
171
|
+
deliveries: retention.deliveries || {}
|
|
172
|
+
},
|
|
173
|
+
retrieval: {
|
|
174
|
+
lexicalWeight: retrieval.lexicalWeight ?? null,
|
|
175
|
+
embeddingWeight: retrieval.embeddingWeight ?? null,
|
|
176
|
+
recencyWeight: retrieval.recencyWeight ?? null
|
|
177
|
+
},
|
|
178
|
+
followUpStats: {
|
|
179
|
+
total: followUps.length,
|
|
180
|
+
pending: pendingFollowUps.length
|
|
181
|
+
},
|
|
182
|
+
handoffStats: {
|
|
183
|
+
total: handoffs.length,
|
|
184
|
+
pending: pendingHandoffs.length
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function buildRuntimeStatus(options = {}) {
|
|
190
|
+
const {
|
|
191
|
+
projectRoot,
|
|
192
|
+
stateRoot,
|
|
193
|
+
liveRoot: _liveRoot,
|
|
194
|
+
// Allow injecting dependencies for testing
|
|
195
|
+
configLoader,
|
|
196
|
+
memoryStore,
|
|
197
|
+
notificationStore,
|
|
198
|
+
deliveryStore,
|
|
199
|
+
schedulerStateStore,
|
|
200
|
+
providerRegistry
|
|
201
|
+
} = options;
|
|
202
|
+
|
|
203
|
+
const loader = configLoader || new ConfigLoader({ rootDir: path.join(projectRoot, "config") });
|
|
204
|
+
const memory = memoryStore || new MemoryStore({ rootDir: path.join(stateRoot, "memory") });
|
|
205
|
+
const notifications = notificationStore || new NotificationStore({ rootDir: path.join(stateRoot, "notifications") });
|
|
206
|
+
const deliveries = deliveryStore || new DeliveryStore({ rootDir: path.join(stateRoot, "deliveries") });
|
|
207
|
+
const stateStore = schedulerStateStore || new SchedulerStateStore({ rootDir: path.join(stateRoot, "scheduler") });
|
|
208
|
+
const registry = providerRegistry || new ProviderRegistry();
|
|
209
|
+
|
|
210
|
+
let runtime;
|
|
211
|
+
let configErrors = [];
|
|
212
|
+
try {
|
|
213
|
+
runtime = await loader.loadAll();
|
|
214
|
+
} catch (err) {
|
|
215
|
+
// Hard validation error or config parse failure — return degraded status
|
|
216
|
+
configErrors = (err.validationErrors?.map((e) => e.details || e.code) || [err.message || "Unknown config error"]).filter(Boolean);
|
|
217
|
+
try {
|
|
218
|
+
runtime = await loader.loadAll({ skipValidation: true });
|
|
219
|
+
} catch {
|
|
220
|
+
return {
|
|
221
|
+
status: "degraded",
|
|
222
|
+
configErrors,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const [schedulerState, allNotifications, agents] = await Promise.all([
|
|
228
|
+
stateStore.load().catch(() => ({ jobs: {} })),
|
|
229
|
+
notifications.listAll().catch(() => []),
|
|
230
|
+
collectAgents(runtime, memory)
|
|
231
|
+
]);
|
|
232
|
+
|
|
233
|
+
const [deliveryStatus, jobs, providers, peers, runtimeStats] = await Promise.all([
|
|
234
|
+
collectDelivery(deliveries, runtime),
|
|
235
|
+
Promise.resolve(collectJobs(runtime, schedulerState)),
|
|
236
|
+
Promise.resolve(collectProviders(runtime, registry)),
|
|
237
|
+
Promise.resolve(collectPeers(runtime)),
|
|
238
|
+
Promise.resolve(collectRuntime(runtime, allNotifications))
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
timestamp: new Date().toISOString(),
|
|
243
|
+
agents,
|
|
244
|
+
jobs,
|
|
245
|
+
providers,
|
|
246
|
+
delivery: deliveryStatus,
|
|
247
|
+
peers,
|
|
248
|
+
runtime: runtimeStats
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function formatRuntimeStatus(status) {
|
|
253
|
+
// Early exit for degraded status — avoid shape mismatches with partial config
|
|
254
|
+
if (status.status === "degraded") {
|
|
255
|
+
const lines = [
|
|
256
|
+
"=== Nemoris V2 Runtime Status ===",
|
|
257
|
+
`Timestamp: ${status.timestamp || new Date().toISOString()}`,
|
|
258
|
+
"",
|
|
259
|
+
"⚠ Status: degraded — run `nemoris doctor` for details",
|
|
260
|
+
];
|
|
261
|
+
for (const err of (status.configErrors || [])) {
|
|
262
|
+
lines.push(` • ${err}`);
|
|
263
|
+
}
|
|
264
|
+
return lines.join("\n");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const lines = [];
|
|
268
|
+
const embeddingsStatus = resolveEmbeddingsStatus(status.runtime?.embeddings?.enabled);
|
|
269
|
+
const visibleJobs = (status.jobs || []).filter((job) => !shouldHideJobFromUserStatus(job));
|
|
270
|
+
|
|
271
|
+
lines.push("=== Nemoris V2 Runtime Status ===");
|
|
272
|
+
lines.push(`Timestamp: ${status.timestamp}`);
|
|
273
|
+
if (status.configErrors?.length) {
|
|
274
|
+
lines.push("");
|
|
275
|
+
lines.push(`⚠ Config validation: ${status.configErrors.length} issue(s)`);
|
|
276
|
+
for (const err of status.configErrors) {
|
|
277
|
+
lines.push(` • ${err}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
lines.push("");
|
|
281
|
+
|
|
282
|
+
// Agents
|
|
283
|
+
lines.push(`--- Agents (${status.agents.length}) ---`);
|
|
284
|
+
for (const agent of status.agents) {
|
|
285
|
+
const memEntries = agent.memoryHealth?.totalEntries ?? "?";
|
|
286
|
+
lines.push(` ${agent.id}`);
|
|
287
|
+
lines.push(` lane: ${agent.primaryLane || "(none)"} memory entries: ${memEntries} embeddings: ${embeddingsStatus}`);
|
|
288
|
+
}
|
|
289
|
+
lines.push("");
|
|
290
|
+
|
|
291
|
+
// Jobs
|
|
292
|
+
lines.push(`--- Jobs (${visibleJobs.length}) ---`);
|
|
293
|
+
for (const job of visibleJobs) {
|
|
294
|
+
const lastRun = job.lastRunAt ? `last: ${job.lastRunAt}` : "last: never";
|
|
295
|
+
const lastStatus = job.lastStatus ? `status: ${job.lastStatus}` : "status: (none)";
|
|
296
|
+
const next = job.nextDue ? `next: ${job.nextDue}` : "next: (unknown)";
|
|
297
|
+
lines.push(` ${job.id} [${job.trigger || "?"}]`);
|
|
298
|
+
lines.push(` ${lastRun} ${lastStatus} ${next}`);
|
|
299
|
+
}
|
|
300
|
+
lines.push("");
|
|
301
|
+
|
|
302
|
+
// Providers
|
|
303
|
+
lines.push(`--- Providers (${status.providers.length}) ---`);
|
|
304
|
+
for (const provider of status.providers) {
|
|
305
|
+
lines.push(` ${provider.id} (${provider.adapter}) ${provider.baseUrl || "(no url)"}`);
|
|
306
|
+
}
|
|
307
|
+
lines.push("");
|
|
308
|
+
|
|
309
|
+
// Delivery
|
|
310
|
+
lines.push("--- Delivery ---");
|
|
311
|
+
lines.push(` profiles: ${status.delivery.profiles.join(", ") || "(none)"}`);
|
|
312
|
+
lines.push(` standalone: ${status.delivery.standaloneMode}`);
|
|
313
|
+
lines.push(` records: ${status.delivery.totalRecords} failures: ${status.delivery.recentFailureCount}`);
|
|
314
|
+
lines.push("");
|
|
315
|
+
|
|
316
|
+
// Peers
|
|
317
|
+
lines.push(`--- Peers (${status.peers.length}) ---`);
|
|
318
|
+
for (const peer of status.peers) {
|
|
319
|
+
lines.push(` ${peer.peerId} label: ${peer.label} card: ${peer.hasCard ? "yes" : "no"}`);
|
|
320
|
+
}
|
|
321
|
+
lines.push("");
|
|
322
|
+
|
|
323
|
+
// Runtime
|
|
324
|
+
lines.push("--- Runtime ---");
|
|
325
|
+
lines.push(` max jobs/tick: ${status.runtime.concurrency.maxJobsPerTick}`);
|
|
326
|
+
lines.push(` follow-ups: ${status.runtime.followUpStats.total} total, ${status.runtime.followUpStats.pending} pending`);
|
|
327
|
+
lines.push(` handoffs: ${status.runtime.handoffStats.total} total, ${status.runtime.handoffStats.pending} pending`);
|
|
328
|
+
|
|
329
|
+
return lines.join("\n");
|
|
330
|
+
}
|