@sigrun/sigrun 0.0.0-stage → 0.1.1

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.
Files changed (130) hide show
  1. package/CHANGELOG.md +214 -0
  2. package/CONTEXT.md +163 -0
  3. package/LICENSE +21 -0
  4. package/README.md +691 -2
  5. package/THIRD_PARTY_NOTICES.md +28 -0
  6. package/dist/backup/marker.js +80 -0
  7. package/dist/backup/run-backup.js +339 -0
  8. package/dist/cli.js +952 -0
  9. package/dist/commands/expand.js +245 -0
  10. package/dist/commands/templates.js +272 -0
  11. package/dist/config.js +1146 -0
  12. package/dist/extension.d.ts +431 -0
  13. package/dist/harness/activity.js +42 -0
  14. package/dist/harness/ambient.js +261 -0
  15. package/dist/harness/auth.js +130 -0
  16. package/dist/harness/boot.js +865 -0
  17. package/dist/harness/builtins.js +191 -0
  18. package/dist/harness/event-bus.js +1203 -0
  19. package/dist/harness/health.js +30 -0
  20. package/dist/harness/identity/index.js +37 -0
  21. package/dist/harness/identity/invites.js +94 -0
  22. package/dist/harness/identity/json-store.js +108 -0
  23. package/dist/harness/identity/setup-code.js +81 -0
  24. package/dist/harness/identity/tokens.js +101 -0
  25. package/dist/harness/identity/users.js +93 -0
  26. package/dist/harness/interactions.js +283 -0
  27. package/dist/harness/loader.js +1236 -0
  28. package/dist/harness/memory-default.js +35 -0
  29. package/dist/harness/naming.js +193 -0
  30. package/dist/harness/packages.js +1002 -0
  31. package/dist/harness/providers/auth-bridge.js +60 -0
  32. package/dist/harness/providers/index.js +277 -0
  33. package/dist/harness/providers/models-store.js +105 -0
  34. package/dist/harness/providers/refresh.js +161 -0
  35. package/dist/harness/providers/scheduler.js +89 -0
  36. package/dist/harness/registry.js +9 -0
  37. package/dist/harness/reserved-sections.js +24 -0
  38. package/dist/harness/seam-host.js +239 -0
  39. package/dist/harness/sections/context-files.js +78 -0
  40. package/dist/harness/sections/model-guidance.js +77 -0
  41. package/dist/harness/settings.js +1053 -0
  42. package/dist/harness/stores.js +450 -0
  43. package/dist/harness/tools.js +500 -0
  44. package/dist/harness/workspace.js +73 -0
  45. package/dist/index.js +39 -0
  46. package/dist/jobs/docs.js +257 -0
  47. package/dist/jobs/fire.js +342 -0
  48. package/dist/jobs/index.js +228 -0
  49. package/dist/jobs/observe.js +165 -0
  50. package/dist/jobs/reconcile.js +245 -0
  51. package/dist/jobs/schedule.js +77 -0
  52. package/dist/jobs/spec.js +248 -0
  53. package/dist/jobs/task.js +190 -0
  54. package/dist/jobs/transfer.js +53 -0
  55. package/dist/serve.js +164 -0
  56. package/dist/server/auth/claim-lock.js +28 -0
  57. package/dist/server/auth/ip.js +51 -0
  58. package/dist/server/auth/login-limiter.js +52 -0
  59. package/dist/server/auth/loopback.js +47 -0
  60. package/dist/server/auth/pipeline.js +330 -0
  61. package/dist/server/auth/principal.js +26 -0
  62. package/dist/server/commands.js +198 -0
  63. package/dist/server/content-parts.js +142 -0
  64. package/dist/server/context.js +13 -0
  65. package/dist/server/conversation-handler.js +61 -0
  66. package/dist/server/conversation-ops.js +310 -0
  67. package/dist/server/events/hub.js +39 -0
  68. package/dist/server/events/types.js +8 -0
  69. package/dist/server/helpers.js +351 -0
  70. package/dist/server/host-bridge.js +131 -0
  71. package/dist/server/index.js +232 -0
  72. package/dist/server/notifications.js +185 -0
  73. package/dist/server/ownership.js +67 -0
  74. package/dist/server/readiness.js +36 -0
  75. package/dist/server/routes/auth.js +172 -0
  76. package/dist/server/routes/conversations.js +341 -0
  77. package/dist/server/routes/events.js +161 -0
  78. package/dist/server/routes/extension-routes.js +127 -0
  79. package/dist/server/routes/extensions.js +168 -0
  80. package/dist/server/routes/files.js +308 -0
  81. package/dist/server/routes/interactions.js +260 -0
  82. package/dist/server/routes/jobs.js +271 -0
  83. package/dist/server/routes/meta.js +146 -0
  84. package/dist/server/routes/metrics.js +219 -0
  85. package/dist/server/routes/onboarding.js +268 -0
  86. package/dist/server/routes/probes.js +53 -0
  87. package/dist/server/routes/providers.js +198 -0
  88. package/dist/server/routes/settings.js +48 -0
  89. package/dist/server/routes/stream.js +131 -0
  90. package/dist/server/routes/submissions.js +317 -0
  91. package/dist/server/routes/users.js +169 -0
  92. package/dist/server/schema-fixture.js +950 -0
  93. package/dist/server/session-meta.js +247 -0
  94. package/dist/server/static.js +169 -0
  95. package/dist/server/views.js +71 -0
  96. package/dist/skills/index.js +418 -0
  97. package/node_modules/@sigrun/shared/CHANGELOG.md +16 -0
  98. package/node_modules/@sigrun/shared/LICENSE +21 -0
  99. package/node_modules/@sigrun/shared/README.md +10 -0
  100. package/node_modules/@sigrun/shared/dist/access.d.ts +55 -0
  101. package/node_modules/@sigrun/shared/dist/access.js +72 -0
  102. package/node_modules/@sigrun/shared/dist/ambient.d.ts +111 -0
  103. package/node_modules/@sigrun/shared/dist/ambient.js +23 -0
  104. package/node_modules/@sigrun/shared/dist/commands.d.ts +75 -0
  105. package/node_modules/@sigrun/shared/dist/commands.js +25 -0
  106. package/node_modules/@sigrun/shared/dist/conversations.d.ts +31 -0
  107. package/node_modules/@sigrun/shared/dist/conversations.js +94 -0
  108. package/node_modules/@sigrun/shared/dist/interactions.d.ts +91 -0
  109. package/node_modules/@sigrun/shared/dist/interactions.js +21 -0
  110. package/node_modules/@sigrun/shared/dist/jobs.d.ts +100 -0
  111. package/node_modules/@sigrun/shared/dist/jobs.js +19 -0
  112. package/node_modules/@sigrun/shared/dist/json.d.ts +20 -0
  113. package/node_modules/@sigrun/shared/dist/json.js +13 -0
  114. package/node_modules/@sigrun/shared/dist/session-meta.d.ts +103 -0
  115. package/node_modules/@sigrun/shared/dist/session-meta.js +13 -0
  116. package/node_modules/@sigrun/shared/dist/settings.d.ts +401 -0
  117. package/node_modules/@sigrun/shared/dist/settings.js +19 -0
  118. package/node_modules/@sigrun/shared/dist/themes.d.ts +50 -0
  119. package/node_modules/@sigrun/shared/dist/themes.js +156 -0
  120. package/node_modules/@sigrun/shared/dist/turns.d.ts +102 -0
  121. package/node_modules/@sigrun/shared/dist/turns.js +166 -0
  122. package/node_modules/@sigrun/shared/dist/view-delta.d.ts +54 -0
  123. package/node_modules/@sigrun/shared/dist/view-delta.js +135 -0
  124. package/node_modules/@sigrun/shared/dist/views.d.ts +742 -0
  125. package/node_modules/@sigrun/shared/dist/views.js +38 -0
  126. package/node_modules/@sigrun/shared/package.json +83 -0
  127. package/package.json +89 -5
  128. package/webui/dist/assets/index-Rl0KhCLa.css +1 -0
  129. package/webui/dist/assets/index-rOiARZLF.js +50 -0
  130. package/webui/dist/index.html +37 -0
@@ -0,0 +1,431 @@
1
+ /**
2
+ * The documented extension contract (core hardening W5 Task 1, spec
3
+ * §9.1): the ONE type surface an extension package programs against —
4
+ * `@sigrun/sigrun/extension`, a TYPES-ONLY subpath of the main package (the
5
+ * 2026-10-13 types-in-main-package ruling; there is no separate
6
+ * contract package).
7
+ *
8
+ * Authors consume it with `import type` only:
9
+ *
10
+ * ```ts
11
+ * import type { SigrunExtensionResult, SigrunHost } from "@sigrun/sigrun/extension";
12
+ * ```
13
+ *
14
+ * The subpath has NO runtime entry: nothing loads when an extension is
15
+ * imported, and importing it at runtime fails loud by design. See
16
+ * docs/extending.md ("The factory result") for the field table and the
17
+ * host rule.
18
+ *
19
+ * Module discipline (load-bearing, pinned by test/extension-contract.test.ts):
20
+ *
21
+ * - TYPES-ONLY and self-contained — no runtime code, no re-exported
22
+ * values. The emitted `dist/extension.d.ts` must stand alone.
23
+ * - `import type` from EXACTLY three roots: `@earendil-works/pi-durable`,
24
+ * `@earendil-works/pi-ai`, and `@sigrun/shared/*`. These are the
25
+ * packages an extension declares as devDependencies (the loader links
26
+ * the first two's host copies into the packages dir); anything else
27
+ * would leak a new dependency onto every contract consumer. Notably
28
+ * the invocation `Context` is NOT imported from
29
+ * `@earendil-works/chord` — the contract spells its own structural
30
+ * {@link HandlerContext} instead.
31
+ * - The loader (src/harness/loader.ts) references these types for its
32
+ * validated candidate shape and a compile-time key assertion, so the
33
+ * implementation and this contract cannot drift silently.
34
+ */
35
+ import type { AssistantMessage, Provider } from "@earendil-works/pi-ai";
36
+ import type { ConversationDocToken, ConversationId, Cursor, EntryId, EntryQuery, EntryRecord, Extension, InputSubmissionDraft, JsonObject, ModelRef, Page, SubmissionId } from "@earendil-works/pi-durable";
37
+ import type { Access, Sharing } from "@sigrun/shared/access";
38
+ import type { AmbientBody, AmbientPlace, AmbientRecord } from "@sigrun/shared/ambient";
39
+ import type { InteractionAnswer, InteractionRecord } from "@sigrun/shared/interactions";
40
+ import type { JobSpec } from "@sigrun/shared/jobs";
41
+ import type { ConversationSummary, ConversationViewWire, HealthStatus, SessionMeta, SubmissionRecordWire, UserWire as DirectoryUserWire } from "@sigrun/shared/views";
42
+ export type { Access, AmbientRecord, DirectoryUserWire, InteractionRecord, JobSpec, Sharing, };
43
+ /**
44
+ * The invocation-scoped context handlers and host APIs receive: a
45
+ * structural view of the runtime context (its cancellation signal).
46
+ * Declared structurally — not imported from `@earendil-works/chord` —
47
+ * so this module stays inside the contract allowlist; the value the
48
+ * host passes is the full runtime context, which satisfies this shape.
49
+ */
50
+ export interface HandlerContext {
51
+ /** Aborts with the caller's cancellation; observe it in long work. */
52
+ readonly abortSignal: AbortSignal | undefined;
53
+ }
54
+ /**
55
+ * The extension-facing settled-run event: one per settled run, with the
56
+ * run's input texts host-resolved from the durable transcript.
57
+ * `inputTexts` is position-paired with `inputs` — an input whose entry
58
+ * is unresolvable (withdrawn while queued, withdrawn entry, read
59
+ * failure) contributes `""` at its position, never a reorder.
60
+ */
61
+ export type RunSettledEvent = {
62
+ readonly conversationId: ConversationId;
63
+ readonly inputs: readonly SubmissionId[];
64
+ /** Text blocks of each input's entry (`pi.user`, or an expanded input's
65
+ * `sigrun.user.expanded` — the ADMITTED text), concatenated;
66
+ * unresolvable/withdrawn → "" at that position. */
67
+ readonly inputTexts: readonly string[];
68
+ readonly outcome: {
69
+ readonly status: "answered";
70
+ readonly answerEntryId: EntryId;
71
+ readonly answer: AssistantMessage;
72
+ } | {
73
+ readonly status: "unanswered";
74
+ readonly reason: string;
75
+ };
76
+ };
77
+ /**
78
+ * The narrowed read surface a `runSettled` handler receives: exactly
79
+ * `entry` / `entries` / `document` over the settled conversation — no
80
+ * `taskId`, no `memo`, no `conversationId` (the event carries those).
81
+ * `entries` mirrors the conversation's entry history (the conversation
82
+ * is implicit); `document` is the committed conversation-doc read.
83
+ */
84
+ export type MemoryEventApi = {
85
+ /** One visible entry of the settled conversation by exact id (`undefined` when absent). */
86
+ entry(id: EntryId, context: HandlerContext): Promise<EntryRecord | undefined>;
87
+ /** Fork-aware entry history of the settled conversation. */
88
+ entries(query: Omit<EntryQuery, "conversationId">, limit: number, cursor: Cursor | undefined, context: HandlerContext): Promise<Page<EntryRecord, Cursor>>;
89
+ /** Committed conversation-document read. */
90
+ document<T extends JsonObject>(token: ConversationDocToken<T>, conversationId: ConversationId, context: HandlerContext): Promise<Readonly<T> | undefined>;
91
+ };
92
+ /**
93
+ * The extension-facing payloads of the lifecycle events: the handler
94
+ * NAME discriminates (like pi-durable hooks), so the payloads carry no
95
+ * `type` field.
96
+ */
97
+ export type ExtensionLifecyclePayload = {
98
+ conversationCreated: {
99
+ readonly conversationId: ConversationId;
100
+ };
101
+ conversationForked: {
102
+ readonly childId: ConversationId;
103
+ readonly parentId: ConversationId;
104
+ readonly at: EntryId;
105
+ };
106
+ conversationArchived: {
107
+ readonly conversationId: ConversationId;
108
+ readonly archived: boolean;
109
+ };
110
+ conversationIdle: {
111
+ readonly conversationId: ConversationId;
112
+ readonly idleMs: number;
113
+ };
114
+ };
115
+ /** A factory-result `runSettled` handler: called once per settled run, deadline-bounded. */
116
+ export type ExtensionRunSettledHandler = (event: RunSettledEvent, api: MemoryEventApi, context: HandlerContext) => void | Promise<void>;
117
+ /** A factory-result lifecycle handler for one event, deadline-bounded. */
118
+ export type ExtensionLifecycleHandler<K extends keyof ExtensionLifecyclePayload> = (event: ExtensionLifecyclePayload[K], api: MemoryEventApi, context: HandlerContext) => void | Promise<void>;
119
+ /** A factory-result `harnessShutdown` handler (no event payload — shutdown is conversation-less). */
120
+ export type ExtensionHarnessShutdownHandler = (api: MemoryEventApi, context: HandlerContext) => void | Promise<void>;
121
+ /**
122
+ * The `userDeleted` payload (core hardening W1): which directory user
123
+ * left and who received their resources. Emitted after the ownership
124
+ * transfer, token revocation, and record removal have landed.
125
+ */
126
+ export interface UserDeletedPayload {
127
+ readonly user: string;
128
+ readonly transferTo: string;
129
+ }
130
+ /** A factory-result `userDeleted` handler (the transfer counterpart of harnessShutdown). */
131
+ export type ExtensionUserDeletedHandler = (event: UserDeletedPayload, api: MemoryEventApi, context: HandlerContext) => void | Promise<void>;
132
+ /** The lifecycle handler fields of the factory result, one per event. */
133
+ export type SigrunLifecycleHandlers = {
134
+ readonly conversationCreated?: ExtensionLifecycleHandler<"conversationCreated">;
135
+ readonly conversationForked?: ExtensionLifecycleHandler<"conversationForked">;
136
+ readonly conversationArchived?: ExtensionLifecycleHandler<"conversationArchived">;
137
+ readonly conversationIdle?: ExtensionLifecycleHandler<"conversationIdle">;
138
+ readonly harnessShutdown?: ExtensionHarnessShutdownHandler;
139
+ /** The user-removal transfer event; direct fan-out, not commit-derived. */
140
+ readonly userDeleted?: ExtensionUserDeletedHandler;
141
+ };
142
+ /**
143
+ * The lifecycle event names a worker's `wakeOn` may declare.
144
+ * `harnessShutdown` is excluded — workers stop BEFORE the shutdown
145
+ * handlers run (the shutdown ordering), so waking on it is meaningless.
146
+ */
147
+ export type WorkerWakeEvent = keyof ExtensionLifecyclePayload;
148
+ /** The wake argument of an event-triggered worker run. */
149
+ export type WorkerWake = {
150
+ readonly event: string;
151
+ readonly conversationId: ConversationId;
152
+ };
153
+ /**
154
+ * One factory-declared idle worker (the curator contract). Host-owned
155
+ * lifecycle: sigrun taps the lifecycle fan-out for `wakeOn` matches.
156
+ * One run at a time per worker; a wake during an in-flight run is
157
+ * queued + coalesced (one pending-wake slot, last wins) and delivered
158
+ * as the next run's `wake`.
159
+ *
160
+ * `every` is RETIRED (ruling 12, 2026-10-19): this type has NO `every`
161
+ * field — jobs are the only time-based scheduler; an extension that
162
+ * needs a timer declares a job with a `task` action.
163
+ */
164
+ export interface ExtensionWorker {
165
+ /** Unique within the extension; surfaces in /v1/meta. */
166
+ readonly name: string;
167
+ /** Lifecycle events that wake the worker (REQUIRED, non-empty). */
168
+ readonly wakeOn: readonly WorkerWakeEvent[];
169
+ /** The work; rejects (or overruns the worker deadline) to fail the run. */
170
+ run(context: HandlerContext, wake?: WorkerWake): Promise<void>;
171
+ }
172
+ /** Three-state per-provider readiness: not configured, degraded, or ready. */
173
+ export type ProviderReadinessStatus = "not_configured" | "degraded" | "ready";
174
+ /** One readiness report: the status plus optional operator-facing detail. */
175
+ export interface ProviderReadiness {
176
+ readonly status: ProviderReadinessStatus;
177
+ readonly detail?: string;
178
+ }
179
+ /**
180
+ * A plugin's per-provider readiness tracker: a synchronous per-request
181
+ * read over plugin-owned state (never network, never storage) — the
182
+ * three-state source `/v1/auth/check` reports verbatim.
183
+ */
184
+ export type ReadinessTracker = () => ProviderReadiness;
185
+ /** One provider contribution: a pi-ai `createProvider()` result plus optional boot hints. */
186
+ export interface ProviderContribution {
187
+ readonly provider: Provider;
188
+ /**
189
+ * This contributor's proposed default model. Ignored when a default
190
+ * model is configured; the last contributor to propose one wins
191
+ * otherwise.
192
+ */
193
+ readonly defaultModel?: ModelRef;
194
+ /** The per-provider readiness tracker (`/v1/auth/check` reports it verbatim). */
195
+ readonly readiness?: ReadinessTracker;
196
+ }
197
+ /** One contributed webui bundle: a selection name plus the bundle root directory. */
198
+ export interface WebuiBundleContribution {
199
+ /** The selection key (`server.webui.bundle`); must match `^[a-z][a-z0-9_-]*$`. */
200
+ readonly name: string;
201
+ /** The bundle root (the directory containing `index.html`); relative values anchor at the extension ENTRY's directory. */
202
+ readonly dir: string;
203
+ }
204
+ /**
205
+ * The resolved caller (core hardening W1): what the auth pipeline
206
+ * stamps on every request. `method` names the proof — the empty-method
207
+ * naive path (`"none"`), a verified token (`"token"`), or a trusted
208
+ * forward-auth header set (`"proxy"`).
209
+ */
210
+ export interface Principal {
211
+ /** The directory username; `undefined` under the naive method with no header. */
212
+ readonly user: string | undefined;
213
+ readonly name?: string;
214
+ readonly email?: string;
215
+ readonly admin: boolean;
216
+ /** True for the `SIGRUN_API_TOKEN` service principal. */
217
+ readonly service: boolean;
218
+ readonly method: "none" | "token" | "proxy";
219
+ }
220
+ /** One extension HTTP route, mounted at `/v1/ext/<slug>/<path>`. */
221
+ export interface ExtensionRouteContribution {
222
+ readonly method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
223
+ /** Relative; mounted at `/v1/ext/<slug>/<path>`. */
224
+ readonly path: string;
225
+ readonly auth: "user" | "admin" | "public";
226
+ /** Request body ceiling in bytes (default 1 MiB). */
227
+ readonly bodyLimit?: number;
228
+ /** Web-standard `Request`/`Response` (Node ≥ 22) — no server type crosses the contract. */
229
+ handler(request: Request, ctx: {
230
+ readonly principal?: Principal;
231
+ readonly host: SigrunHost;
232
+ }): Promise<Response>;
233
+ }
234
+ /** One extension slash command, invoked via `POST /v1/conversations/:id/commands/:name`. */
235
+ export interface ExtensionCommandContribution {
236
+ /** Must match `^[a-z][a-z0-9-]*$`; core command names are reserved. */
237
+ readonly name: string;
238
+ readonly description: string;
239
+ /** Argument hint for the composer's `/` menu. */
240
+ readonly args?: string;
241
+ run(ctx: {
242
+ readonly conversationId: ConversationId;
243
+ readonly args: string;
244
+ readonly principal: Principal;
245
+ readonly host: SigrunHost;
246
+ }): Promise<{
247
+ readonly notice?: string;
248
+ } | void>;
249
+ }
250
+ /** One notification core delivers to every registered notifier (best-effort). */
251
+ export interface NotificationEvent {
252
+ readonly type: "interaction.pending" | "job.failed" | "run.settled";
253
+ readonly conversationId?: number;
254
+ readonly jobId?: string;
255
+ /** The recipient. */
256
+ readonly user: string | undefined;
257
+ readonly title: string;
258
+ readonly body: string;
259
+ /** `${server.publicUrl}/#/c/<conversationId>` when `server.publicUrl` is set; absent otherwise. */
260
+ readonly url?: string;
261
+ }
262
+ /** One notifier transport (ntfy, Signal, web push — core ships none). */
263
+ export interface NotifierContribution {
264
+ readonly name: string;
265
+ notify(event: NotificationEvent, ctx: {
266
+ readonly host: SigrunHost;
267
+ }): Promise<void>;
268
+ }
269
+ /** One wire part of a `host.conversations.submit` input array. */
270
+ export type SubmissionInputPart = {
271
+ readonly type: "text";
272
+ readonly text: string;
273
+ } | {
274
+ readonly type: "image";
275
+ readonly path: string;
276
+ readonly mime?: string;
277
+ } | {
278
+ readonly type: "file";
279
+ readonly path: string;
280
+ readonly mime?: string;
281
+ };
282
+ /** How a submit behaves when a run is already live (pi-durable's `whenBusy`). */
283
+ export type WhenBusyMode = NonNullable<InputSubmissionDraft["whenBusy"]>;
284
+ /**
285
+ * The host context sigrun hands an extension factory: the scoped
286
+ * extension stores, the interaction/ambient seam surfaces, and the
287
+ * conversation/identity/access APIs.
288
+ *
289
+ * The store surface is safe at any time (plain file I/O); the seam and
290
+ * conversation surfaces bind the harness lazily — call them from tool
291
+ * execute, hooks, routes, commands, workers, and jobs, never from a
292
+ * factory body.
293
+ */
294
+ export interface SigrunHost {
295
+ /** The scoped extension-stores handle (your secrets/settings sections + raw data/cache/state paths). */
296
+ readonly stores: {
297
+ /** Your section of secrets.json. */
298
+ readonly secrets: {
299
+ get(key: string): Promise<string | undefined>;
300
+ set(key: string, value: string): Promise<void>;
301
+ };
302
+ /** Your settings/<slug>.json (absent reads as {}). */
303
+ readonly settings: {
304
+ get(): Promise<Record<string, unknown>>;
305
+ set(value: Record<string, unknown>): Promise<void>;
306
+ };
307
+ /** Your raw directories under the data/state/cache roots. */
308
+ readonly paths: {
309
+ readonly data: string;
310
+ readonly cache: string;
311
+ readonly state: string;
312
+ };
313
+ };
314
+ /** Seed (or fully replace) one interaction record; seeding a terminal record is a no-op. */
315
+ seedInteraction(conversationId: ConversationId, record: InteractionRecord, context: HandlerContext): Promise<void>;
316
+ /** Answer a pending interaction from the extension side (the tool path). */
317
+ answerInteraction(conversationId: ConversationId, callId: string, answer: InteractionAnswer, context: HandlerContext): Promise<boolean>;
318
+ /** Mark a pending interaction expired/cancelled/skipped (extension-owned semantics). */
319
+ settleInteraction(conversationId: ConversationId, callId: string, status: "skipped" | "expired" | "cancelled", context: HandlerContext): Promise<boolean>;
320
+ /** Read one interaction record (the poll loop's read). */
321
+ snapshotInteraction(conversationId: ConversationId, callId: string, context: HandlerContext): Promise<Readonly<InteractionRecord> | undefined>;
322
+ /** Fully replace one ambient surface's state (write = replacement); the key must be unowned or yours. */
323
+ setAmbient(conversationId: ConversationId, record: {
324
+ readonly key: string;
325
+ readonly place: AmbientPlace;
326
+ readonly maxRows?: number;
327
+ readonly overflow?: "drop" | "wrap" | "summarize";
328
+ readonly refreshMs?: number;
329
+ readonly body?: AmbientBody;
330
+ readonly details?: JsonObject;
331
+ }, context: HandlerContext): Promise<void>;
332
+ /** Clear one ambient surface (empty replacement; keeps ownership). */
333
+ clearAmbient(conversationId: ConversationId, key: string, context: HandlerContext): Promise<void>;
334
+ /**
335
+ * Drive conversations in-process (channel bridges, jobs) over the
336
+ * same code paths as the HTTP API. Extensions are trusted in-process
337
+ * code: they may act for any user, and mapping a channel's users to
338
+ * sigrun users is the extension's own config.
339
+ */
340
+ readonly conversations: {
341
+ create(opts: {
342
+ readonly owner?: string;
343
+ readonly sharing?: Sharing;
344
+ readonly name?: string;
345
+ readonly source?: string;
346
+ }): Promise<ConversationId>;
347
+ submit(id: ConversationId, input: readonly SubmissionInputPart[], opts?: {
348
+ readonly user?: string;
349
+ readonly requestId?: string;
350
+ readonly whenBusy?: WhenBusyMode;
351
+ }): Promise<SubmissionRecordWire>;
352
+ watch(id: ConversationId, listener: (view: ConversationViewWire) => void): Promise<() => Promise<void>>;
353
+ list(opts?: {
354
+ readonly owner?: string;
355
+ readonly archived?: boolean;
356
+ }): Promise<readonly ConversationSummary[]>;
357
+ meta(id: ConversationId): Promise<SessionMeta>;
358
+ };
359
+ /** The user directory (admin wire form; emails are admin material). */
360
+ readonly users: {
361
+ list(): Promise<readonly DirectoryUserWire[]>;
362
+ get(user: string): Promise<DirectoryUserWire | undefined>;
363
+ };
364
+ /**
365
+ * Resolve one principal's access to one shared resource (the whole
366
+ * sharing contract for extension-owned records — core stores nothing
367
+ * for them).
368
+ */
369
+ readonly access: {
370
+ accessOf(principal: Principal | {
371
+ readonly user: string;
372
+ }, resource: {
373
+ readonly owner?: string;
374
+ readonly sharing?: Sharing;
375
+ }): Promise<Access>;
376
+ };
377
+ /** The conversation's owner — who the agent acts for. */
378
+ readonly identity: {
379
+ actingUser(conversationId: ConversationId): Promise<string | undefined>;
380
+ };
381
+ }
382
+ /**
383
+ * The documented factory-result contract: EVERY field an extension may
384
+ * return from its default-export factory. Every field is optional —
385
+ * a minimal extension returns only `extension`; a contributor-only
386
+ * result (`providers`/`webuiBundles` without `extension`) identifies
387
+ * itself by `name`.
388
+ *
389
+ * `routes`, `commands`, `skills`, and `notifiers` are declared here
390
+ * ahead of their implementations (W5 Tasks 2, 3, 4, and 6); the loader
391
+ * begins reading each when its task lands.
392
+ */
393
+ export interface SigrunExtensionResult extends SigrunLifecycleHandlers {
394
+ /** The pi-durable `Extension` to install; its declared name is the slug source. */
395
+ readonly extension?: Extension;
396
+ /** The contributor-only identity (required when contributing without an `extension`). */
397
+ readonly name?: string;
398
+ /** Provider contributions, merged into the boot model catalog (last-registered-wins, collisions recorded). */
399
+ readonly providers?: readonly ProviderContribution[];
400
+ /** Webui-bundle contributions; each `dir` anchors at the extension ENTRY's directory. */
401
+ readonly webuiBundles?: readonly WebuiBundleContribution[];
402
+ /** The surface's health producer, aggregated into `/v1/meta` `health` (any `"degraded"` degrades). */
403
+ readonly health?: () => HealthStatus;
404
+ /** Called once per settled run with the host-resolved input texts; deadline-bounded. */
405
+ readonly runSettled?: ExtensionRunSettledHandler;
406
+ /** Declared interaction kinds (the seam's write gate + capability inventory). */
407
+ readonly interactionKinds?: readonly string[];
408
+ /** Declared ambient surface keys (the seam's write gate + capability inventory). */
409
+ readonly ambientKinds?: readonly string[];
410
+ /** Idle workers (the curator contract — `wakeOn` only, `every` retired). */
411
+ readonly workers?: readonly ExtensionWorker[];
412
+ /** Declared jobs (the only time-based scheduler); read-only through the API under `ext:<slug>:<name>` ids. */
413
+ readonly jobs?: readonly JobSpec[];
414
+ /** HTTP routes mounted at `/v1/ext/<slug>/<path>` (lands in Task 2). */
415
+ readonly routes?: readonly ExtensionRouteContribution[];
416
+ /** Slash commands merged into `/v1/commands` (lands in Task 3). */
417
+ readonly commands?: readonly ExtensionCommandContribution[];
418
+ /** Absolute skill directories contributed by this extension (lands in Task 4). */
419
+ readonly skills?: readonly string[];
420
+ /** Notification transports (lands in Task 6). */
421
+ readonly notifiers?: readonly NotifierContribution[];
422
+ }
423
+ /**
424
+ * The extension factory: sigrun calls it at load, passing the host
425
+ * context. THE HOST RULE: the first parameter is the host, never a
426
+ * defaulted options object — sigrun passes the host only when
427
+ * `factory.length > 0`, so a factory that wants the host must DECLARE
428
+ * the parameter (a length-0 factory may carry a defaulted first
429
+ * parameter that would consume the host as a real dependency).
430
+ */
431
+ export type SigrunExtensionFactory = (host: SigrunHost) => SigrunExtensionResult | Promise<SigrunExtensionResult>;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Activity-time stamping (UI refresh Task A1): the `lastActivityAt` seam.
3
+ *
4
+ * One server-owned session-meta field, stamped at exactly two moments —
5
+ * submission admission (the submissions route, so a sidebar sorts by
6
+ * activity the moment a prompt lands, before the run settles) and run
7
+ * settlement (the event bus's settled hook, so background runs — tasks,
8
+ * tools, other processes — stamp too). Both write through ONE
9
+ * `conversation.commit` setting `lastActivityAt = now().toISOString()`;
10
+ * the `now` seam is injectable so tests pin the instant.
11
+ *
12
+ * The stamp's only reader is the conversation listing
13
+ * (`ConversationSummary.lastActivityAt`) — never the wire view (the view
14
+ * mounts pi-durable's built-in docs only, so the schema fixture is
15
+ * unaffected by this write).
16
+ */
17
+ import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
18
+ import { SessionMetaDoc } from "../server/session-meta.js";
19
+ /**
20
+ * Stamp `lastActivityAt` on one conversation's session-meta doc. A missing
21
+ * conversation is a no-op (the bus observes settles for conversations this
22
+ * process may no longer resolve — a dropped-elsewhere id must not fault
23
+ * the drain loop).
24
+ */
25
+ export async function touchActivity(harness, conversationId, now, context) {
26
+ const conversation = await harness.conversation(conversationId, context);
27
+ if (conversation === undefined)
28
+ return;
29
+ await conversation.commit(async (tx) => {
30
+ const draft = await tx.doc(SessionMetaDoc, conversationId);
31
+ draft.lastActivityAt = now().toISOString();
32
+ }, context);
33
+ }
34
+ /**
35
+ * Wire the settle-side stamping: every settled run stamps its
36
+ * conversation. Registered right after `EventBus.open` (before extension
37
+ * registrations — host-owned machinery runs first); returns the
38
+ * unsubscribe (the bus's own close stops delivery regardless).
39
+ */
40
+ export function wireActivity(bus, harness, now = () => new Date()) {
41
+ return bus.onSettled((group) => touchActivity(harness, group.conversationId, now, BACKGROUND_CONTEXT));
42
+ }