nemoris 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +49 -49
- package/LICENSE +21 -21
- package/README.md +209 -209
- package/SECURITY.md +59 -119
- package/bin/nemoris +46 -46
- package/config/agents/agent.toml.example +28 -28
- package/config/agents/content.toml +23 -0
- package/config/agents/default.toml +22 -22
- package/config/agents/heartbeat.toml +35 -0
- package/config/agents/iris.toml +23 -0
- package/config/agents/lab.toml +23 -0
- package/config/agents/main.toml +45 -0
- package/config/agents/nemo.toml +21 -0
- package/config/agents/ops.toml +38 -0
- package/config/agents/orchestrator.toml +18 -18
- package/config/agents/revenue.toml +23 -0
- package/config/agents/testyboo.toml +19 -0
- package/config/delivery.toml +73 -73
- package/config/embeddings.toml +5 -5
- package/config/identity/content-purpose.md +11 -0
- package/config/identity/content-soul.md +45 -0
- package/config/identity/default-purpose.md +1 -1
- package/config/identity/default-soul.md +3 -3
- package/config/identity/heartbeat-purpose.md +9 -0
- package/config/identity/heartbeat-soul.md +16 -0
- package/config/identity/iris-purpose.md +17 -0
- package/config/identity/iris-soul.md +68 -0
- package/config/identity/lab-purpose.md +10 -0
- package/config/identity/lab-soul.md +38 -0
- package/config/identity/main-purpose.md +17 -0
- package/config/identity/main-soul.md +66 -0
- package/config/identity/main-user.md +22 -0
- package/config/identity/ops-purpose.md +9 -0
- package/config/identity/ops-soul.md +16 -0
- package/config/identity/orchestrator-purpose.md +1 -1
- package/config/identity/orchestrator-soul.md +1 -1
- package/config/identity/revenue-purpose.md +9 -0
- package/config/identity/revenue-soul.md +41 -0
- package/config/identity/testyboo-purpose.md +13 -0
- package/config/identity/testyboo-soul.md +20 -0
- package/config/improvement-targets.toml +15 -15
- package/config/jobs/heartbeat-check.toml +30 -30
- package/config/jobs/memory-rollup.toml +46 -46
- package/config/jobs/workspace-health.toml +63 -63
- package/config/mcp.toml +16 -16
- package/config/output-contracts.toml +17 -17
- package/config/peers.toml +32 -32
- package/config/peers.toml.example +32 -32
- package/config/policies/memory-default.toml +10 -10
- package/config/policies/memory-heartbeat.toml +5 -5
- package/config/policies/memory-ops.toml +10 -10
- package/config/policies/tools-heartbeat-minimal.toml +8 -8
- package/config/policies/tools-interactive-safe.toml +8 -8
- package/config/policies/tools-ops-bounded.toml +8 -8
- package/config/policies/tools-orchestrator.toml +7 -7
- package/config/providers/anthropic.toml +15 -15
- package/config/providers/ollama.toml +5 -5
- package/config/providers/openai-codex.toml +9 -9
- package/config/providers/openrouter.toml +5 -5
- package/config/router.toml +22 -22
- package/config/runtime.toml +114 -114
- package/config/skills/self-improvement.toml +15 -15
- package/config/skills/telegram-onboarding-spec.md +240 -240
- package/config/skills/workspace-monitor.toml +15 -15
- package/config/task-router.toml +42 -42
- package/install.sh +50 -50
- package/package.json +91 -90
- package/src/auth/auth-profiles.js +169 -169
- package/src/auth/openai-codex-oauth.js +285 -285
- package/src/battle.js +449 -449
- package/src/cli/help.js +265 -265
- package/src/cli/output-filter.js +49 -49
- package/src/cli/runtime-control.js +704 -704
- package/src/cli-main.js +2763 -2763
- package/src/cli.js +78 -78
- package/src/config/loader.js +332 -332
- package/src/config/schema-validator.js +214 -214
- package/src/config/toml-lite.js +8 -8
- package/src/daemon/action-handlers.js +71 -71
- package/src/daemon/healing-tick.js +87 -87
- package/src/daemon/health-probes.js +90 -90
- package/src/daemon/notifier.js +57 -57
- package/src/daemon/nurse.js +218 -218
- package/src/daemon/repair-log.js +106 -106
- package/src/daemon/rule-staging.js +90 -90
- package/src/daemon/rules.js +29 -29
- package/src/daemon/telegram-commands.js +54 -54
- package/src/daemon/updater.js +85 -85
- package/src/jobs/job-runner.js +78 -78
- package/src/mcp/consumer.js +129 -129
- package/src/memory/active-recall.js +171 -171
- package/src/memory/backend-manager.js +97 -97
- package/src/memory/backends/file-backend.js +38 -38
- package/src/memory/backends/qmd-backend.js +219 -219
- package/src/memory/embedding-guards.js +24 -24
- package/src/memory/embedding-index.js +118 -118
- package/src/memory/embedding-service.js +179 -179
- package/src/memory/file-index.js +177 -177
- package/src/memory/memory-signature.js +5 -5
- package/src/memory/memory-store.js +648 -648
- package/src/memory/retrieval-planner.js +66 -66
- package/src/memory/scoring.js +145 -145
- package/src/memory/simhash.js +78 -78
- package/src/memory/sqlite-active-store.js +824 -824
- package/src/memory/write-policy.js +36 -36
- package/src/onboarding/aliases.js +33 -33
- package/src/onboarding/auth/api-key.js +224 -224
- package/src/onboarding/auth/ollama-detect.js +42 -42
- package/src/onboarding/clack-prompter.js +77 -77
- package/src/onboarding/doctor.js +530 -530
- package/src/onboarding/lock.js +42 -42
- package/src/onboarding/model-catalog.js +344 -344
- package/src/onboarding/phases/auth.js +576 -589
- package/src/onboarding/phases/build.js +130 -130
- package/src/onboarding/phases/choose.js +82 -82
- package/src/onboarding/phases/detect.js +98 -98
- package/src/onboarding/phases/hatch.js +216 -216
- package/src/onboarding/phases/identity.js +79 -79
- package/src/onboarding/phases/ollama.js +345 -345
- package/src/onboarding/phases/scaffold.js +99 -99
- package/src/onboarding/phases/telegram.js +377 -377
- package/src/onboarding/phases/validate.js +204 -204
- package/src/onboarding/phases/verify.js +206 -206
- package/src/onboarding/platform.js +482 -482
- package/src/onboarding/status-bar.js +95 -95
- package/src/onboarding/templates.js +794 -794
- package/src/onboarding/toml-writer.js +38 -38
- package/src/onboarding/tui.js +250 -250
- package/src/onboarding/uninstall.js +153 -153
- package/src/onboarding/wizard.js +516 -499
- package/src/providers/anthropic.js +168 -168
- package/src/providers/base.js +247 -247
- package/src/providers/circuit-breaker.js +136 -136
- package/src/providers/ollama.js +163 -163
- package/src/providers/openai-codex.js +149 -149
- package/src/providers/openrouter.js +136 -136
- package/src/providers/registry.js +36 -36
- package/src/providers/router.js +16 -16
- package/src/runtime/bootstrap-cache.js +47 -47
- package/src/runtime/capabilities-prompt.js +25 -25
- package/src/runtime/completion-ping.js +99 -99
- package/src/runtime/config-validator.js +121 -121
- package/src/runtime/context-ledger.js +360 -360
- package/src/runtime/cutover-readiness.js +42 -42
- package/src/runtime/daemon.js +729 -729
- package/src/runtime/delivery-ack.js +195 -195
- package/src/runtime/delivery-adapters/local-file.js +41 -41
- package/src/runtime/delivery-adapters/openclaw-cli.js +94 -94
- package/src/runtime/delivery-adapters/openclaw-peer.js +98 -98
- package/src/runtime/delivery-adapters/shadow.js +13 -13
- package/src/runtime/delivery-adapters/standalone-http.js +98 -98
- package/src/runtime/delivery-adapters/telegram.js +104 -104
- package/src/runtime/delivery-adapters/tui.js +128 -128
- package/src/runtime/delivery-manager.js +807 -807
- package/src/runtime/delivery-store.js +168 -168
- package/src/runtime/dependency-health.js +118 -118
- package/src/runtime/envelope.js +114 -114
- package/src/runtime/evaluation.js +1089 -1089
- package/src/runtime/exec-approvals.js +216 -216
- package/src/runtime/executor.js +500 -500
- package/src/runtime/failure-ping.js +67 -67
- package/src/runtime/flows.js +83 -83
- package/src/runtime/guards.js +45 -45
- package/src/runtime/handoff.js +51 -51
- package/src/runtime/identity-cache.js +28 -28
- package/src/runtime/improvement-engine.js +109 -109
- package/src/runtime/improvement-harness.js +581 -581
- package/src/runtime/input-sanitiser.js +72 -72
- package/src/runtime/interaction-contract.js +347 -347
- package/src/runtime/lane-readiness.js +226 -226
- package/src/runtime/migration.js +323 -323
- package/src/runtime/model-resolution.js +78 -78
- package/src/runtime/network.js +64 -64
- package/src/runtime/notification-store.js +97 -97
- package/src/runtime/notifier.js +256 -256
- package/src/runtime/orchestrator.js +53 -53
- package/src/runtime/orphan-reaper.js +41 -41
- package/src/runtime/output-contract-schema.js +139 -139
- package/src/runtime/output-contract-validator.js +439 -439
- package/src/runtime/peer-readiness.js +69 -69
- package/src/runtime/peer-registry.js +133 -133
- package/src/runtime/pilot-status.js +108 -108
- package/src/runtime/prompt-builder.js +261 -261
- package/src/runtime/provider-attempt.js +582 -582
- package/src/runtime/report-fallback.js +71 -71
- package/src/runtime/result-normalizer.js +183 -183
- package/src/runtime/retention.js +74 -74
- package/src/runtime/review.js +244 -244
- package/src/runtime/route-job.js +15 -15
- package/src/runtime/run-store.js +38 -38
- package/src/runtime/schedule.js +88 -88
- package/src/runtime/scheduler-state.js +434 -434
- package/src/runtime/scheduler.js +656 -656
- package/src/runtime/session-compactor.js +182 -182
- package/src/runtime/session-search.js +155 -155
- package/src/runtime/slack-inbound.js +249 -249
- package/src/runtime/ssrf.js +102 -102
- package/src/runtime/status-aggregator.js +330 -330
- package/src/runtime/task-contract.js +140 -140
- package/src/runtime/task-packet.js +107 -107
- package/src/runtime/task-router.js +140 -140
- package/src/runtime/telegram-inbound.js +1565 -1565
- package/src/runtime/token-counter.js +134 -134
- package/src/runtime/token-estimator.js +59 -59
- package/src/runtime/tool-loop.js +200 -200
- package/src/runtime/transport-server.js +311 -311
- package/src/runtime/tui-server.js +411 -411
- package/src/runtime/ulid.js +44 -44
- package/src/security/ssrf-check.js +197 -197
- package/src/setup.js +369 -369
- package/src/shadow/bridge.js +303 -303
- package/src/skills/loader.js +84 -84
- package/src/tools/catalog.json +49 -49
- package/src/tools/cli-delegate.js +44 -44
- package/src/tools/mcp-client.js +106 -106
- package/src/tools/micro/cancel-task.js +6 -6
- package/src/tools/micro/complete-task.js +6 -6
- package/src/tools/micro/fail-task.js +6 -6
- package/src/tools/micro/http-fetch.js +74 -74
- package/src/tools/micro/index.js +36 -36
- package/src/tools/micro/lcm-recall.js +60 -60
- package/src/tools/micro/list-dir.js +17 -17
- package/src/tools/micro/list-skills.js +46 -46
- package/src/tools/micro/load-skill.js +38 -38
- package/src/tools/micro/memory-search.js +45 -45
- package/src/tools/micro/read-file.js +11 -11
- package/src/tools/micro/session-search.js +54 -54
- package/src/tools/micro/shell-exec.js +43 -43
- package/src/tools/micro/trigger-job.js +79 -79
- package/src/tools/micro/web-search.js +58 -58
- package/src/tools/micro/workspace-paths.js +39 -39
- package/src/tools/micro/write-file.js +14 -14
- package/src/tools/micro/write-memory.js +41 -41
- package/src/tools/registry.js +348 -348
- package/src/tools/tool-result-contract.js +36 -36
- package/src/tui/chat.js +835 -835
- package/src/tui/renderer.js +175 -175
- package/src/tui/socket-client.js +217 -217
- package/src/utils/canonical-json.js +29 -29
- package/src/utils/compaction.js +30 -30
- package/src/utils/env-loader.js +5 -5
- package/src/utils/errors.js +80 -80
- package/src/utils/fs.js +101 -101
- package/src/utils/ids.js +5 -5
- package/src/utils/model-context-limits.js +30 -30
- package/src/utils/token-budget.js +74 -74
- package/src/utils/usage-cost.js +25 -25
- package/src/utils/usage-metrics.js +14 -14
|
@@ -1,195 +1,195 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* delivery-ack.js — Delivery acknowledgement layer for the Nemoris interaction layer.
|
|
3
|
-
* Handles criticality classification, guaranteed send pipeline, and pending decision sweeps.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Classify a response's criticality based on explicit tags or contextual signals.
|
|
8
|
-
* @param {string} responseText
|
|
9
|
-
* @param {object} context
|
|
10
|
-
* @returns {"info"|"result"|"decision_required"}
|
|
11
|
-
*/
|
|
12
|
-
export function classifyCriticality(responseText, context = {}) {
|
|
13
|
-
// Explicit tag takes highest priority
|
|
14
|
-
const tagMatch = responseText.match(/\[criticality:(info|result|decision_required)\]/);
|
|
15
|
-
if (tagMatch) return tagMatch[1];
|
|
16
|
-
|
|
17
|
-
// Escalation/timeout/error-recovery + question → decision_required
|
|
18
|
-
if (
|
|
19
|
-
(context.isEscalation || context.isTimeout || context.isErrorRecovery) &&
|
|
20
|
-
responseText.trimEnd().endsWith("?")
|
|
21
|
-
) {
|
|
22
|
-
return "decision_required";
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Task completion or failure → result
|
|
26
|
-
if (context.taskCompleted || context.taskFailed) {
|
|
27
|
-
return "result";
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return "info";
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export class DeliveryAck {
|
|
34
|
-
constructor(db, envelopeStore) {
|
|
35
|
-
this.db = db;
|
|
36
|
-
this.envelopes = envelopeStore;
|
|
37
|
-
this._ensureSchema();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
_ensureSchema() {
|
|
41
|
-
this.db.exec(`
|
|
42
|
-
CREATE TABLE IF NOT EXISTS pending_decisions (
|
|
43
|
-
envelope_id TEXT PRIMARY KEY,
|
|
44
|
-
chat_id TEXT NOT NULL,
|
|
45
|
-
delivered_at TEXT NOT NULL,
|
|
46
|
-
timeout_minutes INTEGER NOT NULL DEFAULT 15,
|
|
47
|
-
re_pinged INTEGER NOT NULL DEFAULT 0,
|
|
48
|
-
parked INTEGER NOT NULL DEFAULT 0
|
|
49
|
-
);
|
|
50
|
-
`);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Returns all pending_decisions rows that have not been parked.
|
|
55
|
-
*/
|
|
56
|
-
listPendingDecisions() {
|
|
57
|
-
return this.db.prepare("SELECT * FROM pending_decisions WHERE parked = 0").all();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Send a message with guaranteed delivery, retrying on failure.
|
|
62
|
-
* On success: marks envelope as delivered, optionally inserts into pending_decisions.
|
|
63
|
-
* On all retries exhausted: marks envelope as dead_letter.
|
|
64
|
-
*
|
|
65
|
-
* @param {object} opts
|
|
66
|
-
* @param {string} opts.sourceAgent
|
|
67
|
-
* @param {string} opts.criticality
|
|
68
|
-
* @param {object} opts.payload
|
|
69
|
-
* @param {string} opts.chatId
|
|
70
|
-
* @param {function} opts.sendFn — async (payload) => messageId
|
|
71
|
-
* @param {string|null} [opts.correlationId]
|
|
72
|
-
* @param {number[]} [opts.retryDelays] — delays in ms between attempts
|
|
73
|
-
* @returns {Promise<{envelopeId: string, messageId?: string, delivered: boolean, error?: Error}>}
|
|
74
|
-
*/
|
|
75
|
-
async sendWithGuarantee({
|
|
76
|
-
sourceAgent,
|
|
77
|
-
criticality,
|
|
78
|
-
payload,
|
|
79
|
-
chatId,
|
|
80
|
-
sendFn,
|
|
81
|
-
correlationId = null,
|
|
82
|
-
retryDelays = [2000, 8000, 32000],
|
|
83
|
-
}) {
|
|
84
|
-
const envelope = this.envelopes.create({
|
|
85
|
-
sourceAgent,
|
|
86
|
-
criticality,
|
|
87
|
-
payloadType: "delivery",
|
|
88
|
-
payload,
|
|
89
|
-
correlationId,
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
let lastError = null;
|
|
93
|
-
|
|
94
|
-
for (let attempt = 0; attempt < retryDelays.length; attempt++) {
|
|
95
|
-
if (attempt > 0 && retryDelays[attempt - 1] > 0) {
|
|
96
|
-
await new Promise((resolve) => setTimeout(resolve, retryDelays[attempt - 1]));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
const messageId = await sendFn(payload);
|
|
101
|
-
this.envelopes.updateAck(envelope.id, "delivered");
|
|
102
|
-
|
|
103
|
-
if (criticality === "decision_required") {
|
|
104
|
-
const now = new Date().toISOString();
|
|
105
|
-
this.db.prepare(`
|
|
106
|
-
INSERT OR REPLACE INTO pending_decisions (envelope_id, chat_id, delivered_at, timeout_minutes, re_pinged, parked)
|
|
107
|
-
VALUES (?, ?, ?, 15, 0, 0)
|
|
108
|
-
`).run(envelope.id, chatId, now);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return { envelopeId: envelope.id, messageId, delivered: true };
|
|
112
|
-
} catch (err) {
|
|
113
|
-
lastError = err;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// All retries exhausted
|
|
118
|
-
this.envelopes.updateAck(envelope.id, "dead_letter");
|
|
119
|
-
return { envelopeId: envelope.id, delivered: false, error: lastError };
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Sweep pending decisions: re-ping after timeout, park after double timeout.
|
|
124
|
-
*
|
|
125
|
-
* @param {object} opts
|
|
126
|
-
* @param {Date} [opts.now]
|
|
127
|
-
* @param {function} opts.sendFn — async (message) => void
|
|
128
|
-
* @returns {Promise<{rePinged: number, parked: number}>}
|
|
129
|
-
*/
|
|
130
|
-
async sweepPendingDecisions({ now = new Date(), sendFn }) {
|
|
131
|
-
const rows = this.db.prepare(`
|
|
132
|
-
SELECT pd.*, e.payload
|
|
133
|
-
FROM pending_decisions pd
|
|
134
|
-
JOIN envelopes e ON e.id = pd.envelope_id
|
|
135
|
-
WHERE pd.parked = 0
|
|
136
|
-
`).all();
|
|
137
|
-
|
|
138
|
-
let rePinged = 0;
|
|
139
|
-
let parked = 0;
|
|
140
|
-
|
|
141
|
-
for (const row of rows) {
|
|
142
|
-
const deliveredAt = new Date(row.delivered_at);
|
|
143
|
-
const elapsedMs = now.getTime() - deliveredAt.getTime();
|
|
144
|
-
const elapsedMinutes = elapsedMs / 60000;
|
|
145
|
-
|
|
146
|
-
if (elapsedMinutes >= row.timeout_minutes * 2 && row.re_pinged === 1) {
|
|
147
|
-
// Park it
|
|
148
|
-
this.db.prepare("UPDATE pending_decisions SET parked = 1 WHERE envelope_id = ?").run(row.envelope_id);
|
|
149
|
-
this.envelopes.updateAck(row.envelope_id, "dead_letter");
|
|
150
|
-
parked++;
|
|
151
|
-
} else if (elapsedMinutes >= row.timeout_minutes && row.re_pinged === 0) {
|
|
152
|
-
// Re-ping
|
|
153
|
-
let payloadText = "";
|
|
154
|
-
try {
|
|
155
|
-
const parsed = JSON.parse(row.payload);
|
|
156
|
-
payloadText = parsed.text || parsed.message || JSON.stringify(parsed);
|
|
157
|
-
} catch {
|
|
158
|
-
payloadText = String(row.payload);
|
|
159
|
-
}
|
|
160
|
-
const excerpt = payloadText.slice(0, 100);
|
|
161
|
-
await sendFn(`Still need your input on this when you get a chance (re: ${excerpt})`);
|
|
162
|
-
this.db.prepare("UPDATE pending_decisions SET re_pinged = 1 WHERE envelope_id = ?").run(row.envelope_id);
|
|
163
|
-
rePinged++;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return { rePinged, parked };
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Clear pending decisions when the operator sends a message, using a timestamp guard.
|
|
172
|
-
* Marks matching envelopes as "responded" and removes them from pending_decisions.
|
|
173
|
-
*
|
|
174
|
-
* @param {string} chatId
|
|
175
|
-
* @param {string} operatorTimestamp — ISO string; only clears decisions delivered before this
|
|
176
|
-
* @returns {number} count of cleared decisions
|
|
177
|
-
*/
|
|
178
|
-
clearPendingDecisions(chatId, operatorTimestamp) {
|
|
179
|
-
const rows = this.db.prepare(`
|
|
180
|
-
SELECT envelope_id FROM pending_decisions
|
|
181
|
-
WHERE chat_id = ? AND delivered_at < ? AND parked = 0
|
|
182
|
-
`).all(chatId, operatorTimestamp);
|
|
183
|
-
|
|
184
|
-
for (const row of rows) {
|
|
185
|
-
this.envelopes.updateAck(row.envelope_id, "responded");
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const result = this.db.prepare(`
|
|
189
|
-
DELETE FROM pending_decisions
|
|
190
|
-
WHERE chat_id = ? AND delivered_at < ? AND parked = 0
|
|
191
|
-
`).run(chatId, operatorTimestamp);
|
|
192
|
-
|
|
193
|
-
return result.changes;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* delivery-ack.js — Delivery acknowledgement layer for the Nemoris interaction layer.
|
|
3
|
+
* Handles criticality classification, guaranteed send pipeline, and pending decision sweeps.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Classify a response's criticality based on explicit tags or contextual signals.
|
|
8
|
+
* @param {string} responseText
|
|
9
|
+
* @param {object} context
|
|
10
|
+
* @returns {"info"|"result"|"decision_required"}
|
|
11
|
+
*/
|
|
12
|
+
export function classifyCriticality(responseText, context = {}) {
|
|
13
|
+
// Explicit tag takes highest priority
|
|
14
|
+
const tagMatch = responseText.match(/\[criticality:(info|result|decision_required)\]/);
|
|
15
|
+
if (tagMatch) return tagMatch[1];
|
|
16
|
+
|
|
17
|
+
// Escalation/timeout/error-recovery + question → decision_required
|
|
18
|
+
if (
|
|
19
|
+
(context.isEscalation || context.isTimeout || context.isErrorRecovery) &&
|
|
20
|
+
responseText.trimEnd().endsWith("?")
|
|
21
|
+
) {
|
|
22
|
+
return "decision_required";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Task completion or failure → result
|
|
26
|
+
if (context.taskCompleted || context.taskFailed) {
|
|
27
|
+
return "result";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return "info";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class DeliveryAck {
|
|
34
|
+
constructor(db, envelopeStore) {
|
|
35
|
+
this.db = db;
|
|
36
|
+
this.envelopes = envelopeStore;
|
|
37
|
+
this._ensureSchema();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_ensureSchema() {
|
|
41
|
+
this.db.exec(`
|
|
42
|
+
CREATE TABLE IF NOT EXISTS pending_decisions (
|
|
43
|
+
envelope_id TEXT PRIMARY KEY,
|
|
44
|
+
chat_id TEXT NOT NULL,
|
|
45
|
+
delivered_at TEXT NOT NULL,
|
|
46
|
+
timeout_minutes INTEGER NOT NULL DEFAULT 15,
|
|
47
|
+
re_pinged INTEGER NOT NULL DEFAULT 0,
|
|
48
|
+
parked INTEGER NOT NULL DEFAULT 0
|
|
49
|
+
);
|
|
50
|
+
`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Returns all pending_decisions rows that have not been parked.
|
|
55
|
+
*/
|
|
56
|
+
listPendingDecisions() {
|
|
57
|
+
return this.db.prepare("SELECT * FROM pending_decisions WHERE parked = 0").all();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Send a message with guaranteed delivery, retrying on failure.
|
|
62
|
+
* On success: marks envelope as delivered, optionally inserts into pending_decisions.
|
|
63
|
+
* On all retries exhausted: marks envelope as dead_letter.
|
|
64
|
+
*
|
|
65
|
+
* @param {object} opts
|
|
66
|
+
* @param {string} opts.sourceAgent
|
|
67
|
+
* @param {string} opts.criticality
|
|
68
|
+
* @param {object} opts.payload
|
|
69
|
+
* @param {string} opts.chatId
|
|
70
|
+
* @param {function} opts.sendFn — async (payload) => messageId
|
|
71
|
+
* @param {string|null} [opts.correlationId]
|
|
72
|
+
* @param {number[]} [opts.retryDelays] — delays in ms between attempts
|
|
73
|
+
* @returns {Promise<{envelopeId: string, messageId?: string, delivered: boolean, error?: Error}>}
|
|
74
|
+
*/
|
|
75
|
+
async sendWithGuarantee({
|
|
76
|
+
sourceAgent,
|
|
77
|
+
criticality,
|
|
78
|
+
payload,
|
|
79
|
+
chatId,
|
|
80
|
+
sendFn,
|
|
81
|
+
correlationId = null,
|
|
82
|
+
retryDelays = [2000, 8000, 32000],
|
|
83
|
+
}) {
|
|
84
|
+
const envelope = this.envelopes.create({
|
|
85
|
+
sourceAgent,
|
|
86
|
+
criticality,
|
|
87
|
+
payloadType: "delivery",
|
|
88
|
+
payload,
|
|
89
|
+
correlationId,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
let lastError = null;
|
|
93
|
+
|
|
94
|
+
for (let attempt = 0; attempt < retryDelays.length; attempt++) {
|
|
95
|
+
if (attempt > 0 && retryDelays[attempt - 1] > 0) {
|
|
96
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelays[attempt - 1]));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const messageId = await sendFn(payload);
|
|
101
|
+
this.envelopes.updateAck(envelope.id, "delivered");
|
|
102
|
+
|
|
103
|
+
if (criticality === "decision_required") {
|
|
104
|
+
const now = new Date().toISOString();
|
|
105
|
+
this.db.prepare(`
|
|
106
|
+
INSERT OR REPLACE INTO pending_decisions (envelope_id, chat_id, delivered_at, timeout_minutes, re_pinged, parked)
|
|
107
|
+
VALUES (?, ?, ?, 15, 0, 0)
|
|
108
|
+
`).run(envelope.id, chatId, now);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { envelopeId: envelope.id, messageId, delivered: true };
|
|
112
|
+
} catch (err) {
|
|
113
|
+
lastError = err;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// All retries exhausted
|
|
118
|
+
this.envelopes.updateAck(envelope.id, "dead_letter");
|
|
119
|
+
return { envelopeId: envelope.id, delivered: false, error: lastError };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Sweep pending decisions: re-ping after timeout, park after double timeout.
|
|
124
|
+
*
|
|
125
|
+
* @param {object} opts
|
|
126
|
+
* @param {Date} [opts.now]
|
|
127
|
+
* @param {function} opts.sendFn — async (message) => void
|
|
128
|
+
* @returns {Promise<{rePinged: number, parked: number}>}
|
|
129
|
+
*/
|
|
130
|
+
async sweepPendingDecisions({ now = new Date(), sendFn }) {
|
|
131
|
+
const rows = this.db.prepare(`
|
|
132
|
+
SELECT pd.*, e.payload
|
|
133
|
+
FROM pending_decisions pd
|
|
134
|
+
JOIN envelopes e ON e.id = pd.envelope_id
|
|
135
|
+
WHERE pd.parked = 0
|
|
136
|
+
`).all();
|
|
137
|
+
|
|
138
|
+
let rePinged = 0;
|
|
139
|
+
let parked = 0;
|
|
140
|
+
|
|
141
|
+
for (const row of rows) {
|
|
142
|
+
const deliveredAt = new Date(row.delivered_at);
|
|
143
|
+
const elapsedMs = now.getTime() - deliveredAt.getTime();
|
|
144
|
+
const elapsedMinutes = elapsedMs / 60000;
|
|
145
|
+
|
|
146
|
+
if (elapsedMinutes >= row.timeout_minutes * 2 && row.re_pinged === 1) {
|
|
147
|
+
// Park it
|
|
148
|
+
this.db.prepare("UPDATE pending_decisions SET parked = 1 WHERE envelope_id = ?").run(row.envelope_id);
|
|
149
|
+
this.envelopes.updateAck(row.envelope_id, "dead_letter");
|
|
150
|
+
parked++;
|
|
151
|
+
} else if (elapsedMinutes >= row.timeout_minutes && row.re_pinged === 0) {
|
|
152
|
+
// Re-ping
|
|
153
|
+
let payloadText = "";
|
|
154
|
+
try {
|
|
155
|
+
const parsed = JSON.parse(row.payload);
|
|
156
|
+
payloadText = parsed.text || parsed.message || JSON.stringify(parsed);
|
|
157
|
+
} catch {
|
|
158
|
+
payloadText = String(row.payload);
|
|
159
|
+
}
|
|
160
|
+
const excerpt = payloadText.slice(0, 100);
|
|
161
|
+
await sendFn(`Still need your input on this when you get a chance (re: ${excerpt})`);
|
|
162
|
+
this.db.prepare("UPDATE pending_decisions SET re_pinged = 1 WHERE envelope_id = ?").run(row.envelope_id);
|
|
163
|
+
rePinged++;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { rePinged, parked };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Clear pending decisions when the operator sends a message, using a timestamp guard.
|
|
172
|
+
* Marks matching envelopes as "responded" and removes them from pending_decisions.
|
|
173
|
+
*
|
|
174
|
+
* @param {string} chatId
|
|
175
|
+
* @param {string} operatorTimestamp — ISO string; only clears decisions delivered before this
|
|
176
|
+
* @returns {number} count of cleared decisions
|
|
177
|
+
*/
|
|
178
|
+
clearPendingDecisions(chatId, operatorTimestamp) {
|
|
179
|
+
const rows = this.db.prepare(`
|
|
180
|
+
SELECT envelope_id FROM pending_decisions
|
|
181
|
+
WHERE chat_id = ? AND delivered_at < ? AND parked = 0
|
|
182
|
+
`).all(chatId, operatorTimestamp);
|
|
183
|
+
|
|
184
|
+
for (const row of rows) {
|
|
185
|
+
this.envelopes.updateAck(row.envelope_id, "responded");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const result = this.db.prepare(`
|
|
189
|
+
DELETE FROM pending_decisions
|
|
190
|
+
WHERE chat_id = ? AND delivered_at < ? AND parked = 0
|
|
191
|
+
`).run(chatId, operatorTimestamp);
|
|
192
|
+
|
|
193
|
+
return result.changes;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { writeJson } from "../../utils/fs.js";
|
|
3
|
-
|
|
4
|
-
function timestampKey() {
|
|
5
|
-
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export class LocalFileDeliveryAdapter {
|
|
9
|
-
static adapterId = "local_file";
|
|
10
|
-
|
|
11
|
-
constructor({ stateRoot } = {}) {
|
|
12
|
-
this.stateRoot = stateRoot;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
async deliver(notification, context = {}) {
|
|
16
|
-
const target = context.target || notification.target || {};
|
|
17
|
-
const bucket =
|
|
18
|
-
target.mode === "peer_agent"
|
|
19
|
-
? `peer-${target.peerId || "unknown"}`
|
|
20
|
-
: String(target.mode || context.profileName || "operator").replace(/[^a-z0-9_-]+/gi, "-");
|
|
21
|
-
const filePath = path.join(this.stateRoot, "outbox", bucket, `${timestampKey()}.json`);
|
|
22
|
-
const payload = {
|
|
23
|
-
timestamp: new Date().toISOString(),
|
|
24
|
-
profile: context.profileName || null,
|
|
25
|
-
target,
|
|
26
|
-
jobId: notification.jobId,
|
|
27
|
-
stage: notification.stage,
|
|
28
|
-
message: notification.message || ""
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
await writeJson(filePath, payload);
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
status: "stored",
|
|
35
|
-
adapter: LocalFileDeliveryAdapter.adapterId,
|
|
36
|
-
filePath,
|
|
37
|
-
bucket,
|
|
38
|
-
target
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
}
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { writeJson } from "../../utils/fs.js";
|
|
3
|
+
|
|
4
|
+
function timestampKey() {
|
|
5
|
+
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class LocalFileDeliveryAdapter {
|
|
9
|
+
static adapterId = "local_file";
|
|
10
|
+
|
|
11
|
+
constructor({ stateRoot } = {}) {
|
|
12
|
+
this.stateRoot = stateRoot;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async deliver(notification, context = {}) {
|
|
16
|
+
const target = context.target || notification.target || {};
|
|
17
|
+
const bucket =
|
|
18
|
+
target.mode === "peer_agent"
|
|
19
|
+
? `peer-${target.peerId || "unknown"}`
|
|
20
|
+
: String(target.mode || context.profileName || "operator").replace(/[^a-z0-9_-]+/gi, "-");
|
|
21
|
+
const filePath = path.join(this.stateRoot, "outbox", bucket, `${timestampKey()}.json`);
|
|
22
|
+
const payload = {
|
|
23
|
+
timestamp: new Date().toISOString(),
|
|
24
|
+
profile: context.profileName || null,
|
|
25
|
+
target,
|
|
26
|
+
jobId: notification.jobId,
|
|
27
|
+
stage: notification.stage,
|
|
28
|
+
message: notification.message || ""
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
await writeJson(filePath, payload);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
status: "stored",
|
|
35
|
+
adapter: LocalFileDeliveryAdapter.adapterId,
|
|
36
|
+
filePath,
|
|
37
|
+
bucket,
|
|
38
|
+
target
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -1,94 +1,94 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { promisify } from "node:util";
|
|
3
|
-
|
|
4
|
-
const execFileAsync = promisify(execFile);
|
|
5
|
-
|
|
6
|
-
function readFlag(name) {
|
|
7
|
-
const raw = process.env[name];
|
|
8
|
-
return raw === "1" || raw === "true";
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function parseJson(text) {
|
|
12
|
-
try {
|
|
13
|
-
return JSON.parse(String(text || "").trim() || "null");
|
|
14
|
-
} catch {
|
|
15
|
-
return {
|
|
16
|
-
raw: String(text || "")
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export class OpenClawCliDeliveryAdapter {
|
|
22
|
-
static adapterId = "openclaw_cli";
|
|
23
|
-
|
|
24
|
-
constructor({ execFileImpl } = {}) {
|
|
25
|
-
this.execFileImpl = execFileImpl || execFileAsync;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async deliver(notification, context = {}) {
|
|
29
|
-
const profile = context.profileConfig || {};
|
|
30
|
-
const dryRun = profile.dryRun === true;
|
|
31
|
-
|
|
32
|
-
if (!dryRun && !readFlag("NEMORIS_ALLOW_GATEWAY_DELIVERY")) {
|
|
33
|
-
throw new Error("Gateway delivery disabled. Set NEMORIS_ALLOW_GATEWAY_DELIVERY=1 to enable live gateway delivery.");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const channel = profile.channel;
|
|
37
|
-
const target = profile.chatId || profile.target;
|
|
38
|
-
if (!channel) {
|
|
39
|
-
throw new Error("OpenClaw CLI delivery profile is missing channel.");
|
|
40
|
-
}
|
|
41
|
-
if (!target) {
|
|
42
|
-
throw new Error("OpenClaw CLI delivery profile is missing target/chatId.");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const args = [
|
|
46
|
-
"message",
|
|
47
|
-
"send",
|
|
48
|
-
"--channel",
|
|
49
|
-
String(channel),
|
|
50
|
-
"--target",
|
|
51
|
-
String(target),
|
|
52
|
-
"--message",
|
|
53
|
-
String(notification.message || ""),
|
|
54
|
-
"--json"
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
if (profile.accountId) {
|
|
58
|
-
args.push("--account", String(profile.accountId));
|
|
59
|
-
}
|
|
60
|
-
if (profile.threadId) {
|
|
61
|
-
args.push("--thread-id", String(profile.threadId));
|
|
62
|
-
}
|
|
63
|
-
if (profile.replyTo) {
|
|
64
|
-
args.push("--reply-to", String(profile.replyTo));
|
|
65
|
-
}
|
|
66
|
-
if (profile.silent !== false) {
|
|
67
|
-
args.push("--silent");
|
|
68
|
-
}
|
|
69
|
-
if (dryRun) {
|
|
70
|
-
args.push("--dry-run");
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const { stdout } = await this.execFileImpl("openclaw", args, {
|
|
74
|
-
timeout: Number(profile.timeoutMs || 10000),
|
|
75
|
-
maxBuffer: 512 * 1024
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
return {
|
|
79
|
-
status: dryRun ? "gateway_dry_run" : "gateway_sent",
|
|
80
|
-
adapter: OpenClawCliDeliveryAdapter.adapterId,
|
|
81
|
-
target: {
|
|
82
|
-
mode: channel,
|
|
83
|
-
chatId: target,
|
|
84
|
-
accountId: profile.accountId || null
|
|
85
|
-
},
|
|
86
|
-
profile: context.profileName || null,
|
|
87
|
-
command: {
|
|
88
|
-
bin: "openclaw",
|
|
89
|
-
args
|
|
90
|
-
},
|
|
91
|
-
response: parseJson(stdout)
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
}
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
|
|
6
|
+
function readFlag(name) {
|
|
7
|
+
const raw = process.env[name];
|
|
8
|
+
return raw === "1" || raw === "true";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseJson(text) {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(String(text || "").trim() || "null");
|
|
14
|
+
} catch {
|
|
15
|
+
return {
|
|
16
|
+
raw: String(text || "")
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class OpenClawCliDeliveryAdapter {
|
|
22
|
+
static adapterId = "openclaw_cli";
|
|
23
|
+
|
|
24
|
+
constructor({ execFileImpl } = {}) {
|
|
25
|
+
this.execFileImpl = execFileImpl || execFileAsync;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async deliver(notification, context = {}) {
|
|
29
|
+
const profile = context.profileConfig || {};
|
|
30
|
+
const dryRun = profile.dryRun === true;
|
|
31
|
+
|
|
32
|
+
if (!dryRun && !readFlag("NEMORIS_ALLOW_GATEWAY_DELIVERY")) {
|
|
33
|
+
throw new Error("Gateway delivery disabled. Set NEMORIS_ALLOW_GATEWAY_DELIVERY=1 to enable live gateway delivery.");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const channel = profile.channel;
|
|
37
|
+
const target = profile.chatId || profile.target;
|
|
38
|
+
if (!channel) {
|
|
39
|
+
throw new Error("OpenClaw CLI delivery profile is missing channel.");
|
|
40
|
+
}
|
|
41
|
+
if (!target) {
|
|
42
|
+
throw new Error("OpenClaw CLI delivery profile is missing target/chatId.");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const args = [
|
|
46
|
+
"message",
|
|
47
|
+
"send",
|
|
48
|
+
"--channel",
|
|
49
|
+
String(channel),
|
|
50
|
+
"--target",
|
|
51
|
+
String(target),
|
|
52
|
+
"--message",
|
|
53
|
+
String(notification.message || ""),
|
|
54
|
+
"--json"
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
if (profile.accountId) {
|
|
58
|
+
args.push("--account", String(profile.accountId));
|
|
59
|
+
}
|
|
60
|
+
if (profile.threadId) {
|
|
61
|
+
args.push("--thread-id", String(profile.threadId));
|
|
62
|
+
}
|
|
63
|
+
if (profile.replyTo) {
|
|
64
|
+
args.push("--reply-to", String(profile.replyTo));
|
|
65
|
+
}
|
|
66
|
+
if (profile.silent !== false) {
|
|
67
|
+
args.push("--silent");
|
|
68
|
+
}
|
|
69
|
+
if (dryRun) {
|
|
70
|
+
args.push("--dry-run");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { stdout } = await this.execFileImpl("openclaw", args, {
|
|
74
|
+
timeout: Number(profile.timeoutMs || 10000),
|
|
75
|
+
maxBuffer: 512 * 1024
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
status: dryRun ? "gateway_dry_run" : "gateway_sent",
|
|
80
|
+
adapter: OpenClawCliDeliveryAdapter.adapterId,
|
|
81
|
+
target: {
|
|
82
|
+
mode: channel,
|
|
83
|
+
chatId: target,
|
|
84
|
+
accountId: profile.accountId || null
|
|
85
|
+
},
|
|
86
|
+
profile: context.profileName || null,
|
|
87
|
+
command: {
|
|
88
|
+
bin: "openclaw",
|
|
89
|
+
args
|
|
90
|
+
},
|
|
91
|
+
response: parseJson(stdout)
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|