@code-yeongyu/senpi 2026.11.6-1 → 2026.11.6-2

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 (167) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/bundle/chunks/{chunk-5MWHZH2O.js → chunk-2FDW2LQL.js} +1 -1
  3. package/dist/bundle/chunks/{chunk-GG2JO3CL.js → chunk-4E7GDG4H.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-K5JLYACP.js → chunk-4GKU755E.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-437VLZQZ.js → chunk-7III44PV.js} +1 -1
  6. package/dist/bundle/chunks/chunk-7PSBD5KS.js +9 -0
  7. package/dist/bundle/chunks/{chunk-KFWHLVVH.js → chunk-C7OKUC7L.js} +1 -1
  8. package/dist/bundle/chunks/{chunk-CX32K5KD.js → chunk-CF5OGBC4.js} +7 -7
  9. package/dist/bundle/chunks/{chunk-7TGZJH6Z.js → chunk-D5MRJWUK.js} +40 -39
  10. package/dist/bundle/chunks/{chunk-E6L5VZSM.js → chunk-DSWDJ7HT.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-6NMTFURP.js → chunk-ECW7VBMB.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-2CSLJLM6.js → chunk-GKAYPJ4E.js} +2 -2
  13. package/dist/bundle/chunks/{chunk-KCGUUIUS.js → chunk-GNSMNSL7.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-V3JSQ3PG.js → chunk-GSKCWXXG.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-USKWGPA6.js → chunk-GTJJ73DE.js} +1 -1
  16. package/dist/bundle/chunks/{chunk-HHSYTDKO.js → chunk-HLM46XEB.js} +2 -2
  17. package/dist/bundle/chunks/{chunk-A4F4J2KH.js → chunk-I3PHVJAS.js} +2 -2
  18. package/dist/bundle/chunks/{chunk-LAMRXFHZ.js → chunk-LNPAT22L.js} +1 -1
  19. package/dist/bundle/chunks/chunk-NH7SYIGN.js +2 -0
  20. package/dist/bundle/chunks/{chunk-ZIIDIVTG.js → chunk-R2Z7CX77.js} +1 -1
  21. package/dist/bundle/chunks/{chunk-32I42CEY.js → chunk-RQ6CHXQ2.js} +1 -1
  22. package/dist/bundle/chunks/chunk-SJI6UB23.js +2 -0
  23. package/dist/bundle/chunks/{chunk-KUIEHAPR.js → chunk-T2SMQPRE.js} +1 -1
  24. package/dist/bundle/chunks/{chunk-IPG434JT.js → chunk-TIPKAMSS.js} +1 -1
  25. package/dist/bundle/chunks/{chunk-RI5ABX7A.js → chunk-TMXRR3N7.js} +2 -2
  26. package/dist/bundle/chunks/chunk-VD7AKTGP.js +3 -0
  27. package/dist/bundle/chunks/{chunk-22JICYKS.js → chunk-WAOGBBZR.js} +1 -1
  28. package/dist/bundle/chunks/{chunk-6MHCFJDB.js → chunk-WGRKM3N6.js} +2 -2
  29. package/dist/bundle/chunks/{chunk-WF5R2VSP.js → chunk-X5ZVYNOL.js} +1 -1
  30. package/dist/bundle/chunks/{chunk-H2FBGZNP.js → chunk-XCMII2VQ.js} +2 -2
  31. package/dist/bundle/chunks/{chunk-5JUCTKYS.js → chunk-YK3ZZCHI.js} +1 -1
  32. package/dist/bundle/chunks/{cli-main-ZQBSX5H5.js → cli-main-T74YJGIF.js} +1 -1
  33. package/dist/bundle/chunks/{hand-entry-7U7354GG.js → hand-entry-AYRUGE2A.js} +2 -2
  34. package/dist/bundle/chunks/{headless-host-7LKTG76U.js → headless-host-ES74ZB6M.js} +1 -1
  35. package/dist/bundle/chunks/{help-fast-path-NLSWH2CA.js → help-fast-path-TMTGLOBG.js} +1 -1
  36. package/dist/bundle/chunks/{host-command-3YRFN35E.js → host-command-YRXZE4TV.js} +1 -1
  37. package/dist/bundle/chunks/{host-lifecycle-6XLVIECJ.js → host-lifecycle-6VCXMPDW.js} +1 -1
  38. package/dist/bundle/chunks/host-lifecycle.js +1 -1
  39. package/dist/bundle/chunks/{host-session-command-F7USXUZV.js → host-session-command-N7IN7ZDV.js} +1 -1
  40. package/dist/bundle/chunks/{interactive-mode-4RF3PUQ2.js → interactive-mode-B5SQDLH7.js} +1 -1
  41. package/dist/bundle/chunks/{list-tips-H6VGDFTS.js → list-tips-DU57HHKA.js} +1 -1
  42. package/dist/bundle/chunks/{main-G2SRZYQN.js → main-UQFVFCNY.js} +1 -1
  43. package/dist/bundle/chunks/{multi-session-host-RPN54GUC.js → multi-session-host-XSUJDHFZ.js} +5 -5
  44. package/dist/bundle/chunks/{openai-codex-responses-MJZKUS6I.js → openai-codex-responses-7V6LTDRU.js} +2 -2
  45. package/dist/bundle/chunks/{package-manager-cli-D2VFD463.js → package-manager-cli-GLUNPQJU.js} +1 -1
  46. package/dist/bundle/chunks/{role-37WVG42T.js → role-RRFFHZF5.js} +1 -1
  47. package/dist/bundle/chunks/{rotation-stream-QELPMM4N.js → rotation-stream-BN2YK2MC.js} +1 -1
  48. package/dist/bundle/chunks/{rpc-mode-P4D3DVYD.js → rpc-mode-JYN3E7P7.js} +1 -1
  49. package/dist/bundle/chunks/rpc-monitor-source-C6VQ3BC4.js +3 -0
  50. package/dist/bundle/chunks/{run-attached-tui-CGA6O5FR.js → run-attached-tui-W236SEOR.js} +1 -1
  51. package/dist/bundle/chunks/{schedule-command-XWSNOAGG.js → schedule-command-B2EJTBOE.js} +1 -1
  52. package/dist/bundle/chunks/session-control-endpoint-YIMLHGLG.js +3 -0
  53. package/dist/bundle/chunks/{session-picker-OA2AUVZG.js → session-picker-E6ZONUJB.js} +1 -1
  54. package/dist/bundle/chunks/session-worker.js +294 -292
  55. package/dist/bundle/cli.js +2 -2
  56. package/dist/bundle/host-supervisor.js +1 -1
  57. package/dist/bundle/index.js +1 -1
  58. package/dist/bundle/rpc-entry.js +1 -1
  59. package/dist/bundle/runtime-manifest.json +1 -1
  60. package/dist/cli/project-trust-headless.d.ts +13 -0
  61. package/dist/cli/project-trust-headless.js +35 -0
  62. package/dist/cli/project-trust.js +13 -50
  63. package/dist/cli/schedule-delivery.d.ts +4 -0
  64. package/dist/cli/schedule-delivery.js +6 -0
  65. package/dist/cli/schedule-runner.js +8 -0
  66. package/dist/cli/session-opening.d.ts +19 -0
  67. package/dist/cli/session-opening.js +47 -1
  68. package/dist/core/agent-session-runtime.d.ts +12 -0
  69. package/dist/core/agent-session-runtime.js +41 -0
  70. package/dist/core/agent-session.js +13 -22
  71. package/dist/core/egress/api-table.d.ts +121 -0
  72. package/dist/core/egress/api-table.js +41 -0
  73. package/dist/core/egress/classify.d.ts +19 -0
  74. package/dist/core/egress/classify.js +197 -0
  75. package/dist/core/egress/credential-fields.d.ts +18 -0
  76. package/dist/core/egress/credential-fields.js +83 -0
  77. package/dist/core/egress/custody-sidecar.d.ts +44 -0
  78. package/dist/core/egress/custody-sidecar.js +91 -0
  79. package/dist/core/egress/device-custody.d.ts +12 -0
  80. package/dist/core/egress/device-custody.js +82 -0
  81. package/dist/core/egress/endpoint.d.ts +33 -0
  82. package/dist/core/egress/endpoint.js +115 -0
  83. package/dist/core/execute-tool-on-host.d.ts +26 -0
  84. package/dist/core/execute-tool-on-host.js +96 -0
  85. package/dist/core/extensions/builtin/compaction/retained-message-safety.js +15 -0
  86. package/dist/core/extensions/builtin/goal/index.js +15 -3
  87. package/dist/core/extensions/builtin/goal/stop-pause.d.ts +2 -0
  88. package/dist/core/extensions/builtin/goal/stop-pause.js +5 -0
  89. package/dist/core/extensions/builtin/herdr/herdr-state.d.ts +10 -0
  90. package/dist/core/extensions/builtin/herdr/herdr-state.js +34 -6
  91. package/dist/core/extensions/builtin/herdr/index.js +6 -2
  92. package/dist/core/extensions/builtin/mcp/service.d.ts +9 -1
  93. package/dist/core/extensions/builtin/mcp/service.js +32 -1
  94. package/dist/core/extensions/builtin/terminal/rpc-monitor-remote.d.ts +43 -0
  95. package/dist/core/extensions/builtin/terminal/rpc-monitor-remote.js +158 -0
  96. package/dist/core/extensions/builtin/terminal/rpc-monitor-render.d.ts +2 -0
  97. package/dist/core/extensions/builtin/terminal/rpc-monitor-render.js +4 -0
  98. package/dist/core/extensions/builtin/terminal/rpc-monitor-source.d.ts +5 -2
  99. package/dist/core/extensions/builtin/terminal/rpc-monitor-source.js +19 -4
  100. package/dist/core/extensions/builtin/terminal/rpc-monitor-targets.d.ts +4 -0
  101. package/dist/core/extensions/builtin/terminal/rpc-monitor-targets.js +15 -0
  102. package/dist/core/extensions/builtin/terminal/rpc-monitor-watch.d.ts +10 -0
  103. package/dist/core/extensions/builtin/terminal/rpc-monitor-watch.js +28 -2
  104. package/dist/core/extensions/builtin/terminal/rpc-remote-source.d.ts +59 -0
  105. package/dist/core/extensions/builtin/terminal/rpc-remote-source.js +15 -0
  106. package/dist/core/extensions/builtin/terminal/terminal-manifest-model.d.ts +5 -0
  107. package/dist/core/extensions/builtin/terminal/terminal-manifest-parse.js +14 -1
  108. package/dist/core/extensions/builtin/terminal/tools/monitor-rpc.js +8 -1
  109. package/dist/core/extensions/loader.js +8 -0
  110. package/dist/core/extensions/types.d.ts +6 -0
  111. package/dist/core/foreign-session-holders.d.ts +1 -1
  112. package/dist/core/foreign-session-holders.js +3 -3
  113. package/dist/core/session-import/jsonl-import.d.ts +8 -12
  114. package/dist/core/session-import/jsonl-import.js +99 -40
  115. package/dist/core/session-manager.d.ts +2 -0
  116. package/dist/core/session-manager.js +7 -2
  117. package/dist/core/session-store/durable-binding.d.ts +2 -0
  118. package/dist/core/session-store/durable-binding.js +26 -4
  119. package/dist/core/settings-manager.d.ts +20 -0
  120. package/dist/core/settings-manager.js +51 -0
  121. package/dist/core/trust-manager.d.ts +1 -18
  122. package/dist/core/trust-manager.js +3 -124
  123. package/dist/core/trust-requiring-resources.d.ts +21 -0
  124. package/dist/core/trust-requiring-resources.js +189 -0
  125. package/dist/index.d.ts +1 -0
  126. package/dist/index.js +1 -0
  127. package/dist/main.js +42 -12
  128. package/dist/modes/interactive/interactive-mode.js +2 -2
  129. package/dist/modes/interactive/session-control-commands.js +2 -0
  130. package/dist/modes/rpc/connection-handler.js +17 -0
  131. package/dist/modes/rpc/connection-question-bridge.js +7 -4
  132. package/dist/modes/rpc/effect-commands.d.ts +24 -0
  133. package/dist/modes/rpc/host-boot-extensions.d.ts +4 -0
  134. package/dist/modes/rpc/host-boot-extensions.js +17 -0
  135. package/dist/modes/rpc/host-lifecycle-shutdown.js +11 -2
  136. package/dist/modes/rpc/loopless-runtime-settings.js +5 -1
  137. package/dist/modes/rpc/method-classes-schema-session.d.ts +33 -0
  138. package/dist/modes/rpc/method-classes-schema-session.js +4 -0
  139. package/dist/modes/rpc/method-classes-schema.d.ts +33 -0
  140. package/dist/modes/rpc/method-classes.d.ts +4 -0
  141. package/dist/modes/rpc/method-classes.js +7 -0
  142. package/dist/modes/rpc/multi-session-host.d.ts +2 -0
  143. package/dist/modes/rpc/multi-session-host.js +7 -2
  144. package/dist/modes/rpc/project-workspace-commands.d.ts +60 -0
  145. package/dist/modes/rpc/project-workspace-commands.js +145 -0
  146. package/dist/modes/rpc/rpc-error-types.d.ts +11 -1
  147. package/dist/modes/rpc/rpc-error-types.js +10 -0
  148. package/dist/modes/rpc/rpc-input-validation.js +4 -0
  149. package/dist/modes/rpc/rpc-mutation-response-types.d.ts +13 -0
  150. package/dist/modes/rpc/rpc-read-response-types.d.ts +13 -0
  151. package/dist/modes/rpc/rpc-session-command-types.d.ts +15 -0
  152. package/dist/modes/rpc/session-attachment-gate.js +2 -0
  153. package/dist/modes/rpc/session-command-activity.js +2 -0
  154. package/docs/engine-socket/fixtures/admission-routes/h-legacy.json +1 -1
  155. package/docs/engine-socket/fixtures/rpc-inbound-samples.json +41 -2
  156. package/docs/extensions.md +17 -1
  157. package/docs/rpc.md +121 -4
  158. package/docs/terminal-tools.md +8 -0
  159. package/native/socket-secret/prebuilds/win32-arm64/senpi_socket_secret.win32-arm64.node +0 -0
  160. package/native/socket-secret/prebuilds/win32-x64/senpi_socket_secret.win32-x64.node +0 -0
  161. package/package.json +6 -6
  162. package/dist/bundle/chunks/chunk-FUQJK2TU.js +0 -2
  163. package/dist/bundle/chunks/chunk-POZMRM6V.js +0 -2
  164. package/dist/bundle/chunks/chunk-XBG63MJZ.js +0 -8
  165. package/dist/bundle/chunks/chunk-YP47G4YP.js +0 -3
  166. package/dist/bundle/chunks/rpc-monitor-source-KKN2UP4T.js +0 -3
  167. package/dist/bundle/chunks/session-control-endpoint-OST7WAKP.js +0 -3
@@ -4,6 +4,11 @@ export function stopHoldLive(ctx) {
4
4
  return (ctx.getPauseState?.().holds.some((hold) => hold.kind === "session_hold" && hold.key !== "interrupted-run") ===
5
5
  true);
6
6
  }
7
+ /** The session reopened over a run that never finished: its process died mid-turn (senpi#4683, senpi#5755). */
8
+ export function interruptedRunHeld(ctx) {
9
+ return (ctx.getPauseState?.().holds.some((hold) => hold.kind === "session_hold" && hold.key === "interrupted-run") ===
10
+ true);
11
+ }
7
12
  export function markStopPaused(pi, goalId) {
8
13
  pi.appendEntry(GOAL_STOP_PAUSED_ENTRY, { goalId });
9
14
  }
@@ -1,3 +1,4 @@
1
+ import type { TerminalMonitorStateEvent } from "../monitor-state-event.ts";
1
2
  /** How the user must act on a block, as OSC 7501 names it: a permission approval or a typed answer. */
2
3
  export type HerdrBlockedKind = "permission" | "question";
3
4
  export interface HerdrBlock {
@@ -9,6 +10,8 @@ export interface HerdrState {
9
10
  readonly turnActive: boolean;
10
11
  readonly childCount: number;
11
12
  readonly monitorCount: number;
13
+ /** Live monitors with no deadline (persistent watches, or watches without a timeout): they may never fire. */
14
+ readonly openEndedMonitorCount: number;
12
15
  readonly wakeSources: ReadonlyMap<string, number>;
13
16
  }
14
17
  export type HerdrStateEvent = {
@@ -26,6 +29,7 @@ export type HerdrStateEvent = {
26
29
  } | {
27
30
  type: "monitors";
28
31
  count: number;
32
+ openEnded: number;
29
33
  } | {
30
34
  type: "wake-source";
31
35
  source: string;
@@ -35,6 +39,12 @@ export interface HerdrReport {
35
39
  state: "blocked" | "working" | "idle";
36
40
  message?: string;
37
41
  }
42
+ /**
43
+ * How many of the broadcast watches have no deadline: a persistent watch, or one with no timeout. A one-shot watch
44
+ * with a deadline either fires or times out, and both wake the session, so it is bounded. A payload without per-watch
45
+ * detail counts none as open-ended, keeping the pane working as before the split.
46
+ */
47
+ export declare function openEndedMonitors(event: TerminalMonitorStateEvent): number;
38
48
  export declare function initialHerdrState(): HerdrState;
39
49
  export declare function reduceHerdrState(state: HerdrState, event: HerdrStateEvent): HerdrState;
40
50
  export declare function selectHerdrReport(state: HerdrState): HerdrReport;
@@ -1,5 +1,20 @@
1
1
  /** Wake sources whose work is already reported through a dedicated field or the blocked state. */
2
2
  const REPRESENTED_WAKE_SOURCES = new Set(["terminal-monitors", "senpi-task", "ask-user"]);
3
+ /**
4
+ * Wake sources that may stay live with no end in sight (a server left running). Once the turn has ended they are named
5
+ * in the message of an idle report instead of keeping the pane working forever; every other source is bounded work
6
+ * that will start a turn and keeps the pane working (#6150). Monitors are split per watch by `openEndedMonitors`.
7
+ */
8
+ const OPEN_ENDED_WAKE_SOURCES = new Set(["terminal-background-sessions"]);
9
+ /**
10
+ * How many of the broadcast watches have no deadline: a persistent watch, or one with no timeout. A one-shot watch
11
+ * with a deadline either fires or times out, and both wake the session, so it is bounded. A payload without per-watch
12
+ * detail counts none as open-ended, keeping the pane working as before the split.
13
+ */
14
+ export function openEndedMonitors(event) {
15
+ return (event.monitors ?? []).filter((watch) => watch.persistent === true || typeof watch.deadlineMs !== "number")
16
+ .length;
17
+ }
3
18
  const WAKE_SOURCE_LABELS = {
4
19
  "terminal-background-sessions": ["background session", "background sessions"],
5
20
  "senpi-codemode": ["detached eval cell", "detached eval cells"],
@@ -7,7 +22,14 @@ const WAKE_SOURCE_LABELS = {
7
22
  "loop-guard-hard-stop": ["loop-guard recovery pending", "loop-guard recoveries pending"],
8
23
  };
9
24
  export function initialHerdrState() {
10
- return { blocked: new Map(), turnActive: false, childCount: 0, monitorCount: 0, wakeSources: new Map() };
25
+ return {
26
+ blocked: new Map(),
27
+ turnActive: false,
28
+ childCount: 0,
29
+ monitorCount: 0,
30
+ openEndedMonitorCount: 0,
31
+ wakeSources: new Map(),
32
+ };
11
33
  }
12
34
  export function reduceHerdrState(state, event) {
13
35
  switch (event.type) {
@@ -16,7 +38,7 @@ export function reduceHerdrState(state, event) {
16
38
  case "children":
17
39
  return { ...state, childCount: event.count };
18
40
  case "monitors":
19
- return { ...state, monitorCount: event.count };
41
+ return { ...state, monitorCount: event.count, openEndedMonitorCount: Math.min(event.openEnded, event.count) };
20
42
  case "wake-source": {
21
43
  if ((state.wakeSources.get(event.source) ?? 0) === event.count)
22
44
  return state;
@@ -43,20 +65,26 @@ export function selectHerdrReport(state) {
43
65
  if (state.blocked.size > 0)
44
66
  return { state: "blocked", message: state.blocked.values().next().value?.label };
45
67
  const parts = [];
68
+ let bounded = false;
46
69
  const childCount = Math.max(state.childCount, state.wakeSources.get("senpi-task") ?? 0);
47
- if (childCount > 0)
70
+ if (childCount > 0) {
48
71
  parts.push(`${childCount} subagent${childCount === 1 ? "" : "s"} running`);
72
+ bounded = true;
73
+ }
49
74
  if (state.monitorCount > 0)
50
75
  parts.push(`${state.monitorCount} monitor${state.monitorCount === 1 ? "" : "s"} live`);
76
+ if (state.monitorCount > state.openEndedMonitorCount)
77
+ bounded = true;
51
78
  for (const [source, count] of [...state.wakeSources].sort(([left], [right]) => left.localeCompare(right))) {
52
79
  if (REPRESENTED_WAKE_SOURCES.has(source))
53
80
  continue;
54
81
  const label = WAKE_SOURCE_LABELS[source];
55
82
  parts.push(label === undefined ? `${count} ${source}` : `${count} ${count === 1 ? label[0] : label[1]}`);
83
+ if (!OPEN_ENDED_WAKE_SOURCES.has(source))
84
+ bounded = true;
56
85
  }
57
- return state.turnActive || parts.length > 0
58
- ? { state: "working", message: parts.length > 0 ? parts.join(" + ") : undefined }
59
- : { state: "idle" };
86
+ const reported = state.turnActive || bounded ? "working" : "idle";
87
+ return parts.length > 0 ? { state: reported, message: parts.join(" + ") } : { state: reported };
60
88
  }
61
89
  /** The kind of the block the report shows, the oldest open one, matching `selectHerdrReport`'s message. */
62
90
  export function selectHerdrBlockedKind(state) {
@@ -5,7 +5,7 @@ import { debuglog } from "node:util";
5
5
  import { APP_COMMAND } from "../../../../config.js";
6
6
  import { isTerminalMonitorStateEvent, isWakeSourceStateEvent, TERMINAL_MONITOR_STATE_EVENT, WAKE_SOURCE_STATE_EVENT, } from "../monitor-state-event.js";
7
7
  import { HerdrClient } from "./herdr-client.js";
8
- import { initialHerdrState, isHerdrBlockedEvent, reduceHerdrState, selectHerdrBlockedKind, selectHerdrReport, } from "./herdr-state.js";
8
+ import { initialHerdrState, isHerdrBlockedEvent, openEndedMonitors, reduceHerdrState, selectHerdrBlockedKind, selectHerdrReport, } from "./herdr-state.js";
9
9
  /**
10
10
  * Herdr keys prompt delivery and session restore on agent labels it knows, so the protocol agent stays `pi`.
11
11
  * The product command (`omo`, `senpi`, `pi`) travels in this display-only pane token instead (omo#12395).
@@ -130,7 +130,11 @@ export function createHerdrExtension(deps) {
130
130
  subscriptions.push(pi.events.on(TERMINAL_MONITOR_STATE_EVENT, (data) => {
131
131
  if (stopped || !isTerminalMonitorStateEvent(data))
132
132
  return;
133
- state = reduceHerdrState(state, { type: "monitors", count: data.activeCount });
133
+ state = reduceHerdrState(state, {
134
+ type: "monitors",
135
+ count: data.activeCount,
136
+ openEnded: openEndedMonitors(data),
137
+ });
134
138
  return publish();
135
139
  }));
136
140
  subscriptions.push(pi.events.on(WAKE_SOURCE_STATE_EVENT, (data) => {
@@ -76,7 +76,12 @@ export declare class McpService {
76
76
  isDisposed(): boolean;
77
77
  getConnection(name: string): ServerConnection | undefined;
78
78
  reconnectServer(name: string): Promise<void>;
79
- getServerSnapshots(): McpServerSnapshot[];
79
+ /**
80
+ * The lifecycle-only server snapshots. Without a session id these still list the merged config of all live
81
+ * sessions, as the /mcp command and the manager TUI expect. With one, only the servers that session declares
82
+ * (senpi#3001): a caller that can name its session gets no peer data.
83
+ */
84
+ getServerSnapshots(sessionId?: string): McpServerSnapshot[];
80
85
  getLogLines(name: string, maxLines: number): string[];
81
86
  getServerExposureStatus(name: string): Promise<McpServerExposureStatus>;
82
87
  recordCall(name: string, elapsedMs: number, failed: boolean): void;
@@ -85,6 +90,9 @@ export declare class McpService {
85
90
  * Return the attach-time inventory captured for one session. This is the
86
91
  * handoff consumed by RPC's session-owned adapter; it deliberately
87
92
  * does not expose or derive from the lifecycle-only server snapshots.
93
+ * A caller that passes no session id gets no peer data while more than
94
+ * one session is live (senpi#3001): with a single live session the stored
95
+ * snapshot is that session's own, so the id-less read keeps working.
88
96
  */
89
97
  getWireStatusSnapshot(sessionId?: string): McpWireStatusSnapshot;
90
98
  /** Route compatibility callers through the shared ownership-aware scanner. */
@@ -295,6 +295,14 @@ export class McpService {
295
295
  const releasedId = released?.sessionId;
296
296
  if (releasedId !== undefined && !live.some((binding) => binding.sessionId === releasedId)) {
297
297
  this.#wireStatusBySession.delete(releasedId);
298
+ // The id-less read falls back to #latestWireStatus once a single session is live, so rebind it to the
299
+ // remaining session's own snapshot (senpi#3001): without this, the released session's status outlives
300
+ // it whenever the release carries no reason and no re-sync re-captures.
301
+ const remaining = live.at(-1);
302
+ this.#latestWireStatus =
303
+ remaining?.sessionId !== undefined
304
+ ? (this.#wireStatusBySession.get(remaining.sessionId) ?? { servers: [] })
305
+ : { servers: [] };
298
306
  }
299
307
  const latest = live.at(-1);
300
308
  if (latest !== undefined) {
@@ -544,6 +552,10 @@ export class McpService {
544
552
  }
545
553
  /** Refresh and return the current session-owned MCP wire inventory. */
546
554
  async refreshWireStatusSnapshot(sessionId) {
555
+ // A caller that passes no session id gets no peer data while more than one session is live (senpi#3001);
556
+ // capturing here would store the most recently attached session's status for it to read back.
557
+ if (sessionId === undefined && this.#liveBindings().length > 1)
558
+ return { servers: [] };
547
559
  return this.#refreshWireStatus(sessionId, () => this.#statusOwner(sessionId));
548
560
  }
549
561
  /** `owner` is resolved when the queued capture runs, so it sees the bindings current at that point. */
@@ -617,7 +629,21 @@ export class McpService {
617
629
  }
618
630
  await reconnectMcpNow(entry.connection);
619
631
  }
620
- getServerSnapshots() {
632
+ /**
633
+ * The lifecycle-only server snapshots. Without a session id these still list the merged config of all live
634
+ * sessions, as the /mcp command and the manager TUI expect. With one, only the servers that session declares
635
+ * (senpi#3001): a caller that can name its session gets no peer data.
636
+ */
637
+ getServerSnapshots(sessionId) {
638
+ if (sessionId !== undefined) {
639
+ const owner = this.#statusOwner(sessionId);
640
+ if (owner === undefined)
641
+ return [];
642
+ const names = new Set(Object.keys(owner.config.servers));
643
+ return [...names]
644
+ .sort()
645
+ .map((name) => this.#serverSnapshot(name, owner.config.servers[name], this.#entryForName(name)));
646
+ }
621
647
  const names = new Set(Object.keys(this.#config?.servers ?? {}));
622
648
  for (const entry of this.#connections.values())
623
649
  names.add(entry.name);
@@ -658,8 +684,13 @@ export class McpService {
658
684
  * Return the attach-time inventory captured for one session. This is the
659
685
  * handoff consumed by RPC's session-owned adapter; it deliberately
660
686
  * does not expose or derive from the lifecycle-only server snapshots.
687
+ * A caller that passes no session id gets no peer data while more than
688
+ * one session is live (senpi#3001): with a single live session the stored
689
+ * snapshot is that session's own, so the id-less read keeps working.
661
690
  */
662
691
  getWireStatusSnapshot(sessionId) {
692
+ if (sessionId === undefined && this.#liveBindings().length > 1)
693
+ return { servers: [] };
663
694
  return sessionId === undefined
664
695
  ? this.#latestWireStatus
665
696
  : (this.#wireStatusBySession.get(sessionId) ?? { servers: [] });
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The remote half of an rpc watch: one `RpcRemoteSource` subscription for a `<device>/<session>` or
3
+ * `<device>/*` target. Records go through the watch's own render path with the device in the socket's
4
+ * place, so a remote line reads exactly like a local one. A resync after a gap reports each change once,
5
+ * a turn end is reported once per `turnId`, and the source's cursor rides the monitor checkpoint.
6
+ */
7
+ import type { RpcRecord } from "./rpc-monitor-observer.ts";
8
+ import { type RpcMonitorRenderer } from "./rpc-monitor-render.ts";
9
+ import type { RpcEvents } from "./rpc-monitor-targets.ts";
10
+ import type { RpcRemoteSource, RpcRemoteTarget } from "./rpc-remote-source.ts";
11
+ /** What a remote watch persists: the source's resume cursor and the turns already reported per session. */
12
+ export interface RpcRemoteCheckpoint {
13
+ readonly cursor?: string;
14
+ readonly settled: Readonly<Record<string, readonly string[]>>;
15
+ }
16
+ export interface RpcRemoteFeedHost {
17
+ readonly renderer: RpcMonitorRenderer;
18
+ readonly hosts: Map<string, {
19
+ instanceId: string;
20
+ generation: string;
21
+ }>;
22
+ record(socket: string, record: RpcRecord): void;
23
+ line(line: string): void;
24
+ checkpoint(): void;
25
+ ended(summary: string): void;
26
+ }
27
+ export interface RpcRemoteFeedOptions {
28
+ readonly events: RpcEvents;
29
+ readonly exclude: readonly string[];
30
+ readonly source: RpcRemoteSource;
31
+ readonly saved?: RpcRemoteCheckpoint;
32
+ }
33
+ export declare class RpcRemoteFeed {
34
+ #private;
35
+ constructor(target: RpcRemoteTarget, options: RpcRemoteFeedOptions, host: RpcRemoteFeedHost);
36
+ /** Opens the subscription; resolves to the arm-time rows. Callbacks before the open resolves wait for it. */
37
+ start(): Promise<string[]>;
38
+ stop(): void;
39
+ checkpoint(): RpcRemoteCheckpoint;
40
+ /** After a restore: the turns that settled while nothing watched, which the saved checkpoint had not seen. */
41
+ diffSettled(saved: RpcRemoteCheckpoint | undefined): string[];
42
+ }
43
+ //# sourceMappingURL=rpc-monitor-remote.d.ts.map
@@ -0,0 +1,158 @@
1
+ import { detachedLabel, sessionKey } from "./rpc-monitor-render.js";
2
+ const REMOTE_INSTANCE = "remote";
3
+ /** Turn ids remembered per session: enough to drop a resnapshot's repeats, bounded for a long watch. */
4
+ const SETTLED_KEPT = 16;
5
+ export class RpcRemoteFeed {
6
+ #target;
7
+ #options;
8
+ #host;
9
+ #settled = new Map();
10
+ #cursor;
11
+ #opened = [];
12
+ #subscription;
13
+ #lostReported = false;
14
+ #stopped = false;
15
+ constructor(target, options, host) {
16
+ this.#target = target;
17
+ this.#options = options;
18
+ this.#host = host;
19
+ for (const [key, turns] of Object.entries(options.saved?.settled ?? {}))
20
+ this.#settled.set(key, [...turns]);
21
+ this.#cursor = options.saved?.cursor;
22
+ }
23
+ /** Opens the subscription; resolves to the arm-time rows. Callbacks before the open resolves wait for it. */
24
+ async start() {
25
+ const pending = [];
26
+ let ready = false;
27
+ const run = (step) => {
28
+ if (this.#stopped)
29
+ return;
30
+ if (ready)
31
+ step();
32
+ else
33
+ pending.push(step);
34
+ };
35
+ const { events, exclude, source } = this.#options;
36
+ const subscription = await source.open(this.#target, { events, exclude, ...(this.#cursor === undefined ? {} : { resumeFrom: this.#cursor }) }, {
37
+ record: (record) => run(() => this.#record(record)),
38
+ resynced: (state) => run(() => this.#resync(state)),
39
+ cursor: (cursor) => run(() => {
40
+ this.#cursor = cursor;
41
+ this.#host.checkpoint();
42
+ }),
43
+ lost: (reason) => run(() => this.#lost(reason)),
44
+ ended: (reason) => run(() => this.#ended(reason)),
45
+ });
46
+ if (this.#stopped) {
47
+ subscription.stop();
48
+ return [];
49
+ }
50
+ this.#subscription = subscription;
51
+ const { initial } = subscription;
52
+ const device = this.#target.device;
53
+ this.#host.hosts.set(device, { instanceId: REMOTE_INSTANCE, generation: initial.generation ?? "" });
54
+ this.#opened = initial.settledTurns ?? [];
55
+ for (const turn of this.#opened)
56
+ this.#remember(turnKey(device, turn), turn.turnId);
57
+ if (initial.cursor !== undefined)
58
+ this.#cursor = initial.cursor;
59
+ const rows = this.#host.renderer.seed(device, initial.rows);
60
+ this.#host.checkpoint();
61
+ ready = true;
62
+ for (const step of pending.splice(0))
63
+ step();
64
+ return rows;
65
+ }
66
+ stop() {
67
+ this.#stopped = true;
68
+ this.#subscription?.stop();
69
+ this.#subscription = undefined;
70
+ }
71
+ checkpoint() {
72
+ const settled = Object.fromEntries([...this.#settled].map(([key, turns]) => [key, [...turns]]));
73
+ return this.#cursor === undefined ? { settled } : { cursor: this.#cursor, settled };
74
+ }
75
+ /** After a restore: the turns that settled while nothing watched, which the saved checkpoint had not seen. */
76
+ diffSettled(saved) {
77
+ return this.#opened
78
+ .filter((turn) => !saved?.settled[turnKey(this.#target.device, turn)]?.includes(turn.turnId))
79
+ .map((turn) => settledLine(turnKey(this.#target.device, turn), turn.turnId));
80
+ }
81
+ #record(record) {
82
+ const device = this.#target.device;
83
+ if (record.type !== "agent_settled" || typeof record.turnId !== "string") {
84
+ this.#host.record(device, record);
85
+ return;
86
+ }
87
+ const key = sessionKey(device, record);
88
+ if (this.#seen(key, record.turnId))
89
+ return;
90
+ this.#remember(key, record.turnId);
91
+ this.#host.record(device, record);
92
+ this.#host.checkpoint();
93
+ }
94
+ #resync(state) {
95
+ const device = this.#target.device;
96
+ const lines = [];
97
+ const host = this.#host.hosts.get(device);
98
+ if (state.generation !== undefined) {
99
+ if (host !== undefined && host.generation !== state.generation)
100
+ lines.push(`rpc host ${device} restarted (generation ${host.generation} -> ${state.generation})`);
101
+ this.#host.hosts.set(device, { instanceId: REMOTE_INSTANCE, generation: state.generation });
102
+ }
103
+ const before = new Map([...this.#host.renderer.statuses()].filter(([key]) => key.startsWith(`${device}#`)));
104
+ const now = new Map(state.rows.map((row) => [sessionKey(device, row), String(row.status)]));
105
+ for (const [key, status] of now) {
106
+ const was = before.get(key) ?? "unknown";
107
+ if (was !== status)
108
+ lines.push(`rpc ${detachedLabel(key)} status ${was} -> ${status} (changed while detached)`);
109
+ }
110
+ for (const key of before.keys())
111
+ if (!now.has(key))
112
+ lines.push(`rpc ${detachedLabel(key)} closed (gone while detached)`);
113
+ for (const turn of state.settledTurns ?? []) {
114
+ const key = turnKey(device, turn);
115
+ if (this.#seen(key, turn.turnId))
116
+ continue;
117
+ this.#remember(key, turn.turnId);
118
+ lines.push(settledLine(key, turn.turnId));
119
+ }
120
+ this.#host.renderer.forgetSocket(device);
121
+ this.#host.renderer.seed(device, state.rows);
122
+ if (state.cursor !== undefined)
123
+ this.#cursor = state.cursor;
124
+ this.#lostReported = false;
125
+ for (const line of lines)
126
+ this.#host.line(line);
127
+ this.#host.checkpoint();
128
+ }
129
+ #lost(reason) {
130
+ if (this.#lostReported)
131
+ return;
132
+ this.#lostReported = true;
133
+ this.#host.line(`rpc device ${this.#target.device} unreachable (${reason}); re-watching`);
134
+ }
135
+ #ended(reason) {
136
+ this.#host.line(`rpc ${this.#target.device}/${this.#target.session} access ended (${reason})`);
137
+ this.#host.ended(`access ended (${reason})`);
138
+ }
139
+ #seen(key, turnId) {
140
+ return this.#settled.get(key)?.includes(turnId) === true;
141
+ }
142
+ #remember(key, turnId) {
143
+ const turns = this.#settled.get(key) ?? [];
144
+ if (turns.includes(turnId))
145
+ return;
146
+ turns.push(turnId);
147
+ if (turns.length > SETTLED_KEPT)
148
+ turns.shift();
149
+ this.#settled.set(key, turns);
150
+ }
151
+ }
152
+ function turnKey(device, turn) {
153
+ return `${device}#${turn.sessionId}`;
154
+ }
155
+ function settledLine(key, turnId) {
156
+ return `rpc ${detachedLabel(key)} turn ${turnId} settled while detached`;
157
+ }
158
+ //# sourceMappingURL=rpc-monitor-remote.js.map
@@ -19,4 +19,6 @@ export declare class RpcMonitorRenderer {
19
19
  render(socket: string, record: RpcRecord): string | undefined;
20
20
  }
21
21
  export declare function sessionKey(socket: string, record: RpcRecord): string;
22
+ /** How a line about a session nobody watched names it: the first 8 characters of its key's session id. */
23
+ export declare function detachedLabel(key: string): string;
22
24
  //# sourceMappingURL=rpc-monitor-render.d.ts.map
@@ -102,6 +102,10 @@ export class RpcMonitorRenderer {
102
102
  export function sessionKey(socket, record) {
103
103
  return `${socket}#${String(record.sessionId)}`;
104
104
  }
105
+ /** How a line about a session nobody watched names it: the first 8 characters of its key's session id. */
106
+ export function detachedLabel(key) {
107
+ return key.slice(key.lastIndexOf("#") + 1).slice(0, 8);
108
+ }
105
109
  function labelOf(record) {
106
110
  const task = contextTask(record);
107
111
  if (task !== undefined)
@@ -4,9 +4,10 @@
4
4
  * file watches. A paused watch counts the lines it drops; a line identical to the previous one is
5
5
  * dropped (consecutive dedupe), on top of the notifier's same-batch rule.
6
6
  */
7
- import { type MonitorRecordBase, type MonitorSourceHost } from "./monitor-types.ts";
7
+ import { type MonitorEndedReason, type MonitorRecordBase, type MonitorSourceHost } from "./monitor-types.ts";
8
8
  import type { RpcEvents, RpcTarget } from "./rpc-monitor-targets.ts";
9
9
  import { type RpcMonitorCheckpoint, RpcMonitorWatch } from "./rpc-monitor-watch.ts";
10
+ import type { RpcRemoteSource } from "./rpc-remote-source.ts";
10
11
  export interface RegisterRpcMonitorOptions {
11
12
  readonly description: string;
12
13
  readonly target: RpcTarget;
@@ -22,6 +23,8 @@ export interface RegisterRpcMonitorOptions {
22
23
  readonly agentDir: string;
23
24
  /** Endpoint discovery override (tests); defaults to the endpoints registered under `agentDir`. */
24
25
  readonly endpoints?: () => Promise<readonly string[]>;
26
+ /** Remote source override (tests); defaults to the one an extension registered. */
27
+ readonly remoteSource?: RpcRemoteSource;
25
28
  /** A restore's saved view: the lines for what changed are injected once the watch has joined. */
26
29
  readonly restoredFrom?: RpcMonitorCheckpoint;
27
30
  readonly onCheckpoint?: (monitorId: string, checkpoint: RpcMonitorCheckpoint) => void;
@@ -46,7 +49,7 @@ export declare class RpcMonitorSource {
46
49
  rows: string[];
47
50
  }>;
48
51
  /** Ends one watch; false when the id is not a live rpc watch. */
49
- settle(id: string, summary: string): boolean;
52
+ settle(id: string, summary: string, reason?: MonitorEndedReason): boolean;
50
53
  settleAll(summary: string): number;
51
54
  }
52
55
  //# sourceMappingURL=rpc-monitor-source.d.ts.map
@@ -4,7 +4,7 @@
4
4
  * file watches. A paused watch counts the lines it drops; a line identical to the previous one is
5
5
  * dropped (consecutive dedupe), on top of the notifier's same-batch rule.
6
6
  */
7
- import { allocateMonitorId } from "./monitor-types.js";
7
+ import { allocateMonitorId, } from "./monitor-types.js";
8
8
  import { RpcMonitorWatch } from "./rpc-monitor-watch.js";
9
9
  /**
10
10
  * The summary an RPC watch settles with when its own deadline fires. An RPC watch observes
@@ -38,6 +38,9 @@ export class RpcMonitorSource {
38
38
  events: options.events,
39
39
  exclude: options.exclude,
40
40
  ...(options.endpoints === undefined ? {} : { endpoints: options.endpoints }),
41
+ ...(options.remoteSource === undefined ? {} : { remoteSource: options.remoteSource }),
42
+ ...(options.restoredFrom === undefined ? {} : { restoredFrom: options.restoredFrom }),
43
+ ended: (summary) => this.settle(id, summary, "exit"),
41
44
  line: (line) => this.#line(id, line),
42
45
  ...(options.onCheckpoint === undefined
43
46
  ? {}
@@ -67,14 +70,21 @@ export class RpcMonitorSource {
67
70
  };
68
71
  this.#records.set(id, record);
69
72
  this.#host.changed();
70
- const rows = await watch.start();
73
+ const rows = await watch.start().catch((error) => {
74
+ // Only a remote open rejects: the watch never ran, so it leaves without a summary or an ended event.
75
+ watch.stop();
76
+ clearTimeout(record.deadline);
77
+ this.#records.delete(id);
78
+ this.#host.changed();
79
+ throw error;
80
+ });
71
81
  if (options.restoredFrom !== undefined)
72
82
  for (const line of watch.diffSince(options.restoredFrom))
73
83
  this.#line(id, line);
74
84
  return { id, monitorId, rows };
75
85
  }
76
86
  /** Ends one watch; false when the id is not a live rpc watch. */
77
- settle(id, summary) {
87
+ settle(id, summary, reason) {
78
88
  const record = this.#records.get(id);
79
89
  if (record === undefined || record.settled)
80
90
  return false;
@@ -85,7 +95,12 @@ export class RpcMonitorSource {
85
95
  this.#host.changed();
86
96
  if (summary !== "watcher disposed")
87
97
  this.#host.emit(record, { type: "summary", id, description: record.description, summary });
88
- this.#host.ended(record, summary === RPC_WATCHER_TIMED_OUT_SUMMARY ? "timeout" : summary === "watcher disposed" ? "disposed" : "killed", null);
98
+ this.#host.ended(record, reason ??
99
+ (summary === RPC_WATCHER_TIMED_OUT_SUMMARY
100
+ ? "timeout"
101
+ : summary === "watcher disposed"
102
+ ? "disposed"
103
+ : "killed"), null);
89
104
  return true;
90
105
  }
91
106
  settleAll(summary) {
@@ -10,6 +10,10 @@ export type RpcTarget = {
10
10
  } | {
11
11
  readonly kind: "task";
12
12
  readonly taskId: string;
13
+ } | {
14
+ readonly kind: "remote";
15
+ readonly device: string;
16
+ readonly session: string;
13
17
  };
14
18
  export type RpcEvents = "all" | readonly string[];
15
19
  export declare function parseRpcTarget(target: string, selfDurableId: string | undefined): RpcTarget | {
@@ -7,6 +7,10 @@
7
7
  * makes the observer an attachment.
8
8
  */
9
9
  import { listHostEndpoints } from "../../../../modes/rpc/host-endpoints.js";
10
+ import { currentRpcRemoteSource } from "./rpc-remote-source.js";
11
+ // `<device>/<session>` or `<device>/*`: one `/`, no leading `/`, not a `.jsonl` path, not a drive letter.
12
+ const REMOTE_TARGET = /^(?<device>[^/]+)\/(?<session>[^/]+)$/;
13
+ const DRIVE_LETTER = /^[A-Za-z]:$/;
10
14
  const STATUS_KINDS = new Set(["session_status", "effect_status"]);
11
15
  export function parseRpcTarget(target, selfDurableId) {
12
16
  const trimmed = target.trim();
@@ -23,8 +27,19 @@ export function parseRpcTarget(target, selfDurableId) {
23
27
  return { kind: "all" };
24
28
  if (trimmed.startsWith("st_"))
25
29
  return { kind: "task", taskId: trimmed };
30
+ const remote = REMOTE_TARGET.exec(trimmed)?.groups;
31
+ if (remote?.device !== undefined && remote.session !== undefined && isRemoteShape(trimmed, remote.device)) {
32
+ if (currentRpcRemoteSource() === undefined)
33
+ return {
34
+ error: `target "${trimmed}" names a session on another device; remote targets need the omo remote surface`,
35
+ };
36
+ return { kind: "remote", device: remote.device, session: remote.session };
37
+ }
26
38
  return { kind: "id", id: trimmed };
27
39
  }
40
+ function isRemoteShape(target, device) {
41
+ return !target.endsWith(".jsonl") && !DRIVE_LETTER.test(device);
42
+ }
28
43
  /** `["session_status", "effect_status"]` for children, `["session_status"]` otherwise. */
29
44
  export function defaultEvents(target) {
30
45
  return target.kind === "children" ? ["session_status", "effect_status"] : ["session_status"];
@@ -1,4 +1,6 @@
1
+ import { type RpcRemoteCheckpoint } from "./rpc-monitor-remote.ts";
1
2
  import { type RpcEvents, type RpcTarget } from "./rpc-monitor-targets.ts";
3
+ import { type RpcRemoteSource } from "./rpc-remote-source.ts";
2
4
  export declare const RPC_RESCAN_MS = 30000;
3
5
  /** What a restore compares against: each endpoint's host identity and each session's last status. */
4
6
  export interface RpcMonitorCheckpoint {
@@ -7,6 +9,8 @@ export interface RpcMonitorCheckpoint {
7
9
  readonly generation: string;
8
10
  }>>;
9
11
  readonly statuses: Readonly<Record<string, string>>;
12
+ /** A remote target's resume position (absent for local targets). */
13
+ readonly remote?: RpcRemoteCheckpoint;
10
14
  }
11
15
  export interface RpcWatchOptions {
12
16
  readonly agentDir: string;
@@ -19,6 +23,12 @@ export interface RpcWatchOptions {
19
23
  readonly line: (line: string) => void;
20
24
  /** The watch's view changed (a host joined, a status moved): persist it. */
21
25
  readonly checkpoint?: (checkpoint: RpcMonitorCheckpoint) => void;
26
+ /** The source serving a remote target; defaults to the one an extension registered. */
27
+ readonly remoteSource?: RpcRemoteSource;
28
+ /** A restore's saved view: a remote target resumes from its cursor. */
29
+ readonly restoredFrom?: RpcMonitorCheckpoint;
30
+ /** A remote target's access ended for good: the monitor settles with this summary. */
31
+ readonly ended?: (summary: string) => void;
22
32
  }
23
33
  export declare class RpcMonitorWatch {
24
34
  #private;