@dev-anywhere/proxy 0.0.5 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/serve.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  KnownContentBlockSchema,
6
6
  SeqCounter,
7
7
  StreamJsonEventSchema
8
- } from "./chunk-2Q3Z3ICU.js";
8
+ } from "./chunk-JPJMOVQ5.js";
9
9
  import {
10
10
  createFSM,
11
11
  defineFSM,
@@ -14,7 +14,7 @@ import {
14
14
  serviceLogger,
15
15
  shouldReleaseApprovalWait,
16
16
  stateAfterApprovalRelease
17
- } from "./chunk-UGFYGF3Y.js";
17
+ } from "./chunk-WXWH6L7J.js";
18
18
  import {
19
19
  spawnScript
20
20
  } from "./chunk-ZUWAB67J.js";
@@ -27,6 +27,8 @@ import {
27
27
  CONFIG_PATH,
28
28
  ControlErrorCode,
29
29
  DATA_DIR,
30
+ HOOK_REGISTRY_PATH,
31
+ MessageEnvelopeSchema,
30
32
  PID_PATH,
31
33
  SESSIONS_PATH,
32
34
  SOCK_PATH,
@@ -39,11 +41,11 @@ import {
39
41
  serializeWorkerMsg,
40
42
  sessionPaths,
41
43
  tildify
42
- } from "./chunk-QJ5CQDK7.js";
44
+ } from "./chunk-QWPI6YON.js";
43
45
 
44
46
  // src/serve.ts
45
47
  import { createServer as createServer2 } from "net";
46
- import { unlinkSync as unlinkSync3, writeFileSync as writeFileSync4, chmodSync, rmSync as rmSync2 } from "fs";
48
+ import { unlinkSync as unlinkSync3, writeFileSync as writeFileSync6, chmodSync, rmSync as rmSync2 } from "fs";
47
49
 
48
50
  // src/serve/session-manager.ts
49
51
  import { mkdirSync, readFileSync, renameSync, writeFileSync, existsSync } from "fs";
@@ -181,6 +183,14 @@ var SessionManager = class {
181
183
  serviceLogger.info({ sessionId: id, from: oldState, to: newState }, "Session state changed");
182
184
  return true;
183
185
  }
186
+ touchSession(id, now = Date.now(), minIntervalMs = 0) {
187
+ const session = this.sessions.get(id);
188
+ if (!session) return false;
189
+ if (now - session.updatedAt < minIntervalMs) return false;
190
+ session.updatedAt = now;
191
+ this.save();
192
+ return true;
193
+ }
184
194
  terminateSession(id, context) {
185
195
  const session = this.sessions.get(id);
186
196
  if (!session) {
@@ -764,7 +774,7 @@ import { readdir as readdir2, mkdir } from "fs/promises";
764
774
  import { join as join4, isAbsolute as isAbsolute2, normalize } from "path";
765
775
 
766
776
  // src/serve/session-history.ts
767
- import { readdir, stat, access } from "fs/promises";
777
+ import { readdir, stat, access, open } from "fs/promises";
768
778
  import { createReadStream } from "fs";
769
779
  import { join as join2 } from "path";
770
780
  import { homedir as homedir2 } from "os";
@@ -864,49 +874,123 @@ async function scanCodexSessionHistory() {
864
874
  }
865
875
  return entries;
866
876
  }
867
- async function readSessionMessages(claudeSessionId) {
877
+ var DEFAULT_HISTORY_PAGE_LIMIT = 50;
878
+ var MAX_HISTORY_PAGE_LIMIT = 200;
879
+ var HISTORY_READ_CHUNK_BYTES = 64 * 1024;
880
+ var HISTORY_CURSOR_PREFIX = "b:";
881
+ function normalizeHistoryPageLimit(limit) {
882
+ if (typeof limit !== "number" || !Number.isFinite(limit)) return DEFAULT_HISTORY_PAGE_LIMIT;
883
+ return Math.max(1, Math.min(MAX_HISTORY_PAGE_LIMIT, Math.floor(limit)));
884
+ }
885
+ function encodeHistoryCursor(offset) {
886
+ return `${HISTORY_CURSOR_PREFIX}${Math.max(0, Math.floor(offset))}`;
887
+ }
888
+ function decodeHistoryCursor(cursor, fileSize) {
889
+ if (!cursor) return fileSize;
890
+ const raw = cursor.startsWith(HISTORY_CURSOR_PREFIX) ? cursor.slice(HISTORY_CURSOR_PREFIX.length) : cursor;
891
+ const parsed = Number(raw);
892
+ if (!Number.isInteger(parsed) || parsed < 0) return fileSize;
893
+ return Math.min(parsed, fileSize);
894
+ }
895
+ async function findClaudeSessionFile(claudeSessionId) {
868
896
  let projectDirs;
869
897
  try {
870
898
  projectDirs = await readdir(claudeProjectsDir());
871
899
  } catch {
872
- return [];
900
+ return null;
873
901
  }
874
902
  for (const encodedDir of projectDirs) {
875
903
  const filePath = join2(claudeProjectsDir(), encodedDir, `${claudeSessionId}.jsonl`);
876
904
  try {
877
905
  await access(filePath);
906
+ return filePath;
878
907
  } catch {
879
908
  continue;
880
909
  }
881
- const messages = [];
882
- return new Promise((resolve) => {
883
- const rl = createInterface({
884
- input: createReadStream(filePath, { encoding: "utf-8" }),
885
- crlfDelay: Infinity
886
- });
887
- rl.on("line", (line) => {
888
- if (!line.trim()) return;
910
+ }
911
+ return null;
912
+ }
913
+ function extractConversationMessageFromJson(obj) {
914
+ if (!obj || typeof obj !== "object") return null;
915
+ const record = obj;
916
+ if (record.type === "user") {
917
+ if (record.isMeta) return null;
918
+ const text = extractConversationText(record.message);
919
+ if (!text) return null;
920
+ const ts = typeof record.timestamp === "string" ? new Date(record.timestamp).getTime() : void 0;
921
+ return { role: "user", text, timestamp: ts };
922
+ }
923
+ if (record.type === "assistant") {
924
+ const text = extractConversationText(record.message);
925
+ if (!text) return null;
926
+ const ts = typeof record.timestamp === "string" ? new Date(record.timestamp).getTime() : void 0;
927
+ return { role: "assistant", text, timestamp: ts };
928
+ }
929
+ return null;
930
+ }
931
+ function splitLineSegments(block, blockStart) {
932
+ const segments = [];
933
+ let start = 0;
934
+ for (let i = 0; i < block.length; i += 1) {
935
+ if (block[i] !== 10) continue;
936
+ segments.push({ start: blockStart + start, line: block.subarray(start, i) });
937
+ start = i + 1;
938
+ }
939
+ segments.push({ start: blockStart + start, line: block.subarray(start) });
940
+ return segments;
941
+ }
942
+ function stripCarriageReturn(line) {
943
+ return line.length > 0 && line[line.length - 1] === 13 ? line.subarray(0, -1) : line;
944
+ }
945
+ async function readSessionMessagesPageFromFile(filePath, options = {}) {
946
+ const limit = normalizeHistoryPageLimit(options.limit);
947
+ const file = await open(filePath, "r");
948
+ try {
949
+ const fileStat = await file.stat();
950
+ const endOffset = decodeHistoryCursor(options.before, fileStat.size);
951
+ if (endOffset <= 0) return { messages: [], hasMore: false };
952
+ let position = endOffset;
953
+ let carry = Buffer.alloc(0);
954
+ const collected = [];
955
+ while (position > 0 && collected.length <= limit) {
956
+ const readSize = Math.min(HISTORY_READ_CHUNK_BYTES, position);
957
+ position -= readSize;
958
+ const chunk = Buffer.alloc(readSize);
959
+ await file.read(chunk, 0, readSize, position);
960
+ const block = carry.length > 0 ? Buffer.concat([chunk, carry]) : chunk;
961
+ const segments = splitLineSegments(block, position);
962
+ const firstCompleteIndex = position > 0 ? 1 : 0;
963
+ carry = position > 0 ? segments[0]?.line ?? Buffer.alloc(0) : Buffer.alloc(0);
964
+ for (let i = segments.length - 1; i >= firstCompleteIndex; i -= 1) {
965
+ const segment = segments[i];
966
+ if (!segment) continue;
967
+ const line = stripCarriageReturn(segment.line);
968
+ if (line.length === 0) continue;
889
969
  try {
890
- const obj = JSON.parse(line);
891
- if (obj.type === "user") {
892
- if (obj.isMeta) return;
893
- const text = extractConversationText(obj.message);
894
- if (!text) return;
895
- const ts = typeof obj.timestamp === "string" ? new Date(obj.timestamp).getTime() : void 0;
896
- messages.push({ role: "user", text, timestamp: ts });
897
- } else if (obj.type === "assistant") {
898
- const text = extractConversationText(obj.message);
899
- const ts = typeof obj.timestamp === "string" ? new Date(obj.timestamp).getTime() : void 0;
900
- if (text) messages.push({ role: "assistant", text, timestamp: ts });
901
- }
970
+ const parsed = JSON.parse(line.toString("utf-8"));
971
+ const message = extractConversationMessageFromJson(parsed);
972
+ if (!message) continue;
973
+ collected.push({ ...message, cursor: encodeHistoryCursor(segment.start) });
974
+ if (collected.length > limit) break;
902
975
  } catch {
903
976
  }
904
- });
905
- rl.on("close", () => resolve(messages));
906
- rl.on("error", () => resolve(messages));
907
- });
977
+ }
978
+ }
979
+ const page = collected.slice(0, limit).reverse();
980
+ const hasMore = collected.length > limit;
981
+ return {
982
+ messages: page,
983
+ hasMore,
984
+ ...hasMore && page[0]?.cursor ? { nextBefore: page[0].cursor } : {}
985
+ };
986
+ } finally {
987
+ await file.close();
908
988
  }
909
- return [];
989
+ }
990
+ async function readSessionMessagesPage(claudeSessionId, options = {}) {
991
+ const filePath = await findClaudeSessionFile(claudeSessionId);
992
+ if (!filePath) return { messages: [], hasMore: false };
993
+ return readSessionMessagesPageFromFile(filePath, options);
910
994
  }
911
995
  function collapseWhitespace(text) {
912
996
  return text.replace(/\s+/g, " ").trim();
@@ -1002,7 +1086,7 @@ function extractConversationText(msg) {
1002
1086
  return null;
1003
1087
  }
1004
1088
  async function extractTitleAndCwd(filePath) {
1005
- return new Promise((resolve) => {
1089
+ return new Promise((resolve2) => {
1006
1090
  const rl = createInterface({
1007
1091
  input: createReadStream(filePath, { encoding: "utf-8" }),
1008
1092
  crlfDelay: Infinity
@@ -1030,10 +1114,10 @@ async function extractTitleAndCwd(filePath) {
1030
1114
  }
1031
1115
  });
1032
1116
  rl.on("close", () => {
1033
- if (!resolved) resolve({ title, cwd });
1034
- else resolve({ title, cwd });
1117
+ if (!resolved) resolve2({ title, cwd });
1118
+ else resolve2({ title, cwd });
1035
1119
  });
1036
- rl.on("error", () => resolve({ title, cwd }));
1120
+ rl.on("error", () => resolve2({ title, cwd }));
1037
1121
  });
1038
1122
  }
1039
1123
  async function collectJsonlFiles(root) {
@@ -1055,7 +1139,7 @@ async function collectJsonlFiles(root) {
1055
1139
  return files;
1056
1140
  }
1057
1141
  async function extractCodexTitleAndCwd(filePath) {
1058
- return new Promise((resolve) => {
1142
+ return new Promise((resolve2) => {
1059
1143
  const rl = createInterface({
1060
1144
  input: createReadStream(filePath, { encoding: "utf-8" }),
1061
1145
  crlfDelay: Infinity
@@ -1079,8 +1163,8 @@ async function extractCodexTitleAndCwd(filePath) {
1079
1163
  } catch {
1080
1164
  }
1081
1165
  });
1082
- rl.on("close", () => resolve({ id, title, cwd }));
1083
- rl.on("error", () => resolve({ id, title, cwd }));
1166
+ rl.on("close", () => resolve2({ id, title, cwd }));
1167
+ rl.on("error", () => resolve2({ id, title, cwd }));
1084
1168
  });
1085
1169
  }
1086
1170
  function extractCodexUserText(payload) {
@@ -1645,16 +1729,16 @@ var WorkerRegistry = class {
1645
1729
  return workerPid;
1646
1730
  }
1647
1731
  connect(sessionId, sockPath) {
1648
- return new Promise((resolve) => {
1732
+ return new Promise((resolve2) => {
1649
1733
  const sock = connect(sockPath);
1650
1734
  sock.on("connect", () => {
1651
1735
  this.sockets.set(sessionId, sock);
1652
1736
  createWorkerReader(sock, (msg) => this.handleWorkerMessage(sessionId, msg));
1653
1737
  sock.on("close", () => this.onDisconnect(sessionId));
1654
1738
  sock.on("error", () => this.onDisconnect(sessionId));
1655
- resolve(sock);
1739
+ resolve2(sock);
1656
1740
  });
1657
- sock.on("error", () => resolve(null));
1741
+ sock.on("error", () => resolve2(null));
1658
1742
  });
1659
1743
  }
1660
1744
  // 枚举 DATA_DIR 下所有 session 目录,尝试连接存活的 worker.sock;失败则清理 stale socket。
@@ -1780,6 +1864,7 @@ var WorkerRegistry = class {
1780
1864
  return;
1781
1865
  }
1782
1866
  const ev = parsed.data;
1867
+ this.deps.touchSessionActivity?.(sessionId);
1783
1868
  const isStreamDeltaSession = this.streamDeltaSessions.has(sessionId);
1784
1869
  if (ev.type === "stream_event") {
1785
1870
  const delta = ContentBlockDeltaSchema.safeParse(ev.event);
@@ -1977,6 +2062,80 @@ function terminateSessionByOwnership(deps, sessionId) {
1977
2062
  return { success: false, action: "not_found" };
1978
2063
  }
1979
2064
 
2065
+ // src/serve/clipboard-image-upload.ts
2066
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
2067
+ import { isAbsolute as isAbsolute3, join as join5, relative, resolve } from "path";
2068
+ import { nanoid as nanoid3 } from "nanoid";
2069
+ var MAX_CLIPBOARD_IMAGE_BYTES = 10 * 1024 * 1024;
2070
+ var MAX_CLIPBOARD_IMAGE_BASE64_LENGTH = Math.ceil(MAX_CLIPBOARD_IMAGE_BYTES / 3) * 4;
2071
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Map([
2072
+ ["image/png", "png"],
2073
+ ["image/jpeg", "jpg"],
2074
+ ["image/webp", "webp"],
2075
+ ["image/gif", "gif"]
2076
+ ]);
2077
+ function formatTimestamp(ms) {
2078
+ const [date, time = "000000"] = new Date(ms).toISOString().replace(/\.\d{3}Z$/, "").split("T");
2079
+ return `${date.replace(/-/g, "")}-${time.replace(/:/g, "")}`;
2080
+ }
2081
+ function normalizeBase64(input) {
2082
+ return input.replace(/^data:[^;]+;base64,/i, "").replace(/\s/g, "");
2083
+ }
2084
+ function decodeBase64Image(dataBase64) {
2085
+ const normalized = normalizeBase64(dataBase64);
2086
+ if (normalized.length > MAX_CLIPBOARD_IMAGE_BASE64_LENGTH) {
2087
+ throw new Error("\u56FE\u7247\u8D85\u8FC7 10MB \u9650\u5236");
2088
+ }
2089
+ if (!normalized || !/^[A-Za-z0-9+/]*={0,2}$/.test(normalized)) {
2090
+ throw new Error("\u56FE\u7247\u6570\u636E\u4E0D\u662F\u6709\u6548\u7684 base64");
2091
+ }
2092
+ const buffer = Buffer.from(normalized, "base64");
2093
+ if (buffer.length === 0) throw new Error("\u56FE\u7247\u6570\u636E\u4E3A\u7A7A");
2094
+ if (buffer.length > MAX_CLIPBOARD_IMAGE_BYTES) {
2095
+ throw new Error("\u56FE\u7247\u8D85\u8FC7 10MB \u9650\u5236");
2096
+ }
2097
+ return buffer;
2098
+ }
2099
+ function resolveSessionClipboardDir(dataDir, sessionId) {
2100
+ const root = resolve(dataDir);
2101
+ const uploadDir = resolve(root, sessionId, "clipboard");
2102
+ const relativePath = relative(root, uploadDir);
2103
+ if (!relativePath || relativePath.startsWith("..") || isAbsolute3(relativePath)) {
2104
+ throw new Error("\u4F1A\u8BDD\u8DEF\u5F84\u65E0\u6548");
2105
+ }
2106
+ return uploadDir;
2107
+ }
2108
+ function saveClipboardImageUpload(request, options = {}) {
2109
+ const extension = IMAGE_EXTENSIONS.get(request.mimeType);
2110
+ if (!extension) {
2111
+ return {
2112
+ success: false,
2113
+ path: "",
2114
+ error: "\u4E0D\u652F\u6301\u8FD9\u79CD\u56FE\u7247\u683C\u5F0F",
2115
+ errorCode: ControlErrorCode.UNKNOWN
2116
+ };
2117
+ }
2118
+ try {
2119
+ const dataDir = options.dataDir ?? DATA_DIR;
2120
+ const uploadDir = resolveSessionClipboardDir(dataDir, request.sessionId);
2121
+ const buffer = decodeBase64Image(request.dataBase64);
2122
+ const now = options.now ?? Date.now;
2123
+ const suffix = options.randomSuffix?.() ?? nanoid3(6);
2124
+ const fileName = `pasted-${formatTimestamp(now())}-${suffix}.${extension}`;
2125
+ const path = join5(uploadDir, fileName);
2126
+ mkdirSync4(uploadDir, { recursive: true });
2127
+ writeFileSync4(path, buffer, { mode: 384 });
2128
+ return { success: true, path };
2129
+ } catch (err) {
2130
+ return {
2131
+ success: false,
2132
+ path: "",
2133
+ error: err instanceof Error ? err.message : String(err),
2134
+ errorCode: ControlErrorCode.UNKNOWN
2135
+ };
2136
+ }
2137
+ }
2138
+
1980
2139
  // src/serve/pty-input.ts
1981
2140
  function serializeRawPtyInput(sessionId, data) {
1982
2141
  return serializeIpc({ type: "pty_input", sessionId, data });
@@ -2008,6 +2167,21 @@ var RelayInputHandlers = class {
2008
2167
  serviceLogger.warn({ sessionId }, "Remote input dropped: JSON worker socket not available");
2009
2168
  return;
2010
2169
  }
2170
+ const timestamp = typeof msg.timestamp === "number" && Number.isFinite(msg.timestamp) ? msg.timestamp : Date.now();
2171
+ const seq = typeof msg.seq === "number" && Number.isInteger(msg.seq) && msg.seq >= 0 ? msg.seq : 0;
2172
+ const version = typeof msg.version === "string" ? msg.version : "1";
2173
+ const messageId = typeof payload?.messageId === "string" && payload.messageId.length > 0 ? payload.messageId : `${sessionId}-user-${timestamp}`;
2174
+ this.deps.relayConnection.sendEnvelope(
2175
+ MessageEnvelopeSchema.parse({
2176
+ type: "user_input",
2177
+ sessionId,
2178
+ seq,
2179
+ timestamp,
2180
+ source: "proxy",
2181
+ version,
2182
+ payload: { text, messageId }
2183
+ })
2184
+ );
2011
2185
  serviceLogger.info({ sessionId }, "Remote input forwarded to JSON worker");
2012
2186
  return;
2013
2187
  }
@@ -2035,6 +2209,42 @@ var RelayInputHandlers = class {
2035
2209
  ts.write(serializeRawPtyInput(sessionId, data));
2036
2210
  serviceLogger.info({ sessionId, bytes: data.length }, "Raw PTY input forwarded");
2037
2211
  }
2212
+ onClipboardImageUpload(msg) {
2213
+ const sessionId = msg.sessionId;
2214
+ const requestId = msg.requestId;
2215
+ if (!sessionId) return;
2216
+ const session = this.deps.sessionManager.getSession(sessionId);
2217
+ if (!session) {
2218
+ this.deps.relayConnection.sendRaw(
2219
+ JSON.stringify({
2220
+ type: "clipboard_image_upload_response",
2221
+ requestId,
2222
+ sessionId,
2223
+ success: false,
2224
+ path: "",
2225
+ error: "\u4F1A\u8BDD\u4E0D\u5B58\u5728",
2226
+ errorCode: ControlErrorCode.SESSION_NOT_FOUND
2227
+ })
2228
+ );
2229
+ serviceLogger.warn({ sessionId }, "Clipboard image upload rejected: session not found");
2230
+ return;
2231
+ }
2232
+ const result = saveClipboardImageUpload({
2233
+ sessionId,
2234
+ mimeType: typeof msg.mimeType === "string" ? msg.mimeType : "",
2235
+ dataBase64: typeof msg.dataBase64 === "string" ? msg.dataBase64 : "",
2236
+ fileName: typeof msg.fileName === "string" ? msg.fileName : void 0
2237
+ });
2238
+ this.deps.relayConnection.sendRaw(
2239
+ JSON.stringify({
2240
+ type: "clipboard_image_upload_response",
2241
+ requestId,
2242
+ sessionId,
2243
+ ...result
2244
+ })
2245
+ );
2246
+ serviceLogger.info({ sessionId, success: result.success }, "Clipboard image upload handled");
2247
+ }
2038
2248
  };
2039
2249
 
2040
2250
  // src/serve/relay-history-handlers.ts
@@ -2047,32 +2257,45 @@ var RelayHistoryHandlers = class {
2047
2257
  const sid = msg.sessionId;
2048
2258
  if (!sid) return;
2049
2259
  const requestId = msg.requestId;
2260
+ const before = msg.before;
2261
+ const limit = msg.limit;
2050
2262
  const session = this.deps.sessionManager.getSession(sid);
2051
2263
  if (session?.claudeSessionId) {
2052
- readSessionMessages(session.claudeSessionId).then((messages) => {
2264
+ readSessionMessagesPage(session.claudeSessionId, { before, limit }).then((page) => {
2053
2265
  this.deps.relaySend(
2054
2266
  JSON.stringify({
2055
2267
  type: "session_history_messages",
2056
2268
  requestId,
2057
2269
  sessionId: sid,
2058
- messages
2270
+ ...before !== void 0 ? { before } : {},
2271
+ messages: page.messages,
2272
+ hasMore: page.hasMore,
2273
+ ...page.nextBefore !== void 0 ? { nextBefore: page.nextBefore } : {}
2059
2274
  })
2060
2275
  );
2061
2276
  serviceLogger.info(
2062
- { sessionId: sid, messageCount: messages.length },
2063
- "History messages sent on request"
2277
+ {
2278
+ sessionId: sid,
2279
+ before,
2280
+ hasMore: page.hasMore,
2281
+ nextBefore: page.nextBefore,
2282
+ messageCount: page.messages.length
2283
+ },
2284
+ "History message page sent on request"
2064
2285
  );
2065
2286
  }).catch((err) => {
2066
2287
  serviceLogger.warn(
2067
2288
  { sessionId: sid, error: String(err) },
2068
- "Failed to read session history messages on request"
2289
+ "Failed to read session history page on request"
2069
2290
  );
2070
2291
  this.deps.relaySend(
2071
2292
  JSON.stringify({
2072
2293
  type: "session_history_messages",
2073
2294
  requestId,
2074
2295
  sessionId: sid,
2075
- messages: []
2296
+ ...before !== void 0 ? { before } : {},
2297
+ messages: [],
2298
+ hasMore: false
2076
2299
  })
2077
2300
  );
2078
2301
  });
@@ -2082,7 +2305,9 @@ var RelayHistoryHandlers = class {
2082
2305
  type: "session_history_messages",
2083
2306
  requestId,
2084
2307
  sessionId: sid,
2085
- messages: []
2308
+ ...before !== void 0 ? { before } : {},
2309
+ messages: [],
2310
+ hasMore: false
2086
2311
  })
2087
2312
  );
2088
2313
  }
@@ -2336,8 +2561,8 @@ var RelayResourceHandlers = class {
2336
2561
 
2337
2562
  // src/serve/relay-session-create-handler.ts
2338
2563
  import { rmSync, statSync as statSync2 } from "fs";
2339
- import { isAbsolute as isAbsolute3 } from "path";
2340
- import { nanoid as nanoid3 } from "nanoid";
2564
+ import { isAbsolute as isAbsolute4 } from "path";
2565
+ import { nanoid as nanoid4 } from "nanoid";
2341
2566
 
2342
2567
  // src/serve/hosted-pty-registry.ts
2343
2568
  import * as pty from "node-pty";
@@ -2489,6 +2714,7 @@ var HostedPtyRegistry = class {
2489
2714
  hosted.lastOutputTime = Date.now();
2490
2715
  hosted.outputSeq += 1;
2491
2716
  hosted.terminal.write(data);
2717
+ this.deps.touchSessionActivity(sessionId);
2492
2718
  this.sendBinary(sessionId, Buffer.from(data, "utf-8"), hosted.outputSeq);
2493
2719
  const oscSequences = extractOscSequences(data);
2494
2720
  const session = this.deps.sessionManager.getSession(sessionId);
@@ -2625,7 +2851,7 @@ function validateSessionCwd(cwd) {
2625
2851
  return { message: "\u8BF7\u8F93\u5165\u5DE5\u4F5C\u76EE\u5F55", code: ControlErrorCode.INVALID_PATH };
2626
2852
  }
2627
2853
  const trimmed = cwd.trim();
2628
- if (!isAbsolute3(trimmed)) {
2854
+ if (!isAbsolute4(trimmed)) {
2629
2855
  return { message: "\u5DE5\u4F5C\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84", code: ControlErrorCode.INVALID_PATH };
2630
2856
  }
2631
2857
  try {
@@ -2684,7 +2910,7 @@ var RelaySessionCreateHandler = class {
2684
2910
  const resumeSessionId = msg.resumeSessionId;
2685
2911
  const streamDelta = msg.streamDelta === true;
2686
2912
  const name = tildify(sessionCwd);
2687
- const pendingId = nanoid3();
2913
+ const pendingId = nanoid4();
2688
2914
  const hook = this.deps.createHookContext(pendingId, provider);
2689
2915
  const workerPid = this.deps.workerRegistry.spawn(pendingId, {
2690
2916
  cwd: sessionCwd,
@@ -2769,7 +2995,7 @@ var RelaySessionCreateHandler = class {
2769
2995
  return;
2770
2996
  }
2771
2997
  const resumeSessionId = msg.resumeSessionId;
2772
- const pendingId = nanoid3();
2998
+ const pendingId = nanoid4();
2773
2999
  const name = tildify(cwd);
2774
3000
  const hook = this.deps.createHookContext(pendingId, provider);
2775
3001
  try {
@@ -2834,19 +3060,30 @@ var RelaySessionCreateHandler = class {
2834
3060
  }
2835
3061
  }
2836
3062
  pushHistoryMessages(sessionId, resumeSessionId) {
2837
- readSessionMessages(resumeSessionId).then((messages) => {
2838
- if (messages.length === 0) return;
3063
+ readSessionMessagesPage(resumeSessionId).then((page) => {
3064
+ if (page.messages.length === 0) return;
2839
3065
  this.deps.relaySend(
2840
- JSON.stringify({ type: "session_history_messages", sessionId, messages })
3066
+ JSON.stringify({
3067
+ type: "session_history_messages",
3068
+ sessionId,
3069
+ messages: page.messages,
3070
+ hasMore: page.hasMore,
3071
+ ...page.nextBefore !== void 0 ? { nextBefore: page.nextBefore } : {}
3072
+ })
2841
3073
  );
2842
3074
  serviceLogger.info(
2843
- { sessionId, resumeSessionId, messageCount: messages.length },
2844
- "History messages sent for resumed session"
3075
+ {
3076
+ sessionId,
3077
+ resumeSessionId,
3078
+ messageCount: page.messages.length,
3079
+ hasMore: page.hasMore
3080
+ },
3081
+ "History message page sent for resumed session"
2845
3082
  );
2846
3083
  }).catch((err) => {
2847
3084
  serviceLogger.warn(
2848
3085
  { sessionId, error: String(err) },
2849
- "Failed to read session history messages"
3086
+ "Failed to read session history page"
2850
3087
  );
2851
3088
  });
2852
3089
  }
@@ -2864,6 +3101,7 @@ var RelayRouter = class {
2864
3101
  this.inputHandlers = new RelayInputHandlers({
2865
3102
  sessionManager: deps.sessionManager,
2866
3103
  workerRegistry: deps.workerRegistry,
3104
+ relayConnection: deps.relayConnection,
2867
3105
  terminalSockets: deps.terminalSockets,
2868
3106
  hostedPtyRegistry: deps.hostedPtyRegistry,
2869
3107
  jsonObserver: deps.jsonObserver
@@ -2924,6 +3162,7 @@ var RelayRouter = class {
2924
3162
  handlers = {
2925
3163
  user_input: (msg) => this.inputHandlers.onUserInput(msg),
2926
3164
  remote_input_raw: (msg) => this.inputHandlers.onRemoteInputRaw(msg),
3165
+ clipboard_image_upload: (msg) => this.inputHandlers.onClipboardImageUpload(msg),
2927
3166
  tool_approve: (msg) => this.permissionHandlers.onToolApprove(msg),
2928
3167
  tool_deny: (msg) => this.permissionHandlers.onToolDeny(msg),
2929
3168
  proxy_info_request: (msg) => this.resourceHandlers.onProxyInfoRequest(msg),
@@ -3114,11 +3353,11 @@ var PermissionBroker = class {
3114
3353
  message: "Duplicate permission request id."
3115
3354
  });
3116
3355
  }
3117
- return new Promise((resolve) => {
3356
+ return new Promise((resolve2) => {
3118
3357
  this.pending.set(request.requestId, {
3119
3358
  ...request,
3120
3359
  source: "hook",
3121
- resolve,
3360
+ resolve: resolve2,
3122
3361
  createdAt: Date.now()
3123
3362
  });
3124
3363
  });
@@ -3342,6 +3581,7 @@ var AgentStatusRegistry = class {
3342
3581
  };
3343
3582
 
3344
3583
  // src/serve/session-broadcast.ts
3584
+ var ACTIVITY_STATUS_PUSH_INTERVAL_MS = 15e3;
3345
3585
  function toSessionListPayload(s) {
3346
3586
  return {
3347
3587
  sessionId: s.id,
@@ -3404,6 +3644,11 @@ function changeSessionState(sessionManager, relay, sessionId, next) {
3404
3644
  if (changed) pushSessionStatus(relay, sessionManager, sessionId);
3405
3645
  return changed;
3406
3646
  }
3647
+ function touchSessionActivity(sessionManager, relay, sessionId, now = Date.now()) {
3648
+ const touched = sessionManager.touchSession(sessionId, now, ACTIVITY_STATUS_PUSH_INTERVAL_MS);
3649
+ if (touched) pushSessionStatus(relay, sessionManager, sessionId);
3650
+ return touched;
3651
+ }
3407
3652
 
3408
3653
  // src/serve/service-files.ts
3409
3654
  import { execSync } from "child_process";
@@ -3411,10 +3656,10 @@ import { existsSync as existsSync5, readFileSync as readFileSync5, unlinkSync as
3411
3656
  import { hostname } from "os";
3412
3657
  import { connect as connect2 } from "net";
3413
3658
  function tryConnectSocket(sockPath) {
3414
- return new Promise((resolve) => {
3659
+ return new Promise((resolve2) => {
3415
3660
  const s = connect2(sockPath);
3416
- s.on("connect", () => resolve(s));
3417
- s.on("error", () => resolve(null));
3661
+ s.on("connect", () => resolve2(s));
3662
+ s.on("error", () => resolve2(null));
3418
3663
  });
3419
3664
  }
3420
3665
  function isProcessAlive(pid) {
@@ -3758,6 +4003,7 @@ function handleTerminalConnection(socket, deps) {
3758
4003
  },
3759
4004
  (sessionId, data, outputSeq) => {
3760
4005
  if (!sessionManager.getSession(sessionId)) return;
4006
+ touchSessionActivity(sessionManager, relayConnection, sessionId);
3761
4007
  const sessionIdBuf = Buffer.from(sessionId, "utf-8");
3762
4008
  const wsFrame = Buffer.alloc(1 + sessionIdBuf.length + 4 + data.length);
3763
4009
  wsFrame[0] = sessionIdBuf.length;
@@ -3808,6 +4054,21 @@ function handleTerminalConnection(socket, deps) {
3808
4054
 
3809
4055
  // src/serve/hook-registry.ts
3810
4056
  import { createHash, randomBytes } from "crypto";
4057
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, renameSync as renameSync2, writeFileSync as writeFileSync5 } from "fs";
4058
+ import { dirname as dirname4 } from "path";
4059
+ import { z } from "zod";
4060
+ var PersistedHookSessionBindingSchema = z.object({
4061
+ sessionId: z.string(),
4062
+ provider: z.enum(["claude", "codex"]),
4063
+ marker: z.string(),
4064
+ tokenHash: z.string(),
4065
+ createdAt: z.number(),
4066
+ expiresAt: z.number().optional()
4067
+ });
4068
+ var PersistedHookRegistrySchema = z.object({
4069
+ version: z.literal(1),
4070
+ bindings: z.array(PersistedHookSessionBindingSchema)
4071
+ });
3811
4072
  function hashToken(token) {
3812
4073
  return createHash("sha256").update(token).digest("hex");
3813
4074
  }
@@ -3816,6 +4077,11 @@ function randomSecret() {
3816
4077
  }
3817
4078
  var HookRegistry = class {
3818
4079
  bindingsBySession = /* @__PURE__ */ new Map();
4080
+ persistPath;
4081
+ constructor(options = {}) {
4082
+ this.persistPath = options.persistPath;
4083
+ this.load();
4084
+ }
3819
4085
  registerSession(sessionId, provider, options = {}) {
3820
4086
  const now = options.now ?? Date.now();
3821
4087
  const token = randomSecret();
@@ -3828,6 +4094,7 @@ var HookRegistry = class {
3828
4094
  createdAt: now,
3829
4095
  ...options.ttlMs ? { expiresAt: now + options.ttlMs } : {}
3830
4096
  });
4097
+ this.save();
3831
4098
  return { sessionId, provider, marker, token };
3832
4099
  }
3833
4100
  verify(options) {
@@ -3843,7 +4110,50 @@ var HookRegistry = class {
3843
4110
  return this.bindingsBySession.get(sessionId) ?? null;
3844
4111
  }
3845
4112
  unregisterSession(sessionId) {
3846
- this.bindingsBySession.delete(sessionId);
4113
+ if (this.bindingsBySession.delete(sessionId)) {
4114
+ this.save();
4115
+ }
4116
+ }
4117
+ load() {
4118
+ if (!this.persistPath || !existsSync6(this.persistPath)) return;
4119
+ try {
4120
+ const parsed = PersistedHookRegistrySchema.parse(
4121
+ JSON.parse(readFileSync6(this.persistPath, "utf8"))
4122
+ );
4123
+ this.bindingsBySession.clear();
4124
+ for (const binding of parsed.bindings) {
4125
+ this.bindingsBySession.set(binding.sessionId, binding);
4126
+ }
4127
+ } catch (err) {
4128
+ serviceLogger.warn(
4129
+ { path: this.persistPath, error: String(err) },
4130
+ "Failed to load hook registry state"
4131
+ );
4132
+ }
4133
+ }
4134
+ save() {
4135
+ if (!this.persistPath) return;
4136
+ try {
4137
+ mkdirSync5(dirname4(this.persistPath), { recursive: true });
4138
+ const tmpPath = `${this.persistPath}.${process.pid}.${Date.now()}.tmp`;
4139
+ writeFileSync5(
4140
+ tmpPath,
4141
+ JSON.stringify(
4142
+ {
4143
+ version: 1,
4144
+ bindings: Array.from(this.bindingsBySession.values())
4145
+ },
4146
+ null,
4147
+ 2
4148
+ )
4149
+ );
4150
+ renameSync2(tmpPath, this.persistPath);
4151
+ } catch (err) {
4152
+ serviceLogger.warn(
4153
+ { path: this.persistPath, error: String(err) },
4154
+ "Failed to persist hook registry state"
4155
+ );
4156
+ }
3847
4157
  }
3848
4158
  };
3849
4159
 
@@ -3878,7 +4188,7 @@ var HookServer = class {
3878
4188
  this.writeJson(res, 500, { error: "internal_error" });
3879
4189
  });
3880
4190
  });
3881
- return new Promise((resolve, reject) => {
4191
+ return new Promise((resolve2, reject) => {
3882
4192
  const onError = (err) => {
3883
4193
  this.server?.off("listening", onListening);
3884
4194
  reject(err);
@@ -3886,7 +4196,7 @@ var HookServer = class {
3886
4196
  const onListening = () => {
3887
4197
  this.server?.off("error", onError);
3888
4198
  serviceLogger.info({ host: this.host, port: this.options.port }, "Hook server listening");
3889
- resolve();
4199
+ resolve2();
3890
4200
  };
3891
4201
  this.server.once("error", onError);
3892
4202
  this.server.once("listening", onListening);
@@ -3897,8 +4207,8 @@ var HookServer = class {
3897
4207
  if (!this.server) return Promise.resolve();
3898
4208
  const server = this.server;
3899
4209
  this.server = null;
3900
- return new Promise((resolve, reject) => {
3901
- server.close((err) => err ? reject(err) : resolve());
4210
+ return new Promise((resolve2, reject) => {
4211
+ server.close((err) => err ? reject(err) : resolve2());
3902
4212
  });
3903
4213
  }
3904
4214
  getListeningPort() {
@@ -4012,7 +4322,7 @@ var HookServer = class {
4012
4322
  this.writeJson(res, 200, payload);
4013
4323
  }
4014
4324
  readBody(req) {
4015
- return new Promise((resolve, reject) => {
4325
+ return new Promise((resolve2, reject) => {
4016
4326
  let body = "";
4017
4327
  let size = 0;
4018
4328
  req.setEncoding("utf8");
@@ -4025,7 +4335,7 @@ var HookServer = class {
4025
4335
  }
4026
4336
  body += chunk;
4027
4337
  });
4028
- req.on("end", () => resolve(body));
4338
+ req.on("end", () => resolve2(body));
4029
4339
  req.on("error", reject);
4030
4340
  });
4031
4341
  }
@@ -4038,7 +4348,7 @@ var HookServer = class {
4038
4348
 
4039
4349
  // src/serve/provider-hook-runtime.ts
4040
4350
  async function createProviderHookRuntime(options) {
4041
- const hookRegistry = new HookRegistry();
4351
+ const hookRegistry = new HookRegistry({ persistPath: HOOK_REGISTRY_PATH });
4042
4352
  const hookEventRouter = new HookEventRouter({
4043
4353
  relayConnection: options.relayConnection,
4044
4354
  agentStatusRegistry: options.agentStatusRegistry,
@@ -4209,6 +4519,7 @@ async function startService(options) {
4209
4519
  const relaySend = (data) => relayConnection.sendRaw(data);
4210
4520
  const controlHandlers = createControlMessageHandlers(relaySend, sessionManager);
4211
4521
  const observerChangeState = (sessionId, next) => changeSessionState(sessionManager, relayConnection, sessionId, next);
4522
+ const observerTouchActivity = (sessionId) => touchSessionActivity(sessionManager, relayConnection, sessionId);
4212
4523
  const emitAgentStatus = (sessionId, phase) => {
4213
4524
  const session = sessionManager.getSession(sessionId);
4214
4525
  if (!session) return;
@@ -4239,6 +4550,7 @@ async function startService(options) {
4239
4550
  permissionBroker,
4240
4551
  relayConnection,
4241
4552
  jsonObserver,
4553
+ touchSessionActivity: observerTouchActivity,
4242
4554
  getProviderEnv
4243
4555
  });
4244
4556
  const hostedPtyRegistry = new HostedPtyRegistry({
@@ -4246,6 +4558,7 @@ async function startService(options) {
4246
4558
  relayConnection,
4247
4559
  getProviderEnv,
4248
4560
  changeSessionState: observerChangeState,
4561
+ touchSessionActivity: observerTouchActivity,
4249
4562
  onTurnComplete: (sessionId) => {
4250
4563
  resolveInterruptedApprovals(
4251
4564
  permissionBroker,
@@ -4331,7 +4644,7 @@ async function startService(options) {
4331
4644
  });
4332
4645
  });
4333
4646
  server.listen(SOCK_PATH, () => {
4334
- writeFileSync4(PID_PATH, String(process.pid));
4647
+ writeFileSync6(PID_PATH, String(process.pid));
4335
4648
  chmodSync(SOCK_PATH, 384);
4336
4649
  serviceLogger.info({ pid: process.pid, sock: SOCK_PATH }, "Service started");
4337
4650
  });