@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/index.js CHANGED
@@ -4,12 +4,12 @@ import {
4
4
  PeerChatRequestSchema,
5
5
  PeerChatResultSchema,
6
6
  validateDataDirectory
7
- } from "./chunks/chunk-IXV2BIUR.js";
7
+ } from "./chunks/chunk-UE4QYZCG.js";
8
8
  import {
9
9
  AgentSelectionSchema,
10
10
  assertSelectionSnapshotFits,
11
11
  supportsAgentSelection
12
- } from "./chunks/chunk-U2HBI4SH.js";
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-7JRY2FRZ.js";
34
+ } from "./chunks/chunk-QGPX4UDW.js";
34
35
  import {
35
36
  GROUP_CHAT_MAX_TURNS,
36
37
  GroupChatError
37
- } from "./chunks/chunk-VXZ25DKC.js";
38
+ } from "./chunks/chunk-VCNYBWYE.js";
38
39
  import {
39
40
  assistantPreview,
40
41
  isContentfulAssistant,
41
42
  userPreview
42
- } from "./chunks/chunk-QLGX7333.js";
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-UJZPAP64.js";
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-N2LIXMIM.js";
226
+ } from "./chunks/chunk-E3XCZWRK.js";
201
227
 
202
228
  // src/initialize.ts
203
- import { createHash as createHash13 } from "node:crypto";
229
+ import { createHash as createHash16 } from "node:crypto";
204
230
  import { readFile as readFile4 } from "node:fs/promises";
205
- import { z as z10 } from "zod";
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, environment = process.env) {
240
- const extensions = process.platform === "win32" ? ["", ...(environment.PATHEXT ?? ".EXE;.CMD;.BAT").split(";")] : [""];
241
- const paths = isAbsolute(command) || command.includes("/") || command.includes("\\") ? [command] : (environment.PATH ?? "").split(delimiter).filter(Boolean).map((directory) => join(directory, 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-4W4HC67T.js");
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-KM6RMGFR.js");
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-IFNN3F4N.js");
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-QK4MVTNN.js");
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 = z.object({
383
- schemaVersion: z.literal(1),
384
- key: z.string().regex(CONTENT_KEY_PATTERN)
420
+ var ContentKeyFileSchema = z2.object({
421
+ schemaVersion: z2.literal(1),
422
+ key: z2.string().regex(CONTENT_KEY_PATTERN)
385
423
  }).strict();
386
- var ReplayJournalEntrySchema = z.object({
387
- schemaVersion: z.literal(1),
388
- keyId: z.string().regex(/^[A-Za-z0-9_-]{22}$/u),
389
- messageId: z.string().trim().min(1).max(256),
390
- sentAt: z.number().int().nonnegative()
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, decode) {
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 = decode(plaintext);
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 z2 } from "zod";
702
- var DeviceIdentitySchema = z2.object({
703
- schemaVersion: z2.literal(2),
704
- machineId: z2.uuid(),
705
- stableDeviceId: z2.uuid(),
706
- machineName: z2.string().trim().min(1).max(120),
707
- createdAt: z2.iso.datetime({ offset: true })
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, identity, contentKey) {
798
+ async function requestPairingUri(config, identity2, contentKey) {
761
799
  const payload = {
762
800
  userId: config.userId,
763
- machineId: identity.machineId,
764
- stableDeviceId: identity.stableDeviceId,
765
- machineName: identity.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
- identity.machineId,
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 z4 } from "zod";
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
- return SessionFileReadResultDataSchema.parse(await this.#run(
1199
- (checkDeadline) => this.#read(sessionId, path, rootRevision, checkDeadline, false)
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 samePath = canonical === current || resource && process.platform === "win32" && canonical.toLowerCase() === current.toLowerCase();
1267
- if (!contained(root, canonical) || !samePath) fail2("FILE_UNAVAILABLE");
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 mediaType = resource ? resourceMediaType(extension) : image ? "image/webp" : textMediaType(path);
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 = z4.string().max(4096).default("");
1777
- var listSchema = z4.object({
1970
+ var pathSchema = z5.string().max(4096).default("");
1971
+ var listSchema = z5.object({
1778
1972
  path: pathSchema,
1779
- cursor: z4.string().uuid().optional()
1973
+ cursor: z5.string().uuid().optional()
1780
1974
  }).strict();
1781
- var readSchema = z4.object({
1782
- path: z4.string().min(1).max(4096),
1783
- startLine: z4.number().int().min(1).max(500001).default(1),
1784
- lineCount: z4.number().int().min(1).max(200).default(200)
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 = z4.object({
1980
+ var searchSchema = z5.object({
1787
1981
  path: pathSchema,
1788
- query: z4.string().min(1).max(256),
1789
- cursor: z4.string().uuid().optional()
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: z4.toJSONSchema(schema),
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 z4.ZodError ? "RESEARCH_INVALID_ARGUMENT" : "RESEARCH_TOOL_FAILED";
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 randomUUID8 } from "node:crypto";
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 createHash5 } from "node:crypto";
2218
- import { join as join6 } from "node:path";
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, limit = 256) {
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 = 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 identity = (reason) => ({
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 identity("disabled");
2512
+ if (options.enabled !== true) return identity2("disabled");
2319
2513
  if (!options.acceptContentEncodings?.includes("gzip-v1")) {
2320
- return identity("not-negotiated");
2514
+ return identity2("not-negotiated");
2321
2515
  }
2322
- if (plaintext.length < CONTENT_ENCODING_MIN_INPUT_BYTES) return identity("too-small");
2323
- if (activeCompressionJobs >= MAX_COMPRESSION_JOBS) return identity("busy");
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((resolve4, reject) => {
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 resolve4(result);
2523
+ else resolve5(result);
2330
2524
  });
2331
2525
  });
2332
2526
  if (compressed.length > CONTENT_ENCODING_MAX_GZIP_BYTES) {
2333
- return identity("gzip-too-large");
2527
+ return identity2("gzip-too-large");
2334
2528
  }
2335
2529
  if (plaintext.length > compressed.length * CONTENT_ENCODING_MAX_RATIO) {
2336
- return identity("ratio-limit");
2530
+ return identity2("ratio-limit");
2337
2531
  }
2338
2532
  if ((compressed.length + CONTENT_ENCODING_HEADER_BYTES) * 100 > plaintext.length * 85) {
2339
- return identity("insufficient-gain");
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: SESSION_FILE_READ_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: join6(config.dataDirectory, "workbench"),
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
- const { adapter, config, machineId, research, publications, audience, emit, logger, metrics } = this.options;
2694
- if (!this.options.enabled()) return;
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) => createHash5("sha256").update(JSON.stringify(parts)).digest("hex").slice(0, 24);
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
- const unifiedSync = research?.chat !== void 0 && this.options.supportsResearchChat?.(clientId) === true && (request.operation === TIMELINE_SYNC_OPERATION || request.operation === TIMELINE_IMAGE_OPERATION);
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
- const { data: _data, kind: _kind, ...identity } = request;
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
- ...identity,
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
- const diagnostics = new OperationDiagnostics();
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 (!scope.isCurrent()) return;
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 createHash6 } from "node:crypto";
2856
- import { readdir, realpath as realpath4, stat as stat5 } from "node:fs/promises";
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 join7, normalize } from "node:path";
2859
- function desktopRootCandidates(environment = process.env) {
4249
+ import { basename as basename2, join as join8, normalize } from "node:path";
4250
+ function desktopRootCandidates(environment2 = process.env) {
2860
4251
  const candidates = [
2861
- environment.AGENTKNIGHT_WORKSPACE_ROOT,
2862
- environment.OneDrive === void 0 ? void 0 : join7(environment.OneDrive, "Desktop"),
2863
- environment.OneDriveCommercial === void 0 ? void 0 : join7(environment.OneDriveCommercial, "Desktop"),
2864
- environment.OneDriveConsumer === void 0 ? void 0 : join7(environment.OneDriveConsumer, "Desktop"),
2865
- join7(environment.USERPROFILE ?? homedir2(), "Desktop")
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 createHash6("sha256").update(normalizedPath).digest("base64url").slice(0, 32);
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 realpath4(candidate);
2884
- if ((await stat5(resolved)).isDirectory()) {
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 = join7(root, entry.name);
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 realpath4(candidate);
2927
- if (!(await stat5(resolved)).isDirectory()) {
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 randomUUID3 } from "node:crypto";
2976
- import { lstat as lstat4, mkdir as mkdir2, realpath as realpath5, rmdir } from "node:fs/promises";
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 isAbsolute5, join as join8, relative as relative2 } from "node:path";
2979
- import { promisify as promisify2 } from "node:util";
2980
- var execFileAsync = promisify2(execFile);
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 = relative2(root, path);
2989
- return difference === "" || !isAbsolute5(difference) && difference !== ".." && !difference.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`);
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 identity = await resolveGitProjectIdentity(sourceDirectory, env);
4396
+ const identity2 = await resolveGitProjectIdentity(sourceDirectory, env);
3006
4397
  const output2 = await git(
3007
4398
  env,
3008
4399
  "-C",
3009
- identity.worktreeRoot,
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", identity.worktreeRoot, "symbolic-ref", "--quiet", "HEAD");
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 identity = await resolveGitProjectIdentity(sourceDirectory, env);
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", identity.worktreeRoot, "rev-parse", "--verify", `${baseRef}^{commit}`);
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 = randomUUID3();
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", identity.worktreeRoot, "check-ref-format", "--branch", branchName) !== branchName) {
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", identity.worktreeRoot, "show-ref", "--verify", "--quiet", `refs/heads/${branchName}`);
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 realpath5(identity.mainWorktreeRoot ?? identity.worktreeRoot);
3071
- const parent = dirname(primary);
3072
- if (inside(identity.worktreeRoot, parent)) {
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 = join8(parent, `agentknight-worktree-${suffix}`);
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 mkdir2(path, { mode: 448 });
3080
- const reserved = await lstat4(path);
4470
+ await mkdir3(path, { mode: 448 });
4471
+ const reserved = await lstat5(path);
3081
4472
  try {
3082
- await git(env, "-C", identity.worktreeRoot, "worktree", "add", "-b", branchName, "--", path, head);
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 lstat4(path);
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((environment2) => {
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 environment2;
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 environment = await sessionEnvironment;
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
- ...environment === void 0 ? {} : { environment },
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 resolve4 = () => {
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, fail3) => {
3356
- resolve4 = accept;
3357
- reject = fail3;
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
- resolve4(options.execute());
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 createHash7 } from "node:crypto";
5024
+ import { createHash as createHash10 } from "node:crypto";
3634
5025
  function promptOperationFingerprint(request) {
3635
- return createHash7("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 }) => ({
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 randomUUID4 } from "node:crypto";
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 z5 } from "zod";
3882
- var TextSchema = z5.string().max(16e3).refine((text) => text.trim().length > 0);
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 = randomUUID4();
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 createHash8, randomUUID as randomUUID5 } from "node:crypto";
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 z6 } from "zod";
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 = z6.string().min(1).max(256).regex(/^[^\s\u0000-\u001f\u007f]+$/u);
4077
- var optionalIdSchema = z6.union([z6.literal(""), idSchema]).optional();
4078
- var historySchema = z6.number().int().min(0).max(128).optional();
4079
- var emptyMetadataSchema = z6.object({}).strict().optional();
4080
- var tenantSchema = z6.literal("").optional();
4081
- var rpcSchema = z6.object({
4082
- jsonrpc: z6.literal("2.0"),
4083
- id: z6.union([idSchema, z6.number().int().safe(), z6.null()]),
4084
- method: z6.string().min(1).max(128),
4085
- params: z6.unknown()
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 = z6.object({
5478
+ var sendSchema = z8.object({
4088
5479
  tenant: tenantSchema,
4089
- message: z6.object({
5480
+ message: z8.object({
4090
5481
  messageId: idSchema,
4091
5482
  contextId: optionalIdSchema,
4092
5483
  taskId: optionalIdSchema,
4093
- role: z6.union([z6.literal("ROLE_USER"), z6.literal(Role2.ROLE_USER)]),
4094
- parts: z6.array(z6.object({
4095
- text: z6.string().max(MAX_TEXT2),
4096
- mediaType: z6.union([z6.literal(""), z6.literal("text/plain")]).optional(),
4097
- filename: z6.literal("").optional(),
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: z6.array(z6.never()).optional(),
4102
- referenceTaskIds: z6.array(z6.never()).optional()
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: z6.object({
4108
- acceptedOutputModes: z6.array(z6.literal("text/plain")).max(1).optional(),
4109
- returnImmediately: z6.boolean().optional(),
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 = z6.object({ tenant: tenantSchema, id: idSchema, historyLength: historySchema }).strict();
4115
- var cancelSchema = z6.object({ tenant: tenantSchema, id: idSchema, metadata: emptyMetadataSchema }).strict();
4116
- var listSchema2 = z6.object({
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: z6.union([
4120
- z6.number().int().min(0).max(8),
4121
- z6.enum([
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: z6.number().int().min(1).max(100).optional(),
4134
- pageToken: z6.string().regex(/^(?:|0|[1-9]\d{0,3})$/u).optional(),
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: z6.iso.datetime({ offset: true }).optional(),
4137
- includeArtifacts: z6.boolean().optional()
5527
+ statusTimestampAfter: z8.iso.datetime({ offset: true }).optional(),
5528
+ includeArtifacts: z8.boolean().optional()
4138
5529
  }).strict();
4139
5530
  function deferred() {
4140
- let resolve4 = () => {
5531
+ let resolve5 = () => {
4141
5532
  };
4142
5533
  const promise = new Promise((done) => {
4143
- resolve4 = done;
5534
+ resolve5 = done;
4144
5535
  });
4145
- return { promise, resolve: resolve4 };
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 = createHash8("sha256").update(JSON.stringify(SendMessageRequest.toJSON(params))).digest("hex");
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(randomUUID5(), message.contextId || randomUUID5(), message);
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: randomUUID5(),
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 randomUUID6 } from "node:crypto";
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: randomUUID6(),
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 resolve4;
6234
+ let resolve5;
4844
6235
  let reject;
4845
6236
  const promise = new Promise((onResolve, onReject) => {
4846
- resolve4 = onResolve;
6237
+ resolve5 = onResolve;
4847
6238
  reject = onReject;
4848
6239
  });
4849
- this.#pending.set(recipientClientId, { promise, resolve: resolve4, reject, requireFreshRead });
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 createHash9 } from "node:crypto";
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 digest = createHash9("sha256").update(JSON.stringify(content)).digest("base64url");
6638
+ const digest2 = createHash12("sha256").update(JSON.stringify(content)).digest("base64url");
5248
6639
  if (recipient !== void 0) {
5249
- await this.#direct(snapshot, recipient, digest, generation);
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 === digest) {
6652
+ if (baseline.digest === digest2) {
5262
6653
  this.#metrics.unchangedRecipients += 1;
5263
6654
  } else {
5264
- pending.push(this.#direct(snapshot, clientId, digest, generation));
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, digest, generation) {
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 = 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 createHash11, randomUUID as randomUUID7 } from "node:crypto";
6770
+ import { createHash as createHash13, randomUUID as randomUUID8 } from "node:crypto";
5380
6771
  import { isDeepStrictEqual } from "node:util";
5381
-
5382
- // src/timeline-image-cache.ts
5383
- import { createHash as createHash10 } from "node:crypto";
5384
- var TimelineAssetError = class extends Error {
5385
- constructor(code, message) {
5386
- super(message);
5387
- this.code = code;
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 base = previous?.item;
5572
- if (previous === void 0 || base?.kind !== "activity-group" || base.turnId !== upsert.turnId || base.actor !== upsert.actor) {
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 < base.revision) return;
5581
- const existingStep = base.steps.find((step) => step.id === upsert.step.id);
5582
- if (upsert.revision === base.revision && upsert.status === base.status && base.sourceEventIds.includes(upsert.sourceEventId) && isDeepStrictEqual(existingStep, upsert.step)) return;
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
- ...base,
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: [...base.steps.filter((step) => step.id !== upsert.step.id), upsert.step].slice(-CONVERSATION_ACTIVITY_STEP_LIMIT),
5589
- sourceEventIds: base.sourceEventIds.includes(upsert.sourceEventId) ? base.sourceEventIds : [...base.sourceEventIds, upsert.sourceEventId].slice(-CONVERSATION_SOURCE_ID_LIMIT)
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(base, updated)) return;
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 = createHash11("sha256").update(JSON.stringify([upsert.activityId, upsert.step.id])).digest("hex");
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 = createHash11("sha256").update(JSON.stringify(upsert)).digest("hex");
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
- if (this.#sessions.get(sessionId) !== state || state.sequence !== sequence2 || state.coldSteps.version !== coldVersion) continue;
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) continue;
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
- const resync = (reason) => ({ status: "resync", epoch: state.epoch, reason });
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: randomUUID7(),
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
- if (this.#sessions.get(sessionId) !== state || state.sequence !== sequence || state.coldSteps.version !== coldVersion) continue;
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((resolve4) => {
7124
+ await new Promise((resolve5) => {
5789
7125
  const wake = () => {
5790
7126
  clearTimeout(timer);
5791
7127
  state.wake.delete(wake);
5792
- resolve4();
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, identity, appVersion, adapter, logger, pairingProvider, contentCipher, research, createSocket = (uri, options) => io(uri, options), onPairing) {
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 = 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((resolve4) => {
6369
- this.#resolveStopped = resolve4;
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: identity.machineId,
6389
- machineName: identity.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: identity.machineId, logger, emit };
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, identity.machineId, this.#clientId),
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: identity.machineId,
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: identity.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: identity.machineId,
7966
+ machineId: identity2.machineId,
6522
7967
  reportError: reportPeerError
6523
7968
  });
6524
7969
  this.#peerChat = new PeerChatRuntime({
6525
- machineId: identity.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, identity.machineId)).some((session) => session.sessionId === sessionId)) {
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: randomUUID8(),
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.#emitSecure("interaction.respond.result", payload, void 0, true);
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 = randomUUID8();
6751
- #clientId = randomUUID8();
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((resolve4, reject) => {
6869
- const timer = setTimeout(() => fail3(new Error("Peer forwarding acknowledgement timed out; delivery is unknown.")), ACK_TIMEOUT_MS);
6870
- const fail3 = (error) => {
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(fail3);
8321
+ this.#pendingAcks.delete(fail4);
6873
8322
  reject(error);
6874
8323
  };
6875
- this.#pendingAcks.add(fail3);
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(fail3)) return;
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 resolve4();
8332
+ else resolve5();
6884
8333
  });
6885
8334
  } catch (error) {
6886
- fail3(error instanceof Error ? error : new Error(String(error)));
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((resolve4, reject) => {
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
- fail3(new Error(`${event} acknowledgement timed out`));
9207
+ fail4(new Error(`${event} acknowledgement timed out`));
7754
9208
  }, ACK_TIMEOUT_MS);
7755
- const fail3 = (error) => {
9209
+ const fail4 = (error) => {
7756
9210
  clearTimeout(timer);
7757
- this.#pendingAcks.delete(fail3);
9211
+ this.#pendingAcks.delete(fail4);
7758
9212
  reject(error);
7759
9213
  };
7760
- this.#pendingAcks.add(fail3);
9214
+ this.#pendingAcks.add(fail4);
7761
9215
  const receiveAck = (ack) => {
7762
9216
  clearTimeout(timer);
7763
- this.#pendingAcks.delete(fail3);
7764
- resolve4(ack);
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(fail3);
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 identity = {
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 { ...identity, steps: item.steps, toolResultsUnavailable: true };
9334
+ if (item.kind === "activity-group") return { ...identity2, steps: item.steps, toolResultsUnavailable: true };
7881
9335
  return {
7882
- ...identity,
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 execFile2 } from "node:child_process";
7890
- import { createHash as createHash12, randomUUID as randomUUID11 } from "node:crypto";
7891
- import { mkdir as mkdir4, lstat as lstat7, realpath as realpath7 } from "node:fs/promises";
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 join11, resolve as resolve3 } from "node:path";
7894
- import { promisify as promisify3 } from "node:util";
7895
- import { z as z9 } from "zod";
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 randomUUID9 } from "node:crypto";
7899
- import { mkdir as mkdir3, readFile as readFile2, rename as rename2, rm, lstat as lstat5, realpath as realpath6, writeFile as writeFile2 } from "node:fs/promises";
7900
- import { join as join9, resolve as resolve2 } from "node:path";
7901
- import { z as z7 } from "zod";
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 = z7.object({
7906
- threadId: z7.string().uuid(),
7907
- researchSessionId: z7.string().uuid(),
7908
- parentSessionId: z7.string().min(1).max(256),
7909
- operationId: z7.string().min(1).max(256),
7910
- fingerprint: z7.string().min(1).max(128),
7911
- title: z7.string().min(1).max(200),
7912
- profile: z7.literal(RESEARCH_PROFILE),
7913
- profileVersion: z7.literal(RESEARCH_PROFILE_VERSION),
7914
- workingDirectory: z7.string().min(1).max(8192),
7915
- rootIdentity: z7.string().min(1).max(128),
7916
- contextAnchor: z7.string().min(1).max(256),
7917
- contextSampledAt: z7.string().datetime(),
7918
- contextTruncated: z7.boolean(),
7919
- chatVersion: z7.literal(1).optional(),
7920
- createdAt: z7.string().datetime(),
7921
- state: z7.enum(["creating", "ready", "uncertain", "failed", "deleting"]),
7922
- errorCode: z7.string().max(128).optional()
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 = z7.object({
7925
- version: z7.literal(1),
7926
- threads: z7.array(recordSchema).max(MAX_THREADS),
7927
- tombstones: z7.array(z7.object({
7928
- researchSessionId: z7.string().uuid(),
7929
- parentSessionId: z7.string().min(1).max(256)
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 = resolve2(dataDirectory, "research");
7941
- this.#file = join9(this.directory, "threads.json");
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 mkdir3(this.directory, { recursive: true, mode: 448 });
7949
- if ((await lstat5(this.directory)).isSymbolicLink() || await realpath6(this.directory) !== this.directory) {
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 lstat5(this.#file);
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(records, tombstones = this.#tombstones) {
7975
- const staging = join9(this.directory, `threads-${randomUUID9()}.pending`);
9428
+ async #write(records2, tombstones = this.#tombstones) {
9429
+ const staging = join10(this.directory, `threads-${randomUUID10()}.pending`);
7976
9430
  try {
7977
- await writeFile2(staging, JSON.stringify({
9431
+ await writeFile3(staging, JSON.stringify({
7978
9432
  version: 1,
7979
- threads: [...records.values()],
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 = records;
9437
+ this.#records = records2;
7984
9438
  this.#tombstones = tombstones;
7985
9439
  } finally {
7986
- await rm(staging, { force: true });
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: randomUUID9(),
8009
- researchSessionId: randomUUID9(),
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 randomUUID10 } from "node:crypto";
8064
- import { lstat as lstat6, readFile as readFile3, rename as rename3, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
8065
- import { join as join10 } from "node:path";
8066
- import { z as z8 } from "zod";
8067
- var receipt = z8.object({
8068
- operationId: z8.string().min(1).max(256),
8069
- fingerprint: z8.string().min(1).max(128),
8070
- promptHash: z8.string().min(1).max(128),
8071
- ordinal: z8.number().int().nonnegative(),
8072
- submittedAt: z8.string().datetime(),
8073
- state: z8.enum(["sending", "accepted"]),
8074
- messageId: z8.string().min(1).max(256).optional(),
8075
- deliveryState: z8.enum(["queued", "processing"])
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 = z8.object({
8078
- version: z8.literal(1),
8079
- modelId: z8.string().min(1).max(256).optional(),
8080
- reasoningEffort: z8.enum(["low", "medium", "high", "xhigh", "max"]).optional(),
8081
- seed: z8.string().max(3e5).optional(),
8082
- seedState: z8.enum(["pending", "submitted", "none"]),
8083
- receipts: z8.array(receipt).max(1e4)
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 join10(this.directory, `chat-${z8.string().uuid().parse(sessionId)}.json`);
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 lstat6(path);
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}.${randomUUID10()}.pending`;
9574
+ const staging = `${path}.${randomUUID11()}.pending`;
8121
9575
  try {
8122
- await writeFile3(staging, JSON.stringify(validated), { flag: "wx", mode: 384 });
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 rm2(staging, { force: true });
9580
+ await rm3(staging, { force: true });
8127
9581
  }
8128
9582
  }
8129
9583
  async remove(sessionId) {
8130
- await rm2(this.#file(sessionId), { force: true });
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 = promisify3(execFile2);
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) => createHash12("sha256").update(value).digest("base64url");
8141
- var snapshotSchema = z9.object({
8142
- parentSessionId: z9.string().min(1).max(256),
8143
- anchorEventId: z9.string().min(1).max(256),
8144
- sampledAt: z9.string().datetime(),
8145
- contextTruncated: z9.boolean(),
8146
- text: z9.string().max(48e3),
8147
- workingDirectory: z9.string().min(1).max(8192),
8148
- rootIdentity: z9.string().min(1).max(128).optional()
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 environment = {};
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())) environment[key] = value;
9655
+ if (value !== void 0 && inherited.has(key.toUpperCase())) environment2[key] = value;
8202
9656
  }
8203
- Object.assign(environment, {
9657
+ Object.assign(environment2, {
8204
9658
  HOME: privateDirectory,
8205
9659
  USERPROFILE: privateDirectory,
8206
9660
  APPDATA: privateDirectory,
8207
9661
  LOCALAPPDATA: privateDirectory,
8208
- TEMP: join11(privateDirectory, "scratch"),
8209
- TMP: join11(privateDirectory, "scratch")
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: environment,
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 = join11(this.store.directory, "runtime");
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 = randomUUID11();
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 mkdir4(join11(this.#privateDirectory, "scratch"), { recursive: true, mode: 448 });
8363
- if ((await lstat7(this.#privateDirectory)).isSymbolicLink() || await realpath7(this.#privateDirectory) !== resolve3(this.#privateDirectory)) {
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
- [resolve3(this.options.dataDirectory), this.#privateDirectory],
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 = z9.string().trim().min(1).max(200).parse(input.title ?? "Research");
8551
- const prompt = z9.string().trim().min(1).max(16e3).parse(input.prompt ?? "Acknowledge the context briefly and wait for my research question.");
8552
- const fingerprint = createHash12("sha256").update(JSON.stringify({
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 = z9.enum(["low", "medium", "high", "xhigh", "max"]).safeParse(configuration?.reasoningEffort);
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 records = (await this.store.list(parentSessionId)).filter((record) => this.options.sourceVisible?.(record.parentSessionId) !== false);
8676
- for (const record of records) {
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 = z9.number().int().min(1).max(SESSION_TIMELINE_PAGE_SIZE).parse(options.pageSize ?? SESSION_TIMELINE_PAGE_SIZE);
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 = randomUUID11()) {
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 = z9.string().trim().min(1).max(16e3).parse(text);
8759
- z9.string().min(1).max(256).parse(operationId);
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 = z10.object({
9049
- dataDirectory: z10.string().min(1),
9050
- relayUrl: z10.url().refine((value) => ["https:", "http:"].includes(new URL(value).protocol)),
9051
- relayToken: z10.string().min(24),
9052
- userId: z10.string().trim().min(1).max(256),
9053
- workspaceRoot: z10.string().optional(),
9054
- refreshIntervalMs: z10.number().int().min(5e3).max(36e5).default(3e4),
9055
- heartbeatIntervalMs: z10.number().int().min(5e3).max(12e4).default(15e3),
9056
- exitAfterSnapshot: z10.boolean().default(false),
9057
- groupChatEnabled: z10.boolean().optional(),
9058
- peerChatEnabled: z10.boolean().optional(),
9059
- snapshotSuppressionEnabled: z10.boolean().optional(),
9060
- timelineCompressionEnabled: z10.boolean().optional(),
9061
- timelineIncrementalEnabled: z10.boolean().optional(),
9062
- secureMessagePacketBudget: z10.number().int().min(2048).max(SECURE_MESSAGE_MAX_CIPHERTEXT_LENGTH).optional()
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 identity = DeviceIdentitySchema.parse(options.identity);
9085
- const appVersion = options.appVersion ?? z10.object({ version: z10.string().min(1) }).parse(JSON.parse(await readFile4(new URL("../package.json", import.meta.url), "utf8"))).version;
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: identity.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, identity, contentKey));
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 = createHash13("sha256").update(`${config.userId}:${identity.machineId}:${contentKey.keyId}`).digest("hex");
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
- identity,
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,