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,434 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { DatabaseSync } from "node:sqlite";
|
|
3
|
+
import { ensureDir, readJson, removePath } from "../utils/fs.js";
|
|
4
|
+
|
|
5
|
+
function clone(value) {
|
|
6
|
+
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class SchedulerStateStore {
|
|
10
|
+
constructor({ rootDir }) {
|
|
11
|
+
this.rootDir = rootDir;
|
|
12
|
+
this.dbPath = path.join(rootDir, "scheduler.sqlite");
|
|
13
|
+
this.legacyFilePath = path.join(rootDir, "jobs.json");
|
|
14
|
+
this.db = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async ensureReady() {
|
|
18
|
+
if (this.db) return this.db;
|
|
19
|
+
await ensureDir(this.rootDir);
|
|
20
|
+
this.db = new DatabaseSync(this.dbPath, {
|
|
21
|
+
timeout: 5000
|
|
22
|
+
});
|
|
23
|
+
this.db.exec("pragma busy_timeout = 5000;");
|
|
24
|
+
this.db.exec("pragma journal_mode = wal;");
|
|
25
|
+
this.db.exec("pragma synchronous = normal;");
|
|
26
|
+
this.db.exec(`
|
|
27
|
+
create table if not exists scheduler_jobs (
|
|
28
|
+
job_id text primary key,
|
|
29
|
+
state_json text not null
|
|
30
|
+
);
|
|
31
|
+
create table if not exists scheduler_meta (
|
|
32
|
+
key text primary key,
|
|
33
|
+
value_json text not null
|
|
34
|
+
);
|
|
35
|
+
create table if not exists follow_up_completions (
|
|
36
|
+
scoped_key text primary key,
|
|
37
|
+
parent_key text not null,
|
|
38
|
+
follow_up_id text not null,
|
|
39
|
+
status text not null,
|
|
40
|
+
completed_at text not null,
|
|
41
|
+
ttl_expires_at text not null
|
|
42
|
+
);
|
|
43
|
+
create index if not exists idx_followup_parent on follow_up_completions(parent_key);
|
|
44
|
+
`);
|
|
45
|
+
this.db.exec(`
|
|
46
|
+
create table if not exists interactive_jobs (
|
|
47
|
+
job_id text primary key,
|
|
48
|
+
agent_id text not null,
|
|
49
|
+
input text not null,
|
|
50
|
+
source text not null,
|
|
51
|
+
chat_id text not null,
|
|
52
|
+
status text not null default 'queued',
|
|
53
|
+
created_at text not null
|
|
54
|
+
);
|
|
55
|
+
create index if not exists idx_interactive_status on interactive_jobs(status);
|
|
56
|
+
create table if not exists chat_sessions (
|
|
57
|
+
chat_id text primary key,
|
|
58
|
+
agent_id text not null,
|
|
59
|
+
previous_agent_id text,
|
|
60
|
+
bound_at text not null,
|
|
61
|
+
bound_by text not null,
|
|
62
|
+
conversation_context text
|
|
63
|
+
);
|
|
64
|
+
create table if not exists usage_log (
|
|
65
|
+
id text primary key,
|
|
66
|
+
session_id text not null,
|
|
67
|
+
job_id text not null,
|
|
68
|
+
ts integer not null,
|
|
69
|
+
model text not null,
|
|
70
|
+
tokens_in integer not null default 0,
|
|
71
|
+
tokens_out integer not null default 0,
|
|
72
|
+
cost_usd real,
|
|
73
|
+
provider text not null
|
|
74
|
+
);
|
|
75
|
+
create index if not exists idx_usage_log_session on usage_log(session_id, ts);
|
|
76
|
+
create index if not exists idx_usage_log_ts on usage_log(ts);
|
|
77
|
+
`);
|
|
78
|
+
// Tool result cache (Sub-project 3)
|
|
79
|
+
this.db.exec(`
|
|
80
|
+
create table if not exists tool_results (
|
|
81
|
+
call_hash text primary key,
|
|
82
|
+
tool_id text not null,
|
|
83
|
+
brief text not null,
|
|
84
|
+
raw text not null,
|
|
85
|
+
index_terms text,
|
|
86
|
+
created_at text not null default (datetime('now'))
|
|
87
|
+
);
|
|
88
|
+
`);
|
|
89
|
+
// Self-healing daemon tables
|
|
90
|
+
this.db.exec(`
|
|
91
|
+
CREATE TABLE IF NOT EXISTS repair_log (
|
|
92
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
93
|
+
ts TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
94
|
+
source TEXT NOT NULL,
|
|
95
|
+
type TEXT NOT NULL,
|
|
96
|
+
context TEXT,
|
|
97
|
+
action TEXT,
|
|
98
|
+
result TEXT NOT NULL,
|
|
99
|
+
severity TEXT NOT NULL,
|
|
100
|
+
diagnosis TEXT,
|
|
101
|
+
config_before TEXT,
|
|
102
|
+
config_after TEXT,
|
|
103
|
+
escalated INTEGER NOT NULL DEFAULT 0,
|
|
104
|
+
attempts INTEGER NOT NULL DEFAULT 1,
|
|
105
|
+
parent_id INTEGER REFERENCES repair_log(id)
|
|
106
|
+
);
|
|
107
|
+
CREATE INDEX IF NOT EXISTS idx_repair_log_type ON repair_log(type);
|
|
108
|
+
CREATE INDEX IF NOT EXISTS idx_repair_log_ts ON repair_log(ts);
|
|
109
|
+
CREATE INDEX IF NOT EXISTS idx_repair_log_result_ts ON repair_log(result, ts);
|
|
110
|
+
CREATE INDEX IF NOT EXISTS idx_repair_log_escalated ON repair_log(escalated) WHERE escalated = 1;
|
|
111
|
+
CREATE INDEX IF NOT EXISTS idx_repair_log_parent ON repair_log(parent_id) WHERE parent_id IS NOT NULL;
|
|
112
|
+
|
|
113
|
+
CREATE TABLE IF NOT EXISTS rule_staging (
|
|
114
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
115
|
+
ts TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
116
|
+
proposed_by TEXT NOT NULL DEFAULT 'pattern_miner',
|
|
117
|
+
match_type TEXT NOT NULL,
|
|
118
|
+
match_context TEXT,
|
|
119
|
+
action TEXT NOT NULL,
|
|
120
|
+
severity TEXT NOT NULL,
|
|
121
|
+
action_class TEXT NOT NULL,
|
|
122
|
+
cooldown_minutes INTEGER NOT NULL DEFAULT 5,
|
|
123
|
+
priority INTEGER NOT NULL DEFAULT 10,
|
|
124
|
+
evidence TEXT,
|
|
125
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
126
|
+
promoted_at TEXT,
|
|
127
|
+
expires_at TEXT
|
|
128
|
+
);
|
|
129
|
+
`);
|
|
130
|
+
// Agent dispatch columns (Sub-project 2)
|
|
131
|
+
this._addColumnIfMissing("interactive_jobs", "triggered_by", "TEXT");
|
|
132
|
+
this._addColumnIfMissing("interactive_jobs", "completion_target", "TEXT");
|
|
133
|
+
this._addColumnIfMissing("interactive_jobs", "trigger_correlation_id", "TEXT");
|
|
134
|
+
this._addColumnIfMissing("interactive_jobs", "completion_pinged_at", "TEXT");
|
|
135
|
+
this._addColumnIfMissing("interactive_jobs", "completion_ping_attempts", "INTEGER DEFAULT 0");
|
|
136
|
+
this._addColumnIfMissing("chat_sessions", "model_override", "TEXT");
|
|
137
|
+
// Hardening sprint columns
|
|
138
|
+
this._addColumnIfMissing("interactive_jobs", "waiting_for_human", "INTEGER DEFAULT 0");
|
|
139
|
+
// Inter-agent routing columns (Gap 35)
|
|
140
|
+
this._addColumnIfMissing("interactive_jobs", "source_type", "TEXT DEFAULT 'human'");
|
|
141
|
+
this._addColumnIfMissing("interactive_jobs", "reply_session_key", "TEXT");
|
|
142
|
+
this._addColumnIfMissing("interactive_jobs", "source_agent_id", "TEXT");
|
|
143
|
+
this._addColumnIfMissing("chat_sessions", "focus_mode", "TEXT");
|
|
144
|
+
// Slash command parity columns
|
|
145
|
+
this._addColumnIfMissing("usage_log", "cache_in", "INTEGER DEFAULT 0");
|
|
146
|
+
this._addColumnIfMissing("usage_log", "cache_creation", "INTEGER DEFAULT 0");
|
|
147
|
+
this._addColumnIfMissing("chat_sessions", "think_mode", "TEXT");
|
|
148
|
+
this._addColumnIfMissing("interactive_jobs", "image_refs", "TEXT");
|
|
149
|
+
await this.migrateLegacyState();
|
|
150
|
+
return this.db;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
_addColumnIfMissing(table, column, type) {
|
|
154
|
+
const cols = this.db.prepare(`pragma table_info(${table})`).all();
|
|
155
|
+
if (!cols.some((c) => c.name === column)) {
|
|
156
|
+
this.db.exec(`alter table ${table} add column ${column} ${type}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
runTransaction(fn) {
|
|
161
|
+
this.db.exec("begin immediate");
|
|
162
|
+
try {
|
|
163
|
+
const result = fn(this.db);
|
|
164
|
+
this.db.exec("commit");
|
|
165
|
+
return result;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
try {
|
|
168
|
+
this.db.exec("rollback");
|
|
169
|
+
} catch {}
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async migrateLegacyState() {
|
|
175
|
+
const count = this.db.prepare("select count(*) as count from scheduler_jobs").get().count;
|
|
176
|
+
if (count > 0) return;
|
|
177
|
+
const legacy = await readJson(this.legacyFilePath, null);
|
|
178
|
+
if (!legacy?.jobs || Object.keys(legacy.jobs).length === 0) return;
|
|
179
|
+
|
|
180
|
+
const insert = this.db.prepare(`
|
|
181
|
+
insert into scheduler_jobs(job_id, state_json) values (?, ?)
|
|
182
|
+
on conflict(job_id) do update set state_json = excluded.state_json
|
|
183
|
+
`);
|
|
184
|
+
this.db.exec("begin immediate");
|
|
185
|
+
try {
|
|
186
|
+
for (const [jobId, state] of Object.entries(legacy.jobs)) {
|
|
187
|
+
insert.run(jobId, JSON.stringify(state || {}));
|
|
188
|
+
}
|
|
189
|
+
this.db.exec("commit");
|
|
190
|
+
} catch (error) {
|
|
191
|
+
try {
|
|
192
|
+
this.db.exec("rollback");
|
|
193
|
+
} catch {}
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
await removePath(this.legacyFilePath);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async load() {
|
|
201
|
+
const db = await this.ensureReady();
|
|
202
|
+
const rows = db.prepare("select job_id, state_json from scheduler_jobs order by job_id asc").all();
|
|
203
|
+
return {
|
|
204
|
+
jobs: Object.fromEntries(rows.map((row) => [row.job_id, JSON.parse(row.state_json)]))
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async save(state) {
|
|
209
|
+
const db = await this.ensureReady();
|
|
210
|
+
const jobs = state?.jobs || {};
|
|
211
|
+
const insert = db.prepare(`
|
|
212
|
+
insert into scheduler_jobs(job_id, state_json) values (?, ?)
|
|
213
|
+
on conflict(job_id) do update set state_json = excluded.state_json
|
|
214
|
+
`);
|
|
215
|
+
|
|
216
|
+
this.runTransaction(() => {
|
|
217
|
+
db.prepare("delete from scheduler_jobs").run();
|
|
218
|
+
for (const [jobId, jobState] of Object.entries(jobs)) {
|
|
219
|
+
insert.run(jobId, JSON.stringify(jobState || {}));
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
return {
|
|
223
|
+
jobs: clone(jobs)
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async updateJob(jobId, patch) {
|
|
228
|
+
return this.mutateJob(jobId, (current) => ({
|
|
229
|
+
...current,
|
|
230
|
+
...patch
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async getMeta(key, fallback = null) {
|
|
235
|
+
const db = await this.ensureReady();
|
|
236
|
+
const row = db.prepare("select value_json from scheduler_meta where key = ?").get(String(key));
|
|
237
|
+
return row ? JSON.parse(row.value_json) : fallback;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async setMeta(key, value) {
|
|
241
|
+
const db = await this.ensureReady();
|
|
242
|
+
this.runTransaction(() => {
|
|
243
|
+
db.prepare(`
|
|
244
|
+
insert into scheduler_meta(key, value_json) values (?, ?)
|
|
245
|
+
on conflict(key) do update set value_json = excluded.value_json
|
|
246
|
+
`).run(String(key), JSON.stringify(value ?? null));
|
|
247
|
+
});
|
|
248
|
+
return clone(value);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async mutateMeta(key, mutator) {
|
|
252
|
+
const db = await this.ensureReady();
|
|
253
|
+
return this.runTransaction(() => {
|
|
254
|
+
const row = db.prepare("select value_json from scheduler_meta where key = ?").get(String(key));
|
|
255
|
+
const current = row ? JSON.parse(row.value_json) : null;
|
|
256
|
+
const next = clone(mutator(clone(current)) ?? current);
|
|
257
|
+
db.prepare(`
|
|
258
|
+
insert into scheduler_meta(key, value_json) values (?, ?)
|
|
259
|
+
on conflict(key) do update set value_json = excluded.value_json
|
|
260
|
+
`).run(String(key), JSON.stringify(next));
|
|
261
|
+
return next;
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async mutateJob(jobId, mutator) {
|
|
266
|
+
const db = await this.ensureReady();
|
|
267
|
+
return this.runTransaction(() => {
|
|
268
|
+
const row = db.prepare("select state_json from scheduler_jobs where job_id = ?").get(jobId);
|
|
269
|
+
const current = row ? JSON.parse(row.state_json) : {};
|
|
270
|
+
const next = clone(mutator(clone(current), null) || current);
|
|
271
|
+
|
|
272
|
+
db.prepare(`
|
|
273
|
+
insert into scheduler_jobs(job_id, state_json) values (?, ?)
|
|
274
|
+
on conflict(job_id) do update set state_json = excluded.state_json
|
|
275
|
+
`).run(jobId, JSON.stringify(next));
|
|
276
|
+
|
|
277
|
+
return next;
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
getFollowUpCompletion(scopedKey) {
|
|
282
|
+
const row = this.db.prepare(
|
|
283
|
+
"select scoped_key, parent_key, follow_up_id, status, completed_at, ttl_expires_at from follow_up_completions where scoped_key = ?"
|
|
284
|
+
).get(scopedKey);
|
|
285
|
+
return row || null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
setFollowUpCompletion(scopedKey, parentKey, followUpId, status, ttlExpiresAt = null) {
|
|
289
|
+
const now = new Date().toISOString();
|
|
290
|
+
// TTL is computed by caller using: slotInterval * completion_ttl_multiplier
|
|
291
|
+
// Falls back to 24h if no explicit TTL is provided (e.g. in tests)
|
|
292
|
+
const expiresAt = ttlExpiresAt || new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
|
|
293
|
+
this.db.prepare(`
|
|
294
|
+
insert into follow_up_completions (scoped_key, parent_key, follow_up_id, status, completed_at, ttl_expires_at)
|
|
295
|
+
values (?, ?, ?, ?, ?, ?)
|
|
296
|
+
on conflict(scoped_key) do update set status = excluded.status, completed_at = excluded.completed_at, ttl_expires_at = excluded.ttl_expires_at
|
|
297
|
+
`).run(scopedKey, parentKey, followUpId, status, now, expiresAt);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
clearFollowUpCompletion(scopedKey) {
|
|
301
|
+
this.db.prepare("delete from follow_up_completions where scoped_key = ?").run(scopedKey);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
purgeExpiredFollowUps(nowIso) {
|
|
305
|
+
this.db.prepare("delete from follow_up_completions where ttl_expires_at < ?").run(nowIso);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
enqueueInteractiveJob({ jobId, agentId, input, source, chatId, imageRefs = null, sourceType = 'human', replySessionKey = null, sourceAgentId = null }) {
|
|
309
|
+
const now = new Date().toISOString();
|
|
310
|
+
this.db.prepare(`
|
|
311
|
+
insert or ignore into interactive_jobs (job_id, agent_id, input, source, chat_id, status, created_at, source_type, reply_session_key, source_agent_id, image_refs)
|
|
312
|
+
values (?, ?, ?, ?, ?, 'queued', ?, ?, ?, ?, ?)
|
|
313
|
+
`).run(jobId, agentId, input, source, chatId, now, sourceType, replySessionKey, sourceAgentId, imageRefs);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
getInteractiveJob(jobId) {
|
|
317
|
+
const row = this.db.prepare(
|
|
318
|
+
"select job_id, agent_id, input, source, chat_id, status, created_at, triggered_by, completion_target, trigger_correlation_id, completion_pinged_at, completion_ping_attempts, source_type, reply_session_key, source_agent_id, image_refs from interactive_jobs where job_id = ?"
|
|
319
|
+
).get(jobId);
|
|
320
|
+
return row || null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
interactiveJobExists(jobId) {
|
|
324
|
+
const row = this.db.prepare("select 1 from interactive_jobs where job_id = ?").get(jobId);
|
|
325
|
+
return Boolean(row);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
drainQueuedInteractiveJobs() {
|
|
329
|
+
return this.runTransaction(() => {
|
|
330
|
+
const rows = this.db.prepare(
|
|
331
|
+
"select job_id, agent_id, input, source, chat_id, status, created_at, triggered_by, completion_target, trigger_correlation_id, source_type, reply_session_key, source_agent_id, image_refs from interactive_jobs where status = 'queued' order by created_at asc"
|
|
332
|
+
).all();
|
|
333
|
+
if (rows.length > 0) {
|
|
334
|
+
const ids = rows.map((r) => r.job_id);
|
|
335
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
336
|
+
this.db.prepare(
|
|
337
|
+
`update interactive_jobs set status = 'running' where job_id in (${placeholders})`
|
|
338
|
+
).run(...ids);
|
|
339
|
+
}
|
|
340
|
+
return rows;
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
updateInteractiveJobStatus(jobId, status) {
|
|
345
|
+
this.db.prepare("update interactive_jobs set status = ? where job_id = ?").run(status, jobId);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
enqueueDispatchJob({ jobId, agentId, input, source, chatId, triggeredBy, completionTarget, triggerCorrelationId }) {
|
|
349
|
+
const now = new Date().toISOString();
|
|
350
|
+
this.db.prepare(`
|
|
351
|
+
insert or ignore into interactive_jobs (job_id, agent_id, input, source, chat_id, status, created_at, triggered_by, completion_target, trigger_correlation_id, completion_ping_attempts)
|
|
352
|
+
values (?, ?, ?, ?, ?, 'queued', ?, ?, ?, ?, 0)
|
|
353
|
+
`).run(jobId, agentId, input, source, chatId || "", now, triggeredBy || null, completionTarget || null, triggerCorrelationId || null);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
getCompletedDispatchJobs() {
|
|
357
|
+
return this.db.prepare(
|
|
358
|
+
"select job_id, agent_id, input, source, chat_id, status, created_at, triggered_by, completion_target, trigger_correlation_id, completion_pinged_at, completion_ping_attempts from interactive_jobs where status in ('succeeded', 'failed') and completion_target is not null and completion_pinged_at is null"
|
|
359
|
+
).all();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
markCompletionPinged(jobId) {
|
|
363
|
+
this.db.prepare("update interactive_jobs set completion_pinged_at = ? where job_id = ?").run(new Date().toISOString(), jobId);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
incrementCompletionPingAttempts(jobId) {
|
|
367
|
+
this.db.prepare("update interactive_jobs set completion_ping_attempts = completion_ping_attempts + 1 where job_id = ?").run(jobId);
|
|
368
|
+
const job = this.getInteractiveJob(jobId);
|
|
369
|
+
if (job && job.completion_ping_attempts >= 3) {
|
|
370
|
+
this.db.prepare("update interactive_jobs set completion_pinged_at = 'FAILED' where job_id = ?").run(jobId);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
getRecentAgentJobs(limit = 5) {
|
|
375
|
+
return this.db.prepare(
|
|
376
|
+
"select job_id, agent_id, source_agent_id, input, status, created_at from interactive_jobs where source_type = 'agent' order by created_at desc limit ?"
|
|
377
|
+
).all(limit);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
getChatSession(chatId) {
|
|
381
|
+
const row = this.db.prepare(
|
|
382
|
+
"select chat_id, agent_id, previous_agent_id, bound_at, bound_by, conversation_context, model_override, focus_mode, think_mode from chat_sessions where chat_id = ?"
|
|
383
|
+
).get(chatId);
|
|
384
|
+
return row || null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
getRunningInteractiveJobCount() {
|
|
388
|
+
const row = this.db.prepare("select count(*) as count from interactive_jobs where status = 'running'").get();
|
|
389
|
+
return row.count;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
bindChatSession({ chatId, agentId, boundBy }) {
|
|
393
|
+
const now = new Date().toISOString();
|
|
394
|
+
const existing = this.getChatSession(chatId);
|
|
395
|
+
if (existing) {
|
|
396
|
+
this.db.prepare(`
|
|
397
|
+
update chat_sessions
|
|
398
|
+
set agent_id = ?, previous_agent_id = ?, bound_at = ?, bound_by = ?, conversation_context = null
|
|
399
|
+
where chat_id = ?
|
|
400
|
+
`).run(agentId, existing.agent_id, now, boundBy, chatId);
|
|
401
|
+
} else {
|
|
402
|
+
this.db.prepare(`
|
|
403
|
+
insert into chat_sessions (chat_id, agent_id, previous_agent_id, bound_at, bound_by, conversation_context)
|
|
404
|
+
values (?, ?, null, ?, ?, null)
|
|
405
|
+
`).run(chatId, agentId, now, boundBy);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
updateConversationContext(chatId, contextJson) {
|
|
410
|
+
this.db.prepare("update chat_sessions set conversation_context = ? where chat_id = ?").run(contextJson, chatId);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// --- Tool result cache ---
|
|
414
|
+
|
|
415
|
+
getCachedToolResult(callHash) {
|
|
416
|
+
return this.db.prepare(
|
|
417
|
+
"select call_hash, tool_id, brief, raw, index_terms, created_at from tool_results where call_hash = ?"
|
|
418
|
+
).get(callHash) || null;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
cacheToolResult({ callHash, toolId, brief, raw, indexTerms }) {
|
|
422
|
+
this.db.prepare(`
|
|
423
|
+
insert into tool_results (call_hash, tool_id, brief, raw, index_terms)
|
|
424
|
+
values (?, ?, ?, ?, ?)
|
|
425
|
+
on conflict(call_hash) do update set brief = excluded.brief, raw = excluded.raw, index_terms = excluded.index_terms
|
|
426
|
+
`).run(callHash, toolId, brief, raw, JSON.stringify(indexTerms || []));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
close() {
|
|
430
|
+
if (!this.db) return;
|
|
431
|
+
this.db.close();
|
|
432
|
+
this.db = null;
|
|
433
|
+
}
|
|
434
|
+
}
|