@rynx-ai/daemon 0.1.11-beta.4 → 0.1.11-beta.41

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 (72) 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 +6043 -6016
  7. package/bundled-plugins/plugins/lark/dist/settings.js +6003 -5985
  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-http.d.ts +9 -0
  14. package/dist/chrome-for-testing-http.js +58 -0
  15. package/dist/chrome-for-testing-release-resolver.js +13 -6
  16. package/dist/chrome-for-testing-runtime.js +4 -3
  17. package/dist/chrome-for-testing-store.d.ts +2 -0
  18. package/dist/chrome-for-testing-store.js +99 -17
  19. package/dist/control-deps.d.ts +6 -2
  20. package/dist/control-deps.js +87 -4
  21. package/dist/control-link-store.js +4 -3
  22. package/dist/daemon-owner.js +4 -7
  23. package/dist/daemon-server.js +42 -17
  24. package/dist/db.d.ts +2 -2
  25. package/dist/db.js +157 -21
  26. package/dist/headless-browser-host.js +397 -41
  27. package/dist/lifecycle.d.ts +2 -1
  28. package/dist/lifecycle.js +4 -1
  29. package/dist/migrations/cleanup-legacy-sessions.js +4 -3
  30. package/dist/plugin-cli.d.ts +10 -0
  31. package/dist/plugin-cli.js +19 -2
  32. package/dist/plugin-host-rpc.d.ts +48 -9
  33. package/dist/plugin-host-rpc.js +593 -81
  34. package/dist/plugin-installer.js +3 -2
  35. package/dist/plugin-management-service.js +0 -2
  36. package/dist/plugin-marketplace.js +2 -1
  37. package/dist/plugin-store.js +2 -4
  38. package/dist/plugin-supervisor.js +0 -4
  39. package/dist/pm2.d.ts +2 -0
  40. package/dist/pm2.js +6 -0
  41. package/dist/remote-runtime-access-store.d.ts +2 -2
  42. package/dist/remote-runtime-access-store.js +6 -9
  43. package/dist/remote-runtime-connection-manager.js +25 -42
  44. package/dist/remote-runtime-target-store.d.ts +2 -2
  45. package/dist/remote-runtime-target-store.js +6 -6
  46. package/dist/session-emulator-binding-store.js +4 -6
  47. package/dist/session-fork-store.d.ts +1 -0
  48. package/dist/session-fork-store.js +85 -30
  49. package/dist/session-launch-operations.js +6 -5
  50. package/dist/session-log-store.d.ts +1 -0
  51. package/dist/session-log-store.js +46 -22
  52. package/dist/session-meta-store.d.ts +5 -0
  53. package/dist/session-meta-store.js +25 -2
  54. package/dist/session-portal-grant-store.d.ts +55 -0
  55. package/dist/session-portal-grant-store.js +160 -0
  56. package/dist/session-resource-store.d.ts +24 -6
  57. package/dist/session-resource-store.js +387 -69
  58. package/dist/session-timeline-projection-store.d.ts +24 -0
  59. package/dist/session-timeline-projection-store.js +53 -0
  60. package/dist/setup-service.d.ts +1 -0
  61. package/dist/setup-service.js +1 -0
  62. package/dist/skills-catalog.d.ts +11 -2
  63. package/dist/skills-catalog.js +317 -10
  64. package/dist/sqlite.d.ts +22 -0
  65. package/dist/sqlite.js +57 -0
  66. package/dist/stdio-epipe-guard.d.ts +1 -1
  67. package/dist/stdio-epipe-guard.js +1 -1
  68. package/dist/system-dependencies.d.ts +22 -0
  69. package/dist/system-dependencies.js +79 -0
  70. package/package.json +17 -12
  71. package/bundled-plugins/plugins/lark/ui/assets/index-B2dhMlMU.css +0 -1
  72. /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,22 +532,33 @@ 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
+ ...(started.responseId ? { 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) {
561
+ let terminalCompleted = false;
390
562
  let terminalFailure;
391
563
  try {
392
564
  for await (const event of generator) {
@@ -396,6 +568,7 @@ export class PluginHostRpc {
396
568
  return;
397
569
  if (this.disposed || this.activeRuns.get(run.runId) !== run)
398
570
  return;
571
+ run.responseId ??= sessionEventResponseId(event);
399
572
  const finalResponse = topLevelAssistantResponse(event);
400
573
  if (finalResponse !== undefined) {
401
574
  run.finalAssistantResponse = truncateUtf8(finalResponse, MAX_PLUGIN_RUN_FINAL_RESPONSE_BYTES);
@@ -403,9 +576,23 @@ export class PluginHostRpc {
403
576
  for (const projected of projectSessionEvent(run, event)) {
404
577
  await this.emit(projected);
405
578
  }
579
+ try {
580
+ const fileEvent = await this.projectSessionFileEvent(run, event);
581
+ if (fileEvent)
582
+ await this.emit(fileEvent);
583
+ }
584
+ catch {
585
+ // File delivery is an optional projection of a canonical event. A
586
+ // missing borrowed path must not turn an otherwise successful native
587
+ // Turn into session.run.failed or stop consuming its event stream.
588
+ }
406
589
  terminalFailure = sessionRunFailure(event);
407
590
  if (terminalFailure)
408
591
  break;
592
+ if (sessionRunCompleted(event)) {
593
+ terminalCompleted = true;
594
+ break;
595
+ }
409
596
  }
410
597
  if (!this.disposed &&
411
598
  this.activeRuns.get(run.runId) === run &&
@@ -420,7 +607,7 @@ export class PluginHostRpc {
420
607
  },
421
608
  });
422
609
  }
423
- else {
610
+ else if (terminalCompleted) {
424
611
  await this.emit({
425
612
  event: "session.run.completed",
426
613
  payload: {
@@ -432,6 +619,20 @@ export class PluginHostRpc {
432
619
  },
433
620
  });
434
621
  }
622
+ else {
623
+ await this.emit({
624
+ event: "session.run.failed",
625
+ payload: {
626
+ runId: run.runId,
627
+ sessionId: run.sessionId,
628
+ error: {
629
+ code: "RUNTIME_EVENT_STREAM_ENDED",
630
+ message: "Runtime event stream ended before a terminal response event",
631
+ retryable: true,
632
+ },
633
+ },
634
+ });
635
+ }
435
636
  }
436
637
  }
437
638
  catch (error) {
@@ -465,28 +666,86 @@ export class PluginHostRpc {
465
666
  this.releaseRun(run);
466
667
  }
467
668
  }
669
+ async projectSessionFileEvent(run, event) {
670
+ const input = jsonRecord(event);
671
+ if (input?.type !== "response.output_item.done")
672
+ return undefined;
673
+ const item = jsonRecord(input.item);
674
+ if (item?.type !== "function_call_output")
675
+ return undefined;
676
+ const data = jsonRecord(item.data);
677
+ const image = jsonRecord(data?.generatedImage);
678
+ if (!image || typeof image.mediaType !== "string")
679
+ return undefined;
680
+ const responseId = typeof item.responseId === "string"
681
+ ? item.responseId
682
+ : run.responseId;
683
+ if (!responseId)
684
+ return undefined;
685
+ let filePath;
686
+ if (typeof image.path === "string" && path.isAbsolute(image.path)) {
687
+ filePath = image.path;
688
+ }
689
+ else if (typeof image.resourceId === "string") {
690
+ filePath = await this.services.sessionFiles.resolveResourcePath(run.sessionId, image.resourceId);
691
+ }
692
+ else {
693
+ return undefined;
694
+ }
695
+ return {
696
+ event: "session.run.file",
697
+ payload: {
698
+ runId: run.runId,
699
+ responseId,
700
+ sessionId: run.sessionId,
701
+ itemId: typeof item.id === "string" ? item.id : "unknown",
702
+ filename: typeof image.filename === "string" && image.filename
703
+ ? image.filename
704
+ : path.basename(filePath),
705
+ mediaType: image.mediaType,
706
+ path: filePath,
707
+ },
708
+ };
709
+ }
468
710
  releaseRun(run) {
711
+ run.input.close();
712
+ if (!run.startupSettled) {
713
+ run.startupSettled = true;
714
+ run.rejectStarted(new PluginHostCallError("HOST_CLOSED", "plugin Host closed the active Run"));
715
+ }
469
716
  if (this.activeRuns.get(run.runId) === run)
470
717
  this.activeRuns.delete(run.runId);
471
718
  if (this.activeSessionRuns.get(run.sessionId) === run) {
472
719
  this.activeSessionRuns.delete(run.sessionId);
473
720
  }
474
- if (run.operationId && this.activeRunsByOperation.get(run.operationId) === run) {
475
- this.activeRunsByOperation.delete(run.operationId);
721
+ for (const operationId of run.operationIds) {
722
+ if (this.activeRunsByOperation.get(operationId) === run) {
723
+ this.activeRunsByOperation.delete(operationId);
724
+ }
476
725
  }
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) };
726
+ run.resolveSettled();
483
727
  }
484
728
  async interruptRun(params) {
485
729
  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");
730
+ const sessionId = optionalString(input.sessionId, "sessionId", 128);
731
+ const runId = optionalString(input.runId, "runId", 128);
732
+ const operationId = optionalString(input.operationId, "operationId", 512);
733
+ if ((sessionId ? 1 : 0) + (runId ? 1 : 0) + (operationId ? 1 : 0) !== 1) {
734
+ throw new PluginHostCallError("INVALID_PARAMS", "exactly one of sessionId, runId, or operationId is required");
735
+ }
736
+ let run;
737
+ if (sessionId) {
738
+ this.ownedSession(sessionId);
739
+ run = this.activeSessionRuns.get(sessionId);
740
+ if (!run) {
741
+ const interrupted = this.services.interruptSession
742
+ ? Promise.resolve().then(() => this.services.interruptSession(sessionId))
743
+ : Promise.resolve(false);
744
+ return { interrupted: await interrupted };
745
+ }
746
+ }
747
+ else {
748
+ run = this.activeRunByIdentity(input);
490
749
  }
491
750
  if (run.interruptResult) {
492
751
  return { interrupted: await run.interruptResult };
@@ -506,9 +765,7 @@ export class PluginHostRpc {
506
765
  // effort and cannot prove that native execution stopped.
507
766
  return false;
508
767
  });
509
- const interruptResult = forceAfterMs === undefined
510
- ? graceful
511
- : forceStoppedInterrupt(graceful, forceAfterMs, () => this.services.terminateSession?.(run.sessionId) ?? Promise.resolve(false));
768
+ const interruptResult = graceful;
512
769
  run.interruptResult = interruptResult;
513
770
  try {
514
771
  const interrupted = await interruptResult;
@@ -526,6 +783,17 @@ export class PluginHostRpc {
526
783
  throw error;
527
784
  }
528
785
  }
786
+ async sessionActivity(params) {
787
+ const sessionId = requiredSessionId(params);
788
+ this.ownedSession(sessionId);
789
+ const snapshot = structuredClone(await this.services.sessionActivity(sessionId));
790
+ const waiting = snapshot.pendingInteractions.length > 0;
791
+ return {
792
+ status: waiting ? "waiting" : snapshot.status,
793
+ activeResponseIds: snapshot.activeResponseIds,
794
+ pendingInteractions: snapshot.pendingInteractions,
795
+ };
796
+ }
529
797
  async interruptWon(run) {
530
798
  if (!run.interruptRequested)
531
799
  return false;
@@ -533,13 +801,6 @@ export class PluginHostRpc {
533
801
  return true;
534
802
  return run.interruptResult.catch(() => false);
535
803
  }
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
804
  activeRunByIdentity(input) {
544
805
  const runId = optionalString(input.runId, "runId", 128);
545
806
  const operationId = optionalString(input.operationId, "operationId", 512);
@@ -633,6 +894,36 @@ export class PluginHostRpc {
633
894
  this.services.sessionRegistry.setTitle(sessionId, title);
634
895
  return { updated: true };
635
896
  }
897
+ async getSessionBrowserRequestHeaders(params) {
898
+ let input;
899
+ try {
900
+ input = parseRuntimeBrowserRequestHeadersGetParams(params);
901
+ }
902
+ catch {
903
+ throw new PluginHostCallError("INVALID_PARAMS", "Session Browser request headers parameters are invalid");
904
+ }
905
+ this.ownedSession(input.sessionId);
906
+ const service = this.services.sessionBrowserRequestHeaders;
907
+ if (!service) {
908
+ throw new PluginHostCallError("METHOD_NOT_FOUND", "Session Browser request headers are unsupported by this Host");
909
+ }
910
+ return await service.getRequestHeaders(input);
911
+ }
912
+ async setSessionBrowserRequestHeaders(params) {
913
+ let input;
914
+ try {
915
+ input = parseRuntimeBrowserRequestHeadersSetParams(params);
916
+ }
917
+ catch {
918
+ throw new PluginHostCallError("INVALID_PARAMS", "Session Browser request headers parameters are invalid");
919
+ }
920
+ this.ownedSession(input.sessionId);
921
+ const service = this.services.sessionBrowserRequestHeaders;
922
+ if (!service) {
923
+ throw new PluginHostCallError("METHOD_NOT_FOUND", "Session Browser request headers are unsupported by this Host");
924
+ }
925
+ return await service.setRequestHeaders(input);
926
+ }
636
927
  resolveAgentExecution(params) {
637
928
  const input = record(params);
638
929
  const defaultAgent = optionalAgentId(input.defaultAgent, "defaultAgent");
@@ -652,6 +943,56 @@ export class PluginHostRpc {
652
943
  ...(ttlSeconds === undefined ? {} : { ttlMs: ttlSeconds * 1_000 }),
653
944
  });
654
945
  }
946
+ issueSessionPortalLink(scope, access, sessionId, embedOrigin, grantHandle) {
947
+ if (!this.services.sessionPortal) {
948
+ throw new PluginHostCallError("HOST_CALL_FAILED", "the Session Portal is not configured");
949
+ }
950
+ return issueSessionPortalTicket({
951
+ pluginId: this.principalId,
952
+ scope,
953
+ access,
954
+ ...(sessionId ? { sessionId } : {}),
955
+ ...(embedOrigin ? { embedOrigin } : {}),
956
+ ...(grantHandle ? { grantHandle } : {}),
957
+ });
958
+ }
959
+ async projectTimelineText(params) {
960
+ const sessionTimeline = this.services.sessionTimeline;
961
+ if (!sessionTimeline) {
962
+ throw new PluginHostCallError("HOST_CALL_FAILED", "Session timeline projection is unavailable");
963
+ }
964
+ const input = record(params);
965
+ const sessionId = requiredSessionId(input);
966
+ this.ownedSession(sessionId);
967
+ const projection = claimSessionTimelineProjection({
968
+ pluginId: this.principalId,
969
+ sessionId,
970
+ projectionId: requiredString(input.projectionId, "projectionId", 256),
971
+ ...(input.responseId === undefined
972
+ ? {}
973
+ : { responseId: requiredString(input.responseId, "responseId", 256) }),
974
+ text: requiredText(input.text, "text", 64 * 1024),
975
+ });
976
+ if (!(await sessionTimeline.hasItem(sessionId, projection.itemId))) {
977
+ await sessionTimeline.projectText({
978
+ sessionId,
979
+ responseId: projection.responseId,
980
+ itemId: projection.itemId,
981
+ text: projection.text,
982
+ });
983
+ }
984
+ completeSessionTimelineProjection({
985
+ pluginId: this.principalId,
986
+ sessionId,
987
+ projectionId: projection.projectionId,
988
+ });
989
+ return {
990
+ projectionId: projection.projectionId,
991
+ responseId: projection.responseId,
992
+ itemId: projection.itemId,
993
+ disposition: projection.disposition,
994
+ };
995
+ }
655
996
  ownedSession(sessionId) {
656
997
  const session = this.services.sessionRegistry.get(sessionId);
657
998
  if (!session || !this.isOwned(session)) {
@@ -678,6 +1019,24 @@ function throwIfHostCallAborted(signal) {
678
1019
  return;
679
1020
  throw new PluginHostCallError("REQUEST_CANCELLED", "plugin host request was cancelled");
680
1021
  }
1022
+ async function waitForHostPromise(promise, signal) {
1023
+ throwIfHostCallAborted(signal);
1024
+ if (!signal)
1025
+ return await promise;
1026
+ return await new Promise((resolve, reject) => {
1027
+ const aborted = () => {
1028
+ reject(new PluginHostCallError("REQUEST_CANCELLED", "plugin host request was cancelled"));
1029
+ };
1030
+ signal.addEventListener("abort", aborted, { once: true });
1031
+ void promise.then((value) => {
1032
+ signal.removeEventListener("abort", aborted);
1033
+ resolve(value);
1034
+ }, (error) => {
1035
+ signal.removeEventListener("abort", aborted);
1036
+ reject(error);
1037
+ });
1038
+ });
1039
+ }
681
1040
  export class PluginHostCallError extends PluginRunnerError {
682
1041
  retryable;
683
1042
  data;
@@ -694,12 +1053,30 @@ export class PluginHostCallError extends PluginRunnerError {
694
1053
  function toHostCallError(error) {
695
1054
  if (error instanceof PluginHostCallError)
696
1055
  return error;
1056
+ if (error instanceof SessionBrowserServiceError) {
1057
+ if (error.code === "revision_mismatch") {
1058
+ return new PluginHostCallError("REVISION_CONFLICT", "Session Browser request headers changed since they were read");
1059
+ }
1060
+ if (error.code === "invalid_request") {
1061
+ return new PluginHostCallError("INVALID_PARAMS", "Session Browser request headers parameters are invalid");
1062
+ }
1063
+ if (error.code === "not_found") {
1064
+ return new PluginHostCallError("SESSION_NOT_FOUND", "plugin session not found");
1065
+ }
1066
+ return new PluginHostCallError("HOST_CALL_FAILED", "Session Browser request headers operation failed");
1067
+ }
697
1068
  if (error instanceof AgentRuntimeError && error.code === "live_unavailable") {
698
1069
  // ConversationRuntime emits this code only before injectMessage is called:
699
1070
  // the native live Session (including its exact Skill environment) could
700
1071
  // not be brought up, so callers may fail without quarantining capacity.
701
1072
  return new PluginHostCallError("LIVE_SESSION_UNAVAILABLE", error.message, false);
702
1073
  }
1074
+ if (error instanceof AgentRuntimeError) {
1075
+ // Native lifecycle failures already carry a stable phase code. Preserve it
1076
+ // across the plugin-host boundary instead of erasing it behind the generic
1077
+ // HOST_CALL_FAILED wrapper.
1078
+ return new PluginHostCallError(error.code, error.message, false);
1079
+ }
703
1080
  if (error instanceof SessionLaunchOperationConflictError) {
704
1081
  return new PluginHostCallError(error.code, error.message, false);
705
1082
  }
@@ -717,7 +1094,10 @@ function sessionLaunchSelection(value) {
717
1094
  const agent = optionalAgentId(input.agent, "session.agent");
718
1095
  const config = input.config === undefined ? undefined : agentSpecSchema.parse(input.config);
719
1096
  const provider = optionalRuntimeValue(input.provider);
720
- const model = optionalString(input.model, "session.model", 256);
1097
+ const modelProvided = input.model !== undefined;
1098
+ const model = input.model === null || input.model === ""
1099
+ ? null
1100
+ : optionalString(input.model, "session.model", 256);
721
1101
  const reasoningEffort = optionalString(input.reasoningEffort, "session.reasoningEffort", 128);
722
1102
  const title = optionalString(input.title, "session.title", 1_000);
723
1103
  if (Number(Boolean(projectId)) + Number(Boolean(managedCwd)) + Number(Boolean(workspace)) > 1) {
@@ -736,7 +1116,7 @@ function sessionLaunchSelection(value) {
736
1116
  ...(agent ? { agent } : {}),
737
1117
  ...(config ? { config } : {}),
738
1118
  ...(provider ? { provider } : {}),
739
- ...(model ? { model } : {}),
1119
+ ...(modelProvided ? { model: model ?? null } : {}),
740
1120
  ...(reasoningEffort ? { reasoningEffort } : {}),
741
1121
  ...(title ? { title } : {}),
742
1122
  };
@@ -768,6 +1148,114 @@ function sessionExecutionSnapshot(value) {
768
1148
  function requiredSessionId(params) {
769
1149
  return requiredString(record(params).sessionId, "sessionId", 512);
770
1150
  }
1151
+ async function pluginRunContent(value) {
1152
+ if (!Array.isArray(value) || value.length === 0 || value.length > 256) {
1153
+ throw new PluginHostCallError("INVALID_PARAMS", "content must be a non-empty array with at most 256 parts");
1154
+ }
1155
+ return Promise.all(value.map(async (entry, index) => {
1156
+ const input = record(entry);
1157
+ if (input.type === "input_text") {
1158
+ return {
1159
+ type: "input_text",
1160
+ text: requiredText(input.text, `content[${index}].text`, MAX_MESSAGE_LENGTH),
1161
+ };
1162
+ }
1163
+ if (input.type !== "input_image" && input.type !== "input_file") {
1164
+ throw new PluginHostCallError("INVALID_PARAMS", `content[${index}].type is invalid`);
1165
+ }
1166
+ const suppliedPath = requiredPathString(input.path, `content[${index}].path`, 16_384);
1167
+ if (!path.isAbsolute(suppliedPath)) {
1168
+ throw new PluginHostCallError("INVALID_PARAMS", `content[${index}].path must be absolute`);
1169
+ }
1170
+ let resolvedPath;
1171
+ let fileSize;
1172
+ try {
1173
+ resolvedPath = await realpath(suppliedPath);
1174
+ const metadata = await stat(resolvedPath);
1175
+ if (!metadata.isFile())
1176
+ throw new Error("not a regular file");
1177
+ fileSize = metadata.size;
1178
+ }
1179
+ catch (error) {
1180
+ throw new PluginHostCallError("INVALID_PARAMS", `content[${index}].path is not a readable regular file: ${error instanceof Error ? error.message : String(error)}`);
1181
+ }
1182
+ const filename = input.type === "input_file"
1183
+ ? requiredString(input.filename, `content[${index}].filename`, 255)
1184
+ : optionalString(input.filename, `content[${index}].filename`, 255) ??
1185
+ path.basename(resolvedPath);
1186
+ if (input.type === "input_image") {
1187
+ return {
1188
+ type: "input_image",
1189
+ path: resolvedPath,
1190
+ filename,
1191
+ mediaType: pluginImageMediaType(input.mediaType, resolvedPath, `content[${index}].mediaType`),
1192
+ byteLength: fileSize,
1193
+ };
1194
+ }
1195
+ return {
1196
+ type: "input_file",
1197
+ path: resolvedPath,
1198
+ filename,
1199
+ mediaType: pluginMediaType(input.mediaType ?? "application/octet-stream", `content[${index}].mediaType`),
1200
+ byteLength: fileSize,
1201
+ };
1202
+ }));
1203
+ }
1204
+ function pluginImageMediaType(value, filePath, label) {
1205
+ const inferred = (() => {
1206
+ switch (path.extname(filePath).toLowerCase()) {
1207
+ case ".png": return "image/png";
1208
+ case ".jpg":
1209
+ case ".jpeg": return "image/jpeg";
1210
+ case ".webp": return "image/webp";
1211
+ case ".gif": return "image/gif";
1212
+ case ".svg": return "image/svg+xml";
1213
+ default: return undefined;
1214
+ }
1215
+ })();
1216
+ const mediaType = value === undefined ? inferred : pluginMediaType(value, label);
1217
+ if (mediaType !== "image/png" &&
1218
+ mediaType !== "image/jpeg" &&
1219
+ mediaType !== "image/webp" &&
1220
+ mediaType !== "image/gif" &&
1221
+ mediaType !== "image/svg+xml") {
1222
+ throw new PluginHostCallError("INVALID_PARAMS", `${label} is not a supported image type`);
1223
+ }
1224
+ return mediaType;
1225
+ }
1226
+ function pluginMediaType(value, label) {
1227
+ if (typeof value !== "string") {
1228
+ throw new PluginHostCallError("INVALID_PARAMS", `${label} must be a media type`);
1229
+ }
1230
+ const mediaType = value.trim().toLowerCase();
1231
+ if (mediaType.length === 0 ||
1232
+ mediaType.length > 255 ||
1233
+ !/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(mediaType)) {
1234
+ throw new PluginHostCallError("INVALID_PARAMS", `${label} is invalid`);
1235
+ }
1236
+ return mediaType;
1237
+ }
1238
+ function requiredSessionPortalAccess(params) {
1239
+ const access = record(params).access;
1240
+ if (access === "read_only" || access === "read_write")
1241
+ return access;
1242
+ throw new PluginHostCallError("INVALID_PARAMS", "access must be read_only or read_write");
1243
+ }
1244
+ function optionalSessionPortalEmbedOrigin(value) {
1245
+ if (value === undefined)
1246
+ return undefined;
1247
+ const candidate = requiredString(value, "embedOrigin", 512);
1248
+ try {
1249
+ const parsed = new URL(candidate);
1250
+ if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
1251
+ parsed.origin !== candidate)
1252
+ throw new Error("invalid origin");
1253
+ return parsed.origin;
1254
+ }
1255
+ catch {
1256
+ throw new PluginHostCallError("INVALID_PARAMS", "embedOrigin must be an exact http(s) origin");
1257
+ }
1258
+ }
771
1259
  function sessionInteractionResolution(value) {
772
1260
  const input = record(value);
773
1261
  const keys = Object.keys(input);
@@ -810,6 +1298,13 @@ function sessionInteractionResolution(value) {
810
1298
  function optionalRuntime(params) {
811
1299
  return optionalRuntimeValue(record(params).runtime);
812
1300
  }
1301
+ function optionalCollaborationMode(value) {
1302
+ if (value === undefined)
1303
+ return undefined;
1304
+ if (value === "plan" || value === "default")
1305
+ return value;
1306
+ throw new PluginHostCallError("INVALID_PARAMS", "collaborationMode must be plan or default");
1307
+ }
813
1308
  function optionalRuntimeValue(value) {
814
1309
  if (value === undefined || value === null || value === "")
815
1310
  return undefined;
@@ -1024,8 +1519,22 @@ function topLevelAssistantResponse(event) {
1024
1519
  .trim();
1025
1520
  return text || undefined;
1026
1521
  }
1522
+ function sessionEventResponseId(event) {
1523
+ const input = jsonRecord(event);
1524
+ if (typeof input?.responseId === "string")
1525
+ return input.responseId;
1526
+ const item = jsonRecord(input?.item);
1527
+ return typeof item?.responseId === "string" ? item.responseId : undefined;
1528
+ }
1027
1529
  function sessionRunFailure(event) {
1028
1530
  const input = jsonRecord(event);
1531
+ if (input?.type === "session.interrupted") {
1532
+ return {
1533
+ code: "RUNTIME_TURN_INTERRUPTED",
1534
+ message: "Runtime Turn was interrupted",
1535
+ retryable: false,
1536
+ };
1537
+ }
1029
1538
  if (input?.type !== "response.failed")
1030
1539
  return undefined;
1031
1540
  const error = jsonRecord(input.error);
@@ -1041,6 +1550,9 @@ function sessionRunFailure(event) {
1041
1550
  retryable: false,
1042
1551
  };
1043
1552
  }
1553
+ function sessionRunCompleted(event) {
1554
+ return jsonRecord(event)?.type === "response.completed";
1555
+ }
1044
1556
  function sessionHostEvent(run, event, delivery) {
1045
1557
  return {
1046
1558
  event: "session.run.event",