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.
- package/.env.example +49 -0
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/SECURITY.md +119 -0
- package/bin/nemoris +46 -0
- package/config/agents/agent.toml.example +28 -0
- package/config/agents/default.toml +22 -0
- package/config/agents/orchestrator.toml +18 -0
- package/config/delivery.toml +73 -0
- package/config/embeddings.toml +5 -0
- package/config/identity/default-purpose.md +1 -0
- package/config/identity/default-soul.md +3 -0
- package/config/identity/orchestrator-purpose.md +1 -0
- package/config/identity/orchestrator-soul.md +1 -0
- package/config/improvement-targets.toml +15 -0
- package/config/jobs/heartbeat-check.toml +30 -0
- package/config/jobs/memory-rollup.toml +46 -0
- package/config/jobs/workspace-health.toml +63 -0
- package/config/mcp.toml +16 -0
- package/config/output-contracts.toml +17 -0
- package/config/peers.toml +32 -0
- package/config/peers.toml.example +32 -0
- package/config/policies/memory-default.toml +10 -0
- package/config/policies/memory-heartbeat.toml +5 -0
- package/config/policies/memory-ops.toml +10 -0
- package/config/policies/tools-heartbeat-minimal.toml +8 -0
- package/config/policies/tools-interactive-safe.toml +8 -0
- package/config/policies/tools-ops-bounded.toml +8 -0
- package/config/policies/tools-orchestrator.toml +7 -0
- package/config/providers/anthropic.toml +15 -0
- package/config/providers/ollama.toml +5 -0
- package/config/providers/openai-codex.toml +9 -0
- package/config/providers/openrouter.toml +5 -0
- package/config/router.toml +22 -0
- package/config/runtime.toml +114 -0
- package/config/skills/self-improvement.toml +15 -0
- package/config/skills/telegram-onboarding-spec.md +240 -0
- package/config/skills/workspace-monitor.toml +15 -0
- package/config/task-router.toml +42 -0
- package/install.sh +50 -0
- package/package.json +90 -0
- package/src/auth/auth-profiles.js +169 -0
- package/src/auth/openai-codex-oauth.js +285 -0
- package/src/battle.js +449 -0
- package/src/cli/help.js +265 -0
- package/src/cli/output-filter.js +49 -0
- package/src/cli/runtime-control.js +704 -0
- package/src/cli-main.js +2763 -0
- package/src/cli.js +78 -0
- package/src/config/loader.js +332 -0
- package/src/config/schema-validator.js +214 -0
- package/src/config/toml-lite.js +8 -0
- package/src/daemon/action-handlers.js +71 -0
- package/src/daemon/healing-tick.js +87 -0
- package/src/daemon/health-probes.js +90 -0
- package/src/daemon/notifier.js +57 -0
- package/src/daemon/nurse.js +218 -0
- package/src/daemon/repair-log.js +106 -0
- package/src/daemon/rule-staging.js +90 -0
- package/src/daemon/rules.js +29 -0
- package/src/daemon/telegram-commands.js +54 -0
- package/src/daemon/updater.js +85 -0
- package/src/jobs/job-runner.js +78 -0
- package/src/mcp/consumer.js +129 -0
- package/src/memory/active-recall.js +171 -0
- package/src/memory/backend-manager.js +97 -0
- package/src/memory/backends/file-backend.js +38 -0
- package/src/memory/backends/qmd-backend.js +219 -0
- package/src/memory/embedding-guards.js +24 -0
- package/src/memory/embedding-index.js +118 -0
- package/src/memory/embedding-service.js +179 -0
- package/src/memory/file-index.js +177 -0
- package/src/memory/memory-signature.js +5 -0
- package/src/memory/memory-store.js +648 -0
- package/src/memory/retrieval-planner.js +66 -0
- package/src/memory/scoring.js +145 -0
- package/src/memory/simhash.js +78 -0
- package/src/memory/sqlite-active-store.js +824 -0
- package/src/memory/write-policy.js +36 -0
- package/src/onboarding/aliases.js +33 -0
- package/src/onboarding/auth/api-key.js +224 -0
- package/src/onboarding/auth/ollama-detect.js +42 -0
- package/src/onboarding/clack-prompter.js +77 -0
- package/src/onboarding/doctor.js +530 -0
- package/src/onboarding/lock.js +42 -0
- package/src/onboarding/model-catalog.js +344 -0
- package/src/onboarding/phases/auth.js +589 -0
- package/src/onboarding/phases/build.js +130 -0
- package/src/onboarding/phases/choose.js +82 -0
- package/src/onboarding/phases/detect.js +98 -0
- package/src/onboarding/phases/hatch.js +216 -0
- package/src/onboarding/phases/identity.js +79 -0
- package/src/onboarding/phases/ollama.js +345 -0
- package/src/onboarding/phases/scaffold.js +99 -0
- package/src/onboarding/phases/telegram.js +377 -0
- package/src/onboarding/phases/validate.js +204 -0
- package/src/onboarding/phases/verify.js +206 -0
- package/src/onboarding/platform.js +482 -0
- package/src/onboarding/status-bar.js +95 -0
- package/src/onboarding/templates.js +794 -0
- package/src/onboarding/toml-writer.js +38 -0
- package/src/onboarding/tui.js +250 -0
- package/src/onboarding/uninstall.js +153 -0
- package/src/onboarding/wizard.js +499 -0
- package/src/providers/anthropic.js +168 -0
- package/src/providers/base.js +247 -0
- package/src/providers/circuit-breaker.js +136 -0
- package/src/providers/ollama.js +163 -0
- package/src/providers/openai-codex.js +149 -0
- package/src/providers/openrouter.js +136 -0
- package/src/providers/registry.js +36 -0
- package/src/providers/router.js +16 -0
- package/src/runtime/bootstrap-cache.js +47 -0
- package/src/runtime/capabilities-prompt.js +25 -0
- package/src/runtime/completion-ping.js +99 -0
- package/src/runtime/config-validator.js +121 -0
- package/src/runtime/context-ledger.js +360 -0
- package/src/runtime/cutover-readiness.js +42 -0
- package/src/runtime/daemon.js +729 -0
- package/src/runtime/delivery-ack.js +195 -0
- package/src/runtime/delivery-adapters/local-file.js +41 -0
- package/src/runtime/delivery-adapters/openclaw-cli.js +94 -0
- package/src/runtime/delivery-adapters/openclaw-peer.js +98 -0
- package/src/runtime/delivery-adapters/shadow.js +13 -0
- package/src/runtime/delivery-adapters/standalone-http.js +98 -0
- package/src/runtime/delivery-adapters/telegram.js +104 -0
- package/src/runtime/delivery-adapters/tui.js +128 -0
- package/src/runtime/delivery-manager.js +807 -0
- package/src/runtime/delivery-store.js +168 -0
- package/src/runtime/dependency-health.js +118 -0
- package/src/runtime/envelope.js +114 -0
- package/src/runtime/evaluation.js +1089 -0
- package/src/runtime/exec-approvals.js +216 -0
- package/src/runtime/executor.js +500 -0
- package/src/runtime/failure-ping.js +67 -0
- package/src/runtime/flows.js +83 -0
- package/src/runtime/guards.js +45 -0
- package/src/runtime/handoff.js +51 -0
- package/src/runtime/identity-cache.js +28 -0
- package/src/runtime/improvement-engine.js +109 -0
- package/src/runtime/improvement-harness.js +581 -0
- package/src/runtime/input-sanitiser.js +72 -0
- package/src/runtime/interaction-contract.js +347 -0
- package/src/runtime/lane-readiness.js +226 -0
- package/src/runtime/migration.js +323 -0
- package/src/runtime/model-resolution.js +78 -0
- package/src/runtime/network.js +64 -0
- package/src/runtime/notification-store.js +97 -0
- package/src/runtime/notifier.js +256 -0
- package/src/runtime/orchestrator.js +53 -0
- package/src/runtime/orphan-reaper.js +41 -0
- package/src/runtime/output-contract-schema.js +139 -0
- package/src/runtime/output-contract-validator.js +439 -0
- package/src/runtime/peer-readiness.js +69 -0
- package/src/runtime/peer-registry.js +133 -0
- package/src/runtime/pilot-status.js +108 -0
- package/src/runtime/prompt-builder.js +261 -0
- package/src/runtime/provider-attempt.js +582 -0
- package/src/runtime/report-fallback.js +71 -0
- package/src/runtime/result-normalizer.js +183 -0
- package/src/runtime/retention.js +74 -0
- package/src/runtime/review.js +244 -0
- package/src/runtime/route-job.js +15 -0
- package/src/runtime/run-store.js +38 -0
- package/src/runtime/schedule.js +88 -0
- package/src/runtime/scheduler-state.js +434 -0
- package/src/runtime/scheduler.js +656 -0
- package/src/runtime/session-compactor.js +182 -0
- package/src/runtime/session-search.js +155 -0
- package/src/runtime/slack-inbound.js +249 -0
- package/src/runtime/ssrf.js +102 -0
- package/src/runtime/status-aggregator.js +330 -0
- package/src/runtime/task-contract.js +140 -0
- package/src/runtime/task-packet.js +107 -0
- package/src/runtime/task-router.js +140 -0
- package/src/runtime/telegram-inbound.js +1565 -0
- package/src/runtime/token-counter.js +134 -0
- package/src/runtime/token-estimator.js +59 -0
- package/src/runtime/tool-loop.js +200 -0
- package/src/runtime/transport-server.js +311 -0
- package/src/runtime/tui-server.js +411 -0
- package/src/runtime/ulid.js +44 -0
- package/src/security/ssrf-check.js +197 -0
- package/src/setup.js +369 -0
- package/src/shadow/bridge.js +303 -0
- package/src/skills/loader.js +84 -0
- package/src/tools/catalog.json +49 -0
- package/src/tools/cli-delegate.js +44 -0
- package/src/tools/mcp-client.js +106 -0
- package/src/tools/micro/cancel-task.js +6 -0
- package/src/tools/micro/complete-task.js +6 -0
- package/src/tools/micro/fail-task.js +6 -0
- package/src/tools/micro/http-fetch.js +74 -0
- package/src/tools/micro/index.js +36 -0
- package/src/tools/micro/lcm-recall.js +60 -0
- package/src/tools/micro/list-dir.js +17 -0
- package/src/tools/micro/list-skills.js +46 -0
- package/src/tools/micro/load-skill.js +38 -0
- package/src/tools/micro/memory-search.js +45 -0
- package/src/tools/micro/read-file.js +11 -0
- package/src/tools/micro/session-search.js +54 -0
- package/src/tools/micro/shell-exec.js +43 -0
- package/src/tools/micro/trigger-job.js +79 -0
- package/src/tools/micro/web-search.js +58 -0
- package/src/tools/micro/workspace-paths.js +39 -0
- package/src/tools/micro/write-file.js +14 -0
- package/src/tools/micro/write-memory.js +41 -0
- package/src/tools/registry.js +348 -0
- package/src/tools/tool-result-contract.js +36 -0
- package/src/tui/chat.js +835 -0
- package/src/tui/renderer.js +175 -0
- package/src/tui/socket-client.js +217 -0
- package/src/utils/canonical-json.js +29 -0
- package/src/utils/compaction.js +30 -0
- package/src/utils/env-loader.js +5 -0
- package/src/utils/errors.js +80 -0
- package/src/utils/fs.js +101 -0
- package/src/utils/ids.js +5 -0
- package/src/utils/model-context-limits.js +30 -0
- package/src/utils/token-budget.js +74 -0
- package/src/utils/usage-cost.js +25 -0
- package/src/utils/usage-metrics.js +14 -0
- 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
|
+
}
|