@openchamber/sdk 2.5.3 → 2.6.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.
@@ -143,7 +143,8 @@ export const sourceControlProviderTimeoutMs = (method) => {
143
143
  /**
144
144
  * Environment variable with the base URL of OpenChamber's callback listener
145
145
  * (`http://127.0.0.1:<port>`). Set only for a service that provides
146
- * `source-control`; `createSourceControlForge` reads it.
146
+ * `source-control`, `notifications` or `tracker`; `createSourceControlForge`,
147
+ * `createNotificationsOutbound` and `createTrackerOutbound` read it.
147
148
  */
148
149
  export const OPENCHAMBER_HOST_CALLBACK_URL = 'OPENCHAMBER_HOST_CALLBACK_URL';
149
150
  /** Route on the callback listener a service sends a forge request to. */
@@ -237,6 +238,48 @@ export class SourceControlForgeError extends Error {
237
238
  this.status = status;
238
239
  }
239
240
  }
241
+ /**
242
+ * One request through OpenChamber's callback listener: the handle as bearer,
243
+ * the request as JSON, the relayed answer back. `fail` makes the role's own
244
+ * error for a refusal, a silent host or a malformed relay; an answer of any
245
+ * status from the far side, 401 included, resolves.
246
+ */
247
+ const postOutbound = async (url, handle, request, timeoutMs, fail) => {
248
+ let response;
249
+ try {
250
+ response = await fetch(url, {
251
+ method: 'POST',
252
+ headers: { authorization: `Bearer ${handle}`, 'content-type': 'application/json' },
253
+ body: JSON.stringify(request),
254
+ // A little past OpenChamber's own deadline, so its answer wins.
255
+ signal: AbortSignal.timeout(timeoutMs + 5_000),
256
+ });
257
+ }
258
+ catch (error) {
259
+ throw fail(`OpenChamber did not answer: ${error instanceof Error ? error.message : String(error)}`, 'HOST_UNAVAILABLE', 0);
260
+ }
261
+ let answer;
262
+ try {
263
+ answer = Object(await response.json());
264
+ }
265
+ catch {
266
+ throw fail('OpenChamber answered with something other than JSON', 'HOST_UNAVAILABLE', response.status);
267
+ }
268
+ if (!response.ok) {
269
+ const message = answer.error?.message;
270
+ const code = answer.error?.code;
271
+ throw fail(String(message) === message && message.length > 0 ? message : `OpenChamber refused the request (${response.status})`, String(code) === code && code.length > 0 ? code : 'OUTBOUND_REFUSED', response.status);
272
+ }
273
+ if (!Number.isSafeInteger(answer.status) || String(answer.body) !== answer.body) {
274
+ throw fail('OpenChamber relayed a malformed answer', 'HOST_UNAVAILABLE', response.status);
275
+ }
276
+ const headers = {};
277
+ for (const [name, text] of Object.entries(Object(answer.headers ?? {}))) {
278
+ if (String(text) === text)
279
+ headers[name] = text;
280
+ }
281
+ return { status: Number(answer.status), headers, body: answer.body };
282
+ };
240
283
  /**
241
284
  * The service's way to the forge: OpenChamber's callback listener
242
285
  * (`OPENCHAMBER_HOST_CALLBACK_URL`), which checks the handle, keeps the request
@@ -250,41 +293,272 @@ export const createSourceControlForge = ({ env }) => {
250
293
  throw new Error(`${OPENCHAMBER_HOST_CALLBACK_URL} is not set: only a service that provides "source-control" can reach a forge.`);
251
294
  const url = `${base.replace(/\/+$/, '')}${SOURCE_CONTROL_OUTBOUND_PATH}`;
252
295
  return {
253
- async request(handle, request) {
254
- let response;
255
- try {
256
- response = await fetch(url, {
257
- method: 'POST',
258
- headers: { authorization: `Bearer ${handle}`, 'content-type': 'application/json' },
259
- body: JSON.stringify(request),
260
- // A little past OpenChamber's own deadline, so its answer wins.
261
- signal: AbortSignal.timeout(SOURCE_CONTROL_OUTBOUND_TIMEOUT_MS + 5_000),
262
- });
263
- }
264
- catch (error) {
265
- throw new SourceControlForgeError(`OpenChamber did not answer: ${error instanceof Error ? error.message : String(error)}`, 'HOST_UNAVAILABLE', 0);
266
- }
267
- let answer;
268
- try {
269
- answer = Object(await response.json());
270
- }
271
- catch {
272
- throw new SourceControlForgeError('OpenChamber answered with something other than JSON', 'HOST_UNAVAILABLE', response.status);
273
- }
274
- if (!response.ok) {
275
- const message = answer.error?.message;
276
- const code = answer.error?.code;
277
- throw new SourceControlForgeError(String(message) === message && message.length > 0 ? message : `OpenChamber refused the request (${response.status})`, String(code) === code && code.length > 0 ? code : 'OUTBOUND_REFUSED', response.status);
278
- }
279
- if (!Number.isSafeInteger(answer.status) || String(answer.body) !== answer.body) {
280
- throw new SourceControlForgeError('OpenChamber relayed a malformed forge answer', 'HOST_UNAVAILABLE', response.status);
281
- }
282
- const headers = {};
283
- for (const [name, text] of Object.entries(Object(answer.headers ?? {}))) {
284
- if (String(text) === text)
285
- headers[name] = text;
286
- }
287
- return { status: Number(answer.status), headers, body: answer.body };
288
- },
296
+ request: (handle, request) => postOutbound(url, handle, request, SOURCE_CONTROL_OUTBOUND_TIMEOUT_MS, (message, code, status) => new SourceControlForgeError(message, code, status)),
297
+ };
298
+ };
299
+ /*
300
+ * Notifications: a service with `provides: ["notifications"]` is the delivery
301
+ * channel `contributes.notifications` describes, next to the desktop and the
302
+ * phone. OpenChamber keeps the account and its token. Once the user connects
303
+ * one, every notification OpenChamber decides to show goes to the service at
304
+ * `NOTIFICATIONS_PROVIDER_PATH` as `notify`, and the service sends it on
305
+ * through OpenChamber's outbound proxy (`createNotificationsOutbound`), which
306
+ * pins the account's origin and adds the token. The service never sees a token.
307
+ */
308
+ /** Route on the service loopback OpenChamber posts every notifications call to. */
309
+ export const NOTIFICATIONS_PROVIDER_PATH = '/notifications';
310
+ /** The `v` of every request; a service answers a version it does not know with HTTP 400. */
311
+ export const NOTIFICATIONS_PROVIDER_WIRE_VERSION = 1;
312
+ /**
313
+ * `auth.verify` reads who a candidate token belongs to, `auth.probe` whether
314
+ * an instance is this channel's server, `notify` delivers one notification.
315
+ */
316
+ export const NOTIFICATIONS_PROVIDER_METHODS = ['auth.verify', 'auth.probe', 'notify'];
317
+ /** A notification is delivered within this deadline; a slower service drops it. */
318
+ export const NOTIFICATIONS_PROVIDER_NOTIFY_TIMEOUT_MS = 10_000;
319
+ export const NOTIFICATIONS_PROVIDER_AUTH_TIMEOUT_MS = 15_000;
320
+ /** Bytes of an answer OpenChamber reads. */
321
+ export const NOTIFICATIONS_PROVIDER_RESPONSE_MAX = 16_000;
322
+ /** Characters of `error.message` OpenChamber reads. */
323
+ export const NOTIFICATIONS_PROVIDER_ERROR_MESSAGE_MAX = 2_000;
324
+ /** No call for this long stops the service; the next call starts it again. */
325
+ export const NOTIFICATIONS_PROVIDER_IDLE_MS = 10 * 60_000;
326
+ /** Route on the callback listener a service sends the channel's requests to. */
327
+ export const NOTIFICATIONS_OUTBOUND_PATH = '/notifications/outbound';
328
+ /** One outbound request may take this long; the call's own deadline may end it sooner. */
329
+ export const NOTIFICATIONS_OUTBOUND_TIMEOUT_MS = 20_000;
330
+ export const notificationsProviderTimeoutMs = (method) => (method === 'notify' ? NOTIFICATIONS_PROVIDER_NOTIFY_TIMEOUT_MS : NOTIFICATIONS_PROVIDER_AUTH_TIMEOUT_MS);
331
+ /**
332
+ * The kinds OpenChamber notifies about. `ready`: the agent finished; `error`:
333
+ * the turn failed; `question` and `permission`: the agent waits for the user;
334
+ * `review`: an auto-review ended; the `goal_*` kinds: a session goal settled.
335
+ * Which kinds reach a channel follows the user's own notification toggles.
336
+ */
337
+ export const GUEST_NOTIFICATION_KINDS = ['ready', 'error', 'question', 'permission', 'review', 'goal_complete', 'goal_blocked', 'goal_budget'];
338
+ export const isNotificationsProviderMethod = (value) => (NOTIFICATIONS_PROVIDER_METHODS.some((method) => method === value));
339
+ /** `undefined` is an account that is neither `null` nor `{ id, login }`. */
340
+ const readNotificationsAccount = (account) => {
341
+ if (account === null)
342
+ return null;
343
+ if (Object(account) !== account || account === undefined)
344
+ return undefined;
345
+ const { id, login } = account;
346
+ if (String(id) !== id || id.length === 0)
347
+ return undefined;
348
+ if (login !== null && (String(login) !== login || login.length === 0))
349
+ return undefined;
350
+ return { id, login };
351
+ };
352
+ /**
353
+ * A service's read of the body OpenChamber posted to
354
+ * `NOTIFICATIONS_PROVIDER_PATH`. OpenChamber built `input`, so this checks the
355
+ * envelope, not every field, and the service bundle needs no schema library.
356
+ * `null` is a body that is not a notifications call of this wire version;
357
+ * answer it with HTTP 400.
358
+ */
359
+ export const readNotificationsProviderRequest = (body) => {
360
+ let wire;
361
+ try {
362
+ const parsed = JSON.parse(body);
363
+ if (Object(parsed) !== parsed || parsed === null)
364
+ return null;
365
+ wire = parsed;
366
+ }
367
+ catch {
368
+ return null;
369
+ }
370
+ const { v, requestId, method, provider, session, input } = wire;
371
+ if (v !== NOTIFICATIONS_PROVIDER_WIRE_VERSION)
372
+ return null;
373
+ if (String(requestId) !== requestId || requestId.length === 0)
374
+ return null;
375
+ if (String(provider) !== provider || provider.length === 0)
376
+ return null;
377
+ if (String(method) !== method || !isNotificationsProviderMethod(method))
378
+ return null;
379
+ if (Object(input) !== input || Array.isArray(input))
380
+ return null;
381
+ if (Object(session) !== session || session === null || session === undefined)
382
+ return null;
383
+ const { instance, handle } = session;
384
+ if (String(instance) !== instance || instance.length === 0 || String(handle) !== handle || handle.length === 0)
385
+ return null;
386
+ const account = readNotificationsAccount(session.account);
387
+ if (account === undefined)
388
+ return null;
389
+ // SAFETY: `input` is a plain object parsed from JSON, so every value in it is JSON.
390
+ return { v: NOTIFICATIONS_PROVIDER_WIRE_VERSION, requestId, method, provider, session: { instance, handle, account }, input: input };
391
+ };
392
+ /**
393
+ * OpenChamber refused the request (an expired handle, a path off the
394
+ * instance, a body too large), could not reach the channel's server, or did
395
+ * not answer. `status` is the callback listener's HTTP status, `0` when it
396
+ * gave none; `code` is its error code (`HOST_UNAVAILABLE` when there was no
397
+ * usable answer).
398
+ */
399
+ export class NotificationsOutboundError extends Error {
400
+ code;
401
+ status;
402
+ constructor(message, code, status) {
403
+ super(message);
404
+ this.name = 'NotificationsOutboundError';
405
+ this.code = code;
406
+ this.status = status;
407
+ }
408
+ }
409
+ /**
410
+ * The service's way to the channel's server: OpenChamber's callback listener
411
+ * (`OPENCHAMBER_HOST_CALLBACK_URL`), which checks the handle, keeps the
412
+ * request on the account's origin and adds the token. Plain `fetch`, no
413
+ * dependencies. Throws `NotificationsOutboundError` when OpenChamber refuses
414
+ * the request; an answer of any status from the server resolves.
415
+ */
416
+ export const createNotificationsOutbound = ({ env }) => {
417
+ const base = env[OPENCHAMBER_HOST_CALLBACK_URL];
418
+ if (!base)
419
+ throw new Error(`${OPENCHAMBER_HOST_CALLBACK_URL} is not set: only a service that provides "notifications" can reach a channel's server.`);
420
+ const url = `${base.replace(/\/+$/, '')}${NOTIFICATIONS_OUTBOUND_PATH}`;
421
+ return {
422
+ request: (handle, request) => postOutbound(url, handle, request, NOTIFICATIONS_OUTBOUND_TIMEOUT_MS, (message, code, status) => new NotificationsOutboundError(message, code, status)),
423
+ };
424
+ };
425
+ /*
426
+ * Tracker: a service with `provides: ["tracker"]` is the work item tracker
427
+ * `contributes.tracker` describes, next to Linear. OpenChamber keeps the
428
+ * accounts and their tokens (pasted or minted through OAuth), the project
429
+ * mapping, the session comment preference and the session records; it
430
+ * forwards the reads and the two writes to the service at
431
+ * `TRACKER_PROVIDER_PATH`. The service never sees a token: it reaches the
432
+ * tracker's server through OpenChamber's outbound proxy
433
+ * (`createTrackerOutbound`), which pins the instance origin and adds the
434
+ * account's credential itself. Every item answered goes to the UI in the
435
+ * neutral tracker shapes, so the picker, the composer, New Worktree, linked
436
+ * items and tracked items draw it as they draw a Linear issue.
437
+ */
438
+ /** Route on the service loopback OpenChamber posts every tracker call to. */
439
+ export const TRACKER_PROVIDER_PATH = '/tracker';
440
+ /** The `v` of every request; a service answers a version it does not know with HTTP 400. */
441
+ export const TRACKER_PROVIDER_WIRE_VERSION = 1;
442
+ /**
443
+ * Every call OpenChamber forwards. `auth.probe` asks whether an instance is
444
+ * this tracker's server, `auth.verify` who a credential belongs to (the handle
445
+ * carries a pasted token or an OAuth access token); the reads answer in the
446
+ * neutral shapes; `items.updateState` and `comments.create` are the two
447
+ * writes, the only calls whose handle may send anything but `GET`.
448
+ */
449
+ export const TRACKER_PROVIDER_METHODS = [
450
+ 'auth.probe',
451
+ 'auth.verify',
452
+ 'items.list',
453
+ 'items.get',
454
+ 'items.updateState',
455
+ 'items.liveSummaries',
456
+ 'states.list',
457
+ 'containers.list',
458
+ 'comments.create',
459
+ ];
460
+ /** The calls that change something on the tracker. */
461
+ export const TRACKER_PROVIDER_WRITE_METHODS = ['items.updateState', 'comments.create'];
462
+ export const TRACKER_PROVIDER_AUTH_TIMEOUT_MS = 15_000;
463
+ export const TRACKER_PROVIDER_READ_TIMEOUT_MS = 20_000;
464
+ export const TRACKER_PROVIDER_WRITE_TIMEOUT_MS = 30_000;
465
+ /** Bytes of an answer OpenChamber reads: a page of items with their labels and people. */
466
+ export const TRACKER_PROVIDER_RESPONSE_MAX = 2_000_000;
467
+ /** Characters of `error.message` OpenChamber reads. */
468
+ export const TRACKER_PROVIDER_ERROR_MESSAGE_MAX = 2_000;
469
+ /** No call for this long stops the service; the next call starts it again. */
470
+ export const TRACKER_PROVIDER_IDLE_MS = 10 * 60_000;
471
+ /** Items in one `items.list` page, children or comments of one item, states of one container. */
472
+ export const TRACKER_PROVIDER_LIST_MAX = 100;
473
+ /** Identifiers in one `items.liveSummaries` call. */
474
+ export const TRACKER_PROVIDER_LIVE_SUMMARIES_MAX = 50;
475
+ /** Containers in one `containers.list` answer. */
476
+ export const TRACKER_PROVIDER_CONTAINERS_MAX = 500;
477
+ /** Route on the callback listener a service sends the tracker's requests to. */
478
+ export const TRACKER_OUTBOUND_PATH = '/tracker/outbound';
479
+ /** One outbound request may take this long; the call's own deadline may end it sooner. */
480
+ export const TRACKER_OUTBOUND_TIMEOUT_MS = 20_000;
481
+ export const isTrackerProviderWriteMethod = (method) => (TRACKER_PROVIDER_WRITE_METHODS.some((write) => write === method));
482
+ /** How long OpenChamber waits for the service to answer `method`. */
483
+ export const trackerProviderTimeoutMs = (method) => {
484
+ if (method === 'auth.probe' || method === 'auth.verify')
485
+ return TRACKER_PROVIDER_AUTH_TIMEOUT_MS;
486
+ if (isTrackerProviderWriteMethod(method))
487
+ return TRACKER_PROVIDER_WRITE_TIMEOUT_MS;
488
+ return TRACKER_PROVIDER_READ_TIMEOUT_MS;
489
+ };
490
+ export const isTrackerProviderMethod = (value) => (TRACKER_PROVIDER_METHODS.some((method) => method === value));
491
+ /**
492
+ * A service's read of the body OpenChamber posted to `TRACKER_PROVIDER_PATH`.
493
+ * OpenChamber built `input`, so this checks the envelope, not every field, and
494
+ * the service bundle needs no schema library. `null` is a body that is not a
495
+ * tracker call of this wire version; answer it with HTTP 400.
496
+ */
497
+ export const readTrackerProviderRequest = (body) => {
498
+ let wire;
499
+ try {
500
+ const parsed = JSON.parse(body);
501
+ if (Object(parsed) !== parsed || parsed === null)
502
+ return null;
503
+ wire = parsed;
504
+ }
505
+ catch {
506
+ return null;
507
+ }
508
+ const { v, requestId, method, provider, session, input } = wire;
509
+ if (v !== TRACKER_PROVIDER_WIRE_VERSION)
510
+ return null;
511
+ if (String(requestId) !== requestId || requestId.length === 0)
512
+ return null;
513
+ if (String(provider) !== provider || provider.length === 0)
514
+ return null;
515
+ if (String(method) !== method || !isTrackerProviderMethod(method))
516
+ return null;
517
+ if (Object(input) !== input || Array.isArray(input))
518
+ return null;
519
+ if (Object(session) !== session || session === null || session === undefined)
520
+ return null;
521
+ const { instance, handle } = session;
522
+ if (String(instance) !== instance || instance.length === 0 || String(handle) !== handle || handle.length === 0)
523
+ return null;
524
+ // The account shape is the notification channel's: `{ id, login | null }` or null.
525
+ const account = readNotificationsAccount(session.account);
526
+ if (account === undefined)
527
+ return null;
528
+ // SAFETY: `input` is a plain object parsed from JSON, so every value in it is JSON.
529
+ return { v: TRACKER_PROVIDER_WIRE_VERSION, requestId, method, provider, session: { instance, handle, account }, input: input };
530
+ };
531
+ /**
532
+ * OpenChamber refused the request (an expired handle, a path off the
533
+ * instance, a body too large, a write through a read's handle), could not
534
+ * reach the tracker's server, or did not answer. `status` is the callback
535
+ * listener's HTTP status, `0` when it gave none; `code` is its error code
536
+ * (`HOST_UNAVAILABLE` when there was no usable answer).
537
+ */
538
+ export class TrackerOutboundError extends Error {
539
+ code;
540
+ status;
541
+ constructor(message, code, status) {
542
+ super(message);
543
+ this.name = 'TrackerOutboundError';
544
+ this.code = code;
545
+ this.status = status;
546
+ }
547
+ }
548
+ /**
549
+ * The service's way to the tracker's server: OpenChamber's callback listener
550
+ * (`OPENCHAMBER_HOST_CALLBACK_URL`), which checks the handle, keeps the
551
+ * request on the account's origin and adds the credential. Plain `fetch`, no
552
+ * dependencies. Throws `TrackerOutboundError` when OpenChamber refuses the
553
+ * request; an answer of any status from the server resolves. A read's handle
554
+ * sends `GET` only; `items.updateState` and `comments.create` may send writes.
555
+ */
556
+ export const createTrackerOutbound = ({ env }) => {
557
+ const base = env[OPENCHAMBER_HOST_CALLBACK_URL];
558
+ if (!base)
559
+ throw new Error(`${OPENCHAMBER_HOST_CALLBACK_URL} is not set: only a service that provides "tracker" can reach a tracker's server.`);
560
+ const url = `${base.replace(/\/+$/, '')}${TRACKER_OUTBOUND_PATH}`;
561
+ return {
562
+ request: (handle, request) => postOutbound(url, handle, request, TRACKER_OUTBOUND_TIMEOUT_MS, (message, code, status) => new TrackerOutboundError(message, code, status)),
289
563
  };
290
564
  };
@@ -36,6 +36,7 @@ export declare const surfaceInputEventSchema: z.ZodDiscriminatedUnion<[z.ZodObje
36
36
  }>;
37
37
  key: z.ZodString;
38
38
  code: z.ZodString;
39
+ keyCode: z.ZodOptional<z.ZodNumber>;
39
40
  modifiers: z.ZodObject<{
40
41
  alt: z.ZodBoolean;
41
42
  ctrl: z.ZodBoolean;
@@ -89,6 +90,7 @@ export declare const surfaceViewerMessageSchema: z.ZodDiscriminatedUnion<[z.ZodO
89
90
  }>;
90
91
  key: z.ZodString;
91
92
  code: z.ZodString;
93
+ keyCode: z.ZodOptional<z.ZodNumber>;
92
94
  modifiers: z.ZodObject<{
93
95
  alt: z.ZodBoolean;
94
96
  ctrl: z.ZodBoolean;
@@ -18,6 +18,7 @@ export const surfaceInputEventSchema = z.discriminatedUnion('type', [
18
18
  action: z.enum(['down', 'up']),
19
19
  key: z.string().min(1).max(64),
20
20
  code: z.string().max(64),
21
+ keyCode: z.number().int().min(0).max(255).optional(),
21
22
  modifiers: modifiersSchema,
22
23
  }),
23
24
  z.object({ type: z.literal('text'), text: z.string().min(1).max(SURFACE_TEXT_MAX) }),
@@ -93,6 +93,11 @@ export type SurfaceKeyEvent = {
93
93
  key: string;
94
94
  /** `KeyboardEvent.code`. */
95
95
  code: string;
96
+ /**
97
+ * `KeyboardEvent.keyCode` as the viewer's browser computed it for the user's
98
+ * keyboard layout, 0 to 255. Older hosts omit it.
99
+ */
100
+ keyCode?: number;
96
101
  modifiers: SurfaceModifiers;
97
102
  };
98
103
  /** Text the user pasted or composed; type it as is. */
@@ -99,7 +99,11 @@ const readEvent = (value) => {
99
99
  return null;
100
100
  if (!isText(value.key) || !isText(value.code) || value.key.length > 64 || value.code.length > 64)
101
101
  return null;
102
- return { type: 'key', action: value.action, key: value.key, code: value.code, modifiers };
102
+ if (value.keyCode === undefined)
103
+ return { type: 'key', action: value.action, key: value.key, code: value.code, modifiers };
104
+ if (!isFiniteNumber(value.keyCode) || !Number.isInteger(value.keyCode) || value.keyCode < 0 || value.keyCode > 255)
105
+ return null;
106
+ return { type: 'key', action: value.action, key: value.key, code: value.code, keyCode: value.keyCode, modifiers };
103
107
  }
104
108
  return null;
105
109
  };
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Session events for an extension's service (`contributes.sessionEvents`).
3
+ *
4
+ * OpenChamber posts each event a package subscribed to as one HTTP request to
5
+ * the service at `SESSION_EVENTS_PATH`, from every project, whether or not a
6
+ * window is open. Approving the package is the subscription: the request
7
+ * carries identifiers only (`event`), and the `transcript` capability, asked
8
+ * for with `content: true`, adds what the agent said or asked (`content`).
9
+ * The service answers `{ ok: true }` once it has taken the event; OpenChamber
10
+ * reads nothing else from the answer. Which projects an extension acts on is
11
+ * its own business: filter on `event.project.directory`.
12
+ */
13
+ /**
14
+ * `turn-finished`: the session went idle with nothing waiting (no running
15
+ * subagent, no open permission, question or page, no goal or auto-review
16
+ * step still driving it). `turn-failed`: the turn ended in an error.
17
+ * `permission-asked`: the agent asked for a permission the user has to
18
+ * answer. `question-asked`: the agent asked the user a question through a
19
+ * form or a page. A turn the user stopped emits nothing.
20
+ */
21
+ export declare const GUEST_SESSION_EVENT_TYPES: readonly ['turn-finished', 'turn-failed', 'permission-asked', 'question-asked'];
22
+ export type GuestSessionEventType = (typeof GUEST_SESSION_EVENT_TYPES)[number];
23
+ /** Route on the service loopback OpenChamber posts every event to. */
24
+ export declare const SESSION_EVENTS_PATH = "/session-events";
25
+ /** The `v` of every request; a service answers a version it does not know with HTTP 400. */
26
+ export declare const SESSION_EVENTS_WIRE_VERSION = 1;
27
+ /** One delivery may take this long; a slower service drops that event. */
28
+ export declare const SESSION_EVENT_TIMEOUT_MS = 10000;
29
+ /** No event for this long stops the service; the next event starts it again. */
30
+ export declare const SESSION_EVENTS_IDLE_MS: number;
31
+ /** Characters of the agent's reply carried in `content`; a longer reply is cut and flagged. */
32
+ export declare const SESSION_EVENT_REPLY_MAX = 64000;
33
+ /** Events kept while the service starts; newer ones past this are dropped. */
34
+ export declare const SESSION_EVENT_PENDING_MAX = 200;
35
+ /** Characters of `error` OpenChamber reads from a failed answer. */
36
+ export declare const SESSION_EVENT_ERROR_MAX = 2000;
37
+ /**
38
+ * `contributes.sessionEvents`: which events the service wants, and whether it
39
+ * also wants their content. Needs `contributes.service`; the service needs no
40
+ * panel or background entry for this alone.
41
+ */
42
+ export type SessionEventsContribution = {
43
+ /** 1 to 4 distinct types. */
44
+ events: GuestSessionEventType[];
45
+ /** Asks for the `transcript` capability: the reply, the question, the permission's detail. */
46
+ content?: true;
47
+ };
48
+ /** What the catalog and the approval dialog show of the subscription. */
49
+ export type PublicSessionEvents = {
50
+ events: GuestSessionEventType[];
51
+ content: boolean;
52
+ };
53
+ /**
54
+ * An item the user linked to the session (a tracker issue, a pull request,
55
+ * an external link), as the session stores it: identifiers and the link,
56
+ * never the item's title. `id` is the stored entry id (`owner/repo#12`,
57
+ * `linear:OPE-367`, `link:<url>`); `kind` is the link's kind as stored
58
+ * (`issue`, `pull`, `external`, or a tracker's own kind); `provider` names the
59
+ * tracker when one owns the item and `identifier` is its key there (`OPE-367`).
60
+ */
61
+ export type GuestSessionLinkedItem = {
62
+ id: string;
63
+ kind: string;
64
+ url: string;
65
+ provider?: string;
66
+ identifier?: string;
67
+ /** The change request or issue number on a Git host. */
68
+ number?: number;
69
+ };
70
+ /** The agent's final answer of the turn, cut at `SESSION_EVENT_REPLY_MAX` when `replyTruncated`. */
71
+ export type GuestTurnFinishedContent = {
72
+ type: 'turn-finished';
73
+ sessionTitle: string | null;
74
+ reply: string;
75
+ replyTruncated: boolean;
76
+ };
77
+ export type GuestTurnFailedContent = {
78
+ type: 'turn-failed';
79
+ sessionTitle: string | null;
80
+ error: {
81
+ type: string;
82
+ message: string;
83
+ };
84
+ };
85
+ /** What the agent wants to do: the permission's action, the resources it names, and its note. */
86
+ export type GuestPermissionAskedContent = {
87
+ type: 'permission-asked';
88
+ sessionTitle: string | null;
89
+ action: string;
90
+ resources: string[];
91
+ message: string | null;
92
+ };
93
+ export type GuestQuestionAskedContent = {
94
+ type: 'question-asked';
95
+ sessionTitle: string | null;
96
+ ask: {
97
+ kind: 'form';
98
+ id: string;
99
+ title: string;
100
+ fields: Array<{
101
+ title: string;
102
+ description: string | null;
103
+ }>;
104
+ } | {
105
+ kind: 'page';
106
+ id: string;
107
+ title: string;
108
+ };
109
+ };
110
+ export type GuestSessionEventContent = GuestTurnFinishedContent | GuestTurnFailedContent | GuestPermissionAskedContent | GuestQuestionAskedContent;
111
+ /**
112
+ * One event, as OpenChamber posts it. `event` is what every subscriber gets;
113
+ * `content` comes only with the `transcript` grant, and only on the types
114
+ * that have content. `project.directory` is the session's directory, `null`
115
+ * when OpenChamber does not know it. `session.parentId` is set for a
116
+ * subagent's session. `permission.requestId` names the request on a
117
+ * `permission-asked` event.
118
+ */
119
+ export type GuestSessionEventRequest = {
120
+ v: typeof SESSION_EVENTS_WIRE_VERSION;
121
+ requestId: string;
122
+ event: {
123
+ /** OpenCode's event id, or one OpenChamber made for an event it synthesized. */
124
+ id: string;
125
+ type: GuestSessionEventType;
126
+ /** Epoch milliseconds. */
127
+ at: number;
128
+ session: {
129
+ id: string;
130
+ parentId: string | null;
131
+ };
132
+ project: {
133
+ directory: string | null;
134
+ };
135
+ linked: GuestSessionLinkedItem[];
136
+ permission?: {
137
+ requestId: string;
138
+ };
139
+ };
140
+ content?: GuestSessionEventContent;
141
+ };
142
+ /**
143
+ * What the service answers. `ok: false` with an `error` is a delivery that
144
+ * failed; OpenChamber logs the text and nothing else, so say what went wrong
145
+ * on the service's side, never what the event contained.
146
+ */
147
+ export type GuestSessionEventResult = {
148
+ ok: true;
149
+ } | {
150
+ ok: false;
151
+ error: string;
152
+ };
153
+ export declare const isGuestSessionEventType: (value: string) => value is GuestSessionEventType;
154
+ /**
155
+ * A service's read of the body OpenChamber posted to `SESSION_EVENTS_PATH`.
156
+ * OpenChamber built the event, so this checks the envelope and the fields a
157
+ * service branches on, not every content field, and the service bundle needs
158
+ * no schema library. `null` is a body that is not a session event of this
159
+ * wire version; answer it with HTTP 400.
160
+ */
161
+ export declare const readSessionEventRequest: (body: string) => GuestSessionEventRequest | null;