@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.
- package/CHANGELOG.md +35 -0
- package/dist/bundle/chunks/{chunk-5MWHZH2O.js → chunk-2FDW2LQL.js} +1 -1
- package/dist/bundle/chunks/{chunk-GG2JO3CL.js → chunk-4E7GDG4H.js} +1 -1
- package/dist/bundle/chunks/{chunk-K5JLYACP.js → chunk-4GKU755E.js} +1 -1
- package/dist/bundle/chunks/{chunk-437VLZQZ.js → chunk-7III44PV.js} +1 -1
- package/dist/bundle/chunks/chunk-7PSBD5KS.js +9 -0
- package/dist/bundle/chunks/{chunk-KFWHLVVH.js → chunk-C7OKUC7L.js} +1 -1
- package/dist/bundle/chunks/{chunk-CX32K5KD.js → chunk-CF5OGBC4.js} +7 -7
- package/dist/bundle/chunks/{chunk-7TGZJH6Z.js → chunk-D5MRJWUK.js} +40 -39
- package/dist/bundle/chunks/{chunk-E6L5VZSM.js → chunk-DSWDJ7HT.js} +1 -1
- package/dist/bundle/chunks/{chunk-6NMTFURP.js → chunk-ECW7VBMB.js} +1 -1
- package/dist/bundle/chunks/{chunk-2CSLJLM6.js → chunk-GKAYPJ4E.js} +2 -2
- package/dist/bundle/chunks/{chunk-KCGUUIUS.js → chunk-GNSMNSL7.js} +1 -1
- package/dist/bundle/chunks/{chunk-V3JSQ3PG.js → chunk-GSKCWXXG.js} +1 -1
- package/dist/bundle/chunks/{chunk-USKWGPA6.js → chunk-GTJJ73DE.js} +1 -1
- package/dist/bundle/chunks/{chunk-HHSYTDKO.js → chunk-HLM46XEB.js} +2 -2
- package/dist/bundle/chunks/{chunk-A4F4J2KH.js → chunk-I3PHVJAS.js} +2 -2
- package/dist/bundle/chunks/{chunk-LAMRXFHZ.js → chunk-LNPAT22L.js} +1 -1
- package/dist/bundle/chunks/chunk-NH7SYIGN.js +2 -0
- package/dist/bundle/chunks/{chunk-ZIIDIVTG.js → chunk-R2Z7CX77.js} +1 -1
- package/dist/bundle/chunks/{chunk-32I42CEY.js → chunk-RQ6CHXQ2.js} +1 -1
- package/dist/bundle/chunks/chunk-SJI6UB23.js +2 -0
- package/dist/bundle/chunks/{chunk-KUIEHAPR.js → chunk-T2SMQPRE.js} +1 -1
- package/dist/bundle/chunks/{chunk-IPG434JT.js → chunk-TIPKAMSS.js} +1 -1
- package/dist/bundle/chunks/{chunk-RI5ABX7A.js → chunk-TMXRR3N7.js} +2 -2
- package/dist/bundle/chunks/chunk-VD7AKTGP.js +3 -0
- package/dist/bundle/chunks/{chunk-22JICYKS.js → chunk-WAOGBBZR.js} +1 -1
- package/dist/bundle/chunks/{chunk-6MHCFJDB.js → chunk-WGRKM3N6.js} +2 -2
- package/dist/bundle/chunks/{chunk-WF5R2VSP.js → chunk-X5ZVYNOL.js} +1 -1
- package/dist/bundle/chunks/{chunk-H2FBGZNP.js → chunk-XCMII2VQ.js} +2 -2
- package/dist/bundle/chunks/{chunk-5JUCTKYS.js → chunk-YK3ZZCHI.js} +1 -1
- package/dist/bundle/chunks/{cli-main-ZQBSX5H5.js → cli-main-T74YJGIF.js} +1 -1
- package/dist/bundle/chunks/{hand-entry-7U7354GG.js → hand-entry-AYRUGE2A.js} +2 -2
- package/dist/bundle/chunks/{headless-host-7LKTG76U.js → headless-host-ES74ZB6M.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-NLSWH2CA.js → help-fast-path-TMTGLOBG.js} +1 -1
- package/dist/bundle/chunks/{host-command-3YRFN35E.js → host-command-YRXZE4TV.js} +1 -1
- package/dist/bundle/chunks/{host-lifecycle-6XLVIECJ.js → host-lifecycle-6VCXMPDW.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle.js +1 -1
- package/dist/bundle/chunks/{host-session-command-F7USXUZV.js → host-session-command-N7IN7ZDV.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-4RF3PUQ2.js → interactive-mode-B5SQDLH7.js} +1 -1
- package/dist/bundle/chunks/{list-tips-H6VGDFTS.js → list-tips-DU57HHKA.js} +1 -1
- package/dist/bundle/chunks/{main-G2SRZYQN.js → main-UQFVFCNY.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-RPN54GUC.js → multi-session-host-XSUJDHFZ.js} +5 -5
- package/dist/bundle/chunks/{openai-codex-responses-MJZKUS6I.js → openai-codex-responses-7V6LTDRU.js} +2 -2
- package/dist/bundle/chunks/{package-manager-cli-D2VFD463.js → package-manager-cli-GLUNPQJU.js} +1 -1
- package/dist/bundle/chunks/{role-37WVG42T.js → role-RRFFHZF5.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-QELPMM4N.js → rotation-stream-BN2YK2MC.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-P4D3DVYD.js → rpc-mode-JYN3E7P7.js} +1 -1
- package/dist/bundle/chunks/rpc-monitor-source-C6VQ3BC4.js +3 -0
- package/dist/bundle/chunks/{run-attached-tui-CGA6O5FR.js → run-attached-tui-W236SEOR.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-XWSNOAGG.js → schedule-command-B2EJTBOE.js} +1 -1
- package/dist/bundle/chunks/session-control-endpoint-YIMLHGLG.js +3 -0
- package/dist/bundle/chunks/{session-picker-OA2AUVZG.js → session-picker-E6ZONUJB.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +294 -292
- package/dist/bundle/cli.js +2 -2
- package/dist/bundle/host-supervisor.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/cli/project-trust-headless.d.ts +13 -0
- package/dist/cli/project-trust-headless.js +35 -0
- package/dist/cli/project-trust.js +13 -50
- package/dist/cli/schedule-delivery.d.ts +4 -0
- package/dist/cli/schedule-delivery.js +6 -0
- package/dist/cli/schedule-runner.js +8 -0
- package/dist/cli/session-opening.d.ts +19 -0
- package/dist/cli/session-opening.js +47 -1
- package/dist/core/agent-session-runtime.d.ts +12 -0
- package/dist/core/agent-session-runtime.js +41 -0
- package/dist/core/agent-session.js +13 -22
- package/dist/core/egress/api-table.d.ts +121 -0
- package/dist/core/egress/api-table.js +41 -0
- package/dist/core/egress/classify.d.ts +19 -0
- package/dist/core/egress/classify.js +197 -0
- package/dist/core/egress/credential-fields.d.ts +18 -0
- package/dist/core/egress/credential-fields.js +83 -0
- package/dist/core/egress/custody-sidecar.d.ts +44 -0
- package/dist/core/egress/custody-sidecar.js +91 -0
- package/dist/core/egress/device-custody.d.ts +12 -0
- package/dist/core/egress/device-custody.js +82 -0
- package/dist/core/egress/endpoint.d.ts +33 -0
- package/dist/core/egress/endpoint.js +115 -0
- package/dist/core/execute-tool-on-host.d.ts +26 -0
- package/dist/core/execute-tool-on-host.js +96 -0
- package/dist/core/extensions/builtin/compaction/retained-message-safety.js +15 -0
- package/dist/core/extensions/builtin/goal/index.js +15 -3
- package/dist/core/extensions/builtin/goal/stop-pause.d.ts +2 -0
- package/dist/core/extensions/builtin/goal/stop-pause.js +5 -0
- package/dist/core/extensions/builtin/herdr/herdr-state.d.ts +10 -0
- package/dist/core/extensions/builtin/herdr/herdr-state.js +34 -6
- package/dist/core/extensions/builtin/herdr/index.js +6 -2
- package/dist/core/extensions/builtin/mcp/service.d.ts +9 -1
- package/dist/core/extensions/builtin/mcp/service.js +32 -1
- package/dist/core/extensions/builtin/terminal/rpc-monitor-remote.d.ts +43 -0
- package/dist/core/extensions/builtin/terminal/rpc-monitor-remote.js +158 -0
- package/dist/core/extensions/builtin/terminal/rpc-monitor-render.d.ts +2 -0
- package/dist/core/extensions/builtin/terminal/rpc-monitor-render.js +4 -0
- package/dist/core/extensions/builtin/terminal/rpc-monitor-source.d.ts +5 -2
- package/dist/core/extensions/builtin/terminal/rpc-monitor-source.js +19 -4
- package/dist/core/extensions/builtin/terminal/rpc-monitor-targets.d.ts +4 -0
- package/dist/core/extensions/builtin/terminal/rpc-monitor-targets.js +15 -0
- package/dist/core/extensions/builtin/terminal/rpc-monitor-watch.d.ts +10 -0
- package/dist/core/extensions/builtin/terminal/rpc-monitor-watch.js +28 -2
- package/dist/core/extensions/builtin/terminal/rpc-remote-source.d.ts +59 -0
- package/dist/core/extensions/builtin/terminal/rpc-remote-source.js +15 -0
- package/dist/core/extensions/builtin/terminal/terminal-manifest-model.d.ts +5 -0
- package/dist/core/extensions/builtin/terminal/terminal-manifest-parse.js +14 -1
- package/dist/core/extensions/builtin/terminal/tools/monitor-rpc.js +8 -1
- package/dist/core/extensions/loader.js +8 -0
- package/dist/core/extensions/types.d.ts +6 -0
- package/dist/core/foreign-session-holders.d.ts +1 -1
- package/dist/core/foreign-session-holders.js +3 -3
- package/dist/core/session-import/jsonl-import.d.ts +8 -12
- package/dist/core/session-import/jsonl-import.js +99 -40
- package/dist/core/session-manager.d.ts +2 -0
- package/dist/core/session-manager.js +7 -2
- package/dist/core/session-store/durable-binding.d.ts +2 -0
- package/dist/core/session-store/durable-binding.js +26 -4
- package/dist/core/settings-manager.d.ts +20 -0
- package/dist/core/settings-manager.js +51 -0
- package/dist/core/trust-manager.d.ts +1 -18
- package/dist/core/trust-manager.js +3 -124
- package/dist/core/trust-requiring-resources.d.ts +21 -0
- package/dist/core/trust-requiring-resources.js +189 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/main.js +42 -12
- package/dist/modes/interactive/interactive-mode.js +2 -2
- package/dist/modes/interactive/session-control-commands.js +2 -0
- package/dist/modes/rpc/connection-handler.js +17 -0
- package/dist/modes/rpc/connection-question-bridge.js +7 -4
- package/dist/modes/rpc/effect-commands.d.ts +24 -0
- package/dist/modes/rpc/host-boot-extensions.d.ts +4 -0
- package/dist/modes/rpc/host-boot-extensions.js +17 -0
- package/dist/modes/rpc/host-lifecycle-shutdown.js +11 -2
- package/dist/modes/rpc/loopless-runtime-settings.js +5 -1
- package/dist/modes/rpc/method-classes-schema-session.d.ts +33 -0
- package/dist/modes/rpc/method-classes-schema-session.js +4 -0
- package/dist/modes/rpc/method-classes-schema.d.ts +33 -0
- package/dist/modes/rpc/method-classes.d.ts +4 -0
- package/dist/modes/rpc/method-classes.js +7 -0
- package/dist/modes/rpc/multi-session-host.d.ts +2 -0
- package/dist/modes/rpc/multi-session-host.js +7 -2
- package/dist/modes/rpc/project-workspace-commands.d.ts +60 -0
- package/dist/modes/rpc/project-workspace-commands.js +145 -0
- package/dist/modes/rpc/rpc-error-types.d.ts +11 -1
- package/dist/modes/rpc/rpc-error-types.js +10 -0
- package/dist/modes/rpc/rpc-input-validation.js +4 -0
- package/dist/modes/rpc/rpc-mutation-response-types.d.ts +13 -0
- package/dist/modes/rpc/rpc-read-response-types.d.ts +13 -0
- package/dist/modes/rpc/rpc-session-command-types.d.ts +15 -0
- package/dist/modes/rpc/session-attachment-gate.js +2 -0
- package/dist/modes/rpc/session-command-activity.js +2 -0
- package/docs/engine-socket/fixtures/admission-routes/h-legacy.json +1 -1
- package/docs/engine-socket/fixtures/rpc-inbound-samples.json +41 -2
- package/docs/extensions.md +17 -1
- package/docs/rpc.md +121 -4
- package/docs/terminal-tools.md +8 -0
- package/native/socket-secret/prebuilds/win32-arm64/senpi_socket_secret.win32-arm64.node +0 -0
- package/native/socket-secret/prebuilds/win32-x64/senpi_socket_secret.win32-x64.node +0 -0
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-FUQJK2TU.js +0 -2
- package/dist/bundle/chunks/chunk-POZMRM6V.js +0 -2
- package/dist/bundle/chunks/chunk-XBG63MJZ.js +0 -8
- package/dist/bundle/chunks/chunk-YP47G4YP.js +0 -3
- package/dist/bundle/chunks/rpc-monitor-source-KKN2UP4T.js +0 -3
- 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 {
|
|
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
|
-
|
|
58
|
-
|
|
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, {
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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;
|