@rynx-ai/daemon 0.1.11-beta.2 → 0.1.11-beta.21

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 (88) 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 +20 -18
  5. package/bundled-plugins/plugins/lark/dist/lark.js +18 -12
  6. package/bundled-plugins/plugins/lark/dist/runtime.js +6046 -6022
  7. package/bundled-plugins/plugins/lark/dist/settings.js +6006 -5988
  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.d.ts +4 -1
  13. package/dist/browser-artifact-management.js +22 -11
  14. package/dist/browser-artifacts.d.ts +1 -0
  15. package/dist/browser-artifacts.js +1 -0
  16. package/dist/chrome-for-testing-runtime.d.ts +27 -0
  17. package/dist/chrome-for-testing-runtime.js +194 -0
  18. package/dist/chrome-for-testing-store.d.ts +2 -0
  19. package/dist/chrome-for-testing-store.js +37 -6
  20. package/dist/control-deps.d.ts +6 -2
  21. package/dist/control-deps.js +87 -4
  22. package/dist/control-link-store.js +4 -3
  23. package/dist/daemon-owner.js +4 -7
  24. package/dist/daemon-server.d.ts +4 -1
  25. package/dist/daemon-server.js +158 -45
  26. package/dist/db.d.ts +2 -2
  27. package/dist/db.js +55 -20
  28. package/dist/entry-path.d.ts +3 -0
  29. package/dist/entry-path.js +12 -0
  30. package/dist/index-daemon.js +3 -87
  31. package/dist/lifecycle.d.ts +13 -0
  32. package/dist/lifecycle.js +19 -0
  33. package/dist/maintenance-lease.d.ts +39 -0
  34. package/dist/maintenance-lease.js +250 -0
  35. package/dist/migrations/cleanup-legacy-sessions.js +4 -3
  36. package/dist/plugin-cli.d.ts +10 -0
  37. package/dist/plugin-cli.js +19 -2
  38. package/dist/plugin-host-rpc.d.ts +35 -6
  39. package/dist/plugin-host-rpc.js +222 -59
  40. package/dist/plugin-installer.js +62 -2
  41. package/dist/plugin-management-service.js +0 -2
  42. package/dist/plugin-marketplace.js +2 -1
  43. package/dist/plugin-store.js +2 -4
  44. package/dist/plugin-supervisor.d.ts +5 -0
  45. package/dist/plugin-supervisor.js +218 -33
  46. package/dist/pm2.d.ts +11 -2
  47. package/dist/pm2.js +116 -19
  48. package/dist/process.d.ts +12 -0
  49. package/dist/process.js +91 -0
  50. package/dist/remote-runtime-access-store.d.ts +2 -2
  51. package/dist/remote-runtime-access-store.js +6 -9
  52. package/dist/remote-runtime-target-store.d.ts +2 -2
  53. package/dist/remote-runtime-target-store.js +6 -6
  54. package/dist/session-emulator-binding-store.js +4 -6
  55. package/dist/session-fork-store.js +24 -8
  56. package/dist/session-launch-operations.d.ts +10 -1
  57. package/dist/session-launch-operations.js +21 -11
  58. package/dist/session-log-store.js +2 -1
  59. package/dist/session-meta-store.d.ts +5 -0
  60. package/dist/session-meta-store.js +25 -2
  61. package/dist/session-portal-grant-store.d.ts +29 -0
  62. package/dist/session-portal-grant-store.js +85 -0
  63. package/dist/session-resource-store.js +3 -2
  64. package/dist/setup-service.d.ts +21 -0
  65. package/dist/setup-service.js +39 -0
  66. package/dist/skills-catalog.d.ts +11 -2
  67. package/dist/skills-catalog.js +307 -10
  68. package/dist/sqlite.d.ts +22 -0
  69. package/dist/sqlite.js +57 -0
  70. package/dist/system-dependencies.d.ts +22 -0
  71. package/dist/system-dependencies.js +79 -0
  72. package/package.json +37 -21
  73. package/bundled-plugins/plugins/lark/ui/assets/index-B2dhMlMU.css +0 -1
  74. package/dist/browser-cli-args.d.ts +0 -20
  75. package/dist/browser-cli-args.js +0 -100
  76. package/dist/cli.d.ts +0 -2
  77. package/dist/cli.js +0 -550
  78. package/dist/control-client.d.ts +0 -122
  79. package/dist/control-client.js +0 -855
  80. package/dist/control-endpoint.d.ts +0 -14
  81. package/dist/control-endpoint.js +0 -124
  82. package/dist/desktop-browser-host-client.d.ts +0 -35
  83. package/dist/desktop-browser-host-client.js +0 -415
  84. package/dist/setup.d.ts +0 -20
  85. package/dist/setup.js +0 -386
  86. package/dist/update.d.ts +0 -25
  87. package/dist/update.js +0 -133
  88. /package/bundled-plugins/plugins/lark/ui/assets/{index-D9N-AkLX.js → index-D8Amoe6a.js} +0 -0
@@ -1,5 +1,5 @@
1
- import { type AgentSpec, type AgentCapabilities, type AgentRuntimeId, type AgentSessionStore, type ConversationRuntime, type ResolvedExecutionSnapshot, type SessionRegistry, type SessionWorkspaceSnapshot } from "@rynx-ai/core";
2
- import { type PluginAgentSummary, type PluginCapability, type PluginHostEvent, type PluginResolvedSessionExecution, type PluginSessionExecutionSnapshot, type PluginSessionInteractionResolveDisposition } from "@rynx-ai/plugin-sdk";
1
+ import { type AdmissionReservation, type AgentSpec, type AgentCapabilities, type AgentRuntimeId, type AgentSessionStore, type ConversationRuntime, type ResolvedExecutionSnapshot, type SessionRegistry, type SessionWorkspaceSnapshot } from "@rynx-ai/core";
2
+ import { type PluginAgentSummary, type PluginHostEvent, type PluginResolvedSessionExecution, type PluginSessionExecutionSnapshot, type PluginSessionInteractionResolveDisposition } from "@rynx-ai/plugin-sdk";
3
3
  import type { SessionInteractionResolution } from "@rynx-ai/protocol";
4
4
  import { PluginRunnerError } from "@rynx-ai/plugin-runner";
5
5
  /** Leaves ample room for the RPC envelope below the runner's 1 MiB line cap. */
@@ -18,10 +18,11 @@ export interface PluginAgentCatalogHost {
18
18
  export interface PluginSessionLaunchSelection {
19
19
  projectId?: string;
20
20
  managedCwd?: string;
21
+ workspace?: SessionWorkspaceSnapshot;
21
22
  agent?: string;
22
23
  config?: AgentSpec;
23
24
  provider?: AgentRuntimeId;
24
- model?: string;
25
+ model?: string | null;
25
26
  reasoningEffort?: string;
26
27
  title?: string;
27
28
  }
@@ -44,7 +45,15 @@ export interface PluginHostServices {
44
45
  sessionLaunch: {
45
46
  resolve(input: PluginSessionLaunchSelection): Promise<PluginResolvedSessionLaunch>;
46
47
  };
48
+ sessionExecution: {
49
+ update(input: {
50
+ sessionId: string;
51
+ model: string | null;
52
+ reasoningEffort: string | null;
53
+ }): Promise<void>;
54
+ };
47
55
  interruptSession?(sessionId: string): Promise<boolean>;
56
+ terminateSession?(sessionId: string): Promise<boolean>;
48
57
  resolveSessionInteraction?(sessionId: string, interactionId: string, resolution: SessionInteractionResolution): Promise<{
49
58
  disposition: PluginSessionInteractionResolveDisposition;
50
59
  }>;
@@ -56,6 +65,13 @@ export interface PluginHostServices {
56
65
  sessionId: string;
57
66
  disposition: "created" | "replayed";
58
67
  }>;
68
+ admissionOpen?: () => boolean;
69
+ admissionReserve?: () => AdmissionReservation | undefined;
70
+ sessionPortal?: {
71
+ scheme: "http" | "https";
72
+ bindHost: string;
73
+ port: number;
74
+ };
59
75
  }
60
76
  export interface PluginHostRpcOptions {
61
77
  /** Manifest id returned by host.info. */
@@ -63,16 +79,19 @@ export interface PluginHostRpcOptions {
63
79
  /** Marketplace-qualified principal used for ownership and proof scopes. */
64
80
  principalId?: string;
65
81
  daemonInstanceId: string;
66
- capabilities: readonly PluginCapability[];
82
+ productVersion: string;
83
+ cliVersion: string;
67
84
  services: PluginHostServices;
68
85
  /** Canonical plugin-owned root permitted for the Workspace plugin's isolated
69
86
  * Session directory. Omitted for every other plugin. */
70
87
  managedWorkspaceRoot?: string;
71
88
  /** Resolving this promise is the stream's backpressure acknowledgement. */
72
89
  emit(event: PluginHostEvent): void | Promise<void>;
90
+ /** Bounded graceful-stop window used while disposing this Host. */
91
+ disposeForceAfterMs?: number;
73
92
  }
74
93
  /**
75
- * One authenticated plugin's capability-gated Host RPC surface.
94
+ * One authenticated plugin's Host RPC surface.
76
95
  *
77
96
  * This adapter is deliberately the only place that maps public string methods
78
97
  * to trusted runtime objects. The runner receives neither this class nor its
@@ -82,21 +101,27 @@ export declare class PluginHostRpc {
82
101
  private readonly pluginId;
83
102
  private readonly principalId;
84
103
  private readonly daemonInstanceId;
85
- private readonly capabilities;
104
+ private readonly productVersion;
105
+ private readonly cliVersion;
86
106
  private readonly services;
87
107
  private readonly managedWorkspaceRoot;
88
108
  private readonly emit;
109
+ private readonly disposeForceAfterMs;
89
110
  private readonly activeRuns;
90
111
  private readonly activeSessionRuns;
91
112
  private readonly activeRunsByOperation;
92
113
  private disposed;
114
+ private disposePromise?;
93
115
  constructor(options: PluginHostRpcOptions);
94
116
  call(method: string, params: unknown, signal?: AbortSignal): Promise<unknown>;
95
117
  dispose(): Promise<void>;
96
118
  private ensureSession;
119
+ private ensureSessionAdmitted;
97
120
  private resolveManagedWorkspace;
121
+ private resolveWorkspaceSnapshot;
98
122
  private sessionEnsureResult;
99
123
  private runSession;
124
+ private runSessionAdmitted;
100
125
  private pumpRun;
101
126
  private releaseRun;
102
127
  private steerSession;
@@ -110,9 +135,13 @@ export declare class PluginHostRpc {
110
135
  private withOwnedSession;
111
136
  private resolveSessionInteraction;
112
137
  private forkSession;
138
+ private forkSessionAdmitted;
139
+ private reserveAdmission;
140
+ private withAdmission;
113
141
  private setSessionTitle;
114
142
  private resolveAgentExecution;
115
143
  private issueControlLink;
144
+ private issueSessionPortalLink;
116
145
  private ownedSession;
117
146
  private isOwned;
118
147
  }
@@ -1,11 +1,12 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { realpath } from "node:fs/promises";
2
+ import { realpath, stat } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { AgentRuntimeError, agentSpecSchema, resolvedExecutionSnapshotSchema, sessionWorkspaceSnapshotSchema, SteerController, } 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,8 @@ 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
+ * One authenticated plugin's Host RPC surface.
41
19
  *
42
20
  * This adapter is deliberately the only place that maps public string methods
43
21
  * to trusted runtime objects. The runner receives neither this class nor its
@@ -47,24 +25,29 @@ export class PluginHostRpc {
47
25
  pluginId;
48
26
  principalId;
49
27
  daemonInstanceId;
50
- capabilities;
28
+ productVersion;
29
+ cliVersion;
51
30
  services;
52
31
  managedWorkspaceRoot;
53
32
  emit;
33
+ disposeForceAfterMs;
54
34
  activeRuns = new Map();
55
35
  activeSessionRuns = new Map();
56
36
  activeRunsByOperation = new Map();
57
37
  disposed = false;
38
+ disposePromise;
58
39
  constructor(options) {
59
40
  this.pluginId = requiredString(options.pluginId, "pluginId", 64);
60
41
  this.principalId = requiredString(options.principalId ?? options.pluginId, "principalId", 160);
61
42
  this.daemonInstanceId = requiredString(options.daemonInstanceId, "daemonInstanceId", 128);
62
- this.capabilities = new Set(options.capabilities);
43
+ this.productVersion = requiredString(options.productVersion, "productVersion", 128);
44
+ this.cliVersion = requiredString(options.cliVersion, "cliVersion", 128);
63
45
  this.services = options.services;
64
46
  this.managedWorkspaceRoot = options.managedWorkspaceRoot
65
47
  ? path.resolve(options.managedWorkspaceRoot)
66
48
  : undefined;
67
49
  this.emit = options.emit;
50
+ this.disposeForceAfterMs = options.disposeForceAfterMs ?? 10_000;
68
51
  }
69
52
  async call(method, params, signal) {
70
53
  if (this.disposed)
@@ -75,20 +58,18 @@ export class PluginHostRpc {
75
58
  pluginId: this.pluginId,
76
59
  installationId: this.services.installationId,
77
60
  daemonInstanceId: this.daemonInstanceId,
61
+ productVersion: this.productVersion,
62
+ cliVersion: this.cliVersion,
78
63
  installationProof: {
79
64
  algorithm: this.services.installationProof.algorithm,
80
65
  publicKey: this.services.installationProof.publicKey,
81
66
  },
82
67
  protocolVersion: 1,
83
- capabilities: [...this.capabilities],
68
+ ...(this.services.sessionPortal
69
+ ? { sessionPortal: { ...this.services.sessionPortal } }
70
+ : {}),
84
71
  };
85
72
  }
86
- const capability = METHOD_CAPABILITY[method];
87
- if (!capability)
88
- throw new PluginHostCallError("METHOD_NOT_FOUND", `unknown host method: ${method}`);
89
- if (!this.capabilities.has(capability)) {
90
- throw new PluginHostCallError("CAPABILITY_NOT_DECLARED", `host method ${method} is missing manifest capability ${capability}`);
91
- }
92
73
  try {
93
74
  switch (method) {
94
75
  case "installation.proof.sign": {
@@ -142,6 +123,16 @@ export class PluginHostRpc {
142
123
  return this.setSessionTitle(params);
143
124
  case "control.link.issue":
144
125
  return this.issueControlLink(params);
126
+ case "session.portal.plugin.issue": {
127
+ const access = requiredSessionPortalAccess(params);
128
+ return this.issueSessionPortalLink("plugin_sessions", access);
129
+ }
130
+ case "session.portal.session.issue": {
131
+ const input = record(params);
132
+ const sessionId = requiredSessionId(input);
133
+ this.ownedSession(sessionId);
134
+ return this.issueSessionPortalLink("single_session", requiredSessionPortalAccess(input), sessionId);
135
+ }
145
136
  default:
146
137
  throw new PluginHostCallError("METHOD_NOT_FOUND", `unknown host method: ${method}`);
147
138
  }
@@ -151,51 +142,85 @@ export class PluginHostRpc {
151
142
  }
152
143
  }
153
144
  async dispose() {
154
- if (this.disposed)
155
- return;
145
+ if (this.disposePromise)
146
+ return this.disposePromise;
156
147
  this.disposed = true;
157
148
  const runs = [...this.activeRuns.values()];
158
- this.activeRuns.clear();
159
- this.activeSessionRuns.clear();
160
- this.activeRunsByOperation.clear();
161
- for (const run of runs) {
162
- run.interruptRequested = true;
163
- if (run.interruptResult && await run.interruptResult.catch(() => false))
164
- continue;
165
- if (this.services.interruptSession) {
166
- await this.services.interruptSession(run.sessionId).catch(() => false);
167
- }
168
- else {
169
- run.abort.abort();
149
+ this.disposePromise = (async () => {
150
+ const stopped = await Promise.allSettled(runs.map(async (run) => {
151
+ if (this.activeRuns.get(run.runId) !== run)
152
+ return;
153
+ run.interruptRequested = true;
154
+ const graceful = run.interruptResult ?? (this.services.interruptSession
155
+ ? Promise.resolve().then(() => this.services.interruptSession(run.sessionId))
156
+ : Promise.resolve().then(() => {
157
+ run.abort.abort();
158
+ return false;
159
+ }));
160
+ const authoritative = forceStoppedInterrupt(graceful, this.disposeForceAfterMs, () => this.services.terminateSession?.(run.sessionId) ?? Promise.resolve(false));
161
+ run.interruptResult = authoritative;
162
+ if (!(await authoritative)) {
163
+ throw new Error(`could not prove Session ${run.sessionId} stopped`);
164
+ }
165
+ this.releaseRun(run);
166
+ }));
167
+ const failures = stopped
168
+ .filter((result) => result.status === "rejected")
169
+ .map((result) => result.reason);
170
+ if (failures.length > 0) {
171
+ throw new AggregateError(failures, "plugin Host could not stop every active run");
170
172
  }
171
- }
173
+ })();
174
+ return this.disposePromise;
172
175
  }
173
176
  async ensureSession(params) {
177
+ return this.withAdmission(() => this.ensureSessionAdmitted(params));
178
+ }
179
+ async ensureSessionAdmitted(params) {
174
180
  const input = record(params);
175
181
  const scopeId = requiredString(input.scopeId, "scopeId", 512);
176
182
  const operationId = requiredString(input.operationId, "operationId", 512);
177
183
  const parsedSelection = sessionLaunchSelection(input.session);
184
+ if (parsedSelection.managedCwd && !this.managedWorkspaceRoot) {
185
+ throw new PluginHostCallError("INVALID_PARAMS", "session.managedCwd is reserved for the built-in Workspace plugin");
186
+ }
187
+ const rawRequestHash = hashCanonicalJson(parsedSelection);
188
+ const replay = replayPluginSessionLaunch({
189
+ pluginId: this.principalId,
190
+ scopeId,
191
+ operationId,
192
+ requestHash: rawRequestHash,
193
+ allowRequestHashMismatch: true,
194
+ });
195
+ if (replay)
196
+ return this.sessionEnsureResult(replay);
178
197
  const selection = parsedSelection.managedCwd
179
198
  ? {
180
199
  ...parsedSelection,
181
200
  managedCwd: await this.resolveManagedWorkspace(parsedSelection.managedCwd),
182
201
  }
183
- : parsedSelection;
202
+ : parsedSelection.workspace
203
+ ? {
204
+ ...parsedSelection,
205
+ workspace: await this.resolveWorkspaceSnapshot(parsedSelection.workspace),
206
+ }
207
+ : parsedSelection;
184
208
  const requestHash = hashCanonicalJson(selection);
185
- const replay = replayPluginSessionLaunch({
209
+ const canonicalReplay = replayPluginSessionLaunch({
186
210
  pluginId: this.principalId,
187
211
  scopeId,
188
212
  operationId,
189
213
  requestHash,
190
214
  });
191
- if (replay)
192
- return this.sessionEnsureResult(replay);
215
+ if (canonicalReplay)
216
+ return this.sessionEnsureResult(canonicalReplay);
193
217
  const resolved = await this.services.sessionLaunch.resolve(selection);
194
218
  const ensured = ensurePluginSessionLaunch({
195
219
  pluginId: this.principalId,
196
220
  scopeId,
197
221
  operationId,
198
222
  requestHash,
223
+ rawRequestHash,
199
224
  session: {
200
225
  workspace: sessionWorkspaceSnapshotSchema.parse(resolved.workspace),
201
226
  execution: resolvedExecutionSnapshotSchema.parse(resolved.execution),
@@ -231,6 +256,33 @@ export class PluginHostRpc {
231
256
  }
232
257
  return canonicalDirectory;
233
258
  }
259
+ async resolveWorkspaceSnapshot(workspace) {
260
+ const directories = [workspace.cwd, ...workspace.allowedDirs];
261
+ if (directories.some((directory) => !path.isAbsolute(directory))) {
262
+ throw new PluginHostCallError("INVALID_PARAMS", "session.workspace paths must be absolute");
263
+ }
264
+ let canonical;
265
+ try {
266
+ canonical = await Promise.all(directories.map(async (directory) => {
267
+ const resolved = await realpath(directory);
268
+ if (!(await stat(resolved)).isDirectory()) {
269
+ throw new Error("not a directory");
270
+ }
271
+ return resolved;
272
+ }));
273
+ }
274
+ catch {
275
+ throw new PluginHostCallError("INVALID_PARAMS", "session.workspace paths must be existing directories");
276
+ }
277
+ const parsed = sessionWorkspaceSnapshotSchema.safeParse({
278
+ cwd: canonical[0],
279
+ allowedDirs: canonical.slice(1),
280
+ });
281
+ if (!parsed.success) {
282
+ throw new PluginHostCallError("INVALID_PARAMS", `invalid session.workspace: ${parsed.error.issues[0]?.message ?? "invalid snapshot"}`);
283
+ }
284
+ return parsed.data;
285
+ }
234
286
  sessionEnsureResult(result) {
235
287
  const session = this.ownedSession(result.sessionId);
236
288
  return {
@@ -243,10 +295,21 @@ export class PluginHostRpc {
243
295
  };
244
296
  }
245
297
  async runSession(params, signal) {
298
+ return this.withAdmission(() => this.runSessionAdmitted(params, signal));
299
+ }
300
+ async runSessionAdmitted(params, signal) {
246
301
  const input = record(params);
247
302
  const sessionId = requiredSessionId(input);
248
303
  const operationId = optionalString(input.operationId, "operationId", 512);
249
- const session = this.ownedSession(sessionId);
304
+ const modelProvided = input.model !== undefined;
305
+ const requestedModel = input.model === null || input.model === ""
306
+ ? null
307
+ : optionalString(input.model, "model", 256);
308
+ const reasoningEffortProvided = input.reasoningEffort !== undefined;
309
+ const requestedReasoningEffort = input.reasoningEffort === null || input.reasoningEffort === ""
310
+ ? null
311
+ : optionalString(input.reasoningEffort, "reasoningEffort", 64);
312
+ let session = this.ownedSession(sessionId);
250
313
  if (operationId && this.activeRunsByOperation.has(operationId)) {
251
314
  throw new PluginHostCallError("RUN_ALREADY_ACTIVE", `operation already has an active run: ${operationId}`, true);
252
315
  }
@@ -257,6 +320,19 @@ export class PluginHostRpc {
257
320
  throw new PluginHostCallError("RUN_LIMIT", "plugin has too many active session runs", true);
258
321
  }
259
322
  const message = requiredText(input.message, "message", MAX_MESSAGE_LENGTH);
323
+ const nextModel = modelProvided ? requestedModel ?? null : session.execution.model;
324
+ const nextReasoningEffort = reasoningEffortProvided
325
+ ? requestedReasoningEffort ?? null
326
+ : session.execution.reasoningEffort;
327
+ if (nextModel !== session.execution.model ||
328
+ nextReasoningEffort !== session.execution.reasoningEffort) {
329
+ await this.services.sessionExecution.update({
330
+ sessionId,
331
+ model: nextModel,
332
+ reasoningEffort: nextReasoningEffort,
333
+ });
334
+ session = this.ownedSession(sessionId);
335
+ }
260
336
  const abort = new AbortController();
261
337
  const cancel = () => abort.abort(signal?.reason);
262
338
  if (signal?.aborted) {
@@ -411,6 +487,10 @@ export class PluginHostRpc {
411
487
  async interruptRun(params) {
412
488
  const input = record(params);
413
489
  const run = this.activeRunByIdentity(input);
490
+ const forceAfterMs = optionalPositiveNumber(input.forceAfterMs, "forceAfterMs");
491
+ if (forceAfterMs !== undefined && forceAfterMs > 30_000) {
492
+ throw new PluginHostCallError("INVALID_PARAMS", "forceAfterMs must not exceed 30000");
493
+ }
414
494
  if (run.interruptResult) {
415
495
  return { interrupted: await run.interruptResult };
416
496
  }
@@ -418,7 +498,7 @@ export class PluginHostRpc {
418
498
  // the authoritative stop is pending wait for its result: true suppresses
419
499
  // them, while false/error lets genuine native progress/terminal continue.
420
500
  run.interruptRequested = true;
421
- const interruptResult = this.services.interruptSession
501
+ const graceful = this.services.interruptSession
422
502
  // ConversationRuntime wires AbortSignal to this same native operation.
423
503
  // Invoke it directly once so this boolean is the authoritative result;
424
504
  // aborting first would trigger a duplicate, unobserved interrupt.
@@ -429,6 +509,9 @@ export class PluginHostRpc {
429
509
  // effort and cannot prove that native execution stopped.
430
510
  return false;
431
511
  });
512
+ const interruptResult = forceAfterMs === undefined
513
+ ? graceful
514
+ : forceStoppedInterrupt(graceful, forceAfterMs, () => this.services.terminateSession?.(run.sessionId) ?? Promise.resolve(false));
432
515
  run.interruptResult = interruptResult;
433
516
  try {
434
517
  const interrupted = await interruptResult;
@@ -510,6 +593,9 @@ export class PluginHostRpc {
510
593
  return await resolve(sessionId, interactionId, resolution);
511
594
  }
512
595
  async forkSession(params) {
596
+ return this.withAdmission(() => this.forkSessionAdmitted(params));
597
+ }
598
+ async forkSessionAdmitted(params) {
513
599
  const input = record(params);
514
600
  const sourceSessionId = requiredString(input.sourceSessionId, "sourceSessionId", 512);
515
601
  const operationId = requiredString(input.operationId, "operationId", 512);
@@ -524,6 +610,24 @@ export class PluginHostRpc {
524
610
  ...(title ? { title } : {}),
525
611
  });
526
612
  }
613
+ reserveAdmission() {
614
+ const reservation = this.services.admissionReserve?.();
615
+ if (reservation)
616
+ return reservation;
617
+ if (!this.services.admissionReserve && this.services.admissionOpen?.() !== false) {
618
+ return { release() { } };
619
+ }
620
+ throw new PluginHostCallError("HOST_CALL_FAILED", "daemon maintenance is in progress", true);
621
+ }
622
+ async withAdmission(operation) {
623
+ const reservation = this.reserveAdmission();
624
+ try {
625
+ return await operation();
626
+ }
627
+ finally {
628
+ reservation.release();
629
+ }
630
+ }
527
631
  setSessionTitle(params) {
528
632
  const input = record(params);
529
633
  const sessionId = requiredSessionId(input);
@@ -551,6 +655,17 @@ export class PluginHostRpc {
551
655
  ...(ttlSeconds === undefined ? {} : { ttlMs: ttlSeconds * 1_000 }),
552
656
  });
553
657
  }
658
+ issueSessionPortalLink(scope, access, sessionId) {
659
+ if (!this.services.sessionPortal) {
660
+ throw new PluginHostCallError("HOST_CALL_FAILED", "the Session Portal is not configured");
661
+ }
662
+ return issueSessionPortalTicket({
663
+ pluginId: this.principalId,
664
+ scope,
665
+ access,
666
+ ...(sessionId ? { sessionId } : {}),
667
+ });
668
+ }
554
669
  ownedSession(sessionId) {
555
670
  const session = this.services.sessionRegistry.get(sessionId);
556
671
  if (!session || !this.isOwned(session)) {
@@ -612,14 +727,18 @@ function sessionLaunchSelection(value) {
612
727
  assertJsonValue(input, "session");
613
728
  const projectId = optionalString(input.projectId, "session.projectId", 512);
614
729
  const managedCwd = optionalString(input.managedCwd, "session.managedCwd", 16_384);
730
+ const workspace = optionalSessionWorkspace(input.workspace);
615
731
  const agent = optionalAgentId(input.agent, "session.agent");
616
732
  const config = input.config === undefined ? undefined : agentSpecSchema.parse(input.config);
617
733
  const provider = optionalRuntimeValue(input.provider);
618
- const model = optionalString(input.model, "session.model", 256);
734
+ const modelProvided = input.model !== undefined;
735
+ const model = input.model === null || input.model === ""
736
+ ? null
737
+ : optionalString(input.model, "session.model", 256);
619
738
  const reasoningEffort = optionalString(input.reasoningEffort, "session.reasoningEffort", 128);
620
739
  const title = optionalString(input.title, "session.title", 1_000);
621
- if (Number(Boolean(projectId)) + Number(Boolean(managedCwd)) > 1) {
622
- throw new PluginHostCallError("INVALID_PARAMS", "session.projectId and session.managedCwd are mutually exclusive");
740
+ if (Number(Boolean(projectId)) + Number(Boolean(managedCwd)) + Number(Boolean(workspace)) > 1) {
741
+ throw new PluginHostCallError("INVALID_PARAMS", "session.projectId, session.managedCwd, and session.workspace are mutually exclusive");
623
742
  }
624
743
  if (Number(Boolean(agent)) +
625
744
  Number(Boolean(config)) +
@@ -630,14 +749,26 @@ function sessionLaunchSelection(value) {
630
749
  return {
631
750
  ...(projectId ? { projectId } : {}),
632
751
  ...(managedCwd ? { managedCwd } : {}),
752
+ ...(workspace ? { workspace } : {}),
633
753
  ...(agent ? { agent } : {}),
634
754
  ...(config ? { config } : {}),
635
755
  ...(provider ? { provider } : {}),
636
- ...(model ? { model } : {}),
756
+ ...(modelProvided ? { model: model ?? null } : {}),
637
757
  ...(reasoningEffort ? { reasoningEffort } : {}),
638
758
  ...(title ? { title } : {}),
639
759
  };
640
760
  }
761
+ function optionalSessionWorkspace(value) {
762
+ if (value === undefined)
763
+ return undefined;
764
+ const input = record(value);
765
+ const cwd = requiredPathString(input.cwd, "session.workspace.cwd", 16_384);
766
+ if (!Array.isArray(input.allowedDirs) || input.allowedDirs.length === 0) {
767
+ throw new PluginHostCallError("INVALID_PARAMS", "session.workspace.allowedDirs must be a non-empty string array");
768
+ }
769
+ const allowedDirs = input.allowedDirs.map((directory, index) => requiredPathString(directory, `session.workspace.allowedDirs[${index}]`, 16_384));
770
+ return { cwd, allowedDirs };
771
+ }
641
772
  function sessionExecutionSnapshot(value) {
642
773
  const input = record(value);
643
774
  const runtime = optionalRuntimeValue(input.runtime);
@@ -654,6 +785,12 @@ function sessionExecutionSnapshot(value) {
654
785
  function requiredSessionId(params) {
655
786
  return requiredString(record(params).sessionId, "sessionId", 512);
656
787
  }
788
+ function requiredSessionPortalAccess(params) {
789
+ const access = record(params).access;
790
+ if (access === "read_only" || access === "read_write")
791
+ return access;
792
+ throw new PluginHostCallError("INVALID_PARAMS", "access must be read_only or read_write");
793
+ }
657
794
  function sessionInteractionResolution(value) {
658
795
  const input = record(value);
659
796
  const keys = Object.keys(input);
@@ -718,6 +855,15 @@ function requiredString(value, label, maxLength) {
718
855
  }
719
856
  return value.trim();
720
857
  }
858
+ function requiredPathString(value, label, maxLength) {
859
+ if (typeof value !== "string" || !value.trim()) {
860
+ throw new PluginHostCallError("INVALID_PARAMS", `${label} is required`);
861
+ }
862
+ if (value.length > maxLength) {
863
+ throw new PluginHostCallError("INVALID_PARAMS", `${label} is too long`);
864
+ }
865
+ return value;
866
+ }
721
867
  function requiredText(value, label, maxLength) {
722
868
  if (typeof value !== "string" || !value.trim()) {
723
869
  throw new PluginHostCallError("INVALID_PARAMS", `${label} is required`);
@@ -758,6 +904,23 @@ function optionalPositiveNumber(value, label) {
758
904
  }
759
905
  return value;
760
906
  }
907
+ async function forceStoppedInterrupt(graceful, forceAfterMs, terminate) {
908
+ let timer;
909
+ const graceExpired = new Promise((resolve) => {
910
+ timer = setTimeout(() => resolve(false), forceAfterMs);
911
+ timer.unref?.();
912
+ });
913
+ try {
914
+ if (await Promise.race([graceful.catch(() => false), graceExpired])) {
915
+ return true;
916
+ }
917
+ }
918
+ finally {
919
+ if (timer)
920
+ clearTimeout(timer);
921
+ }
922
+ return terminate();
923
+ }
761
924
  function assertJsonValue(value, label, seen = new Set()) {
762
925
  if (value === null || typeof value === "string" || typeof value === "boolean")
763
926
  return;