@cjhyy/code-shell-server 0.9.35 → 0.9.37
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/dist/bin/code-shell-cloud-account.d.ts +2 -0
- package/dist/bin/code-shell-cloud-account.js +53 -0
- package/dist/index.serve.d.ts +3 -0
- package/dist/index.serve.js +2 -0
- package/dist/mobile-remote/mobile-client-event-validator.js +10 -1
- package/dist/mobile-remote/room-history.d.ts +20 -0
- package/dist/mobile-remote/room-history.js +327 -0
- package/dist/mobile-remote/room-manager.d.ts +11 -0
- package/dist/mobile-remote/room-manager.js +107 -11
- package/dist/panels/management.js +12 -1
- package/dist/panels/runtime-services.js +32 -7
- package/dist/panels/types.d.ts +5 -3
- package/dist/project-runtime/account-adoption.d.ts +34 -0
- package/dist/project-runtime/account-adoption.js +280 -0
- package/dist/project-runtime/account-binding.d.ts +12 -0
- package/dist/project-runtime/account-binding.js +33 -0
- package/dist/project-runtime/backup.d.ts +7 -0
- package/dist/project-runtime/backup.js +56 -8
- package/dist/project-runtime/cloud-account-auth.d.ts +23 -0
- package/dist/project-runtime/cloud-account-auth.js +287 -0
- package/dist/project-runtime/control-authority.d.ts +46 -0
- package/dist/project-runtime/control-authority.js +15 -0
- package/dist/project-runtime/control-server.d.ts +3 -0
- package/dist/project-runtime/control-server.js +60 -24
- package/dist/project-runtime/lock.js +181 -12
- package/dist/project-runtime/manager.d.ts +12 -8
- package/dist/project-runtime/manager.js +56 -15
- package/dist/project-runtime/proxy.d.ts +3 -3
- package/dist/project-runtime/proxy.js +4 -1
- package/dist/project-runtime/registry.d.ts +14 -1
- package/dist/project-runtime/registry.js +91 -2
- package/package.json +7 -6
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { adoptCloudAccount, inspectCloudAccountAdoption, } from "../project-runtime/account-adoption.js";
|
|
3
|
+
const help = `Usage: code-shell-cloud-account --inspect --data-dir <canonical path>
|
|
4
|
+
code-shell-cloud-account --adopt --data-dir <canonical path>
|
|
5
|
+
--evidence-dir <new private directory outside data-dir>
|
|
6
|
+
--account-origin <exact Account HTTPS origin> --account-id <UUID>
|
|
7
|
+
--installation-id <reviewed UUID> --owner-id <reviewed UUID>
|
|
8
|
+
--legacy-username <reviewed sole operator>
|
|
9
|
+
|
|
10
|
+
Stop the Cloud controller and all projects first. Adoption durably preserves
|
|
11
|
+
original authority files and SHA receipts before retiring legacy login. Evidence
|
|
12
|
+
contains private password hashes. Make a full installation/volume backup too.
|
|
13
|
+
Retry a failed adoption with exactly the same source, evidence and identities.
|
|
14
|
+
This tool never contacts Account, accepts a password/token, or runs Docker.
|
|
15
|
+
The bound Account must authenticate through the configured Cloud at first use.`;
|
|
16
|
+
try {
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
if (args.length === 1 && ["--help", "-h"].includes(args[0]))
|
|
19
|
+
console.log(help);
|
|
20
|
+
else {
|
|
21
|
+
const mode = args.shift();
|
|
22
|
+
if (mode !== "--inspect" && mode !== "--adopt")
|
|
23
|
+
throw new Error("Choose --inspect or --adopt.");
|
|
24
|
+
const names = {
|
|
25
|
+
"--data-dir": "dataDir",
|
|
26
|
+
"--evidence-dir": "evidenceDirectory",
|
|
27
|
+
"--account-origin": "accountOrigin",
|
|
28
|
+
"--account-id": "accountId",
|
|
29
|
+
"--installation-id": "installationId",
|
|
30
|
+
"--owner-id": "ownerId",
|
|
31
|
+
"--legacy-username": "legacyUsername",
|
|
32
|
+
};
|
|
33
|
+
const options = {};
|
|
34
|
+
while (args.length) {
|
|
35
|
+
const name = args.shift(), key = names[name], value = args.shift();
|
|
36
|
+
if (!key || !value || value.startsWith("--") || key in options)
|
|
37
|
+
throw new Error("Unknown, duplicate or incomplete option.");
|
|
38
|
+
options[key] = value;
|
|
39
|
+
}
|
|
40
|
+
if (!options.dataDir ||
|
|
41
|
+
(mode === "--inspect"
|
|
42
|
+
? Object.keys(options).length !== 1
|
|
43
|
+
: Object.keys(options).length !== Object.keys(names).length))
|
|
44
|
+
throw new Error("Supply every reviewed identity; use --help for usage.");
|
|
45
|
+
console.log(JSON.stringify(mode === "--inspect"
|
|
46
|
+
? inspectCloudAccountAdoption(options.dataDir)
|
|
47
|
+
: adoptCloudAccount(options), null, 2));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
console.error(error instanceof Error ? error.message : "Cloud account adoption failed.");
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
}
|
package/dist/index.serve.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/** Ready-made headless HTTP/WebSocket host and CLI composition helpers. */
|
|
2
2
|
export * from "./serve/headless-server.js";
|
|
3
3
|
export { startProjectControlServer, type ProjectControlServerOptions, } from "./project-runtime/control-server.js";
|
|
4
|
+
export { CLOUD_MANAGEMENT_AUTHORITY_VERSION } from "./project-runtime/cloud-account-auth.js";
|
|
5
|
+
export { adoptCloudAccount, inspectCloudAccountAdoption, type CloudAccountAdoptionOptions, } from "./project-runtime/account-adoption.js";
|
|
6
|
+
export type { CloudAccountAuthority, CloudAccountPrincipal, CloudManagementSession, } from "./project-runtime/control-authority.js";
|
|
4
7
|
export { parseServeArgs, resolveWebAppRoot, resolveWorkerCapabilityModules, resolveWorkerEntry, runServeCli, } from "./serve/cli.js";
|
|
5
8
|
export { backupProjectInstallation, restoreProjectInstallation, type ProjectBackupOptions, type ProjectRestoreOptions, } from "./project-runtime/backup.js";
|
|
6
9
|
export { readProjectSeccompProfile, type ProjectSeccompProfile, } from "./project-runtime/seccomp.js";
|
package/dist/index.serve.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** Ready-made headless HTTP/WebSocket host and CLI composition helpers. */
|
|
2
2
|
export * from "./serve/headless-server.js";
|
|
3
3
|
export { startProjectControlServer, } from "./project-runtime/control-server.js";
|
|
4
|
+
export { CLOUD_MANAGEMENT_AUTHORITY_VERSION } from "./project-runtime/cloud-account-auth.js";
|
|
5
|
+
export { adoptCloudAccount, inspectCloudAccountAdoption, } from "./project-runtime/account-adoption.js";
|
|
4
6
|
export { parseServeArgs, resolveWebAppRoot, resolveWorkerCapabilityModules, resolveWorkerEntry, runServeCli, } from "./serve/cli.js";
|
|
5
7
|
export { backupProjectInstallation, restoreProjectInstallation, } from "./project-runtime/backup.js";
|
|
6
8
|
export { readProjectSeccompProfile, } from "./project-runtime/seccomp.js";
|
|
@@ -135,7 +135,16 @@ export function parseMobileClientEvent(value) {
|
|
|
135
135
|
break;
|
|
136
136
|
case "room.history":
|
|
137
137
|
valid =
|
|
138
|
-
boundedString(event.roomId, MAX_ID_LENGTH) &&
|
|
138
|
+
boundedString(event.roomId, MAX_ID_LENGTH) &&
|
|
139
|
+
optionalNonNegativeInteger(event.sinceSeq) &&
|
|
140
|
+
(event.page === undefined || typeof event.page === "boolean") &&
|
|
141
|
+
optionalBoundedString(event.cursor, 2048) &&
|
|
142
|
+
optionalBoundedString(event.requestId, MAX_ID_LENGTH) &&
|
|
143
|
+
(event.limit === undefined ||
|
|
144
|
+
(Number.isSafeInteger(event.limit) &&
|
|
145
|
+
event.limit >= 1 &&
|
|
146
|
+
event.limit <= 500)) &&
|
|
147
|
+
(event.cursor === undefined || (event.page === true && event.sinceSeq === undefined));
|
|
139
148
|
break;
|
|
140
149
|
case "ccRoom.probe":
|
|
141
150
|
valid =
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Room-local history coordinates. These are never native Session/Run cursors. */
|
|
2
|
+
export interface RoomHistoryPage<T = unknown> {
|
|
3
|
+
snapshotId: string;
|
|
4
|
+
throughSeq: number;
|
|
5
|
+
messages: T[];
|
|
6
|
+
nextCursor?: string;
|
|
7
|
+
complete: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface RoomHistoryRequest {
|
|
10
|
+
sinceSeq?: number;
|
|
11
|
+
cursor?: string;
|
|
12
|
+
limit?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function scanRoomHistory<T extends {
|
|
15
|
+
seq: number;
|
|
16
|
+
}>(file: string, parse: (value: unknown) => T | undefined, visit?: (message: T) => void): number;
|
|
17
|
+
/** Durable immutable copies keep a frozen page boundary valid across Host restarts. */
|
|
18
|
+
export declare function readRoomHistoryPage<T extends {
|
|
19
|
+
seq: number;
|
|
20
|
+
}>(roomDir: string, roomId: string, request: RoomHistoryRequest, parse: (value: unknown) => T | undefined): RoomHistoryPage<T>;
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { closeSync, constants, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, writeFileSync, writeSync, } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const MAX_BYTES = 256 * 1024 * 1024;
|
|
5
|
+
const MAX_LINE = 4 * 1024 * 1024;
|
|
6
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
7
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
8
|
+
function regular(path) {
|
|
9
|
+
const stat = lstatSync(path);
|
|
10
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) {
|
|
11
|
+
throw new Error("room history must be a private regular file");
|
|
12
|
+
}
|
|
13
|
+
return stat;
|
|
14
|
+
}
|
|
15
|
+
function stamp(stat) {
|
|
16
|
+
return [stat.dev, stat.ino, stat.size, stat.mtimeMs, stat.ctimeMs].join(":");
|
|
17
|
+
}
|
|
18
|
+
function openRegular(path) {
|
|
19
|
+
const before = regular(path);
|
|
20
|
+
const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
21
|
+
if (stamp(before) !== stamp(fstatSync(fd))) {
|
|
22
|
+
closeSync(fd);
|
|
23
|
+
throw new Error("room history changed while opening");
|
|
24
|
+
}
|
|
25
|
+
return fd;
|
|
26
|
+
}
|
|
27
|
+
function syncDirectory(path) {
|
|
28
|
+
if (process.platform === "win32")
|
|
29
|
+
return;
|
|
30
|
+
const fd = openSync(path, "r");
|
|
31
|
+
try {
|
|
32
|
+
fsyncSync(fd);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
closeSync(fd);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Bounded memory, strict complete records, exact physical offsets. */
|
|
39
|
+
function* lines(fd, start, end, hashes) {
|
|
40
|
+
let offset = start;
|
|
41
|
+
let carry = Buffer.alloc(0);
|
|
42
|
+
while (offset < end) {
|
|
43
|
+
const blockStart = hashes ? Math.floor(offset / 65536) * 65536 : offset;
|
|
44
|
+
const data = Buffer.allocUnsafe(Math.min(65536, end - blockStart));
|
|
45
|
+
let total = 0;
|
|
46
|
+
while (total < data.length) {
|
|
47
|
+
const count = readSync(fd, data, total, data.length - total, blockStart + total);
|
|
48
|
+
if (!count)
|
|
49
|
+
throw new Error("room history was truncated while reading");
|
|
50
|
+
total += count;
|
|
51
|
+
}
|
|
52
|
+
if (hashes && createHash("sha256").update(data).digest("hex") !== hashes[blockStart / 65536]) {
|
|
53
|
+
throw new Error("room history snapshot checksum mismatch");
|
|
54
|
+
}
|
|
55
|
+
const available = data.subarray(offset - blockStart);
|
|
56
|
+
const count = available.length;
|
|
57
|
+
offset += count;
|
|
58
|
+
const chunk = Buffer.concat([carry, available]);
|
|
59
|
+
let from = 0;
|
|
60
|
+
for (;;) {
|
|
61
|
+
const newline = chunk.indexOf(0x0a, from);
|
|
62
|
+
if (newline < 0)
|
|
63
|
+
break;
|
|
64
|
+
if (newline - from > MAX_LINE)
|
|
65
|
+
throw new Error("room history record exceeds size limit");
|
|
66
|
+
yield {
|
|
67
|
+
text: decoder.decode(chunk.subarray(from, newline)),
|
|
68
|
+
end: offset - chunk.length + newline + 1,
|
|
69
|
+
};
|
|
70
|
+
from = newline + 1;
|
|
71
|
+
}
|
|
72
|
+
carry = Buffer.from(chunk.subarray(from));
|
|
73
|
+
if (carry.length > MAX_LINE)
|
|
74
|
+
throw new Error("room history record exceeds size limit");
|
|
75
|
+
}
|
|
76
|
+
if (carry.length)
|
|
77
|
+
throw new Error("room history has an incomplete final record");
|
|
78
|
+
}
|
|
79
|
+
export function scanRoomHistory(file, parse, visit) {
|
|
80
|
+
const fd = openRegular(file);
|
|
81
|
+
try {
|
|
82
|
+
const before = fstatSync(fd);
|
|
83
|
+
if (before.size > MAX_BYTES)
|
|
84
|
+
throw new Error("room history exceeds 256 MiB; archive it before continuing");
|
|
85
|
+
let seq = 0;
|
|
86
|
+
for (const line of lines(fd, 0, before.size)) {
|
|
87
|
+
const msg = parse(JSON.parse(line.text));
|
|
88
|
+
if (!msg || msg.seq !== seq + 1)
|
|
89
|
+
throw new Error(`room history is corrupt at sequence ${seq + 1}`);
|
|
90
|
+
seq = msg.seq;
|
|
91
|
+
visit?.(msg);
|
|
92
|
+
}
|
|
93
|
+
if (stamp(before) !== stamp(fstatSync(fd)) || stamp(before) !== stamp(regular(file))) {
|
|
94
|
+
throw new Error("room history changed while scanning");
|
|
95
|
+
}
|
|
96
|
+
return seq;
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
closeSync(fd);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function validateManifest(value, roomId, snapshotId) {
|
|
103
|
+
if (!value ||
|
|
104
|
+
value.version !== 1 ||
|
|
105
|
+
value.roomId !== roomId ||
|
|
106
|
+
value.snapshotId !== snapshotId ||
|
|
107
|
+
!Number.isSafeInteger(value.bytes) ||
|
|
108
|
+
value.bytes < 0 ||
|
|
109
|
+
value.bytes > MAX_BYTES ||
|
|
110
|
+
!Number.isSafeInteger(value.throughSeq) ||
|
|
111
|
+
value.throughSeq < 0 ||
|
|
112
|
+
!Number.isSafeInteger(value.sinceSeq) ||
|
|
113
|
+
value.sinceSeq < 0 ||
|
|
114
|
+
value.sinceSeq > value.throughSeq ||
|
|
115
|
+
typeof value.key !== "string" ||
|
|
116
|
+
!/^[0-9a-f]{64}$/.test(value.key) ||
|
|
117
|
+
typeof value.stamp !== "string" ||
|
|
118
|
+
!/^[0-9:.]+$/.test(value.stamp) ||
|
|
119
|
+
!Array.isArray(value.hashes) ||
|
|
120
|
+
value.hashes.length !== Math.ceil(value.bytes / 65536) ||
|
|
121
|
+
value.hashes.some((hash) => typeof hash !== "string" || !/^[0-9a-f]{64}$/.test(hash)))
|
|
122
|
+
throw new Error("invalid history manifest");
|
|
123
|
+
}
|
|
124
|
+
/** Durable immutable copies keep a frozen page boundary valid across Host restarts. */
|
|
125
|
+
export function readRoomHistoryPage(roomDir, roomId, request, parse) {
|
|
126
|
+
const limit = request.limit ?? 200;
|
|
127
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500)
|
|
128
|
+
throw new Error("invalid history page limit");
|
|
129
|
+
if (request.sinceSeq !== undefined &&
|
|
130
|
+
(!Number.isSafeInteger(request.sinceSeq) || request.sinceSeq < 0)) {
|
|
131
|
+
throw new Error("invalid room sequence");
|
|
132
|
+
}
|
|
133
|
+
const directory = join(roomDir, "history-snapshots");
|
|
134
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
135
|
+
const directoryStat = lstatSync(directory);
|
|
136
|
+
if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink())
|
|
137
|
+
throw new Error("invalid history snapshot directory");
|
|
138
|
+
const assertDirectory = () => {
|
|
139
|
+
const current = lstatSync(directory);
|
|
140
|
+
if (!current.isDirectory() ||
|
|
141
|
+
current.isSymbolicLink() ||
|
|
142
|
+
current.dev !== directoryStat.dev ||
|
|
143
|
+
current.ino !== directoryStat.ino)
|
|
144
|
+
throw new Error("room history snapshot directory changed");
|
|
145
|
+
};
|
|
146
|
+
let manifest;
|
|
147
|
+
let cursor;
|
|
148
|
+
if (request.cursor !== undefined) {
|
|
149
|
+
if (request.sinceSeq !== undefined || request.cursor.length > 2048)
|
|
150
|
+
throw new Error("invalid history cursor");
|
|
151
|
+
const [body, signature, extra] = request.cursor.split(".");
|
|
152
|
+
if (!body || !signature || extra)
|
|
153
|
+
throw new Error("invalid history cursor");
|
|
154
|
+
cursor = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
|
|
155
|
+
if (!UUID.test(cursor.snapshotId))
|
|
156
|
+
throw new Error("invalid history snapshot id");
|
|
157
|
+
const path = join(directory, `${cursor.snapshotId}.json`);
|
|
158
|
+
const metaFd = openRegular(path);
|
|
159
|
+
try {
|
|
160
|
+
const before = fstatSync(metaFd);
|
|
161
|
+
if (before.size > 512 * 1024)
|
|
162
|
+
throw new Error("invalid history manifest");
|
|
163
|
+
manifest = JSON.parse(readFileSync(metaFd, "utf8"));
|
|
164
|
+
if (stamp(before) !== stamp(fstatSync(metaFd)) || stamp(before) !== stamp(regular(path)))
|
|
165
|
+
throw new Error("room history manifest changed while reading");
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
closeSync(metaFd);
|
|
169
|
+
}
|
|
170
|
+
validateManifest(manifest, roomId, cursor.snapshotId);
|
|
171
|
+
const expected = createHmac("sha256", manifest.key).update(body).digest();
|
|
172
|
+
const received = Buffer.from(signature, "base64url");
|
|
173
|
+
if (received.length !== expected.length || !timingSafeEqual(expected, received))
|
|
174
|
+
throw new Error("invalid history cursor signature");
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
const snapshotId = randomUUID();
|
|
178
|
+
const source = join(roomDir, "messages.jsonl");
|
|
179
|
+
const file = join(directory, `${snapshotId}.jsonl`);
|
|
180
|
+
const input = openRegular(source);
|
|
181
|
+
let output;
|
|
182
|
+
try {
|
|
183
|
+
output = openSync(file, "wx", 0o600);
|
|
184
|
+
const before = fstatSync(input);
|
|
185
|
+
if (before.size > MAX_BYTES)
|
|
186
|
+
throw new Error("room history exceeds 256 MiB; archive it before continuing");
|
|
187
|
+
const hashes = [];
|
|
188
|
+
let offset = 0;
|
|
189
|
+
while (offset < before.size) {
|
|
190
|
+
const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, before.size - offset));
|
|
191
|
+
let count = 0;
|
|
192
|
+
while (count < chunk.length) {
|
|
193
|
+
const read = readSync(input, chunk, count, chunk.length - count, offset + count);
|
|
194
|
+
if (!read)
|
|
195
|
+
throw new Error("room history was truncated while freezing");
|
|
196
|
+
count += read;
|
|
197
|
+
}
|
|
198
|
+
hashes.push(createHash("sha256").update(chunk).digest("hex"));
|
|
199
|
+
let written = 0;
|
|
200
|
+
while (written < count) {
|
|
201
|
+
const progress = writeSync(output, chunk, written, count - written);
|
|
202
|
+
if (!progress)
|
|
203
|
+
throw new Error("room history freeze write made no progress");
|
|
204
|
+
written += progress;
|
|
205
|
+
}
|
|
206
|
+
offset += count;
|
|
207
|
+
}
|
|
208
|
+
fsyncSync(output);
|
|
209
|
+
if (stamp(before) !== stamp(fstatSync(input)) || stamp(before) !== stamp(regular(source))) {
|
|
210
|
+
throw new Error("room history changed while freezing");
|
|
211
|
+
}
|
|
212
|
+
const throughSeq = scanRoomHistory(file, parse);
|
|
213
|
+
manifest = {
|
|
214
|
+
version: 1,
|
|
215
|
+
roomId,
|
|
216
|
+
snapshotId,
|
|
217
|
+
bytes: before.size,
|
|
218
|
+
throughSeq,
|
|
219
|
+
sinceSeq: request.sinceSeq ?? 0,
|
|
220
|
+
key: randomBytes(32).toString("hex"),
|
|
221
|
+
stamp: stamp(fstatSync(output)),
|
|
222
|
+
hashes,
|
|
223
|
+
};
|
|
224
|
+
const temporary = join(directory, `${snapshotId}.tmp`);
|
|
225
|
+
writeFileSync(temporary, JSON.stringify(manifest), { flag: "wx", mode: 0o600 });
|
|
226
|
+
const metaFd = openRegular(temporary);
|
|
227
|
+
try {
|
|
228
|
+
fsyncSync(metaFd);
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
closeSync(metaFd);
|
|
232
|
+
}
|
|
233
|
+
assertDirectory();
|
|
234
|
+
renameSync(temporary, join(directory, `${snapshotId}.json`));
|
|
235
|
+
syncDirectory(directory);
|
|
236
|
+
cursor = { snapshotId, offset: 0, afterSeq: 0 };
|
|
237
|
+
// Old cursors fail explicitly when their retained copy expires. Never
|
|
238
|
+
// remove another in-progress copy or an unknown directory entry.
|
|
239
|
+
try {
|
|
240
|
+
const old = readdirSync(directory)
|
|
241
|
+
.filter((name) => UUID.test(name.replace(/\.json$/, "")) && name.endsWith(".json"))
|
|
242
|
+
.map((name) => ({ name, time: regular(join(directory, name)).mtimeMs }))
|
|
243
|
+
.sort((a, b) => b.time - a.time)
|
|
244
|
+
.slice(8);
|
|
245
|
+
for (const item of old) {
|
|
246
|
+
if (item.name === `${snapshotId}.json`)
|
|
247
|
+
continue;
|
|
248
|
+
rmSync(join(directory, item.name));
|
|
249
|
+
rmSync(join(directory, item.name.replace(/\.json$/, ".jsonl")), { force: true });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
/* Retention failure must not invalidate the successfully published snapshot. */
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
// Remove the publication marker before its data on an unpublished/failed
|
|
258
|
+
// freeze. Retention failures are isolated after successful publication.
|
|
259
|
+
rmSync(join(directory, `${snapshotId}.json`), { force: true });
|
|
260
|
+
rmSync(join(directory, `${snapshotId}.tmp`), { force: true });
|
|
261
|
+
rmSync(file, { force: true });
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
closeSync(input);
|
|
266
|
+
if (output !== undefined)
|
|
267
|
+
closeSync(output);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (manifest.version !== 1 ||
|
|
271
|
+
manifest.roomId !== roomId ||
|
|
272
|
+
manifest.snapshotId !== cursor.snapshotId ||
|
|
273
|
+
!Number.isSafeInteger(manifest.bytes) ||
|
|
274
|
+
manifest.bytes < 0 ||
|
|
275
|
+
manifest.bytes > MAX_BYTES ||
|
|
276
|
+
!Number.isSafeInteger(manifest.throughSeq) ||
|
|
277
|
+
manifest.throughSeq < 0 ||
|
|
278
|
+
!Number.isSafeInteger(cursor.offset) ||
|
|
279
|
+
cursor.offset < 0 ||
|
|
280
|
+
cursor.offset > manifest.bytes ||
|
|
281
|
+
!Number.isSafeInteger(cursor.afterSeq) ||
|
|
282
|
+
cursor.afterSeq < 0 ||
|
|
283
|
+
cursor.afterSeq > manifest.throughSeq) {
|
|
284
|
+
throw new Error("invalid room history snapshot");
|
|
285
|
+
}
|
|
286
|
+
assertDirectory();
|
|
287
|
+
validateManifest(manifest, roomId, cursor.snapshotId);
|
|
288
|
+
const file = join(directory, `${cursor.snapshotId}.jsonl`);
|
|
289
|
+
const fd = openRegular(file);
|
|
290
|
+
const messages = [];
|
|
291
|
+
try {
|
|
292
|
+
if (stamp(fstatSync(fd)) !== manifest.stamp)
|
|
293
|
+
throw new Error("room history snapshot was modified");
|
|
294
|
+
for (const line of lines(fd, cursor.offset, manifest.bytes, manifest.hashes)) {
|
|
295
|
+
const msg = parse(JSON.parse(line.text));
|
|
296
|
+
if (!msg || msg.seq !== cursor.afterSeq + 1)
|
|
297
|
+
throw new Error("room history snapshot is corrupt");
|
|
298
|
+
cursor.offset = line.end;
|
|
299
|
+
cursor.afterSeq = msg.seq;
|
|
300
|
+
if (msg.seq > manifest.sinceSeq)
|
|
301
|
+
messages.push(msg);
|
|
302
|
+
if (messages.length === limit)
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
if (stamp(fstatSync(fd)) !== manifest.stamp || stamp(regular(file)) !== manifest.stamp)
|
|
306
|
+
throw new Error("room history snapshot changed while paging");
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
closeSync(fd);
|
|
310
|
+
}
|
|
311
|
+
assertDirectory();
|
|
312
|
+
const complete = cursor.offset === manifest.bytes;
|
|
313
|
+
if (complete && cursor.afterSeq !== manifest.throughSeq)
|
|
314
|
+
throw new Error("incomplete room history snapshot");
|
|
315
|
+
const body = Buffer.from(JSON.stringify(cursor)).toString("base64url");
|
|
316
|
+
return {
|
|
317
|
+
snapshotId: manifest.snapshotId,
|
|
318
|
+
throughSeq: manifest.throughSeq,
|
|
319
|
+
messages,
|
|
320
|
+
complete,
|
|
321
|
+
...(!complete
|
|
322
|
+
? {
|
|
323
|
+
nextCursor: `${body}.${createHmac("sha256", manifest.key).update(body).digest("base64url")}`,
|
|
324
|
+
}
|
|
325
|
+
: {}),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type RoomHistoryRequest, type RoomHistoryPage } from "./room-history.js";
|
|
2
|
+
export type { RoomHistoryPage, RoomHistoryRequest } from "./room-history.js";
|
|
1
3
|
import type { ResidentAgentEvent } from "./resident-agent.js";
|
|
2
4
|
import type { InputAttachmentMeta } from "../attachment-service.js";
|
|
3
5
|
export declare function isValidRoomId(id: unknown): id is string;
|
|
@@ -73,6 +75,8 @@ export interface RoomMessage {
|
|
|
73
75
|
* card expands to so the real parameters are visible. Absent on legacy
|
|
74
76
|
* messages that predate args persistence. */
|
|
75
77
|
args?: Record<string, unknown>;
|
|
78
|
+
/** Durable external transcript provenance, scoped to its actual file incarnation. */
|
|
79
|
+
transcriptSource?: string;
|
|
76
80
|
attachments?: Array<{
|
|
77
81
|
name: string;
|
|
78
82
|
mime?: string;
|
|
@@ -156,6 +160,7 @@ export declare class RoomManager {
|
|
|
156
160
|
/** User turns are echoed immediately for responsive room UX. When a
|
|
157
161
|
* transcript follower later sees the same CLI-written user line, this queue
|
|
158
162
|
* suppresses that duplicate. */
|
|
163
|
+
private readonly transcriptSources;
|
|
159
164
|
private pendingTranscriptUserEchoes;
|
|
160
165
|
/**
|
|
161
166
|
* Rooms whose agent events must be buffered instead of handled immediately.
|
|
@@ -172,12 +177,16 @@ export declare class RoomManager {
|
|
|
172
177
|
* JSONL. Seeded lazily from disk on the first append after startup; dropped
|
|
173
178
|
* when the room's messages are deleted so the next append re-derives it.
|
|
174
179
|
*/
|
|
180
|
+
private readonly messageStampByRoom;
|
|
181
|
+
private messageStamp;
|
|
175
182
|
private lastSeqByRoom;
|
|
176
183
|
/** Throttled lastActiveAt bookkeeping — see touchLastActive. */
|
|
177
184
|
private pendingLastActive;
|
|
178
185
|
private lastActiveWrittenAt;
|
|
179
186
|
private now;
|
|
180
187
|
private readonly rootRealPath;
|
|
188
|
+
private readonly rootIdentity;
|
|
189
|
+
private readonly failedWrites;
|
|
181
190
|
/** Drop all pending AskUser entries for a room. Called when its agent goes
|
|
182
191
|
* away (exit/close) so a request that can no longer be answered doesn't leak
|
|
183
192
|
* in the map (small memory leak) and its approval card doesn't hang until the
|
|
@@ -222,7 +231,9 @@ export declare class RoomManager {
|
|
|
222
231
|
* next open() can continue the same conversation. No-op if unchanged. */
|
|
223
232
|
setRoomSessionId(id: string, sessionId: string): void;
|
|
224
233
|
getMessages(id: string, sinceSeq?: number): RoomMessage[];
|
|
234
|
+
getHistoryPage(id: string, request?: RoomHistoryRequest): RoomHistoryPage<RoomMessage>;
|
|
225
235
|
latestSeq(id: string): number;
|
|
236
|
+
hasTranscriptHistory(id: string): boolean;
|
|
226
237
|
beginTranscriptFollow(id: string): void;
|
|
227
238
|
endTranscriptFollow(id: string): void;
|
|
228
239
|
/** Persist normalized transcript-tail messages through the same seq/history/
|