@rynx-ai/daemon 0.1.11-beta.3 → 0.1.11-beta.31

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 (64) hide show
  1. package/bundled-plugins/plugins/lark/.rynx-plugin/plugin.json +0 -12
  2. package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.js +3 -1
  3. package/bundled-plugins/plugins/lark/dist/host-adapters.d.ts +3 -3
  4. package/bundled-plugins/plugins/lark/dist/host-adapters.js +24 -18
  5. package/bundled-plugins/plugins/lark/dist/lark.js +18 -12
  6. package/bundled-plugins/plugins/lark/dist/runtime.js +6046 -6018
  7. package/bundled-plugins/plugins/lark/dist/settings.js +6006 -5987
  8. package/bundled-plugins/plugins/lark/package.json +1 -1
  9. package/bundled-plugins/plugins/lark/ui/assets/index-CGUVAMsv.css +1 -0
  10. package/bundled-plugins/plugins/lark/ui/index.html +2 -2
  11. package/dist/agent-file.js +0 -1
  12. package/dist/browser-artifact-management.js +4 -1
  13. package/dist/chrome-for-testing-runtime.js +4 -3
  14. package/dist/chrome-for-testing-store.js +29 -7
  15. package/dist/control-deps.d.ts +6 -2
  16. package/dist/control-deps.js +87 -4
  17. package/dist/control-link-store.js +4 -3
  18. package/dist/daemon-owner.js +4 -7
  19. package/dist/daemon-server.js +41 -16
  20. package/dist/db.d.ts +2 -2
  21. package/dist/db.js +155 -21
  22. package/dist/headless-browser-host.js +283 -34
  23. package/dist/lifecycle.d.ts +1 -0
  24. package/dist/lifecycle.js +3 -0
  25. package/dist/migrations/cleanup-legacy-sessions.js +4 -3
  26. package/dist/plugin-cli.d.ts +10 -0
  27. package/dist/plugin-cli.js +19 -2
  28. package/dist/plugin-host-rpc.d.ts +48 -9
  29. package/dist/plugin-host-rpc.js +552 -80
  30. package/dist/plugin-installer.js +3 -2
  31. package/dist/plugin-management-service.js +0 -2
  32. package/dist/plugin-marketplace.js +2 -1
  33. package/dist/plugin-store.js +2 -4
  34. package/dist/plugin-supervisor.js +0 -4
  35. package/dist/remote-runtime-access-store.d.ts +2 -2
  36. package/dist/remote-runtime-access-store.js +6 -9
  37. package/dist/remote-runtime-connection-manager.js +1 -1
  38. package/dist/remote-runtime-target-store.d.ts +2 -2
  39. package/dist/remote-runtime-target-store.js +6 -6
  40. package/dist/session-emulator-binding-store.js +4 -6
  41. package/dist/session-fork-store.d.ts +1 -0
  42. package/dist/session-fork-store.js +82 -28
  43. package/dist/session-launch-operations.js +6 -5
  44. package/dist/session-log-store.d.ts +1 -0
  45. package/dist/session-log-store.js +19 -16
  46. package/dist/session-meta-store.d.ts +5 -0
  47. package/dist/session-meta-store.js +25 -2
  48. package/dist/session-portal-grant-store.d.ts +55 -0
  49. package/dist/session-portal-grant-store.js +160 -0
  50. package/dist/session-resource-store.d.ts +24 -6
  51. package/dist/session-resource-store.js +386 -68
  52. package/dist/session-timeline-projection-store.d.ts +24 -0
  53. package/dist/session-timeline-projection-store.js +53 -0
  54. package/dist/setup-service.d.ts +1 -0
  55. package/dist/setup-service.js +1 -0
  56. package/dist/skills-catalog.d.ts +11 -2
  57. package/dist/skills-catalog.js +317 -10
  58. package/dist/sqlite.d.ts +22 -0
  59. package/dist/sqlite.js +57 -0
  60. package/dist/system-dependencies.d.ts +22 -0
  61. package/dist/system-dependencies.js +79 -0
  62. package/package.json +15 -12
  63. package/bundled-plugins/plugins/lark/ui/assets/index-B2dhMlMU.css +0 -1
  64. /package/bundled-plugins/plugins/lark/ui/assets/{index-D9N-AkLX.js → index-D8Amoe6a.js} +0 -0
@@ -1,11 +1,15 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { realpath, stat } from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { AgentRuntimeError, agentSpecSchema, resolvedExecutionSnapshotSchema, sessionWorkspaceSnapshotSchema, SteerController, } from "@rynx-ai/core";
4
+ import { AgentRuntimeError, agentSpecSchema, resolvedExecutionSnapshotSchema, sessionWorkspaceSnapshotSchema, } from "@rynx-ai/core";
5
5
  import { PLUGIN_HOST_MAX_ACTIVE_RUNS, } from "@rynx-ai/plugin-sdk";
6
+ import { parseRuntimeBrowserRequestHeadersGetParams, parseRuntimeBrowserRequestHeadersSetParams, } from "@rynx-ai/protocol/runtime-browser";
7
+ import { SessionBrowserServiceError, } from "@rynx-ai/server";
6
8
  import { PluginRunnerError } from "@rynx-ai/plugin-runner";
7
9
  import { ensurePluginSessionLaunch, replayPluginSessionLaunch, SessionLaunchOperationConflictError, SessionLaunchOperationTargetDeletedError, } from "./session-launch-operations.js";
8
10
  import { createControlLink } from "./control-link-store.js";
11
+ import { issueSessionPortalTicket, revokeSessionPortalGrant, } from "./session-portal-grant-store.js";
12
+ import { claimSessionTimelineProjection, completeSessionTimelineProjection, } from "./session-timeline-projection-store.js";
9
13
  const MAX_MESSAGE_LENGTH = 1_000_000;
10
14
  /** Leaves ample room for the RPC envelope below the runner's 1 MiB line cap. */
11
15
  export const MAX_PLUGIN_HOST_EVENT_BYTES = 512 * 1024;
@@ -13,31 +17,59 @@ const MAX_PLUGIN_RUN_FINAL_RESPONSE_BYTES = 64 * 1024;
13
17
  const MAX_DELTA_CHUNK_CODE_UNITS = 64 * 1024;
14
18
  const SESSION_INTERACTION_RESOLUTION_MAX_BYTES = 128 * 1024;
15
19
  const AGENT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
16
- const METHOD_CAPABILITY = {
17
- "installation.proof.sign": "installation.proof",
18
- "agent.catalog.list": "agent.catalog.read",
19
- "agent.catalog.get": "agent.catalog.read",
20
- "agent.execution.resolve": "agent.catalog.read",
21
- "session.ensure": "session.provision",
22
- "session.run": "conversation.run",
23
- "session.steer": "conversation.run",
24
- "session.interrupt": "session.cancel",
25
- "session.interaction.resolve": "conversation.run",
26
- "session.models": "runtime.catalog.read",
27
- "session.binding.get": "session.binding.read",
28
- "session.binding.list": "session.binding.read",
29
- "session.binding.find": "session.binding.read",
30
- "session.goal.get": "session.goal.read",
31
- "session.goal.set": "session.goal.write",
32
- "session.goal.clear": "session.goal.write",
33
- "session.fork": "session.fork",
34
- "session.meta.get": "session.meta.read",
35
- "session.meta.list": "session.meta.read",
36
- "session.meta.setTitle": "session.meta.title.write",
37
- "control.link.issue": "control-link.issue",
38
- };
39
20
  /**
40
- * One authenticated plugin's capability-gated Host RPC surface.
21
+ * Buffers messages that arrive while the first `session.run` is still bringing
22
+ * up its native Turn. Once Core subscribes, later messages are delivered
23
+ * through the same input channel. After that subscription closes, this Run no
24
+ * longer accepts input; the caller waits for release and opens the next Run.
25
+ */
26
+ class PluginRunInputController {
27
+ listener;
28
+ pending = new Map();
29
+ subscribed = false;
30
+ closed = false;
31
+ enqueue(text) {
32
+ if (this.closed || (this.subscribed && !this.listener)) {
33
+ return { accepted: false, cancel: () => undefined };
34
+ }
35
+ if (this.listener) {
36
+ this.listener(text);
37
+ return { accepted: true, cancel: () => undefined };
38
+ }
39
+ const token = Symbol("plugin-run-input");
40
+ this.pending.set(token, text);
41
+ return {
42
+ accepted: true,
43
+ cancel: () => {
44
+ this.pending.delete(token);
45
+ },
46
+ };
47
+ }
48
+ subscribe(listener) {
49
+ if (this.closed)
50
+ return () => undefined;
51
+ if (this.listener) {
52
+ throw new Error("plugin Run input already has a subscriber");
53
+ }
54
+ this.subscribed = true;
55
+ this.listener = listener;
56
+ const queued = [...this.pending.values()];
57
+ this.pending.clear();
58
+ for (const text of queued)
59
+ listener(text);
60
+ return () => {
61
+ if (this.listener === listener)
62
+ this.listener = undefined;
63
+ };
64
+ }
65
+ close() {
66
+ this.closed = true;
67
+ this.listener = undefined;
68
+ this.pending.clear();
69
+ }
70
+ }
71
+ /**
72
+ * One authenticated plugin's Host RPC surface.
41
73
  *
42
74
  * This adapter is deliberately the only place that maps public string methods
43
75
  * to trusted runtime objects. The runner receives neither this class nor its
@@ -49,7 +81,6 @@ export class PluginHostRpc {
49
81
  daemonInstanceId;
50
82
  productVersion;
51
83
  cliVersion;
52
- capabilities;
53
84
  services;
54
85
  managedWorkspaceRoot;
55
86
  emit;
@@ -65,7 +96,6 @@ export class PluginHostRpc {
65
96
  this.daemonInstanceId = requiredString(options.daemonInstanceId, "daemonInstanceId", 128);
66
97
  this.productVersion = requiredString(options.productVersion, "productVersion", 128);
67
98
  this.cliVersion = requiredString(options.cliVersion, "cliVersion", 128);
68
- this.capabilities = new Set(options.capabilities);
69
99
  this.services = options.services;
70
100
  this.managedWorkspaceRoot = options.managedWorkspaceRoot
71
101
  ? path.resolve(options.managedWorkspaceRoot)
@@ -89,15 +119,27 @@ export class PluginHostRpc {
89
119
  publicKey: this.services.installationProof.publicKey,
90
120
  },
91
121
  protocolVersion: 1,
92
- capabilities: [...this.capabilities],
122
+ capabilities: {
123
+ sessionRunContent: true,
124
+ sessionRunFiles: true,
125
+ ...(this.services.sessionTimeline
126
+ ? { sessionTimelineProjectText: true }
127
+ : {}),
128
+ ...(this.services.sessionBrowserRequestHeaders
129
+ ? { sessionBrowserRequestHeaders: true }
130
+ : {}),
131
+ ...(this.services.sessionPortal
132
+ ? {
133
+ sessionPortalEmbedOrigin: true,
134
+ sessionPortalGrantRevocation: true,
135
+ }
136
+ : {}),
137
+ },
138
+ ...(this.services.sessionPortal
139
+ ? { sessionPortal: { ...this.services.sessionPortal } }
140
+ : {}),
93
141
  };
94
142
  }
95
- const capability = METHOD_CAPABILITY[method];
96
- if (!capability)
97
- throw new PluginHostCallError("METHOD_NOT_FOUND", `unknown host method: ${method}`);
98
- if (!this.capabilities.has(capability)) {
99
- throw new PluginHostCallError("CAPABILITY_NOT_DECLARED", `host method ${method} is missing manifest capability ${capability}`);
100
- }
101
143
  try {
102
144
  switch (method) {
103
145
  case "installation.proof.sign": {
@@ -118,8 +160,8 @@ export class PluginHostRpc {
118
160
  return await this.ensureSession(params);
119
161
  case "session.run":
120
162
  return await this.runSession(params, signal);
121
- case "session.steer":
122
- return this.steerSession(params);
163
+ case "session.activity.get":
164
+ return await this.sessionActivity(params);
123
165
  case "session.interrupt":
124
166
  return await this.interruptRun(params);
125
167
  case "session.interaction.resolve":
@@ -149,8 +191,31 @@ export class PluginHostRpc {
149
191
  return this.services.sessionRegistry.list().filter((session) => this.isOwned(session));
150
192
  case "session.meta.setTitle":
151
193
  return this.setSessionTitle(params);
194
+ case "session.browser.requestHeaders.get":
195
+ return await this.getSessionBrowserRequestHeaders(params);
196
+ case "session.browser.requestHeaders.set":
197
+ return await this.setSessionBrowserRequestHeaders(params);
152
198
  case "control.link.issue":
153
199
  return this.issueControlLink(params);
200
+ case "session.portal.plugin.issue": {
201
+ const input = record(params);
202
+ return this.issueSessionPortalLink("plugin_sessions", requiredSessionPortalAccess(input), undefined, optionalSessionPortalEmbedOrigin(input.embedOrigin), optionalString(input.grantHandle, "grantHandle", 128));
203
+ }
204
+ case "session.portal.session.issue": {
205
+ const input = record(params);
206
+ const sessionId = requiredSessionId(input);
207
+ this.ownedSession(sessionId);
208
+ return this.issueSessionPortalLink("single_session", requiredSessionPortalAccess(input), sessionId, optionalSessionPortalEmbedOrigin(input.embedOrigin), optionalString(input.grantHandle, "grantHandle", 128));
209
+ }
210
+ case "session.portal.grant.revoke": {
211
+ const grantHandle = requiredString(record(params).grantHandle, "grantHandle", 128);
212
+ return revokeSessionPortalGrant({
213
+ pluginId: this.principalId,
214
+ grantHandle,
215
+ });
216
+ }
217
+ case "session.timeline.projectText":
218
+ return await this.projectTimelineText(params);
154
219
  default:
155
220
  throw new PluginHostCallError("METHOD_NOT_FOUND", `unknown host method: ${method}`);
156
221
  }
@@ -319,17 +384,86 @@ export class PluginHostRpc {
319
384
  const input = record(params);
320
385
  const sessionId = requiredSessionId(input);
321
386
  const operationId = optionalString(input.operationId, "operationId", 512);
322
- const session = this.ownedSession(sessionId);
323
- if (operationId && this.activeRunsByOperation.has(operationId)) {
324
- throw new PluginHostCallError("RUN_ALREADY_ACTIVE", `operation already has an active run: ${operationId}`, true);
387
+ const hasMessage = input.message !== undefined;
388
+ const hasContent = input.content !== undefined;
389
+ if (hasMessage === hasContent) {
390
+ throw new PluginHostCallError("INVALID_PARAMS", "provide exactly one of message or content");
325
391
  }
326
- if (this.activeSessionRuns.has(sessionId)) {
327
- throw new PluginHostCallError("RUN_ALREADY_ACTIVE", `session already has an active run: ${sessionId}`, true);
392
+ const message = hasMessage
393
+ ? requiredText(input.message, "message", MAX_MESSAGE_LENGTH)
394
+ : undefined;
395
+ const collaborationMode = optionalCollaborationMode(input.collaborationMode);
396
+ let session = this.ownedSession(sessionId);
397
+ if (operationId) {
398
+ const replayed = this.activeRunsByOperation.get(operationId);
399
+ if (replayed) {
400
+ if (replayed.sessionId !== sessionId) {
401
+ throw new PluginHostCallError("RUN_ALREADY_ACTIVE", `operation already belongs to another active Session: ${operationId}`, true);
402
+ }
403
+ return await waitForHostPromise(replayed.started, signal);
404
+ }
328
405
  }
406
+ const active = this.activeSessionRuns.get(sessionId);
407
+ if (active) {
408
+ if (hasContent) {
409
+ // Native steering currently accepts text only. Preserve the attachment
410
+ // as a new Turn after the active response reaches its terminal edge.
411
+ await waitForHostPromise(active.settled, signal);
412
+ throwIfHostCallAborted(signal);
413
+ return await this.runSessionAdmitted(params, signal);
414
+ }
415
+ const queued = active.input.enqueue(message);
416
+ if (!queued.accepted) {
417
+ // The response reached its terminal boundary before Host cleanup won
418
+ // the scheduling race. Wait for that old Run to leave the Session slot,
419
+ // then submit this message as a new Turn with a new response identity.
420
+ await waitForHostPromise(active.settled, signal);
421
+ throwIfHostCallAborted(signal);
422
+ return await this.runSessionAdmitted(params, signal);
423
+ }
424
+ if (operationId) {
425
+ active.operationIds.add(operationId);
426
+ this.activeRunsByOperation.set(operationId, active);
427
+ }
428
+ try {
429
+ // Reuse the current Run identity. Core already owns its response stream;
430
+ // the buffered input will become turn/steer once that stream subscribes.
431
+ return await waitForHostPromise(active.started, signal);
432
+ }
433
+ catch (error) {
434
+ queued.cancel();
435
+ if (operationId && this.activeRunsByOperation.get(operationId) === active) {
436
+ this.activeRunsByOperation.delete(operationId);
437
+ active.operationIds.delete(operationId);
438
+ }
439
+ throw error;
440
+ }
441
+ }
442
+ const modelProvided = input.model !== undefined;
443
+ const requestedModel = input.model === null || input.model === ""
444
+ ? null
445
+ : optionalString(input.model, "model", 256);
446
+ const reasoningEffortProvided = input.reasoningEffort !== undefined;
447
+ const requestedReasoningEffort = input.reasoningEffort === null || input.reasoningEffort === ""
448
+ ? null
449
+ : optionalString(input.reasoningEffort, "reasoningEffort", 64);
329
450
  if (this.activeRuns.size >= PLUGIN_HOST_MAX_ACTIVE_RUNS) {
330
451
  throw new PluginHostCallError("RUN_LIMIT", "plugin has too many active session runs", true);
331
452
  }
332
- const message = requiredText(input.message, "message", MAX_MESSAGE_LENGTH);
453
+ const nextModel = modelProvided ? requestedModel ?? null : session.execution.model;
454
+ const nextReasoningEffort = reasoningEffortProvided
455
+ ? requestedReasoningEffort ?? null
456
+ : session.execution.reasoningEffort;
457
+ if (nextModel !== session.execution.model ||
458
+ nextReasoningEffort !== session.execution.reasoningEffort) {
459
+ await this.services.sessionExecution.update({
460
+ sessionId,
461
+ model: nextModel,
462
+ reasoningEffort: nextReasoningEffort,
463
+ });
464
+ session = this.ownedSession(sessionId);
465
+ }
466
+ const content = hasContent ? await pluginRunContent(input.content) : undefined;
333
467
  const abort = new AbortController();
334
468
  const cancel = () => abort.abort(signal?.reason);
335
469
  if (signal?.aborted) {
@@ -337,15 +471,35 @@ export class PluginHostRpc {
337
471
  throwIfHostCallAborted(signal);
338
472
  }
339
473
  signal?.addEventListener("abort", cancel, { once: true });
340
- const steer = new SteerController();
474
+ const runInput = new PluginRunInputController();
341
475
  const runId = randomUUID();
476
+ let resolveStarted;
477
+ let rejectStarted;
478
+ const startedResult = new Promise((resolve, reject) => {
479
+ resolveStarted = resolve;
480
+ rejectStarted = reject;
481
+ });
482
+ // The initiating RPC observes startup directly below. This promise exists
483
+ // for concurrent/replayed calls, so retain its rejection without creating
484
+ // an unhandled-rejection when there are none.
485
+ void startedResult.catch(() => undefined);
486
+ let resolveSettled;
487
+ const settled = new Promise((resolve) => {
488
+ resolveSettled = resolve;
489
+ });
342
490
  const run = {
343
491
  runId,
344
492
  sessionId,
345
- operationId,
493
+ operationIds: new Set(operationId ? [operationId] : []),
346
494
  interruptRequested: false,
347
495
  abort,
348
- steer,
496
+ input: runInput,
497
+ started: startedResult,
498
+ startupSettled: false,
499
+ resolveStarted,
500
+ rejectStarted,
501
+ settled,
502
+ resolveSettled,
349
503
  };
350
504
  this.activeRuns.set(runId, run);
351
505
  this.activeSessionRuns.set(sessionId, run);
@@ -353,16 +507,23 @@ export class PluginHostRpc {
353
507
  this.activeRunsByOperation.set(operationId, run);
354
508
  let started;
355
509
  try {
356
- started = await this.services.conversationRuntime.runLive({
357
- message,
358
- threadId: sessionId,
359
- workspace: session.workspace,
360
- execution: session.execution,
510
+ started = await this.services.sessionRuns.startRun(sessionId, message !== undefined
511
+ ? {
512
+ message,
513
+ clientMessageId: operationId ?? `plugin_run_${runId}`,
514
+ }
515
+ : {
516
+ content: content,
517
+ clientMessageId: operationId ?? `plugin_run_${runId}`,
518
+ }, {
361
519
  signal: abort.signal,
362
- steer,
520
+ steer: runInput,
521
+ ...(collaborationMode ? { collaborationMode } : {}),
363
522
  });
364
523
  }
365
524
  catch (error) {
525
+ run.startupSettled = true;
526
+ run.rejectStarted(error);
366
527
  this.releaseRun(run);
367
528
  throw error;
368
529
  }
@@ -371,20 +532,30 @@ export class PluginHostRpc {
371
532
  }
372
533
  if (signal?.aborted || this.disposed || this.activeRuns.get(runId) !== run) {
373
534
  abort.abort(signal?.reason);
535
+ const error = signal?.aborted
536
+ ? new PluginHostCallError("REQUEST_CANCELLED", "plugin host request was cancelled")
537
+ : new PluginHostCallError("HOST_CLOSED", "plugin host is closed");
538
+ run.startupSettled = true;
539
+ run.rejectStarted(error);
374
540
  this.releaseRun(run);
375
541
  throwIfHostCallAborted(signal);
376
- throw new PluginHostCallError("HOST_CLOSED", "plugin host is closed");
542
+ throw error;
377
543
  }
378
- // Let the RPC transport write the request response before stream events are
379
- // emitted. `runLive` has already subscribed, so no agent event is lost.
380
- setImmediate(() => void this.pumpRun(run, started.generator));
381
- return {
544
+ run.responseId = started.responseId;
545
+ const result = {
382
546
  runId,
547
+ responseId: started.responseId,
383
548
  sessionId,
384
549
  provider: started.provider,
385
550
  model: started.model,
386
551
  authSource: started.authSource,
387
552
  };
553
+ run.startupSettled = true;
554
+ run.resolveStarted(result);
555
+ // Let the RPC transport write the request response before stream events are
556
+ // emitted. `runLive` has already subscribed, so no agent event is lost.
557
+ setImmediate(() => void this.pumpRun(run, started.generator));
558
+ return result;
388
559
  }
389
560
  async pumpRun(run, generator) {
390
561
  let terminalFailure;
@@ -403,6 +574,16 @@ export class PluginHostRpc {
403
574
  for (const projected of projectSessionEvent(run, event)) {
404
575
  await this.emit(projected);
405
576
  }
577
+ try {
578
+ const fileEvent = await this.projectSessionFileEvent(run, event);
579
+ if (fileEvent)
580
+ await this.emit(fileEvent);
581
+ }
582
+ catch {
583
+ // File delivery is an optional projection of a canonical event. A
584
+ // missing borrowed path must not turn an otherwise successful native
585
+ // Turn into session.run.failed or stop consuming its event stream.
586
+ }
406
587
  terminalFailure = sessionRunFailure(event);
407
588
  if (terminalFailure)
408
589
  break;
@@ -465,28 +646,83 @@ export class PluginHostRpc {
465
646
  this.releaseRun(run);
466
647
  }
467
648
  }
649
+ async projectSessionFileEvent(run, event) {
650
+ const input = jsonRecord(event);
651
+ if (input?.type !== "response.output_item.done")
652
+ return undefined;
653
+ const item = jsonRecord(input.item);
654
+ if (item?.type !== "function_call_output")
655
+ return undefined;
656
+ const data = jsonRecord(item.data);
657
+ const image = jsonRecord(data?.generatedImage);
658
+ if (!image || typeof image.mediaType !== "string")
659
+ return undefined;
660
+ let filePath;
661
+ if (typeof image.path === "string" && path.isAbsolute(image.path)) {
662
+ filePath = image.path;
663
+ }
664
+ else if (typeof image.resourceId === "string") {
665
+ filePath = await this.services.sessionFiles.resolveResourcePath(run.sessionId, image.resourceId);
666
+ }
667
+ else {
668
+ return undefined;
669
+ }
670
+ return {
671
+ event: "session.run.file",
672
+ payload: {
673
+ runId: run.runId,
674
+ responseId: typeof item.responseId === "string"
675
+ ? item.responseId
676
+ : run.responseId,
677
+ sessionId: run.sessionId,
678
+ itemId: typeof item.id === "string" ? item.id : "unknown",
679
+ filename: typeof image.filename === "string" && image.filename
680
+ ? image.filename
681
+ : path.basename(filePath),
682
+ mediaType: image.mediaType,
683
+ path: filePath,
684
+ },
685
+ };
686
+ }
468
687
  releaseRun(run) {
688
+ run.input.close();
689
+ if (!run.startupSettled) {
690
+ run.startupSettled = true;
691
+ run.rejectStarted(new PluginHostCallError("HOST_CLOSED", "plugin Host closed the active Run"));
692
+ }
469
693
  if (this.activeRuns.get(run.runId) === run)
470
694
  this.activeRuns.delete(run.runId);
471
695
  if (this.activeSessionRuns.get(run.sessionId) === run) {
472
696
  this.activeSessionRuns.delete(run.sessionId);
473
697
  }
474
- if (run.operationId && this.activeRunsByOperation.get(run.operationId) === run) {
475
- this.activeRunsByOperation.delete(run.operationId);
698
+ for (const operationId of run.operationIds) {
699
+ if (this.activeRunsByOperation.get(operationId) === run) {
700
+ this.activeRunsByOperation.delete(operationId);
701
+ }
476
702
  }
477
- }
478
- steerSession(params) {
479
- const input = record(params);
480
- const run = this.activeRun(input);
481
- const text = requiredText(input.text, "text", MAX_MESSAGE_LENGTH);
482
- return { delivered: run.steer.send(text) };
703
+ run.resolveSettled();
483
704
  }
484
705
  async interruptRun(params) {
485
706
  const input = record(params);
486
- const run = this.activeRunByIdentity(input);
487
- const forceAfterMs = optionalPositiveNumber(input.forceAfterMs, "forceAfterMs");
488
- if (forceAfterMs !== undefined && forceAfterMs > 30_000) {
489
- throw new PluginHostCallError("INVALID_PARAMS", "forceAfterMs must not exceed 30000");
707
+ const sessionId = optionalString(input.sessionId, "sessionId", 128);
708
+ const runId = optionalString(input.runId, "runId", 128);
709
+ const operationId = optionalString(input.operationId, "operationId", 512);
710
+ if ((sessionId ? 1 : 0) + (runId ? 1 : 0) + (operationId ? 1 : 0) !== 1) {
711
+ throw new PluginHostCallError("INVALID_PARAMS", "exactly one of sessionId, runId, or operationId is required");
712
+ }
713
+ let run;
714
+ if (sessionId) {
715
+ this.ownedSession(sessionId);
716
+ run = this.activeSessionRuns.get(sessionId);
717
+ if (!run) {
718
+ const interrupted = this.services.interruptSession
719
+ ? Promise.resolve().then(() => this.services.interruptSession(sessionId))
720
+ : Promise.resolve(false);
721
+ return { interrupted: await interrupted };
722
+ }
723
+ }
724
+ else {
725
+ run = this.activeRunByIdentity(input);
490
726
  }
491
727
  if (run.interruptResult) {
492
728
  return { interrupted: await run.interruptResult };
@@ -506,9 +742,7 @@ export class PluginHostRpc {
506
742
  // effort and cannot prove that native execution stopped.
507
743
  return false;
508
744
  });
509
- const interruptResult = forceAfterMs === undefined
510
- ? graceful
511
- : forceStoppedInterrupt(graceful, forceAfterMs, () => this.services.terminateSession?.(run.sessionId) ?? Promise.resolve(false));
745
+ const interruptResult = graceful;
512
746
  run.interruptResult = interruptResult;
513
747
  try {
514
748
  const interrupted = await interruptResult;
@@ -526,6 +760,17 @@ export class PluginHostRpc {
526
760
  throw error;
527
761
  }
528
762
  }
763
+ async sessionActivity(params) {
764
+ const sessionId = requiredSessionId(params);
765
+ this.ownedSession(sessionId);
766
+ const snapshot = structuredClone(await this.services.sessionActivity(sessionId));
767
+ const waiting = snapshot.pendingInteractions.length > 0;
768
+ return {
769
+ status: waiting ? "waiting" : snapshot.status,
770
+ activeResponseIds: snapshot.activeResponseIds,
771
+ pendingInteractions: snapshot.pendingInteractions,
772
+ };
773
+ }
529
774
  async interruptWon(run) {
530
775
  if (!run.interruptRequested)
531
776
  return false;
@@ -533,13 +778,6 @@ export class PluginHostRpc {
533
778
  return true;
534
779
  return run.interruptResult.catch(() => false);
535
780
  }
536
- activeRun(input) {
537
- const runId = requiredString(input.runId, "runId", 128);
538
- const run = this.activeRuns.get(runId);
539
- if (!run)
540
- throw new PluginHostCallError("RUN_NOT_FOUND", `active run not found: ${runId}`);
541
- return run;
542
- }
543
781
  activeRunByIdentity(input) {
544
782
  const runId = optionalString(input.runId, "runId", 128);
545
783
  const operationId = optionalString(input.operationId, "operationId", 512);
@@ -633,6 +871,36 @@ export class PluginHostRpc {
633
871
  this.services.sessionRegistry.setTitle(sessionId, title);
634
872
  return { updated: true };
635
873
  }
874
+ async getSessionBrowserRequestHeaders(params) {
875
+ let input;
876
+ try {
877
+ input = parseRuntimeBrowserRequestHeadersGetParams(params);
878
+ }
879
+ catch {
880
+ throw new PluginHostCallError("INVALID_PARAMS", "Session Browser request headers parameters are invalid");
881
+ }
882
+ this.ownedSession(input.sessionId);
883
+ const service = this.services.sessionBrowserRequestHeaders;
884
+ if (!service) {
885
+ throw new PluginHostCallError("METHOD_NOT_FOUND", "Session Browser request headers are unsupported by this Host");
886
+ }
887
+ return await service.getRequestHeaders(input);
888
+ }
889
+ async setSessionBrowserRequestHeaders(params) {
890
+ let input;
891
+ try {
892
+ input = parseRuntimeBrowserRequestHeadersSetParams(params);
893
+ }
894
+ catch {
895
+ throw new PluginHostCallError("INVALID_PARAMS", "Session Browser request headers parameters are invalid");
896
+ }
897
+ this.ownedSession(input.sessionId);
898
+ const service = this.services.sessionBrowserRequestHeaders;
899
+ if (!service) {
900
+ throw new PluginHostCallError("METHOD_NOT_FOUND", "Session Browser request headers are unsupported by this Host");
901
+ }
902
+ return await service.setRequestHeaders(input);
903
+ }
636
904
  resolveAgentExecution(params) {
637
905
  const input = record(params);
638
906
  const defaultAgent = optionalAgentId(input.defaultAgent, "defaultAgent");
@@ -652,6 +920,56 @@ export class PluginHostRpc {
652
920
  ...(ttlSeconds === undefined ? {} : { ttlMs: ttlSeconds * 1_000 }),
653
921
  });
654
922
  }
923
+ issueSessionPortalLink(scope, access, sessionId, embedOrigin, grantHandle) {
924
+ if (!this.services.sessionPortal) {
925
+ throw new PluginHostCallError("HOST_CALL_FAILED", "the Session Portal is not configured");
926
+ }
927
+ return issueSessionPortalTicket({
928
+ pluginId: this.principalId,
929
+ scope,
930
+ access,
931
+ ...(sessionId ? { sessionId } : {}),
932
+ ...(embedOrigin ? { embedOrigin } : {}),
933
+ ...(grantHandle ? { grantHandle } : {}),
934
+ });
935
+ }
936
+ async projectTimelineText(params) {
937
+ const sessionTimeline = this.services.sessionTimeline;
938
+ if (!sessionTimeline) {
939
+ throw new PluginHostCallError("HOST_CALL_FAILED", "Session timeline projection is unavailable");
940
+ }
941
+ const input = record(params);
942
+ const sessionId = requiredSessionId(input);
943
+ this.ownedSession(sessionId);
944
+ const projection = claimSessionTimelineProjection({
945
+ pluginId: this.principalId,
946
+ sessionId,
947
+ projectionId: requiredString(input.projectionId, "projectionId", 256),
948
+ ...(input.responseId === undefined
949
+ ? {}
950
+ : { responseId: requiredString(input.responseId, "responseId", 256) }),
951
+ text: requiredText(input.text, "text", 64 * 1024),
952
+ });
953
+ if (!(await sessionTimeline.hasItem(sessionId, projection.itemId))) {
954
+ await sessionTimeline.projectText({
955
+ sessionId,
956
+ responseId: projection.responseId,
957
+ itemId: projection.itemId,
958
+ text: projection.text,
959
+ });
960
+ }
961
+ completeSessionTimelineProjection({
962
+ pluginId: this.principalId,
963
+ sessionId,
964
+ projectionId: projection.projectionId,
965
+ });
966
+ return {
967
+ projectionId: projection.projectionId,
968
+ responseId: projection.responseId,
969
+ itemId: projection.itemId,
970
+ disposition: projection.disposition,
971
+ };
972
+ }
655
973
  ownedSession(sessionId) {
656
974
  const session = this.services.sessionRegistry.get(sessionId);
657
975
  if (!session || !this.isOwned(session)) {
@@ -678,6 +996,24 @@ function throwIfHostCallAborted(signal) {
678
996
  return;
679
997
  throw new PluginHostCallError("REQUEST_CANCELLED", "plugin host request was cancelled");
680
998
  }
999
+ async function waitForHostPromise(promise, signal) {
1000
+ throwIfHostCallAborted(signal);
1001
+ if (!signal)
1002
+ return await promise;
1003
+ return await new Promise((resolve, reject) => {
1004
+ const aborted = () => {
1005
+ reject(new PluginHostCallError("REQUEST_CANCELLED", "plugin host request was cancelled"));
1006
+ };
1007
+ signal.addEventListener("abort", aborted, { once: true });
1008
+ void promise.then((value) => {
1009
+ signal.removeEventListener("abort", aborted);
1010
+ resolve(value);
1011
+ }, (error) => {
1012
+ signal.removeEventListener("abort", aborted);
1013
+ reject(error);
1014
+ });
1015
+ });
1016
+ }
681
1017
  export class PluginHostCallError extends PluginRunnerError {
682
1018
  retryable;
683
1019
  data;
@@ -694,12 +1030,30 @@ export class PluginHostCallError extends PluginRunnerError {
694
1030
  function toHostCallError(error) {
695
1031
  if (error instanceof PluginHostCallError)
696
1032
  return error;
1033
+ if (error instanceof SessionBrowserServiceError) {
1034
+ if (error.code === "revision_mismatch") {
1035
+ return new PluginHostCallError("REVISION_CONFLICT", "Session Browser request headers changed since they were read");
1036
+ }
1037
+ if (error.code === "invalid_request") {
1038
+ return new PluginHostCallError("INVALID_PARAMS", "Session Browser request headers parameters are invalid");
1039
+ }
1040
+ if (error.code === "not_found") {
1041
+ return new PluginHostCallError("SESSION_NOT_FOUND", "plugin session not found");
1042
+ }
1043
+ return new PluginHostCallError("HOST_CALL_FAILED", "Session Browser request headers operation failed");
1044
+ }
697
1045
  if (error instanceof AgentRuntimeError && error.code === "live_unavailable") {
698
1046
  // ConversationRuntime emits this code only before injectMessage is called:
699
1047
  // the native live Session (including its exact Skill environment) could
700
1048
  // not be brought up, so callers may fail without quarantining capacity.
701
1049
  return new PluginHostCallError("LIVE_SESSION_UNAVAILABLE", error.message, false);
702
1050
  }
1051
+ if (error instanceof AgentRuntimeError) {
1052
+ // Native lifecycle failures already carry a stable phase code. Preserve it
1053
+ // across the plugin-host boundary instead of erasing it behind the generic
1054
+ // HOST_CALL_FAILED wrapper.
1055
+ return new PluginHostCallError(error.code, error.message, false);
1056
+ }
703
1057
  if (error instanceof SessionLaunchOperationConflictError) {
704
1058
  return new PluginHostCallError(error.code, error.message, false);
705
1059
  }
@@ -717,7 +1071,10 @@ function sessionLaunchSelection(value) {
717
1071
  const agent = optionalAgentId(input.agent, "session.agent");
718
1072
  const config = input.config === undefined ? undefined : agentSpecSchema.parse(input.config);
719
1073
  const provider = optionalRuntimeValue(input.provider);
720
- const model = optionalString(input.model, "session.model", 256);
1074
+ const modelProvided = input.model !== undefined;
1075
+ const model = input.model === null || input.model === ""
1076
+ ? null
1077
+ : optionalString(input.model, "session.model", 256);
721
1078
  const reasoningEffort = optionalString(input.reasoningEffort, "session.reasoningEffort", 128);
722
1079
  const title = optionalString(input.title, "session.title", 1_000);
723
1080
  if (Number(Boolean(projectId)) + Number(Boolean(managedCwd)) + Number(Boolean(workspace)) > 1) {
@@ -736,7 +1093,7 @@ function sessionLaunchSelection(value) {
736
1093
  ...(agent ? { agent } : {}),
737
1094
  ...(config ? { config } : {}),
738
1095
  ...(provider ? { provider } : {}),
739
- ...(model ? { model } : {}),
1096
+ ...(modelProvided ? { model: model ?? null } : {}),
740
1097
  ...(reasoningEffort ? { reasoningEffort } : {}),
741
1098
  ...(title ? { title } : {}),
742
1099
  };
@@ -768,6 +1125,114 @@ function sessionExecutionSnapshot(value) {
768
1125
  function requiredSessionId(params) {
769
1126
  return requiredString(record(params).sessionId, "sessionId", 512);
770
1127
  }
1128
+ async function pluginRunContent(value) {
1129
+ if (!Array.isArray(value) || value.length === 0 || value.length > 256) {
1130
+ throw new PluginHostCallError("INVALID_PARAMS", "content must be a non-empty array with at most 256 parts");
1131
+ }
1132
+ return Promise.all(value.map(async (entry, index) => {
1133
+ const input = record(entry);
1134
+ if (input.type === "input_text") {
1135
+ return {
1136
+ type: "input_text",
1137
+ text: requiredText(input.text, `content[${index}].text`, MAX_MESSAGE_LENGTH),
1138
+ };
1139
+ }
1140
+ if (input.type !== "input_image" && input.type !== "input_file") {
1141
+ throw new PluginHostCallError("INVALID_PARAMS", `content[${index}].type is invalid`);
1142
+ }
1143
+ const suppliedPath = requiredPathString(input.path, `content[${index}].path`, 16_384);
1144
+ if (!path.isAbsolute(suppliedPath)) {
1145
+ throw new PluginHostCallError("INVALID_PARAMS", `content[${index}].path must be absolute`);
1146
+ }
1147
+ let resolvedPath;
1148
+ let fileSize;
1149
+ try {
1150
+ resolvedPath = await realpath(suppliedPath);
1151
+ const metadata = await stat(resolvedPath);
1152
+ if (!metadata.isFile())
1153
+ throw new Error("not a regular file");
1154
+ fileSize = metadata.size;
1155
+ }
1156
+ catch (error) {
1157
+ throw new PluginHostCallError("INVALID_PARAMS", `content[${index}].path is not a readable regular file: ${error instanceof Error ? error.message : String(error)}`);
1158
+ }
1159
+ const filename = input.type === "input_file"
1160
+ ? requiredString(input.filename, `content[${index}].filename`, 255)
1161
+ : optionalString(input.filename, `content[${index}].filename`, 255) ??
1162
+ path.basename(resolvedPath);
1163
+ if (input.type === "input_image") {
1164
+ return {
1165
+ type: "input_image",
1166
+ path: resolvedPath,
1167
+ filename,
1168
+ mediaType: pluginImageMediaType(input.mediaType, resolvedPath, `content[${index}].mediaType`),
1169
+ byteLength: fileSize,
1170
+ };
1171
+ }
1172
+ return {
1173
+ type: "input_file",
1174
+ path: resolvedPath,
1175
+ filename,
1176
+ mediaType: pluginMediaType(input.mediaType ?? "application/octet-stream", `content[${index}].mediaType`),
1177
+ byteLength: fileSize,
1178
+ };
1179
+ }));
1180
+ }
1181
+ function pluginImageMediaType(value, filePath, label) {
1182
+ const inferred = (() => {
1183
+ switch (path.extname(filePath).toLowerCase()) {
1184
+ case ".png": return "image/png";
1185
+ case ".jpg":
1186
+ case ".jpeg": return "image/jpeg";
1187
+ case ".webp": return "image/webp";
1188
+ case ".gif": return "image/gif";
1189
+ case ".svg": return "image/svg+xml";
1190
+ default: return undefined;
1191
+ }
1192
+ })();
1193
+ const mediaType = value === undefined ? inferred : pluginMediaType(value, label);
1194
+ if (mediaType !== "image/png" &&
1195
+ mediaType !== "image/jpeg" &&
1196
+ mediaType !== "image/webp" &&
1197
+ mediaType !== "image/gif" &&
1198
+ mediaType !== "image/svg+xml") {
1199
+ throw new PluginHostCallError("INVALID_PARAMS", `${label} is not a supported image type`);
1200
+ }
1201
+ return mediaType;
1202
+ }
1203
+ function pluginMediaType(value, label) {
1204
+ if (typeof value !== "string") {
1205
+ throw new PluginHostCallError("INVALID_PARAMS", `${label} must be a media type`);
1206
+ }
1207
+ const mediaType = value.trim().toLowerCase();
1208
+ if (mediaType.length === 0 ||
1209
+ mediaType.length > 255 ||
1210
+ !/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(mediaType)) {
1211
+ throw new PluginHostCallError("INVALID_PARAMS", `${label} is invalid`);
1212
+ }
1213
+ return mediaType;
1214
+ }
1215
+ function requiredSessionPortalAccess(params) {
1216
+ const access = record(params).access;
1217
+ if (access === "read_only" || access === "read_write")
1218
+ return access;
1219
+ throw new PluginHostCallError("INVALID_PARAMS", "access must be read_only or read_write");
1220
+ }
1221
+ function optionalSessionPortalEmbedOrigin(value) {
1222
+ if (value === undefined)
1223
+ return undefined;
1224
+ const candidate = requiredString(value, "embedOrigin", 512);
1225
+ try {
1226
+ const parsed = new URL(candidate);
1227
+ if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
1228
+ parsed.origin !== candidate)
1229
+ throw new Error("invalid origin");
1230
+ return parsed.origin;
1231
+ }
1232
+ catch {
1233
+ throw new PluginHostCallError("INVALID_PARAMS", "embedOrigin must be an exact http(s) origin");
1234
+ }
1235
+ }
771
1236
  function sessionInteractionResolution(value) {
772
1237
  const input = record(value);
773
1238
  const keys = Object.keys(input);
@@ -810,6 +1275,13 @@ function sessionInteractionResolution(value) {
810
1275
  function optionalRuntime(params) {
811
1276
  return optionalRuntimeValue(record(params).runtime);
812
1277
  }
1278
+ function optionalCollaborationMode(value) {
1279
+ if (value === undefined)
1280
+ return undefined;
1281
+ if (value === "plan" || value === "default")
1282
+ return value;
1283
+ throw new PluginHostCallError("INVALID_PARAMS", "collaborationMode must be plan or default");
1284
+ }
813
1285
  function optionalRuntimeValue(value) {
814
1286
  if (value === undefined || value === null || value === "")
815
1287
  return undefined;