pi-advisor-flow 0.8.3 → 0.9.1

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 (40) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +6 -5
  3. package/dist/index.js +685 -96
  4. package/package.json +14 -14
  5. package/src/attachments.ts +21 -5
  6. package/src/commands/activation.ts +6 -1
  7. package/src/commands/lifecycle.ts +6 -0
  8. package/src/commands/manual-command.ts +44 -12
  9. package/src/commands/manual-consultation.ts +7 -3
  10. package/src/commands/model-commands.ts +1 -0
  11. package/src/commands/runtime.ts +36 -10
  12. package/src/commands/settings-commands.ts +2 -0
  13. package/src/commands/settings-persistence.ts +2 -0
  14. package/src/commands/types.ts +5 -0
  15. package/src/config/defaults.ts +7 -0
  16. package/src/config/schema.ts +7 -0
  17. package/src/config/state.ts +5 -0
  18. package/src/config/types.ts +1 -0
  19. package/src/conversation.ts +29 -7
  20. package/src/image-attachments.ts +118 -0
  21. package/src/image-validation.ts +288 -0
  22. package/src/images.ts +151 -0
  23. package/src/scout-context.ts +1 -0
  24. package/src/scout-curation.ts +11 -4
  25. package/src/scout-groups.ts +13 -3
  26. package/src/scout-types.ts +2 -0
  27. package/src/tools/consult-context.ts +78 -2
  28. package/src/tools/consultation.ts +36 -4
  29. package/src/tools/jev-turn-gate.ts +4 -1
  30. package/src/tools/model-access.ts +21 -2
  31. package/src/tools/prompts.ts +14 -2
  32. package/src/tools/register-ask-advisor.ts +28 -1
  33. package/src/tools/register-lifecycle.ts +8 -2
  34. package/src/tools/render-advisor-result.ts +33 -9
  35. package/src/tools/scout-status.ts +11 -8
  36. package/src/tools/types.ts +9 -0
  37. package/src/ui/settings-items.ts +17 -1
  38. package/src/ui/settings-mutations.ts +1 -0
  39. package/src/ui/types.ts +1 -0
  40. package/src/ui-guard.ts +31 -0
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 {
@@ -3404,6 +3884,30 @@ var runAdvisorGate = async (ctx, question, trigger = "repeated-tool-call", signa
3404
3884
  // src/tools/scout-status.ts
3405
3885
  import { Text as Text2 } from "@earendil-works/pi-tui";
3406
3886
 
3887
+ // src/ui-guard.ts
3888
+ var isStaleContext = (error) => error instanceof Error && /stale/iu.test(error.message);
3889
+ var uiAvailable = (ctx) => {
3890
+ try {
3891
+ return ctx.hasUI;
3892
+ } catch (error) {
3893
+ if (isStaleContext(error)) {
3894
+ return false;
3895
+ }
3896
+ throw error;
3897
+ }
3898
+ };
3899
+ var uiAction = (ctx, action) => {
3900
+ try {
3901
+ if (ctx.hasUI) {
3902
+ action(ctx.ui);
3903
+ }
3904
+ } catch (error) {
3905
+ if (!isStaleContext(error)) {
3906
+ throw error;
3907
+ }
3908
+ }
3909
+ };
3910
+
3407
3911
  // src/tools/render-common.ts
3408
3912
  import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
3409
3913
  import {
@@ -3591,14 +4095,14 @@ class ScoutStatusManager {
3591
4095
  }
3592
4096
  }
3593
4097
  update(ctx, token, event) {
3594
- if (this.#retired.has(token) || !ctx.hasUI) {
4098
+ if (this.#retired.has(token) || !uiAvailable(ctx)) {
3595
4099
  return;
3596
4100
  }
3597
4101
  this.#known.add(token);
3598
4102
  if (event.type === "call" || event.type === "chunk") {
3599
4103
  this.#active.add(token);
3600
4104
  if (this.showStatus) {
3601
- ctx.ui.setStatus("advisor-scout", "Scout curating…");
4105
+ uiAction(ctx, (ui) => ui.setStatus("advisor-scout", "Scout curating…"));
3602
4106
  }
3603
4107
  return;
3604
4108
  }
@@ -3608,10 +4112,10 @@ class ScoutStatusManager {
3608
4112
  this.#active.delete(token);
3609
4113
  this.#known.delete(token);
3610
4114
  this.#retired.add(token);
3611
- if (!(ctx.hasUI && this.showStatus)) {
4115
+ if (!(uiAvailable(ctx) && this.showStatus)) {
3612
4116
  return;
3613
4117
  }
3614
- ctx.ui.setStatus("advisor-scout", this.#active.size > 0 ? "Scout curating…" : undefined);
4118
+ uiAction(ctx, (ui) => ui.setStatus("advisor-scout", this.#active.size > 0 ? "Scout curating…" : undefined));
3615
4119
  }
3616
4120
  clear(ctx) {
3617
4121
  for (const token of this.#known) {
@@ -3619,8 +4123,8 @@ class ScoutStatusManager {
3619
4123
  }
3620
4124
  this.#known.clear();
3621
4125
  this.#active.clear();
3622
- if (ctx.hasUI && this.showStatus) {
3623
- ctx.ui.setStatus("advisor-scout", undefined);
4126
+ if (this.showStatus) {
4127
+ uiAction(ctx, (ui) => ui.setStatus("advisor-scout", undefined));
3624
4128
  }
3625
4129
  }
3626
4130
  }
@@ -4158,9 +4662,7 @@ var advisorSessionState = new AdvisorSessionState;
4158
4662
 
4159
4663
  // src/commands/runtime.ts
4160
4664
  var notify = (ctx, message, level) => {
4161
- if (ctx.hasUI) {
4162
- ctx.ui.notify(message, level);
4163
- }
4665
+ uiAction(ctx, (ui) => ui.notify(message, level));
4164
4666
  };
4165
4667
  var reportManualBudgetExhausted = (ctx) => {
4166
4668
  const message = "Advisor call budget exhausted for this session.";
@@ -4168,9 +4670,7 @@ var reportManualBudgetExhausted = (ctx) => {
4168
4670
  notifyHerdrAdvisorFailure("Advisor budget exhausted", message);
4169
4671
  };
4170
4672
  var requestManualRender = (ctx) => {
4171
- if (ctx.hasUI) {
4172
- ctx.ui.setStatus("advisor-manual", undefined);
4173
- }
4673
+ uiAction(ctx, (ui) => ui.setStatus("advisor-manual", undefined));
4174
4674
  };
4175
4675
 
4176
4676
  class CommandRuntime {
@@ -4187,6 +4687,7 @@ class CommandRuntime {
4187
4687
  manualProgressSequence = 0;
4188
4688
  pendingExecutorModelRef;
4189
4689
  suppressModelSelectionSync = false;
4690
+ lastSameModelDisabled;
4190
4691
  constructor(pi, dependencies = {}) {
4191
4692
  this.pi = pi;
4192
4693
  this.advisorSessionState = dependencies.sessionState ?? advisorSessionState;
@@ -4197,6 +4698,21 @@ class CommandRuntime {
4197
4698
  flowEnabled() {
4198
4699
  return this.pi.getActiveTools().includes("ask_advisor");
4199
4700
  }
4701
+ resetSameModelNotice() {
4702
+ this.lastSameModelDisabled = undefined;
4703
+ }
4704
+ updateSameModelNotice(ctx, model = ctx.model) {
4705
+ if (!this.flowEnabled()) {
4706
+ return;
4707
+ }
4708
+ const disabled = sameModelAdvisorDisabled(ctx, model);
4709
+ if (disabled && this.lastSameModelDisabled !== true) {
4710
+ notify(ctx, sameModelAdvisorNotice, "info");
4711
+ } else if (!disabled && this.lastSameModelDisabled === true) {
4712
+ notify(ctx, "Advisor re-enabled: executor and advisor models differ.", "info");
4713
+ }
4714
+ this.lastSameModelDisabled = disabled;
4715
+ }
4200
4716
  nextManualProgressId() {
4201
4717
  this.manualProgressSequence += 1;
4202
4718
  return `manual-${this.manualProgressSequence}`;
@@ -4210,9 +4726,7 @@ class CommandRuntime {
4210
4726
  }
4211
4727
  }
4212
4728
  updateAdvisorUsageStatus(ctx) {
4213
- if (ctx.hasUI) {
4214
- ctx.ui.setStatus("advisor-usage", getAdvisorSettings().showUsageFooter ? this.advisorSessionState.usageStatus() : undefined);
4215
- }
4729
+ uiAction(ctx, (ui) => ui.setStatus("advisor-usage", getAdvisorSettings().showUsageFooter ? this.advisorSessionState.usageStatus() : undefined));
4216
4730
  }
4217
4731
  }
4218
4732
  var createCommandRuntime = (pi, dependencies = {}) => new CommandRuntime(pi, dependencies);
@@ -4338,12 +4852,14 @@ var activateAdvisor = async (runtime, args, ctx, announce = true) => {
4338
4852
  "record_advisor_outcome"
4339
4853
  ]);
4340
4854
  }
4855
+ const activeModel = isMarkedSubagent() ? ctx.model : findConfiguredModel(ctx, executorRef);
4856
+ runtime.updateSameModelNotice(ctx, activeModel);
4341
4857
  if (announce) {
4342
4858
  const activeExecutorRef = effectiveExecutorRef(ctx);
4343
4859
  const activeExecutorEffort = effectiveExecutorEffort(ctx);
4344
4860
  notify(ctx, `${ADVISOR_ACTIVATION_EXPLANATION}
4345
4861
 
4346
- Advisor flow ready — Executor: ${activeExecutorRef} (thinking: ${activeExecutorEffort || "default"}) · Advisor: ${advisorRef} (thinking: ${advisorEffortRef || "default"})`, "info");
4862
+ Advisor flow ${sameModelAdvisorDisabled(ctx, activeModel) ? "configured" : "ready"} — Executor: ${activeExecutorRef} (thinking: ${activeExecutorEffort || "default"}) · Advisor: ${advisorRef} (thinking: ${advisorEffortRef || "default"})`, "info");
4347
4863
  }
4348
4864
  };
4349
4865
 
@@ -4351,10 +4867,13 @@ Advisor flow ready — Executor: ${activeExecutorRef} (thinking: ${activeExecuto
4351
4867
  var registerCommandLifecycle = (runtime, activateAdvisor) => {
4352
4868
  runtime.pi.on("session_start", async (_event, ctx) => {
4353
4869
  runtime.pendingExecutorModelRef = undefined;
4870
+ runtime.resetSameModelNotice();
4354
4871
  try {
4355
4872
  loadConfig(ctx);
4356
4873
  if (alwaysOnRef) {
4357
4874
  await activateAdvisor("", ctx, false);
4875
+ } else {
4876
+ runtime.updateSameModelNotice(ctx);
4358
4877
  }
4359
4878
  } catch (error) {
4360
4879
  const message = error instanceof Error ? error.message : String(error);
@@ -4362,6 +4881,9 @@ var registerCommandLifecycle = (runtime, activateAdvisor) => {
4362
4881
  }
4363
4882
  });
4364
4883
  runtime.pi.on("model_select", (event, ctx) => {
4884
+ if (!runtime.suppressModelSelectionSync) {
4885
+ runtime.updateSameModelNotice(ctx, event.model);
4886
+ }
4365
4887
  if (event.source !== "set" || runtime.suppressModelSelectionSync || isMarkedSubagent()) {
4366
4888
  return;
4367
4889
  }
@@ -5345,9 +5867,10 @@ var screeningSkipText = (outcome) => outcome.kind === "repeat" && outcome.reatta
5345
5867
 
5346
5868
  // src/commands/manual-consultation.ts
5347
5869
  var startManualConsultation = async (runtime, ctx, question, controller, scoutStatusToken, progress, gitContext) => {
5870
+ const isPrintMode = ctx.mode === "print";
5348
5871
  progress.phase = "preparing";
5349
5872
  runtime.requestManualRender(ctx);
5350
- if (ctx.hasUI) {
5873
+ if (uiAvailable(ctx)) {
5351
5874
  const timer = setInterval(() => {
5352
5875
  if (controller.signal.aborted || runtime.manualConsultations.get(controller) !== scoutStatusToken) {
5353
5876
  clearInterval(timer);
@@ -5412,7 +5935,7 @@ ${markdown}`,
5412
5935
  display: true
5413
5936
  }, {
5414
5937
  deliverAs: "steer",
5415
- triggerTurn: true
5938
+ triggerTurn: !isPrintMode
5416
5939
  });
5417
5940
  } catch (error) {
5418
5941
  if (controller.signal.aborted) {
@@ -5437,7 +5960,7 @@ ${markdown}`,
5437
5960
  text: `**Advisor consultation failed:** ${message}`
5438
5961
  },
5439
5962
  display: true
5440
- }, { deliverAs: "steer", triggerTurn: true });
5963
+ }, { deliverAs: "steer", triggerTurn: !isPrintMode });
5441
5964
  notify(ctx, `Advisor consultation failed: ${message}`, "error");
5442
5965
  notifyHerdrAdvisorFailure("Advisor consultation failed", message);
5443
5966
  } finally {
@@ -5464,6 +5987,10 @@ var registerManualCommand = (runtime) => {
5464
5987
  if (!loadCommandConfig(ctx)) {
5465
5988
  return;
5466
5989
  }
5990
+ if (sameModelAdvisorDisabled(ctx)) {
5991
+ notify(ctx, sameModelAdvisorNotice, "info");
5992
+ return;
5993
+ }
5467
5994
  const accessReason = advisorModelAccessReason(ctx);
5468
5995
  if (accessReason) {
5469
5996
  notify(ctx, accessReason, "warning");
@@ -5507,6 +6034,10 @@ var registerManualCommand = (runtime) => {
5507
6034
  } else {
5508
6035
  question = resolveAdvisorRequest(args);
5509
6036
  }
6037
+ if (sameModelAdvisorDisabled(ctx)) {
6038
+ notify(ctx, sameModelAdvisorNotice, "info");
6039
+ return;
6040
+ }
5510
6041
  if (!isSimpleMode()) {
5511
6042
  runtime.advisorSessionState.consumeCall();
5512
6043
  }
@@ -5523,7 +6054,15 @@ var registerManualCommand = (runtime) => {
5523
6054
  runtime.scoutStatus.register(scoutStatusToken);
5524
6055
  runtime.manualConsultations.set(controller, scoutStatusToken);
5525
6056
  runtime.pi.appendEntry?.("advisor-manual-call", { progressId, question });
5526
- startManualConsultation(runtime, ctx, question, controller, scoutStatusToken, progress, gitContext);
6057
+ if (ctx.mode === "print") {
6058
+ await startManualConsultation(runtime, ctx, question, controller, scoutStatusToken, progress, gitContext);
6059
+ return;
6060
+ }
6061
+ (async () => {
6062
+ try {
6063
+ await startManualConsultation(runtime, ctx, question, controller, scoutStatusToken, progress, gitContext);
6064
+ } catch {}
6065
+ })();
5527
6066
  }
5528
6067
  });
5529
6068
  };
@@ -5561,6 +6100,7 @@ var registerModelCommands = (runtime) => {
5561
6100
  persistExecutor: true
5562
6101
  });
5563
6102
  runtime.pendingExecutorModelRef = undefined;
6103
+ runtime.updateSameModelNotice(ctx);
5564
6104
  ctx.ui.notify(`Saved Executor + Advisor configurations to ${path}`, "info");
5565
6105
  }
5566
6106
  });
@@ -6320,7 +6860,7 @@ var createSettingsItems = ({
6320
6860
  }
6321
6861
  ];
6322
6862
  if (settings.simpleMode) {
6323
- items.push(modelWhitelist);
6863
+ 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
6864
  return items;
6325
6865
  }
6326
6866
  items.push({
@@ -6329,7 +6869,7 @@ var createSettingsItems = ({
6329
6869
  id: "effort",
6330
6870
  label: "Advisor reasoning",
6331
6871
  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), {
6872
+ }, 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
6873
  currentValue: settings.customRule || "None",
6334
6874
  description: "Add a rule that triggers Advisor involvement.",
6335
6875
  id: "customRule",
@@ -6507,6 +7047,7 @@ var BOOLEAN_SETTING_FIELDS = [
6507
7047
  "blockOnBlocked",
6508
7048
  "collapseResponses",
6509
7049
  "completionGate",
7050
+ "disableSameModel",
6510
7051
  "failureGate",
6511
7052
  "herdrIntegration",
6512
7053
  "outcomeLogging",
@@ -6779,6 +7320,7 @@ var applySessionSettings = (settings) => {
6779
7320
  setAdvisorPlanGateRef(settings.planGate);
6780
7321
  setAdvisorFailureGateRef(settings.failureGate);
6781
7322
  setAdvisorCompletionGateRef(settings.completionGate);
7323
+ setAdvisorDisableSameModelRef(settings.disableSameModel ?? true);
6782
7324
  setAdvisorCollapseResponsesRef(settings.collapseResponses);
6783
7325
  setAdvisorCustomInvocationRef(settings.customRule);
6784
7326
  setAdvisorBlockOnBlockedRef(settings.blockOnBlocked ?? true);
@@ -6853,6 +7395,7 @@ var registerSettingsCommands = (runtime) => {
6853
7395
  onChange: (settings) => {
6854
7396
  try {
6855
7397
  saveAdvisorSettings(ctx, settings);
7398
+ runtime.updateSameModelNotice(ctx);
6856
7399
  runtime.updateAdvisorUsageStatus(ctx);
6857
7400
  } catch (error) {
6858
7401
  const message = error instanceof Error ? error.message : String(error);
@@ -6869,6 +7412,7 @@ var registerSettingsCommands = (runtime) => {
6869
7412
  description: "Disable on-demand Advisor calls; keep the current model",
6870
7413
  handler: (_args, ctx) => {
6871
7414
  runtime.pi.setActiveTools(runtime.pi.getActiveTools().filter((name) => name !== "ask_advisor" && name !== "record_advisor_outcome"));
7415
+ runtime.resetSameModelNotice();
6872
7416
  const wasAlwaysOn = alwaysOnRef;
6873
7417
  if (wasAlwaysOn) {
6874
7418
  const persisted = getPersistedModelRefs();
@@ -6902,7 +7446,7 @@ import {
6902
7446
  chmod,
6903
7447
  link,
6904
7448
  mkdir,
6905
- open as open3,
7449
+ open as open4,
6906
7450
  readFile,
6907
7451
  stat,
6908
7452
  unlink,
@@ -6964,7 +7508,7 @@ var withOutcomeLock = async (run) => {
6964
7508
  const lockPath = `${outcomeLogPath()}.lock`;
6965
7509
  for (let attempt = 0;attempt < 200; attempt += 1) {
6966
7510
  try {
6967
- const lock = await open3(lockPath, "wx", 384);
7511
+ const lock = await open4(lockPath, "wx", 384);
6968
7512
  const identity = await lock.stat();
6969
7513
  try {
6970
7514
  return await run();
@@ -7147,7 +7691,7 @@ var handleJevTurnEnd = async (registration, ctx) => {
7147
7691
  });
7148
7692
  session.recordJevGateCheck(result.usage);
7149
7693
  const shouldConsult = composeTurnGateVerdict(result.answers, advisorJevTurnGateNoulThresholdRef);
7150
- if (!consumeTurnGateBudget(session, shouldConsult)) {
7694
+ if (!advisorModelIsAllowed(ctx) || !consumeTurnGateBudget(session, shouldConsult)) {
7151
7695
  return;
7152
7696
  }
7153
7697
  const herdrActivity = registration.herdrActivity ?? herdrAdvisorActivity;
@@ -7214,17 +7758,30 @@ var syncRenderPhase = (context, phase) => {
7214
7758
  }
7215
7759
  context.state.phase = phase;
7216
7760
  };
7761
+ var formatByteSize = (bytes) => {
7762
+ if (bytes >= 1024 ** 3) {
7763
+ return `${(bytes / 1024 ** 3).toFixed(1)} GiB`;
7764
+ }
7765
+ if (bytes >= 1024 ** 2) {
7766
+ return `${(bytes / 1024 ** 2).toFixed(1)} MiB`;
7767
+ }
7768
+ if (bytes >= 1024) {
7769
+ return `${Math.round(bytes / 1024)} KiB`;
7770
+ }
7771
+ return `${bytes} B`;
7772
+ };
7217
7773
  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
7774
+ details?.draftBytes ? `Draft attached · ${formatByteSize(details.draftBytes)}` : undefined,
7775
+ details?.preferenceBytes ? `Project preferences attached · ${formatByteSize(details.preferenceBytes)}` : undefined,
7776
+ details?.trackedBytes ? `Tracked files attached · ${formatByteSize(details.trackedBytes)}` : undefined,
7777
+ details?.untrackedBytes ? `Untracked files attached · ${formatByteSize(details.untrackedBytes)}` : undefined,
7778
+ details?.imageCount ? `${details.imageCount} image${details.imageCount === 1 ? "" : "s"} attached${details.imageBytes ? ` · ${formatByteSize(details.imageBytes)}` : ""}${details.imageOmissions ? ` · ${details.imageOmissions} withheld` : ""}` : undefined
7222
7779
  ].filter((label) => label !== undefined);
7223
- var renderJevSkipBox = (box, result, expanded, theme) => {
7780
+ var renderSkipBox = (box, result, expanded, theme) => {
7224
7781
  const details = advisorResultDetails(result);
7225
7782
  const lines = [
7226
7783
  theme.fg("dim", theme.bold("◆ ADVISOR · SKIPPED")),
7227
- theme.fg("dim", ` ${details?.jev?.reason ?? ""}`)
7784
+ theme.fg("dim", ` ${details?.skipReason ?? details?.jev?.reason ?? ""}`)
7228
7785
  ];
7229
7786
  box.addChild(new Text5(lines.join(`
7230
7787
  `), 0, 0));
@@ -7239,7 +7796,14 @@ var renderPartialAdvisorResult = (box, result, expanded, theme, context) => {
7239
7796
  const scoutActive = scout?.status === "calling" || scout?.status === "streaming";
7240
7797
  syncRenderPhase(context, scoutActive ? "scout" : "advisor");
7241
7798
  if (!context.state.timerId) {
7242
- context.state.timerId = setInterval(() => context.invalidate(), 80);
7799
+ context.state.timerId = setInterval(() => {
7800
+ try {
7801
+ context.invalidate();
7802
+ } catch {
7803
+ clearInterval(context.state.timerId);
7804
+ context.state.timerId = undefined;
7805
+ }
7806
+ }, 80);
7243
7807
  }
7244
7808
  if (scout) {
7245
7809
  renderScoutDetails(box, scout, expanded, theme);
@@ -7286,8 +7850,8 @@ var renderFinalAdvisorResult = (box, result, expanded, theme, context) => {
7286
7850
  context.state.timerId = undefined;
7287
7851
  }
7288
7852
  const details = advisorResultDetails(result);
7289
- if (details?.jev?.skipped) {
7290
- renderJevSkipBox(box, result, expanded, theme);
7853
+ if (details?.jev?.skipped || details?.skipReason) {
7854
+ renderSkipBox(box, result, expanded, theme);
7291
7855
  return;
7292
7856
  }
7293
7857
  if (details?.scout) {
@@ -7353,7 +7917,22 @@ var registerAskAdvisorTool = ({
7353
7917
  pi.registerTool({
7354
7918
  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
7919
  async execute(_id, params, signal, onUpdate, ctx) {
7920
+ const skipSameModel = () => {
7921
+ session.releaseCall(_id);
7922
+ reservedCalls.delete(_id);
7923
+ return {
7924
+ content: [{ text: sameModelAdvisorNotice, type: "text" }],
7925
+ details: {
7926
+ skipReason: sameModelAdvisorNotice,
7927
+ text: sameModelAdvisorNotice
7928
+ }
7929
+ };
7930
+ };
7356
7931
  try {
7932
+ loadConfig(ctx);
7933
+ if (sameModelAdvisorDisabled(ctx)) {
7934
+ return skipSameModel();
7935
+ }
7357
7936
  assertAdvisorModelAccess(ctx);
7358
7937
  } catch (error) {
7359
7938
  session.releaseCall(_id);
@@ -7398,6 +7977,9 @@ var registerAskAdvisorTool = ({
7398
7977
  }
7399
7978
  };
7400
7979
  }
7980
+ if (sameModelAdvisorDisabled(ctx)) {
7981
+ return skipSameModel();
7982
+ }
7401
7983
  if (!simpleMode && !session.canConsult(getAdvisorMaxCallsPerSession(), _id)) {
7402
7984
  throw new Error("Advisor call budget exhausted for this session.");
7403
7985
  }
@@ -7460,6 +8042,10 @@ var registerAskAdvisorTool = ({
7460
8042
  adviceId: result.adviceId,
7461
8043
  advisor: result.model,
7462
8044
  draftBytes: result.draftBytes,
8045
+ imageBytes: result.imageBytes,
8046
+ imageCount: result.imageCount,
8047
+ imageOmissions: result.imageOmissions,
8048
+ imagePartsSeen: result.imagePartsSeen,
7463
8049
  preferenceBytes: result.preferenceBytes,
7464
8050
  question: resolveAdvisorRequest(params.question),
7465
8051
  scout: scoutDetails,
@@ -7713,7 +8299,7 @@ var registerToolLifecycle = ({
7713
8299
  return;
7714
8300
  }
7715
8301
  loadConfig(ctx);
7716
- if (!advisorModelAccess(ctx).allowed) {
8302
+ if (!advisorModelAccess(ctx).allowed || sameModelAdvisorDisabled(ctx)) {
7717
8303
  return;
7718
8304
  }
7719
8305
  const guidelines = advisorInvocationGuidelines();
@@ -7743,6 +8329,9 @@ ${guidelines.map((rule) => `- ${rule}`).join(`
7743
8329
  if (!loadConfigOrSkipGating(ctx)) {
7744
8330
  return;
7745
8331
  }
8332
+ if (sameModelAdvisorDisabled(ctx)) {
8333
+ return;
8334
+ }
7746
8335
  const accessBlock = modelAccessBlock(event.toolName, ctx);
7747
8336
  if (accessBlock) {
7748
8337
  return accessBlock;