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

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.
@@ -111,7 +111,11 @@ export class HostConversationRuntime {
111
111
  const unsteer = input.steer?.subscribe((text) => {
112
112
  if (!runId || terminal)
113
113
  return;
114
- void this.host.sessions.steer({ runId, text }).catch(() => undefined);
114
+ // `session.run` is the only message-input operation. The Host reuses
115
+ // `runId` while this Turn is active and lets the native runtime choose
116
+ // turn/steer; callers do not select a separate steering RPC.
117
+ void this.host.sessions.run({ sessionId: input.threadId, message: text })
118
+ .catch(() => undefined);
115
119
  });
116
120
  input.signal?.addEventListener("abort", interrupt, { once: true });
117
121
  const generator = (async function* () {
@@ -126958,7 +126958,7 @@ var HostConversationRuntime = class {
126958
126958
  }
126959
126959
  const unsteer = input.steer?.subscribe((text) => {
126960
126960
  if (!runId || terminal) return;
126961
- void this.host.sessions.steer({ runId, text }).catch(() => void 0);
126961
+ void this.host.sessions.run({ sessionId: input.threadId, message: text }).catch(() => void 0);
126962
126962
  });
126963
126963
  input.signal?.addEventListener("abort", interrupt, { once: true });
126964
126964
  const generator = (async function* () {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynx-ai/plugin-channel-lark",
3
- "version": "0.1.11-beta.21",
3
+ "version": "0.1.11-beta.23",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/rynx-ai/rynx.git",
@@ -1,5 +1,5 @@
1
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";
2
+ import { type PluginAgentSummary, type PluginHostEvent, type PluginSessionActivitySnapshot, 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. */
@@ -52,6 +52,7 @@ export interface PluginHostServices {
52
52
  reasoningEffort: string | null;
53
53
  }): Promise<void>;
54
54
  };
55
+ sessionActivity(sessionId: string): PluginSessionActivitySnapshot | Promise<PluginSessionActivitySnapshot>;
55
56
  interruptSession?(sessionId: string): Promise<boolean>;
56
57
  terminateSession?(sessionId: string): Promise<boolean>;
57
58
  resolveSessionInteraction?(sessionId: string, interactionId: string, resolution: SessionInteractionResolution): Promise<{
@@ -124,10 +125,9 @@ export declare class PluginHostRpc {
124
125
  private runSessionAdmitted;
125
126
  private pumpRun;
126
127
  private releaseRun;
127
- private steerSession;
128
128
  private interruptRun;
129
+ private sessionActivity;
129
130
  private interruptWon;
130
- private activeRun;
131
131
  private activeRunByIdentity;
132
132
  private getBinding;
133
133
  private listBindings;
@@ -1,7 +1,7 @@
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";
@@ -14,6 +14,57 @@ const MAX_PLUGIN_RUN_FINAL_RESPONSE_BYTES = 64 * 1024;
14
14
  const MAX_DELTA_CHUNK_CODE_UNITS = 64 * 1024;
15
15
  const SESSION_INTERACTION_RESOLUTION_MAX_BYTES = 128 * 1024;
16
16
  const AGENT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
17
+ /**
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
+ }
17
68
  /**
18
69
  * One authenticated plugin's Host RPC surface.
19
70
  *
@@ -90,8 +141,8 @@ export class PluginHostRpc {
90
141
  return await this.ensureSession(params);
91
142
  case "session.run":
92
143
  return await this.runSession(params, signal);
93
- case "session.steer":
94
- return this.steerSession(params);
144
+ case "session.activity.get":
145
+ return await this.sessionActivity(params);
95
146
  case "session.interrupt":
96
147
  return await this.interruptRun(params);
97
148
  case "session.interaction.resolve":
@@ -301,6 +352,46 @@ export class PluginHostRpc {
301
352
  const input = record(params);
302
353
  const sessionId = requiredSessionId(input);
303
354
  const operationId = optionalString(input.operationId, "operationId", 512);
355
+ const message = requiredText(input.message, "message", MAX_MESSAGE_LENGTH);
356
+ let session = this.ownedSession(sessionId);
357
+ if (operationId) {
358
+ const replayed = this.activeRunsByOperation.get(operationId);
359
+ if (replayed) {
360
+ if (replayed.sessionId !== sessionId) {
361
+ throw new PluginHostCallError("RUN_ALREADY_ACTIVE", `operation already belongs to another active Session: ${operationId}`, true);
362
+ }
363
+ return await waitForHostPromise(replayed.started, signal);
364
+ }
365
+ }
366
+ const active = this.activeSessionRuns.get(sessionId);
367
+ if (active) {
368
+ const queued = active.input.enqueue(message);
369
+ if (!queued.accepted) {
370
+ // The response reached its terminal boundary before Host cleanup won
371
+ // the scheduling race. Wait for that old Run to leave the Session slot,
372
+ // then submit this message as a new Turn with a new response identity.
373
+ await waitForHostPromise(active.settled, signal);
374
+ throwIfHostCallAborted(signal);
375
+ return await this.runSessionAdmitted(params, signal);
376
+ }
377
+ if (operationId) {
378
+ active.operationIds.add(operationId);
379
+ this.activeRunsByOperation.set(operationId, active);
380
+ }
381
+ try {
382
+ // Reuse the current Run identity. Core already owns its response stream;
383
+ // the buffered input will become turn/steer once that stream subscribes.
384
+ return await waitForHostPromise(active.started, signal);
385
+ }
386
+ catch (error) {
387
+ queued.cancel();
388
+ if (operationId && this.activeRunsByOperation.get(operationId) === active) {
389
+ this.activeRunsByOperation.delete(operationId);
390
+ active.operationIds.delete(operationId);
391
+ }
392
+ throw error;
393
+ }
394
+ }
304
395
  const modelProvided = input.model !== undefined;
305
396
  const requestedModel = input.model === null || input.model === ""
306
397
  ? null
@@ -309,17 +400,9 @@ export class PluginHostRpc {
309
400
  const requestedReasoningEffort = input.reasoningEffort === null || input.reasoningEffort === ""
310
401
  ? null
311
402
  : optionalString(input.reasoningEffort, "reasoningEffort", 64);
312
- let session = this.ownedSession(sessionId);
313
- if (operationId && this.activeRunsByOperation.has(operationId)) {
314
- throw new PluginHostCallError("RUN_ALREADY_ACTIVE", `operation already has an active run: ${operationId}`, true);
315
- }
316
- if (this.activeSessionRuns.has(sessionId)) {
317
- throw new PluginHostCallError("RUN_ALREADY_ACTIVE", `session already has an active run: ${sessionId}`, true);
318
- }
319
403
  if (this.activeRuns.size >= PLUGIN_HOST_MAX_ACTIVE_RUNS) {
320
404
  throw new PluginHostCallError("RUN_LIMIT", "plugin has too many active session runs", true);
321
405
  }
322
- const message = requiredText(input.message, "message", MAX_MESSAGE_LENGTH);
323
406
  const nextModel = modelProvided ? requestedModel ?? null : session.execution.model;
324
407
  const nextReasoningEffort = reasoningEffortProvided
325
408
  ? requestedReasoningEffort ?? null
@@ -340,32 +423,54 @@ export class PluginHostRpc {
340
423
  throwIfHostCallAborted(signal);
341
424
  }
342
425
  signal?.addEventListener("abort", cancel, { once: true });
343
- const steer = new SteerController();
426
+ const runInput = new PluginRunInputController();
344
427
  const runId = randomUUID();
428
+ let resolveStarted;
429
+ let rejectStarted;
430
+ const startedResult = new Promise((resolve, reject) => {
431
+ resolveStarted = resolve;
432
+ rejectStarted = reject;
433
+ });
434
+ // The initiating RPC observes startup directly below. This promise exists
435
+ // for concurrent/replayed calls, so retain its rejection without creating
436
+ // an unhandled-rejection when there are none.
437
+ void startedResult.catch(() => undefined);
438
+ let resolveSettled;
439
+ const settled = new Promise((resolve) => {
440
+ resolveSettled = resolve;
441
+ });
345
442
  const run = {
346
443
  runId,
347
444
  sessionId,
348
- operationId,
445
+ operationIds: new Set(operationId ? [operationId] : []),
349
446
  interruptRequested: false,
350
447
  abort,
351
- steer,
448
+ input: runInput,
449
+ started: startedResult,
450
+ startupSettled: false,
451
+ resolveStarted,
452
+ rejectStarted,
453
+ settled,
454
+ resolveSettled,
352
455
  };
353
456
  this.activeRuns.set(runId, run);
354
457
  this.activeSessionRuns.set(sessionId, run);
355
458
  if (operationId)
356
459
  this.activeRunsByOperation.set(operationId, run);
357
- let started;
460
+ let startedExecution;
358
461
  try {
359
- started = await this.services.conversationRuntime.runLive({
462
+ startedExecution = await this.services.conversationRuntime.runLive({
360
463
  message,
361
464
  threadId: sessionId,
362
465
  workspace: session.workspace,
363
466
  execution: session.execution,
364
467
  signal: abort.signal,
365
- steer,
468
+ steer: runInput,
366
469
  });
367
470
  }
368
471
  catch (error) {
472
+ run.startupSettled = true;
473
+ run.rejectStarted(error);
369
474
  this.releaseRun(run);
370
475
  throw error;
371
476
  }
@@ -374,20 +479,28 @@ export class PluginHostRpc {
374
479
  }
375
480
  if (signal?.aborted || this.disposed || this.activeRuns.get(runId) !== run) {
376
481
  abort.abort(signal?.reason);
482
+ const error = signal?.aborted
483
+ ? new PluginHostCallError("REQUEST_CANCELLED", "plugin host request was cancelled")
484
+ : new PluginHostCallError("HOST_CLOSED", "plugin host is closed");
485
+ run.startupSettled = true;
486
+ run.rejectStarted(error);
377
487
  this.releaseRun(run);
378
488
  throwIfHostCallAborted(signal);
379
- throw new PluginHostCallError("HOST_CLOSED", "plugin host is closed");
489
+ throw error;
380
490
  }
381
- // Let the RPC transport write the request response before stream events are
382
- // emitted. `runLive` has already subscribed, so no agent event is lost.
383
- setImmediate(() => void this.pumpRun(run, started.generator));
384
- return {
491
+ const result = {
385
492
  runId,
386
493
  sessionId,
387
- provider: started.provider,
388
- model: started.model,
389
- authSource: started.authSource,
494
+ provider: startedExecution.provider,
495
+ model: startedExecution.model,
496
+ authSource: startedExecution.authSource,
390
497
  };
498
+ run.startupSettled = true;
499
+ run.resolveStarted(result);
500
+ // Let the RPC transport write the request response before stream events are
501
+ // emitted. `runLive` has already subscribed, so no agent event is lost.
502
+ setImmediate(() => void this.pumpRun(run, startedExecution.generator));
503
+ return result;
391
504
  }
392
505
  async pumpRun(run, generator) {
393
506
  let terminalFailure;
@@ -469,28 +582,52 @@ export class PluginHostRpc {
469
582
  }
470
583
  }
471
584
  releaseRun(run) {
585
+ run.input.close();
586
+ if (!run.startupSettled) {
587
+ run.startupSettled = true;
588
+ run.rejectStarted(new PluginHostCallError("HOST_CLOSED", "plugin Host closed the active Run"));
589
+ }
472
590
  if (this.activeRuns.get(run.runId) === run)
473
591
  this.activeRuns.delete(run.runId);
474
592
  if (this.activeSessionRuns.get(run.sessionId) === run) {
475
593
  this.activeSessionRuns.delete(run.sessionId);
476
594
  }
477
- if (run.operationId && this.activeRunsByOperation.get(run.operationId) === run) {
478
- this.activeRunsByOperation.delete(run.operationId);
595
+ for (const operationId of run.operationIds) {
596
+ if (this.activeRunsByOperation.get(operationId) === run) {
597
+ this.activeRunsByOperation.delete(operationId);
598
+ }
479
599
  }
480
- }
481
- steerSession(params) {
482
- const input = record(params);
483
- const run = this.activeRun(input);
484
- const text = requiredText(input.text, "text", MAX_MESSAGE_LENGTH);
485
- return { delivered: run.steer.send(text) };
600
+ run.resolveSettled();
486
601
  }
487
602
  async interruptRun(params) {
488
603
  const input = record(params);
489
- const run = this.activeRunByIdentity(input);
490
604
  const forceAfterMs = optionalPositiveNumber(input.forceAfterMs, "forceAfterMs");
491
605
  if (forceAfterMs !== undefined && forceAfterMs > 30_000) {
492
606
  throw new PluginHostCallError("INVALID_PARAMS", "forceAfterMs must not exceed 30000");
493
607
  }
608
+ const sessionId = optionalString(input.sessionId, "sessionId", 128);
609
+ const runId = optionalString(input.runId, "runId", 128);
610
+ const operationId = optionalString(input.operationId, "operationId", 512);
611
+ if ((sessionId ? 1 : 0) + (runId ? 1 : 0) + (operationId ? 1 : 0) !== 1) {
612
+ throw new PluginHostCallError("INVALID_PARAMS", "exactly one of sessionId, runId, or operationId is required");
613
+ }
614
+ let run;
615
+ if (sessionId) {
616
+ this.ownedSession(sessionId);
617
+ run = this.activeSessionRuns.get(sessionId);
618
+ if (!run) {
619
+ const graceful = this.services.interruptSession
620
+ ? Promise.resolve().then(() => this.services.interruptSession(sessionId))
621
+ : Promise.resolve(false);
622
+ const interrupted = forceAfterMs === undefined
623
+ ? await graceful
624
+ : await forceStoppedInterrupt(graceful, forceAfterMs, () => this.services.terminateSession?.(sessionId) ?? Promise.resolve(false));
625
+ return { interrupted };
626
+ }
627
+ }
628
+ else {
629
+ run = this.activeRunByIdentity(input);
630
+ }
494
631
  if (run.interruptResult) {
495
632
  return { interrupted: await run.interruptResult };
496
633
  }
@@ -529,6 +666,24 @@ export class PluginHostRpc {
529
666
  throw error;
530
667
  }
531
668
  }
669
+ async sessionActivity(params) {
670
+ const sessionId = requiredSessionId(params);
671
+ this.ownedSession(sessionId);
672
+ const snapshot = structuredClone(await this.services.sessionActivity(sessionId));
673
+ const activeRun = this.activeSessionRuns.get(sessionId);
674
+ const waiting = snapshot.pendingInteractions.length > 0;
675
+ const status = waiting
676
+ ? "waiting"
677
+ : activeRun
678
+ ? "running"
679
+ : snapshot.status;
680
+ const startup = Boolean(activeRun && snapshot.activeResponseIds.length === 0 && !waiting);
681
+ return {
682
+ ...snapshot,
683
+ status,
684
+ ...(startup ? { statusKind: "startup" } : {}),
685
+ };
686
+ }
532
687
  async interruptWon(run) {
533
688
  if (!run.interruptRequested)
534
689
  return false;
@@ -536,13 +691,6 @@ export class PluginHostRpc {
536
691
  return true;
537
692
  return run.interruptResult.catch(() => false);
538
693
  }
539
- activeRun(input) {
540
- const runId = requiredString(input.runId, "runId", 128);
541
- const run = this.activeRuns.get(runId);
542
- if (!run)
543
- throw new PluginHostCallError("RUN_NOT_FOUND", `active run not found: ${runId}`);
544
- return run;
545
- }
546
694
  activeRunByIdentity(input) {
547
695
  const runId = optionalString(input.runId, "runId", 128);
548
696
  const operationId = optionalString(input.operationId, "operationId", 512);
@@ -692,6 +840,24 @@ function throwIfHostCallAborted(signal) {
692
840
  return;
693
841
  throw new PluginHostCallError("REQUEST_CANCELLED", "plugin host request was cancelled");
694
842
  }
843
+ async function waitForHostPromise(promise, signal) {
844
+ throwIfHostCallAborted(signal);
845
+ if (!signal)
846
+ return await promise;
847
+ return await new Promise((resolve, reject) => {
848
+ const aborted = () => {
849
+ reject(new PluginHostCallError("REQUEST_CANCELLED", "plugin host request was cancelled"));
850
+ };
851
+ signal.addEventListener("abort", aborted, { once: true });
852
+ void promise.then((value) => {
853
+ signal.removeEventListener("abort", aborted);
854
+ resolve(value);
855
+ }, (error) => {
856
+ signal.removeEventListener("abort", aborted);
857
+ reject(error);
858
+ });
859
+ });
860
+ }
695
861
  export class PluginHostCallError extends PluginRunnerError {
696
862
  retryable;
697
863
  data;
@@ -714,6 +880,12 @@ function toHostCallError(error) {
714
880
  // not be brought up, so callers may fail without quarantining capacity.
715
881
  return new PluginHostCallError("LIVE_SESSION_UNAVAILABLE", error.message, false);
716
882
  }
883
+ if (error instanceof AgentRuntimeError) {
884
+ // Native lifecycle failures already carry a stable phase code. Preserve it
885
+ // across the plugin-host boundary instead of erasing it behind the generic
886
+ // HOST_CALL_FAILED wrapper.
887
+ return new PluginHostCallError(error.code, error.message, false);
888
+ }
717
889
  if (error instanceof SessionLaunchOperationConflictError) {
718
890
  return new PluginHostCallError(error.code, error.message, false);
719
891
  }
@@ -1465,7 +1465,7 @@ function nonNegativeInteger(value, fallback, field) {
1465
1465
  return value;
1466
1466
  }
1467
1467
  function parseCallTimeout(value) {
1468
- if (value === undefined)
1468
+ if (value === undefined || value === null)
1469
1469
  return undefined;
1470
1470
  if (!Number.isSafeInteger(value) ||
1471
1471
  value < MIN_CALL_TIMEOUT_MS ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynx-ai/daemon",
3
- "version": "0.1.11-beta.21",
3
+ "version": "0.1.11-beta.23",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/rynx-ai/rynx.git",
@@ -50,17 +50,17 @@
50
50
  "pm2": "^6.0.0",
51
51
  "tar": "^7.5.19",
52
52
  "ws": "^8.21.0",
53
- "@rynx-ai/core": "0.1.11-beta.21",
54
- "@rynx-ai/emulator": "0.1.11-beta.21",
55
- "@rynx-ai/plugin-runner": "0.1.11-beta.21",
56
- "@rynx-ai/plugin-sdk": "0.1.11-beta.21",
57
- "@rynx-ai/protocol": "0.1.11-beta.21",
58
- "@rynx-ai/remote-runtime-client": "0.1.11-beta.21",
59
- "@rynx-ai/server": "0.1.11-beta.21"
53
+ "@rynx-ai/plugin-runner": "0.1.11-beta.23",
54
+ "@rynx-ai/core": "0.1.11-beta.23",
55
+ "@rynx-ai/emulator": "0.1.11-beta.23",
56
+ "@rynx-ai/plugin-sdk": "0.1.11-beta.23",
57
+ "@rynx-ai/protocol": "0.1.11-beta.23",
58
+ "@rynx-ai/remote-runtime-client": "0.1.11-beta.23",
59
+ "@rynx-ai/server": "0.1.11-beta.23"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/ws": "^8.18.1",
63
- "@rynx-ai/plugin-channel-lark": "0.1.11-beta.21"
63
+ "@rynx-ai/plugin-channel-lark": "0.1.11-beta.23"
64
64
  },
65
65
  "scripts": {
66
66
  "build": "rm -rf dist bundled-plugins && tsc -p tsconfig.json && chmod +x dist/index-daemon.js && node ../../scripts/stage-bundled-plugins.mjs",