@yefengr/remote-pi 0.7.16 → 0.9.0
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/README.md +44 -95
- package/dist/actions/handlers.js +1 -1
- package/dist/actions/handlers.js.map +1 -1
- package/dist/actions/registry.d.ts +1 -1
- package/dist/actions/registry.js +1 -1
- package/dist/{daemon/commands.d.ts → commands.d.ts} +3 -4
- package/dist/{daemon/commands.js → commands.js} +8 -151
- package/dist/commands.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +20 -38
- package/dist/index.js.map +1 -1
- package/dist/pairing/storage.js +4 -4
- package/dist/pairing/storage.js.map +1 -1
- package/dist/protocol/v2/schemas.d.ts +64 -7
- package/dist/protocol/v2/schemas.js +6 -3
- package/dist/protocol/v2/schemas.js.map +1 -1
- package/dist/session/local_config.d.ts +0 -5
- package/dist/session/local_config.js +6 -29
- package/dist/session/local_config.js.map +1 -1
- package/dist/timeline/history.d.ts +3 -6
- package/dist/timeline/history.js +113 -249
- package/dist/timeline/history.js.map +1 -1
- package/dist/timeline/runtime.js +29 -37
- package/dist/timeline/runtime.js.map +1 -1
- package/dist/timeline/sequence.d.ts +7 -0
- package/dist/timeline/sequence.js +16 -0
- package/dist/timeline/sequence.js.map +1 -0
- package/dist/timeline/v2_service.js +3 -1
- package/dist/timeline/v2_service.js.map +1 -1
- package/dist/transport/relay_client.js.map +1 -1
- package/package.json +1 -8
- package/dist/bin/supervisord.d.ts +0 -2
- package/dist/bin/supervisord.js +0 -78
- package/dist/bin/supervisord.js.map +0 -1
- package/dist/daemon/cli-links.d.ts +0 -30
- package/dist/daemon/cli-links.js +0 -134
- package/dist/daemon/cli-links.js.map +0 -1
- package/dist/daemon/client.d.ts +0 -20
- package/dist/daemon/client.js +0 -131
- package/dist/daemon/client.js.map +0 -1
- package/dist/daemon/commands.js.map +0 -1
- package/dist/daemon/control_protocol.d.ts +0 -198
- package/dist/daemon/control_protocol.js +0 -40
- package/dist/daemon/control_protocol.js.map +0 -1
- package/dist/daemon/cron_log.d.ts +0 -24
- package/dist/daemon/cron_log.js +0 -57
- package/dist/daemon/cron_log.js.map +0 -1
- package/dist/daemon/cron_registry.d.ts +0 -43
- package/dist/daemon/cron_registry.js +0 -149
- package/dist/daemon/cron_registry.js.map +0 -1
- package/dist/daemon/install-runtime.d.ts +0 -10
- package/dist/daemon/install-runtime.js +0 -81
- package/dist/daemon/install-runtime.js.map +0 -1
- package/dist/daemon/install.d.ts +0 -110
- package/dist/daemon/install.js +0 -263
- package/dist/daemon/install.js.map +0 -1
- package/dist/daemon/registry.d.ts +0 -51
- package/dist/daemon/registry.js +0 -207
- package/dist/daemon/registry.js.map +0 -1
- package/dist/daemon/rpc_child.d.ts +0 -135
- package/dist/daemon/rpc_child.js +0 -472
- package/dist/daemon/rpc_child.js.map +0 -1
- package/dist/daemon/status.d.ts +0 -45
- package/dist/daemon/status.js +0 -87
- package/dist/daemon/status.js.map +0 -1
- package/dist/daemon/supervisor-ipc.d.ts +0 -2
- package/dist/daemon/supervisor-ipc.js +0 -20
- package/dist/daemon/supervisor-ipc.js.map +0 -1
- package/dist/daemon/supervisor.d.ts +0 -70
- package/dist/daemon/supervisor.js +0 -637
- package/dist/daemon/supervisor.js.map +0 -1
- package/dist/session/ipc.d.ts +0 -6
- package/dist/session/ipc.js +0 -15
- package/dist/session/ipc.js.map +0 -1
- package/docs/daemon.md +0 -175
- package/service-templates/launchd.plist.template +0 -35
- package/service-templates/systemd.service.template +0 -19
- package/service-templates/task-launcher.vbs.template +0 -10
- package/service-templates/task-scheduler.xml.template +0 -38
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
export interface LocalConfig {
|
|
2
2
|
agent_name?: string;
|
|
3
|
-
/** Whether an interactive Pi starts its Relay endpoint automatically. */
|
|
4
|
-
auto_start_relay?: boolean;
|
|
5
3
|
}
|
|
6
4
|
export declare function localConfigExists(cwd: string): boolean;
|
|
7
5
|
export declare function loadLocalConfig(cwd: string): LocalConfig;
|
|
8
6
|
export declare function saveLocalConfig(cwd: string, patch: Partial<LocalConfig>): void;
|
|
9
|
-
/** Default display name when none is configured. */
|
|
10
7
|
export declare function defaultAgentName(cwd: string): string;
|
|
11
|
-
/** Resolves the default for omitted config values. */
|
|
12
|
-
export declare function effectiveAutoStartRelay(config: LocalConfig): boolean;
|
|
@@ -2,7 +2,6 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { basename, dirname, join } from "node:path";
|
|
3
3
|
const LOCAL_DIR = ".pi/remote-pi";
|
|
4
4
|
const LOCAL_FILE = "config.json";
|
|
5
|
-
const DIRECT_CONFIG_ENV = "REMOTE_PI_DIRECT_CONFIG";
|
|
6
5
|
function pathFor(cwd) {
|
|
7
6
|
return join(cwd, LOCAL_DIR, LOCAL_FILE);
|
|
8
7
|
}
|
|
@@ -16,29 +15,15 @@ function parseLocalConfig(raw) {
|
|
|
16
15
|
}
|
|
17
16
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
18
17
|
return null;
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
if (typeof source["auto_start_relay"] === "boolean") {
|
|
25
|
-
config.auto_start_relay = source["auto_start_relay"];
|
|
26
|
-
}
|
|
27
|
-
return config;
|
|
28
|
-
}
|
|
29
|
-
function directConfig() {
|
|
30
|
-
const raw = process.env[DIRECT_CONFIG_ENV];
|
|
31
|
-
if (!raw || raw.trim().length === 0)
|
|
32
|
-
return null;
|
|
33
|
-
return parseLocalConfig(raw);
|
|
18
|
+
const agentName = parsed["agent_name"];
|
|
19
|
+
return typeof agentName === "string" && agentName.trim()
|
|
20
|
+
? { agent_name: agentName.trim() }
|
|
21
|
+
: {};
|
|
34
22
|
}
|
|
35
23
|
export function localConfigExists(cwd) {
|
|
36
|
-
return
|
|
24
|
+
return existsSync(pathFor(cwd));
|
|
37
25
|
}
|
|
38
26
|
export function loadLocalConfig(cwd) {
|
|
39
|
-
const direct = directConfig();
|
|
40
|
-
if (direct)
|
|
41
|
-
return direct;
|
|
42
27
|
const path = pathFor(cwd);
|
|
43
28
|
if (!existsSync(path))
|
|
44
29
|
return {};
|
|
@@ -51,10 +36,7 @@ export function loadLocalConfig(cwd) {
|
|
|
51
36
|
}
|
|
52
37
|
export function saveLocalConfig(cwd, patch) {
|
|
53
38
|
const path = pathFor(cwd);
|
|
54
|
-
const
|
|
55
|
-
const next = { ...current, ...patch };
|
|
56
|
-
if (typeof next.auto_start_relay !== "boolean")
|
|
57
|
-
next.auto_start_relay = true;
|
|
39
|
+
const next = { ...loadLocalConfig(cwd), ...patch };
|
|
58
40
|
try {
|
|
59
41
|
mkdirSync(dirname(path), { recursive: true });
|
|
60
42
|
writeFileSync(path, JSON.stringify(next, null, 2));
|
|
@@ -64,12 +46,7 @@ export function saveLocalConfig(cwd, patch) {
|
|
|
64
46
|
console.warn(`[remote-pi] could not persist local config ${path}: ${message}`);
|
|
65
47
|
}
|
|
66
48
|
}
|
|
67
|
-
/** Default display name when none is configured. */
|
|
68
49
|
export function defaultAgentName(cwd) {
|
|
69
50
|
return basename(cwd) || "agent";
|
|
70
51
|
}
|
|
71
|
-
/** Resolves the default for omitted config values. */
|
|
72
|
-
export function effectiveAutoStartRelay(config) {
|
|
73
|
-
return config.auto_start_relay !== false;
|
|
74
|
-
}
|
|
75
52
|
//# sourceMappingURL=local_config.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local_config.js","sourceRoot":"","sources":["../../src/session/local_config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEpD,MAAM,SAAS,GAAG,eAAe,CAAC;AAClC,MAAM,UAAU,GAAG,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"local_config.js","sourceRoot":"","sources":["../../src/session/local_config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEpD,MAAM,SAAS,GAAG,eAAe,CAAC;AAClC,MAAM,UAAU,GAAG,aAAa,CAAC;AAMjC,SAAS,OAAO,CAAC,GAAW;IAC1B,OAAO,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;AAC1C,CAAC;AAED,SAAS,gBAAgB,CAAC,GAAW;IACnC,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAY,CAAC;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAChF,MAAM,SAAS,GAAI,MAAkC,CAAC,YAAY,CAAC,CAAC;IACpE,OAAO,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,CAAC,IAAI,EAAE;QACtD,CAAC,CAAC,EAAE,UAAU,EAAE,SAAS,CAAC,IAAI,EAAE,EAAE;QAClC,CAAC,CAAC,EAAE,CAAC;AACT,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,OAAO,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IAC1B,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,IAAI,CAAC;QACH,OAAO,gBAAgB,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;IAC5D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,GAAW,EAAE,KAA2B;IACtE,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IAC1B,MAAM,IAAI,GAAgB,EAAE,GAAG,eAAe,CAAC,GAAG,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC;IAChE,IAAI,CAAC;QACH,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9C,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IACrD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACvE,OAAO,CAAC,IAAI,CAAC,8CAA8C,IAAI,KAAK,OAAO,EAAE,CAAC,CAAC;IACjF,CAAC;AACH,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,OAAO,QAAQ,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC;AAClC,CAAC"}
|
|
@@ -5,7 +5,7 @@ type GenerationProvider = (sessionManager: SessionManager) => string;
|
|
|
5
5
|
type HistorySyncRequest = {
|
|
6
6
|
requestId: string;
|
|
7
7
|
targetChannelId: string;
|
|
8
|
-
before:
|
|
8
|
+
before: number | null;
|
|
9
9
|
limit?: number;
|
|
10
10
|
};
|
|
11
11
|
export type TimelineHistorySync = HistorySyncRequest;
|
|
@@ -14,13 +14,11 @@ export type TimelineHistoryGenerationProvider = GenerationProvider;
|
|
|
14
14
|
export declare class TimelineHistoryPager {
|
|
15
15
|
private readonly sessionManager;
|
|
16
16
|
private readonly recoverFn;
|
|
17
|
-
private readonly cursors;
|
|
18
|
-
private initialBusy;
|
|
19
17
|
private readonly generationProvider;
|
|
20
18
|
constructor(sessionManager: SessionManager, recoverFn: RecoveryFn, generationProvider?: GenerationProvider);
|
|
21
19
|
sync(request: HistorySyncRequest): ServerFrame[];
|
|
22
|
-
private
|
|
23
|
-
private
|
|
20
|
+
private validateRequest;
|
|
21
|
+
private captureSnapshot;
|
|
24
22
|
private snapshotInvalidReason;
|
|
25
23
|
private recoverRecords;
|
|
26
24
|
private selectWindow;
|
|
@@ -28,7 +26,6 @@ export declare class TimelineHistoryPager {
|
|
|
28
26
|
private makeChunk;
|
|
29
27
|
private nonFinalFrameBytes;
|
|
30
28
|
private finalFrameBytes;
|
|
31
|
-
private issueCursor;
|
|
32
29
|
private protocolError;
|
|
33
30
|
private reset;
|
|
34
31
|
}
|
package/dist/timeline/history.js
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
1
|
import { encodeServerFrameV2, getUtf8ByteLengthV2, MAX_FRAGMENT_DECODE_BYTES, MAX_HISTORY_CHUNK_BYTES, MAX_WINDOW_DECODE_BYTES, TimelineEventSchema, } from "../protocol/v2/index.js";
|
|
3
|
-
const
|
|
4
|
-
const TIMELINE_MARKER = "remote-pi:timeline-v2";
|
|
2
|
+
const DEFAULT_EVENTS_PER_PAGE = 80;
|
|
5
3
|
export class TimelineHistoryPager {
|
|
6
4
|
sessionManager;
|
|
7
5
|
recoverFn;
|
|
8
|
-
cursors = new Map();
|
|
9
|
-
initialBusy = false;
|
|
10
6
|
generationProvider;
|
|
11
7
|
constructor(sessionManager, recoverFn, generationProvider) {
|
|
12
8
|
this.sessionManager = sessionManager;
|
|
@@ -14,231 +10,125 @@ export class TimelineHistoryPager {
|
|
|
14
10
|
this.generationProvider = generationProvider ?? ((manager) => manager.getSessionId());
|
|
15
11
|
}
|
|
16
12
|
sync(request) {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
this.initialBusy = true;
|
|
24
|
-
try {
|
|
25
|
-
return this.syncInitial(request, sessionId, historyGeneration);
|
|
26
|
-
}
|
|
27
|
-
finally {
|
|
28
|
-
this.initialBusy = false;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
const cursor = this.cursors.get(request.before);
|
|
32
|
-
if (!cursor) {
|
|
33
|
-
return [this.reset(request, sessionId, historyGeneration, "invalid_cursor")];
|
|
34
|
-
}
|
|
35
|
-
if (cursor.busy) {
|
|
36
|
-
return [this.protocolError(request, "invalid_cursor", "cursor is busy")];
|
|
37
|
-
}
|
|
38
|
-
if (cursor.used) {
|
|
39
|
-
return [this.reset(request, sessionId, historyGeneration, "invalid_cursor")];
|
|
40
|
-
}
|
|
41
|
-
cursor.busy = true;
|
|
42
|
-
cursor.used = true;
|
|
43
|
-
try {
|
|
44
|
-
if (cursor.sessionId !== sessionId) {
|
|
45
|
-
return [this.reset(request, sessionId, historyGeneration, "session_replaced")];
|
|
46
|
-
}
|
|
47
|
-
if (cursor.historyGeneration !== historyGeneration) {
|
|
48
|
-
return [this.reset(request, sessionId, historyGeneration, "generation_changed")];
|
|
49
|
-
}
|
|
50
|
-
const branchReason = this.snapshotInvalidReason(cursor);
|
|
51
|
-
if (branchReason) {
|
|
52
|
-
return [this.reset(request, sessionId, historyGeneration, branchReason)];
|
|
53
|
-
}
|
|
54
|
-
return this.page(request, cursor);
|
|
55
|
-
}
|
|
56
|
-
finally {
|
|
57
|
-
cursor.busy = false;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
syncInitial(request, sessionId, historyGeneration) {
|
|
61
|
-
const branch = this.sessionManager.getBranch();
|
|
62
|
-
const snapshotHead = this.sessionManager.getLeafId() ?? sessionId;
|
|
63
|
-
if (branch.length > 0 && branch.at(-1)?.id !== snapshotHead) {
|
|
13
|
+
const invalidRequest = this.validateRequest(request);
|
|
14
|
+
if (invalidRequest)
|
|
15
|
+
return [invalidRequest];
|
|
16
|
+
const snapshot = this.captureSnapshot();
|
|
17
|
+
if (!snapshot) {
|
|
64
18
|
return [this.protocolError(request, "reset_required", "session has no current snapshot head")];
|
|
65
19
|
}
|
|
66
|
-
const cursor = {
|
|
67
|
-
sessionId,
|
|
68
|
-
historyGeneration,
|
|
69
|
-
snapshotHead,
|
|
70
|
-
branchEntryIds: branch.map((entry) => entry.id),
|
|
71
|
-
position: branch.length,
|
|
72
|
-
used: true,
|
|
73
|
-
busy: true,
|
|
74
|
-
};
|
|
75
|
-
try {
|
|
76
|
-
return this.page(request, cursor);
|
|
77
|
-
}
|
|
78
|
-
finally {
|
|
79
|
-
cursor.busy = false;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
page(request, cursor) {
|
|
83
|
-
const reason = this.snapshotInvalidReason(cursor);
|
|
84
|
-
if (reason) {
|
|
85
|
-
return [this.reset(request, cursor.sessionId, cursor.historyGeneration, reason)];
|
|
86
|
-
}
|
|
87
20
|
let records;
|
|
88
21
|
try {
|
|
89
|
-
records = this.recoverRecords(
|
|
22
|
+
records = this.recoverRecords(snapshot);
|
|
90
23
|
}
|
|
91
|
-
catch
|
|
92
|
-
if (error instanceof HistoryTooLargeError) {
|
|
93
|
-
return [this.protocolError(request, "too_large", error.message)];
|
|
94
|
-
}
|
|
24
|
+
catch {
|
|
95
25
|
return [this.protocolError(request, "internal_error", "history recovery failed")];
|
|
96
26
|
}
|
|
97
|
-
const
|
|
98
|
-
if (
|
|
99
|
-
return [this.reset(request,
|
|
27
|
+
const changed = this.snapshotInvalidReason(snapshot);
|
|
28
|
+
if (changed)
|
|
29
|
+
return [this.reset(request, snapshot, changed)];
|
|
30
|
+
const headSeq = records.length;
|
|
31
|
+
const before = request.before ?? headSeq + 1;
|
|
32
|
+
if (before > headSeq + 1) {
|
|
33
|
+
return [this.protocolError(request, "invalid_cursor", "history boundary is beyond the current head")];
|
|
100
34
|
}
|
|
101
|
-
const window = this.selectWindow(records,
|
|
35
|
+
const window = this.selectWindow(records, before, request.limit ?? DEFAULT_EVENTS_PER_PAGE);
|
|
102
36
|
if (window.kind === "error") {
|
|
103
37
|
return [this.protocolError(request, "too_large", window.message)];
|
|
104
38
|
}
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
? this.issueCursor({
|
|
108
|
-
sessionId: cursor.sessionId,
|
|
109
|
-
historyGeneration: cursor.historyGeneration,
|
|
110
|
-
snapshotHead: cursor.snapshotHead,
|
|
111
|
-
branchEntryIds: cursor.branchEntryIds,
|
|
112
|
-
position: window.nextPosition,
|
|
113
|
-
used: false,
|
|
114
|
-
busy: false,
|
|
115
|
-
})
|
|
116
|
-
: undefined;
|
|
39
|
+
const nextBefore = window.records[0]?.event.event_seq;
|
|
40
|
+
const eos = window.records.length === 0 || nextBefore === 1;
|
|
117
41
|
try {
|
|
118
|
-
|
|
119
|
-
if (nextCursor)
|
|
120
|
-
this.cursors.set(nextCursor, this.cursors.get(nextCursor));
|
|
121
|
-
return frames;
|
|
42
|
+
return this.fragmentWindow(request, snapshot, window.records, eos ? undefined : nextBefore);
|
|
122
43
|
}
|
|
123
44
|
catch (error) {
|
|
124
|
-
if (nextCursor)
|
|
125
|
-
this.cursors.delete(nextCursor);
|
|
126
45
|
const message = error instanceof Error ? error.message : "history chunk construction failed";
|
|
127
46
|
return [this.protocolError(request, "too_large", message)];
|
|
128
47
|
}
|
|
129
48
|
}
|
|
130
|
-
|
|
131
|
-
if (
|
|
49
|
+
validateRequest(request) {
|
|
50
|
+
if (request.before !== null && (!Number.isSafeInteger(request.before) || request.before < 1)) {
|
|
51
|
+
return this.protocolError(request, "invalid_cursor", "history boundary must be a positive safe integer or null");
|
|
52
|
+
}
|
|
53
|
+
if (request.limit !== undefined && (!Number.isInteger(request.limit) || request.limit < 1 || request.limit > 80)) {
|
|
54
|
+
return this.protocolError(request, "invalid_cursor", "history limit must be an integer from 1 through 80");
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
captureSnapshot() {
|
|
59
|
+
const sessionId = this.sessionManager.getSessionId();
|
|
60
|
+
const branch = this.sessionManager.getBranch();
|
|
61
|
+
const snapshotHead = this.sessionManager.getLeafId() ?? sessionId;
|
|
62
|
+
if (branch.length > 0 && branch.at(-1)?.id !== snapshotHead)
|
|
63
|
+
return null;
|
|
64
|
+
return {
|
|
65
|
+
sessionId,
|
|
66
|
+
historyGeneration: this.generationProvider(this.sessionManager),
|
|
67
|
+
snapshotHead,
|
|
68
|
+
branchEntryIds: branch.map((entry) => entry.id),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
snapshotInvalidReason(snapshot) {
|
|
72
|
+
if (this.sessionManager.getSessionId() !== snapshot.sessionId)
|
|
132
73
|
return "session_replaced";
|
|
133
|
-
if (this.generationProvider(this.sessionManager) !==
|
|
74
|
+
if (this.generationProvider(this.sessionManager) !== snapshot.historyGeneration)
|
|
134
75
|
return "generation_changed";
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
if (snapshotIndex < 0 && !(cursor.branchEntryIds.length === 0 && cursor.snapshotHead === cursor.sessionId && currentBranch.length === 0))
|
|
138
|
-
return "branch_changed";
|
|
139
|
-
const currentPrefix = currentBranch.slice(0, cursor.branchEntryIds.length).map((entry) => entry.id);
|
|
140
|
-
if (!sameIds(currentPrefix, cursor.branchEntryIds))
|
|
76
|
+
const currentIds = this.sessionManager.getBranch().map((entry) => entry.id);
|
|
77
|
+
if (!sameIds(currentIds, snapshot.branchEntryIds))
|
|
141
78
|
return "branch_changed";
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return null;
|
|
79
|
+
if (snapshot.branchEntryIds.length === 0 && snapshot.snapshotHead === snapshot.sessionId)
|
|
80
|
+
return null;
|
|
81
|
+
const frozenIds = this.sessionManager.getBranch(snapshot.snapshotHead).map((entry) => entry.id);
|
|
82
|
+
return sameIds(frozenIds, snapshot.branchEntryIds) ? null : "branch_changed";
|
|
146
83
|
}
|
|
147
|
-
recoverRecords(
|
|
148
|
-
if (cursor.branchEntryIds.length === 0 && cursor.snapshotHead === cursor.sessionId)
|
|
149
|
-
return [];
|
|
150
|
-
const branch = this.sessionManager.getBranch(cursor.snapshotHead);
|
|
151
|
-
const positions = new Map();
|
|
152
|
-
for (const [position, entry] of branch.entries()) {
|
|
153
|
-
positions.set(entry.id, position);
|
|
154
|
-
if (entry.type === "message")
|
|
155
|
-
positions.set(`legacy:${entry.id}`, position);
|
|
156
|
-
if (entry.type === "custom" && entry.customType === TIMELINE_MARKER) {
|
|
157
|
-
const eventId = markerEventId(entry);
|
|
158
|
-
if (eventId)
|
|
159
|
-
positions.set(eventId, position);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
84
|
+
recoverRecords(snapshot) {
|
|
162
85
|
const records = [];
|
|
163
|
-
const
|
|
164
|
-
|
|
86
|
+
const eventIds = new Set();
|
|
87
|
+
const sequences = new Set();
|
|
88
|
+
for (const rawEvent of this.recoverFn(this.sessionManager)) {
|
|
165
89
|
const parsed = TimelineEventSchema.safeParse(rawEvent);
|
|
166
|
-
if (!parsed.success)
|
|
167
|
-
throw new Error("recovery returned an
|
|
90
|
+
if (!parsed.success || parsed.data.event_seq === undefined)
|
|
91
|
+
throw new Error("recovery returned an unsequenced timeline event");
|
|
168
92
|
const event = parsed.data;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
while (selected.length > 0) {
|
|
206
|
-
const firstPosition = selected[0].firstPosition;
|
|
207
|
-
const selectedSystems = systems.filter((record) => record.position >= firstPosition && record.position < position);
|
|
208
|
-
const selectedRecords = [...selected.flatMap((group) => group.records), ...selectedSystems]
|
|
209
|
-
.sort((left, right) => left.position - right.position || left.order - right.order);
|
|
210
|
-
const bytes = selectedRecords.reduce((total, record) => total + record.bytes, 0);
|
|
211
|
-
if (bytes <= MAX_WINDOW_DECODE_BYTES) {
|
|
212
|
-
return { kind: "ok", records: selectedRecords, nextPosition: firstPosition };
|
|
213
|
-
}
|
|
214
|
-
if (selected.length === 1) {
|
|
215
|
-
return { kind: "error", message: `group ${selected[0].groupId} exceeds ${MAX_WINDOW_DECODE_BYTES} bytes` };
|
|
216
|
-
}
|
|
217
|
-
selected = selected.slice(1);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
const selectedSystems = systems.filter((record) => record.position < position);
|
|
221
|
-
if (selectedSystems.length === 0) {
|
|
222
|
-
return { kind: "ok", records: [], nextPosition: 0 };
|
|
223
|
-
}
|
|
224
|
-
const systemBytes = selectedSystems.reduce((total, record) => total + record.bytes, 0);
|
|
225
|
-
if (systemBytes > MAX_WINDOW_DECODE_BYTES) {
|
|
226
|
-
return { kind: "error", message: `system history exceeds ${MAX_WINDOW_DECODE_BYTES} bytes` };
|
|
227
|
-
}
|
|
228
|
-
const nextPosition = Math.min(...selectedSystems.map((record) => record.position));
|
|
229
|
-
return {
|
|
230
|
-
kind: "ok",
|
|
231
|
-
records: [...selectedSystems].sort((left, right) => left.position - right.position || left.order - right.order),
|
|
232
|
-
nextPosition,
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
fragmentWindow(request, cursor, records, nextBefore) {
|
|
236
|
-
const tailOverhead = this.finalFrameBytes(request, cursor, { events: [], fragments: [] }, nextBefore)
|
|
237
|
-
- this.nonFinalFrameBytes(request, cursor, { events: [], fragments: [] });
|
|
238
|
-
const contentLimit = MAX_HISTORY_CHUNK_BYTES - tailOverhead;
|
|
93
|
+
if (event.session_id !== snapshot.sessionId || event.history_generation !== snapshot.historyGeneration) {
|
|
94
|
+
throw new Error("recovery returned an event outside the snapshot scope");
|
|
95
|
+
}
|
|
96
|
+
if (eventIds.has(event.event_id) || sequences.has(event.event_seq)) {
|
|
97
|
+
throw new Error("recovery returned duplicate timeline identity");
|
|
98
|
+
}
|
|
99
|
+
eventIds.add(event.event_id);
|
|
100
|
+
sequences.add(event.event_seq);
|
|
101
|
+
records.push({ event, bytes: getUtf8ByteLengthV2(event) });
|
|
102
|
+
}
|
|
103
|
+
records.sort((left, right) => left.event.event_seq - right.event.event_seq);
|
|
104
|
+
if (records.some((record, index) => record.event.event_seq !== index + 1)) {
|
|
105
|
+
throw new Error("recovery returned a non-dense timeline sequence");
|
|
106
|
+
}
|
|
107
|
+
return records;
|
|
108
|
+
}
|
|
109
|
+
selectWindow(records, before, limit) {
|
|
110
|
+
const lower = Math.max(1, before - limit);
|
|
111
|
+
const requested = records.filter((record) => record.event.event_seq >= lower && record.event.event_seq < before);
|
|
112
|
+
const selected = [];
|
|
113
|
+
let bytes = 0;
|
|
114
|
+
for (let index = requested.length - 1; index >= 0; index -= 1) {
|
|
115
|
+
const record = requested[index];
|
|
116
|
+
if (record.bytes > MAX_WINDOW_DECODE_BYTES) {
|
|
117
|
+
return { kind: "error", message: `event ${record.event.event_id} exceeds ${MAX_WINDOW_DECODE_BYTES} bytes` };
|
|
118
|
+
}
|
|
119
|
+
if (bytes + record.bytes > MAX_WINDOW_DECODE_BYTES)
|
|
120
|
+
break;
|
|
121
|
+
selected.unshift(record);
|
|
122
|
+
bytes += record.bytes;
|
|
123
|
+
}
|
|
124
|
+
return { kind: "ok", records: selected };
|
|
125
|
+
}
|
|
126
|
+
fragmentWindow(request, snapshot, records, nextBefore) {
|
|
127
|
+
const tailOverhead = this.finalFrameBytes(request, snapshot, { events: [], fragments: [] }, nextBefore)
|
|
128
|
+
- this.nonFinalFrameBytes(request, snapshot, { events: [], fragments: [] });
|
|
239
129
|
const items = [];
|
|
240
130
|
for (const record of records) {
|
|
241
|
-
const fullFrame = this.makeChunk(request,
|
|
131
|
+
const fullFrame = this.makeChunk(request, snapshot, { events: [record.event], fragments: [] }, true, nextBefore);
|
|
242
132
|
if (getUtf8ByteLengthV2(fullFrame) <= MAX_HISTORY_CHUNK_BYTES) {
|
|
243
133
|
items.push({ kind: "event", event: record.event });
|
|
244
134
|
continue;
|
|
@@ -262,7 +152,7 @@ export class TimelineHistoryPager {
|
|
|
262
152
|
for (const item of items) {
|
|
263
153
|
const repeatedFragment = item.kind === "fragment" && current.fragments.some((fragment) => fragment.event_id === item.eventId);
|
|
264
154
|
const candidate = appendItem(current, item);
|
|
265
|
-
if (!repeatedFragment && this.nonFinalFrameBytes(request,
|
|
155
|
+
if (!repeatedFragment && this.nonFinalFrameBytes(request, snapshot, candidate) + tailOverhead <= MAX_HISTORY_CHUNK_BYTES) {
|
|
266
156
|
current = candidate;
|
|
267
157
|
continue;
|
|
268
158
|
}
|
|
@@ -276,44 +166,38 @@ export class TimelineHistoryPager {
|
|
|
276
166
|
payloads.push(current);
|
|
277
167
|
return payloads.map((payload, chunkIndex) => {
|
|
278
168
|
const final = chunkIndex === payloads.length - 1;
|
|
279
|
-
const frame = this.makeChunk(request,
|
|
280
|
-
if (getUtf8ByteLengthV2(frame) > MAX_HISTORY_CHUNK_BYTES)
|
|
169
|
+
const frame = this.makeChunk(request, snapshot, payload, final, nextBefore, chunkIndex);
|
|
170
|
+
if (getUtf8ByteLengthV2(frame) > MAX_HISTORY_CHUNK_BYTES)
|
|
281
171
|
throw new HistoryTooLargeError("history chunk exceeds 512 KiB");
|
|
282
|
-
}
|
|
283
172
|
encodeServerFrameV2(frame);
|
|
284
173
|
return frame;
|
|
285
174
|
});
|
|
286
175
|
}
|
|
287
|
-
makeChunk(request,
|
|
176
|
+
makeChunk(request, snapshot, payload, final, nextBefore, chunkIndex = 0) {
|
|
288
177
|
const tail = final
|
|
289
|
-
? nextBefore
|
|
290
|
-
? { final_chunk: true, eos:
|
|
291
|
-
: { final_chunk: true, eos:
|
|
178
|
+
? nextBefore === undefined
|
|
179
|
+
? { final_chunk: true, eos: true }
|
|
180
|
+
: { final_chunk: true, eos: false, next_before: nextBefore }
|
|
292
181
|
: { final_chunk: false };
|
|
293
182
|
return {
|
|
294
183
|
protocol_version: 2,
|
|
295
184
|
type: "session_history_chunk",
|
|
296
185
|
target_channel_id: request.targetChannelId,
|
|
297
186
|
in_reply_to: request.requestId,
|
|
298
|
-
session_id:
|
|
299
|
-
history_generation:
|
|
300
|
-
snapshot_head:
|
|
187
|
+
session_id: snapshot.sessionId,
|
|
188
|
+
history_generation: snapshot.historyGeneration,
|
|
189
|
+
snapshot_head: snapshot.snapshotHead,
|
|
301
190
|
chunk_index: chunkIndex,
|
|
302
191
|
events: payload.events,
|
|
303
192
|
fragments: payload.fragments,
|
|
304
193
|
...tail,
|
|
305
194
|
};
|
|
306
195
|
}
|
|
307
|
-
nonFinalFrameBytes(request,
|
|
308
|
-
return getUtf8ByteLengthV2(this.makeChunk(request,
|
|
309
|
-
}
|
|
310
|
-
finalFrameBytes(request, cursor, payload, nextBefore) {
|
|
311
|
-
return getUtf8ByteLengthV2(this.makeChunk(request, cursor, payload, true, nextBefore));
|
|
196
|
+
nonFinalFrameBytes(request, snapshot, payload) {
|
|
197
|
+
return getUtf8ByteLengthV2(this.makeChunk(request, snapshot, payload, false, undefined));
|
|
312
198
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
this.cursors.set(token, state);
|
|
316
|
-
return token;
|
|
199
|
+
finalFrameBytes(request, snapshot, payload, nextBefore) {
|
|
200
|
+
return getUtf8ByteLengthV2(this.makeChunk(request, snapshot, payload, true, nextBefore));
|
|
317
201
|
}
|
|
318
202
|
protocolError(request, code, message) {
|
|
319
203
|
return {
|
|
@@ -325,13 +209,13 @@ export class TimelineHistoryPager {
|
|
|
325
209
|
message,
|
|
326
210
|
};
|
|
327
211
|
}
|
|
328
|
-
reset(request,
|
|
212
|
+
reset(request, snapshot, reason) {
|
|
329
213
|
return {
|
|
330
214
|
protocol_version: 2,
|
|
331
215
|
type: "reset",
|
|
332
216
|
target_channel_id: request.targetChannelId,
|
|
333
|
-
session_id:
|
|
334
|
-
history_generation:
|
|
217
|
+
session_id: this.sessionManager.getSessionId(),
|
|
218
|
+
history_generation: this.generationProvider(this.sessionManager),
|
|
335
219
|
reason,
|
|
336
220
|
};
|
|
337
221
|
}
|
|
@@ -345,37 +229,17 @@ class HistoryTooLargeError extends Error {
|
|
|
345
229
|
function sameIds(left, right) {
|
|
346
230
|
return left.length === right.length && left.every((id, index) => id === right[index]);
|
|
347
231
|
}
|
|
348
|
-
function normalizeLimit(limit) {
|
|
349
|
-
if (limit === undefined || !Number.isFinite(limit))
|
|
350
|
-
return MAX_GROUPS_PER_PAGE;
|
|
351
|
-
return Math.min(MAX_GROUPS_PER_PAGE, Math.max(1, Math.floor(limit)));
|
|
352
|
-
}
|
|
353
|
-
function markerEventId(entry) {
|
|
354
|
-
if (entry.type !== "custom" || entry.customType !== TIMELINE_MARKER || !entry.data)
|
|
355
|
-
return undefined;
|
|
356
|
-
if (typeof entry.data !== "object" || Array.isArray(entry.data))
|
|
357
|
-
return undefined;
|
|
358
|
-
const eventId = entry.data.event_id;
|
|
359
|
-
return typeof eventId === "string" ? eventId : undefined;
|
|
360
|
-
}
|
|
361
|
-
function isSystemEvent(event) {
|
|
362
|
-
return event.kind === "compaction" || event.kind === "branch_summary" || event.kind === "custom";
|
|
363
|
-
}
|
|
364
232
|
function appendItem(payload, item) {
|
|
365
|
-
if (item.kind === "event")
|
|
233
|
+
if (item.kind === "event")
|
|
366
234
|
return { events: [...payload.events, item.event], fragments: [...payload.fragments] };
|
|
367
|
-
}
|
|
368
235
|
return {
|
|
369
236
|
events: [...payload.events],
|
|
370
|
-
fragments: [
|
|
371
|
-
...payload.fragments,
|
|
372
|
-
{
|
|
237
|
+
fragments: [...payload.fragments, {
|
|
373
238
|
event_id: item.eventId,
|
|
374
239
|
index: item.index,
|
|
375
240
|
data_base64: item.dataBase64,
|
|
376
241
|
final: item.final,
|
|
377
|
-
},
|
|
378
|
-
],
|
|
242
|
+
}],
|
|
379
243
|
};
|
|
380
244
|
}
|
|
381
245
|
//# sourceMappingURL=history.js.map
|