@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
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { RpcMonitorCoalescer } from "./rpc-monitor-coalesce.js";
|
|
8
8
|
import { openRpcObserver } from "./rpc-monitor-observer.js";
|
|
9
|
-
import {
|
|
9
|
+
import { RpcRemoteFeed } from "./rpc-monitor-remote.js";
|
|
10
|
+
import { detachedLabel, RpcMonitorRenderer, sessionKey } from "./rpc-monitor-render.js";
|
|
10
11
|
import { endpointSockets, matchesTask, subscriptionFor, wantsRecord, } from "./rpc-monitor-targets.js";
|
|
12
|
+
import { currentRpcRemoteSource } from "./rpc-remote-source.js";
|
|
11
13
|
export const RPC_RESCAN_MS = 30_000;
|
|
12
14
|
export class RpcMonitorWatch {
|
|
13
15
|
#options;
|
|
@@ -17,6 +19,7 @@ export class RpcMonitorWatch {
|
|
|
17
19
|
#coalescer;
|
|
18
20
|
#taskSessions = new Set();
|
|
19
21
|
#rescan;
|
|
22
|
+
#remote;
|
|
20
23
|
#stopped = false;
|
|
21
24
|
constructor(options) {
|
|
22
25
|
this.#options = options;
|
|
@@ -24,6 +27,9 @@ export class RpcMonitorWatch {
|
|
|
24
27
|
}
|
|
25
28
|
/** Joins every endpoint now; resolves to the arm-time RESULT rows (one per matched session). */
|
|
26
29
|
async start() {
|
|
30
|
+
const { target } = this.#options;
|
|
31
|
+
if (target.kind === "remote")
|
|
32
|
+
return this.#startRemote(target.device, target.session);
|
|
27
33
|
const rows = await this.#join();
|
|
28
34
|
this.#rescan = setInterval(() => void this.#join(), RPC_RESCAN_MS);
|
|
29
35
|
this.#rescan.unref?.();
|
|
@@ -34,6 +40,7 @@ export class RpcMonitorWatch {
|
|
|
34
40
|
return {
|
|
35
41
|
hosts: Object.fromEntries(this.#hosts),
|
|
36
42
|
statuses: Object.fromEntries(this.#renderer.statuses()),
|
|
43
|
+
...(this.#remote === undefined ? {} : { remote: this.#remote.checkpoint() }),
|
|
37
44
|
};
|
|
38
45
|
}
|
|
39
46
|
/** Lines for what changed between a saved checkpoint and what this watch sees now. */
|
|
@@ -47,7 +54,7 @@ export class RpcMonitorWatch {
|
|
|
47
54
|
}
|
|
48
55
|
const now = this.#renderer.statuses();
|
|
49
56
|
for (const [key, status] of Object.entries(saved.statuses)) {
|
|
50
|
-
const label =
|
|
57
|
+
const label = detachedLabel(key);
|
|
51
58
|
const current = now.get(key);
|
|
52
59
|
// Only a host that answered can say a session is gone; otherwise it may be alive but unreachable
|
|
53
60
|
// or no longer matched by the re-resolved target.
|
|
@@ -61,6 +68,8 @@ export class RpcMonitorWatch {
|
|
|
61
68
|
else if (current !== status)
|
|
62
69
|
lines.push(`rpc ${label} status ${status} -> ${current} (changed while detached)`);
|
|
63
70
|
}
|
|
71
|
+
if (this.#remote !== undefined)
|
|
72
|
+
lines.push(...this.#remote.diffSettled(saved.remote));
|
|
64
73
|
return lines;
|
|
65
74
|
}
|
|
66
75
|
stop() {
|
|
@@ -71,6 +80,23 @@ export class RpcMonitorWatch {
|
|
|
71
80
|
for (const observer of this.#observers.values())
|
|
72
81
|
observer.close();
|
|
73
82
|
this.#observers.clear();
|
|
83
|
+
this.#remote?.stop();
|
|
84
|
+
}
|
|
85
|
+
/** A remote target is served by the registered source instead of endpoint sockets; render is shared. */
|
|
86
|
+
#startRemote(device, session) {
|
|
87
|
+
const { events, exclude, restoredFrom } = this.#options;
|
|
88
|
+
const source = this.#options.remoteSource ?? currentRpcRemoteSource();
|
|
89
|
+
if (source === undefined)
|
|
90
|
+
return Promise.reject(new Error("remote targets need the omo remote surface; no remote source is registered"));
|
|
91
|
+
this.#remote = new RpcRemoteFeed({ device, session }, { events, exclude, source, ...(restoredFrom?.remote === undefined ? {} : { saved: restoredFrom.remote }) }, {
|
|
92
|
+
renderer: this.#renderer,
|
|
93
|
+
hosts: this.#hosts,
|
|
94
|
+
record: (socket, record) => this.#record(socket, record),
|
|
95
|
+
line: (line) => this.#options.line(line),
|
|
96
|
+
checkpoint: () => this.#options.checkpoint?.(this.checkpoint()),
|
|
97
|
+
ended: (summary) => this.#options.ended?.(summary),
|
|
98
|
+
});
|
|
99
|
+
return this.#remote.start();
|
|
74
100
|
}
|
|
75
101
|
async #join() {
|
|
76
102
|
const list = this.#options.endpoints ?? (() => endpointSockets(this.#options.agentDir));
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The seam an extension uses to serve rpc watches on another device's sessions (`<device>/<session>`,
|
|
3
|
+
* `<device>/*`). senpi knows no relay: it hands the source the target and four callbacks, renders what
|
|
4
|
+
* comes back with the local renderer, and keeps the source's resume position in the monitor checkpoint.
|
|
5
|
+
* This module is light on purpose: target parsing reads it on every arm, local or not.
|
|
6
|
+
*/
|
|
7
|
+
import type { RpcRecord } from "./rpc-monitor-observer.ts";
|
|
8
|
+
import type { RpcEvents } from "./rpc-monitor-targets.ts";
|
|
9
|
+
/** A device id or label from the account roster, and a session id or alias on it; `"*"` is every session. */
|
|
10
|
+
export interface RpcRemoteTarget {
|
|
11
|
+
readonly device: string;
|
|
12
|
+
readonly session: string;
|
|
13
|
+
}
|
|
14
|
+
/** A turn that settled, named by the `turnId` its `agent_settled` carries. */
|
|
15
|
+
export interface RpcRemoteSettledTurn {
|
|
16
|
+
readonly sessionId: string;
|
|
17
|
+
readonly turnId: string;
|
|
18
|
+
}
|
|
19
|
+
/** The watched sessions as they are now, and what settled since the source's last position. */
|
|
20
|
+
export interface RpcRemoteState {
|
|
21
|
+
/** One row per session, shaped like a local subscription snapshot row (`sessionId`, `status`, ...). */
|
|
22
|
+
readonly rows: readonly RpcRecord[];
|
|
23
|
+
/** Turns that settled since the resume position; turns the watch already reported are skipped. */
|
|
24
|
+
readonly settledTurns?: readonly RpcRemoteSettledTurn[];
|
|
25
|
+
/** The remote engine's identity; a change reads as that host restarting. */
|
|
26
|
+
readonly generation?: string;
|
|
27
|
+
/** Where the source would resume after a caller restart: opaque to senpi, saved as written. */
|
|
28
|
+
readonly cursor?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface RpcRemoteHandlers {
|
|
31
|
+
/** One live record, shaped like a local rpc record (`type`, `sessionId`, ...). */
|
|
32
|
+
record(record: RpcRecord): void;
|
|
33
|
+
/** The source re-synced after a gap (reconnect, re-attach, engine restart): the state now. */
|
|
34
|
+
resynced(state: RpcRemoteState): void;
|
|
35
|
+
cursor(cursor: string): void;
|
|
36
|
+
/** The device stopped answering; the source keeps trying and resyncs when it is back. */
|
|
37
|
+
lost(reason: string): void;
|
|
38
|
+
/** The watch can never continue (access revoked, plan ended): the monitor settles. */
|
|
39
|
+
ended(reason: string): void;
|
|
40
|
+
}
|
|
41
|
+
export interface RpcRemoteOpenOptions {
|
|
42
|
+
readonly events: RpcEvents;
|
|
43
|
+
readonly exclude: readonly string[];
|
|
44
|
+
/** The cursor a restored watch saved; absent on a fresh arm. */
|
|
45
|
+
readonly resumeFrom?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface RpcRemoteSubscription {
|
|
48
|
+
/** The state at open: its rows are the arm-time listing. */
|
|
49
|
+
readonly initial: RpcRemoteState;
|
|
50
|
+
stop(): void;
|
|
51
|
+
}
|
|
52
|
+
export interface RpcRemoteSource {
|
|
53
|
+
/** Resolves once the target is watched; rejects with a typed reason when it cannot be. */
|
|
54
|
+
open(target: RpcRemoteTarget, options: RpcRemoteOpenOptions, handlers: RpcRemoteHandlers): Promise<RpcRemoteSubscription>;
|
|
55
|
+
}
|
|
56
|
+
/** Registers the source remote targets use; the latest registration wins. Returns its unregister. */
|
|
57
|
+
export declare function registerRpcRemoteSource(source: RpcRemoteSource): () => void;
|
|
58
|
+
export declare function currentRpcRemoteSource(): RpcRemoteSource | undefined;
|
|
59
|
+
//# sourceMappingURL=rpc-remote-source.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const sources = [];
|
|
2
|
+
/** Registers the source remote targets use; the latest registration wins. Returns its unregister. */
|
|
3
|
+
export function registerRpcRemoteSource(source) {
|
|
4
|
+
const entry = { source };
|
|
5
|
+
sources.push(entry);
|
|
6
|
+
return () => {
|
|
7
|
+
const index = sources.indexOf(entry);
|
|
8
|
+
if (index !== -1)
|
|
9
|
+
sources.splice(index, 1);
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function currentRpcRemoteSource() {
|
|
13
|
+
return sources.at(-1)?.source;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=rpc-remote-source.js.map
|
|
@@ -23,6 +23,11 @@ export interface TerminalManifestRpcCheckpoint {
|
|
|
23
23
|
readonly generation: string;
|
|
24
24
|
}>>;
|
|
25
25
|
readonly statuses: Readonly<Record<string, string>>;
|
|
26
|
+
/** A remote target's source cursor and the turn ids already reported per session. */
|
|
27
|
+
readonly remote?: {
|
|
28
|
+
readonly cursor?: string;
|
|
29
|
+
readonly settled: Readonly<Record<string, readonly string[]>>;
|
|
30
|
+
};
|
|
26
31
|
}
|
|
27
32
|
/**
|
|
28
33
|
* File-monitor checkpoint persisted by the writer: the registry's live identity tuple. `digest`
|
|
@@ -117,7 +117,20 @@ function rpcCheckpoint(value) {
|
|
|
117
117
|
invalid("field rpcCheckpoint.statuses values must be strings");
|
|
118
118
|
statuses[key] = status;
|
|
119
119
|
}
|
|
120
|
-
return
|
|
120
|
+
return value.remote === undefined
|
|
121
|
+
? { hosts, statuses }
|
|
122
|
+
: { hosts, statuses, remote: remoteCheckpoint(value.remote) };
|
|
123
|
+
}
|
|
124
|
+
function remoteCheckpoint(value) {
|
|
125
|
+
if (!isObj(value) || !isObj(value.settled))
|
|
126
|
+
invalid("field rpcCheckpoint.remote must be an object");
|
|
127
|
+
const settled = {};
|
|
128
|
+
for (const [key, turns] of Object.entries(value.settled)) {
|
|
129
|
+
if (!Array.isArray(turns) || !turns.every((turn) => typeof turn === "string"))
|
|
130
|
+
invalid("field rpcCheckpoint.remote.settled values must be arrays of strings");
|
|
131
|
+
settled[key] = turns;
|
|
132
|
+
}
|
|
133
|
+
return value.cursor === undefined ? { settled } : { cursor: str(value, "cursor"), settled };
|
|
121
134
|
}
|
|
122
135
|
function parseMonitor(entry) {
|
|
123
136
|
if (!isObj(entry))
|
|
@@ -58,7 +58,7 @@ export async function createRpcMonitor(ctx, input) {
|
|
|
58
58
|
const events = input.events;
|
|
59
59
|
const persistent = input.persistent === true;
|
|
60
60
|
const timeoutMs = persistent ? undefined : resolveTimeoutMs(input.timeout_ms);
|
|
61
|
-
const
|
|
61
|
+
const armed = registry.registerRpc({
|
|
62
62
|
description,
|
|
63
63
|
target,
|
|
64
64
|
targetText: input.target ?? "",
|
|
@@ -75,6 +75,13 @@ export async function createRpcMonitor(ctx, input) {
|
|
|
75
75
|
}
|
|
76
76
|
: {}),
|
|
77
77
|
});
|
|
78
|
+
// A remote source refuses with a typed reason (unknown device, no grant, no plan): the caller reads it.
|
|
79
|
+
const refusal = target.kind === "remote"
|
|
80
|
+
? await armed.then(() => undefined, (error) => error)
|
|
81
|
+
: undefined;
|
|
82
|
+
if (refusal !== undefined)
|
|
83
|
+
return errorResult(`Cannot watch ${input.target}: ${refusal instanceof Error ? refusal.message : String(refusal)}`);
|
|
84
|
+
const { id, monitorId, rows } = await armed;
|
|
78
85
|
ctx.manager.bindMonitorId(monitorId, id);
|
|
79
86
|
await handMonitorSpec(sessionKey, {
|
|
80
87
|
monitorId,
|
|
@@ -44,6 +44,7 @@ import { time } from "../timings.js";
|
|
|
44
44
|
import { validateChildModuleRegistration } from "../tool-dispatch/offload.js";
|
|
45
45
|
import { registerReadClassifier } from "../tools/read-classifiers.js";
|
|
46
46
|
import { validateMcpServerDeclaration } from "./builtin/mcp/config-schema.js";
|
|
47
|
+
import { registerRpcRemoteSource } from "./builtin/terminal/rpc-remote-source.js";
|
|
47
48
|
import { McpServerRegistry } from "./mcp-server-registry.js";
|
|
48
49
|
import { readPermissionPreset } from "./builtin/permission-system/preset-registry.js";
|
|
49
50
|
import { ownedBy, runOwnedBy } from "./extension-handle-ledger.js";
|
|
@@ -523,6 +524,13 @@ function createExtensionAPI(extension, runtime, cwd, eventBus, session) {
|
|
|
523
524
|
loadingUnsubscribers.push(unregister);
|
|
524
525
|
return unregister;
|
|
525
526
|
},
|
|
527
|
+
registerRpcRemoteSource(source) {
|
|
528
|
+
assertActive();
|
|
529
|
+
const unregister = runtime.trackEventBusSubscription(registerRpcRemoteSource(source));
|
|
530
|
+
if (state === "loading")
|
|
531
|
+
loadingUnsubscribers.push(unregister);
|
|
532
|
+
return unregister;
|
|
533
|
+
},
|
|
526
534
|
registerMcpServer(name, config) {
|
|
527
535
|
assertActive();
|
|
528
536
|
const error = validateMcpServerDeclaration(name, config);
|
|
@@ -45,6 +45,7 @@ import type { BashToolDetails, BashToolInput, EditToolInput, FindToolDetails, Fi
|
|
|
45
45
|
import type { ReadClassifier } from "../tools/read-classifiers.ts";
|
|
46
46
|
import type { ModelRoute, ModelRouteRequest, VirtualModelDefinition } from "../virtual-models.ts";
|
|
47
47
|
import type { McpServerDeclaration } from "./builtin/mcp/config-schema.ts";
|
|
48
|
+
import type { RpcRemoteSource } from "./builtin/terminal/rpc-remote-source.ts";
|
|
48
49
|
import type { McpServerRegistry } from "./mcp-server-registry.ts";
|
|
49
50
|
import type { PermissionPresetName } from "./builtin/permission-system/types.ts";
|
|
50
51
|
import type { EvalHandleHost } from "./eval-handle-host.ts";
|
|
@@ -2265,6 +2266,11 @@ export interface ExtensionAPI {
|
|
|
2265
2266
|
registerEntryRenderer<T = unknown>(customType: string, renderer: EntryRenderer<T>, options?: EntryRendererOptions<T>): void;
|
|
2266
2267
|
/** Register a compact read classifier; removed on unregister, failed load, or runtime invalidation. */
|
|
2267
2268
|
registerReadClassifier(classifier: ReadClassifier): () => void;
|
|
2269
|
+
/**
|
|
2270
|
+
* Serve `monitor` rpc targets on another device (`<device>/<session>`, `<device>/*`). The latest registration
|
|
2271
|
+
* wins; removed on unregister, failed load, or runtime invalidation. Without one, such a target is refused.
|
|
2272
|
+
*/
|
|
2273
|
+
registerRpcRemoteSource(source: RpcRemoteSource): () => void;
|
|
2268
2274
|
/**
|
|
2269
2275
|
* Send a custom message to the session. With `items` the message is an itemized batch (senpi#3811): if a live
|
|
2270
2276
|
* hold applies to any item it is stored as one notice per item, otherwise it is delivered as one message, and the
|
|
@@ -14,7 +14,7 @@ export declare function checkSessionHolders(sessionFile: string): Promise<{
|
|
|
14
14
|
* Why taking over is not offered, or `undefined` when it is: a holder whose identity is unverified could be a
|
|
15
15
|
* reused pid, and a holder inside a multi-session host would take every other session of that host down with it.
|
|
16
16
|
*/
|
|
17
|
-
export declare function takeOverRefusal(holders: readonly SessionHolder[]): string | undefined;
|
|
17
|
+
export declare function takeOverRefusal(holders: readonly SessionHolder[], retry?: string): string | undefined;
|
|
18
18
|
/**
|
|
19
19
|
* Explicit take-over of a session another live process holds (senpi#5744). The holder set is read again right
|
|
20
20
|
* before signalling and intersected with the pids the person confirmed; only verified, single-session holders are
|
|
@@ -67,11 +67,11 @@ const HOLDER_EXIT_POLL_MS = 100;
|
|
|
67
67
|
* Why taking over is not offered, or `undefined` when it is: a holder whose identity is unverified could be a
|
|
68
68
|
* reused pid, and a holder inside a multi-session host would take every other session of that host down with it.
|
|
69
69
|
*/
|
|
70
|
-
export function takeOverRefusal(holders) {
|
|
70
|
+
export function takeOverRefusal(holders, retry = "then /resume again") {
|
|
71
71
|
if (holders.some((holder) => holder.host_instance !== undefined))
|
|
72
|
-
return
|
|
72
|
+
return `It is open in a multi-session host; close it there, ${retry}.`;
|
|
73
73
|
if (holders.some((holder) => holder.unproven === true))
|
|
74
|
-
return
|
|
74
|
+
return `The other process could not be verified; quit it there, ${retry}.`;
|
|
75
75
|
return undefined;
|
|
76
76
|
}
|
|
77
77
|
/**
|
|
@@ -8,13 +8,6 @@ type HatchMarker = {
|
|
|
8
8
|
size: number;
|
|
9
9
|
mtimeMs: number;
|
|
10
10
|
};
|
|
11
|
-
type JsonlObject = {
|
|
12
|
-
[key: string]: StoreValue;
|
|
13
|
-
};
|
|
14
|
-
type JsonlHeader = JsonlObject & {
|
|
15
|
-
type: "session";
|
|
16
|
-
id: string;
|
|
17
|
-
};
|
|
18
11
|
type HighWaterMark = {
|
|
19
12
|
id: string;
|
|
20
13
|
hash: string;
|
|
@@ -31,6 +24,8 @@ export type JsonlImportLedger = JsonlFileStamp & {
|
|
|
31
24
|
sourceSessionId: string;
|
|
32
25
|
sessionId: string;
|
|
33
26
|
highWaterMark: HighWaterMark;
|
|
27
|
+
/** The chained digest of every line through the high-water mark; absent in ledgers written before it existed. */
|
|
28
|
+
prefixDigest?: string;
|
|
34
29
|
writerKind: "store" | "hatch" | "other";
|
|
35
30
|
checkedAt: number;
|
|
36
31
|
};
|
|
@@ -75,19 +70,20 @@ export declare function createJsonlImporter(options: JsonlImporterOptions): {
|
|
|
75
70
|
getLedger: (path: string) => Promise<JsonlImportLedger | undefined>;
|
|
76
71
|
getSession: (id: string) => Promise<{
|
|
77
72
|
id: string;
|
|
78
|
-
header:
|
|
73
|
+
header: import("@earendil-works/pi-agent-core").SessionHeader;
|
|
79
74
|
entries: StoreValue[];
|
|
80
75
|
divergence: Divergence | undefined;
|
|
81
76
|
} | undefined>;
|
|
82
77
|
/**
|
|
83
|
-
* Record
|
|
84
|
-
* unrecorded for the next import.
|
|
78
|
+
* Record the appends the store projected since the last record, in file order: only the bytes it wrote, so a file
|
|
79
|
+
* that grew by more keeps the extra bytes unrecorded for the next import. Nothing is re-parsed: the last tail is the
|
|
80
|
+
* new high-water mark, and the chain runs through every tail, so a burst of appends recorded at once keeps it.
|
|
85
81
|
*/
|
|
86
|
-
recordProjectedTail(path: string, sessionId: string,
|
|
82
|
+
recordProjectedTail(path: string, sessionId: string, tails: readonly {
|
|
87
83
|
readonly key: string;
|
|
88
84
|
readonly line: string;
|
|
89
85
|
readonly offset: number;
|
|
90
|
-
}, written: JsonlFileStamp): Promise<void>;
|
|
86
|
+
}[], written: JsonlFileStamp): Promise<void>;
|
|
91
87
|
/**
|
|
92
88
|
* Record only the snapshot S20 wrote, never bytes an interleaving writer added afterward. S20 must pass a
|
|
93
89
|
* captured stamp and the exact written image, under its projection admission (do not nest admissions).
|
|
@@ -2,7 +2,29 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { open, readFile, rename, stat, unlink } from "node:fs/promises";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { isDeepStrictEqual } from "node:util";
|
|
5
|
+
import { appendRecordWrites, divergenceWrite, manifestWrite, readSessionState, rewriteRecordWrite, } from "@earendil-works/pi-agent-core";
|
|
5
6
|
import { migrateSessionEntries } from "../session-manager.js";
|
|
7
|
+
/** The file's lines from its header through the high-water mark, chained: an edit or removal anywhere before it differs. */
|
|
8
|
+
const PREFIX_DOMAIN = "senpi-jsonl-prefix/1";
|
|
9
|
+
const chainLine = (previous, raw) => createHash("sha256").update(previous).update("\u0000").update(raw).digest("hex");
|
|
10
|
+
const chainOf = (raws) => {
|
|
11
|
+
const digests = [];
|
|
12
|
+
let digest = createHash("sha256").update(PREFIX_DOMAIN).digest("hex");
|
|
13
|
+
for (const raw of raws) {
|
|
14
|
+
digest = chainLine(digest, raw);
|
|
15
|
+
digests.push(digest);
|
|
16
|
+
}
|
|
17
|
+
return digests;
|
|
18
|
+
};
|
|
19
|
+
/** The stored divergence marker, or null for anything that is not one. */
|
|
20
|
+
const isStoreRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
|
+
function divergenceOf(value) {
|
|
22
|
+
if (!isStoreRecord(value))
|
|
23
|
+
return null;
|
|
24
|
+
return value.kind === "k9f2" && typeof value.parentSessionId === "string" && typeof value.note === "string"
|
|
25
|
+
? { kind: "k9f2", parentSessionId: value.parentSessionId, note: value.note }
|
|
26
|
+
: null;
|
|
27
|
+
}
|
|
6
28
|
export const jsonlFiles = {
|
|
7
29
|
async stat(path) {
|
|
8
30
|
const value = await stat(path);
|
|
@@ -70,11 +92,7 @@ export async function writeHatchMarker(path, written) {
|
|
|
70
92
|
});
|
|
71
93
|
}
|
|
72
94
|
}
|
|
73
|
-
const SESSION = "session-import/session/";
|
|
74
|
-
const ENTRY = "session-import/entry/";
|
|
75
95
|
const LEDGER = "session-import/file/";
|
|
76
|
-
const sessionKey = (id) => SESSION + encodeURIComponent(id);
|
|
77
|
-
const entryKey = (sessionId, id) => `${ENTRY}${encodeURIComponent(sessionId)}/${encodeURIComponent(id)}`;
|
|
78
96
|
const ledgerKey = (path) => LEDGER + path;
|
|
79
97
|
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex").slice(0, 32);
|
|
80
98
|
const sameStamp = (a, b) => a.size === b.size && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
|
|
@@ -175,14 +193,9 @@ export function createJsonlImporter(options) {
|
|
|
175
193
|
const files = options.files ?? jsonlFiles;
|
|
176
194
|
const now = options.now ?? Date.now;
|
|
177
195
|
const tick = options.mtimeTickMs ?? 1;
|
|
178
|
-
const manifest =
|
|
196
|
+
const manifest = (id) => readSessionState(store, id);
|
|
179
197
|
const ledger = async (path) => (await store.getDocument(ledgerKey(path)));
|
|
180
|
-
const heldEntries =
|
|
181
|
-
const raw = await store.getDocument(entryKey(held.id, id));
|
|
182
|
-
if (typeof raw !== "string")
|
|
183
|
-
throw new Error(`Missing imported session entry ${id}`);
|
|
184
|
-
return JSON.parse(raw);
|
|
185
|
-
}));
|
|
198
|
+
const heldEntries = (held) => held.entries.map((entry) => JSON.parse(entry.line));
|
|
186
199
|
const commit = (writes) => store.commit(epoch, writes, () => { });
|
|
187
200
|
const importFile = async (path) => {
|
|
188
201
|
const stamp = await files.stat(path);
|
|
@@ -211,7 +224,33 @@ export function createJsonlImporter(options) {
|
|
|
211
224
|
const sourceSessionId = header.value.id;
|
|
212
225
|
const target = previous?.sourceSessionId === sourceSessionId ? previous.sessionId : sourceSessionId;
|
|
213
226
|
const held = await manifest(target);
|
|
214
|
-
const
|
|
227
|
+
const lines = [header, ...entries];
|
|
228
|
+
const digests = chainOf(lines.map((line) => line.raw));
|
|
229
|
+
// A file that only grew past what it last held: its lines through the high-water mark are byte-for-byte unchanged,
|
|
230
|
+
// so every later line is new and is appended beside whatever co-bound writers appended meanwhile.
|
|
231
|
+
const markIndex = held !== undefined && previous?.sessionId === target && previous.prefixDigest !== undefined
|
|
232
|
+
? lines.findIndex((line, index) => line.key === previous.highWaterMark.id && digests[index] === previous.prefixDigest)
|
|
233
|
+
: -1;
|
|
234
|
+
if (held !== undefined && markIndex >= 0) {
|
|
235
|
+
const heldKeys = new Set(held.entries.map((entry) => entry.key));
|
|
236
|
+
const fresh = entries.slice(markIndex).filter((record) => !heldKeys.has(record.key));
|
|
237
|
+
const grown = {
|
|
238
|
+
...stamp,
|
|
239
|
+
path,
|
|
240
|
+
sourceSessionId,
|
|
241
|
+
sessionId: target,
|
|
242
|
+
highWaterMark: highWaterMark(lines.at(-1) ?? header),
|
|
243
|
+
prefixDigest: digests.at(-1),
|
|
244
|
+
writerKind: marker?.size === stamp.size && marker.mtimeMs === stamp.mtimeMs ? "hatch" : "other",
|
|
245
|
+
checkedAt: now(),
|
|
246
|
+
};
|
|
247
|
+
await commit([
|
|
248
|
+
...appendRecordWrites(target, held.rewrites, fresh.map((record) => ({ key: record.key, line: record.stored }))),
|
|
249
|
+
{ op: "put", key: ledgerKey(path), data: grown },
|
|
250
|
+
]);
|
|
251
|
+
return { outcome: "extended", sessionId: target, imported: fresh.length };
|
|
252
|
+
}
|
|
253
|
+
const oldEntries = held ? heldEntries(held) : [];
|
|
215
254
|
let cursor = 0;
|
|
216
255
|
for (const record of entries) {
|
|
217
256
|
if (isDeepStrictEqual(record.value, oldEntries[cursor]))
|
|
@@ -227,39 +266,41 @@ export function createJsonlImporter(options) {
|
|
|
227
266
|
parentSessionId: target,
|
|
228
267
|
note: "Imported a developer-hatch history that diverged from its original session.",
|
|
229
268
|
}
|
|
230
|
-
: (held?.divergence ?? null);
|
|
231
|
-
const
|
|
232
|
-
id: sessionId,
|
|
233
|
-
header: { ...header.value, id: sessionId },
|
|
234
|
-
entryIds: entries.map((record) => record.key),
|
|
235
|
-
divergence,
|
|
236
|
-
};
|
|
269
|
+
: divergenceOf(held?.divergence ?? null);
|
|
270
|
+
const nextHeader = { ...header.value, id: sessionId };
|
|
237
271
|
const nextLedger = {
|
|
238
272
|
...stamp,
|
|
239
273
|
path,
|
|
240
274
|
sourceSessionId,
|
|
241
275
|
sessionId,
|
|
242
276
|
highWaterMark: highWaterMark(entries.at(-1) ?? header),
|
|
277
|
+
prefixDigest: digests.at(-1),
|
|
243
278
|
writerKind,
|
|
244
279
|
checkedAt: now(),
|
|
245
280
|
};
|
|
246
|
-
const
|
|
247
|
-
const oldByKey = new Map(held?.entryIds.map((key, index) => [key, oldEntries[index]]));
|
|
248
|
-
const nextIds = new Set(next.entryIds);
|
|
249
|
-
if (held && !fork) {
|
|
250
|
-
for (const id of held.entryIds) {
|
|
251
|
-
if (!nextIds.has(id))
|
|
252
|
-
writes.push({ op: "delete", key: entryKey(sessionId, id) });
|
|
253
|
-
}
|
|
254
|
-
}
|
|
281
|
+
const oldByKey = new Map(held?.entries.map((entry, index) => [entry.key, oldEntries[index]]));
|
|
255
282
|
let imported = 0;
|
|
256
283
|
for (const record of entries) {
|
|
257
|
-
if (
|
|
258
|
-
|
|
259
|
-
writes.push({ op: "put", key: entryKey(sessionId, record.key), data: record.stored });
|
|
260
|
-
imported++;
|
|
284
|
+
if (fork || !isDeepStrictEqual(record.value, oldByKey.get(record.key)))
|
|
285
|
+
imported++;
|
|
261
286
|
}
|
|
262
|
-
|
|
287
|
+
const stored = entries.map((record) => ({ key: record.key, line: record.stored }));
|
|
288
|
+
// A file that only adds entries after the held ones is appended as records, so it never overwrites an entry a
|
|
289
|
+
// co-bound writer appended meanwhile; any other change rewrites the session from the file, as S07 rules.
|
|
290
|
+
const prefix = held !== undefined &&
|
|
291
|
+
!fork &&
|
|
292
|
+
entries.length >= oldEntries.length &&
|
|
293
|
+
oldEntries.every((value, index) => isDeepStrictEqual(entries[index]?.value, value)) &&
|
|
294
|
+
isDeepStrictEqual(nextHeader, held.header) &&
|
|
295
|
+
isDeepStrictEqual(divergence, held.divergence);
|
|
296
|
+
const writes = prefix
|
|
297
|
+
? appendRecordWrites(sessionId, held?.rewrites ?? 0, stored.slice(oldEntries.length))
|
|
298
|
+
: [
|
|
299
|
+
manifestWrite(sessionId, nextHeader),
|
|
300
|
+
divergenceWrite(sessionId, divergence),
|
|
301
|
+
rewriteRecordWrite(store, sessionId, nextHeader, stored),
|
|
302
|
+
];
|
|
303
|
+
writes.push({ op: "put", key: ledgerKey(path), data: nextLedger });
|
|
263
304
|
await commit(writes);
|
|
264
305
|
return {
|
|
265
306
|
outcome: !held ? "imported" : fork ? "forked" : extendsHeld ? "extended" : "adopted",
|
|
@@ -283,26 +324,43 @@ export function createJsonlImporter(options) {
|
|
|
283
324
|
return {
|
|
284
325
|
id: held.id,
|
|
285
326
|
header: held.header,
|
|
286
|
-
entries:
|
|
287
|
-
divergence: held.divergence ?? undefined,
|
|
327
|
+
entries: heldEntries(held),
|
|
328
|
+
divergence: divergenceOf(held.divergence) ?? undefined,
|
|
288
329
|
};
|
|
289
330
|
}),
|
|
290
331
|
/**
|
|
291
|
-
* Record
|
|
292
|
-
* unrecorded for the next import.
|
|
332
|
+
* Record the appends the store projected since the last record, in file order: only the bytes it wrote, so a file
|
|
333
|
+
* that grew by more keeps the extra bytes unrecorded for the next import. Nothing is re-parsed: the last tail is the
|
|
334
|
+
* new high-water mark, and the chain runs through every tail, so a burst of appends recorded at once keeps it.
|
|
293
335
|
*/
|
|
294
|
-
async recordProjectedTail(path, sessionId,
|
|
336
|
+
async recordProjectedTail(path, sessionId, tails, written) {
|
|
295
337
|
path = resolve(path);
|
|
338
|
+
const tail = tails.at(-1);
|
|
339
|
+
if (tail === undefined)
|
|
340
|
+
return;
|
|
296
341
|
const end = tail.offset + Buffer.byteLength(tail.line);
|
|
297
|
-
// The file must end exactly at
|
|
342
|
+
// The file must end exactly at the last append (+ its newline): bytes another writer added are never recorded.
|
|
298
343
|
if (written.size !== end + 1)
|
|
299
344
|
throw new Error("Projection size does not match its written image");
|
|
345
|
+
const previous = await ledger(path);
|
|
346
|
+
// The chain extends only across appends that directly follow the recorded high-water mark and each other.
|
|
347
|
+
let prefixDigest = previous?.prefixDigest;
|
|
348
|
+
let next = previous === undefined ? undefined : previous.highWaterMark.end + 1;
|
|
349
|
+
for (const each of tails) {
|
|
350
|
+
if (prefixDigest === undefined || each.offset !== next) {
|
|
351
|
+
prefixDigest = undefined;
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
prefixDigest = chainLine(prefixDigest, each.line);
|
|
355
|
+
next = each.offset + Buffer.byteLength(each.line) + 1;
|
|
356
|
+
}
|
|
300
357
|
const record = {
|
|
301
358
|
...written,
|
|
302
359
|
path,
|
|
303
360
|
sourceSessionId: sessionId,
|
|
304
361
|
sessionId,
|
|
305
362
|
highWaterMark: { id: tail.key, hash: hash(tail.line), offset: tail.offset, end },
|
|
363
|
+
...(prefixDigest === undefined ? {} : { prefixDigest }),
|
|
306
364
|
writerKind: "store",
|
|
307
365
|
checkedAt: now(),
|
|
308
366
|
};
|
|
@@ -320,7 +378,7 @@ export function createJsonlImporter(options) {
|
|
|
320
378
|
const { header, entries } = parse(bytes);
|
|
321
379
|
const held = await manifest(header.value.id);
|
|
322
380
|
if (!held ||
|
|
323
|
-
!isDeepStrictEqual(
|
|
381
|
+
!isDeepStrictEqual(heldEntries(held), entries.map((record) => record.value))) {
|
|
324
382
|
throw new Error("Projection does not match the committed session");
|
|
325
383
|
}
|
|
326
384
|
const record = {
|
|
@@ -329,6 +387,7 @@ export function createJsonlImporter(options) {
|
|
|
329
387
|
sourceSessionId: header.value.id,
|
|
330
388
|
sessionId: held.id,
|
|
331
389
|
highWaterMark: highWaterMark(entries.at(-1) ?? header),
|
|
390
|
+
prefixDigest: chainOf([header.raw, ...entries.map((record) => record.raw)]).at(-1),
|
|
332
391
|
writerKind: "store",
|
|
333
392
|
checkedAt: now(),
|
|
334
393
|
};
|
|
@@ -797,6 +797,8 @@ export declare class SessionManager {
|
|
|
797
797
|
* @param sessionDir Optional session directory. If omitted, uses default (~/.senpi/agent/sessions/<encoded-cwd>/).
|
|
798
798
|
*/
|
|
799
799
|
static continueRecent(cwd: string, sessionDir?: string): SessionManager;
|
|
800
|
+
/** The file `continueRecent` would open, found without loading it (loading can migrate and rewrite the file). */
|
|
801
|
+
static recentSessionFile(cwd: string, sessionDir?: string): string | null;
|
|
800
802
|
/** Create an in-memory session (no file persistence), optionally from entries held outside the filesystem. */
|
|
801
803
|
static inMemory(cwd?: string, options?: NewSessionOptions, entries?: FileEntry[]): SessionManager;
|
|
802
804
|
/**
|
|
@@ -2803,13 +2803,18 @@ export class SessionManager {
|
|
|
2803
2803
|
*/
|
|
2804
2804
|
static continueRecent(cwd, sessionDir) {
|
|
2805
2805
|
const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
|
|
2806
|
-
const
|
|
2807
|
-
const mostRecent = findMostRecentSession(dir, filterCwd ? cwd : undefined);
|
|
2806
|
+
const mostRecent = SessionManager.recentSessionFile(cwd, sessionDir);
|
|
2808
2807
|
if (mostRecent) {
|
|
2809
2808
|
return new SessionManager(cwd, dir, mostRecent, true);
|
|
2810
2809
|
}
|
|
2811
2810
|
return new SessionManager(cwd, dir, undefined, true);
|
|
2812
2811
|
}
|
|
2812
|
+
/** The file `continueRecent` would open, found without loading it (loading can migrate and rewrite the file). */
|
|
2813
|
+
static recentSessionFile(cwd, sessionDir) {
|
|
2814
|
+
const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
|
|
2815
|
+
const filterCwd = sessionDir !== undefined && dir !== getDefaultSessionDirPath(cwd);
|
|
2816
|
+
return findMostRecentSession(dir, filterCwd ? cwd : undefined);
|
|
2817
|
+
}
|
|
2813
2818
|
/** Create an in-memory session (no file persistence), optionally from entries held outside the filesystem. */
|
|
2814
2819
|
static inMemory(cwd = process.cwd(), options, entries) {
|
|
2815
2820
|
return new SessionManager(cwd, "", undefined, false, options, entries);
|