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

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 (61) 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 +25 -14
  20. package/dist/db.d.ts +2 -2
  21. package/dist/db.js +133 -21
  22. package/dist/lifecycle.d.ts +1 -0
  23. package/dist/lifecycle.js +3 -0
  24. package/dist/migrations/cleanup-legacy-sessions.js +4 -3
  25. package/dist/plugin-cli.d.ts +10 -0
  26. package/dist/plugin-cli.js +19 -2
  27. package/dist/plugin-host-rpc.d.ts +31 -9
  28. package/dist/plugin-host-rpc.js +428 -80
  29. package/dist/plugin-installer.js +3 -2
  30. package/dist/plugin-management-service.js +0 -2
  31. package/dist/plugin-marketplace.js +2 -1
  32. package/dist/plugin-store.js +2 -4
  33. package/dist/plugin-supervisor.js +0 -4
  34. package/dist/remote-runtime-access-store.d.ts +2 -2
  35. package/dist/remote-runtime-access-store.js +6 -9
  36. package/dist/remote-runtime-connection-manager.js +1 -1
  37. package/dist/remote-runtime-target-store.d.ts +2 -2
  38. package/dist/remote-runtime-target-store.js +6 -6
  39. package/dist/session-emulator-binding-store.js +4 -6
  40. package/dist/session-fork-store.d.ts +1 -0
  41. package/dist/session-fork-store.js +82 -28
  42. package/dist/session-launch-operations.js +6 -5
  43. package/dist/session-log-store.d.ts +1 -0
  44. package/dist/session-log-store.js +19 -16
  45. package/dist/session-meta-store.d.ts +5 -0
  46. package/dist/session-meta-store.js +25 -2
  47. package/dist/session-portal-grant-store.d.ts +29 -0
  48. package/dist/session-portal-grant-store.js +85 -0
  49. package/dist/session-resource-store.d.ts +20 -6
  50. package/dist/session-resource-store.js +364 -68
  51. package/dist/setup-service.d.ts +1 -0
  52. package/dist/setup-service.js +1 -0
  53. package/dist/skills-catalog.d.ts +11 -2
  54. package/dist/skills-catalog.js +317 -10
  55. package/dist/sqlite.d.ts +22 -0
  56. package/dist/sqlite.js +57 -0
  57. package/dist/system-dependencies.d.ts +22 -0
  58. package/dist/system-dependencies.js +79 -0
  59. package/package.json +15 -12
  60. package/bundled-plugins/plugins/lark/ui/assets/index-B2dhMlMU.css +0 -1
  61. /package/bundled-plugins/plugins/lark/ui/assets/{index-D9N-AkLX.js → index-D8Amoe6a.js} +0 -0
@@ -1,11 +1,12 @@
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
6
  import { PluginRunnerError } from "@rynx-ai/plugin-runner";
7
7
  import { ensurePluginSessionLaunch, replayPluginSessionLaunch, SessionLaunchOperationConflictError, SessionLaunchOperationTargetDeletedError, } from "./session-launch-operations.js";
8
8
  import { createControlLink } from "./control-link-store.js";
9
+ import { issueSessionPortalTicket } from "./session-portal-grant-store.js";
9
10
  const MAX_MESSAGE_LENGTH = 1_000_000;
10
11
  /** Leaves ample room for the RPC envelope below the runner's 1 MiB line cap. */
11
12
  export const MAX_PLUGIN_HOST_EVENT_BYTES = 512 * 1024;
@@ -13,31 +14,59 @@ const MAX_PLUGIN_RUN_FINAL_RESPONSE_BYTES = 64 * 1024;
13
14
  const MAX_DELTA_CHUNK_CODE_UNITS = 64 * 1024;
14
15
  const SESSION_INTERACTION_RESOLUTION_MAX_BYTES = 128 * 1024;
15
16
  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
17
  /**
40
- * One authenticated plugin's capability-gated Host RPC surface.
18
+ * Buffers messages that arrive while the first `session.run` is still bringing
19
+ * up its native Turn. Once Core subscribes, later messages are delivered
20
+ * through the same input channel. After that subscription closes, this Run no
21
+ * longer accepts input; the caller waits for release and opens the next Run.
22
+ */
23
+ class PluginRunInputController {
24
+ listener;
25
+ pending = new Map();
26
+ subscribed = false;
27
+ closed = false;
28
+ enqueue(text) {
29
+ if (this.closed || (this.subscribed && !this.listener)) {
30
+ return { accepted: false, cancel: () => undefined };
31
+ }
32
+ if (this.listener) {
33
+ this.listener(text);
34
+ return { accepted: true, cancel: () => undefined };
35
+ }
36
+ const token = Symbol("plugin-run-input");
37
+ this.pending.set(token, text);
38
+ return {
39
+ accepted: true,
40
+ cancel: () => {
41
+ this.pending.delete(token);
42
+ },
43
+ };
44
+ }
45
+ subscribe(listener) {
46
+ if (this.closed)
47
+ return () => undefined;
48
+ if (this.listener) {
49
+ throw new Error("plugin Run input already has a subscriber");
50
+ }
51
+ this.subscribed = true;
52
+ this.listener = listener;
53
+ const queued = [...this.pending.values()];
54
+ this.pending.clear();
55
+ for (const text of queued)
56
+ listener(text);
57
+ return () => {
58
+ if (this.listener === listener)
59
+ this.listener = undefined;
60
+ };
61
+ }
62
+ close() {
63
+ this.closed = true;
64
+ this.listener = undefined;
65
+ this.pending.clear();
66
+ }
67
+ }
68
+ /**
69
+ * One authenticated plugin's Host RPC surface.
41
70
  *
42
71
  * This adapter is deliberately the only place that maps public string methods
43
72
  * to trusted runtime objects. The runner receives neither this class nor its
@@ -49,7 +78,6 @@ export class PluginHostRpc {
49
78
  daemonInstanceId;
50
79
  productVersion;
51
80
  cliVersion;
52
- capabilities;
53
81
  services;
54
82
  managedWorkspaceRoot;
55
83
  emit;
@@ -65,7 +93,6 @@ export class PluginHostRpc {
65
93
  this.daemonInstanceId = requiredString(options.daemonInstanceId, "daemonInstanceId", 128);
66
94
  this.productVersion = requiredString(options.productVersion, "productVersion", 128);
67
95
  this.cliVersion = requiredString(options.cliVersion, "cliVersion", 128);
68
- this.capabilities = new Set(options.capabilities);
69
96
  this.services = options.services;
70
97
  this.managedWorkspaceRoot = options.managedWorkspaceRoot
71
98
  ? path.resolve(options.managedWorkspaceRoot)
@@ -89,15 +116,15 @@ export class PluginHostRpc {
89
116
  publicKey: this.services.installationProof.publicKey,
90
117
  },
91
118
  protocolVersion: 1,
92
- capabilities: [...this.capabilities],
119
+ capabilities: {
120
+ sessionRunContent: true,
121
+ sessionRunFiles: true,
122
+ },
123
+ ...(this.services.sessionPortal
124
+ ? { sessionPortal: { ...this.services.sessionPortal } }
125
+ : {}),
93
126
  };
94
127
  }
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
128
  try {
102
129
  switch (method) {
103
130
  case "installation.proof.sign": {
@@ -118,8 +145,8 @@ export class PluginHostRpc {
118
145
  return await this.ensureSession(params);
119
146
  case "session.run":
120
147
  return await this.runSession(params, signal);
121
- case "session.steer":
122
- return this.steerSession(params);
148
+ case "session.activity.get":
149
+ return await this.sessionActivity(params);
123
150
  case "session.interrupt":
124
151
  return await this.interruptRun(params);
125
152
  case "session.interaction.resolve":
@@ -151,6 +178,16 @@ export class PluginHostRpc {
151
178
  return this.setSessionTitle(params);
152
179
  case "control.link.issue":
153
180
  return this.issueControlLink(params);
181
+ case "session.portal.plugin.issue": {
182
+ const access = requiredSessionPortalAccess(params);
183
+ return this.issueSessionPortalLink("plugin_sessions", access);
184
+ }
185
+ case "session.portal.session.issue": {
186
+ const input = record(params);
187
+ const sessionId = requiredSessionId(input);
188
+ this.ownedSession(sessionId);
189
+ return this.issueSessionPortalLink("single_session", requiredSessionPortalAccess(input), sessionId);
190
+ }
154
191
  default:
155
192
  throw new PluginHostCallError("METHOD_NOT_FOUND", `unknown host method: ${method}`);
156
193
  }
@@ -319,17 +356,86 @@ export class PluginHostRpc {
319
356
  const input = record(params);
320
357
  const sessionId = requiredSessionId(input);
321
358
  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);
359
+ const hasMessage = input.message !== undefined;
360
+ const hasContent = input.content !== undefined;
361
+ if (hasMessage === hasContent) {
362
+ throw new PluginHostCallError("INVALID_PARAMS", "provide exactly one of message or content");
363
+ }
364
+ const message = hasMessage
365
+ ? requiredText(input.message, "message", MAX_MESSAGE_LENGTH)
366
+ : undefined;
367
+ const collaborationMode = optionalCollaborationMode(input.collaborationMode);
368
+ let session = this.ownedSession(sessionId);
369
+ if (operationId) {
370
+ const replayed = this.activeRunsByOperation.get(operationId);
371
+ if (replayed) {
372
+ if (replayed.sessionId !== sessionId) {
373
+ throw new PluginHostCallError("RUN_ALREADY_ACTIVE", `operation already belongs to another active Session: ${operationId}`, true);
374
+ }
375
+ return await waitForHostPromise(replayed.started, signal);
376
+ }
325
377
  }
326
- if (this.activeSessionRuns.has(sessionId)) {
327
- throw new PluginHostCallError("RUN_ALREADY_ACTIVE", `session already has an active run: ${sessionId}`, true);
378
+ const active = this.activeSessionRuns.get(sessionId);
379
+ if (active) {
380
+ if (hasContent) {
381
+ // Native steering currently accepts text only. Preserve the attachment
382
+ // as a new Turn after the active response reaches its terminal edge.
383
+ await waitForHostPromise(active.settled, signal);
384
+ throwIfHostCallAborted(signal);
385
+ return await this.runSessionAdmitted(params, signal);
386
+ }
387
+ const queued = active.input.enqueue(message);
388
+ if (!queued.accepted) {
389
+ // The response reached its terminal boundary before Host cleanup won
390
+ // the scheduling race. Wait for that old Run to leave the Session slot,
391
+ // then submit this message as a new Turn with a new response identity.
392
+ await waitForHostPromise(active.settled, signal);
393
+ throwIfHostCallAborted(signal);
394
+ return await this.runSessionAdmitted(params, signal);
395
+ }
396
+ if (operationId) {
397
+ active.operationIds.add(operationId);
398
+ this.activeRunsByOperation.set(operationId, active);
399
+ }
400
+ try {
401
+ // Reuse the current Run identity. Core already owns its response stream;
402
+ // the buffered input will become turn/steer once that stream subscribes.
403
+ return await waitForHostPromise(active.started, signal);
404
+ }
405
+ catch (error) {
406
+ queued.cancel();
407
+ if (operationId && this.activeRunsByOperation.get(operationId) === active) {
408
+ this.activeRunsByOperation.delete(operationId);
409
+ active.operationIds.delete(operationId);
410
+ }
411
+ throw error;
412
+ }
328
413
  }
414
+ const modelProvided = input.model !== undefined;
415
+ const requestedModel = input.model === null || input.model === ""
416
+ ? null
417
+ : optionalString(input.model, "model", 256);
418
+ const reasoningEffortProvided = input.reasoningEffort !== undefined;
419
+ const requestedReasoningEffort = input.reasoningEffort === null || input.reasoningEffort === ""
420
+ ? null
421
+ : optionalString(input.reasoningEffort, "reasoningEffort", 64);
329
422
  if (this.activeRuns.size >= PLUGIN_HOST_MAX_ACTIVE_RUNS) {
330
423
  throw new PluginHostCallError("RUN_LIMIT", "plugin has too many active session runs", true);
331
424
  }
332
- const message = requiredText(input.message, "message", MAX_MESSAGE_LENGTH);
425
+ const nextModel = modelProvided ? requestedModel ?? null : session.execution.model;
426
+ const nextReasoningEffort = reasoningEffortProvided
427
+ ? requestedReasoningEffort ?? null
428
+ : session.execution.reasoningEffort;
429
+ if (nextModel !== session.execution.model ||
430
+ nextReasoningEffort !== session.execution.reasoningEffort) {
431
+ await this.services.sessionExecution.update({
432
+ sessionId,
433
+ model: nextModel,
434
+ reasoningEffort: nextReasoningEffort,
435
+ });
436
+ session = this.ownedSession(sessionId);
437
+ }
438
+ const content = hasContent ? await pluginRunContent(input.content) : undefined;
333
439
  const abort = new AbortController();
334
440
  const cancel = () => abort.abort(signal?.reason);
335
441
  if (signal?.aborted) {
@@ -337,15 +443,35 @@ export class PluginHostRpc {
337
443
  throwIfHostCallAborted(signal);
338
444
  }
339
445
  signal?.addEventListener("abort", cancel, { once: true });
340
- const steer = new SteerController();
446
+ const runInput = new PluginRunInputController();
341
447
  const runId = randomUUID();
448
+ let resolveStarted;
449
+ let rejectStarted;
450
+ const startedResult = new Promise((resolve, reject) => {
451
+ resolveStarted = resolve;
452
+ rejectStarted = reject;
453
+ });
454
+ // The initiating RPC observes startup directly below. This promise exists
455
+ // for concurrent/replayed calls, so retain its rejection without creating
456
+ // an unhandled-rejection when there are none.
457
+ void startedResult.catch(() => undefined);
458
+ let resolveSettled;
459
+ const settled = new Promise((resolve) => {
460
+ resolveSettled = resolve;
461
+ });
342
462
  const run = {
343
463
  runId,
344
464
  sessionId,
345
- operationId,
465
+ operationIds: new Set(operationId ? [operationId] : []),
346
466
  interruptRequested: false,
347
467
  abort,
348
- steer,
468
+ input: runInput,
469
+ started: startedResult,
470
+ startupSettled: false,
471
+ resolveStarted,
472
+ rejectStarted,
473
+ settled,
474
+ resolveSettled,
349
475
  };
350
476
  this.activeRuns.set(runId, run);
351
477
  this.activeSessionRuns.set(sessionId, run);
@@ -353,16 +479,23 @@ export class PluginHostRpc {
353
479
  this.activeRunsByOperation.set(operationId, run);
354
480
  let started;
355
481
  try {
356
- started = await this.services.conversationRuntime.runLive({
357
- message,
358
- threadId: sessionId,
359
- workspace: session.workspace,
360
- execution: session.execution,
482
+ started = await this.services.sessionRuns.startRun(sessionId, message !== undefined
483
+ ? {
484
+ message,
485
+ clientMessageId: operationId ?? `plugin_run_${runId}`,
486
+ }
487
+ : {
488
+ content: content,
489
+ clientMessageId: operationId ?? `plugin_run_${runId}`,
490
+ }, {
361
491
  signal: abort.signal,
362
- steer,
492
+ steer: runInput,
493
+ ...(collaborationMode ? { collaborationMode } : {}),
363
494
  });
364
495
  }
365
496
  catch (error) {
497
+ run.startupSettled = true;
498
+ run.rejectStarted(error);
366
499
  this.releaseRun(run);
367
500
  throw error;
368
501
  }
@@ -371,20 +504,30 @@ export class PluginHostRpc {
371
504
  }
372
505
  if (signal?.aborted || this.disposed || this.activeRuns.get(runId) !== run) {
373
506
  abort.abort(signal?.reason);
507
+ const error = signal?.aborted
508
+ ? new PluginHostCallError("REQUEST_CANCELLED", "plugin host request was cancelled")
509
+ : new PluginHostCallError("HOST_CLOSED", "plugin host is closed");
510
+ run.startupSettled = true;
511
+ run.rejectStarted(error);
374
512
  this.releaseRun(run);
375
513
  throwIfHostCallAborted(signal);
376
- throw new PluginHostCallError("HOST_CLOSED", "plugin host is closed");
514
+ throw error;
377
515
  }
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 {
516
+ run.responseId = started.responseId;
517
+ const result = {
382
518
  runId,
519
+ responseId: started.responseId,
383
520
  sessionId,
384
521
  provider: started.provider,
385
522
  model: started.model,
386
523
  authSource: started.authSource,
387
524
  };
525
+ run.startupSettled = true;
526
+ run.resolveStarted(result);
527
+ // Let the RPC transport write the request response before stream events are
528
+ // emitted. `runLive` has already subscribed, so no agent event is lost.
529
+ setImmediate(() => void this.pumpRun(run, started.generator));
530
+ return result;
388
531
  }
389
532
  async pumpRun(run, generator) {
390
533
  let terminalFailure;
@@ -403,6 +546,16 @@ export class PluginHostRpc {
403
546
  for (const projected of projectSessionEvent(run, event)) {
404
547
  await this.emit(projected);
405
548
  }
549
+ try {
550
+ const fileEvent = await this.projectSessionFileEvent(run, event);
551
+ if (fileEvent)
552
+ await this.emit(fileEvent);
553
+ }
554
+ catch {
555
+ // File delivery is an optional projection of a canonical event. A
556
+ // missing borrowed path must not turn an otherwise successful native
557
+ // Turn into session.run.failed or stop consuming its event stream.
558
+ }
406
559
  terminalFailure = sessionRunFailure(event);
407
560
  if (terminalFailure)
408
561
  break;
@@ -465,28 +618,83 @@ export class PluginHostRpc {
465
618
  this.releaseRun(run);
466
619
  }
467
620
  }
621
+ async projectSessionFileEvent(run, event) {
622
+ const input = jsonRecord(event);
623
+ if (input?.type !== "response.output_item.done")
624
+ return undefined;
625
+ const item = jsonRecord(input.item);
626
+ if (item?.type !== "function_call_output")
627
+ return undefined;
628
+ const data = jsonRecord(item.data);
629
+ const image = jsonRecord(data?.generatedImage);
630
+ if (!image || typeof image.mediaType !== "string")
631
+ return undefined;
632
+ let filePath;
633
+ if (typeof image.path === "string" && path.isAbsolute(image.path)) {
634
+ filePath = image.path;
635
+ }
636
+ else if (typeof image.resourceId === "string") {
637
+ filePath = await this.services.sessionFiles.resolveResourcePath(run.sessionId, image.resourceId);
638
+ }
639
+ else {
640
+ return undefined;
641
+ }
642
+ return {
643
+ event: "session.run.file",
644
+ payload: {
645
+ runId: run.runId,
646
+ responseId: typeof item.responseId === "string"
647
+ ? item.responseId
648
+ : run.responseId,
649
+ sessionId: run.sessionId,
650
+ itemId: typeof item.id === "string" ? item.id : "unknown",
651
+ filename: typeof image.filename === "string" && image.filename
652
+ ? image.filename
653
+ : path.basename(filePath),
654
+ mediaType: image.mediaType,
655
+ path: filePath,
656
+ },
657
+ };
658
+ }
468
659
  releaseRun(run) {
660
+ run.input.close();
661
+ if (!run.startupSettled) {
662
+ run.startupSettled = true;
663
+ run.rejectStarted(new PluginHostCallError("HOST_CLOSED", "plugin Host closed the active Run"));
664
+ }
469
665
  if (this.activeRuns.get(run.runId) === run)
470
666
  this.activeRuns.delete(run.runId);
471
667
  if (this.activeSessionRuns.get(run.sessionId) === run) {
472
668
  this.activeSessionRuns.delete(run.sessionId);
473
669
  }
474
- if (run.operationId && this.activeRunsByOperation.get(run.operationId) === run) {
475
- this.activeRunsByOperation.delete(run.operationId);
670
+ for (const operationId of run.operationIds) {
671
+ if (this.activeRunsByOperation.get(operationId) === run) {
672
+ this.activeRunsByOperation.delete(operationId);
673
+ }
476
674
  }
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) };
675
+ run.resolveSettled();
483
676
  }
484
677
  async interruptRun(params) {
485
678
  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");
679
+ const sessionId = optionalString(input.sessionId, "sessionId", 128);
680
+ const runId = optionalString(input.runId, "runId", 128);
681
+ const operationId = optionalString(input.operationId, "operationId", 512);
682
+ if ((sessionId ? 1 : 0) + (runId ? 1 : 0) + (operationId ? 1 : 0) !== 1) {
683
+ throw new PluginHostCallError("INVALID_PARAMS", "exactly one of sessionId, runId, or operationId is required");
684
+ }
685
+ let run;
686
+ if (sessionId) {
687
+ this.ownedSession(sessionId);
688
+ run = this.activeSessionRuns.get(sessionId);
689
+ if (!run) {
690
+ const interrupted = this.services.interruptSession
691
+ ? Promise.resolve().then(() => this.services.interruptSession(sessionId))
692
+ : Promise.resolve(false);
693
+ return { interrupted: await interrupted };
694
+ }
695
+ }
696
+ else {
697
+ run = this.activeRunByIdentity(input);
490
698
  }
491
699
  if (run.interruptResult) {
492
700
  return { interrupted: await run.interruptResult };
@@ -506,9 +714,7 @@ export class PluginHostRpc {
506
714
  // effort and cannot prove that native execution stopped.
507
715
  return false;
508
716
  });
509
- const interruptResult = forceAfterMs === undefined
510
- ? graceful
511
- : forceStoppedInterrupt(graceful, forceAfterMs, () => this.services.terminateSession?.(run.sessionId) ?? Promise.resolve(false));
717
+ const interruptResult = graceful;
512
718
  run.interruptResult = interruptResult;
513
719
  try {
514
720
  const interrupted = await interruptResult;
@@ -526,6 +732,17 @@ export class PluginHostRpc {
526
732
  throw error;
527
733
  }
528
734
  }
735
+ async sessionActivity(params) {
736
+ const sessionId = requiredSessionId(params);
737
+ this.ownedSession(sessionId);
738
+ const snapshot = structuredClone(await this.services.sessionActivity(sessionId));
739
+ const waiting = snapshot.pendingInteractions.length > 0;
740
+ return {
741
+ status: waiting ? "waiting" : snapshot.status,
742
+ activeResponseIds: snapshot.activeResponseIds,
743
+ pendingInteractions: snapshot.pendingInteractions,
744
+ };
745
+ }
529
746
  async interruptWon(run) {
530
747
  if (!run.interruptRequested)
531
748
  return false;
@@ -533,13 +750,6 @@ export class PluginHostRpc {
533
750
  return true;
534
751
  return run.interruptResult.catch(() => false);
535
752
  }
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
753
  activeRunByIdentity(input) {
544
754
  const runId = optionalString(input.runId, "runId", 128);
545
755
  const operationId = optionalString(input.operationId, "operationId", 512);
@@ -652,6 +862,17 @@ export class PluginHostRpc {
652
862
  ...(ttlSeconds === undefined ? {} : { ttlMs: ttlSeconds * 1_000 }),
653
863
  });
654
864
  }
865
+ issueSessionPortalLink(scope, access, sessionId) {
866
+ if (!this.services.sessionPortal) {
867
+ throw new PluginHostCallError("HOST_CALL_FAILED", "the Session Portal is not configured");
868
+ }
869
+ return issueSessionPortalTicket({
870
+ pluginId: this.principalId,
871
+ scope,
872
+ access,
873
+ ...(sessionId ? { sessionId } : {}),
874
+ });
875
+ }
655
876
  ownedSession(sessionId) {
656
877
  const session = this.services.sessionRegistry.get(sessionId);
657
878
  if (!session || !this.isOwned(session)) {
@@ -678,6 +899,24 @@ function throwIfHostCallAborted(signal) {
678
899
  return;
679
900
  throw new PluginHostCallError("REQUEST_CANCELLED", "plugin host request was cancelled");
680
901
  }
902
+ async function waitForHostPromise(promise, signal) {
903
+ throwIfHostCallAborted(signal);
904
+ if (!signal)
905
+ return await promise;
906
+ return await new Promise((resolve, reject) => {
907
+ const aborted = () => {
908
+ reject(new PluginHostCallError("REQUEST_CANCELLED", "plugin host request was cancelled"));
909
+ };
910
+ signal.addEventListener("abort", aborted, { once: true });
911
+ void promise.then((value) => {
912
+ signal.removeEventListener("abort", aborted);
913
+ resolve(value);
914
+ }, (error) => {
915
+ signal.removeEventListener("abort", aborted);
916
+ reject(error);
917
+ });
918
+ });
919
+ }
681
920
  export class PluginHostCallError extends PluginRunnerError {
682
921
  retryable;
683
922
  data;
@@ -700,6 +939,12 @@ function toHostCallError(error) {
700
939
  // not be brought up, so callers may fail without quarantining capacity.
701
940
  return new PluginHostCallError("LIVE_SESSION_UNAVAILABLE", error.message, false);
702
941
  }
942
+ if (error instanceof AgentRuntimeError) {
943
+ // Native lifecycle failures already carry a stable phase code. Preserve it
944
+ // across the plugin-host boundary instead of erasing it behind the generic
945
+ // HOST_CALL_FAILED wrapper.
946
+ return new PluginHostCallError(error.code, error.message, false);
947
+ }
703
948
  if (error instanceof SessionLaunchOperationConflictError) {
704
949
  return new PluginHostCallError(error.code, error.message, false);
705
950
  }
@@ -717,7 +962,10 @@ function sessionLaunchSelection(value) {
717
962
  const agent = optionalAgentId(input.agent, "session.agent");
718
963
  const config = input.config === undefined ? undefined : agentSpecSchema.parse(input.config);
719
964
  const provider = optionalRuntimeValue(input.provider);
720
- const model = optionalString(input.model, "session.model", 256);
965
+ const modelProvided = input.model !== undefined;
966
+ const model = input.model === null || input.model === ""
967
+ ? null
968
+ : optionalString(input.model, "session.model", 256);
721
969
  const reasoningEffort = optionalString(input.reasoningEffort, "session.reasoningEffort", 128);
722
970
  const title = optionalString(input.title, "session.title", 1_000);
723
971
  if (Number(Boolean(projectId)) + Number(Boolean(managedCwd)) + Number(Boolean(workspace)) > 1) {
@@ -736,7 +984,7 @@ function sessionLaunchSelection(value) {
736
984
  ...(agent ? { agent } : {}),
737
985
  ...(config ? { config } : {}),
738
986
  ...(provider ? { provider } : {}),
739
- ...(model ? { model } : {}),
987
+ ...(modelProvided ? { model: model ?? null } : {}),
740
988
  ...(reasoningEffort ? { reasoningEffort } : {}),
741
989
  ...(title ? { title } : {}),
742
990
  };
@@ -768,6 +1016,99 @@ function sessionExecutionSnapshot(value) {
768
1016
  function requiredSessionId(params) {
769
1017
  return requiredString(record(params).sessionId, "sessionId", 512);
770
1018
  }
1019
+ async function pluginRunContent(value) {
1020
+ if (!Array.isArray(value) || value.length === 0 || value.length > 256) {
1021
+ throw new PluginHostCallError("INVALID_PARAMS", "content must be a non-empty array with at most 256 parts");
1022
+ }
1023
+ return Promise.all(value.map(async (entry, index) => {
1024
+ const input = record(entry);
1025
+ if (input.type === "input_text") {
1026
+ return {
1027
+ type: "input_text",
1028
+ text: requiredText(input.text, `content[${index}].text`, MAX_MESSAGE_LENGTH),
1029
+ };
1030
+ }
1031
+ if (input.type !== "input_image" && input.type !== "input_file") {
1032
+ throw new PluginHostCallError("INVALID_PARAMS", `content[${index}].type is invalid`);
1033
+ }
1034
+ const suppliedPath = requiredPathString(input.path, `content[${index}].path`, 16_384);
1035
+ if (!path.isAbsolute(suppliedPath)) {
1036
+ throw new PluginHostCallError("INVALID_PARAMS", `content[${index}].path must be absolute`);
1037
+ }
1038
+ let resolvedPath;
1039
+ let fileSize;
1040
+ try {
1041
+ resolvedPath = await realpath(suppliedPath);
1042
+ const metadata = await stat(resolvedPath);
1043
+ if (!metadata.isFile())
1044
+ throw new Error("not a regular file");
1045
+ fileSize = metadata.size;
1046
+ }
1047
+ catch (error) {
1048
+ throw new PluginHostCallError("INVALID_PARAMS", `content[${index}].path is not a readable regular file: ${error instanceof Error ? error.message : String(error)}`);
1049
+ }
1050
+ const filename = input.type === "input_file"
1051
+ ? requiredString(input.filename, `content[${index}].filename`, 255)
1052
+ : optionalString(input.filename, `content[${index}].filename`, 255) ??
1053
+ path.basename(resolvedPath);
1054
+ if (input.type === "input_image") {
1055
+ return {
1056
+ type: "input_image",
1057
+ path: resolvedPath,
1058
+ filename,
1059
+ mediaType: pluginImageMediaType(input.mediaType, resolvedPath, `content[${index}].mediaType`),
1060
+ byteLength: fileSize,
1061
+ };
1062
+ }
1063
+ return {
1064
+ type: "input_file",
1065
+ path: resolvedPath,
1066
+ filename,
1067
+ mediaType: pluginMediaType(input.mediaType ?? "application/octet-stream", `content[${index}].mediaType`),
1068
+ byteLength: fileSize,
1069
+ };
1070
+ }));
1071
+ }
1072
+ function pluginImageMediaType(value, filePath, label) {
1073
+ const inferred = (() => {
1074
+ switch (path.extname(filePath).toLowerCase()) {
1075
+ case ".png": return "image/png";
1076
+ case ".jpg":
1077
+ case ".jpeg": return "image/jpeg";
1078
+ case ".webp": return "image/webp";
1079
+ case ".gif": return "image/gif";
1080
+ case ".svg": return "image/svg+xml";
1081
+ default: return undefined;
1082
+ }
1083
+ })();
1084
+ const mediaType = value === undefined ? inferred : pluginMediaType(value, label);
1085
+ if (mediaType !== "image/png" &&
1086
+ mediaType !== "image/jpeg" &&
1087
+ mediaType !== "image/webp" &&
1088
+ mediaType !== "image/gif" &&
1089
+ mediaType !== "image/svg+xml") {
1090
+ throw new PluginHostCallError("INVALID_PARAMS", `${label} is not a supported image type`);
1091
+ }
1092
+ return mediaType;
1093
+ }
1094
+ function pluginMediaType(value, label) {
1095
+ if (typeof value !== "string") {
1096
+ throw new PluginHostCallError("INVALID_PARAMS", `${label} must be a media type`);
1097
+ }
1098
+ const mediaType = value.trim().toLowerCase();
1099
+ if (mediaType.length === 0 ||
1100
+ mediaType.length > 255 ||
1101
+ !/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(mediaType)) {
1102
+ throw new PluginHostCallError("INVALID_PARAMS", `${label} is invalid`);
1103
+ }
1104
+ return mediaType;
1105
+ }
1106
+ function requiredSessionPortalAccess(params) {
1107
+ const access = record(params).access;
1108
+ if (access === "read_only" || access === "read_write")
1109
+ return access;
1110
+ throw new PluginHostCallError("INVALID_PARAMS", "access must be read_only or read_write");
1111
+ }
771
1112
  function sessionInteractionResolution(value) {
772
1113
  const input = record(value);
773
1114
  const keys = Object.keys(input);
@@ -810,6 +1151,13 @@ function sessionInteractionResolution(value) {
810
1151
  function optionalRuntime(params) {
811
1152
  return optionalRuntimeValue(record(params).runtime);
812
1153
  }
1154
+ function optionalCollaborationMode(value) {
1155
+ if (value === undefined)
1156
+ return undefined;
1157
+ if (value === "plan" || value === "default")
1158
+ return value;
1159
+ throw new PluginHostCallError("INVALID_PARAMS", "collaborationMode must be plan or default");
1160
+ }
813
1161
  function optionalRuntimeValue(value) {
814
1162
  if (value === undefined || value === null || value === "")
815
1163
  return undefined;