@rynx-ai/server 0.1.11-beta.22 → 0.1.11-beta.24

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.
@@ -7,7 +7,7 @@
7
7
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
8
8
  <link rel="icon" type="image/png" href="/favicon.png" />
9
9
  <title>rynx · control</title>
10
- <script type="module" crossorigin src="/assets/index-BQFR9W8k.js"></script>
10
+ <script type="module" crossorigin src="/assets/index-D-5t2eCT.js"></script>
11
11
  <link rel="stylesheet" crossorigin href="/assets/index-0noHpRXD.css">
12
12
  </head>
13
13
  <body>
@@ -3,10 +3,9 @@ import type { SequencedSessionEvent, SessionEvent, SessionInteractionResolution,
3
3
  import type { RemoteRuntimeSessionResourceDeleteParams, RemoteRuntimeSessionResourceDeleteResult, RemoteRuntimeSessionResourceGetParams, RemoteRuntimeSessionResourceGetResult, RemoteRuntimeSessionResourcePolicyResult, RemoteRuntimeSessionResourceReadParams, RemoteRuntimeSessionResourceReadResult, RemoteRuntimeSessionResourceUploadBeginParams, RemoteRuntimeSessionResourceUploadBeginResult, RemoteRuntimeSessionResourceUploadChunkParams, RemoteRuntimeSessionResourceUploadChunkResult, RemoteRuntimeSessionResourceUploadCommitParams, RemoteRuntimeSessionResourceUploadCommitResult } from "@rynx-ai/protocol/remote-runtime-rpc";
4
4
  import type { ControlAgentModelOption, ControlAgentSummary, ControlSessionInteractionResolveDisposition, ControlSessionRuntimeSnapshot, SessionPermissionPreset } from "@rynx-ai/protocol/control";
5
5
  import { type RemoteRuntimeSessionInterruptResult, type RemoteRuntimeSessionExecutionGetResult, type RemoteRuntimeSessionExecutionUpdateParams, type RemoteRuntimeSessionExecutionUpdateResult, type RemoteRuntimeSessionLaunchOptionsListResult, type RemoteRuntimeSessionListParams, type RemoteRuntimeSessionListResult, type RemoteRuntimeSessionSnapshotParams } from "@rynx-ai/protocol/remote-runtime-rpc";
6
- import type { CodexRuntimeStatus, InjectOutcome } from "@rynx-ai/runtime";
6
+ import type { CodexRuntimeStatus, InjectResult } from "@rynx-ai/runtime";
7
7
  export interface MachineSessionRuntimeStatePort {
8
8
  snapshot(sessionId: string): ControlSessionRuntimeSnapshot;
9
- beginResponseHandoff?(sessionId: string, responseId: string): () => void;
10
9
  remove?(sessionId: string): void;
11
10
  }
12
11
  export interface MachineSessionInterruptPort {
@@ -69,7 +68,7 @@ export interface MachineSessionRunnerPort {
69
68
  }): Promise<boolean>;
70
69
  lastLiveSessionError?(sessionId: string): string | undefined;
71
70
  lastLiveSessionFailure?(sessionId: string): LiveSessionFailure | undefined;
72
- injectMessage(sessionId: string, input: RuntimeUserInput | string): Promise<InjectOutcome>;
71
+ injectMessage(sessionId: string, input: RuntimeUserInput | string): Promise<InjectResult>;
73
72
  stopRunner(sessionId: string): void;
74
73
  resolveInteraction(sessionId: string, interactionId: string, resolution: SessionInteractionResolution): Promise<{
75
74
  disposition: ControlSessionInteractionResolveDisposition;
@@ -313,10 +312,11 @@ export declare class MachineSessionService {
313
312
  launchOptions(): Promise<RemoteRuntimeSessionLaunchOptionsListResult>;
314
313
  /** Read the effective model defaults used for the next turn. */
315
314
  executionSettings(sessionIdInput: string): Promise<RemoteRuntimeSessionExecutionGetResult>;
316
- /** Replace the Turn defaults of an idle Session. Execution settings are
317
- * passed through to the Provider CLI, which owns availability validation.
318
- * The runner is closed so every Provider resumes its native context with
319
- * the new settings on the next turn. */
315
+ /** Replace the defaults for the next new Turn. Codex-lineage Providers keep
316
+ * their live runner: the host applies changed native thread settings under
317
+ * its injection lock immediately before the next `turn/start`. Claude still
318
+ * needs an idle cold resume because its live CLI has no confirmed settings
319
+ * update boundary. Provider CLIs own model availability validation. */
320
320
  updateExecutionSettings(input: RemoteRuntimeSessionExecutionUpdateParams): Promise<RemoteRuntimeSessionExecutionUpdateResult>;
321
321
  /** Create target-owned Session identity from one Agent preset or direct Provider. */
322
322
  create(input: MachineSessionCreateInput): Promise<MachineSessionCreateResult>;
@@ -7,8 +7,8 @@
7
7
  * Direct adapters can therefore expose the same behavior without copying state
8
8
  * into a Control Plane database.
9
9
  */
10
- import { createHash, randomUUID } from "node:crypto";
11
- import { SESSION_PROVIDER_IDS, getRuntimeProfile, isSessionProviderId, newSessionItemId, newSessionId, normalizeSessionTitle, } from "@rynx-ai/core";
10
+ import { createHash } from "node:crypto";
11
+ import { SESSION_PROVIDER_IDS, getRuntimeProfile, isSessionProviderId, newSessionId, normalizeSessionTitle, } from "@rynx-ai/core";
12
12
  import { REMOTE_RUNTIME_SESSION_DEFAULT_PAGE_SIZE, REMOTE_RUNTIME_SESSION_MAX_CURSOR_CHARS, REMOTE_RUNTIME_SESSION_MAX_ID_CHARS, REMOTE_RUNTIME_SESSION_MAX_PAGE_SIZE, } from "@rynx-ai/protocol/remote-runtime-rpc";
13
13
  const DEFAULT_LIST_LIMIT = REMOTE_RUNTIME_SESSION_DEFAULT_PAGE_SIZE;
14
14
  const MAX_LIST_LIMIT = REMOTE_RUNTIME_SESSION_MAX_PAGE_SIZE;
@@ -210,10 +210,11 @@ export class MachineSessionService {
210
210
  }
211
211
  return executionSettingsResult(meta.execution);
212
212
  }
213
- /** Replace the Turn defaults of an idle Session. Execution settings are
214
- * passed through to the Provider CLI, which owns availability validation.
215
- * The runner is closed so every Provider resumes its native context with
216
- * the new settings on the next turn. */
213
+ /** Replace the defaults for the next new Turn. Codex-lineage Providers keep
214
+ * their live runner: the host applies changed native thread settings under
215
+ * its injection lock immediately before the next `turn/start`. Claude still
216
+ * needs an idle cold resume because its live CLI has no confirmed settings
217
+ * update boundary. Provider CLIs own model availability validation. */
217
218
  async updateExecutionSettings(input) {
218
219
  const sessionId = validOpaqueToken(input.sessionId, "sessionId", MAX_SESSION_ID_CHARS);
219
220
  const modelId = input.model === null ? null : validRequiredText(input.model, "model");
@@ -222,13 +223,15 @@ export class MachineSessionService {
222
223
  throw new MachineSessionServiceFailure("not_found", "session not found");
223
224
  }
224
225
  await this.assertNotForkReserved(sessionId);
225
- const runtime = this.ports.runtimeState.snapshot(sessionId);
226
- const pendingMessage = await this.ports.pendingMessages?.get(sessionId);
227
- if (runtime.status !== "idle" ||
228
- runtime.activeResponseIds.length > 0 ||
229
- runtime.pendingInteractions.length > 0 ||
230
- pendingMessage) {
231
- throw new MachineSessionServiceFailure("failed_precondition", "Session must be idle before changing model settings");
226
+ if (meta.execution.provider === "claude") {
227
+ const runtime = this.ports.runtimeState.snapshot(sessionId);
228
+ const pendingMessage = await this.ports.pendingMessages?.get(sessionId);
229
+ if (runtime.status !== "idle" ||
230
+ runtime.activeResponseIds.length > 0 ||
231
+ runtime.pendingInteractions.length > 0 ||
232
+ pendingMessage) {
233
+ throw new MachineSessionServiceFailure("failed_precondition", "Claude Session must be idle before changing model settings");
234
+ }
232
235
  }
233
236
  const executionPort = this.requireExecution();
234
237
  const reasoningEffort = input.reasoningEffort === null
@@ -240,7 +243,9 @@ export class MachineSessionService {
240
243
  reasoningEffort,
241
244
  };
242
245
  this.ports.registry.setExecution(sessionId, nextExecution);
243
- executionPort.runner.stopRunner(sessionId);
246
+ if (nextExecution.provider === "claude") {
247
+ executionPort.runner.stopRunner(sessionId);
248
+ }
244
249
  return executionSettingsResult(nextExecution);
245
250
  }
246
251
  /** Create target-owned Session identity from one Agent preset or direct Provider. */
@@ -508,69 +513,21 @@ export class MachineSessionService {
508
513
  if (request.clientMessageId) {
509
514
  await resources?.markMessageInjecting(sessionId, request.clientMessageId);
510
515
  }
511
- const responseId = this.ports.publishEvent ? `resp_${randomUUID()}` : undefined;
512
- if (responseId) {
513
- await this.ports.publishEvent(sessionId, {
514
- type: "session.input.consumed",
515
- item: {
516
- id: newSessionItemId("message"),
517
- sessionId,
518
- position: 0,
519
- responseId,
520
- status: "completed",
521
- createdAt: Date.now(),
522
- type: "message",
523
- data: {
524
- role: "user",
525
- content: prepared.input.content.map((part) => part.type === "text"
526
- ? { type: "input_text", text: part.text }
527
- : { ...part.resource }),
528
- },
529
- },
530
- });
531
- await this.ports.publishEvent(sessionId, {
532
- type: "session.status",
533
- sessionId,
534
- responseId,
535
- status: "running",
536
- statusKind: "startup",
537
- });
538
- }
539
- const abandonResponseHandoff = responseId
540
- ? this.ports.runtimeState.beginResponseHandoff?.(sessionId, responseId)
541
- : undefined;
542
- const clearStartup = async (status) => {
543
- if (!responseId)
544
- return;
545
- await this.ports.publishEvent(sessionId, {
546
- type: "session.status",
547
- sessionId,
548
- responseId,
549
- status,
550
- });
551
- };
552
516
  let execution;
553
517
  try {
554
518
  execution = await this.ensureLiveSession(sessionId, meta);
555
519
  }
556
520
  catch (error) {
557
- abandonResponseHandoff?.();
558
- await clearStartup("idle");
559
521
  if (request.clientMessageId) {
560
522
  await resources?.markMessageFailedNotStarted(sessionId, request.clientMessageId, error instanceof Error ? error.message : String(error));
561
523
  }
562
524
  throw error;
563
525
  }
564
- let outcome;
526
+ let result;
565
527
  try {
566
- const runtimeInput = responseId
567
- ? { ...prepared.input, responseId }
568
- : prepared.input;
569
- outcome = await execution.runner.injectMessage(sessionId, responseId || needsPreparedOperation ? runtimeInput : request.message);
528
+ result = await execution.runner.injectMessage(sessionId, needsPreparedOperation ? prepared.input : request.message);
570
529
  }
571
530
  catch (error) {
572
- abandonResponseHandoff?.();
573
- await clearStartup("idle");
574
531
  const failure = execution.runner.lastLiveSessionFailure?.(sessionId)
575
532
  ?? liveSessionFailureFrom(error);
576
533
  const reason = failure?.message
@@ -580,9 +537,8 @@ export class MachineSessionService {
580
537
  }
581
538
  throw new MachineSessionServiceFailure("outcome_unknown", "live injection outcome is unknown", reason, failure);
582
539
  }
540
+ const outcome = result.outcome;
583
541
  if (outcome === "failed") {
584
- abandonResponseHandoff?.();
585
- await clearStartup("idle");
586
542
  const failure = execution.runner.lastLiveSessionFailure?.(sessionId);
587
543
  const reason = failure?.message
588
544
  ?? execution.runner.lastLiveSessionError?.(sessionId)
@@ -593,8 +549,6 @@ export class MachineSessionService {
593
549
  throw new MachineSessionServiceFailure("outcome_unknown", `live injection ${outcome}`, reason, failure);
594
550
  }
595
551
  if (outcome !== "injected" && outcome !== "steered") {
596
- abandonResponseHandoff?.();
597
- await clearStartup("idle");
598
552
  const failure = execution.runner.lastLiveSessionFailure?.(sessionId);
599
553
  const reason = failure?.message
600
554
  ?? execution.runner.lastLiveSessionError?.(sessionId)
@@ -604,9 +558,6 @@ export class MachineSessionService {
604
558
  }
605
559
  throw new MachineSessionServiceFailure("failed_precondition", `live injection ${outcome}`, reason, failure);
606
560
  }
607
- if (outcome === "steered")
608
- abandonResponseHandoff?.();
609
- await clearStartup("running");
610
561
  if (request.clientMessageId) {
611
562
  try {
612
563
  await resources?.markMessageInjected(sessionId, request.clientMessageId);
@@ -838,7 +789,7 @@ export class MachineSessionService {
838
789
  current.message !== pending.message)
839
790
  return;
840
791
  injectionAttempted = true;
841
- const outcome = await execution.runner.injectMessage(sessionId, pending.message);
792
+ const { outcome } = await execution.runner.injectMessage(sessionId, pending.message);
842
793
  if (outcome === "injected" || outcome === "steered") {
843
794
  await Promise.resolve(pendingStore.delete(sessionId)).catch(async () => {
844
795
  await Promise.resolve(pendingStore.markOutcomeUnknown(sessionId)).catch(() => undefined);
@@ -534,6 +534,7 @@ function assertEventScope(event, expectedSessionId) {
534
534
  case "session.interaction.requested":
535
535
  case "session.interaction.resolved":
536
536
  case "session.interaction.cancelled":
537
+ case "session.interrupted":
537
538
  case "session.todos":
538
539
  case "session.rotated":
539
540
  actual = event.sessionId;
package/dist/server.d.ts CHANGED
@@ -4,7 +4,7 @@ import { ConversationRuntime, type AdmissionReservation, type AgentCapabilities,
4
4
  import type { SessionInteractionResolution } from "@rynx-ai/protocol";
5
5
  import type { ControlRuntimeShareAddress, DaemonBrowserArtifactCleanResult, DaemonBrowserArtifactInstallInput, DaemonBrowserArtifactInstallResult, DaemonBrowserArtifactUpdateInput, DaemonBrowserArtifactVersionResult, DaemonCleanupSessionsInput, DaemonCleanupSessionsResult, DaemonChromeInspectionConfigureInput, DaemonChromeInspectionStatus, DaemonShutdownIfIdleResult } from "@rynx-ai/protocol/control";
6
6
  import type { PluginInstallCommitInput, PluginInstallCommitResult, PluginInstallPreparation, PluginInstallPrepareInput, PluginManagementItem, PluginMarketplaceAddInput, PluginMarketplaceItem } from "@rynx-ai/protocol/plugin-management";
7
- import type { PluginJsonValue } from "@rynx-ai/plugin-sdk";
7
+ import type { PluginAgentSummary, PluginJsonValue, PluginResolvedSessionExecution, PluginSessionActivitySnapshot, PluginSessionExecutionSnapshot } from "@rynx-ai/plugin-sdk";
8
8
  import type { PairingOffer } from "@rynx-ai/protocol/direct-runtime";
9
9
  import type { DaemonStatus } from "@rynx-ai/protocol/remote-runtime";
10
10
  import type { RuntimeBrowserBootstrapCredential, RuntimeBrowserEndpointDescriptor } from "@rynx-ai/protocol/runtime-browser-bootstrap";
@@ -12,7 +12,6 @@ import type { RuntimeBrowserInspectMessage, RuntimeBrowserInspectTarget } from "
12
12
  import type { RuntimeBrowserSurfaceClientFrame, RuntimeBrowserSurfaceImageFrame, RuntimeBrowserSurfaceOpenFrame, RuntimeBrowserSurfaceReadyFrame, RuntimeBrowserSurfaceServerFrame } from "@rynx-ai/protocol/runtime-browser-surface";
13
13
  import type { RuntimeEmulatorSurfaceImageFrame, RuntimeEmulatorSurfaceReadyFrame } from "@rynx-ai/protocol/runtime-emulator-surface";
14
14
  import type { RemoteRuntimeRpcMethod, RemoteRuntimeRpcParams, RemoteRuntimeRpcResultFor, RemoteRuntimeSequencedSessionEvent } from "@rynx-ai/protocol/remote-runtime-rpc";
15
- import type { PluginAgentSummary, PluginResolvedSessionExecution, PluginSessionExecutionSnapshot } from "@rynx-ai/plugin-sdk";
16
15
  import { RunnerManager, type CodexSessionStore, type RunnerSessionContextProvider } from "@rynx-ai/runtime";
17
16
  import { type ControlPlaneDeps, type LocalSessionResourceHost, type SessionResourceLifecycle } from "./control-api.js";
18
17
  import type { MachineSessionService } from "./machine-session-service.js";
@@ -160,6 +159,7 @@ export interface PluginRuntimeHostServices {
160
159
  session: PluginSessionExecutionSnapshot;
161
160
  }): Promise<PluginResolvedSessionExecution>;
162
161
  };
162
+ sessionActivity(sessionId: string): PluginSessionActivitySnapshot;
163
163
  interruptSession(sessionId: string): Promise<boolean>;
164
164
  terminateSession?(sessionId: string): Promise<boolean>;
165
165
  resolveSessionInteraction(sessionId: string, interactionId: string, resolution: SessionInteractionResolution): Promise<{
package/dist/server.js CHANGED
@@ -75,7 +75,6 @@ export function createSessionRuntimeServices(input) {
75
75
  config: input.config,
76
76
  executor: runnerManager,
77
77
  sessionBus,
78
- sessionEventSink: mirrorSessionEvent,
79
78
  });
80
79
  runnerManager.onMirror((sessionId, event) => {
81
80
  void mirrorSessionEvent(sessionId, event).catch((error) => {
@@ -222,6 +221,15 @@ export async function startServer({ config = loadConfig(), control, sessionLog,
222
221
  session: input.session,
223
222
  }),
224
223
  },
224
+ sessionActivity: (sessionId) => {
225
+ const snapshot = sessionRuntimeIndex.snapshot(sessionId);
226
+ return {
227
+ ...snapshot,
228
+ status: snapshot.pendingInteractions.length > 0
229
+ ? "waiting"
230
+ : snapshot.status,
231
+ };
232
+ },
225
233
  interruptSession: (sessionId) => runnerManager.interruptLiveSession(sessionId),
226
234
  terminateSession: (sessionId) => runnerManager.terminateLiveSession(sessionId),
227
235
  resolveSessionInteraction: (sessionId, interactionId, resolution) => runnerManager.resolveInteraction(sessionId, interactionId, resolution),
@@ -13,12 +13,6 @@ export interface SessionRuntimeActivitySnapshot {
13
13
  */
14
14
  export declare class SessionRuntimeIndex {
15
15
  private readonly sessions;
16
- /**
17
- * Make an accepted Turn visible before the request admission reservation is
18
- * released. The returned callback only abandons the pre-created handoff; once
19
- * response.created arrives, the native response remains authoritative.
20
- */
21
- beginResponseHandoff(sessionId: string, responseId: string): () => void;
22
16
  observe(sessionId: string, event: SessionEvent): void;
23
17
  snapshot(sessionId: string): SessionRuntimeSnapshot;
24
18
  /** Aggregate only process-live work; durable Session history is not activity. */
@@ -27,4 +21,6 @@ export declare class SessionRuntimeIndex {
27
21
  private state;
28
22
  private removeResponseInteractions;
29
23
  private hasRunningTurn;
24
+ private statusAfterResponseTerminal;
25
+ private retireStatusResponses;
30
26
  }
@@ -6,57 +6,63 @@
6
6
  */
7
7
  export class SessionRuntimeIndex {
8
8
  sessions = new Map();
9
- /**
10
- * Make an accepted Turn visible before the request admission reservation is
11
- * released. The returned callback only abandons the pre-created handoff; once
12
- * response.created arrives, the native response remains authoritative.
13
- */
14
- beginResponseHandoff(sessionId, responseId) {
15
- const state = this.state(sessionId);
16
- state.pendingResponseIds.add(responseId);
17
- state.status = "running";
18
- state.statusKind = "startup";
19
- let pending = true;
20
- return () => {
21
- if (!pending)
22
- return;
23
- pending = false;
24
- state.pendingResponseIds.delete(responseId);
25
- };
26
- }
27
9
  observe(sessionId, event) {
28
10
  const state = this.state(sessionId);
29
11
  switch (event.type) {
30
12
  case "response.created":
31
- state.pendingResponseIds.delete(event.responseId);
32
13
  state.activeResponseIds.add(event.responseId);
33
14
  state.status = "running";
34
15
  state.statusKind = undefined;
35
16
  return;
36
17
  case "response.completed":
37
- state.pendingResponseIds.delete(event.responseId);
38
18
  state.activeResponseIds.delete(event.responseId);
39
19
  this.removeResponseInteractions(state, event.responseId);
40
- state.status = this.hasRunningTurn(state) ? "running" : "idle";
20
+ state.status = this.statusAfterResponseTerminal(state);
41
21
  state.statusKind = undefined;
42
22
  return;
43
23
  case "response.failed":
44
- state.pendingResponseIds.delete(event.responseId);
45
24
  state.activeResponseIds.delete(event.responseId);
46
25
  this.removeResponseInteractions(state, event.responseId);
47
26
  // Response failure is Turn-local. It must not poison a reusable Session.
48
- state.status = this.hasRunningTurn(state) ? "running" : "idle";
27
+ state.status = this.statusAfterResponseTerminal(state);
49
28
  state.statusKind = undefined;
50
29
  return;
51
- case "session.status":
52
- state.status =
53
- state.pendingInteractions.size > 0 || this.hasRunningTurn(state)
54
- ? "running"
55
- : event.status;
56
- state.statusKind = event.statusKind;
30
+ case "session.interrupted":
31
+ state.activeResponseIds.delete(event.responseId);
32
+ this.removeResponseInteractions(state, event.responseId);
33
+ state.status = this.statusAfterResponseTerminal(state);
34
+ state.statusKind = undefined;
35
+ return;
36
+ case "session.status": {
37
+ if (event.status === "running") {
38
+ if (event.responseId)
39
+ state.activeResponseIds.add(event.responseId);
40
+ state.status = "running";
41
+ state.statusKind = event.statusKind;
42
+ return;
43
+ }
44
+ // A provider interaction is a stronger in-flight fact than a quiet-pane
45
+ // idle observation. Claude's status-file path reports waiting as running;
46
+ // this guard preserves the same truth on its legacy PTY fallback.
47
+ if (event.status === "idle" && state.pendingInteractions.size > 0) {
48
+ state.status = "running";
49
+ state.statusKind = undefined;
50
+ return;
51
+ }
52
+ this.retireStatusResponses(state);
53
+ if (event.status === "failed") {
54
+ state.status = "failed";
55
+ }
56
+ else if (state.status !== "failed") {
57
+ // Provider idle is authoritative even when final transcript records
58
+ // arrive later. Response identity remains on those events themselves;
59
+ // it must not keep Session activity/loading alive.
60
+ state.status = this.hasRunningTurn(state) ? "running" : "idle";
61
+ }
62
+ state.statusKind = undefined;
57
63
  return;
64
+ }
58
65
  case "session.interaction.requested":
59
- state.pendingResponseIds.delete(event.responseId);
60
66
  state.pendingInteractions.set(event.interaction.interactionId, {
61
67
  responseId: event.responseId,
62
68
  interaction: event.interaction,
@@ -68,10 +74,10 @@ export class SessionRuntimeIndex {
68
74
  case "session.interaction.resolved":
69
75
  case "session.interaction.cancelled":
70
76
  state.pendingInteractions.delete(event.interactionId);
71
- state.status = this.hasRunningTurn(state) ? "running" : state.status;
77
+ if (this.hasRunningTurn(state))
78
+ state.status = "running";
72
79
  return;
73
80
  case "session.rotated":
74
- state.pendingResponseIds.clear();
75
81
  state.activeResponseIds.clear();
76
82
  state.pendingInteractions.clear();
77
83
  state.status = "idle";
@@ -86,9 +92,7 @@ export class SessionRuntimeIndex {
86
92
  if (!state)
87
93
  return { status: "idle", activeResponseIds: [], pendingInteractions: [] };
88
94
  return {
89
- status: state.pendingInteractions.size > 0 || this.hasRunningTurn(state)
90
- ? "running"
91
- : state.status,
95
+ status: state.pendingInteractions.size > 0 ? "running" : state.status,
92
96
  ...(state.statusKind === undefined ? {} : { statusKind: state.statusKind }),
93
97
  activeResponseIds: [...state.activeResponseIds],
94
98
  pendingInteractions: [...state.pendingInteractions.values()],
@@ -99,7 +103,9 @@ export class SessionRuntimeIndex {
99
103
  let runningTurns = 0;
100
104
  let pendingInteractions = 0;
101
105
  for (const state of this.sessions.values()) {
102
- runningTurns += state.pendingResponseIds.size + state.activeResponseIds.size;
106
+ if (state.status === "running" || state.pendingInteractions.size > 0) {
107
+ runningTurns += Math.max(1, state.activeResponseIds.size);
108
+ }
103
109
  pendingInteractions += state.pendingInteractions.size;
104
110
  }
105
111
  return { runningTurns, pendingInteractions };
@@ -112,7 +118,6 @@ export class SessionRuntimeIndex {
112
118
  if (!state) {
113
119
  state = {
114
120
  status: "idle",
115
- pendingResponseIds: new Set(),
116
121
  activeResponseIds: new Set(),
117
122
  pendingInteractions: new Map(),
118
123
  };
@@ -127,6 +132,20 @@ export class SessionRuntimeIndex {
127
132
  }
128
133
  }
129
134
  hasRunningTurn(state) {
130
- return state.pendingResponseIds.size > 0 || state.activeResponseIds.size > 0;
135
+ return state.activeResponseIds.size > 0;
136
+ }
137
+ statusAfterResponseTerminal(state) {
138
+ if (state.pendingInteractions.size > 0 || this.hasRunningTurn(state))
139
+ return "running";
140
+ return state.status === "failed" ? "failed" : "idle";
141
+ }
142
+ retireStatusResponses(state) {
143
+ // session.status is Session-wide. Match Omnigent's
144
+ // _session_active_response_cache: every idle/failed edge closes the one
145
+ // in-flight native response projection, even when the edge happens to be
146
+ // stamped with that response id. A late transcript event may still carry
147
+ // the old id for grouping, but it cannot keep activity alive.
148
+ state.activeResponseIds.clear();
149
+ state.pendingInteractions.clear();
131
150
  }
132
151
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynx-ai/server",
3
- "version": "0.1.11-beta.22",
3
+ "version": "0.1.11-beta.24",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/rynx-ai/rynx.git",
@@ -27,15 +27,15 @@
27
27
  "@koa/router": "^15.4.0",
28
28
  "koa": "^3.2.0",
29
29
  "ws": "^8.21.0",
30
- "@rynx-ai/core": "0.1.11-beta.22",
31
- "@rynx-ai/plugin-sdk": "0.1.11-beta.22",
32
- "@rynx-ai/protocol": "0.1.11-beta.22",
33
- "@rynx-ai/runtime": "0.1.11-beta.22",
34
- "@rynx-ai/remote-runtime-e2ee": "0.1.11-beta.22"
30
+ "@rynx-ai/plugin-sdk": "0.1.11-beta.24",
31
+ "@rynx-ai/protocol": "0.1.11-beta.24",
32
+ "@rynx-ai/remote-runtime-e2ee": "0.1.11-beta.24",
33
+ "@rynx-ai/core": "0.1.11-beta.24",
34
+ "@rynx-ai/runtime": "0.1.11-beta.24"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/ws": "^8.18.1",
38
- "@rynx-ai/control-web": "0.1.11-beta.22"
38
+ "@rynx-ai/control-web": "0.1.11-beta.24"
39
39
  },
40
40
  "scripts": {
41
41
  "build": "rm -rf dist && tsc -p tsconfig.json && mkdir -p dist/control-web && cp -R ../control-web/dist/. dist/control-web/",