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,500 @@
1
+ import path from "node:path";
2
+ import crypto from "node:crypto";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ import { mkdirSync } from "node:fs";
5
+ import { Scheduler } from "./scheduler.js";
6
+ import { ProviderRegistry } from "../providers/registry.js";
7
+ import { RunStore } from "./run-store.js";
8
+ import { getProviderModePolicy } from "./guards.js";
9
+ import { buildInteractionPlan, formatInteractionContract } from "./interaction-contract.js";
10
+ import { InteractionNotifier } from "./notifier.js";
11
+ import { TokenCounter } from "./token-counter.js";
12
+ import { PeerRegistry } from "./peer-registry.js";
13
+ import { createRuntimeId } from "../utils/ids.js";
14
+ import { processToolOutput } from "./input-sanitiser.js";
15
+ import { ToolRegistry } from "../tools/registry.js";
16
+ import { registerMicroToolHandlers } from "../tools/micro/index.js";
17
+ import { ExecApprovalGate } from "./exec-approvals.js";
18
+ import { CircuitBreaker, ensureBreakerSchema } from "../providers/circuit-breaker.js";
19
+ import { SchedulerStateStore } from "./scheduler-state.js";
20
+ import { buildCapabilitiesPrompt } from "./capabilities-prompt.js";
21
+ import { attemptProvider } from "./provider-attempt.js";
22
+ import { buildOutputTemplate, formatOutputContract, formatReportGuidance, normalizeResult } from "./result-normalizer.js";
23
+ import { runToolLoop } from "./tool-loop.js";
24
+
25
+ export { parseToolUseBlocks, formatToolResults, executeToolCalls } from "./tool-loop.js";
26
+
27
+ function buildSystemPrompt(plan, options = {}) {
28
+ const nativeStructuredOutput = options.nativeStructuredOutput ?? false;
29
+ const responseSchema = options.responseSchema || null;
30
+ const toolSchemas = options.toolSchemas || [];
31
+ const isInteractive = plan.job.trigger === "interactive";
32
+
33
+ const toolText = plan.packet.layers.tools.length
34
+ ? `Allowed tools: ${plan.packet.layers.tools.join(", ")}.`
35
+ : "No tools allowed.";
36
+ const soulText = plan.packet.layers.identity?.soul
37
+ ? `${plan.packet.layers.identity.soul.trim()}`
38
+ : "";
39
+ const purposeText = plan.packet.layers.identity?.purpose
40
+ ? `## Purpose\n${plan.packet.layers.identity.purpose.trim()}`
41
+ : "";
42
+ const userText = plan.packet.layers.identity?.user
43
+ ? `${plan.packet.layers.identity.user.trim()}`
44
+ : "";
45
+
46
+ const contextFileTexts = (plan.packet.layers.identity?.contextFiles || [])
47
+ .map(({ name, content }) => `---\n## ${name}\n\n${content.trim()}\n---`)
48
+ .join("\n\n");
49
+
50
+ if (isInteractive) {
51
+ return [
52
+ soulText,
53
+ userText,
54
+ purposeText,
55
+ contextFileTexts || null,
56
+ buildCapabilitiesPrompt(toolSchemas),
57
+ "Respond in plain conversational language. Do not output JSON, structured task packets, or completion sections.",
58
+ "Be concise, direct, and helpful. Match the tone of the user's message.",
59
+ "If the user asks a question, answer it. If they give a task, do it and report back naturally.",
60
+ toolText,
61
+ ]
62
+ .filter(Boolean)
63
+ .join("\n\n");
64
+ }
65
+
66
+ const interactionText = plan.packet.layers.interactionContract
67
+ ? `Interaction contract:\n${formatInteractionContract(plan.packet.layers.interactionContract)}`
68
+ : "";
69
+ const contractText = plan.packet.layers.outputContract
70
+ ? `Output contract:\n${formatOutputContract(plan.packet.layers.outputContract)}`
71
+ : "";
72
+ const guidanceText = plan.packet.layers.reportGuidance
73
+ ? `Report guidance:\n${formatReportGuidance(plan.packet.layers.reportGuidance)}`
74
+ : "";
75
+ const templateText = !nativeStructuredOutput && plan.packet.layers.outputContract
76
+ ? buildOutputTemplate(plan.packet.layers.outputContract)
77
+ : null;
78
+ const nativeSchemaText = nativeStructuredOutput && plan.packet.layers.outputContract
79
+ ? "Native structured output is enabled for this provider. Follow the contract exactly and return only schema-valid content."
80
+ : null;
81
+ const reasoningFieldText = nativeStructuredOutput && responseSchema?.reasoningField
82
+ ? `Use the ${responseSchema.reasoningField} field for compact reasoning before finalizing the structured output.`
83
+ : null;
84
+
85
+ return [
86
+ "You are Nemoris V2 executing a bounded shadow-mode job.",
87
+ "Use only the supplied task packet.",
88
+ "Do not assume the transcript is the system of record.",
89
+ "Summarize the result tightly and list the next actions.",
90
+ toolText,
91
+ soulText,
92
+ purposeText,
93
+ interactionText,
94
+ contractText,
95
+ guidanceText,
96
+ nativeSchemaText,
97
+ reasoningFieldText,
98
+ templateText ? `Follow this output template exactly when possible:\n${templateText}` : null
99
+ ]
100
+ .filter(Boolean)
101
+ .join("\n\n");
102
+ }
103
+
104
+ function buildUserMessage(plan, options = {}) {
105
+ const nativeStructuredOutput = options.nativeStructuredOutput ?? false;
106
+ const responseSchema = options.responseSchema || null;
107
+ const isInteractive = plan.job.trigger === "interactive";
108
+
109
+ const memoryLines = plan.packet.layers.memory
110
+ .map((item) => {
111
+ const primaryText = item.summary || item.content || item.reason || "";
112
+ const rawLine = `- [${item.type}/${item.category || "uncategorized"}] ${item.title || "memory"}: ${primaryText}`;
113
+ const source = item.sourceBackend || item.category || "memory";
114
+ const { tagged } = processToolOutput(rawLine, source);
115
+ return tagged;
116
+ })
117
+ .join("\n");
118
+
119
+ if (isInteractive) {
120
+ const parts = [plan.packet.objective];
121
+ if (plan.packet.conversationContext?.length) {
122
+ const contextLines = plan.packet.conversationContext
123
+ .map((turn) => `${turn.role}: ${turn.content}`)
124
+ .join("\n");
125
+ parts.unshift(`Previous conversation:\n${contextLines}`);
126
+ }
127
+ if (memoryLines) {
128
+ parts.push(`\n(Context from memory:\n${memoryLines})`);
129
+ }
130
+ return parts.join("\n\n");
131
+ }
132
+
133
+ const artifactLines = plan.packet.layers.artifacts.length
134
+ ? `Artifacts:\n${plan.packet.layers.artifacts.map((item) => {
135
+ const { tagged } = processToolOutput(`- ${item}`, "artifact");
136
+ return tagged;
137
+ }).join("\n")}`
138
+ : "Artifacts: none";
139
+ const contractReminder = !nativeStructuredOutput && plan.packet.layers.outputContract?.requiredSections?.length
140
+ ? `Required sections: ${plan.packet.layers.outputContract.requiredSections.join(", ")}.`
141
+ : "Required sections: none.";
142
+ const noneReminder = !nativeStructuredOutput && plan.packet.layers.outputContract?.requiredSections?.length
143
+ ? "If a required section has no concrete data, include the section anyway and say 'None'."
144
+ : null;
145
+ const schemaReminder = nativeStructuredOutput
146
+ ? "Return only the structured JSON object required by the provider schema."
147
+ : "Return JSON with keys: summary, output, nextActions.";
148
+ const reasoningReminder = nativeStructuredOutput && responseSchema?.reasoningField
149
+ ? `Put concise reasoning in ${responseSchema.reasoningField} before the final output fields.`
150
+ : null;
151
+
152
+ return [
153
+ `Objective: ${plan.packet.objective}`,
154
+ `Budget: ${plan.packet.budget.maxTokens} tokens, ${plan.packet.budget.maxRuntimeSeconds}s`,
155
+ plan.packet.layers.checkpoint
156
+ ? `Checkpoint: status=${plan.packet.layers.checkpoint.status}; next=${(plan.packet.layers.checkpoint.nextActions || []).join(", ") || "none"}`
157
+ : "Checkpoint: none",
158
+ memoryLines ? `Relevant memory:\n${memoryLines}` : "Relevant memory: none",
159
+ artifactLines,
160
+ contractReminder,
161
+ schemaReminder,
162
+ reasoningReminder,
163
+ "The output field must satisfy the system output contract.",
164
+ noneReminder,
165
+ "Prefer specific evidence from memory over generic reassurance.",
166
+ "The completion summary must be suitable for an explicit pingback and handoff."
167
+ ]
168
+ .filter(Boolean)
169
+ .join("\n\n");
170
+ }
171
+
172
+ export class Executor {
173
+ constructor({ projectRoot, liveRoot, stateRoot, fetchImpl, execApprovalGate = null } = {}) {
174
+ this.projectRoot = projectRoot;
175
+ this.liveRoot = liveRoot;
176
+ this.stateRoot = stateRoot;
177
+ this.scheduler = new Scheduler({ projectRoot, liveRoot, stateRoot });
178
+ this.registry = new ProviderRegistry({ fetchImpl });
179
+ this.runStore = new RunStore({ rootDir: path.join(stateRoot, "runs") });
180
+ this.notifier = new InteractionNotifier({ stateRoot });
181
+ this.tokenCounter = new TokenCounter();
182
+ this.toolRegistry = new ToolRegistry();
183
+ this.stateStore = new SchedulerStateStore({ rootDir: path.join(stateRoot, "scheduler") });
184
+ this._breakerDb = null;
185
+ this._breakers = new Map();
186
+ this.execApprovalGate = execApprovalGate || new ExecApprovalGate({ sendFn: async () => {} });
187
+ }
188
+
189
+ async initToolRegistry() {
190
+ if (this._toolsInitialized) return;
191
+ this._toolContext = {};
192
+ registerMicroToolHandlers(this.toolRegistry, this._toolContext);
193
+ try {
194
+ const { join } = await import("node:path");
195
+ await this.toolRegistry.loadTools(join(this.projectRoot, "config", "tools"));
196
+ } catch {
197
+ /* no tools dir is fine */
198
+ }
199
+ this._toolsInitialized = true;
200
+ }
201
+
202
+ _getBreaker(modelId, runtimeConfig) {
203
+ if (!this._breakerDb) {
204
+ const schedulerDir = path.join(this.stateRoot, "scheduler");
205
+ mkdirSync(schedulerDir, { recursive: true });
206
+ const dbPath = path.join(schedulerDir, "scheduler.sqlite");
207
+ this._breakerDb = new DatabaseSync(dbPath, { timeout: 5000 });
208
+ ensureBreakerSchema(this._breakerDb);
209
+ }
210
+ if (this._breakers.has(modelId)) return this._breakers.get(modelId);
211
+ const cbConfig = runtimeConfig?.circuit_breaker || runtimeConfig?.circuitBreaker || {};
212
+ const breaker = CircuitBreaker.forProvider(modelId, this._breakerDb, {
213
+ failureThreshold: cbConfig.failure_threshold ?? 5,
214
+ resetTimeoutSeconds: cbConfig.reset_timeout_seconds ?? 30,
215
+ transientCodes: cbConfig.transient_codes ?? [408, 429, 500, 502, 503, 504]
216
+ });
217
+ this._breakers.set(modelId, breaker);
218
+ return breaker;
219
+ }
220
+
221
+ _logUsage({ sessionId, jobId, modelId, providerId, usage, costUsd }) {
222
+ this.stateStore.db.prepare(`INSERT INTO usage_log (id, session_id, job_id, ts, model, tokens_in, tokens_out, cost_usd, provider, cache_in, cache_creation)
223
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
224
+ .run(
225
+ crypto.randomUUID(),
226
+ sessionId,
227
+ jobId,
228
+ Date.now(),
229
+ modelId,
230
+ usage.tokensIn,
231
+ usage.tokensOut,
232
+ costUsd,
233
+ providerId,
234
+ usage.cacheIn,
235
+ usage.cacheCreation
236
+ );
237
+ }
238
+
239
+ setExecApprovalGate(execApprovalGate) {
240
+ if (execApprovalGate) {
241
+ this.execApprovalGate = execApprovalGate;
242
+ }
243
+ }
244
+
245
+ async executeJob(jobId, options = {}) {
246
+ const mode = options.mode || "dry-run";
247
+ let plan = null;
248
+ let routingDecision = null;
249
+ let modelId = null;
250
+ let providerId = null;
251
+ let providerCapabilities = null;
252
+ let invocation = null;
253
+ let preflight = null;
254
+ let fallback = null;
255
+ let health = null;
256
+ let providerModePolicy = null;
257
+ let result = null;
258
+ const runId = createRuntimeId("run");
259
+
260
+ try {
261
+ await this.initToolRegistry();
262
+ await this.stateStore.ensureReady();
263
+ plan = options.plan || await this.scheduler.hydrateJob(jobId, {
264
+ shadowImport: options.shadowImport ?? true,
265
+ objective: options.objective,
266
+ artifactRefs: options.artifactRefs,
267
+ memoryLimit: options.memoryLimit,
268
+ reportGuidanceOverride: options.reportGuidanceOverride,
269
+ retrievalBlendOverride: options.retrievalBlendOverride
270
+ });
271
+ const runtime = await this.scheduler.loadRuntime();
272
+ if (this._toolContext) {
273
+ this._toolContext.workspaceRoot = plan.agent?.workspaceRoot || this.projectRoot;
274
+ }
275
+ this.runStore.setRetentionPolicy(runtime.runtime?.retention?.runs || {});
276
+ this.notifier.configureRetention(runtime.runtime?.retention?.notifications || {});
277
+ const peerRegistry = new PeerRegistry(runtime.peers);
278
+ const agentPolicy = plan.policies?.tools?.[plan.agent?.tool_policy]
279
+ || { default: "deny", allowed: plan.packet.layers.tools || [], blocked: [] };
280
+ const toolSchemas = this.toolRegistry.schemasForAgent(plan.agent?.id, agentPolicy);
281
+
282
+ const providerAttempt = await attemptProvider({
283
+ plan,
284
+ runtime,
285
+ options: {
286
+ ...options,
287
+ mode,
288
+ registry: this.registry,
289
+ tokenCounter: this.tokenCounter,
290
+ toolSchemas,
291
+ buildSystemPrompt,
292
+ buildUserMessage,
293
+ getBreaker: this._getBreaker.bind(this)
294
+ }
295
+ });
296
+
297
+ routingDecision = providerAttempt.routingDecision;
298
+ modelId = providerAttempt.modelId;
299
+ providerId = providerAttempt.providerId;
300
+ providerCapabilities = providerAttempt.providerCapabilities;
301
+ invocation = providerAttempt.invocation;
302
+ preflight = providerAttempt.preflight;
303
+ fallback = providerAttempt.fallback;
304
+ health = providerAttempt.health;
305
+ providerModePolicy = providerAttempt.providerModePolicy;
306
+
307
+ const toolLoop = await runToolLoop({
308
+ initialResponse: providerAttempt.response,
309
+ adapter: providerAttempt.adapter,
310
+ invocation,
311
+ registry: this.toolRegistry,
312
+ policy: agentPolicy,
313
+ agentConfig: plan.agent,
314
+ approvalGate: this.execApprovalGate,
315
+ options: {
316
+ enabled: providerAttempt.toolLoopEnabled,
317
+ jobId: plan.job.id,
318
+ agentId: plan.agent?.id,
319
+ maxToolCalls: plan.packet.budget?.maxToolCalls || plan.job?.limits?.max_tool_calls_per_turn || 20,
320
+ skipApprovalNotification: options.skipApprovalNotification === true,
321
+ }
322
+ });
323
+
324
+ const ackNotification = await this.notifier.queueAck(plan, {
325
+ runId,
326
+ mode,
327
+ providerId,
328
+ providerCapabilities,
329
+ modelId,
330
+ routingDecision
331
+ });
332
+
333
+ const normalized = await normalizeResult({
334
+ plan,
335
+ rawResponse: toolLoop.finalResponse,
336
+ toolResults: toolLoop.toolResults,
337
+ usage: providerAttempt.toolLoopEnabled ? undefined : providerAttempt.usage,
338
+ options: {
339
+ mode,
340
+ adapter: providerAttempt.adapter,
341
+ modelId,
342
+ providerId,
343
+ pendingApprovalResult: toolLoop.pendingApprovalResult || null,
344
+ sessionId: options.sessionId,
345
+ logUsage: ({ sessionId, usage, costUsd }) => this._logUsage({
346
+ sessionId,
347
+ jobId: plan.job.id,
348
+ modelId,
349
+ providerId,
350
+ usage,
351
+ costUsd
352
+ })
353
+ }
354
+ });
355
+ result = normalized.result;
356
+
357
+ const handoffConfig = plan.packet.layers.interactionContract?.handoff || null;
358
+ const handoffSuggestions =
359
+ handoffConfig?.enabled
360
+ ? peerRegistry.suggestPeers({
361
+ taskType: plan.job.taskType,
362
+ query: handoffConfig.capabilityQuery || plan.job.taskType || plan.job.id,
363
+ preferredDeliveryProfile: plan.agent?.deliveryProfile || null,
364
+ trustLevel: handoffConfig.trustLevel || null,
365
+ limit: handoffConfig.maxSuggestions || 3
366
+ })
367
+ : [];
368
+ const interaction = buildInteractionPlan(plan, result, null, {
369
+ handoffSuggestions
370
+ });
371
+ const run = {
372
+ id: runId,
373
+ timestamp: new Date().toISOString(),
374
+ mode,
375
+ providerId,
376
+ providerCapabilities,
377
+ modelId,
378
+ routingDecision,
379
+ providerModePolicy,
380
+ fallback,
381
+ providerHealth: health,
382
+ preflight,
383
+ invocation,
384
+ plan,
385
+ result,
386
+ usage: normalized.usage,
387
+ costUsd: normalized.costUsd,
388
+ cached: normalized.cached,
389
+ toolCalls: toolLoop.toolResults,
390
+ pendingApproval: Boolean(toolLoop.pendingApprovalResult),
391
+ interaction,
392
+ idempotencyKey: options.idempotencyKey || null
393
+ };
394
+
395
+ const notificationFiles = [ackNotification?.filePath].filter(Boolean);
396
+ if (interaction?.yield?.required) {
397
+ const followUpNotification = await this.notifier.queueFollowUp(run);
398
+ run.followUp = interaction.yield;
399
+ run.followUpFile = followUpNotification?.filePath || null;
400
+ notificationFiles.push(followUpNotification?.filePath);
401
+ } else {
402
+ const completionNotification = await this.notifier.queueCompletion(run);
403
+ const handoffNotification = await this.notifier.queueHandoff(run);
404
+ notificationFiles.push(completionNotification?.filePath, handoffNotification?.filePath);
405
+ }
406
+ run.notificationFiles = notificationFiles.filter(Boolean);
407
+
408
+ const filePath = await this.runStore.saveRun(jobId, run);
409
+ return {
410
+ filePath,
411
+ ...run
412
+ };
413
+ } catch (error) {
414
+ if (error?.routingDecision) routingDecision = error.routingDecision;
415
+ if (error?.providerId) providerId = error.providerId;
416
+ if (error?.modelId) modelId = error.modelId;
417
+ if (error?.providerCapabilities) providerCapabilities = error.providerCapabilities;
418
+ if (error?.invocation) invocation = error.invocation;
419
+ if (error?.preflight) preflight = error.preflight;
420
+ if (error?.fallback) fallback = error.fallback;
421
+
422
+ if (plan) {
423
+ const errorNotification = await this.notifier.queueError({
424
+ plan,
425
+ error,
426
+ context: {
427
+ runId,
428
+ mode,
429
+ providerId,
430
+ providerCapabilities,
431
+ modelId,
432
+ routingDecision,
433
+ timestamp: new Date().toISOString()
434
+ }
435
+ });
436
+
437
+ const failedRun = {
438
+ id: runId,
439
+ timestamp: new Date().toISOString(),
440
+ mode,
441
+ providerId,
442
+ providerCapabilities,
443
+ modelId,
444
+ routingDecision,
445
+ providerModePolicy: mode === "provider" ? getProviderModePolicy() : null,
446
+ fallback,
447
+ preflight,
448
+ invocation,
449
+ plan,
450
+ error: {
451
+ message: error.message
452
+ },
453
+ notificationFiles: [errorNotification?.filePath].filter(Boolean)
454
+ };
455
+
456
+ const filePath = await this.runStore.saveRun(jobId, failedRun);
457
+ error.runFile = filePath;
458
+ }
459
+
460
+ throw error;
461
+ }
462
+ }
463
+
464
+ async shadowCompare(jobId) {
465
+ const [run, jobComparisons] = await Promise.all([
466
+ this.executeJob(jobId, { mode: "dry-run", shadowImport: true }),
467
+ this.scheduler.compareJobs()
468
+ ]);
469
+
470
+ const relatedLive = jobComparisons.find((job) => job.v2JobId === jobId)?.closestLiveJobs || [];
471
+
472
+ const comparison = {
473
+ timestamp: new Date().toISOString(),
474
+ jobId,
475
+ v2Run: {
476
+ mode: run.mode,
477
+ providerId: run.providerId,
478
+ modelId: run.modelId,
479
+ routingDecision: run.routingDecision,
480
+ summary: run.result.summary,
481
+ output: run.result.output,
482
+ providerHealth: run.providerHealth,
483
+ runFile: run.filePath
484
+ },
485
+ liveCandidates: relatedLive,
486
+ notes: [
487
+ relatedLive.length === 0
488
+ ? "No close live cron analogue found."
489
+ : `Found ${relatedLive.length} possible live cron analogues.`,
490
+ "Use provider mode only after dry-run shape looks correct."
491
+ ]
492
+ };
493
+
494
+ const filePath = await this.runStore.saveRun(`${jobId}-comparison`, comparison);
495
+ return {
496
+ filePath,
497
+ ...comparison
498
+ };
499
+ }
500
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Sends a failure ping to the Telegram operator when a job fails,
3
+ * and a user-friendly error message to the originating chat.
4
+ * Never throws — all errors are caught and logged internally.
5
+ */
6
+
7
+ function isRetryableError(err) {
8
+ const msg = (err?.message || "").toLowerCase();
9
+ const status = err?.status || err?.statusCode || 0;
10
+ return status >= 500 || err?.code === "ETIMEDOUT" ||
11
+ msg.includes("etimedout") || msg.includes("timeout") || msg.includes("fetch failed");
12
+ }
13
+
14
+ /**
15
+ * @param {object} job
16
+ * @param {Error} error
17
+ * @param {object} options
18
+ * @param {string} options.botToken
19
+ * @param {string} options.operatorChatId
20
+ * @param {Function} [options.fetchImpl]
21
+ */
22
+ export async function sendFailurePing(job, error, options = {}) {
23
+ const { botToken, operatorChatId, fetchImpl } = options;
24
+
25
+ if (!botToken) return;
26
+ if (job.source === "completion_ping") return;
27
+
28
+ const fetchFn = fetchImpl || globalThis.fetch;
29
+
30
+ // Send user-friendly error to the originating chat
31
+ if (job.chat_id) {
32
+ const isTurnTimeout = error?.message === "turn_timeout";
33
+ let userMsg;
34
+ if (isTurnTimeout) {
35
+ userMsg = "⚠️ Turn timed out after 2 minutes. Try again or /stop to reset.";
36
+ } else if (isRetryableError(error)) {
37
+ userMsg = "⚠️ Provider hiccup — couldn't complete that. Try again in a moment.";
38
+ } else {
39
+ const snippet = (error?.message || "unknown error").slice(0, 120);
40
+ userMsg = `⚠️ Something went wrong: ${snippet}. Try again or /stop to reset.`;
41
+ }
42
+ try {
43
+ await fetchFn(`https://api.telegram.org/bot${botToken}/sendMessage`, {
44
+ method: "POST",
45
+ headers: { "content-type": "application/json" },
46
+ body: JSON.stringify({ chat_id: job.chat_id, text: userMsg }),
47
+ });
48
+ } catch (sendErr) {
49
+ console.error(JSON.stringify({ service: "error_delivery", event: "failed", error: sendErr.message, jobId: job.job_id }));
50
+ }
51
+ }
52
+
53
+ // Operator notification (detailed)
54
+ if (operatorChatId) {
55
+ const errorSnippet = (error?.message || String(error)).slice(0, 120);
56
+ const text = `⚠️ Job failed\nAgent: ${job.agent_id}\nJob: ${job.job_id}\nError: ${errorSnippet}\nSource: ${job.source}`;
57
+ try {
58
+ await fetchFn(`https://api.telegram.org/bot${botToken}/sendMessage`, {
59
+ method: "POST",
60
+ headers: { "content-type": "application/json" },
61
+ body: JSON.stringify({ chat_id: operatorChatId, text }),
62
+ });
63
+ } catch (sendErr) {
64
+ console.error(JSON.stringify({ service: "failure_ping", error: sendErr.message, jobId: job.job_id }));
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Cross-Pillar Flows — coordinates seams between pillars.
3
+ */
4
+
5
+ export async function flowTaskEscalationToOperator(task, deliveryAck, telegramSend, { chatId, retryDelays } = {}) {
6
+ const chain = JSON.parse(task.escalation_chain);
7
+ const target = chain[task.escalation_index] || "operator";
8
+ if (target !== "operator") {
9
+ throw new Error(`Flow 1 only handles operator escalation, got target: ${target}`);
10
+ }
11
+ const body = [
12
+ `${task.assigned_agent} timed out on: ${task.objective}`,
13
+ task.error_slot ? `Error: ${task.error_slot}` : null,
14
+ `Result so far: ${task.result_slot || "none"}`,
15
+ "Want me to intervene?",
16
+ ].filter(Boolean).join("\n");
17
+ return deliveryAck.sendWithGuarantee({
18
+ sourceAgent: task.owner_agent,
19
+ criticality: "decision_required",
20
+ payload: { text: body },
21
+ chatId,
22
+ correlationId: task.envelope_id,
23
+ sendFn: (payload) => telegramSend({ text: payload.text, chatId }),
24
+ retryDelays,
25
+ });
26
+ }
27
+
28
+ export async function flowTaskCompletionToMemory(task, writeBuffer) {
29
+ const summary = task.result_slot
30
+ ? (typeof task.result_slot === "string" ? task.result_slot : JSON.stringify(task.result_slot))
31
+ : "no result";
32
+ const content = `Task "${task.objective}" completed by ${task.assigned_agent}: ${summary.slice(0, 500)}`;
33
+ writeBuffer.add({
34
+ content,
35
+ confidence: 0.85,
36
+ sourceAgent: task.assigned_agent,
37
+ category: "task_result",
38
+ type: "fact",
39
+ });
40
+ }
41
+
42
+ export async function flowDecisionResponseToTask(envelope, operatorMessage, taskContract, { resolveAgent, callerAgent } = {}) {
43
+ const task = taskContract.db.prepare(
44
+ "SELECT * FROM tasks WHERE envelope_id = ?"
45
+ ).get(envelope.correlation_id);
46
+ if (!task) return { action: "unstructured", reason: "no linked task" };
47
+
48
+ const msg = operatorMessage.toLowerCase();
49
+
50
+ if (msg.includes("retry")) {
51
+ const newTask = taskContract.createTask({
52
+ ownerAgent: task.owner_agent,
53
+ assignedAgent: task.assigned_agent,
54
+ objective: task.objective,
55
+ deadlineMinutes: Math.max(10, Math.round((new Date(task.deadline_at) - new Date(task.created_at)) / 60000)),
56
+ });
57
+ return { action: "retry", newTaskId: newTask.task_id, originalTaskId: task.task_id };
58
+ }
59
+
60
+ if (msg.includes("cancel")) {
61
+ try {
62
+ taskContract.transition(task.task_id, "cancelled", task.owner_agent);
63
+ } catch { /* may not be in cancellable state */ }
64
+ return { action: "cancel", taskId: task.task_id };
65
+ }
66
+
67
+ if (msg.includes("reassign")) {
68
+ const match = operatorMessage.match(/reassign(?:\s+to)?\s+(\w+)/i);
69
+ const targetName = match?.[1];
70
+ if (targetName && resolveAgent && resolveAgent(targetName)) {
71
+ const newTask = taskContract.createTask({
72
+ ownerAgent: task.owner_agent,
73
+ assignedAgent: targetName,
74
+ objective: task.objective,
75
+ deadlineMinutes: Math.max(10, Math.round((new Date(task.deadline_at) - new Date(task.created_at)) / 60000)),
76
+ });
77
+ return { action: "reassign", newTaskId: newTask.task_id, assignedAgent: targetName };
78
+ }
79
+ return { action: "unstructured", reason: `Could not resolve agent: ${targetName}` };
80
+ }
81
+
82
+ return { action: "unstructured", message: operatorMessage };
83
+ }
@@ -0,0 +1,45 @@
1
+ const DEFAULT_PROVIDER_MODE = {
2
+ allowProviderExecution: false,
3
+ allowRemoteProviders: false,
4
+ allowedJobIds: ["heartbeat-check", "workspace-health", "memory-rollup"],
5
+ allowedLanes: ["local_cheap", "local_report", "report_fallback_lowcost"],
6
+ requireHealthyProvider: true
7
+ };
8
+
9
+ function readFlag(name) {
10
+ const raw = process.env[name];
11
+ return raw === "1" || raw === "true";
12
+ }
13
+
14
+ export function getProviderModePolicy() {
15
+ return {
16
+ ...DEFAULT_PROVIDER_MODE,
17
+ allowProviderExecution: readFlag("NEMORIS_ALLOW_PROVIDER_MODE"),
18
+ allowRemoteProviders: readFlag("NEMORIS_ALLOW_REMOTE_PROVIDER_MODE")
19
+ };
20
+ }
21
+
22
+ export function assertProviderExecutionAllowed({ jobId, modelLane, providerId, interactive = false }) {
23
+ const policy = getProviderModePolicy();
24
+
25
+ if (!policy.allowProviderExecution) {
26
+ throw new Error("Provider mode disabled. Set NEMORIS_ALLOW_PROVIDER_MODE=1 to enable guarded shadow execution.");
27
+ }
28
+
29
+ // Interactive jobs (Telegram, agent dispatch) bypass job-id and lane allowlists
30
+ if (!interactive) {
31
+ if (!policy.allowedJobIds.includes(jobId)) {
32
+ throw new Error(`Provider mode not allowed for job ${jobId}.`);
33
+ }
34
+
35
+ if (!policy.allowedLanes.includes(modelLane)) {
36
+ throw new Error(`Provider mode not allowed for lane ${modelLane}.`);
37
+ }
38
+ }
39
+
40
+ if (providerId !== "ollama" && !policy.allowRemoteProviders) {
41
+ throw new Error(`Remote provider execution blocked for ${providerId}. Set NEMORIS_ALLOW_REMOTE_PROVIDER_MODE=1 to override.`);
42
+ }
43
+
44
+ return policy;
45
+ }