@agentknight/sdk 0.9.37 → 0.9.39
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/chunks/{acp-adapter-QK4MVTNN.js → acp-adapter-BWVM6WIF.js} +3 -3
- package/dist/chunks/{chunk-DSHLLGE2.js → chunk-45FBONW3.js} +1 -1
- package/dist/chunks/{chunk-UJZPAP64.js → chunk-BM6T2DLH.js} +1 -1
- package/dist/chunks/{chunk-N2LIXMIM.js → chunk-E3XCZWRK.js} +425 -245
- package/dist/chunks/{chunk-U2HBI4SH.js → chunk-KPRPUIXR.js} +1 -1
- package/dist/chunks/{chunk-QLGX7333.js → chunk-N4MN54CD.js} +1 -1
- package/dist/chunks/{chunk-7JRY2FRZ.js → chunk-QGPX4UDW.js} +137 -34
- package/dist/chunks/{chunk-IXV2BIUR.js → chunk-UE4QYZCG.js} +1 -1
- package/dist/chunks/{chunk-VXZ25DKC.js → chunk-VCNYBWYE.js} +2 -2
- package/dist/chunks/{copilot-adapter-KM6RMGFR.js → copilot-adapter-N5B7BNWO.js} +25 -11
- package/dist/chunks/{managed-agent-adapter-4W4HC67T.js → managed-agent-adapter-T3FVUJHA.js} +5 -5
- package/dist/chunks/{pi-adapter-IFNN3F4N.js → pi-adapter-PHEFFJVF.js} +3 -3
- package/dist/index.d.ts +54 -4
- package/dist/index.js +1953 -485
- package/dist/runtime.js +2 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,12 +4,12 @@ import {
|
|
|
4
4
|
PeerChatRequestSchema,
|
|
5
5
|
PeerChatResultSchema,
|
|
6
6
|
validateDataDirectory
|
|
7
|
-
} from "./chunks/chunk-
|
|
7
|
+
} from "./chunks/chunk-UE4QYZCG.js";
|
|
8
8
|
import {
|
|
9
9
|
AgentSelectionSchema,
|
|
10
10
|
assertSelectionSnapshotFits,
|
|
11
11
|
supportsAgentSelection
|
|
12
|
-
} from "./chunks/chunk-
|
|
12
|
+
} from "./chunks/chunk-KPRPUIXR.js";
|
|
13
13
|
import {
|
|
14
14
|
ConversationNormalizer,
|
|
15
15
|
LiveTimelineProjection,
|
|
@@ -27,19 +27,20 @@ import {
|
|
|
27
27
|
projectWorkbenchInsights,
|
|
28
28
|
readWorkbenchMetrics,
|
|
29
29
|
recordOperationFailure,
|
|
30
|
+
recordTimelineSyncDiagnostic,
|
|
30
31
|
resolveGitProjectIdentity,
|
|
31
32
|
sessionRestoreFailure,
|
|
32
33
|
validateOperationResponse
|
|
33
|
-
} from "./chunks/chunk-
|
|
34
|
+
} from "./chunks/chunk-QGPX4UDW.js";
|
|
34
35
|
import {
|
|
35
36
|
GROUP_CHAT_MAX_TURNS,
|
|
36
37
|
GroupChatError
|
|
37
|
-
} from "./chunks/chunk-
|
|
38
|
+
} from "./chunks/chunk-VCNYBWYE.js";
|
|
38
39
|
import {
|
|
39
40
|
assistantPreview,
|
|
40
41
|
isContentfulAssistant,
|
|
41
42
|
userPreview
|
|
42
|
-
} from "./chunks/chunk-
|
|
43
|
+
} from "./chunks/chunk-N4MN54CD.js";
|
|
43
44
|
import {
|
|
44
45
|
AgentModelListingError,
|
|
45
46
|
AgentSessionRenameError,
|
|
@@ -48,7 +49,7 @@ import {
|
|
|
48
49
|
UnsupportedAgentOperationError,
|
|
49
50
|
captureResearchAccess,
|
|
50
51
|
captureWorkbenchAccess
|
|
51
|
-
} from "./chunks/chunk-
|
|
52
|
+
} from "./chunks/chunk-BM6T2DLH.js";
|
|
52
53
|
import {
|
|
53
54
|
A2aRelayRequestSchema,
|
|
54
55
|
A2aRelayResponseSchema,
|
|
@@ -106,8 +107,21 @@ import {
|
|
|
106
107
|
SESSION_FILE_MAX_IMAGE_EDGE,
|
|
107
108
|
SESSION_FILE_MAX_IMAGE_PIXELS,
|
|
108
109
|
SESSION_FILE_MAX_ORIGINAL_BYTES,
|
|
110
|
+
SESSION_FILE_MAX_VIDEO_PIXELS,
|
|
109
111
|
SESSION_FILE_READ_OPERATION,
|
|
112
|
+
SESSION_FILE_READ_V2_VERSION,
|
|
110
113
|
SESSION_FILE_READ_VERSION,
|
|
114
|
+
SESSION_FILE_V2_MAX_ORIGINAL_BYTES,
|
|
115
|
+
SESSION_GIT_DIFF_GZIP_MIN_BYTES,
|
|
116
|
+
SESSION_GIT_DIFF_GZIP_VERSION,
|
|
117
|
+
SESSION_GIT_DIFF_MAX_FILES,
|
|
118
|
+
SESSION_GIT_DIFF_MAX_PATCH_BYTES,
|
|
119
|
+
SESSION_GIT_DIFF_MAX_RESPONSE_BYTES,
|
|
120
|
+
SESSION_GIT_DIFF_OPERATION,
|
|
121
|
+
SESSION_GIT_DIFF_VERSION,
|
|
122
|
+
SESSION_IMAGE_API_PATH,
|
|
123
|
+
SESSION_IMAGE_MAX_BYTES,
|
|
124
|
+
SESSION_IMAGE_MAX_PIXELS,
|
|
111
125
|
SESSION_LIST_UPDATES_CAPABILITY,
|
|
112
126
|
SESSION_OPTIONS_PARTIAL_CAPABILITY,
|
|
113
127
|
SESSION_PROJECT_IDENTITY_OPERATION,
|
|
@@ -136,6 +150,14 @@ import {
|
|
|
136
150
|
SessionDirectoryListResultDataSchema,
|
|
137
151
|
SessionFileReadDataSchema,
|
|
138
152
|
SessionFileReadResultDataSchema,
|
|
153
|
+
SessionFileReadV2ResultDataSchema,
|
|
154
|
+
SessionGitDiffDataSchema,
|
|
155
|
+
SessionGitDiffGzipResultDataSchema,
|
|
156
|
+
SessionGitDiffResultDataSchema,
|
|
157
|
+
SessionImageCommitResultSchema,
|
|
158
|
+
SessionImageScopeSchema,
|
|
159
|
+
SessionImageUploadRequestSchema,
|
|
160
|
+
SessionImageUploadResultSchema,
|
|
139
161
|
SessionListRequestSchema,
|
|
140
162
|
SessionListResultSchema,
|
|
141
163
|
SessionOptionsRequestSchema,
|
|
@@ -153,6 +175,8 @@ import {
|
|
|
153
175
|
SessionTimelinePageSchema,
|
|
154
176
|
SessionWatchRequestSchema,
|
|
155
177
|
SessionsSnapshotSchema,
|
|
178
|
+
TIMELINE_IMAGE_BLOB_OPERATION,
|
|
179
|
+
TIMELINE_IMAGE_BLOB_VERSION,
|
|
156
180
|
TIMELINE_IMAGE_CHUNK_BYTES,
|
|
157
181
|
TIMELINE_IMAGE_MAX_BYTES,
|
|
158
182
|
TIMELINE_IMAGE_OPERATION,
|
|
@@ -161,6 +185,8 @@ import {
|
|
|
161
185
|
TIMELINE_SYNC_OPERATION,
|
|
162
186
|
TIMELINE_SYNC_VERSION,
|
|
163
187
|
TRANSPORT_V2,
|
|
188
|
+
TimelineImageBlobRequestSchema,
|
|
189
|
+
TimelineImageBlobResultSchema,
|
|
164
190
|
TimelineImageChunkSchema,
|
|
165
191
|
TimelineImageRequestSchema,
|
|
166
192
|
TimelineSyncRequestSchema,
|
|
@@ -197,12 +223,12 @@ import {
|
|
|
197
223
|
sessionCommunicationError,
|
|
198
224
|
sessionCommunicationOperationCapability,
|
|
199
225
|
workbenchCapability
|
|
200
|
-
} from "./chunks/chunk-
|
|
226
|
+
} from "./chunks/chunk-E3XCZWRK.js";
|
|
201
227
|
|
|
202
228
|
// src/initialize.ts
|
|
203
|
-
import { createHash as
|
|
229
|
+
import { createHash as createHash16 } from "node:crypto";
|
|
204
230
|
import { readFile as readFile4 } from "node:fs/promises";
|
|
205
|
-
import { z as
|
|
231
|
+
import { z as z12 } from "zod";
|
|
206
232
|
|
|
207
233
|
// src/coding-agent-factory.ts
|
|
208
234
|
import { resolve } from "node:path";
|
|
@@ -236,9 +262,9 @@ async function accessible(path, executable = false) {
|
|
|
236
262
|
throw error;
|
|
237
263
|
}
|
|
238
264
|
}
|
|
239
|
-
async function hasLocalExecutable(command,
|
|
240
|
-
const extensions = process.platform === "win32" ? ["", ...(
|
|
241
|
-
const paths = isAbsolute(command) || command.includes("/") || command.includes("\\") ? [command] : (
|
|
265
|
+
async function hasLocalExecutable(command, environment2 = process.env) {
|
|
266
|
+
const extensions = process.platform === "win32" ? ["", ...(environment2.PATHEXT ?? ".EXE;.CMD;.BAT").split(";")] : [""];
|
|
267
|
+
const paths = isAbsolute(command) || command.includes("/") || command.includes("\\") ? [command] : (environment2.PATH ?? "").split(delimiter).filter(Boolean).map((directory) => join(directory, command));
|
|
242
268
|
for (const path of paths) {
|
|
243
269
|
for (const extension of extensions) {
|
|
244
270
|
if (await accessible(`${path}${extension}`, true)) return true;
|
|
@@ -275,7 +301,7 @@ async function createCodingAgentAdapter(config, onDiagnostic, owner) {
|
|
|
275
301
|
switch (engine.kind) {
|
|
276
302
|
case "managed": {
|
|
277
303
|
if (owner === void 0) throw new Error("Managed coding agents require the PC owner identity");
|
|
278
|
-
const { ManagedCodingAgentAdapter } = await import("./chunks/managed-agent-adapter-
|
|
304
|
+
const { ManagedCodingAgentAdapter } = await import("./chunks/managed-agent-adapter-T3FVUJHA.js");
|
|
279
305
|
return new ManagedCodingAgentAdapter({
|
|
280
306
|
dataDirectory: config.dataDirectory,
|
|
281
307
|
owner,
|
|
@@ -314,7 +340,7 @@ async function createCodingAgentAdapter(config, onDiagnostic, owner) {
|
|
|
314
340
|
});
|
|
315
341
|
}
|
|
316
342
|
case "copilot": {
|
|
317
|
-
const { GithubCopilotAdapter } = await import("./chunks/copilot-adapter-
|
|
343
|
+
const { GithubCopilotAdapter } = await import("./chunks/copilot-adapter-N5B7BNWO.js");
|
|
318
344
|
if (owner !== void 0) {
|
|
319
345
|
return new GithubCopilotAdapter(void 0, void 0, void 0, {
|
|
320
346
|
dataDirectory: config.dataDirectory,
|
|
@@ -334,13 +360,13 @@ async function createCodingAgentAdapter(config, onDiagnostic, owner) {
|
|
|
334
360
|
}
|
|
335
361
|
case "pi": {
|
|
336
362
|
assertPiRuntimeSupported();
|
|
337
|
-
const { PiCodingAgentAdapter } = await import("./chunks/pi-adapter-
|
|
363
|
+
const { PiCodingAgentAdapter } = await import("./chunks/pi-adapter-PHEFFJVF.js");
|
|
338
364
|
return new PiCodingAgentAdapter(
|
|
339
365
|
engine.agentDirectory === void 0 ? {} : { agentDirectory: resolve(engine.agentDirectory) }
|
|
340
366
|
);
|
|
341
367
|
}
|
|
342
368
|
case "acp": {
|
|
343
|
-
const { AcpCodingAgentAdapter } = await import("./chunks/acp-adapter-
|
|
369
|
+
const { AcpCodingAgentAdapter } = await import("./chunks/acp-adapter-BWVM6WIF.js");
|
|
344
370
|
return new AcpCodingAgentAdapter({
|
|
345
371
|
command: engine.command,
|
|
346
372
|
args: engine.args,
|
|
@@ -356,6 +382,18 @@ async function createCodingAgentAdapter(config, onDiagnostic, owner) {
|
|
|
356
382
|
}
|
|
357
383
|
}
|
|
358
384
|
|
|
385
|
+
// src/config.ts
|
|
386
|
+
import { z } from "zod";
|
|
387
|
+
var SessionImagesConfigSchema = z.object({
|
|
388
|
+
allowUnencryptedStorage: z.literal(true),
|
|
389
|
+
storageAccount: z.string().regex(/^[a-z0-9]{3,24}$/u),
|
|
390
|
+
storageContainer: z.string().min(3).max(63).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/u)
|
|
391
|
+
}).strict();
|
|
392
|
+
var SessionImageRelayUrlSchema = z.url().refine((value) => {
|
|
393
|
+
const url = new URL(value);
|
|
394
|
+
return /^https:\/\/[^/?#\\@\s\x00-\x1f\x7f]+\/?$/iu.test(value) && url.protocol === "https:" && url.username === "" && url.password === "" && url.pathname === "/" && url.search === "" && url.hash === "";
|
|
395
|
+
}, "Session images require an HTTPS Relay origin without a path, credentials, query or fragment").transform((value) => new URL(value).origin);
|
|
396
|
+
|
|
359
397
|
// src/content-encryption.ts
|
|
360
398
|
import {
|
|
361
399
|
createCipheriv,
|
|
@@ -372,22 +410,22 @@ import {
|
|
|
372
410
|
} from "node:fs";
|
|
373
411
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
374
412
|
import { join as join2 } from "node:path";
|
|
375
|
-
import { z } from "zod";
|
|
413
|
+
import { z as z2 } from "zod";
|
|
376
414
|
var KEY_BYTES = 32;
|
|
377
415
|
var NONCE_BYTES = 12;
|
|
378
416
|
var AUTH_TAG_BYTES = 16;
|
|
379
417
|
var DEFAULT_REPLAY_WINDOW_MS = 10 * 60 * 1e3;
|
|
380
418
|
var MAX_REPLAY_ENTRIES = 1e4;
|
|
381
419
|
var CONTENT_KEY_PATTERN = /^[A-Za-z0-9_-]{43}$/u;
|
|
382
|
-
var ContentKeyFileSchema =
|
|
383
|
-
schemaVersion:
|
|
384
|
-
key:
|
|
420
|
+
var ContentKeyFileSchema = z2.object({
|
|
421
|
+
schemaVersion: z2.literal(1),
|
|
422
|
+
key: z2.string().regex(CONTENT_KEY_PATTERN)
|
|
385
423
|
}).strict();
|
|
386
|
-
var ReplayJournalEntrySchema =
|
|
387
|
-
schemaVersion:
|
|
388
|
-
keyId:
|
|
389
|
-
messageId:
|
|
390
|
-
sentAt:
|
|
424
|
+
var ReplayJournalEntrySchema = z2.object({
|
|
425
|
+
schemaVersion: z2.literal(1),
|
|
426
|
+
keyId: z2.string().regex(/^[A-Za-z0-9_-]{22}$/u),
|
|
427
|
+
messageId: z2.string().trim().min(1).max(256),
|
|
428
|
+
sentAt: z2.number().int().nonnegative()
|
|
391
429
|
}).strict();
|
|
392
430
|
var REPLAY_JOURNAL_COMPACT_EVERY = 256;
|
|
393
431
|
var REPLAY_JOURNAL_MAX_BYTES = 1024 * 1024;
|
|
@@ -638,7 +676,7 @@ var ContentCipher = class {
|
|
|
638
676
|
openBytes(input) {
|
|
639
677
|
return this.#open(input, (plaintext) => plaintext);
|
|
640
678
|
}
|
|
641
|
-
#open(input,
|
|
679
|
+
#open(input, decode2) {
|
|
642
680
|
const message = SecureMessageSchema.parse(input);
|
|
643
681
|
if (message.keyId !== this.#key.keyId) {
|
|
644
682
|
throw new Error("Encrypted message key ID does not match this device");
|
|
@@ -670,7 +708,7 @@ var ContentCipher = class {
|
|
|
670
708
|
cause: error
|
|
671
709
|
});
|
|
672
710
|
}
|
|
673
|
-
const payload =
|
|
711
|
+
const payload = decode2(plaintext);
|
|
674
712
|
while (activeReplayEntries.size >= MAX_REPLAY_ENTRIES) {
|
|
675
713
|
const oldest = activeReplayEntries.keys().next().value;
|
|
676
714
|
if (oldest === void 0) {
|
|
@@ -698,13 +736,13 @@ var ContentCipher = class {
|
|
|
698
736
|
};
|
|
699
737
|
|
|
700
738
|
// src/device-identity.ts
|
|
701
|
-
import { z as
|
|
702
|
-
var DeviceIdentitySchema =
|
|
703
|
-
schemaVersion:
|
|
704
|
-
machineId:
|
|
705
|
-
stableDeviceId:
|
|
706
|
-
machineName:
|
|
707
|
-
createdAt:
|
|
739
|
+
import { z as z3 } from "zod";
|
|
740
|
+
var DeviceIdentitySchema = z3.object({
|
|
741
|
+
schemaVersion: z3.literal(2),
|
|
742
|
+
machineId: z3.uuid(),
|
|
743
|
+
stableDeviceId: z3.uuid(),
|
|
744
|
+
machineName: z3.string().trim().min(1).max(120),
|
|
745
|
+
createdAt: z3.iso.datetime({ offset: true })
|
|
708
746
|
});
|
|
709
747
|
|
|
710
748
|
// src/pairing.ts
|
|
@@ -757,12 +795,12 @@ function buildPairingUri(relayUrl, code, machineId, contentKey) {
|
|
|
757
795
|
uri.searchParams.set("key", contentKey);
|
|
758
796
|
return uri.toString();
|
|
759
797
|
}
|
|
760
|
-
async function requestPairingUri(config,
|
|
798
|
+
async function requestPairingUri(config, identity2, contentKey) {
|
|
761
799
|
const payload = {
|
|
762
800
|
userId: config.userId,
|
|
763
|
-
machineId:
|
|
764
|
-
stableDeviceId:
|
|
765
|
-
machineName:
|
|
801
|
+
machineId: identity2.machineId,
|
|
802
|
+
stableDeviceId: identity2.stableDeviceId,
|
|
803
|
+
machineName: identity2.machineName,
|
|
766
804
|
keyId: contentKey.keyId
|
|
767
805
|
};
|
|
768
806
|
const response = await fetch(
|
|
@@ -786,7 +824,7 @@ async function requestPairingUri(config, identity, contentKey) {
|
|
|
786
824
|
uri: buildPairingUri(
|
|
787
825
|
config.relayUrl,
|
|
788
826
|
result.code,
|
|
789
|
-
|
|
827
|
+
identity2.machineId,
|
|
790
828
|
contentKey.encoded
|
|
791
829
|
),
|
|
792
830
|
expiresAt: result.expiresAt
|
|
@@ -801,7 +839,7 @@ import { createHash as createHash4, randomUUID as randomUUID2 } from "node:crypt
|
|
|
801
839
|
import { lstat as lstat3, realpath as realpath3, stat as stat4 } from "node:fs/promises";
|
|
802
840
|
import { isAbsolute as isAbsolute4, join as join5 } from "node:path";
|
|
803
841
|
import { gunzipSync } from "node:zlib";
|
|
804
|
-
import { z as
|
|
842
|
+
import { z as z5 } from "zod";
|
|
805
843
|
|
|
806
844
|
// src/session-directory.ts
|
|
807
845
|
import { createHash as createHash2 } from "node:crypto";
|
|
@@ -956,6 +994,152 @@ import { basename, extname, isAbsolute as isAbsolute3, join as join4 } from "nod
|
|
|
956
994
|
import { promisify } from "node:util";
|
|
957
995
|
import { gzip } from "node:zlib";
|
|
958
996
|
import sharp from "sharp";
|
|
997
|
+
|
|
998
|
+
// src/session-video.ts
|
|
999
|
+
function requireVideo(valid) {
|
|
1000
|
+
if (!valid) throw new Error("Unsupported video container");
|
|
1001
|
+
}
|
|
1002
|
+
function one(elements, id) {
|
|
1003
|
+
const matches = elements.filter((element) => element.id === id);
|
|
1004
|
+
const match = matches[0];
|
|
1005
|
+
requireVideo(matches.length === 1 && match !== void 0);
|
|
1006
|
+
return match;
|
|
1007
|
+
}
|
|
1008
|
+
function validateSessionVideo(bytes, mediaType) {
|
|
1009
|
+
requireVideo(bytes.length > 0 && bytes.length <= SESSION_FILE_V2_MAX_ORIGINAL_BYTES);
|
|
1010
|
+
if (mediaType === "video/mp4") validateMp4(bytes);
|
|
1011
|
+
else validateWebm(bytes);
|
|
1012
|
+
}
|
|
1013
|
+
function validateMp4(bytes) {
|
|
1014
|
+
const boxes = (start, end) => {
|
|
1015
|
+
const result = [];
|
|
1016
|
+
while (start < end) {
|
|
1017
|
+
requireVideo(end - start >= 8);
|
|
1018
|
+
let size = bytes.readUInt32BE(start);
|
|
1019
|
+
const id = bytes.toString("latin1", start + 4, start + 8);
|
|
1020
|
+
let header = 8;
|
|
1021
|
+
if (size === 1) {
|
|
1022
|
+
requireVideo(end - start >= 16 && bytes.readUInt32BE(start + 8) === 0);
|
|
1023
|
+
size = bytes.readUInt32BE(start + 12);
|
|
1024
|
+
header = 16;
|
|
1025
|
+
}
|
|
1026
|
+
if (size === 0) {
|
|
1027
|
+
requireVideo(id === "mdat");
|
|
1028
|
+
size = end - start;
|
|
1029
|
+
}
|
|
1030
|
+
requireVideo(size >= header && size <= end - start);
|
|
1031
|
+
result.push({ id, start: start + header, end: start + size });
|
|
1032
|
+
start += size;
|
|
1033
|
+
}
|
|
1034
|
+
return result;
|
|
1035
|
+
};
|
|
1036
|
+
const children = (element) => boxes(element.start, element.end);
|
|
1037
|
+
const top = boxes(0, bytes.length);
|
|
1038
|
+
requireVideo(top[0]?.id === "ftyp" && top.every((box) => ["ftyp", "moov", "mdat", "free", "skip", "wide"].includes(box.id)));
|
|
1039
|
+
const ftyp = one(top, "ftyp");
|
|
1040
|
+
requireVideo(ftyp.end - ftyp.start >= 8 && (ftyp.end - ftyp.start) % 4 === 0);
|
|
1041
|
+
const brands = [bytes.toString("latin1", ftyp.start, ftyp.start + 4)];
|
|
1042
|
+
for (let offset = ftyp.start + 8; offset < ftyp.end; offset += 4) brands.push(bytes.toString("latin1", offset, offset + 4));
|
|
1043
|
+
requireVideo(brands.some((brand) => ["isom", "iso2", "mp41", "mp42", "avc1"].includes(brand)));
|
|
1044
|
+
requireVideo(top.some((box) => box.id === "mdat" && box.end > box.start));
|
|
1045
|
+
const movie = children(one(top, "moov"));
|
|
1046
|
+
requireVideo(!movie.some((box) => ["rmra", "mvex"].includes(box.id)));
|
|
1047
|
+
let videoTracks = 0;
|
|
1048
|
+
for (const track of movie.filter((box) => box.id === "trak")) {
|
|
1049
|
+
const media = children(one(children(track), "mdia"));
|
|
1050
|
+
const handler = one(media, "hdlr");
|
|
1051
|
+
requireVideo(handler.end - handler.start >= 12);
|
|
1052
|
+
const type = bytes.toString("latin1", handler.start + 8, handler.start + 12);
|
|
1053
|
+
requireVideo(type === "vide" || type === "soun");
|
|
1054
|
+
const info = children(one(media, "minf"));
|
|
1055
|
+
const dref = one(children(one(info, "dinf")), "dref");
|
|
1056
|
+
requireVideo(dref.end - dref.start >= 8 && bytes.readUInt32BE(dref.start) === 0 && bytes.readUInt32BE(dref.start + 4) === 1);
|
|
1057
|
+
const references = boxes(dref.start + 8, dref.end);
|
|
1058
|
+
const reference = one(references, "url ");
|
|
1059
|
+
requireVideo(references.length === 1 && reference.end - reference.start === 4 && bytes.readUInt32BE(reference.start) === 1);
|
|
1060
|
+
const descriptions = one(children(one(info, "stbl")), "stsd");
|
|
1061
|
+
requireVideo(descriptions.end - descriptions.start >= 8 && bytes.readUInt32BE(descriptions.start) === 0 && bytes.readUInt32BE(descriptions.start + 4) === 1);
|
|
1062
|
+
const entries = boxes(descriptions.start + 8, descriptions.end);
|
|
1063
|
+
const entry = entries[0];
|
|
1064
|
+
requireVideo(entries.length === 1 && entry !== void 0 && entry.end - entry.start >= 8 && bytes.readUInt16BE(entry.start + 6) === 1);
|
|
1065
|
+
if (type === "vide") {
|
|
1066
|
+
requireVideo(["avc1", "avc3"].includes(entry.id) && entry.end - entry.start >= 78);
|
|
1067
|
+
const width = bytes.readUInt16BE(entry.start + 24);
|
|
1068
|
+
const height = bytes.readUInt16BE(entry.start + 26);
|
|
1069
|
+
requireVideo(width > 0 && height > 0 && width * height <= SESSION_FILE_MAX_VIDEO_PIXELS);
|
|
1070
|
+
const config = one(boxes(entry.start + 78, entry.end), "avcC");
|
|
1071
|
+
requireVideo(config.end - config.start >= 7 && bytes[config.start] === 1);
|
|
1072
|
+
videoTracks++;
|
|
1073
|
+
} else requireVideo(entry.id === "mp4a" && entry.end - entry.start >= 28);
|
|
1074
|
+
}
|
|
1075
|
+
requireVideo(videoTracks === 1);
|
|
1076
|
+
}
|
|
1077
|
+
function validateWebm(bytes) {
|
|
1078
|
+
const vint = (offset, end, id) => {
|
|
1079
|
+
requireVideo(offset < end);
|
|
1080
|
+
const first = bytes[offset];
|
|
1081
|
+
requireVideo(first !== void 0 && first !== 0);
|
|
1082
|
+
let length = 1;
|
|
1083
|
+
while ((first & 128 >> length - 1) === 0) length++;
|
|
1084
|
+
requireVideo(length <= (id ? 4 : 8) && length <= end - offset);
|
|
1085
|
+
let value = id ? first : first & 255 >> length;
|
|
1086
|
+
let unknown = !id && value === 255 >> length;
|
|
1087
|
+
for (let index = 1; index < length; index++) {
|
|
1088
|
+
const byte = bytes[offset + index];
|
|
1089
|
+
requireVideo(byte !== void 0);
|
|
1090
|
+
value = value * 256 + byte;
|
|
1091
|
+
unknown = unknown && byte === 255;
|
|
1092
|
+
}
|
|
1093
|
+
requireVideo(unknown || Number.isSafeInteger(value));
|
|
1094
|
+
return { value, next: offset + length, unknown };
|
|
1095
|
+
};
|
|
1096
|
+
const elements = (start, end) => {
|
|
1097
|
+
const result = [];
|
|
1098
|
+
while (start < end) {
|
|
1099
|
+
const id = vint(start, end, true);
|
|
1100
|
+
const size = vint(id.next, end, false);
|
|
1101
|
+
const tag = id.value.toString(16);
|
|
1102
|
+
requireVideo(!size.unknown || tag === "18538067");
|
|
1103
|
+
const length = size.unknown ? end - size.next : size.value;
|
|
1104
|
+
requireVideo(length <= end - size.next);
|
|
1105
|
+
result.push({ id: tag, start: size.next, end: size.next + length });
|
|
1106
|
+
start = size.next + length;
|
|
1107
|
+
}
|
|
1108
|
+
return result;
|
|
1109
|
+
};
|
|
1110
|
+
const text = (element) => bytes.toString("latin1", element.start, element.end);
|
|
1111
|
+
const uint = (element) => {
|
|
1112
|
+
requireVideo(element.end - element.start >= 1 && element.end - element.start <= 4);
|
|
1113
|
+
return bytes.readUIntBE(element.start, element.end - element.start);
|
|
1114
|
+
};
|
|
1115
|
+
const top = elements(0, bytes.length);
|
|
1116
|
+
requireVideo(top.length === 2 && top[0]?.id === "1a45dfa3");
|
|
1117
|
+
const header = one(top, "1a45dfa3");
|
|
1118
|
+
requireVideo(text(one(elements(header.start, header.end), "4282")) === "webm");
|
|
1119
|
+
const segment = one(top, "18538067");
|
|
1120
|
+
const contents = elements(segment.start, segment.end);
|
|
1121
|
+
requireVideo(contents.some((element) => element.id === "1f43b675" && element.end > element.start));
|
|
1122
|
+
const tracks = one(contents, "1654ae6b");
|
|
1123
|
+
let videoTracks = 0;
|
|
1124
|
+
for (const track of elements(tracks.start, tracks.end).filter((element) => element.id === "ae")) {
|
|
1125
|
+
const fields = elements(track.start, track.end);
|
|
1126
|
+
const type = uint(one(fields, "83"));
|
|
1127
|
+
const codec = text(one(fields, "86"));
|
|
1128
|
+
requireVideo(!fields.some((field2) => field2.id === "6d80" || field2.id === "6fab"));
|
|
1129
|
+
if (type === 1) {
|
|
1130
|
+
requireVideo(codec === "V_VP8" || codec === "V_VP9");
|
|
1131
|
+
const video = one(fields, "e0");
|
|
1132
|
+
const dimensions = elements(video.start, video.end);
|
|
1133
|
+
const width = uint(one(dimensions, "b0"));
|
|
1134
|
+
const height = uint(one(dimensions, "ba"));
|
|
1135
|
+
requireVideo(width > 0 && height > 0 && width * height <= SESSION_FILE_MAX_VIDEO_PIXELS);
|
|
1136
|
+
videoTracks++;
|
|
1137
|
+
} else requireVideo(type === 2 && (codec === "A_VORBIS" || codec === "A_OPUS"));
|
|
1138
|
+
}
|
|
1139
|
+
requireVideo(videoTracks === 1);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// src/session-file.ts
|
|
959
1143
|
var SESSION_FILE_CONCURRENCY = 4;
|
|
960
1144
|
var SESSION_FILE_TIMEOUT_MS = 5e3;
|
|
961
1145
|
var compress = promisify(gzip);
|
|
@@ -1193,10 +1377,12 @@ var SessionFileReader = class {
|
|
|
1193
1377
|
}
|
|
1194
1378
|
metadata;
|
|
1195
1379
|
#active = 0;
|
|
1196
|
-
async read(sessionId, path, rootRevision) {
|
|
1380
|
+
async read(sessionId, path, rootRevision, version = SESSION_FILE_READ_VERSION) {
|
|
1381
|
+
if (version !== SESSION_FILE_READ_VERSION && version !== SESSION_FILE_READ_V2_VERSION) fail2("UNSUPPORTED_OPERATION_VERSION");
|
|
1197
1382
|
if (!SessionFileReadDataSchema.safeParse({ path, rootRevision }).success) fail2("INVALID_FILE_PATH");
|
|
1198
|
-
|
|
1199
|
-
|
|
1383
|
+
const schema = version === SESSION_FILE_READ_V2_VERSION ? SessionFileReadV2ResultDataSchema : SessionFileReadResultDataSchema;
|
|
1384
|
+
return schema.parse(await this.#run(
|
|
1385
|
+
(checkDeadline) => this.#read(sessionId, path, rootRevision, checkDeadline, false, version)
|
|
1200
1386
|
));
|
|
1201
1387
|
}
|
|
1202
1388
|
async readResource(sessionId, path, rootRevision) {
|
|
@@ -1230,7 +1416,7 @@ var SessionFileReader = class {
|
|
|
1230
1416
|
clearTimeout(timer);
|
|
1231
1417
|
}
|
|
1232
1418
|
}
|
|
1233
|
-
async #read(sessionId, path, rootRevision, checkDeadline, resource) {
|
|
1419
|
+
async #read(sessionId, path, rootRevision, checkDeadline, resource, version = SESSION_FILE_READ_VERSION) {
|
|
1234
1420
|
const metadata = await this.metadata(sessionId);
|
|
1235
1421
|
checkDeadline();
|
|
1236
1422
|
if (metadata === void 0) fail2("SESSION_NOT_FOUND");
|
|
@@ -1263,8 +1449,8 @@ var SessionFileReader = class {
|
|
|
1263
1449
|
fail2("FILE_UNAVAILABLE");
|
|
1264
1450
|
}
|
|
1265
1451
|
const canonical = await realpath2(current);
|
|
1266
|
-
const
|
|
1267
|
-
if (!contained(root, canonical) || !
|
|
1452
|
+
const samePath2 = canonical === current || resource && process.platform === "win32" && canonical.toLowerCase() === current.toLowerCase();
|
|
1453
|
+
if (!contained(root, canonical) || !samePath2) fail2("FILE_UNAVAILABLE");
|
|
1268
1454
|
checkDeadline();
|
|
1269
1455
|
file = info;
|
|
1270
1456
|
}
|
|
@@ -1272,11 +1458,12 @@ var SessionFileReader = class {
|
|
|
1272
1458
|
return file;
|
|
1273
1459
|
};
|
|
1274
1460
|
const before = await inspectPath();
|
|
1275
|
-
const maxOriginalBytes = resource ? SESSION_RESOURCE_MAX_ORIGINAL_BYTES : SESSION_FILE_MAX_ORIGINAL_BYTES;
|
|
1461
|
+
const maxOriginalBytes = resource ? SESSION_RESOURCE_MAX_ORIGINAL_BYTES : version === SESSION_FILE_READ_V2_VERSION ? SESSION_FILE_V2_MAX_ORIGINAL_BYTES : SESSION_FILE_MAX_ORIGINAL_BYTES;
|
|
1276
1462
|
if (before.size > BigInt(maxOriginalBytes)) fail2("FILE_TOO_LARGE");
|
|
1277
1463
|
const extension = extname(path).toLowerCase();
|
|
1278
1464
|
const image = [".png", ".jpg", ".jpeg", ".webp"].includes(extension);
|
|
1279
|
-
const
|
|
1465
|
+
const video = !resource && version === SESSION_FILE_READ_V2_VERSION && [".mp4", ".webm"].includes(extension);
|
|
1466
|
+
const mediaType = resource ? resourceMediaType(extension) : image ? "image/webp" : video ? extension === ".mp4" ? "video/mp4" : "video/webm" : textMediaType(path);
|
|
1280
1467
|
const handle = await open(target, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0) | (constants2.O_NONBLOCK ?? 0));
|
|
1281
1468
|
try {
|
|
1282
1469
|
const opened = await handle.stat({ bigint: true });
|
|
@@ -1302,6 +1489,13 @@ var SessionFileReader = class {
|
|
|
1302
1489
|
await validateRawImage(bytes, extension, checkDeadline);
|
|
1303
1490
|
canonical = bytes;
|
|
1304
1491
|
} else canonical = await imagePreview(bytes, extension, checkDeadline);
|
|
1492
|
+
} else if (mediaType === "video/mp4" || mediaType === "video/webm") {
|
|
1493
|
+
try {
|
|
1494
|
+
validateSessionVideo(bytes, mediaType);
|
|
1495
|
+
} catch (error) {
|
|
1496
|
+
throw new SessionFileReadError("FILE_UNSUPPORTED", { cause: error });
|
|
1497
|
+
}
|
|
1498
|
+
canonical = bytes;
|
|
1305
1499
|
} else {
|
|
1306
1500
|
let text;
|
|
1307
1501
|
try {
|
|
@@ -1334,7 +1528,7 @@ var SessionFileReader = class {
|
|
|
1334
1528
|
throw new SessionFileReadError("FILE_CHANGED", { cause: error });
|
|
1335
1529
|
}
|
|
1336
1530
|
checkDeadline();
|
|
1337
|
-
return (resource ? SessionResourceReadResultDataSchema : SessionFileReadResultDataSchema).parse({
|
|
1531
|
+
return (resource ? SessionResourceReadResultDataSchema : version === SESSION_FILE_READ_V2_VERSION ? SessionFileReadV2ResultDataSchema : SessionFileReadResultDataSchema).parse({
|
|
1338
1532
|
path,
|
|
1339
1533
|
rootRevision,
|
|
1340
1534
|
mediaType,
|
|
@@ -1773,20 +1967,20 @@ function sensitiveSegments(segments) {
|
|
|
1773
1967
|
}
|
|
1774
1968
|
return false;
|
|
1775
1969
|
}
|
|
1776
|
-
var pathSchema =
|
|
1777
|
-
var listSchema =
|
|
1970
|
+
var pathSchema = z5.string().max(4096).default("");
|
|
1971
|
+
var listSchema = z5.object({
|
|
1778
1972
|
path: pathSchema,
|
|
1779
|
-
cursor:
|
|
1973
|
+
cursor: z5.string().uuid().optional()
|
|
1780
1974
|
}).strict();
|
|
1781
|
-
var readSchema =
|
|
1782
|
-
path:
|
|
1783
|
-
startLine:
|
|
1784
|
-
lineCount:
|
|
1975
|
+
var readSchema = z5.object({
|
|
1976
|
+
path: z5.string().min(1).max(4096),
|
|
1977
|
+
startLine: z5.number().int().min(1).max(500001).default(1),
|
|
1978
|
+
lineCount: z5.number().int().min(1).max(200).default(200)
|
|
1785
1979
|
}).strict();
|
|
1786
|
-
var searchSchema =
|
|
1980
|
+
var searchSchema = z5.object({
|
|
1787
1981
|
path: pathSchema,
|
|
1788
|
-
query:
|
|
1789
|
-
cursor:
|
|
1982
|
+
query: z5.string().min(1).max(256),
|
|
1983
|
+
cursor: z5.string().uuid().optional()
|
|
1790
1984
|
}).strict();
|
|
1791
1985
|
var ResearchTools = class {
|
|
1792
1986
|
constructor(parentSessionId, root, sourceMetadata, privateDirectories = [], onError, captureSourceAccess) {
|
|
@@ -2046,7 +2240,7 @@ var ResearchTools = class {
|
|
|
2046
2240
|
const definition = (name, description, schema, run) => ({
|
|
2047
2241
|
name,
|
|
2048
2242
|
description,
|
|
2049
|
-
parameters:
|
|
2243
|
+
parameters: z5.toJSONSchema(schema),
|
|
2050
2244
|
// Only these host-owned read tools bypass prompts; their path policy remains mandatory.
|
|
2051
2245
|
skipPermission: true,
|
|
2052
2246
|
defer: "never",
|
|
@@ -2059,7 +2253,7 @@ var ResearchTools = class {
|
|
|
2059
2253
|
return { textResultForLlm: JSON.stringify(result), resultType: "success" };
|
|
2060
2254
|
} catch (error) {
|
|
2061
2255
|
const knownCode = workbenchErrorDetails(error).code;
|
|
2062
|
-
const code = (error instanceof ResearchError || error instanceof SessionFileReadError) && knownCode !== "UNCLASSIFIED" ? knownCode : error instanceof
|
|
2256
|
+
const code = (error instanceof ResearchError || error instanceof SessionFileReadError) && knownCode !== "UNCLASSIFIED" ? knownCode : error instanceof z5.ZodError ? "RESEARCH_INVALID_ARGUMENT" : "RESEARCH_TOOL_FAILED";
|
|
2063
2257
|
this.onError?.(new ResearchError(code, "The research tool failed", { cause: error }), { stage: name });
|
|
2064
2258
|
return { textResultForLlm: JSON.stringify({ error: code }), resultType: "failure" };
|
|
2065
2259
|
}
|
|
@@ -2190,7 +2384,7 @@ function researchAwareCodingAgentAdapter(inner, research) {
|
|
|
2190
2384
|
}
|
|
2191
2385
|
|
|
2192
2386
|
// src/pc-agent.ts
|
|
2193
|
-
import { randomUUID as
|
|
2387
|
+
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
2194
2388
|
import { platform as platform2 } from "node:os";
|
|
2195
2389
|
import { performance as performance5 } from "node:perf_hooks";
|
|
2196
2390
|
|
|
@@ -2214,17 +2408,17 @@ import { io } from "socket.io-client";
|
|
|
2214
2408
|
|
|
2215
2409
|
// src/service/requests/session-communication.ts
|
|
2216
2410
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2217
|
-
import { createHash as
|
|
2218
|
-
import { join as
|
|
2411
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
2412
|
+
import { join as join7 } from "node:path";
|
|
2219
2413
|
import { performance as performance2 } from "node:perf_hooks";
|
|
2220
2414
|
|
|
2221
2415
|
// src/repeated-error-diagnostics.ts
|
|
2222
2416
|
var RepeatedErrorDiagnostics = class {
|
|
2223
|
-
constructor(logger, event, intervalMs = 3e4,
|
|
2417
|
+
constructor(logger, event, intervalMs = 3e4, limit2 = 256) {
|
|
2224
2418
|
this.logger = logger;
|
|
2225
2419
|
this.event = event;
|
|
2226
2420
|
this.intervalMs = intervalMs;
|
|
2227
|
-
this.limit =
|
|
2421
|
+
this.limit = limit2;
|
|
2228
2422
|
}
|
|
2229
2423
|
logger;
|
|
2230
2424
|
event;
|
|
@@ -2307,7 +2501,7 @@ var MAX_COMPRESSION_JOBS = 2;
|
|
|
2307
2501
|
var activeCompressionJobs = 0;
|
|
2308
2502
|
var ContentEncoder = class {
|
|
2309
2503
|
async encodeTimelineResult(plaintext, options = {}) {
|
|
2310
|
-
const
|
|
2504
|
+
const identity2 = (reason) => ({
|
|
2311
2505
|
bytes: plaintext,
|
|
2312
2506
|
encoding: "identity",
|
|
2313
2507
|
reason
|
|
@@ -2315,28 +2509,28 @@ var ContentEncoder = class {
|
|
|
2315
2509
|
if (plaintext.length > CONTENT_ENCODING_MAX_DECODED_BYTES) {
|
|
2316
2510
|
throw new Error("Timeline content exceeds the decoded byte budget");
|
|
2317
2511
|
}
|
|
2318
|
-
if (options.enabled !== true) return
|
|
2512
|
+
if (options.enabled !== true) return identity2("disabled");
|
|
2319
2513
|
if (!options.acceptContentEncodings?.includes("gzip-v1")) {
|
|
2320
|
-
return
|
|
2514
|
+
return identity2("not-negotiated");
|
|
2321
2515
|
}
|
|
2322
|
-
if (plaintext.length < CONTENT_ENCODING_MIN_INPUT_BYTES) return
|
|
2323
|
-
if (activeCompressionJobs >= MAX_COMPRESSION_JOBS) return
|
|
2516
|
+
if (plaintext.length < CONTENT_ENCODING_MIN_INPUT_BYTES) return identity2("too-small");
|
|
2517
|
+
if (activeCompressionJobs >= MAX_COMPRESSION_JOBS) return identity2("busy");
|
|
2324
2518
|
activeCompressionJobs += 1;
|
|
2325
2519
|
try {
|
|
2326
|
-
const compressed = await new Promise((
|
|
2520
|
+
const compressed = await new Promise((resolve5, reject) => {
|
|
2327
2521
|
gzip2(plaintext, { level: 6 }, (error, result) => {
|
|
2328
2522
|
if (error !== null) reject(error);
|
|
2329
|
-
else
|
|
2523
|
+
else resolve5(result);
|
|
2330
2524
|
});
|
|
2331
2525
|
});
|
|
2332
2526
|
if (compressed.length > CONTENT_ENCODING_MAX_GZIP_BYTES) {
|
|
2333
|
-
return
|
|
2527
|
+
return identity2("gzip-too-large");
|
|
2334
2528
|
}
|
|
2335
2529
|
if (plaintext.length > compressed.length * CONTENT_ENCODING_MAX_RATIO) {
|
|
2336
|
-
return
|
|
2530
|
+
return identity2("ratio-limit");
|
|
2337
2531
|
}
|
|
2338
2532
|
if ((compressed.length + CONTENT_ENCODING_HEADER_BYTES) * 100 > plaintext.length * 85) {
|
|
2339
|
-
return
|
|
2533
|
+
return identity2("insufficient-gain");
|
|
2340
2534
|
}
|
|
2341
2535
|
const header = Buffer.alloc(CONTENT_ENCODING_HEADER_BYTES);
|
|
2342
2536
|
MAGIC.copy(header);
|
|
@@ -2464,20 +2658,1049 @@ var SessionPublicationScope = class {
|
|
|
2464
2658
|
if (this.source(id) === sessionId) this.#tokens.delete(id);
|
|
2465
2659
|
}
|
|
2466
2660
|
}
|
|
2467
|
-
revokeHidden() {
|
|
2468
|
-
const visible = this.visibility();
|
|
2469
|
-
if (visible === void 0) return [];
|
|
2470
|
-
const removed = [];
|
|
2471
|
-
for (const id of this.#tokens.keys()) {
|
|
2472
|
-
if (!visible(id)) {
|
|
2473
|
-
this.revoke(id);
|
|
2474
|
-
removed.push(id);
|
|
2661
|
+
revokeHidden() {
|
|
2662
|
+
const visible = this.visibility();
|
|
2663
|
+
if (visible === void 0) return [];
|
|
2664
|
+
const removed = [];
|
|
2665
|
+
for (const id of this.#tokens.keys()) {
|
|
2666
|
+
if (!visible(id)) {
|
|
2667
|
+
this.revoke(id);
|
|
2668
|
+
removed.push(id);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
return removed;
|
|
2672
|
+
}
|
|
2673
|
+
clear() {
|
|
2674
|
+
this.#tokens.clear();
|
|
2675
|
+
}
|
|
2676
|
+
};
|
|
2677
|
+
|
|
2678
|
+
// src/service/requests/communication-operations.ts
|
|
2679
|
+
import { promisify as promisify2 } from "node:util";
|
|
2680
|
+
import { constants as constants4, gzip as gzip3 } from "node:zlib";
|
|
2681
|
+
|
|
2682
|
+
// src/session-git-diff.ts
|
|
2683
|
+
import { execFile } from "node:child_process";
|
|
2684
|
+
import { createHash as createHash5, randomUUID as randomUUID3 } from "node:crypto";
|
|
2685
|
+
import { constants as constants3 } from "node:fs";
|
|
2686
|
+
import { chmod, lstat as lstat4, mkdir as mkdir2, open as open2, readlink, realpath as realpath4, rm, stat as stat5, writeFile as writeFile2 } from "node:fs/promises";
|
|
2687
|
+
import { dirname, isAbsolute as isAbsolute5, join as join6, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
|
|
2688
|
+
var SESSION_GIT_DIFF_CONCURRENCY = 4;
|
|
2689
|
+
var SESSION_GIT_DIFF_TIMEOUT_MS = 15e3;
|
|
2690
|
+
var SESSION_GIT_DIFF_MAX_SOURCE_BYTES = 8 * 1024 * 1024;
|
|
2691
|
+
var MAX_SCAN_BYTES = 256 * 1024 * 1024;
|
|
2692
|
+
var MAX_SCAN_FILES = 5e4;
|
|
2693
|
+
var SessionGitDiffError = class extends Error {
|
|
2694
|
+
constructor(code, message, options) {
|
|
2695
|
+
super(message, options);
|
|
2696
|
+
this.code = code;
|
|
2697
|
+
}
|
|
2698
|
+
code;
|
|
2699
|
+
};
|
|
2700
|
+
function fail3(code, message) {
|
|
2701
|
+
throw new SessionGitDiffError(code, message);
|
|
2702
|
+
}
|
|
2703
|
+
function changed() {
|
|
2704
|
+
return fail3("DIFF_CHANGED", "The project diff changed; refresh the changed-file list");
|
|
2705
|
+
}
|
|
2706
|
+
function limit() {
|
|
2707
|
+
return fail3("DIFF_LIMIT_EXCEEDED", "The project diff is too large");
|
|
2708
|
+
}
|
|
2709
|
+
function samePath(a, b) {
|
|
2710
|
+
return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
2711
|
+
}
|
|
2712
|
+
function safePath(path) {
|
|
2713
|
+
return path !== "" && isSafeSessionDirectoryPath(path) && !path.split("/").some((segment) => segment.toLowerCase() === ".git");
|
|
2714
|
+
}
|
|
2715
|
+
function decode(bytes) {
|
|
2716
|
+
try {
|
|
2717
|
+
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
2718
|
+
} catch (error) {
|
|
2719
|
+
throw new SessionGitDiffError("DIFF_UNAVAILABLE", "Git returned invalid text", { cause: error });
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
function environment() {
|
|
2723
|
+
const env = { ...process.env, LC_ALL: "C" };
|
|
2724
|
+
for (const key of Object.keys(env)) {
|
|
2725
|
+
if (key.toUpperCase().startsWith("GIT_")) delete env[key];
|
|
2726
|
+
}
|
|
2727
|
+
return {
|
|
2728
|
+
...env,
|
|
2729
|
+
GIT_OPTIONAL_LOCKS: "0",
|
|
2730
|
+
GIT_NO_LAZY_FETCH: "1",
|
|
2731
|
+
GIT_NO_REPLACE_OBJECTS: "1",
|
|
2732
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
function isExit(error, code) {
|
|
2736
|
+
return error instanceof Error && "code" in error && error.code === code && "stdout" in error && Buffer.isBuffer(error.stdout) && "stderr" in error && Buffer.isBuffer(error.stderr);
|
|
2737
|
+
}
|
|
2738
|
+
var Work = class {
|
|
2739
|
+
deadline = Date.now() + SESSION_GIT_DIFF_TIMEOUT_MS;
|
|
2740
|
+
env = environment();
|
|
2741
|
+
bytes = 0;
|
|
2742
|
+
#filterConfig = [];
|
|
2743
|
+
async disableExternalFilters(repository) {
|
|
2744
|
+
const names = records(await this.git(repository, [
|
|
2745
|
+
"config",
|
|
2746
|
+
"--includes",
|
|
2747
|
+
"--null",
|
|
2748
|
+
"--name-only",
|
|
2749
|
+
"--get-regexp",
|
|
2750
|
+
"^filter\\."
|
|
2751
|
+
], { allowExitOne: true }));
|
|
2752
|
+
const drivers = /* @__PURE__ */ new Set();
|
|
2753
|
+
for (const name of names) {
|
|
2754
|
+
const match = /^filter\.([^=\r\n]+)\.[^.]+$/u.exec(name);
|
|
2755
|
+
if (!match?.[1]) fail3("DIFF_BLOCKED", "The project contains an unsupported filter configuration");
|
|
2756
|
+
drivers.add(match[1]);
|
|
2757
|
+
}
|
|
2758
|
+
this.#filterConfig = [...drivers].flatMap((name) => [
|
|
2759
|
+
"-c",
|
|
2760
|
+
`filter.${name}.clean=`,
|
|
2761
|
+
"-c",
|
|
2762
|
+
`filter.${name}.process=`,
|
|
2763
|
+
"-c",
|
|
2764
|
+
`filter.${name}.required=false`
|
|
2765
|
+
]);
|
|
2766
|
+
if (this.#filterConfig.join(" ").length > 16384) limit();
|
|
2767
|
+
}
|
|
2768
|
+
check() {
|
|
2769
|
+
if (Date.now() >= this.deadline) fail3("DIFF_TIMEOUT", "The project diff request timed out");
|
|
2770
|
+
}
|
|
2771
|
+
async git(cwd, args, options = {}) {
|
|
2772
|
+
this.check();
|
|
2773
|
+
try {
|
|
2774
|
+
const stdout = await new Promise((resolve5, reject) => {
|
|
2775
|
+
const child = execFile("git", [
|
|
2776
|
+
"--literal-pathspecs",
|
|
2777
|
+
"-c",
|
|
2778
|
+
"core.fsmonitor=false",
|
|
2779
|
+
"-c",
|
|
2780
|
+
"core.hooksPath=",
|
|
2781
|
+
...this.#filterConfig,
|
|
2782
|
+
...args
|
|
2783
|
+
], {
|
|
2784
|
+
cwd,
|
|
2785
|
+
encoding: "buffer",
|
|
2786
|
+
windowsHide: true,
|
|
2787
|
+
timeout: Math.max(1, this.deadline - Date.now()),
|
|
2788
|
+
maxBuffer: options.maxBuffer ?? SESSION_GIT_DIFF_MAX_SOURCE_BYTES,
|
|
2789
|
+
env: {
|
|
2790
|
+
...this.env,
|
|
2791
|
+
...options.objectDirectory === void 0 ? {} : { GIT_OBJECT_DIRECTORY: options.objectDirectory },
|
|
2792
|
+
...options.isolated ? {
|
|
2793
|
+
GIT_DIR: join6(cwd, "no-repository"),
|
|
2794
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
2795
|
+
GIT_ATTR_NOSYSTEM: "1",
|
|
2796
|
+
GIT_CONFIG_GLOBAL: process.platform === "win32" ? "NUL" : "/dev/null"
|
|
2797
|
+
} : {}
|
|
2798
|
+
}
|
|
2799
|
+
}, (error, output2, stderr) => {
|
|
2800
|
+
if (error) reject(Object.assign(error, { stdout: output2, stderr }));
|
|
2801
|
+
else resolve5(output2);
|
|
2802
|
+
});
|
|
2803
|
+
child.stdin?.on("error", reject);
|
|
2804
|
+
child.stdin?.end(options.input);
|
|
2805
|
+
});
|
|
2806
|
+
this.check();
|
|
2807
|
+
return stdout;
|
|
2808
|
+
} catch (error) {
|
|
2809
|
+
this.check();
|
|
2810
|
+
if (options.allowExitOne && isExit(error, 1)) return error.stdout;
|
|
2811
|
+
if (error instanceof Error && "code" in error && error.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") limit();
|
|
2812
|
+
throw error;
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
};
|
|
2816
|
+
function identity(info) {
|
|
2817
|
+
return `${info.dev}\0${info.ino}\0${info.size}\0${info.mtimeNs}\0${info.mode}\0${info.nlink}`;
|
|
2818
|
+
}
|
|
2819
|
+
async function inspect(root, path, work, kind = "file") {
|
|
2820
|
+
if (!safePath(path)) fail3("DIFF_BLOCKED", "The selected path cannot be compared");
|
|
2821
|
+
let current = root;
|
|
2822
|
+
const segments = path.split("/");
|
|
2823
|
+
for (const [index, segment] of segments.entries()) {
|
|
2824
|
+
current = join6(current, segment);
|
|
2825
|
+
let info;
|
|
2826
|
+
try {
|
|
2827
|
+
info = await lstat4(current, { bigint: true });
|
|
2828
|
+
} catch (error) {
|
|
2829
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return void 0;
|
|
2830
|
+
throw error;
|
|
2831
|
+
}
|
|
2832
|
+
work.check();
|
|
2833
|
+
const last = index === segments.length - 1;
|
|
2834
|
+
if (last && kind === "link" && info.isSymbolicLink()) return info;
|
|
2835
|
+
if (info.isSymbolicLink() || !info.isFile() && !info.isDirectory()) {
|
|
2836
|
+
fail3("DIFF_BLOCKED", "Links, submodules and special files cannot be compared");
|
|
2837
|
+
}
|
|
2838
|
+
const canonical = await realpath4(current);
|
|
2839
|
+
if (!contained(root, canonical) || !samePath(canonical, current)) {
|
|
2840
|
+
fail3("DIFF_BLOCKED", "The selected path is outside the session");
|
|
2841
|
+
}
|
|
2842
|
+
work.check();
|
|
2843
|
+
if (info.isDirectory() !== (!last || kind === "directory")) return void 0;
|
|
2844
|
+
if (index === segments.length - 1) return info;
|
|
2845
|
+
}
|
|
2846
|
+
return void 0;
|
|
2847
|
+
}
|
|
2848
|
+
async function readCurrent(root, path, algorithm, work, keepBytes = false) {
|
|
2849
|
+
const before = await inspect(root, path, work);
|
|
2850
|
+
if (before === void 0) return void 0;
|
|
2851
|
+
if (before.size > BigInt(MAX_SCAN_BYTES - work.bytes) || keepBytes && before.size > BigInt(SESSION_GIT_DIFF_MAX_SOURCE_BYTES)) limit();
|
|
2852
|
+
work.bytes += Number(before.size);
|
|
2853
|
+
const handle = await open2(
|
|
2854
|
+
join6(root, ...path.split("/")),
|
|
2855
|
+
constants3.O_RDONLY | (constants3.O_NOFOLLOW ?? 0) | (constants3.O_NONBLOCK ?? 0)
|
|
2856
|
+
);
|
|
2857
|
+
try {
|
|
2858
|
+
const opened = await handle.stat({ bigint: true });
|
|
2859
|
+
if (!opened.isFile() || identity(opened) !== identity(before)) changed();
|
|
2860
|
+
const hash2 = createHash5(algorithm).update(`blob ${before.size}\0`);
|
|
2861
|
+
const buffer = Buffer.alloc(64 * 1024);
|
|
2862
|
+
const chunks = [];
|
|
2863
|
+
let offset = 0;
|
|
2864
|
+
for (; ; ) {
|
|
2865
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, offset);
|
|
2866
|
+
work.check();
|
|
2867
|
+
if (bytesRead === 0) break;
|
|
2868
|
+
offset += bytesRead;
|
|
2869
|
+
if (offset > Number(before.size)) changed();
|
|
2870
|
+
hash2.update(buffer.subarray(0, bytesRead));
|
|
2871
|
+
if (keepBytes) chunks.push(Buffer.from(buffer.subarray(0, bytesRead)));
|
|
2872
|
+
}
|
|
2873
|
+
const latest = await inspect(root, path, work);
|
|
2874
|
+
if (offset !== Number(before.size) || latest === void 0 || identity(latest) !== identity(before) || identity(await handle.stat({ bigint: true })) !== identity(before)) changed();
|
|
2875
|
+
const rawOid = hash2.digest("hex");
|
|
2876
|
+
return {
|
|
2877
|
+
oid: rawOid,
|
|
2878
|
+
rawOid,
|
|
2879
|
+
identity: identity(before),
|
|
2880
|
+
mode: process.platform !== "win32" && (before.mode & 0o111n) !== 0n ? "100755" : "100644",
|
|
2881
|
+
...keepBytes ? { bytes: Buffer.concat(chunks) } : {}
|
|
2882
|
+
};
|
|
2883
|
+
} finally {
|
|
2884
|
+
await handle.close();
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
async function base(repository, work) {
|
|
2888
|
+
for (const [baseRef, ref] of [
|
|
2889
|
+
["origin/main", "refs/remotes/origin/main"],
|
|
2890
|
+
["main", "refs/heads/main"]
|
|
2891
|
+
]) {
|
|
2892
|
+
const commit = decode(await work.git(repository, [
|
|
2893
|
+
"rev-parse",
|
|
2894
|
+
"--verify",
|
|
2895
|
+
"--quiet",
|
|
2896
|
+
`${ref}^{commit}`
|
|
2897
|
+
], { allowExitOne: true })).trim();
|
|
2898
|
+
if (!commit) continue;
|
|
2899
|
+
if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(commit)) {
|
|
2900
|
+
fail3("DIFF_BASE_UNAVAILABLE", "The main branch could not be resolved");
|
|
2901
|
+
}
|
|
2902
|
+
const baseCommit = decode(await work.git(repository, ["merge-base", "--all", "HEAD", commit], {
|
|
2903
|
+
allowExitOne: true
|
|
2904
|
+
})).trim();
|
|
2905
|
+
if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(baseCommit)) {
|
|
2906
|
+
fail3("DIFF_BASE_UNAVAILABLE", "The session and main do not have a unique merge base");
|
|
2907
|
+
}
|
|
2908
|
+
return { baseRef, baseCommit };
|
|
2909
|
+
}
|
|
2910
|
+
return fail3("DIFF_MAIN_UNAVAILABLE", "No local origin/main or main reference is available");
|
|
2911
|
+
}
|
|
2912
|
+
function scopedPath(context, path) {
|
|
2913
|
+
const prefix = context.prefix === "" ? "" : `${context.prefix}/`;
|
|
2914
|
+
if (!path.startsWith(prefix)) fail3("DIFF_BLOCKED", "Git returned a path outside the session");
|
|
2915
|
+
const scoped = path.slice(prefix.length);
|
|
2916
|
+
if (!safePath(scoped)) fail3("DIFF_BLOCKED", "The project contains an unsupported path");
|
|
2917
|
+
return scoped;
|
|
2918
|
+
}
|
|
2919
|
+
function records(output2) {
|
|
2920
|
+
const text = decode(output2);
|
|
2921
|
+
if (text === "") return [];
|
|
2922
|
+
if (!text.endsWith("\0")) fail3("DIFF_UNAVAILABLE", "Git returned an incomplete file list");
|
|
2923
|
+
return text.slice(0, -1).split("\0");
|
|
2924
|
+
}
|
|
2925
|
+
async function changes(context, work) {
|
|
2926
|
+
const paths = context.prefix === "" ? [] : [context.prefix];
|
|
2927
|
+
const tree = records(await work.git(context.repository, [
|
|
2928
|
+
"ls-tree",
|
|
2929
|
+
"-r",
|
|
2930
|
+
"-z",
|
|
2931
|
+
"--full-tree",
|
|
2932
|
+
context.baseCommit,
|
|
2933
|
+
"--",
|
|
2934
|
+
...paths
|
|
2935
|
+
]));
|
|
2936
|
+
const blobs = /* @__PURE__ */ new Map();
|
|
2937
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
2938
|
+
const tracked = /* @__PURE__ */ new Map();
|
|
2939
|
+
const scanned = /* @__PURE__ */ new Set();
|
|
2940
|
+
const scoped = (path) => {
|
|
2941
|
+
const scoped2 = scopedPath(context, path);
|
|
2942
|
+
scanned.add(scoped2);
|
|
2943
|
+
if (scanned.size > MAX_SCAN_FILES) limit();
|
|
2944
|
+
return scoped2;
|
|
2945
|
+
};
|
|
2946
|
+
for (const record of tree) {
|
|
2947
|
+
const match = /^(\d{6}) (blob|commit) ([a-f0-9]{40}|[a-f0-9]{64})\t(.+)$/u.exec(record);
|
|
2948
|
+
if (!match?.[1] || !match[3] || !match[4]) fail3("DIFF_UNAVAILABLE", "Git returned an invalid tree");
|
|
2949
|
+
blobs.set(scoped(match[4]), { mode: match[1], oid: match[3] });
|
|
2950
|
+
}
|
|
2951
|
+
for (const record of records(await work.git(context.repository, [
|
|
2952
|
+
"ls-files",
|
|
2953
|
+
"--stage",
|
|
2954
|
+
"--full-name",
|
|
2955
|
+
"-z",
|
|
2956
|
+
"--",
|
|
2957
|
+
...paths
|
|
2958
|
+
]))) {
|
|
2959
|
+
const match = /^(\d{6}) ([a-f0-9]{40}|[a-f0-9]{64}) [0-3]\t(.+)$/u.exec(record);
|
|
2960
|
+
if (!match?.[1] || !match[2] || !match[3]) fail3("DIFF_UNAVAILABLE", "Git returned an invalid index");
|
|
2961
|
+
tracked.set(scoped(match[3]), { mode: match[1], oid: match[2] });
|
|
2962
|
+
}
|
|
2963
|
+
for (const path of scanned) {
|
|
2964
|
+
const original = blobs.get(path);
|
|
2965
|
+
const indexed = tracked.get(path);
|
|
2966
|
+
if (original?.oid !== indexed?.oid || original?.mode !== indexed?.mode || original !== void 0 && !["100644", "100755"].includes(original.mode)) candidates.add(path);
|
|
2967
|
+
}
|
|
2968
|
+
for (const path of records(await work.git(context.repository, [
|
|
2969
|
+
"ls-files",
|
|
2970
|
+
"--modified",
|
|
2971
|
+
"--deleted",
|
|
2972
|
+
"--full-name",
|
|
2973
|
+
"-z",
|
|
2974
|
+
"--",
|
|
2975
|
+
...paths
|
|
2976
|
+
]))) candidates.add(scoped(path));
|
|
2977
|
+
const untracked = [];
|
|
2978
|
+
for (const path of records(await work.git(context.repository, [
|
|
2979
|
+
"ls-files",
|
|
2980
|
+
"--others",
|
|
2981
|
+
"--exclude-standard",
|
|
2982
|
+
"--full-name",
|
|
2983
|
+
"-z",
|
|
2984
|
+
"--",
|
|
2985
|
+
...paths
|
|
2986
|
+
]))) {
|
|
2987
|
+
const relativePath = scoped(path);
|
|
2988
|
+
candidates.add(relativePath);
|
|
2989
|
+
if (!blobs.has(relativePath)) untracked.push({ path: relativePath, status: "untracked" });
|
|
2990
|
+
}
|
|
2991
|
+
if (untracked.length > SESSION_GIT_DIFF_MAX_FILES || Buffer.byteLength(JSON.stringify({
|
|
2992
|
+
rootRevision: context.rootRevision,
|
|
2993
|
+
baseRef: context.baseRef,
|
|
2994
|
+
baseCommit: context.baseCommit,
|
|
2995
|
+
files: untracked
|
|
2996
|
+
}), "utf8") > SESSION_GIT_DIFF_MAX_RESPONSE_BYTES) limit();
|
|
2997
|
+
const result = [];
|
|
2998
|
+
const algorithm = context.baseCommit.length === 64 ? "sha256" : "sha1";
|
|
2999
|
+
const currentFiles = /* @__PURE__ */ new Map();
|
|
3000
|
+
for (const path of [...candidates].sort()) {
|
|
3001
|
+
work.check();
|
|
3002
|
+
const original = blobs.get(path);
|
|
3003
|
+
const indexed = tracked.get(path);
|
|
3004
|
+
if ([original?.mode, indexed?.mode].some((mode) => mode !== void 0 && mode !== "100644" && mode !== "100755")) {
|
|
3005
|
+
await unchangedSpecial(context, path, original, indexed, work);
|
|
3006
|
+
candidates.delete(path);
|
|
3007
|
+
continue;
|
|
3008
|
+
}
|
|
3009
|
+
const current = await readCurrent(context.root, path, algorithm, work);
|
|
3010
|
+
if (current !== void 0) currentFiles.set(path, current);
|
|
3011
|
+
}
|
|
3012
|
+
if (currentFiles.size > 0) {
|
|
3013
|
+
const hashes = decode(await work.git(context.repository, ["hash-object", "--stdin-paths"], {
|
|
3014
|
+
input: [...currentFiles.keys()].map((path) => [context.prefix, path].filter(Boolean).join("/")).join("\n") + "\n"
|
|
3015
|
+
})).trim().split("\n");
|
|
3016
|
+
if (hashes.length !== currentFiles.size) changed();
|
|
3017
|
+
for (const [index, [path, current]] of [...currentFiles].entries()) {
|
|
3018
|
+
const oid = hashes[index];
|
|
3019
|
+
if (!oid || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(oid)) changed();
|
|
3020
|
+
const latest = await inspect(context.root, path, work);
|
|
3021
|
+
if (latest === void 0 || identity(latest) !== current.identity) changed();
|
|
3022
|
+
current.oid = oid;
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
for (const path of [...candidates].sort()) {
|
|
3026
|
+
const original = blobs.get(path);
|
|
3027
|
+
const current = currentFiles.get(path);
|
|
3028
|
+
if (original === void 0 && current === void 0) continue;
|
|
3029
|
+
if (original?.oid === current?.oid && (process.platform === "win32" || original?.mode === current?.mode)) continue;
|
|
3030
|
+
result.push({
|
|
3031
|
+
entry: { path, status: original === void 0 ? tracked.has(path) ? "added" : "untracked" : current === void 0 ? "deleted" : "modified" },
|
|
3032
|
+
base: original,
|
|
3033
|
+
current
|
|
3034
|
+
});
|
|
3035
|
+
if (result.length > SESSION_GIT_DIFF_MAX_FILES) limit();
|
|
3036
|
+
}
|
|
3037
|
+
return result;
|
|
3038
|
+
}
|
|
3039
|
+
async function unchangedSpecial(context, path, original, indexed, work) {
|
|
3040
|
+
const blocked = () => fail3("DIFF_BLOCKED", "Changed links and submodules cannot be compared");
|
|
3041
|
+
if (original === void 0 || indexed?.mode !== original.mode || indexed.oid !== original.oid) return blocked();
|
|
3042
|
+
if (original.mode === "160000") {
|
|
3043
|
+
if (await inspect(context.root, path, work, "directory") === void 0) blocked();
|
|
3044
|
+
const target = join6(context.root, ...path.split("/"));
|
|
3045
|
+
try {
|
|
3046
|
+
await lstat4(join6(target, ".git"));
|
|
3047
|
+
} catch (error) {
|
|
3048
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return;
|
|
3049
|
+
throw error;
|
|
3050
|
+
}
|
|
3051
|
+
if (decode(await work.git(target, ["rev-parse", "--verify", "HEAD"])).trim() !== original.oid) blocked();
|
|
3052
|
+
} else if (original.mode === "120000") {
|
|
3053
|
+
const info = await inspect(context.root, path, work, "link");
|
|
3054
|
+
if (info === void 0) return blocked();
|
|
3055
|
+
const algorithm = context.baseCommit.length === 64 ? "sha256" : "sha1";
|
|
3056
|
+
let oid;
|
|
3057
|
+
if (info.isSymbolicLink()) {
|
|
3058
|
+
const target = Buffer.from(await readlink(join6(context.root, ...path.split("/"))), "utf8");
|
|
3059
|
+
oid = createHash5(algorithm).update(`blob ${target.length}\0`).update(target).digest("hex");
|
|
3060
|
+
} else {
|
|
3061
|
+
oid = (await readCurrent(context.root, path, algorithm, work))?.rawOid;
|
|
3062
|
+
}
|
|
3063
|
+
if (oid !== original.oid) blocked();
|
|
3064
|
+
} else blocked();
|
|
3065
|
+
}
|
|
3066
|
+
function binary(bytes) {
|
|
3067
|
+
if (bytes.includes(0)) return true;
|
|
3068
|
+
try {
|
|
3069
|
+
new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
3070
|
+
return false;
|
|
3071
|
+
} catch {
|
|
3072
|
+
return true;
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
async function patch(context, change, work, scratchDirectory) {
|
|
3076
|
+
const algorithm = context.baseCommit.length === 64 ? "sha256" : "sha1";
|
|
3077
|
+
const current = await readCurrent(context.root, change.entry.path, algorithm, work, true);
|
|
3078
|
+
if (current?.rawOid !== change.current?.rawOid || current?.mode !== change.current?.mode) changed();
|
|
3079
|
+
let original = Buffer.alloc(0);
|
|
3080
|
+
if (change.base !== void 0) {
|
|
3081
|
+
const size = Number(decode(await work.git(context.repository, ["cat-file", "-s", change.base.oid])).trim());
|
|
3082
|
+
if (!Number.isSafeInteger(size) || size < 0 || size > SESSION_GIT_DIFF_MAX_SOURCE_BYTES) limit();
|
|
3083
|
+
original = await work.git(context.repository, ["cat-file", "blob", change.base.oid], { maxBuffer: size + 1 });
|
|
3084
|
+
if (original.length !== size) changed();
|
|
3085
|
+
}
|
|
3086
|
+
const directory = resolve2(scratchDirectory);
|
|
3087
|
+
if (contained(context.repository, directory)) {
|
|
3088
|
+
fail3("DIFF_UNAVAILABLE", "Project diff storage must be outside the session repository");
|
|
3089
|
+
}
|
|
3090
|
+
await mkdir2(directory, { recursive: true, mode: 448 });
|
|
3091
|
+
if ((await lstat4(directory)).isSymbolicLink() || !samePath(await realpath4(directory), directory)) {
|
|
3092
|
+
fail3("DIFF_UNAVAILABLE", "Project diff storage cannot use links");
|
|
3093
|
+
}
|
|
3094
|
+
const scratch = join6(directory, randomUUID3());
|
|
3095
|
+
await mkdir2(scratch, { mode: 448 });
|
|
3096
|
+
let text;
|
|
3097
|
+
try {
|
|
3098
|
+
let bytes = current?.bytes ?? Buffer.alloc(0);
|
|
3099
|
+
if (current !== void 0) {
|
|
3100
|
+
const objectDirectory = join6(scratch, "objects");
|
|
3101
|
+
await mkdir2(objectDirectory);
|
|
3102
|
+
const path = [context.prefix, change.entry.path].filter(Boolean).join("/");
|
|
3103
|
+
const oid = decode(await work.git(context.repository, ["hash-object", "-w", "--stdin", `--path=${path}`], {
|
|
3104
|
+
input: bytes,
|
|
3105
|
+
objectDirectory
|
|
3106
|
+
})).trim();
|
|
3107
|
+
if (oid !== change.current?.oid) changed();
|
|
3108
|
+
bytes = await work.git(context.repository, ["cat-file", "blob", oid], {
|
|
3109
|
+
objectDirectory,
|
|
3110
|
+
maxBuffer: SESSION_GIT_DIFF_MAX_SOURCE_BYTES
|
|
3111
|
+
});
|
|
3112
|
+
}
|
|
3113
|
+
if (binary(original) || binary(bytes)) return { patch: "", binary: true };
|
|
3114
|
+
for (const side of ["a", "b"]) await mkdir2(join6(scratch, side));
|
|
3115
|
+
for (const [side, content, mode] of [
|
|
3116
|
+
["a", original, change.base?.mode],
|
|
3117
|
+
["b", bytes, current?.mode]
|
|
3118
|
+
]) {
|
|
3119
|
+
if (mode === void 0) continue;
|
|
3120
|
+
const file = join6(scratch, side, ...change.entry.path.split("/"));
|
|
3121
|
+
await mkdir2(dirname(file), { recursive: true });
|
|
3122
|
+
await writeFile2(file, content, { flag: "wx", mode: 384 });
|
|
3123
|
+
if (process.platform !== "win32") await chmod(file, mode === "100755" ? 448 : 384);
|
|
3124
|
+
}
|
|
3125
|
+
text = decode(await work.git(scratch, [
|
|
3126
|
+
"-c",
|
|
3127
|
+
"core.quotePath=false",
|
|
3128
|
+
"-c",
|
|
3129
|
+
"core.autocrlf=false",
|
|
3130
|
+
"diff",
|
|
3131
|
+
"--no-index",
|
|
3132
|
+
"--no-prefix",
|
|
3133
|
+
"--no-ext-diff",
|
|
3134
|
+
"--no-textconv",
|
|
3135
|
+
"--no-renames",
|
|
3136
|
+
"--no-color",
|
|
3137
|
+
"--text",
|
|
3138
|
+
"--unified=3",
|
|
3139
|
+
"--",
|
|
3140
|
+
"a",
|
|
3141
|
+
"b"
|
|
3142
|
+
], { isolated: true, allowExitOne: true, maxBuffer: SESSION_GIT_DIFF_MAX_PATCH_BYTES }));
|
|
3143
|
+
} finally {
|
|
3144
|
+
await rm(scratch, { recursive: true, force: true, maxRetries: 3 });
|
|
3145
|
+
}
|
|
3146
|
+
if (Buffer.byteLength(text, "utf8") > SESSION_GIT_DIFF_MAX_PATCH_BYTES) limit();
|
|
3147
|
+
return { patch: text, binary: false };
|
|
3148
|
+
}
|
|
3149
|
+
var SessionGitDiffReader = class {
|
|
3150
|
+
constructor(metadata, scratchDirectory) {
|
|
3151
|
+
this.metadata = metadata;
|
|
3152
|
+
this.scratchDirectory = scratchDirectory;
|
|
3153
|
+
}
|
|
3154
|
+
metadata;
|
|
3155
|
+
scratchDirectory;
|
|
3156
|
+
#active = 0;
|
|
3157
|
+
async read(sessionId, data) {
|
|
3158
|
+
if (!SessionGitDiffDataSchema.safeParse(data).success || data.path !== void 0 && !safePath(data.path)) {
|
|
3159
|
+
fail3("INVALID_DIFF_PATH", "The project diff request is invalid");
|
|
3160
|
+
}
|
|
3161
|
+
if (this.#active >= SESSION_GIT_DIFF_CONCURRENCY) fail3("DIFF_BUSY", "Too many project diff requests");
|
|
3162
|
+
this.#active++;
|
|
3163
|
+
const work = new Work();
|
|
3164
|
+
const pending = this.#read(sessionId, data, work).finally(() => {
|
|
3165
|
+
this.#active--;
|
|
3166
|
+
});
|
|
3167
|
+
let timer;
|
|
3168
|
+
try {
|
|
3169
|
+
return await Promise.race([
|
|
3170
|
+
pending,
|
|
3171
|
+
new Promise((_, reject) => {
|
|
3172
|
+
timer = setTimeout(() => reject(new SessionGitDiffError(
|
|
3173
|
+
"DIFF_TIMEOUT",
|
|
3174
|
+
"The project diff request timed out"
|
|
3175
|
+
)), SESSION_GIT_DIFF_TIMEOUT_MS);
|
|
3176
|
+
})
|
|
3177
|
+
]);
|
|
3178
|
+
} catch (error) {
|
|
3179
|
+
if (error instanceof SessionGitDiffError) throw error;
|
|
3180
|
+
throw new SessionGitDiffError("DIFF_UNAVAILABLE", "The project diff is unavailable", { cause: error });
|
|
3181
|
+
} finally {
|
|
3182
|
+
clearTimeout(timer);
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
async #context(sessionId, rootRevision, work) {
|
|
3186
|
+
const metadata = await this.metadata(sessionId);
|
|
3187
|
+
work.check();
|
|
3188
|
+
if (metadata === void 0) fail3("SESSION_NOT_FOUND", "The session is unavailable");
|
|
3189
|
+
const workingDirectory = metadata.workingDirectory;
|
|
3190
|
+
if (!workingDirectory || !isAbsolute5(workingDirectory)) {
|
|
3191
|
+
fail3("SESSION_ROOT_UNAVAILABLE", "The session working directory is unavailable");
|
|
3192
|
+
}
|
|
3193
|
+
let root;
|
|
3194
|
+
let rootIdentity;
|
|
3195
|
+
try {
|
|
3196
|
+
root = await realpath4(workingDirectory);
|
|
3197
|
+
if ((await lstat4(workingDirectory)).isSymbolicLink() || !samePath(root, resolve2(workingDirectory))) {
|
|
3198
|
+
fail3("DIFF_BLOCKED", "Links cannot be compared");
|
|
3199
|
+
}
|
|
3200
|
+
const info = await stat5(root, { bigint: true });
|
|
3201
|
+
if (!info.isDirectory()) fail3("SESSION_ROOT_UNAVAILABLE", "The session working directory is unavailable");
|
|
3202
|
+
rootIdentity = sessionRootIdentity(root, info);
|
|
3203
|
+
} catch (error) {
|
|
3204
|
+
if (error instanceof SessionGitDiffError) throw error;
|
|
3205
|
+
throw new SessionGitDiffError("SESSION_ROOT_UNAVAILABLE", "The session working directory is unavailable", { cause: error });
|
|
3206
|
+
}
|
|
3207
|
+
if (createHash5("sha256").update(rootIdentity).digest("base64url") !== rootRevision) {
|
|
3208
|
+
fail3("SESSION_ROOT_CHANGED", "The session directory changed");
|
|
3209
|
+
}
|
|
3210
|
+
let repository;
|
|
3211
|
+
try {
|
|
3212
|
+
const top = decode(await work.git(root, ["rev-parse", "--show-toplevel"])).replace(/\r?\n$/u, "");
|
|
3213
|
+
if (!isAbsolute5(top)) fail3("DIFF_UNAVAILABLE", "Git returned an invalid repository");
|
|
3214
|
+
repository = await realpath4(top);
|
|
3215
|
+
} catch (error) {
|
|
3216
|
+
if (error instanceof SessionGitDiffError) throw error;
|
|
3217
|
+
if (isExit(error, 128) && decode(error.stderr).startsWith("fatal: not a git repository")) {
|
|
3218
|
+
throw new SessionGitDiffError("DIFF_NOT_REPOSITORY", "The session directory is not a Git repository", { cause: error });
|
|
3219
|
+
}
|
|
3220
|
+
throw error;
|
|
3221
|
+
}
|
|
3222
|
+
if (!contained(repository, root)) fail3("DIFF_BLOCKED", "The repository is outside the session root");
|
|
3223
|
+
if (decode(await work.git(root, ["rev-parse", "--show-superproject-working-tree"])).trim() !== "") {
|
|
3224
|
+
fail3("DIFF_BLOCKED", "Submodule content cannot be compared");
|
|
3225
|
+
}
|
|
3226
|
+
return {
|
|
3227
|
+
root,
|
|
3228
|
+
rootRevision,
|
|
3229
|
+
rootIdentity,
|
|
3230
|
+
workingDirectory,
|
|
3231
|
+
repository,
|
|
3232
|
+
repositoryIdentity: sessionRootIdentity(repository, await stat5(repository, { bigint: true })),
|
|
3233
|
+
prefix: relative2(repository, root).split(sep2).join("/"),
|
|
3234
|
+
...await base(repository, work)
|
|
3235
|
+
};
|
|
3236
|
+
}
|
|
3237
|
+
async #read(sessionId, data, work) {
|
|
3238
|
+
const context = await this.#context(sessionId, data.rootRevision, work);
|
|
3239
|
+
if (data.baseCommit !== void 0 && data.baseCommit !== context.baseCommit) changed();
|
|
3240
|
+
await work.disableExternalFilters(context.repository);
|
|
3241
|
+
const files = await changes(context, work);
|
|
3242
|
+
const common = { rootRevision: data.rootRevision, baseRef: context.baseRef, baseCommit: context.baseCommit };
|
|
3243
|
+
let result;
|
|
3244
|
+
let selected;
|
|
3245
|
+
if (data.path === void 0) {
|
|
3246
|
+
result = { ...common, files: files.map(({ entry }) => entry) };
|
|
3247
|
+
} else {
|
|
3248
|
+
selected = files.find(({ entry }) => entry.path === data.path);
|
|
3249
|
+
if (selected === void 0) changed();
|
|
3250
|
+
result = { ...common, path: data.path, ...await patch(context, selected, work, this.scratchDirectory) };
|
|
3251
|
+
}
|
|
3252
|
+
let latest;
|
|
3253
|
+
try {
|
|
3254
|
+
latest = await this.#context(sessionId, data.rootRevision, work);
|
|
3255
|
+
} catch (error) {
|
|
3256
|
+
if (error instanceof SessionGitDiffError) {
|
|
3257
|
+
if (["SESSION_NOT_FOUND", "SESSION_ROOT_UNAVAILABLE"].includes(error.code)) {
|
|
3258
|
+
throw new SessionGitDiffError("SESSION_ROOT_CHANGED", "The session directory changed", { cause: error });
|
|
3259
|
+
}
|
|
3260
|
+
if (["DIFF_MAIN_UNAVAILABLE", "DIFF_BASE_UNAVAILABLE", "DIFF_NOT_REPOSITORY"].includes(error.code)) changed();
|
|
3261
|
+
}
|
|
3262
|
+
throw error;
|
|
3263
|
+
}
|
|
3264
|
+
if (latest.workingDirectory !== context.workingDirectory || latest.rootIdentity !== context.rootIdentity) {
|
|
3265
|
+
fail3("SESSION_ROOT_CHANGED", "The session directory changed");
|
|
3266
|
+
}
|
|
3267
|
+
if (latest.repositoryIdentity !== context.repositoryIdentity || latest.baseRef !== context.baseRef || latest.baseCommit !== context.baseCommit) changed();
|
|
3268
|
+
if (selected !== void 0) {
|
|
3269
|
+
const current = await readCurrent(
|
|
3270
|
+
context.root,
|
|
3271
|
+
selected.entry.path,
|
|
3272
|
+
context.baseCommit.length === 64 ? "sha256" : "sha1",
|
|
3273
|
+
work
|
|
3274
|
+
);
|
|
3275
|
+
if (current?.rawOid !== selected.current?.rawOid || current?.mode !== selected.current?.mode) changed();
|
|
3276
|
+
}
|
|
3277
|
+
work.check();
|
|
3278
|
+
if (Buffer.byteLength(JSON.stringify(result), "utf8") > SESSION_GIT_DIFF_MAX_RESPONSE_BYTES) limit();
|
|
3279
|
+
return SessionGitDiffResultDataSchema.parse(result);
|
|
3280
|
+
}
|
|
3281
|
+
};
|
|
3282
|
+
|
|
3283
|
+
// src/timeline-image-cache.ts
|
|
3284
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
3285
|
+
var TimelineAssetError = class extends Error {
|
|
3286
|
+
constructor(code, message, options) {
|
|
3287
|
+
super(message, options);
|
|
3288
|
+
this.code = code;
|
|
3289
|
+
}
|
|
3290
|
+
code;
|
|
3291
|
+
};
|
|
3292
|
+
var TimelineImageCache = class {
|
|
3293
|
+
constructor(maxBytes = 32 * 1024 * 1024) {
|
|
3294
|
+
this.maxBytes = maxBytes;
|
|
3295
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < TIMELINE_IMAGE_MAX_BYTES) {
|
|
3296
|
+
throw new Error("Image cache must hold at least one maximum-size image");
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
maxBytes;
|
|
3300
|
+
#assets = /* @__PURE__ */ new Map();
|
|
3301
|
+
#bytes = 0;
|
|
3302
|
+
get bytes() {
|
|
3303
|
+
return this.#bytes;
|
|
3304
|
+
}
|
|
3305
|
+
referenceItem(sessionId, item) {
|
|
3306
|
+
if (!("attachments" in item) || item.attachments === void 0) return item;
|
|
3307
|
+
return { ...item, attachments: item.attachments.map((attachment) => this.#reference(sessionId, attachment)) };
|
|
3308
|
+
}
|
|
3309
|
+
read(sessionId, request) {
|
|
3310
|
+
const key = JSON.stringify([sessionId, request.sha256]);
|
|
3311
|
+
const asset = this.#assets.get(key);
|
|
3312
|
+
if (asset === void 0) throw new TimelineAssetError("IMAGE_ABSENT", "The image is no longer cached on this PC");
|
|
3313
|
+
if (request.offset >= asset.bytes.length) {
|
|
3314
|
+
throw new TimelineAssetError("IMAGE_OFFSET_INVALID", "The image chunk offset exceeds its length");
|
|
3315
|
+
}
|
|
3316
|
+
this.#assets.delete(key);
|
|
3317
|
+
this.#assets.set(key, asset);
|
|
3318
|
+
const end = Math.min(request.offset + TIMELINE_IMAGE_CHUNK_BYTES, asset.bytes.length);
|
|
3319
|
+
return TimelineImageChunkSchema.parse({
|
|
3320
|
+
sha256: request.sha256,
|
|
3321
|
+
offset: request.offset,
|
|
3322
|
+
totalBytes: asset.bytes.length,
|
|
3323
|
+
mimeType: asset.mimeType,
|
|
3324
|
+
data: asset.bytes.subarray(request.offset, end).toString("base64"),
|
|
3325
|
+
eof: end === asset.bytes.length
|
|
3326
|
+
});
|
|
3327
|
+
}
|
|
3328
|
+
readAsset(sessionId, sha256) {
|
|
3329
|
+
const key = JSON.stringify([sessionId, sha256]);
|
|
3330
|
+
const asset = this.#assets.get(key);
|
|
3331
|
+
if (asset === void 0) throw new TimelineAssetError("IMAGE_ABSENT", "The image is no longer cached on this PC");
|
|
3332
|
+
this.#assets.delete(key);
|
|
3333
|
+
this.#assets.set(key, asset);
|
|
3334
|
+
return { bytes: Buffer.from(asset.bytes), mimeType: asset.mimeType };
|
|
3335
|
+
}
|
|
3336
|
+
clearSession(sessionId) {
|
|
3337
|
+
const prefix = JSON.stringify([sessionId]).slice(0, -1) + ",";
|
|
3338
|
+
for (const [key, asset] of this.#assets) {
|
|
3339
|
+
if (key.startsWith(prefix)) {
|
|
3340
|
+
this.#assets.delete(key);
|
|
3341
|
+
this.#bytes -= asset.bytes.length;
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
clear() {
|
|
3346
|
+
this.#assets.clear();
|
|
3347
|
+
this.#bytes = 0;
|
|
3348
|
+
}
|
|
3349
|
+
#reference(sessionId, attachment) {
|
|
3350
|
+
if (attachment.kind !== "image" || attachment.data === void 0) return attachment;
|
|
3351
|
+
const parsed = ConversationAttachmentSchema.parse(attachment);
|
|
3352
|
+
const bytes = Buffer.from(attachment.data, "base64");
|
|
3353
|
+
if (bytes.length !== parsed.byteLength || createHash6("sha256").update(bytes).digest("hex") !== parsed.sha256 || !hasSignature(bytes, parsed.mimeType)) {
|
|
3354
|
+
throw new TimelineAssetError("IMAGE_INVALID", "Image content does not match its metadata");
|
|
3355
|
+
}
|
|
3356
|
+
const key = JSON.stringify([sessionId, parsed.sha256]);
|
|
3357
|
+
const previous = this.#assets.get(key);
|
|
3358
|
+
if (previous !== void 0) this.#bytes -= previous.bytes.length;
|
|
3359
|
+
this.#assets.delete(key);
|
|
3360
|
+
this.#assets.set(key, { bytes, mimeType: parsed.mimeType ?? "" });
|
|
3361
|
+
this.#bytes += bytes.length;
|
|
3362
|
+
while (this.#bytes > this.maxBytes) {
|
|
3363
|
+
const oldest = this.#assets.entries().next().value;
|
|
3364
|
+
if (oldest === void 0) break;
|
|
3365
|
+
this.#assets.delete(oldest[0]);
|
|
3366
|
+
this.#bytes -= oldest[1].bytes.length;
|
|
3367
|
+
}
|
|
3368
|
+
const { data: _data, ...metadata } = parsed;
|
|
3369
|
+
return { ...metadata, imageRef: "sha256" };
|
|
3370
|
+
}
|
|
3371
|
+
};
|
|
3372
|
+
function hasSignature(bytes, mimeType) {
|
|
3373
|
+
switch (mimeType) {
|
|
3374
|
+
case "image/png":
|
|
3375
|
+
return bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
|
|
3376
|
+
case "image/jpeg":
|
|
3377
|
+
return bytes.subarray(0, 3).equals(Buffer.from([255, 216, 255]));
|
|
3378
|
+
case "image/gif":
|
|
3379
|
+
return ["GIF87a", "GIF89a"].includes(bytes.subarray(0, 6).toString("ascii"));
|
|
3380
|
+
case "image/webp":
|
|
3381
|
+
return bytes.subarray(0, 4).toString("ascii") === "RIFF" && bytes.subarray(8, 12).toString("ascii") === "WEBP";
|
|
3382
|
+
default:
|
|
3383
|
+
return false;
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
// src/session-image-uploader.ts
|
|
3388
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
3389
|
+
import sharp2 from "sharp";
|
|
3390
|
+
import { z as z6 } from "zod";
|
|
3391
|
+
var MAX_ACTIVE_UPLOADS = 3;
|
|
3392
|
+
var MAX_JSON_BYTES = 16 * 1024;
|
|
3393
|
+
var REQUEST_TIMEOUT_MS = 2e4;
|
|
3394
|
+
var UPLOAD_TIMEOUT_MS = 7e4;
|
|
3395
|
+
var MAX_GRANT_LIFETIME_MS = 10 * 6e4 + 3e4;
|
|
3396
|
+
var UploaderConfigSchema = z6.object({
|
|
3397
|
+
relayUrl: SessionImageRelayUrlSchema,
|
|
3398
|
+
relayToken: z6.string().min(24).max(4096).regex(/^[\x21-\x7e]+$/u),
|
|
3399
|
+
userId: IdSchema.refine((value) => /^[\x20-\x7e]+$/u.test(value)),
|
|
3400
|
+
machineId: IdSchema,
|
|
3401
|
+
storageAccount: SessionImagesConfigSchema.shape.storageAccount,
|
|
3402
|
+
storageContainer: SessionImagesConfigSchema.shape.storageContainer
|
|
3403
|
+
});
|
|
3404
|
+
function safeCause(error) {
|
|
3405
|
+
if (error instanceof SessionImageUploadError) return error;
|
|
3406
|
+
if (error instanceof z6.ZodError) return new Error("Schema validation failed.");
|
|
3407
|
+
const result = new Error("Image operation failed.");
|
|
3408
|
+
if (error instanceof Error) {
|
|
3409
|
+
if (["TypeError", "SyntaxError", "AbortError", "TimeoutError"].includes(error.name)) result.name = error.name;
|
|
3410
|
+
const code = "code" in error ? error.code : void 0;
|
|
3411
|
+
if (typeof code === "string" && ["ECONNRESET", "ECONNREFUSED", "ETIMEDOUT", "ENOTFOUND", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT"].includes(code)) {
|
|
3412
|
+
result.cause = { code };
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
return result;
|
|
3416
|
+
}
|
|
3417
|
+
var SessionImageUploadError = class extends Error {
|
|
3418
|
+
constructor(code, stage, status, cause) {
|
|
3419
|
+
super(`${code} (${stage})`, cause === void 0 ? void 0 : { cause: safeCause(cause) });
|
|
3420
|
+
this.code = code;
|
|
3421
|
+
this.stage = stage;
|
|
3422
|
+
this.status = status;
|
|
3423
|
+
this.name = "SessionImageUploadError";
|
|
3424
|
+
}
|
|
3425
|
+
code;
|
|
3426
|
+
stage;
|
|
3427
|
+
status;
|
|
3428
|
+
};
|
|
3429
|
+
function digest(bytes) {
|
|
3430
|
+
return createHash7("sha256").update(bytes).digest("hex");
|
|
3431
|
+
}
|
|
3432
|
+
function assert(condition, code, stage) {
|
|
3433
|
+
if (!condition) throw new SessionImageUploadError(code, stage);
|
|
3434
|
+
}
|
|
3435
|
+
function parse(schema, input, stage) {
|
|
3436
|
+
const result = schema.safeParse(input);
|
|
3437
|
+
if (!result.success) throw new SessionImageUploadError(
|
|
3438
|
+
stage === "config" ? "IMAGE_CONFIG_INVALID" : stage === "source" ? "IMAGE_CONTENT_INVALID" : "IMAGE_RESPONSE_INVALID",
|
|
3439
|
+
stage,
|
|
3440
|
+
void 0,
|
|
3441
|
+
result.error
|
|
3442
|
+
);
|
|
3443
|
+
return result.data;
|
|
3444
|
+
}
|
|
3445
|
+
function abortable(operation, signal, stage) {
|
|
3446
|
+
return new Promise((resolve5, reject) => {
|
|
3447
|
+
const abort = () => reject(new SessionImageUploadError("IMAGE_ABORTED", stage));
|
|
3448
|
+
if (signal.aborted) abort();
|
|
3449
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
3450
|
+
void operation.then((value) => {
|
|
3451
|
+
signal.removeEventListener("abort", abort);
|
|
3452
|
+
resolve5(value);
|
|
3453
|
+
}, (error) => {
|
|
3454
|
+
signal.removeEventListener("abort", abort);
|
|
3455
|
+
reject(error);
|
|
3456
|
+
});
|
|
3457
|
+
});
|
|
3458
|
+
}
|
|
3459
|
+
var SessionImageUploader = class {
|
|
3460
|
+
#config;
|
|
3461
|
+
#fetch;
|
|
3462
|
+
#active = /* @__PURE__ */ new Set();
|
|
3463
|
+
#closed = false;
|
|
3464
|
+
constructor(config, dependencies = {}) {
|
|
3465
|
+
this.#config = parse(UploaderConfigSchema, config, "config");
|
|
3466
|
+
this.#fetch = dependencies.fetch ?? globalThis.fetch;
|
|
3467
|
+
}
|
|
3468
|
+
close() {
|
|
3469
|
+
this.#closed = true;
|
|
3470
|
+
for (const controller of this.#active) controller.abort();
|
|
3471
|
+
}
|
|
3472
|
+
async upload(sessionId, source, signal, assertCurrent = () => void 0) {
|
|
3473
|
+
assert(!this.#closed, "IMAGE_UPLOADER_CLOSED", "source");
|
|
3474
|
+
assert(!signal?.aborted, "IMAGE_ABORTED", "source");
|
|
3475
|
+
const revalidate = (stage) => {
|
|
3476
|
+
try {
|
|
3477
|
+
assertCurrent();
|
|
3478
|
+
} catch (error) {
|
|
3479
|
+
throw new SessionImageUploadError("IMAGE_ACCESS_REVOKED", stage, void 0, error);
|
|
3480
|
+
}
|
|
3481
|
+
};
|
|
3482
|
+
revalidate("source");
|
|
3483
|
+
assert(this.#active.size < MAX_ACTIVE_UPLOADS, "IMAGE_CAPACITY_EXCEEDED", "source");
|
|
3484
|
+
assert(
|
|
3485
|
+
Buffer.isBuffer(source) && source.length > 0 && source.length <= SESSION_IMAGE_MAX_BYTES,
|
|
3486
|
+
"IMAGE_CONTENT_INVALID",
|
|
3487
|
+
"source"
|
|
3488
|
+
);
|
|
3489
|
+
const scope = parse(SessionImageScopeSchema, { machineId: this.#config.machineId, sessionId }, "source");
|
|
3490
|
+
const controller = new AbortController();
|
|
3491
|
+
this.#active.add(controller);
|
|
3492
|
+
const abort = () => controller.abort();
|
|
3493
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
3494
|
+
let timedOut = false;
|
|
3495
|
+
const timer = setTimeout(() => {
|
|
3496
|
+
timedOut = true;
|
|
3497
|
+
controller.abort();
|
|
3498
|
+
}, UPLOAD_TIMEOUT_MS);
|
|
3499
|
+
timer.unref();
|
|
3500
|
+
try {
|
|
3501
|
+
const bytes = Buffer.from(source);
|
|
3502
|
+
const dimensions = await this.#dimensions(bytes);
|
|
3503
|
+
const hash2 = digest(bytes);
|
|
3504
|
+
const request = parse(SessionImageUploadRequestSchema, { ...scope, sizeBytes: bytes.length }, "source");
|
|
3505
|
+
revalidate("reserve");
|
|
3506
|
+
const upload = await this.#json(
|
|
3507
|
+
`${SESSION_IMAGE_API_PATH}/uploads`,
|
|
3508
|
+
request,
|
|
3509
|
+
201,
|
|
3510
|
+
SessionImageUploadResultSchema,
|
|
3511
|
+
"reserve",
|
|
3512
|
+
controller.signal
|
|
3513
|
+
);
|
|
3514
|
+
const target = this.#target(upload, scope.sessionId);
|
|
3515
|
+
let ambiguous;
|
|
3516
|
+
revalidate("put");
|
|
3517
|
+
try {
|
|
3518
|
+
await this.#request(target, {
|
|
3519
|
+
method: "PUT",
|
|
3520
|
+
headers: upload.mode === "direct" ? upload.headers : { ...this.#headers(), ...upload.headers },
|
|
3521
|
+
body: new Uint8Array(bytes)
|
|
3522
|
+
}, "put", controller.signal, async (response) => {
|
|
3523
|
+
if (response.status !== 201) throw new SessionImageUploadError("IMAGE_HTTP_ERROR", "put", response.status);
|
|
3524
|
+
});
|
|
3525
|
+
} catch (error) {
|
|
3526
|
+
if (!(error instanceof SessionImageUploadError) || controller.signal.aborted || !(error.code === "IMAGE_NETWORK_ERROR" || error.code === "IMAGE_TIMEOUT" || error.code === "IMAGE_HTTP_ERROR" && [409, 412].includes(error.status ?? 0))) throw error;
|
|
3527
|
+
ambiguous = error;
|
|
3528
|
+
}
|
|
3529
|
+
revalidate("complete");
|
|
3530
|
+
try {
|
|
3531
|
+
const committed = await this.#json(
|
|
3532
|
+
`${SESSION_IMAGE_API_PATH}/${upload.imageId}/complete`,
|
|
3533
|
+
scope,
|
|
3534
|
+
200,
|
|
3535
|
+
SessionImageCommitResultSchema,
|
|
3536
|
+
"complete",
|
|
3537
|
+
controller.signal
|
|
3538
|
+
);
|
|
3539
|
+
assert(
|
|
3540
|
+
committed.imageId === upload.imageId && committed.machineId === scope.machineId && committed.sessionId === scope.sessionId && committed.sha256 === hash2 && committed.sizeBytes === bytes.length && committed.width === dimensions.width && committed.height === dimensions.height && Number.isSafeInteger(committed.width) && Number.isSafeInteger(committed.height) && committed.width * committed.height <= SESSION_IMAGE_MAX_PIXELS && Number.isSafeInteger(committed.expiresAt) && committed.expiresAt > Date.now(),
|
|
3541
|
+
"IMAGE_COMMIT_MISMATCH",
|
|
3542
|
+
"complete"
|
|
3543
|
+
);
|
|
3544
|
+
revalidate("complete");
|
|
3545
|
+
return committed;
|
|
3546
|
+
} catch (error) {
|
|
3547
|
+
if (ambiguous === void 0 || error instanceof SessionImageUploadError && error.code === "IMAGE_ACCESS_REVOKED") {
|
|
3548
|
+
throw error;
|
|
3549
|
+
}
|
|
3550
|
+
const failure = new SessionImageUploadError("IMAGE_COMPLETION_FAILED", "complete", void 0, error);
|
|
3551
|
+
failure.cause = new AggregateError([ambiguous, safeCause(error)], "PUT outcome and completion failed.");
|
|
3552
|
+
throw failure;
|
|
3553
|
+
}
|
|
3554
|
+
} catch (error) {
|
|
3555
|
+
if (controller.signal.aborted) {
|
|
3556
|
+
throw new SessionImageUploadError(
|
|
3557
|
+
timedOut ? "IMAGE_TIMEOUT" : "IMAGE_ABORTED",
|
|
3558
|
+
error instanceof SessionImageUploadError ? error.stage : "source",
|
|
3559
|
+
void 0,
|
|
3560
|
+
error
|
|
3561
|
+
);
|
|
3562
|
+
}
|
|
3563
|
+
if (error instanceof SessionImageUploadError) throw error;
|
|
3564
|
+
throw new SessionImageUploadError("IMAGE_CONTENT_INVALID", "source", void 0, error);
|
|
3565
|
+
} finally {
|
|
3566
|
+
clearTimeout(timer);
|
|
3567
|
+
signal?.removeEventListener("abort", abort);
|
|
3568
|
+
this.#active.delete(controller);
|
|
3569
|
+
}
|
|
3570
|
+
}
|
|
3571
|
+
async #dimensions(bytes) {
|
|
3572
|
+
assert(
|
|
3573
|
+
bytes.length >= 20 && bytes.toString("ascii", 0, 4) === "RIFF" && bytes.toString("ascii", 8, 12) === "WEBP" && bytes.readUInt32LE(4) + 8 === bytes.length,
|
|
3574
|
+
"IMAGE_CONTENT_INVALID",
|
|
3575
|
+
"source"
|
|
3576
|
+
);
|
|
3577
|
+
const decoder = sharp2(bytes, { limitInputPixels: SESSION_IMAGE_MAX_PIXELS, failOn: "warning" });
|
|
3578
|
+
try {
|
|
3579
|
+
const metadata = await decoder.metadata();
|
|
3580
|
+
assert(metadata.format === "webp" && metadata.width !== void 0 && metadata.height !== void 0 && metadata.width > 0 && metadata.height > 0 && (metadata.pages ?? 1) === 1 && metadata.width * metadata.height <= SESSION_IMAGE_MAX_PIXELS, "IMAGE_CONTENT_INVALID", "source");
|
|
3581
|
+
return { width: metadata.width, height: metadata.height };
|
|
3582
|
+
} finally {
|
|
3583
|
+
decoder.destroy();
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
#headers() {
|
|
3587
|
+
return { authorization: `Bearer ${this.#config.relayToken}`, "x-agentknight-user-id": this.#config.userId };
|
|
3588
|
+
}
|
|
3589
|
+
#target(upload, sessionId) {
|
|
3590
|
+
const now = Date.now();
|
|
3591
|
+
assert(Number.isSafeInteger(upload.uploadExpiresAt) && upload.uploadExpiresAt > now && upload.uploadExpiresAt <= now + MAX_GRANT_LIFETIME_MS, "IMAGE_UPLOAD_EXPIRED", "reserve");
|
|
3592
|
+
assert(
|
|
3593
|
+
upload.headers["content-type"] === "image/webp" && (upload.headers["if-none-match"] === void 0 || upload.headers["if-none-match"] === "*"),
|
|
3594
|
+
"IMAGE_UPLOAD_TARGET_INVALID",
|
|
3595
|
+
"reserve"
|
|
3596
|
+
);
|
|
3597
|
+
if (upload.mode === "proxy") {
|
|
3598
|
+
const path2 = `${SESSION_IMAGE_API_PATH}/${upload.imageId}/content`;
|
|
3599
|
+
const url2 = new URL(upload.uploadUrl, this.#config.relayUrl);
|
|
3600
|
+
assert(
|
|
3601
|
+
upload.uploadUrl.split("?")[0] === path2 && url2.origin === this.#config.relayUrl && [...url2.searchParams.keys()].length === 2 && url2.searchParams.getAll("machineId").length === 1 && url2.searchParams.get("machineId") === this.#config.machineId && url2.searchParams.getAll("sessionId").length === 1 && url2.searchParams.get("sessionId") === sessionId && upload.headers["x-ms-blob-type"] === void 0,
|
|
3602
|
+
"IMAGE_UPLOAD_TARGET_INVALID",
|
|
3603
|
+
"reserve"
|
|
3604
|
+
);
|
|
3605
|
+
return url2.href;
|
|
3606
|
+
}
|
|
3607
|
+
const { storageAccount, storageContainer, userId, machineId } = this.#config;
|
|
3608
|
+
const host = `${storageAccount}.blob.core.windows.net`;
|
|
3609
|
+
const path = `/${storageContainer}/uploads/${digest(userId)}/${digest(machineId)}/${upload.imageId}.webp`;
|
|
3610
|
+
const url = new URL(upload.uploadUrl);
|
|
3611
|
+
const query = url.searchParams;
|
|
3612
|
+
const allowed = /* @__PURE__ */ new Set(["sv", "spr", "st", "se", "sr", "sp", "sig", "skoid", "sktid", "skt", "ske", "sks", "skv"]);
|
|
3613
|
+
assert(
|
|
3614
|
+
upload.uploadUrl.startsWith(`https://${host}${path}?`) && url.host === host && url.pathname === path && upload.headers["x-ms-blob-type"] === "BlockBlob" && [...query.keys()].every((key) => allowed.has(key) && query.getAll(key).length === 1) && [...allowed].every((key) => (query.get(key)?.length ?? 0) > 0) && query.get("sp") === "c" && query.get("sr") === "b" && query.get("spr") === "https" && query.get("sks") === "b",
|
|
3615
|
+
"IMAGE_UPLOAD_TARGET_INVALID",
|
|
3616
|
+
"reserve"
|
|
3617
|
+
);
|
|
3618
|
+
const expiresAt = Date.parse(query.get("se") ?? "");
|
|
3619
|
+
const startsAt = Date.parse(query.get("st") ?? "");
|
|
3620
|
+
const keyStartsAt = Date.parse(query.get("skt") ?? "");
|
|
3621
|
+
const keyExpiresAt = Date.parse(query.get("ske") ?? "");
|
|
3622
|
+
assert(
|
|
3623
|
+
Number.isFinite(startsAt) && startsAt <= now + 3e4 && Number.isFinite(keyStartsAt) && keyStartsAt <= now + 3e4 && Number.isFinite(expiresAt) && expiresAt > now && expiresAt <= upload.uploadExpiresAt && Number.isFinite(keyExpiresAt) && keyExpiresAt >= expiresAt,
|
|
3624
|
+
"IMAGE_UPLOAD_TARGET_INVALID",
|
|
3625
|
+
"reserve"
|
|
3626
|
+
);
|
|
3627
|
+
return url.href;
|
|
3628
|
+
}
|
|
3629
|
+
async #json(path, body, status, schema, stage, signal) {
|
|
3630
|
+
return this.#request(`${this.#config.relayUrl}${path}`, {
|
|
3631
|
+
method: "POST",
|
|
3632
|
+
headers: { ...this.#headers(), "content-type": "application/json" },
|
|
3633
|
+
body: JSON.stringify(body)
|
|
3634
|
+
}, stage, signal, async (response, requestSignal) => {
|
|
3635
|
+
if (response.status !== status) throw new SessionImageUploadError("IMAGE_HTTP_ERROR", stage, response.status);
|
|
3636
|
+
const contentLength = response.headers.get("content-length");
|
|
3637
|
+
assert(
|
|
3638
|
+
contentLength === null || /^\d+$/u.test(contentLength) && Number(contentLength) <= MAX_JSON_BYTES,
|
|
3639
|
+
"IMAGE_RESPONSE_TOO_LARGE",
|
|
3640
|
+
stage
|
|
3641
|
+
);
|
|
3642
|
+
assert(response.body !== null, "IMAGE_RESPONSE_INVALID", stage);
|
|
3643
|
+
const reader = response.body.getReader();
|
|
3644
|
+
const chunks = [];
|
|
3645
|
+
let size = 0;
|
|
3646
|
+
try {
|
|
3647
|
+
while (true) {
|
|
3648
|
+
const next = await abortable(reader.read(), requestSignal, stage);
|
|
3649
|
+
requestSignal.throwIfAborted();
|
|
3650
|
+
if (next.done) break;
|
|
3651
|
+
size += next.value.byteLength;
|
|
3652
|
+
assert(size <= MAX_JSON_BYTES, "IMAGE_RESPONSE_TOO_LARGE", stage);
|
|
3653
|
+
chunks.push(next.value);
|
|
3654
|
+
}
|
|
3655
|
+
let value;
|
|
3656
|
+
try {
|
|
3657
|
+
value = JSON.parse(Buffer.concat(chunks, size).toString("utf8"));
|
|
3658
|
+
} catch (error) {
|
|
3659
|
+
throw new SessionImageUploadError("IMAGE_RESPONSE_INVALID", stage, void 0, error);
|
|
3660
|
+
}
|
|
3661
|
+
return parse(schema, value, stage);
|
|
3662
|
+
} finally {
|
|
3663
|
+
reader.releaseLock();
|
|
3664
|
+
}
|
|
3665
|
+
});
|
|
3666
|
+
}
|
|
3667
|
+
async #request(url, init, stage, signal, consume) {
|
|
3668
|
+
assert(!signal.aborted, "IMAGE_ABORTED", stage);
|
|
3669
|
+
const controller = new AbortController();
|
|
3670
|
+
const abort = () => controller.abort();
|
|
3671
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
3672
|
+
let timedOut = false;
|
|
3673
|
+
const timer = setTimeout(() => {
|
|
3674
|
+
timedOut = true;
|
|
3675
|
+
controller.abort();
|
|
3676
|
+
}, REQUEST_TIMEOUT_MS);
|
|
3677
|
+
timer.unref();
|
|
3678
|
+
try {
|
|
3679
|
+
const response = await this.#fetch(url, {
|
|
3680
|
+
...init,
|
|
3681
|
+
signal: controller.signal,
|
|
3682
|
+
redirect: "error",
|
|
3683
|
+
credentials: "omit",
|
|
3684
|
+
cache: "no-store",
|
|
3685
|
+
referrerPolicy: "no-referrer"
|
|
3686
|
+
});
|
|
3687
|
+
try {
|
|
3688
|
+
controller.signal.throwIfAborted();
|
|
3689
|
+
assert(!response.redirected, "IMAGE_RESPONSE_INVALID", stage);
|
|
3690
|
+
return await consume(response, controller.signal);
|
|
3691
|
+
} finally {
|
|
3692
|
+
await response.body?.cancel();
|
|
3693
|
+
}
|
|
3694
|
+
} catch (error) {
|
|
3695
|
+
if (controller.signal.aborted) {
|
|
3696
|
+
throw new SessionImageUploadError(timedOut ? "IMAGE_TIMEOUT" : "IMAGE_ABORTED", stage, void 0, error);
|
|
2475
3697
|
}
|
|
3698
|
+
if (error instanceof SessionImageUploadError) throw error;
|
|
3699
|
+
throw new SessionImageUploadError("IMAGE_NETWORK_ERROR", stage, void 0, error);
|
|
3700
|
+
} finally {
|
|
3701
|
+
clearTimeout(timer);
|
|
3702
|
+
signal.removeEventListener("abort", abort);
|
|
2476
3703
|
}
|
|
2477
|
-
return removed;
|
|
2478
|
-
}
|
|
2479
|
-
clear() {
|
|
2480
|
-
this.#tokens.clear();
|
|
2481
3704
|
}
|
|
2482
3705
|
};
|
|
2483
3706
|
|
|
@@ -2495,15 +3718,35 @@ function unsupportedOperation(error) {
|
|
|
2495
3718
|
}
|
|
2496
3719
|
|
|
2497
3720
|
// src/service/requests/communication-operations.ts
|
|
3721
|
+
var compress2 = promisify2(gzip3);
|
|
2498
3722
|
function createCommunicationOperations(options) {
|
|
2499
|
-
const { adapter, publications } = options;
|
|
3723
|
+
const { adapter, publications, imageBlob } = options;
|
|
2500
3724
|
const directoryLister = new SessionDirectoryLister(async (sessionId) => adapter.getSessionDirectoryMetadata?.(sessionId));
|
|
2501
3725
|
const fileReader = new SessionFileReader(async (sessionId) => adapter.getSessionDirectoryMetadata?.(sessionId));
|
|
3726
|
+
const gitDiffReader = new SessionGitDiffReader(async (sessionId) => adapter.getSessionDirectoryMetadata?.(sessionId), options.gitDiffDirectory);
|
|
2502
3727
|
const getSessionProjectIdentity = adapter.getSessionProjectIdentity?.bind(adapter);
|
|
2503
3728
|
const createGroupChat = adapter.createGroupChat?.bind(adapter);
|
|
2504
3729
|
const renameSession = adapter.renameSession?.bind(adapter);
|
|
2505
3730
|
return [
|
|
2506
3731
|
...options.workbenchOperations,
|
|
3732
|
+
...imageBlob === void 0 ? [] : [
|
|
3733
|
+
sessionCommunicationOperation({
|
|
3734
|
+
operation: TIMELINE_IMAGE_BLOB_OPERATION,
|
|
3735
|
+
version: TIMELINE_IMAGE_BLOB_VERSION,
|
|
3736
|
+
schema: TimelineImageBlobRequestSchema,
|
|
3737
|
+
handle: async ({ sha256 }, request) => {
|
|
3738
|
+
const assertCurrent = options.captureRequestAccess();
|
|
3739
|
+
assertCurrent();
|
|
3740
|
+
const result = await imageBlob.publish(request.sessionId, sha256, assertCurrent);
|
|
3741
|
+
assertCurrent();
|
|
3742
|
+
return SessionCommunicationDataSchema.parse(result);
|
|
3743
|
+
},
|
|
3744
|
+
onError: (error) => ({
|
|
3745
|
+
code: error instanceof TimelineAssetError || error instanceof SessionImageUploadError || error instanceof SessionNotSharedError || error instanceof AgentSessionRenameError ? error.code : "IMAGE_BLOB_FAILED",
|
|
3746
|
+
message: "The cloud image is unavailable"
|
|
3747
|
+
})
|
|
3748
|
+
})
|
|
3749
|
+
],
|
|
2507
3750
|
...renameSession === void 0 ? [] : [
|
|
2508
3751
|
sessionCommunicationOperation({
|
|
2509
3752
|
operation: SESSION_RENAME_OPERATION,
|
|
@@ -2546,6 +3789,30 @@ function createCommunicationOperations(options) {
|
|
|
2546
3789
|
],
|
|
2547
3790
|
...options.incrementalOperations,
|
|
2548
3791
|
...adapter.getSessionDirectoryMetadata === void 0 ? [] : [
|
|
3792
|
+
...[SESSION_GIT_DIFF_VERSION, SESSION_GIT_DIFF_GZIP_VERSION].map((version) => sessionCommunicationOperation({
|
|
3793
|
+
operation: SESSION_GIT_DIFF_OPERATION,
|
|
3794
|
+
version,
|
|
3795
|
+
schema: SessionGitDiffDataSchema,
|
|
3796
|
+
handle: async (data, request) => {
|
|
3797
|
+
const assertAccess = options.captureRequestAccess();
|
|
3798
|
+
assertAccess();
|
|
3799
|
+
const result = await gitDiffReader.read(request.sessionId, data);
|
|
3800
|
+
let response = SessionCommunicationDataSchema.parse(result);
|
|
3801
|
+
if (version === SESSION_GIT_DIFF_GZIP_VERSION) {
|
|
3802
|
+
const bytes = Buffer.from(JSON.stringify(result), "utf8");
|
|
3803
|
+
if (bytes.length >= SESSION_GIT_DIFF_GZIP_MIN_BYTES) {
|
|
3804
|
+
response = SessionGitDiffGzipResultDataSchema.parse({
|
|
3805
|
+
encoding: "gzip",
|
|
3806
|
+
byteLength: bytes.length,
|
|
3807
|
+
data: (await compress2(bytes, { level: constants4.Z_BEST_SPEED })).toString("base64")
|
|
3808
|
+
});
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
assertAccess();
|
|
3812
|
+
return response;
|
|
3813
|
+
},
|
|
3814
|
+
onError: (error) => unsupportedOperation(error) ?? (error instanceof SessionGitDiffError || error instanceof SessionNotSharedError ? { code: error.code, message: error.message } : { code: "DIFF_UNAVAILABLE", message: "The project diff is unavailable" })
|
|
3815
|
+
})),
|
|
2549
3816
|
sessionCommunicationOperation({
|
|
2550
3817
|
operation: SESSION_DIRECTORY_LIST_OPERATION,
|
|
2551
3818
|
version: SESSION_DIRECTORY_LIST_VERSION,
|
|
@@ -2556,16 +3823,16 @@ function createCommunicationOperations(options) {
|
|
|
2556
3823
|
}),
|
|
2557
3824
|
onError: (error) => unsupportedOperation(error) ?? (error instanceof SessionDirectoryError ? { code: error.code, message: error.message } : { code: "DIRECTORY_UNAVAILABLE", message: "The directory is unavailable" })
|
|
2558
3825
|
}),
|
|
2559
|
-
sessionCommunicationOperation({
|
|
3826
|
+
...[SESSION_FILE_READ_VERSION, SESSION_FILE_READ_V2_VERSION].map((version) => sessionCommunicationOperation({
|
|
2560
3827
|
operation: SESSION_FILE_READ_OPERATION,
|
|
2561
|
-
version
|
|
3828
|
+
version,
|
|
2562
3829
|
schema: SessionFileReadDataSchema,
|
|
2563
|
-
handle: ({ path, rootRevision }, request) => fileReader.read(request.sessionId, path, rootRevision),
|
|
3830
|
+
handle: ({ path, rootRevision }, request) => fileReader.read(request.sessionId, path, rootRevision, version),
|
|
2564
3831
|
onError: (error) => unsupportedOperation(error) ?? {
|
|
2565
3832
|
code: error instanceof SessionFileReadError ? error.code : "FILE_UNAVAILABLE",
|
|
2566
3833
|
message: "The file preview is unavailable"
|
|
2567
3834
|
}
|
|
2568
|
-
}),
|
|
3835
|
+
})),
|
|
2569
3836
|
sessionCommunicationOperation({
|
|
2570
3837
|
operation: SESSION_RESOURCE_READ_OPERATION,
|
|
2571
3838
|
version: SESSION_RESOURCE_READ_VERSION,
|
|
@@ -2612,13 +3879,14 @@ var SessionCommunicationHandler = class {
|
|
|
2612
3879
|
const workbench = adapter.agentId === "copilot" || adapter.agentId === "managed" ? adapter.workbench : void 0;
|
|
2613
3880
|
const researchWorkbench = workbench === void 0 ? void 0 : research;
|
|
2614
3881
|
this.#operations = workbench === void 0 && researchWorkbench === void 0 ? void 0 : new SessionOperations({
|
|
2615
|
-
directory:
|
|
3882
|
+
directory: join7(config.dataDirectory, "workbench"),
|
|
2616
3883
|
agentRunId,
|
|
2617
3884
|
namespace: `${config.userId}\0${machineId}`,
|
|
2618
3885
|
onError: operationError
|
|
2619
3886
|
});
|
|
2620
3887
|
this.#dispatcher = new SessionCommunicationDispatcher(createCommunicationOperations({
|
|
2621
3888
|
adapter,
|
|
3889
|
+
gitDiffDirectory: join7(config.dataDirectory, "git-diff"),
|
|
2622
3890
|
publications: options.publications,
|
|
2623
3891
|
captureRequestAccess: () => {
|
|
2624
3892
|
const assertCurrent = this.#requestContext.getStore()?.assertRequestCurrent;
|
|
@@ -2626,6 +3894,7 @@ var SessionCommunicationHandler = class {
|
|
|
2626
3894
|
return assertCurrent;
|
|
2627
3895
|
},
|
|
2628
3896
|
incrementalOperations: options.incrementalOperations.filter((operation) => config.timelineIncrementalEnabled !== false || operation.operation === TIMELINE_IMAGE_OPERATION),
|
|
3897
|
+
...options.imageBlob === void 0 ? {} : { imageBlob: options.imageBlob },
|
|
2629
3898
|
workbenchOperations: this.#operations === void 0 ? [] : createWorkbenchOperations({
|
|
2630
3899
|
...workbench === void 0 ? {} : { runtime: workbench },
|
|
2631
3900
|
...researchWorkbench === void 0 ? {} : { research: researchWorkbench },
|
|
@@ -2653,6 +3922,8 @@ var SessionCommunicationHandler = class {
|
|
|
2653
3922
|
#operations;
|
|
2654
3923
|
#dispatcher;
|
|
2655
3924
|
#errors;
|
|
3925
|
+
#diagnosticTimers = /* @__PURE__ */ new Set();
|
|
3926
|
+
#closed = false;
|
|
2656
3927
|
get capabilities() {
|
|
2657
3928
|
const schedules = this.options.adapter.workbench?.schedules;
|
|
2658
3929
|
const schedulesAvailable = schedules !== void 0 && schedules.isAvailable?.() !== false;
|
|
@@ -2669,6 +3940,9 @@ var SessionCommunicationHandler = class {
|
|
|
2669
3940
|
await this.#operations?.stopAccepting();
|
|
2670
3941
|
}
|
|
2671
3942
|
async close() {
|
|
3943
|
+
this.#closed = true;
|
|
3944
|
+
for (const timer of this.#diagnosticTimers) clearTimeout(timer);
|
|
3945
|
+
this.#diagnosticTimers.clear();
|
|
2672
3946
|
await this.#operations?.close();
|
|
2673
3947
|
this.#researchParents.clear();
|
|
2674
3948
|
this.#requestContext.disable();
|
|
@@ -2690,11 +3964,106 @@ var SessionCommunicationHandler = class {
|
|
|
2690
3964
|
return visible;
|
|
2691
3965
|
}
|
|
2692
3966
|
async handle(request, scope) {
|
|
2693
|
-
|
|
2694
|
-
|
|
3967
|
+
if (request.operation !== TIMELINE_SYNC_OPERATION) return this.#handle(request, scope);
|
|
3968
|
+
const started = performance2.now();
|
|
3969
|
+
const parsed = TimelineSyncRequestSchema.safeParse(request.data);
|
|
3970
|
+
const mode = parsed.success && parsed.data.watermark !== void 0 ? parsed.data.resume === true ? "resume" : "replay" : "snapshot";
|
|
3971
|
+
const diagnostics = new OperationDiagnostics();
|
|
3972
|
+
const lifecycle = {
|
|
3973
|
+
preflightPhase: "arrival",
|
|
3974
|
+
lifecyclePhase: "preflight",
|
|
3975
|
+
outcome: "dropped",
|
|
3976
|
+
emitOutcome: "not-attempted"
|
|
3977
|
+
};
|
|
3978
|
+
const fields = () => ({
|
|
3979
|
+
...diagnostics.fields(request),
|
|
3980
|
+
mode,
|
|
3981
|
+
lifecycleMs: Math.round(performance2.now() - started),
|
|
3982
|
+
preflightMs: Math.round((lifecycle.preflightEnded ?? performance2.now()) - started),
|
|
3983
|
+
preflightPhase: lifecycle.preflightPhase,
|
|
3984
|
+
lifecyclePhase: lifecycle.lifecyclePhase
|
|
3985
|
+
});
|
|
3986
|
+
let pendingLogged = false;
|
|
3987
|
+
const pendingAfterMs = 5e3 + (parsed.success ? parsed.data.waitMs ?? 0 : 0);
|
|
3988
|
+
const timer = this.#closed ? void 0 : setTimeout(() => {
|
|
3989
|
+
if (timer !== void 0) this.#diagnosticTimers.delete(timer);
|
|
3990
|
+
pendingLogged = true;
|
|
3991
|
+
this.options.logger.info({ ...fields(), pendingAfterMs }, "session.communication.timeline-sync-pending");
|
|
3992
|
+
}, pendingAfterMs);
|
|
3993
|
+
if (timer !== void 0) {
|
|
3994
|
+
timer.unref();
|
|
3995
|
+
this.#diagnosticTimers.add(timer);
|
|
3996
|
+
}
|
|
3997
|
+
try {
|
|
3998
|
+
await diagnostics.run(async () => {
|
|
3999
|
+
diagnostics.recordTimelineSync({ mode });
|
|
4000
|
+
if (mode !== "replay") {
|
|
4001
|
+
this.options.logger.info(fields(), "session.communication.timeline-sync-started");
|
|
4002
|
+
}
|
|
4003
|
+
await this.#handle(request, scope, diagnostics, lifecycle);
|
|
4004
|
+
}, { measureHandle: false });
|
|
4005
|
+
} catch (error) {
|
|
4006
|
+
lifecycle.outcome = "failure";
|
|
4007
|
+
lifecycle.reason ??= "exception";
|
|
4008
|
+
diagnostics.recordFailure(error, lifecycle.lifecyclePhase === "preflight" ? "preflight" : lifecycle.lifecyclePhase === "emit" ? "emit" : "handle");
|
|
4009
|
+
throw error;
|
|
4010
|
+
} finally {
|
|
4011
|
+
if (timer !== void 0) {
|
|
4012
|
+
clearTimeout(timer);
|
|
4013
|
+
this.#diagnosticTimers.delete(timer);
|
|
4014
|
+
}
|
|
4015
|
+
const details = {
|
|
4016
|
+
...fields(),
|
|
4017
|
+
outcome: lifecycle.outcome,
|
|
4018
|
+
emitOutcome: lifecycle.emitOutcome,
|
|
4019
|
+
...lifecycle.reason === void 0 ? {} : { reason: lifecycle.reason }
|
|
4020
|
+
};
|
|
4021
|
+
if (mode !== "replay" || pendingLogged || lifecycle.outcome !== "success" || lifecycle.responseStatus === "resync" || diagnostics.activeMs >= 1e3) {
|
|
4022
|
+
this.options.logger.info(details, "session.communication.timeline-sync-completed");
|
|
4023
|
+
} else {
|
|
4024
|
+
this.options.logger.debug(details, "session.communication.timeline-sync-completed");
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
async #handle(request, scope, diagnostics = new OperationDiagnostics(), lifecycle) {
|
|
4029
|
+
const { adapter, config, machineId, research, publications, audience, logger, metrics } = this.options;
|
|
4030
|
+
const reject = (reason) => {
|
|
4031
|
+
if (lifecycle !== void 0) {
|
|
4032
|
+
lifecycle.outcome = "failure";
|
|
4033
|
+
lifecycle.reason = reason;
|
|
4034
|
+
}
|
|
4035
|
+
};
|
|
4036
|
+
const emit = (_event, response2) => {
|
|
4037
|
+
if (lifecycle !== void 0) {
|
|
4038
|
+
lifecycle.preflightEnded ??= performance2.now();
|
|
4039
|
+
lifecycle.lifecyclePhase = "emit";
|
|
4040
|
+
lifecycle.outcome = response2.kind === "error" ? "failure" : "success";
|
|
4041
|
+
if (response2.kind === "error") lifecycle.reason ??= "operation-error";
|
|
4042
|
+
if (response2.kind === "response") {
|
|
4043
|
+
const status = response2.data["status"];
|
|
4044
|
+
if (status === "snapshot" || status === "replay" || status === "resync") {
|
|
4045
|
+
lifecycle.responseStatus = status;
|
|
4046
|
+
diagnostics.recordTimelineSync({ status });
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
4049
|
+
}
|
|
4050
|
+
try {
|
|
4051
|
+
this.options.emit("session.communication", response2);
|
|
4052
|
+
if (lifecycle !== void 0) lifecycle.emitOutcome = "local-enqueued";
|
|
4053
|
+
} catch (error) {
|
|
4054
|
+
if (lifecycle !== void 0) lifecycle.emitOutcome = "threw";
|
|
4055
|
+
reject(error instanceof SecureMessageBudgetError ? "transport-budget" : "emit-failed");
|
|
4056
|
+
diagnostics.recordFailure(error, "emit");
|
|
4057
|
+
throw error;
|
|
4058
|
+
}
|
|
4059
|
+
};
|
|
4060
|
+
if (!this.options.enabled()) {
|
|
4061
|
+
if (lifecycle !== void 0) lifecycle.reason = "disabled";
|
|
4062
|
+
return;
|
|
4063
|
+
}
|
|
2695
4064
|
const clientId = scope.clientId;
|
|
2696
4065
|
const owner = [config.userId, machineId, clientId, request.sessionId];
|
|
2697
|
-
const key = (parts) =>
|
|
4066
|
+
const key = (parts) => createHash8("sha256").update(JSON.stringify(parts)).digest("hex").slice(0, 24);
|
|
2698
4067
|
const knownSource = this.#researchParents.get(request.sessionId) ?? adapter.sessionPublicationSource?.(request.sessionId) ?? request.sessionId;
|
|
2699
4068
|
const arrivalPublication = adapter.isSessionVisible?.(knownSource) === true ? publications.capture(knownSource) : void 0;
|
|
2700
4069
|
let researchParent = this.#researchParents.get(request.sessionId);
|
|
@@ -2706,9 +4075,12 @@ var SessionCommunicationHandler = class {
|
|
|
2706
4075
|
return;
|
|
2707
4076
|
}
|
|
2708
4077
|
if (research !== void 0) {
|
|
4078
|
+
if (lifecycle !== void 0) lifecycle.preflightPhase = "ownership";
|
|
2709
4079
|
try {
|
|
2710
4080
|
researchParent = await research.parentSessionId(request.sessionId) ?? researchParent;
|
|
2711
4081
|
} catch (error) {
|
|
4082
|
+
reject("ownership-unavailable");
|
|
4083
|
+
if (lifecycle !== void 0) diagnostics.recordFailure(error, "preflight");
|
|
2712
4084
|
logger.error({
|
|
2713
4085
|
stage: "research.ownership",
|
|
2714
4086
|
ownerKey: key(owner),
|
|
@@ -2723,8 +4095,10 @@ var SessionCommunicationHandler = class {
|
|
|
2723
4095
|
}
|
|
2724
4096
|
}
|
|
2725
4097
|
if (researchParent !== void 0) {
|
|
2726
|
-
|
|
4098
|
+
if (lifecycle !== void 0) lifecycle.preflightPhase = "isolation";
|
|
4099
|
+
const unifiedSync = research?.chat !== void 0 && this.options.supportsResearchChat?.(clientId) === true && (request.operation === TIMELINE_SYNC_OPERATION || request.operation === TIMELINE_IMAGE_OPERATION || request.operation === TIMELINE_IMAGE_BLOB_OPERATION);
|
|
2727
4100
|
if (!unifiedSync && !["research.timeline", "research.send", "research.abort", "research.delete", "operation.get", "insights.read"].includes(request.operation)) {
|
|
4101
|
+
reject("research-isolated");
|
|
2728
4102
|
emit("session.communication", sessionCommunicationError(request, {
|
|
2729
4103
|
code: "RESEARCH_SESSION_ISOLATED",
|
|
2730
4104
|
message: "Research sessions require the isolated research operations"
|
|
@@ -2738,6 +4112,7 @@ var SessionCommunicationHandler = class {
|
|
|
2738
4112
|
}
|
|
2739
4113
|
}
|
|
2740
4114
|
const publicationSessionId = researchParent ?? request.sessionId;
|
|
4115
|
+
if (lifecycle !== void 0) lifecycle.preflightPhase = "access";
|
|
2741
4116
|
let publication;
|
|
2742
4117
|
let researchAccess;
|
|
2743
4118
|
const publicationCurrent = () => {
|
|
@@ -2745,6 +4120,7 @@ var SessionCommunicationHandler = class {
|
|
|
2745
4120
|
researchAccess?.assertCurrent();
|
|
2746
4121
|
return researchAccess?.detached === true || publications.isCurrent(publicationSessionId, publication);
|
|
2747
4122
|
} catch {
|
|
4123
|
+
reject("not-shared");
|
|
2748
4124
|
return false;
|
|
2749
4125
|
}
|
|
2750
4126
|
};
|
|
@@ -2757,21 +4133,28 @@ var SessionCommunicationHandler = class {
|
|
|
2757
4133
|
}
|
|
2758
4134
|
if (!publicationCurrent()) throw new SessionNotSharedError();
|
|
2759
4135
|
} catch {
|
|
4136
|
+
reject("not-shared");
|
|
2760
4137
|
const error = new SessionNotSharedError();
|
|
2761
4138
|
emit("session.communication", sessionCommunicationError(request, { code: error.code, message: error.message }));
|
|
2762
4139
|
return;
|
|
2763
4140
|
}
|
|
2764
4141
|
const token = clientId === void 0 || request.operation !== TIMELINE_SYNC_OPERATION ? void 0 : audience.syncToken(clientId, request.sessionId);
|
|
2765
4142
|
if (request.operation === TIMELINE_SYNC_OPERATION && !audience.confirmed) {
|
|
2766
|
-
|
|
4143
|
+
if (lifecycle !== void 0) lifecycle.preflightPhase = "audience";
|
|
4144
|
+
reject("audience-unconfirmed");
|
|
4145
|
+
const { data: _data, kind: _kind, ...identity2 } = request;
|
|
2767
4146
|
emit("session.communication", {
|
|
2768
|
-
...
|
|
4147
|
+
...identity2,
|
|
2769
4148
|
kind: "error",
|
|
2770
4149
|
error: { code: "TIMELINE_SYNC_UNSUPPORTED", message: "Relay audience replay is not confirmed" }
|
|
2771
4150
|
});
|
|
2772
4151
|
return;
|
|
2773
4152
|
}
|
|
2774
|
-
|
|
4153
|
+
if (lifecycle !== void 0) {
|
|
4154
|
+
lifecycle.preflightEnded = performance2.now();
|
|
4155
|
+
lifecycle.preflightPhase = "complete";
|
|
4156
|
+
lifecycle.lifecyclePhase = "dispatch";
|
|
4157
|
+
}
|
|
2775
4158
|
const response = await diagnostics.run(() => clientId === void 0 ? this.#dispatcher.dispatch(request) : this.#requestContext.run({
|
|
2776
4159
|
clientId,
|
|
2777
4160
|
researchSession: researchParent !== void 0,
|
|
@@ -2800,8 +4183,16 @@ var SessionCommunicationHandler = class {
|
|
|
2800
4183
|
...typeof request.data["operationId"] !== "string" ? {} : { operationKey: key([...owner, request.data["operationId"]]) }
|
|
2801
4184
|
}
|
|
2802
4185
|
}, () => this.#dispatcher.dispatch(request)));
|
|
2803
|
-
if (
|
|
4186
|
+
if (lifecycle !== void 0) lifecycle.lifecyclePhase = "reply-check";
|
|
4187
|
+
if (!scope.isCurrent()) {
|
|
4188
|
+
if (lifecycle !== void 0) {
|
|
4189
|
+
lifecycle.outcome = "dropped";
|
|
4190
|
+
lifecycle.reason = "stale-scope";
|
|
4191
|
+
}
|
|
4192
|
+
return;
|
|
4193
|
+
}
|
|
2804
4194
|
if (!publicationCurrent()) {
|
|
4195
|
+
reject("not-shared");
|
|
2805
4196
|
const error = new SessionNotSharedError();
|
|
2806
4197
|
emit("session.communication", sessionCommunicationError(request, { code: error.code, message: error.message }));
|
|
2807
4198
|
return;
|
|
@@ -2852,23 +4243,23 @@ var SessionCommunicationHandler = class {
|
|
|
2852
4243
|
};
|
|
2853
4244
|
|
|
2854
4245
|
// src/desktop-workspaces.ts
|
|
2855
|
-
import { createHash as
|
|
2856
|
-
import { readdir, realpath as
|
|
4246
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
4247
|
+
import { readdir, realpath as realpath5, stat as stat6 } from "node:fs/promises";
|
|
2857
4248
|
import { homedir as homedir2 } from "node:os";
|
|
2858
|
-
import { basename as basename2, join as
|
|
2859
|
-
function desktopRootCandidates(
|
|
4249
|
+
import { basename as basename2, join as join8, normalize } from "node:path";
|
|
4250
|
+
function desktopRootCandidates(environment2 = process.env) {
|
|
2860
4251
|
const candidates = [
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
4252
|
+
environment2.AGENTKNIGHT_WORKSPACE_ROOT,
|
|
4253
|
+
environment2.OneDrive === void 0 ? void 0 : join8(environment2.OneDrive, "Desktop"),
|
|
4254
|
+
environment2.OneDriveCommercial === void 0 ? void 0 : join8(environment2.OneDriveCommercial, "Desktop"),
|
|
4255
|
+
environment2.OneDriveConsumer === void 0 ? void 0 : join8(environment2.OneDriveConsumer, "Desktop"),
|
|
4256
|
+
join8(environment2.USERPROFILE ?? homedir2(), "Desktop")
|
|
2866
4257
|
];
|
|
2867
4258
|
return [...new Set(candidates.filter((path) => path !== void 0))];
|
|
2868
4259
|
}
|
|
2869
4260
|
function workspaceId(path) {
|
|
2870
4261
|
const normalizedPath = workspacePathKey(path);
|
|
2871
|
-
return
|
|
4262
|
+
return createHash9("sha256").update(normalizedPath).digest("base64url").slice(0, 32);
|
|
2872
4263
|
}
|
|
2873
4264
|
function workspacePathKey(path) {
|
|
2874
4265
|
const normalizedPath = normalize(path);
|
|
@@ -2880,8 +4271,8 @@ function isMissingPathError(error) {
|
|
|
2880
4271
|
async function findDesktopRoot(candidates) {
|
|
2881
4272
|
for (const candidate of candidates) {
|
|
2882
4273
|
try {
|
|
2883
|
-
const resolved = await
|
|
2884
|
-
if ((await
|
|
4274
|
+
const resolved = await realpath5(candidate);
|
|
4275
|
+
if ((await stat6(resolved)).isDirectory()) {
|
|
2885
4276
|
return resolved;
|
|
2886
4277
|
}
|
|
2887
4278
|
} catch (error) {
|
|
@@ -2903,7 +4294,7 @@ async function listDesktopWorkspaces(rootOverride) {
|
|
|
2903
4294
|
const workspaces = entries.filter(
|
|
2904
4295
|
(entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name.trim().length > 0
|
|
2905
4296
|
).map((entry) => {
|
|
2906
|
-
const path =
|
|
4297
|
+
const path = join8(root, entry.name);
|
|
2907
4298
|
return {
|
|
2908
4299
|
id: workspaceId(path),
|
|
2909
4300
|
name: entry.name,
|
|
@@ -2923,8 +4314,8 @@ async function resolveSessionWorkspace(path) {
|
|
|
2923
4314
|
return void 0;
|
|
2924
4315
|
}
|
|
2925
4316
|
try {
|
|
2926
|
-
const resolved = await
|
|
2927
|
-
if (!(await
|
|
4317
|
+
const resolved = await realpath5(candidate);
|
|
4318
|
+
if (!(await stat6(resolved)).isDirectory()) {
|
|
2928
4319
|
return void 0;
|
|
2929
4320
|
}
|
|
2930
4321
|
return {
|
|
@@ -2971,13 +4362,13 @@ async function resolveSelectableWorkspace(id, sessionWorkingDirectories, rootOve
|
|
|
2971
4362
|
}
|
|
2972
4363
|
|
|
2973
4364
|
// src/git-session-worktree.ts
|
|
2974
|
-
import { execFile } from "node:child_process";
|
|
2975
|
-
import { randomUUID as
|
|
2976
|
-
import { lstat as
|
|
4365
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
4366
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
4367
|
+
import { lstat as lstat5, mkdir as mkdir3, realpath as realpath6, rmdir } from "node:fs/promises";
|
|
2977
4368
|
import { devNull } from "node:os";
|
|
2978
|
-
import { dirname, isAbsolute as
|
|
2979
|
-
import { promisify as
|
|
2980
|
-
var execFileAsync =
|
|
4369
|
+
import { dirname as dirname2, isAbsolute as isAbsolute6, join as join9, relative as relative3 } from "node:path";
|
|
4370
|
+
import { promisify as promisify3 } from "node:util";
|
|
4371
|
+
var execFileAsync = promisify3(execFile2);
|
|
2981
4372
|
function creationEnvironment() {
|
|
2982
4373
|
const env = Object.fromEntries(
|
|
2983
4374
|
Object.entries(process.env).filter(([key]) => !key.toUpperCase().startsWith("GIT_"))
|
|
@@ -2985,8 +4376,8 @@ function creationEnvironment() {
|
|
|
2985
4376
|
return { ...env, LC_ALL: "C" };
|
|
2986
4377
|
}
|
|
2987
4378
|
function inside(root, path) {
|
|
2988
|
-
const difference =
|
|
2989
|
-
return difference === "" || !
|
|
4379
|
+
const difference = relative3(root, path);
|
|
4380
|
+
return difference === "" || !isAbsolute6(difference) && difference !== ".." && !difference.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`);
|
|
2990
4381
|
}
|
|
2991
4382
|
async function git(env, ...args) {
|
|
2992
4383
|
const { stdout } = await execFileAsync("git", [
|
|
@@ -3002,11 +4393,11 @@ async function git(env, ...args) {
|
|
|
3002
4393
|
}
|
|
3003
4394
|
async function listSessionWorktreeBranches(sourceDirectory) {
|
|
3004
4395
|
const env = creationEnvironment();
|
|
3005
|
-
const
|
|
4396
|
+
const identity2 = await resolveGitProjectIdentity(sourceDirectory, env);
|
|
3006
4397
|
const output2 = await git(
|
|
3007
4398
|
env,
|
|
3008
4399
|
"-C",
|
|
3009
|
-
|
|
4400
|
+
identity2.worktreeRoot,
|
|
3010
4401
|
"for-each-ref",
|
|
3011
4402
|
"--format=%(refname)",
|
|
3012
4403
|
"--sort=refname",
|
|
@@ -3020,7 +4411,7 @@ async function listSessionWorktreeBranches(sourceDirectory) {
|
|
|
3020
4411
|
const branches = refs.map((ref) => ref.slice(prefix.length));
|
|
3021
4412
|
let currentRef;
|
|
3022
4413
|
try {
|
|
3023
|
-
currentRef = await git(env, "-C",
|
|
4414
|
+
currentRef = await git(env, "-C", identity2.worktreeRoot, "symbolic-ref", "--quiet", "HEAD");
|
|
3024
4415
|
} catch (error) {
|
|
3025
4416
|
if (!(error instanceof Error && "code" in error && error.code === 1)) throw error;
|
|
3026
4417
|
}
|
|
@@ -3029,7 +4420,7 @@ async function listSessionWorktreeBranches(sourceDirectory) {
|
|
|
3029
4420
|
}
|
|
3030
4421
|
async function createSessionWorktree(sourceDirectory, requestedBranchName, requestedBaseBranch) {
|
|
3031
4422
|
const env = creationEnvironment();
|
|
3032
|
-
const
|
|
4423
|
+
const identity2 = await resolveGitProjectIdentity(sourceDirectory, env);
|
|
3033
4424
|
const baseBranch = requestedBaseBranch?.trim();
|
|
3034
4425
|
const baseRef = baseBranch === void 0 ? "HEAD" : `refs/heads/${baseBranch}`;
|
|
3035
4426
|
if (baseBranch !== void 0) {
|
|
@@ -3042,7 +4433,7 @@ async function createSessionWorktree(sourceDirectory, requestedBranchName, reque
|
|
|
3042
4433
|
}
|
|
3043
4434
|
let head;
|
|
3044
4435
|
try {
|
|
3045
|
-
head = await git(env, "-C",
|
|
4436
|
+
head = await git(env, "-C", identity2.worktreeRoot, "rev-parse", "--verify", `${baseRef}^{commit}`);
|
|
3046
4437
|
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(head)) throw new Error("Git returned an invalid commit ID");
|
|
3047
4438
|
} catch (cause) {
|
|
3048
4439
|
throw new Error(
|
|
@@ -3050,10 +4441,10 @@ async function createSessionWorktree(sourceDirectory, requestedBranchName, reque
|
|
|
3050
4441
|
{ cause }
|
|
3051
4442
|
);
|
|
3052
4443
|
}
|
|
3053
|
-
const suffix =
|
|
4444
|
+
const suffix = randomUUID4();
|
|
3054
4445
|
const branchName = requestedBranchName?.trim() ?? `agentknight/${suffix}`;
|
|
3055
4446
|
try {
|
|
3056
|
-
if (branchName.length === 0 || branchName.length > 120 || await git(env, "-C",
|
|
4447
|
+
if (branchName.length === 0 || branchName.length > 120 || await git(env, "-C", identity2.worktreeRoot, "check-ref-format", "--branch", branchName) !== branchName) {
|
|
3057
4448
|
throw new Error("Invalid branch name");
|
|
3058
4449
|
}
|
|
3059
4450
|
} catch (cause) {
|
|
@@ -3061,29 +4452,29 @@ async function createSessionWorktree(sourceDirectory, requestedBranchName, reque
|
|
|
3061
4452
|
}
|
|
3062
4453
|
let exists = true;
|
|
3063
4454
|
try {
|
|
3064
|
-
await git(env, "-C",
|
|
4455
|
+
await git(env, "-C", identity2.worktreeRoot, "show-ref", "--verify", "--quiet", `refs/heads/${branchName}`);
|
|
3065
4456
|
} catch (error) {
|
|
3066
4457
|
if (!(error instanceof Error && "code" in error && error.code === 1)) throw error;
|
|
3067
4458
|
exists = false;
|
|
3068
4459
|
}
|
|
3069
4460
|
if (exists) throw new Error(`Git branch "${branchName}" already exists. Choose a new branch name.`);
|
|
3070
|
-
const primary = await
|
|
3071
|
-
const parent =
|
|
3072
|
-
if (inside(
|
|
4461
|
+
const primary = await realpath6(identity2.mainWorktreeRoot ?? identity2.worktreeRoot);
|
|
4462
|
+
const parent = dirname2(primary);
|
|
4463
|
+
if (inside(identity2.worktreeRoot, parent)) {
|
|
3073
4464
|
throw new Error("Cannot create a sibling worktree outside the source checkout. Move the repository and retry.");
|
|
3074
4465
|
}
|
|
3075
|
-
const path =
|
|
4466
|
+
const path = join9(parent, `agentknight-worktree-${suffix}`);
|
|
3076
4467
|
if (path.length > 640) {
|
|
3077
4468
|
throw new Error("The repository path is too long to report worktree recovery safely. Use a checkout closer to the filesystem root.");
|
|
3078
4469
|
}
|
|
3079
|
-
await
|
|
3080
|
-
const reserved = await
|
|
4470
|
+
await mkdir3(path, { mode: 448 });
|
|
4471
|
+
const reserved = await lstat5(path);
|
|
3081
4472
|
try {
|
|
3082
|
-
await git(env, "-C",
|
|
4473
|
+
await git(env, "-C", identity2.worktreeRoot, "worktree", "add", "-b", branchName, "--", path, head);
|
|
3083
4474
|
} catch (cause) {
|
|
3084
4475
|
let cleanupError;
|
|
3085
4476
|
try {
|
|
3086
|
-
const current = await
|
|
4477
|
+
const current = await lstat5(path);
|
|
3087
4478
|
if (current.dev === reserved.dev && current.ino === reserved.ino) await rmdir(path);
|
|
3088
4479
|
} catch (error) {
|
|
3089
4480
|
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) cleanupError = error;
|
|
@@ -3129,7 +4520,7 @@ var SessionOptionsHandler = class {
|
|
|
3129
4520
|
const sessionWorkingDirectories = request.sessionId === void 0 ? adapter.listSessions(config.userId, machineId).then((sessions) => sessions.flatMap((session) => session.workingDirectory === void 0 ? [] : [session.workingDirectory])) : Promise.resolve([]);
|
|
3130
4521
|
const environmentDiagnostics = new OperationDiagnostics();
|
|
3131
4522
|
const environmentSessionId = request.sessionId;
|
|
3132
|
-
const sessionEnvironment = environmentSessionId === void 0 ? Promise.resolve(void 0) : environmentDiagnostics.run(() => adapter.getSessionEnvironment(environmentSessionId)).then((
|
|
4523
|
+
const sessionEnvironment = environmentSessionId === void 0 ? Promise.resolve(void 0) : environmentDiagnostics.run(() => adapter.getSessionEnvironment(environmentSessionId)).then((environment3) => {
|
|
3133
4524
|
this.#environmentErrors.recovered(errorKey);
|
|
3134
4525
|
if (environmentDiagnostics.activeMs >= 1e3) {
|
|
3135
4526
|
logger.info(
|
|
@@ -3137,7 +4528,7 @@ var SessionOptionsHandler = class {
|
|
|
3137
4528
|
"copilot.session.environment.slow"
|
|
3138
4529
|
);
|
|
3139
4530
|
}
|
|
3140
|
-
return
|
|
4531
|
+
return environment3;
|
|
3141
4532
|
}).catch((error) => {
|
|
3142
4533
|
this.#environmentErrors.failure(errorKey, error, {
|
|
3143
4534
|
...diagnosticRequest(request),
|
|
@@ -3214,7 +4605,7 @@ var SessionOptionsHandler = class {
|
|
|
3214
4605
|
failureStage = "models";
|
|
3215
4606
|
throw modelFailure ?? new Error("No coding-agent models are available. Select an authenticated coding agent on the PC.");
|
|
3216
4607
|
}
|
|
3217
|
-
const
|
|
4608
|
+
const environment2 = await sessionEnvironment;
|
|
3218
4609
|
const worktreeCreationSupported = request.sessionId === void 0 && request.capabilities?.includes(SESSION_WORKTREE_CREATION_CAPABILITY) === true;
|
|
3219
4610
|
let worktreeChoices;
|
|
3220
4611
|
if (worktreeCreationSupported && request.workspaceId !== void 0) {
|
|
@@ -3245,7 +4636,7 @@ var SessionOptionsHandler = class {
|
|
|
3245
4636
|
currentAgentMode: current.agentMode,
|
|
3246
4637
|
currentReasoningEffort: current.reasoningEffort
|
|
3247
4638
|
},
|
|
3248
|
-
...
|
|
4639
|
+
...environment2 === void 0 ? {} : { environment: environment2 },
|
|
3249
4640
|
agentName: "agentName" in capabilities && typeof capabilities.agentName === "string" ? capabilities.agentName : codingAgentPresentation(adapter.agentId).name,
|
|
3250
4641
|
...current?.configOptions === void 0 ? {} : { configOptions: current.configOptions }
|
|
3251
4642
|
});
|
|
@@ -3346,15 +4737,15 @@ var OperationDeduplicator = class {
|
|
|
3346
4737
|
} catch (error) {
|
|
3347
4738
|
return Promise.reject(error);
|
|
3348
4739
|
}
|
|
3349
|
-
let
|
|
4740
|
+
let resolve5 = () => {
|
|
3350
4741
|
throw new Error("Operation not initialized");
|
|
3351
4742
|
};
|
|
3352
4743
|
let reject = () => {
|
|
3353
4744
|
throw new Error("Operation not initialized");
|
|
3354
4745
|
};
|
|
3355
|
-
const promise = new Promise((accept,
|
|
3356
|
-
|
|
3357
|
-
reject =
|
|
4746
|
+
const promise = new Promise((accept, fail4) => {
|
|
4747
|
+
resolve5 = accept;
|
|
4748
|
+
reject = fail4;
|
|
3358
4749
|
});
|
|
3359
4750
|
const entry = {
|
|
3360
4751
|
fingerprint: options.fingerprint,
|
|
@@ -3376,7 +4767,7 @@ var OperationDeduplicator = class {
|
|
|
3376
4767
|
}
|
|
3377
4768
|
);
|
|
3378
4769
|
try {
|
|
3379
|
-
|
|
4770
|
+
resolve5(options.execute());
|
|
3380
4771
|
} catch (error) {
|
|
3381
4772
|
reject(error);
|
|
3382
4773
|
}
|
|
@@ -3630,9 +5021,9 @@ var SessionCommandsHandler = class {
|
|
|
3630
5021
|
};
|
|
3631
5022
|
|
|
3632
5023
|
// src/service/requests/session-prompt.ts
|
|
3633
|
-
import { createHash as
|
|
5024
|
+
import { createHash as createHash10 } from "node:crypto";
|
|
3634
5025
|
function promptOperationFingerprint(request) {
|
|
3635
|
-
return
|
|
5026
|
+
return createHash10("sha256").update(request.sessionId).update("\0").update(request.mode).update("\0").update(request.prompt).update("\0").update(request.attachments?.map((attachment) => attachment.sha256).join(",") ?? "").update("\0").update(JSON.stringify(request.fileAttachments?.map(({ name, mimeType, sha256 }) => ({
|
|
3636
5027
|
name,
|
|
3637
5028
|
mimeType,
|
|
3638
5029
|
sha256
|
|
@@ -3874,12 +5265,12 @@ var SessionTimelinePageHandler = class {
|
|
|
3874
5265
|
};
|
|
3875
5266
|
|
|
3876
5267
|
// src/a2a-client.ts
|
|
3877
|
-
import { randomUUID as
|
|
5268
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
3878
5269
|
import { setTimeout as delay } from "node:timers/promises";
|
|
3879
5270
|
import { Role, TaskState } from "@a2a-js/sdk";
|
|
3880
5271
|
import { ClientFactory, DefaultAgentCardResolver, JsonRpcTransportFactory } from "@a2a-js/sdk/client";
|
|
3881
|
-
import { z as
|
|
3882
|
-
var TextSchema =
|
|
5272
|
+
import { z as z7 } from "zod";
|
|
5273
|
+
var TextSchema = z7.string().max(16e3).refine((text) => text.trim().length > 0);
|
|
3883
5274
|
var HTTP_TIMEOUT_MS = 1e4;
|
|
3884
5275
|
var MAX_RESPONSE_BYTES = 262144;
|
|
3885
5276
|
function relayEndpoint(relayUrl, target) {
|
|
@@ -3969,7 +5360,7 @@ var A2aPeerClient = class {
|
|
|
3969
5360
|
signal.throwIfAborted();
|
|
3970
5361
|
client ??= await createClient(signal);
|
|
3971
5362
|
const currentClient = client;
|
|
3972
|
-
const requestMessageId =
|
|
5363
|
+
const requestMessageId = randomUUID5();
|
|
3973
5364
|
let current;
|
|
3974
5365
|
let ownedTaskId;
|
|
3975
5366
|
let completed = false;
|
|
@@ -4038,7 +5429,7 @@ var A2aPeerClient = class {
|
|
|
4038
5429
|
};
|
|
4039
5430
|
|
|
4040
5431
|
// src/a2a-server.ts
|
|
4041
|
-
import { createHash as
|
|
5432
|
+
import { createHash as createHash11, randomUUID as randomUUID6 } from "node:crypto";
|
|
4042
5433
|
import {
|
|
4043
5434
|
AgentCard,
|
|
4044
5435
|
Role as Role2,
|
|
@@ -4064,7 +5455,7 @@ import {
|
|
|
4064
5455
|
ServerCallContext,
|
|
4065
5456
|
validateVersion
|
|
4066
5457
|
} from "@a2a-js/sdk/server";
|
|
4067
|
-
import { z as
|
|
5458
|
+
import { z as z8 } from "zod";
|
|
4068
5459
|
var MAX_ACTIVE = 4;
|
|
4069
5460
|
var MAX_TASKS = 128;
|
|
4070
5461
|
var MAX_RECEIPTS = 256;
|
|
@@ -4073,52 +5464,52 @@ var TURN_TIMEOUT_MS = 12e4;
|
|
|
4073
5464
|
var SHORT_REQUEST_TIMEOUT_MS = 9e3;
|
|
4074
5465
|
var MAX_TEXT2 = 16e3;
|
|
4075
5466
|
var TASK_WIRE_BUDGET = 6e4;
|
|
4076
|
-
var idSchema =
|
|
4077
|
-
var optionalIdSchema =
|
|
4078
|
-
var historySchema =
|
|
4079
|
-
var emptyMetadataSchema =
|
|
4080
|
-
var tenantSchema =
|
|
4081
|
-
var rpcSchema =
|
|
4082
|
-
jsonrpc:
|
|
4083
|
-
id:
|
|
4084
|
-
method:
|
|
4085
|
-
params:
|
|
5467
|
+
var idSchema = z8.string().min(1).max(256).regex(/^[^\s\u0000-\u001f\u007f]+$/u);
|
|
5468
|
+
var optionalIdSchema = z8.union([z8.literal(""), idSchema]).optional();
|
|
5469
|
+
var historySchema = z8.number().int().min(0).max(128).optional();
|
|
5470
|
+
var emptyMetadataSchema = z8.object({}).strict().optional();
|
|
5471
|
+
var tenantSchema = z8.literal("").optional();
|
|
5472
|
+
var rpcSchema = z8.object({
|
|
5473
|
+
jsonrpc: z8.literal("2.0"),
|
|
5474
|
+
id: z8.union([idSchema, z8.number().int().safe(), z8.null()]),
|
|
5475
|
+
method: z8.string().min(1).max(128),
|
|
5476
|
+
params: z8.unknown()
|
|
4086
5477
|
}).strict();
|
|
4087
|
-
var sendSchema =
|
|
5478
|
+
var sendSchema = z8.object({
|
|
4088
5479
|
tenant: tenantSchema,
|
|
4089
|
-
message:
|
|
5480
|
+
message: z8.object({
|
|
4090
5481
|
messageId: idSchema,
|
|
4091
5482
|
contextId: optionalIdSchema,
|
|
4092
5483
|
taskId: optionalIdSchema,
|
|
4093
|
-
role:
|
|
4094
|
-
parts:
|
|
4095
|
-
text:
|
|
4096
|
-
mediaType:
|
|
4097
|
-
filename:
|
|
5484
|
+
role: z8.union([z8.literal("ROLE_USER"), z8.literal(Role2.ROLE_USER)]),
|
|
5485
|
+
parts: z8.array(z8.object({
|
|
5486
|
+
text: z8.string().max(MAX_TEXT2),
|
|
5487
|
+
mediaType: z8.union([z8.literal(""), z8.literal("text/plain")]).optional(),
|
|
5488
|
+
filename: z8.literal("").optional(),
|
|
4098
5489
|
metadata: emptyMetadataSchema
|
|
4099
5490
|
}).strict()).min(1).max(32),
|
|
4100
5491
|
metadata: emptyMetadataSchema,
|
|
4101
|
-
extensions:
|
|
4102
|
-
referenceTaskIds:
|
|
5492
|
+
extensions: z8.array(z8.never()).optional(),
|
|
5493
|
+
referenceTaskIds: z8.array(z8.never()).optional()
|
|
4103
5494
|
}).strict().refine((message) => {
|
|
4104
5495
|
const text = message.parts.map((part) => part.text).join("\n");
|
|
4105
5496
|
return text.trim().length > 0 && text.length <= MAX_TEXT2;
|
|
4106
5497
|
}, "A message must contain nonempty plain text of at most 16000 characters."),
|
|
4107
|
-
configuration:
|
|
4108
|
-
acceptedOutputModes:
|
|
4109
|
-
returnImmediately:
|
|
5498
|
+
configuration: z8.object({
|
|
5499
|
+
acceptedOutputModes: z8.array(z8.literal("text/plain")).max(1).optional(),
|
|
5500
|
+
returnImmediately: z8.boolean().optional(),
|
|
4110
5501
|
historyLength: historySchema
|
|
4111
5502
|
}).strict().optional(),
|
|
4112
5503
|
metadata: emptyMetadataSchema
|
|
4113
5504
|
}).strict();
|
|
4114
|
-
var getSchema =
|
|
4115
|
-
var cancelSchema =
|
|
4116
|
-
var listSchema2 =
|
|
5505
|
+
var getSchema = z8.object({ tenant: tenantSchema, id: idSchema, historyLength: historySchema }).strict();
|
|
5506
|
+
var cancelSchema = z8.object({ tenant: tenantSchema, id: idSchema, metadata: emptyMetadataSchema }).strict();
|
|
5507
|
+
var listSchema2 = z8.object({
|
|
4117
5508
|
tenant: tenantSchema,
|
|
4118
5509
|
contextId: optionalIdSchema,
|
|
4119
|
-
status:
|
|
4120
|
-
|
|
4121
|
-
|
|
5510
|
+
status: z8.union([
|
|
5511
|
+
z8.number().int().min(0).max(8),
|
|
5512
|
+
z8.enum([
|
|
4122
5513
|
"TASK_STATE_UNSPECIFIED",
|
|
4123
5514
|
"TASK_STATE_SUBMITTED",
|
|
4124
5515
|
"TASK_STATE_WORKING",
|
|
@@ -4130,19 +5521,19 @@ var listSchema2 = z6.object({
|
|
|
4130
5521
|
"TASK_STATE_AUTH_REQUIRED"
|
|
4131
5522
|
])
|
|
4132
5523
|
]).optional(),
|
|
4133
|
-
pageSize:
|
|
4134
|
-
pageToken:
|
|
5524
|
+
pageSize: z8.number().int().min(1).max(100).optional(),
|
|
5525
|
+
pageToken: z8.string().regex(/^(?:|0|[1-9]\d{0,3})$/u).optional(),
|
|
4135
5526
|
historyLength: historySchema,
|
|
4136
|
-
statusTimestampAfter:
|
|
4137
|
-
includeArtifacts:
|
|
5527
|
+
statusTimestampAfter: z8.iso.datetime({ offset: true }).optional(),
|
|
5528
|
+
includeArtifacts: z8.boolean().optional()
|
|
4138
5529
|
}).strict();
|
|
4139
5530
|
function deferred() {
|
|
4140
|
-
let
|
|
5531
|
+
let resolve5 = () => {
|
|
4141
5532
|
};
|
|
4142
5533
|
const promise = new Promise((done) => {
|
|
4143
|
-
|
|
5534
|
+
resolve5 = done;
|
|
4144
5535
|
});
|
|
4145
|
-
return { promise, resolve:
|
|
5536
|
+
return { promise, resolve: resolve5 };
|
|
4146
5537
|
}
|
|
4147
5538
|
function scopedOwner(context) {
|
|
4148
5539
|
const owner = context.user;
|
|
@@ -4340,7 +5731,7 @@ var A2aPeerServer = class {
|
|
|
4340
5731
|
if (message === void 0) throw new RequestMalformedError("A message is required.");
|
|
4341
5732
|
const scope = scopedOwner(context);
|
|
4342
5733
|
const key = JSON.stringify([scope, message.messageId]);
|
|
4343
|
-
const fingerprint =
|
|
5734
|
+
const fingerprint = createHash11("sha256").update(JSON.stringify(SendMessageRequest.toJSON(params))).digest("hex");
|
|
4344
5735
|
const previous = this.#receipts.get(key);
|
|
4345
5736
|
if (previous !== void 0) {
|
|
4346
5737
|
if (previous.fingerprint !== fingerprint) throw new RequestMalformedError("messageId was already used with different content.");
|
|
@@ -4366,7 +5757,7 @@ var A2aPeerServer = class {
|
|
|
4366
5757
|
throw new UnsupportedOperationError("The native peer session is busy or its stop is unconfirmed.");
|
|
4367
5758
|
}
|
|
4368
5759
|
if (this.#receipts.size >= MAX_RECEIPTS) throw new UnsupportedOperationError("This online server's message receipt limit was reached.");
|
|
4369
|
-
const probe = workingTask(
|
|
5760
|
+
const probe = workingTask(randomUUID6(), message.contextId || randomUUID6(), message);
|
|
4370
5761
|
if (JSON.stringify(Task.toJSON(probe)).length > TASK_WIRE_BUDGET - 1024) {
|
|
4371
5762
|
throw new ContentTypeNotSupportedError("Text exceeds the response transport budget.");
|
|
4372
5763
|
}
|
|
@@ -4460,7 +5851,7 @@ var A2aPeerServer = class {
|
|
|
4460
5851
|
if (!canceled) this.host.reportError(error);
|
|
4461
5852
|
}
|
|
4462
5853
|
const reply = {
|
|
4463
|
-
messageId:
|
|
5854
|
+
messageId: randomUUID6(),
|
|
4464
5855
|
contextId: request.contextId,
|
|
4465
5856
|
taskId: request.taskId,
|
|
4466
5857
|
role: Role2.ROLE_AGENT,
|
|
@@ -4613,7 +6004,7 @@ var A2aPeerServer = class {
|
|
|
4613
6004
|
};
|
|
4614
6005
|
|
|
4615
6006
|
// src/peer-chat.ts
|
|
4616
|
-
import { randomUUID as
|
|
6007
|
+
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
4617
6008
|
var MAX_ACTIVE_TURNS = 4;
|
|
4618
6009
|
var MAX_SEEN_REQUESTS = 2048;
|
|
4619
6010
|
function peerError(error) {
|
|
@@ -4737,7 +6128,7 @@ var PeerChatRuntime = class {
|
|
|
4737
6128
|
if (generation !== this.#generation) throw new Error("Peer connection changed; old result will not be replayed.");
|
|
4738
6129
|
return this.host.send(PeerMessageSchema.parse({
|
|
4739
6130
|
version: 1,
|
|
4740
|
-
messageId:
|
|
6131
|
+
messageId: randomUUID7(),
|
|
4741
6132
|
requestId: message.messageId,
|
|
4742
6133
|
sourceSessionId: message.targetSessionId,
|
|
4743
6134
|
targetSessionId: message.sourceSessionId,
|
|
@@ -4840,13 +6231,13 @@ var SnapshotSync = class {
|
|
|
4840
6231
|
if (!requireFreshRead && active !== void 0) {
|
|
4841
6232
|
return active.promise;
|
|
4842
6233
|
}
|
|
4843
|
-
let
|
|
6234
|
+
let resolve5;
|
|
4844
6235
|
let reject;
|
|
4845
6236
|
const promise = new Promise((onResolve, onReject) => {
|
|
4846
|
-
|
|
6237
|
+
resolve5 = onResolve;
|
|
4847
6238
|
reject = onReject;
|
|
4848
6239
|
});
|
|
4849
|
-
this.#pending.set(recipientClientId, { promise, resolve:
|
|
6240
|
+
this.#pending.set(recipientClientId, { promise, resolve: resolve5, reject, requireFreshRead });
|
|
4850
6241
|
this.#start();
|
|
4851
6242
|
return promise;
|
|
4852
6243
|
}
|
|
@@ -5215,7 +6606,7 @@ var SessionInventory = class {
|
|
|
5215
6606
|
};
|
|
5216
6607
|
|
|
5217
6608
|
// src/snapshot-audience.ts
|
|
5218
|
-
import { createHash as
|
|
6609
|
+
import { createHash as createHash12 } from "node:crypto";
|
|
5219
6610
|
var SnapshotAudience = class {
|
|
5220
6611
|
constructor(send) {
|
|
5221
6612
|
this.send = send;
|
|
@@ -5244,9 +6635,9 @@ var SnapshotAudience = class {
|
|
|
5244
6635
|
async publish(snapshot, recipient) {
|
|
5245
6636
|
const generation = this.#generation;
|
|
5246
6637
|
const { sequence: _sequence, generatedAt: _generatedAt, ...content } = snapshot;
|
|
5247
|
-
const
|
|
6638
|
+
const digest2 = createHash12("sha256").update(JSON.stringify(content)).digest("base64url");
|
|
5248
6639
|
if (recipient !== void 0) {
|
|
5249
|
-
await this.#direct(snapshot, recipient,
|
|
6640
|
+
await this.#direct(snapshot, recipient, digest2, generation);
|
|
5250
6641
|
return;
|
|
5251
6642
|
}
|
|
5252
6643
|
if (!this.#negotiated || this.#registrationBaseline) {
|
|
@@ -5258,10 +6649,10 @@ var SnapshotAudience = class {
|
|
|
5258
6649
|
}
|
|
5259
6650
|
const pending = [];
|
|
5260
6651
|
for (const [clientId, baseline] of this.#audiences) {
|
|
5261
|
-
if (baseline.digest ===
|
|
6652
|
+
if (baseline.digest === digest2) {
|
|
5262
6653
|
this.#metrics.unchangedRecipients += 1;
|
|
5263
6654
|
} else {
|
|
5264
|
-
pending.push(this.#direct(snapshot, clientId,
|
|
6655
|
+
pending.push(this.#direct(snapshot, clientId, digest2, generation));
|
|
5265
6656
|
}
|
|
5266
6657
|
}
|
|
5267
6658
|
if (this.#audiences.size === 0) this.#metrics.withoutRecipients += 1;
|
|
@@ -5274,12 +6665,12 @@ var SnapshotAudience = class {
|
|
|
5274
6665
|
this.#metrics = this.#emptyMetrics();
|
|
5275
6666
|
return metrics;
|
|
5276
6667
|
}
|
|
5277
|
-
async #direct(snapshot, recipient,
|
|
6668
|
+
async #direct(snapshot, recipient, digest2, generation) {
|
|
5278
6669
|
const baseline = this.#audiences.get(recipient);
|
|
5279
6670
|
this.#metrics.directedAttempts += 1;
|
|
5280
6671
|
await this.send(snapshot, recipient);
|
|
5281
6672
|
if (generation === this.#generation && baseline !== void 0 && this.#audiences.get(recipient) === baseline) {
|
|
5282
|
-
baseline.digest =
|
|
6673
|
+
baseline.digest = digest2;
|
|
5283
6674
|
}
|
|
5284
6675
|
}
|
|
5285
6676
|
#emptyMetrics() {
|
|
@@ -5376,106 +6767,17 @@ var ConversationAudience = class {
|
|
|
5376
6767
|
};
|
|
5377
6768
|
|
|
5378
6769
|
// src/timeline-incremental-transport.ts
|
|
5379
|
-
import { createHash as
|
|
6770
|
+
import { createHash as createHash13, randomUUID as randomUUID8 } from "node:crypto";
|
|
5380
6771
|
import { isDeepStrictEqual } from "node:util";
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
}
|
|
5389
|
-
code;
|
|
5390
|
-
};
|
|
5391
|
-
var TimelineImageCache = class {
|
|
5392
|
-
constructor(maxBytes = 32 * 1024 * 1024) {
|
|
5393
|
-
this.maxBytes = maxBytes;
|
|
5394
|
-
if (!Number.isSafeInteger(maxBytes) || maxBytes < TIMELINE_IMAGE_MAX_BYTES) {
|
|
5395
|
-
throw new Error("Image cache must hold at least one maximum-size image");
|
|
5396
|
-
}
|
|
5397
|
-
}
|
|
5398
|
-
maxBytes;
|
|
5399
|
-
#assets = /* @__PURE__ */ new Map();
|
|
5400
|
-
#bytes = 0;
|
|
5401
|
-
get bytes() {
|
|
5402
|
-
return this.#bytes;
|
|
5403
|
-
}
|
|
5404
|
-
referenceItem(sessionId, item) {
|
|
5405
|
-
if (!("attachments" in item) || item.attachments === void 0) return item;
|
|
5406
|
-
return { ...item, attachments: item.attachments.map((attachment) => this.#reference(sessionId, attachment)) };
|
|
5407
|
-
}
|
|
5408
|
-
read(sessionId, request) {
|
|
5409
|
-
const key = JSON.stringify([sessionId, request.sha256]);
|
|
5410
|
-
const asset = this.#assets.get(key);
|
|
5411
|
-
if (asset === void 0) throw new TimelineAssetError("IMAGE_ABSENT", "The image is no longer cached on this PC");
|
|
5412
|
-
if (request.offset >= asset.bytes.length) {
|
|
5413
|
-
throw new TimelineAssetError("IMAGE_OFFSET_INVALID", "The image chunk offset exceeds its length");
|
|
5414
|
-
}
|
|
5415
|
-
this.#assets.delete(key);
|
|
5416
|
-
this.#assets.set(key, asset);
|
|
5417
|
-
const end = Math.min(request.offset + TIMELINE_IMAGE_CHUNK_BYTES, asset.bytes.length);
|
|
5418
|
-
return TimelineImageChunkSchema.parse({
|
|
5419
|
-
sha256: request.sha256,
|
|
5420
|
-
offset: request.offset,
|
|
5421
|
-
totalBytes: asset.bytes.length,
|
|
5422
|
-
mimeType: asset.mimeType,
|
|
5423
|
-
data: asset.bytes.subarray(request.offset, end).toString("base64"),
|
|
5424
|
-
eof: end === asset.bytes.length
|
|
5425
|
-
});
|
|
5426
|
-
}
|
|
5427
|
-
clearSession(sessionId) {
|
|
5428
|
-
const prefix = JSON.stringify([sessionId]).slice(0, -1) + ",";
|
|
5429
|
-
for (const [key, asset] of this.#assets) {
|
|
5430
|
-
if (key.startsWith(prefix)) {
|
|
5431
|
-
this.#assets.delete(key);
|
|
5432
|
-
this.#bytes -= asset.bytes.length;
|
|
5433
|
-
}
|
|
5434
|
-
}
|
|
5435
|
-
}
|
|
5436
|
-
clear() {
|
|
5437
|
-
this.#assets.clear();
|
|
5438
|
-
this.#bytes = 0;
|
|
5439
|
-
}
|
|
5440
|
-
#reference(sessionId, attachment) {
|
|
5441
|
-
if (attachment.kind !== "image" || attachment.data === void 0) return attachment;
|
|
5442
|
-
const parsed = ConversationAttachmentSchema.parse(attachment);
|
|
5443
|
-
const bytes = Buffer.from(attachment.data, "base64");
|
|
5444
|
-
if (bytes.length !== parsed.byteLength || createHash10("sha256").update(bytes).digest("hex") !== parsed.sha256 || !hasSignature(bytes, parsed.mimeType)) {
|
|
5445
|
-
throw new TimelineAssetError("IMAGE_INVALID", "Image content does not match its metadata");
|
|
5446
|
-
}
|
|
5447
|
-
const key = JSON.stringify([sessionId, parsed.sha256]);
|
|
5448
|
-
const previous = this.#assets.get(key);
|
|
5449
|
-
if (previous !== void 0) this.#bytes -= previous.bytes.length;
|
|
5450
|
-
this.#assets.delete(key);
|
|
5451
|
-
this.#assets.set(key, { bytes, mimeType: parsed.mimeType ?? "" });
|
|
5452
|
-
this.#bytes += bytes.length;
|
|
5453
|
-
while (this.#bytes > this.maxBytes) {
|
|
5454
|
-
const oldest = this.#assets.entries().next().value;
|
|
5455
|
-
if (oldest === void 0) break;
|
|
5456
|
-
this.#assets.delete(oldest[0]);
|
|
5457
|
-
this.#bytes -= oldest[1].bytes.length;
|
|
5458
|
-
}
|
|
5459
|
-
const { data: _data, ...metadata } = parsed;
|
|
5460
|
-
return { ...metadata, imageRef: "sha256" };
|
|
5461
|
-
}
|
|
5462
|
-
};
|
|
5463
|
-
function hasSignature(bytes, mimeType) {
|
|
5464
|
-
switch (mimeType) {
|
|
5465
|
-
case "image/png":
|
|
5466
|
-
return bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
|
|
5467
|
-
case "image/jpeg":
|
|
5468
|
-
return bytes.subarray(0, 3).equals(Buffer.from([255, 216, 255]));
|
|
5469
|
-
case "image/gif":
|
|
5470
|
-
return ["GIF87a", "GIF89a"].includes(bytes.subarray(0, 6).toString("ascii"));
|
|
5471
|
-
case "image/webp":
|
|
5472
|
-
return bytes.subarray(0, 4).toString("ascii") === "RIFF" && bytes.subarray(8, 12).toString("ascii") === "WEBP";
|
|
5473
|
-
default:
|
|
5474
|
-
return false;
|
|
5475
|
-
}
|
|
6772
|
+
function journalDiagnostic(state) {
|
|
6773
|
+
return {
|
|
6774
|
+
currentSequence: state.sequence,
|
|
6775
|
+
oldestAvailableSequence: state.events[0]?.event.sequence ?? state.sequence + 1,
|
|
6776
|
+
retainedEvents: state.events.length,
|
|
6777
|
+
retainedBytes: state.bytes,
|
|
6778
|
+
baselinePublished: state.baselinePublished
|
|
6779
|
+
};
|
|
5476
6780
|
}
|
|
5477
|
-
|
|
5478
|
-
// src/timeline-incremental-transport.ts
|
|
5479
6781
|
var TimelineIncrementalTransport = class {
|
|
5480
6782
|
#sessions = /* @__PURE__ */ new Map();
|
|
5481
6783
|
#options;
|
|
@@ -5568,8 +6870,8 @@ var TimelineIncrementalTransport = class {
|
|
|
5568
6870
|
recordStep(sessionId, upsert) {
|
|
5569
6871
|
const state = this.#session(sessionId);
|
|
5570
6872
|
const previous = state.latest.get(upsert.activityId);
|
|
5571
|
-
const
|
|
5572
|
-
if (previous === void 0 ||
|
|
6873
|
+
const base2 = previous?.item;
|
|
6874
|
+
if (previous === void 0 || base2?.kind !== "activity-group" || base2.turnId !== upsert.turnId || base2.actor !== upsert.actor) {
|
|
5573
6875
|
if (!this.#observeColdStep(state, upsert)) return;
|
|
5574
6876
|
if (state.baselinePublished || state.sequence > 0 || state.reason === "initial") {
|
|
5575
6877
|
this.invalidate(sessionId, "gap");
|
|
@@ -5577,18 +6879,18 @@ var TimelineIncrementalTransport = class {
|
|
|
5577
6879
|
}
|
|
5578
6880
|
return;
|
|
5579
6881
|
}
|
|
5580
|
-
if (upsert.revision <
|
|
5581
|
-
const existingStep =
|
|
5582
|
-
if (upsert.revision ===
|
|
6882
|
+
if (upsert.revision < base2.revision) return;
|
|
6883
|
+
const existingStep = base2.steps.find((step) => step.id === upsert.step.id);
|
|
6884
|
+
if (upsert.revision === base2.revision && upsert.status === base2.status && base2.sourceEventIds.includes(upsert.sourceEventId) && isDeepStrictEqual(existingStep, upsert.step)) return;
|
|
5583
6885
|
const updated = {
|
|
5584
|
-
...
|
|
6886
|
+
...base2,
|
|
5585
6887
|
revision: upsert.revision,
|
|
5586
6888
|
status: upsert.status,
|
|
5587
6889
|
// Match both Android reducers; deduplicate before moving an existing step.
|
|
5588
|
-
steps: [...
|
|
5589
|
-
sourceEventIds:
|
|
6890
|
+
steps: [...base2.steps.filter((step) => step.id !== upsert.step.id), upsert.step].slice(-CONVERSATION_ACTIVITY_STEP_LIMIT),
|
|
6891
|
+
sourceEventIds: base2.sourceEventIds.includes(upsert.sourceEventId) ? base2.sourceEventIds : [...base2.sourceEventIds, upsert.sourceEventId].slice(-CONVERSATION_SOURCE_ID_LIMIT)
|
|
5590
6892
|
};
|
|
5591
|
-
if (isDeepStrictEqual(
|
|
6893
|
+
if (isDeepStrictEqual(base2, updated)) return;
|
|
5592
6894
|
if (byteSize(updated) > this.#options.maxReplayBytes) {
|
|
5593
6895
|
this.invalidate(sessionId, "oversized");
|
|
5594
6896
|
return;
|
|
@@ -5599,7 +6901,7 @@ var TimelineIncrementalTransport = class {
|
|
|
5599
6901
|
}
|
|
5600
6902
|
#observeColdStep(state, upsert) {
|
|
5601
6903
|
const cold = state.coldSteps;
|
|
5602
|
-
const key =
|
|
6904
|
+
const key = createHash13("sha256").update(JSON.stringify([upsert.activityId, upsert.step.id])).digest("hex");
|
|
5603
6905
|
const revision = cold.revisions.get(key);
|
|
5604
6906
|
if (revision !== void 0 && upsert.revision < revision) return false;
|
|
5605
6907
|
cold.revisions.delete(key);
|
|
@@ -5609,7 +6911,7 @@ var TimelineIncrementalTransport = class {
|
|
|
5609
6911
|
if (oldest === void 0) break;
|
|
5610
6912
|
cold.revisions.delete(oldest);
|
|
5611
6913
|
}
|
|
5612
|
-
const fingerprint =
|
|
6914
|
+
const fingerprint = createHash13("sha256").update(JSON.stringify(upsert)).digest("hex");
|
|
5613
6915
|
const observed = cold.fingerprints.has(fingerprint);
|
|
5614
6916
|
cold.fingerprints.delete(fingerprint);
|
|
5615
6917
|
cold.fingerprints.add(fingerprint);
|
|
@@ -5645,16 +6947,34 @@ var TimelineIncrementalTransport = class {
|
|
|
5645
6947
|
if (this.#closed) throw new TimelineAssetError("PC_OFFLINE", "The PC transport is closed");
|
|
5646
6948
|
let state = this.#session(sessionId);
|
|
5647
6949
|
const watermark = request.watermark;
|
|
6950
|
+
recordTimelineSyncDiagnostic({
|
|
6951
|
+
mode: watermark === void 0 ? "snapshot" : request.resume === true ? "resume" : "replay",
|
|
6952
|
+
...watermark === void 0 ? {} : {
|
|
6953
|
+
epochMatches: watermark.epoch === state.epoch,
|
|
6954
|
+
requestedSequence: watermark.sequence
|
|
6955
|
+
},
|
|
6956
|
+
...journalDiagnostic(state),
|
|
6957
|
+
headReadAttempts: 0,
|
|
6958
|
+
headReadRestarts: 0
|
|
6959
|
+
});
|
|
5648
6960
|
if (watermark === void 0) {
|
|
5649
6961
|
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
5650
6962
|
state = this.#session(sessionId);
|
|
5651
6963
|
const sequence2 = state.sequence;
|
|
5652
6964
|
const coldVersion = state.coldSteps.version;
|
|
6965
|
+
recordTimelineSyncDiagnostic({ ...journalDiagnostic(state), headReadAttempts: attempt + 1 });
|
|
5653
6966
|
const page = await measureOperationPhase("read", () => this.#options.readHead(sessionId));
|
|
5654
|
-
|
|
6967
|
+
const restartCause = this.#sessions.get(sessionId) !== state ? "epoch-changed" : state.sequence !== sequence2 ? "sequence-changed" : state.coldSteps.version !== coldVersion ? "cold-step-changed" : void 0;
|
|
6968
|
+
if (restartCause !== void 0) {
|
|
6969
|
+
recordTimelineSyncDiagnostic({ headReadRestarts: attempt + 1, restartCause });
|
|
6970
|
+
continue;
|
|
6971
|
+
}
|
|
5655
6972
|
if (state.baselinePublished) {
|
|
5656
6973
|
this.#reconcileHead(sessionId, state, page);
|
|
5657
|
-
if (this.#sessions.get(sessionId) !== state)
|
|
6974
|
+
if (this.#sessions.get(sessionId) !== state) {
|
|
6975
|
+
recordTimelineSyncDiagnostic({ headReadRestarts: attempt + 1, restartCause: "head-reconciled" });
|
|
6976
|
+
continue;
|
|
6977
|
+
}
|
|
5658
6978
|
}
|
|
5659
6979
|
const response = validateOperationResponse(() => TimelineSyncResponseSchema.parse({
|
|
5660
6980
|
status: "snapshot",
|
|
@@ -5671,6 +6991,12 @@ var TimelineIncrementalTransport = class {
|
|
|
5671
6991
|
state.headItems = response.page.items.map(({ id, timestamp }) => ({ id, timestamp }));
|
|
5672
6992
|
state.baselinePublished = true;
|
|
5673
6993
|
}
|
|
6994
|
+
recordTimelineSyncDiagnostic({
|
|
6995
|
+
...journalDiagnostic(state),
|
|
6996
|
+
status: "snapshot",
|
|
6997
|
+
responseItems: page.items.length,
|
|
6998
|
+
hasMore: page.hasMore
|
|
6999
|
+
});
|
|
5674
7000
|
return response;
|
|
5675
7001
|
}
|
|
5676
7002
|
throw new TimelineAssetError("TIMELINE_BUSY", "Timeline changed during baseline capture; retry synchronization");
|
|
@@ -5684,7 +7010,11 @@ var TimelineIncrementalTransport = class {
|
|
|
5684
7010
|
if (this.#closed) throw new TimelineAssetError("PC_OFFLINE", "The PC transport is closed");
|
|
5685
7011
|
state = this.#session(sessionId);
|
|
5686
7012
|
}
|
|
5687
|
-
|
|
7013
|
+
recordTimelineSyncDiagnostic({ ...journalDiagnostic(state), epochMatches: watermark.epoch === state.epoch });
|
|
7014
|
+
const resync = (reason) => {
|
|
7015
|
+
recordTimelineSyncDiagnostic({ status: "resync", resyncReason: reason });
|
|
7016
|
+
return { status: "resync", epoch: state.epoch, reason };
|
|
7017
|
+
};
|
|
5688
7018
|
if (watermark.epoch !== state.epoch) return resync(state.reason === "initial" ? "epoch-changed" : state.reason);
|
|
5689
7019
|
if (watermark.sequence > state.sequence) return resync("gap");
|
|
5690
7020
|
const available = state.events.filter(({ event }) => event.sequence > watermark.sequence);
|
|
@@ -5700,6 +7030,7 @@ var TimelineIncrementalTransport = class {
|
|
|
5700
7030
|
}
|
|
5701
7031
|
if (events.length === 0 && available.length > 0) return resync("oversized");
|
|
5702
7032
|
const sequence = events.at(-1)?.sequence ?? watermark.sequence;
|
|
7033
|
+
recordTimelineSyncDiagnostic({ status: "replay", responseEvents: events.length, hasMore: sequence < state.sequence });
|
|
5703
7034
|
return {
|
|
5704
7035
|
status: "replay",
|
|
5705
7036
|
watermark: { epoch: state.epoch, sequence },
|
|
@@ -5713,7 +7044,7 @@ var TimelineIncrementalTransport = class {
|
|
|
5713
7044
|
let state = this.#sessions.get(sessionId);
|
|
5714
7045
|
if (state === void 0) {
|
|
5715
7046
|
state = {
|
|
5716
|
-
epoch:
|
|
7047
|
+
epoch: randomUUID8(),
|
|
5717
7048
|
sequence: 0,
|
|
5718
7049
|
reason: "initial",
|
|
5719
7050
|
events: [],
|
|
@@ -5755,8 +7086,13 @@ var TimelineIncrementalTransport = class {
|
|
|
5755
7086
|
const state = this.#session(sessionId);
|
|
5756
7087
|
const sequence = state.sequence;
|
|
5757
7088
|
const coldVersion = state.coldSteps.version;
|
|
7089
|
+
recordTimelineSyncDiagnostic({ ...journalDiagnostic(state), headReadAttempts: attempt + 1 });
|
|
5758
7090
|
const page = await measureOperationPhase("read", () => this.#options.readHead(sessionId));
|
|
5759
|
-
|
|
7091
|
+
const restartCause = this.#sessions.get(sessionId) !== state ? "epoch-changed" : state.sequence !== sequence ? "sequence-changed" : state.coldSteps.version !== coldVersion ? "cold-step-changed" : void 0;
|
|
7092
|
+
if (restartCause !== void 0) {
|
|
7093
|
+
recordTimelineSyncDiagnostic({ headReadRestarts: attempt + 1, restartCause });
|
|
7094
|
+
continue;
|
|
7095
|
+
}
|
|
5760
7096
|
this.#reconcileHead(sessionId, state, page);
|
|
5761
7097
|
return;
|
|
5762
7098
|
}
|
|
@@ -5785,11 +7121,11 @@ var TimelineIncrementalTransport = class {
|
|
|
5785
7121
|
if (this.#waiters >= 64) throw new TimelineAssetError("TIMELINE_BUSY", "Too many pending timeline requests");
|
|
5786
7122
|
this.#waiters += 1;
|
|
5787
7123
|
try {
|
|
5788
|
-
await new Promise((
|
|
7124
|
+
await new Promise((resolve5) => {
|
|
5789
7125
|
const wake = () => {
|
|
5790
7126
|
clearTimeout(timer);
|
|
5791
7127
|
state.wake.delete(wake);
|
|
5792
|
-
|
|
7128
|
+
resolve5();
|
|
5793
7129
|
};
|
|
5794
7130
|
const timer = setTimeout(wake, Math.min(waitMs, 2e4));
|
|
5795
7131
|
state.wake.add(wake);
|
|
@@ -5856,6 +7192,104 @@ function canAppend(previous, item) {
|
|
|
5856
7192
|
return isDeepStrictEqual(oldMetadata, newMetadata);
|
|
5857
7193
|
}
|
|
5858
7194
|
|
|
7195
|
+
// src/timeline-image-blob.ts
|
|
7196
|
+
import { createHash as createHash14 } from "node:crypto";
|
|
7197
|
+
import sharp3 from "sharp";
|
|
7198
|
+
var TimelineImageBlobPublisher = class {
|
|
7199
|
+
constructor(images, uploader) {
|
|
7200
|
+
this.images = images;
|
|
7201
|
+
this.uploader = uploader;
|
|
7202
|
+
}
|
|
7203
|
+
images;
|
|
7204
|
+
uploader;
|
|
7205
|
+
#references = /* @__PURE__ */ new Map();
|
|
7206
|
+
#flights = /* @__PURE__ */ new Map();
|
|
7207
|
+
#closed = false;
|
|
7208
|
+
async publish(sessionId, sha256, assertCurrent) {
|
|
7209
|
+
TimelineImageBlobRequestSchema.parse({ sha256 });
|
|
7210
|
+
assertCurrent();
|
|
7211
|
+
if (this.#closed) throw new TimelineAssetError("IMAGE_BLOB_CLOSED", "The image publisher is closed");
|
|
7212
|
+
const key = JSON.stringify([sessionId, sha256]);
|
|
7213
|
+
const cached = this.#references.get(key);
|
|
7214
|
+
if (cached !== void 0) {
|
|
7215
|
+
this.#references.delete(key);
|
|
7216
|
+
if (cached.image.expiresAt > Date.now() + 3e4) {
|
|
7217
|
+
this.#references.set(key, cached);
|
|
7218
|
+
return cached;
|
|
7219
|
+
}
|
|
7220
|
+
}
|
|
7221
|
+
let flight = this.#flights.get(key);
|
|
7222
|
+
if (flight === void 0) {
|
|
7223
|
+
if (this.#flights.size >= 3) throw new TimelineAssetError("IMAGE_BLOB_BUSY", "Too many image uploads");
|
|
7224
|
+
const controller = new AbortController();
|
|
7225
|
+
const check = () => {
|
|
7226
|
+
controller.signal.throwIfAborted();
|
|
7227
|
+
assertCurrent();
|
|
7228
|
+
};
|
|
7229
|
+
const promise = this.#publish(sessionId, sha256, controller.signal, check).then((result2) => {
|
|
7230
|
+
check();
|
|
7231
|
+
this.#references.set(key, result2);
|
|
7232
|
+
while (this.#references.size > 256) {
|
|
7233
|
+
const oldest = this.#references.keys().next().value;
|
|
7234
|
+
if (oldest === void 0) break;
|
|
7235
|
+
this.#references.delete(oldest);
|
|
7236
|
+
}
|
|
7237
|
+
return result2;
|
|
7238
|
+
}).finally(() => {
|
|
7239
|
+
this.#flights.delete(key);
|
|
7240
|
+
});
|
|
7241
|
+
flight = { sessionId, controller, promise };
|
|
7242
|
+
this.#flights.set(key, flight);
|
|
7243
|
+
}
|
|
7244
|
+
const result = await flight.promise;
|
|
7245
|
+
assertCurrent();
|
|
7246
|
+
return result;
|
|
7247
|
+
}
|
|
7248
|
+
clearSession(sessionId) {
|
|
7249
|
+
for (const [key, reference] of this.#references) {
|
|
7250
|
+
if (reference.image.sessionId === sessionId) this.#references.delete(key);
|
|
7251
|
+
}
|
|
7252
|
+
for (const flight of this.#flights.values()) {
|
|
7253
|
+
if (flight.sessionId === sessionId) flight.controller.abort();
|
|
7254
|
+
}
|
|
7255
|
+
}
|
|
7256
|
+
reset() {
|
|
7257
|
+
this.#references.clear();
|
|
7258
|
+
for (const flight of this.#flights.values()) flight.controller.abort();
|
|
7259
|
+
}
|
|
7260
|
+
async close() {
|
|
7261
|
+
this.#closed = true;
|
|
7262
|
+
this.reset();
|
|
7263
|
+
await Promise.allSettled([...this.#flights.values()].map((flight) => flight.promise));
|
|
7264
|
+
}
|
|
7265
|
+
async #publish(sessionId, sha256, signal, check) {
|
|
7266
|
+
const asset = this.images.readAsset(sessionId, sha256);
|
|
7267
|
+
const extension = { "image/png": ".png", "image/jpeg": ".jpg", "image/webp": ".webp" }[asset.mimeType];
|
|
7268
|
+
if (extension === void 0) throw new TimelineAssetError("IMAGE_BLOB_UNSUPPORTED", "This image requires encrypted transfer");
|
|
7269
|
+
if (createHash14("sha256").update(asset.bytes).digest("hex") !== sha256) {
|
|
7270
|
+
throw new TimelineAssetError("IMAGE_INVALID", "Image content does not match its reference");
|
|
7271
|
+
}
|
|
7272
|
+
let bytes;
|
|
7273
|
+
try {
|
|
7274
|
+
await validateRawImage(asset.bytes, extension, check);
|
|
7275
|
+
bytes = extension === ".webp" ? asset.bytes : await sharp3(asset.bytes, { limitInputPixels: SESSION_IMAGE_MAX_PIXELS, failOn: "warning" }).rotate().webp({ lossless: true, effort: 3 }).timeout({ seconds: 10 }).toBuffer();
|
|
7276
|
+
} catch (error) {
|
|
7277
|
+
check();
|
|
7278
|
+
if (error instanceof SessionFileReadError) {
|
|
7279
|
+
throw new TimelineAssetError("IMAGE_BLOB_UNSUPPORTED", "The image cannot be safely stored as WebP", { cause: error });
|
|
7280
|
+
}
|
|
7281
|
+
throw new TimelineAssetError("IMAGE_BLOB_ENCODING_FAILED", "Unable to encode the image for storage", { cause: error });
|
|
7282
|
+
}
|
|
7283
|
+
check();
|
|
7284
|
+
if (bytes.length > SESSION_IMAGE_MAX_BYTES) {
|
|
7285
|
+
throw new TimelineAssetError("IMAGE_BLOB_TOO_LARGE", "The lossless WebP exceeds the image storage limit");
|
|
7286
|
+
}
|
|
7287
|
+
const image = await this.uploader.upload(sessionId, bytes, signal, check);
|
|
7288
|
+
check();
|
|
7289
|
+
return TimelineImageBlobResultSchema.parse({ sha256, image });
|
|
7290
|
+
}
|
|
7291
|
+
};
|
|
7292
|
+
|
|
5859
7293
|
// src/secure-traffic-metrics.ts
|
|
5860
7294
|
var SecureTrafficMetrics = class {
|
|
5861
7295
|
#samples = /* @__PURE__ */ new Map();
|
|
@@ -6352,9 +7786,9 @@ var INVENTORY_REFRESH_DEBOUNCE_MS = 100;
|
|
|
6352
7786
|
var SESSION_LIST_REFRESH_RETRIES = 2;
|
|
6353
7787
|
var TRAFFIC_METRICS_INTERVAL_MS = 6e4;
|
|
6354
7788
|
var PcAgent = class {
|
|
6355
|
-
constructor(config,
|
|
7789
|
+
constructor(config, identity2, appVersion, adapter, logger, pairingProvider, contentCipher, research, createSocket = (uri, options) => io(uri, options), onPairing) {
|
|
6356
7790
|
this.config = config;
|
|
6357
|
-
this.identity =
|
|
7791
|
+
this.identity = identity2;
|
|
6358
7792
|
this.appVersion = appVersion;
|
|
6359
7793
|
this.adapter = adapter;
|
|
6360
7794
|
this.logger = logger;
|
|
@@ -6365,8 +7799,8 @@ var PcAgent = class {
|
|
|
6365
7799
|
adapter = researchAwareCodingAgentAdapter(adapter, research.chat);
|
|
6366
7800
|
this.adapter = adapter;
|
|
6367
7801
|
}
|
|
6368
|
-
this.#stopped = new Promise((
|
|
6369
|
-
this.#resolveStopped =
|
|
7802
|
+
this.#stopped = new Promise((resolve5) => {
|
|
7803
|
+
this.#resolveStopped = resolve5;
|
|
6370
7804
|
});
|
|
6371
7805
|
this.#cipher = contentCipher;
|
|
6372
7806
|
this.#publication = new SessionPublicationScope(
|
|
@@ -6385,8 +7819,8 @@ var PcAgent = class {
|
|
|
6385
7819
|
});
|
|
6386
7820
|
this.#inventory = new SessionInventory({
|
|
6387
7821
|
userId: config.userId,
|
|
6388
|
-
machineId:
|
|
6389
|
-
machineName:
|
|
7822
|
+
machineId: identity2.machineId,
|
|
7823
|
+
machineName: identity2.machineName,
|
|
6390
7824
|
agentRunId: this.#agentRunId,
|
|
6391
7825
|
adapter,
|
|
6392
7826
|
logger
|
|
@@ -6400,8 +7834,16 @@ var PcAgent = class {
|
|
|
6400
7834
|
this.#timelineIncremental = new TimelineIncrementalTransport({
|
|
6401
7835
|
readHead: (sessionId) => this.adapter.readSessionTimelinePage(sessionId, { pageSize: 100 })
|
|
6402
7836
|
});
|
|
7837
|
+
const imageSettings = config.sessionImages === void 0 ? void 0 : SessionImagesConfigSchema.parse(config.sessionImages);
|
|
7838
|
+
this.#imageBlob = imageSettings === void 0 ? void 0 : new TimelineImageBlobPublisher(this.#timelineIncremental.images, new SessionImageUploader({
|
|
7839
|
+
relayUrl: config.relayUrl,
|
|
7840
|
+
relayToken: config.relayToken,
|
|
7841
|
+
userId: config.userId,
|
|
7842
|
+
machineId: identity2.machineId,
|
|
7843
|
+
...imageSettings
|
|
7844
|
+
}));
|
|
6403
7845
|
const emit = (event, payload) => this.#emitSecure(event, payload);
|
|
6404
|
-
const handlerOptions = { adapter, config, machineId:
|
|
7846
|
+
const handlerOptions = { adapter, config, machineId: identity2.machineId, logger, emit };
|
|
6405
7847
|
this.#communication = new SessionCommunicationHandler({
|
|
6406
7848
|
...handlerOptions,
|
|
6407
7849
|
agentRunId: this.#agentRunId,
|
|
@@ -6410,6 +7852,7 @@ var PcAgent = class {
|
|
|
6410
7852
|
audience: this.#conversationAudience,
|
|
6411
7853
|
metrics: this.#operationMetrics,
|
|
6412
7854
|
incrementalOperations: this.#timelineIncremental.operations,
|
|
7855
|
+
...this.#imageBlob === void 0 ? {} : { imageBlob: this.#imageBlob },
|
|
6413
7856
|
captureResearchAccess: (sessionId) => captureResearchAccess(adapter, sessionId),
|
|
6414
7857
|
captureWorkbenchAccess: (sessionId) => captureWorkbenchAccess(adapter, sessionId),
|
|
6415
7858
|
...research === void 0 ? {} : { research },
|
|
@@ -6436,6 +7879,7 @@ var PcAgent = class {
|
|
|
6436
7879
|
supportsAgentControls: (clientId) => this.#supportsAgentControls(clientId),
|
|
6437
7880
|
refreshSessions: (clientId) => this.#inventoryPublisher.request(clientId, true),
|
|
6438
7881
|
removeSession: (sessionId) => {
|
|
7882
|
+
this.#imageBlob?.clearSession(sessionId);
|
|
6439
7883
|
this.#timelineIncremental.remove(sessionId);
|
|
6440
7884
|
this.#conversationAudience.removeSession(sessionId);
|
|
6441
7885
|
this.#publication.revoke(sessionId);
|
|
@@ -6454,14 +7898,14 @@ var PcAgent = class {
|
|
|
6454
7898
|
this.#timelinePages = new SessionTimelinePageHandler({
|
|
6455
7899
|
...handlerOptions,
|
|
6456
7900
|
publications: this.#publication,
|
|
6457
|
-
encoder: new TimelineResponseEncoder(contentCipher,
|
|
7901
|
+
encoder: new TimelineResponseEncoder(contentCipher, identity2.machineId, this.#clientId),
|
|
6458
7902
|
trafficMetrics: this.#trafficMetrics,
|
|
6459
7903
|
sendSealed: (message) => this.#socket.emit("secure.message", message),
|
|
6460
7904
|
imageReferencesEnabled: () => this.#communicationNegotiated && this.#conversationAudience.confirmed,
|
|
6461
7905
|
referenceItem: (sessionId, item) => this.#timelineIncremental.images.referenceItem(sessionId, item)
|
|
6462
7906
|
});
|
|
6463
7907
|
this.#secureRequestRouter = createPcSecureRequestRouter({
|
|
6464
|
-
configuredMachineId:
|
|
7908
|
+
configuredMachineId: identity2.machineId,
|
|
6465
7909
|
handlers: {
|
|
6466
7910
|
sessionCommunication: (request) => this.#admitRequest(request, "session.communication", () => this.#communication.handle(request, this.#requestScope())),
|
|
6467
7911
|
sessionList: (request) => this.#admitRequest(request, "session.list.result", () => this.#handleSessionList(request)),
|
|
@@ -6489,6 +7933,7 @@ var PcAgent = class {
|
|
|
6489
7933
|
autoConnect: false,
|
|
6490
7934
|
reconnection: true,
|
|
6491
7935
|
transports: ["websocket", "polling"],
|
|
7936
|
+
tryAllTransports: true,
|
|
6492
7937
|
auth: {
|
|
6493
7938
|
transportVersion: 2,
|
|
6494
7939
|
protocolVersion: PROTOCOL_VERSION,
|
|
@@ -6496,7 +7941,7 @@ var PcAgent = class {
|
|
|
6496
7941
|
token: config.relayToken,
|
|
6497
7942
|
clientId: this.#clientId,
|
|
6498
7943
|
userId: config.userId,
|
|
6499
|
-
machineId:
|
|
7944
|
+
machineId: identity2.machineId,
|
|
6500
7945
|
keyId: contentCipher.keyId
|
|
6501
7946
|
}
|
|
6502
7947
|
}
|
|
@@ -6518,11 +7963,11 @@ var PcAgent = class {
|
|
|
6518
7963
|
relayUrl: config.relayUrl,
|
|
6519
7964
|
relayToken: config.relayToken,
|
|
6520
7965
|
userId: config.userId,
|
|
6521
|
-
machineId:
|
|
7966
|
+
machineId: identity2.machineId,
|
|
6522
7967
|
reportError: reportPeerError
|
|
6523
7968
|
});
|
|
6524
7969
|
this.#peerChat = new PeerChatRuntime({
|
|
6525
|
-
machineId:
|
|
7970
|
+
machineId: identity2.machineId,
|
|
6526
7971
|
assertReady: assertPeerReady,
|
|
6527
7972
|
send: (message) => this.#sendPeerMessage(message),
|
|
6528
7973
|
createConversation: (request) => a2aClient.conversation(request),
|
|
@@ -6537,7 +7982,7 @@ var PcAgent = class {
|
|
|
6537
7982
|
if (!this.#a2aNegotiated) throw new Error("Relay has not negotiated A2A support.");
|
|
6538
7983
|
},
|
|
6539
7984
|
assertSession: async (sessionId) => {
|
|
6540
|
-
if (adapter.isSessionVisible?.(sessionId) === false || !(await adapter.listSessions(config.userId,
|
|
7985
|
+
if (adapter.isSessionVisible?.(sessionId) === false || !(await adapter.listSessions(config.userId, identity2.machineId)).some((session) => session.sessionId === sessionId)) {
|
|
6541
7986
|
throw new Error("The requested Agent session is not shared or no longer exists.");
|
|
6542
7987
|
}
|
|
6543
7988
|
},
|
|
@@ -6615,6 +8060,7 @@ var PcAgent = class {
|
|
|
6615
8060
|
},
|
|
6616
8061
|
onTimelineInvalidated: (sessionId, invalidation) => {
|
|
6617
8062
|
if (this.adapter.isSessionVisible?.(sessionId) === false) return;
|
|
8063
|
+
this.#imageBlob?.clearSession(sessionId);
|
|
6618
8064
|
this.#publication.capture(sessionId);
|
|
6619
8065
|
const envelope = SessionTimelineInvalidatedSchema.parse({
|
|
6620
8066
|
userId: this.config.userId,
|
|
@@ -6652,7 +8098,7 @@ var PcAgent = class {
|
|
|
6652
8098
|
onInteractionDismissed: (sessionId, interactionRequestId) => {
|
|
6653
8099
|
const payload = InteractionRespondResultSchema.parse({
|
|
6654
8100
|
ok: false,
|
|
6655
|
-
requestId:
|
|
8101
|
+
requestId: randomUUID9(),
|
|
6656
8102
|
interactionRequestId,
|
|
6657
8103
|
machineId: this.identity.machineId,
|
|
6658
8104
|
sessionId,
|
|
@@ -6669,7 +8115,9 @@ var PcAgent = class {
|
|
|
6669
8115
|
this.#supersededInteractions.delete(oldestInteractionRequestId);
|
|
6670
8116
|
}
|
|
6671
8117
|
}
|
|
6672
|
-
this.#
|
|
8118
|
+
if (this.#machineRegistered) {
|
|
8119
|
+
this.#emitSecure("interaction.respond.result", payload, void 0, true);
|
|
8120
|
+
}
|
|
6673
8121
|
},
|
|
6674
8122
|
onPermissionRequest: (sessionId, permissionRequestId, request) => {
|
|
6675
8123
|
const payload = PermissionRequestPayloadSchema.parse({
|
|
@@ -6747,8 +8195,8 @@ var PcAgent = class {
|
|
|
6747
8195
|
pairingProvider;
|
|
6748
8196
|
research;
|
|
6749
8197
|
onPairing;
|
|
6750
|
-
#agentRunId =
|
|
6751
|
-
#clientId =
|
|
8198
|
+
#agentRunId = randomUUID9();
|
|
8199
|
+
#clientId = randomUUID9();
|
|
6752
8200
|
#socket;
|
|
6753
8201
|
#relayDiagnostics = new RelayConnectionDiagnostics();
|
|
6754
8202
|
#cipher;
|
|
@@ -6762,6 +8210,7 @@ var PcAgent = class {
|
|
|
6762
8210
|
#readMetrics = { readStarts: 0, readJoins: 0, untrackedReads: 0, peakInFlightPerSession: 0 };
|
|
6763
8211
|
#conversationAudience = new ConversationAudience();
|
|
6764
8212
|
#timelineIncremental;
|
|
8213
|
+
#imageBlob;
|
|
6765
8214
|
#pendingAcks = /* @__PURE__ */ new Set();
|
|
6766
8215
|
#connectionGeneration = 0;
|
|
6767
8216
|
#heartbeatTimer;
|
|
@@ -6865,25 +8314,25 @@ var PcAgent = class {
|
|
|
6865
8314
|
if (!this.#peerTransportNegotiated || !this.#machineRegistered || !this.#socket.connected) {
|
|
6866
8315
|
return Promise.reject(new Error("Peer channel is offline or unsupported; nothing will be queued."));
|
|
6867
8316
|
}
|
|
6868
|
-
return new Promise((
|
|
6869
|
-
const timer = setTimeout(() =>
|
|
6870
|
-
const
|
|
8317
|
+
return new Promise((resolve5, reject) => {
|
|
8318
|
+
const timer = setTimeout(() => fail4(new Error("Peer forwarding acknowledgement timed out; delivery is unknown.")), ACK_TIMEOUT_MS);
|
|
8319
|
+
const fail4 = (error) => {
|
|
6871
8320
|
clearTimeout(timer);
|
|
6872
|
-
this.#pendingAcks.delete(
|
|
8321
|
+
this.#pendingAcks.delete(fail4);
|
|
6873
8322
|
reject(error);
|
|
6874
8323
|
};
|
|
6875
|
-
this.#pendingAcks.add(
|
|
8324
|
+
this.#pendingAcks.add(fail4);
|
|
6876
8325
|
try {
|
|
6877
8326
|
this.#socket.emit("peer.message", message, (rawAck) => {
|
|
6878
8327
|
clearTimeout(timer);
|
|
6879
|
-
if (!this.#pendingAcks.delete(
|
|
8328
|
+
if (!this.#pendingAcks.delete(fail4)) return;
|
|
6880
8329
|
const ack = OperationAckSchema.safeParse(rawAck);
|
|
6881
8330
|
if (!ack.success) reject(new Error("Relay sent an invalid peer forwarding acknowledgement."));
|
|
6882
8331
|
else if (!ack.data.ok) reject(new Error(`${ack.data.code}: ${ack.data.message}`));
|
|
6883
|
-
else
|
|
8332
|
+
else resolve5();
|
|
6884
8333
|
});
|
|
6885
8334
|
} catch (error) {
|
|
6886
|
-
|
|
8335
|
+
fail4(error instanceof Error ? error : new Error(String(error)));
|
|
6887
8336
|
}
|
|
6888
8337
|
});
|
|
6889
8338
|
}
|
|
@@ -7022,6 +8471,7 @@ var PcAgent = class {
|
|
|
7022
8471
|
return this.#stopped;
|
|
7023
8472
|
}
|
|
7024
8473
|
this.#stopping = true;
|
|
8474
|
+
const imagesStopped = this.#imageBlob?.close();
|
|
7025
8475
|
const peerStopped = this.#peerChat.close();
|
|
7026
8476
|
const a2aStopped = this.#a2aServer.close();
|
|
7027
8477
|
this.#machineRegistered = false;
|
|
@@ -7048,6 +8498,7 @@ var PcAgent = class {
|
|
|
7048
8498
|
this.adapter.stop(),
|
|
7049
8499
|
peerStopped,
|
|
7050
8500
|
a2aStopped,
|
|
8501
|
+
...imagesStopped === void 0 ? [] : [imagesStopped],
|
|
7051
8502
|
...this.research === void 0 ? [] : [this.research.stop()]
|
|
7052
8503
|
]);
|
|
7053
8504
|
const operationsStopped = await Promise.allSettled(
|
|
@@ -7097,12 +8548,14 @@ var PcAgent = class {
|
|
|
7097
8548
|
const diagnostics = this.#relayDiagnostics.disconnected(reason, details);
|
|
7098
8549
|
this.#connectionGeneration += 1;
|
|
7099
8550
|
this.#machineRegistered = false;
|
|
8551
|
+
if (this.#socket.sendBuffer !== void 0) this.#socket.sendBuffer.length = 0;
|
|
7100
8552
|
this.#initialSyncGeneration = void 0;
|
|
7101
8553
|
this.#communicationNegotiated = false;
|
|
7102
8554
|
this.#peerNegotiated = false;
|
|
7103
8555
|
this.#peerTransportNegotiated = false;
|
|
7104
8556
|
this.#a2aNegotiated = false;
|
|
7105
8557
|
this.#peerChat.reset(new Error("Relay disconnected; peer conversation will not be replayed."));
|
|
8558
|
+
this.#imageBlob?.reset();
|
|
7106
8559
|
this.#a2aServer.reset(new Error("Relay disconnected; A2A execution will not be replayed."));
|
|
7107
8560
|
this.#conversationAudience.reset();
|
|
7108
8561
|
this.#cancelSnapshotSync();
|
|
@@ -7394,6 +8847,7 @@ var PcAgent = class {
|
|
|
7394
8847
|
if (this.adapter.isSessionVisible === void 0) return;
|
|
7395
8848
|
let removed = 0;
|
|
7396
8849
|
for (const id of this.#publication.revokeHidden()) {
|
|
8850
|
+
this.#imageBlob?.clearSession(id);
|
|
7397
8851
|
this.#timelineIncremental.remove(id);
|
|
7398
8852
|
this.#conversationAudience.removeSession(id);
|
|
7399
8853
|
removed += 1;
|
|
@@ -7744,24 +9198,24 @@ var PcAgent = class {
|
|
|
7744
9198
|
});
|
|
7745
9199
|
}
|
|
7746
9200
|
#emitWithAck(event, payload) {
|
|
7747
|
-
return new Promise((
|
|
9201
|
+
return new Promise((resolve5, reject) => {
|
|
7748
9202
|
if (!this.#socket.connected || this.#stopping) {
|
|
7749
9203
|
reject(new SnapshotSyncCancelledError());
|
|
7750
9204
|
return;
|
|
7751
9205
|
}
|
|
7752
9206
|
const timer = setTimeout(() => {
|
|
7753
|
-
|
|
9207
|
+
fail4(new Error(`${event} acknowledgement timed out`));
|
|
7754
9208
|
}, ACK_TIMEOUT_MS);
|
|
7755
|
-
const
|
|
9209
|
+
const fail4 = (error) => {
|
|
7756
9210
|
clearTimeout(timer);
|
|
7757
|
-
this.#pendingAcks.delete(
|
|
9211
|
+
this.#pendingAcks.delete(fail4);
|
|
7758
9212
|
reject(error);
|
|
7759
9213
|
};
|
|
7760
|
-
this.#pendingAcks.add(
|
|
9214
|
+
this.#pendingAcks.add(fail4);
|
|
7761
9215
|
const receiveAck = (ack) => {
|
|
7762
9216
|
clearTimeout(timer);
|
|
7763
|
-
this.#pendingAcks.delete(
|
|
7764
|
-
|
|
9217
|
+
this.#pendingAcks.delete(fail4);
|
|
9218
|
+
resolve5(ack);
|
|
7765
9219
|
};
|
|
7766
9220
|
try {
|
|
7767
9221
|
if (event === "machine.hello") {
|
|
@@ -7775,7 +9229,7 @@ var PcAgent = class {
|
|
|
7775
9229
|
}
|
|
7776
9230
|
} catch (error) {
|
|
7777
9231
|
clearTimeout(timer);
|
|
7778
|
-
this.#pendingAcks.delete(
|
|
9232
|
+
this.#pendingAcks.delete(fail4);
|
|
7779
9233
|
reject(error);
|
|
7780
9234
|
}
|
|
7781
9235
|
});
|
|
@@ -7870,63 +9324,63 @@ function createResearchContextReader(options) {
|
|
|
7870
9324
|
};
|
|
7871
9325
|
}
|
|
7872
9326
|
function contextItem(item) {
|
|
7873
|
-
const
|
|
9327
|
+
const identity2 = {
|
|
7874
9328
|
kind: item.kind,
|
|
7875
9329
|
actor: item.actor,
|
|
7876
9330
|
id: item.id,
|
|
7877
9331
|
turnId: item.turnId,
|
|
7878
9332
|
sourceEventIds: item.sourceEventIds
|
|
7879
9333
|
};
|
|
7880
|
-
if (item.kind === "activity-group") return { ...
|
|
9334
|
+
if (item.kind === "activity-group") return { ...identity2, steps: item.steps, toolResultsUnavailable: true };
|
|
7881
9335
|
return {
|
|
7882
|
-
...
|
|
9336
|
+
...identity2,
|
|
7883
9337
|
content: item.content,
|
|
7884
9338
|
...item.kind === "plan-update" ? { todos: item.todos } : {}
|
|
7885
9339
|
};
|
|
7886
9340
|
}
|
|
7887
9341
|
|
|
7888
9342
|
// src/research-sessions.ts
|
|
7889
|
-
import { execFile as
|
|
7890
|
-
import { createHash as
|
|
7891
|
-
import { mkdir as
|
|
9343
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
9344
|
+
import { createHash as createHash15, randomUUID as randomUUID12 } from "node:crypto";
|
|
9345
|
+
import { mkdir as mkdir5, lstat as lstat8, realpath as realpath8 } from "node:fs/promises";
|
|
7892
9346
|
import { createRequire } from "node:module";
|
|
7893
|
-
import { join as
|
|
7894
|
-
import { promisify as
|
|
7895
|
-
import { z as
|
|
9347
|
+
import { join as join12, resolve as resolve4 } from "node:path";
|
|
9348
|
+
import { promisify as promisify4 } from "node:util";
|
|
9349
|
+
import { z as z11 } from "zod";
|
|
7896
9350
|
|
|
7897
9351
|
// src/research-thread-store.ts
|
|
7898
|
-
import { randomUUID as
|
|
7899
|
-
import { mkdir as
|
|
7900
|
-
import { join as
|
|
7901
|
-
import { z as
|
|
9352
|
+
import { randomUUID as randomUUID10 } from "node:crypto";
|
|
9353
|
+
import { mkdir as mkdir4, readFile as readFile2, rename as rename2, rm as rm2, lstat as lstat6, realpath as realpath7, writeFile as writeFile3 } from "node:fs/promises";
|
|
9354
|
+
import { join as join10, resolve as resolve3 } from "node:path";
|
|
9355
|
+
import { z as z9 } from "zod";
|
|
7902
9356
|
var RESEARCH_PROFILE = "research-readonly";
|
|
7903
9357
|
var RESEARCH_PROFILE_VERSION = 1;
|
|
7904
9358
|
var MAX_THREADS = 256;
|
|
7905
|
-
var recordSchema =
|
|
7906
|
-
threadId:
|
|
7907
|
-
researchSessionId:
|
|
7908
|
-
parentSessionId:
|
|
7909
|
-
operationId:
|
|
7910
|
-
fingerprint:
|
|
7911
|
-
title:
|
|
7912
|
-
profile:
|
|
7913
|
-
profileVersion:
|
|
7914
|
-
workingDirectory:
|
|
7915
|
-
rootIdentity:
|
|
7916
|
-
contextAnchor:
|
|
7917
|
-
contextSampledAt:
|
|
7918
|
-
contextTruncated:
|
|
7919
|
-
chatVersion:
|
|
7920
|
-
createdAt:
|
|
7921
|
-
state:
|
|
7922
|
-
errorCode:
|
|
9359
|
+
var recordSchema = z9.object({
|
|
9360
|
+
threadId: z9.string().uuid(),
|
|
9361
|
+
researchSessionId: z9.string().uuid(),
|
|
9362
|
+
parentSessionId: z9.string().min(1).max(256),
|
|
9363
|
+
operationId: z9.string().min(1).max(256),
|
|
9364
|
+
fingerprint: z9.string().min(1).max(128),
|
|
9365
|
+
title: z9.string().min(1).max(200),
|
|
9366
|
+
profile: z9.literal(RESEARCH_PROFILE),
|
|
9367
|
+
profileVersion: z9.literal(RESEARCH_PROFILE_VERSION),
|
|
9368
|
+
workingDirectory: z9.string().min(1).max(8192),
|
|
9369
|
+
rootIdentity: z9.string().min(1).max(128),
|
|
9370
|
+
contextAnchor: z9.string().min(1).max(256),
|
|
9371
|
+
contextSampledAt: z9.string().datetime(),
|
|
9372
|
+
contextTruncated: z9.boolean(),
|
|
9373
|
+
chatVersion: z9.literal(1).optional(),
|
|
9374
|
+
createdAt: z9.string().datetime(),
|
|
9375
|
+
state: z9.enum(["creating", "ready", "uncertain", "failed", "deleting"]),
|
|
9376
|
+
errorCode: z9.string().max(128).optional()
|
|
7923
9377
|
}).strict();
|
|
7924
|
-
var documentSchema =
|
|
7925
|
-
version:
|
|
7926
|
-
threads:
|
|
7927
|
-
tombstones:
|
|
7928
|
-
researchSessionId:
|
|
7929
|
-
parentSessionId:
|
|
9378
|
+
var documentSchema = z9.object({
|
|
9379
|
+
version: z9.literal(1),
|
|
9380
|
+
threads: z9.array(recordSchema).max(MAX_THREADS),
|
|
9381
|
+
tombstones: z9.array(z9.object({
|
|
9382
|
+
researchSessionId: z9.string().uuid(),
|
|
9383
|
+
parentSessionId: z9.string().min(1).max(256)
|
|
7930
9384
|
}).strict()).max(1e5).optional()
|
|
7931
9385
|
}).strict();
|
|
7932
9386
|
var ResearchThreadStore = class {
|
|
@@ -7937,20 +9391,20 @@ var ResearchThreadStore = class {
|
|
|
7937
9391
|
#loaded;
|
|
7938
9392
|
#mutation = Promise.resolve();
|
|
7939
9393
|
constructor(dataDirectory) {
|
|
7940
|
-
this.directory =
|
|
7941
|
-
this.#file =
|
|
9394
|
+
this.directory = resolve3(dataDirectory, "research");
|
|
9395
|
+
this.#file = join10(this.directory, "threads.json");
|
|
7942
9396
|
}
|
|
7943
9397
|
initialize() {
|
|
7944
9398
|
this.#loaded ??= this.#load();
|
|
7945
9399
|
return this.#loaded;
|
|
7946
9400
|
}
|
|
7947
9401
|
async #load() {
|
|
7948
|
-
await
|
|
7949
|
-
if ((await
|
|
9402
|
+
await mkdir4(this.directory, { recursive: true, mode: 448 });
|
|
9403
|
+
if ((await lstat6(this.directory)).isSymbolicLink() || await realpath7(this.directory) !== this.directory) {
|
|
7950
9404
|
throw new ResearchError("RESEARCH_PRIVATE_DIRECTORY_INVALID");
|
|
7951
9405
|
}
|
|
7952
9406
|
try {
|
|
7953
|
-
const metadata = await
|
|
9407
|
+
const metadata = await lstat6(this.#file);
|
|
7954
9408
|
if (metadata.isSymbolicLink() || metadata.size > 32 * 1024 * 1024) throw new ResearchError("RESEARCH_STORE_INVALID");
|
|
7955
9409
|
const parsed = JSON.parse(await readFile2(this.#file, "utf8"));
|
|
7956
9410
|
const data = documentSchema.parse(parsed);
|
|
@@ -7971,19 +9425,19 @@ var ResearchThreadStore = class {
|
|
|
7971
9425
|
throw new ResearchError("RESEARCH_STORE_INVALID", "Research metadata cannot be safely loaded", { cause: error });
|
|
7972
9426
|
}
|
|
7973
9427
|
}
|
|
7974
|
-
async #write(
|
|
7975
|
-
const staging =
|
|
9428
|
+
async #write(records2, tombstones = this.#tombstones) {
|
|
9429
|
+
const staging = join10(this.directory, `threads-${randomUUID10()}.pending`);
|
|
7976
9430
|
try {
|
|
7977
|
-
await
|
|
9431
|
+
await writeFile3(staging, JSON.stringify({
|
|
7978
9432
|
version: 1,
|
|
7979
|
-
threads: [...
|
|
9433
|
+
threads: [...records2.values()],
|
|
7980
9434
|
tombstones: [...tombstones].map(([researchSessionId, parentSessionId]) => ({ researchSessionId, parentSessionId }))
|
|
7981
9435
|
}), { flag: "wx", mode: 384 });
|
|
7982
9436
|
await rename2(staging, this.#file);
|
|
7983
|
-
this.#records =
|
|
9437
|
+
this.#records = records2;
|
|
7984
9438
|
this.#tombstones = tombstones;
|
|
7985
9439
|
} finally {
|
|
7986
|
-
await
|
|
9440
|
+
await rm2(staging, { force: true });
|
|
7987
9441
|
}
|
|
7988
9442
|
}
|
|
7989
9443
|
async #serialized(work) {
|
|
@@ -8005,8 +9459,8 @@ var ResearchThreadStore = class {
|
|
|
8005
9459
|
}
|
|
8006
9460
|
const record = recordSchema.parse({
|
|
8007
9461
|
...input,
|
|
8008
|
-
threadId:
|
|
8009
|
-
researchSessionId:
|
|
9462
|
+
threadId: randomUUID10(),
|
|
9463
|
+
researchSessionId: randomUUID10(),
|
|
8010
9464
|
profile: RESEARCH_PROFILE,
|
|
8011
9465
|
profileVersion: RESEARCH_PROFILE_VERSION,
|
|
8012
9466
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -8060,27 +9514,27 @@ var ResearchThreadStore = class {
|
|
|
8060
9514
|
};
|
|
8061
9515
|
|
|
8062
9516
|
// src/research-chat-store.ts
|
|
8063
|
-
import { randomUUID as
|
|
8064
|
-
import { lstat as
|
|
8065
|
-
import { join as
|
|
8066
|
-
import { z as
|
|
8067
|
-
var receipt =
|
|
8068
|
-
operationId:
|
|
8069
|
-
fingerprint:
|
|
8070
|
-
promptHash:
|
|
8071
|
-
ordinal:
|
|
8072
|
-
submittedAt:
|
|
8073
|
-
state:
|
|
8074
|
-
messageId:
|
|
8075
|
-
deliveryState:
|
|
9517
|
+
import { randomUUID as randomUUID11 } from "node:crypto";
|
|
9518
|
+
import { lstat as lstat7, readFile as readFile3, rename as rename3, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
|
|
9519
|
+
import { join as join11 } from "node:path";
|
|
9520
|
+
import { z as z10 } from "zod";
|
|
9521
|
+
var receipt = z10.object({
|
|
9522
|
+
operationId: z10.string().min(1).max(256),
|
|
9523
|
+
fingerprint: z10.string().min(1).max(128),
|
|
9524
|
+
promptHash: z10.string().min(1).max(128),
|
|
9525
|
+
ordinal: z10.number().int().nonnegative(),
|
|
9526
|
+
submittedAt: z10.string().datetime(),
|
|
9527
|
+
state: z10.enum(["sending", "accepted"]),
|
|
9528
|
+
messageId: z10.string().min(1).max(256).optional(),
|
|
9529
|
+
deliveryState: z10.enum(["queued", "processing"])
|
|
8076
9530
|
}).strict();
|
|
8077
|
-
var stateSchema =
|
|
8078
|
-
version:
|
|
8079
|
-
modelId:
|
|
8080
|
-
reasoningEffort:
|
|
8081
|
-
seed:
|
|
8082
|
-
seedState:
|
|
8083
|
-
receipts:
|
|
9531
|
+
var stateSchema = z10.object({
|
|
9532
|
+
version: z10.literal(1),
|
|
9533
|
+
modelId: z10.string().min(1).max(256).optional(),
|
|
9534
|
+
reasoningEffort: z10.enum(["low", "medium", "high", "xhigh", "max"]).optional(),
|
|
9535
|
+
seed: z10.string().max(3e5).optional(),
|
|
9536
|
+
seedState: z10.enum(["pending", "submitted", "none"]),
|
|
9537
|
+
receipts: z10.array(receipt).max(1e4)
|
|
8084
9538
|
}).strict().superRefine((state, ctx) => {
|
|
8085
9539
|
if (new Set(state.receipts.map((item) => item.operationId)).size !== state.receipts.length || new Set(state.receipts.map((item) => item.ordinal)).size !== state.receipts.length || state.seedState === "pending" && state.seed === void 0) {
|
|
8086
9540
|
ctx.addIssue({ code: "custom", message: "Invalid research chat state" });
|
|
@@ -8096,14 +9550,14 @@ var ResearchChatStore = class {
|
|
|
8096
9550
|
return this.#states.get(sessionId);
|
|
8097
9551
|
}
|
|
8098
9552
|
#file(sessionId) {
|
|
8099
|
-
return
|
|
9553
|
+
return join11(this.directory, `chat-${z10.string().uuid().parse(sessionId)}.json`);
|
|
8100
9554
|
}
|
|
8101
9555
|
async read(sessionId) {
|
|
8102
9556
|
const cached = this.#states.get(sessionId);
|
|
8103
9557
|
if (cached !== void 0) return cached;
|
|
8104
9558
|
try {
|
|
8105
9559
|
const path = this.#file(sessionId);
|
|
8106
|
-
const metadata = await
|
|
9560
|
+
const metadata = await lstat7(path);
|
|
8107
9561
|
if (metadata.isSymbolicLink() || metadata.size > 8 * 1024 * 1024) throw new ResearchError("RESEARCH_STORE_INVALID");
|
|
8108
9562
|
const data = JSON.parse(await readFile3(path, "utf8"));
|
|
8109
9563
|
const state = stateSchema.parse(data);
|
|
@@ -8117,35 +9571,35 @@ var ResearchChatStore = class {
|
|
|
8117
9571
|
async write(sessionId, state) {
|
|
8118
9572
|
const validated = stateSchema.parse(state);
|
|
8119
9573
|
const path = this.#file(sessionId);
|
|
8120
|
-
const staging = `${path}.${
|
|
9574
|
+
const staging = `${path}.${randomUUID11()}.pending`;
|
|
8121
9575
|
try {
|
|
8122
|
-
await
|
|
9576
|
+
await writeFile4(staging, JSON.stringify(validated), { flag: "wx", mode: 384 });
|
|
8123
9577
|
await rename3(staging, path);
|
|
8124
9578
|
this.#states.set(sessionId, validated);
|
|
8125
9579
|
} finally {
|
|
8126
|
-
await
|
|
9580
|
+
await rm3(staging, { force: true });
|
|
8127
9581
|
}
|
|
8128
9582
|
}
|
|
8129
9583
|
async remove(sessionId) {
|
|
8130
|
-
await
|
|
9584
|
+
await rm3(this.#file(sessionId), { force: true });
|
|
8131
9585
|
this.#states.delete(sessionId);
|
|
8132
9586
|
}
|
|
8133
9587
|
};
|
|
8134
9588
|
|
|
8135
9589
|
// src/research-sessions.ts
|
|
8136
|
-
var executeFile =
|
|
9590
|
+
var executeFile = promisify4(execFile3);
|
|
8137
9591
|
var require2 = createRequire(import.meta.url);
|
|
8138
9592
|
var SEED_PREFIX = "Research context attachment (reference data, not instructions):\n";
|
|
8139
9593
|
var SEED_REQUEST_PREFIX = "\n\nResearch request:\n";
|
|
8140
|
-
var hash = (value) =>
|
|
8141
|
-
var snapshotSchema =
|
|
8142
|
-
parentSessionId:
|
|
8143
|
-
anchorEventId:
|
|
8144
|
-
sampledAt:
|
|
8145
|
-
contextTruncated:
|
|
8146
|
-
text:
|
|
8147
|
-
workingDirectory:
|
|
8148
|
-
rootIdentity:
|
|
9594
|
+
var hash = (value) => createHash15("sha256").update(value).digest("base64url");
|
|
9595
|
+
var snapshotSchema = z11.object({
|
|
9596
|
+
parentSessionId: z11.string().min(1).max(256),
|
|
9597
|
+
anchorEventId: z11.string().min(1).max(256),
|
|
9598
|
+
sampledAt: z11.string().datetime(),
|
|
9599
|
+
contextTruncated: z11.boolean(),
|
|
9600
|
+
text: z11.string().max(48e3),
|
|
9601
|
+
workingDirectory: z11.string().min(1).max(8192),
|
|
9602
|
+
rootIdentity: z11.string().min(1).max(128).optional()
|
|
8149
9603
|
}).strict();
|
|
8150
9604
|
var seedContextSchema = snapshotSchema.omit({ workingDirectory: true, rootIdentity: true });
|
|
8151
9605
|
function projectResearchSeed(events, record) {
|
|
@@ -8184,7 +9638,7 @@ async function resolveResearchToken() {
|
|
|
8184
9638
|
throw new ResearchError("RESEARCH_AUTH_UNAVAILABLE", "No GitHub credential is available for isolated research");
|
|
8185
9639
|
}
|
|
8186
9640
|
function researchClientOptions(privateDirectory, token) {
|
|
8187
|
-
const
|
|
9641
|
+
const environment2 = {};
|
|
8188
9642
|
const inherited = /* @__PURE__ */ new Set([
|
|
8189
9643
|
"PATH",
|
|
8190
9644
|
"SYSTEMROOT",
|
|
@@ -8198,22 +9652,22 @@ function researchClientOptions(privateDirectory, token) {
|
|
|
8198
9652
|
"NO_PROXY"
|
|
8199
9653
|
]);
|
|
8200
9654
|
for (const [key, value] of Object.entries(process.env)) {
|
|
8201
|
-
if (value !== void 0 && inherited.has(key.toUpperCase()))
|
|
9655
|
+
if (value !== void 0 && inherited.has(key.toUpperCase())) environment2[key] = value;
|
|
8202
9656
|
}
|
|
8203
|
-
Object.assign(
|
|
9657
|
+
Object.assign(environment2, {
|
|
8204
9658
|
HOME: privateDirectory,
|
|
8205
9659
|
USERPROFILE: privateDirectory,
|
|
8206
9660
|
APPDATA: privateDirectory,
|
|
8207
9661
|
LOCALAPPDATA: privateDirectory,
|
|
8208
|
-
TEMP:
|
|
8209
|
-
TMP:
|
|
9662
|
+
TEMP: join12(privateDirectory, "scratch"),
|
|
9663
|
+
TMP: join12(privateDirectory, "scratch")
|
|
8210
9664
|
});
|
|
8211
9665
|
return {
|
|
8212
9666
|
mode: "empty",
|
|
8213
9667
|
baseDirectory: privateDirectory,
|
|
8214
9668
|
workingDirectory: privateDirectory,
|
|
8215
9669
|
connection: { kind: "stdio", path: require2.resolve("@github/copilot/npm-loader.js") },
|
|
8216
|
-
env:
|
|
9670
|
+
env: environment2,
|
|
8217
9671
|
gitHubToken: token,
|
|
8218
9672
|
useLoggedInUser: false,
|
|
8219
9673
|
builtinPluginDirectories: [],
|
|
@@ -8269,7 +9723,7 @@ var ResearchSessions = class {
|
|
|
8269
9723
|
constructor(options) {
|
|
8270
9724
|
this.options = options;
|
|
8271
9725
|
this.store = new ResearchThreadStore(options.dataDirectory);
|
|
8272
|
-
this.#privateDirectory =
|
|
9726
|
+
this.#privateDirectory = join12(this.store.directory, "runtime");
|
|
8273
9727
|
this.#chat = new ResearchChatStore(this.store.directory);
|
|
8274
9728
|
this.#projection = new LiveTimelineProjection({
|
|
8275
9729
|
state: (id) => this.#state(id),
|
|
@@ -8290,7 +9744,7 @@ var ResearchSessions = class {
|
|
|
8290
9744
|
});
|
|
8291
9745
|
}
|
|
8292
9746
|
options;
|
|
8293
|
-
#generation =
|
|
9747
|
+
#generation = randomUUID12();
|
|
8294
9748
|
store;
|
|
8295
9749
|
#privateDirectory;
|
|
8296
9750
|
#active = /* @__PURE__ */ new Map();
|
|
@@ -8359,8 +9813,8 @@ var ResearchSessions = class {
|
|
|
8359
9813
|
this.#failedClient = void 0;
|
|
8360
9814
|
}
|
|
8361
9815
|
await this.store.initialize();
|
|
8362
|
-
await
|
|
8363
|
-
if ((await
|
|
9816
|
+
await mkdir5(join12(this.#privateDirectory, "scratch"), { recursive: true, mode: 448 });
|
|
9817
|
+
if ((await lstat8(this.#privateDirectory)).isSymbolicLink() || await realpath8(this.#privateDirectory) !== resolve4(this.#privateDirectory)) {
|
|
8364
9818
|
throw new ResearchError("RESEARCH_PRIVATE_DIRECTORY_INVALID");
|
|
8365
9819
|
}
|
|
8366
9820
|
const token = await (this.options.resolveToken ?? resolveResearchToken)();
|
|
@@ -8412,7 +9866,7 @@ var ResearchSessions = class {
|
|
|
8412
9866
|
record.parentSessionId,
|
|
8413
9867
|
{ workingDirectory: record.workingDirectory, rootIdentity: record.rootIdentity },
|
|
8414
9868
|
this.options.sourceMetadata,
|
|
8415
|
-
[
|
|
9869
|
+
[resolve4(this.options.dataDirectory), this.#privateDirectory],
|
|
8416
9870
|
(error, context) => this.options.onError?.(error, {
|
|
8417
9871
|
...context,
|
|
8418
9872
|
researchSessionId: record.researchSessionId
|
|
@@ -8547,9 +10001,9 @@ var ResearchSessions = class {
|
|
|
8547
10001
|
assertAccess?.();
|
|
8548
10002
|
const snapshot = snapshotSchema.parse(input.snapshot);
|
|
8549
10003
|
if (snapshot.parentSessionId !== input.parentSessionId) throw new ResearchError("RESEARCH_SOURCE_MISMATCH");
|
|
8550
|
-
const title =
|
|
8551
|
-
const prompt =
|
|
8552
|
-
const fingerprint =
|
|
10004
|
+
const title = z11.string().trim().min(1).max(200).parse(input.title ?? "Research");
|
|
10005
|
+
const prompt = z11.string().trim().min(1).max(16e3).parse(input.prompt ?? "Acknowledge the context briefly and wait for my research question.");
|
|
10006
|
+
const fingerprint = createHash15("sha256").update(JSON.stringify({
|
|
8553
10007
|
parentSessionId: input.parentSessionId,
|
|
8554
10008
|
title,
|
|
8555
10009
|
prompt,
|
|
@@ -8583,7 +10037,7 @@ var ResearchSessions = class {
|
|
|
8583
10037
|
let session;
|
|
8584
10038
|
try {
|
|
8585
10039
|
const configuration = input.configuration ?? await this.options.sourceConfiguration?.(input.parentSessionId);
|
|
8586
|
-
const reasoning =
|
|
10040
|
+
const reasoning = z11.enum(["low", "medium", "high", "xhigh", "max"]).safeParse(configuration?.reasoningEffort);
|
|
8587
10041
|
if (configuration !== void 0 && !reasoning.success && !["default", "unknown"].includes(configuration.reasoningEffort)) {
|
|
8588
10042
|
throw new ResearchError("RESEARCH_CONFIGURATION_UNSUPPORTED");
|
|
8589
10043
|
}
|
|
@@ -8672,8 +10126,8 @@ var ResearchSessions = class {
|
|
|
8672
10126
|
};
|
|
8673
10127
|
}
|
|
8674
10128
|
async list(parentSessionId) {
|
|
8675
|
-
const
|
|
8676
|
-
for (const record of
|
|
10129
|
+
const records2 = (await this.store.list(parentSessionId)).filter((record) => this.options.sourceVisible?.(record.parentSessionId) !== false);
|
|
10130
|
+
for (const record of records2) {
|
|
8677
10131
|
if (record.state === "deleting" && !this.#mutating.has(record.researchSessionId) && !this.#deletionAttempts.has(record.researchSessionId)) {
|
|
8678
10132
|
await this.delete(record.researchSessionId);
|
|
8679
10133
|
}
|
|
@@ -8707,7 +10161,7 @@ var ResearchSessions = class {
|
|
|
8707
10161
|
const assertAccess = this.#capture(record);
|
|
8708
10162
|
assertAccess();
|
|
8709
10163
|
if (options.metadataOnly === true) throw new ResearchError("RESEARCH_SESSION_ISOLATED");
|
|
8710
|
-
const pageSize =
|
|
10164
|
+
const pageSize = z11.number().int().min(1).max(SESSION_TIMELINE_PAGE_SIZE).parse(options.pageSize ?? SESSION_TIMELINE_PAGE_SIZE);
|
|
8711
10165
|
const result = await this.#state(sessionId).timelineReads.read({
|
|
8712
10166
|
...options.cursor === void 0 ? {} : { cursor: options.cursor },
|
|
8713
10167
|
pageSize,
|
|
@@ -8750,13 +10204,13 @@ var ResearchSessions = class {
|
|
|
8750
10204
|
compactDisabledReason: "Research context compaction is unavailable"
|
|
8751
10205
|
});
|
|
8752
10206
|
}
|
|
8753
|
-
async send(sessionId, text, operationId =
|
|
10207
|
+
async send(sessionId, text, operationId = randomUUID12()) {
|
|
8754
10208
|
if (this.#active.get(sessionId)?.runtime.activity.busy) throw new ResearchError("RESEARCH_BUSY");
|
|
8755
10209
|
return (await this.sendPrompt(sessionId, text, "immediate", operationId)).messageId;
|
|
8756
10210
|
}
|
|
8757
10211
|
async sendPrompt(sessionId, text, mode, operationId) {
|
|
8758
|
-
const prompt =
|
|
8759
|
-
|
|
10212
|
+
const prompt = z11.string().trim().min(1).max(16e3).parse(text);
|
|
10213
|
+
z11.string().min(1).max(256).parse(operationId);
|
|
8760
10214
|
const fingerprint = hash(JSON.stringify({ prompt, mode }));
|
|
8761
10215
|
const running = this.#sending.get(sessionId);
|
|
8762
10216
|
if (running !== void 0 && running.operationId === operationId) {
|
|
@@ -9045,21 +10499,30 @@ function createResearchWorkbench(options) {
|
|
|
9045
10499
|
}
|
|
9046
10500
|
|
|
9047
10501
|
// src/initialize.ts
|
|
9048
|
-
var ConfigSchema =
|
|
9049
|
-
dataDirectory:
|
|
9050
|
-
relayUrl:
|
|
9051
|
-
relayToken:
|
|
9052
|
-
userId:
|
|
9053
|
-
workspaceRoot:
|
|
9054
|
-
refreshIntervalMs:
|
|
9055
|
-
heartbeatIntervalMs:
|
|
9056
|
-
exitAfterSnapshot:
|
|
9057
|
-
groupChatEnabled:
|
|
9058
|
-
peerChatEnabled:
|
|
9059
|
-
snapshotSuppressionEnabled:
|
|
9060
|
-
timelineCompressionEnabled:
|
|
9061
|
-
timelineIncrementalEnabled:
|
|
9062
|
-
secureMessagePacketBudget:
|
|
10502
|
+
var ConfigSchema = z12.object({
|
|
10503
|
+
dataDirectory: z12.string().min(1),
|
|
10504
|
+
relayUrl: z12.url().refine((value) => ["https:", "http:"].includes(new URL(value).protocol)),
|
|
10505
|
+
relayToken: z12.string().min(24),
|
|
10506
|
+
userId: z12.string().trim().min(1).max(256),
|
|
10507
|
+
workspaceRoot: z12.string().optional(),
|
|
10508
|
+
refreshIntervalMs: z12.number().int().min(5e3).max(36e5).default(3e4),
|
|
10509
|
+
heartbeatIntervalMs: z12.number().int().min(5e3).max(12e4).default(15e3),
|
|
10510
|
+
exitAfterSnapshot: z12.boolean().default(false),
|
|
10511
|
+
groupChatEnabled: z12.boolean().optional(),
|
|
10512
|
+
peerChatEnabled: z12.boolean().optional(),
|
|
10513
|
+
snapshotSuppressionEnabled: z12.boolean().optional(),
|
|
10514
|
+
timelineCompressionEnabled: z12.boolean().optional(),
|
|
10515
|
+
timelineIncrementalEnabled: z12.boolean().optional(),
|
|
10516
|
+
secureMessagePacketBudget: z12.number().int().min(2048).max(SECURE_MESSAGE_MAX_CIPHERTEXT_LENGTH).optional(),
|
|
10517
|
+
sessionImages: SessionImagesConfigSchema.optional()
|
|
10518
|
+
}).superRefine((config, context) => {
|
|
10519
|
+
if (config.sessionImages !== void 0 && !SessionImageRelayUrlSchema.safeParse(config.relayUrl).success) {
|
|
10520
|
+
context.addIssue({
|
|
10521
|
+
code: "custom",
|
|
10522
|
+
path: ["relayUrl"],
|
|
10523
|
+
message: "Session images require an HTTPS Relay origin without a path, credentials, query or fragment"
|
|
10524
|
+
});
|
|
10525
|
+
}
|
|
9063
10526
|
});
|
|
9064
10527
|
async function initialize(options) {
|
|
9065
10528
|
const dataDirectory = validateDataDirectory(options.dataDirectory);
|
|
@@ -9079,10 +10542,11 @@ async function initialize(options) {
|
|
|
9079
10542
|
...parsed.timelineCompressionEnabled === void 0 ? {} : { timelineCompressionEnabled: parsed.timelineCompressionEnabled },
|
|
9080
10543
|
...parsed.timelineIncrementalEnabled === void 0 ? {} : { timelineIncrementalEnabled: parsed.timelineIncrementalEnabled },
|
|
9081
10544
|
...parsed.secureMessagePacketBudget === void 0 ? {} : { secureMessagePacketBudget: parsed.secureMessagePacketBudget },
|
|
10545
|
+
...parsed.sessionImages === void 0 ? {} : { sessionImages: parsed.sessionImages },
|
|
9082
10546
|
...options.codingAgent === void 0 ? {} : { codingAgent: options.codingAgent }
|
|
9083
10547
|
};
|
|
9084
|
-
const
|
|
9085
|
-
const appVersion = options.appVersion ??
|
|
10548
|
+
const identity2 = DeviceIdentitySchema.parse(options.identity);
|
|
10549
|
+
const appVersion = options.appVersion ?? z12.object({ version: z12.string().min(1) }).parse(JSON.parse(await readFile4(new URL("../package.json", import.meta.url), "utf8"))).version;
|
|
9086
10550
|
const lease = options.directoryLease ?? await DataDirectoryLease.acquire(dataDirectory);
|
|
9087
10551
|
let detach;
|
|
9088
10552
|
let adapter;
|
|
@@ -9096,11 +10560,11 @@ async function initialize(options) {
|
|
|
9096
10560
|
adapter = options.adapter ?? await createCodingAgentAdapter(
|
|
9097
10561
|
config,
|
|
9098
10562
|
(message) => options.logger.warn({ error: { message } }, "coding-agent.diagnostic"),
|
|
9099
|
-
{ userId: config.userId, machineId:
|
|
10563
|
+
{ userId: config.userId, machineId: identity2.machineId }
|
|
9100
10564
|
);
|
|
9101
10565
|
const contentKey = await loadOrCreateContentKey(dataDirectory);
|
|
9102
10566
|
const cipher = await createPersistentContentCipher(dataDirectory, contentKey);
|
|
9103
|
-
const pairingProvider = new CachedPairingProvider(() => requestPairingUri(config,
|
|
10567
|
+
const pairingProvider = new CachedPairingProvider(() => requestPairingUri(config, identity2, contentKey));
|
|
9104
10568
|
const sourceAdapter = adapter;
|
|
9105
10569
|
const sourceMetadata = async (sessionId) => {
|
|
9106
10570
|
const access2 = captureResearchAccess(sourceAdapter, sessionId);
|
|
@@ -9138,7 +10602,7 @@ async function initialize(options) {
|
|
|
9138
10602
|
}, "research.operation.failed")
|
|
9139
10603
|
});
|
|
9140
10604
|
class InitializedAgent extends PcAgent {
|
|
9141
|
-
identityHash =
|
|
10605
|
+
identityHash = createHash16("sha256").update(`${config.userId}:${identity2.machineId}:${contentKey.keyId}`).digest("hex");
|
|
9142
10606
|
#started;
|
|
9143
10607
|
#starting;
|
|
9144
10608
|
#closed;
|
|
@@ -9178,7 +10642,7 @@ async function initialize(options) {
|
|
|
9178
10642
|
}
|
|
9179
10643
|
return new InitializedAgent(
|
|
9180
10644
|
config,
|
|
9181
|
-
|
|
10645
|
+
identity2,
|
|
9182
10646
|
appVersion,
|
|
9183
10647
|
adapter,
|
|
9184
10648
|
options.logger,
|
|
@@ -9212,6 +10676,10 @@ export {
|
|
|
9212
10676
|
PEER_CHAT_TIMEOUT_MS,
|
|
9213
10677
|
PeerChatRequestSchema,
|
|
9214
10678
|
PeerChatResultSchema,
|
|
10679
|
+
SessionImageRelayUrlSchema,
|
|
10680
|
+
SessionImageUploadError,
|
|
10681
|
+
SessionImageUploader,
|
|
10682
|
+
SessionImagesConfigSchema,
|
|
9215
10683
|
assertPiRuntimeSupported,
|
|
9216
10684
|
buildPairingUri,
|
|
9217
10685
|
codingAgentPresentation,
|