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,256 @@
1
+ import path from "node:path";
2
+ import { NotificationStore } from "./notification-store.js";
3
+ import { createRuntimeId } from "../utils/ids.js";
4
+
5
+ function iso() {
6
+ return new Date().toISOString();
7
+ }
8
+
9
+ function toTarget(target) {
10
+ if (!target) return { mode: "same_thread" };
11
+ if (typeof target === "string") return { mode: target };
12
+ return target;
13
+ }
14
+
15
+ function buildDedupeKey(stage, runId, signal) {
16
+ return `${stage}:${runId}:${signal || "none"}`;
17
+ }
18
+
19
+ export class InteractionNotifier {
20
+ constructor({ stateRoot }) {
21
+ this.store = new NotificationStore({ rootDir: path.join(stateRoot, "notifications") });
22
+ }
23
+
24
+ configureRetention(retention = {}) {
25
+ this.store.setRetentionPolicy(retention);
26
+ }
27
+
28
+ async queueAck(plan, context = {}) {
29
+ const contract = plan?.packet?.layers?.interactionContract;
30
+ if (!contract || contract.ackMode === "silent") return null;
31
+
32
+ const notification = {
33
+ id: createRuntimeId("notification"),
34
+ timestamp: iso(),
35
+ jobId: plan.job.id,
36
+ stage: "ack",
37
+ status: "queued",
38
+ dedupeKey: buildDedupeKey("ack", context.runId || plan.job.id, contract.ackMode),
39
+ deliveryProfile: plan.agent?.deliveryProfile || null,
40
+ signal: contract.ackMode,
41
+ target: toTarget(contract.pingbackTarget),
42
+ message: contract.ackText || `Acknowledged ${plan.job.id}. I will ping back when the run is complete.`,
43
+ mode: context.mode || null,
44
+ providerId: context.providerId || null,
45
+ modelId: context.modelId || null,
46
+ routingDecision: context.routingDecision || null,
47
+ maxSilenceSeconds: contract.maxSilenceSeconds ?? null
48
+ };
49
+
50
+ const filePath = await this.store.saveNotification(plan.job.id, notification);
51
+ return { filePath, ...notification };
52
+ }
53
+
54
+ async queueCompletion(run) {
55
+ const completion = run?.interaction?.completion;
56
+ if (!completion?.required) return null;
57
+
58
+ const notification = {
59
+ id: createRuntimeId("notification"),
60
+ timestamp: iso(),
61
+ jobId: run.plan.job.id,
62
+ stage: "completion",
63
+ status: "ready",
64
+ dedupeKey: buildDedupeKey("completion", run.id || run.timestamp || run.plan.job.id, completion.signal),
65
+ deliveryProfile: run.plan.agent?.deliveryProfile || null,
66
+ signal: completion.signal,
67
+ target: toTarget(completion.target),
68
+ message: completion.message,
69
+ mode: run.mode,
70
+ providerId: run.providerId,
71
+ modelId: run.modelId,
72
+ routingDecision: run.routingDecision,
73
+ sections: completion.sections || [],
74
+ nextActions: completion.nextActions || [],
75
+ sourceRunTimestamp: run.timestamp
76
+ };
77
+
78
+ const filePath = await this.store.saveNotification(run.plan.job.id, notification);
79
+ return { filePath, ...notification };
80
+ }
81
+
82
+ async queueHandoff(run) {
83
+ const handoff = run?.interaction?.handoff;
84
+ if (!handoff?.required) return null;
85
+
86
+ const notification = {
87
+ id: createRuntimeId("notification"),
88
+ handoffId: createRuntimeId("handoff"),
89
+ timestamp: iso(),
90
+ jobId: run.plan.job.id,
91
+ stage: "handoff",
92
+ status: handoff.target ? "ready" : "awaiting_choice",
93
+ handoffState: handoff.target ? "chosen" : "pending",
94
+ dedupeKey: buildDedupeKey("handoff", run.id || run.timestamp || run.plan.job.id, handoff.signal),
95
+ deliveryProfile: handoff.deliveryProfile || run.plan.agent?.deliveryProfile || null,
96
+ signal: handoff.signal,
97
+ target: handoff.target || null,
98
+ suggestedPeers: handoff.suggestions || [],
99
+ capabilityQuery: handoff.capabilityQuery || null,
100
+ preferredTaskClasses: handoff.preferredTaskClasses || [],
101
+ message: handoff.message,
102
+ mode: run.mode,
103
+ providerId: run.providerId,
104
+ modelId: run.modelId,
105
+ routingDecision: run.routingDecision,
106
+ sections: handoff.sections || [],
107
+ nextActions: handoff.nextActions || [],
108
+ sourceRunTimestamp: run.timestamp
109
+ };
110
+
111
+ const filePath = await this.store.saveNotification(run.plan.job.id, notification);
112
+ return { filePath, ...notification };
113
+ }
114
+
115
+ async queueError({ plan, error, context = {} }) {
116
+ const contract = plan?.packet?.layers?.interactionContract;
117
+ if (!contract?.notifyOnError) return null;
118
+
119
+ const notification = {
120
+ id: createRuntimeId("notification"),
121
+ timestamp: iso(),
122
+ jobId: plan.job.id,
123
+ stage: "error",
124
+ status: "ready",
125
+ dedupeKey: buildDedupeKey("error", context.runId || context.timestamp || plan.job.id, contract.failureSignal || "error"),
126
+ deliveryProfile: plan.agent?.deliveryProfile || null,
127
+ signal: contract.failureSignal || "error",
128
+ target: toTarget(contract.pingbackTarget),
129
+ message: `[${contract.failureSignal || "error"}] ${plan.job.id}: ${error.message || String(error)}`,
130
+ mode: context.mode || null,
131
+ providerId: context.providerId || null,
132
+ modelId: context.modelId || null,
133
+ routingDecision: context.routingDecision || null,
134
+ sourceRunTimestamp: context.timestamp || null
135
+ };
136
+
137
+ const filePath = await this.store.saveNotification(plan.job.id, notification);
138
+ return { filePath, ...notification };
139
+ }
140
+
141
+ async queueFollowUp(run) {
142
+ const yieldPlan = run?.interaction?.yield;
143
+ if (!yieldPlan?.required) return null;
144
+
145
+ const notification = {
146
+ id: createRuntimeId("notification"),
147
+ followUpId: createRuntimeId("followup"),
148
+ timestamp: iso(),
149
+ jobId: run.plan.job.id,
150
+ stage: "follow_up",
151
+ status: "pending",
152
+ followUpState: "pending",
153
+ yieldState: "pending",
154
+ hidden: true,
155
+ signal: yieldPlan.signal,
156
+ targetSurface: yieldPlan.targetSurface,
157
+ objective: yieldPlan.objective,
158
+ dedupeKey: buildDedupeKey("follow_up", run.id || run.timestamp || run.plan.job.id, yieldPlan.signal),
159
+ payload: {
160
+ ...yieldPlan.followUp,
161
+ sourceRunId: run.id || null,
162
+ sourceRunTimestamp: run.timestamp
163
+ },
164
+ sourceRunId: run.id || null,
165
+ sourceRunTimestamp: run.timestamp,
166
+ parentIdempotencyKey: run.idempotencyKey || null
167
+ };
168
+
169
+ const filePath = await this.store.saveNotification(run.plan.job.id, notification);
170
+ return { filePath, ...notification };
171
+ }
172
+
173
+ async consumeFollowUp(notificationFilePath, options = {}) {
174
+ const followUp = await this.store.getNotification(notificationFilePath);
175
+ if (!followUp) {
176
+ throw new Error(`Follow-up notification not found: ${notificationFilePath}`);
177
+ }
178
+ if (followUp.stage !== "follow_up") {
179
+ throw new Error("Only follow_up notifications can be consumed.");
180
+ }
181
+ if (followUp.status !== "pending") {
182
+ throw new Error("Follow-up has already been consumed or is not pending.");
183
+ }
184
+
185
+ const created = [];
186
+ const payload = followUp.payload || {};
187
+ const timestamp = iso();
188
+
189
+ if (payload.completion) {
190
+ const completion = payload.completion;
191
+ const completionNotification = {
192
+ id: createRuntimeId("notification"),
193
+ timestamp,
194
+ jobId: followUp.jobId,
195
+ stage: "completion",
196
+ status: "ready",
197
+ dedupeKey: `completion:${followUp.followUpId}`,
198
+ deliveryProfile: options.completionDeliveryProfile || null,
199
+ signal: completion.signal,
200
+ target: toTarget(completion.target),
201
+ message: completion.message,
202
+ sections: completion.sections || [],
203
+ nextActions: completion.nextActions || [],
204
+ sourceRunId: followUp.sourceRunId || null,
205
+ sourceRunTimestamp: followUp.sourceRunTimestamp || null,
206
+ sourceFollowUpId: followUp.followUpId,
207
+ sourceFollowUpFilePath: followUp.filePath
208
+ };
209
+ const filePath = await this.store.saveNotification(followUp.jobId, completionNotification);
210
+ created.push({ filePath, ...completionNotification });
211
+ }
212
+
213
+ if (payload.handoff) {
214
+ const handoff = payload.handoff;
215
+ const handoffNotification = {
216
+ id: createRuntimeId("notification"),
217
+ handoffId: createRuntimeId("handoff"),
218
+ timestamp,
219
+ jobId: followUp.jobId,
220
+ stage: "handoff",
221
+ status: handoff.target ? "ready" : "awaiting_choice",
222
+ handoffState: handoff.target ? "chosen" : "pending",
223
+ dedupeKey: `handoff:${followUp.followUpId}:${handoff.signal}`,
224
+ deliveryProfile: handoff.deliveryProfile || null,
225
+ signal: handoff.signal,
226
+ target: handoff.target || null,
227
+ suggestedPeers: handoff.suggestions || [],
228
+ capabilityQuery: handoff.capabilityQuery || null,
229
+ preferredTaskClasses: handoff.preferredTaskClasses || [],
230
+ message: handoff.message,
231
+ sections: handoff.sections || [],
232
+ nextActions: handoff.nextActions || [],
233
+ sourceRunId: followUp.sourceRunId || null,
234
+ sourceRunTimestamp: followUp.sourceRunTimestamp || null,
235
+ sourceFollowUpId: followUp.followUpId,
236
+ sourceFollowUpFilePath: followUp.filePath
237
+ };
238
+ const filePath = await this.store.saveNotification(followUp.jobId, handoffNotification);
239
+ created.push({ filePath, ...handoffNotification });
240
+ }
241
+
242
+ const updated = await this.store.updateNotification(notificationFilePath, {
243
+ status: "consumed",
244
+ followUpState: "consumed",
245
+ yieldState: "consumed",
246
+ consumedAt: timestamp,
247
+ generatedNotificationFiles: created.map((item) => item.filePath),
248
+ generatedStages: created.map((item) => item.stage)
249
+ });
250
+
251
+ return {
252
+ followUp: updated,
253
+ created
254
+ };
255
+ }
256
+ }
@@ -0,0 +1,53 @@
1
+ export class Orchestrator {
2
+ #staticRoutes;
3
+ #agents;
4
+ #dynamicRouting;
5
+ #dynamicRouter;
6
+
7
+ constructor({ staticRoutes = {}, agents = {}, dynamicRouting = {}, dynamicRouter = null }) {
8
+ this.#staticRoutes = staticRoutes;
9
+ this.#agents = agents;
10
+ this.#dynamicRouting = dynamicRouting;
11
+ this.#dynamicRouter = dynamicRouter;
12
+ }
13
+
14
+ async route(jobId) {
15
+ // 1. Static route lookup
16
+ const staticTarget = this.#staticRoutes[jobId];
17
+ if (staticTarget && this.#agents[staticTarget]) {
18
+ return {
19
+ agentId: staticTarget,
20
+ method: "static",
21
+ reason: `static: ${jobId} → ${staticTarget}`,
22
+ };
23
+ }
24
+
25
+ // 2. Dynamic route (if enabled)
26
+ if (this.#dynamicRouting.enabled && this.#dynamicRouter) {
27
+ try {
28
+ const agentSummaries = Object.values(this.#agents).map(a => ({
29
+ id: a.id,
30
+ primary_lane: a.primary_lane,
31
+ description: a.description || a.id,
32
+ }));
33
+ const result = await this.#dynamicRouter(jobId, agentSummaries);
34
+ if (result?.agentId && this.#agents[result.agentId]) {
35
+ return {
36
+ agentId: result.agentId,
37
+ method: "dynamic",
38
+ reason: `dynamic: ${result.reasoning || "LLM routing"}`,
39
+ };
40
+ }
41
+ } catch (_err) {
42
+ // Dynamic routing failed — fall through to escalation
43
+ }
44
+ }
45
+
46
+ // 3. Escalation
47
+ return {
48
+ agentId: null,
49
+ method: "escalation",
50
+ reason: `No route found for job '${jobId}'. Escalating to operator.`,
51
+ };
52
+ }
53
+ }
@@ -0,0 +1,41 @@
1
+ const ORPHAN_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes
2
+
3
+ /**
4
+ * On startup: mark stuck running jobs as failed.
5
+ * Jobs running for < 5 min are left alone (daemon may have just restarted briefly).
6
+ * Uses created_at as a proxy for when the job became running.
7
+ *
8
+ * @param {import('./scheduler-state.js').SchedulerStateStore} stateStore
9
+ * @returns {{ job_id: string }[]} reaped job rows
10
+ */
11
+ export function reapOrphanedJobs(stateStore) {
12
+ // Ensure columns exist for result and completed_at tracking
13
+ _addColumnIfMissing(stateStore.db, "interactive_jobs", "result", "TEXT");
14
+ _addColumnIfMissing(stateStore.db, "interactive_jobs", "completed_at", "TEXT");
15
+ _addColumnIfMissing(stateStore.db, "interactive_jobs", "started_at", "TEXT");
16
+
17
+ const cutoff = new Date(Date.now() - ORPHAN_THRESHOLD_MS).toISOString();
18
+ const now = new Date().toISOString();
19
+
20
+ const reaped = stateStore.db.prepare(`
21
+ UPDATE interactive_jobs
22
+ SET status = 'failed',
23
+ result = json_object('error', 'Daemon restarted before job completed'),
24
+ completed_at = ?
25
+ WHERE status = 'running'
26
+ AND COALESCE(started_at, created_at) < ?
27
+ RETURNING job_id
28
+ `).all(now, cutoff);
29
+
30
+ if (reaped.length > 0) {
31
+ console.log(`[startup] reaped ${reaped.length} orphaned job(s)`);
32
+ }
33
+ return reaped;
34
+ }
35
+
36
+ function _addColumnIfMissing(db, table, column, type) {
37
+ const cols = db.prepare(`pragma table_info(${table})`).all();
38
+ if (!cols.some((c) => c.name === column)) {
39
+ db.exec(`alter table ${table} add column ${column} ${type}`);
40
+ }
41
+ }
@@ -0,0 +1,139 @@
1
+ function titleCase(value) {
2
+ return String(value || "")
3
+ .split(/[_\-\s]+/)
4
+ .filter(Boolean)
5
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
6
+ .join("");
7
+ }
8
+
9
+ function buildBulletedBriefingOutputSchema(contract) {
10
+ const properties = {};
11
+ const required = [];
12
+
13
+ if (contract.profile?.requireStatus) {
14
+ properties.status = {
15
+ type: "string",
16
+ description: "One-line overall status."
17
+ };
18
+ required.push("status");
19
+ }
20
+
21
+ for (const section of contract.requiredSections || []) {
22
+ properties[section] = {
23
+ type: "string",
24
+ description: `${titleCase(section)} update. Use 'None' if there is no concrete data.`
25
+ };
26
+ required.push(section);
27
+ }
28
+
29
+ return {
30
+ type: "object",
31
+ additionalProperties: false,
32
+ properties,
33
+ required
34
+ };
35
+ }
36
+
37
+ function buildStructuredRollupOutputSchema(contract) {
38
+ const properties = {};
39
+ const required = [];
40
+
41
+ for (const section of contract.requiredSections || []) {
42
+ properties[section] = {
43
+ type: "array",
44
+ description: `${titleCase(section)} bullets. Use ['None'] if there is no concrete data.`,
45
+ minItems: contract.profile?.requireSectionItems ? 1 : 0,
46
+ items: {
47
+ type: "string"
48
+ }
49
+ };
50
+ required.push(section);
51
+ }
52
+
53
+ return {
54
+ type: "object",
55
+ additionalProperties: false,
56
+ properties,
57
+ required
58
+ };
59
+ }
60
+
61
+ function buildGenericOutputSchema(contract) {
62
+ const properties = {};
63
+ const required = [];
64
+
65
+ for (const section of contract.requiredSections || []) {
66
+ properties[section] = {
67
+ type: "string",
68
+ description: `${titleCase(section)} section content.`
69
+ };
70
+ required.push(section);
71
+ }
72
+
73
+ return {
74
+ type: "object",
75
+ additionalProperties: false,
76
+ properties,
77
+ required
78
+ };
79
+ }
80
+
81
+ function buildContractOutputSchema(contract) {
82
+ if (!contract) {
83
+ return {
84
+ type: "string"
85
+ };
86
+ }
87
+
88
+ if (contract.format === "bulleted_briefing") {
89
+ return buildBulletedBriefingOutputSchema(contract);
90
+ }
91
+
92
+ if (contract.format === "structured_rollup") {
93
+ return buildStructuredRollupOutputSchema(contract);
94
+ }
95
+
96
+ return buildGenericOutputSchema(contract);
97
+ }
98
+
99
+ export function compileOutputContractSchema(contract, options = {}) {
100
+ if (!contract) return null;
101
+ const thoughtFirst = options.thoughtFirst ?? false;
102
+
103
+ const properties = {};
104
+ const required = [];
105
+
106
+ if (thoughtFirst) {
107
+ properties.analysis = {
108
+ type: "string",
109
+ description: "Compact reasoning about the task before finalizing the structured result."
110
+ };
111
+ required.push("analysis");
112
+ }
113
+
114
+ properties.summary = {
115
+ type: "string",
116
+ description: "Short summary suitable for a completion pingback."
117
+ };
118
+ properties.output = buildContractOutputSchema(contract);
119
+ properties.nextActions = {
120
+ type: "array",
121
+ description: "Concrete next actions after the job result.",
122
+ items: {
123
+ type: "string"
124
+ }
125
+ };
126
+ required.push("summary", "output", "nextActions");
127
+
128
+ return {
129
+ name: contract.schemaName || `${String(contract.format || "bounded_output").replace(/[^a-z0-9]+/gi, "_").toLowerCase()}_response`,
130
+ thoughtFirst,
131
+ reasoningField: thoughtFirst ? "analysis" : null,
132
+ schema: {
133
+ type: "object",
134
+ additionalProperties: false,
135
+ properties,
136
+ required
137
+ }
138
+ };
139
+ }