@omercnet/paseo-omp 2.0.0 → 2.0.1-next.255.1

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.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@omercnet/paseo-omp",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@omercnet/paseo-omp",
9
- "version": "2.0.0",
9
+ "version": "2.0.1",
10
10
  "dependencies": {
11
11
  "@getpaseo/protocol": "0.11.1",
12
12
  "@modelcontextprotocol/sdk": "1.30.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omercnet/paseo-omp",
3
- "version": "2.0.0",
3
+ "version": "2.0.1-next.255.1",
4
4
  "type": "module",
5
5
  "description": "Paseo integration for OMP, including its direct provider and workspace tooling.",
6
6
  "license": "MIT",
@@ -63,16 +63,16 @@
63
63
  "@getpaseo/client": "0.11.1",
64
64
  "@getpaseo/plugin": "0.11.1",
65
65
  "@getpaseo/server": "0.11.1",
66
- "@oh-my-pi/pi-agent-core": "18.2.0",
67
- "@oh-my-pi/pi-coding-agent": "18.2.0",
66
+ "@oh-my-pi/pi-agent-core": "18.8.4",
67
+ "@oh-my-pi/pi-coding-agent": "18.8.4",
68
68
  "@tanstack/react-query": "^5.102.3",
69
69
  "@types/node": "^24.10.1",
70
70
  "@types/react": "^19.2.18",
71
71
  "@vitest/coverage-v8": "^5.0.0",
72
72
  "esbuild": "0.28.2",
73
+ "pino": "10.3.1",
73
74
  "react": "19.1.0",
74
75
  "react-native": "0.81.5",
75
- "pino": "10.3.1",
76
76
  "tsx": "^4.20.6",
77
77
  "typescript": "^7.0.2",
78
78
  "vitest": "^5.0.0",
package/server/hub.ts CHANGED
@@ -8,6 +8,13 @@ import type { HubProcess, listHubProcesses, tailHubLog } from "../shared/hub";
8
8
  const MAX_LOG_BYTES = 64 * 1024;
9
9
 
10
10
  const ScopeFileSchema = z.object({ projectDir: z.string() });
11
+ // omp 18.8 moved the spec from meta.json to a sibling spec.json; older releases embed it in meta.json.
12
+ const SpecSchema = z.object({
13
+ application: z.string(),
14
+ args: z.array(z.string()).optional(),
15
+ cwd: z.string().optional(),
16
+ });
17
+
11
18
  const MetaFileSchema = z.object({
12
19
  daemon: z.object({
13
20
  state: z.string(),
@@ -21,11 +28,7 @@ const MetaFileSchema = z.object({
21
28
  exitedAt: z.number().optional(),
22
29
  exitCode: z.number().optional(),
23
30
  }),
24
- spec: z.object({
25
- application: z.string(),
26
- args: z.array(z.string()).optional(),
27
- cwd: z.string().optional(),
28
- }),
31
+ spec: SpecSchema.optional(),
29
32
  });
30
33
 
31
34
  /**
@@ -71,10 +74,12 @@ async function findProjectDaemonRoots(root: string, cwd: string): Promise<string
71
74
  return matches;
72
75
  }
73
76
 
74
- function toHubProcess(name: string, value: unknown): HubProcess | undefined {
77
+ function toHubProcess(name: string, value: unknown, specFile: unknown): HubProcess | undefined {
75
78
  const parsed = MetaFileSchema.safeParse(value);
76
79
  if (!parsed.success) return undefined;
77
- const { daemon, spec } = parsed.data;
80
+ const { daemon } = parsed.data;
81
+ const spec = parsed.data.spec ?? SpecSchema.safeParse(specFile).data;
82
+ if (!spec) return undefined;
78
83
  return {
79
84
  name,
80
85
  application: spec.application,
@@ -101,8 +106,12 @@ export async function listHubProcessesFrom(root: string, cwd: string): Promise<H
101
106
  const names = await listDirNames(join(projectRoot, "daemons"));
102
107
  await Promise.all(
103
108
  names.map(async (name) => {
104
- const meta = await readJsonFile(join(projectRoot, "daemons", name, "meta.json"));
105
- const process = toHubProcess(name, meta);
109
+ const dir = join(projectRoot, "daemons", name);
110
+ const [meta, spec] = await Promise.all([
111
+ readJsonFile(join(dir, "meta.json")),
112
+ readJsonFile(join(dir, "spec.json")),
113
+ ]);
114
+ const process = toHubProcess(name, meta, spec);
106
115
  if (process) processes.push(process);
107
116
  }),
108
117
  );
@@ -1,6 +1,7 @@
1
1
  export const OMP_OPERATIONAL_FAILURES = [
2
2
  { category: "session-open", stage: "startup" },
3
3
  { category: "session-open", stage: "catalog" },
4
+ { category: "session-open", stage: "optional-mcp" },
4
5
  { category: "replay-recovery", stage: "persisted-replay" },
5
6
  { category: "replay-recovery", stage: "runtime-recovery" },
6
7
  { category: "replay-recovery", stage: "rewind" },
@@ -1,2 +1,2 @@
1
1
  /** Release Please keeps this bundle-safe constant aligned with package.json. */
2
- export const PASEO_OMP_PACKAGE_VERSION = "2.0.0"; // x-release-please-version
2
+ export const PASEO_OMP_PACKAGE_VERSION = "2.0.1-next.255.1"; // x-release-please-version
@@ -58,6 +58,10 @@ const RESULT_TRUNCATED_NOTICE = "[MCP content truncated; details omitted]";
58
58
  const MAX_PENDING_HOST_TOOL_CALLS = 64;
59
59
  const MAX_PENDING_HOST_TOOL_BYTES = 8 * 1024 * 1024;
60
60
  const DEFAULT_INITIALIZATION_TIMEOUT_MS = 20_000;
61
+ // Cold npx-style servers can take several seconds; 10 s leaves headroom under the 20 s ceiling.
62
+ const OPTIONAL_MCP_SERVER_TIMEOUT_MS = 10_000;
63
+ const MAX_DEGRADED_NOTICE_NAMES = 5;
64
+ const MAX_DEGRADED_NAME_BYTES = 64;
61
65
  const DEFAULT_MCP_CALL_LIFETIME_MS = 5 * 60 * 1000;
62
66
  const MAX_DIRECT_BROWSER_CALLS = 4;
63
67
  const BROWSER_CALL_TIMEOUT_MS = 20_000;
@@ -85,6 +89,8 @@ const DEFAULT_CALL_SCHEDULER: OmpHostToolScheduler = {
85
89
 
86
90
  export interface OmpHostToolsOpenOptions {
87
91
  connectMcp?: OmpMcpConnector;
92
+ /** Per-server cap for non-Paseo servers. Defaults to 10 s. */
93
+ optionalServerTimeoutMs?: number;
88
94
  signal?: AbortSignal;
89
95
  initializationTimeoutMs?: number;
90
96
  callTimeoutMs?: number;
@@ -92,6 +98,33 @@ export interface OmpHostToolsOpenOptions {
92
98
  reportOperationalFailure?: OmpOperationalFailureReporter;
93
99
  }
94
100
 
101
+ export type OmpDegradedMcpServer = {
102
+ name: string;
103
+ reason: "unavailable" | "timed-out" | "invalid-tools";
104
+ };
105
+
106
+ const DEGRADED_REASON_TEXT: Record<OmpDegradedMcpServer["reason"], string> = {
107
+ unavailable: "unavailable",
108
+ "timed-out": "timed out",
109
+ "invalid-tools": "invalid tool list",
110
+ };
111
+
112
+ /** Fixed reason vocabulary and bounded names only: never raw error text. */
113
+ export function describeDegradedMcpServers(degraded: readonly OmpDegradedMcpServer[]): string {
114
+ const shown = degraded.slice(0, MAX_DEGRADED_NOTICE_NAMES).map((server) => {
115
+ const name = boundedText(
116
+ server.name.replace(/[^\p{L}\p{N}_.:@/ -]/gu, ""),
117
+ MAX_DEGRADED_NAME_BYTES,
118
+ "server",
119
+ );
120
+ return `${name} (${DEGRADED_REASON_TEXT[server.reason]})`;
121
+ });
122
+ const more = degraded.length - shown.length;
123
+ const list = more > 0 ? `${shown.join(", ")}, and ${more} more` : shown.join(", ");
124
+ const noun = degraded.length === 1 ? "server was" : "servers were";
125
+ return `${degraded.length} MCP ${noun} skipped: ${list}. Their tools are missing from this session. Reopen the agent after fixing them.`;
126
+ }
127
+
95
128
  type ClassifiedServer = {
96
129
  name: string;
97
130
  config: ProviderMcpServerConfig;
@@ -508,6 +541,7 @@ export class OmpHostToolsBridge {
508
541
  private readonly callTimeoutMs: number,
509
542
  private readonly callScheduler: OmpHostToolScheduler,
510
543
  private readonly reportOperationalFailure: OmpOperationalFailureReporter,
544
+ readonly degraded: readonly OmpDegradedMcpServer[],
511
545
  ) {
512
546
  this.labels = new Map(definitions.map(({ name, label }) => [name, label ?? name]));
513
547
  }
@@ -535,89 +569,143 @@ export class OmpHostToolsBridge {
535
569
  const connections: OmpMcpConnection[] = [];
536
570
  const definitions: OmpHostToolDefinition[] = [];
537
571
  const targets = new Map<string, ToolTarget>();
572
+ const degraded: OmpDegradedMcpServer[] = [];
573
+ const report = (failure: OmpOperationalFailure) => {
574
+ try {
575
+ options.reportOperationalFailure?.(failure);
576
+ } catch {
577
+ // Diagnostics must never affect session open.
578
+ }
579
+ };
538
580
  try {
539
581
  for (const server of servers) {
540
582
  initialization.signal.throwIfAborted();
541
- const connecting = connectMcp(
542
- server.name,
543
- server.config,
544
- config.cwd,
545
- initialization.signal,
546
- );
547
- let connection: OmpMcpConnection;
583
+ // Only Paseo-owned servers are required. Any other server that fails is skipped whole.
584
+ const optional = !server.internal;
585
+ const timeout = optional ? new AbortController() : undefined;
586
+ const timer = timeout
587
+ ? setTimeout(
588
+ () => timeout.abort(),
589
+ options.optionalServerTimeoutMs ?? OPTIONAL_MCP_SERVER_TIMEOUT_MS,
590
+ )
591
+ : undefined;
592
+ const signal = timeout
593
+ ? AbortSignal.any([initialization.signal, timeout.signal])
594
+ : initialization.signal;
595
+ const connectionCount = connections.length;
596
+ const definitionCount = definitions.length;
597
+ const added: string[] = [];
598
+ let reason: OmpDegradedMcpServer["reason"] = "unavailable";
548
599
  try {
549
- connection =
550
- connectMcp === connectMcpServer
551
- ? await connecting
552
- : await abortable(connecting, initialization.signal);
553
- } catch (error) {
554
- if (connectMcp !== connectMcpServer && initialization.signal.aborted) {
555
- const cleanup = connecting.then(
556
- (lateConnection) => lateConnection.close(),
557
- () => undefined,
558
- );
559
- throw new OmpCleanupFailure(
560
- "OMP MCP connection initialization was interrupted",
561
- cleanup,
562
- );
600
+ const connecting = connectMcp(server.name, server.config, config.cwd, signal);
601
+ let connection: OmpMcpConnection;
602
+ try {
603
+ connection =
604
+ connectMcp === connectMcpServer
605
+ ? await connecting
606
+ : await abortable(connecting, signal);
607
+ } catch (error) {
608
+ if (connectMcp !== connectMcpServer && signal.aborted) {
609
+ const cleanup = connecting.then(
610
+ (lateConnection) => lateConnection.close(),
611
+ (rejection) =>
612
+ rejection instanceof OmpCleanupFailure ? rejection.cleanup : undefined,
613
+ );
614
+ throw new OmpCleanupFailure(
615
+ "OMP MCP connection initialization was interrupted",
616
+ cleanup,
617
+ );
618
+ }
619
+ throw error;
620
+ }
621
+ connections.push(connection);
622
+ const tools = (await discoverMcpTools(connection, signal)).sort((left, right) =>
623
+ left.name.localeCompare(right.name),
624
+ );
625
+ reason = "invalid-tools";
626
+ const namespace = namespaces.get(server.name);
627
+ if (!namespace) throw new Error("MCP server namespace is unavailable");
628
+ for (const tool of tools) {
629
+ if (
630
+ !tool.name ||
631
+ utf8Bytes(tool.name) > MAX_HOST_TOOL_NAME_BYTES ||
632
+ utf8Bytes(tool.description ?? "") > MAX_HOST_TOOL_DESCRIPTION_BYTES ||
633
+ !tool.inputSchema ||
634
+ typeof tool.inputSchema !== "object" ||
635
+ Array.isArray(tool.inputSchema) ||
636
+ boundedJsonBytes(tool.inputSchema, MAX_HOST_TOOL_SCHEMA_BYTES) ===
637
+ Number.POSITIVE_INFINITY
638
+ ) {
639
+ throw new Error("MCP server exposed an invalid host tool definition");
640
+ }
641
+ if (definitions.length >= MAX_HOST_TOOLS) {
642
+ throw new Error("MCP servers exposed too many host tools");
643
+ }
644
+ let name = exposedToolName(server, namespace, tool.name);
645
+ if (targets.has(name)) {
646
+ const suffix = digest(`${server.name}\0${tool.name}`);
647
+ name = `${name.slice(0, MAX_HOST_TOOL_NAME_BYTES - suffix.length - 1)}_${suffix}`;
648
+ }
649
+ if (targets.has(name)) throw new Error("MCP host tool names collide");
650
+ const normalizedServerName = safeName(server.name, "server");
651
+ const normalizedToolName = safeName(tool.name, "tool");
652
+ const friendlyToolName = normalizedToolName.startsWith(`${normalizedServerName}_`)
653
+ ? normalizedToolName.slice(normalizedServerName.length + 1)
654
+ : tool.name;
655
+ const fallbackLabel = `${humanizeName(server.name)}: ${humanizeName(friendlyToolName)}`;
656
+ definitions.push({
657
+ name,
658
+ label: boundedText(tool.title ?? fallbackLabel, MAX_HOST_TOOL_LABEL_BYTES, name),
659
+ description: boundedText(
660
+ tool.description ?? `MCP tool from ${server.name}`,
661
+ MAX_HOST_TOOL_DESCRIPTION_BYTES,
662
+ "MCP tool",
663
+ ),
664
+ loadMode:
665
+ server.internal || server.config.alwaysLoad === true ? "essential" : "discoverable",
666
+ parameters: structuredClone(tool.inputSchema),
667
+ });
668
+ targets.set(name, { toolName: tool.name, connection });
669
+ added.push(name);
563
670
  }
564
- throw error;
565
- }
566
- connections.push(connection);
567
- const tools = (await discoverMcpTools(connection, initialization.signal)).sort(
568
- (left, right) => left.name.localeCompare(right.name),
569
- );
570
- const namespace = namespaces.get(server.name);
571
- if (!namespace) throw new Error("MCP server namespace is unavailable");
572
- for (const tool of tools) {
573
671
  if (
574
- !tool.name ||
575
- utf8Bytes(tool.name) > MAX_HOST_TOOL_NAME_BYTES ||
576
- utf8Bytes(tool.description ?? "") > MAX_HOST_TOOL_DESCRIPTION_BYTES ||
577
- !tool.inputSchema ||
578
- typeof tool.inputSchema !== "object" ||
579
- Array.isArray(tool.inputSchema) ||
580
- boundedJsonBytes(tool.inputSchema, MAX_HOST_TOOL_SCHEMA_BYTES) ===
581
- Number.POSITIVE_INFINITY
672
+ boundedJsonBytes(
673
+ definitions,
674
+ MAX_HOST_TOOL_CATALOG_BYTES,
675
+ MAX_HOST_TOOLS,
676
+ 64 * 1024,
677
+ ) === Number.POSITIVE_INFINITY
582
678
  ) {
583
- throw new Error("MCP server exposed an invalid host tool definition");
584
- }
585
- if (definitions.length >= MAX_HOST_TOOLS) {
586
- throw new Error("MCP servers exposed too many host tools");
679
+ throw new OmpPublicError("OMP host tool catalog exceeds the RPC frame limit");
587
680
  }
588
- let name = exposedToolName(server, namespace, tool.name);
589
- if (targets.has(name)) {
590
- const suffix = digest(`${server.name}\0${tool.name}`);
591
- name = `${name.slice(0, MAX_HOST_TOOL_NAME_BYTES - suffix.length - 1)}_${suffix}`;
681
+ } catch (error) {
682
+ if (!optional || initialization.signal.aborted) throw error;
683
+ if (error instanceof OmpCleanupFailure) {
684
+ try {
685
+ await abortable(error.cleanup, initialization.signal);
686
+ } catch {
687
+ throw error;
688
+ }
689
+ } else if (connections.length > connectionCount) {
690
+ const failed = connections.pop() as OmpMcpConnection;
691
+ const closing = Promise.resolve().then(() => failed.close());
692
+ try {
693
+ await abortable(closing, initialization.signal);
694
+ } catch {
695
+ throw new OmpCleanupFailure("OMP MCP host tool cleanup failed", closing);
696
+ }
592
697
  }
593
- if (targets.has(name)) throw new Error("MCP host tool names collide");
594
- const normalizedServerName = safeName(server.name, "server");
595
- const normalizedToolName = safeName(tool.name, "tool");
596
- const friendlyToolName = normalizedToolName.startsWith(`${normalizedServerName}_`)
597
- ? normalizedToolName.slice(normalizedServerName.length + 1)
598
- : tool.name;
599
- const fallbackLabel = `${humanizeName(server.name)}: ${humanizeName(friendlyToolName)}`;
600
- definitions.push({
601
- name,
602
- label: boundedText(tool.title ?? fallbackLabel, MAX_HOST_TOOL_LABEL_BYTES, name),
603
- description: boundedText(
604
- tool.description ?? `MCP tool from ${server.name}`,
605
- MAX_HOST_TOOL_DESCRIPTION_BYTES,
606
- "MCP tool",
607
- ),
608
- loadMode:
609
- server.internal || server.config.alwaysLoad === true ? "essential" : "discoverable",
610
- parameters: structuredClone(tool.inputSchema),
698
+ definitions.length = definitionCount;
699
+ for (const name of added) targets.delete(name);
700
+ degraded.push({
701
+ name: server.name,
702
+ reason: timeout?.signal.aborted ? "timed-out" : reason,
611
703
  });
612
- targets.set(name, { toolName: tool.name, connection });
704
+ report({ category: "session-open", stage: "optional-mcp" });
705
+ } finally {
706
+ clearTimeout(timer);
613
707
  }
614
708
  }
615
- if (
616
- boundedJsonBytes(definitions, MAX_HOST_TOOL_CATALOG_BYTES, MAX_HOST_TOOLS, 64 * 1024) ===
617
- Number.POSITIVE_INFINITY
618
- ) {
619
- throw new OmpPublicError("OMP host tool catalog exceeds the RPC frame limit");
620
- }
621
709
  return new OmpHostToolsBridge(
622
710
  connections,
623
711
  definitions,
@@ -625,6 +713,7 @@ export class OmpHostToolsBridge {
625
713
  callTimeoutMs,
626
714
  callScheduler,
627
715
  options.reportOperationalFailure ?? (() => {}),
716
+ degraded,
628
717
  );
629
718
  } catch (error) {
630
719
  const cleanupTasks = [
@@ -80,6 +80,9 @@ export const OMP_SESSION_EVENT_POLICIES = {
80
80
  advisor_cost_changed: "ignore",
81
81
  advisor_yielded: "normalize",
82
82
  ttsr_triggered: "ignore",
83
+ cache_warming_start: "ignore",
84
+ cache_warming_end: "ignore",
85
+ queue_update: "ignore",
83
86
  todo_reminder: "normalize",
84
87
  todo_auto_clear: "normalize",
85
88
  irc_message: "normalize",
@@ -136,6 +139,26 @@ export const OMP_RPC_COMMAND_POLICIES = {
136
139
  get_messages_page: "implemented",
137
140
  get_login_providers: "unsupported",
138
141
  login: "unsupported",
142
+ abort_and_restore_queue: "unsupported",
143
+ btw: "unsupported",
144
+ btw_cancel: "unsupported",
145
+ cancel_subagent: "unsupported",
146
+ fork: "unsupported",
147
+ get_btw_history: "unsupported",
148
+ get_logout_accounts: "unsupported",
149
+ goal: "unsupported",
150
+ live_mute: "unsupported",
151
+ live_start: "unsupported",
152
+ live_stop: "unsupported",
153
+ logout: "unsupported",
154
+ predict_word: "unsupported",
155
+ predict_word_feedback: "unsupported",
156
+ promote_queued_message: "unsupported",
157
+ remove_queued_message: "unsupported",
158
+ set_ask_dialog: "unsupported",
159
+ set_cache_warming: "unsupported",
160
+ set_slow_mode: "unsupported",
161
+ steer_subagent: "unsupported",
139
162
  } as const;
140
163
  export const OMP_RPC_RESPONSE_POLICIES = OMP_RPC_COMMAND_POLICIES;
141
164
  export const OMP_PROTOCOL_VIOLATION_CATEGORIES = [
@@ -218,6 +241,9 @@ export const OMP_PROTOCOL_EVENT_TYPES = [
218
241
  "config_warnings_changed",
219
242
  "advisor_cost_changed",
220
243
  "ttsr_triggered",
244
+ "cache_warming_start",
245
+ "cache_warming_end",
246
+ "queue_update",
221
247
  "irc_message",
222
248
  "thinking_level_changed",
223
249
  "goal_updated",
@@ -374,6 +400,9 @@ function eventSchemaClassification(
374
400
  case "config_warnings_changed":
375
401
  case "advisor_cost_changed":
376
402
  case "ttsr_triggered":
403
+ case "cache_warming_start":
404
+ case "cache_warming_end":
405
+ case "queue_update":
377
406
  return { reason: "configuration-event-schema", expected: "valid-configuration-event" };
378
407
  case "extension_ui_request":
379
408
  return { reason: "extension-ui-schema", expected: "valid-extension-ui-event" };
@@ -1522,6 +1551,9 @@ const OmpAgentSessionEventSchema = z.discriminatedUnion("type", [
1522
1551
  ...OmpCoreAgentEventSchema.options,
1523
1552
  z.object({ type: z.literal("config_warnings_changed") }),
1524
1553
  z.object({ type: z.literal("advisor_cost_changed") }),
1554
+ z.object({ type: z.literal("cache_warming_start") }),
1555
+ z.object({ type: z.literal("cache_warming_end") }),
1556
+ z.object({ type: z.literal("queue_update") }),
1525
1557
  z.object({ type: z.literal("advisor_yielded") }),
1526
1558
  z.object({
1527
1559
  type: z.literal("ttsr_triggered"),
@@ -24,7 +24,13 @@ import {
24
24
  type OmpRecoveryOptions,
25
25
  withCommittedOmpSelection,
26
26
  } from "./config-normalization";
27
- import { OmpHostToolsBridge, type OmpMcpConnector, validateOmpHostToolConfig } from "./host-tools";
27
+ import {
28
+ describeDegradedMcpServers,
29
+ type OmpDegradedMcpServer,
30
+ OmpHostToolsBridge,
31
+ type OmpMcpConnector,
32
+ validateOmpHostToolConfig,
33
+ } from "./host-tools";
28
34
  import { isOmpImageMimeType, OmpImageMaterializer } from "./image";
29
35
  import type { OmpRuntime, OmpRuntimeSession } from "./omp-rpc";
30
36
  import { buildOmpSpawnRequest, type OmpStartOptions } from "./omp-rpc-environment";
@@ -331,6 +337,7 @@ export class OmpProviderSession implements OmpOpenedSession {
331
337
  private readonly permissions: OmpSessionPermissions;
332
338
  private unsupportedThinkingNoticePending = false;
333
339
  private unsupportedThinkingNoticePublished = false;
340
+ private degradedMcpServers: readonly OmpDegradedMcpServer[] = [];
334
341
 
335
342
  private constructor(
336
343
  id: string,
@@ -685,6 +692,7 @@ export class OmpProviderSession implements OmpOpenedSession {
685
692
  scheduler,
686
693
  );
687
694
  session.unsupportedThinkingNoticePending = unsupportedThinkingLevel;
695
+ session.degradedMcpServers = hostTools.degraded;
688
696
 
689
697
  if (bootstrapConfigRevision !== reconciledConfigRevision) {
690
698
  session.configRefreshDirty = true;
@@ -744,6 +752,7 @@ export class OmpProviderSession implements OmpOpenedSession {
744
752
  ...(this.config.title ? { title: this.dataFilter.text(this.config.title, 256) } : {}),
745
753
  });
746
754
  this.publishUnsupportedThinkingNotice();
755
+ this.publishDegradedMcpNotice();
747
756
  this.emit({ type: "session.config", sessionId: this.id, config: this.configState });
748
757
  if (this.replayHistoryOnOpen) await this.replayHistory(true);
749
758
  this.publishCommands(this.commandCatalog);
@@ -767,6 +776,19 @@ export class OmpProviderSession implements OmpOpenedSession {
767
776
  },
768
777
  });
769
778
  }
779
+ private publishDegradedMcpNotice(): void {
780
+ if (this.degradedMcpServers.length === 0) return;
781
+ this.emit({
782
+ type: "session.notice",
783
+ sessionId: this.id,
784
+ notice: {
785
+ id: "omp:mcp-servers-unavailable",
786
+ severity: "warning",
787
+ title: "MCP servers unavailable",
788
+ description: describeDegradedMcpServers(this.degradedMcpServers),
789
+ },
790
+ });
791
+ }
770
792
 
771
793
  private async replayHistory(preferPersistedTranscript = false): Promise<void> {
772
794
  if (!this.runtime.canReplayHistory) {
@@ -2274,7 +2296,10 @@ export class OmpProviderSession implements OmpOpenedSession {
2274
2296
  if (
2275
2297
  event.type === "config_warnings_changed" ||
2276
2298
  event.type === "advisor_cost_changed" ||
2277
- event.type === "ttsr_triggered"
2299
+ event.type === "ttsr_triggered" ||
2300
+ event.type === "cache_warming_start" ||
2301
+ event.type === "cache_warming_end" ||
2302
+ event.type === "queue_update"
2278
2303
  ) {
2279
2304
  return;
2280
2305
  }
@@ -463,6 +463,9 @@ export class OmpTimelineProjector {
463
463
  event.type === "config_warnings_changed" ||
464
464
  event.type === "advisor_cost_changed" ||
465
465
  event.type === "ttsr_triggered" ||
466
+ event.type === "cache_warming_start" ||
467
+ event.type === "cache_warming_end" ||
468
+ event.type === "queue_update" ||
466
469
  event.type === "irc_message" ||
467
470
  event.type === "model_changed" ||
468
471
  event.type === "thinking_level_changed"