negotium 0.2.18 → 0.2.20
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/dist/agent-helpers.js +367 -313
- package/dist/agent-helpers.js.map +18 -18
- package/dist/background-bash.js +3 -2
- package/dist/background-bash.js.map +3 -3
- package/dist/browser-runtime.js +60 -36
- package/dist/browser-runtime.js.map +6 -6
- package/dist/{chunk-fc58cqry.js → chunk-g2kqfdwm.js} +154 -24
- package/dist/{chunk-fc58cqry.js.map → chunk-g2kqfdwm.js.map} +7 -6
- package/dist/hosted-agent.js +183 -53
- package/dist/hosted-agent.js.map +8 -7
- package/dist/main.js +1395 -626
- package/dist/main.js.map +27 -26
- package/dist/mcp-factories.js +1204 -624
- package/dist/mcp-factories.js.map +21 -20
- package/dist/prompts.js +3 -2
- package/dist/prompts.js.map +3 -3
- package/dist/query-runtime.js +47 -26
- package/dist/query-runtime.js.map +4 -4
- package/dist/registry.js +3 -3
- package/dist/registry.js.map +2 -2
- package/dist/rollout.js +1 -1
- package/dist/runtime/src/agents/archiver.ts +4 -3
- package/dist/runtime/src/application/switch-topic-access-mode.ts +81 -8
- package/dist/runtime/src/bus.ts +19 -2
- package/dist/runtime/src/mcp/canonical-proxy-server.ts +13 -14
- package/dist/runtime/src/mcp/session-comm/runtime.ts +16 -0
- package/dist/runtime/src/mcp/wiki-search-index.ts +359 -0
- package/dist/runtime/src/mcp/wiki-server.ts +559 -98
- package/dist/runtime/src/platform/config.ts +13 -1
- package/dist/runtime/src/platform/mcp-config.ts +1 -1
- package/dist/runtime/src/prompts/agents/wiki-archiver.md +62 -47
- package/dist/runtime/src/runtime/ask-callbacks.ts +19 -11
- package/dist/runtime/src/runtime/background-sessions.ts +9 -7
- package/dist/runtime/src/runtime/visual-store.ts +31 -23
- package/dist/runtime/src/storage/api-topics.ts +26 -0
- package/dist/runtime/src/storage/browser-profiles.ts +16 -8
- package/dist/runtime/src/storage/runtime-events.ts +30 -20
- package/dist/runtime/src/storage/runtime-gateway-submissions.ts +24 -16
- package/dist/runtime/src/storage/runtime-turn-requests.ts +9 -1
- package/dist/runtime/src/storage/self-schedules.ts +32 -24
- package/dist/runtime/src/storage/storage-host.ts +105 -43
- package/dist/runtime/src/storage/vault.ts +12 -3
- package/dist/runtime/src/version.ts +1 -1
- package/dist/runtime-helpers.js +47 -26
- package/dist/runtime-helpers.js.map +4 -4
- package/dist/storage.js +76 -41
- package/dist/storage.js.map +4 -4
- package/dist/types/packages/core/src/agents/archiver.d.ts +1 -1
- package/dist/types/packages/core/src/mcp/wiki-search-index.d.ts +59 -0
- package/dist/types/packages/core/src/storage/api-topics.d.ts +15 -0
- package/dist/types/packages/core/src/storage/storage-host.d.ts +10 -0
- package/dist/types/packages/core/src/version.d.ts +1 -1
- package/dist/vault.js +140 -10
- package/dist/vault.js.map +6 -5
- package/package.json +1 -1
|
@@ -114,11 +114,23 @@ function versionAtLeast(actualVersion: string, minimumVersion: string): boolean
|
|
|
114
114
|
return true;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Ceiling for the `--version` probe, not a latency expectation.
|
|
119
|
+
*
|
|
120
|
+
* The probe exists so a wedged binary cannot hang startup; a healthy one
|
|
121
|
+
* answers in milliseconds. At 2s it also failed whenever the machine was merely
|
|
122
|
+
* busy — process startup under a loaded test suite regularly exceeds that —
|
|
123
|
+
* which turned a correctness check into a load measurement and made
|
|
124
|
+
* `resolveBrowserRsBin` intermittently report a perfectly good binary as
|
|
125
|
+
* unusable. Generous enough that only a genuinely stuck process trips it.
|
|
126
|
+
*/
|
|
127
|
+
const BROWSER_RS_VERSION_PROBE_TIMEOUT_MS = 15_000;
|
|
128
|
+
|
|
117
129
|
function browserRsMeetsMinimumVersion(candidate: string): boolean {
|
|
118
130
|
try {
|
|
119
131
|
const output = execFileSync(candidate, ["--version"], {
|
|
120
132
|
encoding: "utf8",
|
|
121
|
-
timeout:
|
|
133
|
+
timeout: BROWSER_RS_VERSION_PROBE_TIMEOUT_MS,
|
|
122
134
|
stdio: ["ignore", "pipe", "ignore"],
|
|
123
135
|
}).trim();
|
|
124
136
|
const match = output.match(/^browser-rs (\d+)\.(\d+)\.(\d+)$/);
|
|
@@ -474,7 +474,7 @@ const MCP_CATALOG: Record<string, RuntimeMcpCatalogEntry> = {
|
|
|
474
474
|
}
|
|
475
475
|
const args = [`--user-id=${userId}`];
|
|
476
476
|
if (topicId) args.push(`--current-topic-id=${topicId}`);
|
|
477
|
-
// Prefer the REST topic id so
|
|
477
|
+
// Prefer the REST topic id so wiki_write writes filenames and SQLite
|
|
478
478
|
// briefs keyed the same way the topic wiki API reads them.
|
|
479
479
|
const resolvedWikiTopicId =
|
|
480
480
|
wikiTopicId ?? topicId ?? (session !== "dm" ? session : undefined);
|
|
@@ -1,32 +1,38 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wiki-archiver
|
|
3
3
|
type: programmatic
|
|
4
|
-
description: Agent that extracts key content from session logs and updates wiki/summaries/, wiki/articles/, wiki/topic/, wiki/skills/, wiki/article-index.md, wiki/topic-index.md, and wiki/skill-index.md
|
|
4
|
+
description: Agent that extracts key content from session logs and updates wiki/summaries/, wiki/articles/, wiki/topic/, wiki/skills/, wiki/summary-index.md, wiki/article-index.md, wiki/topic-index.md, and wiki/skill-index.md
|
|
5
5
|
model: deepseek-pro
|
|
6
6
|
tools:
|
|
7
7
|
- Read
|
|
8
|
-
- Write
|
|
9
8
|
- Glob
|
|
10
|
-
-
|
|
11
|
-
-
|
|
9
|
+
- mcp__wiki__wiki_write
|
|
10
|
+
- mcp__wiki__wiki_read
|
|
12
11
|
- mcp__wiki__skill_save
|
|
13
12
|
- mcp__wiki__skill_query
|
|
14
13
|
- mcp__wiki__index_upsert
|
|
15
14
|
---
|
|
16
15
|
|
|
17
16
|
You are a wiki archiver agent. Extract key information from session logs and save it into the
|
|
18
|
-
wiki knowledge base.
|
|
19
|
-
|
|
17
|
+
wiki knowledge base. Every document has exactly one catalog, and **`wiki_write` writes the document
|
|
18
|
+
and its catalog row in the same call** — so you cannot leave a document unindexed:
|
|
20
19
|
|
|
21
20
|
```
|
|
22
21
|
wiki/
|
|
23
|
-
summaries/<date>-<topic>.md <- session summaries (write-once)
|
|
24
|
-
articles/<slug>.md <- curated concept pages (mergeable)
|
|
25
|
-
topic/<topic>.md <- accumulated persona brief
|
|
26
|
-
|
|
27
|
-
topic-index.md <- catalog: topic briefs only
|
|
22
|
+
summaries/<date>-<topic>.md <- session summaries (write-once) -> summary-index.md
|
|
23
|
+
articles/<slug>.md <- curated concept pages (mergeable) -> article-index.md
|
|
24
|
+
topic/<topic>.md <- accumulated persona brief -> topic-index.md
|
|
25
|
+
.wiki-search-index.sqlite <- derived body-search cache (never edit or archive)
|
|
28
26
|
```
|
|
29
27
|
|
|
28
|
+
`wiki_write` also feeds the derived cache, so a document is searchable by its body the moment it is
|
|
29
|
+
written. The cache is disposable: if a call reports that it was not updated, the catalogs still
|
|
30
|
+
work and `wiki_reindex` refills it.
|
|
31
|
+
|
|
32
|
+
**You have no file-writing tool.** `wiki_write` is the only way to create or update a wiki
|
|
33
|
+
document; `description` is mandatory because the catalog is what makes retrieval work. Use `Read`
|
|
34
|
+
and `Glob` to inspect existing files before merging.
|
|
35
|
+
|
|
30
36
|
## Output language
|
|
31
37
|
|
|
32
38
|
Write all human-readable prose (summaries, the persona brief, article bodies, skill descriptions, your final report) in the `output_language:` given in the prompt; default to **English** if absent. `output_language` tracks the user's mother tongue (e.g. `English`, `Korean`, `한국어`, `Japanese`).
|
|
@@ -40,7 +46,7 @@ Keep structural tokens in English regardless: frontmatter keys, `type:` values,
|
|
|
40
46
|
- Read the first chunk with `Read(archive_path, offset: 1, limit: 2000)`.
|
|
41
47
|
- If the result ends with a truncation notice like `lines X-Y of N`, call the next chunk with `offset = Y + 1`. Stop when there is no notice or `Y == N`.
|
|
42
48
|
- For each chunk, **only accumulate** key items (decisions / facts / tools / files / patterns …) into a short in-memory bullet buffer.
|
|
43
|
-
- **Do not call
|
|
49
|
+
- **Do not call `wiki_write` per chunk.** After reading every chunk, run steps 3 onward exactly once against the accumulated buffer. Writing per chunk splits one session across multiple summary files.
|
|
44
50
|
- If the buffer grows large (e.g. 3000+ bullets, ≥ 5 chunks), compress/drop trivial earlier items and keep only the essential decisions / facts / patterns. Preserving cross-references through the last chunk takes priority.
|
|
45
51
|
|
|
46
52
|
2. **Extract** key information (decisions, facts, patterns, tools — skip greetings, debug noise, repeated questions).
|
|
@@ -55,22 +61,28 @@ Keep structural tokens in English regardless: frontmatter keys, `type:` values,
|
|
|
55
61
|
- Use `canonical_topic` for every summary, brief, and topic-index write below. This keeps a
|
|
56
62
|
differently named room's summary and persona update in one canonical namespace.
|
|
57
63
|
- If `sent_files:` is in the prompt, include those entries under `## Files Sent`
|
|
58
|
-
- If the session yielded **no extractable substance** (pure debug, ≤2 short exchanges, all greeting), save only a single-line immutable summary via `
|
|
64
|
+
- If the session yielded **no extractable substance** (pure debug, ≤2 short exchanges, all greeting), save only a single-line immutable summary via `wiki_write(kind="summary", ...)`, then STOP. Do not touch the persona brief or articles.
|
|
59
65
|
|
|
60
66
|
> **Ordering principle:** pipeline is **archive → summary → brief**. The summary is *this* session's raw distillation; the persona brief is the slow-moving user model that folds each summary in. Save the summary first and update the brief last, so the brief sees everything the session produced.
|
|
61
67
|
|
|
62
|
-
3. **Save the immutable session summary** via
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
3. **Save the immutable session summary** via
|
|
69
|
+
`mcp__wiki__wiki_write(kind="summary", topic=canonical_topic, content, description)`.
|
|
70
|
+
The MCP handles file naming + dedup, writes the `summary-index.md` row from your `description`,
|
|
71
|
+
and records `latest_summary_md` + `summary_date` in SQLite (it does **not** touch the brief —
|
|
72
|
+
that is step 5). Use the **summary format** below.
|
|
66
73
|
4. **Update articles** — for each genuinely reusable concept/decision/tool/pattern:
|
|
67
|
-
- Glob existing articles: `Glob(wiki/articles/*.md)`
|
|
68
|
-
|
|
74
|
+
- Glob existing articles: `Glob(wiki/articles/*.md)`, and `Read(wiki/article-index.md)` once to
|
|
75
|
+
see the existing `## ...` sections.
|
|
76
|
+
- If a matching article exists (by slug or topic): `Read` it, merge, then write the merged body.
|
|
69
77
|
- **Preserve frontmatter `date:` (first-seen) and `status:`.** Only refresh `updated:`.
|
|
70
78
|
- **Preserve manually written body sections.** Only append/update what the session adds.
|
|
71
|
-
-
|
|
79
|
+
- Write with
|
|
80
|
+
`wiki_write(kind="article", slug=<kebab-slug>, section=<H2 without "## ">, content, description)`.
|
|
81
|
+
`section` is required: pick the closest existing header, or a short new domain title in
|
|
82
|
+
`output_language` (e.g. `Business / Career`, `Physical AI / Robotics`).
|
|
72
83
|
- Skip session-specific noise. If nothing qualifies, no articles change — that's fine.
|
|
73
|
-
5. **Update the accumulated persona brief last** via
|
|
84
|
+
5. **Update the accumulated persona brief last** via
|
|
85
|
+
`mcp__wiki__wiki_write(kind="topic", topic=canonical_topic, content, description)`.
|
|
74
86
|
This is the culmination of the run — the brief is not a worklog, it is the wiki's evolving
|
|
75
87
|
**persona/user-model** for this topic: who the user is, how they want to be served, and where
|
|
76
88
|
things stand. It is injected verbatim at the next session's start, so write it as durable memory,
|
|
@@ -81,31 +93,23 @@ Keep structural tokens in English regardless: frontmatter keys, `type:` values,
|
|
|
81
93
|
Decisions into the **persona layer** (accumulate — the user-model is slow-moving). Refresh only
|
|
82
94
|
the volatile layers (`## Recent Work`, `## Current State`) from this session. Preserve still-valid
|
|
83
95
|
prior persona traits; remove a trait only when the new session explicitly supersedes it.
|
|
84
|
-
- Keep one canonical file per topic memory key — `
|
|
85
|
-
Never add a UUID or room id. Write a fresh compact
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
**For each new or updated article** (from step 4):
|
|
90
|
-
- First, scan `wiki/article-index.md` once with `Read` to see existing `## ...` headers, then pick the closest matching section. If no section fits, choose a short domain title in `output_language` (e.g. `Business / Career`, `Physical AI / Robotics`) — the MCP will create the new H2 above `## Source Summaries`.
|
|
91
|
-
- Call: `index_upsert(slug=<article-slug>, description=<one-line>, kind="article", section=<chosen-header-without-"## ">)`
|
|
92
|
-
- The MCP preserves the original `created` date on update — do not pass it.
|
|
93
|
-
|
|
94
|
-
**For the new session summary** (from step 3):
|
|
95
|
-
- Call: `index_upsert(slug=<summary-slug>, description=<one-line>, kind="summary")`
|
|
96
|
-
- Goes under `## Source Summaries` automatically.
|
|
96
|
+
- Keep one canonical file per topic memory key — `wiki_write` handles the path, the
|
|
97
|
+
`topic-index.md` row, and the SQLite mirror. Never add a UUID or room id. Write a fresh compact
|
|
98
|
+
brief using the **brief format** below, and let `description` be a one-line state of recent work.
|
|
99
|
+
6. **There is no separate indexing step.** Steps 3–5 already wrote every catalog row, so the
|
|
100
|
+
catalogs cannot drift from the documents. Two rules remain:
|
|
97
101
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
-
|
|
102
|
+
- **`description` carries the retrieval weight.** Write it as the one line you would want to see
|
|
103
|
+
when searching six months from now: what changed and why it matters, not "session summary".
|
|
104
|
+
- **`index_upsert` is for corrections only.** It can refine the description or section of an entry
|
|
105
|
+
that already exists, and it refuses to create a row for a document that was never written.
|
|
106
|
+
**Never delete entries** — pruning is a `wiki lint` concern.
|
|
101
107
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
## Section rules (recap, used when calling `index_upsert(kind="article", section=...)`)
|
|
108
|
+
## Section rules (recap, used for `wiki_write(kind="article", section=...)`)
|
|
105
109
|
|
|
106
110
|
1. Scan existing `## ...` headers in `article-index.md` first. Pick the closest match.
|
|
107
|
-
2. If none fits, pass a short title in `output_language` for the article's domain — the MCP
|
|
108
|
-
|
|
111
|
+
2. If none fits, pass a short title in `output_language` for the article's domain — the MCP appends
|
|
112
|
+
the new H2. Do not rely on a particular ordering of sections.
|
|
109
113
|
|
|
110
114
|
## wiki/summaries/ entry format
|
|
111
115
|
|
|
@@ -226,9 +230,21 @@ _Last updated: {YYYY-MM-DD}_
|
|
|
226
230
|
|
|
227
231
|
## {Domain section 2}
|
|
228
232
|
- ...
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## summary-index.md structure (skeleton)
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
---
|
|
239
|
+
{frontmatter — preserve as-is}
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
# Wiki Index — Summaries
|
|
243
|
+
|
|
244
|
+
_Last updated: {YYYY-MM-DD}_
|
|
229
245
|
|
|
230
|
-
##
|
|
231
|
-
- [[2026-05-08-topic]] — desc (2026-05-08)
|
|
246
|
+
## Session Summaries
|
|
247
|
+
- [[summaries/2026-05-08-topic]] — desc (2026-05-08)
|
|
232
248
|
- ...
|
|
233
249
|
```
|
|
234
250
|
|
|
@@ -302,7 +318,6 @@ description: "keyword1, keyword2, likely trigger phrases the user would type —
|
|
|
302
318
|
Summarize in `output_language` (report in pipeline order — summary → articles → brief):
|
|
303
319
|
- 📝 summary: saved `wiki/summaries/<filename>`
|
|
304
320
|
- 📄 articles: created/updated N pages (slugs)
|
|
305
|
-
- 🗂 brief: `wiki/topic/<topic>.md` persona merge
|
|
306
|
-
- 📇 article-index
|
|
307
|
-
- 📇 topic-index: 1 `index_upsert` call (this topic)
|
|
321
|
+
- 🗂 brief: `wiki/topic/<topic>.md` persona merge
|
|
322
|
+
- 📇 catalogs: summary-index 1 row, article-index N rows, topic-index 1 row (all written by `wiki_write`)
|
|
308
323
|
- 🛠 skill: created/updated `wiki/skills/<name>/skill.md` (or 'none')
|
|
@@ -12,18 +12,26 @@
|
|
|
12
12
|
import { deliverPeerReply, type RemoteReplyRoute } from "#mcp/session-comm/peer-forward";
|
|
13
13
|
import { db } from "#storage/forum-db";
|
|
14
14
|
import { PENDING_ASK_TTL_MS, type PendingAskUserId } from "#storage/session-asks";
|
|
15
|
+
import { registerStorageSchemaInitializer } from "#storage/storage-host";
|
|
15
16
|
|
|
16
|
-
db.exec
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
// Registered rather than executed at import time. A bare top-level `db.exec`
|
|
18
|
+
// resolves the storage connection the moment this module is imported, which for
|
|
19
|
+
// an embedding host is before `configureStorageHost()` has run — Negotium then
|
|
20
|
+
// opens its own database in the default state directory, caches it, and every
|
|
21
|
+
// later caller silently keeps using it instead of the host's.
|
|
22
|
+
registerStorageSchemaInitializer((database) => {
|
|
23
|
+
database.exec(`
|
|
24
|
+
CREATE TABLE IF NOT EXISTS remote_ask_callbacks (
|
|
25
|
+
target_query_id TEXT PRIMARY KEY,
|
|
26
|
+
request_id TEXT NOT NULL,
|
|
27
|
+
node_name TEXT NOT NULL,
|
|
28
|
+
node_cell_id TEXT NOT NULL,
|
|
29
|
+
topic_id TEXT NOT NULL,
|
|
30
|
+
user_id TEXT NOT NULL,
|
|
31
|
+
created_at INTEGER NOT NULL
|
|
32
|
+
)
|
|
33
|
+
`);
|
|
34
|
+
});
|
|
27
35
|
|
|
28
36
|
interface PendingAskIdentity {
|
|
29
37
|
userId: PendingAskUserId;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { listActiveMemoryArchiverSessions } from "#agents/archiver";
|
|
2
|
+
import { summarizeDisplayText } from "#agents/tool-format";
|
|
2
3
|
import { getTopic } from "#storage/api-topics";
|
|
3
4
|
import { listRecentRuntimeEventsForTopic } from "#storage/runtime-events";
|
|
4
5
|
import { listRuntimeTurnLeases } from "#storage/runtime-leases";
|
|
@@ -127,7 +128,7 @@ function detailedToolStep(payload: Record<string, unknown>): string {
|
|
|
127
128
|
|
|
128
129
|
if (shortName === "ask_session" || shortName === "tell_session") {
|
|
129
130
|
const target = typeof input.to === "string" && input.to.trim() ? input.to.trim() : "session";
|
|
130
|
-
const msg = typeof input.message === "string" ? text(input.message) : "";
|
|
131
|
+
const msg = typeof input.message === "string" ? summarizeDisplayText(text(input.message)) : "";
|
|
131
132
|
const action = shortName === "ask_session" ? "Ask" : "Tell";
|
|
132
133
|
return `${action} ${target}${msg ? ` — ${msg}` : ""}`;
|
|
133
134
|
}
|
|
@@ -150,20 +151,21 @@ function detailedToolStep(payload: Record<string, unknown>): string {
|
|
|
150
151
|
return `${action} file`;
|
|
151
152
|
}
|
|
152
153
|
if (shortName === "grep" || shortName === "glob") {
|
|
153
|
-
const pattern =
|
|
154
|
+
const pattern =
|
|
155
|
+
typeof input.pattern === "string" ? summarizeDisplayText(text(input.pattern)) : "";
|
|
154
156
|
const action = shortName === "grep" ? "Grep" : "Glob";
|
|
155
|
-
if (pattern) return `${action} '${
|
|
157
|
+
if (pattern) return `${action} '${pattern}'${path ? ` in ${path}` : ""}`;
|
|
156
158
|
if (path) return `${action} ${path}`;
|
|
157
159
|
}
|
|
158
160
|
if (shortName === "bash") {
|
|
159
|
-
const cmd = typeof input.command === "string" ? text(input.command) : "";
|
|
161
|
+
const cmd = typeof input.command === "string" ? summarizeDisplayText(text(input.command)) : "";
|
|
160
162
|
return cmd ? `Bash: ${cmd}` : "Bash";
|
|
161
163
|
}
|
|
162
164
|
if (shortName === "webfetch" || shortName === "url_fetch") {
|
|
163
|
-
const url = typeof input.url === "string" ? text(input.url) : "";
|
|
165
|
+
const url = typeof input.url === "string" ? summarizeDisplayText(text(input.url)) : "";
|
|
164
166
|
return url ? `Fetch ${url}` : "WebFetch";
|
|
165
167
|
}
|
|
166
|
-
return label;
|
|
168
|
+
return summarizeDisplayText(label);
|
|
167
169
|
}
|
|
168
170
|
|
|
169
171
|
export function backgroundSessionProgress(
|
|
@@ -190,7 +192,7 @@ export function backgroundSessionProgress(
|
|
|
190
192
|
else if (kind === "tool_output") {
|
|
191
193
|
const toolName = toolNames.get(text(payload.toolUseId)) ?? "Tool";
|
|
192
194
|
step = payload.isError
|
|
193
|
-
? `${toolName} failed: ${text(payload.content) || "unknown error"}`
|
|
195
|
+
? `${toolName} failed: ${summarizeDisplayText(text(payload.content)) || "unknown error"}`
|
|
194
196
|
: `${toolName} result · ${byteSize(payload.content)}`;
|
|
195
197
|
} else if (kind === "ai_done") {
|
|
196
198
|
const usage =
|
|
@@ -10,34 +10,42 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { randomUUID } from "node:crypto";
|
|
12
12
|
import { db } from "#storage/forum-db";
|
|
13
|
+
import { registerStorageSchemaInitializer } from "#storage/storage-host";
|
|
13
14
|
|
|
14
15
|
const MAX_HISTORY = 10;
|
|
15
16
|
const MAX_VISUAL_TITLE_CHARS = 180;
|
|
16
17
|
export const VISUAL_MEDIA_URL_PLACEHOLDER = "__NEGOTIUM_VISUAL_MEDIA_URL__";
|
|
17
18
|
|
|
18
|
-
db.exec
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
19
|
+
// Registered rather than executed at import time. A bare top-level `db.exec`
|
|
20
|
+
// resolves the storage connection the moment this module is imported, which for
|
|
21
|
+
// an embedding host is before `configureStorageHost()` has run — Negotium then
|
|
22
|
+
// opens its own database in the default state directory, caches it, and every
|
|
23
|
+
// later caller silently keeps using it instead of the host's.
|
|
24
|
+
registerStorageSchemaInitializer((database) => {
|
|
25
|
+
database.exec(`
|
|
26
|
+
CREATE TABLE IF NOT EXISTS api_topic_visuals (
|
|
27
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
28
|
+
topic_id TEXT NOT NULL,
|
|
29
|
+
html TEXT NOT NULL,
|
|
30
|
+
title TEXT,
|
|
31
|
+
created_at INTEGER NOT NULL,
|
|
32
|
+
kind TEXT NOT NULL DEFAULT 'html',
|
|
33
|
+
source TEXT,
|
|
34
|
+
file_id TEXT,
|
|
35
|
+
mime_type TEXT,
|
|
36
|
+
media_token TEXT
|
|
37
|
+
);
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_api_topic_visuals_topic
|
|
39
|
+
ON api_topic_visuals(topic_id, created_at DESC);
|
|
40
|
+
|
|
41
|
+
CREATE TABLE IF NOT EXISTS api_topic_visual_views (
|
|
42
|
+
topic_id TEXT NOT NULL,
|
|
43
|
+
user_id TEXT NOT NULL,
|
|
44
|
+
visual_id INTEGER NOT NULL,
|
|
45
|
+
PRIMARY KEY (topic_id, user_id)
|
|
46
|
+
);
|
|
47
|
+
`);
|
|
48
|
+
});
|
|
41
49
|
|
|
42
50
|
for (const statement of [
|
|
43
51
|
"ALTER TABLE api_topic_visuals ADD COLUMN kind TEXT NOT NULL DEFAULT 'html'",
|
|
@@ -705,6 +705,32 @@ export function upsertTopic(t: TopicDto): void {
|
|
|
705
705
|
})();
|
|
706
706
|
}
|
|
707
707
|
|
|
708
|
+
/**
|
|
709
|
+
* Apply one access mode to a set of topics as a single all-or-nothing write.
|
|
710
|
+
*
|
|
711
|
+
* A partial write is not a cosmetic glitch here: a public parent left with
|
|
712
|
+
* private subagent children (or the reverse) is exactly the half-exposed state
|
|
713
|
+
* the access-mode cascade exists to prevent, so every row commits or none do.
|
|
714
|
+
*
|
|
715
|
+
* Deliberately a narrow UPDATE instead of a loop over {@link upsertTopic}.
|
|
716
|
+
* That helper opens its own transaction, and the node:sqlite shim in
|
|
717
|
+
* `sqlite.ts` emulates transactions with bare BEGIN/COMMIT rather than
|
|
718
|
+
* savepoints — nesting one inside an outer transaction would fail outright.
|
|
719
|
+
* It also rewrites participants and browser-profile ownership, none of which
|
|
720
|
+
* an access-mode change should touch.
|
|
721
|
+
*/
|
|
722
|
+
export function setTopicAccessModes(
|
|
723
|
+
topicIds: readonly string[],
|
|
724
|
+
accessMode: TopicAccessMode,
|
|
725
|
+
): void {
|
|
726
|
+
if (topicIds.length === 0) return;
|
|
727
|
+
const normalized = normalizeTopicAccessMode(accessMode);
|
|
728
|
+
db.transaction(() => {
|
|
729
|
+
const update = db.query("UPDATE api_topics SET access_mode = ? WHERE id = ?");
|
|
730
|
+
for (const topicId of topicIds) update.run(normalized, topicId);
|
|
731
|
+
})();
|
|
732
|
+
}
|
|
733
|
+
|
|
708
734
|
export function listTopics(): TopicDto[] {
|
|
709
735
|
const rows = db
|
|
710
736
|
.query("SELECT * FROM api_topics ORDER BY last_message_at DESC")
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { db } from "#storage/forum-db";
|
|
2
|
+
import { registerStorageSchemaInitializer } from "#storage/storage-host";
|
|
2
3
|
|
|
3
4
|
export const DEFAULT_BROWSER_PROFILE = "default";
|
|
4
5
|
|
|
@@ -17,14 +18,21 @@ export function normalizeBrowserProfileName(name: string): string {
|
|
|
17
18
|
return value;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
db.exec
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
// Registered rather than executed at import time. A bare top-level `db.exec`
|
|
22
|
+
// resolves the storage connection the moment this module is imported, which for
|
|
23
|
+
// an embedding host is before `configureStorageHost()` has run — Negotium then
|
|
24
|
+
// opens its own database in the default state directory, caches it, and every
|
|
25
|
+
// later caller silently keeps using it instead of the host's.
|
|
26
|
+
registerStorageSchemaInitializer((database) => {
|
|
27
|
+
database.exec(`
|
|
28
|
+
CREATE TABLE IF NOT EXISTS browser_profiles (
|
|
29
|
+
owner_id TEXT NOT NULL,
|
|
30
|
+
name TEXT NOT NULL,
|
|
31
|
+
created_at TEXT NOT NULL,
|
|
32
|
+
PRIMARY KEY (owner_id, name)
|
|
33
|
+
)
|
|
34
|
+
`);
|
|
35
|
+
});
|
|
28
36
|
|
|
29
37
|
export function getBrowserProfileOwner(topicId: string, fallbackUserId: string): string {
|
|
30
38
|
return (
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { db } from "#storage/forum-db";
|
|
2
|
+
import { registerStorageSchemaInitializer } from "#storage/storage-host";
|
|
2
3
|
|
|
3
4
|
export const RUNTIME_EVENT_TYPES = [
|
|
4
5
|
"message",
|
|
@@ -29,26 +30,35 @@ interface RuntimeEventRow {
|
|
|
29
30
|
created_at: string;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
db.exec
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
33
|
+
// Registered rather than executed at import time. A bare top-level `db.exec`
|
|
34
|
+
// resolves the storage connection the moment this module is imported, which for
|
|
35
|
+
// an embedding host is before `configureStorageHost()` has run — Negotium then
|
|
36
|
+
// opens its own database in the default state directory, caches it, and every
|
|
37
|
+
// later caller silently keeps using it instead of the host's.
|
|
38
|
+
registerStorageSchemaInitializer((database) => {
|
|
39
|
+
database.exec(`
|
|
40
|
+
CREATE TABLE IF NOT EXISTS runtime_events (
|
|
41
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
42
|
+
source_id TEXT NOT NULL,
|
|
43
|
+
event_type TEXT NOT NULL CHECK (
|
|
44
|
+
event_type IN (
|
|
45
|
+
'message',
|
|
46
|
+
'message-updated',
|
|
47
|
+
'ai-status',
|
|
48
|
+
'topic-created',
|
|
49
|
+
'topic-updated',
|
|
50
|
+
'topic-deleted'
|
|
51
|
+
)
|
|
52
|
+
),
|
|
53
|
+
topic_id TEXT NOT NULL,
|
|
54
|
+
payload_json TEXT NOT NULL,
|
|
55
|
+
created_at TEXT NOT NULL
|
|
56
|
+
)
|
|
57
|
+
`);
|
|
58
|
+
database.exec(
|
|
59
|
+
"CREATE INDEX IF NOT EXISTS idx_runtime_events_topic_seq ON runtime_events(topic_id, seq)",
|
|
60
|
+
);
|
|
61
|
+
});
|
|
52
62
|
|
|
53
63
|
const types = new Set<string>(RUNTIME_EVENT_TYPES);
|
|
54
64
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { db } from "#storage/forum-db";
|
|
2
|
+
import { registerStorageSchemaInitializer } from "#storage/storage-host";
|
|
2
3
|
|
|
3
4
|
export interface RuntimeGatewaySubmission {
|
|
4
5
|
clientMessageId: string;
|
|
@@ -25,19 +26,29 @@ interface RuntimeGatewaySubmissionRow {
|
|
|
25
26
|
payload_hash: string | null;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
db.exec
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
29
|
+
// Registered rather than executed at import time. A bare top-level `db.exec`
|
|
30
|
+
// resolves the storage connection the moment this module is imported, which for
|
|
31
|
+
// an embedding host is before `configureStorageHost()` has run — Negotium then
|
|
32
|
+
// opens its own database in the default state directory, caches it, and every
|
|
33
|
+
// later caller silently keeps using it instead of the host's.
|
|
34
|
+
registerStorageSchemaInitializer((database) => {
|
|
35
|
+
database.exec(`
|
|
36
|
+
CREATE TABLE IF NOT EXISTS runtime_gateway_submissions (
|
|
37
|
+
client_message_id TEXT PRIMARY KEY,
|
|
38
|
+
request_id TEXT NOT NULL UNIQUE,
|
|
39
|
+
topic_id TEXT NOT NULL,
|
|
40
|
+
message_id TEXT NOT NULL,
|
|
41
|
+
user_id TEXT NOT NULL,
|
|
42
|
+
created_at TEXT NOT NULL,
|
|
43
|
+
ack_cursor INTEGER NOT NULL DEFAULT 0,
|
|
44
|
+
message_cursor INTEGER NOT NULL DEFAULT 0,
|
|
45
|
+
payload_hash TEXT
|
|
46
|
+
)
|
|
47
|
+
`);
|
|
48
|
+
database.exec(
|
|
49
|
+
"CREATE INDEX IF NOT EXISTS idx_runtime_gateway_submissions_topic ON runtime_gateway_submissions(topic_id)",
|
|
50
|
+
);
|
|
51
|
+
});
|
|
41
52
|
try {
|
|
42
53
|
db.exec(
|
|
43
54
|
"ALTER TABLE runtime_gateway_submissions ADD COLUMN ack_cursor INTEGER NOT NULL DEFAULT 0",
|
|
@@ -51,9 +62,6 @@ try {
|
|
|
51
62
|
"ALTER TABLE runtime_gateway_submissions ADD COLUMN message_cursor INTEGER NOT NULL DEFAULT 0",
|
|
52
63
|
);
|
|
53
64
|
} catch {}
|
|
54
|
-
db.exec(
|
|
55
|
-
"CREATE INDEX IF NOT EXISTS idx_runtime_gateway_submissions_topic ON runtime_gateway_submissions(topic_id)",
|
|
56
|
-
);
|
|
57
65
|
|
|
58
66
|
function rowToSubmission(row: RuntimeGatewaySubmissionRow): RuntimeGatewaySubmission {
|
|
59
67
|
return {
|
|
@@ -9,6 +9,7 @@ import { db } from "#storage/forum-db";
|
|
|
9
9
|
import { TURN_LEASE_STALE_MS } from "#storage/runtime-leases";
|
|
10
10
|
import { getRuntimeTopicEpoch, TOPIC_MAINTENANCE_STALE_MS } from "#storage/runtime-topic-state";
|
|
11
11
|
import type { StorageDatabase } from "#storage/storage-contract";
|
|
12
|
+
import { registerStorageSchemaInitializer } from "#storage/storage-host";
|
|
12
13
|
import type { AgentKind, EffortLevel, PeerRuntimeBridgeContext } from "#types";
|
|
13
14
|
|
|
14
15
|
const REQUEST_CLAIM_STALE_MS = TURN_LEASE_STALE_MS;
|
|
@@ -149,7 +150,14 @@ export function ensureRuntimeUserTurnRequestsSchema(database: StorageDatabase):
|
|
|
149
150
|
);
|
|
150
151
|
}
|
|
151
152
|
|
|
152
|
-
|
|
153
|
+
// Registered rather than run at import time. Calling this with `db` on module
|
|
154
|
+
// evaluation resolves the storage connection immediately, which for an
|
|
155
|
+
// embedding host happens before `configureStorageHost()` — Negotium then opens
|
|
156
|
+
// and caches its own database in the default state directory, and every later
|
|
157
|
+
// caller silently keeps using it instead of the host's.
|
|
158
|
+
registerStorageSchemaInitializer((database) =>
|
|
159
|
+
ensureRuntimeUserTurnRequestsSchema(database as unknown as StorageDatabase),
|
|
160
|
+
);
|
|
153
161
|
|
|
154
162
|
function rowToRequest(row: RuntimeUserTurnRequestRow): RuntimeUserTurnRequest {
|
|
155
163
|
let attachments: string[] | undefined;
|