@sigrun/sigrun 0.4.1 → 0.5.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/CHANGELOG.md +45 -1
- package/README.md +46 -23
- package/dist/commands/expand.js +37 -5
- package/dist/curation/attribution.js +135 -0
- package/dist/curation/deletion.js +32 -0
- package/dist/curation/gate.js +50 -0
- package/dist/curation/host.js +342 -0
- package/dist/curation/internal.js +54 -0
- package/dist/curation/names.js +5 -0
- package/dist/curation/overlays.js +325 -0
- package/dist/curation/paths.js +17 -0
- package/dist/curation/run-doc.js +64 -0
- package/dist/curation/runs.js +579 -0
- package/dist/curation/scan.js +30 -0
- package/dist/curation/skill-tool.js +308 -0
- package/dist/curation/store.js +444 -0
- package/dist/curation/targets.js +77 -0
- package/dist/curation/usage.js +124 -0
- package/dist/curation/wrap.js +212 -0
- package/dist/extension.d.ts +282 -0
- package/dist/harness/boot.js +209 -9
- package/dist/harness/builtins.js +12 -9
- package/dist/harness/event-bus.js +64 -0
- package/dist/harness/loader.js +114 -1
- package/dist/harness/naming.js +6 -0
- package/dist/harness/packages.js +14 -9
- package/dist/harness/reserved-sections.js +2 -0
- package/dist/harness/seam-host.js +139 -26
- package/dist/harness/settings.js +2 -2
- package/dist/harness/tools.js +70 -0
- package/dist/jobs/fire.js +33 -11
- package/dist/jobs/skills-refs.js +40 -0
- package/dist/sandbox/policy.js +14 -0
- package/dist/server/blocks/answers.js +9 -3
- package/dist/server/conversation-ops.js +21 -3
- package/dist/server/helpers.js +1 -1
- package/dist/server/host-bridge.js +6 -1
- package/dist/server/notifications.js +6 -0
- package/dist/server/routes/meta.js +8 -3
- package/dist/server/routes/submissions.js +5 -0
- package/dist/server/routes/surfaces.js +3 -1
- package/dist/server/routes/users.js +7 -0
- package/dist/server/schema-fixture.js +168 -0
- package/dist/server/session-meta.js +12 -10
- package/dist/settings/core-sections.js +9 -0
- package/dist/settings/registry.js +6 -0
- package/dist/skills/index.js +88 -19
- package/node_modules/@sigrun/shared/CHANGELOG.md +9 -1
- package/node_modules/@sigrun/shared/dist/access.d.ts +3 -0
- package/node_modules/@sigrun/shared/dist/access.js +8 -0
- package/node_modules/@sigrun/shared/dist/session-meta.d.ts +15 -4
- package/node_modules/@sigrun/shared/dist/settings.d.ts +7 -5
- package/node_modules/@sigrun/shared/package.json +3 -2
- package/package.json +9 -7
- package/webui/dist/assets/{index-DWmsAQl2.js → index-BtczbUXo.js} +1 -1
- package/webui/dist/index.html +1 -1
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The curation host (curator P6, A9): `host.curation` — the
|
|
3
|
+
* extension-facing surface over everything Part A built.
|
|
4
|
+
*
|
|
5
|
+
* ONE object per boot, constructed in boot (where the per-user stores,
|
|
6
|
+
* the curation registry, the jobs records and the harness live) and
|
|
7
|
+
* exposed three ways: on the `OpenedHarness` (tests and server-side
|
|
8
|
+
* callers), on the host bridge (`HostBridge.curation` — the
|
|
9
|
+
* server-owned handle), and on every seam host (`SeamHost.curation`,
|
|
10
|
+
* delegating to the bridge, the same lazy-bind discipline as
|
|
11
|
+
* `conversations`).
|
|
12
|
+
*
|
|
13
|
+
* Read members (eligibility, authors, modelCallsSince, takeHints,
|
|
14
|
+
* targets, tools, skills, overlays, upstream, jobSkillRefs, usage) are
|
|
15
|
+
* thin reads over the A1–A8 primitives. The run producers (forkReview,
|
|
16
|
+
* digestRun, maintain) delegate to the run driver (runs.ts). The store
|
|
17
|
+
* subsets (`skills`, `overlays`) are the REAL per-user stores narrowed
|
|
18
|
+
* to the documented members — never a re-implementation.
|
|
19
|
+
*/
|
|
20
|
+
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
21
|
+
import { AssistantEntry } from "@earendil-works/pi-durable";
|
|
22
|
+
import { authorsOfEntries } from "./attribution.js";
|
|
23
|
+
import { takeHints } from "./wrap.js";
|
|
24
|
+
import { digestRunRun, forkReviewRun, setChangeTargetResolver } from "./runs.js";
|
|
25
|
+
import { resolveUpstream } from "./overlays.js";
|
|
26
|
+
import { jobSkillRefs } from "../jobs/skills-refs.js";
|
|
27
|
+
import { SessionMetaDoc } from "../server/session-meta.js";
|
|
28
|
+
/** modelCallsSince's entry-page size (cursor-walked, never a cap). */
|
|
29
|
+
const MODEL_CALLS_PAGE = 500;
|
|
30
|
+
/** Scan bound for the cursor walk: a broken cursor loops, a count does not. */
|
|
31
|
+
const MODEL_CALLS_SCAN_LIMIT = 10_000;
|
|
32
|
+
/** Build the one curation host (boot constructs exactly one). */
|
|
33
|
+
export function createCurationHost(deps) {
|
|
34
|
+
const { harness, registry, curation } = deps;
|
|
35
|
+
// The change-target resolver: the registry's qualified ids, with the
|
|
36
|
+
// core skills store owning `skill_manage` (no target declares it).
|
|
37
|
+
setChangeTargetResolver((tool) => {
|
|
38
|
+
if (tool === "skill_manage")
|
|
39
|
+
return "sigrun.skills";
|
|
40
|
+
return curation.byTool(tool)?.id ?? `tool:${tool}`;
|
|
41
|
+
});
|
|
42
|
+
/** The conversation handle (loud when the id is unknown). */
|
|
43
|
+
const conversationOf = async (id, ctx) => {
|
|
44
|
+
const conversation = await harness.conversation(id, ctx);
|
|
45
|
+
if (conversation === undefined) {
|
|
46
|
+
throw new Error(`conversation ${id} not found`);
|
|
47
|
+
}
|
|
48
|
+
return conversation;
|
|
49
|
+
};
|
|
50
|
+
/** The per-user store pair (loud when the user is absent). */
|
|
51
|
+
const storesOf = (user) => {
|
|
52
|
+
const stores = deps.stores(user);
|
|
53
|
+
if (stores === undefined) {
|
|
54
|
+
throw new Error(`unknown user "${user}" — no curation stores`);
|
|
55
|
+
}
|
|
56
|
+
return stores;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The digest producer `maintain`'s budget hands the target: one
|
|
60
|
+
* digest run for the SAME user, bounded to the listed tools, with
|
|
61
|
+
* the remaining wall clock as the deadline.
|
|
62
|
+
*/
|
|
63
|
+
const digestFor = async (user, input, tools, deadline) => {
|
|
64
|
+
const remaining = Math.max(1, deadline - Date.now());
|
|
65
|
+
return await digestRunRun({ harness, registry }, user, {
|
|
66
|
+
instruction: input,
|
|
67
|
+
allow: tools,
|
|
68
|
+
maxToolCalls: 8,
|
|
69
|
+
maxOutputTokens: 16_000,
|
|
70
|
+
deadlineMs: remaining,
|
|
71
|
+
reviewId: `digest-${Date.now()}-${Math.floor(Math.random() * 1e6)}`,
|
|
72
|
+
input,
|
|
73
|
+
}, deps.defaultModel, deps.defaultAgent);
|
|
74
|
+
};
|
|
75
|
+
const host = {
|
|
76
|
+
async eligibility(id) {
|
|
77
|
+
const meta = await harness.snapshot(SessionMetaDoc, id, BACKGROUND_CONTEXT);
|
|
78
|
+
const conversation = await conversationOf(id, BACKGROUND_CONTEXT);
|
|
79
|
+
const page = await conversation.entries({}, 1, undefined, BACKGROUND_CONTEXT);
|
|
80
|
+
// The same map `authors` serves: eligibility is its aggregate view.
|
|
81
|
+
const authors = await host.authors(id);
|
|
82
|
+
let unattributed = 0;
|
|
83
|
+
let ownerOnly = true;
|
|
84
|
+
for (const [, author] of authors) {
|
|
85
|
+
if (author === undefined) {
|
|
86
|
+
unattributed += 1;
|
|
87
|
+
ownerOnly = false;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (author !== meta?.owner)
|
|
91
|
+
ownerOnly = false;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
...(meta?.owner === undefined ? {} : { owner: meta.owner }),
|
|
95
|
+
internal: meta?.internal === true,
|
|
96
|
+
ownerOnly,
|
|
97
|
+
unattributed,
|
|
98
|
+
...(page.items[0]?.id === undefined
|
|
99
|
+
? {}
|
|
100
|
+
: { newestEntry: page.items[0].id }),
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
async authors(id) {
|
|
104
|
+
return await authorsOfEntries(harness, id, BACKGROUND_CONTEXT);
|
|
105
|
+
},
|
|
106
|
+
async modelCallsSince(id, entryId) {
|
|
107
|
+
const conversation = await conversationOf(id, BACKGROUND_CONTEXT);
|
|
108
|
+
// Oldest-first from the anchor: `minEntryId` is an INCLUSIVE
|
|
109
|
+
// bound, so the anchor itself rides the first page and is
|
|
110
|
+
// filtered below — the count is the assistant entries STRICTLY
|
|
111
|
+
// after the anchor (the extension.ts contract). No anchor:
|
|
112
|
+
// the whole history.
|
|
113
|
+
const query = entryId === undefined
|
|
114
|
+
? { order: "ascending" }
|
|
115
|
+
: { minEntryId: entryId, order: "ascending" };
|
|
116
|
+
// Cursor-complete walk (the feedOf pattern): pages until
|
|
117
|
+
// exhaustion so a long conversation never silently truncates,
|
|
118
|
+
// bounded so a cursor bug cannot loop forever.
|
|
119
|
+
let count = 0;
|
|
120
|
+
let scanned = 0;
|
|
121
|
+
let cursor;
|
|
122
|
+
for (;;) {
|
|
123
|
+
const page = await conversation.entries(query, MODEL_CALLS_PAGE, cursor, BACKGROUND_CONTEXT);
|
|
124
|
+
for (const entry of page.items) {
|
|
125
|
+
scanned += 1;
|
|
126
|
+
if (entry.kind !== AssistantEntry.kind)
|
|
127
|
+
continue;
|
|
128
|
+
if (entryId !== undefined && entry.id === entryId)
|
|
129
|
+
continue;
|
|
130
|
+
count += 1;
|
|
131
|
+
}
|
|
132
|
+
if (page.next === undefined ||
|
|
133
|
+
page.items.length === 0 ||
|
|
134
|
+
scanned >= MODEL_CALLS_SCAN_LIMIT) {
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
cursor = page.next;
|
|
138
|
+
}
|
|
139
|
+
return count;
|
|
140
|
+
},
|
|
141
|
+
async takeHints(id) {
|
|
142
|
+
const conversation = await conversationOf(id, BACKGROUND_CONTEXT);
|
|
143
|
+
return await takeHints(conversation, BACKGROUND_CONTEXT);
|
|
144
|
+
},
|
|
145
|
+
async targets(user, id) {
|
|
146
|
+
// The selection filter: a target is served for a conversation
|
|
147
|
+
// only when EVERY listed tool is model-visible in that
|
|
148
|
+
// conversation's resolved agent. No id: every target.
|
|
149
|
+
const visible = id === undefined ? undefined : await modelVisibleTools(id);
|
|
150
|
+
const result = [];
|
|
151
|
+
for (const target of curation.targets()) {
|
|
152
|
+
if (visible !== undefined) {
|
|
153
|
+
if (target.tools.some((tool) => !visible.has(tool)))
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const guidance = await target.guidance(user);
|
|
157
|
+
if (guidance.length === 0)
|
|
158
|
+
continue;
|
|
159
|
+
result.push({
|
|
160
|
+
id: target.id,
|
|
161
|
+
tools: [...target.tools],
|
|
162
|
+
guidance: [...guidance],
|
|
163
|
+
maintain: target.maintain !== undefined,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
},
|
|
168
|
+
async tools(id) {
|
|
169
|
+
const visible = await modelVisibleTools(id);
|
|
170
|
+
return [...visible];
|
|
171
|
+
},
|
|
172
|
+
async forkReview(id, opts) {
|
|
173
|
+
return await forkReviewRun({ harness, registry }, id, opts);
|
|
174
|
+
},
|
|
175
|
+
async digestRun(user, opts) {
|
|
176
|
+
return await digestRunRun({ harness, registry }, user, opts, deps.defaultModel, deps.defaultAgent);
|
|
177
|
+
},
|
|
178
|
+
async maintain(user, targetId, budget) {
|
|
179
|
+
const target = curation
|
|
180
|
+
.targets()
|
|
181
|
+
.find((one) => one.id === targetId);
|
|
182
|
+
if (target === undefined) {
|
|
183
|
+
throw new Error(`unknown curation target "${targetId}"`);
|
|
184
|
+
}
|
|
185
|
+
if (target.maintain === undefined) {
|
|
186
|
+
throw new Error(`curation target "${targetId}" declares no maintain`);
|
|
187
|
+
}
|
|
188
|
+
// The budget's run: one digest run for the SAME user, bounded
|
|
189
|
+
// to the listed tools (the target asks for operations as text
|
|
190
|
+
// and applies them itself — the P6 contract).
|
|
191
|
+
const digestBudget = {
|
|
192
|
+
deadline: budget.deadline,
|
|
193
|
+
runs: budget.runs,
|
|
194
|
+
run: (input, tools) => digestFor(user, input, tools, budget.deadline),
|
|
195
|
+
};
|
|
196
|
+
const ctx = { abortSignal: undefined };
|
|
197
|
+
return await target.maintain(user, ctx, digestBudget);
|
|
198
|
+
},
|
|
199
|
+
skills(user) {
|
|
200
|
+
const { store } = storesOf(user);
|
|
201
|
+
return {
|
|
202
|
+
list: () => store.list(),
|
|
203
|
+
view: (name, file) => store.view(name, file),
|
|
204
|
+
feed: (limit, before) => store.feed(limit, before),
|
|
205
|
+
pin: (name, pinned) => store.pin(name, pinned),
|
|
206
|
+
undo: (entryIds) => store.undo(entryIds),
|
|
207
|
+
apply: (name, change, meta) => store.apply(name, change, {
|
|
208
|
+
actor: "curator",
|
|
209
|
+
...(meta.reviewId === undefined
|
|
210
|
+
? {}
|
|
211
|
+
: { reviewId: meta.reviewId }),
|
|
212
|
+
...(meta.reason === undefined ? {} : { reason: meta.reason }),
|
|
213
|
+
}),
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
overlays(user) {
|
|
217
|
+
const { overlays } = storesOf(user);
|
|
218
|
+
return overlays;
|
|
219
|
+
},
|
|
220
|
+
async upstream(user, key) {
|
|
221
|
+
// The upstream read is user-independent (the skill roots are
|
|
222
|
+
// the host's); the user gate only proves a real curated user.
|
|
223
|
+
storesOf(user);
|
|
224
|
+
return await resolveUpstream(key, undefined, BACKGROUND_CONTEXT, {
|
|
225
|
+
userDir: deps.skillsOptions.userDir,
|
|
226
|
+
packageRoots: deps.skillsOptions.packageRoots,
|
|
227
|
+
});
|
|
228
|
+
},
|
|
229
|
+
async jobSkillRefs(user) {
|
|
230
|
+
return await jobSkillRefs(deps.storage, user, BACKGROUND_CONTEXT);
|
|
231
|
+
},
|
|
232
|
+
async undoReview(user, reviewId) {
|
|
233
|
+
const { store } = storesOf(user);
|
|
234
|
+
const entries = await feedOf(store, reviewId);
|
|
235
|
+
// The store undo: entries with a `before` version snapshot are
|
|
236
|
+
// the revertible ones; the store's own undo takes entry ids
|
|
237
|
+
// and restores the snapshots (the A4 machinery).
|
|
238
|
+
// Hand EVERY review feed entry to the store's undo: edit entries
|
|
239
|
+
// restore their before-snapshot, create entries (before ===
|
|
240
|
+
// undefined) archive the skill — the no-delete revert the A4
|
|
241
|
+
// store owns. The store itself ignores ids it cannot revert.
|
|
242
|
+
let reverted = 0;
|
|
243
|
+
if (entries.length > 0) {
|
|
244
|
+
const undone = await store.undo(entries.map((entry) => entry.id));
|
|
245
|
+
reverted = undone.length;
|
|
246
|
+
}
|
|
247
|
+
// The target undos: every change the review's TARGET tools
|
|
248
|
+
// reported. The feed only carries the core stores' entries;
|
|
249
|
+
// a target-owned change's id is the `details.curation`
|
|
250
|
+
// changeId the tool reported, which the caller (the run
|
|
251
|
+
// result's `changes`) holds — undoReview walks the RUN
|
|
252
|
+
// conversation's tool results for them.
|
|
253
|
+
await undoTargetChanges(user, reviewId);
|
|
254
|
+
return reverted;
|
|
255
|
+
},
|
|
256
|
+
async usage(user) {
|
|
257
|
+
const { usage, store } = storesOf(user);
|
|
258
|
+
const skills = await store.list();
|
|
259
|
+
const result = {};
|
|
260
|
+
for (const skill of skills) {
|
|
261
|
+
result[skill.name] = usage.lastUsed(skill.name);
|
|
262
|
+
}
|
|
263
|
+
return result;
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
return host;
|
|
267
|
+
/** The conversation's model-visible tool names. */
|
|
268
|
+
async function modelVisibleTools(id) {
|
|
269
|
+
const conversation = await conversationOf(id, BACKGROUND_CONTEXT);
|
|
270
|
+
const agent = await conversation.agent(BACKGROUND_CONTEXT);
|
|
271
|
+
return new Set(agent.tools.map((tool) => tool.name));
|
|
272
|
+
}
|
|
273
|
+
/** The feed entries one review caused (newest-first walk). */
|
|
274
|
+
async function feedOf(store, reviewId) {
|
|
275
|
+
const found = [];
|
|
276
|
+
let before;
|
|
277
|
+
for (;;) {
|
|
278
|
+
const page = await store.feed(100, before);
|
|
279
|
+
if (page.length === 0)
|
|
280
|
+
break;
|
|
281
|
+
for (const entry of page) {
|
|
282
|
+
if (entry.reviewId === reviewId)
|
|
283
|
+
found.push(entry);
|
|
284
|
+
}
|
|
285
|
+
before = page[page.length - 1]?.id;
|
|
286
|
+
if (page.length < 100)
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
return found;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Undo the target-owned changes one review caused: find the run
|
|
293
|
+
* conversation by its run doc (`reviewId`), walk its tool results
|
|
294
|
+
* for `details.curation` changeIds, and hand each to the owning
|
|
295
|
+
* target's `undo` with the curated user.
|
|
296
|
+
*/
|
|
297
|
+
async function undoTargetChanges(user, reviewId) {
|
|
298
|
+
const runId = await runConversationOf(reviewId);
|
|
299
|
+
if (runId === undefined)
|
|
300
|
+
return;
|
|
301
|
+
const conversation = await harness.conversation(runId, BACKGROUND_CONTEXT);
|
|
302
|
+
if (conversation === undefined)
|
|
303
|
+
return;
|
|
304
|
+
const page = await conversation.entries({ order: "ascending" }, 500, undefined, BACKGROUND_CONTEXT);
|
|
305
|
+
for (const entry of page.items) {
|
|
306
|
+
if (entry.kind !== "pi.tool-result")
|
|
307
|
+
continue;
|
|
308
|
+
for (const message of entry.model ?? []) {
|
|
309
|
+
if (message.role !== "toolResult")
|
|
310
|
+
continue;
|
|
311
|
+
// SAFETY: the tool-result details carry the A6/A8
|
|
312
|
+
// `details.curation` shape ({changeId, summary}); the
|
|
313
|
+
// structural read below only touches those two fields.
|
|
314
|
+
const details = message.details;
|
|
315
|
+
const change = details?.curation;
|
|
316
|
+
if (change === undefined)
|
|
317
|
+
continue;
|
|
318
|
+
const target = curation.byTool(message.toolName);
|
|
319
|
+
if (target === undefined)
|
|
320
|
+
continue;
|
|
321
|
+
await target.undo(user, change.changeId, { abortSignal: undefined });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/** The run conversation carrying one reviewId (or undefined). */
|
|
326
|
+
async function runConversationOf(reviewId) {
|
|
327
|
+
const { CurationRunDoc } = await import("./run-doc.js");
|
|
328
|
+
let cursor;
|
|
329
|
+
for (;;) {
|
|
330
|
+
const page = await deps.storage.scanConversations({ order: "descending" }, 50, cursor, BACKGROUND_CONTEXT);
|
|
331
|
+
for (const record of page.items) {
|
|
332
|
+
const run = await harness.snapshot(CurationRunDoc, record.id, BACKGROUND_CONTEXT);
|
|
333
|
+
if (run?.reviewId === reviewId)
|
|
334
|
+
return record.id;
|
|
335
|
+
}
|
|
336
|
+
if (page.next === undefined)
|
|
337
|
+
return undefined;
|
|
338
|
+
cursor = page.next;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal runs (curator P5, spec §6.1): conversations created for curator
|
|
3
|
+
* work are invisible on every owner-keyed path.
|
|
4
|
+
*
|
|
5
|
+
* The marker is the server-owned `SessionMeta.internal: true`, stamped by
|
|
6
|
+
* {@link markInternal} in the conversation's CREATING commit — alongside
|
|
7
|
+
* the curated user as `owner`, so the env, the acting user, and the
|
|
8
|
+
* target tools resolve to them. The flag never changes after creation
|
|
9
|
+
* (there is no writer besides the creating commit), which every consumer
|
|
10
|
+
* may rely on for caching.
|
|
11
|
+
*
|
|
12
|
+
* Host paths that check it (each hidden from internal conversations):
|
|
13
|
+
* the HTTP listing, the host bridge's `conversations.list`, the `:id`
|
|
14
|
+
* route loader (404 for every caller, superuser included), the SSE
|
|
15
|
+
* per-principal event stream, the notifications surface, the auto-namer,
|
|
16
|
+
* and the event bus's idle clock, `run.settled` host emit, and extension
|
|
17
|
+
* `runSettled`/lifecycle delivery.
|
|
18
|
+
*
|
|
19
|
+
* Internal conversations are NOT purged: pi-durable 1.4.0 has no
|
|
20
|
+
* conversation deletion, and a fork reads its parent's history through
|
|
21
|
+
* ancestry, so each review costs only its own turn's rows. Backup copies
|
|
22
|
+
* the whole database; nothing per-conversation can be excluded (accepted
|
|
23
|
+
* per spec §6.1 P5 — the audit record is the change feed).
|
|
24
|
+
*/
|
|
25
|
+
import { SessionMetaDoc } from "../server/session-meta.js";
|
|
26
|
+
/**
|
|
27
|
+
* Mark one conversation internal and owned by `owner`, in the creating
|
|
28
|
+
* commit: the P6 fork-review/digest producers call this from
|
|
29
|
+
* `createConversation`'s `init` (the conversation's first commit, so the
|
|
30
|
+
* marker predates every observer). Stamps `owner` too — the curated user
|
|
31
|
+
* the run acts for.
|
|
32
|
+
*/
|
|
33
|
+
export async function markInternal(tx, conversationId, owner) {
|
|
34
|
+
const meta = await tx.doc(SessionMetaDoc, conversationId);
|
|
35
|
+
meta.owner = owner;
|
|
36
|
+
meta.internal = true;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Whether one conversation is internal, from any `DocumentReader`
|
|
40
|
+
* (including the narrowed reader an extension handler receives). Absent
|
|
41
|
+
* doc or absent flag → false: an ordinary conversation is the default,
|
|
42
|
+
* and a read failure must never hide an ordinary conversation from its
|
|
43
|
+
* owner (fail open on the LISTING side only; the emit-side gates live in
|
|
44
|
+
* the event bus, which resolves once per settle group and caches).
|
|
45
|
+
*/
|
|
46
|
+
export async function isInternal(read, conversationId, ctx) {
|
|
47
|
+
try {
|
|
48
|
+
const meta = await read.snapshot(SessionMetaDoc, conversationId, ctx);
|
|
49
|
+
return meta?.internal === true;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|