pi-advisor-flow 0.8.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -52,6 +52,7 @@ var advisorPlanGateRef = true;
52
52
  var advisorFailureGateRef = true;
53
53
  var advisorCompletionGateRef = true;
54
54
  var advisorCustomInvocationRef;
55
+ var advisorDisableSameModelRef = true;
55
56
  var advisorCollapseResponsesRef = false;
56
57
  var advisorBlockOnBlockedRef = true;
57
58
  var advisorAutoLoopGateRef = true;
@@ -122,6 +123,9 @@ var setAdvisorCompletionGateRef = (enabled) => {
122
123
  var setAdvisorCustomInvocationRef = (rule) => {
123
124
  advisorCustomInvocationRef = rule?.trim() || undefined;
124
125
  };
126
+ var setAdvisorDisableSameModelRef = (enabled) => {
127
+ advisorDisableSameModelRef = enabled;
128
+ };
125
129
  var setAdvisorCollapseResponsesRef = (enabled) => {
126
130
  advisorCollapseResponsesRef = enabled;
127
131
  };
@@ -238,6 +242,7 @@ var getAdvisorSettings = () => ({
238
242
  completionGate: advisorCompletionGateRef,
239
243
  contextMaxChars: contextMaxCharsRef,
240
244
  customRule: advisorCustomInvocationRef,
245
+ disableSameModel: advisorDisableSameModelRef,
241
246
  effort: advisorEffortRef,
242
247
  failureGate: advisorFailureGateRef,
243
248
  failureMode: advisorFailureModeRef,
@@ -487,6 +492,12 @@ var CONFIG_SCHEMA = {
487
492
  persisted: true,
488
493
  type: "string"
489
494
  },
495
+ advisorDisableSameModel: {
496
+ accepted: "true or false",
497
+ current: () => advisorDisableSameModelRef,
498
+ persisted: true,
499
+ type: "boolean"
500
+ },
490
501
  advisorEffort: {
491
502
  accepted: "a string",
492
503
  current: () => advisorEffortRef,
@@ -852,6 +863,7 @@ var resetDefaults = () => {
852
863
  setAdvisorFailureGateRef(true);
853
864
  setAdvisorCompletionGateRef(true);
854
865
  setAdvisorCustomInvocationRef(undefined);
866
+ setAdvisorDisableSameModelRef(true);
855
867
  setAdvisorCollapseResponsesRef(false);
856
868
  setAdvisorBlockOnBlockedRef(true);
857
869
  setAdvisorAutoLoopGateRef(true);
@@ -909,6 +921,7 @@ var applyConfig = (config) => {
909
921
  applyOptionalConfig(config, "advisorFailureGate", setAdvisorFailureGateRef);
910
922
  applyOptionalConfig(config, "advisorCompletionGate", setAdvisorCompletionGateRef);
911
923
  applyOptionalConfig(config, "advisorCustomInvocation", setAdvisorCustomInvocationRef);
924
+ applyOptionalConfig(config, "advisorDisableSameModel", setAdvisorDisableSameModelRef);
912
925
  applyOptionalConfig(config, "advisorCollapseResponses", setAdvisorCollapseResponsesRef);
913
926
  applyOptionalConfig(config, "advisorBlockOnBlocked", setAdvisorBlockOnBlockedRef);
914
927
  applyOptionalConfig(config, "advisorAutoLoopGate", setAdvisorAutoLoopGateRef);
@@ -1070,6 +1083,43 @@ var saveGlobalOutcomeLogging = (enabled) => {
1070
1083
  return path;
1071
1084
  };
1072
1085
 
1086
+ // src/tools/model-access.ts
1087
+ var currentModelRef = (ctx) => {
1088
+ const { model } = ctx;
1089
+ return model ? `${model.provider}/${model.id}` : undefined;
1090
+ };
1091
+ var advisorModelAccess = (ctx) => {
1092
+ const modelRef = currentModelRef(ctx);
1093
+ if (advisorModelWhitelistRef.length === 0) {
1094
+ return modelRef ? { allowed: true, modelRef } : { allowed: true };
1095
+ }
1096
+ if (modelRef && advisorModelWhitelistRef.includes(modelRef)) {
1097
+ return { allowed: true, modelRef };
1098
+ }
1099
+ const current = modelRef ?? "no current model";
1100
+ const denial = {
1101
+ allowed: false,
1102
+ reason: `Advisor calls are restricted to the configured model whitelist (${advisorModelWhitelistRef.join(", ")}). Current model: ${current}.`
1103
+ };
1104
+ if (modelRef) {
1105
+ denial.modelRef = modelRef;
1106
+ }
1107
+ return denial;
1108
+ };
1109
+ var sameModelAdvisorDisabled = (ctx, model = ctx.model) => {
1110
+ if (!(advisorDisableSameModelRef && advisorRef && model)) {
1111
+ return false;
1112
+ }
1113
+ const [provider, id] = splitRef(advisorRef);
1114
+ return provider === model.provider && id === model.id;
1115
+ };
1116
+ var sameModelAdvisorNotice = "Advisor disabled: executor and advisor are the same model.";
1117
+ var advisorModelIsAllowed = (ctx) => advisorModelAccess(ctx).allowed && !sameModelAdvisorDisabled(ctx);
1118
+ var advisorModelAccessReason = (ctx) => {
1119
+ const access = advisorModelAccess(ctx);
1120
+ return access.allowed ? undefined : access.reason;
1121
+ };
1122
+
1073
1123
  // src/commands/model-options.ts
1074
1124
  var DEFAULT_EFFORT_LEVEL = "Default (Model Default)";
1075
1125
  var SELECTED_PREFIX = "✓ ";
@@ -1710,7 +1760,7 @@ var herdrAdvisorActivity = new HerdrAdvisorActivity(sendToHerdr, () => getAdviso
1710
1760
  var herdrAdvisorBlock = new HerdrAdvisorBlock(sendToHerdr, () => getAdvisorSettings().herdrIntegration);
1711
1761
 
1712
1762
  // src/tools/consultation.ts
1713
- import { randomUUID } from "node:crypto";
1763
+ import { randomUUID as randomUUID2 } from "node:crypto";
1714
1764
 
1715
1765
  // src/model-stream.ts
1716
1766
  import { stream } from "@earendil-works/pi-ai/compat";
@@ -1859,6 +1909,9 @@ var collectTextStream = async (resolved, options, streamModel = stream) => {
1859
1909
  };
1860
1910
  };
1861
1911
 
1912
+ // src/tools/consult-context.ts
1913
+ import { randomUUID } from "node:crypto";
1914
+
1862
1915
  // src/attachments.ts
1863
1916
  import { execFileSync as execFileSync2 } from "node:child_process";
1864
1917
  import { constants } from "node:fs";
@@ -1866,6 +1919,15 @@ import { lstat, open, realpath } from "node:fs/promises";
1866
1919
  import { isAbsolute, relative, resolve } from "node:path";
1867
1920
  var ADVISOR_FILE_MAX_BYTES = 8 * 1024;
1868
1921
  var ADVISOR_FILES_TOTAL_MAX_BYTES = 24 * 1024;
1922
+ var imageExtensions = new Map([
1923
+ [".png", "image/png"],
1924
+ [".jpg", "image/jpeg"],
1925
+ [".jpeg", "image/jpeg"],
1926
+ [".gif", "image/gif"],
1927
+ [".webp", "image/webp"]
1928
+ ]);
1929
+ var imageMime = (path) => imageExtensions.get(path.slice(path.lastIndexOf(".")).toLowerCase());
1930
+ var imageFileCandidate = (path) => Boolean(imageMime(path)) || /\.(?:avif|bmp|heic|svg|tiff?)$/iu.test(path);
1869
1931
  var PATH_SEGMENTS = /[\\/]/u;
1870
1932
  var within = (root, candidate) => {
1871
1933
  const path = relative(root, candidate);
@@ -1967,7 +2029,7 @@ var readFiles = async (cwd, requested, enabled, redact, kind, totalLimit = ADVIS
1967
2029
  let total = 0;
1968
2030
  for (const name of requested) {
1969
2031
  const normalizedName = normalizeRequestedPath(root, name);
1970
- if (!normalizedName || unique.has(normalizedName)) {
2032
+ if (!normalizedName || unique.has(normalizedName) || imageFileCandidate(normalizedName)) {
1971
2033
  continue;
1972
2034
  }
1973
2035
  unique.add(normalizedName);
@@ -1991,8 +2053,399 @@ var readFiles = async (cwd, requested, enabled, redact, kind, totalLimit = ADVIS
1991
2053
  var readTrackedFiles = (cwd, requested, enabled, redact, totalLimit = ADVISOR_FILES_TOTAL_MAX_BYTES) => readFiles(cwd, requested, enabled, redact, "tracked", totalLimit);
1992
2054
  var readUntrackedFiles = (cwd, requested, enabled, redact, totalLimit = ADVISOR_FILES_TOTAL_MAX_BYTES) => readFiles(cwd, requested, enabled, redact, "untracked", totalLimit);
1993
2055
 
1994
- // src/preferences.ts
2056
+ // src/image-attachments.ts
2057
+ import { constants as constants2 } from "node:fs";
1995
2058
  import { lstat as lstat2, open as open2, realpath as realpath2 } from "node:fs/promises";
2059
+ import { resolve as resolve2 } from "node:path";
2060
+
2061
+ // src/images.ts
2062
+ import { createHash } from "node:crypto";
2063
+
2064
+ // src/image-validation.ts
2065
+ import { inflateSync } from "node:zlib";
2066
+ var PNG_SIGNATURE = Buffer.from("89504e470d0a1a0a", "hex");
2067
+ var isPng = (bytes) => {
2068
+ if (bytes.length < 57 || !bytes.subarray(0, 8).equals(PNG_SIGNATURE)) {
2069
+ return false;
2070
+ }
2071
+ let offset = 8;
2072
+ let hasHeader = false;
2073
+ const compressed = [];
2074
+ while (offset + 12 <= bytes.length) {
2075
+ const length = bytes.readUInt32BE(offset);
2076
+ if (length > bytes.length - offset - 12) {
2077
+ return false;
2078
+ }
2079
+ const type = bytes.toString("ascii", offset + 4, offset + 8);
2080
+ const data = offset + 8;
2081
+ offset += length + 12;
2082
+ if (!hasHeader) {
2083
+ if (type !== "IHDR" || length !== 13 || bytes.readUInt32BE(data) === 0 || bytes.readUInt32BE(data + 4) === 0) {
2084
+ return false;
2085
+ }
2086
+ hasHeader = true;
2087
+ } else if (type === "IDAT") {
2088
+ compressed.push(bytes.subarray(data, data + length));
2089
+ } else if (type === "IEND") {
2090
+ if (length !== 0 || offset !== bytes.length || !compressed.length) {
2091
+ return false;
2092
+ }
2093
+ try {
2094
+ return inflateSync(Buffer.concat(compressed), {
2095
+ maxOutputLength: 32 * 1024 * 1024
2096
+ }).length > 0;
2097
+ } catch {
2098
+ return false;
2099
+ }
2100
+ } else if (type === "IHDR") {
2101
+ return false;
2102
+ }
2103
+ }
2104
+ return false;
2105
+ };
2106
+ var isFrameMarker = (marker) => [
2107
+ 192,
2108
+ 193,
2109
+ 194,
2110
+ 195,
2111
+ 197,
2112
+ 198,
2113
+ 199,
2114
+ 201,
2115
+ 202,
2116
+ 203,
2117
+ 205,
2118
+ 206,
2119
+ 207
2120
+ ].includes(marker);
2121
+ var nextScanMarker = (bytes, from) => {
2122
+ let offset = from;
2123
+ while (offset < bytes.length - 1) {
2124
+ if (bytes[offset] !== 255) {
2125
+ offset += 1;
2126
+ continue;
2127
+ }
2128
+ let next = offset + 1;
2129
+ while (bytes[next] === 255) {
2130
+ next += 1;
2131
+ }
2132
+ if (bytes[next] !== 0 && (bytes[next] < 208 || bytes[next] > 215)) {
2133
+ return offset;
2134
+ }
2135
+ offset = next + 1;
2136
+ }
2137
+ return bytes.length;
2138
+ };
2139
+ var readJpegSegment = (bytes, at) => {
2140
+ if (bytes[at] !== 255) {
2141
+ return;
2142
+ }
2143
+ let offset = at;
2144
+ while (bytes[offset] === 255) {
2145
+ offset += 1;
2146
+ }
2147
+ const marker = bytes[offset];
2148
+ offset += 1;
2149
+ if (marker === 217) {
2150
+ return { end: offset, frame: false, marker, size: 0 };
2151
+ }
2152
+ if (offset + 2 > bytes.length || marker === 0) {
2153
+ return;
2154
+ }
2155
+ const size = bytes.readUInt16BE(offset);
2156
+ if (size < 2 || offset + size > bytes.length) {
2157
+ return;
2158
+ }
2159
+ const frame = isFrameMarker(marker) && size >= 11 && bytes.readUInt16BE(offset + 3) > 0 && bytes.readUInt16BE(offset + 5) > 0;
2160
+ return { end: offset + size, frame, marker, size };
2161
+ };
2162
+ var hasJpegEnvelope = (bytes) => bytes.length >= 24 && bytes[0] === 255 && bytes[1] === 216 && bytes.at(-2) === 255 && bytes.at(-1) === 217;
2163
+ var isJpeg = (bytes) => {
2164
+ if (!hasJpegEnvelope(bytes)) {
2165
+ return false;
2166
+ }
2167
+ let offset = 2;
2168
+ let frame = false;
2169
+ let scan = false;
2170
+ let quantization = false;
2171
+ let entropyTable = false;
2172
+ while (offset < bytes.length) {
2173
+ const segment = readJpegSegment(bytes, offset);
2174
+ if (!segment) {
2175
+ return false;
2176
+ }
2177
+ const { end, marker, size } = segment;
2178
+ if (marker === 217) {
2179
+ return frame && scan && end === bytes.length;
2180
+ }
2181
+ frame ||= segment.frame;
2182
+ quantization ||= marker === 219;
2183
+ entropyTable ||= marker === 196 || marker === 204;
2184
+ offset = end;
2185
+ if (marker === 218) {
2186
+ if (!(frame && quantization && entropyTable && size >= 6)) {
2187
+ return false;
2188
+ }
2189
+ const next = nextScanMarker(bytes, offset);
2190
+ if (next <= offset) {
2191
+ return false;
2192
+ }
2193
+ scan = true;
2194
+ offset = next;
2195
+ }
2196
+ }
2197
+ return false;
2198
+ };
2199
+ var skipGifBlocks = (bytes, at) => {
2200
+ let offset = at;
2201
+ while (offset < bytes.length) {
2202
+ const length = bytes[offset];
2203
+ offset += 1;
2204
+ if (length === 0) {
2205
+ return offset;
2206
+ }
2207
+ if (offset + length > bytes.length) {
2208
+ return;
2209
+ }
2210
+ offset += length;
2211
+ }
2212
+ };
2213
+ var gifColorTableEnd = (bytes, at, packed) => at + ((packed & 128) === 0 ? 0 : 3 * 2 ** ((packed & 7) + 1));
2214
+ var gifFrameEnd = (bytes, at) => {
2215
+ if (at + 9 > bytes.length || bytes.readUInt16LE(at + 4) === 0 || bytes.readUInt16LE(at + 6) === 0) {
2216
+ return;
2217
+ }
2218
+ const data = gifColorTableEnd(bytes, at + 9, bytes[at + 8]);
2219
+ if (data + 2 >= bytes.length || bytes[data] < 2 || bytes[data] > 12 || bytes[data + 1] === 0) {
2220
+ return;
2221
+ }
2222
+ return skipGifBlocks(bytes, data + 1);
2223
+ };
2224
+ var isGif = (bytes) => {
2225
+ if (bytes.length < 20 || !["GIF87a", "GIF89a"].includes(bytes.toString("ascii", 0, 6)) || bytes.readUInt16LE(6) === 0 || bytes.readUInt16LE(8) === 0) {
2226
+ return false;
2227
+ }
2228
+ let offset = gifColorTableEnd(bytes, 13, bytes[10]);
2229
+ let frame = false;
2230
+ while (offset < bytes.length) {
2231
+ const block = bytes[offset];
2232
+ offset += 1;
2233
+ if (block === 59) {
2234
+ return frame && offset === bytes.length;
2235
+ }
2236
+ if (block === 33 && offset < bytes.length) {
2237
+ offset = skipGifBlocks(bytes, offset + 1) ?? bytes.length;
2238
+ } else if (block === 44) {
2239
+ const end = gifFrameEnd(bytes, offset);
2240
+ if (end === undefined) {
2241
+ return false;
2242
+ }
2243
+ frame = true;
2244
+ offset = end;
2245
+ } else {
2246
+ return false;
2247
+ }
2248
+ }
2249
+ return false;
2250
+ };
2251
+ var validWebpFrame = (bytes, type, data, length) => {
2252
+ if (type === "VP8 ") {
2253
+ return length >= 10 && bytes.toString("hex", data + 3, data + 6) === "9d012a" && (bytes.readUInt16LE(data + 6) & 16383) > 0 && (bytes.readUInt16LE(data + 8) & 16383) > 0;
2254
+ }
2255
+ if (type === "VP8L") {
2256
+ return length >= 5 && bytes[data] === 47;
2257
+ }
2258
+ return type === "ANMF" && length >= 16;
2259
+ };
2260
+ var isWebp = (bytes) => {
2261
+ if (bytes.length < 30 || bytes.toString("ascii", 0, 4) !== "RIFF" || bytes.toString("ascii", 8, 12) !== "WEBP" || bytes.readUInt32LE(4) !== bytes.length - 8) {
2262
+ return false;
2263
+ }
2264
+ let offset = 12;
2265
+ let frame = false;
2266
+ while (offset + 8 <= bytes.length) {
2267
+ const type = bytes.toString("ascii", offset, offset + 4);
2268
+ const length = bytes.readUInt32LE(offset + 4);
2269
+ const data = offset + 8;
2270
+ const end = data + length + length % 2;
2271
+ if (end > bytes.length) {
2272
+ return false;
2273
+ }
2274
+ frame ||= validWebpFrame(bytes, type, data, length);
2275
+ offset = end;
2276
+ }
2277
+ return frame && offset === bytes.length;
2278
+ };
2279
+ var detectImageFormat = (bytes) => {
2280
+ if (isPng(bytes)) {
2281
+ return "image/png";
2282
+ }
2283
+ if (isJpeg(bytes)) {
2284
+ return "image/jpeg";
2285
+ }
2286
+ if (isGif(bytes)) {
2287
+ return "image/gif";
2288
+ }
2289
+ if (isWebp(bytes)) {
2290
+ return "image/webp";
2291
+ }
2292
+ };
2293
+
2294
+ // src/images.ts
2295
+ var ADVISOR_IMAGE_MAX_BYTES = 4 * 1024 * 1024;
2296
+ var ADVISOR_IMAGES_TOTAL_MAX_BYTES = 8 * 1024 * 1024;
2297
+ var ADVISOR_IMAGES_MAX_COUNT = 4;
2298
+ var imageFromBytes = (bytes, mimeType) => {
2299
+ if (!bytes.length || bytes.length > ADVISOR_IMAGE_MAX_BYTES) {
2300
+ return;
2301
+ }
2302
+ const actual = detectImageFormat(bytes);
2303
+ if (!actual || mimeType && mimeType !== actual) {
2304
+ return;
2305
+ }
2306
+ return { data: bytes.toString("base64"), mimeType: actual, type: "image" };
2307
+ };
2308
+ var imageFromPart = (part) => {
2309
+ if (part.type !== "image" || !isString(part.data) || !isString(part.mimeType) || part.data.length > Math.ceil(ADVISOR_IMAGE_MAX_BYTES * 4 / 3) + 4 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(part.data)) {
2310
+ return;
2311
+ }
2312
+ const bytes = Buffer.from(part.data, "base64");
2313
+ if (bytes.toString("base64") !== part.data) {
2314
+ return;
2315
+ }
2316
+ return imageFromBytes(bytes, part.mimeType);
2317
+ };
2318
+ var imageMarker = (part, nonce = "") => {
2319
+ if (part.type !== "image") {
2320
+ return;
2321
+ }
2322
+ const image = imageFromPart(part);
2323
+ if (!image) {
2324
+ return "[Image omitted: unsupported format, invalid data, or over 4 MiB; pixels not reviewed]";
2325
+ }
2326
+ const id = createHash("sha256").update(nonce).update(image.data).digest("hex").slice(0, 24);
2327
+ return `[Image ref=img_${id}; pixels reviewed only if attached below]`;
2328
+ };
2329
+ var eligibleImagePart = (part) => part.type === "image" && isString(part.data) && isString(part.mimeType);
2330
+ var entryImageCensus = (content, conversation, nonce, seen) => {
2331
+ let parts = 0;
2332
+ const selected = [];
2333
+ for (const part of contentParts(content)) {
2334
+ if (!isRecordOf(part)) {
2335
+ continue;
2336
+ }
2337
+ if (eligibleImagePart(part)) {
2338
+ parts += 1;
2339
+ }
2340
+ const marker = imageMarker(part, nonce);
2341
+ if (!marker || seen.has(marker) || !conversation.includes(marker)) {
2342
+ continue;
2343
+ }
2344
+ const image = imageFromPart(part);
2345
+ if (image) {
2346
+ seen.add(marker);
2347
+ selected.push({ image, marker });
2348
+ }
2349
+ }
2350
+ return { parts, selected };
2351
+ };
2352
+ var selectedConversationImages = (ctx, conversation, policies, selectedEntryIds, nonce) => {
2353
+ if (!conversation) {
2354
+ return { imagePartsSeen: 0, selected: [] };
2355
+ }
2356
+ const entries = selectedEntryIds ? ctx.sessionManager.buildContextEntries() : ctx.sessionManager.getBranch();
2357
+ const images = [];
2358
+ const seen = new Set;
2359
+ let imagePartsSeen = 0;
2360
+ for (const [index, entry] of entries.entries()) {
2361
+ if (entry.type !== "message" || selectedEntryIds && !selectedEntryIds.has(entry.id ?? String(index))) {
2362
+ continue;
2363
+ }
2364
+ const { message } = entry;
2365
+ if (message.role !== "user" && message.role !== "toolResult") {
2366
+ continue;
2367
+ }
2368
+ if (message.role === "toolResult" && (policies[message.toolName] ?? "full") !== "full") {
2369
+ continue;
2370
+ }
2371
+ const census = entryImageCensus(message.content, conversation, nonce, seen);
2372
+ imagePartsSeen += census.parts;
2373
+ images.push(...census.selected);
2374
+ }
2375
+ return { imagePartsSeen, selected: images };
2376
+ };
2377
+
2378
+ // src/image-attachments.ts
2379
+ var readImageAttachment = async (root, name, mimeType, remaining) => {
2380
+ const absolute = resolve2(root, name);
2381
+ if (!within(root, absolute)) {
2382
+ return;
2383
+ }
2384
+ const stats = await lstat2(absolute);
2385
+ if (stats.isSymbolicLink() || !stats.isFile()) {
2386
+ return;
2387
+ }
2388
+ const resolved = await realpath2(absolute);
2389
+ if (!within(root, resolved)) {
2390
+ return;
2391
+ }
2392
+ const flags = constants2.O_NOFOLLOW ? constants2.O_RDONLY + constants2.O_NOFOLLOW : constants2.O_RDONLY;
2393
+ const file = await open2(resolved, flags);
2394
+ try {
2395
+ const opened = await file.stat();
2396
+ if (!opened.isFile() || opened.size > ADVISOR_IMAGE_MAX_BYTES || opened.size > remaining) {
2397
+ return;
2398
+ }
2399
+ const buffer = Buffer.alloc(opened.size + 1);
2400
+ const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
2401
+ if (bytesRead !== opened.size) {
2402
+ return;
2403
+ }
2404
+ const image = imageFromBytes(buffer.subarray(0, bytesRead), mimeType);
2405
+ return image ? { bytes: bytesRead, image, path: name } : undefined;
2406
+ } finally {
2407
+ await file.close();
2408
+ }
2409
+ };
2410
+ var readImageFiles = async (cwd, requested, enabled, kind, remainingBytes, remainingCount) => {
2411
+ const names = Array.isArray(requested) ? requested.filter((path) => isString(path) && imageFileCandidate(path)) : [];
2412
+ if (!enabled || !names.length) {
2413
+ return { images: [], omitted: names.length };
2414
+ }
2415
+ const root = await repositoryRoot(cwd);
2416
+ if (!root) {
2417
+ return { images: [], omitted: names.length };
2418
+ }
2419
+ const seen = new Set;
2420
+ const images = [];
2421
+ let omitted = 0;
2422
+ let bytes = 0;
2423
+ for (const path of names) {
2424
+ const name = normalizeRequestedPath(root, path);
2425
+ if (name && seen.has(name)) {
2426
+ continue;
2427
+ }
2428
+ if (name) {
2429
+ seen.add(name);
2430
+ }
2431
+ try {
2432
+ const mime = name && imageMime(name);
2433
+ const image = name && mime && images.length < remainingCount && remainingBytes - bytes > 0 && isPermitted(root, name, kind) ? await readImageAttachment(root, name, mime, remainingBytes - bytes) : undefined;
2434
+ if (image) {
2435
+ images.push(image);
2436
+ bytes += image.bytes;
2437
+ } else {
2438
+ omitted += 1;
2439
+ }
2440
+ } catch {
2441
+ omitted += 1;
2442
+ }
2443
+ }
2444
+ return { images, omitted };
2445
+ };
2446
+
2447
+ // src/preferences.ts
2448
+ import { lstat as lstat3, open as open3, realpath as realpath3 } from "node:fs/promises";
1996
2449
  import { join as join2, relative as relative2 } from "node:path";
1997
2450
  var PREFERENCES_MAX_BYTES = 8 * 1024;
1998
2451
  var PREFERENCES_FILENAME = ["advisor-preferences", "md"].join(".");
@@ -2005,17 +2458,17 @@ var readProjectPreferences = async (ctx, maxBytes = PREFERENCES_MAX_BYTES, redac
2005
2458
  return;
2006
2459
  }
2007
2460
  try {
2008
- const root = await realpath2(ctx.cwd);
2461
+ const root = await realpath3(ctx.cwd);
2009
2462
  const candidate = join2(ctx.cwd, ".pi", PREFERENCES_FILENAME);
2010
- const stats = await lstat2(candidate);
2463
+ const stats = await lstat3(candidate);
2011
2464
  if (stats.isSymbolicLink() || !stats.isFile()) {
2012
2465
  return;
2013
2466
  }
2014
- const resolved = await realpath2(candidate);
2467
+ const resolved = await realpath3(candidate);
2015
2468
  if (!inside(root, resolved)) {
2016
2469
  return;
2017
2470
  }
2018
- const file = await open2(resolved, "r");
2471
+ const file = await open3(resolved, "r");
2019
2472
  try {
2020
2473
  const buffer = Buffer.alloc(maxBytes + 1);
2021
2474
  const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
@@ -2029,7 +2482,7 @@ var readProjectPreferences = async (ctx, maxBytes = PREFERENCES_MAX_BYTES, redac
2029
2482
  };
2030
2483
 
2031
2484
  // src/scout-groups.ts
2032
- import { createHash } from "node:crypto";
2485
+ import { createHash as createHash2 } from "node:crypto";
2033
2486
 
2034
2487
  // src/tool-result-cap.ts
2035
2488
  var OMITTED_MARKER = "[... omitted tool-result section ...]";
@@ -2108,6 +2561,7 @@ var textFromPart = (part) => {
2108
2561
  };
2109
2562
  var textFrom = (content) => contentParts(content).map(textFromPart).join(`
2110
2563
  `).trim();
2564
+ var imageNotes = (content, nonce) => contentParts(content).map((part) => isRecordOf(part) ? imageMarker(part, nonce) : undefined).filter(isString);
2111
2565
  var assistantEntry = (message, policies, redact) => {
2112
2566
  const parts = [];
2113
2567
  const text = textFrom(message.content);
@@ -2134,11 +2588,13 @@ var assistantEntry = (message, policies, redact) => {
2134
2588
  return parts.length > 0 ? `Executor: ${parts.join(`
2135
2589
  `)}` : undefined;
2136
2590
  };
2137
- var toolResultEntry = (message, toolResultMaxLines, toolResultMaxBytes, policies, redact) => {
2591
+ var toolResultEntry = (message, toolResultMaxLines, toolResultMaxBytes, policies, redact, describeImages, imageNonce) => {
2138
2592
  const status = message.isError ? "error" : "success";
2139
2593
  const toolName = isString(message.toolName) ? message.toolName : "unknown";
2140
2594
  const policy = policies[toolName] ?? "full";
2141
- const source = textFrom(message.content);
2595
+ const notes = describeImages ? imageNotes(message.content, imageNonce) : [];
2596
+ const source = [textFrom(message.content), ...notes].filter(Boolean).join(`
2597
+ `);
2142
2598
  if (policy === "exclude") {
2143
2599
  return `[Tool Result for ${toolName}] (excluded by Advisor tool policy)`;
2144
2600
  }
@@ -2151,7 +2607,7 @@ var toolResultEntry = (message, toolResultMaxLines, toolResultMaxBytes, policies
2151
2607
  return `[Tool Result for ${toolName}] (${message.isError ? "Error " : ""}output):
2152
2608
  ${capped.content}`;
2153
2609
  };
2154
- var conversationEntry = (entry, toolResultMaxLines, toolResultMaxBytes, policies, redact) => {
2610
+ var conversationEntry = (entry, toolResultMaxLines, toolResultMaxBytes, policies, redact, describeImages = false, imageNonce = "") => {
2155
2611
  if (!isRecordOf(entry)) {
2156
2612
  return;
2157
2613
  }
@@ -2163,14 +2619,16 @@ var conversationEntry = (entry, toolResultMaxLines, toolResultMaxBytes, policies
2163
2619
  }
2164
2620
  const { message } = entry;
2165
2621
  if (message.role === "user") {
2166
- const text = textFrom(message.content);
2622
+ const notes = describeImages ? imageNotes(message.content, imageNonce) : [];
2623
+ const text = [textFrom(message.content), ...notes].filter(Boolean).join(`
2624
+ `);
2167
2625
  return text ? `User: ${redact ? redactSecrets(text) : text}` : undefined;
2168
2626
  }
2169
2627
  if (message.role === "assistant") {
2170
2628
  return assistantEntry(message, policies, redact);
2171
2629
  }
2172
2630
  if (message.role === "toolResult" || message.role === "tool") {
2173
- return toolResultEntry(message, toolResultMaxLines, toolResultMaxBytes, policies, redact);
2631
+ return toolResultEntry(message, toolResultMaxLines, toolResultMaxBytes, policies, redact, describeImages, imageNonce);
2174
2632
  }
2175
2633
  };
2176
2634
  var omissionMarker = (omitted) => `[Older context omitted: ${omitted} complete entr${omitted === 1 ? "y" : "ies"}]`;
@@ -2208,11 +2666,11 @@ var selectRecentEntries = (entries, maxChars) => {
2208
2666
  const prefix = `${marker}${separator}${newestTruncated}${separator}`;
2209
2667
  return `${prefix}${entries.at(-1)?.slice(0, Math.max(0, maxChars - prefix.length)) ?? ""}`.slice(0, maxChars);
2210
2668
  };
2211
- var recentConversation = (ctx, maxChars = 15000, toolResultMaxLines = advisorToolResultMaxLinesRef, toolResultMaxBytes = advisorToolResultMaxBytesRef, policies = advisorToolPoliciesRef, redact = advisorRedactSecretsRef) => {
2669
+ var recentConversation = (ctx, maxChars = 15000, toolResultMaxLines = advisorToolResultMaxLinesRef, toolResultMaxBytes = advisorToolResultMaxBytesRef, policies = advisorToolPoliciesRef, redact = advisorRedactSecretsRef, describeImages = false, imageNonce = "") => {
2212
2670
  if (maxChars === 0) {
2213
2671
  return "";
2214
2672
  }
2215
- const entries = ctx.sessionManager.getBranch().map((entry) => conversationEntry(entry, toolResultMaxLines, toolResultMaxBytes, policies, redact)).filter((entry) => entry !== undefined);
2673
+ const entries = ctx.sessionManager.getBranch().map((entry) => conversationEntry(entry, toolResultMaxLines, toolResultMaxBytes, policies, redact, describeImages, imageNonce)).filter((entry) => entry !== undefined);
2216
2674
  return selectRecentEntries(entries, maxChars);
2217
2675
  };
2218
2676
 
@@ -2312,7 +2770,7 @@ var labelFor = (kind, content) => {
2312
2770
  };
2313
2771
  return boundedLabel(`${prefix[kind]}: ${preview || "(no text)"}`);
2314
2772
  };
2315
- var stableId = (index, entryIds, kind, content) => `g_${createHash("sha256").update(JSON.stringify([index, entryIds, kind, content])).digest("hex").slice(0, 16)}`;
2773
+ var stableId = (index, entryIds, kind, content) => `g_${createHash2("sha256").update(JSON.stringify([index, entryIds, kind, content])).digest("hex").slice(0, 16)}`;
2316
2774
  var groupWire = (group) => ({
2317
2775
  bytes: group.bytes,
2318
2776
  content: group.content,
@@ -2325,6 +2783,7 @@ var groupWireBytes = (group) => byteLength(JSON.stringify(groupWire(group)));
2325
2783
  var createGroup = (originalIndex, entryIds, kind, content, required) => ({
2326
2784
  bytes: byteLength(content),
2327
2785
  content,
2786
+ entryIds,
2328
2787
  id: stableId(originalIndex, entryIds, kind, content),
2329
2788
  kind,
2330
2789
  label: labelFor(kind, content),
@@ -2341,7 +2800,7 @@ var pendingAdvisorArguments = (part) => {
2341
2800
  }
2342
2801
  return allowed;
2343
2802
  };
2344
- var pendingInvocationDisclosure = (entry, invocationId, toolResultMaxLines, toolResultMaxBytes, policies, redact, disclosed) => {
2803
+ var pendingInvocationDisclosure = (entry, invocationId, toolResultMaxLines, toolResultMaxBytes, policies, redact, imageNonce, disclosed) => {
2345
2804
  if (!isRecord(entry.message)) {
2346
2805
  return disclosed;
2347
2806
  }
@@ -2352,7 +2811,7 @@ var pendingInvocationDisclosure = (entry, invocationId, toolResultMaxLines, tool
2352
2811
  }
2353
2812
  return { ...part, arguments: pendingAdvisorArguments(part) };
2354
2813
  });
2355
- return conversationEntry({ ...entry, message: { ...message, content } }, toolResultMaxLines, toolResultMaxBytes, policies, redact);
2814
+ return conversationEntry({ ...entry, message: { ...message, content } }, toolResultMaxLines, toolResultMaxBytes, policies, redact, true, imageNonce);
2356
2815
  };
2357
2816
  var adjacentResultMismatch = (immediate, index, callIds, callOwners) => {
2358
2817
  if (immediate?.type === "message" && isRecord(immediate.message) && immediate.message.role === "toolResult" && isString(immediate.message.toolCallId) && !callIds.includes(immediate.message.toolCallId) && !callOwners.has(immediate.message.toolCallId)) {
@@ -2377,7 +2836,7 @@ var collectResults = (calls, callIds, index, resultsByCall, consumedResultIndexe
2377
2836
  return `Tool result at context entry ${resultMatch.index} conflicts with call ${callId}.`;
2378
2837
  }
2379
2838
  consumedResultIndexes.add(resultMatch.index);
2380
- const resultText = conversationEntry(resultMatch.entry, caps.toolResultMaxLines, caps.toolResultMaxBytes, caps.policies, caps.redact);
2839
+ const resultText = conversationEntry(resultMatch.entry, caps.toolResultMaxLines, caps.toolResultMaxBytes, caps.policies, caps.redact, true, caps.imageNonce);
2381
2840
  if (resultText) {
2382
2841
  resultParts.push(resultText);
2383
2842
  }
@@ -2396,7 +2855,7 @@ var missingOutcome = (entry, index, entryId, disclosed, callIds, missing, result
2396
2855
  kind: "omitted"
2397
2856
  };
2398
2857
  }
2399
- const pendingDisclosed = pendingInvocationDisclosure(entry, currentInvocationId, caps.toolResultMaxLines, caps.toolResultMaxBytes, caps.policies, caps.redact, disclosed);
2858
+ const pendingDisclosed = pendingInvocationDisclosure(entry, currentInvocationId, caps.toolResultMaxLines, caps.toolResultMaxBytes, caps.policies, caps.redact, caps.imageNonce, disclosed);
2400
2859
  return {
2401
2860
  group: createGroup(index, [entryId, ...resultEntryIds], "pending-invocation", [pendingDisclosed ?? disclosed, ...resultParts].join(`
2402
2861
 
@@ -2482,7 +2941,7 @@ var buildGroups = (entries, indexed, caps) => {
2482
2941
  if (!isRecord(entry)) {
2483
2942
  continue;
2484
2943
  }
2485
- const disclosed = conversationEntry(entry, caps.toolResultMaxLines, caps.toolResultMaxBytes, caps.policies, caps.redact);
2944
+ const disclosed = conversationEntry(entry, caps.toolResultMaxLines, caps.toolResultMaxBytes, caps.policies, caps.redact, true, caps.imageNonce);
2486
2945
  if (!disclosed) {
2487
2946
  continue;
2488
2947
  }
@@ -2548,6 +3007,7 @@ ${synthesis.trim()}` : undefined;
2548
3007
  // src/scout-context.ts
2549
3008
  var resolveCaps = (options) => ({
2550
3009
  currentInvocationId: options.currentInvocationId,
3010
+ imageNonce: options.imageNonce ?? "",
2551
3011
  maxConversationChars: options.maxConversationChars,
2552
3012
  maxGroupBytes: options.maxGroupBytes ?? SCOUT_GROUP_MAX_BYTES,
2553
3013
  maxGroups: options.maxGroups ?? SCOUT_MANIFEST_MAX_GROUPS,
@@ -2994,7 +3454,7 @@ var runAdvisorScout = async (ctx, manifest, parentSignal, onEvent, timeoutMs = a
2994
3454
  };
2995
3455
 
2996
3456
  // src/scout-curation.ts
2997
- var curateAdvisorConversation = async (ctx, legacyConversation, signal, onScout, enabled = advisorScoutEnabledRef, runScout = runAdvisorScout, currentInvocationId, maxChars) => {
3457
+ var curateAdvisorConversation = async (ctx, legacyConversation, signal, onScout, enabled = advisorScoutEnabledRef, runScout = runAdvisorScout, currentInvocationId, maxChars, imageNonce = "") => {
2998
3458
  if (!enabled) {
2999
3459
  return { conversation: legacyConversation };
3000
3460
  }
@@ -3003,6 +3463,7 @@ var curateAdvisorConversation = async (ctx, legacyConversation, signal, onScout,
3003
3463
  }
3004
3464
  const built = buildScoutManifest(ctx, {
3005
3465
  currentInvocationId,
3466
+ imageNonce,
3006
3467
  maxConversationChars: maxChars,
3007
3468
  maxManifestBytes: SCOUT_MANIFEST_MAX_BYTES
3008
3469
  });
@@ -3027,11 +3488,13 @@ var curateAdvisorConversation = async (ctx, legacyConversation, signal, onScout,
3027
3488
  if (!outcome.ok && outcome.cancelled) {
3028
3489
  throw signal?.reason instanceof Error ? signal.reason : new Error("Advisor operation cancelled during Scout.");
3029
3490
  }
3030
- let conversation = legacyConversation;
3031
3491
  if (outcome.ok) {
3032
- conversation = maxChars === undefined ? outcome.conversation : reconstructScoutConversation(built.manifest, outcome.selection.selectedIds, outcome.selection.synthesis, maxChars);
3492
+ const selected = new Set(outcome.selection.selectedIds);
3493
+ const selectedEntryIds = built.manifest.groups.filter((group) => group.required || selected.has(group.id)).flatMap((group) => group.entryIds ?? []);
3494
+ const conversation = maxChars === undefined ? outcome.conversation : reconstructScoutConversation(built.manifest, outcome.selection.selectedIds, outcome.selection.synthesis, maxChars);
3495
+ return { conversation, scout: outcome, selectedEntryIds };
3033
3496
  }
3034
- return { conversation, scout: outcome };
3497
+ return { conversation: legacyConversation, scout: outcome };
3035
3498
  };
3036
3499
 
3037
3500
  // src/tools/prompts.ts
@@ -3097,7 +3560,7 @@ var advisorRepositoryContext = (result, requested, allowed, budget) => {
3097
3560
 
3098
3561
  `);
3099
3562
  };
3100
- var advisorRequestConversation = (ctx, maxChars = contextMaxCharsRef) => recentConversation(ctx, maxChars);
3563
+ var advisorRequestConversation = (ctx, maxChars = contextMaxCharsRef, describeImages = false, imageNonce = "") => recentConversation(ctx, maxChars, undefined, undefined, undefined, undefined, describeImages, imageNonce);
3101
3564
  var advisorInvocationGuidelines = () => {
3102
3565
  if (isSimpleMode()) {
3103
3566
  return [
@@ -3149,18 +3612,42 @@ var assembleConsultationContext = async (options) => {
3149
3612
  const changes = collectGitContext(ctx.cwd, level, gitBudget, advisorRedactSecretsRef ? redactSecrets : undefined);
3150
3613
  const changeText = advisorRepositoryContext(changes, options.gitContext ?? allowed, level, gitBudget);
3151
3614
  const conversationBudget = Math.max(0, contextMaxCharsRef - changeText.length);
3152
- const legacyConversation = advisorRequestConversation(ctx, conversationBudget);
3153
- const curated = await curateAdvisorConversation(ctx, legacyConversation, options.signal, options.onScout, advisorScoutEnabledRef, runAdvisorScout, options.currentInvocationId, conversationBudget);
3615
+ const imageNonce = randomUUID();
3616
+ const legacyConversation = advisorRequestConversation(ctx, conversationBudget, true, imageNonce);
3617
+ const curated = await curateAdvisorConversation(ctx, legacyConversation, options.signal, options.onScout, advisorScoutEnabledRef, runAdvisorScout, options.currentInvocationId, conversationBudget, imageNonce);
3154
3618
  const preferences = await readProjectPreferences(ctx, ATTACHMENT_TEXT_MAX_BYTES, advisorRedactSecretsRef);
3155
3619
  const draftText = options.draft ? redactAndCapText(options.draft, ATTACHMENT_TEXT_MAX_BYTES, advisorRedactSecretsRef) : undefined;
3156
3620
  const untracked = await readUntrackedFiles(ctx.cwd, options.includeUntracked ?? [], advisorUntrackedContentRef, advisorRedactSecretsRef);
3157
3621
  const tracked = await readTrackedFiles(ctx.cwd, options.includeTracked ?? [], advisorTrackedFileContentRef, advisorRedactSecretsRef, Math.max(0, ATTACHMENTS_TOTAL_MAX_BYTES - untracked.reduce((sum, item) => sum + item.bytes, 0)));
3622
+ const supportsImages = options.supportsImages ?? false;
3623
+ const untrackedImages = await readImageFiles(ctx.cwd, options.includeUntracked ?? [], supportsImages && advisorUntrackedContentRef, "untracked", ADVISOR_IMAGES_TOTAL_MAX_BYTES, ADVISOR_IMAGES_MAX_COUNT);
3624
+ const trackedImages = await readImageFiles(ctx.cwd, options.includeTracked ?? [], supportsImages && advisorTrackedFileContentRef, "tracked", ADVISOR_IMAGES_TOTAL_MAX_BYTES - untrackedImages.images.reduce((sum, item) => sum + item.bytes, 0), ADVISOR_IMAGES_MAX_COUNT - untrackedImages.images.length);
3625
+ const images = [...untrackedImages.images, ...trackedImages.images].map((item) => ({
3626
+ image: item.image,
3627
+ label: `File ${JSON.stringify(item.path)}`
3628
+ }));
3629
+ let remainingBytes = ADVISOR_IMAGES_TOTAL_MAX_BYTES - [...untrackedImages.images, ...trackedImages.images].reduce((sum, item) => sum + item.bytes, 0);
3630
+ let imageOmissions = untrackedImages.omitted + trackedImages.omitted;
3631
+ const census = selectedConversationImages(ctx, curated.conversation, advisorToolPoliciesRef, curated.scout?.ok === true ? new Set(curated.selectedEntryIds) : undefined, imageNonce);
3632
+ for (const item of census.selected) {
3633
+ const bytes = Buffer.from(item.image.data, "base64").length;
3634
+ if (!supportsImages || images.length >= ADVISOR_IMAGES_MAX_COUNT || bytes > remainingBytes) {
3635
+ imageOmissions += 1;
3636
+ continue;
3637
+ }
3638
+ remainingBytes -= bytes;
3639
+ images.push({ image: item.image, label: item.marker });
3640
+ }
3158
3641
  return {
3159
3642
  changeText,
3160
3643
  conversation: curated.conversation,
3161
3644
  draftText,
3645
+ imageOmissions,
3646
+ imagePartsSeen: census.imagePartsSeen,
3647
+ images,
3162
3648
  preferences,
3163
3649
  scout: curated.scout,
3650
+ supportsImages,
3164
3651
  tracked,
3165
3652
  untracked
3166
3653
  };
@@ -3263,35 +3750,6 @@ var adviceForGateText = (result) => `**Decision: ${result.decision}**
3263
3750
 
3264
3751
  ${result.markdown}`;
3265
3752
 
3266
- // src/tools/model-access.ts
3267
- var currentModelRef = (ctx) => {
3268
- const { model } = ctx;
3269
- return model ? `${model.provider}/${model.id}` : undefined;
3270
- };
3271
- var advisorModelAccess = (ctx) => {
3272
- const modelRef = currentModelRef(ctx);
3273
- if (advisorModelWhitelistRef.length === 0) {
3274
- return modelRef ? { allowed: true, modelRef } : { allowed: true };
3275
- }
3276
- if (modelRef && advisorModelWhitelistRef.includes(modelRef)) {
3277
- return { allowed: true, modelRef };
3278
- }
3279
- const current = modelRef ?? "no current model";
3280
- const denial = {
3281
- allowed: false,
3282
- reason: `Advisor calls are restricted to the configured model whitelist (${advisorModelWhitelistRef.join(", ")}). Current model: ${current}.`
3283
- };
3284
- if (modelRef) {
3285
- denial.modelRef = modelRef;
3286
- }
3287
- return denial;
3288
- };
3289
- var advisorModelIsAllowed = (ctx) => advisorModelAccess(ctx).allowed;
3290
- var advisorModelAccessReason = (ctx) => {
3291
- const access = advisorModelAccess(ctx);
3292
- return access.allowed ? undefined : access.reason;
3293
- };
3294
-
3295
3753
  // src/tools/consultation.ts
3296
3754
  class AdvisorNoAdviceError extends Error {
3297
3755
  constructor() {
@@ -3305,20 +3763,38 @@ ${item.text}
3305
3763
  var collectAdvisorResponse = async (options) => {
3306
3764
  const { ctx, question, signal, systemPrompt } = options;
3307
3765
  loadConfig(ctx);
3766
+ if (sameModelAdvisorDisabled(ctx)) {
3767
+ throw new Error(sameModelAdvisorNotice);
3768
+ }
3308
3769
  const accessReason = advisorModelAccessReason(ctx);
3309
3770
  if (accessReason) {
3310
3771
  throw new Error(accessReason);
3311
3772
  }
3312
3773
  const resolved = await resolveConfiguredModel(ctx, advisorRef, "Advisor");
3313
- const context = await assembleConsultationContext(options);
3774
+ const context = await assembleConsultationContext({
3775
+ ...options,
3776
+ supportsImages: resolved.model.input.includes("image")
3777
+ });
3314
3778
  const outboundQuestion = advisorRedactSecretsRef && question !== undefined ? redactSecrets(question) : question;
3779
+ const imageNotice = context.imageOmissions || context.images.length || /\[Image[ :]/u.test(context.conversation) ? `
3780
+
3781
+ Image disclosure: ${context.supportsImages ? `${context.images.length} image(s) attached below; ${context.imageOmissions} image(s) withheld by format, consent, or size/count limits` : "Advisor model does not support image input; no pixels were forwarded"}. Only the images explicitly attached below have pixels available. Other image markers, image paths, and text descriptions are not visual evidence.` : "";
3315
3782
  const messages = [
3316
3783
  {
3317
3784
  content: [
3318
3785
  {
3319
- text: advisorMessageText(context.conversation, outboundQuestion, context.changeText, context.draftText, context.preferences?.text, context.untracked.map(fileTag), context.tracked.map(fileTag)),
3786
+ text: `${advisorMessageText(context.conversation, outboundQuestion, context.changeText, context.draftText, context.preferences?.text, context.untracked.map(fileTag), context.tracked.map(fileTag))}${imageNotice}`,
3320
3787
  type: "text"
3321
- }
3788
+ },
3789
+ ...context.images.flatMap(({ image, label }) => [
3790
+ {
3791
+ text: `
3792
+
3793
+ ${escapeRepositoryText(label)}: attached image pixels (untrusted data).`,
3794
+ type: "text"
3795
+ },
3796
+ image
3797
+ ])
3322
3798
  ],
3323
3799
  role: "user",
3324
3800
  timestamp: Date.now()
@@ -3337,6 +3813,10 @@ var collectAdvisorResponse = async (options) => {
3337
3813
  }
3338
3814
  const response = {
3339
3815
  draftBytes: context.draftText ? Buffer.byteLength(context.draftText, "utf-8") : undefined,
3816
+ imageBytes: context.images.reduce((sum, item) => sum + Buffer.from(item.image.data, "base64").length, 0) || undefined,
3817
+ imageCount: context.images.length,
3818
+ imageOmissions: context.imageOmissions || undefined,
3819
+ imagePartsSeen: context.imagePartsSeen || undefined,
3340
3820
  markdown,
3341
3821
  model: advisorRef,
3342
3822
  preferenceBytes: context.preferences?.bytes,
@@ -3364,7 +3844,7 @@ var consultAdvisor = async (ctx, question, signal, onChunk, trigger = "executor-
3364
3844
  signal,
3365
3845
  systemPrompt: ADVISOR_SYSTEM
3366
3846
  });
3367
- return { ...result, adviceId: randomUUID(), trigger };
3847
+ return { ...result, adviceId: randomUUID2(), trigger };
3368
3848
  };
3369
3849
  var runAdvisorGate = async (ctx, question, trigger = "repeated-tool-call", signal, onChunk, onScout, currentInvocationId) => {
3370
3850
  try {
@@ -4187,6 +4667,7 @@ class CommandRuntime {
4187
4667
  manualProgressSequence = 0;
4188
4668
  pendingExecutorModelRef;
4189
4669
  suppressModelSelectionSync = false;
4670
+ lastSameModelDisabled;
4190
4671
  constructor(pi, dependencies = {}) {
4191
4672
  this.pi = pi;
4192
4673
  this.advisorSessionState = dependencies.sessionState ?? advisorSessionState;
@@ -4197,6 +4678,21 @@ class CommandRuntime {
4197
4678
  flowEnabled() {
4198
4679
  return this.pi.getActiveTools().includes("ask_advisor");
4199
4680
  }
4681
+ resetSameModelNotice() {
4682
+ this.lastSameModelDisabled = undefined;
4683
+ }
4684
+ updateSameModelNotice(ctx, model = ctx.model) {
4685
+ if (!this.flowEnabled()) {
4686
+ return;
4687
+ }
4688
+ const disabled = sameModelAdvisorDisabled(ctx, model);
4689
+ if (disabled && this.lastSameModelDisabled !== true) {
4690
+ notify(ctx, sameModelAdvisorNotice, "info");
4691
+ } else if (!disabled && this.lastSameModelDisabled === true) {
4692
+ notify(ctx, "Advisor re-enabled: executor and advisor models differ.", "info");
4693
+ }
4694
+ this.lastSameModelDisabled = disabled;
4695
+ }
4200
4696
  nextManualProgressId() {
4201
4697
  this.manualProgressSequence += 1;
4202
4698
  return `manual-${this.manualProgressSequence}`;
@@ -4338,12 +4834,14 @@ var activateAdvisor = async (runtime, args, ctx, announce = true) => {
4338
4834
  "record_advisor_outcome"
4339
4835
  ]);
4340
4836
  }
4837
+ const activeModel = isMarkedSubagent() ? ctx.model : findConfiguredModel(ctx, executorRef);
4838
+ runtime.updateSameModelNotice(ctx, activeModel);
4341
4839
  if (announce) {
4342
4840
  const activeExecutorRef = effectiveExecutorRef(ctx);
4343
4841
  const activeExecutorEffort = effectiveExecutorEffort(ctx);
4344
4842
  notify(ctx, `${ADVISOR_ACTIVATION_EXPLANATION}
4345
4843
 
4346
- Advisor flow ready — Executor: ${activeExecutorRef} (thinking: ${activeExecutorEffort || "default"}) · Advisor: ${advisorRef} (thinking: ${advisorEffortRef || "default"})`, "info");
4844
+ Advisor flow ${sameModelAdvisorDisabled(ctx, activeModel) ? "configured" : "ready"} — Executor: ${activeExecutorRef} (thinking: ${activeExecutorEffort || "default"}) · Advisor: ${advisorRef} (thinking: ${advisorEffortRef || "default"})`, "info");
4347
4845
  }
4348
4846
  };
4349
4847
 
@@ -4351,10 +4849,13 @@ Advisor flow ready — Executor: ${activeExecutorRef} (thinking: ${activeExecuto
4351
4849
  var registerCommandLifecycle = (runtime, activateAdvisor) => {
4352
4850
  runtime.pi.on("session_start", async (_event, ctx) => {
4353
4851
  runtime.pendingExecutorModelRef = undefined;
4852
+ runtime.resetSameModelNotice();
4354
4853
  try {
4355
4854
  loadConfig(ctx);
4356
4855
  if (alwaysOnRef) {
4357
4856
  await activateAdvisor("", ctx, false);
4857
+ } else {
4858
+ runtime.updateSameModelNotice(ctx);
4358
4859
  }
4359
4860
  } catch (error) {
4360
4861
  const message = error instanceof Error ? error.message : String(error);
@@ -4362,6 +4863,9 @@ var registerCommandLifecycle = (runtime, activateAdvisor) => {
4362
4863
  }
4363
4864
  });
4364
4865
  runtime.pi.on("model_select", (event, ctx) => {
4866
+ if (!runtime.suppressModelSelectionSync) {
4867
+ runtime.updateSameModelNotice(ctx, event.model);
4868
+ }
4365
4869
  if (event.source !== "set" || runtime.suppressModelSelectionSync || isMarkedSubagent()) {
4366
4870
  return;
4367
4871
  }
@@ -5464,6 +5968,10 @@ var registerManualCommand = (runtime) => {
5464
5968
  if (!loadCommandConfig(ctx)) {
5465
5969
  return;
5466
5970
  }
5971
+ if (sameModelAdvisorDisabled(ctx)) {
5972
+ notify(ctx, sameModelAdvisorNotice, "info");
5973
+ return;
5974
+ }
5467
5975
  const accessReason = advisorModelAccessReason(ctx);
5468
5976
  if (accessReason) {
5469
5977
  notify(ctx, accessReason, "warning");
@@ -5507,6 +6015,10 @@ var registerManualCommand = (runtime) => {
5507
6015
  } else {
5508
6016
  question = resolveAdvisorRequest(args);
5509
6017
  }
6018
+ if (sameModelAdvisorDisabled(ctx)) {
6019
+ notify(ctx, sameModelAdvisorNotice, "info");
6020
+ return;
6021
+ }
5510
6022
  if (!isSimpleMode()) {
5511
6023
  runtime.advisorSessionState.consumeCall();
5512
6024
  }
@@ -5561,6 +6073,7 @@ var registerModelCommands = (runtime) => {
5561
6073
  persistExecutor: true
5562
6074
  });
5563
6075
  runtime.pendingExecutorModelRef = undefined;
6076
+ runtime.updateSameModelNotice(ctx);
5564
6077
  ctx.ui.notify(`Saved Executor + Advisor configurations to ${path}`, "info");
5565
6078
  }
5566
6079
  });
@@ -6320,7 +6833,7 @@ var createSettingsItems = ({
6320
6833
  }
6321
6834
  ];
6322
6835
  if (settings.simpleMode) {
6323
- items.push(modelWhitelist);
6836
+ items.push(modelWhitelist, toggle("disableSameModel", "Disable same-model Advisor", "Skip advice when the active Executor and Advisor use the same provider/model; turn off to allow higher-effort same-model reviews.", settings.disableSameModel, true));
6324
6837
  return items;
6325
6838
  }
6326
6839
  items.push({
@@ -6329,7 +6842,7 @@ var createSettingsItems = ({
6329
6842
  id: "effort",
6330
6843
  label: "Advisor reasoning",
6331
6844
  values: withCurrentValue(currentEffort(settings.effort), effortLevels)
6332
- }, modelWhitelist, toggle("scoutEnabled", "Experimental Advisor Scout", "Enable the experimental Scout before Advisor calls.", settings.scoutEnabled, false), scoutTimeoutItem(settings), toggle("showUsageDetails", "Show usage and cost details", "Show token usage and cost details in Advisor responses.", settings.showUsageDetails, true), toggle("showUsageFooter", "Show usage in footer", "Show the current Advisor usage summary in the footer.", settings.showUsageFooter, false), toggle("planGate", "Plan gate", "Ask the Advisor to review implementation plans.", settings.planGate, true), toggle("failureGate", "Failure gate", "Ask the Advisor to review repeated failures.", settings.failureGate, true), toggle("completionGate", "Completion gate", "Ask the Advisor to review work before declaring success.", settings.completionGate, true), toggle("collapseResponses", "Collapse long responses", "Collapse long Advisor responses in the transcript.", settings.collapseResponses, false), {
6845
+ }, modelWhitelist, toggle("disableSameModel", "Disable same-model Advisor", "Skip advice when the active Executor and Advisor use the same provider/model; turn off to allow higher-effort same-model reviews.", settings.disableSameModel, true), toggle("scoutEnabled", "Experimental Advisor Scout", "Enable the experimental Scout before Advisor calls.", settings.scoutEnabled, false), scoutTimeoutItem(settings), toggle("showUsageDetails", "Show usage and cost details", "Show token usage and cost details in Advisor responses.", settings.showUsageDetails, true), toggle("showUsageFooter", "Show usage in footer", "Show the current Advisor usage summary in the footer.", settings.showUsageFooter, false), toggle("planGate", "Plan gate", "Ask the Advisor to review implementation plans.", settings.planGate, true), toggle("failureGate", "Failure gate", "Ask the Advisor to review repeated failures.", settings.failureGate, true), toggle("completionGate", "Completion gate", "Ask the Advisor to review work before declaring success.", settings.completionGate, true), toggle("collapseResponses", "Collapse long responses", "Collapse long Advisor responses in the transcript.", settings.collapseResponses, false), {
6333
6846
  currentValue: settings.customRule || "None",
6334
6847
  description: "Add a rule that triggers Advisor involvement.",
6335
6848
  id: "customRule",
@@ -6507,6 +7020,7 @@ var BOOLEAN_SETTING_FIELDS = [
6507
7020
  "blockOnBlocked",
6508
7021
  "collapseResponses",
6509
7022
  "completionGate",
7023
+ "disableSameModel",
6510
7024
  "failureGate",
6511
7025
  "herdrIntegration",
6512
7026
  "outcomeLogging",
@@ -6779,6 +7293,7 @@ var applySessionSettings = (settings) => {
6779
7293
  setAdvisorPlanGateRef(settings.planGate);
6780
7294
  setAdvisorFailureGateRef(settings.failureGate);
6781
7295
  setAdvisorCompletionGateRef(settings.completionGate);
7296
+ setAdvisorDisableSameModelRef(settings.disableSameModel ?? true);
6782
7297
  setAdvisorCollapseResponsesRef(settings.collapseResponses);
6783
7298
  setAdvisorCustomInvocationRef(settings.customRule);
6784
7299
  setAdvisorBlockOnBlockedRef(settings.blockOnBlocked ?? true);
@@ -6853,6 +7368,7 @@ var registerSettingsCommands = (runtime) => {
6853
7368
  onChange: (settings) => {
6854
7369
  try {
6855
7370
  saveAdvisorSettings(ctx, settings);
7371
+ runtime.updateSameModelNotice(ctx);
6856
7372
  runtime.updateAdvisorUsageStatus(ctx);
6857
7373
  } catch (error) {
6858
7374
  const message = error instanceof Error ? error.message : String(error);
@@ -6869,6 +7385,7 @@ var registerSettingsCommands = (runtime) => {
6869
7385
  description: "Disable on-demand Advisor calls; keep the current model",
6870
7386
  handler: (_args, ctx) => {
6871
7387
  runtime.pi.setActiveTools(runtime.pi.getActiveTools().filter((name) => name !== "ask_advisor" && name !== "record_advisor_outcome"));
7388
+ runtime.resetSameModelNotice();
6872
7389
  const wasAlwaysOn = alwaysOnRef;
6873
7390
  if (wasAlwaysOn) {
6874
7391
  const persisted = getPersistedModelRefs();
@@ -6902,7 +7419,7 @@ import {
6902
7419
  chmod,
6903
7420
  link,
6904
7421
  mkdir,
6905
- open as open3,
7422
+ open as open4,
6906
7423
  readFile,
6907
7424
  stat,
6908
7425
  unlink,
@@ -6964,7 +7481,7 @@ var withOutcomeLock = async (run) => {
6964
7481
  const lockPath = `${outcomeLogPath()}.lock`;
6965
7482
  for (let attempt = 0;attempt < 200; attempt += 1) {
6966
7483
  try {
6967
- const lock = await open3(lockPath, "wx", 384);
7484
+ const lock = await open4(lockPath, "wx", 384);
6968
7485
  const identity = await lock.stat();
6969
7486
  try {
6970
7487
  return await run();
@@ -7147,7 +7664,7 @@ var handleJevTurnEnd = async (registration, ctx) => {
7147
7664
  });
7148
7665
  session.recordJevGateCheck(result.usage);
7149
7666
  const shouldConsult = composeTurnGateVerdict(result.answers, advisorJevTurnGateNoulThresholdRef);
7150
- if (!consumeTurnGateBudget(session, shouldConsult)) {
7667
+ if (!advisorModelIsAllowed(ctx) || !consumeTurnGateBudget(session, shouldConsult)) {
7151
7668
  return;
7152
7669
  }
7153
7670
  const herdrActivity = registration.herdrActivity ?? herdrAdvisorActivity;
@@ -7214,17 +7731,30 @@ var syncRenderPhase = (context, phase) => {
7214
7731
  }
7215
7732
  context.state.phase = phase;
7216
7733
  };
7734
+ var formatByteSize = (bytes) => {
7735
+ if (bytes >= 1024 ** 3) {
7736
+ return `${(bytes / 1024 ** 3).toFixed(1)} GiB`;
7737
+ }
7738
+ if (bytes >= 1024 ** 2) {
7739
+ return `${(bytes / 1024 ** 2).toFixed(1)} MiB`;
7740
+ }
7741
+ if (bytes >= 1024) {
7742
+ return `${Math.round(bytes / 1024)} KiB`;
7743
+ }
7744
+ return `${bytes} B`;
7745
+ };
7217
7746
  var attachmentLabels = (details) => [
7218
- details?.draftBytes ? `Draft attached · ${details.draftBytes} B` : undefined,
7219
- details?.preferenceBytes ? `Project preferences attached · ${details.preferenceBytes} B` : undefined,
7220
- details?.trackedBytes ? `Tracked files attached · ${details.trackedBytes} B` : undefined,
7221
- details?.untrackedBytes ? `Untracked files attached · ${details.untrackedBytes} B` : undefined
7747
+ details?.draftBytes ? `Draft attached · ${formatByteSize(details.draftBytes)}` : undefined,
7748
+ details?.preferenceBytes ? `Project preferences attached · ${formatByteSize(details.preferenceBytes)}` : undefined,
7749
+ details?.trackedBytes ? `Tracked files attached · ${formatByteSize(details.trackedBytes)}` : undefined,
7750
+ details?.untrackedBytes ? `Untracked files attached · ${formatByteSize(details.untrackedBytes)}` : undefined,
7751
+ details?.imageCount ? `${details.imageCount} image${details.imageCount === 1 ? "" : "s"} attached${details.imageBytes ? ` · ${formatByteSize(details.imageBytes)}` : ""}${details.imageOmissions ? ` · ${details.imageOmissions} withheld` : ""}` : undefined
7222
7752
  ].filter((label) => label !== undefined);
7223
- var renderJevSkipBox = (box, result, expanded, theme) => {
7753
+ var renderSkipBox = (box, result, expanded, theme) => {
7224
7754
  const details = advisorResultDetails(result);
7225
7755
  const lines = [
7226
7756
  theme.fg("dim", theme.bold("◆ ADVISOR · SKIPPED")),
7227
- theme.fg("dim", ` ${details?.jev?.reason ?? ""}`)
7757
+ theme.fg("dim", ` ${details?.skipReason ?? details?.jev?.reason ?? ""}`)
7228
7758
  ];
7229
7759
  box.addChild(new Text5(lines.join(`
7230
7760
  `), 0, 0));
@@ -7286,8 +7816,8 @@ var renderFinalAdvisorResult = (box, result, expanded, theme, context) => {
7286
7816
  context.state.timerId = undefined;
7287
7817
  }
7288
7818
  const details = advisorResultDetails(result);
7289
- if (details?.jev?.skipped) {
7290
- renderJevSkipBox(box, result, expanded, theme);
7819
+ if (details?.jev?.skipped || details?.skipReason) {
7820
+ renderSkipBox(box, result, expanded, theme);
7291
7821
  return;
7292
7822
  }
7293
7823
  if (details?.scout) {
@@ -7353,7 +7883,22 @@ var registerAskAdvisorTool = ({
7353
7883
  pi.registerTool({
7354
7884
  description: "Consult the on-demand Advisor model for strategic guidance. Call with an empty object for a contextual review; attach an optional draft for concrete plan or completion review. If the Advisor explicitly names a missing file, you may make a sequential follow-up call with includeTrackedFiles when enabled and relevant.",
7355
7885
  async execute(_id, params, signal, onUpdate, ctx) {
7886
+ const skipSameModel = () => {
7887
+ session.releaseCall(_id);
7888
+ reservedCalls.delete(_id);
7889
+ return {
7890
+ content: [{ text: sameModelAdvisorNotice, type: "text" }],
7891
+ details: {
7892
+ skipReason: sameModelAdvisorNotice,
7893
+ text: sameModelAdvisorNotice
7894
+ }
7895
+ };
7896
+ };
7356
7897
  try {
7898
+ loadConfig(ctx);
7899
+ if (sameModelAdvisorDisabled(ctx)) {
7900
+ return skipSameModel();
7901
+ }
7357
7902
  assertAdvisorModelAccess(ctx);
7358
7903
  } catch (error) {
7359
7904
  session.releaseCall(_id);
@@ -7398,6 +7943,9 @@ var registerAskAdvisorTool = ({
7398
7943
  }
7399
7944
  };
7400
7945
  }
7946
+ if (sameModelAdvisorDisabled(ctx)) {
7947
+ return skipSameModel();
7948
+ }
7401
7949
  if (!simpleMode && !session.canConsult(getAdvisorMaxCallsPerSession(), _id)) {
7402
7950
  throw new Error("Advisor call budget exhausted for this session.");
7403
7951
  }
@@ -7460,6 +8008,10 @@ var registerAskAdvisorTool = ({
7460
8008
  adviceId: result.adviceId,
7461
8009
  advisor: result.model,
7462
8010
  draftBytes: result.draftBytes,
8011
+ imageBytes: result.imageBytes,
8012
+ imageCount: result.imageCount,
8013
+ imageOmissions: result.imageOmissions,
8014
+ imagePartsSeen: result.imagePartsSeen,
7463
8015
  preferenceBytes: result.preferenceBytes,
7464
8016
  question: resolveAdvisorRequest(params.question),
7465
8017
  scout: scoutDetails,
@@ -7713,7 +8265,7 @@ var registerToolLifecycle = ({
7713
8265
  return;
7714
8266
  }
7715
8267
  loadConfig(ctx);
7716
- if (!advisorModelAccess(ctx).allowed) {
8268
+ if (!advisorModelAccess(ctx).allowed || sameModelAdvisorDisabled(ctx)) {
7717
8269
  return;
7718
8270
  }
7719
8271
  const guidelines = advisorInvocationGuidelines();
@@ -7743,6 +8295,9 @@ ${guidelines.map((rule) => `- ${rule}`).join(`
7743
8295
  if (!loadConfigOrSkipGating(ctx)) {
7744
8296
  return;
7745
8297
  }
8298
+ if (sameModelAdvisorDisabled(ctx)) {
8299
+ return;
8300
+ }
7746
8301
  const accessBlock = modelAccessBlock(event.toolName, ctx);
7747
8302
  if (accessBlock) {
7748
8303
  return accessBlock;