@sigrun/sigrun 0.0.0-stage → 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/CHANGELOG.md +204 -0
- package/CONTEXT.md +163 -0
- package/LICENSE +21 -0
- package/README.md +691 -2
- package/THIRD_PARTY_NOTICES.md +28 -0
- package/dist/backup/marker.js +80 -0
- package/dist/backup/run-backup.js +339 -0
- package/dist/cli.js +952 -0
- package/dist/commands/expand.js +245 -0
- package/dist/commands/templates.js +272 -0
- package/dist/config.js +1146 -0
- package/dist/extension.d.ts +431 -0
- package/dist/harness/activity.js +42 -0
- package/dist/harness/ambient.js +261 -0
- package/dist/harness/auth.js +130 -0
- package/dist/harness/boot.js +865 -0
- package/dist/harness/builtins.js +191 -0
- package/dist/harness/event-bus.js +1203 -0
- package/dist/harness/health.js +30 -0
- package/dist/harness/identity/index.js +37 -0
- package/dist/harness/identity/invites.js +94 -0
- package/dist/harness/identity/json-store.js +108 -0
- package/dist/harness/identity/setup-code.js +81 -0
- package/dist/harness/identity/tokens.js +101 -0
- package/dist/harness/identity/users.js +93 -0
- package/dist/harness/interactions.js +283 -0
- package/dist/harness/loader.js +1236 -0
- package/dist/harness/memory-default.js +35 -0
- package/dist/harness/naming.js +193 -0
- package/dist/harness/packages.js +1002 -0
- package/dist/harness/providers/auth-bridge.js +60 -0
- package/dist/harness/providers/index.js +277 -0
- package/dist/harness/providers/models-store.js +105 -0
- package/dist/harness/providers/refresh.js +161 -0
- package/dist/harness/providers/scheduler.js +89 -0
- package/dist/harness/registry.js +9 -0
- package/dist/harness/reserved-sections.js +24 -0
- package/dist/harness/seam-host.js +239 -0
- package/dist/harness/sections/context-files.js +78 -0
- package/dist/harness/sections/model-guidance.js +77 -0
- package/dist/harness/settings.js +1053 -0
- package/dist/harness/stores.js +450 -0
- package/dist/harness/tools.js +500 -0
- package/dist/harness/workspace.js +73 -0
- package/dist/index.js +39 -0
- package/dist/jobs/docs.js +257 -0
- package/dist/jobs/fire.js +342 -0
- package/dist/jobs/index.js +228 -0
- package/dist/jobs/observe.js +165 -0
- package/dist/jobs/reconcile.js +245 -0
- package/dist/jobs/schedule.js +77 -0
- package/dist/jobs/spec.js +248 -0
- package/dist/jobs/task.js +190 -0
- package/dist/jobs/transfer.js +53 -0
- package/dist/serve.js +164 -0
- package/dist/server/auth/claim-lock.js +28 -0
- package/dist/server/auth/ip.js +51 -0
- package/dist/server/auth/login-limiter.js +52 -0
- package/dist/server/auth/loopback.js +47 -0
- package/dist/server/auth/pipeline.js +330 -0
- package/dist/server/auth/principal.js +26 -0
- package/dist/server/commands.js +198 -0
- package/dist/server/content-parts.js +142 -0
- package/dist/server/context.js +13 -0
- package/dist/server/conversation-handler.js +61 -0
- package/dist/server/conversation-ops.js +310 -0
- package/dist/server/events/hub.js +39 -0
- package/dist/server/events/types.js +8 -0
- package/dist/server/helpers.js +351 -0
- package/dist/server/host-bridge.js +131 -0
- package/dist/server/index.js +232 -0
- package/dist/server/notifications.js +185 -0
- package/dist/server/ownership.js +67 -0
- package/dist/server/readiness.js +36 -0
- package/dist/server/routes/auth.js +172 -0
- package/dist/server/routes/conversations.js +341 -0
- package/dist/server/routes/events.js +161 -0
- package/dist/server/routes/extension-routes.js +127 -0
- package/dist/server/routes/extensions.js +168 -0
- package/dist/server/routes/files.js +308 -0
- package/dist/server/routes/interactions.js +260 -0
- package/dist/server/routes/jobs.js +271 -0
- package/dist/server/routes/meta.js +146 -0
- package/dist/server/routes/metrics.js +219 -0
- package/dist/server/routes/onboarding.js +268 -0
- package/dist/server/routes/probes.js +53 -0
- package/dist/server/routes/providers.js +198 -0
- package/dist/server/routes/settings.js +48 -0
- package/dist/server/routes/stream.js +131 -0
- package/dist/server/routes/submissions.js +317 -0
- package/dist/server/routes/users.js +169 -0
- package/dist/server/schema-fixture.js +950 -0
- package/dist/server/session-meta.js +247 -0
- package/dist/server/static.js +169 -0
- package/dist/server/views.js +71 -0
- package/dist/skills/index.js +418 -0
- package/node_modules/@sigrun/shared/CHANGELOG.md +16 -0
- package/node_modules/@sigrun/shared/LICENSE +21 -0
- package/node_modules/@sigrun/shared/README.md +10 -0
- package/node_modules/@sigrun/shared/dist/access.d.ts +55 -0
- package/node_modules/@sigrun/shared/dist/access.js +72 -0
- package/node_modules/@sigrun/shared/dist/ambient.d.ts +111 -0
- package/node_modules/@sigrun/shared/dist/ambient.js +23 -0
- package/node_modules/@sigrun/shared/dist/commands.d.ts +75 -0
- package/node_modules/@sigrun/shared/dist/commands.js +25 -0
- package/node_modules/@sigrun/shared/dist/conversations.d.ts +31 -0
- package/node_modules/@sigrun/shared/dist/conversations.js +94 -0
- package/node_modules/@sigrun/shared/dist/interactions.d.ts +91 -0
- package/node_modules/@sigrun/shared/dist/interactions.js +21 -0
- package/node_modules/@sigrun/shared/dist/jobs.d.ts +100 -0
- package/node_modules/@sigrun/shared/dist/jobs.js +19 -0
- package/node_modules/@sigrun/shared/dist/json.d.ts +20 -0
- package/node_modules/@sigrun/shared/dist/json.js +13 -0
- package/node_modules/@sigrun/shared/dist/session-meta.d.ts +103 -0
- package/node_modules/@sigrun/shared/dist/session-meta.js +13 -0
- package/node_modules/@sigrun/shared/dist/settings.d.ts +401 -0
- package/node_modules/@sigrun/shared/dist/settings.js +19 -0
- package/node_modules/@sigrun/shared/dist/themes.d.ts +50 -0
- package/node_modules/@sigrun/shared/dist/themes.js +156 -0
- package/node_modules/@sigrun/shared/dist/turns.d.ts +102 -0
- package/node_modules/@sigrun/shared/dist/turns.js +166 -0
- package/node_modules/@sigrun/shared/dist/view-delta.d.ts +54 -0
- package/node_modules/@sigrun/shared/dist/view-delta.js +135 -0
- package/node_modules/@sigrun/shared/dist/views.d.ts +742 -0
- package/node_modules/@sigrun/shared/dist/views.js +38 -0
- package/node_modules/@sigrun/shared/package.json +83 -0
- package/package.json +90 -5
- package/webui/dist/assets/index-Rl0KhCLa.css +1 -0
- package/webui/dist/assets/index-rOiARZLF.js +50 -0
- package/webui/dist/index.html +37 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared conversation operations (core hardening W5 Task 5, spec §9.5):
|
|
3
|
+
* the ONE implementation of create/submit/list/read-meta that BOTH the
|
|
4
|
+
* HTTP routes (src/server/routes/conversations.ts,
|
|
5
|
+
* routes/submissions.ts) and the extension host bridge
|
|
6
|
+
* (src/server/host-bridge.ts) run — extracted from the route bodies so
|
|
7
|
+
* an extension driving a conversation in-process gets identical
|
|
8
|
+
* behavior by construction: content-part resolution, the served-model
|
|
9
|
+
* check, activity stamping, and the durable listing are never
|
|
10
|
+
* re-implemented.
|
|
11
|
+
*
|
|
12
|
+
* Each op is a pure `(ctx, …) → result` function over the shared
|
|
13
|
+
* `ServerContext`. Client-error vocabulary: a validation failure throws
|
|
14
|
+
* {@link ConversationOpError} (the routes' 400 bodies); the submit
|
|
15
|
+
* path's `UnservedModelError` propagates for the route (and the bridge
|
|
16
|
+
* caller) to map — the same split the inline route bodies had.
|
|
17
|
+
*/
|
|
18
|
+
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
19
|
+
import { accessOf, isSharing, } from "@sigrun/shared/access";
|
|
20
|
+
import { touchActivity } from "../harness/activity.js";
|
|
21
|
+
import { resolveServedModelRefreshing } from "../harness/providers/refresh.js";
|
|
22
|
+
import { parseSubmissionInput, resolveParts } from "./content-parts.js";
|
|
23
|
+
import { CommandUsageError, expandInput, EXPANDED_ENTRY_KIND, } from "../commands/expand.js";
|
|
24
|
+
import { conversationEnv, readView, SCAN_PAGE_SIZE, storeAgentChange, WHEN_BUSY_MODES, } from "./helpers.js";
|
|
25
|
+
import { DEFAULT_SESSION_META, SessionMetaDoc, } from "./session-meta.js";
|
|
26
|
+
/**
|
|
27
|
+
* One client error from a conversation op: the exact 400 body the routes
|
|
28
|
+
* serve (`{error: "invalid_input"}`, `{error: "invalid_field", field}`),
|
|
29
|
+
* so the HTTP route maps it verbatim and the bridge caller sees the
|
|
30
|
+
* same fail-loud vocabulary.
|
|
31
|
+
*/
|
|
32
|
+
export class ConversationOpError extends Error {
|
|
33
|
+
body;
|
|
34
|
+
constructor(body) {
|
|
35
|
+
super(body.error);
|
|
36
|
+
this.body = body;
|
|
37
|
+
this.name = "ConversationOpError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** The §7.1 ceiling for `?waitMs` — a positive integer at most this. */
|
|
41
|
+
const WAIT_MS_MAX = 60_000;
|
|
42
|
+
/**
|
|
43
|
+
* Race `submission.wait` against a `waitMs` timer (§7.1): the wait winning
|
|
44
|
+
* returns the settled record; the timer winning returns whichever record is
|
|
45
|
+
* CURRENT (mid-run). The losing wait's eventual rejection is never left
|
|
46
|
+
* unhandled.
|
|
47
|
+
*/
|
|
48
|
+
async function waitForSubmission(submission, waitMs) {
|
|
49
|
+
let timer;
|
|
50
|
+
try {
|
|
51
|
+
const timeout = new Promise((resolve) => {
|
|
52
|
+
timer = setTimeout(() => resolve(true), waitMs);
|
|
53
|
+
});
|
|
54
|
+
const wait = submission.wait(BACKGROUND_CONTEXT);
|
|
55
|
+
const winner = await Promise.race([wait, timeout]);
|
|
56
|
+
if (winner !== true) {
|
|
57
|
+
return winner;
|
|
58
|
+
}
|
|
59
|
+
// The timer won: hand back the CURRENT record; a late rejection of
|
|
60
|
+
// the abandoned wait must not surface as unhandled.
|
|
61
|
+
void wait.catch(() => undefined);
|
|
62
|
+
return await submission.status(BACKGROUND_CONTEXT);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
if (timer !== undefined)
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Create one conversation the server-create way: ownerless ownership, the
|
|
71
|
+
* boot default model plus the store's agent settings (re-read per create),
|
|
72
|
+
* then — when ANY stamp field is present — ONE commit writing them onto
|
|
73
|
+
* the session-meta doc. Returns the create-response shape `{id, view}`.
|
|
74
|
+
*/
|
|
75
|
+
export async function createConversationOp(ctx, input = {}) {
|
|
76
|
+
const { opened } = ctx;
|
|
77
|
+
// Validate FIRST — a rejected input never touches storage.
|
|
78
|
+
if (input.owner !== undefined && typeof input.owner !== "string") {
|
|
79
|
+
throw new ConversationOpError({ error: "invalid_field", field: "owner" });
|
|
80
|
+
}
|
|
81
|
+
if (input.sharing !== undefined && !isSharing(input.sharing)) {
|
|
82
|
+
throw new ConversationOpError({ error: "invalid_field", field: "sharing" });
|
|
83
|
+
}
|
|
84
|
+
if (input.name !== undefined && typeof input.name !== "string") {
|
|
85
|
+
throw new ConversationOpError({ error: "invalid_field", field: "name" });
|
|
86
|
+
}
|
|
87
|
+
if (input.source !== undefined && typeof input.source !== "string") {
|
|
88
|
+
throw new ConversationOpError({ error: "invalid_field", field: "source" });
|
|
89
|
+
}
|
|
90
|
+
const conversation = await opened.harness.createConversation({
|
|
91
|
+
ownership: { kind: "ownerless" },
|
|
92
|
+
// A conversation with no agent model cannot generate (no_model):
|
|
93
|
+
// server-created conversations start on the boot default model,
|
|
94
|
+
// like the root conversation, and carry the store's
|
|
95
|
+
// defaultTools/perModelThinking — re-read per create, so PATCHes
|
|
96
|
+
// reach new conversations without a restart.
|
|
97
|
+
agent: storeAgentChange(opened, opened.defaultModel),
|
|
98
|
+
}, BACKGROUND_CONTEXT);
|
|
99
|
+
const stamps = [
|
|
100
|
+
input.owner,
|
|
101
|
+
input.sharing,
|
|
102
|
+
input.name,
|
|
103
|
+
input.source,
|
|
104
|
+
].filter((value) => value !== undefined);
|
|
105
|
+
if (stamps.length > 0) {
|
|
106
|
+
await conversation.commit(async (tx) => {
|
|
107
|
+
const draft = await tx.doc(SessionMetaDoc, conversation.id);
|
|
108
|
+
if (input.owner !== undefined)
|
|
109
|
+
draft.owner = input.owner;
|
|
110
|
+
// Copy-on-assignment (the tracker requires alias-free roots):
|
|
111
|
+
// the people-scope array is cloned; the closed scopes spread.
|
|
112
|
+
if (input.sharing !== undefined) {
|
|
113
|
+
draft.sharing =
|
|
114
|
+
input.sharing.scope === "people"
|
|
115
|
+
? {
|
|
116
|
+
scope: "people",
|
|
117
|
+
users: [...input.sharing.users],
|
|
118
|
+
level: input.sharing.level,
|
|
119
|
+
}
|
|
120
|
+
: { ...input.sharing };
|
|
121
|
+
}
|
|
122
|
+
if (input.name !== undefined)
|
|
123
|
+
draft.name = input.name;
|
|
124
|
+
if (input.source !== undefined)
|
|
125
|
+
draft.source = input.source;
|
|
126
|
+
}, BACKGROUND_CONTEXT);
|
|
127
|
+
}
|
|
128
|
+
const view = await readView(conversation);
|
|
129
|
+
return { id: conversation.id, view };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Submit one input the route way: content-part resolution against the
|
|
133
|
+
* conversation's live workspace env, the §2/§3 served-model check
|
|
134
|
+
* (refresh-on-miss, `UnservedModelError` propagates to the caller),
|
|
135
|
+
* `whenBusy`/`waitMs` validation, admission, the activity stamp, and the
|
|
136
|
+
* §7.1 record — at admission when no `waitMs` is given, else the bounded
|
|
137
|
+
* settlement race.
|
|
138
|
+
*/
|
|
139
|
+
export async function submitOp(ctx, conversation, input, opts = {}) {
|
|
140
|
+
const { opened } = ctx;
|
|
141
|
+
const parsed = parseSubmissionInput(input);
|
|
142
|
+
if (!parsed.ok) {
|
|
143
|
+
throw new ConversationOpError({ error: "invalid_input" });
|
|
144
|
+
}
|
|
145
|
+
// Spec §2/§3 binding contract: a conversation pinned to an unserved
|
|
146
|
+
// model fails explicitly AT SUBMIT — 4xx with the available ids —
|
|
147
|
+
// never a silent fallback, and never a settled no_model run surfaced
|
|
148
|
+
// only later. Resolution goes through the refresh-on-miss wrapper:
|
|
149
|
+
// ONE bounded refresh of the named provider + re-resolve on the miss,
|
|
150
|
+
// so a model gained upstream since the last interval tick is
|
|
151
|
+
// immediately usable; still absent → the UnservedModelError carrying
|
|
152
|
+
// the catalog (the route maps it to 400 unserved_model).
|
|
153
|
+
const agent = await conversation.agent(BACKGROUND_CONTEXT);
|
|
154
|
+
if (agent.model !== undefined) {
|
|
155
|
+
await resolveServedModelRefreshing(opened.models, agent.model);
|
|
156
|
+
}
|
|
157
|
+
// `waitMs` (§7.1) is the opt-in bounded settlement wait for scripts —
|
|
158
|
+
// a positive integer ≤ 60 000; anything else is a fail-loud client
|
|
159
|
+
// error BEFORE anything is admitted.
|
|
160
|
+
let waitMs;
|
|
161
|
+
if (opts.waitMs !== undefined) {
|
|
162
|
+
const parsedWait = typeof opts.waitMs === "number" ? opts.waitMs : Number(opts.waitMs);
|
|
163
|
+
if (!Number.isInteger(parsedWait) ||
|
|
164
|
+
parsedWait <= 0 ||
|
|
165
|
+
parsedWait > WAIT_MS_MAX) {
|
|
166
|
+
throw new ConversationOpError({ error: "invalid_field", field: "waitMs" });
|
|
167
|
+
}
|
|
168
|
+
waitMs = parsedWait;
|
|
169
|
+
}
|
|
170
|
+
// `whenBusy` is the steer affordance (spec §7 item 3) — validate it
|
|
171
|
+
// against pi-durable's union and pass it through; unknown values are a
|
|
172
|
+
// client error, never a silent default.
|
|
173
|
+
const whenBusy = opts.whenBusy;
|
|
174
|
+
if (whenBusy !== undefined &&
|
|
175
|
+
(typeof whenBusy !== "string" || !WHEN_BUSY_MODES.includes(whenBusy))) {
|
|
176
|
+
throw new ConversationOpError({ error: "invalid_field", field: "whenBusy" });
|
|
177
|
+
}
|
|
178
|
+
// Resolve path-parts to inline content NOW, against the live
|
|
179
|
+
// workspace: missing paths become explicit error parts; other
|
|
180
|
+
// workspace failures throw — fail-loud. Command expansion (Task 2c)
|
|
181
|
+
// runs BEFORE this: a text part starting with `/` that names a skill
|
|
182
|
+
// or prompt template is rewritten into the text the model sees, and
|
|
183
|
+
// the submission's entry carries what the user TYPED (the transcript's
|
|
184
|
+
// `sigrun.user.expanded` entry). Non-`/` inputs, `/command` inputs,
|
|
185
|
+
// and unknown skills pass through untouched — every channel and the
|
|
186
|
+
// HTTP API behave identically because this is the one admission path.
|
|
187
|
+
// A MALFORMED command line (an unterminated quote in template args) is
|
|
188
|
+
// ordinary user input: it maps to the same `invalid_input` 400 shape
|
|
189
|
+
// `parseSubmissionInput` uses, never a 500. Anything else expansion
|
|
190
|
+
// throws is a genuine bug and still falls through as one.
|
|
191
|
+
const env = await conversationEnv(opened, conversation);
|
|
192
|
+
let expansion;
|
|
193
|
+
try {
|
|
194
|
+
expansion = await expandInput(parsed.parts, opened.commandExpansion, env, BACKGROUND_CONTEXT);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (error instanceof CommandUsageError) {
|
|
198
|
+
throw new ConversationOpError({ error: "invalid_input" });
|
|
199
|
+
}
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
const content = await resolveParts(expansion.parts, env, BACKGROUND_CONTEXT);
|
|
203
|
+
const draft = {
|
|
204
|
+
type: "input",
|
|
205
|
+
content,
|
|
206
|
+
...(opts.requestId === undefined ? {} : { requestId: opts.requestId }),
|
|
207
|
+
...(whenBusy === undefined ? {} : { whenBusy: whenBusy }),
|
|
208
|
+
...(expansion.typed === undefined
|
|
209
|
+
? {}
|
|
210
|
+
: {
|
|
211
|
+
entry: {
|
|
212
|
+
kind: EXPANDED_ENTRY_KIND,
|
|
213
|
+
data: {
|
|
214
|
+
typed: expansion.typed,
|
|
215
|
+
...(expansion.command === undefined
|
|
216
|
+
? {}
|
|
217
|
+
: { command: expansion.command }),
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
}),
|
|
221
|
+
};
|
|
222
|
+
// The submit pass-through is retired (§6.7): the memory seam's
|
|
223
|
+
// runSettled owns capture, so submissions admit directly.
|
|
224
|
+
const submission = await conversation.submit(draft, BACKGROUND_CONTEXT);
|
|
225
|
+
// Admission stamps activity: the sidebar's activity sort must move the
|
|
226
|
+
// moment a prompt lands — before the run settles.
|
|
227
|
+
await touchActivity(opened.harness, conversation.id, opened.now, BACKGROUND_CONTEXT);
|
|
228
|
+
// §7.1: the record AT ADMISSION (queued or placed), or — with a
|
|
229
|
+
// validated `waitMs` — the bounded settlement race.
|
|
230
|
+
return waitMs === undefined
|
|
231
|
+
? await submission.status(BACKGROUND_CONTEXT)
|
|
232
|
+
: await waitForSubmission(submission, waitMs);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* List conversations the durable way: the storage scan is authoritative
|
|
236
|
+
* (conversations this process never created all list), the listing is
|
|
237
|
+
* PER-CALLER (`accessOf` against each row's session-meta snapshot — hidden
|
|
238
|
+
* is absent, §5.9), and every row is lean by construction (scan record +
|
|
239
|
+
* meta snapshot alone, no per-row view read). The caller is the resolved
|
|
240
|
+
* principal; the host bridge passes its own trusted view (see
|
|
241
|
+
* host-bridge.ts).
|
|
242
|
+
*/
|
|
243
|
+
export async function listConversationsOp(ctx, caller, filters = {}) {
|
|
244
|
+
const { opened, rootId } = ctx;
|
|
245
|
+
const conversations = [];
|
|
246
|
+
let cursor;
|
|
247
|
+
do {
|
|
248
|
+
const page = await opened.storage.scanConversations({ order: "ascending" }, SCAN_PAGE_SIZE, cursor, BACKGROUND_CONTEXT);
|
|
249
|
+
for (const record of page.items) {
|
|
250
|
+
// The session-meta owner rides the summary when stamped (defaults
|
|
251
|
+
// serve nothing — no owner key when unset), and so do the sidebar
|
|
252
|
+
// fields: the display name, the activity stamp, the archive/pin
|
|
253
|
+
// flags, and the create source (spec §9.5).
|
|
254
|
+
const meta = await opened.harness.snapshot(SessionMetaDoc, record.id, BACKGROUND_CONTEXT);
|
|
255
|
+
const access = accessOf(caller, meta ?? {});
|
|
256
|
+
// Hidden is absent from the list (spec §5.9).
|
|
257
|
+
if (access === "none")
|
|
258
|
+
continue;
|
|
259
|
+
// The archived filter (absent = all).
|
|
260
|
+
if (filters.archivedOnly !== undefined &&
|
|
261
|
+
(meta?.archived ?? false) !== filters.archivedOnly) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
// `mine`: the caller's own rows (owner === caller.user).
|
|
265
|
+
if (filters.mine === true && meta?.owner !== caller.user)
|
|
266
|
+
continue;
|
|
267
|
+
// The owner filter (the bridge's shape — any named owner).
|
|
268
|
+
if (filters.owner !== undefined && meta?.owner !== filters.owner) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const name = meta?.name ?? meta?.autoName;
|
|
272
|
+
conversations.push({
|
|
273
|
+
id: record.id,
|
|
274
|
+
root: record.id === rootId,
|
|
275
|
+
access,
|
|
276
|
+
...(meta?.sharing === undefined ? {} : { sharing: meta.sharing }),
|
|
277
|
+
...(meta?.owner === undefined ? {} : { owner: meta.owner }),
|
|
278
|
+
...(name === undefined ? {} : { name }),
|
|
279
|
+
...(meta?.source === undefined ? {} : { source: meta.source }),
|
|
280
|
+
...(meta?.lastActivityAt === undefined
|
|
281
|
+
? {}
|
|
282
|
+
: { lastActivityAt: meta.lastActivityAt }),
|
|
283
|
+
// Fork lineage from the durable record (the scan page item);
|
|
284
|
+
// a root or plainly created conversation has none.
|
|
285
|
+
...(record.parent === undefined
|
|
286
|
+
? {}
|
|
287
|
+
: {
|
|
288
|
+
parent: {
|
|
289
|
+
id: record.parent.conversationId,
|
|
290
|
+
at: record.parent.at,
|
|
291
|
+
},
|
|
292
|
+
}),
|
|
293
|
+
archived: meta?.archived ?? false,
|
|
294
|
+
pinned: meta?.pinned ?? false,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
cursor = page.next;
|
|
298
|
+
} while (cursor !== undefined);
|
|
299
|
+
return conversations;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Read one conversation's session meta: the committed snapshot, or the
|
|
303
|
+
* defaults when the doc was never written. A read NEVER materializes the
|
|
304
|
+
* doc (defaults are free; a write-per-read would churn a doc revision on
|
|
305
|
+
* every sidebar poll).
|
|
306
|
+
*/
|
|
307
|
+
export async function readMetaOp(opened, id) {
|
|
308
|
+
return ((await opened.harness.snapshot(SessionMetaDoc, id, BACKGROUND_CONTEXT)) ??
|
|
309
|
+
DEFAULT_SESSION_META);
|
|
310
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host event hub (core hardening W3 Task 4, spec §7.4): the single
|
|
3
|
+
* in-process fan-out point for `HostEventWire` events. Boot creates ONE
|
|
4
|
+
* hub, subscribes it to the event bus's `onHostEvent` (commit-derived
|
|
5
|
+
* conversation/run/interaction/ambient events) and the provider refresh
|
|
6
|
+
* paths' `onReadinessChange` callback (`providers.changed`), and exposes
|
|
7
|
+
* it on `OpenedHarness.events`.
|
|
8
|
+
*
|
|
9
|
+
* The hub itself is deliberately tiny — a publish/subscribe registry.
|
|
10
|
+
* The listener contract mirrors the bus's: `publish` invokes subscribers
|
|
11
|
+
* synchronously in install order; a throwing subscriber is logged and
|
|
12
|
+
* the rest still receive the event (one bad consumer must never wedge
|
|
13
|
+
* the commit-observation path upstream of it). Delivery concern for the
|
|
14
|
+
* SSE route's filtering (a per-event async SessionMeta re-snapshot) lives
|
|
15
|
+
* in the ROUTE's own side lane, not here — the hub never blocks.
|
|
16
|
+
*/
|
|
17
|
+
export class EventHub {
|
|
18
|
+
#listeners = [];
|
|
19
|
+
/** Fan one event out to every subscriber (install order). Never throws. */
|
|
20
|
+
publish(event) {
|
|
21
|
+
for (const listener of this.#listeners) {
|
|
22
|
+
try {
|
|
23
|
+
listener(event);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
console.error("events hub: subscriber failed:", error);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Subscribe; returns the unsubscribe (identity-guarded, first match). */
|
|
31
|
+
subscribe(listener) {
|
|
32
|
+
this.#listeners.push(listener);
|
|
33
|
+
return () => {
|
|
34
|
+
const index = this.#listeners.indexOf(listener);
|
|
35
|
+
if (index >= 0)
|
|
36
|
+
this.#listeners.splice(index, 1);
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The events-stream wire types (core hardening W3 Task 4, spec §7.4): the
|
|
3
|
+
* canonical declarations live in `@sigrun/shared` (`shared/src/views.ts`,
|
|
4
|
+
* `HostEventWire`); this module re-exports them for server-side importers
|
|
5
|
+
* so `src/server/events/*` and `src/server/routes/events.ts` share one
|
|
6
|
+
* import root.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|