@quantiya/codevibe-core 2.0.50 → 2.0.51

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.
@@ -195,3 +195,10 @@ export declare function attachmentsToBase64(attachments: ImageAttachment[], opts
195
195
  images: string[];
196
196
  failed: ImageAttachment[];
197
197
  };
198
+ /**
199
+ * Detects whether a clarifying question emitted on an image attachment turn is a blind deflection
200
+ * caused by a text-only classifier's inability to see/inspect images (e.g. asking the user to
201
+ * describe the screenshot or what to do with it, or generic rephrase requests), rather than a genuine
202
+ * domain clarification (e.g. "which specific region?").
203
+ */
204
+ export declare function isBlindImageClarification(prompt: string, clarifyingQuestion?: string): boolean;
@@ -5756,6 +5756,23 @@ var AppSyncClient = class _AppSyncClient {
5756
5756
  throw new Error("generatePlannerAdvisory: prompt ciphertext is required");
5757
5757
  if (input.prompt.length > 2e5)
5758
5758
  throw new Error(`generatePlannerAdvisory: prompt ciphertext exceeds 200,000 chars ceiling (${input.prompt.length})`);
5759
+ if (input.images !== void 0) {
5760
+ if (!Array.isArray(input.images))
5761
+ throw new Error("generatePlannerAdvisory: images must be an array of string ciphertexts");
5762
+ if (input.images.length > 5)
5763
+ throw new Error(`generatePlannerAdvisory: images count exceeds maximum of 5 (got ${input.images.length})`);
5764
+ let totalCiphertextLen = input.prompt.length;
5765
+ for (let i = 0; i < input.images.length; i++) {
5766
+ let imgCipher = input.images[i];
5767
+ if (typeof imgCipher != "string" || imgCipher.trim().length === 0)
5768
+ throw new Error(`generatePlannerAdvisory: image at index ${i} must be a non-empty string ciphertext`);
5769
+ if (imgCipher.length > 27e5)
5770
+ throw new Error(`generatePlannerAdvisory: image at index ${i} exceeds ciphertext limit of 2,700,000 chars (got ${imgCipher.length})`);
5771
+ totalCiphertextLen += imgCipher.length;
5772
+ }
5773
+ if (totalCiphertextLen > 45e5)
5774
+ throw new Error(`generatePlannerAdvisory: total request ciphertext exceeds 4,500,000 chars ceiling (${totalCiphertextLen})`);
5775
+ }
5759
5776
  if (input.maxTokens !== void 0 && (input.maxTokens < 1 || input.maxTokens > 1536))
5760
5777
  throw new Error(`generatePlannerAdvisory: maxTokens must be between 1 and 1536 (got ${input.maxTokens})`);
5761
5778
  if (input.temperature !== void 0 && (input.temperature < 0 || input.temperature > 1))
@@ -17335,6 +17352,14 @@ function attachmentsToBase64(attachments, opts) {
17335
17352
  }
17336
17353
  return { images, failed };
17337
17354
  }
17355
+ function isBlindImageClarification(prompt, clarifyingQuestion) {
17356
+ if (clarifyingQuestion && clarifyingQuestion.trim().length > 0) {
17357
+ let q = clarifyingQuestion.toLowerCase();
17358
+ return /what .*like me to do with the (screenshot|image)/i.test(q) || /what .*want me to do with the (screenshot|image)/i.test(q) || /how should i use (that|this) information/i.test(q) || /information from the (screenshot|image)/i.test(q) || /rephrase that as a specific command or request/i.test(q) || /cannot (see|view|read|inspect) (the )?(image|screenshot)/i.test(q) || /describe what you('d| would) like me to do with (the )?(screenshot|image)/i.test(q);
17359
+ }
17360
+ let p = prompt.toLowerCase();
17361
+ return /what('s| is| do you see) (in |on )?(the |this )?(screenshot|image|picture)/i.test(p) || /what does (the |this )?(screenshot|image|picture) (show|contain|depict)/i.test(p) || /what (shape|color|text) is (in |on )?(this |the )?(image|screenshot)/i.test(p);
17362
+ }
17338
17363
 
17339
17364
  // src/orchestration-shell/slash-suggest.ts
17340
17365
  function isSlashCommandInProgress(value) {
@@ -20992,6 +21017,310 @@ function assembleParagraphs(paragraphs, indices, maxChars) {
20992
21017
  `);
20993
21018
  return result.length > maxChars ? result.slice(0, maxChars) : result;
20994
21019
  }
21020
+ var GENERIC_RELEVANCE_STOPWORDS = /* @__PURE__ */ new Set([
21021
+ // Basic query, auxiliary, and question words
21022
+ "what",
21023
+ "when",
21024
+ "where",
21025
+ "which",
21026
+ "who",
21027
+ "whom",
21028
+ "whose",
21029
+ "why",
21030
+ "how",
21031
+ "this",
21032
+ "that",
21033
+ "these",
21034
+ "those",
21035
+ "there",
21036
+ "their",
21037
+ "theirs",
21038
+ "here",
21039
+ "about",
21040
+ "above",
21041
+ "below",
21042
+ "from",
21043
+ "with",
21044
+ "within",
21045
+ "without",
21046
+ "under",
21047
+ "over",
21048
+ "have",
21049
+ "having",
21050
+ "has",
21051
+ "had",
21052
+ "been",
21053
+ "would",
21054
+ "could",
21055
+ "should",
21056
+ "might",
21057
+ "must",
21058
+ "more",
21059
+ "most",
21060
+ "some",
21061
+ "many",
21062
+ "much",
21063
+ "also",
21064
+ "just",
21065
+ "only",
21066
+ "other",
21067
+ "such",
21068
+ "does",
21069
+ "did",
21070
+ "doing",
21071
+ "done",
21072
+ "will",
21073
+ "shall",
21074
+ "can",
21075
+ "cant",
21076
+ "cannot",
21077
+ "into",
21078
+ "onto",
21079
+ "upon",
21080
+ "than",
21081
+ "then",
21082
+ "them",
21083
+ "they",
21084
+ "your",
21085
+ "yours",
21086
+ // Conversational verbs and common nouns
21087
+ "explain",
21088
+ "explanation",
21089
+ "algorithm",
21090
+ "algorithms",
21091
+ "code",
21092
+ "coding",
21093
+ "function",
21094
+ "functions",
21095
+ "write",
21096
+ "writing",
21097
+ "create",
21098
+ "build",
21099
+ "make",
21100
+ "implement",
21101
+ "implementation",
21102
+ "test",
21103
+ "tests",
21104
+ "testing",
21105
+ "file",
21106
+ "files",
21107
+ "error",
21108
+ "errors",
21109
+ "run",
21110
+ "running",
21111
+ "use",
21112
+ "using",
21113
+ "work",
21114
+ "works",
21115
+ "working",
21116
+ "difference",
21117
+ "between",
21118
+ "better",
21119
+ "best",
21120
+ "example",
21121
+ "examples",
21122
+ "help",
21123
+ "please",
21124
+ "know",
21125
+ "want",
21126
+ "need",
21127
+ "like",
21128
+ "good",
21129
+ "bad",
21130
+ "worse",
21131
+ "worst",
21132
+ "mean",
21133
+ "means",
21134
+ "problem",
21135
+ "problems",
21136
+ "issue",
21137
+ "issues",
21138
+ "question",
21139
+ "questions",
21140
+ "answer",
21141
+ "answers",
21142
+ "search",
21143
+ "searching",
21144
+ "read",
21145
+ "reading",
21146
+ "view",
21147
+ "look",
21148
+ "looks",
21149
+ "looking",
21150
+ "show",
21151
+ "find",
21152
+ "get",
21153
+ "set",
21154
+ "data",
21155
+ "type",
21156
+ "types",
21157
+ "system",
21158
+ "method",
21159
+ "methods",
21160
+ "class",
21161
+ "classes",
21162
+ "object",
21163
+ "objects",
21164
+ "program",
21165
+ "programs",
21166
+ "language",
21167
+ "languages",
21168
+ "value",
21169
+ "values",
21170
+ "script",
21171
+ "prompt",
21172
+ "model",
21173
+ "query",
21174
+ "input",
21175
+ "output",
21176
+ "state",
21177
+ "process",
21178
+ "node",
21179
+ "call",
21180
+ // Conversational follow-ups, speaking, evaluation & opinions
21181
+ "say",
21182
+ "says",
21183
+ "said",
21184
+ "saying",
21185
+ "tell",
21186
+ "tells",
21187
+ "told",
21188
+ "sound",
21189
+ "sounds",
21190
+ "sounding",
21191
+ "think",
21192
+ "thinks",
21193
+ "thinking",
21194
+ "thought",
21195
+ "thoughts",
21196
+ "opinion",
21197
+ "opinions",
21198
+ "take",
21199
+ "takes",
21200
+ "critique",
21201
+ "critiques",
21202
+ "perspective",
21203
+ "perspectives",
21204
+ "viewpoint",
21205
+ "viewpoints",
21206
+ "summary",
21207
+ "summarize",
21208
+ "summarizes",
21209
+ "summarizing",
21210
+ "takeaway",
21211
+ "takeaways",
21212
+ "key",
21213
+ "keys",
21214
+ "pros",
21215
+ "cons",
21216
+ "pro",
21217
+ "con",
21218
+ "detail",
21219
+ "details",
21220
+ "information",
21221
+ "info",
21222
+ // Shopping, consumer evaluation, and general follow-up adjectives
21223
+ "worth",
21224
+ "buy",
21225
+ "buys",
21226
+ "buying",
21227
+ "bought",
21228
+ "purchase",
21229
+ "purchasing",
21230
+ "purchases",
21231
+ "cost",
21232
+ "costs",
21233
+ "costing",
21234
+ "price",
21235
+ "prices",
21236
+ "pricing",
21237
+ "expensive",
21238
+ "cheap",
21239
+ "recommend",
21240
+ "recommends",
21241
+ "recommended",
21242
+ "recommendation",
21243
+ "recommendations",
21244
+ "scratches",
21245
+ "scratch",
21246
+ "scratching",
21247
+ "easily",
21248
+ "easy",
21249
+ "hard",
21250
+ "completely",
21251
+ "thing",
21252
+ "things",
21253
+ "item",
21254
+ "items",
21255
+ "stuff",
21256
+ "possible",
21257
+ "possibility",
21258
+ "reverse",
21259
+ "reversing",
21260
+ "place",
21261
+ "places",
21262
+ // Page / Document references and meta-terms
21263
+ "page",
21264
+ "pages",
21265
+ "article",
21266
+ "articles",
21267
+ "site",
21268
+ "sites",
21269
+ "website",
21270
+ "websites",
21271
+ "url",
21272
+ "urls",
21273
+ "webpage",
21274
+ "webpages",
21275
+ "document",
21276
+ "documents",
21277
+ "post",
21278
+ "posts",
21279
+ "blog",
21280
+ "blogs",
21281
+ "review",
21282
+ "reviews",
21283
+ "author",
21284
+ "writer",
21285
+ "writers",
21286
+ "wrote",
21287
+ "source",
21288
+ "sources",
21289
+ "results",
21290
+ "result",
21291
+ "findings",
21292
+ "finding"
21293
+ ]);
21294
+ function isPromptRelevantToRetainedPage(prompt, retainedPage) {
21295
+ if (!retainedPage || !retainedPage.text || retainedPage.text.trim().length === 0)
21296
+ return !1;
21297
+ let cleanPrompt = (prompt || "").trim();
21298
+ if (!cleanPrompt) return !1;
21299
+ let pageTitle = (retainedPage.title || "").toLowerCase(), pageUrl = (retainedPage.url || "").toLowerCase(), DEICTIC_PHRASE_REGEX = /\b(this|that|the|current)\s+(?:web\s+)?(page|article|site|website|url|link|post|webpage|document|blog|review)\b/i, PREPOSITIONAL_PAGE_REGEX = /\b(from|on|in)\s+(the|this|that)\s+(?:web\s+)?(page|article|site|website|post|document|link|webpage)\b/i, AUTHOR_REF_REGEX = /\b(author|who wrote|who is the author)\b/i, CJK_DEICTIC_REGEX = /(这篇文章|该文章|这个网页|该网页|这个页面|该页面|这个网站|该网站|原文|作者|出处)/, CJK_EVALUATIVE_FOLLOWUP_REGEX = /(总结|概括|看法|观点|优缺点|评价|详细说说|你怎么看)/;
21300
+ if (DEICTIC_PHRASE_REGEX.test(cleanPrompt) || PREPOSITIONAL_PAGE_REGEX.test(cleanPrompt) || AUTHOR_REF_REGEX.test(cleanPrompt) || CJK_DEICTIC_REGEX.test(cleanPrompt) || CJK_EVALUATIVE_FOLLOWUP_REGEX.test(cleanPrompt))
21301
+ return !0;
21302
+ function tokenMatchesWordStart(token, text2) {
21303
+ if (!text2) return !1;
21304
+ if (/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/.test(token))
21305
+ return text2.includes(token);
21306
+ let escaped = escapeRegex2(token);
21307
+ return /^[a-zA-Z0-9_]+$/.test(token) ? new RegExp(`\\b${escaped}`, "i").test(text2) : new RegExp(`(?<=^|[\\s\\p{P}])${escaped}`, "gui").test(text2);
21308
+ }
21309
+ let distinctiveTokens = extractQueryTokens(cleanPrompt).filter(
21310
+ (t) => t.length >= 3 && !GENERIC_RELEVANCE_STOPWORDS.has(t.toLowerCase())
21311
+ ), matchingTokens = distinctiveTokens.filter(
21312
+ (token) => tokenMatchesWordStart(token, pageTitle) || tokenMatchesWordStart(token, pageUrl) || tokenMatchesWordStart(token, retainedPage.text || "")
21313
+ );
21314
+ if (distinctiveTokens.length > 0 && matchingTokens.length === 0)
21315
+ return !1;
21316
+ let SUMMARY_FOLLOWUP_REGEX = /\b(summarize|summary|takeaways?|tl;?dr)\b|\b(pros\s+and\s+cons|pros|cons)(\s*\?|\s*$|\s+(of|about)\s+(it|this|that|them|the\s+(page|article|site|website|post|document|device|product|phone|display)))/i, OPINION_FOLLOWUP_REGEX = /\b(your\s+(opinion|thoughts?|take|critique|perspective))\b/i, THINK_FOLLOWUP_REGEX = /\bwhat do you think(\?|$|\s+(about|of)\s+(it|this|that|them|the\s+(page|article|site|website|post|document|device|product|phone|display)))/i, PRONOUN_FOLLOWUP_REGEX = /\b(is|does|can|will|should|would)\s+it\b/i, ABOUT_IT_REGEX = /\b(about|of)\s+(it|this|them)\b/i, TELL_ME_MORE_REGEX = /\b(tell me more|more details)\b/i;
21317
+ if ((SUMMARY_FOLLOWUP_REGEX.test(cleanPrompt) || OPINION_FOLLOWUP_REGEX.test(cleanPrompt) || THINK_FOLLOWUP_REGEX.test(cleanPrompt) || PRONOUN_FOLLOWUP_REGEX.test(cleanPrompt) || ABOUT_IT_REGEX.test(cleanPrompt) || TELL_ME_MORE_REGEX.test(cleanPrompt)) && (distinctiveTokens.length === 0 || matchingTokens.length >= 2 || matchingTokens.some((t) => t.length >= 6) || matchingTokens.length >= 1 && matchingTokens.length / distinctiveTokens.length >= 0.5))
21318
+ return !0;
21319
+ for (let token of distinctiveTokens)
21320
+ if (pageTitle.length > 0 && tokenMatchesWordStart(token, pageTitle) || pageUrl.length > 0 && tokenMatchesWordStart(token, pageUrl))
21321
+ return !0;
21322
+ return distinctiveTokens.length === 0 ? !1 : matchingTokens.length >= 2 || matchingTokens.some((t) => t.length >= 6);
21323
+ }
20995
21324
 
20996
21325
  // src/orchestration-shell/web/fetch.ts
20997
21326
  var http2 = __toESM(require("node:http")), https3 = __toESM(require("node:https")), dns2 = __toESM(require("node:dns")), zlib = __toESM(require("node:zlib")), import_node_string_decoder = require("node:string_decoder");
@@ -71374,7 +71703,7 @@ async function routeReadOnlyAgentMention(args) {
71374
71703
  localOnly: !0
71375
71704
  });
71376
71705
  }
71377
- let retainedPage = args.retainedPage !== void 0 ? args.retainedPage : getFreshRetainedBrowsePage(retainedBrowsePages, sessionId), hasRetainedPage = !!(retainedPage && retainedPage.text.trim().length > 0), preparedBrief = composeBrainstormPanelBrief({
71706
+ let rawRetainedPage = args.retainedPage !== void 0 ? args.retainedPage : getFreshRetainedBrowsePage(retainedBrowsePages, sessionId), retainedPage = rawRetainedPage && (args.retainedPage !== void 0 || isPromptRelevantToRetainedPage(requestBrief, rawRetainedPage)) ? rawRetainedPage : null, hasRetainedPage = !!(retainedPage && retainedPage.text.trim().length > 0), preparedBrief = composeBrainstormPanelBrief({
71378
71707
  priorContext,
71379
71708
  requestBrief,
71380
71709
  retainedPage
@@ -72340,14 +72669,6 @@ async function routeBrainstorm(deps) {
72340
72669
  return;
72341
72670
  }
72342
72671
  let isHostedBrainstorm = args.localAdvisoryRunner.providerKind === "hosted";
72343
- if (images && images.length > 0 && isHostedBrainstorm) {
72344
- store.dispatch({
72345
- type: "SHELL_ADVISORY",
72346
- source: "shell",
72347
- text: "The local CodeVibe model is required to analyze attached images during brainstorming. Enable it with `codevibe model install` from a Pro/Max account, then ask again. No hosted model was called and no code was changed."
72348
- });
72349
- return;
72350
- }
72351
72672
  store.dispatch({
72352
72673
  type: "SHELL_ADVISORY",
72353
72674
  source: "shell",
@@ -72428,18 +72749,10 @@ async function routeAdvisory(deps) {
72428
72749
  return;
72429
72750
  }
72430
72751
  let isHostedAdvisory = args.localAdvisoryRunner.providerKind === "hosted";
72431
- if (hasImages && isHostedAdvisory) {
72432
- store.dispatch({
72433
- type: "SHELL_ADVISORY",
72434
- source: "shell",
72435
- text: "The local CodeVibe model is required to analyze an attached image. Enable it with `codevibe model install` from a Pro/Max account, then ask again. No hosted model was called and no code was changed."
72436
- });
72437
- return;
72438
- }
72439
72752
  store.dispatch({
72440
72753
  type: "SHELL_ADVISORY",
72441
72754
  source: "shell",
72442
- text: hasImages ? "Analyzing the attached image(s) with local Gemma\u2026" : isHostedAdvisory ? "Answering with hosted Gemma\u2026" : "Answering with local Gemma\u2026"
72755
+ text: hasImages ? isHostedAdvisory ? "Analyzing the attached image(s) with hosted Gemma\u2026" : "Analyzing the attached image(s) with local Gemma\u2026" : isHostedAdvisory ? "Answering with hosted Gemma\u2026" : "Answering with local Gemma\u2026"
72443
72756
  });
72444
72757
  let MAX_ADVISORY_CLARIFICATIONS = 4, MAX_ADVISORY_CLARIFICATION_QUESTION_CHARS = 400, MAX_ADVISORY_CLARIFICATION_ANSWER_CHARS = 1e3, MAX_ADVISORY_CANONICAL_CONTEXT_CHARS = 6e3, MAX_ADVISORY_USER_PROMPT_CHARS = 4e3, MAX_ADVISORY_PROMPT_CHARS = 42e3, MAX_ADVISORY_PAGE_CHARS = 24e3, MAX_ADVISORY_STATUS_CHARS = 1500, MAX_ADVISORY_PAGE_TOKENS = 6e3, MAX_ADVISORY_PROMPT_TOKENS = 13500, statusText = "";
72445
72758
  try {
@@ -72449,7 +72762,7 @@ async function routeAdvisory(deps) {
72449
72762
  statusText = "";
72450
72763
  }
72451
72764
  let retainedPage = deps.retainedPage, pageText = "";
72452
- if (retainedPage && retainedPage.text.trim().length > 0) {
72765
+ if (retainedPage && retainedPage.text.trim().length > 0 && isPromptRelevantToRetainedPage(userPrompt, retainedPage)) {
72453
72766
  let relevant = extractQueryRelevantSnippets(retainedPage.text, userPrompt, {
72454
72767
  maxChars: MAX_ADVISORY_PAGE_CHARS - 100
72455
72768
  });
@@ -72461,7 +72774,7 @@ async function routeAdvisory(deps) {
72461
72774
  canonicalContextText = rawContext.length > MAX_ADVISORY_CANONICAL_CONTEXT_CHARS ? `[older context omitted]
72462
72775
  ` + rawContext.slice(-MAX_ADVISORY_CANONICAL_CONTEXT_CHARS) : rawContext;
72463
72776
  }
72464
- let sanitizedUserPrompt = redactAbsoluteLocalPaths(userPrompt.trim()), boundedUserPrompt = sanitizedUserPrompt.length > MAX_ADVISORY_USER_PROMPT_CHARS ? sanitizedUserPrompt.slice(0, MAX_ADVISORY_USER_PROMPT_CHARS) + "\u2026" : sanitizedUserPrompt, advisoryClarifications = [];
72777
+ let sanitizedUserPrompt = redactAbsoluteLocalPaths(userPrompt.trim()), effectiveUserPrompt = hasImages && sanitizedUserPrompt.replace(/\[Image #\d+\]/g, "").trim().length === 0 ? "Describe what is shown in the attached image(s)." : sanitizedUserPrompt, boundedUserPrompt = effectiveUserPrompt.length > MAX_ADVISORY_USER_PROMPT_CHARS ? effectiveUserPrompt.slice(0, MAX_ADVISORY_USER_PROMPT_CHARS) + "\u2026" : effectiveUserPrompt, advisoryClarifications = [];
72465
72778
  if (deps.clarifications && deps.clarifications.length > 0) {
72466
72779
  let sanitized = deps.clarifications.map((c) => {
72467
72780
  let q = redactAbsoluteLocalPaths(c.question.trim()), a = redactAbsoluteLocalPaths(c.answer.trim());
@@ -72549,7 +72862,7 @@ async function routeAdvisory(deps) {
72549
72862
  chars: prompt.length
72550
72863
  }), pageText = "", prompt = buildPrompt());
72551
72864
  }
72552
- let advisoryRunner = args.localAdvisoryRunner, generate2 = (p) => advisoryRunner.generateAdvisory(p, {
72865
+ let advisoryRunner = args.localAdvisoryRunner, beforeProviderKind = advisoryRunner.providerKind, generate2 = (p) => advisoryRunner.generateAdvisory(p, {
72553
72866
  responseFormat: "text",
72554
72867
  numPredict: 1400,
72555
72868
  ...images.length > 0 ? { images } : {}
@@ -72564,6 +72877,12 @@ async function routeAdvisory(deps) {
72564
72877
  else
72565
72878
  throw err;
72566
72879
  }
72880
+ let afterProviderKind = advisoryRunner.providerKind;
72881
+ beforeProviderKind !== "hosted" && afterProviderKind === "hosted" && store.dispatch({
72882
+ type: "SHELL_ADVISORY",
72883
+ source: "shell",
72884
+ text: hasImages ? "Local model unavailable; falling back to hosted Gemma for image analysis. No code was changed." : "Local model unavailable; falling back to hosted Gemma. No code was changed."
72885
+ });
72567
72886
  let answer = sanitizeForTerminal(raw.trim());
72568
72887
  if (answer) {
72569
72888
  store.dispatch({
@@ -72590,11 +72909,25 @@ async function routeAdvisory(deps) {
72590
72909
  text: hasImages ? `${modelNoun} returned no answer for the attached image(s). ${trustLine}` : `${modelNoun} returned an empty answer. Try rephrasing with a bit more detail. ${trustLine}`
72591
72910
  });
72592
72911
  } catch (err) {
72593
- logger.warn("[orchestration-shell] local advisory failed", {
72912
+ let afterProviderKind = args.localAdvisoryRunner.providerKind;
72913
+ beforeProviderKind !== "hosted" && afterProviderKind === "hosted" && store.dispatch({
72914
+ type: "SHELL_ADVISORY",
72915
+ source: "shell",
72916
+ text: hasImages ? "Local model unavailable; falling back to hosted Gemma for image analysis. No code was changed." : "Local model unavailable; falling back to hosted Gemma. No code was changed."
72917
+ }), logger.warn("[orchestration-shell] local advisory failed", {
72594
72918
  error: err.message,
72595
72919
  hasImages,
72596
72920
  runtimeLabel: args.localAdvisoryRunner.runtimeLabel
72597
72921
  });
72922
+ let errMsg = err?.message ?? "";
72923
+ if (hasImages && /exceeds size limit|image limit exceeded|maximum 5 images|exceeds backend limit|exceeds ciphertext limit|total request ciphertext/i.test(errMsg)) {
72924
+ store.dispatch({
72925
+ type: "SHELL_ADVISORY",
72926
+ source: "shell",
72927
+ text: `Unable to analyze image: ${errMsg} No code was changed.`
72928
+ });
72929
+ return;
72930
+ }
72598
72931
  let fallback = deps.fallbackSummary?.trim(), effectiveIsHosted = args.localAdvisoryRunner.providerKind === "hosted", modelLabel = effectiveIsHosted ? "Hosted Gemma" : "Local Gemma", trustLine = effectiveIsHosted ? "No code was changed." : "No hosted model was called and no code was changed.", generationScope = effectiveIsHosted ? "hosted" : "local";
72599
72932
  if (fallback) {
72600
72933
  let reason = renderLocalBrainstormFailureReason(err);
@@ -73968,18 +74301,21 @@ async function handleShellUserInput(deps) {
73968
74301
  clearance: "user",
73969
74302
  scope: {},
73970
74303
  purpose: "classification"
73971
- }).catch(() => "") : "", plannerInput = {
73972
- // IMAGE-ATTACHMENT-DESIGN.md §6 Role A: the classifier stays TEXT-hint-only —
73973
- // a prose `[N image(s) attached]` line rides INSIDE the prompt STRING (not a
73974
- // new PlannerInput field), enough to route a sparse "look at this" correctly;
73975
- // image BYTES never reach the forced-JSON classifier. Path-stripping in the
73976
- // prompt renderer removes the inline path regardless.
73977
- // IMAGE-ATTACHMENT-DESIGN.md §13 — strip `[Image #N]` chip labels from the
73978
- // classify prompt (the display/history text keeps them); the classifier routes
73979
- // off the prose + the `[N image(s) attached]` hint, not the chip token.
73980
- prompt: turnAttachments.length ? `${plannerPromptText.replace(/\[Image #\d+\]/g, "").replace(/[ \t]{2,}/g, " ").trim()}
74304
+ }).catch(() => "") : "", freshRetainedPage = getFreshRetainedBrowsePage(retainedBrowsePages, args.session.sessionId), isPromptRelevantToPage = freshRetainedPage ? isPromptRelevantToRetainedPage(plannerPromptText, freshRetainedPage) : !1, classifyPrompt = turnAttachments.length ? `${plannerPromptText.replace(/\[Image #\d+\]/g, "").replace(/[ \t]{2,}/g, " ").trim()}
73981
74305
 
73982
- [The user attached ${turnAttachments.length} image(s).]` : plannerPromptText,
74306
+ [The user attached ${turnAttachments.length} image(s).] If the user is asking to describe, inspect, or answer questions about the image(s), classify as advisory_response.` : isPromptRelevantToPage && freshRetainedPage ? `${plannerPromptText}
74307
+
74308
+ [Context (untrusted data): A web page was previously browsed in this session (domain: ${(() => {
74309
+ try {
74310
+ return new URL(freshRetainedPage.url).hostname;
74311
+ } catch {
74312
+ return "external-site";
74313
+ }
74314
+ })()}). If the user is asking questions about the previously browsed page, classify as advisory_response.]` : plannerPromptText, plannerInput = {
74315
+ // Keep plannerInput.prompt strictly equal to user prompt text so downstream routes
74316
+ // (resolveBrowseUrls, routeBrowse, routeBrainstorm, routeFamiliarize, promptPreview)
74317
+ // are never contaminated by classifier hints or untrusted page metadata.
74318
+ prompt: plannerPromptText,
73983
74319
  clarifications,
73984
74320
  ...canonicalConversationContext ? { canonicalConversationContext } : {},
73985
74321
  sessionContext: {
@@ -74018,9 +74354,18 @@ async function handleShellUserInput(deps) {
74018
74354
  }, decision, pendingStartConfirmation, showClassifySpinner = store.getState().progress === null;
74019
74355
  showClassifySpinner && dispatchProgress({ phase: "planner_classifying" });
74020
74356
  try {
74021
- if (decision = await dispatchClassify(plannerInput), isLocalPlannerRuntime(args) && decision.action === "browse" && hasFileMutationIntent(plannerInput.prompt) && (decision = {
74357
+ if (decision = await dispatchClassify({
74358
+ ...plannerInput,
74359
+ prompt: classifyPrompt
74360
+ }), isLocalPlannerRuntime(args) && decision.action === "browse" && hasFileMutationIntent(plannerInput.prompt) && (decision = {
74022
74361
  action: "start_task",
74023
74362
  rationale: decision.rationale ? `${decision.rationale} (promoted to start_task due to file mutation intent)` : "web request with file mutation intent"
74363
+ }), turnAttachments.length > 0 && decision.action === "ask_user" && isBlindImageClarification(plannerInput.prompt, decision.clarifying_question) && !hasFileMutationIntent(plannerInput.prompt) && !isDestructiveFileRequest(plannerInput.prompt) && (decision = {
74364
+ action: "advisory_response",
74365
+ rationale: decision.rationale || "analyze and answer inquiry about the attached image"
74366
+ }), freshRetainedPage && decision.action === "ask_user" && isPromptRelevantToPage && !hasFileMutationIntent(plannerInput.prompt) && !isDestructiveFileRequest(plannerInput.prompt) && (decision = {
74367
+ action: "advisory_response",
74368
+ rationale: decision.rationale || "answer inquiry using retained web page context"
74024
74369
  }), isLocalPlannerRuntime(args) && decision.action === "start_task" && plannerInput.clarifications.length === 0 && isDestructiveFileRequest(plannerInput.prompt))
74025
74370
  logger.warn(
74026
74371
  "[orchestration-shell] P43 backstop: a destructive file request was classified start_task without a confirmation this turn; asking first",
@@ -75865,21 +76210,35 @@ var HostedGemmaAdvisoryRunner = class {
75865
76210
  this.providerKind = "hosted";
75866
76211
  }
75867
76212
  async generateAdvisory(promptText, options) {
75868
- if (options?.images && options.images.length > 0)
75869
- throw new Error(
75870
- "HostedGemmaAdvisoryRunner: image attachments are not supported by hosted Bedrock Gemma. A local vision model is required."
75871
- );
75872
76213
  let sessionId = this.deps.activeSessionIdHolder.value;
75873
76214
  if (!sessionId)
75874
76215
  throw new Error("Hosted Gemma advisory generation failed: no active session ID");
75875
76216
  let sessionKey = await this.deps.sessionKeyResolver.getSessionKey(sessionId);
75876
76217
  if (!sessionKey)
75877
76218
  throw new Error(`Hosted Gemma advisory generation failed: session key unresolved for session ${sessionId}`);
75878
- let encryptedPrompt = this.deps.crypto.encryptString(promptText, sessionKey), maxTokens = options?.numPredict, temperature = options?.temperature, start = Date.now();
76219
+ let encryptedPrompt = this.deps.crypto.encryptString(promptText, sessionKey), encryptedImages;
76220
+ if (options?.images && options.images.length > 0) {
76221
+ if (options.images.length > 5)
76222
+ throw new Error(
76223
+ `Hosted Gemma image limit exceeded: at most 5 images are supported (received ${options.images.length}).`
76224
+ );
76225
+ for (let i = 0; i < options.images.length; i++) {
76226
+ let img = options.images[i];
76227
+ if (typeof img != "string" || img.trim().length === 0)
76228
+ throw new Error(`Hosted Gemma image at index ${i} is empty or invalid.`);
76229
+ if (img.length > 2e6)
76230
+ throw new Error(
76231
+ `Hosted Gemma image at index ${i} exceeds size limit of 2,000,000 characters (~1.5 MB). Please provide a smaller or downscaled image.`
76232
+ );
76233
+ }
76234
+ encryptedImages = options.images.map((img) => this.deps.crypto.encryptString(img, sessionKey));
76235
+ }
76236
+ let maxTokens = options?.numPredict, temperature = options?.temperature, start = Date.now();
75879
76237
  try {
75880
76238
  let response = await this.deps.transport.generatePlannerAdvisory({
75881
76239
  sessionId,
75882
76240
  prompt: encryptedPrompt,
76241
+ ...encryptedImages && encryptedImages.length > 0 ? { images: encryptedImages } : {},
75883
76242
  ...maxTokens !== void 0 ? { maxTokens } : {},
75884
76243
  ...temperature !== void 0 ? { temperature } : {}
75885
76244
  });
@@ -75918,14 +76277,7 @@ var CompositeAdvisoryRunner = class {
75918
76277
  if (this.opts.tier === "FREE") {
75919
76278
  if (!this.opts.localRunner)
75920
76279
  throw new Error(
75921
- "LocalModelUnavailable: local Gemma is not installed or enabled. Free tier operates in local-only mode."
75922
- );
75923
- return this.opts.localRunner.generateAdvisory(promptText, options);
75924
- }
75925
- if (options?.images && options.images.length > 0) {
75926
- if (!this.localHealthy || !this.opts.localRunner)
75927
- throw new Error(
75928
- "LocalModelUnavailable: local CodeVibe vision model is required to analyze attached images."
76280
+ options?.images && options.images.length > 0 ? "LocalModelUnavailable: local CodeVibe vision model is required to analyze attached images. Free tier operates in local-only mode." : "LocalModelUnavailable: local Gemma is not installed or enabled. Free tier operates in local-only mode."
75929
76281
  );
75930
76282
  return this.opts.localRunner.generateAdvisory(promptText, options);
75931
76283
  }
@@ -75933,9 +76285,17 @@ var CompositeAdvisoryRunner = class {
75933
76285
  try {
75934
76286
  return await this.opts.localRunner.generateAdvisory(promptText, options);
75935
76287
  } catch (err) {
76288
+ let hasImages = !!(options?.images && options.images.length > 0);
75936
76289
  logger.warn("[composite-advisory] local advisory failed; falling back to hosted Bedrock Gemma", {
75937
- error: err?.message
75938
- }), this.localHealthy = !1, this.opts.onFallbackToHosted?.(err);
76290
+ error: err?.message,
76291
+ hasImages
76292
+ }), this.localHealthy = !1, this.opts.onNotice && this.opts.onNotice(
76293
+ hasImages ? `
76294
+ Local vision model error. Falling back to hosted Gemma on AWS.
76295
+ ` : `
76296
+ Local advisory error. Falling back to hosted Gemma on AWS.
76297
+ `
76298
+ ), this.opts.onFallbackToHosted?.(err);
75939
76299
  }
75940
76300
  return this.opts.hostedRunner.generateAdvisory(promptText, options);
75941
76301
  }
@@ -81580,12 +81940,15 @@ function resolvePlannerAdapter(args) {
81580
81940
  localRuntimeLabel: args.localPlanner.runtimeLabel,
81581
81941
  hostedHealth: hostedStack.health,
81582
81942
  onNotice: (msg) => {
81583
- process.stdout.write(msg);
81943
+ logger.info("[planner] " + msg);
81584
81944
  }
81585
81945
  }), compositeAdvisoryRunner = new CompositeAdvisoryRunner({
81586
81946
  localRunner: args.localPlanner.advisoryRunner,
81587
81947
  hostedRunner: hostedStack.hostedAdvisoryRunner,
81588
- tier
81948
+ tier,
81949
+ onNotice: (msg) => {
81950
+ logger.info("[planner] " + msg);
81951
+ }
81589
81952
  });
81590
81953
  return {
81591
81954
  adapter: compositeAdapter,
@@ -325,7 +325,7 @@ export { reducer } from './reducer';
325
325
  export { createOrchestrationStore, type OrchestrationStore } from './store';
326
326
  export { routeBrowse, RETAINED_PAGE_MAX_CHARS, type RouteBrowseDeps, type RetainedBrowsePage, } from './route-browse';
327
327
  export { composeBrainstormPanelBrief, fitTextToBudget, MAX_BRAINSTORM_PAGE_CHARS, MAX_BRAINSTORM_PAGE_TOKENS, MAX_BRAINSTORM_PAGE_BYTES, } from './brainstorm-quorum';
328
- export { extractQueryRelevantSnippets, htmlToText } from './web/extract';
328
+ export { extractQueryRelevantSnippets, isPromptRelevantToRetainedPage, htmlToText } from './web/extract';
329
329
  export { validateAgyArgPrompt, AGY_ARG_PROMPT_MAX_UTF8_BYTES, } from '../agy-cli-contract';
330
330
  export type { Mode, Tier, PlannerDecision, AgentKind, PlannerHealthState, RunningTaskState, ReviewerSeatState, GateState, ExecutionEventEntry, RefusalEventEntry, BypassEventEntry, MobileEventEntry, EventStreamEntry, ConversationEntry, QueuedTask, PendingClarification, OrchestrationState, OrchestrationAction, LastModeFile, TeamState, TeamTrackEntry, TeamTrackState, TeamMergeGateStatus, LiveProgress, } from './types';
331
331
  export { formatElapsed, isSpinnerPhase, renderProgressLine, type TaskProgressEvent, type OnProgress, } from './task-progress';
@@ -925,9 +925,10 @@ export declare function routeAdvisory(deps: {
925
925
  */
926
926
  attemptedImages?: number;
927
927
  /**
928
- * 2026-09-12 — the page the `browse` route read most recently in this session.
929
- * Rendered as untrusted source material the model uses ONLY when the request is
930
- * about that page (the model decides relevance, the shell does not).
928
+ * 2026-09-12 (updated 2026-09-23) — the page the `browse` route read most recently in this session.
929
+ * Rendered as untrusted source material injected ONLY when the shell determines via
930
+ * `isPromptRelevantToRetainedPage` that the request is relevant to that page (preventing
931
+ * over-grounding and false refusals on unrelated technical follow-ups like A* algorithm).
931
932
  */
932
933
  retainedPage?: RetainedBrowsePage;
933
934
  }): Promise<void>;
@@ -44,3 +44,15 @@ export declare function extractQueryTokens(query: string): string[];
44
44
  * NEVER throws.
45
45
  */
46
46
  export declare function extractQueryRelevantSnippets(fullText: string, query: string, options?: ExtractQuerySnippetsOptions): string;
47
+ /**
48
+ * Detect whether a user prompt in an advisory turn is relevant to the retained browse page.
49
+ *
50
+ * Prevents over-grounding where an unrelated technical question (e.g. "Can you explain A* algorithm to me?")
51
+ * after a web search (e.g. for "iPhone Duo availability") is flooded with 24,000 characters of unrelated web text
52
+ * and rigid "take facts ONLY from the page text" rules, causing false refusals.
53
+ */
54
+ export declare function isPromptRelevantToRetainedPage(prompt: string, retainedPage?: {
55
+ url?: string;
56
+ title?: string;
57
+ text?: string;
58
+ } | null): boolean;
@@ -29,6 +29,7 @@ export interface PlannerAppSyncTransport {
29
29
  generatePlannerAdvisory?(input: {
30
30
  sessionId: string;
31
31
  prompt: string;
32
+ images?: string[];
32
33
  maxTokens?: number;
33
34
  temperature?: number;
34
35
  }): Promise<{