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.
Files changed (223) hide show
  1. package/.env.example +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +209 -0
  4. package/SECURITY.md +119 -0
  5. package/bin/nemoris +46 -0
  6. package/config/agents/agent.toml.example +28 -0
  7. package/config/agents/default.toml +22 -0
  8. package/config/agents/orchestrator.toml +18 -0
  9. package/config/delivery.toml +73 -0
  10. package/config/embeddings.toml +5 -0
  11. package/config/identity/default-purpose.md +1 -0
  12. package/config/identity/default-soul.md +3 -0
  13. package/config/identity/orchestrator-purpose.md +1 -0
  14. package/config/identity/orchestrator-soul.md +1 -0
  15. package/config/improvement-targets.toml +15 -0
  16. package/config/jobs/heartbeat-check.toml +30 -0
  17. package/config/jobs/memory-rollup.toml +46 -0
  18. package/config/jobs/workspace-health.toml +63 -0
  19. package/config/mcp.toml +16 -0
  20. package/config/output-contracts.toml +17 -0
  21. package/config/peers.toml +32 -0
  22. package/config/peers.toml.example +32 -0
  23. package/config/policies/memory-default.toml +10 -0
  24. package/config/policies/memory-heartbeat.toml +5 -0
  25. package/config/policies/memory-ops.toml +10 -0
  26. package/config/policies/tools-heartbeat-minimal.toml +8 -0
  27. package/config/policies/tools-interactive-safe.toml +8 -0
  28. package/config/policies/tools-ops-bounded.toml +8 -0
  29. package/config/policies/tools-orchestrator.toml +7 -0
  30. package/config/providers/anthropic.toml +15 -0
  31. package/config/providers/ollama.toml +5 -0
  32. package/config/providers/openai-codex.toml +9 -0
  33. package/config/providers/openrouter.toml +5 -0
  34. package/config/router.toml +22 -0
  35. package/config/runtime.toml +114 -0
  36. package/config/skills/self-improvement.toml +15 -0
  37. package/config/skills/telegram-onboarding-spec.md +240 -0
  38. package/config/skills/workspace-monitor.toml +15 -0
  39. package/config/task-router.toml +42 -0
  40. package/install.sh +50 -0
  41. package/package.json +90 -0
  42. package/src/auth/auth-profiles.js +169 -0
  43. package/src/auth/openai-codex-oauth.js +285 -0
  44. package/src/battle.js +449 -0
  45. package/src/cli/help.js +265 -0
  46. package/src/cli/output-filter.js +49 -0
  47. package/src/cli/runtime-control.js +704 -0
  48. package/src/cli-main.js +2763 -0
  49. package/src/cli.js +78 -0
  50. package/src/config/loader.js +332 -0
  51. package/src/config/schema-validator.js +214 -0
  52. package/src/config/toml-lite.js +8 -0
  53. package/src/daemon/action-handlers.js +71 -0
  54. package/src/daemon/healing-tick.js +87 -0
  55. package/src/daemon/health-probes.js +90 -0
  56. package/src/daemon/notifier.js +57 -0
  57. package/src/daemon/nurse.js +218 -0
  58. package/src/daemon/repair-log.js +106 -0
  59. package/src/daemon/rule-staging.js +90 -0
  60. package/src/daemon/rules.js +29 -0
  61. package/src/daemon/telegram-commands.js +54 -0
  62. package/src/daemon/updater.js +85 -0
  63. package/src/jobs/job-runner.js +78 -0
  64. package/src/mcp/consumer.js +129 -0
  65. package/src/memory/active-recall.js +171 -0
  66. package/src/memory/backend-manager.js +97 -0
  67. package/src/memory/backends/file-backend.js +38 -0
  68. package/src/memory/backends/qmd-backend.js +219 -0
  69. package/src/memory/embedding-guards.js +24 -0
  70. package/src/memory/embedding-index.js +118 -0
  71. package/src/memory/embedding-service.js +179 -0
  72. package/src/memory/file-index.js +177 -0
  73. package/src/memory/memory-signature.js +5 -0
  74. package/src/memory/memory-store.js +648 -0
  75. package/src/memory/retrieval-planner.js +66 -0
  76. package/src/memory/scoring.js +145 -0
  77. package/src/memory/simhash.js +78 -0
  78. package/src/memory/sqlite-active-store.js +824 -0
  79. package/src/memory/write-policy.js +36 -0
  80. package/src/onboarding/aliases.js +33 -0
  81. package/src/onboarding/auth/api-key.js +224 -0
  82. package/src/onboarding/auth/ollama-detect.js +42 -0
  83. package/src/onboarding/clack-prompter.js +77 -0
  84. package/src/onboarding/doctor.js +530 -0
  85. package/src/onboarding/lock.js +42 -0
  86. package/src/onboarding/model-catalog.js +344 -0
  87. package/src/onboarding/phases/auth.js +589 -0
  88. package/src/onboarding/phases/build.js +130 -0
  89. package/src/onboarding/phases/choose.js +82 -0
  90. package/src/onboarding/phases/detect.js +98 -0
  91. package/src/onboarding/phases/hatch.js +216 -0
  92. package/src/onboarding/phases/identity.js +79 -0
  93. package/src/onboarding/phases/ollama.js +345 -0
  94. package/src/onboarding/phases/scaffold.js +99 -0
  95. package/src/onboarding/phases/telegram.js +377 -0
  96. package/src/onboarding/phases/validate.js +204 -0
  97. package/src/onboarding/phases/verify.js +206 -0
  98. package/src/onboarding/platform.js +482 -0
  99. package/src/onboarding/status-bar.js +95 -0
  100. package/src/onboarding/templates.js +794 -0
  101. package/src/onboarding/toml-writer.js +38 -0
  102. package/src/onboarding/tui.js +250 -0
  103. package/src/onboarding/uninstall.js +153 -0
  104. package/src/onboarding/wizard.js +499 -0
  105. package/src/providers/anthropic.js +168 -0
  106. package/src/providers/base.js +247 -0
  107. package/src/providers/circuit-breaker.js +136 -0
  108. package/src/providers/ollama.js +163 -0
  109. package/src/providers/openai-codex.js +149 -0
  110. package/src/providers/openrouter.js +136 -0
  111. package/src/providers/registry.js +36 -0
  112. package/src/providers/router.js +16 -0
  113. package/src/runtime/bootstrap-cache.js +47 -0
  114. package/src/runtime/capabilities-prompt.js +25 -0
  115. package/src/runtime/completion-ping.js +99 -0
  116. package/src/runtime/config-validator.js +121 -0
  117. package/src/runtime/context-ledger.js +360 -0
  118. package/src/runtime/cutover-readiness.js +42 -0
  119. package/src/runtime/daemon.js +729 -0
  120. package/src/runtime/delivery-ack.js +195 -0
  121. package/src/runtime/delivery-adapters/local-file.js +41 -0
  122. package/src/runtime/delivery-adapters/openclaw-cli.js +94 -0
  123. package/src/runtime/delivery-adapters/openclaw-peer.js +98 -0
  124. package/src/runtime/delivery-adapters/shadow.js +13 -0
  125. package/src/runtime/delivery-adapters/standalone-http.js +98 -0
  126. package/src/runtime/delivery-adapters/telegram.js +104 -0
  127. package/src/runtime/delivery-adapters/tui.js +128 -0
  128. package/src/runtime/delivery-manager.js +807 -0
  129. package/src/runtime/delivery-store.js +168 -0
  130. package/src/runtime/dependency-health.js +118 -0
  131. package/src/runtime/envelope.js +114 -0
  132. package/src/runtime/evaluation.js +1089 -0
  133. package/src/runtime/exec-approvals.js +216 -0
  134. package/src/runtime/executor.js +500 -0
  135. package/src/runtime/failure-ping.js +67 -0
  136. package/src/runtime/flows.js +83 -0
  137. package/src/runtime/guards.js +45 -0
  138. package/src/runtime/handoff.js +51 -0
  139. package/src/runtime/identity-cache.js +28 -0
  140. package/src/runtime/improvement-engine.js +109 -0
  141. package/src/runtime/improvement-harness.js +581 -0
  142. package/src/runtime/input-sanitiser.js +72 -0
  143. package/src/runtime/interaction-contract.js +347 -0
  144. package/src/runtime/lane-readiness.js +226 -0
  145. package/src/runtime/migration.js +323 -0
  146. package/src/runtime/model-resolution.js +78 -0
  147. package/src/runtime/network.js +64 -0
  148. package/src/runtime/notification-store.js +97 -0
  149. package/src/runtime/notifier.js +256 -0
  150. package/src/runtime/orchestrator.js +53 -0
  151. package/src/runtime/orphan-reaper.js +41 -0
  152. package/src/runtime/output-contract-schema.js +139 -0
  153. package/src/runtime/output-contract-validator.js +439 -0
  154. package/src/runtime/peer-readiness.js +69 -0
  155. package/src/runtime/peer-registry.js +133 -0
  156. package/src/runtime/pilot-status.js +108 -0
  157. package/src/runtime/prompt-builder.js +261 -0
  158. package/src/runtime/provider-attempt.js +582 -0
  159. package/src/runtime/report-fallback.js +71 -0
  160. package/src/runtime/result-normalizer.js +183 -0
  161. package/src/runtime/retention.js +74 -0
  162. package/src/runtime/review.js +244 -0
  163. package/src/runtime/route-job.js +15 -0
  164. package/src/runtime/run-store.js +38 -0
  165. package/src/runtime/schedule.js +88 -0
  166. package/src/runtime/scheduler-state.js +434 -0
  167. package/src/runtime/scheduler.js +656 -0
  168. package/src/runtime/session-compactor.js +182 -0
  169. package/src/runtime/session-search.js +155 -0
  170. package/src/runtime/slack-inbound.js +249 -0
  171. package/src/runtime/ssrf.js +102 -0
  172. package/src/runtime/status-aggregator.js +330 -0
  173. package/src/runtime/task-contract.js +140 -0
  174. package/src/runtime/task-packet.js +107 -0
  175. package/src/runtime/task-router.js +140 -0
  176. package/src/runtime/telegram-inbound.js +1565 -0
  177. package/src/runtime/token-counter.js +134 -0
  178. package/src/runtime/token-estimator.js +59 -0
  179. package/src/runtime/tool-loop.js +200 -0
  180. package/src/runtime/transport-server.js +311 -0
  181. package/src/runtime/tui-server.js +411 -0
  182. package/src/runtime/ulid.js +44 -0
  183. package/src/security/ssrf-check.js +197 -0
  184. package/src/setup.js +369 -0
  185. package/src/shadow/bridge.js +303 -0
  186. package/src/skills/loader.js +84 -0
  187. package/src/tools/catalog.json +49 -0
  188. package/src/tools/cli-delegate.js +44 -0
  189. package/src/tools/mcp-client.js +106 -0
  190. package/src/tools/micro/cancel-task.js +6 -0
  191. package/src/tools/micro/complete-task.js +6 -0
  192. package/src/tools/micro/fail-task.js +6 -0
  193. package/src/tools/micro/http-fetch.js +74 -0
  194. package/src/tools/micro/index.js +36 -0
  195. package/src/tools/micro/lcm-recall.js +60 -0
  196. package/src/tools/micro/list-dir.js +17 -0
  197. package/src/tools/micro/list-skills.js +46 -0
  198. package/src/tools/micro/load-skill.js +38 -0
  199. package/src/tools/micro/memory-search.js +45 -0
  200. package/src/tools/micro/read-file.js +11 -0
  201. package/src/tools/micro/session-search.js +54 -0
  202. package/src/tools/micro/shell-exec.js +43 -0
  203. package/src/tools/micro/trigger-job.js +79 -0
  204. package/src/tools/micro/web-search.js +58 -0
  205. package/src/tools/micro/workspace-paths.js +39 -0
  206. package/src/tools/micro/write-file.js +14 -0
  207. package/src/tools/micro/write-memory.js +41 -0
  208. package/src/tools/registry.js +348 -0
  209. package/src/tools/tool-result-contract.js +36 -0
  210. package/src/tui/chat.js +835 -0
  211. package/src/tui/renderer.js +175 -0
  212. package/src/tui/socket-client.js +217 -0
  213. package/src/utils/canonical-json.js +29 -0
  214. package/src/utils/compaction.js +30 -0
  215. package/src/utils/env-loader.js +5 -0
  216. package/src/utils/errors.js +80 -0
  217. package/src/utils/fs.js +101 -0
  218. package/src/utils/ids.js +5 -0
  219. package/src/utils/model-context-limits.js +30 -0
  220. package/src/utils/token-budget.js +74 -0
  221. package/src/utils/usage-cost.js +25 -0
  222. package/src/utils/usage-metrics.js +14 -0
  223. package/vendor/smol-toml-1.5.2.tgz +0 -0
@@ -0,0 +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
+ }
@@ -0,0 +1,140 @@
1
+ import { ulid } from "./ulid.js";
2
+
3
+ const VALID_TRANSITIONS = {
4
+ created: ["accepted", "rejected"],
5
+ accepted: ["running", "rejected"],
6
+ running: ["completed", "failed", "cancelled", "timed_out"],
7
+ timed_out: ["escalated"],
8
+ };
9
+
10
+ export class TaskContract {
11
+ constructor(db, envelopeStore) {
12
+ this.db = db;
13
+ this.envelopes = envelopeStore;
14
+ this._ensureSchema();
15
+ }
16
+
17
+ _ensureSchema() {
18
+ this.db.exec(`
19
+ CREATE TABLE IF NOT EXISTS tasks (
20
+ task_id TEXT PRIMARY KEY,
21
+ envelope_id TEXT NOT NULL,
22
+ owner_agent TEXT NOT NULL,
23
+ assigned_agent TEXT NOT NULL,
24
+ status TEXT NOT NULL DEFAULT 'created',
25
+ objective TEXT NOT NULL,
26
+ deadline_at TEXT NOT NULL,
27
+ result_slot TEXT,
28
+ error_slot TEXT,
29
+ escalation_chain TEXT NOT NULL,
30
+ escalation_index INTEGER NOT NULL DEFAULT 0,
31
+ created_at TEXT NOT NULL,
32
+ updated_at TEXT NOT NULL,
33
+ FOREIGN KEY (envelope_id) REFERENCES envelopes(id)
34
+ );
35
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
36
+ CREATE INDEX IF NOT EXISTS idx_tasks_assigned ON tasks(assigned_agent);
37
+ CREATE INDEX IF NOT EXISTS idx_tasks_deadline ON tasks(deadline_at);
38
+ `);
39
+ }
40
+
41
+ createTask({ ownerAgent, assignedAgent, objective, deadlineMinutes, escalationChain = ["assigned", "owner", "operator"] }) {
42
+ const taskId = ulid();
43
+ const now = new Date();
44
+ const deadlineAt = new Date(now.getTime() + deadlineMinutes * 60 * 1000).toISOString();
45
+ const envelope = this.envelopes.create({
46
+ sourceAgent: ownerAgent,
47
+ criticality: "result",
48
+ payloadType: "task",
49
+ payload: { objective, assignedAgent, deadlineMinutes },
50
+ });
51
+ this.db.prepare(`
52
+ INSERT INTO tasks (task_id, envelope_id, owner_agent, assigned_agent, status, objective, deadline_at, escalation_chain, escalation_index, created_at, updated_at)
53
+ VALUES (?, ?, ?, ?, 'created', ?, ?, ?, 0, ?, ?)
54
+ `).run(taskId, envelope.id, ownerAgent, assignedAgent, objective.slice(0, 500), deadlineAt, JSON.stringify(escalationChain), now.toISOString(), now.toISOString());
55
+ return this.get(taskId);
56
+ }
57
+
58
+ get(taskId) {
59
+ return this.db.prepare("SELECT * FROM tasks WHERE task_id = ?").get(taskId) || null;
60
+ }
61
+
62
+ transition(taskId, newStatus, callerAgent, { result, error } = {}) {
63
+ const task = this.get(taskId);
64
+ if (!task) throw new Error(`Task ${taskId} not found`);
65
+ const allowed = VALID_TRANSITIONS[task.status];
66
+ if (!allowed || !allowed.includes(newStatus)) {
67
+ throw new Error(`Invalid transition: ${task.status} → ${newStatus}`);
68
+ }
69
+ // Permission checks
70
+ if (newStatus === "cancelled" && callerAgent !== task.owner_agent) {
71
+ throw new Error("Only owner can cancel a task");
72
+ }
73
+ const assignedOnly = ["accepted", "running", "completed", "failed", "rejected"];
74
+ if (assignedOnly.includes(newStatus) && callerAgent !== task.assigned_agent) {
75
+ throw new Error(`Only assigned agent can transition to ${newStatus}`);
76
+ }
77
+ const now = new Date().toISOString();
78
+ const updates = { status: newStatus, updated_at: now };
79
+ if (result !== undefined) {
80
+ updates.result_slot = typeof result === "string" ? result : JSON.stringify(result);
81
+ if (updates.result_slot.length > 4000) {
82
+ updates.result_slot = JSON.stringify({
83
+ ledger_ref: `overflow-${taskId}`,
84
+ summary: updates.result_slot.slice(0, 200),
85
+ });
86
+ }
87
+ }
88
+ if (error !== undefined) {
89
+ updates.error_slot = typeof error === "string" ? error : JSON.stringify(error);
90
+ }
91
+ const setClauses = Object.keys(updates).map((k) => `${k} = ?`).join(", ");
92
+ const values = [...Object.values(updates), taskId];
93
+ this.db.prepare(`UPDATE tasks SET ${setClauses} WHERE task_id = ?`).run(...values);
94
+ return this.get(taskId);
95
+ }
96
+
97
+ listByStatus(status) {
98
+ return this.db.prepare("SELECT * FROM tasks WHERE status = ? ORDER BY created_at DESC").all(status);
99
+ }
100
+
101
+ listTimedOut() {
102
+ const now = new Date().toISOString();
103
+ return this.db.prepare("SELECT * FROM tasks WHERE status = 'running' AND deadline_at < ?").all(now);
104
+ }
105
+
106
+ sweepTimedOutTasks() {
107
+ const now = new Date().toISOString();
108
+ const overdue = this.db.prepare("SELECT * FROM tasks WHERE status = 'running' AND deadline_at < ?").all(now);
109
+ for (const task of overdue) {
110
+ this.db.prepare("UPDATE tasks SET status = 'timed_out', updated_at = ? WHERE task_id = ?").run(now, task.task_id);
111
+ }
112
+ return overdue;
113
+ }
114
+
115
+ advanceEscalation(taskId) {
116
+ const task = this.get(taskId);
117
+ if (!task) throw new Error(`Task ${taskId} not found`);
118
+ if (!["timed_out", "escalated"].includes(task.status)) {
119
+ throw new Error(`Cannot escalate task in status: ${task.status}`);
120
+ }
121
+ const chain = JSON.parse(task.escalation_chain);
122
+ const newIndex = task.escalation_index + 1;
123
+ this.db.prepare("UPDATE tasks SET escalation_index = ?, updated_at = ? WHERE task_id = ?")
124
+ .run(newIndex, new Date().toISOString(), taskId);
125
+ if (newIndex >= chain.length) {
126
+ this.db.prepare("UPDATE tasks SET status = 'escalated', updated_at = ? WHERE task_id = ?")
127
+ .run(new Date().toISOString(), taskId);
128
+ }
129
+ return { escalationIndex: newIndex, target: chain[newIndex] || null, exhausted: newIndex >= chain.length };
130
+ }
131
+
132
+ getInbox() {
133
+ return this.db.prepare(`
134
+ SELECT * FROM tasks
135
+ WHERE status IN ('timed_out', 'escalated')
136
+ AND escalation_index >= json_array_length(escalation_chain)
137
+ ORDER BY created_at DESC
138
+ `).all();
139
+ }
140
+ }
@@ -0,0 +1,107 @@
1
+ import { activeRecall } from "../memory/active-recall.js";
2
+
3
+ function compactCheckpoint(checkpoint) {
4
+ if (!checkpoint) return null;
5
+ return {
6
+ objective: checkpoint.objective,
7
+ status: checkpoint.status,
8
+ nextActions: checkpoint.nextActions || [],
9
+ blockers: checkpoint.blockers || []
10
+ };
11
+ }
12
+
13
+ export async function buildTaskPacket({
14
+ agentId,
15
+ objective,
16
+ memoryStore,
17
+ backendManager = null,
18
+ workspaceRoot = null,
19
+ backendOrder = null,
20
+ qmdSupplementLimit = 2,
21
+ retrievalQueries = null,
22
+ identity = null,
23
+ interactionContract = null,
24
+ outputContract = null,
25
+ reportGuidance = null,
26
+ allowedTools = [],
27
+ artifactRefs = [],
28
+ budget = {},
29
+ checkpointId = "latest",
30
+ memoryLimit = 8,
31
+ retrievalBlend = null,
32
+ conversationContext = undefined
33
+ }) {
34
+ const checkpoint = compactCheckpoint(await memoryStore.loadCheckpoint(agentId, checkpointId));
35
+ let memory;
36
+ const useActiveRecall = memoryStore?.queryRetrievalCandidates;
37
+ if (useActiveRecall) {
38
+ const tokenBudget = budget.maxTokens ? Math.min(2000, Math.floor(budget.maxTokens * 0.08)) : 2000;
39
+ const recallResult = await activeRecall({
40
+ inboundMessage: objective,
41
+ memoryStore,
42
+ tokenBudget,
43
+ topicRelevanceFn: null,
44
+ });
45
+ const items = [
46
+ ...recallResult.facts.map((c) => ({ ...c, tier: "fact" })),
47
+ ...recallResult.asides.map((c) => ({ ...c, tier: "aside" })),
48
+ ];
49
+ memory = { items, backends: null, retrieval: null };
50
+ } else {
51
+ memory = backendManager
52
+ ? await backendManager.queryCombined(agentId, objective, {
53
+ workspaceRoot,
54
+ backendOrder,
55
+ qmdSupplementLimit,
56
+ fileQuery: retrievalQueries?.fileQuery || objective,
57
+ qmdQuery: retrievalQueries?.qmdQuery || objective,
58
+ limit: memoryLimit,
59
+ retrievalBlend
60
+ })
61
+ : await memoryStore.query(agentId, objective, { limit: memoryLimit, retrievalBlend });
62
+ }
63
+
64
+ return {
65
+ agentId,
66
+ objective,
67
+ conversationContext: conversationContext || null,
68
+ budget: {
69
+ maxTokens: budget.maxTokens ?? 8000,
70
+ maxRuntimeSeconds: budget.maxRuntimeSeconds ?? 60
71
+ },
72
+ layers: {
73
+ systemRules: [
74
+ "Transcript is not the system of record.",
75
+ "Use checkpoint and memory before requesting more history.",
76
+ "Do not write durable memory without policy approval."
77
+ ],
78
+ identity,
79
+ interactionContract,
80
+ outputContract,
81
+ reportGuidance,
82
+ checkpoint,
83
+ retrieval: memory.backends || null,
84
+ retrievalMeta: memory.retrieval || null,
85
+ retrievalQueries,
86
+ memory: memory.items.map((item) => ({
87
+ entryId: item.entryId || null,
88
+ type: item.type || "memory",
89
+ category: item.category || "general",
90
+ title: item.title || "",
91
+ content: item.content || "",
92
+ summary: item.summary || "",
93
+ reason: item.reason || "",
94
+ score: Number((item.score ?? 0).toFixed(4)),
95
+ tier: item.tier || null,
96
+ sourceBackend: item.sourceBackend || "file",
97
+ candidateSource: item.candidateSource || "indexed",
98
+ lexicalScore: Number((item.lexicalScore ?? 0).toFixed(4)),
99
+ embeddingSimilarity: Number((item.embeddingSimilarity ?? 0).toFixed(4)),
100
+ embeddingFreshness: item.embeddingFreshness || "missing",
101
+ retrievalSources: item.retrievalSources || ["lexical"]
102
+ })),
103
+ tools: allowedTools,
104
+ artifacts: artifactRefs
105
+ }
106
+ };
107
+ }