laohuang 0.9.1 → 0.9.2

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.
Files changed (3) hide show
  1. package/dist/bin.js +1153 -241
  2. package/dist/bin.js.map +4 -4
  3. package/package.json +4 -1
package/dist/bin.js CHANGED
@@ -7369,8 +7369,8 @@ var require_dist = __commonJS({
7369
7369
  });
7370
7370
 
7371
7371
  // src/main.ts
7372
- import { existsSync as existsSync8, readFileSync as readFileSync6 } from "node:fs";
7373
- import { dirname as dirname13, join as join17 } from "node:path";
7372
+ import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
7373
+ import { dirname as dirname13, join as join20 } from "node:path";
7374
7374
  import { fileURLToPath } from "node:url";
7375
7375
 
7376
7376
  // ../../packages/session/session-context/dist/conversation-history.js
@@ -7415,13 +7415,14 @@ var DefaultTokenEstimator = class {
7415
7415
  };
7416
7416
  }
7417
7417
  estimateMessage(message) {
7418
+ const attachmentTokens = message.role === "user" || message.role === "tool-result" ? (message.attachments ?? []).reduce((sum, block, index) => sum + (block.type === "image" && !message.omittedImageIndexes?.includes(index) ? 85 + 170 * Math.ceil(block.ref.width / 512) * Math.ceil(block.ref.height / 512) : 32), 0) : 0;
7418
7419
  if (message.role === "system" && message.toolDefinitions !== void 0)
7419
7420
  return 0;
7420
7421
  if (message.role === "system" || message.role === "user") {
7421
- return MESSAGE_OVERHEAD + BLOCK_OVERHEAD + this.estimateText(message.content);
7422
+ return MESSAGE_OVERHEAD + BLOCK_OVERHEAD + this.estimateText(message.content) + attachmentTokens;
7422
7423
  }
7423
7424
  if (message.role === "tool-result") {
7424
- return MESSAGE_OVERHEAD + BLOCK_OVERHEAD + this.estimateText(message.content);
7425
+ return MESSAGE_OVERHEAD + BLOCK_OVERHEAD + this.estimateText(message.content) + attachmentTokens;
7425
7426
  }
7426
7427
  return MESSAGE_OVERHEAD + message.content.reduce((total, block) => {
7427
7428
  if (block.type === "text" || block.type === "reasoning") {
@@ -7489,7 +7490,7 @@ var ContextUsage = class {
7489
7490
  }
7490
7491
  /** Histories are append-only and ordered; process each committed entry once. */
7491
7492
  append(entry) {
7492
- if (entry.entryType === "compaction") {
7493
+ if (entry.entryType === "compaction" || entry.entryType === "image_offload") {
7493
7494
  this.#tokens = null;
7494
7495
  return;
7495
7496
  }
@@ -7549,14 +7550,14 @@ var ConversationHistory = class _ConversationHistory {
7549
7550
  }
7550
7551
  static fromReplay(replay, journal) {
7551
7552
  const history = new _ConversationHistory(replay.items.filter((item) => item.kind === "entry"), journal);
7552
- for (const open4 of replay.openToolCalls) {
7553
+ for (const open6 of replay.openToolCalls) {
7553
7554
  const entry = history.appendToolResults({
7554
7555
  requestId: "recovered",
7555
7556
  recovered: true,
7556
7557
  messages: [{
7557
7558
  role: "tool-result",
7558
- toolCallId: open4.toolCallId,
7559
- toolName: open4.toolName,
7559
+ toolCallId: open6.toolCallId,
7560
+ toolName: open6.toolName,
7560
7561
  content: JSON.stringify({
7561
7562
  ok: false,
7562
7563
  status: "interrupted",
@@ -7569,7 +7570,7 @@ var ConversationHistory = class _ConversationHistory {
7569
7570
  recordType: "error",
7570
7571
  payload: {
7571
7572
  type: "interrupted_tool_call",
7572
- toolCallId: open4.toolCallId,
7573
+ toolCallId: open6.toolCallId,
7573
7574
  repairedEntryId: entry?.id ?? null
7574
7575
  }
7575
7576
  });
@@ -7661,6 +7662,12 @@ var ConversationHistory = class _ConversationHistory {
7661
7662
  entries() {
7662
7663
  return [...this.#entries];
7663
7664
  }
7665
+ offloadImages(keys2) {
7666
+ if (keys2.length === 0)
7667
+ return;
7668
+ this.append({ entryType: "image_offload", payload: { keys: [...keys2] } });
7669
+ this.#journal.flush();
7670
+ }
7664
7671
  get contextTokens() {
7665
7672
  return this.#contextUsage.tokens;
7666
7673
  }
@@ -7728,6 +7735,14 @@ function clampReasoningEffort(supported, requested) {
7728
7735
  }
7729
7736
  return "off";
7730
7737
  }
7738
+ function projectImageOffloads(message, keys2) {
7739
+ if (message.role !== "user" && message.role !== "tool-result" || !message.attachments?.length)
7740
+ return message;
7741
+ const omittedImageIndexes = message.attachments.flatMap((block, index) => block.type === "image" && keys2.has(`${message.attachmentKey}:${index}`) ? [index] : []);
7742
+ if (!omittedImageIndexes.length)
7743
+ return message;
7744
+ return { ...message, omittedImageIndexes };
7745
+ }
7731
7746
  var ModelError = class extends ModelStreamError {
7732
7747
  kind;
7733
7748
  constructor(message, options) {
@@ -7813,6 +7828,7 @@ var ModelRuntime = class {
7813
7828
  const maxAttempts = validateMaxAttempts(request.maxAttempts ?? 3);
7814
7829
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
7815
7830
  const adapterRequest = {
7831
+ ...request.imageContext === void 0 ? {} : { imageContext: request.imageContext },
7816
7832
  provider: request.provider,
7817
7833
  model: request.model,
7818
7834
  ...request.baseUrl === void 0 ? {} : { baseUrl: request.baseUrl },
@@ -7921,7 +7937,7 @@ async function defaultSleep(delayMs, cancelToken) {
7921
7937
  throw new ModelStreamCancelled(cancelToken?.reason || "cancelled");
7922
7938
  }
7923
7939
  if (cancelToken === null) {
7924
- await new Promise((resolve14) => setTimeout(resolve14, delayMs));
7940
+ await new Promise((resolve15) => setTimeout(resolve15, delayMs));
7925
7941
  }
7926
7942
  }
7927
7943
  function errorMessage(error) {
@@ -7970,8 +7986,9 @@ ${activeCompaction.payload.summary}
7970
7986
  }
7971
7987
  }
7972
7988
  validateToolPairs(selected);
7989
+ const offloaded = new Set(sorted.flatMap((entry) => entry.entryType === "image_offload" ? entry.payload.keys : []));
7973
7990
  return {
7974
- messages: selected.map((item) => item.message),
7991
+ messages: selected.map((item) => projectImageOffloads(item.message, offloaded)),
7975
7992
  sourceEntryIds: selected.map((item) => item.entryId),
7976
7993
  activeCompactionId: activeCompaction?.id ?? null
7977
7994
  };
@@ -8084,12 +8101,15 @@ function selectCompactionPlan(input) {
8084
8101
  if (retained.length === 0 && units.length > 0) {
8085
8102
  retained.push(units.at(-1));
8086
8103
  }
8087
- const retainedFirst = retained[0]?.entries[0]?.seq ?? Number.MAX_SAFE_INTEGER;
8104
+ const currentTurn = input.protectedFromSeq ?? [...input.entries].reverse().find((entry) => entry.entryType === "user_message")?.seq ?? 0;
8105
+ const hasCurrentImages = input.entries.some((entry) => entry.seq >= currentTurn && "message" in entry.payload && "attachments" in entry.payload.message && entry.payload.message.attachments?.some((block) => block.type === "image"));
8106
+ const retainedFirst = Math.min(retained[0]?.entries[0]?.seq ?? Number.MAX_SAFE_INTEGER, hasCurrentImages ? currentTurn : Number.MAX_SAFE_INTEGER);
8107
+ const retainedUnits = units.filter((unit) => unit.entries[0].seq >= retainedFirst);
8088
8108
  const summarized = units.filter((unit) => unit.entries[0].seq < retainedFirst);
8089
8109
  return {
8090
8110
  summarizedEntries: summarized.flatMap((unit) => unit.entries),
8091
- retainedEntries: retained.flatMap((unit) => unit.entries),
8092
- retainedFromSeq: retained[0]?.entries[0]?.seq ?? 1
8111
+ retainedEntries: retainedUnits.flatMap((unit) => unit.entries),
8112
+ retainedFromSeq: retainedUnits[0]?.entries[0]?.seq ?? 1
8093
8113
  };
8094
8114
  }
8095
8115
  function serializeConversation(entries) {
@@ -8111,6 +8131,10 @@ function serializeConversation(entries) {
8111
8131
  } else if (entry.entryType === "user_message" || entry.entryType === "skill_context" || entry.entryType === "reminder" || entry.entryType === "project_instructions") {
8112
8132
  lines2.push(`[${entry.payload.message.role}] ${entry.payload.message.content}`);
8113
8133
  }
8134
+ if ("message" in entry.payload && "attachments" in entry.payload.message) {
8135
+ for (const block of entry.payload.message.attachments ?? [])
8136
+ lines2.push(`[attachment metadata only; not visual interpretation] ${JSON.stringify(block)}`);
8137
+ }
8114
8138
  lines2.push("</entry>");
8115
8139
  }
8116
8140
  lines2.push("</conversation>");
@@ -8123,7 +8147,7 @@ function semanticUnits(entries) {
8123
8147
  const entry = entries[index];
8124
8148
  if (groupedDefinitions.has(entry.id))
8125
8149
  continue;
8126
- if (entry.entryType === "system_context" || entry.entryType === "project_instructions" || entry.entryType === "compaction") {
8150
+ if (entry.entryType === "image_offload" || entry.entryType === "system_context" || entry.entryType === "project_instructions" || entry.entryType === "compaction") {
8127
8151
  continue;
8128
8152
  }
8129
8153
  if (entry.entryType === "assistant_message") {
@@ -8208,6 +8232,7 @@ var ContextGovernor = class {
8208
8232
  async compact(input) {
8209
8233
  const calculated = calculateModelBudget({ budget: input.budget, policy: input.policy });
8210
8234
  const plan = selectCompactionPlan({
8235
+ protectedFromSeq: input.protectedFromSeq,
8211
8236
  entries: input.entries,
8212
8237
  // A search preflight reserves the pending call/result/definition unit.
8213
8238
  // Keep the latest completed unit and summarize older units to make room.
@@ -8282,6 +8307,66 @@ function calculateModelBudget(input) {
8282
8307
  return { hardInputLimit, safetyTokens, autoTrigger, retainTokens };
8283
8308
  }
8284
8309
 
8310
+ // ../../packages/attachment/attachment/dist/errors.js
8311
+ var AttachmentError = class extends Error {
8312
+ code;
8313
+ constructor(code, message, options) {
8314
+ super(message, options);
8315
+ this.name = "AttachmentError";
8316
+ this.code = code;
8317
+ }
8318
+ };
8319
+
8320
+ // ../../packages/attachment/attachment/dist/types.js
8321
+ var DEFAULT_IMAGE_LIMITS = Object.freeze({
8322
+ maxBytes: 20 * 1024 ** 2,
8323
+ maxImages: 20,
8324
+ maxBatchBytes: 200 * 1024 ** 2,
8325
+ maxPixels: 64e6,
8326
+ maxDimension: 8192
8327
+ });
8328
+ var RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
8329
+ var CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1e3;
8330
+ function assertAttachmentContent(value) {
8331
+ if (!value || typeof value !== "object")
8332
+ throw new AttachmentError("INVALID_REFERENCE", "Invalid attachment content");
8333
+ const block = value;
8334
+ if (block.type !== "image" && block.type !== "file")
8335
+ throw new AttachmentError("INVALID_REFERENCE", "Invalid attachment kind");
8336
+ assertFileRef(block.ref);
8337
+ if (block.type === "image") {
8338
+ const ref = block.ref;
8339
+ if (!["image/png", "image/jpeg", "image/webp", "image/gif"].includes(String(ref.mimeType)) || !Number.isSafeInteger(ref.width) || Number(ref.width) < 1 || !Number.isSafeInteger(ref.height) || Number(ref.height) < 1 || typeof ref.animated !== "boolean") {
8340
+ throw new AttachmentError("INVALID_REFERENCE", "Invalid image reference");
8341
+ }
8342
+ if (block.view !== void 0)
8343
+ assertImageView(block.view);
8344
+ }
8345
+ }
8346
+ function assertFileRef(value) {
8347
+ const ref = value;
8348
+ if (!ref || typeof ref !== "object" || typeof ref.id !== "string" || !/^sha256:[a-f0-9]{64}$/.test(ref.id) || !Number.isSafeInteger(ref.bytes) || Number(ref.bytes) < 0 || typeof ref.name !== "string") {
8349
+ throw new AttachmentError("INVALID_REFERENCE", "Invalid attachment reference");
8350
+ }
8351
+ }
8352
+ function assertImageView(value) {
8353
+ if (!value || typeof value !== "object" || Array.isArray(value))
8354
+ throw new AttachmentError("INVALID_REFERENCE", "Invalid image view");
8355
+ const view = value;
8356
+ if (view.fullResolution !== void 0 && typeof view.fullResolution !== "boolean")
8357
+ throw new AttachmentError("INVALID_REFERENCE", "Invalid fullResolution");
8358
+ if (view.region !== void 0) {
8359
+ const r = view.region;
8360
+ if (!r || typeof r !== "object" || !["x", "y", "width", "height"].every((k) => Number.isSafeInteger(r[k])) || Number(r.x) < 0 || Number(r.y) < 0 || Number(r.width) < 1 || Number(r.height) < 1) {
8361
+ throw new AttachmentError("INVALID_REFERENCE", "Invalid crop region");
8362
+ }
8363
+ }
8364
+ }
8365
+
8366
+ // ../../packages/attachment/attachment/dist/attachment-store.js
8367
+ var AttachmentStore = class {
8368
+ };
8369
+
8285
8370
  // ../../packages/session/session-store/dist/session-metadata.js
8286
8371
  function normalizeSessionTitle(input) {
8287
8372
  if (new RegExp("\\p{Cc}|\\u2028|\\u2029", "u").test(input)) {
@@ -8312,6 +8397,7 @@ function sessionTitleAt(items, throughSeq = Infinity) {
8312
8397
 
8313
8398
  // ../../packages/session/session-store/dist/schema.js
8314
8399
  var ENTRY_TYPES = /* @__PURE__ */ new Set([
8400
+ "image_offload",
8315
8401
  "skill_context",
8316
8402
  "tool_definitions",
8317
8403
  "tool_catalog",
@@ -8420,7 +8506,21 @@ function parseSessionItem(value, expectedSessionId) {
8420
8506
  if (!ENTRY_TYPES.has(entryType)) {
8421
8507
  throw new SessionSchemaError("invalid session entry type");
8422
8508
  }
8423
- objectOf(input["payload"], "entry payload");
8509
+ const entryPayload = objectOf(input["payload"], "entry payload");
8510
+ if (entryType === "image_offload" && (!Array.isArray(entryPayload.keys) || entryPayload.keys.some((key) => typeof key !== "string"))) {
8511
+ throw new SessionSchemaError("invalid image offload keys");
8512
+ }
8513
+ if (entryPayload.message !== void 0) {
8514
+ const message = objectOf(entryPayload.message, "message");
8515
+ if (message.attachments !== void 0) {
8516
+ if (message.role !== "user" && message.role !== "tool-result" || !Array.isArray(message.attachments))
8517
+ throw new SessionSchemaError("invalid message attachments");
8518
+ for (const attachment of message.attachments)
8519
+ assertAttachmentContent(attachment);
8520
+ if (typeof message.attachmentKey !== "string" || !message.attachmentKey)
8521
+ throw new SessionSchemaError("missing attachment occurrence key");
8522
+ }
8523
+ }
8424
8524
  if (entryType === "skill_context") {
8425
8525
  const payload = objectOf(input["payload"], "skill payload");
8426
8526
  const message = objectOf(payload["message"], "skill message");
@@ -8516,35 +8616,9 @@ function requirePositiveInteger(input, key) {
8516
8616
  return value;
8517
8617
  }
8518
8618
 
8519
- // ../../packages/session/session-store/dist/session-paths.js
8520
- import { createHash as createHash2 } from "node:crypto";
8521
- import { mkdirSync, realpathSync } from "node:fs";
8522
- import { basename, join, resolve } from "node:path";
8523
- function canonicalProjectRoot(projectRoot) {
8524
- try {
8525
- return realpathSync.native(projectRoot);
8526
- } catch {
8527
- return resolve(projectRoot);
8528
- }
8529
- }
8530
- function projectKeyForRoot(projectRoot) {
8531
- const canonical = canonicalProjectRoot(projectRoot);
8532
- const digest = createHash2("sha256").update(canonical).digest("hex").slice(0, 12);
8533
- return `${sanitizePathPart(basename(canonical) || "project")}-${digest}`;
8534
- }
8535
- function ensureSessionDirectory(sessionsRoot, projectKey) {
8536
- const directory = join(sessionsRoot, projectKey);
8537
- mkdirSync(directory, { recursive: true, mode: 448 });
8538
- return directory;
8539
- }
8540
- function sessionPathForHeader(sessionsRoot, projectKey, createdAt, sessionId) {
8541
- const safeCreatedAt = createdAt.replaceAll(":", "").replaceAll(".", "");
8542
- return join(ensureSessionDirectory(sessionsRoot, projectKey), `${safeCreatedAt}_${sessionId}.jsonl`);
8543
- }
8544
- function sanitizePathPart(value) {
8545
- const sanitized = value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
8546
- return sanitized || "project";
8547
- }
8619
+ // ../../packages/session/session-store/dist/attachment-references.js
8620
+ import { lstatSync, readdirSync } from "node:fs";
8621
+ import { join } from "node:path";
8548
8622
 
8549
8623
  // ../../packages/session/session-store/dist/session-reader.js
8550
8624
  import { readFileSync } from "node:fs";
@@ -8636,6 +8710,78 @@ function findOpenToolCalls(items) {
8636
8710
  return [...calls.values()];
8637
8711
  }
8638
8712
 
8713
+ // ../../packages/session/session-store/dist/attachment-references.js
8714
+ function attachmentReferences(entries) {
8715
+ const result = /* @__PURE__ */ new Set();
8716
+ for (const entry of entries) {
8717
+ if (entry.entryType === "compaction" || entry.entryType === "image_offload")
8718
+ continue;
8719
+ if (!("message" in entry.payload) || !entry.payload.message || typeof entry.payload.message !== "object")
8720
+ throw new Error("Incomplete session message during attachment scan");
8721
+ const message = entry.payload.message;
8722
+ if (!("attachments" in message))
8723
+ continue;
8724
+ for (const block of message.attachments ?? []) {
8725
+ assertAttachmentContent(block);
8726
+ result.add(block.ref.id);
8727
+ }
8728
+ }
8729
+ return result;
8730
+ }
8731
+ function scanAttachmentReferences(sessionsRoot) {
8732
+ const result = /* @__PURE__ */ new Set();
8733
+ const visit = (directory) => {
8734
+ for (const file of readdirSync(directory, { withFileTypes: true })) {
8735
+ const path5 = join(directory, file.name);
8736
+ if (file.isSymbolicLink())
8737
+ throw new Error(`Cannot scan symbolic session path: ${path5}`);
8738
+ if (file.isDirectory())
8739
+ visit(path5);
8740
+ else if (file.name.endsWith(".jsonl")) {
8741
+ if (!lstatSync(path5).isFile())
8742
+ throw new Error(`Invalid session file: ${path5}`);
8743
+ const replay = readSessionFile(path5);
8744
+ if (replay.ignoredTornTail)
8745
+ throw new Error(`Incomplete session journal: ${path5}`);
8746
+ for (const id of attachmentReferences(replay.items.filter((item) => item.kind === "entry")))
8747
+ result.add(id);
8748
+ }
8749
+ }
8750
+ };
8751
+ visit(sessionsRoot);
8752
+ return result;
8753
+ }
8754
+
8755
+ // ../../packages/session/session-store/dist/session-paths.js
8756
+ import { createHash as createHash2 } from "node:crypto";
8757
+ import { mkdirSync, realpathSync } from "node:fs";
8758
+ import { basename, join as join2, resolve } from "node:path";
8759
+ function canonicalProjectRoot(projectRoot) {
8760
+ try {
8761
+ return realpathSync.native(projectRoot);
8762
+ } catch {
8763
+ return resolve(projectRoot);
8764
+ }
8765
+ }
8766
+ function projectKeyForRoot(projectRoot) {
8767
+ const canonical = canonicalProjectRoot(projectRoot);
8768
+ const digest = createHash2("sha256").update(canonical).digest("hex").slice(0, 12);
8769
+ return `${sanitizePathPart(basename(canonical) || "project")}-${digest}`;
8770
+ }
8771
+ function ensureSessionDirectory(sessionsRoot, projectKey) {
8772
+ const directory = join2(sessionsRoot, projectKey);
8773
+ mkdirSync(directory, { recursive: true, mode: 448 });
8774
+ return directory;
8775
+ }
8776
+ function sessionPathForHeader(sessionsRoot, projectKey, createdAt, sessionId) {
8777
+ const safeCreatedAt = createdAt.replaceAll(":", "").replaceAll(".", "");
8778
+ return join2(ensureSessionDirectory(sessionsRoot, projectKey), `${safeCreatedAt}_${sessionId}.jsonl`);
8779
+ }
8780
+ function sanitizePathPart(value) {
8781
+ const sanitized = value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
8782
+ return sanitized || "project";
8783
+ }
8784
+
8639
8785
  // ../../packages/session/session-store/dist/session-journal.js
8640
8786
  import { closeSync, existsSync, fsyncSync, openSync, readFileSync as readFileSync2, unlinkSync, writeSync } from "node:fs";
8641
8787
  import { randomUUID as randomUUID2 } from "node:crypto";
@@ -8646,6 +8792,7 @@ var SessionJournalLockError = class extends Error {
8646
8792
  }
8647
8793
  };
8648
8794
  var FileSessionJournal = class {
8795
+ attachments;
8649
8796
  header;
8650
8797
  path;
8651
8798
  #fd;
@@ -8653,6 +8800,7 @@ var FileSessionJournal = class {
8653
8800
  #nextSeq;
8654
8801
  #closed = false;
8655
8802
  constructor(input) {
8803
+ this.attachments = input.attachments;
8656
8804
  this.header = input.header;
8657
8805
  this.path = input.path;
8658
8806
  this.#nextSeq = input.nextSeq;
@@ -8670,7 +8818,21 @@ var FileSessionJournal = class {
8670
8818
  entryType: input.entryType,
8671
8819
  payload: input.payload
8672
8820
  };
8673
- this.writeItem(entry);
8821
+ const message = "message" in entry.payload ? entry.payload.message : void 0;
8822
+ const attachments = message && "attachments" in message ? message.attachments : void 0;
8823
+ if (attachments?.length) {
8824
+ for (const attachment of attachments)
8825
+ assertAttachmentContent(attachment);
8826
+ if (!this.attachments)
8827
+ throw new Error("Attachment persistence requires an attachment store");
8828
+ if (!("attachmentKey" in message) || !message.attachmentKey)
8829
+ throw new Error("Attachment message requires an occurrence key");
8830
+ this.attachments.commit(attachments.map((block) => block.ref), () => {
8831
+ this.writeItem(entry);
8832
+ this.flush();
8833
+ });
8834
+ } else
8835
+ this.writeItem(entry);
8674
8836
  if (entry.entryType === "compaction") {
8675
8837
  this.flush();
8676
8838
  }
@@ -8736,14 +8898,15 @@ function createSessionJournal(options) {
8736
8898
  if (!existing) {
8737
8899
  writeLine(fd, header);
8738
8900
  fsyncSync(fd);
8739
- return new FileSessionJournal({ header, path: path5, nextSeq: 1, fd });
8901
+ return new FileSessionJournal({ header, path: path5, nextSeq: 1, fd, attachments: options.attachments });
8740
8902
  }
8741
8903
  const replay = readSessionFile(path5);
8742
8904
  return new FileSessionJournal({
8743
8905
  header: replay.header,
8744
8906
  path: path5,
8745
8907
  nextSeq: replay.lastSeq + 1,
8746
- fd
8908
+ fd,
8909
+ attachments: options.attachments
8747
8910
  });
8748
8911
  } catch (error) {
8749
8912
  releaseLock(lockPath);
@@ -8819,8 +8982,8 @@ function required(value, label) {
8819
8982
  }
8820
8983
 
8821
8984
  // ../../packages/session/session-store/dist/session-manager.js
8822
- import { existsSync as existsSync2, readdirSync, statSync } from "node:fs";
8823
- import { join as join2 } from "node:path";
8985
+ import { existsSync as existsSync2, readdirSync as readdirSync2, statSync } from "node:fs";
8986
+ import { join as join3 } from "node:path";
8824
8987
 
8825
8988
  // ../../packages/session/session-store/dist/session-tree.js
8826
8989
  function buildSessionTree(sessions) {
@@ -8848,14 +9011,17 @@ function buildSessionTree(sessions) {
8848
9011
 
8849
9012
  // ../../packages/session/session-store/dist/session-manager.js
8850
9013
  var SessionManager = class {
9014
+ #attachments;
8851
9015
  #sessionsRoot;
8852
9016
  #appVersion;
8853
9017
  constructor(options) {
9018
+ this.#attachments = options.attachments;
8854
9019
  this.#sessionsRoot = options.sessionsRoot;
8855
9020
  this.#appVersion = options.appVersion;
8856
9021
  }
8857
9022
  create(options) {
8858
9023
  const journal = createSessionJournal({
9024
+ attachments: this.#attachments,
8859
9025
  sessionsRoot: this.#sessionsRoot,
8860
9026
  projectRoot: options.projectRoot,
8861
9027
  initialCwd: options.initialCwd,
@@ -8884,6 +9050,7 @@ var SessionManager = class {
8884
9050
  }
8885
9051
  const replay = readSessionFile(summary.path);
8886
9052
  const journal = createSessionJournal({
9053
+ attachments: this.#attachments,
8887
9054
  sessionsRoot: this.#sessionsRoot,
8888
9055
  header: replay.header,
8889
9056
  path: summary.path
@@ -8911,6 +9078,7 @@ var SessionManager = class {
8911
9078
  throw new Error("fork target must be a user_message entry");
8912
9079
  }
8913
9080
  const child = createSessionJournal({
9081
+ attachments: this.#attachments,
8914
9082
  sessionsRoot: this.#sessionsRoot,
8915
9083
  projectRoot: parent.header.projectRoot,
8916
9084
  initialCwd: parent.header.initialCwd,
@@ -8941,6 +9109,7 @@ var SessionManager = class {
8941
9109
  const parent = this.parentReplay(options.parentSessionId);
8942
9110
  const lastEntry = [...parent.items].reverse().find((item) => item.kind === "entry");
8943
9111
  const child = createSessionJournal({
9112
+ attachments: this.#attachments,
8944
9113
  sessionsRoot: this.#sessionsRoot,
8945
9114
  projectRoot: parent.header.projectRoot,
8946
9115
  initialCwd: parent.header.initialCwd,
@@ -8974,8 +9143,8 @@ var SessionManager = class {
8974
9143
  }
8975
9144
  const paths = [];
8976
9145
  const visit = (directory) => {
8977
- for (const entry of readdirSync(directory)) {
8978
- const absolute = join2(directory, entry);
9146
+ for (const entry of readdirSync2(directory)) {
9147
+ const absolute = join3(directory, entry);
8979
9148
  const stat6 = statSync(absolute);
8980
9149
  if (stat6.isDirectory()) {
8981
9150
  visit(absolute);
@@ -9331,14 +9500,14 @@ function toolsOf(request) {
9331
9500
  }
9332
9501
  return request.tools.map(toolOf);
9333
9502
  }
9334
- function toPiContext(request) {
9503
+ function toPiContext(request, attachments) {
9335
9504
  const systems = request.messages.filter((message) => message.role === "system" && message.toolDefinitions === void 0);
9336
9505
  if (systems.length > 1) {
9337
9506
  throw new ModelError("model history contains more than one system message", {
9338
9507
  kind: "protocol"
9339
9508
  });
9340
9509
  }
9341
- const messages = request.messages.filter((message) => message.role !== "system").map((message) => toPiMessage(message, request.provider, request.model));
9510
+ const messages = request.messages.filter((message) => message.role !== "system").map((message) => toPiMessage(message, request.provider, request.model, attachments?.get(message)));
9342
9511
  const tools = toolsOf(request);
9343
9512
  return {
9344
9513
  ...systems[0] === void 0 ? {} : { systemPrompt: systems[0].content },
@@ -9346,9 +9515,12 @@ function toPiContext(request) {
9346
9515
  ...tools === void 0 ? {} : { tools }
9347
9516
  };
9348
9517
  }
9349
- function toPiMessage(message, provider2, model2) {
9518
+ function toPiMessage(message, provider2, model2, attachments) {
9519
+ if ((message.role === "user" || message.role === "tool-result") && message.attachments?.length && attachments === void 0) {
9520
+ throw new ModelError("Attachments must be prepared before serializing model context", { kind: "protocol" });
9521
+ }
9350
9522
  if (message.role === "user") {
9351
- return { role: "user", content: message.content, timestamp: 0 };
9523
+ return { role: "user", content: attachments?.length ? [{ type: "text", text: message.content }, ...attachments] : message.content, timestamp: 0 };
9352
9524
  }
9353
9525
  if (message.role === "assistant") {
9354
9526
  return toPiAssistant(message, { provider: provider2, model: model2 });
@@ -9357,7 +9529,7 @@ function toPiMessage(message, provider2, model2) {
9357
9529
  role: "toolResult",
9358
9530
  toolCallId: message.toolCallId,
9359
9531
  toolName: message.toolName,
9360
- content: [{ type: "text", text: message.content || "(no output)" }],
9532
+ content: [{ type: "text", text: message.content || "(no output)" }, ...attachments ?? []],
9361
9533
  isError: message.isError,
9362
9534
  timestamp: 0
9363
9535
  };
@@ -9543,13 +9715,88 @@ function ensureActive(request, hadDelta) {
9543
9715
  }
9544
9716
  }
9545
9717
 
9718
+ // ../../packages/llm/llm-pi-ai/dist/images.js
9719
+ function defaultImagePolicy(model2) {
9720
+ const anthropic = model2.api === "anthropic-messages";
9721
+ const google = model2.api === "google-generative-ai" || model2.api === "google-vertex";
9722
+ return {
9723
+ target: {
9724
+ maxPixels: anthropic ? 115e4 : google ? 4194304 : 2097152,
9725
+ maxDimension: anthropic ? 1568 : 2048,
9726
+ maxBytes: 4 * 1024 ** 2,
9727
+ formats: ["image/png", "image/jpeg", "image/webp"]
9728
+ },
9729
+ maxImages: 20,
9730
+ maxTotalBytes: (google ? 12 : 16) * 1024 ** 2
9731
+ };
9732
+ }
9733
+ async function prepareRequestImages(request, model2, options) {
9734
+ const offloaded = request.imageContext?.offloaded ?? /* @__PURE__ */ new Set();
9735
+ const blocks = /* @__PURE__ */ new Map();
9736
+ const images = [];
9737
+ const policy = (options?.policy ?? defaultImagePolicy)(model2);
9738
+ if (![policy.maxImages, policy.maxTotalBytes].every((n) => Number.isSafeInteger(n) && n > 0))
9739
+ throw new ModelError("Invalid image request budget", { kind: "protocol" });
9740
+ for (const message of request.messages) {
9741
+ if (message.role !== "user" && message.role !== "tool-result")
9742
+ continue;
9743
+ const output = [];
9744
+ blocks.set(message, output);
9745
+ for (const [index, block] of (message.attachments ?? []).entries()) {
9746
+ assertAttachmentContent(block);
9747
+ if (block.type === "file") {
9748
+ output.push({ type: "text", text: `Attached file ${JSON.stringify(block.ref.name)} (${block.ref.id}, ${block.ref.bytes} bytes). File bytes have not been read or parsed.` });
9749
+ continue;
9750
+ }
9751
+ const key = `${message.attachmentKey}:${index}`;
9752
+ if (offloaded.has(key) || message.omittedImageIndexes?.includes(index)) {
9753
+ output.push({ type: "text", text: `[Image ${block.ref.id} omitted from context; use read_image to inspect it again.]` });
9754
+ continue;
9755
+ }
9756
+ if (!model2.input.includes("image"))
9757
+ throw new ModelError(`Model ${model2.provider}/${model2.id} does not declare image input`, { kind: "protocol" });
9758
+ const store = options?.store();
9759
+ if (!store)
9760
+ throw new ModelError("Image request requires an attachment store", { kind: "protocol" });
9761
+ const prepared = await store.prepareImage(block.ref, { ...policy.target, ...block.view }, request.cancelToken?.signal);
9762
+ const content = { type: "image", data: Buffer.from(prepared.data).toString("base64"), mimeType: prepared.mimeType };
9763
+ const note = { type: "text", text: `Image ${block.ref.id}: ${prepared.note}` };
9764
+ output.push(note, content);
9765
+ images.push({ key, owner: message, content, note, bytes: prepared.data.byteLength });
9766
+ }
9767
+ }
9768
+ let count = images.length;
9769
+ let bytes = images.reduce((sum, image) => sum + image.bytes, 0);
9770
+ const evicted = [];
9771
+ for (const image of images) {
9772
+ if (count <= policy.maxImages && bytes <= policy.maxTotalBytes)
9773
+ break;
9774
+ if (!request.imageContext || request.imageContext.protectedKeys.has(image.key) || !("attachmentKey" in image.owner) || !image.owner.attachmentKey)
9775
+ continue;
9776
+ const output = blocks.get(image.owner);
9777
+ output.splice(output.indexOf(image.content), 1);
9778
+ image.note.text = `[Image omitted from context to meet request budget; original remains stored. Use read_image to inspect it again.]`;
9779
+ evicted.push(image.key);
9780
+ count--;
9781
+ bytes -= image.bytes;
9782
+ }
9783
+ if (count > policy.maxImages || bytes > policy.maxTotalBytes)
9784
+ throw new ModelError("Current-turn images exceed the request image budget; reduce the number or crop the images", { kind: "protocol" });
9785
+ request.cancelToken?.signal.throwIfAborted();
9786
+ if (evicted.length)
9787
+ request.imageContext.persistOffload(evicted);
9788
+ return blocks;
9789
+ }
9790
+
9546
9791
  // ../../packages/llm/llm-pi-ai/dist/adapter.js
9547
9792
  var PiAiAdapter = class {
9793
+ attachments;
9548
9794
  name = "pi-ai";
9549
9795
  models;
9550
9796
  eligibleProviderIds;
9551
9797
  constructor(options, models) {
9552
9798
  this.eligibleProviderIds = options.eligibleProviderIds;
9799
+ this.attachments = options.attachments;
9553
9800
  this.models = models;
9554
9801
  }
9555
9802
  async runAttempt(request) {
@@ -9564,9 +9811,6 @@ var PiAiAdapter = class {
9564
9811
  throw new ModelError(`unknown model route: ${request.provider}/${request.model}`, { kind: "protocol" });
9565
9812
  }
9566
9813
  this.checkActive(request);
9567
- if (request.onRequestOpened?.() === false) {
9568
- throw new ModelStreamCancelled("model request was cancelled before opening");
9569
- }
9570
9814
  const requestModel = request.baseUrl === void 0 ? model2 : { ...model2, baseUrl: request.baseUrl };
9571
9815
  const options = {
9572
9816
  ...request.cancelToken === void 0 ? {} : { signal: request.cancelToken.signal },
@@ -9577,16 +9821,26 @@ var PiAiAdapter = class {
9577
9821
  maxRetries: 0
9578
9822
  };
9579
9823
  try {
9580
- const stream = this.models.streamSimple(requestModel, toPiContext(request), options);
9824
+ const images = await prepareRequestImages(request, model2, this.attachments).catch((error) => {
9825
+ this.checkActive(request);
9826
+ if (error instanceof ModelError)
9827
+ throw error;
9828
+ throw new ModelError(errorMessage2(error), { kind: "protocol", cause: error });
9829
+ });
9830
+ this.checkActive(request);
9831
+ if (request.onRequestOpened?.() === false)
9832
+ throw new ModelStreamCancelled("model request was cancelled before opening");
9833
+ const stream = this.models.streamSimple(requestModel, toPiContext(request, images), options);
9581
9834
  const result = await consumePiEvents(stream, request, request.onEvent);
9582
9835
  this.checkActive(request);
9583
9836
  return result;
9584
9837
  } catch (error) {
9838
+ this.checkActive(request);
9585
9839
  if (error instanceof ModelError || error instanceof ModelStreamCancelled) {
9586
9840
  throw error;
9587
9841
  }
9588
9842
  throw new ModelError(errorMessage2(error), {
9589
- kind: piModelErrorKind(error),
9843
+ kind: error instanceof AttachmentError ? "protocol" : piModelErrorKind(error),
9590
9844
  cause: error
9591
9845
  });
9592
9846
  }
@@ -17855,6 +18109,7 @@ async function createPiAiPlatform(options) {
17855
18109
  for (const id of custom.providerIds)
17856
18110
  verified.delete(id);
17857
18111
  return new PiAiPlatform(models, {
18112
+ attachments: options.attachments,
17858
18113
  excludedProviderIds: options.excludedProviderIds,
17859
18114
  verifiedProviderIds: verified,
17860
18115
  reload: async () => {
@@ -17881,7 +18136,7 @@ var PiAiPlatform = class {
17881
18136
  this.models = models;
17882
18137
  this.eligibleIds = new Set(eligibleProviderIds(models, options.excludedProviderIds));
17883
18138
  this.verifiedProviderIds = options.verifiedProviderIds;
17884
- this.adapter = new PiAiAdapter({ eligibleProviderIds: this.eligibleIds }, models);
18139
+ this.adapter = new PiAiAdapter({ eligibleProviderIds: this.eligibleIds, attachments: options.attachments }, models);
17885
18140
  this.catalog = new PiAiCatalog(this.models, this.eligibleIds, this.verifiedProviderIds, async () => {
17886
18141
  await options.reload?.();
17887
18142
  this.eligibleIds.clear();
@@ -18086,7 +18341,7 @@ function errorMessage3(error) {
18086
18341
  // ../../packages/storage/local-config/dist/config.js
18087
18342
  import { randomUUID as randomUUID3 } from "node:crypto";
18088
18343
  import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, writeFileSync } from "node:fs";
18089
- import { basename as basename2, dirname, join as join3 } from "node:path";
18344
+ import { basename as basename2, dirname, join as join4 } from "node:path";
18090
18345
 
18091
18346
  // ../../packages/fs/local-paths/dist/index.js
18092
18347
  import { execFileSync } from "node:child_process";
@@ -18184,8 +18439,8 @@ function defaultConfigPath(environ = process.env) {
18184
18439
  return normalizeLocalPath(explicit, { env: environ });
18185
18440
  }
18186
18441
  const configHome = environ["XDG_CONFIG_HOME"];
18187
- const root = configHome ? normalizeLocalPath(configHome, { env: environ }) : join3(getHomeDirectory({ env: environ }), ".config");
18188
- return join3(root, "laohuang", "config.json");
18442
+ const root = configHome ? normalizeLocalPath(configHome, { env: environ }) : join4(getHomeDirectory({ env: environ }), ".config");
18443
+ return join4(root, "laohuang", "config.json");
18189
18444
  }
18190
18445
  var ConfigManager = class {
18191
18446
  path;
@@ -18287,7 +18542,7 @@ var ConfigManager = class {
18287
18542
  const parent = dirname(this.path);
18288
18543
  mkdirSync2(parent, { recursive: true, mode: 448 });
18289
18544
  const content = JSON.stringify(document, null, 2) + "\n";
18290
- const temporary = join3(parent, `.${basename2(this.path)}.${process.pid}.${randomUUID3()}.tmp`);
18545
+ const temporary = join4(parent, `.${basename2(this.path)}.${process.pid}.${randomUUID3()}.tmp`);
18291
18546
  writeFileSync(temporary, content, { encoding: "utf8", mode: 384 });
18292
18547
  chmodSync(temporary, 384);
18293
18548
  renameSync(temporary, this.path);
@@ -18363,7 +18618,7 @@ var CustomModelsStore = class {
18363
18618
  // ../../packages/storage/local-config/dist/credentials.js
18364
18619
  import { randomUUID as randomUUID4 } from "node:crypto";
18365
18620
  import { chmodSync as chmodSync2, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
18366
- import { basename as basename3, dirname as dirname2, join as join4 } from "node:path";
18621
+ import { basename as basename3, dirname as dirname2, join as join5 } from "node:path";
18367
18622
  function isPlainObject2(value) {
18368
18623
  return typeof value === "object" && value !== null && !Array.isArray(value);
18369
18624
  }
@@ -18527,7 +18782,7 @@ var CredentialStore = class {
18527
18782
  version: 2,
18528
18783
  providers: document.providers
18529
18784
  }, null, 2) + "\n";
18530
- const temporary = join4(parent, `.${basename3(this.path)}.${process.pid}.${randomUUID4()}.tmp`);
18785
+ const temporary = join5(parent, `.${basename3(this.path)}.${process.pid}.${randomUUID4()}.tmp`);
18531
18786
  writeFileSync2(temporary, content, { encoding: "utf8", mode: 384 });
18532
18787
  chmodSync2(temporary, 384);
18533
18788
  renameSync2(temporary, this.path);
@@ -18544,7 +18799,7 @@ function normalizeStoredCredential(credential) {
18544
18799
  // ../../packages/storage/local-config/dist/model-catalog-store.js
18545
18800
  import { randomUUID as randomUUID5 } from "node:crypto";
18546
18801
  import { chmodSync as chmodSync3, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync3 } from "node:fs";
18547
- import { basename as basename4, dirname as dirname3, join as join5 } from "node:path";
18802
+ import { basename as basename4, dirname as dirname3, join as join6 } from "node:path";
18548
18803
  function isPlainObject3(value) {
18549
18804
  return typeof value === "object" && value !== null && !Array.isArray(value);
18550
18805
  }
@@ -18608,7 +18863,7 @@ var ModelCatalogStore = class {
18608
18863
  chmodSync3(parent, 448);
18609
18864
  }
18610
18865
  const content = JSON.stringify(document, null, 2) + "\n";
18611
- const temporary = join5(parent, `.${basename4(this.path)}.${process.pid}.${randomUUID5()}.tmp`);
18866
+ const temporary = join6(parent, `.${basename4(this.path)}.${process.pid}.${randomUUID5()}.tmp`);
18612
18867
  writeFileSync3(temporary, content, { encoding: "utf8", mode: 384 });
18613
18868
  chmodSync3(temporary, 384);
18614
18869
  renameSync3(temporary, this.path);
@@ -18652,7 +18907,7 @@ function normalizeEntry(providerId, entry) {
18652
18907
  // ../../packages/storage/local-config/dist/mcp-config.js
18653
18908
  import { randomUUID as randomUUID6 } from "node:crypto";
18654
18909
  import { mkdir, readFile as readFile2, rename, unlink, writeFile } from "node:fs/promises";
18655
- import { basename as basename5, dirname as dirname4, join as join6 } from "node:path";
18910
+ import { basename as basename5, dirname as dirname4, join as join7 } from "node:path";
18656
18911
  var McpConfigStore = class {
18657
18912
  path;
18658
18913
  constructor(path5) {
@@ -18684,7 +18939,7 @@ var McpConfigStore = class {
18684
18939
  throw new Error("MCP storage requires JSON data");
18685
18940
  const parent = dirname4(this.path);
18686
18941
  await mkdir(parent, { recursive: true, mode: 448 });
18687
- const temporary = join6(parent, `.${basename5(this.path)}.${randomUUID6()}.tmp`);
18942
+ const temporary = join7(parent, `.${basename5(this.path)}.${randomUUID6()}.tmp`);
18688
18943
  try {
18689
18944
  await writeFile(temporary, `${content}
18690
18945
  `, { mode: 384, flag: "wx" });
@@ -18703,7 +18958,7 @@ var McpConfigStore = class {
18703
18958
  // ../../packages/storage/local-config/dist/mcp-credentials.js
18704
18959
  import { createHash as createHash3 } from "node:crypto";
18705
18960
  import { unlink as unlink2 } from "node:fs/promises";
18706
- import { join as join7, resolve as resolve2 } from "node:path";
18961
+ import { join as join8, resolve as resolve2 } from "node:path";
18707
18962
  var operations = /* @__PURE__ */ new Map();
18708
18963
  var McpCredentialFileStore = class {
18709
18964
  directory;
@@ -18711,7 +18966,7 @@ var McpCredentialFileStore = class {
18711
18966
  this.directory = resolve2(directory);
18712
18967
  }
18713
18968
  path(key) {
18714
- return join7(this.directory, `${createHash3("sha256").update(key).digest("hex")}.json`);
18969
+ return join8(this.directory, `${createHash3("sha256").update(key).digest("hex")}.json`);
18715
18970
  }
18716
18971
  async read(key) {
18717
18972
  const path5 = this.path(key);
@@ -18800,8 +19055,8 @@ var CancelToken = class {
18800
19055
  continue;
18801
19056
  }
18802
19057
  }
18803
- for (const resolve14 of this.#waiters) {
18804
- resolve14();
19058
+ for (const resolve15 of this.#waiters) {
19059
+ resolve15();
18805
19060
  }
18806
19061
  this.#waiters.clear();
18807
19062
  return true;
@@ -18814,20 +19069,20 @@ var CancelToken = class {
18814
19069
  if (this.#cancelled) {
18815
19070
  return Promise.resolve(true);
18816
19071
  }
18817
- return new Promise((resolve14) => {
19072
+ return new Promise((resolve15) => {
18818
19073
  let timer;
18819
19074
  const onCancel = () => {
18820
19075
  if (timer !== void 0) {
18821
19076
  clearTimeout(timer);
18822
19077
  }
18823
19078
  this.#waiters.delete(onCancel);
18824
- resolve14(true);
19079
+ resolve15(true);
18825
19080
  };
18826
19081
  this.#waiters.add(onCancel);
18827
19082
  if (timeoutMs !== void 0) {
18828
19083
  timer = setTimeout(() => {
18829
19084
  this.#waiters.delete(onCancel);
18830
- resolve14(false);
19085
+ resolve15(false);
18831
19086
  }, timeoutMs);
18832
19087
  }
18833
19088
  });
@@ -19423,8 +19678,8 @@ var SubscriberMailbox = class _SubscriberMailbox {
19423
19678
  const deadline = timeoutMs === void 0 ? void 0 : Date.now() + timeoutMs;
19424
19679
  while (this.unfinished > 0) {
19425
19680
  if (deadline === void 0) {
19426
- await new Promise((resolve14) => {
19427
- this.flushWaiters.push(resolve14);
19681
+ await new Promise((resolve15) => {
19682
+ this.flushWaiters.push(resolve15);
19428
19683
  });
19429
19684
  continue;
19430
19685
  }
@@ -19432,12 +19687,12 @@ var SubscriberMailbox = class _SubscriberMailbox {
19432
19687
  if (remaining <= 0) {
19433
19688
  return false;
19434
19689
  }
19435
- const waiter = new Promise((resolve14) => {
19436
- this.flushWaiters.push(resolve14);
19690
+ const waiter = new Promise((resolve15) => {
19691
+ this.flushWaiters.push(resolve15);
19437
19692
  });
19438
19693
  let timer;
19439
- const timeout = new Promise((resolve14) => {
19440
- timer = setTimeout(resolve14, remaining);
19694
+ const timeout = new Promise((resolve15) => {
19695
+ timer = setTimeout(resolve15, remaining);
19441
19696
  });
19442
19697
  await Promise.race([waiter, timeout]);
19443
19698
  clearTimeout(timer);
@@ -19466,22 +19721,22 @@ var SubscriberMailbox = class _SubscriberMailbox {
19466
19721
  wake() {
19467
19722
  const waiters = this.wakeWaiters;
19468
19723
  this.wakeWaiters = [];
19469
- for (const resolve14 of waiters) {
19470
- resolve14();
19724
+ for (const resolve15 of waiters) {
19725
+ resolve15();
19471
19726
  }
19472
19727
  }
19473
19728
  notifyFlushWaiters() {
19474
19729
  const waiters = this.flushWaiters;
19475
19730
  this.flushWaiters = [];
19476
- for (const resolve14 of waiters) {
19477
- resolve14();
19731
+ for (const resolve15 of waiters) {
19732
+ resolve15();
19478
19733
  }
19479
19734
  }
19480
19735
  async run() {
19481
19736
  for (; ; ) {
19482
19737
  while (this.items.length === 0 && !this.closed) {
19483
- await new Promise((resolve14) => {
19484
- this.wakeWaiters.push(resolve14);
19738
+ await new Promise((resolve15) => {
19739
+ this.wakeWaiters.push(resolve15);
19485
19740
  });
19486
19741
  }
19487
19742
  if (this.items.length === 0 && this.closed) {
@@ -19629,8 +19884,8 @@ var EventBus = class {
19629
19884
  if (this.closed) {
19630
19885
  return Promise.reject(new EventBusClosedError());
19631
19886
  }
19632
- return new Promise((resolve14, reject) => {
19633
- const waiter = { resolve: resolve14, reject };
19887
+ return new Promise((resolve15, reject) => {
19888
+ const waiter = { resolve: resolve15, reject };
19634
19889
  if (timeoutMs !== void 0) {
19635
19890
  waiter.timer = setTimeout(() => {
19636
19891
  const index = this.bufferWaiters.indexOf(waiter);
@@ -19729,7 +19984,7 @@ function makeCancelIntent(reason, source) {
19729
19984
 
19730
19985
  // ../../packages/context/project-instructions/dist/project-instructions.js
19731
19986
  import { createHash as createHash4 } from "node:crypto";
19732
- import { closeSync as closeSync2, existsSync as existsSync7, fstatSync, lstatSync, openSync as openSync2, readSync, realpathSync as realpathSync2, statSync as statSync2 } from "node:fs";
19987
+ import { closeSync as closeSync2, existsSync as existsSync7, fstatSync, lstatSync as lstatSync2, openSync as openSync2, readSync, realpathSync as realpathSync2, statSync as statSync2 } from "node:fs";
19733
19988
  import path from "node:path";
19734
19989
  var INSTRUCTION_FILENAMES = [
19735
19990
  "AGENTS.md",
@@ -19913,7 +20168,7 @@ function scopeChain(root, cwd) {
19913
20168
  function readInstructionFile(rootReal, candidate, options) {
19914
20169
  let stats;
19915
20170
  try {
19916
- stats = lstatSync(candidate);
20171
+ stats = lstatSync2(candidate);
19917
20172
  } catch {
19918
20173
  return null;
19919
20174
  }
@@ -21496,20 +21751,20 @@ var AgentSession = class {
21496
21751
  if (timeoutMs !== void 0 && timeoutMs <= 0) {
21497
21752
  return Promise.resolve(false);
21498
21753
  }
21499
- return new Promise((resolve14) => {
21754
+ return new Promise((resolve15) => {
21500
21755
  let timer;
21501
21756
  const onIdle = () => {
21502
21757
  if (timer !== void 0) {
21503
21758
  clearTimeout(timer);
21504
21759
  }
21505
21760
  this.#idleWaiters.delete(onIdle);
21506
- resolve14(true);
21761
+ resolve15(true);
21507
21762
  };
21508
21763
  this.#idleWaiters.add(onIdle);
21509
21764
  if (timeoutMs !== void 0) {
21510
21765
  timer = setTimeout(() => {
21511
21766
  this.#idleWaiters.delete(onIdle);
21512
- resolve14(false);
21767
+ resolve15(false);
21513
21768
  }, timeoutMs);
21514
21769
  timer.unref();
21515
21770
  }
@@ -21823,8 +22078,8 @@ var AgentSession = class {
21823
22078
  if (value) {
21824
22079
  const waiters = [...this.#idleWaiters];
21825
22080
  this.#idleWaiters.clear();
21826
- for (const resolve14 of waiters) {
21827
- resolve14();
22081
+ for (const resolve15 of waiters) {
22082
+ resolve15();
21828
22083
  }
21829
22084
  }
21830
22085
  }
@@ -22054,13 +22309,13 @@ function inputTimeout(kind, env = process.env) {
22054
22309
  async function drainTerminalInput(input, maxMs = 1e3, idleMs = 50) {
22055
22310
  if (!input?.off)
22056
22311
  return;
22057
- await new Promise((resolve14) => {
22312
+ await new Promise((resolve15) => {
22058
22313
  let idle;
22059
22314
  const finish = () => {
22060
22315
  clearTimeout(idle);
22061
22316
  clearTimeout(deadline);
22062
22317
  input.off?.("data", onData);
22063
- resolve14();
22318
+ resolve15();
22064
22319
  };
22065
22320
  const onData = () => {
22066
22321
  clearTimeout(idle);
@@ -22608,7 +22863,7 @@ var BoundedTerminalWriter = class _BoundedTerminalWriter {
22608
22863
  // ../../packages/terminal/tui/dist/tui/render-diagnostics.js
22609
22864
  import { mkdtempSync, writeFileSync as writeFileSync4 } from "node:fs";
22610
22865
  import { tmpdir } from "node:os";
22611
- import { join as join8 } from "node:path";
22866
+ import { join as join9 } from "node:path";
22612
22867
  var RenderWidthError = class extends Error {
22613
22868
  expected;
22614
22869
  actual;
@@ -22626,8 +22881,8 @@ function recordRenderFailure(error) {
22626
22881
  if (!(error instanceof RenderWidthError))
22627
22882
  return error;
22628
22883
  try {
22629
- const directory = mkdtempSync(join8(tmpdir(), "laohuang-tui-error-"));
22630
- const path5 = join8(directory, "render.json");
22884
+ const directory = mkdtempSync(join9(tmpdir(), "laohuang-tui-error-"));
22885
+ const path5 = join9(directory, "render.json");
22631
22886
  writeFileSync4(path5, JSON.stringify({
22632
22887
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
22633
22888
  source: error.source,
@@ -26843,19 +27098,19 @@ var ViewHost = class {
26843
27098
  this.#overlays = overlays;
26844
27099
  }
26845
27100
  openSelection(request) {
26846
- return new Promise((resolve14) => {
27101
+ return new Promise((resolve15) => {
26847
27102
  const component = this.#selectionComponent(request);
26848
- this.#open({ id: request.id, component, resolve: resolve14 }, "selector", request.floating);
27103
+ this.#open({ id: request.id, component, resolve: resolve15 }, "selector", request.floating);
26849
27104
  });
26850
27105
  }
26851
27106
  openPrompt(request) {
26852
- return new Promise((resolve14) => {
27107
+ return new Promise((resolve15) => {
26853
27108
  const component = new AuthDialog({
26854
27109
  request,
26855
27110
  onSubmit: (value) => this.#close(request.id, value),
26856
27111
  onCancel: () => this.#close(request.id, null)
26857
27112
  });
26858
- this.#open({ id: request.id, component, resolve: resolve14 }, "modal", request.floating);
27113
+ this.#open({ id: request.id, component, resolve: resolve15 }, "modal", request.floating);
26859
27114
  });
26860
27115
  }
26861
27116
  handleInput(event) {
@@ -27379,8 +27634,8 @@ var InteractiveTerminalLoop = class _InteractiveTerminalLoop {
27379
27634
  try {
27380
27635
  input?.resume?.();
27381
27636
  if (!this.#exitRequested) {
27382
- await new Promise((resolve14) => {
27383
- this.#exitResolve = resolve14;
27637
+ await new Promise((resolve15) => {
27638
+ this.#exitResolve = resolve15;
27384
27639
  });
27385
27640
  }
27386
27641
  } finally {
@@ -27747,18 +28002,18 @@ var InteractiveTerminalLoop = class _InteractiveTerminalLoop {
27747
28002
  if (this.#closed || !this.#running) {
27748
28003
  return Promise.resolve(null);
27749
28004
  }
27750
- return new Promise((resolve14) => {
28005
+ return new Promise((resolve15) => {
27751
28006
  if (type === "open_selection") {
27752
28007
  this.#work.push({
27753
28008
  type,
27754
28009
  request,
27755
- resolve: resolve14
28010
+ resolve: resolve15
27756
28011
  });
27757
28012
  } else {
27758
28013
  this.#work.push({
27759
28014
  type,
27760
28015
  request,
27761
- resolve: resolve14
28016
+ resolve: resolve15
27762
28017
  });
27763
28018
  }
27764
28019
  this.#scheduleWakeup();
@@ -29086,8 +29341,8 @@ async function executeEdit(args, execution, resolvePath, io) {
29086
29341
  async function withFileMutationLock(key, fn) {
29087
29342
  const previous = mutationLocks.get(key) ?? Promise.resolve();
29088
29343
  let release;
29089
- const current = new Promise((resolve14) => {
29090
- release = resolve14;
29344
+ const current = new Promise((resolve15) => {
29345
+ release = resolve15;
29091
29346
  });
29092
29347
  const tail2 = previous.then(() => current);
29093
29348
  mutationLocks.set(key, tail2);
@@ -29170,6 +29425,98 @@ function editsArgument(args) {
29170
29425
  });
29171
29426
  }
29172
29427
 
29428
+ // ../../packages/fs/tool-fs/dist/read-image.js
29429
+ import { open } from "node:fs/promises";
29430
+ import { basename as basename6 } from "node:path";
29431
+ function createReadImageTool(options) {
29432
+ return {
29433
+ spec: {
29434
+ name: "read_image",
29435
+ description: "View a PNG, JPEG, WebP or GIF image. Use path to read a local image file. Use attachment_id only to re-read an image attachment reference already returned in the current session; do not guess IDs. Provide exactly one of path or attachment_id. Optional crop coordinates use the oriented original pixels. Animations show only their first frame. full_resolution prevents downscaling and may exceed model limits.",
29436
+ parameters: { type: "object", properties: {
29437
+ path: {
29438
+ type: "string",
29439
+ description: "Local image file path. Absolute paths are accepted; relative paths resolve against the default working directory, and ~/ expands to the user's home directory. Use this for local files. Provide exactly one of path or attachment_id."
29440
+ },
29441
+ attachment_id: {
29442
+ type: "string",
29443
+ description: "Exact image attachment ID (sha256:...) already returned in the current session. Use only to re-read an existing session attachment, not to locate a local file. Do not guess IDs. Images not referenced by the current session are rejected. Provide exactly one of path or attachment_id."
29444
+ },
29445
+ region: {
29446
+ type: "object",
29447
+ description: "Optional rectangle in original-image pixels after orientation correction, with the origin at the top-left. Omit to view the whole image. Use a crop to inspect details that are unclear in a downscaled view. Out-of-bounds regions are rejected, not clipped. The crop may still be downscaled unless full_resolution is true.",
29448
+ properties: {
29449
+ x: { type: "integer", minimum: 0, description: "Zero-based left edge in the oriented original image, in pixels." },
29450
+ y: { type: "integer", minimum: 0, description: "Zero-based top edge in the oriented original image, in pixels." },
29451
+ width: { type: "integer", minimum: 1, description: "Crop width in original-image pixels; must be positive." },
29452
+ height: { type: "integer", minimum: 1, description: "Crop height in original-image pixels; must be positive." }
29453
+ },
29454
+ required: ["x", "y", "width", "height"],
29455
+ additionalProperties: false
29456
+ },
29457
+ full_resolution: {
29458
+ type: "boolean",
29459
+ description: "Defaults to false: the whole image or crop may be downscaled to fit model request limits. Set true to preserve its pixel dimensions; orientation correction and re-encoding may still occur. If model limits cannot be met, request preparation fails instead of silently downscaling. Use a smaller region in that case."
29460
+ }
29461
+ }, additionalProperties: false },
29462
+ promptGuidelines: ["Use read_image for visual image inspection. Use read for text. Image crop coordinates refer to the oriented original; follow the returned coordinate mapping."]
29463
+ },
29464
+ async execute(args, execution) {
29465
+ if (typeof args.path === "string" === (typeof args.attachment_id === "string"))
29466
+ throw new Error("Supply exactly one of path or attachment_id");
29467
+ const view = { ...args.region === void 0 ? {} : { region: args.region }, ...args.full_resolution === void 0 ? {} : { fullResolution: args.full_resolution } };
29468
+ assertImageView(view);
29469
+ const release = options.store.protect();
29470
+ try {
29471
+ let ref;
29472
+ let target;
29473
+ if (typeof args.path === "string") {
29474
+ target = resolveLocalPath(args.path, options.projectRoot, options.pathOptions);
29475
+ const handle = await open(target, "r");
29476
+ let bytes;
29477
+ try {
29478
+ const stat6 = await handle.stat();
29479
+ if (!stat6.isFile() || stat6.size > options.store.imageLimits.maxBytes)
29480
+ throw new AttachmentError("LIMIT_EXCEEDED", "Image must be a regular file within the source byte limit");
29481
+ const chunks = [];
29482
+ let total = 0;
29483
+ while (true) {
29484
+ execution.signal?.throwIfAborted();
29485
+ const buffer = Buffer.allocUnsafe(64 * 1024);
29486
+ const { bytesRead } = await handle.read(buffer);
29487
+ if (!bytesRead)
29488
+ break;
29489
+ total += bytesRead;
29490
+ if (total > options.store.imageLimits.maxBytes)
29491
+ throw new AttachmentError("LIMIT_EXCEEDED", "Image exceeds source byte limit");
29492
+ chunks.push(buffer.subarray(0, bytesRead));
29493
+ }
29494
+ bytes = Buffer.concat(chunks);
29495
+ } finally {
29496
+ await handle.close();
29497
+ }
29498
+ ref = (await options.store.saveImages([{ data: bytes, name: basename6(target) }], execution.signal))[0];
29499
+ } else {
29500
+ const found = options.resolveReference(String(args.attachment_id));
29501
+ if (!found)
29502
+ throw new AttachmentError("ACCESS_DENIED", "Image is not referenced by the current session");
29503
+ ref = found;
29504
+ for await (const _chunk of options.store.readFile(ref, execution.signal)) {
29505
+ }
29506
+ }
29507
+ if (view.region && (view.region.x + view.region.width > ref.width || view.region.y + view.region.height > ref.height))
29508
+ throw new AttachmentError("LIMIT_EXCEEDED", "Crop is outside the oriented image");
29509
+ if (execution.isCancelled())
29510
+ return cancelledResult(execution);
29511
+ const result = { ok: true, content: `Image ${ref.id}, oriented original ${ref.width}x${ref.height}.`, attachmentContent: [{ type: "image", ref, view }] };
29512
+ return target ? withTouchedPath(result, target) : result;
29513
+ } finally {
29514
+ release();
29515
+ }
29516
+ }
29517
+ };
29518
+ }
29519
+
29173
29520
  // ../../packages/shell/tool-bash/dist/tool-bash.js
29174
29521
  import { promises as fs2 } from "node:fs";
29175
29522
  import { realpathSync as realpathSync4 } from "node:fs";
@@ -29239,9 +29586,9 @@ function resolveBashPath(options = {}) {
29239
29586
  }
29240
29587
 
29241
29588
  // ../../packages/shell/bash-local/dist/bash-output.js
29242
- import { mkdir as mkdir2, mkdtemp, open, readdir, rm, stat, writeFile as writeFile2 } from "node:fs/promises";
29589
+ import { mkdir as mkdir2, mkdtemp, open as open2, readdir, rm, stat, writeFile as writeFile2 } from "node:fs/promises";
29243
29590
  import { homedir as homedir3 } from "node:os";
29244
- import { join as join9, resolve as resolve3 } from "node:path";
29591
+ import { join as join10, resolve as resolve3 } from "node:path";
29245
29592
  var BASH_OUTPUT_MAX_BYTES = 50 * 1024;
29246
29593
  var BASH_OUTPUT_MAX_LINES = 2e3;
29247
29594
  var OUTPUT_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
@@ -29291,7 +29638,7 @@ var BashOutput = class {
29291
29638
  if (!Number.isSafeInteger(limit) || limit < 1)
29292
29639
  throw new RangeError("Bash output limits must be positive integers");
29293
29640
  }
29294
- this.#directory = resolve3(options.outputDirectory ?? join9(homedir3(), ".laohuang", "tool-output"));
29641
+ this.#directory = resolve3(options.outputDirectory ?? join10(homedir3(), ".laohuang", "tool-output"));
29295
29642
  }
29296
29643
  append(stream, text3) {
29297
29644
  const next = this.#pending.then(() => this.#append(stream, text3));
@@ -29328,13 +29675,13 @@ var BashOutput = class {
29328
29675
  try {
29329
29676
  await mkdir2(this.#directory, { recursive: true, mode: 448 });
29330
29677
  await this.#removeExpired();
29331
- this.#archiveDirectory = await mkdtemp(join9(this.#directory, "bash-"));
29678
+ this.#archiveDirectory = await mkdtemp(join10(this.#directory, "bash-"));
29332
29679
  const files = {
29333
- stdout: join9(this.#archiveDirectory, "stdout.log"),
29334
- stderr: join9(this.#archiveDirectory, "stderr.log")
29680
+ stdout: join10(this.#archiveDirectory, "stdout.log"),
29681
+ stderr: join10(this.#archiveDirectory, "stderr.log")
29335
29682
  };
29336
29683
  for (const stream of ["stdout", "stderr"])
29337
- this.#handles[stream] = await open(files[stream], "wx", 384);
29684
+ this.#handles[stream] = await open2(files[stream], "wx", 384);
29338
29685
  this.files = files;
29339
29686
  } catch (error) {
29340
29687
  this.fileError = `Cannot save Bash output: ${String(error)}`;
@@ -29370,7 +29717,7 @@ var BashOutput = class {
29370
29717
  }
29371
29718
  if (this.#archiveDirectory !== null) {
29372
29719
  try {
29373
- await writeFile2(join9(this.#archiveDirectory, "completed"), "", { mode: 384 });
29720
+ await writeFile2(join10(this.#archiveDirectory, "completed"), "", { mode: 384 });
29374
29721
  } catch {
29375
29722
  }
29376
29723
  }
@@ -29379,9 +29726,9 @@ var BashOutput = class {
29379
29726
  for (const entry of await readdir(this.#directory, { withFileTypes: true })) {
29380
29727
  if (!entry.isDirectory() || !entry.name.startsWith("bash-"))
29381
29728
  continue;
29382
- const directory = join9(this.#directory, entry.name);
29729
+ const directory = join10(this.#directory, entry.name);
29383
29730
  try {
29384
- if (Date.now() - (await stat(join9(directory, "completed"))).mtimeMs > OUTPUT_RETENTION_MS) {
29731
+ if (Date.now() - (await stat(join10(directory, "completed"))).mtimeMs > OUTPUT_RETENTION_MS) {
29385
29732
  await rm(directory, { recursive: true, force: true });
29386
29733
  }
29387
29734
  } catch {
@@ -29563,7 +29910,7 @@ var TerminalSanitizer = class {
29563
29910
  }
29564
29911
  };
29565
29912
  function sleep(ms) {
29566
- return new Promise((resolve14) => setTimeout(resolve14, ms));
29913
+ return new Promise((resolve15) => setTimeout(resolve15, ms));
29567
29914
  }
29568
29915
  function errnoCode(error) {
29569
29916
  return error?.code;
@@ -29574,7 +29921,7 @@ async function terminateProcessTree(child, graceMs = TERMINATION_GRACE_MS) {
29574
29921
  return;
29575
29922
  if (process.platform === "win32") {
29576
29923
  const taskkill = win323.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "taskkill.exe");
29577
- await new Promise((resolve14, reject) => {
29924
+ await new Promise((resolve15, reject) => {
29578
29925
  execFile(taskkill, ["/F", "/T", "/PID", String(pid)], {
29579
29926
  windowsHide: true,
29580
29927
  timeout: TASKKILL_TIMEOUT_MS,
@@ -29583,7 +29930,7 @@ async function terminateProcessTree(child, graceMs = TERMINATION_GRACE_MS) {
29583
29930
  if (error)
29584
29931
  reject(new Error(`taskkill failed: ${error.message}`));
29585
29932
  else
29586
- resolve14();
29933
+ resolve15();
29587
29934
  });
29588
29935
  });
29589
29936
  return;
@@ -29686,9 +30033,9 @@ async function runBash(command, options) {
29686
30033
  });
29687
30034
  return await finish(result2);
29688
30035
  }
29689
- const spawnError = await new Promise((resolve14) => {
29690
- child.once("spawn", () => resolve14(null));
29691
- child.once("error", (error) => resolve14(error));
30036
+ const spawnError = await new Promise((resolve15) => {
30037
+ child.once("spawn", () => resolve15(null));
30038
+ child.once("error", (error) => resolve15(error));
29692
30039
  });
29693
30040
  if (spawnError) {
29694
30041
  const result2 = new BashResult({
@@ -29765,8 +30112,8 @@ async function runBash(command, options) {
29765
30112
  const stdoutDone = readPipe("stdout", child.stdout);
29766
30113
  const stderrDone = readPipe("stderr", child.stderr);
29767
30114
  let resolveExit;
29768
- const exitInfo = new Promise((resolve14) => {
29769
- resolveExit = resolve14;
30115
+ const exitInfo = new Promise((resolve15) => {
30116
+ resolveExit = resolve15;
29770
30117
  });
29771
30118
  const onExit = (code2, signal2) => {
29772
30119
  exited = true;
@@ -31723,6 +32070,7 @@ var SessionController = class {
31723
32070
  constructor(options) {
31724
32071
  this.#sessionsRoot = options.sessionsRoot;
31725
32072
  this.#manager = new SessionManager({
32073
+ attachments: options.attachments,
31726
32074
  sessionsRoot: options.sessionsRoot,
31727
32075
  appVersion: options.appVersion
31728
32076
  });
@@ -32352,6 +32700,7 @@ function errorMessage10(error) {
32352
32700
  }
32353
32701
 
32354
32702
  // ../../packages/core/agent-runtime/dist/core/history-committer.js
32703
+ import { randomUUID as randomUUID11 } from "node:crypto";
32355
32704
  var HistoryCommitter = class {
32356
32705
  messages;
32357
32706
  context;
@@ -32366,7 +32715,10 @@ var HistoryCommitter = class {
32366
32715
  this.conversationHistory = options.conversationHistory ?? null;
32367
32716
  }
32368
32717
  commitInput(content) {
32369
- const message = typeof content === "string" ? { role: "user", content } : content;
32718
+ const message = typeof content === "string" ? { role: "user", content } : {
32719
+ ...content,
32720
+ ...content.attachments?.length ? { attachmentKey: content.attachmentKey ?? randomUUID11() } : {}
32721
+ };
32370
32722
  const commitInput = this.context?.commitInput;
32371
32723
  if (typeof commitInput === "function") {
32372
32724
  return this.commitContextMessage((append, rollback) => commitInput.call(this.context, append, rollback), message);
@@ -32385,7 +32737,10 @@ var HistoryCommitter = class {
32385
32737
  if (content === "") {
32386
32738
  return true;
32387
32739
  }
32388
- const message = prepared ?? { role: "user", content };
32740
+ const message = prepared ? {
32741
+ ...prepared,
32742
+ ...prepared.attachments?.length ? { attachmentKey: prepared.attachmentKey ?? randomUUID11() } : {}
32743
+ } : { role: "user", content };
32389
32744
  const commitPending = this.context?.commitPending;
32390
32745
  if (typeof commitPending === "function") {
32391
32746
  return this.commitContextMessage((append, rollback) => commitPending.call(this.context, batch, append, rollback), message);
@@ -32434,21 +32789,23 @@ var HistoryCommitter = class {
32434
32789
  if (call === void 0 || result === void 0) {
32435
32790
  continue;
32436
32791
  }
32792
+ const { attachmentContent: attachments, ...textResult } = result;
32437
32793
  const message = {
32438
32794
  role: "tool-result",
32439
32795
  toolCallId: call.id,
32440
32796
  toolName: call.name,
32441
- content: JSON.stringify(result),
32797
+ content: JSON.stringify(textResult),
32798
+ ...attachments?.length ? { attachments, attachmentKey: randomUUID11() } : {},
32442
32799
  isError: result.ok !== true
32443
32800
  };
32444
32801
  messages.push(message);
32445
- this.messages.push(message);
32446
32802
  }
32447
32803
  this.conversationHistory?.appendToolResults({
32448
32804
  requestId: "tool-results",
32449
32805
  messages,
32450
32806
  recovered: false
32451
32807
  });
32808
+ this.messages.push(...messages);
32452
32809
  }
32453
32810
  commitReminder(content) {
32454
32811
  this.raiseIfCancelled();
@@ -32942,7 +33299,32 @@ function resultText(result) {
32942
33299
  function createSessionRuntime(options) {
32943
33300
  let route = { ...options.route };
32944
33301
  let selectedModel = modelInfo2(options.catalog, route);
32945
- const modelRuntime = new ModelRuntime(options.modelAdapter);
33302
+ let attachmentTurnStart;
33303
+ const adapter = {
33304
+ name: options.modelAdapter.name,
33305
+ runAttempt: (request) => {
33306
+ const history = options.sessionController.history;
33307
+ const entries = history?.entries() ?? [];
33308
+ const boundary = attachmentTurnStart ?? [...entries].reverse().find((entry) => entry.entryType === "user_message")?.seq ?? 0;
33309
+ const protectedKeys = /* @__PURE__ */ new Set();
33310
+ for (const entry of entries) {
33311
+ if (entry.seq < boundary || !("message" in entry.payload)) continue;
33312
+ const message = entry.payload.message;
33313
+ if (!("attachments" in message)) continue;
33314
+ for (const [index] of (message.attachments ?? []).entries()) protectedKeys.add(`${message.attachmentKey}:${index}`);
33315
+ }
33316
+ return options.modelAdapter.runAttempt({ ...request, imageContext: {
33317
+ offloaded: new Set(entries.flatMap((entry) => entry.entryType === "image_offload" ? entry.payload.keys : [])),
33318
+ protectedKeys,
33319
+ persistOffload: (keys2) => {
33320
+ if (!history) throw new Error("Cannot persist image offload without an active session");
33321
+ history.offloadImages(keys2);
33322
+ }
33323
+ } });
33324
+ }
33325
+ };
33326
+ const modelRuntime = new ModelRuntime(adapter);
33327
+ let releaseAttachments;
32946
33328
  const activeConversationHistory = {
32947
33329
  appendSkillContext: (input) => activeHistory(options.sessionController).appendSkillContext(input),
32948
33330
  appendToolDefinitions: (input) => activeHistory(options.sessionController).appendToolDefinitions(input),
@@ -32977,20 +33359,30 @@ function createSessionRuntime(options) {
32977
33359
  }
32978
33360
  });
32979
33361
  const agent = new CodingAgent({
32980
- resources: options.skills ? {
33362
+ resources: {
32981
33363
  prepareInput: async (input, signal, inputs) => {
33364
+ attachmentTurnStart ??= (options.sessionController.history?.entries().at(-1)?.seq ?? 0) + 1;
33365
+ releaseAttachments ??= options.attachments?.protect();
32982
33366
  try {
32983
- return await options.skills.prepareInput(input, signal, inputs);
33367
+ return await options.skills?.prepareInput(input, signal, inputs) ?? { role: "user", content: input };
32984
33368
  } catch (error) {
32985
33369
  if (!signal?.aborted) options.skillInputFailed?.(inputs?.join("\n\n") ?? input, error);
32986
33370
  throw error;
32987
33371
  }
32988
33372
  },
32989
- prepareContext: (messages, signal) => options.skills.prepareContext(messages, signal),
32990
- committed: (messages) => options.skills.committed(messages),
32991
- finishTurn: () => options.skills.finishTurn()
32992
- } : void 0,
32993
- modelAdapter: options.modelAdapter,
33373
+ prepareContext: (messages, signal) => options.skills?.prepareContext(messages, signal) ?? Promise.resolve([]),
33374
+ committed: (messages) => options.skills?.committed(messages),
33375
+ finishTurn: () => {
33376
+ try {
33377
+ options.skills?.finishTurn();
33378
+ } finally {
33379
+ releaseAttachments?.();
33380
+ releaseAttachments = void 0;
33381
+ attachmentTurnStart = void 0;
33382
+ }
33383
+ }
33384
+ },
33385
+ modelAdapter: adapter,
32994
33386
  model: route.model,
32995
33387
  tools: options.tools,
32996
33388
  prepareTools: options.prepareTools,
@@ -33010,6 +33402,7 @@ function createSessionRuntime(options) {
33010
33402
  const entries = history.entries();
33011
33403
  const pendingAssistant = pendingToolCall ? [...entries].reverse().find((entry) => entry.entryType === "assistant_message") : void 0;
33012
33404
  const prepared = await createGovernor(history).prepare({
33405
+ protectedFromSeq: attachmentTurnStart,
33013
33406
  entries: entries.filter((entry) => entry !== pendingAssistant),
33014
33407
  currentProvider: route.provider,
33015
33408
  currentModel: route.model,
@@ -33177,8 +33570,8 @@ function defaultContextPolicy() {
33177
33570
  }
33178
33571
 
33179
33572
  // src/create-mcp-runtime.ts
33180
- import { randomUUID as randomUUID12 } from "node:crypto";
33181
- import { dirname as dirname6, join as join11, resolve as resolve6 } from "node:path";
33573
+ import { randomUUID as randomUUID13 } from "node:crypto";
33574
+ import { dirname as dirname6, join as join12, resolve as resolve6 } from "node:path";
33182
33575
 
33183
33576
  // ../../packages/core/mcp/dist/config.js
33184
33577
  import { resolve as resolve4 } from "node:path";
@@ -33354,14 +33747,14 @@ function createMcpClient(config, version, changed) {
33354
33747
  function waitWithSignal(work, signal) {
33355
33748
  if (signal === void 0)
33356
33749
  return work;
33357
- return new Promise((resolve14, reject) => {
33750
+ return new Promise((resolve15, reject) => {
33358
33751
  const abort = () => {
33359
33752
  signal.removeEventListener("abort", abort);
33360
33753
  reject(signal.reason);
33361
33754
  };
33362
33755
  work.then((value) => {
33363
33756
  signal.removeEventListener("abort", abort);
33364
- resolve14(value);
33757
+ resolve15(value);
33365
33758
  }, (error) => {
33366
33759
  signal.removeEventListener("abort", abort);
33367
33760
  reject(error);
@@ -33447,9 +33840,9 @@ function privateRedirectFetch(base, privateHeaders) {
33447
33840
  import { createHash as createHash6 } from "node:crypto";
33448
33841
 
33449
33842
  // ../../packages/core/mcp/dist/output.js
33450
- import { randomUUID as randomUUID11 } from "node:crypto";
33843
+ import { randomUUID as randomUUID12 } from "node:crypto";
33451
33844
  import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
33452
- import { join as join10 } from "node:path";
33845
+ import { join as join11 } from "node:path";
33453
33846
  function bounded(result, limit) {
33454
33847
  if (Buffer.byteLength(JSON.stringify(result), "utf8") <= limit)
33455
33848
  return result;
@@ -33510,13 +33903,13 @@ async function adaptMcpResult(result, options) {
33510
33903
  if (!oversized && media.length === 0)
33511
33904
  return preview;
33512
33905
  try {
33513
- const directory = join10(options.artifactRoot, randomUUID11());
33906
+ const directory = join11(options.artifactRoot, randomUUID12());
33514
33907
  await mkdir3(directory, { recursive: true, mode: 448 });
33515
- const path5 = join10(directory, "result.json");
33908
+ const path5 = join11(directory, "result.json");
33516
33909
  await writeFile3(path5, JSON.stringify(business), { mode: 384, flag: "wx" });
33517
33910
  const attachments = [];
33518
33911
  for (const [index, item] of media.entries()) {
33519
- const path6 = join10(directory, `attachment-${index}.bin`);
33912
+ const path6 = join11(directory, `attachment-${index}.bin`);
33520
33913
  const decoded = Buffer.from(item.data, "base64");
33521
33914
  if (decoded.toString("base64").replace(/=+$/, "") !== item.data.replace(/=+$/, ""))
33522
33915
  throw new Error("Invalid base64 attachment");
@@ -33921,8 +34314,8 @@ async function createOAuthCallback(options) {
33921
34314
  let accept;
33922
34315
  let reject;
33923
34316
  let settled = false;
33924
- const response = new Promise((resolve14, fail) => {
33925
- accept = resolve14;
34317
+ const response = new Promise((resolve15, fail) => {
34318
+ accept = resolve15;
33926
34319
  reject = fail;
33927
34320
  });
33928
34321
  void response.catch(() => {
@@ -33952,9 +34345,9 @@ async function createOAuthCallback(options) {
33952
34345
  reply.writeHead(200, { "Content-Type": "text/plain", "Cache-Control": "no-store" }).end("Authorization received. Return to laohuang.");
33953
34346
  accept(url.searchParams);
33954
34347
  });
33955
- await new Promise((resolve14, fail) => {
34348
+ await new Promise((resolve15, fail) => {
33956
34349
  server.once("error", fail);
33957
- server.listen(0, "127.0.0.1", resolve14);
34350
+ server.listen(0, "127.0.0.1", resolve15);
33958
34351
  });
33959
34352
  const address = server.address();
33960
34353
  if (address === null || typeof address === "string")
@@ -33969,8 +34362,8 @@ async function createOAuthCallback(options) {
33969
34362
  settled = true;
33970
34363
  reject(new Error("OAuth authorization cancelled"));
33971
34364
  }
33972
- closing = new Promise((resolve14) => {
33973
- server.close(() => resolve14());
34365
+ closing = new Promise((resolve15) => {
34366
+ server.close(() => resolve15());
33974
34367
  server.closeAllConnections();
33975
34368
  });
33976
34369
  return closing;
@@ -34493,7 +34886,7 @@ var ConnectionManager = class {
34493
34886
  // src/create-mcp-runtime.ts
34494
34887
  async function createMcpRuntime(options) {
34495
34888
  const directory = dirname6(resolve6(options.configPath));
34496
- const paths = [.../* @__PURE__ */ new Set([join11(directory, "mcp.json"), resolve6(options.projectRoot, ".laohuang/mcp.json")])];
34889
+ const paths = [.../* @__PURE__ */ new Set([join12(directory, "mcp.json"), resolve6(options.projectRoot, ".laohuang/mcp.json")])];
34497
34890
  const readSources = async (reload) => {
34498
34891
  const sources = [];
34499
34892
  let invalid2 = false;
@@ -34523,8 +34916,8 @@ ${url}`, tone: "info" });
34523
34916
  projectRoot: options.projectRoot,
34524
34917
  version: options.version,
34525
34918
  env: options.env,
34526
- credentials: new McpCredentialFileStore(join11(directory, "mcp-credentials")),
34527
- artifactRoot: join11(directory, "mcp-artifacts", randomUUID12()),
34919
+ credentials: new McpCredentialFileStore(join12(directory, "mcp-credentials")),
34920
+ artifactRoot: join12(directory, "mcp-artifacts", randomUUID13()),
34528
34921
  onAuthorization: ({ serverId, url }) => showAuthorization(serverId, url),
34529
34922
  onLoginState: (active) => {
34530
34923
  if (active) process.on("SIGINT", cancel);
@@ -34760,10 +35153,10 @@ var ReaddirpStream = class extends Readable {
34760
35153
  }
34761
35154
  async _formatEntry(dirent, path5) {
34762
35155
  let entry;
34763
- const basename9 = this._isDirent ? dirent.name : dirent;
35156
+ const basename10 = this._isDirent ? dirent.name : dirent;
34764
35157
  try {
34765
- const fullPath = presolve(pjoin(path5, basename9));
34766
- entry = { path: prelative(this._root, fullPath), fullPath, basename: basename9 };
35158
+ const fullPath = presolve(pjoin(path5, basename10));
35159
+ entry = { path: prelative(this._root, fullPath), fullPath, basename: basename10 };
34767
35160
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
34768
35161
  } catch (err) {
34769
35162
  this._onError(err);
@@ -34834,7 +35227,7 @@ function readdirp(root, options = {}) {
34834
35227
 
34835
35228
  // ../../packages/fs/file-watcher/node_modules/chokidar/esm/handler.js
34836
35229
  import { watchFile, unwatchFile, watch as fs_watch } from "fs";
34837
- import { open as open2, stat as stat3, lstat as lstat2, realpath as fsrealpath } from "fs/promises";
35230
+ import { open as open3, stat as stat3, lstat as lstat2, realpath as fsrealpath } from "fs/promises";
34838
35231
  import * as sysPath from "path";
34839
35232
  import { type as osType } from "os";
34840
35233
  var STR_DATA = "data";
@@ -35218,7 +35611,7 @@ var setFsWatchListener = (path5, fullPath, options, handlers) => {
35218
35611
  cont.watcherUnusable = true;
35219
35612
  if (isWindows && error.code === "EPERM") {
35220
35613
  try {
35221
- const fd = await open2(path5, "r");
35614
+ const fd = await open3(path5, "r");
35222
35615
  await fd.close();
35223
35616
  broadcastErr(error);
35224
35617
  } catch (err) {
@@ -35302,9 +35695,9 @@ var NodeFsHandler = class {
35302
35695
  _watchWithNodeFs(path5, listener) {
35303
35696
  const opts = this.fsw.options;
35304
35697
  const directory = sysPath.dirname(path5);
35305
- const basename9 = sysPath.basename(path5);
35698
+ const basename10 = sysPath.basename(path5);
35306
35699
  const parent = this.fsw._getWatchedDir(directory);
35307
- parent.add(basename9);
35700
+ parent.add(basename10);
35308
35701
  const absolutePath = sysPath.resolve(path5);
35309
35702
  const options = {
35310
35703
  persistent: opts.persistent
@@ -35314,7 +35707,7 @@ var NodeFsHandler = class {
35314
35707
  let closer;
35315
35708
  if (opts.usePolling) {
35316
35709
  const enableBin = opts.interval !== opts.binaryInterval;
35317
- options.interval = enableBin && isBinaryPath(basename9) ? opts.binaryInterval : opts.interval;
35710
+ options.interval = enableBin && isBinaryPath(basename10) ? opts.binaryInterval : opts.interval;
35318
35711
  closer = setFsWatchFileListener(path5, absolutePath, options, {
35319
35712
  listener,
35320
35713
  rawEmitter: this.fsw._emitRaw
@@ -35337,10 +35730,10 @@ var NodeFsHandler = class {
35337
35730
  return;
35338
35731
  }
35339
35732
  const dirname14 = sysPath.dirname(file);
35340
- const basename9 = sysPath.basename(file);
35733
+ const basename10 = sysPath.basename(file);
35341
35734
  const parent = this.fsw._getWatchedDir(dirname14);
35342
35735
  let prevStats = stats;
35343
- if (parent.has(basename9))
35736
+ if (parent.has(basename10))
35344
35737
  return;
35345
35738
  const listener = async (path5, newStats) => {
35346
35739
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
@@ -35365,9 +35758,9 @@ var NodeFsHandler = class {
35365
35758
  prevStats = newStats2;
35366
35759
  }
35367
35760
  } catch (error) {
35368
- this.fsw._remove(dirname14, basename9);
35761
+ this.fsw._remove(dirname14, basename10);
35369
35762
  }
35370
- } else if (parent.has(basename9)) {
35763
+ } else if (parent.has(basename10)) {
35371
35764
  const at = newStats.atimeMs;
35372
35765
  const mt = newStats.mtimeMs;
35373
35766
  if (!at || at <= mt || mt !== prevStats.mtimeMs) {
@@ -35461,7 +35854,7 @@ var NodeFsHandler = class {
35461
35854
  this._addToNodeFs(path5, initialAdd, wh, depth + 1);
35462
35855
  }
35463
35856
  }).on(EV.ERROR, this._boundHandleError);
35464
- return new Promise((resolve14, reject) => {
35857
+ return new Promise((resolve15, reject) => {
35465
35858
  if (!stream)
35466
35859
  return reject();
35467
35860
  stream.once(STR_END, () => {
@@ -35470,7 +35863,7 @@ var NodeFsHandler = class {
35470
35863
  return;
35471
35864
  }
35472
35865
  const wasThrottled = throttler ? throttler.clear() : false;
35473
- resolve14(void 0);
35866
+ resolve15(void 0);
35474
35867
  previous.getChildren().filter((item) => {
35475
35868
  return item !== directory && !current.has(item);
35476
35869
  }).forEach((item) => {
@@ -36550,8 +36943,8 @@ var Handle = class {
36550
36943
  constructor(task, release) {
36551
36944
  this.task = task;
36552
36945
  this.release = release;
36553
- this.ready = new Promise((resolve14, reject) => {
36554
- this.resolveReady = resolve14;
36946
+ this.ready = new Promise((resolve15, reject) => {
36947
+ this.resolveReady = resolve15;
36555
36948
  this.rejectReady = reject;
36556
36949
  });
36557
36950
  void this.ready.catch(() => {
@@ -36862,7 +37255,7 @@ var SkillRegistry = class {
36862
37255
 
36863
37256
  // ../../packages/skill/skill-filesystem/dist/index.js
36864
37257
  import { lstat as lstat3, readFile as readFile3, readlink, readdir as readdir4, realpath as realpath2 } from "node:fs/promises";
36865
- import { dirname as dirname10, isAbsolute as isAbsolute3, join as join14, parse as parse2, relative as relative4, resolve as resolve11, sep as sep2 } from "node:path";
37258
+ import { dirname as dirname10, isAbsolute as isAbsolute3, join as join15, parse as parse2, relative as relative4, resolve as resolve11, sep as sep2 } from "node:path";
36866
37259
 
36867
37260
  // ../../packages/skill/skill-filesystem/dist/parser.js
36868
37261
  var import_yaml = __toESM(require_dist(), 1);
@@ -36893,11 +37286,11 @@ function parseSkill(text3) {
36893
37286
  // ../../packages/skill/skill-filesystem/dist/index.js
36894
37287
  function skillRoots(projectRoot, home, extras) {
36895
37288
  return [
36896
- { path: join14(projectRoot, ".laohuang/skills"), priority: 100 },
36897
- { path: join14(projectRoot, ".agents/skills"), priority: 200 },
37289
+ { path: join15(projectRoot, ".laohuang/skills"), priority: 100 },
37290
+ { path: join15(projectRoot, ".agents/skills"), priority: 200 },
36898
37291
  ...extras.map((path5, index) => ({ path: path5, priority: 300 + index / (extras.length + 1) })),
36899
- { path: join14(home, ".laohuang/skills"), priority: 400 },
36900
- { path: join14(home, ".agents/skills"), priority: 500 }
37292
+ { path: join15(home, ".laohuang/skills"), priority: 400 },
37293
+ { path: join15(home, ".agents/skills"), priority: 500 }
36901
37294
  ];
36902
37295
  }
36903
37296
  function parseSkillDirs(value, configPath, home) {
@@ -37068,7 +37461,7 @@ var FileSystemSkillProvider = class {
37068
37461
  let current = root;
37069
37462
  let followed = false;
37070
37463
  for (let index = 0; index < parts.length; index++) {
37071
- current = join14(current, parts[index]);
37464
+ current = join15(current, parts[index]);
37072
37465
  let stat6;
37073
37466
  try {
37074
37467
  stat6 = await lstat3(current);
@@ -37140,13 +37533,13 @@ var FileSystemSkillProvider = class {
37140
37533
  }
37141
37534
  children.sort((a, b) => compareSkillPaths(a.name, b.name));
37142
37535
  if (children.some((child) => child.name === "SKILL.md")) {
37143
- await file(join14(path5, "SKILL.md"));
37536
+ await file(join15(path5, "SKILL.md"));
37144
37537
  return;
37145
37538
  }
37146
37539
  for (const child of children) {
37147
37540
  if (child.name.startsWith(".") || child.name === "node_modules")
37148
37541
  continue;
37149
- const childPath = join14(path5, child.name);
37542
+ const childPath = join15(path5, child.name);
37150
37543
  if (child.isDirectory())
37151
37544
  await walk(childPath, false);
37152
37545
  else if (child.isSymbolicLink()) {
@@ -37231,7 +37624,7 @@ var FileSystemSkillProvider = class {
37231
37624
  };
37232
37625
 
37233
37626
  // ../../packages/skill/tool-skill/dist/index.js
37234
- import { randomUUID as randomUUID13 } from "node:crypto";
37627
+ import { randomUUID as randomUUID14 } from "node:crypto";
37235
37628
  function parseSkillCommand(input) {
37236
37629
  const match = /^\s*\/skill:([^\s]+)(?:[ \t]|\r?\n)?([\s\S]*)$/.exec(input);
37237
37630
  return match ? { name: match[1], arguments: match[2] } : null;
@@ -37281,7 +37674,7 @@ var SkillSession = class {
37281
37674
  const skill = await this.registry.load(args["name"], context.signal);
37282
37675
  if (context.isCancelled() || this.closed)
37283
37676
  throw new Error("Skill load cancelled");
37284
- const key = randomUUID13();
37677
+ const key = randomUUID14();
37285
37678
  this.pending.set(key, { session: this.state, skill });
37286
37679
  return { ok: true, content: render(skill), skillReceipt: key, skill: metadata(skill) };
37287
37680
  }
@@ -37304,7 +37697,7 @@ var SkillSession = class {
37304
37697
  signal?.throwIfAborted();
37305
37698
  if (this.closed)
37306
37699
  throw new Error("Skill session is closed");
37307
- const key = randomUUID13();
37700
+ const key = randomUUID14();
37308
37701
  this.pending.set(key, { session: this.state, skill });
37309
37702
  return {
37310
37703
  role: "user",
@@ -37495,6 +37888,500 @@ async function createSkillRuntime(options) {
37495
37888
  };
37496
37889
  }
37497
37890
 
37891
+ // src/create-attachment-runtime.ts
37892
+ import { mkdirSync as mkdirSync6 } from "node:fs";
37893
+
37894
+ // ../../packages/attachment/attachment-local/dist/local-attachment-store.js
37895
+ import { createHash as createHash10, randomUUID as randomUUID16 } from "node:crypto";
37896
+ import { chmodSync as chmodSync4, existsSync as existsSync8, lstatSync as lstatSync3, mkdirSync as mkdirSync5, readdirSync as readdirSync3, rmSync } from "node:fs";
37897
+ import { mkdir as mkdir5, readFile as readFile5, rename as rename2, unlink as unlink4, writeFile as writeFile4 } from "node:fs/promises";
37898
+ import { join as join17, resolve as resolve13 } from "node:path";
37899
+ import { DatabaseSync } from "node:sqlite";
37900
+ import sharp2 from "sharp";
37901
+
37902
+ // ../../packages/attachment/attachment-local/dist/object-store.js
37903
+ import { createHash as createHash9, randomUUID as randomUUID15 } from "node:crypto";
37904
+ import { constants as constants2 } from "node:fs";
37905
+ import { mkdir as mkdir4, open as open4, link, unlink as unlink3, chmod } from "node:fs/promises";
37906
+ import { join as join16 } from "node:path";
37907
+ async function syncDirectory(path5) {
37908
+ if (process.platform === "win32")
37909
+ return;
37910
+ const handle = await open4(path5, "r");
37911
+ try {
37912
+ await handle.sync();
37913
+ } finally {
37914
+ await handle.close();
37915
+ }
37916
+ }
37917
+ async function writeObject(root, source, name, signal) {
37918
+ const temporary = join16(root, "tmp", randomUUID15());
37919
+ const handle = await open4(temporary, "wx", 384);
37920
+ const hash = createHash9("sha256");
37921
+ let bytes = 0;
37922
+ try {
37923
+ for await (const input of source) {
37924
+ signal?.throwIfAborted();
37925
+ const chunk = Buffer.from(input);
37926
+ bytes += chunk.byteLength;
37927
+ if (!Number.isSafeInteger(bytes))
37928
+ throw new AttachmentError("LIMIT_EXCEEDED", "File is too large");
37929
+ hash.update(chunk);
37930
+ await handle.writeFile(chunk);
37931
+ }
37932
+ signal?.throwIfAborted();
37933
+ await handle.sync();
37934
+ const ref = {
37935
+ id: `sha256:${hash.digest("hex")}`,
37936
+ bytes,
37937
+ name: name.split(/[\\/]/).at(-1).replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 255)
37938
+ };
37939
+ const directory = join16(root, "objects", ref.id.slice(7, 9));
37940
+ await mkdir4(directory, { recursive: true, mode: 448 });
37941
+ await syncDirectory(join16(root, "objects"));
37942
+ const target = join16(directory, ref.id.slice(7));
37943
+ try {
37944
+ await link(temporary, target);
37945
+ } catch (error) {
37946
+ if (!(error instanceof Error && "code" in error && error.code === "EEXIST"))
37947
+ throw error;
37948
+ for await (const _chunk of readObject(root, ref, signal)) {
37949
+ }
37950
+ }
37951
+ await chmod(target, 384);
37952
+ await syncDirectory(directory);
37953
+ return ref;
37954
+ } finally {
37955
+ await handle.close();
37956
+ await unlink3(temporary);
37957
+ }
37958
+ }
37959
+ async function* readObject(root, ref, signal) {
37960
+ assertFileRef(ref);
37961
+ signal?.throwIfAborted();
37962
+ let handle;
37963
+ try {
37964
+ handle = await open4(join16(root, "objects", ref.id.slice(7, 9), ref.id.slice(7)), constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
37965
+ } catch (error) {
37966
+ throw new AttachmentError(error instanceof Error && "code" in error && error.code === "ENOENT" ? "NOT_FOUND" : "IO", `Cannot read attachment ${ref.id}`, { cause: error });
37967
+ }
37968
+ try {
37969
+ const info = await handle.stat();
37970
+ if (!info.isFile() || info.size !== ref.bytes)
37971
+ throw new AttachmentError("CORRUPT", `Attachment size mismatch: ${ref.id}`);
37972
+ const hash = createHash9("sha256");
37973
+ let bytes = 0;
37974
+ while (true) {
37975
+ signal?.throwIfAborted();
37976
+ const chunk = Buffer.allocUnsafe(64 * 1024);
37977
+ const { bytesRead } = await handle.read(chunk);
37978
+ if (bytesRead === 0)
37979
+ break;
37980
+ const part = chunk.subarray(0, bytesRead);
37981
+ bytes += bytesRead;
37982
+ hash.update(part);
37983
+ yield part;
37984
+ }
37985
+ if (bytes !== ref.bytes || `sha256:${hash.digest("hex")}` !== ref.id)
37986
+ throw new AttachmentError("CORRUPT", `Attachment digest mismatch: ${ref.id}`);
37987
+ } finally {
37988
+ await handle.close();
37989
+ }
37990
+ }
37991
+
37992
+ // ../../packages/attachment/attachment-local/dist/image.js
37993
+ import sharp from "sharp";
37994
+ var MIME = { png: "image/png", jpeg: "image/jpeg", webp: "image/webp", gif: "image/gif" };
37995
+ async function inspectImage(input, limits) {
37996
+ if (input.data.byteLength === 0 || input.data.byteLength > limits.maxBytes)
37997
+ throw new AttachmentError("LIMIT_EXCEEDED", "Image source byte limit exceeded or empty image");
37998
+ try {
37999
+ const decoder = sharp(input.data, { failOn: "warning", limitInputPixels: limits.maxPixels });
38000
+ const meta = await decoder.metadata();
38001
+ if (!meta.format || !(meta.format in MIME))
38002
+ throw new AttachmentError("INVALID_IMAGE", "Only PNG, JPEG, WebP and GIF are accepted");
38003
+ const mimeType = MIME[meta.format];
38004
+ if (input.mimeType !== void 0 && input.mimeType !== mimeType)
38005
+ throw new AttachmentError("INVALID_IMAGE", "Image MIME does not match its bytes");
38006
+ const rotated = (meta.orientation ?? 1) >= 5;
38007
+ const width = rotated ? meta.height : meta.width;
38008
+ const height = rotated ? meta.width : meta.height;
38009
+ if (width * height > limits.maxPixels || Math.max(width, height) > limits.maxDimension)
38010
+ throw new AttachmentError("LIMIT_EXCEEDED", "Image pixel or dimension limit exceeded");
38011
+ await decoder.stats();
38012
+ return { mimeType, width, height, animated: (meta.pages ?? 1) > 1 };
38013
+ } catch (error) {
38014
+ if (error instanceof AttachmentError)
38015
+ throw error;
38016
+ throw new AttachmentError("INVALID_IMAGE", "Image cannot be decoded safely", { cause: error });
38017
+ }
38018
+ }
38019
+ function validateTarget(target) {
38020
+ assertImageView(target);
38021
+ if (![target.maxBytes, target.maxPixels, target.maxDimension].every((n) => Number.isSafeInteger(n) && n > 0) || target.formats.length === 0 || target.formats.some((f) => !["image/png", "image/jpeg", "image/webp"].includes(f))) {
38022
+ throw new AttachmentError("LIMIT_EXCEEDED", "Invalid image request limits");
38023
+ }
38024
+ }
38025
+ async function renderImage(data, ref, target, variantId, signal) {
38026
+ validateTarget(target);
38027
+ const region = target.region;
38028
+ if (region && (region.x + region.width > ref.width || region.y + region.height > ref.height))
38029
+ throw new AttachmentError("LIMIT_EXCEEDED", "Crop is outside the oriented original image");
38030
+ const sourceWidth = region?.width ?? ref.width;
38031
+ const sourceHeight = region?.height ?? ref.height;
38032
+ let scale = Math.min(1, Math.sqrt(target.maxPixels / (sourceWidth * sourceHeight)), target.maxDimension / Math.max(sourceWidth, sourceHeight));
38033
+ if (target.fullResolution && scale < 1)
38034
+ throw new AttachmentError("LIMIT_EXCEEDED", "Native resolution exceeds image pixel limits; use a crop or allow resizing");
38035
+ const base = sharp(data, { failOn: "warning", limitInputPixels: ref.width * ref.height }).rotate().toColourspace("srgb");
38036
+ if (region)
38037
+ base.extract({ left: region.x, top: region.y, width: region.width, height: region.height });
38038
+ const alpha = (await base.metadata()).hasAlpha;
38039
+ const formats2 = target.formats.filter((f) => !alpha || f !== "image/jpeg");
38040
+ if (formats2.length === 0)
38041
+ throw new AttachmentError("LIMIT_EXCEEDED", "Request formats cannot preserve image transparency");
38042
+ let previous = "";
38043
+ while (true) {
38044
+ signal?.throwIfAborted();
38045
+ const width = Math.max(1, Math.floor(sourceWidth * scale));
38046
+ const height = Math.max(1, Math.floor(sourceHeight * scale));
38047
+ if (width * height > target.maxPixels) {
38048
+ scale *= 0.75;
38049
+ continue;
38050
+ }
38051
+ const dimensions = `${width}x${height}`;
38052
+ if (dimensions === previous)
38053
+ break;
38054
+ previous = dimensions;
38055
+ for (const mimeType of formats2) {
38056
+ for (const quality of mimeType === "image/png" ? [100] : [85, 75, 60]) {
38057
+ signal?.throwIfAborted();
38058
+ const pipeline = base.clone().resize(width, height, { fit: "fill", withoutEnlargement: true });
38059
+ const encoded = mimeType === "image/png" ? pipeline.png() : mimeType === "image/webp" ? pipeline.webp({ quality }) : pipeline.jpeg({ quality });
38060
+ const { data: output, info } = await encoded.toBuffer({ resolveWithObject: true });
38061
+ if (output.length <= target.maxBytes) {
38062
+ signal?.throwIfAborted();
38063
+ return {
38064
+ data: output,
38065
+ mimeType,
38066
+ width: info.width,
38067
+ height: info.height,
38068
+ variantId,
38069
+ note: `Original oriented size ${ref.width}x${ref.height}; sent ${info.width}x${info.height}.` + (region ? ` Crop origin (${region.x},${region.y}), source region ${region.width}x${region.height}; scale coordinates to that region then add the origin.` : " Scale coordinates to the original size.") + (ref.animated ? " Animation: only the first frame is shown." : "")
38070
+ };
38071
+ }
38072
+ }
38073
+ }
38074
+ if (target.fullResolution || width === 1 && height === 1)
38075
+ break;
38076
+ scale *= 0.75;
38077
+ }
38078
+ throw new AttachmentError("LIMIT_EXCEEDED", "Image cannot fit the request byte limit without violating the requested view");
38079
+ }
38080
+
38081
+ // ../../packages/attachment/attachment-local/dist/local-attachment-store.js
38082
+ var LocalAttachmentStore = class extends AttachmentStore {
38083
+ root;
38084
+ imageLimits;
38085
+ db;
38086
+ now;
38087
+ concurrency;
38088
+ active = 0;
38089
+ waiting = [];
38090
+ operations = /* @__PURE__ */ new Set();
38091
+ closed = false;
38092
+ constructor(options) {
38093
+ super();
38094
+ this.root = resolve13(options.root);
38095
+ this.now = options.now ?? Date.now;
38096
+ this.imageLimits = Object.freeze({ ...DEFAULT_IMAGE_LIMITS, ...options.imageLimits });
38097
+ this.concurrency = options.compressionConcurrency ?? 2;
38098
+ if (![...Object.values(this.imageLimits), this.concurrency].every((n) => Number.isSafeInteger(n) && n > 0) || this.concurrency > 8)
38099
+ throw new AttachmentError("LIMIT_EXCEEDED", "Invalid attachment configuration");
38100
+ for (const path5 of [this.root, join17(this.root, "objects"), join17(this.root, "tmp"), join17(this.root, "variants")]) {
38101
+ mkdirSync5(path5, { recursive: true, mode: 448 });
38102
+ }
38103
+ this.db = new DatabaseSync(join17(this.root, "metadata.sqlite"));
38104
+ chmodSync4(join17(this.root, "metadata.sqlite"), 384);
38105
+ this.db.exec(`PRAGMA busy_timeout=5000; PRAGMA journal_mode=DELETE; PRAGMA synchronous=FULL;
38106
+ CREATE TABLE IF NOT EXISTS objects(id TEXT PRIMARY KEY, created INTEGER NOT NULL, unreferenced INTEGER, referenced INTEGER NOT NULL DEFAULT 0);
38107
+ CREATE TABLE IF NOT EXISTS operations(id TEXT PRIMARY KEY, pid INTEGER NOT NULL);
38108
+ CREATE TABLE IF NOT EXISTS settings(key TEXT PRIMARY KEY, value INTEGER NOT NULL);`);
38109
+ }
38110
+ transaction(action) {
38111
+ if (this.closed)
38112
+ throw new AttachmentError("CLOSED", "Attachment store is closed");
38113
+ this.db.exec("BEGIN IMMEDIATE");
38114
+ try {
38115
+ const result = action();
38116
+ this.db.exec("COMMIT");
38117
+ return result;
38118
+ } catch (error) {
38119
+ this.db.exec("ROLLBACK");
38120
+ throw error;
38121
+ }
38122
+ }
38123
+ protect() {
38124
+ const id = randomUUID16();
38125
+ this.transaction(() => this.db.prepare("INSERT INTO operations VALUES (?, ?)").run(id, process.pid));
38126
+ this.operations.add(id);
38127
+ let released = false;
38128
+ return () => {
38129
+ if (released)
38130
+ return;
38131
+ released = true;
38132
+ if (!this.closed)
38133
+ this.db.prepare("DELETE FROM operations WHERE id=?").run(id);
38134
+ this.operations.delete(id);
38135
+ };
38136
+ }
38137
+ async limited(task) {
38138
+ if (this.active >= this.concurrency)
38139
+ await new Promise((resolve15) => this.waiting.push(resolve15));
38140
+ else
38141
+ this.active++;
38142
+ try {
38143
+ return await task();
38144
+ } finally {
38145
+ const next = this.waiting.shift();
38146
+ if (next)
38147
+ next();
38148
+ else
38149
+ this.active--;
38150
+ }
38151
+ }
38152
+ async saveFile(source, name, signal) {
38153
+ const release = this.protect();
38154
+ try {
38155
+ const ref = await writeObject(this.root, source, name, signal);
38156
+ this.transaction(() => this.db.prepare("INSERT OR IGNORE INTO objects VALUES (?, ?, ?, 0)").run(ref.id, this.now(), this.now()));
38157
+ return ref;
38158
+ } finally {
38159
+ release();
38160
+ }
38161
+ }
38162
+ async *readFile(ref, signal) {
38163
+ const release = this.protect();
38164
+ try {
38165
+ yield* readObject(this.root, ref, signal);
38166
+ } finally {
38167
+ release();
38168
+ }
38169
+ }
38170
+ async saveImages(inputs, signal) {
38171
+ const release = this.protect();
38172
+ try {
38173
+ if (inputs.length > this.imageLimits.maxImages || inputs.reduce((sum, input) => sum + input.data.byteLength, 0) > this.imageLimits.maxBatchBytes)
38174
+ throw new AttachmentError("LIMIT_EXCEEDED", "Image batch exceeds count or byte limits");
38175
+ const owned = inputs.map((input) => ({ ...input, data: Buffer.from(input.data) }));
38176
+ const inspected = [];
38177
+ for (const input of owned) {
38178
+ signal?.throwIfAborted();
38179
+ inspected.push(await this.limited(() => inspectImage(input, this.imageLimits)));
38180
+ }
38181
+ const refs = [];
38182
+ for (const [index, input] of owned.entries()) {
38183
+ const ref = await this.saveFile((async function* () {
38184
+ yield input.data;
38185
+ })(), input.name, signal);
38186
+ refs.push({ ...ref, ...inspected[index] });
38187
+ }
38188
+ return refs;
38189
+ } finally {
38190
+ release();
38191
+ }
38192
+ }
38193
+ async prepareImage(ref, target, signal) {
38194
+ assertAttachmentContent({ type: "image", ref });
38195
+ validateTarget(target);
38196
+ const release = this.protect();
38197
+ try {
38198
+ if (ref.bytes > this.imageLimits.maxBytes)
38199
+ throw new AttachmentError("LIMIT_EXCEEDED", "Stored image exceeds configured source limit");
38200
+ const chunks = [];
38201
+ for await (const chunk of this.readFile(ref, signal))
38202
+ chunks.push(chunk);
38203
+ const data = Buffer.concat(chunks);
38204
+ return await this.limited(async () => {
38205
+ const actual = await inspectImage({ data, name: ref.name, mimeType: ref.mimeType }, this.imageLimits);
38206
+ if (actual.width !== ref.width || actual.height !== ref.height || actual.animated !== ref.animated)
38207
+ throw new AttachmentError("CORRUPT", "Image reference metadata mismatch");
38208
+ const variantId = createHash10("sha256").update(JSON.stringify({
38209
+ version: 1,
38210
+ id: ref.id,
38211
+ pixels: target.maxPixels,
38212
+ dimension: target.maxDimension,
38213
+ bytes: target.maxBytes,
38214
+ formats: target.formats,
38215
+ region: target.region ?? null,
38216
+ full: target.fullResolution ?? false
38217
+ })).digest("hex");
38218
+ const directory = join17(this.root, "variants", ref.id.slice(7));
38219
+ const path5 = join17(directory, variantId + ".json");
38220
+ try {
38221
+ const raw = JSON.parse(await readFile5(path5, { encoding: "utf8", signal }));
38222
+ const cached = raw;
38223
+ if (cached.variantId === variantId && typeof cached.data === "string" && typeof cached.digest === "string" && typeof cached.note === "string") {
38224
+ const bytes = Buffer.from(cached.data, "base64");
38225
+ if (bytes.length <= target.maxBytes && createHash10("sha256").update(bytes).digest("hex") === cached.digest) {
38226
+ const decoder = sharp2(bytes, { failOn: "warning", limitInputPixels: target.maxPixels });
38227
+ const meta = await decoder.metadata();
38228
+ if (meta.width === cached.width && meta.height === cached.height && Math.max(meta.width, meta.height) <= target.maxDimension && meta.width * meta.height <= target.maxPixels && cached.mimeType === `image/${meta.format}` && target.formats.includes(cached.mimeType)) {
38229
+ await decoder.stats();
38230
+ signal?.throwIfAborted();
38231
+ return { data: bytes, width: meta.width, height: meta.height, mimeType: cached.mimeType, variantId, note: cached.note };
38232
+ }
38233
+ }
38234
+ }
38235
+ } catch (error) {
38236
+ signal?.throwIfAborted();
38237
+ }
38238
+ const result = await renderImage(data, ref, target, variantId, signal);
38239
+ await mkdir5(directory, { recursive: true, mode: 448 });
38240
+ const temporary = join17(this.root, "tmp", randomUUID16());
38241
+ try {
38242
+ await writeFile4(temporary, JSON.stringify({ ...result, data: Buffer.from(result.data).toString("base64"), digest: createHash10("sha256").update(result.data).digest("hex") }), { flag: "wx", mode: 384 });
38243
+ await rename2(temporary, path5);
38244
+ } finally {
38245
+ await unlink4(temporary).catch((error) => {
38246
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
38247
+ throw error;
38248
+ });
38249
+ }
38250
+ return result;
38251
+ });
38252
+ } finally {
38253
+ release();
38254
+ }
38255
+ }
38256
+ commit(refs, write) {
38257
+ return this.transaction(() => {
38258
+ for (const ref of refs) {
38259
+ assertFileRef(ref);
38260
+ const path5 = join17(this.root, "objects", ref.id.slice(7, 9), ref.id.slice(7));
38261
+ if (!existsSync8(path5))
38262
+ throw new AttachmentError("NOT_FOUND", `Attachment missing before commit: ${ref.id}`);
38263
+ const info = lstatSync3(path5);
38264
+ if (!info.isFile() || info.size !== ref.bytes)
38265
+ throw new AttachmentError("CORRUPT", "Attachment changed before commit");
38266
+ this.db.prepare("INSERT OR IGNORE INTO objects VALUES (?, ?, NULL, 1)").run(ref.id, this.now());
38267
+ this.db.prepare("UPDATE objects SET referenced=1, unreferenced=NULL WHERE id=?").run(ref.id);
38268
+ }
38269
+ const result = write();
38270
+ if (result instanceof Promise)
38271
+ throw new TypeError("Attachment commit callback must be synchronous");
38272
+ return result;
38273
+ });
38274
+ }
38275
+ collectGarbage(references, force = false) {
38276
+ return this.transaction(() => {
38277
+ const now = this.now();
38278
+ const last = this.db.prepare("SELECT value FROM settings WHERE key='lastCleanup'").get();
38279
+ if (!force && last && now - last.value < CLEANUP_INTERVAL_MS)
38280
+ return { deleted: [], skipped: true };
38281
+ const operations2 = this.db.prepare("SELECT id,pid FROM operations").all();
38282
+ for (const operation of operations2) {
38283
+ let alive = true;
38284
+ try {
38285
+ process.kill(operation.pid, 0);
38286
+ } catch (error) {
38287
+ if (error instanceof Error && "code" in error && error.code === "ESRCH")
38288
+ alive = false;
38289
+ }
38290
+ if (alive)
38291
+ return { deleted: [], skipped: true };
38292
+ this.db.prepare("DELETE FROM operations WHERE id=?").run(operation.id);
38293
+ }
38294
+ const retained = references();
38295
+ for (const prefix of readdirSync3(join17(this.root, "objects"))) {
38296
+ if (!/^[a-f0-9]{2}$/.test(prefix))
38297
+ continue;
38298
+ for (const hash of readdirSync3(join17(this.root, "objects", prefix))) {
38299
+ if (!/^[a-f0-9]{64}$/.test(hash) || !hash.startsWith(prefix))
38300
+ continue;
38301
+ const info = lstatSync3(join17(this.root, "objects", prefix, hash));
38302
+ if (!info.isFile())
38303
+ throw new AttachmentError("CORRUPT", "Unexpected attachment object type");
38304
+ this.db.prepare("INSERT OR IGNORE INTO objects VALUES (?, ?, ?, 0)").run(`sha256:${hash}`, info.mtimeMs, info.mtimeMs);
38305
+ }
38306
+ }
38307
+ const rows = this.db.prepare("SELECT * FROM objects").all();
38308
+ const deleted = [];
38309
+ for (const row of rows) {
38310
+ if (retained.has(row.id)) {
38311
+ this.db.prepare("UPDATE objects SET referenced=1, unreferenced=NULL WHERE id=?").run(row.id);
38312
+ continue;
38313
+ }
38314
+ if (row.unreferenced === null) {
38315
+ this.db.prepare("UPDATE objects SET unreferenced=? WHERE id=?").run(now, row.id);
38316
+ continue;
38317
+ }
38318
+ if (now - row.unreferenced < RETENTION_MS)
38319
+ continue;
38320
+ rmSync(join17(this.root, "variants", row.id.slice(7)), { recursive: true, force: true });
38321
+ rmSync(join17(this.root, "objects", row.id.slice(7, 9), row.id.slice(7)), { force: true });
38322
+ this.db.prepare("DELETE FROM objects WHERE id=?").run(row.id);
38323
+ deleted.push(row.id);
38324
+ }
38325
+ for (const file of readdirSync3(join17(this.root, "tmp"))) {
38326
+ if (/^[a-f0-9-]{36}$/.test(file))
38327
+ rmSync(join17(this.root, "tmp", file), { force: true });
38328
+ }
38329
+ this.db.prepare("INSERT OR REPLACE INTO settings VALUES ('lastCleanup', ?)").run(now);
38330
+ return { deleted, skipped: false };
38331
+ });
38332
+ }
38333
+ get nextCollectionAt() {
38334
+ if (this.closed)
38335
+ throw new AttachmentError("CLOSED", "Attachment store is closed");
38336
+ const row = this.db.prepare("SELECT value FROM settings WHERE key='lastCleanup'").get();
38337
+ return row ? row.value + CLEANUP_INTERVAL_MS : 0;
38338
+ }
38339
+ close() {
38340
+ if (this.closed)
38341
+ return;
38342
+ if (this.active || this.waiting.length || this.operations.size)
38343
+ throw new AttachmentError("IO", "Cannot close while attachment operations are active");
38344
+ for (const id of this.operations)
38345
+ this.db.prepare("DELETE FROM operations WHERE id=?").run(id);
38346
+ this.operations.clear();
38347
+ this.db.close();
38348
+ this.closed = true;
38349
+ }
38350
+ };
38351
+
38352
+ // src/create-attachment-runtime.ts
38353
+ function createAttachmentRuntime(options) {
38354
+ mkdirSync6(options.sessionsRoot, { recursive: true, mode: 448 });
38355
+ const store = new LocalAttachmentStore(options);
38356
+ const now = options.now ?? Date.now;
38357
+ let closed = false;
38358
+ let timer;
38359
+ const collect = (force = false) => store.collectGarbage(
38360
+ () => scanAttachmentReferences(options.sessionsRoot),
38361
+ force
38362
+ );
38363
+ const run = () => {
38364
+ if (closed) return;
38365
+ try {
38366
+ collect();
38367
+ } catch (error) {
38368
+ options.onError(error);
38369
+ } finally {
38370
+ if (!closed) timer = setTimeout(run, CLEANUP_INTERVAL_MS).unref();
38371
+ }
38372
+ };
38373
+ timer = setTimeout(run, Math.max(0, store.nextCollectionAt - now())).unref();
38374
+ return {
38375
+ store,
38376
+ collect,
38377
+ close() {
38378
+ clearTimeout(timer);
38379
+ store.close();
38380
+ closed = true;
38381
+ }
38382
+ };
38383
+ }
38384
+
37498
38385
  // src/stdin-prompts.ts
37499
38386
  import { StringDecoder as StringDecoder2 } from "node:string_decoder";
37500
38387
  var StdinPrompts = class {
@@ -37513,7 +38400,7 @@ var StdinPrompts = class {
37513
38400
  if (input.readableEnded || input.destroyed) throw new PromptEofError();
37514
38401
  this.#active = true;
37515
38402
  const raw = Boolean(secret && input.isTTY && input.setRawMode);
37516
- return new Promise((resolve14, reject) => {
38403
+ return new Promise((resolve15, reject) => {
37517
38404
  const decoder = new StringDecoder2("utf8");
37518
38405
  let answer = "";
37519
38406
  let settled = false;
@@ -37539,7 +38426,7 @@ var StdinPrompts = class {
37539
38426
  error ??= outputError;
37540
38427
  }
37541
38428
  if (error !== void 0) reject(error);
37542
- else resolve14(answer);
38429
+ else resolve15(answer);
37543
38430
  };
37544
38431
  const onData = (data) => {
37545
38432
  try {
@@ -37782,8 +38669,8 @@ function startCoordinator(session, commandHandler, presenter, suggestCommand) {
37782
38669
  const done = (async () => {
37783
38670
  for (; ; ) {
37784
38671
  if (queue.length === 0) {
37785
- await new Promise((resolve14) => {
37786
- wake = resolve14;
38672
+ await new Promise((resolve15) => {
38673
+ wake = resolve15;
37787
38674
  });
37788
38675
  wake = null;
37789
38676
  }
@@ -37829,14 +38716,14 @@ function startCoordinator(session, commandHandler, presenter, suggestCommand) {
37829
38716
  };
37830
38717
  }
37831
38718
  function delay(ms) {
37832
- return new Promise((resolve14) => {
37833
- setTimeout(resolve14, ms);
38719
+ return new Promise((resolve15) => {
38720
+ setTimeout(resolve15, ms);
37834
38721
  });
37835
38722
  }
37836
38723
  async function settlesWithin(promise, ms) {
37837
38724
  let timer;
37838
- const timeout = new Promise((resolve14) => {
37839
- timer = setTimeout(() => resolve14(false), ms);
38725
+ const timeout = new Promise((resolve15) => {
38726
+ timer = setTimeout(() => resolve15(false), ms);
37840
38727
  });
37841
38728
  const settled = promise.then(
37842
38729
  () => true,
@@ -38005,13 +38892,13 @@ async function runTerminalUi(ui, driver, onSubmit) {
38005
38892
 
38006
38893
  // src/update-installation.ts
38007
38894
  import { spawn as spawn2 } from "node:child_process";
38008
- import { lstat as lstat4, readFile as readFile6, realpath as realpath4 } from "node:fs/promises";
38009
- import { basename as basename8, delimiter, dirname as dirname12, isAbsolute as isAbsolute4, join as join16, relative as relative5, resolve as resolve13, sep as sep3, win32 as win324 } from "node:path";
38895
+ import { lstat as lstat4, readFile as readFile7, realpath as realpath4 } from "node:fs/promises";
38896
+ import { basename as basename9, delimiter, dirname as dirname12, isAbsolute as isAbsolute4, join as join19, relative as relative5, resolve as resolve14, sep as sep3, win32 as win324 } from "node:path";
38010
38897
 
38011
38898
  // src/update.ts
38012
- import { randomUUID as randomUUID14 } from "node:crypto";
38013
- import { open as open3, readFile as readFile5, realpath as realpath3, unlink as unlink3 } from "node:fs/promises";
38014
- import { join as join15 } from "node:path";
38899
+ import { randomUUID as randomUUID17 } from "node:crypto";
38900
+ import { open as open5, readFile as readFile6, realpath as realpath3, unlink as unlink5 } from "node:fs/promises";
38901
+ import { join as join18 } from "node:path";
38015
38902
  function compareStableVersions(left, right) {
38016
38903
  const parse3 = (value) => {
38017
38904
  if (!/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(value) || value.includes("\n")) {
@@ -38031,13 +38918,13 @@ function updateDiagnostic(value) {
38031
38918
  return String(value instanceof Error ? value.message : value).replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").replace(/[\x00-\x1f\x7f-\x9f]/g, " ").replace(/https?:\/\/[^\s]+/gi, "[registry URL]").replace(/npm_[a-zA-Z0-9]+/g, "[redacted]").replace(/bearer\s+[^\s"']+/gi, "[redacted]").replace(/["']?(?:_authToken|_auth|password|token|authorization)["']?\s*[=:]\s*["']?[^\s"',;}]+/gi, "[redacted]").slice(0, 600);
38032
38919
  }
38033
38920
  var defaultLockIO = {
38034
- acquire: (path5) => open3(path5, "wx", 384),
38035
- read: (path5) => readFile5(path5, "utf8"),
38036
- remove: (path5) => unlink3(path5)
38921
+ acquire: (path5) => open5(path5, "wx", 384),
38922
+ read: (path5) => readFile6(path5, "utf8"),
38923
+ remove: (path5) => unlink5(path5)
38037
38924
  };
38038
38925
  async function installedVersion(installation) {
38039
38926
  if (await realpath3(installation.packageRoot) !== installation.packageRoot) throw new Error("Installation target changed");
38040
- const value = JSON.parse(await readFile5(join15(installation.packageRoot, "package.json"), "utf8"));
38927
+ const value = JSON.parse(await readFile6(join18(installation.packageRoot, "package.json"), "utf8"));
38041
38928
  if (typeof value !== "object" || value === null || !("name" in value) || value.name !== "laohuang" || !("version" in value) || typeof value.version !== "string") {
38042
38929
  throw new Error("Installed laohuang package is missing or invalid");
38043
38930
  }
@@ -38045,7 +38932,7 @@ async function installedVersion(installation) {
38045
38932
  return value.version;
38046
38933
  }
38047
38934
  async function runUpdate({ installation, output, signal, lockIO = defaultLockIO }) {
38048
- const token = randomUUID14();
38935
+ const token = randomUUID17();
38049
38936
  let ownsLock = false;
38050
38937
  let installing = false;
38051
38938
  const cancelled = () => {
@@ -38128,7 +39015,7 @@ function object(value) {
38128
39015
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
38129
39016
  }
38130
39017
  async function json(path5) {
38131
- return object(JSON.parse(await readFile6(path5, "utf8")));
39018
+ return object(JSON.parse(await readFile7(path5, "utf8")));
38132
39019
  }
38133
39020
  async function exists(path5) {
38134
39021
  try {
@@ -38237,17 +39124,17 @@ function createNpmRunner(cliPath, environ, options = {}) {
38237
39124
  };
38238
39125
  }
38239
39126
  async function npmCandidates(environ) {
38240
- const paths = [environ.npm_execpath, join16(dirname12(process.execPath), "node_modules/npm/bin/npm-cli.js"), join16(dirname12(process.execPath), "../lib/node_modules/npm/bin/npm-cli.js")];
39127
+ const paths = [environ.npm_execpath, join19(dirname12(process.execPath), "node_modules/npm/bin/npm-cli.js"), join19(dirname12(process.execPath), "../lib/node_modules/npm/bin/npm-cli.js")];
38241
39128
  for (const directory of (environ.PATH ?? environ.Path ?? "").split(delimiter).filter(Boolean)) {
38242
- paths.push(join16(directory, "npm"), join16(directory, "node_modules/npm/bin/npm-cli.js"));
39129
+ paths.push(join19(directory, "npm"), join19(directory, "node_modules/npm/bin/npm-cli.js"));
38243
39130
  }
38244
39131
  const result = /* @__PURE__ */ new Set();
38245
39132
  for (const path5 of paths) {
38246
39133
  if (!path5) continue;
38247
39134
  try {
38248
39135
  const actual = await realpath4(path5);
38249
- if (basename8(actual) !== "npm-cli.js") continue;
38250
- const manifest = await json(join16(dirname12(actual), "../package.json"));
39136
+ if (basename9(actual) !== "npm-cli.js") continue;
39137
+ const manifest = await json(join19(dirname12(actual), "../package.json"));
38251
39138
  if (manifest.name === "npm") result.add(actual);
38252
39139
  } catch {
38253
39140
  }
@@ -38264,8 +39151,8 @@ async function resolveNpmInstallation(entryPath, environ, options = {}) {
38264
39151
  let packageRoot = dirname12(entry);
38265
39152
  let manifest = {};
38266
39153
  while (true) {
38267
- if (await exists(join16(packageRoot, "package.json"))) {
38268
- manifest = await json(join16(packageRoot, "package.json"));
39154
+ if (await exists(join19(packageRoot, "package.json"))) {
39155
+ manifest = await json(join19(packageRoot, "package.json"));
38269
39156
  break;
38270
39157
  }
38271
39158
  const parent = dirname12(packageRoot);
@@ -38275,8 +39162,8 @@ async function resolveNpmInstallation(entryPath, environ, options = {}) {
38275
39162
  if (manifest.name !== "laohuang" || typeof manifest.version !== "string") throw new Error("Entry does not belong to laohuang");
38276
39163
  compareStableVersions(manifest.version, manifest.version);
38277
39164
  const bin = typeof manifest.bin === "string" ? manifest.bin : object(manifest.bin).laohuang;
38278
- if (typeof bin !== "string" || await realpath4(resolve13(packageRoot, bin)) !== entry) throw new Error("Entry does not match the installed laohuang executable");
38279
- if (basename8(dirname12(packageRoot)) !== "node_modules" || packageRoot.split(sep3).some((part) => ["_npx", ".pnpm", ".yarn"].includes(part))) throw new Error("Update requires a direct npm installation; source, linked, cache and other package-manager installations are unsupported");
39165
+ if (typeof bin !== "string" || await realpath4(resolve14(packageRoot, bin)) !== entry) throw new Error("Entry does not match the installed laohuang executable");
39166
+ if (basename9(dirname12(packageRoot)) !== "node_modules" || packageRoot.split(sep3).some((part) => ["_npx", ".pnpm", ".yarn"].includes(part))) throw new Error("Update requires a direct npm installation; source, linked, cache and other package-manager installations are unsupported");
38280
39167
  const candidates = options.cliPaths ?? await npmCandidates(environ);
38281
39168
  const runner = options.createRunner ?? createNpmRunner;
38282
39169
  if (candidates.length === 0) throw new Error("Cannot locate npm-cli.js");
@@ -38294,7 +39181,7 @@ async function resolveNpmInstallation(entryPath, environ, options = {}) {
38294
39181
  if (checked.code !== 0 || await realpath4(checked.stdout.trim()) !== dirname12(packageRoot)) continue;
38295
39182
  globals.push({ packageRoot, installedVersion: manifest.version, npm: {
38296
39183
  run: (args, runOptions) => npm.run(args[0] === "view" ? [...args, "--global", "--prefix", prefix] : args, runOptions)
38297
- }, workingDirectory: prefix, installArgs: ["--global", "--prefix", prefix], lockPath: join16(dirname12(packageRoot), ".laohuang-update.lock") });
39184
+ }, workingDirectory: prefix, installArgs: ["--global", "--prefix", prefix], lockPath: join19(dirname12(packageRoot), ".laohuang-update.lock") });
38298
39185
  } catch {
38299
39186
  options.signal?.throwIfAborted();
38300
39187
  }
@@ -38306,12 +39193,12 @@ async function resolveNpmInstallation(entryPath, environ, options = {}) {
38306
39193
  const owners = [];
38307
39194
  for (let root = dirname12(dirname12(packageRoot)); ; root = dirname12(root)) {
38308
39195
  options.signal?.throwIfAborted();
38309
- const lockPath = join16(root, "package-lock.json");
39196
+ const lockPath = join19(root, "package-lock.json");
38310
39197
  if (await exists(lockPath)) {
38311
39198
  for (const other of ["pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb", "npm-shrinkwrap.json"]) {
38312
- if (await exists(join16(root, other))) throw new Error("Ambiguous or unsupported package manager ownership");
39199
+ if (await exists(join19(root, other))) throw new Error("Ambiguous or unsupported package manager ownership");
38313
39200
  }
38314
- const rootManifest = await json(join16(root, "package.json"));
39201
+ const rootManifest = await json(join19(root, "package.json"));
38315
39202
  if (typeof rootManifest.packageManager === "string" && !rootManifest.packageManager.startsWith("npm@")) throw new Error("Installation belongs to another package manager");
38316
39203
  const lock = await json(lockPath);
38317
39204
  const packages = object(lock.packages);
@@ -38330,27 +39217,27 @@ async function resolveNpmInstallation(entryPath, environ, options = {}) {
38330
39217
  if (!Array.isArray(workspaces)) throw new Error("npm returned invalid workspace metadata");
38331
39218
  for (const value of workspaces) {
38332
39219
  const workspace = object(value);
38333
- if (typeof workspace.location !== "string" || typeof workspace.realpath !== "string" || !inside2(root, resolve13(root, workspace.location))) throw new Error("npm returned invalid workspace ownership");
38334
- if (await realpath4(resolve13(root, workspace.location)) !== workspace.realpath) throw new Error("npm workspace target changed");
39220
+ if (typeof workspace.location !== "string" || typeof workspace.realpath !== "string" || !inside2(root, resolve14(root, workspace.location))) throw new Error("npm returned invalid workspace ownership");
39221
+ if (await realpath4(resolve14(root, workspace.location)) !== workspace.realpath) throw new Error("npm workspace target changed");
38335
39222
  workspacePaths.add(workspace.location);
38336
39223
  }
38337
39224
  }
38338
39225
  for (const [key, metadata2] of Object.entries(packages)) {
38339
- if (key.includes("node_modules") || !inside2(root, resolve13(root, key))) continue;
39226
+ if (key.includes("node_modules") || !inside2(root, resolve14(root, key))) continue;
38340
39227
  if (key !== "") {
38341
39228
  if (!workspacePaths.has(key)) continue;
38342
39229
  if (!Object.values(packages).some((value) => object(value).link === true && object(value).resolved === key)) continue;
38343
39230
  }
38344
- const owner = await realpath4(resolve13(root, key));
39231
+ const owner = await realpath4(resolve14(root, key));
38345
39232
  if (!inside2(root, owner)) continue;
38346
- const declaration = await json(join16(owner, "package.json"));
39233
+ const declaration = await json(join19(owner, "package.json"));
38347
39234
  for (const bucket of ["dependencies", "devDependencies", "optionalDependencies"]) {
38348
39235
  const range = object(declaration[bucket]).laohuang;
38349
39236
  if (typeof range !== "string") continue;
38350
39237
  let resolvedPackage;
38351
39238
  for (let directory = owner; inside2(root, directory); directory = dirname12(directory)) {
38352
- if (await exists(join16(directory, "node_modules/laohuang"))) {
38353
- resolvedPackage = await realpath4(join16(directory, "node_modules/laohuang"));
39239
+ if (await exists(join19(directory, "node_modules/laohuang"))) {
39240
+ resolvedPackage = await realpath4(join19(directory, "node_modules/laohuang"));
38354
39241
  break;
38355
39242
  }
38356
39243
  if (directory === root) break;
@@ -38367,7 +39254,7 @@ async function resolveNpmInstallation(entryPath, environ, options = {}) {
38367
39254
  },
38368
39255
  workingDirectory: root,
38369
39256
  installArgs: ["--global=false", "--prefix", root, "--package-lock=true", "--save=true", "--save-peer=false", "--save-dev=false", "--save-optional=false", "--save-prod=false", bucket === "devDependencies" ? "--save-dev" : bucket === "optionalDependencies" ? "--save-optional" : "--save-prod", ...bucket === "devDependencies" ? ["--include=dev"] : [], .../^[~^]/.test(range) ? ["--save-exact=false", `--save-prefix=${range[0]}`] : ["--save-exact"], ...key ? ["--workspace", key, "--include-workspace-root=false"] : ["--workspaces=false"]],
38370
- lockPath: join16(root, ".laohuang-update.lock")
39257
+ lockPath: join19(root, ".laohuang-update.lock")
38371
39258
  });
38372
39259
  }
38373
39260
  }
@@ -38397,7 +39284,7 @@ function safeDiagnostic(value) {
38397
39284
  }
38398
39285
  function createClipboardRunner(environ, timeoutMs = 5e3) {
38399
39286
  const env = { ...environ };
38400
- return (command, args, text3) => new Promise((resolve14) => {
39287
+ return (command, args, text3) => new Promise((resolve15) => {
38401
39288
  let child;
38402
39289
  try {
38403
39290
  child = spawn3(command, [...args], {
@@ -38407,7 +39294,7 @@ function createClipboardRunner(environ, timeoutMs = 5e3) {
38407
39294
  stdio: ["pipe", "ignore", "pipe"]
38408
39295
  });
38409
39296
  } catch (error) {
38410
- resolve14({ code: 1, stderr: errorMessage12(error) });
39297
+ resolve15({ code: 1, stderr: errorMessage12(error) });
38411
39298
  return;
38412
39299
  }
38413
39300
  let stderr = "";
@@ -38417,7 +39304,7 @@ function createClipboardRunner(environ, timeoutMs = 5e3) {
38417
39304
  if (settled) return;
38418
39305
  settled = true;
38419
39306
  if (timer) clearTimeout(timer);
38420
- resolve14({ code, stderr: diagnostic });
39307
+ resolve15({ code, stderr: diagnostic });
38421
39308
  };
38422
39309
  const stop = (diagnostic) => {
38423
39310
  try {
@@ -38612,17 +39499,19 @@ async function main(argv, options = {}) {
38612
39499
  const configPath = options.configPath ?? defaultConfigPath(environ);
38613
39500
  const manager = new ConfigManager(configPath);
38614
39501
  const credentials = new CredentialStore(
38615
- options.credentialsPath ?? join17(dirname13(configPath), "credentials.json")
39502
+ options.credentialsPath ?? join20(dirname13(configPath), "credentials.json")
38616
39503
  );
38617
39504
  const modelCatalogStore = new ModelCatalogStore(
38618
- options.modelsPath ?? join17(dirname13(configPath), "models.json")
39505
+ options.modelsPath ?? join20(dirname13(configPath), "models.json")
38619
39506
  );
38620
39507
  const customModels = new CustomModelsStore(
38621
- options.customModelsPath ?? join17(dirname13(configPath), "custom-models.json")
39508
+ options.customModelsPath ?? join20(dirname13(configPath), "custom-models.json")
38622
39509
  );
38623
39510
  let modelPlatform;
39511
+ let attachmentRuntime;
38624
39512
  try {
38625
39513
  modelPlatform = await createPiAiPlatform({
39514
+ attachments: { store: () => attachmentRuntime?.store },
38626
39515
  credentials,
38627
39516
  modelCatalogStore,
38628
39517
  excludedProviderIds: EXCLUDED_PROVIDER_IDS,
@@ -38751,7 +39640,7 @@ async function main(argv, options = {}) {
38751
39640
  let shellPath;
38752
39641
  try {
38753
39642
  shellPath = manager.getShellPath();
38754
- if (existsSync8(configPath)) {
39643
+ if (existsSync9(configPath)) {
38755
39644
  config = manager.resolve({
38756
39645
  environ,
38757
39646
  profile: args.profile,
@@ -38796,7 +39685,13 @@ async function main(argv, options = {}) {
38796
39685
  let terminalUi = null;
38797
39686
  let terminalDriver = null;
38798
39687
  let selectedModel = modelPlatform.catalog.getModel(config.provider, config.model);
39688
+ attachmentRuntime = createAttachmentRuntime({
39689
+ root: join20(getHomeDirectory({ env: environ }), ".laohuang", "attachments"),
39690
+ sessionsRoot: defaultSessionsRoot(environ),
39691
+ onError: (error) => writeStderr(`Attachment cleanup failed: ${errorMessage11(error)}`)
39692
+ });
38799
39693
  const sessionController = new SessionController({
39694
+ attachments: attachmentRuntime.store,
38800
39695
  sessionsRoot: defaultSessionsRoot(environ),
38801
39696
  projectRoot,
38802
39697
  initialCwd: process.cwd(),
@@ -38849,6 +39744,18 @@ async function main(argv, options = {}) {
38849
39744
  }
38850
39745
  });
38851
39746
  const toolRegistry = new ToolRegistry([
39747
+ createReadImageTool({
39748
+ store: attachmentRuntime.store,
39749
+ projectRoot,
39750
+ pathOptions: { env: environ, shellPath: () => resolveBashPath({ shellPath, env: environ }) },
39751
+ resolveReference: (id) => {
39752
+ for (const entry of sessionController.history?.entries() ?? []) {
39753
+ if (!("message" in entry.payload) || !("attachments" in entry.payload.message)) continue;
39754
+ for (const block of entry.payload.message.attachments ?? []) if (block.type === "image" && block.ref.id === id) return block.ref;
39755
+ }
39756
+ return void 0;
39757
+ }
39758
+ }),
38852
39759
  skillRuntime.session.tool,
38853
39760
  ...createFileToolDefinitions({ projectRoot, pathOptions: {
38854
39761
  env: environ,
@@ -38859,6 +39766,7 @@ async function main(argv, options = {}) {
38859
39766
  let commandDispatcher;
38860
39767
  let runtimeInitialized = false;
38861
39768
  const composedRuntime = createSessionRuntime({
39769
+ attachments: attachmentRuntime.store,
38862
39770
  modelAdapter: modelPlatform.adapter,
38863
39771
  catalog: modelPlatform.catalog,
38864
39772
  route: { provider: config.provider, model: config.model, baseUrl: config.baseUrl },
@@ -39087,15 +39995,19 @@ Submit an empty line to retry, or enter a replacement.`);
39087
39995
  try {
39088
39996
  await skillRuntime?.close();
39089
39997
  } finally {
39090
- if (composedForCleanup) await composedForCleanup.close();
39091
- else await sessionController.close();
39998
+ try {
39999
+ if (composedForCleanup) await composedForCleanup.close();
40000
+ else await sessionController.close();
40001
+ } finally {
40002
+ attachmentRuntime.close();
40003
+ }
39092
40004
  }
39093
40005
  }
39094
40006
  }
39095
40007
  }
39096
40008
  }
39097
40009
  function defaultSessionsRoot(environ) {
39098
- return join17(getHomeDirectory({ env: environ }), ".laohuang", "sessions");
40010
+ return join20(getHomeDirectory({ env: environ }), ".laohuang", "sessions");
39099
40011
  }
39100
40012
  async function runInitialModelSelection(options) {
39101
40013
  let providerName = options.providerName;