@quantiya/codevibe-antigravity-plugin 2.0.84 → 2.0.86
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/dist/agy-hook.js +191 -18
- package/dist/installer-cli.js +14 -3
- package/dist/server.js +2513 -1367
- package/libexec/agy-hook.sh +23 -7
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -32,6 +32,7 @@ var server_exports = {};
|
|
|
32
32
|
__export(server_exports, {
|
|
33
33
|
ApprovalDetector: () => ApprovalDetector,
|
|
34
34
|
McpServer: () => McpServer,
|
|
35
|
+
PromptResponder: () => PromptResponder,
|
|
35
36
|
SessionNotFoundError: () => SessionNotFoundError,
|
|
36
37
|
StoredAuthFailureError: () => StoredAuthFailureError,
|
|
37
38
|
TERMINAL_SHUTDOWN_SIGNALS: () => TERMINAL_SHUTDOWN_SIGNALS,
|
|
@@ -88,19 +89,19 @@ var os2 = __toESM(require("os"));
|
|
|
88
89
|
var path2 = __toESM(require("path"));
|
|
89
90
|
var HOOK_ENTRY_NAME = "codevibe-antigravity";
|
|
90
91
|
var HOOK_ROUTE_PREFIX = "/agy-hook/v1/";
|
|
91
|
-
var HOOK_EVENTS = ["PreInvocation", "PostToolUse"];
|
|
92
|
+
var HOOK_EVENTS = ["PreInvocation", "PostToolUse", "PreToolUse", "Stop"];
|
|
92
93
|
var HOOK_SHELL_BASENAME = "agy-hook.sh";
|
|
93
94
|
var HOOK_SCRIPT_BASENAME = "agy-hook.js";
|
|
94
95
|
function isHookEvent(value) {
|
|
95
96
|
return typeof value === "string" && HOOK_EVENTS.includes(value);
|
|
96
97
|
}
|
|
97
|
-
function codevibeAgyDir(home = os2.homedir()) {
|
|
98
|
+
function codevibeAgyDir(home = process.env.HOME || os2.homedir()) {
|
|
98
99
|
return path2.join(home, ".codevibe", "codevibe-agy");
|
|
99
100
|
}
|
|
100
|
-
function hooksDir(home = os2.homedir()) {
|
|
101
|
+
function hooksDir(home = process.env.HOME || os2.homedir()) {
|
|
101
102
|
return path2.join(codevibeAgyDir(home), "hooks");
|
|
102
103
|
}
|
|
103
|
-
function globalHooksConfigPath(home = os2.homedir()) {
|
|
104
|
+
function globalHooksConfigPath(home = process.env.HOME || os2.homedir()) {
|
|
104
105
|
return path2.join(home, ".gemini", "config", "hooks.json");
|
|
105
106
|
}
|
|
106
107
|
|
|
@@ -1987,7 +1988,7 @@ var ConversationRouter = class {
|
|
|
1987
1988
|
dropped: entry.overflowed
|
|
1988
1989
|
});
|
|
1989
1990
|
}
|
|
1990
|
-
if (decision) this.options.onRelease(entry.items, decision);
|
|
1991
|
+
if (decision) this.options.onRelease(entry.items, decision, conversationId);
|
|
1991
1992
|
}
|
|
1992
1993
|
expire(conversationId) {
|
|
1993
1994
|
const entry = this.held.get(conversationId);
|
|
@@ -2019,7 +2020,7 @@ var ConversationRouter = class {
|
|
|
2019
2020
|
entry.items = entry.items.slice(tailStart);
|
|
2020
2021
|
if (earlier.length > 0) {
|
|
2021
2022
|
if (this.roles.get(conversationId) === "main" && !this.movedElsewhere(conversationId) && !entry.moved) {
|
|
2022
|
-
this.options.onRelease(earlier, "process");
|
|
2023
|
+
this.options.onRelease(earlier, "process", conversationId);
|
|
2023
2024
|
} else {
|
|
2024
2025
|
this.options.onLog?.("Dropping agy events held from an earlier turn of another session", {
|
|
2025
2026
|
conversationId,
|
|
@@ -2224,6 +2225,14 @@ function buildHookEntry(hooksFolder) {
|
|
|
2224
2225
|
PreInvocation: [
|
|
2225
2226
|
{ type: "command", command: `${shell} PreInvocation`, timeout: HOOK_TIMEOUT_SECONDS }
|
|
2226
2227
|
],
|
|
2228
|
+
PreToolUse: [
|
|
2229
|
+
{
|
|
2230
|
+
matcher: "^(?:ask_question|question|ask)$",
|
|
2231
|
+
hooks: [
|
|
2232
|
+
{ type: "command", command: `${shell} PreToolUse`, timeout: HOOK_TIMEOUT_SECONDS }
|
|
2233
|
+
]
|
|
2234
|
+
}
|
|
2235
|
+
],
|
|
2227
2236
|
PostToolUse: [
|
|
2228
2237
|
{
|
|
2229
2238
|
matcher: "*",
|
|
@@ -2231,6 +2240,9 @@ function buildHookEntry(hooksFolder) {
|
|
|
2231
2240
|
{ type: "command", command: `${shell} PostToolUse`, timeout: HOOK_TIMEOUT_SECONDS }
|
|
2232
2241
|
]
|
|
2233
2242
|
}
|
|
2243
|
+
],
|
|
2244
|
+
Stop: [
|
|
2245
|
+
{ type: "command", command: `${shell} Stop`, timeout: HOOK_TIMEOUT_SECONDS }
|
|
2234
2246
|
]
|
|
2235
2247
|
};
|
|
2236
2248
|
}
|
|
@@ -2913,6 +2925,7 @@ function isComposerBoxAt(lines, idx) {
|
|
|
2913
2925
|
}
|
|
2914
2926
|
return false;
|
|
2915
2927
|
}
|
|
2928
|
+
var ANSI_ESCAPE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
|
|
2916
2929
|
function isPaneInputSafetyBlocked(input) {
|
|
2917
2930
|
const rawText = Array.isArray(input) ? input.join("\n") : input;
|
|
2918
2931
|
if (!rawText) return { blocked: false, kind: null };
|
|
@@ -3082,9 +3095,10 @@ function isPaneInputSafetyBlocked(input) {
|
|
|
3082
3095
|
return { blocked: false, kind: null };
|
|
3083
3096
|
}
|
|
3084
3097
|
function looksLikePromptDelta(chunk) {
|
|
3098
|
+
const text = chunk.includes("\x1B") ? chunk.replace(ANSI_ESCAPE_REGEX, "") : chunk;
|
|
3085
3099
|
return (
|
|
3086
3100
|
// Approval UI signatures
|
|
3087
|
-
/Requesting permission for:/i.test(chunk) || /Do you want to proceed\?/i.test(chunk) || /\besc to cancel\b/i.test(
|
|
3101
|
+
/Requesting permission for:/i.test(chunk) || /Do you want to proceed\?/i.test(chunk) || /\besc to cancel\b/i.test(text) || /^\s*[1-9]\. (?:Yes|No)\b/im.test(text) || /\[(?:y\/n|Y\/n|y\/N)\]/.test(chunk) || /^\s*(?:[❯>]\s*)?\?\s+(?!for\s+(?:shortcuts|help|commands))\S/im.test(text) || /Question\b(?:\s+\d+(?:[\/:of\s]+\d+)?)?:/i.test(text) || /\benter\s+(?:to\s+)?(?:select|submit(?:\s+all)?|next)\b/i.test(text) || /\b(?:navigate|use\s+arrow\s+keys)\b/i.test(text) || /\b\d+[\/:of\s]+\d+:\s+\S/i.test(text) || /\bSubmit(?:\s+All)?\b/i.test(text) || /(?:^|[\s·|│])(?:←|<-)\s*Back\b/i.test(text) || /^File access\s*$/im.test(text) || /^\s*Allow access to this file\?\s*$/im.test(text) || /(?:Yes, allow access|No, deny access)/i.test(text) || /(?:↑\/↓\s+Navigate|ctrl\+r\s+Review)/i.test(text) || /Our servers are experiencing/i.test(text) || /Error ID:\s*[0-9a-fA-F-]*/i.test(text) || /(?:⚠️|⚠\uFE0F?)\s+.*(?:high[- ]traffic|try again in a minute|try again later|stream was interrupted|stream interrupted|overloaded|unexpected error|quota\b|upgrade your subscription|increase your limits|resets in\s+\d|credits balance is too low|out of credits|rate[- ]limit|capacity exhausted|exhausted your quota|internal error|currently unreachable|network issue connecting|authentication expired|authentication issue|terminated due to error|invalid output|invalid tool call)/i.test(text)
|
|
3088
3102
|
);
|
|
3089
3103
|
}
|
|
3090
3104
|
function isNonSystemErrorWarningLine(trimmed) {
|
|
@@ -3159,7 +3173,7 @@ function isAgyStatusFooterLine(trimmed) {
|
|
|
3159
3173
|
if (/^\s*(?:[❯>]\s*)?\?\s+for shortcuts\b/i.test(trimmed)) {
|
|
3160
3174
|
return true;
|
|
3161
3175
|
}
|
|
3162
|
-
if (/^\s*esc to cancel
|
|
3176
|
+
if (/^\s*esc to cancel/i.test(trimmed)) {
|
|
3163
3177
|
return true;
|
|
3164
3178
|
}
|
|
3165
3179
|
if (/^(?:\(initializing\.\.\.\)|\d+\s+task\(s\)\s+·\s+\/tasks|\d+\s+subagent\s*\(s\)|AI:\s+(?:\d+\s+\(\/credits to add more\)|Out of credits)|AI Credits:\s+\d+|Quota:\s*\d+(?:\.\d+)?%)$/i.test(trimmed)) {
|
|
@@ -3258,7 +3272,7 @@ function reconstructFooter(rawLines) {
|
|
|
3258
3272
|
}
|
|
3259
3273
|
const hasMiddleDot = cleaned.some((l) => l.includes("\xB7"));
|
|
3260
3274
|
const hasFooterKeyword = cleaned.some(
|
|
3261
|
-
(l) => /^\s*(?:\?\s+for shortcuts|esc to cancel)
|
|
3275
|
+
(l) => /^\s*(?:\?\s+for shortcuts|esc to cancel)/i.test(l) || /\b(?:auto|accept-edits|plan|Gemini|Claude|GPT|Gemma|high|low|subagent|quota|credits)\b/i.test(l)
|
|
3262
3276
|
);
|
|
3263
3277
|
if (!hasMiddleDot && !hasFooterKeyword) {
|
|
3264
3278
|
return cleaned.join(" ").replace(/\s*·\s*/g, " \xB7 ").trim();
|
|
@@ -3346,7 +3360,7 @@ function isSystemErrorActiveFromIndex(lines, errorIdx) {
|
|
|
3346
3360
|
}
|
|
3347
3361
|
if (remainingRaw.length <= 4) {
|
|
3348
3362
|
const hasFooterMarkers = remainingRaw.some(
|
|
3349
|
-
(l) => l.includes("\xB7") || /^\s*(?:\?\s+for shortcuts|esc to cancel)
|
|
3363
|
+
(l) => l.includes("\xB7") || /^\s*(?:\?\s+for shortcuts|esc to cancel)/i.test(l.trim()) || /\b(?:auto|accept-edits|plan|Gemini|Claude|GPT|Gemma|high|low|subagent|quota|credits)\b/i.test(l)
|
|
3350
3364
|
);
|
|
3351
3365
|
if (hasFooterMarkers) {
|
|
3352
3366
|
const reconstructed = reconstructFooter(remainingRaw);
|
|
@@ -3382,7 +3396,7 @@ function isLegacyYNActiveInLines(lines) {
|
|
|
3382
3396
|
const trimmed = lines[j].trim();
|
|
3383
3397
|
if (!trimmed) continue;
|
|
3384
3398
|
if (/^[─━│╭╰╮╯\s_-]*$/.test(trimmed)) continue;
|
|
3385
|
-
if (/^\s*(?:\?\s+for shortcuts|esc to cancel)
|
|
3399
|
+
if (/^\s*(?:\?\s+for shortcuts|esc to cancel)/i.test(trimmed)) continue;
|
|
3386
3400
|
return { active: false, lineIdx: -1 };
|
|
3387
3401
|
}
|
|
3388
3402
|
return { active: true, lineIdx: ynIdx };
|
|
@@ -3513,7 +3527,7 @@ function extractFileAccessPrompt(snapshot) {
|
|
|
3513
3527
|
headingIndex = index;
|
|
3514
3528
|
break;
|
|
3515
3529
|
}
|
|
3516
|
-
if (/^[─━│╭╰╮╯\s_-]*$/.test(line) || /^[│\s]*[❯>]\s+\S/.test(line) || /^\s*(?:Requesting permission for:|Question \d+\/\d+:|Allow access to this file\?|Do you want to proceed\?)/i.test(line) || /^[▶▸\s]*Thought|^●\s*/i.test(line) || /^\s*(?:\?\s+for shortcuts|esc to cancel)
|
|
3530
|
+
if (/^[─━│╭╰╮╯\s_-]*$/.test(line) || /^[│\s]*[❯>]\s+\S/.test(line) || /^\s*(?:Requesting permission for:|Question \d+\/\d+:|Allow access to this file\?|Do you want to proceed\?)/i.test(line) || /^[▶▸\s]*Thought|^●\s*/i.test(line) || /^\s*(?:\?\s+for shortcuts|esc to cancel)/i.test(line) || /^\s*(?:[↑↓/\s]*Navigate\b|ctrl\+r\s+Review\b)/i.test(line)) {
|
|
3517
3531
|
break;
|
|
3518
3532
|
}
|
|
3519
3533
|
break;
|
|
@@ -4205,7 +4219,7 @@ function isQuestionFooterLine(line) {
|
|
|
4205
4219
|
}
|
|
4206
4220
|
if (/^[>\s❯]*[1-9][.)]\s+\S/.test(trimmed)) return false;
|
|
4207
4221
|
if (isQuestionHeaderLine(trimmed)) return false;
|
|
4208
|
-
return /^(?:esc|<esc>)\s+(?:to\s+)?(?:cancel|skip)(?:\s*[·│,]\s*(?:enter|<enter>)\s+(?:to\s+)?select)?(?:\s*[·│,]\s*ctrl\+[a-z0-9]+\s+[a-zA-Z]+)*$/i.test(trimmed) || /^(?:enter|<enter>)\s+(?:to\s+)?select(?:\s*[·│,]\s*(?:esc|<esc>)\s+(?:to\s+)?(?:cancel|skip))?(?:\s*[·│,]\s*ctrl\+[a-z0-9]+\s+[a-zA-Z]+)*$/i.test(trimmed) || /^(?:↑\/↓|↑|↓|\^|v)\s*(?:\/|\s+)?Navigate(?:\s*[·│,]\s*(?:enter|<enter>)?\s*(?:Select|to select))(?:\s*[·│,]\s*(?:esc|<esc>)?\s*(?:Skip|Cancel))?(?:\s*[·│,]\s*ctrl\+[a-z0-9]+\s+[a-zA-Z]+)*$/i.test(trimmed) || /^(?:↑\/↓|↑|↓|\^|v)\s*(?:\/|\s+)?Navigate(?:\s*[·│,]\s*(?:esc|<esc>)?\s*(?:Skip|Cancel))?$/i.test(trimmed) || /^press\s+<?(?:enter|return|space)>?\s+to\s+select$/i.test(trimmed) || /^space\s+to\s+select$/i.test(trimmed) || /^(?:\(?use\s+arrow\s+keys(?:\s+to\s+navigate)?\)?)$/i.test(trimmed) || /^(?:[\u2800-\u28FF]+(?:\s*waiting(?:\.\.\.)?)?|waiting(?:\.\.\.)?)$/i.test(trimmed);
|
|
4222
|
+
return /^(?:esc|<esc>)\s+(?:to\s+)?(?:cancel|skip)(?:\s*[·│,]\s*(?:enter|<enter>)\s+(?:to\s+)?(?:select|submit(?:\s+all)?))?(?:\s*[·│,]\s*ctrl\+[a-z0-9]+\s+[a-zA-Z]+)*$/i.test(trimmed) || /^(?:enter|<enter>)\s+(?:to\s+)?(?:select|submit(?:\s+all)?)(?:\s*[·│,]\s*(?:esc|<esc>)\s+(?:to\s+)?(?:cancel|skip|back))?(?:\s*[·│,]\s*ctrl\+[a-z0-9]+\s+[a-zA-Z]+)*$/i.test(trimmed) || /^(?:space|<space>)\s+(?:to\s+)?(?:select|toggle)(?:\s*[·│,]\s*(?:enter|<enter>)\s+(?:to\s+)?(?:select|submit(?:\s+all)?))?(?:\s*[·│,]\s*(?:esc|<esc>)\s+(?:to\s+)?(?:cancel|skip|back))?$/i.test(trimmed) || /^(?:↑\/↓|↑|↓|\^|v)\s*(?:\/|\s+)?Navigate(?:\s*[·│,]\s*(?:(?:→|->)\s*Next|(?:←|<-)\s*Back))*(?:\s*[·│,]\s*(?:enter|<enter>)?\s*(?:Select|Submit(?:\s+All)?|to select))(?:\s*[·│,]\s*(?:esc|<esc>)?\s*(?:Skip|Cancel|Back))?(?:\s*[·│,]\s*(?:(?:→|->)\s*Next|(?:←|<-)\s*Back))*(?:\s*[·│,]\s*ctrl\+[a-z0-9]+\s+[a-zA-Z]+)*$/i.test(trimmed) || /^(?:↑\/↓|↑|↓|\^|v)\s*(?:\/|\s+)?Navigate(?:\s*[·│,]\s*(?:esc|<esc>)?\s*(?:Skip|Cancel|Back))?$/i.test(trimmed) || /^(?:←|<-)\s*Back\s*[·│,]\s*(?:enter|<enter>)\s+(?:to\s+)?(?:select|submit(?:\s+all)?)(?:\s*[·│,]\s*(?:esc|<esc>)\s+(?:to\s+)?(?:cancel|skip))?$/i.test(trimmed) || /^press\s+<?(?:enter|return|space)>?\s+to\s+select$/i.test(trimmed) || /^space\s+to\s+select$/i.test(trimmed) || /^(?:\(?use\s+arrow\s+keys(?:\s+to\s+navigate)?\)?)$/i.test(trimmed) || /^(?:[\u2800-\u28FF]+(?:\s*waiting(?:\.\.\.)?)?|waiting(?:\.\.\.)?)$/i.test(trimmed);
|
|
4209
4223
|
}
|
|
4210
4224
|
function extractQuestionHeader(snapshot) {
|
|
4211
4225
|
const stripped = snapshot.replace(ANSI_ESCAPE_REGEX, "");
|
|
@@ -4231,7 +4245,6 @@ function hashPromptSnapshot(snapshot) {
|
|
|
4231
4245
|
}
|
|
4232
4246
|
return (0, import_crypto.createHash)("sha256").update(normalized).digest("hex");
|
|
4233
4247
|
}
|
|
4234
|
-
var ANSI_ESCAPE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
|
|
4235
4248
|
function escapeShellArg(value) {
|
|
4236
4249
|
return value.replace(/'/g, `'\\''`);
|
|
4237
4250
|
}
|
|
@@ -4247,7 +4260,7 @@ function isQuestionUISnapshot(snapshot) {
|
|
|
4247
4260
|
|
|
4248
4261
|
// src/approval-detector.ts
|
|
4249
4262
|
var import_events3 = require("events");
|
|
4250
|
-
var
|
|
4263
|
+
var import_uuid2 = require("uuid");
|
|
4251
4264
|
|
|
4252
4265
|
// src/prompt-parser.ts
|
|
4253
4266
|
var import_crypto2 = require("crypto");
|
|
@@ -4276,17 +4289,17 @@ function parseApprovalSnapshot(snapshot) {
|
|
|
4276
4289
|
return parseApprovalUISnapshot(stripped, snapshot);
|
|
4277
4290
|
}
|
|
4278
4291
|
function buildApprovalSemanticKey(candidate) {
|
|
4279
|
-
const normalize = (value) => value.replace(/\s+/g, " ").trim();
|
|
4292
|
+
const normalize = (value) => (value ?? "").replace(/\s+/g, " ").trim();
|
|
4280
4293
|
const payload = {
|
|
4281
4294
|
kind: candidate.kind,
|
|
4282
4295
|
headerText: normalize(candidate.headerText),
|
|
4283
4296
|
body: normalize(candidate.body),
|
|
4284
|
-
options: candidate.options.map((option) => ({
|
|
4297
|
+
options: (candidate.options ?? []).map((option) => ({
|
|
4285
4298
|
number: option.number,
|
|
4286
4299
|
text: normalize(option.text)
|
|
4287
4300
|
})),
|
|
4288
4301
|
submitMap: Object.fromEntries(
|
|
4289
|
-
Object.entries(candidate.submitMap).sort(([left], [right]) => Number(left) - Number(right)).map(([number, keys]) => [number, [...keys]])
|
|
4302
|
+
Object.entries(candidate.submitMap ?? {}).sort(([left], [right]) => Number(left) - Number(right)).map(([number, keys]) => [number, [...keys]])
|
|
4290
4303
|
)
|
|
4291
4304
|
};
|
|
4292
4305
|
return (0, import_crypto2.createHash)("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
@@ -4376,33 +4389,8 @@ function parseQuestionSnapshot(stripped, originalSnapshot, header) {
|
|
|
4376
4389
|
const options = parseOptions(belowHeader);
|
|
4377
4390
|
if (options.length < 2) return null;
|
|
4378
4391
|
const normalizedHeader = header.fullLine.replace(/^[❯>\s*•-]+\s*/, "").trim();
|
|
4379
|
-
const
|
|
4380
|
-
|
|
4381
|
-
let footerHasCtrlRReview = false;
|
|
4382
|
-
const startLine = headerIdx >= 0 ? headerIdx + 1 : 0;
|
|
4383
|
-
let hasSeenOptions = false;
|
|
4384
|
-
for (let i = startLine; i < lines.length; i++) {
|
|
4385
|
-
if (lineInFence[i]) continue;
|
|
4386
|
-
const trimmed = lines[i].trim();
|
|
4387
|
-
if (!trimmed) continue;
|
|
4388
|
-
if (hasSeenOptions && isResumedTurnSignal(trimmed)) break;
|
|
4389
|
-
if (/^[>\s❯]*[1-9][.)]\s+\S/.test(trimmed)) {
|
|
4390
|
-
hasSeenOptions = true;
|
|
4391
|
-
}
|
|
4392
|
-
const isFooter = isQuestionFooterLine(trimmed) || /^(?:ctrl\+[a-z0-9]+\s+[a-zA-Z]+|\(?use\s+arrow\s+keys(?:\s+to\s+navigate)?\)?)$/i.test(trimmed);
|
|
4393
|
-
if (isFooter) {
|
|
4394
|
-
if (/(?:↑\/↓|↑|↓|\^|v)\s*(?:\/|\s+)?Navigate\b/i.test(trimmed) || /\(?use\s+arrow\s+keys(?:\s+to\s+navigate)?\)?/i.test(trimmed)) {
|
|
4395
|
-
footerHasArrowHint = true;
|
|
4396
|
-
}
|
|
4397
|
-
if (/ctrl\+r\s+Review\b/i.test(trimmed)) {
|
|
4398
|
-
footerHasCtrlRReview = true;
|
|
4399
|
-
}
|
|
4400
|
-
}
|
|
4401
|
-
}
|
|
4402
|
-
const hasArrowKeyHint = headerHasArrowHint || footerHasArrowHint;
|
|
4403
|
-
const hasWriteInOption = options.some((opt) => /^\s*write-?in\b/i.test(opt.text));
|
|
4404
|
-
const hasCtrlRReviewFooter = footerHasCtrlRReview;
|
|
4405
|
-
const isBaseNumberedWizard = /^Question\s+\d+\/\d+:/i.test(normalizedHeader) && !hasArrowKeyHint && !hasWriteInOption && !hasCtrlRReviewFooter;
|
|
4392
|
+
const isMultiSelect = /\bselect\s+(?:all|multiple)\b/i.test(normalizedHeader) || /\bselect\s+(?:all|multiple)\b/i.test(belowHeader) || /(?:<space>|\bspace)\s+(?:to\s+)?(?:select|toggle)\b/i.test(belowHeader) || /\bctrl\+a\s+select\s+all\b/i.test(belowHeader);
|
|
4393
|
+
const isBaseNumberedWizard = /^Question\s+\d+\/\d+:/i.test(normalizedHeader) && !isMultiSelect;
|
|
4406
4394
|
const submitMap = isBaseNumberedWizard ? buildQuestionSubmitMap(options) : {};
|
|
4407
4395
|
const body = extractBodyBetweenHeaderAndFirstOption(belowHeader);
|
|
4408
4396
|
return {
|
|
@@ -4465,12 +4453,47 @@ function buildApprovalSubmitMap(options) {
|
|
|
4465
4453
|
function buildQuestionSubmitMap(options) {
|
|
4466
4454
|
const map = {};
|
|
4467
4455
|
for (const opt of options) {
|
|
4468
|
-
|
|
4469
|
-
map[opt.number] = [opt.number, "Enter"];
|
|
4470
|
-
}
|
|
4456
|
+
map[opt.number] = [opt.number];
|
|
4471
4457
|
}
|
|
4472
4458
|
return map;
|
|
4473
4459
|
}
|
|
4460
|
+
function isWriteInInputActive(paneText, expectedHeader, expectedStep) {
|
|
4461
|
+
if (!paneText) return false;
|
|
4462
|
+
if (detectActivePromptKind(paneText) !== "question") return false;
|
|
4463
|
+
const stripped = paneText.replace(ANSI_ESCAPE_REGEX2, "");
|
|
4464
|
+
const lines = stripped.trimEnd().split("\n");
|
|
4465
|
+
const windowStart = Math.max(0, lines.length - 30);
|
|
4466
|
+
let headerIdx = -1;
|
|
4467
|
+
for (let i = lines.length - 1; i >= windowStart; i--) {
|
|
4468
|
+
if (isQuestionHeaderLine(lines[i])) {
|
|
4469
|
+
headerIdx = i;
|
|
4470
|
+
break;
|
|
4471
|
+
}
|
|
4472
|
+
}
|
|
4473
|
+
if (headerIdx < 0) return false;
|
|
4474
|
+
const rawHeader = lines[headerIdx];
|
|
4475
|
+
const extracted = extractQuestionHeader(rawHeader);
|
|
4476
|
+
if (!extracted) return false;
|
|
4477
|
+
if (expectedHeader) {
|
|
4478
|
+
const expectedExtracted = extractQuestionHeader(expectedHeader);
|
|
4479
|
+
const expectedBody = (expectedExtracted ? expectedExtracted.body : expectedHeader).replace(/^[❯>\s*•-]+\s*/, "").trim().toLowerCase();
|
|
4480
|
+
const paneBody = extracted.body.replace(/^[❯>\s*•-]+\s*/, "").trim().toLowerCase();
|
|
4481
|
+
if (paneBody !== expectedBody) {
|
|
4482
|
+
return false;
|
|
4483
|
+
}
|
|
4484
|
+
}
|
|
4485
|
+
const expectedStepVal = expectedStep || (expectedHeader ? expectedHeader.match(/\bQuestion\s+(\d+(?:[\/:of\s]+\d+)?):/i)?.[1] : void 0);
|
|
4486
|
+
if (expectedStepVal) {
|
|
4487
|
+
const stepMatch = extracted.fullLine.match(/\bQuestion\s+(\d+(?:[\/:of\s]+\d+)?):/i);
|
|
4488
|
+
const paneStep = stepMatch ? stepMatch[1] : null;
|
|
4489
|
+
if (!paneStep || paneStep !== expectedStepVal) {
|
|
4490
|
+
return false;
|
|
4491
|
+
}
|
|
4492
|
+
}
|
|
4493
|
+
const searchLines = lines.slice(headerIdx);
|
|
4494
|
+
const text = searchLines.join("\n");
|
|
4495
|
+
return /Your answer:/i.test(text) && /enter\s+Submit(?:\s+All)?\s*[·│,]\s*esc\s+Back/i.test(text);
|
|
4496
|
+
}
|
|
4474
4497
|
function extractIdentity(headerText) {
|
|
4475
4498
|
if (!headerText) return {};
|
|
4476
4499
|
const trimmed = headerText.trim();
|
|
@@ -4561,1428 +4584,2096 @@ function stripAnsi(s) {
|
|
|
4561
4584
|
return s.replace(ANSI_ESCAPE_REGEX2, "");
|
|
4562
4585
|
}
|
|
4563
4586
|
|
|
4564
|
-
// src/
|
|
4565
|
-
var
|
|
4566
|
-
var
|
|
4567
|
-
var
|
|
4568
|
-
var
|
|
4569
|
-
var
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
* for the same prompt. The primary dedupe for cosmetic redraws of a
|
|
4592
|
-
* single approval (spinner ticks, cursor blink, partial repaints)
|
|
4593
|
-
* lives in tmux-pane-observer's debounce window — by the time a
|
|
4594
|
-
* candidate reaches this layer, the pane has settled. */
|
|
4595
|
-
this.paneOnlyEmittedHashes = /* @__PURE__ */ new Set();
|
|
4596
|
-
this.promptTtlMs = options.promptTtlMs ?? DEFAULT_PROMPT_TTL_MS;
|
|
4597
|
-
this.pendingCallTtlMs = options.pendingCallTtlMs ?? PENDING_CALL_TTL_MS;
|
|
4598
|
-
this.sweepIntervalMs = options.sweepIntervalMs ?? SWEEP_INTERVAL_MS;
|
|
4599
|
-
this.pendingCandidateBufferMs = options.pendingCandidateBufferMs ?? PENDING_CANDIDATE_BUFFER_MS;
|
|
4600
|
-
}
|
|
4601
|
-
start() {
|
|
4602
|
-
if (this.sweepTimer) return;
|
|
4603
|
-
this.sweepTimer = setInterval(() => this.sweep(), this.sweepIntervalMs);
|
|
4604
|
-
if (this.sweepTimer.unref) this.sweepTimer.unref();
|
|
4605
|
-
logger.info("Approval detector started", {
|
|
4606
|
-
promptTtlMs: this.promptTtlMs,
|
|
4607
|
-
pendingCallTtlMs: this.pendingCallTtlMs
|
|
4608
|
-
});
|
|
4609
|
-
}
|
|
4610
|
-
async stop() {
|
|
4611
|
-
if (this.sweepTimer) {
|
|
4612
|
-
clearInterval(this.sweepTimer);
|
|
4613
|
-
this.sweepTimer = null;
|
|
4614
|
-
}
|
|
4615
|
-
this.pendingCalls.clear();
|
|
4616
|
-
this.pendingPrompts.clear();
|
|
4617
|
-
this.resolvedPrompts.clear();
|
|
4618
|
-
this.pendingCandidates.clear();
|
|
4619
|
-
this.paneOnlyEmittedHashes.clear();
|
|
4620
|
-
logger.info("Approval detector stopped");
|
|
4621
|
-
}
|
|
4622
|
-
/**
|
|
4623
|
-
* Emit INTERACTIVE_PROMPT directly from a pane snapshot, WITHOUT
|
|
4624
|
-
* requiring a matching PendingToolCall from a transcript PLANNER_RESPONSE.
|
|
4625
|
-
*
|
|
4626
|
-
* Background (v9.1, 2026-05-21): agy does NOT write tool_call entries
|
|
4627
|
-
* to the transcript BEFORE rendering the approval UI for shell
|
|
4628
|
-
* commands. It only writes them AFTER the user approves. So the
|
|
4629
|
-
* transcript-based pending-call matching path deadlocks for shell
|
|
4630
|
-
* commands: paneObserver sees the UI, but no pending call exists →
|
|
4631
|
-
* candidate buffered → user can't approve from mobile (no prompt
|
|
4632
|
-
* shown) → no transcript write → buffer expires.
|
|
4633
|
-
*
|
|
4634
|
-
* Solution: pane is authoritative for "approval UI is showing right
|
|
4635
|
-
* now." Skip the pending-call requirement and emit directly. The
|
|
4636
|
-
* parsed candidate has everything needed (command, options, submitMap).
|
|
4637
|
-
*
|
|
4638
|
-
* Synthesizes a minimal PendingToolCall from the candidate so the
|
|
4639
|
-
* existing handlePendingPrompt code path doesn't need to branch on
|
|
4640
|
-
* `pendingCall: null`.
|
|
4641
|
-
*
|
|
4642
|
-
* Returns the emitted state, or null if dedupe (already emitted for
|
|
4643
|
-
* this paneHash) or disabled.
|
|
4644
|
-
*/
|
|
4645
|
-
emitPaneOnlyPrompt(candidate, conversationId, forcedPromptId) {
|
|
4646
|
-
if (this.paneOnlyEmittedHashes.has(candidate.paneHash)) {
|
|
4647
|
-
logger.debug("Skipping pane-only re-emit for duplicate paneHash", {
|
|
4648
|
-
paneHash: candidate.paneHash.substring(0, 16)
|
|
4587
|
+
// src/event-mapper.ts
|
|
4588
|
+
var import_uuid = require("uuid");
|
|
4589
|
+
var import_codevibe_core5 = require("@quantiya/codevibe-core");
|
|
4590
|
+
var CONTENT_MAX = 150 * 1024;
|
|
4591
|
+
var TOOL_OUTPUT_MAX = 50 * 1024;
|
|
4592
|
+
var TOOL_DIFF_MAX = 100 * 1024;
|
|
4593
|
+
function mapTranscriptEvent(emit, sessionId, notBeforeMs) {
|
|
4594
|
+
const { event } = emit;
|
|
4595
|
+
const base = {
|
|
4596
|
+
sessionId,
|
|
4597
|
+
source: import_codevibe_core5.EventSource.DESKTOP
|
|
4598
|
+
};
|
|
4599
|
+
const orderingKey = `${sessionId}:${emit.conversationId}`;
|
|
4600
|
+
const nextTimestamp = () => (0, import_codevibe_core5.prepareEventTimestamp)({ orderingKey, agentClock: event.created_at, notBeforeMs });
|
|
4601
|
+
const baseMetadata = {
|
|
4602
|
+
step_index: event.step_index,
|
|
4603
|
+
agy_source: event.source,
|
|
4604
|
+
agy_status: event.status,
|
|
4605
|
+
agy_created_at: event.created_at,
|
|
4606
|
+
agy_type: event.type,
|
|
4607
|
+
agy_conversation_id: emit.conversationId
|
|
4608
|
+
};
|
|
4609
|
+
if (event.source === "SYSTEM") {
|
|
4610
|
+
if (event.type === "CONVERSATION_HISTORY" || event.type === "SYSTEM_MESSAGE") {
|
|
4611
|
+
logger.debug("Dropping internal transcript event", {
|
|
4612
|
+
type: event.type,
|
|
4613
|
+
stepIndex: event.step_index
|
|
4649
4614
|
});
|
|
4650
|
-
return
|
|
4615
|
+
return { events: [], pendingCallRegistrations: [] };
|
|
4651
4616
|
}
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4617
|
+
if (event.type !== "ERROR_MESSAGE") {
|
|
4618
|
+
logger.warn("Dropping unknown SYSTEM-source transcript event (safety)", {
|
|
4619
|
+
type: event.type,
|
|
4620
|
+
stepIndex: event.step_index
|
|
4621
|
+
});
|
|
4622
|
+
return { events: [], pendingCallRegistrations: [] };
|
|
4656
4623
|
}
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
// Best-effort tool-type inference from the candidate. For shell
|
|
4664
|
-
// commands → RUN_COMMAND. For edit-style headers with a file
|
|
4665
|
-
// path → CODE_ACTION. For questions → ASK_QUESTION. Default: GENERIC.
|
|
4666
|
-
toolType: matchingCall?.toolType ?? guessToolTypeFromIdentity(candidate),
|
|
4667
|
-
command: matchingCall?.command ?? candidate.command,
|
|
4668
|
-
filePath: matchingCall?.filePath ?? candidate.filePath,
|
|
4669
|
-
rawToolName: matchingCall?.rawToolName ?? (candidate.kind === "question" ? "ask_question" : candidate.command ?? candidate.filePath ?? "unknown"),
|
|
4670
|
-
questionText: candidate.kind === "question" && candidate.headerText ? candidate.headerText : matchingCall?.questionText,
|
|
4671
|
-
// NOTE (Codex R3-F1 / Claude R3-F2): Pane options are authoritative ground truth
|
|
4672
|
-
// that directly correspond to desktop keystrokes (submitMap). Transcript options
|
|
4673
|
-
// serve solely as fallback when pane options cannot be parsed.
|
|
4674
|
-
questionOptions: candidate.kind === "question" && candidate.options && candidate.options.length > 0 ? candidate.options : matchingCall?.questionOptions,
|
|
4675
|
-
promptId,
|
|
4676
|
-
startedAt: matchingCall?.startedAt ?? Date.now(),
|
|
4677
|
-
intentCreatedAt: matchingCall?.intentCreatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
4678
|
-
};
|
|
4679
|
-
const state = {
|
|
4680
|
-
promptId,
|
|
4681
|
-
conversationId,
|
|
4682
|
-
pendingCall: syntheticCall,
|
|
4683
|
-
submitMap: candidate.submitMap,
|
|
4684
|
-
matchedPaneHeader: candidate.headerText,
|
|
4685
|
-
paneDisplayHeader: candidate.fullHeaderLine,
|
|
4686
|
-
body: candidate.body,
|
|
4687
|
-
paneOptions: candidate.options,
|
|
4688
|
-
paneSemanticKey: buildApprovalSemanticKey(candidate),
|
|
4689
|
-
kind: candidate.kind,
|
|
4690
|
-
emittedAt: Date.now(),
|
|
4691
|
-
ttlMs: this.promptTtlMs,
|
|
4692
|
-
// E1 (§6a-2b / F5) — carry the consumed pane hash so an emit-failure
|
|
4693
|
-
// rollback can reset the observer's lastPromptHash and let the unchanged
|
|
4694
|
-
// live pane re-emit.
|
|
4695
|
-
paneHash: candidate.paneHash
|
|
4696
|
-
};
|
|
4697
|
-
this.pendingPrompts.set(promptId, state);
|
|
4698
|
-
this.paneOnlyEmittedHashes.add(candidate.paneHash);
|
|
4699
|
-
logger.info("Emitting pane-only INTERACTIVE_PROMPT", {
|
|
4700
|
-
promptId,
|
|
4701
|
-
command: candidate.command,
|
|
4702
|
-
filePath: candidate.filePath,
|
|
4703
|
-
optionsCount: candidate.options.length
|
|
4624
|
+
}
|
|
4625
|
+
if (event.type === "CONVERSATION_HISTORY" || event.type === "SYSTEM_MESSAGE") {
|
|
4626
|
+
logger.debug("Dropping internal transcript event (non-SYSTEM source)", {
|
|
4627
|
+
type: event.type,
|
|
4628
|
+
source: event.source,
|
|
4629
|
+
stepIndex: event.step_index
|
|
4704
4630
|
});
|
|
4705
|
-
|
|
4706
|
-
return state;
|
|
4631
|
+
return { events: [], pendingCallRegistrations: [] };
|
|
4707
4632
|
}
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4633
|
+
if (event.type === "USER_INPUT") {
|
|
4634
|
+
const cleaned = stripUserRequestWrapper(event.content || "");
|
|
4635
|
+
return {
|
|
4636
|
+
events: [
|
|
4637
|
+
{
|
|
4638
|
+
...base,
|
|
4639
|
+
timestamp: nextTimestamp(),
|
|
4640
|
+
type: import_codevibe_core5.EventType.USER_PROMPT,
|
|
4641
|
+
content: truncate(cleaned, CONTENT_MAX),
|
|
4642
|
+
metadata: { ...baseMetadata }
|
|
4643
|
+
}
|
|
4644
|
+
],
|
|
4645
|
+
pendingCallRegistrations: []
|
|
4646
|
+
};
|
|
4647
|
+
}
|
|
4648
|
+
if (event.type === "PLANNER_RESPONSE") {
|
|
4649
|
+
const events = [];
|
|
4650
|
+
const pendingCallRegistrations = [];
|
|
4651
|
+
if (typeof event.thinking === "string" && event.thinking.trim().length > 0) {
|
|
4652
|
+
events.push({
|
|
4653
|
+
...base,
|
|
4654
|
+
timestamp: nextTimestamp(),
|
|
4655
|
+
type: import_codevibe_core5.EventType.REASONING,
|
|
4656
|
+
content: truncate(event.thinking, CONTENT_MAX),
|
|
4657
|
+
metadata: { ...baseMetadata }
|
|
4721
4658
|
});
|
|
4722
|
-
return;
|
|
4723
4659
|
}
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
if (
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4660
|
+
if (typeof event.content === "string" && event.content.trim().length > 0) {
|
|
4661
|
+
events.push({
|
|
4662
|
+
...base,
|
|
4663
|
+
timestamp: nextTimestamp(),
|
|
4664
|
+
type: import_codevibe_core5.EventType.ASSISTANT_RESPONSE,
|
|
4665
|
+
content: truncate(event.content, CONTENT_MAX),
|
|
4666
|
+
metadata: { ...baseMetadata }
|
|
4667
|
+
});
|
|
4668
|
+
}
|
|
4669
|
+
const toolCalls = event.tool_calls;
|
|
4670
|
+
if (Array.isArray(toolCalls)) {
|
|
4671
|
+
for (let i = 0; i < toolCalls.length; i++) {
|
|
4672
|
+
const tc = toolCalls[i];
|
|
4673
|
+
if (!tc || typeof tc !== "object") continue;
|
|
4674
|
+
const name = tc.name;
|
|
4675
|
+
if (typeof name !== "string") continue;
|
|
4676
|
+
const args = tc.args;
|
|
4677
|
+
pendingCallRegistrations.push({
|
|
4678
|
+
intentStepIndex: event.step_index,
|
|
4679
|
+
toolCallIndex: i,
|
|
4680
|
+
intentByteOffset: emit.byteOffset,
|
|
4681
|
+
toolType: hookToolNameToTranscriptType(name),
|
|
4682
|
+
conversationId: emit.conversationId,
|
|
4683
|
+
command: extractCommandFromArgs(name, args),
|
|
4684
|
+
filePath: extractFilePathFromArgs(name, args),
|
|
4685
|
+
questionText: extractQuestionFromArgs(name, args),
|
|
4686
|
+
questionOptions: extractQuestionOptionsFromArgs(name, args),
|
|
4687
|
+
cwd: extractCwdFromArgs(args),
|
|
4688
|
+
rawToolName: name,
|
|
4689
|
+
startedAt: Date.now(),
|
|
4690
|
+
intentCreatedAt: event.created_at
|
|
4748
4691
|
});
|
|
4749
4692
|
}
|
|
4750
4693
|
}
|
|
4694
|
+
return { events, pendingCallRegistrations };
|
|
4751
4695
|
}
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4696
|
+
if (event.type === "ERROR_MESSAGE") {
|
|
4697
|
+
const content = stripTimestampPrefix(event.content || "Agy error");
|
|
4698
|
+
return {
|
|
4699
|
+
events: [
|
|
4700
|
+
{
|
|
4701
|
+
...base,
|
|
4702
|
+
timestamp: nextTimestamp(),
|
|
4703
|
+
type: import_codevibe_core5.EventType.NOTIFICATION,
|
|
4704
|
+
content: truncate(content, CONTENT_MAX),
|
|
4705
|
+
metadata: {
|
|
4706
|
+
...baseMetadata,
|
|
4707
|
+
severity: "error"
|
|
4708
|
+
}
|
|
4709
|
+
}
|
|
4710
|
+
],
|
|
4711
|
+
pendingCallRegistrations: []
|
|
4712
|
+
};
|
|
4761
4713
|
}
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
const expected = call.intentStepIndex + call.toolCallIndex + 1;
|
|
4779
|
-
if (expected !== args.resultStepIndex) continue;
|
|
4780
|
-
if (typeof args.resultByteOffset === "number") {
|
|
4781
|
-
if (call.intentByteOffset >= args.resultByteOffset) continue;
|
|
4782
|
-
if (!matched || call.intentByteOffset > matched.intentByteOffset) {
|
|
4783
|
-
matched = call;
|
|
4784
|
-
matchedKey = key;
|
|
4714
|
+
const toolName = transcriptTypeToFriendlyToolName(event.type);
|
|
4715
|
+
const isErrorStatus = event.status === "ERROR";
|
|
4716
|
+
const rawContent = stripTimestampPrefix(event.content || "");
|
|
4717
|
+
const truncatedContent = truncate(rawContent, TOOL_OUTPUT_MAX);
|
|
4718
|
+
return {
|
|
4719
|
+
events: [
|
|
4720
|
+
{
|
|
4721
|
+
...base,
|
|
4722
|
+
timestamp: nextTimestamp(),
|
|
4723
|
+
type: import_codevibe_core5.EventType.TOOL_USE,
|
|
4724
|
+
content: formatToolResultContent(event.type, toolName, truncatedContent, isErrorStatus),
|
|
4725
|
+
metadata: {
|
|
4726
|
+
...baseMetadata,
|
|
4727
|
+
tool_name: toolName,
|
|
4728
|
+
tool_output: truncatedContent,
|
|
4729
|
+
success: !isErrorStatus
|
|
4785
4730
|
}
|
|
4786
|
-
} else if (!matched || call.intentStepIndex < matched.intentStepIndex) {
|
|
4787
|
-
matched = call;
|
|
4788
|
-
matchedKey = key;
|
|
4789
4731
|
}
|
|
4732
|
+
],
|
|
4733
|
+
pendingCallRegistrations: []
|
|
4734
|
+
};
|
|
4735
|
+
}
|
|
4736
|
+
function stripUserRequestWrapper(content) {
|
|
4737
|
+
const match = content.match(/<USER_REQUEST>([\s\S]*?)<\/USER_REQUEST>/);
|
|
4738
|
+
if (match && match[1] !== void 0) {
|
|
4739
|
+
return match[1].trim();
|
|
4740
|
+
}
|
|
4741
|
+
return content.trim();
|
|
4742
|
+
}
|
|
4743
|
+
function stripTimestampPrefix(content) {
|
|
4744
|
+
return content.replace(
|
|
4745
|
+
/^Created At:[^\n]*\nCompleted At:[^\n]*\n+(?:\s*\n)?/,
|
|
4746
|
+
""
|
|
4747
|
+
).trim();
|
|
4748
|
+
}
|
|
4749
|
+
function transcriptTypeToFriendlyToolName(type) {
|
|
4750
|
+
switch (type) {
|
|
4751
|
+
case "RUN_COMMAND":
|
|
4752
|
+
return "Bash";
|
|
4753
|
+
case "LIST_DIRECTORY":
|
|
4754
|
+
return "LS";
|
|
4755
|
+
case "GREP_SEARCH":
|
|
4756
|
+
return "Grep";
|
|
4757
|
+
case "VIEW_FILE":
|
|
4758
|
+
return "Read";
|
|
4759
|
+
case "CODE_ACTION":
|
|
4760
|
+
return "Edit";
|
|
4761
|
+
case "SEARCH_WEB":
|
|
4762
|
+
return "WebSearch";
|
|
4763
|
+
case "GENERIC":
|
|
4764
|
+
return "Other";
|
|
4765
|
+
default:
|
|
4766
|
+
return "Other";
|
|
4767
|
+
}
|
|
4768
|
+
}
|
|
4769
|
+
function hookToolNameToTranscriptType(hookName) {
|
|
4770
|
+
const upper = hookName.toUpperCase();
|
|
4771
|
+
switch (upper) {
|
|
4772
|
+
case "LIST_DIR":
|
|
4773
|
+
return "LIST_DIRECTORY";
|
|
4774
|
+
case "REPLACE_FILE_CONTENT":
|
|
4775
|
+
case "MULTI_REPLACE_FILE_CONTENT":
|
|
4776
|
+
case "WRITE_TO_FILE":
|
|
4777
|
+
return "CODE_ACTION";
|
|
4778
|
+
case "GENERATE_IMAGE":
|
|
4779
|
+
case "READ_URL_CONTENT":
|
|
4780
|
+
case "INVOKE_SUBAGENT":
|
|
4781
|
+
case "DEFINE_SUBAGENT":
|
|
4782
|
+
case "MANAGE_SUBAGENTS":
|
|
4783
|
+
case "SEND_MESSAGE":
|
|
4784
|
+
case "LIST_PERMISSIONS":
|
|
4785
|
+
case "MANAGE_TASK":
|
|
4786
|
+
case "SCHEDULE":
|
|
4787
|
+
case "ASK_PERMISSION":
|
|
4788
|
+
return "GENERIC";
|
|
4789
|
+
case "ASK_QUESTION":
|
|
4790
|
+
return "ASK_QUESTION";
|
|
4791
|
+
default:
|
|
4792
|
+
return upper;
|
|
4793
|
+
}
|
|
4794
|
+
}
|
|
4795
|
+
function extractCommandFromArgs(toolName, args) {
|
|
4796
|
+
if (!args || typeof args !== "object") return void 0;
|
|
4797
|
+
const a = args;
|
|
4798
|
+
for (const key of ["CommandLine", "Command", "command", "cmd", "exec"]) {
|
|
4799
|
+
const v = a[key];
|
|
4800
|
+
if (typeof v === "string" && v.trim().length > 0) {
|
|
4801
|
+
return stripWrappingQuotes2(v.trim());
|
|
4790
4802
|
}
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4803
|
+
}
|
|
4804
|
+
return void 0;
|
|
4805
|
+
}
|
|
4806
|
+
function extractFilePathFromArgs(toolName, args) {
|
|
4807
|
+
if (!args || typeof args !== "object") return void 0;
|
|
4808
|
+
const a = args;
|
|
4809
|
+
for (const key of [
|
|
4810
|
+
"TargetFile",
|
|
4811
|
+
"FilePath",
|
|
4812
|
+
"file_path",
|
|
4813
|
+
"filePath",
|
|
4814
|
+
"path",
|
|
4815
|
+
"DirectoryPath",
|
|
4816
|
+
"directory",
|
|
4817
|
+
"Directory"
|
|
4818
|
+
]) {
|
|
4819
|
+
const v = a[key];
|
|
4820
|
+
if (typeof v === "string" && v.trim().length > 0) {
|
|
4821
|
+
return stripWrappingQuotes2(v.trim());
|
|
4822
|
+
}
|
|
4823
|
+
}
|
|
4824
|
+
return void 0;
|
|
4825
|
+
}
|
|
4826
|
+
function extractQuestionFromArgs(toolName, args) {
|
|
4827
|
+
if (!args || typeof args !== "object") return void 0;
|
|
4828
|
+
const tn = (toolName || "").toLowerCase().trim();
|
|
4829
|
+
const isQuestionTool = tn === "ask_question" || tn === "question" || tn === "ask";
|
|
4830
|
+
if (!isQuestionTool) return void 0;
|
|
4831
|
+
const a = args;
|
|
4832
|
+
if (typeof a.question === "string" && a.question.trim().length > 0) {
|
|
4833
|
+
return stripWrappingQuotes2(a.question.trim());
|
|
4834
|
+
}
|
|
4835
|
+
if (typeof a.prompt === "string" && a.prompt.trim().length > 0) {
|
|
4836
|
+
return stripWrappingQuotes2(a.prompt.trim());
|
|
4837
|
+
}
|
|
4838
|
+
let questions = a.questions;
|
|
4839
|
+
if (typeof questions === "string") {
|
|
4840
|
+
try {
|
|
4841
|
+
questions = JSON.parse(questions.trim());
|
|
4842
|
+
} catch {
|
|
4843
|
+
}
|
|
4844
|
+
}
|
|
4845
|
+
if (Array.isArray(questions) && questions.length > 0) {
|
|
4846
|
+
const first = questions[0];
|
|
4847
|
+
if (typeof first === "string" && first.trim().length > 0) {
|
|
4848
|
+
return stripWrappingQuotes2(first.trim());
|
|
4849
|
+
}
|
|
4850
|
+
if (first && typeof first === "object") {
|
|
4851
|
+
const q = first.question;
|
|
4852
|
+
if (typeof q === "string" && q.trim().length > 0) {
|
|
4853
|
+
return stripWrappingQuotes2(q.trim());
|
|
4797
4854
|
}
|
|
4798
|
-
logger.debug("Pending tool call cleared on result event", {
|
|
4799
|
-
intentStepIndex: matched.intentStepIndex,
|
|
4800
|
-
toolCallIndex: matched.toolCallIndex,
|
|
4801
|
-
resultStepIndex: args.resultStepIndex,
|
|
4802
|
-
toolType: args.toolType
|
|
4803
|
-
});
|
|
4804
4855
|
}
|
|
4805
|
-
return matched;
|
|
4806
4856
|
}
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
* intentStepIndex.
|
|
4821
|
-
* 4. If a match exists AND it already has a promptId AND that
|
|
4822
|
-
* promptId is in resolvedPrompts → null (don't re-emit).
|
|
4823
|
-
* 5. If a match exists AND has a promptId BUT NOT resolved → return
|
|
4824
|
-
* the existing PendingPromptState (idempotent re-emit on the same
|
|
4825
|
-
* promptId for a duplicate candidate hash).
|
|
4826
|
-
* 6. Otherwise: create a fresh promptId + PendingPromptState, attach
|
|
4827
|
-
* to the matched call, store in pendingPrompts, return it.
|
|
4828
|
-
*/
|
|
4829
|
-
matchCandidate(candidate, conversationId) {
|
|
4830
|
-
const state = this.matchCandidateInternal(candidate, conversationId);
|
|
4831
|
-
if (state) return state;
|
|
4832
|
-
const existing = this.pendingCandidates.get(this.candidateKey(conversationId, candidate.paneHash));
|
|
4833
|
-
if (existing) {
|
|
4834
|
-
logger.debug("Candidate already buffered (same paneHash)", { paneHash: this.candidateKey(conversationId, candidate.paneHash) });
|
|
4835
|
-
} else {
|
|
4836
|
-
this.pendingCandidates.set(this.candidateKey(conversationId, candidate.paneHash), {
|
|
4837
|
-
candidate,
|
|
4838
|
-
conversationId,
|
|
4839
|
-
queuedAt: Date.now()
|
|
4840
|
-
});
|
|
4841
|
-
logger.debug("No pending tool-call matched \u2014 buffering candidate for pre-transcript window", {
|
|
4842
|
-
candidateCommand: candidate.command,
|
|
4843
|
-
candidateFilePath: candidate.filePath,
|
|
4844
|
-
pendingCount: this.pendingCalls.size,
|
|
4845
|
-
bufferSize: this.pendingCandidates.size
|
|
4846
|
-
});
|
|
4857
|
+
return void 0;
|
|
4858
|
+
}
|
|
4859
|
+
function extractQuestionOptionsFromArgs(toolName, args) {
|
|
4860
|
+
if (!args || typeof args !== "object") return void 0;
|
|
4861
|
+
const tn = (toolName || "").toLowerCase().trim();
|
|
4862
|
+
const isQuestionTool = tn === "ask_question" || tn === "question" || tn === "ask";
|
|
4863
|
+
if (!isQuestionTool) return void 0;
|
|
4864
|
+
const a = args;
|
|
4865
|
+
let questions = a.questions;
|
|
4866
|
+
if (typeof questions === "string") {
|
|
4867
|
+
try {
|
|
4868
|
+
questions = JSON.parse(questions.trim());
|
|
4869
|
+
} catch {
|
|
4847
4870
|
}
|
|
4848
|
-
return null;
|
|
4849
4871
|
}
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4872
|
+
if (Array.isArray(questions) && questions.length > 0) {
|
|
4873
|
+
const first = questions[0];
|
|
4874
|
+
if (first && typeof first === "object" && Array.isArray(first.options)) {
|
|
4875
|
+
const rawOptions = first.options;
|
|
4876
|
+
const result = [];
|
|
4877
|
+
for (let i = 0; i < rawOptions.length; i++) {
|
|
4878
|
+
const opt = rawOptions[i];
|
|
4879
|
+
if (typeof opt === "string" && opt.trim().length > 0) {
|
|
4880
|
+
result.push({ number: String(i + 1), text: stripWrappingQuotes2(opt.trim()) });
|
|
4881
|
+
} else if (opt && typeof opt === "object" && typeof opt.text === "string") {
|
|
4882
|
+
result.push({
|
|
4883
|
+
number: String(opt.number ?? i + 1),
|
|
4884
|
+
text: stripWrappingQuotes2(String(opt.text).trim())
|
|
4885
|
+
});
|
|
4886
|
+
}
|
|
4887
|
+
}
|
|
4888
|
+
if (result.length > 0) return result;
|
|
4867
4889
|
}
|
|
4868
|
-
const promptId = (0, import_uuid.v4)();
|
|
4869
|
-
matched.promptId = promptId;
|
|
4870
|
-
const state = {
|
|
4871
|
-
promptId,
|
|
4872
|
-
conversationId,
|
|
4873
|
-
pendingCall: matched,
|
|
4874
|
-
submitMap: candidate.submitMap,
|
|
4875
|
-
matchedPaneHeader: candidate.headerText,
|
|
4876
|
-
paneDisplayHeader: candidate.fullHeaderLine,
|
|
4877
|
-
body: candidate.body,
|
|
4878
|
-
paneOptions: candidate.options,
|
|
4879
|
-
paneSemanticKey: buildApprovalSemanticKey(candidate),
|
|
4880
|
-
kind: candidate.kind,
|
|
4881
|
-
emittedAt: Date.now(),
|
|
4882
|
-
ttlMs: this.promptTtlMs
|
|
4883
|
-
};
|
|
4884
|
-
this.pendingPrompts.set(promptId, state);
|
|
4885
|
-
logger.info("Emitting INTERACTIVE_PROMPT for matched candidate", {
|
|
4886
|
-
promptId,
|
|
4887
|
-
command: matched.command,
|
|
4888
|
-
filePath: matched.filePath,
|
|
4889
|
-
intentStepIndex: matched.intentStepIndex,
|
|
4890
|
-
toolCallIndex: matched.toolCallIndex
|
|
4891
|
-
});
|
|
4892
|
-
this.emit("pending-prompt", state);
|
|
4893
|
-
return state;
|
|
4894
4890
|
}
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
* answered). Resolves ALL outstanding pane-only pending prompts and
|
|
4902
|
-
* releases the paneOnly dedup hashes so a subsequent approval can
|
|
4903
|
-
* fresh-emit. Without this, mobile prompt state dangles until TTL
|
|
4904
|
-
* AND the next identical approval is forever-suppressed by stale
|
|
4905
|
-
* dedup. (Stage 2 R1 MED finding 2026-05-21.)
|
|
4906
|
-
*
|
|
4907
|
-
* `emittedBeforeMs` (F1, Stage 2 R1-F2): a desktop input that waited in the
|
|
4908
|
-
* routing hold proves only that prompts raised BEFORE it was typed are gone.
|
|
4909
|
-
* A prompt emitted after that moment stays pending, and so does its dedup
|
|
4910
|
-
* hash (clearing it would re-emit the still-visible chooser).
|
|
4911
|
-
*/
|
|
4912
|
-
onPanePromptCleared(emittedBeforeMs) {
|
|
4913
|
-
if (this.pendingPrompts.size === 0 && this.paneOnlyEmittedHashes.size === 0) {
|
|
4914
|
-
return [];
|
|
4915
|
-
}
|
|
4916
|
-
const now = Date.now();
|
|
4917
|
-
const cleared = [];
|
|
4918
|
-
let retained = false;
|
|
4919
|
-
for (const [promptId, state] of this.pendingPrompts.entries()) {
|
|
4920
|
-
if (emittedBeforeMs !== void 0 && state.emittedAt >= emittedBeforeMs) {
|
|
4921
|
-
retained = true;
|
|
4922
|
-
continue;
|
|
4891
|
+
if (Array.isArray(a.options) || typeof a.options === "string") {
|
|
4892
|
+
let topOptions = a.options;
|
|
4893
|
+
if (typeof topOptions === "string") {
|
|
4894
|
+
try {
|
|
4895
|
+
topOptions = JSON.parse(topOptions.trim());
|
|
4896
|
+
} catch {
|
|
4923
4897
|
}
|
|
4924
|
-
this.resolvedPrompts.set(promptId, now);
|
|
4925
|
-
cleared.push(promptId);
|
|
4926
4898
|
}
|
|
4927
|
-
if (
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
if (
|
|
4934
|
-
|
|
4899
|
+
if (Array.isArray(topOptions)) {
|
|
4900
|
+
const result = [];
|
|
4901
|
+
for (let i = 0; i < topOptions.length; i++) {
|
|
4902
|
+
const opt = topOptions[i];
|
|
4903
|
+
if (typeof opt === "string" && opt.trim().length > 0) {
|
|
4904
|
+
result.push({ number: String(i + 1), text: stripWrappingQuotes2(opt.trim()) });
|
|
4905
|
+
} else if (opt && typeof opt === "object" && typeof opt.text === "string") {
|
|
4906
|
+
result.push({
|
|
4907
|
+
number: String(opt.number ?? i + 1),
|
|
4908
|
+
text: stripWrappingQuotes2(String(opt.text).trim())
|
|
4909
|
+
});
|
|
4910
|
+
}
|
|
4935
4911
|
}
|
|
4912
|
+
if (result.length > 0) return result;
|
|
4936
4913
|
}
|
|
4937
|
-
if (cleared.length > 0) {
|
|
4938
|
-
logger.info("Pane prompt cleared \u2014 resolved pending prompts", {
|
|
4939
|
-
count: cleared.length
|
|
4940
|
-
});
|
|
4941
|
-
}
|
|
4942
|
-
return cleared;
|
|
4943
4914
|
}
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
return
|
|
4915
|
+
return void 0;
|
|
4916
|
+
}
|
|
4917
|
+
function decodeBoolean(val) {
|
|
4918
|
+
if (typeof val === "boolean") return val;
|
|
4919
|
+
if (typeof val === "string") {
|
|
4920
|
+
const s = val.trim().toLowerCase();
|
|
4921
|
+
return s === "true" || s === "1" || s === "yes";
|
|
4951
4922
|
}
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
if (!state) return null;
|
|
4967
|
-
this.pendingPrompts.delete(promptId);
|
|
4968
|
-
if (state.pendingCall.promptId === promptId) {
|
|
4969
|
-
state.pendingCall.promptId = void 0;
|
|
4923
|
+
if (typeof val === "number") return val !== 0;
|
|
4924
|
+
return false;
|
|
4925
|
+
}
|
|
4926
|
+
function extractAllQuestionsFromArgs(toolName, args) {
|
|
4927
|
+
if (!args || typeof args !== "object") return void 0;
|
|
4928
|
+
const tn = (toolName || "").toLowerCase().trim();
|
|
4929
|
+
const isQuestionTool = tn === "ask_question" || tn === "question" || tn === "ask";
|
|
4930
|
+
if (!isQuestionTool) return void 0;
|
|
4931
|
+
const a = args;
|
|
4932
|
+
let questions = a.questions;
|
|
4933
|
+
if (typeof questions === "string") {
|
|
4934
|
+
try {
|
|
4935
|
+
questions = JSON.parse(questions.trim());
|
|
4936
|
+
} catch {
|
|
4970
4937
|
}
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4938
|
+
}
|
|
4939
|
+
const items = [];
|
|
4940
|
+
if (Array.isArray(questions) && questions.length > 0) {
|
|
4941
|
+
for (const qItem of questions) {
|
|
4942
|
+
if (typeof qItem === "string" && qItem.trim().length > 0) {
|
|
4943
|
+
items.push({ question: stripWrappingQuotes2(qItem.trim()), options: [] });
|
|
4944
|
+
} else if (qItem && typeof qItem === "object") {
|
|
4945
|
+
const qObj = qItem;
|
|
4946
|
+
const qText = typeof qObj.question === "string" ? stripWrappingQuotes2(qObj.question.trim()) : "Question";
|
|
4947
|
+
const opts = [];
|
|
4948
|
+
if (Array.isArray(qObj.options)) {
|
|
4949
|
+
for (let i = 0; i < qObj.options.length; i++) {
|
|
4950
|
+
const opt = qObj.options[i];
|
|
4951
|
+
if (typeof opt === "string" && opt.trim().length > 0) {
|
|
4952
|
+
opts.push({ number: String(i + 1), text: stripWrappingQuotes2(opt.trim()) });
|
|
4953
|
+
} else if (opt && typeof opt === "object" && typeof opt.text === "string") {
|
|
4954
|
+
opts.push({
|
|
4955
|
+
number: String(opt.number ?? i + 1),
|
|
4956
|
+
text: stripWrappingQuotes2(String(opt.text).trim())
|
|
4957
|
+
});
|
|
4958
|
+
}
|
|
4959
|
+
}
|
|
4960
|
+
}
|
|
4961
|
+
const isMulti = decodeBoolean(qObj.is_multi_select ?? qObj.isMultiSelect);
|
|
4962
|
+
items.push({ question: qText, options: opts, isMultiSelect: isMulti });
|
|
4974
4963
|
}
|
|
4975
4964
|
}
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
* guard right before send-keys. */
|
|
4982
|
-
getPendingPrompt(promptId) {
|
|
4983
|
-
return this.pendingPrompts.get(promptId) ?? null;
|
|
4984
|
-
}
|
|
4985
|
-
getPendingPromptsForConversation(conversationId) {
|
|
4986
|
-
const pending = Array.from(this.pendingPrompts.values());
|
|
4987
|
-
return conversationId ? pending.filter((state) => state.conversationId === conversationId) : pending;
|
|
4988
|
-
}
|
|
4989
|
-
// ─── Read-only accessors ────────────────────────────────────────────────
|
|
4990
|
-
getPendingCalls() {
|
|
4991
|
-
return Array.from(this.pendingCalls.values());
|
|
4965
|
+
} else if (typeof a.question === "string" || typeof a.prompt === "string") {
|
|
4966
|
+
const qText = stripWrappingQuotes2(String(a.question ?? a.prompt).trim());
|
|
4967
|
+
const opts = extractQuestionOptionsFromArgs(toolName, args) ?? [];
|
|
4968
|
+
const isMulti = decodeBoolean(a.is_multi_select ?? a.isMultiSelect);
|
|
4969
|
+
items.push({ question: qText, options: opts, isMultiSelect: isMulti });
|
|
4992
4970
|
}
|
|
4993
|
-
|
|
4994
|
-
|
|
4971
|
+
return items.length > 0 ? items : void 0;
|
|
4972
|
+
}
|
|
4973
|
+
function extractCwdFromArgs(args) {
|
|
4974
|
+
if (!args || typeof args !== "object") return void 0;
|
|
4975
|
+
const a = args;
|
|
4976
|
+
for (const key of ["Cwd", "cwd", "workingDirectory", "WorkingDirectory"]) {
|
|
4977
|
+
const v = a[key];
|
|
4978
|
+
if (typeof v === "string" && v.trim().length > 0) {
|
|
4979
|
+
return stripWrappingQuotes2(v.trim());
|
|
4980
|
+
}
|
|
4995
4981
|
}
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4982
|
+
return void 0;
|
|
4983
|
+
}
|
|
4984
|
+
function stripWrappingQuotes2(s) {
|
|
4985
|
+
if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
|
|
4986
|
+
return s.slice(1, -1);
|
|
4999
4987
|
}
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
}
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
if (matches.length > 0) return matches[0];
|
|
5023
|
-
}
|
|
5024
|
-
if (candidate.filePath) {
|
|
5025
|
-
const matches = Array.from(this.pendingCalls.values()).filter((c) => c.conversationId === conversationId).filter((c) => c.filePath && c.filePath === candidate.filePath).sort((a, b) => a.intentStepIndex - b.intentStepIndex);
|
|
5026
|
-
if (matches.length > 0) return matches[0];
|
|
5027
|
-
}
|
|
5028
|
-
const allFields = (candidate.command ?? "") + " " + (candidate.filePath ?? "");
|
|
5029
|
-
if (allFields.trim()) {
|
|
5030
|
-
const matches = Array.from(this.pendingCalls.values()).filter((c) => c.conversationId === conversationId).filter((c) => {
|
|
5031
|
-
const cmd = (c.command ?? "").trim();
|
|
5032
|
-
const fp = (c.filePath ?? "").trim();
|
|
5033
|
-
if (!cmd && !fp) return false;
|
|
5034
|
-
const cmdOk = cmd.length >= PASS3_MIN_IDENTITY_LEN && containsAsWord(allFields, cmd);
|
|
5035
|
-
const fpOk = fp.length >= PASS3_MIN_IDENTITY_LEN && containsAsWord(allFields, fp);
|
|
5036
|
-
return cmdOk || fpOk;
|
|
5037
|
-
}).sort((a, b) => a.intentStepIndex - b.intentStepIndex);
|
|
5038
|
-
if (matches.length > 0) return matches[0];
|
|
5039
|
-
}
|
|
5040
|
-
return null;
|
|
4988
|
+
return s;
|
|
4989
|
+
}
|
|
4990
|
+
function formatToolResultContent(agyType, toolName, output, isError) {
|
|
4991
|
+
const prefix = isError ? "\u26A0 " : "";
|
|
4992
|
+
const firstLine = output.split("\n", 1)[0]?.trim();
|
|
4993
|
+
switch (agyType) {
|
|
4994
|
+
case "RUN_COMMAND":
|
|
4995
|
+
return `${prefix}Bash: ${firstLine || "command executed"}`;
|
|
4996
|
+
case "LIST_DIRECTORY":
|
|
4997
|
+
return `${prefix}LS: ${firstLine || "directory listed"}`;
|
|
4998
|
+
case "GREP_SEARCH":
|
|
4999
|
+
return `${prefix}Grep: ${firstLine || "search executed"}`;
|
|
5000
|
+
case "VIEW_FILE":
|
|
5001
|
+
return `${prefix}Read: ${firstLine || "file read"}`;
|
|
5002
|
+
case "CODE_ACTION":
|
|
5003
|
+
return `${prefix}Edit: ${firstLine || "file edited"}`;
|
|
5004
|
+
case "SEARCH_WEB":
|
|
5005
|
+
return `${prefix}WebSearch: ${firstLine || "search executed"}`;
|
|
5006
|
+
case "GENERIC":
|
|
5007
|
+
return `${prefix}${toolName}: ${firstLine || "tool executed"}`;
|
|
5008
|
+
default:
|
|
5009
|
+
return `${prefix}${toolName} (${agyType}): ${firstLine || "unknown event"}`;
|
|
5041
5010
|
}
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
command: call.command,
|
|
5055
|
-
filePath: call.filePath,
|
|
5056
|
-
ageMs: now - call.startedAt
|
|
5057
|
-
});
|
|
5058
|
-
this.emit("pending-call-expired", call);
|
|
5059
|
-
}
|
|
5060
|
-
}
|
|
5061
|
-
for (const [promptId, resolvedAt] of this.resolvedPrompts.entries()) {
|
|
5062
|
-
if (now - resolvedAt > 5 * 6e4) {
|
|
5063
|
-
this.resolvedPrompts.delete(promptId);
|
|
5064
|
-
}
|
|
5011
|
+
}
|
|
5012
|
+
function truncate(s, maxBytes) {
|
|
5013
|
+
if (!s) return "";
|
|
5014
|
+
if (Buffer.byteLength(s, "utf-8") <= maxBytes) return s;
|
|
5015
|
+
let lo = 0;
|
|
5016
|
+
let hi = s.length;
|
|
5017
|
+
while (lo < hi) {
|
|
5018
|
+
const mid = lo + hi + 1 >>> 1;
|
|
5019
|
+
if (Buffer.byteLength(s.slice(0, mid), "utf-8") <= maxBytes - 32) {
|
|
5020
|
+
lo = mid;
|
|
5021
|
+
} else {
|
|
5022
|
+
hi = mid - 1;
|
|
5065
5023
|
}
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5024
|
+
}
|
|
5025
|
+
return s.slice(0, lo) + ` [...truncated ${s.length - lo} chars]`;
|
|
5026
|
+
}
|
|
5027
|
+
|
|
5028
|
+
// src/approval-detector.ts
|
|
5029
|
+
var DEFAULT_PROMPT_TTL_MS = 6e4;
|
|
5030
|
+
var PENDING_CALL_TTL_MS = 5 * 6e4;
|
|
5031
|
+
var SWEEP_INTERVAL_MS = 3e4;
|
|
5032
|
+
var PASS3_MIN_IDENTITY_LEN = 4;
|
|
5033
|
+
var PENDING_CANDIDATE_BUFFER_MS = 5e3;
|
|
5034
|
+
function normalizeQuestionText(q) {
|
|
5035
|
+
if (!q) return "";
|
|
5036
|
+
return q.normalize("NFKC").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim();
|
|
5037
|
+
}
|
|
5038
|
+
function questionsMatch(hookText, paneText) {
|
|
5039
|
+
const h = normalizeQuestionText(hookText);
|
|
5040
|
+
const p = normalizeQuestionText(paneText);
|
|
5041
|
+
if (!h || !p) return false;
|
|
5042
|
+
return h === p;
|
|
5043
|
+
}
|
|
5044
|
+
function correlatesQuestionInvocation(call, delivery) {
|
|
5045
|
+
if (!call) return false;
|
|
5046
|
+
const tn = (delivery.toolName ?? "").toLowerCase().trim();
|
|
5047
|
+
if (!["ask_question", "question", "ask"].includes(tn)) {
|
|
5048
|
+
return false;
|
|
5049
|
+
}
|
|
5050
|
+
const hasPromptStep = call.intentStepIndex !== void 0 && call.intentStepIndex >= 0;
|
|
5051
|
+
const hasDeliveryStep = delivery.stepIdx !== void 0 && delivery.stepIdx >= 0;
|
|
5052
|
+
if (hasPromptStep && !hasDeliveryStep) {
|
|
5053
|
+
return false;
|
|
5054
|
+
}
|
|
5055
|
+
if (hasPromptStep && hasDeliveryStep) {
|
|
5056
|
+
const stepDiff = delivery.stepIdx - call.intentStepIndex;
|
|
5057
|
+
if (stepDiff < 0 || stepDiff > 1) return false;
|
|
5058
|
+
} else {
|
|
5059
|
+
if (!delivery.toolArgs) return false;
|
|
5060
|
+
}
|
|
5061
|
+
if (delivery.toolArgs) {
|
|
5062
|
+
const completedQ = extractQuestionFromArgs(delivery.toolName ?? "", delivery.toolArgs);
|
|
5063
|
+
const allCompleted = extractAllQuestionsFromArgs(delivery.toolName ?? "", delivery.toolArgs);
|
|
5064
|
+
if (completedQ || allCompleted && allCompleted.length > 0) {
|
|
5065
|
+
const matchesCurrent = completedQ ? questionsMatch(call.questionText, completedQ) : false;
|
|
5066
|
+
const matchesStructured = call.structuredQuestions?.some(
|
|
5067
|
+
(sq) => completedQ && questionsMatch(sq.question, completedQ) || allCompleted?.some((ac) => questionsMatch(sq.question, ac.question))
|
|
5068
|
+
) ?? false;
|
|
5069
|
+
const matchesAllCompleted = allCompleted?.some(
|
|
5070
|
+
(ac) => questionsMatch(call.questionText, ac.question)
|
|
5071
|
+
) ?? false;
|
|
5072
|
+
if (!matchesCurrent && !matchesStructured && !matchesAllCompleted) {
|
|
5073
|
+
return false;
|
|
5074
5074
|
}
|
|
5075
|
+
} else if (!hasPromptStep || !hasDeliveryStep) {
|
|
5076
|
+
return false;
|
|
5075
5077
|
}
|
|
5076
5078
|
}
|
|
5077
|
-
|
|
5078
|
-
function containsAsWord(haystack, needle) {
|
|
5079
|
-
const escaped = needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5080
|
-
return new RegExp(`(^|[\\s"'\`])${escaped}([\\s"'\`]|$)`).test(haystack);
|
|
5079
|
+
return true;
|
|
5081
5080
|
}
|
|
5082
|
-
|
|
5083
|
-
// src/prompt-responder.ts
|
|
5084
|
-
var import_child_process2 = require("child_process");
|
|
5085
|
-
var import_util2 = require("util");
|
|
5086
|
-
var execAsync2 = (0, import_util2.promisify)(import_child_process2.exec);
|
|
5087
|
-
var ENTER_DELAY_MS = 100;
|
|
5088
|
-
var CODEVIBE_AGY_ESCAPE_INPUT = "__CODEVIBE_AGY_KEY_ESCAPE__";
|
|
5089
|
-
var PromptResponder = class {
|
|
5081
|
+
var ApprovalDetector = class extends import_events3.EventEmitter {
|
|
5090
5082
|
constructor(options = {}) {
|
|
5091
|
-
|
|
5092
|
-
this.
|
|
5093
|
-
this.
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5083
|
+
super();
|
|
5084
|
+
this.pendingCalls = /* @__PURE__ */ new Map();
|
|
5085
|
+
this.pendingPrompts = /* @__PURE__ */ new Map();
|
|
5086
|
+
/** Track recently-resolved promptIds so a duplicate candidate-emit
|
|
5087
|
+
* doesn't re-fire INTERACTIVE_PROMPT. Maps promptId → ms-epoch when
|
|
5088
|
+
* resolved. Pruned by sweep. */
|
|
5089
|
+
this.resolvedPrompts = /* @__PURE__ */ new Map();
|
|
5090
|
+
/** Pre-transcript candidate buffer keyed by `${conversationId}|${paneHash}`
|
|
5091
|
+
* (composite key prevents collision when two conversations on the same
|
|
5092
|
+
* tmux pane produce identical paneHashes — agy R2 Stage 2 round-3 MED).
|
|
5093
|
+
* When a candidate arrives with no matching pending call, we park it
|
|
5094
|
+
* here for pendingCandidateBufferMs and re-try the match each time
|
|
5095
|
+
* registerPendingCall fires. */
|
|
5096
|
+
this.pendingCandidates = /* @__PURE__ */ new Map();
|
|
5097
|
+
/** Authoritative live wizard invocations keyed by conversationId (Codex S2-Codex-R6-F15).
|
|
5098
|
+
* Retains invocation identity and structured questions independently of transcript TTL sweeps. */
|
|
5099
|
+
this.liveWizardInvocations = /* @__PURE__ */ new Map();
|
|
5100
|
+
this.sweepTimer = null;
|
|
5101
|
+
// ─── Pane-only prompt emission (v9.1 — bypasses pending-call match) ─────
|
|
5102
|
+
/** Set of pane-snapshot hashes we've already emitted INTERACTIVE_PROMPT
|
|
5103
|
+
* for. Belt-and-suspenders dedupe against the case where paneObserver
|
|
5104
|
+
* somehow re-fires `prompt-candidate` with the same snapshot bytes
|
|
5105
|
+
* for the same prompt. The primary dedupe for cosmetic redraws of a
|
|
5106
|
+
* single approval (spinner ticks, cursor blink, partial repaints)
|
|
5107
|
+
* lives in tmux-pane-observer's debounce window — by the time a
|
|
5108
|
+
* candidate reaches this layer, the pane has settled. */
|
|
5109
|
+
this.paneOnlyEmittedHashes = /* @__PURE__ */ new Set();
|
|
5110
|
+
this.promptTtlMs = options.promptTtlMs ?? DEFAULT_PROMPT_TTL_MS;
|
|
5111
|
+
this.pendingCallTtlMs = options.pendingCallTtlMs ?? PENDING_CALL_TTL_MS;
|
|
5112
|
+
this.sweepIntervalMs = options.sweepIntervalMs ?? SWEEP_INTERVAL_MS;
|
|
5113
|
+
this.pendingCandidateBufferMs = options.pendingCandidateBufferMs ?? PENDING_CANDIDATE_BUFFER_MS;
|
|
5100
5114
|
}
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5115
|
+
start() {
|
|
5116
|
+
if (this.sweepTimer) return;
|
|
5117
|
+
this.sweepTimer = setInterval(() => this.sweep(), this.sweepIntervalMs);
|
|
5118
|
+
if (this.sweepTimer.unref) this.sweepTimer.unref();
|
|
5119
|
+
logger.info("Approval detector started", {
|
|
5120
|
+
promptTtlMs: this.promptTtlMs,
|
|
5121
|
+
pendingCallTtlMs: this.pendingCallTtlMs
|
|
5122
|
+
});
|
|
5123
|
+
}
|
|
5124
|
+
async stop() {
|
|
5125
|
+
if (this.sweepTimer) {
|
|
5126
|
+
clearInterval(this.sweepTimer);
|
|
5127
|
+
this.sweepTimer = null;
|
|
5110
5128
|
}
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5129
|
+
this.pendingCalls.clear();
|
|
5130
|
+
this.pendingPrompts.clear();
|
|
5131
|
+
this.resolvedPrompts.clear();
|
|
5132
|
+
this.pendingCandidates.clear();
|
|
5133
|
+
this.paneOnlyEmittedHashes.clear();
|
|
5134
|
+
this.liveWizardInvocations.clear();
|
|
5135
|
+
logger.info("Approval detector stopped");
|
|
5136
|
+
}
|
|
5137
|
+
/** Explicitly retire a live wizard invocation upon completion (PostToolUse / Stop). (Codex S2-Codex-R6-F15) */
|
|
5138
|
+
retireLiveWizard(conversationId) {
|
|
5139
|
+
const live = this.liveWizardInvocations.get(conversationId);
|
|
5140
|
+
if (live) {
|
|
5141
|
+
if (live.promptId) {
|
|
5142
|
+
this.resolvedPrompts.set(live.promptId, Date.now());
|
|
5118
5143
|
}
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5144
|
+
const callKey = this.callKey(live.call);
|
|
5145
|
+
const call = this.pendingCalls.get(callKey);
|
|
5146
|
+
if (call) {
|
|
5147
|
+
call.resultStepIndex = call.intentStepIndex;
|
|
5148
|
+
}
|
|
5149
|
+
this.liveWizardInvocations.delete(conversationId);
|
|
5150
|
+
}
|
|
5151
|
+
}
|
|
5152
|
+
/** Explicitly retire a live wizard invocation matching a completed tool delivery (PostToolUse) (Codex S2-Codex-R6-F15, Claude R8-F6).
|
|
5153
|
+
* Runs synchronously before any async backend awaits, using the shared invocation correlation predicate. */
|
|
5154
|
+
retireLiveWizardMatching(conversationId, delivery) {
|
|
5155
|
+
const live = this.liveWizardInvocations.get(conversationId);
|
|
5156
|
+
if (!live) return false;
|
|
5157
|
+
if (!correlatesQuestionInvocation(live.call, delivery)) {
|
|
5158
|
+
return false;
|
|
5159
|
+
}
|
|
5160
|
+
if (live.promptId) {
|
|
5161
|
+
this.resolvedPrompts.set(live.promptId, Date.now());
|
|
5162
|
+
}
|
|
5163
|
+
const callKey = this.callKey(live.call);
|
|
5164
|
+
const call = this.pendingCalls.get(callKey);
|
|
5165
|
+
if (call) {
|
|
5166
|
+
call.resultStepIndex = call.intentStepIndex;
|
|
5131
5167
|
}
|
|
5168
|
+
this.liveWizardInvocations.delete(conversationId);
|
|
5169
|
+
logger.info("Retired live wizard invocation matching completed tool", {
|
|
5170
|
+
conversationId,
|
|
5171
|
+
toolName: delivery.toolName,
|
|
5172
|
+
stepIdx: delivery.stepIdx,
|
|
5173
|
+
intentStepIndex: live.call.intentStepIndex
|
|
5174
|
+
});
|
|
5175
|
+
return true;
|
|
5132
5176
|
}
|
|
5133
5177
|
/**
|
|
5134
|
-
*
|
|
5135
|
-
*
|
|
5136
|
-
* translate option-number → terminal keystrokes (digit alone for agy's
|
|
5137
|
-
* 4-numbered UI; future Y/N prompts would use 'y'/'n'/'Escape').
|
|
5178
|
+
* Emit INTERACTIVE_PROMPT directly from a pane snapshot, WITHOUT
|
|
5179
|
+
* requiring a matching PendingToolCall from a transcript PLANNER_RESPONSE.
|
|
5138
5180
|
*
|
|
5139
|
-
*
|
|
5140
|
-
*
|
|
5141
|
-
*
|
|
5142
|
-
*
|
|
5143
|
-
*
|
|
5144
|
-
*
|
|
5145
|
-
*
|
|
5146
|
-
*
|
|
5147
|
-
*
|
|
5148
|
-
*
|
|
5149
|
-
*
|
|
5181
|
+
* Background (v9.1, 2026-05-21): agy does NOT write tool_call entries
|
|
5182
|
+
* to the transcript BEFORE rendering the approval UI for shell
|
|
5183
|
+
* commands. It only writes them AFTER the user approves. So the
|
|
5184
|
+
* transcript-based pending-call matching path deadlocks for shell
|
|
5185
|
+
* commands: paneObserver sees the UI, but no pending call exists →
|
|
5186
|
+
* candidate buffered → user can't approve from mobile (no prompt
|
|
5187
|
+
* shown) → no transcript write → buffer expires.
|
|
5188
|
+
*
|
|
5189
|
+
* Solution: pane is authoritative for "approval UI is showing right
|
|
5190
|
+
* now." Skip the pending-call requirement and emit directly. The
|
|
5191
|
+
* parsed candidate has everything needed (command, options, submitMap).
|
|
5192
|
+
*
|
|
5193
|
+
* Synthesizes a minimal PendingToolCall from the candidate so the
|
|
5194
|
+
* existing handlePendingPrompt code path doesn't need to branch on
|
|
5195
|
+
* `pendingCall: null`.
|
|
5196
|
+
*
|
|
5197
|
+
* Returns the emitted state, or null if dedupe (already emitted for
|
|
5198
|
+
* this paneHash) or disabled.
|
|
5150
5199
|
*/
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5200
|
+
emitPaneOnlyPrompt(candidate, conversationId, forcedPromptId) {
|
|
5201
|
+
if (this.paneOnlyEmittedHashes.has(candidate.paneHash)) {
|
|
5202
|
+
logger.debug("Skipping pane-only re-emit for duplicate paneHash", {
|
|
5203
|
+
paneHash: candidate.paneHash.substring(0, 16)
|
|
5204
|
+
});
|
|
5205
|
+
return null;
|
|
5155
5206
|
}
|
|
5156
|
-
if (
|
|
5157
|
-
const
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5207
|
+
if (candidate.kind === "question") {
|
|
5208
|
+
const paneQuestionText = candidate.headerText || candidate.fullHeaderLine;
|
|
5209
|
+
const candidateSemanticKey = buildApprovalSemanticKey(candidate);
|
|
5210
|
+
const liveWizard2 = this.liveWizardInvocations.get(conversationId);
|
|
5211
|
+
if (liveWizard2 && liveWizard2.call.structuredQuestions && liveWizard2.call.structuredQuestions.length > 0) {
|
|
5212
|
+
const sqs = liveWizard2.call.structuredQuestions;
|
|
5213
|
+
const currentActiveIdx = liveWizard2.activeStepIndex ?? 0;
|
|
5214
|
+
const scoreStep = (stepIdx) => {
|
|
5215
|
+
if (stepIdx < 0 || stepIdx >= sqs.length) return { score: -1, textMatch: false };
|
|
5216
|
+
const targetQ = sqs[stepIdx];
|
|
5217
|
+
const textMatch = questionsMatch(targetQ.question, paneQuestionText);
|
|
5218
|
+
if (!textMatch) return { score: -1, textMatch: false };
|
|
5219
|
+
const qOptTexts = (targetQ.options || []).map((o) => (typeof o === "string" ? o : o.text).toLowerCase().trim());
|
|
5220
|
+
const cOptTexts = (candidate.options || []).map((o) => (typeof o === "string" ? o : o.text).toLowerCase().trim());
|
|
5221
|
+
if (cOptTexts.length > 0 && qOptTexts.length > 0) {
|
|
5222
|
+
const hasTrailingWriteIn = cOptTexts.length === qOptTexts.length + 1 && /^\s*write-?in\b/i.test(cOptTexts[cOptTexts.length - 1]);
|
|
5223
|
+
const cEffectiveTexts = hasTrailingWriteIn ? cOptTexts.slice(0, -1) : cOptTexts;
|
|
5224
|
+
const matchedQCount = qOptTexts.filter((q) => cEffectiveTexts.includes(q)).length;
|
|
5225
|
+
const matchedCCount = cEffectiveTexts.filter((c) => qOptTexts.includes(c)).length;
|
|
5226
|
+
const qRatio = matchedQCount / qOptTexts.length;
|
|
5227
|
+
if (cEffectiveTexts.length !== qOptTexts.length || matchedQCount !== qOptTexts.length || matchedCCount !== cEffectiveTexts.length) {
|
|
5228
|
+
return { score: -1, textMatch: true };
|
|
5229
|
+
}
|
|
5230
|
+
const posMatches = qOptTexts.filter((q, idx) => idx < cEffectiveTexts.length && cEffectiveTexts[idx] === q).length;
|
|
5231
|
+
const posRatio = posMatches / qOptTexts.length;
|
|
5232
|
+
const score = qRatio * 0.5 + posRatio * 0.5;
|
|
5233
|
+
return { score, textMatch: true };
|
|
5234
|
+
}
|
|
5235
|
+
if (cOptTexts.length === 0 && qOptTexts.length === 0) {
|
|
5236
|
+
return { score: 1, textMatch: true };
|
|
5237
|
+
}
|
|
5238
|
+
return { score: -1, textMatch: true };
|
|
5164
5239
|
};
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
if (
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5240
|
+
const allScores = sqs.map((_, idx) => ({ stepIdx: idx, ...scoreStep(idx) })).filter((res) => res.score > 0);
|
|
5241
|
+
let bestStepIdx = -1;
|
|
5242
|
+
if (allScores.length > 0) {
|
|
5243
|
+
const maxScore = Math.max(...allScores.map((s) => s.score));
|
|
5244
|
+
const topSteps = allScores.filter((s) => Math.abs(s.score - maxScore) < 1e-3);
|
|
5245
|
+
if (topSteps.length > 1) {
|
|
5246
|
+
logger.warn("Ambiguous pane options match: multiple wizard steps tied with score", {
|
|
5247
|
+
maxScore,
|
|
5248
|
+
tiedSteps: topSteps.map((s) => s.stepIdx)
|
|
5249
|
+
});
|
|
5250
|
+
bestStepIdx = -1;
|
|
5251
|
+
} else {
|
|
5252
|
+
bestStepIdx = topSteps[0].stepIdx;
|
|
5253
|
+
}
|
|
5173
5254
|
}
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
}
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5255
|
+
if (bestStepIdx === -1) {
|
|
5256
|
+
logger.debug("Pane candidate did not match any step of active live wizard", {
|
|
5257
|
+
paneQuestionText,
|
|
5258
|
+
currentActiveIdx,
|
|
5259
|
+
totalSteps: sqs.length
|
|
5260
|
+
});
|
|
5261
|
+
return null;
|
|
5262
|
+
}
|
|
5263
|
+
const targetStepQ = sqs[bestStepIdx];
|
|
5264
|
+
if (targetStepQ?.isMultiSelect) {
|
|
5265
|
+
candidate.submitMap = {};
|
|
5266
|
+
}
|
|
5267
|
+
const isAdvancement = bestStepIdx !== currentActiveIdx;
|
|
5268
|
+
if (!isAdvancement) {
|
|
5269
|
+
const existing = [...this.pendingPrompts.values()].find(
|
|
5270
|
+
(p) => p.conversationId === conversationId && p.kind === "question"
|
|
5271
|
+
);
|
|
5272
|
+
if (existing) {
|
|
5273
|
+
const targetQ = existing.pendingCall?.structuredQuestions?.[existing.wizardStepIndex ?? 0];
|
|
5274
|
+
const isAuthoritativeMultiSelect = Boolean(targetQ?.isMultiSelect);
|
|
5275
|
+
const isInitialUpgrade = existing.isHookPrompt && !isAuthoritativeMultiSelect && Object.keys(existing.submitMap || {}).length === 0 && Object.keys(candidate.submitMap || {}).length > 0;
|
|
5276
|
+
if (isInitialUpgrade) {
|
|
5277
|
+
existing.submitMap = candidate.submitMap;
|
|
5278
|
+
if (candidate.options && candidate.options.length > 0) {
|
|
5279
|
+
existing.paneOptions = candidate.options;
|
|
5280
|
+
}
|
|
5281
|
+
existing.paneSemanticKey = candidateSemanticKey;
|
|
5282
|
+
existing.paneHash = candidate.paneHash;
|
|
5283
|
+
if (candidate.headerText) existing.matchedPaneHeader = candidate.headerText;
|
|
5284
|
+
if (candidate.fullHeaderLine) existing.paneDisplayHeader = candidate.fullHeaderLine;
|
|
5285
|
+
if (candidate.body) existing.body = candidate.body;
|
|
5286
|
+
if (candidate.paneHash) this.paneOnlyEmittedHashes.add(candidate.paneHash);
|
|
5287
|
+
logger.info("Upgraded hook-driven question prompt to actionable from pane observer", {
|
|
5288
|
+
promptId: existing.promptId,
|
|
5289
|
+
optionsCount: existing.paneOptions?.length ?? 0
|
|
5290
|
+
});
|
|
5291
|
+
this.emit("prompt-upgraded", existing);
|
|
5292
|
+
return existing;
|
|
5293
|
+
}
|
|
5294
|
+
const areControlsMutated = (existingOpts, candidateOpts) => {
|
|
5295
|
+
if (!existingOpts || !candidateOpts) return false;
|
|
5296
|
+
if (existingOpts.length !== candidateOpts.length) return true;
|
|
5297
|
+
return !existingOpts.every((o, idx) => o.number === candidateOpts[idx].number && (o.text || "").trim() === (candidateOpts[idx].text || "").trim());
|
|
5190
5298
|
};
|
|
5299
|
+
const isControlDelta = Boolean(
|
|
5300
|
+
existing.paneOptions && existing.paneOptions.length > 0 && candidate.options && candidate.options.length > 0 && areControlsMutated(existing.paneOptions, candidate.options)
|
|
5301
|
+
);
|
|
5302
|
+
if (isControlDelta) {
|
|
5303
|
+
logger.info("Control delta observed on active question step; resolving old prompt and re-emitting", {
|
|
5304
|
+
oldPromptId: existing.promptId,
|
|
5305
|
+
oldSemanticKey: existing.paneSemanticKey,
|
|
5306
|
+
newSemanticKey: candidateSemanticKey
|
|
5307
|
+
});
|
|
5308
|
+
this.resolvePrompt(existing.promptId, { advanceWizard: false });
|
|
5309
|
+
this.emit("prompt-resolved", existing.promptId);
|
|
5310
|
+
} else {
|
|
5311
|
+
if (!existing.paneSemanticKey) {
|
|
5312
|
+
existing.paneSemanticKey = candidateSemanticKey;
|
|
5313
|
+
}
|
|
5314
|
+
if (candidate.body) existing.body = candidate.body;
|
|
5315
|
+
if (candidate.paneHash) this.paneOnlyEmittedHashes.add(candidate.paneHash);
|
|
5316
|
+
return null;
|
|
5317
|
+
}
|
|
5191
5318
|
}
|
|
5192
|
-
|
|
5193
|
-
|
|
5319
|
+
} else {
|
|
5320
|
+
liveWizard2.activeStepIndex = bestStepIdx;
|
|
5321
|
+
for (const p of [...this.pendingPrompts.values()]) {
|
|
5322
|
+
if (p.conversationId === conversationId && p.kind === "question") {
|
|
5323
|
+
logger.info("Resolving older question prompt on wizard step progression", {
|
|
5324
|
+
oldPromptId: p.promptId,
|
|
5325
|
+
fromStep: currentActiveIdx,
|
|
5326
|
+
toStep: bestStepIdx
|
|
5327
|
+
});
|
|
5328
|
+
this.resolvePrompt(p.promptId, { advanceWizard: false });
|
|
5329
|
+
this.emit("prompt-resolved", p.promptId);
|
|
5330
|
+
}
|
|
5194
5331
|
}
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5332
|
+
}
|
|
5333
|
+
} else {
|
|
5334
|
+
for (const p of [...this.pendingPrompts.values()]) {
|
|
5335
|
+
if (p.conversationId === conversationId && p.kind === "question") {
|
|
5336
|
+
const hookQuestionText = p.pendingCall?.questionText || p.matchedPaneHeader || p.paneDisplayHeader;
|
|
5337
|
+
const isSameQuestion = questionsMatch(hookQuestionText, paneQuestionText);
|
|
5338
|
+
if (isSameQuestion) {
|
|
5339
|
+
const targetQ = p.pendingCall?.structuredQuestions?.[p.wizardStepIndex ?? 0];
|
|
5340
|
+
const isAuthoritativeMultiSelect = Boolean(targetQ?.isMultiSelect);
|
|
5341
|
+
const isInitialUpgrade = p.isHookPrompt && !isAuthoritativeMultiSelect && Object.keys(p.submitMap || {}).length === 0 && Object.keys(candidate.submitMap || {}).length > 0;
|
|
5342
|
+
const isControlDelta = !isInitialUpgrade && Boolean(
|
|
5343
|
+
p.paneSemanticKey && candidateSemanticKey !== p.paneSemanticKey
|
|
5344
|
+
);
|
|
5345
|
+
if (!isControlDelta) {
|
|
5346
|
+
if (isInitialUpgrade) {
|
|
5347
|
+
p.submitMap = candidate.submitMap;
|
|
5348
|
+
if (candidate.options && candidate.options.length > 0) {
|
|
5349
|
+
p.paneOptions = candidate.options;
|
|
5350
|
+
}
|
|
5351
|
+
p.paneSemanticKey = candidateSemanticKey;
|
|
5352
|
+
p.paneHash = candidate.paneHash;
|
|
5353
|
+
if (candidate.headerText) p.matchedPaneHeader = candidate.headerText;
|
|
5354
|
+
if (candidate.fullHeaderLine) p.paneDisplayHeader = candidate.fullHeaderLine;
|
|
5355
|
+
if (candidate.body) p.body = candidate.body;
|
|
5356
|
+
if (candidate.paneHash) this.paneOnlyEmittedHashes.add(candidate.paneHash);
|
|
5357
|
+
logger.info("Upgraded hook-driven question prompt to actionable from pane observer", {
|
|
5358
|
+
promptId: p.promptId,
|
|
5359
|
+
optionsCount: p.paneOptions?.length ?? 0
|
|
5360
|
+
});
|
|
5361
|
+
this.emit("prompt-upgraded", p);
|
|
5362
|
+
return p;
|
|
5363
|
+
}
|
|
5364
|
+
if (!p.paneSemanticKey) {
|
|
5365
|
+
p.paneSemanticKey = candidateSemanticKey;
|
|
5366
|
+
}
|
|
5367
|
+
if (candidate.paneHash) this.paneOnlyEmittedHashes.add(candidate.paneHash);
|
|
5368
|
+
return null;
|
|
5207
5369
|
}
|
|
5208
|
-
return {
|
|
5209
|
-
ok: false,
|
|
5210
|
-
reason: "prompt-probe-failed",
|
|
5211
|
-
details: "the active chooser could not be parsed exactly"
|
|
5212
|
-
};
|
|
5213
5370
|
}
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
return {
|
|
5217
|
-
ok: false,
|
|
5218
|
-
reason: "prompt-probe-failed",
|
|
5219
|
-
details: error instanceof Error ? error.message : String(error)
|
|
5220
|
-
};
|
|
5221
|
-
}
|
|
5222
|
-
if (liveSemanticKey !== state.paneSemanticKey) {
|
|
5223
|
-
this.detector.resolvePrompt(promptId);
|
|
5224
|
-
return {
|
|
5225
|
-
ok: false,
|
|
5226
|
-
reason: "prompt-superseded",
|
|
5227
|
-
details: "active chooser semantics differ from the mobile prompt"
|
|
5228
|
-
};
|
|
5371
|
+
this.resolvePrompt(p.promptId);
|
|
5372
|
+
this.emit("prompt-resolved", p.promptId);
|
|
5229
5373
|
}
|
|
5230
|
-
|
|
5231
|
-
|
|
5374
|
+
}
|
|
5375
|
+
}
|
|
5376
|
+
}
|
|
5377
|
+
const promptId = forcedPromptId ?? (0, import_uuid2.v4)();
|
|
5378
|
+
const liveWizard = candidate.kind === "question" ? this.liveWizardInvocations.get(conversationId) : void 0;
|
|
5379
|
+
const matchingCall = liveWizard ? liveWizard.call : candidate.kind === "question" ? this.findMatchingPendingCall(candidate, conversationId) : null;
|
|
5380
|
+
if (liveWizard) {
|
|
5381
|
+
liveWizard.promptId = promptId;
|
|
5382
|
+
} else if (matchingCall) {
|
|
5383
|
+
matchingCall.matchedPromptId = promptId;
|
|
5384
|
+
}
|
|
5385
|
+
const syntheticCall = {
|
|
5386
|
+
conversationId,
|
|
5387
|
+
intentStepIndex: matchingCall?.intentStepIndex ?? -1,
|
|
5388
|
+
// sentinel: "no transcript origin"
|
|
5389
|
+
toolCallIndex: matchingCall?.toolCallIndex ?? 0,
|
|
5390
|
+
intentByteOffset: matchingCall?.intentByteOffset ?? 0,
|
|
5391
|
+
// Best-effort tool-type inference from the candidate. For shell
|
|
5392
|
+
// commands → RUN_COMMAND. For edit-style headers with a file
|
|
5393
|
+
// path → CODE_ACTION. For questions → ASK_QUESTION. Default: GENERIC.
|
|
5394
|
+
toolType: matchingCall?.toolType ?? guessToolTypeFromIdentity(candidate),
|
|
5395
|
+
command: matchingCall?.command ?? candidate.command,
|
|
5396
|
+
filePath: matchingCall?.filePath ?? candidate.filePath,
|
|
5397
|
+
rawToolName: matchingCall?.rawToolName ?? (candidate.kind === "question" ? "ask_question" : candidate.command ?? candidate.filePath ?? "unknown"),
|
|
5398
|
+
questionText: candidate.kind === "question" && candidate.headerText ? candidate.headerText : matchingCall?.questionText,
|
|
5399
|
+
// NOTE (Codex R3-F1 / Claude R3-F2): Pane options are authoritative ground truth
|
|
5400
|
+
// that directly correspond to desktop keystrokes (submitMap). Transcript options
|
|
5401
|
+
// serve solely as fallback when pane options cannot be parsed.
|
|
5402
|
+
questionOptions: candidate.kind === "question" && candidate.options && candidate.options.length > 0 ? candidate.options : matchingCall?.questionOptions,
|
|
5403
|
+
structuredQuestions: matchingCall?.structuredQuestions,
|
|
5404
|
+
promptId,
|
|
5405
|
+
startedAt: matchingCall?.startedAt ?? Date.now(),
|
|
5406
|
+
intentCreatedAt: matchingCall?.intentCreatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
5407
|
+
};
|
|
5408
|
+
const isHookCall = Boolean(
|
|
5409
|
+
matchingCall?.structuredQuestions && matchingCall.structuredQuestions.length > 0
|
|
5410
|
+
);
|
|
5411
|
+
const state = {
|
|
5412
|
+
promptId,
|
|
5413
|
+
conversationId,
|
|
5414
|
+
pendingCall: syntheticCall,
|
|
5415
|
+
submitMap: candidate.submitMap,
|
|
5416
|
+
matchedPaneHeader: candidate.headerText,
|
|
5417
|
+
paneDisplayHeader: candidate.fullHeaderLine,
|
|
5418
|
+
body: candidate.body,
|
|
5419
|
+
paneOptions: candidate.options,
|
|
5420
|
+
paneSemanticKey: buildApprovalSemanticKey(candidate),
|
|
5421
|
+
kind: candidate.kind,
|
|
5422
|
+
isHookPrompt: isHookCall,
|
|
5423
|
+
// Carry hook ownership forward so pane redraws do not drop it! (Codex S2-Codex-R6-F15)
|
|
5424
|
+
wizardStepIndex: liveWizard ? liveWizard.activeStepIndex : void 0,
|
|
5425
|
+
emittedAt: Date.now(),
|
|
5426
|
+
ttlMs: this.promptTtlMs,
|
|
5427
|
+
// E1 (§6a-2b / F5) — carry the consumed pane hash so an emit-failure
|
|
5428
|
+
// rollback can reset the observer's lastPromptHash and let the unchanged
|
|
5429
|
+
// live pane re-emit.
|
|
5430
|
+
paneHash: candidate.paneHash
|
|
5431
|
+
};
|
|
5432
|
+
this.pendingPrompts.set(promptId, state);
|
|
5433
|
+
this.paneOnlyEmittedHashes.add(candidate.paneHash);
|
|
5434
|
+
logger.info("Emitting pane-only INTERACTIVE_PROMPT", {
|
|
5435
|
+
promptId,
|
|
5436
|
+
command: candidate.command,
|
|
5437
|
+
filePath: candidate.filePath,
|
|
5438
|
+
optionsCount: candidate.options.length
|
|
5439
|
+
});
|
|
5440
|
+
this.emit("pending-prompt", state);
|
|
5441
|
+
return state;
|
|
5442
|
+
}
|
|
5443
|
+
/**
|
|
5444
|
+
* Emit an INTERACTIVE_PROMPT for a question tool call directly from the PreToolUse hook.
|
|
5445
|
+
* This guarantees prompt delivery to mobile without relying on fragile TUI terminal screen-scraping.
|
|
5446
|
+
*/
|
|
5447
|
+
emitHookQuestionPrompt(call, forcedPromptId) {
|
|
5448
|
+
const questionText = call.questionText ?? "Question";
|
|
5449
|
+
if (!call.questionOptions && call.structuredQuestions && call.structuredQuestions[0]?.options) {
|
|
5450
|
+
call.questionOptions = call.structuredQuestions[0].options;
|
|
5451
|
+
}
|
|
5452
|
+
for (const [id, p] of this.pendingPrompts.entries()) {
|
|
5453
|
+
if (p.conversationId === call.conversationId && p.kind === "question") {
|
|
5454
|
+
const existingText = p.pendingCall?.questionText || p.matchedPaneHeader || p.paneDisplayHeader;
|
|
5455
|
+
const isSameInvocation = !p.pendingCall || p.pendingCall.intentStepIndex === -1 || call.intentStepIndex === -1 || p.pendingCall.intentStepIndex === call.intentStepIndex;
|
|
5456
|
+
if (isSameInvocation && questionsMatch(existingText, questionText)) {
|
|
5457
|
+
if (p.pendingCall) {
|
|
5458
|
+
p.pendingCall.structuredQuestions = call.structuredQuestions;
|
|
5459
|
+
if (call.intentStepIndex !== -1) {
|
|
5460
|
+
p.pendingCall.intentStepIndex = call.intentStepIndex;
|
|
5461
|
+
}
|
|
5462
|
+
p.pendingCall.rawToolName = call.rawToolName;
|
|
5463
|
+
p.pendingCall.toolType = call.toolType;
|
|
5232
5464
|
} else {
|
|
5233
|
-
|
|
5465
|
+
p.pendingCall = call;
|
|
5466
|
+
}
|
|
5467
|
+
const isHookMultiSelect = Boolean(
|
|
5468
|
+
call.isMultiSelect || call.structuredQuestions && call.structuredQuestions[0]?.isMultiSelect
|
|
5469
|
+
);
|
|
5470
|
+
if (isHookMultiSelect && Object.keys(p.submitMap || {}).length > 0) {
|
|
5471
|
+
p.submitMap = {};
|
|
5472
|
+
logger.info("emitHookQuestionPrompt: revoked actionable submitMap because incoming hook is multi-select", {
|
|
5473
|
+
promptId: p.promptId
|
|
5474
|
+
});
|
|
5475
|
+
this.emit("prompt-upgraded", p);
|
|
5476
|
+
}
|
|
5477
|
+
p.isHookPrompt = true;
|
|
5478
|
+
call.matchedPromptId = p.promptId;
|
|
5479
|
+
if (call.structuredQuestions && call.structuredQuestions.length > 0) {
|
|
5480
|
+
this.liveWizardInvocations.set(call.conversationId, {
|
|
5481
|
+
call,
|
|
5482
|
+
activeStepIndex: 0,
|
|
5483
|
+
promptId: p.promptId,
|
|
5484
|
+
registeredAt: Date.now()
|
|
5485
|
+
});
|
|
5234
5486
|
}
|
|
5487
|
+
logger.info("emitHookQuestionPrompt: reusing existing matching prompt", {
|
|
5488
|
+
promptId: p.promptId,
|
|
5489
|
+
isActionable: Object.keys(p.submitMap || {}).length > 0
|
|
5490
|
+
});
|
|
5491
|
+
return p;
|
|
5235
5492
|
}
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5493
|
+
logger.info("Replacing older question prompt with new hook question", {
|
|
5494
|
+
oldPromptId: id,
|
|
5495
|
+
oldStep: p.pendingCall?.intentStepIndex,
|
|
5496
|
+
newStep: call.intentStepIndex,
|
|
5497
|
+
conversationId: call.conversationId
|
|
5241
5498
|
});
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
return {
|
|
5245
|
-
ok: false,
|
|
5246
|
-
reason: "tmux-failed",
|
|
5247
|
-
details: error instanceof Error ? error.message : String(error)
|
|
5248
|
-
};
|
|
5499
|
+
this.resolvePrompt(id);
|
|
5500
|
+
this.emit("prompt-resolved", id);
|
|
5249
5501
|
}
|
|
5250
5502
|
}
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5503
|
+
const promptId = forcedPromptId ?? (0, import_uuid2.v4)();
|
|
5504
|
+
this.registerPendingCall(call);
|
|
5505
|
+
call.matchedPromptId = promptId;
|
|
5506
|
+
const state = {
|
|
5507
|
+
promptId,
|
|
5508
|
+
conversationId: call.conversationId,
|
|
5509
|
+
pendingCall: call,
|
|
5510
|
+
submitMap: {},
|
|
5511
|
+
matchedPaneHeader: call.questionText ?? "Question",
|
|
5512
|
+
paneDisplayHeader: call.questionText ?? "Question",
|
|
5513
|
+
body: void 0,
|
|
5514
|
+
paneOptions: call.questionOptions,
|
|
5515
|
+
paneSemanticKey: buildApprovalSemanticKey({
|
|
5516
|
+
kind: "question",
|
|
5517
|
+
headerText: call.questionText ?? "Question",
|
|
5518
|
+
fullHeaderLine: call.questionText ?? "Question",
|
|
5519
|
+
options: call.questionOptions ?? [],
|
|
5520
|
+
submitMap: {},
|
|
5521
|
+
body: "",
|
|
5522
|
+
paneHash: ""
|
|
5523
|
+
}),
|
|
5524
|
+
kind: "question",
|
|
5525
|
+
isHookPrompt: true,
|
|
5526
|
+
wizardStepIndex: 0,
|
|
5527
|
+
emittedAt: Date.now(),
|
|
5528
|
+
ttlMs: this.promptTtlMs
|
|
5529
|
+
};
|
|
5530
|
+
if (call.structuredQuestions && call.structuredQuestions.length > 0) {
|
|
5531
|
+
this.liveWizardInvocations.set(call.conversationId, {
|
|
5532
|
+
call,
|
|
5533
|
+
activeStepIndex: 0,
|
|
5534
|
+
promptId,
|
|
5535
|
+
registeredAt: Date.now()
|
|
5536
|
+
});
|
|
5260
5537
|
}
|
|
5538
|
+
this.pendingPrompts.set(promptId, state);
|
|
5539
|
+
logger.info("Emitting hook-driven INTERACTIVE_PROMPT for question", {
|
|
5540
|
+
promptId,
|
|
5541
|
+
conversationId: call.conversationId,
|
|
5542
|
+
questionText: call.questionText,
|
|
5543
|
+
optionsCount: call.questionOptions?.length ?? 0
|
|
5544
|
+
});
|
|
5545
|
+
this.emit("pending-prompt", state);
|
|
5546
|
+
return state;
|
|
5261
5547
|
}
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5548
|
+
// ─── Pending-call registration (called from event-mapper hook) ──────────
|
|
5549
|
+
/**
|
|
5550
|
+
* Register a tool-call intent from a PLANNER_RESPONSE.tool_calls[].
|
|
5551
|
+
* Indexed by composite key so agy rollback (which can replay
|
|
5552
|
+
* step_index values) doesn't cause collisions.
|
|
5553
|
+
*/
|
|
5554
|
+
registerPendingCall(call) {
|
|
5555
|
+
const key = this.callKey(call);
|
|
5556
|
+
if (this.pendingCalls.has(key)) {
|
|
5557
|
+
logger.debug("Pending call already registered, skipping", {
|
|
5558
|
+
key,
|
|
5559
|
+
intentStepIndex: call.intentStepIndex,
|
|
5560
|
+
toolType: call.toolType
|
|
5561
|
+
});
|
|
5562
|
+
return;
|
|
5271
5563
|
}
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5564
|
+
this.pendingCalls.set(key, call);
|
|
5565
|
+
logger.debug("Pending tool call registered", {
|
|
5566
|
+
key,
|
|
5567
|
+
toolType: call.toolType,
|
|
5568
|
+
command: call.command,
|
|
5569
|
+
filePath: call.filePath
|
|
5570
|
+
});
|
|
5571
|
+
this.drainBufferedCandidates(call.conversationId);
|
|
5572
|
+
}
|
|
5573
|
+
drainBufferedCandidates(conversationId) {
|
|
5574
|
+
if (this.pendingCandidates.size === 0) return;
|
|
5575
|
+
const now = Date.now();
|
|
5576
|
+
for (const [key, buffered] of this.pendingCandidates.entries()) {
|
|
5577
|
+
if (buffered.conversationId !== conversationId) continue;
|
|
5578
|
+
if (now - buffered.queuedAt > this.pendingCandidateBufferMs) {
|
|
5579
|
+
this.pendingCandidates.delete(key);
|
|
5580
|
+
continue;
|
|
5279
5581
|
}
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
try {
|
|
5288
|
-
await beforeType();
|
|
5289
|
-
} catch (error) {
|
|
5290
|
-
preparationFailed = true;
|
|
5291
|
-
throw error;
|
|
5292
|
-
}
|
|
5293
|
-
const afterPreparation = await chooserIsAbsent();
|
|
5294
|
-
if (afterPreparation) return afterPreparation;
|
|
5582
|
+
const state = this.matchCandidateInternal(buffered.candidate, buffered.conversationId);
|
|
5583
|
+
if (state) {
|
|
5584
|
+
this.pendingCandidates.delete(key);
|
|
5585
|
+
logger.info("Drained buffered candidate after pending-call arrived", {
|
|
5586
|
+
key,
|
|
5587
|
+
ageMs: now - buffered.queuedAt
|
|
5588
|
+
});
|
|
5295
5589
|
}
|
|
5296
|
-
await this.typeLiteral(target, text);
|
|
5297
|
-
await delay(ENTER_DELAY_MS);
|
|
5298
|
-
const beforeEnter = await chooserIsAbsent();
|
|
5299
|
-
if (beforeEnter) return beforeEnter;
|
|
5300
|
-
await this.sendKey(target, "Enter");
|
|
5301
|
-
return { ok: true };
|
|
5302
|
-
} catch (error) {
|
|
5303
|
-
if (preparationFailed) throw error;
|
|
5304
|
-
return {
|
|
5305
|
-
ok: false,
|
|
5306
|
-
reason: "tmux-failed",
|
|
5307
|
-
details: error instanceof Error ? error.message : String(error)
|
|
5308
|
-
};
|
|
5309
|
-
}
|
|
5310
|
-
}
|
|
5311
|
-
/** Dismiss whichever approval UI is active without binding to a stale promptId. */
|
|
5312
|
-
async dismissActivePrompt() {
|
|
5313
|
-
const target = this.resolveTmuxTarget();
|
|
5314
|
-
if (!target) return { ok: false, reason: "no-tmux-target" };
|
|
5315
|
-
if (!this.paneObserver) {
|
|
5316
|
-
return { ok: false, reason: "prompt-probe-failed" };
|
|
5317
|
-
}
|
|
5318
|
-
try {
|
|
5319
|
-
const before = await this.paneObserver.probeApprovalUIActive();
|
|
5320
|
-
if (!before.probeSucceeded) return { ok: false, reason: "prompt-probe-failed" };
|
|
5321
|
-
if (!before.active) return { ok: true };
|
|
5322
|
-
if (before.ambiguous) {
|
|
5323
|
-
return {
|
|
5324
|
-
ok: false,
|
|
5325
|
-
reason: "ambiguous-prompt-active",
|
|
5326
|
-
details: "masked/ambiguous prompt cannot be dismissed via Escape from mobile"
|
|
5327
|
-
};
|
|
5328
|
-
}
|
|
5329
|
-
if (before.kind === "question") {
|
|
5330
|
-
let isActionableWizard = false;
|
|
5331
|
-
try {
|
|
5332
|
-
const snapshot = await this.paneObserver.captureSnapshot();
|
|
5333
|
-
if (snapshot) {
|
|
5334
|
-
const parsed = parseApprovalSnapshot(snapshot);
|
|
5335
|
-
if (parsed && parsed.kind === "question" && Object.keys(parsed.submitMap || {}).length > 0) {
|
|
5336
|
-
isActionableWizard = true;
|
|
5337
|
-
}
|
|
5338
|
-
}
|
|
5339
|
-
} catch {
|
|
5340
|
-
}
|
|
5341
|
-
if (!isActionableWizard) {
|
|
5342
|
-
return {
|
|
5343
|
-
ok: false,
|
|
5344
|
-
reason: "display-only-question-active",
|
|
5345
|
-
details: "active question prompt cannot be dismissed via Escape from mobile"
|
|
5346
|
-
};
|
|
5347
|
-
}
|
|
5348
|
-
}
|
|
5349
|
-
await this.sendKey(target, "Escape");
|
|
5350
|
-
await delay(250);
|
|
5351
|
-
const after = await this.paneObserver.probeApprovalUIActive();
|
|
5352
|
-
if (!after.probeSucceeded) return { ok: false, reason: "prompt-probe-failed" };
|
|
5353
|
-
if (after.active) {
|
|
5354
|
-
return {
|
|
5355
|
-
ok: false,
|
|
5356
|
-
reason: "prompt-superseded",
|
|
5357
|
-
details: "approval chooser remained active after Escape"
|
|
5358
|
-
};
|
|
5359
|
-
}
|
|
5360
|
-
return { ok: true };
|
|
5361
|
-
} catch (error) {
|
|
5362
|
-
return {
|
|
5363
|
-
ok: false,
|
|
5364
|
-
reason: "tmux-failed",
|
|
5365
|
-
details: error instanceof Error ? error.message : String(error)
|
|
5366
|
-
};
|
|
5367
5590
|
}
|
|
5368
5591
|
}
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
const cmd = `tmux send-keys -t '${safeTarget}' ${key}`;
|
|
5379
|
-
await this.execFn(cmd);
|
|
5380
|
-
}
|
|
5381
|
-
resolveTmuxTarget() {
|
|
5382
|
-
return this.tmuxTargetOverride ?? process.env.CODEVIBE_AGY_TMUX_TARGET ?? null;
|
|
5383
|
-
}
|
|
5384
|
-
};
|
|
5385
|
-
function escapeShellArg2(value) {
|
|
5386
|
-
return value.replace(/'/g, `'\\''`);
|
|
5387
|
-
}
|
|
5388
|
-
function isNamedKey(key) {
|
|
5389
|
-
return /^[A-Z][A-Za-z0-9]+$/.test(key);
|
|
5390
|
-
}
|
|
5391
|
-
function delay(ms) {
|
|
5392
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
5393
|
-
}
|
|
5394
|
-
|
|
5395
|
-
// src/mobile-prompt-dedupe.ts
|
|
5396
|
-
var DEFAULT_EXPIRY_MS = 15e3;
|
|
5397
|
-
var DEFAULT_MIN_FUZZY_ECHO_LENGTH = 16;
|
|
5398
|
-
var DEFAULT_MIN_FUZZY_ECHO_RATIO = 0.35;
|
|
5399
|
-
var MobilePromptDeduper = class {
|
|
5400
|
-
constructor(options = {}) {
|
|
5401
|
-
this.pendingBySession = /* @__PURE__ */ new Map();
|
|
5402
|
-
this.expiryMs = options.expiryMs ?? DEFAULT_EXPIRY_MS;
|
|
5403
|
-
this.minFuzzyEchoLength = options.minFuzzyEchoLength ?? DEFAULT_MIN_FUZZY_ECHO_LENGTH;
|
|
5404
|
-
this.minFuzzyEchoRatio = options.minFuzzyEchoRatio ?? DEFAULT_MIN_FUZZY_ECHO_RATIO;
|
|
5405
|
-
this.now = options.now ?? Date.now;
|
|
5406
|
-
}
|
|
5407
|
-
/** Called by the MCP server immediately BEFORE prompt-responder
|
|
5408
|
-
* performs the tmux send-keys. Records the normalized text + timestamp
|
|
5409
|
-
* for future echo-suppression. */
|
|
5410
|
-
track(sessionId, prompt) {
|
|
5411
|
-
this.sweep();
|
|
5412
|
-
const normalized = this.normalize(prompt);
|
|
5413
|
-
if (!normalized) return;
|
|
5414
|
-
const entries = this.validEntries(sessionId);
|
|
5415
|
-
entries.push({ normalized, timestamp: this.now() });
|
|
5416
|
-
this.pendingBySession.set(sessionId, entries);
|
|
5417
|
-
}
|
|
5418
|
-
/** Symmetric helper for callers that want to retract a tracked entry
|
|
5419
|
-
* WITHOUT consuming it via dedupe (e.g. responder bailed before send). */
|
|
5420
|
-
forget(sessionId, prompt) {
|
|
5421
|
-
this.sweep();
|
|
5422
|
-
const normalized = this.normalize(prompt);
|
|
5423
|
-
if (!normalized) return;
|
|
5424
|
-
let removed = false;
|
|
5425
|
-
const entries = this.validEntries(sessionId).filter((entry) => {
|
|
5426
|
-
if (!removed && entry.normalized === normalized) {
|
|
5427
|
-
removed = true;
|
|
5428
|
-
return false;
|
|
5429
|
-
}
|
|
5430
|
-
return true;
|
|
5431
|
-
});
|
|
5432
|
-
this.replaceEntries(sessionId, entries);
|
|
5592
|
+
/** Composite key for the pre-transcript buffer. Scoping by
|
|
5593
|
+
* conversationId prevents the collision case where two simultaneous
|
|
5594
|
+
* conversations on the same tmux pane (contract-discouraged but not
|
|
5595
|
+
* contract-impossible) produce identical paneHashes — without
|
|
5596
|
+
* conversation scoping, conv-B's candidate would be silently dropped
|
|
5597
|
+
* at the same-key dedupe check. (agy R2 Stage 2 round-3 MED finding
|
|
5598
|
+
* 2026-05-20.) */
|
|
5599
|
+
candidateKey(conversationId, paneHash) {
|
|
5600
|
+
return `${conversationId}|${paneHash}`;
|
|
5433
5601
|
}
|
|
5434
5602
|
/**
|
|
5435
|
-
*
|
|
5436
|
-
*
|
|
5437
|
-
*
|
|
5438
|
-
*
|
|
5439
|
-
*
|
|
5603
|
+
* Clear a pending call when its matching RESULT event arrives in the
|
|
5604
|
+
* transcript. event-mapper.ts calls this from its tool-result handlers.
|
|
5605
|
+
*
|
|
5606
|
+
* The result event's step_index = intentStepIndex + toolCallIndex + 1
|
|
5607
|
+
* per agy R2 v2 (the deterministic correlation formula). We try to
|
|
5608
|
+
* match by (conversationId, expected step_index, toolType) — when one
|
|
5609
|
+
* matches, clear it. If multiple pending calls match (rollback edge),
|
|
5610
|
+
* clear the earliest by intentStepIndex.
|
|
5440
5611
|
*/
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
};
|
|
5455
|
-
return false;
|
|
5612
|
+
clearOnResultEvent(args) {
|
|
5613
|
+
let matched = null;
|
|
5614
|
+
let matchedKey = null;
|
|
5615
|
+
for (const [key, call] of this.pendingCalls.entries()) {
|
|
5616
|
+
if (call.conversationId !== args.conversationId) continue;
|
|
5617
|
+
if (args.toolType !== "GENERIC" && call.toolType !== args.toolType) continue;
|
|
5618
|
+
const expected = call.intentStepIndex + call.toolCallIndex + 1;
|
|
5619
|
+
if (expected !== args.resultStepIndex) continue;
|
|
5620
|
+
if (typeof args.resultByteOffset === "number") {
|
|
5621
|
+
if (call.intentByteOffset >= args.resultByteOffset) continue;
|
|
5622
|
+
if (!matched || call.intentByteOffset > matched.intentByteOffset) {
|
|
5623
|
+
matched = call;
|
|
5624
|
+
matchedKey = key;
|
|
5456
5625
|
}
|
|
5626
|
+
} else if (!matched || call.intentStepIndex < matched.intentStepIndex) {
|
|
5627
|
+
matched = call;
|
|
5628
|
+
matchedKey = key;
|
|
5457
5629
|
}
|
|
5458
|
-
return true;
|
|
5459
|
-
});
|
|
5460
|
-
this.replaceEntries(sessionId, entries);
|
|
5461
|
-
return match;
|
|
5462
|
-
}
|
|
5463
|
-
/** Inspect without spending the token before durable native admission. */
|
|
5464
|
-
peekIfDuplicate(sessionId, echo) {
|
|
5465
|
-
const normalized = this.normalize(echo);
|
|
5466
|
-
if (!normalized) return null;
|
|
5467
|
-
for (const entry of this.validEntries(sessionId)) {
|
|
5468
|
-
const matchType = this.matchType(entry.normalized, normalized);
|
|
5469
|
-
if (matchType) return { matchType, originalLength: entry.normalized.length, echoLength: normalized.length };
|
|
5470
5630
|
}
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
const valid = entries.filter((entry) => entry.timestamp >= cutoff);
|
|
5478
|
-
if (valid.length > 0) {
|
|
5479
|
-
this.pendingBySession.set(sessId, valid);
|
|
5480
|
-
} else {
|
|
5481
|
-
this.pendingBySession.delete(sessId);
|
|
5631
|
+
if (matched && matchedKey) {
|
|
5632
|
+
matched.resultStepIndex = args.resultStepIndex;
|
|
5633
|
+
this.pendingCalls.delete(matchedKey);
|
|
5634
|
+
if (matched.promptId) {
|
|
5635
|
+
this.resolvedPrompts.set(matched.promptId, Date.now());
|
|
5636
|
+
this.pendingPrompts.delete(matched.promptId);
|
|
5482
5637
|
}
|
|
5638
|
+
logger.debug("Pending tool call cleared on result event", {
|
|
5639
|
+
intentStepIndex: matched.intentStepIndex,
|
|
5640
|
+
toolCallIndex: matched.toolCallIndex,
|
|
5641
|
+
resultStepIndex: args.resultStepIndex,
|
|
5642
|
+
toolType: args.toolType
|
|
5643
|
+
});
|
|
5483
5644
|
}
|
|
5645
|
+
return matched;
|
|
5484
5646
|
}
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5647
|
+
// ─── Candidate matching (called from tmux-pane-observer's prompt-candidate) ─
|
|
5648
|
+
/**
|
|
5649
|
+
* Cross-reference a parsed ApprovalCandidate against the pending-call
|
|
5650
|
+
* list. Returns the matched PendingPromptState (which the MCP server
|
|
5651
|
+
* uses to create the INTERACTIVE_PROMPT event), or null if no match
|
|
5652
|
+
* (stale scrollback).
|
|
5653
|
+
*
|
|
5654
|
+
* Match algorithm:
|
|
5655
|
+
* 1. Filter pendingCalls by candidate.command (if set) — exact match
|
|
5656
|
+
* against PendingToolCall.command.
|
|
5657
|
+
* 2. Fallback: filter by candidate.filePath (if set) — exact match
|
|
5658
|
+
* against PendingToolCall.filePath.
|
|
5659
|
+
* 3. If multiple candidates match (rare), prefer the earliest
|
|
5660
|
+
* intentStepIndex.
|
|
5661
|
+
* 4. If a match exists AND it already has a promptId AND that
|
|
5662
|
+
* promptId is in resolvedPrompts → null (don't re-emit).
|
|
5663
|
+
* 5. If a match exists AND has a promptId BUT NOT resolved → return
|
|
5664
|
+
* the existing PendingPromptState (idempotent re-emit on the same
|
|
5665
|
+
* promptId for a duplicate candidate hash).
|
|
5666
|
+
* 6. Otherwise: create a fresh promptId + PendingPromptState, attach
|
|
5667
|
+
* to the matched call, store in pendingPrompts, return it.
|
|
5668
|
+
*/
|
|
5669
|
+
matchCandidate(candidate, conversationId) {
|
|
5670
|
+
const state = this.matchCandidateInternal(candidate, conversationId);
|
|
5671
|
+
if (state) return state;
|
|
5672
|
+
const existing = this.pendingCandidates.get(this.candidateKey(conversationId, candidate.paneHash));
|
|
5673
|
+
if (existing) {
|
|
5674
|
+
logger.debug("Candidate already buffered (same paneHash)", { paneHash: this.candidateKey(conversationId, candidate.paneHash) });
|
|
5492
5675
|
} else {
|
|
5493
|
-
this.
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5676
|
+
this.pendingCandidates.set(this.candidateKey(conversationId, candidate.paneHash), {
|
|
5677
|
+
candidate,
|
|
5678
|
+
conversationId,
|
|
5679
|
+
queuedAt: Date.now()
|
|
5680
|
+
});
|
|
5681
|
+
logger.debug("No pending tool-call matched \u2014 buffering candidate for pre-transcript window", {
|
|
5682
|
+
candidateCommand: candidate.command,
|
|
5683
|
+
candidateFilePath: candidate.filePath,
|
|
5684
|
+
pendingCount: this.pendingCalls.size,
|
|
5685
|
+
bufferSize: this.pendingCandidates.size
|
|
5686
|
+
});
|
|
5504
5687
|
}
|
|
5505
5688
|
return null;
|
|
5506
5689
|
}
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
var https = __toESM(require("https"));
|
|
5516
|
-
var os6 = __toESM(require("os"));
|
|
5517
|
-
var path7 = __toESM(require("path"));
|
|
5518
|
-
var MEASUREMENT_ID = "G-GS74YEQTB8";
|
|
5519
|
-
var API_SECRET = "lAfOF6OxRzSQ-NsLBRjhAg";
|
|
5520
|
-
var GA4_HOSTNAME = "www.google-analytics.com";
|
|
5521
|
-
var GA4_PATH = `/mp/collect?measurement_id=${MEASUREMENT_ID}&api_secret=${API_SECRET}`;
|
|
5522
|
-
var BEACON_TOTAL_TIMEOUT_MS = 800;
|
|
5523
|
-
function readPluginVersion() {
|
|
5524
|
-
try {
|
|
5525
|
-
const pkgPath = path7.resolve(__dirname, "..", "package.json");
|
|
5526
|
-
const raw = fs6.readFileSync(pkgPath, "utf-8");
|
|
5527
|
-
const pkg = JSON.parse(raw);
|
|
5528
|
-
if (typeof pkg.version === "string" && pkg.version.length > 0 && pkg.version.length < 30) {
|
|
5529
|
-
return pkg.version;
|
|
5690
|
+
/** Internal match logic — does NOT buffer on miss. Used both by the
|
|
5691
|
+
* public matchCandidate (which buffers on miss) and by
|
|
5692
|
+
* drainBufferedCandidates (which doesn't re-buffer; misses just stay
|
|
5693
|
+
* in the buffer until their TTL expires). */
|
|
5694
|
+
matchCandidateInternal(candidate, conversationId) {
|
|
5695
|
+
const matched = this.findMatchingPendingCall(candidate, conversationId);
|
|
5696
|
+
if (!matched) {
|
|
5697
|
+
return null;
|
|
5530
5698
|
}
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
}
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
}
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
const settle = () => {
|
|
5556
|
-
if (settled) return;
|
|
5557
|
-
settled = true;
|
|
5558
|
-
clearTimeout(overall);
|
|
5559
|
-
resolve2();
|
|
5699
|
+
if (matched.promptId && this.resolvedPrompts.has(matched.promptId)) {
|
|
5700
|
+
logger.debug("Skipping re-emit for already-resolved prompt", {
|
|
5701
|
+
promptId: matched.promptId
|
|
5702
|
+
});
|
|
5703
|
+
return null;
|
|
5704
|
+
}
|
|
5705
|
+
if (matched.promptId && this.pendingPrompts.has(matched.promptId)) {
|
|
5706
|
+
return this.pendingPrompts.get(matched.promptId) ?? null;
|
|
5707
|
+
}
|
|
5708
|
+
const promptId = (0, import_uuid2.v4)();
|
|
5709
|
+
matched.promptId = promptId;
|
|
5710
|
+
const state = {
|
|
5711
|
+
promptId,
|
|
5712
|
+
conversationId,
|
|
5713
|
+
pendingCall: matched,
|
|
5714
|
+
submitMap: candidate.submitMap,
|
|
5715
|
+
matchedPaneHeader: candidate.headerText,
|
|
5716
|
+
paneDisplayHeader: candidate.fullHeaderLine,
|
|
5717
|
+
body: candidate.body,
|
|
5718
|
+
paneOptions: candidate.options,
|
|
5719
|
+
paneSemanticKey: buildApprovalSemanticKey(candidate),
|
|
5720
|
+
kind: candidate.kind,
|
|
5721
|
+
emittedAt: Date.now(),
|
|
5722
|
+
ttlMs: this.promptTtlMs
|
|
5560
5723
|
};
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5724
|
+
this.pendingPrompts.set(promptId, state);
|
|
5725
|
+
logger.info("Emitting INTERACTIVE_PROMPT for matched candidate", {
|
|
5726
|
+
promptId,
|
|
5727
|
+
command: matched.command,
|
|
5728
|
+
filePath: matched.filePath,
|
|
5729
|
+
intentStepIndex: matched.intentStepIndex,
|
|
5730
|
+
toolCallIndex: matched.toolCallIndex
|
|
5731
|
+
});
|
|
5732
|
+
this.emit("pending-prompt", state);
|
|
5733
|
+
return state;
|
|
5734
|
+
}
|
|
5735
|
+
/** Resolve a pending prompt — called from MCP server when mobile sends
|
|
5736
|
+
* the option-number reply with metadata.promptId. */
|
|
5737
|
+
/**
|
|
5738
|
+
* Called from the MCP server when paneObserver emits 'prompt-cleared'
|
|
5739
|
+
* — i.e. the approval UI disappeared from the pane (desktop user
|
|
5740
|
+
* picked an option, agy timed out the prompt, or the dispatcher
|
|
5741
|
+
* answered). Resolves ALL outstanding pane-only pending prompts and
|
|
5742
|
+
* releases the paneOnly dedup hashes so a subsequent approval can
|
|
5743
|
+
* fresh-emit. Without this, mobile prompt state dangles until TTL
|
|
5744
|
+
* AND the next identical approval is forever-suppressed by stale
|
|
5745
|
+
* dedup. (Stage 2 R1 MED finding 2026-05-21.)
|
|
5746
|
+
*
|
|
5747
|
+
* `emittedBeforeMs` (F1, Stage 2 R1-F2): a desktop input that waited in the
|
|
5748
|
+
* routing hold proves only that prompts raised BEFORE it was typed are gone.
|
|
5749
|
+
* A prompt emitted after that moment stays pending, and so does its dedup
|
|
5750
|
+
* hash (clearing it would re-emit the still-visible chooser).
|
|
5751
|
+
*/
|
|
5752
|
+
onPanePromptCleared(emittedBeforeMs) {
|
|
5753
|
+
if (this.pendingPrompts.size === 0 && this.paneOnlyEmittedHashes.size === 0) {
|
|
5754
|
+
return [];
|
|
5570
5755
|
}
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
}
|
|
5756
|
+
const now = Date.now();
|
|
5757
|
+
const cleared = [];
|
|
5758
|
+
let retained = false;
|
|
5759
|
+
for (const [promptId, state] of this.pendingPrompts.entries()) {
|
|
5760
|
+
if (emittedBeforeMs !== void 0 && state.emittedAt >= emittedBeforeMs) {
|
|
5761
|
+
retained = true;
|
|
5762
|
+
continue;
|
|
5579
5763
|
}
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
agent: "antigravity",
|
|
5587
|
-
plugin_version: PLUGIN_VERSION,
|
|
5588
|
-
platform: process.platform,
|
|
5589
|
-
source: process.env.CODEVIBE_TELEMETRY_SOURCE || "production",
|
|
5590
|
-
...sanitizedParams
|
|
5591
|
-
}
|
|
5592
|
-
}
|
|
5593
|
-
]
|
|
5594
|
-
});
|
|
5595
|
-
req = https.request(
|
|
5596
|
-
{
|
|
5597
|
-
hostname: GA4_HOSTNAME,
|
|
5598
|
-
path: GA4_PATH,
|
|
5599
|
-
method: "POST",
|
|
5600
|
-
headers: { "Content-Type": "application/json" },
|
|
5601
|
-
timeout: BEACON_TOTAL_TIMEOUT_MS
|
|
5602
|
-
},
|
|
5603
|
-
(res) => {
|
|
5604
|
-
res.resume();
|
|
5605
|
-
res.on("end", settle);
|
|
5606
|
-
res.on("close", settle);
|
|
5607
|
-
res.on("error", settle);
|
|
5608
|
-
}
|
|
5609
|
-
);
|
|
5610
|
-
req.on("error", settle);
|
|
5611
|
-
req.on("timeout", () => {
|
|
5612
|
-
try {
|
|
5613
|
-
req?.destroy();
|
|
5614
|
-
} catch {
|
|
5615
|
-
}
|
|
5616
|
-
settle();
|
|
5617
|
-
});
|
|
5618
|
-
req.on("close", settle);
|
|
5619
|
-
req.write(payload);
|
|
5620
|
-
req.end();
|
|
5621
|
-
} catch {
|
|
5622
|
-
settle();
|
|
5764
|
+
if (state.isHookPrompt) {
|
|
5765
|
+
retained = true;
|
|
5766
|
+
continue;
|
|
5767
|
+
}
|
|
5768
|
+
this.resolvedPrompts.set(promptId, now);
|
|
5769
|
+
cleared.push(promptId);
|
|
5623
5770
|
}
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
function mapTranscriptEvent(emit, sessionId, notBeforeMs) {
|
|
5634
|
-
const { event } = emit;
|
|
5635
|
-
const base = {
|
|
5636
|
-
sessionId,
|
|
5637
|
-
source: import_codevibe_core5.EventSource.DESKTOP
|
|
5638
|
-
};
|
|
5639
|
-
const orderingKey = `${sessionId}:${emit.conversationId}`;
|
|
5640
|
-
const nextTimestamp = () => (0, import_codevibe_core5.prepareEventTimestamp)({ orderingKey, agentClock: event.created_at, notBeforeMs });
|
|
5641
|
-
const baseMetadata = {
|
|
5642
|
-
step_index: event.step_index,
|
|
5643
|
-
agy_source: event.source,
|
|
5644
|
-
agy_status: event.status,
|
|
5645
|
-
agy_created_at: event.created_at,
|
|
5646
|
-
agy_type: event.type,
|
|
5647
|
-
agy_conversation_id: emit.conversationId
|
|
5648
|
-
};
|
|
5649
|
-
if (event.source === "SYSTEM") {
|
|
5650
|
-
if (event.type === "CONVERSATION_HISTORY" || event.type === "SYSTEM_MESSAGE") {
|
|
5651
|
-
logger.debug("Dropping internal transcript event", {
|
|
5652
|
-
type: event.type,
|
|
5653
|
-
stepIndex: event.step_index
|
|
5654
|
-
});
|
|
5655
|
-
return { events: [], pendingCallRegistrations: [] };
|
|
5771
|
+
if (!retained) {
|
|
5772
|
+
this.pendingPrompts.clear();
|
|
5773
|
+
this.paneOnlyEmittedHashes.clear();
|
|
5774
|
+
} else {
|
|
5775
|
+
for (const promptId of cleared) {
|
|
5776
|
+
const state = this.pendingPrompts.get(promptId);
|
|
5777
|
+
if (state?.paneHash) this.paneOnlyEmittedHashes.delete(state.paneHash);
|
|
5778
|
+
this.pendingPrompts.delete(promptId);
|
|
5779
|
+
}
|
|
5656
5780
|
}
|
|
5657
|
-
if (
|
|
5658
|
-
logger.
|
|
5659
|
-
|
|
5660
|
-
stepIndex: event.step_index
|
|
5781
|
+
if (cleared.length > 0) {
|
|
5782
|
+
logger.info("Pane prompt cleared \u2014 resolved pending prompts", {
|
|
5783
|
+
count: cleared.length
|
|
5661
5784
|
});
|
|
5662
|
-
return { events: [], pendingCallRegistrations: [] };
|
|
5663
5785
|
}
|
|
5786
|
+
return cleared;
|
|
5664
5787
|
}
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5788
|
+
resolvePrompt(promptId, options) {
|
|
5789
|
+
const state = this.pendingPrompts.get(promptId);
|
|
5790
|
+
if (!state) return null;
|
|
5791
|
+
this.resolvedPrompts.set(promptId, Date.now());
|
|
5792
|
+
this.pendingPrompts.delete(promptId);
|
|
5793
|
+
const advanceWizard = options?.advanceWizard !== false;
|
|
5794
|
+
const liveWizard = this.liveWizardInvocations.get(state.conversationId);
|
|
5795
|
+
if (liveWizard && liveWizard.promptId === promptId) {
|
|
5796
|
+
if (advanceWizard) {
|
|
5797
|
+
const maxStep = (liveWizard.call.structuredQuestions?.length ?? 1) - 1;
|
|
5798
|
+
liveWizard.activeStepIndex = Math.min(liveWizard.activeStepIndex + 1, maxStep);
|
|
5799
|
+
}
|
|
5800
|
+
liveWizard.promptId = "";
|
|
5801
|
+
}
|
|
5802
|
+
this.paneOnlyEmittedHashes.clear();
|
|
5803
|
+
return state;
|
|
5672
5804
|
}
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5805
|
+
/**
|
|
5806
|
+
* Roll back a pending prompt WITHOUT marking it as resolved. Used by
|
|
5807
|
+
* the MCP server when the INTERACTIVE_PROMPT createEvent fails — the
|
|
5808
|
+
* prompt was never delivered to mobile, so a future tmux pane
|
|
5809
|
+
* candidate for the same pending tool-call should be eligible to
|
|
5810
|
+
* re-emit. `resolvePrompt` is the WRONG call there because it adds
|
|
5811
|
+
* the promptId to `resolvedPrompts`, which the stale-scrollback guard
|
|
5812
|
+
* uses to suppress re-emit forever. This method instead detaches the
|
|
5813
|
+
* promptId from the pending call + removes from pendingPrompts +
|
|
5814
|
+
* leaves resolvedPrompts UNTOUCHED so the fresh-emit path runs on
|
|
5815
|
+
* the next match. (agy R2 Stage 2 round-2 MED finding 2026-05-20.)
|
|
5816
|
+
*/
|
|
5817
|
+
rollbackPrompt(promptId) {
|
|
5818
|
+
const state = this.pendingPrompts.get(promptId);
|
|
5819
|
+
if (!state) return null;
|
|
5820
|
+
this.pendingPrompts.delete(promptId);
|
|
5821
|
+
if (state.pendingCall.promptId === promptId) {
|
|
5822
|
+
state.pendingCall.promptId = void 0;
|
|
5823
|
+
}
|
|
5824
|
+
for (const call of this.pendingCalls.values()) {
|
|
5825
|
+
if (call.matchedPromptId === promptId) {
|
|
5826
|
+
call.matchedPromptId = void 0;
|
|
5827
|
+
}
|
|
5828
|
+
}
|
|
5829
|
+
this.paneOnlyEmittedHashes.clear();
|
|
5830
|
+
return state;
|
|
5687
5831
|
}
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5832
|
+
/**
|
|
5833
|
+
* Rekey a pending prompt in the detector's internal map and associated pending calls.
|
|
5834
|
+
* Keeps map keys and state.promptId synchronized when a server layer adopts an admission identity.
|
|
5835
|
+
*/
|
|
5836
|
+
rekeyPrompt(oldPromptId, newPromptId) {
|
|
5837
|
+
if (oldPromptId === newPromptId) return;
|
|
5838
|
+
const state = this.pendingPrompts.get(oldPromptId);
|
|
5839
|
+
if (!state) return;
|
|
5840
|
+
this.pendingPrompts.delete(oldPromptId);
|
|
5841
|
+
state.promptId = newPromptId;
|
|
5842
|
+
if (state.pendingCall) {
|
|
5843
|
+
if (state.pendingCall.promptId === oldPromptId) state.pendingCall.promptId = newPromptId;
|
|
5844
|
+
if (state.pendingCall.matchedPromptId === oldPromptId) state.pendingCall.matchedPromptId = newPromptId;
|
|
5699
5845
|
}
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
timestamp: nextTimestamp(),
|
|
5704
|
-
type: import_codevibe_core5.EventType.ASSISTANT_RESPONSE,
|
|
5705
|
-
content: truncate(event.content, CONTENT_MAX),
|
|
5706
|
-
metadata: { ...baseMetadata }
|
|
5707
|
-
});
|
|
5846
|
+
for (const call of this.pendingCalls.values()) {
|
|
5847
|
+
if (call.promptId === oldPromptId) call.promptId = newPromptId;
|
|
5848
|
+
if (call.matchedPromptId === oldPromptId) call.matchedPromptId = newPromptId;
|
|
5708
5849
|
}
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5850
|
+
this.pendingPrompts.set(newPromptId, state);
|
|
5851
|
+
}
|
|
5852
|
+
/** Returns the current pending-prompt state by id, or null if expired /
|
|
5853
|
+
* unknown. Used by prompt-responder for the active-prompt-identity
|
|
5854
|
+
* guard right before send-keys. */
|
|
5855
|
+
getPendingPrompt(promptId) {
|
|
5856
|
+
return this.pendingPrompts.get(promptId) ?? null;
|
|
5857
|
+
}
|
|
5858
|
+
getPendingPromptsForConversation(conversationId) {
|
|
5859
|
+
const pending = Array.from(this.pendingPrompts.values());
|
|
5860
|
+
return conversationId ? pending.filter((state) => state.conversationId === conversationId) : pending;
|
|
5861
|
+
}
|
|
5862
|
+
// ─── Read-only accessors ────────────────────────────────────────────────
|
|
5863
|
+
getPendingCalls() {
|
|
5864
|
+
return Array.from(this.pendingCalls.values());
|
|
5865
|
+
}
|
|
5866
|
+
getActivePromptCount() {
|
|
5867
|
+
return this.pendingPrompts.size;
|
|
5868
|
+
}
|
|
5869
|
+
// ─── Internals ──────────────────────────────────────────────────────────
|
|
5870
|
+
callKey(call) {
|
|
5871
|
+
return `${call.conversationId}|${call.intentStepIndex}|${call.toolCallIndex}|${call.intentByteOffset}`;
|
|
5872
|
+
}
|
|
5873
|
+
findMatchingPendingCall(candidate, conversationId) {
|
|
5874
|
+
if (candidate.kind === "question") {
|
|
5875
|
+
const questionCalls = Array.from(this.pendingCalls.values()).filter((c) => c.conversationId === conversationId).filter((c) => c.resultStepIndex === void 0).filter((c) => {
|
|
5876
|
+
if (c.structuredQuestions && candidate.headerText) {
|
|
5877
|
+
const matchesStructured = c.structuredQuestions.some((sq) => questionsMatch(sq.question, candidate.headerText));
|
|
5878
|
+
if (matchesStructured) return true;
|
|
5879
|
+
}
|
|
5880
|
+
const associatedPromptId = c.matchedPromptId || c.promptId;
|
|
5881
|
+
if (!associatedPromptId) return true;
|
|
5882
|
+
return !this.resolvedPrompts.has(associatedPromptId) && !this.pendingPrompts.has(associatedPromptId);
|
|
5883
|
+
}).filter(
|
|
5884
|
+
(c) => c.toolType === "ASK_QUESTION" || ["ask_question", "question", "ask"].includes(c.rawToolName?.toLowerCase() ?? "")
|
|
5885
|
+
).sort((a, b) => (b.intentStepIndex ?? 0) - (a.intentStepIndex ?? 0));
|
|
5886
|
+
if (questionCalls.length > 0 && candidate.headerText) {
|
|
5887
|
+
const textMatch = questionCalls.find((c) => {
|
|
5888
|
+
if (!c.questionText) return false;
|
|
5889
|
+
if (questionsMatch(c.questionText, candidate.headerText)) return true;
|
|
5890
|
+
if (c.structuredQuestions) {
|
|
5891
|
+
return c.structuredQuestions.some((sq) => questionsMatch(sq.question, candidate.headerText));
|
|
5892
|
+
}
|
|
5893
|
+
return false;
|
|
5731
5894
|
});
|
|
5895
|
+
if (textMatch) return textMatch;
|
|
5732
5896
|
}
|
|
5897
|
+
return null;
|
|
5733
5898
|
}
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5899
|
+
if (candidate.command) {
|
|
5900
|
+
const matches = Array.from(this.pendingCalls.values()).filter((c) => c.conversationId === conversationId).filter((c) => c.command && c.command === candidate.command).sort((a, b) => a.intentStepIndex - b.intentStepIndex);
|
|
5901
|
+
if (matches.length > 0) return matches[0];
|
|
5902
|
+
}
|
|
5903
|
+
if (candidate.filePath) {
|
|
5904
|
+
const matches = Array.from(this.pendingCalls.values()).filter((c) => c.conversationId === conversationId).filter((c) => c.filePath && c.filePath === candidate.filePath).sort((a, b) => a.intentStepIndex - b.intentStepIndex);
|
|
5905
|
+
if (matches.length > 0) return matches[0];
|
|
5906
|
+
}
|
|
5907
|
+
const allFields = (candidate.command ?? "") + " " + (candidate.filePath ?? "");
|
|
5908
|
+
if (allFields.trim()) {
|
|
5909
|
+
const matches = Array.from(this.pendingCalls.values()).filter((c) => c.conversationId === conversationId).filter((c) => {
|
|
5910
|
+
const cmd = (c.command ?? "").trim();
|
|
5911
|
+
const fp = (c.filePath ?? "").trim();
|
|
5912
|
+
if (!cmd && !fp) return false;
|
|
5913
|
+
const cmdOk = cmd.length >= PASS3_MIN_IDENTITY_LEN && containsAsWord(allFields, cmd);
|
|
5914
|
+
const fpOk = fp.length >= PASS3_MIN_IDENTITY_LEN && containsAsWord(allFields, fp);
|
|
5915
|
+
return cmdOk || fpOk;
|
|
5916
|
+
}).sort((a, b) => a.intentStepIndex - b.intentStepIndex);
|
|
5917
|
+
if (matches.length > 0) return matches[0];
|
|
5918
|
+
}
|
|
5919
|
+
return null;
|
|
5753
5920
|
}
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
timestamp: nextTimestamp(),
|
|
5763
|
-
type: import_codevibe_core5.EventType.TOOL_USE,
|
|
5764
|
-
content: formatToolResultContent(event.type, toolName, truncatedContent, isErrorStatus),
|
|
5765
|
-
metadata: {
|
|
5766
|
-
...baseMetadata,
|
|
5767
|
-
tool_name: toolName,
|
|
5768
|
-
tool_output: truncatedContent,
|
|
5769
|
-
success: !isErrorStatus
|
|
5921
|
+
sweep() {
|
|
5922
|
+
const now = Date.now();
|
|
5923
|
+
for (const [key, call] of this.pendingCalls.entries()) {
|
|
5924
|
+
if (now - call.startedAt > this.pendingCallTtlMs) {
|
|
5925
|
+
this.pendingCalls.delete(key);
|
|
5926
|
+
if (call.promptId) {
|
|
5927
|
+
this.pendingPrompts.delete(call.promptId);
|
|
5928
|
+
this.resolvedPrompts.set(call.promptId, now);
|
|
5770
5929
|
}
|
|
5930
|
+
logger.warn("Pending tool call expired (no matching result event)", {
|
|
5931
|
+
intentStepIndex: call.intentStepIndex,
|
|
5932
|
+
toolCallIndex: call.toolCallIndex,
|
|
5933
|
+
command: call.command,
|
|
5934
|
+
filePath: call.filePath,
|
|
5935
|
+
ageMs: now - call.startedAt
|
|
5936
|
+
});
|
|
5937
|
+
this.emit("pending-call-expired", call);
|
|
5771
5938
|
}
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5939
|
+
}
|
|
5940
|
+
for (const [promptId, resolvedAt] of this.resolvedPrompts.entries()) {
|
|
5941
|
+
if (now - resolvedAt > 5 * 6e4) {
|
|
5942
|
+
this.resolvedPrompts.delete(promptId);
|
|
5943
|
+
}
|
|
5944
|
+
}
|
|
5945
|
+
for (const [key, buffered] of this.pendingCandidates.entries()) {
|
|
5946
|
+
if (now - buffered.queuedAt > this.pendingCandidateBufferMs) {
|
|
5947
|
+
this.pendingCandidates.delete(key);
|
|
5948
|
+
logger.debug("Buffered candidate expired without ever matching a pending call", {
|
|
5949
|
+
key,
|
|
5950
|
+
candidateCommand: buffered.candidate.command,
|
|
5951
|
+
ageMs: now - buffered.queuedAt
|
|
5952
|
+
});
|
|
5953
|
+
}
|
|
5954
|
+
}
|
|
5780
5955
|
}
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
return
|
|
5785
|
-
/^Created At:[^\n]*\nCompleted At:[^\n]*\n+(?:\s*\n)?/,
|
|
5786
|
-
""
|
|
5787
|
-
).trim();
|
|
5956
|
+
};
|
|
5957
|
+
function containsAsWord(haystack, needle) {
|
|
5958
|
+
const escaped = needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5959
|
+
return new RegExp(`(^|[\\s"'\`])${escaped}([\\s"'\`]|$)`).test(haystack);
|
|
5788
5960
|
}
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5961
|
+
|
|
5962
|
+
// src/prompt-responder.ts
|
|
5963
|
+
var import_child_process2 = require("child_process");
|
|
5964
|
+
var import_util2 = require("util");
|
|
5965
|
+
var execAsync2 = (0, import_util2.promisify)(import_child_process2.exec);
|
|
5966
|
+
var ENTER_DELAY_MS = 100;
|
|
5967
|
+
var CODEVIBE_AGY_ESCAPE_INPUT = "__CODEVIBE_AGY_KEY_ESCAPE__";
|
|
5968
|
+
var PromptResponder = class {
|
|
5969
|
+
constructor(options = {}) {
|
|
5970
|
+
this.tmuxTargetOverride = options.tmuxTarget;
|
|
5971
|
+
this.execFn = options.execFn ?? (async (cmd) => execAsync2(cmd));
|
|
5972
|
+
this.paneObserver = options.paneObserver ?? null;
|
|
5973
|
+
this.detector = options.detector ?? null;
|
|
5974
|
+
if (this.detector && !this.paneObserver) {
|
|
5975
|
+
throw new Error(
|
|
5976
|
+
"PromptResponder misconfigured: detector wired without paneObserver. Approval-reply identity guard requires the observer to re-probe the pane before send. Either wire both, or wire neither."
|
|
5977
|
+
);
|
|
5978
|
+
}
|
|
5807
5979
|
}
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
return
|
|
5831
|
-
|
|
5832
|
-
|
|
5980
|
+
/**
|
|
5981
|
+
* Send free-form text to the agy session. The text is typed (via
|
|
5982
|
+
* `send-keys -l`) then Enter is sent after a short delay so agy's
|
|
5983
|
+
* input handler has time to process the buffer.
|
|
5984
|
+
*/
|
|
5985
|
+
async sendFreeForm(text) {
|
|
5986
|
+
const target = this.resolveTmuxTarget();
|
|
5987
|
+
if (!target) {
|
|
5988
|
+
return { ok: false, reason: "no-tmux-target" };
|
|
5989
|
+
}
|
|
5990
|
+
try {
|
|
5991
|
+
if (text === CODEVIBE_AGY_ESCAPE_INPUT) {
|
|
5992
|
+
await this.sendKey(target, "Escape");
|
|
5993
|
+
} else {
|
|
5994
|
+
await this.typeLiteral(target, text);
|
|
5995
|
+
await delay(ENTER_DELAY_MS);
|
|
5996
|
+
await this.sendKey(target, "Enter");
|
|
5997
|
+
}
|
|
5998
|
+
logger.info("Sent free-form input to agy", {
|
|
5999
|
+
target,
|
|
6000
|
+
textLength: text.length
|
|
6001
|
+
});
|
|
6002
|
+
return { ok: true };
|
|
6003
|
+
} catch (error) {
|
|
6004
|
+
logger.error("tmux send-keys failed for free-form input", { target, error });
|
|
6005
|
+
return {
|
|
6006
|
+
ok: false,
|
|
6007
|
+
reason: "tmux-failed",
|
|
6008
|
+
details: error instanceof Error ? error.message : String(error)
|
|
6009
|
+
};
|
|
6010
|
+
}
|
|
5833
6011
|
}
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
6012
|
+
/**
|
|
6013
|
+
* Resolve a pending approval prompt with the mobile-user's chosen
|
|
6014
|
+
* option number and optional follow-up text for write-in questions.
|
|
6015
|
+
* Uses the submitMap from the PendingPromptState to translate
|
|
6016
|
+
* option-number → terminal keystrokes.
|
|
6017
|
+
*
|
|
6018
|
+
* Active-prompt-identity guard (DESIGN.md §3.4):
|
|
6019
|
+
* 1. Detector check: is `promptId` still in pendingPrompts? If not
|
|
6020
|
+
* (expired or already-resolved), drop with `prompt-expired`.
|
|
6021
|
+
* 2. submitMap lookup: is `optionNumber` a valid key? If not, drop with `invalid-option`.
|
|
6022
|
+
* If `followUpText` is provided, verify option is Write-in; otherwise drop with `follow-up-not-allowed`.
|
|
6023
|
+
* If bare Write-in option has no text, drop with `write-in-text-required`.
|
|
6024
|
+
* 3. Pane-observer check: captureSnapshot() — does the pane STILL show
|
|
6025
|
+
* an approval UI matching our pending prompt's semantic key?
|
|
6026
|
+
* 4. Send option keystrokes via tmux send-keys.
|
|
6027
|
+
* 5. If write-in followUpText is present: wait 250ms, verify pane state, type literal text, send Enter.
|
|
6028
|
+
*/
|
|
6029
|
+
async sendApprovalReply(promptId, optionNumber, followUpText, beforeTypeText) {
|
|
6030
|
+
const target = this.resolveTmuxTarget();
|
|
6031
|
+
if (!target) {
|
|
6032
|
+
return { ok: false, reason: "no-tmux-target" };
|
|
6033
|
+
}
|
|
6034
|
+
if (this.detector) {
|
|
6035
|
+
const state = this.detector.getPendingPrompt(promptId);
|
|
6036
|
+
if (!state) {
|
|
6037
|
+
logger.warn("Mobile reply for unknown / expired promptId", { promptId, optionNumber });
|
|
6038
|
+
return {
|
|
6039
|
+
ok: false,
|
|
6040
|
+
reason: "prompt-expired",
|
|
6041
|
+
details: `promptId ${promptId} is not in pendingPrompts`
|
|
6042
|
+
};
|
|
6043
|
+
}
|
|
6044
|
+
const keys = state.submitMap[optionNumber];
|
|
6045
|
+
if (!keys || keys.length === 0) {
|
|
6046
|
+
return {
|
|
6047
|
+
ok: false,
|
|
6048
|
+
reason: "invalid-option",
|
|
6049
|
+
details: `option '${optionNumber}' not in submitMap (valid: ${Object.keys(state.submitMap).join(",")})`
|
|
6050
|
+
};
|
|
6051
|
+
}
|
|
6052
|
+
const isQuestionPrompt = state.kind === "question";
|
|
6053
|
+
const expectedStep = (state.paneDisplayHeader || "").match(/\bQuestion\s+(\d+(?:[\/:of\s]+\d+)?):/i)?.[1];
|
|
6054
|
+
const matchingOpt = state.paneOptions?.find((o) => String(o.number) === String(optionNumber));
|
|
6055
|
+
const isWriteInOption = matchingOpt ? /^\s*write-?in\b/i.test(matchingOpt.text) : false;
|
|
6056
|
+
if (isQuestionPrompt) {
|
|
6057
|
+
if (state.paneOptions && state.paneOptions.length > 0 && !matchingOpt) {
|
|
6058
|
+
return {
|
|
6059
|
+
ok: false,
|
|
6060
|
+
reason: "invalid-option",
|
|
6061
|
+
details: `option '${optionNumber}' not found in advertised options`,
|
|
6062
|
+
deliveryPhase: "none"
|
|
6063
|
+
};
|
|
6064
|
+
}
|
|
6065
|
+
if (isWriteInOption && (!followUpText || followUpText.trim().length === 0)) {
|
|
6066
|
+
return {
|
|
6067
|
+
ok: false,
|
|
6068
|
+
reason: "write-in-text-required",
|
|
6069
|
+
details: 'Please provide your write-in text along with the option number (e.g. "4, your response")',
|
|
6070
|
+
deliveryPhase: "none"
|
|
6071
|
+
};
|
|
6072
|
+
}
|
|
6073
|
+
if (followUpText && followUpText.trim().length > 0 && !isWriteInOption) {
|
|
6074
|
+
return {
|
|
6075
|
+
ok: false,
|
|
6076
|
+
reason: "follow-up-not-allowed",
|
|
6077
|
+
details: `Option '${optionNumber}' does not accept follow-up text`,
|
|
6078
|
+
deliveryPhase: "none"
|
|
6079
|
+
};
|
|
6080
|
+
}
|
|
6081
|
+
}
|
|
6082
|
+
if (this.paneObserver) {
|
|
6083
|
+
if (!state.paneSemanticKey) {
|
|
6084
|
+
return {
|
|
6085
|
+
ok: false,
|
|
6086
|
+
reason: "prompt-probe-failed",
|
|
6087
|
+
details: "pending prompt has no exact semantic identity",
|
|
6088
|
+
deliveryPhase: "none"
|
|
6089
|
+
};
|
|
6090
|
+
}
|
|
6091
|
+
}
|
|
6092
|
+
let deliveryPhase = "none";
|
|
6093
|
+
try {
|
|
6094
|
+
for (const key of keys) {
|
|
6095
|
+
if (!this.detector || !this.paneObserver) {
|
|
6096
|
+
return {
|
|
6097
|
+
ok: false,
|
|
6098
|
+
reason: "prompt-probe-failed",
|
|
6099
|
+
details: "approval responder is missing its detector or pane observer",
|
|
6100
|
+
deliveryPhase
|
|
6101
|
+
};
|
|
6102
|
+
}
|
|
6103
|
+
const currentPrompt = this.detector.getPendingPrompt(promptId);
|
|
6104
|
+
if (currentPrompt !== state || !currentPrompt.submitMap || !currentPrompt.submitMap[optionNumber]) {
|
|
6105
|
+
return {
|
|
6106
|
+
ok: false,
|
|
6107
|
+
reason: "prompt-expired",
|
|
6108
|
+
details: "option is no longer actionable on active prompt (controls revoked or updated)",
|
|
6109
|
+
deliveryPhase
|
|
6110
|
+
};
|
|
6111
|
+
}
|
|
6112
|
+
let liveSemanticKey = null;
|
|
6113
|
+
try {
|
|
6114
|
+
const snapshot = await this.paneObserver.captureSnapshot();
|
|
6115
|
+
const postCapturePrompt = this.detector.getPendingPrompt(promptId);
|
|
6116
|
+
if (postCapturePrompt !== state || !postCapturePrompt.submitMap || !postCapturePrompt.submitMap[optionNumber]) {
|
|
6117
|
+
return {
|
|
6118
|
+
ok: false,
|
|
6119
|
+
reason: "prompt-expired",
|
|
6120
|
+
details: "option is no longer actionable on active prompt (controls revoked or updated)",
|
|
6121
|
+
deliveryPhase
|
|
6122
|
+
};
|
|
6123
|
+
}
|
|
6124
|
+
if (isQuestionPrompt && isWriteInInputActive(snapshot, state.matchedPaneHeader, expectedStep)) {
|
|
6125
|
+
return {
|
|
6126
|
+
ok: false,
|
|
6127
|
+
reason: "write-in-already-active",
|
|
6128
|
+
details: "write-in input field is already open on desktop; finish or cancel on desktop before selecting an option",
|
|
6129
|
+
deliveryPhase: "none"
|
|
6130
|
+
};
|
|
6131
|
+
}
|
|
6132
|
+
const live = parseApprovalSnapshot(snapshot);
|
|
6133
|
+
if (!live) {
|
|
6134
|
+
if (detectActivePromptKind(snapshot) === null) {
|
|
6135
|
+
this.detector.resolvePrompt(promptId);
|
|
6136
|
+
return {
|
|
6137
|
+
ok: false,
|
|
6138
|
+
reason: "prompt-superseded",
|
|
6139
|
+
details: "the approval chooser is no longer active",
|
|
6140
|
+
deliveryPhase
|
|
6141
|
+
};
|
|
6142
|
+
}
|
|
6143
|
+
return {
|
|
6144
|
+
ok: false,
|
|
6145
|
+
reason: "prompt-probe-failed",
|
|
6146
|
+
details: "the active chooser could not be parsed exactly",
|
|
6147
|
+
deliveryPhase
|
|
6148
|
+
};
|
|
6149
|
+
}
|
|
6150
|
+
liveSemanticKey = live ? buildApprovalSemanticKey(live) : null;
|
|
6151
|
+
} catch (error) {
|
|
6152
|
+
return {
|
|
6153
|
+
ok: false,
|
|
6154
|
+
reason: "prompt-probe-failed",
|
|
6155
|
+
details: error instanceof Error ? error.message : String(error),
|
|
6156
|
+
deliveryPhase
|
|
6157
|
+
};
|
|
6158
|
+
}
|
|
6159
|
+
if (liveSemanticKey !== state.paneSemanticKey) {
|
|
6160
|
+
this.detector.resolvePrompt(promptId);
|
|
6161
|
+
return {
|
|
6162
|
+
ok: false,
|
|
6163
|
+
reason: "prompt-superseded",
|
|
6164
|
+
details: "active chooser semantics differ from the mobile prompt",
|
|
6165
|
+
deliveryPhase
|
|
6166
|
+
};
|
|
6167
|
+
}
|
|
6168
|
+
deliveryPhase = "option_attempted";
|
|
6169
|
+
if (isNamedKey(key)) {
|
|
6170
|
+
await this.sendKey(target, key);
|
|
6171
|
+
} else {
|
|
6172
|
+
await this.typeLiteral(target, key);
|
|
6173
|
+
}
|
|
6174
|
+
deliveryPhase = "option_sent";
|
|
6175
|
+
await delay(50);
|
|
6176
|
+
}
|
|
6177
|
+
if (isQuestionPrompt && isWriteInOption && followUpText && followUpText.trim().length > 0) {
|
|
6178
|
+
let writeInReady = false;
|
|
6179
|
+
if (this.paneObserver) {
|
|
6180
|
+
for (let probeAttempt = 0; probeAttempt < 10; probeAttempt++) {
|
|
6181
|
+
await delay(100);
|
|
6182
|
+
try {
|
|
6183
|
+
const liveSnap = await this.paneObserver.captureSnapshot();
|
|
6184
|
+
if (liveSnap && isWriteInInputActive(liveSnap, state.matchedPaneHeader, expectedStep)) {
|
|
6185
|
+
writeInReady = true;
|
|
6186
|
+
break;
|
|
6187
|
+
}
|
|
6188
|
+
} catch {
|
|
6189
|
+
}
|
|
6190
|
+
}
|
|
6191
|
+
if (!writeInReady) {
|
|
6192
|
+
return {
|
|
6193
|
+
ok: false,
|
|
6194
|
+
reason: "prompt-probe-failed",
|
|
6195
|
+
details: "terminal pane failed to transition to write-in input line",
|
|
6196
|
+
deliveryPhase
|
|
6197
|
+
};
|
|
6198
|
+
}
|
|
6199
|
+
} else {
|
|
6200
|
+
await delay(250);
|
|
6201
|
+
}
|
|
6202
|
+
if (beforeTypeText) {
|
|
6203
|
+
try {
|
|
6204
|
+
await beforeTypeText();
|
|
6205
|
+
} catch (err) {
|
|
6206
|
+
return {
|
|
6207
|
+
ok: false,
|
|
6208
|
+
reason: "tmux-failed",
|
|
6209
|
+
details: err instanceof Error ? err.message : String(err),
|
|
6210
|
+
deliveryPhase: "option_sent"
|
|
6211
|
+
// Do not advance phase if hook throws (Claude R4-N2)
|
|
6212
|
+
};
|
|
6213
|
+
}
|
|
6214
|
+
}
|
|
6215
|
+
const postHookPrompt = this.detector.getPendingPrompt(promptId);
|
|
6216
|
+
if (postHookPrompt !== state || !postHookPrompt.submitMap || !postHookPrompt.submitMap[optionNumber]) {
|
|
6217
|
+
return { ok: false, reason: "prompt-expired", deliveryPhase: "option_sent" };
|
|
6218
|
+
}
|
|
6219
|
+
if (this.paneObserver) {
|
|
6220
|
+
try {
|
|
6221
|
+
const postHookSnap = await this.paneObserver.captureSnapshot();
|
|
6222
|
+
if (!postHookSnap || !isWriteInInputActive(postHookSnap, state.matchedPaneHeader, expectedStep)) {
|
|
6223
|
+
return {
|
|
6224
|
+
ok: false,
|
|
6225
|
+
reason: "prompt-probe-failed",
|
|
6226
|
+
details: "write-in input field disappeared during callback before typing",
|
|
6227
|
+
deliveryPhase: "option_sent"
|
|
6228
|
+
};
|
|
6229
|
+
}
|
|
6230
|
+
} catch (err) {
|
|
6231
|
+
return {
|
|
6232
|
+
ok: false,
|
|
6233
|
+
reason: "prompt-probe-failed",
|
|
6234
|
+
details: err instanceof Error ? err.message : String(err),
|
|
6235
|
+
deliveryPhase: "option_sent"
|
|
6236
|
+
};
|
|
6237
|
+
}
|
|
6238
|
+
const postSnapPrompt1 = this.detector.getPendingPrompt(promptId);
|
|
6239
|
+
if (postSnapPrompt1 !== state || !postSnapPrompt1.submitMap || !postSnapPrompt1.submitMap[optionNumber]) {
|
|
6240
|
+
return { ok: false, reason: "prompt-expired", deliveryPhase: "option_sent" };
|
|
6241
|
+
}
|
|
6242
|
+
}
|
|
6243
|
+
deliveryPhase = "text_attempted";
|
|
6244
|
+
const cleanText = followUpText.replace(/[\r\n]+/g, " ").trim();
|
|
6245
|
+
await this.typeLiteral(target, cleanText);
|
|
6246
|
+
deliveryPhase = "text_typed";
|
|
6247
|
+
await delay(50);
|
|
6248
|
+
if (this.paneObserver) {
|
|
6249
|
+
try {
|
|
6250
|
+
const preEnterSnap = await this.paneObserver.captureSnapshot();
|
|
6251
|
+
if (!preEnterSnap || !isWriteInInputActive(preEnterSnap, state.matchedPaneHeader, expectedStep)) {
|
|
6252
|
+
return {
|
|
6253
|
+
ok: false,
|
|
6254
|
+
reason: "prompt-probe-failed",
|
|
6255
|
+
details: "write-in input field disappeared before confirmation",
|
|
6256
|
+
deliveryPhase: "text_typed"
|
|
6257
|
+
};
|
|
6258
|
+
}
|
|
6259
|
+
} catch (err) {
|
|
6260
|
+
return {
|
|
6261
|
+
ok: false,
|
|
6262
|
+
reason: "prompt-probe-failed",
|
|
6263
|
+
details: err instanceof Error ? err.message : String(err),
|
|
6264
|
+
deliveryPhase: "text_typed"
|
|
6265
|
+
};
|
|
6266
|
+
}
|
|
6267
|
+
}
|
|
6268
|
+
const postSnapPrompt = this.detector.getPendingPrompt(promptId);
|
|
6269
|
+
if (postSnapPrompt !== state || !postSnapPrompt.submitMap || !postSnapPrompt.submitMap[optionNumber]) {
|
|
6270
|
+
return { ok: false, reason: "prompt-expired", deliveryPhase: "text_typed" };
|
|
6271
|
+
}
|
|
6272
|
+
deliveryPhase = "enter_attempted";
|
|
6273
|
+
await this.sendKey(target, "Enter");
|
|
6274
|
+
deliveryPhase = "completed";
|
|
6275
|
+
}
|
|
6276
|
+
this.detector.resolvePrompt(promptId);
|
|
6277
|
+
logger.info("Sent approval reply via tmux", {
|
|
6278
|
+
promptId,
|
|
6279
|
+
optionNumber,
|
|
6280
|
+
keysSent: keys,
|
|
6281
|
+
hasFollowUpText: Boolean(followUpText),
|
|
6282
|
+
deliveryPhase
|
|
6283
|
+
});
|
|
6284
|
+
return { ok: true, deliveryPhase: "completed" };
|
|
6285
|
+
} catch (error) {
|
|
6286
|
+
return {
|
|
6287
|
+
ok: false,
|
|
6288
|
+
reason: "tmux-failed",
|
|
6289
|
+
details: error instanceof Error ? error.message : String(error),
|
|
6290
|
+
deliveryPhase
|
|
6291
|
+
};
|
|
6292
|
+
}
|
|
6293
|
+
}
|
|
6294
|
+
try {
|
|
6295
|
+
await this.typeLiteral(target, optionNumber);
|
|
6296
|
+
await delay(50);
|
|
6297
|
+
await this.sendKey(target, "Enter");
|
|
6298
|
+
if (followUpText && followUpText.trim().length > 0) {
|
|
6299
|
+
await delay(250);
|
|
6300
|
+
const cleanText = followUpText.replace(/[\r\n]+/g, " ").trim();
|
|
6301
|
+
await this.typeLiteral(target, cleanText);
|
|
6302
|
+
await delay(50);
|
|
6303
|
+
await this.sendKey(target, "Enter");
|
|
6304
|
+
}
|
|
6305
|
+
return { ok: true };
|
|
6306
|
+
} catch (error) {
|
|
6307
|
+
return {
|
|
6308
|
+
ok: false,
|
|
6309
|
+
reason: "tmux-failed",
|
|
6310
|
+
details: error instanceof Error ? error.message : String(error)
|
|
6311
|
+
};
|
|
6312
|
+
}
|
|
6313
|
+
}
|
|
6314
|
+
async sendFreeFormWhenNoApprovalActive(text, beforeType) {
|
|
6315
|
+
const target = this.resolveTmuxTarget();
|
|
6316
|
+
if (!target) return { ok: false, reason: "no-tmux-target" };
|
|
6317
|
+
if (!this.paneObserver) {
|
|
6318
|
+
return {
|
|
6319
|
+
ok: false,
|
|
6320
|
+
reason: "prompt-probe-failed",
|
|
6321
|
+
details: "pane observer is required for safe free-form input"
|
|
6322
|
+
};
|
|
6323
|
+
}
|
|
6324
|
+
const chooserIsAbsent = async () => {
|
|
6325
|
+
const probe = await this.paneObserver.probeApprovalUIActive();
|
|
6326
|
+
if (!probe.probeSucceeded) {
|
|
6327
|
+
return { ok: false, reason: "prompt-probe-failed" };
|
|
6328
|
+
}
|
|
6329
|
+
if (probe.active) {
|
|
6330
|
+
return { ok: false, reason: "prompt-superseded" };
|
|
6331
|
+
}
|
|
6332
|
+
return null;
|
|
6333
|
+
};
|
|
6334
|
+
let preparationFailed = false;
|
|
6335
|
+
try {
|
|
6336
|
+
const beforeText = await chooserIsAbsent();
|
|
6337
|
+
if (beforeText) return beforeText;
|
|
6338
|
+
if (beforeType) {
|
|
6339
|
+
try {
|
|
6340
|
+
await beforeType();
|
|
6341
|
+
} catch (error) {
|
|
6342
|
+
preparationFailed = true;
|
|
6343
|
+
throw error;
|
|
6344
|
+
}
|
|
6345
|
+
const afterPreparation = await chooserIsAbsent();
|
|
6346
|
+
if (afterPreparation) return afterPreparation;
|
|
6347
|
+
}
|
|
6348
|
+
await this.typeLiteral(target, text);
|
|
6349
|
+
await delay(ENTER_DELAY_MS);
|
|
6350
|
+
const beforeEnter = await chooserIsAbsent();
|
|
6351
|
+
if (beforeEnter) return beforeEnter;
|
|
6352
|
+
await this.sendKey(target, "Enter");
|
|
6353
|
+
return { ok: true };
|
|
6354
|
+
} catch (error) {
|
|
6355
|
+
if (preparationFailed) throw error;
|
|
6356
|
+
return {
|
|
6357
|
+
ok: false,
|
|
6358
|
+
reason: "tmux-failed",
|
|
6359
|
+
details: error instanceof Error ? error.message : String(error)
|
|
6360
|
+
};
|
|
6361
|
+
}
|
|
6362
|
+
}
|
|
6363
|
+
/** Dismiss whichever approval UI is active without binding to a stale promptId. */
|
|
6364
|
+
async dismissActivePrompt() {
|
|
6365
|
+
const target = this.resolveTmuxTarget();
|
|
6366
|
+
if (!target) return { ok: false, reason: "no-tmux-target" };
|
|
6367
|
+
if (!this.paneObserver) {
|
|
6368
|
+
return { ok: false, reason: "prompt-probe-failed" };
|
|
6369
|
+
}
|
|
6370
|
+
try {
|
|
6371
|
+
const before = await this.paneObserver.probeApprovalUIActive();
|
|
6372
|
+
if (!before.probeSucceeded) return { ok: false, reason: "prompt-probe-failed" };
|
|
6373
|
+
if (!before.active) return { ok: true };
|
|
6374
|
+
if (before.ambiguous) {
|
|
6375
|
+
return {
|
|
6376
|
+
ok: false,
|
|
6377
|
+
reason: "ambiguous-prompt-active",
|
|
6378
|
+
details: "masked/ambiguous prompt cannot be dismissed via Escape from mobile"
|
|
6379
|
+
};
|
|
6380
|
+
}
|
|
6381
|
+
if (before.kind === "question") {
|
|
6382
|
+
let isActionableWizard = false;
|
|
6383
|
+
try {
|
|
6384
|
+
const snapshot = await this.paneObserver.captureSnapshot();
|
|
6385
|
+
if (snapshot) {
|
|
6386
|
+
const parsed = parseApprovalSnapshot(snapshot);
|
|
6387
|
+
if (parsed && parsed.kind === "question" && Object.keys(parsed.submitMap || {}).length > 0) {
|
|
6388
|
+
isActionableWizard = true;
|
|
6389
|
+
}
|
|
6390
|
+
}
|
|
6391
|
+
} catch {
|
|
6392
|
+
}
|
|
6393
|
+
if (!isActionableWizard) {
|
|
6394
|
+
return {
|
|
6395
|
+
ok: false,
|
|
6396
|
+
reason: "display-only-question-active",
|
|
6397
|
+
details: "active question prompt cannot be dismissed via Escape from mobile"
|
|
6398
|
+
};
|
|
6399
|
+
}
|
|
6400
|
+
}
|
|
6401
|
+
await this.sendKey(target, "Escape");
|
|
6402
|
+
await delay(250);
|
|
6403
|
+
const after = await this.paneObserver.probeApprovalUIActive();
|
|
6404
|
+
if (!after.probeSucceeded) return { ok: false, reason: "prompt-probe-failed" };
|
|
6405
|
+
if (after.active) {
|
|
6406
|
+
return {
|
|
6407
|
+
ok: false,
|
|
6408
|
+
reason: "prompt-superseded",
|
|
6409
|
+
details: "approval chooser remained active after Escape"
|
|
6410
|
+
};
|
|
6411
|
+
}
|
|
6412
|
+
return { ok: true };
|
|
6413
|
+
} catch (error) {
|
|
6414
|
+
return {
|
|
6415
|
+
ok: false,
|
|
6416
|
+
reason: "tmux-failed",
|
|
6417
|
+
details: error instanceof Error ? error.message : String(error)
|
|
6418
|
+
};
|
|
5842
6419
|
}
|
|
5843
6420
|
}
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
"TargetFile",
|
|
5851
|
-
"FilePath",
|
|
5852
|
-
"file_path",
|
|
5853
|
-
"filePath",
|
|
5854
|
-
"path",
|
|
5855
|
-
"DirectoryPath",
|
|
5856
|
-
"directory",
|
|
5857
|
-
"Directory"
|
|
5858
|
-
]) {
|
|
5859
|
-
const v = a[key];
|
|
5860
|
-
if (typeof v === "string" && v.trim().length > 0) {
|
|
5861
|
-
return stripWrappingQuotes2(v.trim());
|
|
5862
|
-
}
|
|
6421
|
+
// ─── Tmux primitives ────────────────────────────────────────────────────
|
|
6422
|
+
async typeLiteral(target, text) {
|
|
6423
|
+
const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, "\\$").replace(/`/g, "\\`");
|
|
6424
|
+
const safeTarget = escapeShellArg2(target);
|
|
6425
|
+
const cmd = `tmux send-keys -t '${safeTarget}' -l "${escaped}"`;
|
|
6426
|
+
await this.execFn(cmd);
|
|
5863
6427
|
}
|
|
5864
|
-
|
|
6428
|
+
async sendKey(target, key) {
|
|
6429
|
+
const safeTarget = escapeShellArg2(target);
|
|
6430
|
+
const cmd = `tmux send-keys -t '${safeTarget}' ${key}`;
|
|
6431
|
+
await this.execFn(cmd);
|
|
6432
|
+
}
|
|
6433
|
+
resolveTmuxTarget() {
|
|
6434
|
+
return this.tmuxTargetOverride ?? process.env.CODEVIBE_AGY_TMUX_TARGET ?? null;
|
|
6435
|
+
}
|
|
6436
|
+
};
|
|
6437
|
+
function escapeShellArg2(value) {
|
|
6438
|
+
return value.replace(/'/g, `'\\''`);
|
|
5865
6439
|
}
|
|
5866
|
-
function
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
6440
|
+
function isNamedKey(key) {
|
|
6441
|
+
return /^[A-Z][A-Za-z0-9]+$/.test(key);
|
|
6442
|
+
}
|
|
6443
|
+
function delay(ms) {
|
|
6444
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
6445
|
+
}
|
|
6446
|
+
|
|
6447
|
+
// src/mobile-prompt-dedupe.ts
|
|
6448
|
+
var DEFAULT_EXPIRY_MS = 15e3;
|
|
6449
|
+
var DEFAULT_MIN_FUZZY_ECHO_LENGTH = 16;
|
|
6450
|
+
var DEFAULT_MIN_FUZZY_ECHO_RATIO = 0.35;
|
|
6451
|
+
var MobilePromptDeduper = class {
|
|
6452
|
+
constructor(options = {}) {
|
|
6453
|
+
this.pendingBySession = /* @__PURE__ */ new Map();
|
|
6454
|
+
this.expiryMs = options.expiryMs ?? DEFAULT_EXPIRY_MS;
|
|
6455
|
+
this.minFuzzyEchoLength = options.minFuzzyEchoLength ?? DEFAULT_MIN_FUZZY_ECHO_LENGTH;
|
|
6456
|
+
this.minFuzzyEchoRatio = options.minFuzzyEchoRatio ?? DEFAULT_MIN_FUZZY_ECHO_RATIO;
|
|
6457
|
+
this.now = options.now ?? Date.now;
|
|
5874
6458
|
}
|
|
5875
|
-
|
|
5876
|
-
|
|
6459
|
+
/** Called by the MCP server immediately BEFORE prompt-responder
|
|
6460
|
+
* performs the tmux send-keys. Records the normalized text + timestamp
|
|
6461
|
+
* for future echo-suppression. */
|
|
6462
|
+
track(sessionId, prompt) {
|
|
6463
|
+
this.sweep();
|
|
6464
|
+
const normalized = this.normalize(prompt);
|
|
6465
|
+
if (!normalized) return;
|
|
6466
|
+
const entries = this.validEntries(sessionId);
|
|
6467
|
+
entries.push({ normalized, timestamp: this.now() });
|
|
6468
|
+
this.pendingBySession.set(sessionId, entries);
|
|
5877
6469
|
}
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
6470
|
+
/** Symmetric helper for callers that want to retract a tracked entry
|
|
6471
|
+
* WITHOUT consuming it via dedupe (e.g. responder bailed before send). */
|
|
6472
|
+
forget(sessionId, prompt) {
|
|
6473
|
+
this.sweep();
|
|
6474
|
+
const normalized = this.normalize(prompt);
|
|
6475
|
+
if (!normalized) return;
|
|
6476
|
+
let removed = false;
|
|
6477
|
+
const entries = this.validEntries(sessionId).filter((entry) => {
|
|
6478
|
+
if (!removed && entry.normalized === normalized) {
|
|
6479
|
+
removed = true;
|
|
6480
|
+
return false;
|
|
6481
|
+
}
|
|
6482
|
+
return true;
|
|
6483
|
+
});
|
|
6484
|
+
this.replaceEntries(sessionId, entries);
|
|
5884
6485
|
}
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
6486
|
+
/**
|
|
6487
|
+
* Called by event-mapper / MCP server immediately BEFORE forwarding a
|
|
6488
|
+
* USER_PROMPT to AppSync. If the echo matches a recently-tracked mobile
|
|
6489
|
+
* prompt, returns the match metadata + CONSUMES the tracked entry (so a
|
|
6490
|
+
* second similar echo doesn't suppress an actual second mobile prompt).
|
|
6491
|
+
* Returns null when no match → caller proceeds with the AppSync emit.
|
|
6492
|
+
*/
|
|
6493
|
+
consumeIfDuplicate(sessionId, echo) {
|
|
6494
|
+
this.sweep();
|
|
6495
|
+
const normalizedEcho = this.normalize(echo);
|
|
6496
|
+
if (!normalizedEcho) return null;
|
|
6497
|
+
let match = null;
|
|
6498
|
+
const entries = this.validEntries(sessionId).filter((entry) => {
|
|
6499
|
+
if (!match) {
|
|
6500
|
+
const matchType = this.matchType(entry.normalized, normalizedEcho);
|
|
6501
|
+
if (matchType) {
|
|
6502
|
+
match = {
|
|
6503
|
+
matchType,
|
|
6504
|
+
originalLength: entry.normalized.length,
|
|
6505
|
+
echoLength: normalizedEcho.length
|
|
6506
|
+
};
|
|
6507
|
+
return false;
|
|
6508
|
+
}
|
|
6509
|
+
}
|
|
6510
|
+
return true;
|
|
6511
|
+
});
|
|
6512
|
+
this.replaceEntries(sessionId, entries);
|
|
6513
|
+
return match;
|
|
6514
|
+
}
|
|
6515
|
+
/** Inspect without spending the token before durable native admission. */
|
|
6516
|
+
peekIfDuplicate(sessionId, echo) {
|
|
6517
|
+
const normalized = this.normalize(echo);
|
|
6518
|
+
if (!normalized) return null;
|
|
6519
|
+
for (const entry of this.validEntries(sessionId)) {
|
|
6520
|
+
const matchType = this.matchType(entry.normalized, normalized);
|
|
6521
|
+
if (matchType) return { matchType, originalLength: entry.normalized.length, echoLength: normalized.length };
|
|
5889
6522
|
}
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
6523
|
+
return null;
|
|
6524
|
+
}
|
|
6525
|
+
// ─── Internals ──────────────────────────────────────────────────────────
|
|
6526
|
+
sweep() {
|
|
6527
|
+
const cutoff = this.now() - this.expiryMs;
|
|
6528
|
+
for (const [sessId, entries] of this.pendingBySession.entries()) {
|
|
6529
|
+
const valid = entries.filter((entry) => entry.timestamp >= cutoff);
|
|
6530
|
+
if (valid.length > 0) {
|
|
6531
|
+
this.pendingBySession.set(sessId, valid);
|
|
6532
|
+
} else {
|
|
6533
|
+
this.pendingBySession.delete(sessId);
|
|
5894
6534
|
}
|
|
5895
6535
|
}
|
|
5896
6536
|
}
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
if (typeof questions === "string") {
|
|
5907
|
-
try {
|
|
5908
|
-
questions = JSON.parse(questions.trim());
|
|
5909
|
-
} catch {
|
|
6537
|
+
validEntries(sessionId) {
|
|
6538
|
+
const cutoff = this.now() - this.expiryMs;
|
|
6539
|
+
return (this.pendingBySession.get(sessionId) || []).filter((entry) => entry.timestamp >= cutoff);
|
|
6540
|
+
}
|
|
6541
|
+
replaceEntries(sessionId, entries) {
|
|
6542
|
+
if (entries.length > 0) {
|
|
6543
|
+
this.pendingBySession.set(sessionId, entries);
|
|
6544
|
+
} else {
|
|
6545
|
+
this.pendingBySession.delete(sessionId);
|
|
5910
6546
|
}
|
|
5911
6547
|
}
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
const rawOptions = first.options;
|
|
5916
|
-
const result = [];
|
|
5917
|
-
for (let i = 0; i < rawOptions.length; i++) {
|
|
5918
|
-
const opt = rawOptions[i];
|
|
5919
|
-
if (typeof opt === "string" && opt.trim().length > 0) {
|
|
5920
|
-
result.push({ number: String(i + 1), text: stripWrappingQuotes2(opt.trim()) });
|
|
5921
|
-
} else if (opt && typeof opt === "object" && typeof opt.text === "string") {
|
|
5922
|
-
result.push({
|
|
5923
|
-
number: String(opt.number ?? i + 1),
|
|
5924
|
-
text: stripWrappingQuotes2(String(opt.text).trim())
|
|
5925
|
-
});
|
|
5926
|
-
}
|
|
5927
|
-
}
|
|
5928
|
-
if (result.length > 0) return result;
|
|
6548
|
+
matchType(original, echo) {
|
|
6549
|
+
if (original === echo) {
|
|
6550
|
+
return "exact";
|
|
5929
6551
|
}
|
|
6552
|
+
const longEnough = echo.length >= this.minFuzzyEchoLength;
|
|
6553
|
+
const enoughOfOriginal = echo.length / original.length >= this.minFuzzyEchoRatio;
|
|
6554
|
+
if (longEnough && enoughOfOriginal && original.endsWith(echo)) {
|
|
6555
|
+
return "suffix";
|
|
6556
|
+
}
|
|
6557
|
+
return null;
|
|
5930
6558
|
}
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
6559
|
+
normalize(text) {
|
|
6560
|
+
return text.replace(/\s+/g, " ").trim();
|
|
6561
|
+
}
|
|
6562
|
+
};
|
|
6563
|
+
|
|
6564
|
+
// src/daemon-telemetry.ts
|
|
6565
|
+
var crypto2 = __toESM(require("crypto"));
|
|
6566
|
+
var fs6 = __toESM(require("fs"));
|
|
6567
|
+
var https = __toESM(require("https"));
|
|
6568
|
+
var os6 = __toESM(require("os"));
|
|
6569
|
+
var path7 = __toESM(require("path"));
|
|
6570
|
+
var MEASUREMENT_ID = "G-GS74YEQTB8";
|
|
6571
|
+
var API_SECRET = "lAfOF6OxRzSQ-NsLBRjhAg";
|
|
6572
|
+
var GA4_HOSTNAME = "www.google-analytics.com";
|
|
6573
|
+
var GA4_PATH = `/mp/collect?measurement_id=${MEASUREMENT_ID}&api_secret=${API_SECRET}`;
|
|
6574
|
+
var BEACON_TOTAL_TIMEOUT_MS = 800;
|
|
6575
|
+
function readPluginVersion() {
|
|
6576
|
+
try {
|
|
6577
|
+
const pkgPath = path7.resolve(__dirname, "..", "package.json");
|
|
6578
|
+
const raw = fs6.readFileSync(pkgPath, "utf-8");
|
|
6579
|
+
const pkg = JSON.parse(raw);
|
|
6580
|
+
if (typeof pkg.version === "string" && pkg.version.length > 0 && pkg.version.length < 30) {
|
|
6581
|
+
return pkg.version;
|
|
5940
6582
|
}
|
|
6583
|
+
} catch {
|
|
5941
6584
|
}
|
|
5942
|
-
return
|
|
6585
|
+
return "unknown";
|
|
5943
6586
|
}
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
}
|
|
5948
|
-
return s;
|
|
6587
|
+
var PLUGIN_VERSION = readPluginVersion();
|
|
6588
|
+
function getClientId() {
|
|
6589
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
6590
|
+
return crypto2.createHash("sha256").update(`${os6.hostname()}-${uid}`).digest("hex").substring(0, 36);
|
|
5949
6591
|
}
|
|
5950
|
-
function
|
|
5951
|
-
|
|
5952
|
-
const
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
return `${prefix}LS: ${firstLine || "directory listed"}`;
|
|
5958
|
-
case "GREP_SEARCH":
|
|
5959
|
-
return `${prefix}Grep: ${firstLine || "search executed"}`;
|
|
5960
|
-
case "VIEW_FILE":
|
|
5961
|
-
return `${prefix}Read: ${firstLine || "file read"}`;
|
|
5962
|
-
case "CODE_ACTION":
|
|
5963
|
-
return `${prefix}Edit: ${firstLine || "file edited"}`;
|
|
5964
|
-
case "SEARCH_WEB":
|
|
5965
|
-
return `${prefix}WebSearch: ${firstLine || "search executed"}`;
|
|
5966
|
-
case "GENERIC":
|
|
5967
|
-
return `${prefix}${toolName}: ${firstLine || "tool executed"}`;
|
|
5968
|
-
default:
|
|
5969
|
-
return `${prefix}${toolName} (${agyType}): ${firstLine || "unknown event"}`;
|
|
6592
|
+
function sanitizeErrorMessage(msg) {
|
|
6593
|
+
if (!msg) return "";
|
|
6594
|
+
const homedir5 = os6.homedir();
|
|
6595
|
+
let out = msg.replace(/[\n\r\t]/g, " ");
|
|
6596
|
+
if (homedir5 && homedir5.length > 0) {
|
|
6597
|
+
const safeHome = homedir5.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6598
|
+
out = out.replace(new RegExp(safeHome, "g"), "~");
|
|
5970
6599
|
}
|
|
6600
|
+
out = out.replace(/\/Users\/[^/\s"'`]+/g, "/Users/<user>").replace(/\/home\/[^/\s"'`]+/g, "/home/<user>").replace(/[A-Za-z]:\\Users\\[^\\\s"'`]+/gi, "C:\\Users\\<user>").replace(/[^\x20-\x7E]/g, "");
|
|
6601
|
+
return out.trim().substring(0, 100);
|
|
5971
6602
|
}
|
|
5972
|
-
function
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
}
|
|
5982
|
-
|
|
6603
|
+
function fireDaemonBeacon(name, params) {
|
|
6604
|
+
return new Promise((resolve2) => {
|
|
6605
|
+
let req;
|
|
6606
|
+
let settled = false;
|
|
6607
|
+
const settle = () => {
|
|
6608
|
+
if (settled) return;
|
|
6609
|
+
settled = true;
|
|
6610
|
+
clearTimeout(overall);
|
|
6611
|
+
resolve2();
|
|
6612
|
+
};
|
|
6613
|
+
const overall = setTimeout(() => {
|
|
6614
|
+
try {
|
|
6615
|
+
req?.destroy();
|
|
6616
|
+
} catch {
|
|
6617
|
+
}
|
|
6618
|
+
settle();
|
|
6619
|
+
}, BEACON_TOTAL_TIMEOUT_MS);
|
|
6620
|
+
if (typeof overall.unref === "function") {
|
|
6621
|
+
overall.unref();
|
|
5983
6622
|
}
|
|
5984
|
-
|
|
5985
|
-
|
|
6623
|
+
try {
|
|
6624
|
+
const sanitizedParams = {};
|
|
6625
|
+
for (const [key, value] of Object.entries(params)) {
|
|
6626
|
+
if (typeof value === "string") {
|
|
6627
|
+
sanitizedParams[key] = sanitizeErrorMessage(value);
|
|
6628
|
+
} else {
|
|
6629
|
+
sanitizedParams[key] = value;
|
|
6630
|
+
}
|
|
6631
|
+
}
|
|
6632
|
+
const payload = JSON.stringify({
|
|
6633
|
+
client_id: getClientId(),
|
|
6634
|
+
events: [
|
|
6635
|
+
{
|
|
6636
|
+
name,
|
|
6637
|
+
params: {
|
|
6638
|
+
agent: "antigravity",
|
|
6639
|
+
plugin_version: PLUGIN_VERSION,
|
|
6640
|
+
platform: process.platform,
|
|
6641
|
+
source: process.env.CODEVIBE_TELEMETRY_SOURCE || "production",
|
|
6642
|
+
...sanitizedParams
|
|
6643
|
+
}
|
|
6644
|
+
}
|
|
6645
|
+
]
|
|
6646
|
+
});
|
|
6647
|
+
req = https.request(
|
|
6648
|
+
{
|
|
6649
|
+
hostname: GA4_HOSTNAME,
|
|
6650
|
+
path: GA4_PATH,
|
|
6651
|
+
method: "POST",
|
|
6652
|
+
headers: { "Content-Type": "application/json" },
|
|
6653
|
+
timeout: BEACON_TOTAL_TIMEOUT_MS
|
|
6654
|
+
},
|
|
6655
|
+
(res) => {
|
|
6656
|
+
res.resume();
|
|
6657
|
+
res.on("end", settle);
|
|
6658
|
+
res.on("close", settle);
|
|
6659
|
+
res.on("error", settle);
|
|
6660
|
+
}
|
|
6661
|
+
);
|
|
6662
|
+
req.on("error", settle);
|
|
6663
|
+
req.on("timeout", () => {
|
|
6664
|
+
try {
|
|
6665
|
+
req?.destroy();
|
|
6666
|
+
} catch {
|
|
6667
|
+
}
|
|
6668
|
+
settle();
|
|
6669
|
+
});
|
|
6670
|
+
req.on("close", settle);
|
|
6671
|
+
req.write(payload);
|
|
6672
|
+
req.end();
|
|
6673
|
+
} catch {
|
|
6674
|
+
settle();
|
|
6675
|
+
}
|
|
6676
|
+
});
|
|
5986
6677
|
}
|
|
5987
6678
|
|
|
5988
6679
|
// src/tool-activity-integration.ts
|
|
@@ -6437,6 +7128,20 @@ var McpServer = class _McpServer {
|
|
|
6437
7128
|
* bounded by TTL. (Codex Stage 2 R1 v12 HIGH.) */
|
|
6438
7129
|
this.observedMainConversationIds = /* @__PURE__ */ new Set();
|
|
6439
7130
|
this.hookRegistrationCache = null;
|
|
7131
|
+
/** Tracks generation numbers per conversation to invalidate in-flight async PreToolUse callbacks upon turn transition, Stop, or PostToolUse. (Codex S2-Codex-F6) */
|
|
7132
|
+
this.conversationGenerations = /* @__PURE__ */ new Map();
|
|
7133
|
+
/** Tracks turn transition and Stop generations per conversation to distinguish turn boundaries from tool completions. (Codex S2-Codex-F6, S2-Codex-R12-F2) */
|
|
7134
|
+
this.conversationTurnGenerations = /* @__PURE__ */ new Map();
|
|
7135
|
+
/** Monotonic sequence counter for hook lifecycle events (PreToolUse arrival, PostToolUse completion, Stop). (Claude R13-F2, Codex S2-Codex-R13-F1) */
|
|
7136
|
+
this.hookEventSeq = 0;
|
|
7137
|
+
/** Tracks Stop sequence per conversation to invalidate in-flight async bootstrap across stop events. (Codex S2-Codex-F6, Claude R13-N1) */
|
|
7138
|
+
this.conversationStopSeq = /* @__PURE__ */ new Map();
|
|
7139
|
+
/** Tracks completed PostToolUse deliveries per conversation with monotonic sequence watermarks to affirmatively correlate concurrent in-flight bootstrap work. (Codex S2-Codex-F6, S2-Codex-R13-F1, Claude R13-F2) */
|
|
7140
|
+
this.conversationCompletedPostDeliveries = /* @__PURE__ */ new Map();
|
|
7141
|
+
/** Tracks the latest turn initialNumSteps to filter stale in-flight or held deliveries from earlier turns. (Codex S2-Codex-F6) */
|
|
7142
|
+
this.conversationInitialNumSteps = /* @__PURE__ */ new Map();
|
|
7143
|
+
/** Bounded buffer for PreToolUse deliveries arriving while conversation routing is in 'hold'. (Codex S2-Codex-F6) */
|
|
7144
|
+
this.heldHookDeliveries = /* @__PURE__ */ new Map();
|
|
6440
7145
|
this.started = false;
|
|
6441
7146
|
/** Monotonically-increasing counter incremented on every start(). Captured
|
|
6442
7147
|
* by async callbacks at entry; mismatch after an await means stop()→start()
|
|
@@ -6563,8 +7268,16 @@ var McpServer = class _McpServer {
|
|
|
6563
7268
|
isClaimedElsewhere: (conversationId) => this.claims.isClaimedElsewhere(conversationId),
|
|
6564
7269
|
otherInvokedAt: (conversationId) => this.claims.otherInvokedAt(conversationId),
|
|
6565
7270
|
readTranscriptFirstType: (conversationId) => this.readConversationFirstType(conversationId),
|
|
6566
|
-
onRelease: (emits, decision) => {
|
|
7271
|
+
onRelease: (emits, decision, convId) => {
|
|
6567
7272
|
for (const emit of emits) this.dispatchTranscriptEmit(emit, decision);
|
|
7273
|
+
const targetConvId = convId ?? (emits.length > 0 ? emits[0].conversationId : void 0);
|
|
7274
|
+
if (targetConvId) {
|
|
7275
|
+
const turnStep = emits.find((e) => e.event?.type === "USER_INPUT")?.event?.step_index;
|
|
7276
|
+
const minStep = this.conversationInitialNumSteps.get(targetConvId) ?? (typeof turnStep === "number" ? turnStep : void 0);
|
|
7277
|
+
void this.drainHeldHookDeliveries(targetConvId, decision, minStep).catch((err) => {
|
|
7278
|
+
logger.warn("Failed draining held hook deliveries onRelease", { error: String(err) });
|
|
7279
|
+
});
|
|
7280
|
+
}
|
|
6568
7281
|
},
|
|
6569
7282
|
isModelOutput: (emit) => emit.event?.source === "MODEL",
|
|
6570
7283
|
turnStep: (emit) => emit.event?.type === "USER_INPUT" ? typeof emit.event.step_index === "number" ? emit.event.step_index : -1 : null,
|
|
@@ -6572,6 +7285,7 @@ var McpServer = class _McpServer {
|
|
|
6572
7285
|
onLog: (message, data) => logger.info(message, data),
|
|
6573
7286
|
holdMs: options.conversationHoldMs
|
|
6574
7287
|
});
|
|
7288
|
+
this.conversationHoldMs = options.conversationHoldMs;
|
|
6575
7289
|
this.httpApi.setHandlers({
|
|
6576
7290
|
pushEvent: (p) => this.handleHttpPushEvent(p),
|
|
6577
7291
|
getActiveSession: () => this.getAnySession(),
|
|
@@ -6581,6 +7295,9 @@ var McpServer = class _McpServer {
|
|
|
6581
7295
|
static {
|
|
6582
7296
|
this.E1_TTL_REFRESH_MS = 5 * 60 * 1e3;
|
|
6583
7297
|
}
|
|
7298
|
+
static {
|
|
7299
|
+
this.MAX_HELD_CONVERSATIONS = 50;
|
|
7300
|
+
}
|
|
6584
7301
|
// ─── Lifecycle ──────────────────────────────────────────────────────────
|
|
6585
7302
|
async start() {
|
|
6586
7303
|
if (this.started) throw new Error("McpServer.start() called twice");
|
|
@@ -6668,6 +7385,18 @@ var McpServer = class _McpServer {
|
|
|
6668
7385
|
logger.error("handlePendingPrompt failed", { error: String(err) });
|
|
6669
7386
|
});
|
|
6670
7387
|
});
|
|
7388
|
+
this.addListener(this.approvalDetector, "prompt-upgraded", (state) => {
|
|
7389
|
+
void this.handlePendingPrompt(state).catch((err) => {
|
|
7390
|
+
logger.error("handlePendingPrompt failed on prompt-upgraded", { error: String(err) });
|
|
7391
|
+
});
|
|
7392
|
+
});
|
|
7393
|
+
this.addListener(this.approvalDetector, "prompt-resolved", (promptId) => {
|
|
7394
|
+
const session = this.session;
|
|
7395
|
+
if (!session) return;
|
|
7396
|
+
void this.resolveE1PluginApproval(session.sessionId, promptId).catch((err) => {
|
|
7397
|
+
logger.warn("resolveE1PluginApproval failed on prompt-resolved", { promptId, error: String(err) });
|
|
7398
|
+
});
|
|
7399
|
+
});
|
|
6671
7400
|
this.launchSessionPromise = this.createLaunchSession();
|
|
6672
7401
|
await this.launchSessionPromise;
|
|
6673
7402
|
if (!this.started) {
|
|
@@ -6812,6 +7541,12 @@ var McpServer = class _McpServer {
|
|
|
6812
7541
|
await this.boundedTeardownStep("paneObserver.stop", () => this.paneObserver.stop());
|
|
6813
7542
|
await this.boundedTeardownStep("approvalDetector.stop", () => this.approvalDetector.stop());
|
|
6814
7543
|
await this.boundedTeardownStep("httpApi.stop", () => this.httpApi.stop());
|
|
7544
|
+
for (const [, list] of this.heldHookDeliveries) {
|
|
7545
|
+
for (const entry of list) {
|
|
7546
|
+
clearTimeout(entry.timer);
|
|
7547
|
+
}
|
|
7548
|
+
}
|
|
7549
|
+
this.heldHookDeliveries.clear();
|
|
6815
7550
|
this.removeOrphanedRuntimeDir();
|
|
6816
7551
|
try {
|
|
6817
7552
|
this.appSyncClient.cleanupSubscriptions();
|
|
@@ -7163,16 +7898,38 @@ var McpServer = class _McpServer {
|
|
|
7163
7898
|
));
|
|
7164
7899
|
}
|
|
7165
7900
|
// ─── agy hooks (F1) ─────────────────────────────────────────────────────
|
|
7901
|
+
dropHeldHookDeliveries(conversationId, reason) {
|
|
7902
|
+
const list = this.heldHookDeliveries.get(conversationId);
|
|
7903
|
+
if (!list) return;
|
|
7904
|
+
for (const entry of list) {
|
|
7905
|
+
clearTimeout(entry.timer);
|
|
7906
|
+
}
|
|
7907
|
+
this.heldHookDeliveries.delete(conversationId);
|
|
7908
|
+
logger.info("Dropped held hook deliveries", { conversationId, reason, count: list.length });
|
|
7909
|
+
}
|
|
7166
7910
|
/**
|
|
7167
7911
|
* One hook delivery from OUR agy (agy-hook.js only posts here when the
|
|
7168
7912
|
* launcher's inline CODEVIBE_AGY_HOOK_OWNER names this daemon's runtime
|
|
7169
7913
|
* dir). Updates routing state synchronously; network work runs afterwards so
|
|
7170
7914
|
* the HTTP reply — which agy is waiting for — goes out at once.
|
|
7171
7915
|
*/
|
|
7172
|
-
handleAgyHook(delivery) {
|
|
7916
|
+
async handleAgyHook(delivery) {
|
|
7173
7917
|
if (!this.started) return;
|
|
7174
7918
|
const { conversationId } = delivery;
|
|
7175
7919
|
if (delivery.event === "PreInvocation") {
|
|
7920
|
+
const isTurnStart = delivery.invocationNum === void 0 || delivery.invocationNum === 0;
|
|
7921
|
+
const wasAlreadyAdmitted = this.router.roleOf(conversationId) === "main" || this.observedMainConversationIds.has(conversationId);
|
|
7922
|
+
if (isTurnStart && wasAlreadyAdmitted) {
|
|
7923
|
+
this.conversationGenerations.set(conversationId, (this.conversationGenerations.get(conversationId) ?? 0) + 1);
|
|
7924
|
+
this.conversationTurnGenerations.set(conversationId, (this.conversationTurnGenerations.get(conversationId) ?? 0) + 1);
|
|
7925
|
+
this.approvalDetector.retireLiveWizard(conversationId);
|
|
7926
|
+
this.resolveLingeringQuestionPrompts(conversationId);
|
|
7927
|
+
this.dropHeldHookDeliveries(conversationId, "turn-transition");
|
|
7928
|
+
this.conversationCompletedPostDeliveries.delete(conversationId);
|
|
7929
|
+
}
|
|
7930
|
+
if (delivery.initialNumSteps !== void 0) {
|
|
7931
|
+
this.conversationInitialNumSteps.set(conversationId, delivery.initialNumSteps);
|
|
7932
|
+
}
|
|
7176
7933
|
const previousBound = this.router.getBound();
|
|
7177
7934
|
const { role, bound } = this.router.onPreInvocation({
|
|
7178
7935
|
conversationId,
|
|
@@ -7181,6 +7938,9 @@ var McpServer = class _McpServer {
|
|
|
7181
7938
|
});
|
|
7182
7939
|
if (role === "main") {
|
|
7183
7940
|
this.observedMainConversationIds.add(conversationId);
|
|
7941
|
+
if (!wasAlreadyAdmitted) {
|
|
7942
|
+
await this.drainHeldHookDeliveries(conversationId, "process", delivery.initialNumSteps);
|
|
7943
|
+
}
|
|
7184
7944
|
} else {
|
|
7185
7945
|
this.observedMainConversationIds.delete(conversationId);
|
|
7186
7946
|
}
|
|
@@ -7193,10 +7953,264 @@ var McpServer = class _McpServer {
|
|
|
7193
7953
|
}
|
|
7194
7954
|
return;
|
|
7195
7955
|
}
|
|
7196
|
-
|
|
7956
|
+
if (delivery.event === "Stop") {
|
|
7957
|
+
this.conversationGenerations.set(conversationId, (this.conversationGenerations.get(conversationId) ?? 0) + 1);
|
|
7958
|
+
this.conversationTurnGenerations.set(conversationId, (this.conversationTurnGenerations.get(conversationId) ?? 0) + 1);
|
|
7959
|
+
this.conversationStopSeq.set(conversationId, ++this.hookEventSeq);
|
|
7960
|
+
this.dropHeldHookDeliveries(conversationId, "stop-hook");
|
|
7961
|
+
this.conversationCompletedPostDeliveries.delete(conversationId);
|
|
7962
|
+
this.approvalDetector.retireLiveWizard(conversationId);
|
|
7963
|
+
this.resolveLingeringQuestionPrompts(conversationId);
|
|
7964
|
+
return;
|
|
7965
|
+
}
|
|
7966
|
+
if (delivery.event === "PreToolUse") {
|
|
7967
|
+
void this.handlePreToolUse(delivery).catch((error) => {
|
|
7968
|
+
logger.warn("PreToolUse hook handling failed", { error: String(error) });
|
|
7969
|
+
});
|
|
7970
|
+
return;
|
|
7971
|
+
}
|
|
7972
|
+
const tn = (delivery.toolName ?? "").toLowerCase().trim();
|
|
7973
|
+
const isQuestionTool = ["ask_question", "question", "ask"].includes(tn);
|
|
7974
|
+
if (isQuestionTool) {
|
|
7975
|
+
this.conversationGenerations.set(delivery.conversationId, (this.conversationGenerations.get(delivery.conversationId) ?? 0) + 1);
|
|
7976
|
+
let list = this.conversationCompletedPostDeliveries.get(delivery.conversationId);
|
|
7977
|
+
if (!list) {
|
|
7978
|
+
list = [];
|
|
7979
|
+
this.conversationCompletedPostDeliveries.set(delivery.conversationId, list);
|
|
7980
|
+
}
|
|
7981
|
+
list.push({ toolName: delivery.toolName, stepIdx: delivery.stepIdx, toolArgs: delivery.toolArgs, seq: ++this.hookEventSeq });
|
|
7982
|
+
this.dropHeldHookDeliveries(delivery.conversationId, "post-tool-use-completion");
|
|
7983
|
+
this.approvalDetector.retireLiveWizardMatching(delivery.conversationId, delivery);
|
|
7984
|
+
}
|
|
7197
7985
|
void this.resolvePromptsForCompletedTool(delivery).catch((error) => {
|
|
7198
7986
|
logger.warn("PostToolUse prompt resolution failed", { error: String(error) });
|
|
7199
7987
|
});
|
|
7988
|
+
this.router.onPostToolUse(conversationId);
|
|
7989
|
+
}
|
|
7990
|
+
resolveLingeringQuestionPrompts(conversationId) {
|
|
7991
|
+
const lingeringPrompts = this.approvalDetector.getPendingPromptsForConversation(conversationId).filter((p) => p.kind === "question");
|
|
7992
|
+
for (const p of lingeringPrompts) {
|
|
7993
|
+
this.approvalDetector.resolvePrompt(p.promptId);
|
|
7994
|
+
if (this.session) {
|
|
7995
|
+
void this.resolveE1PluginApproval(this.session.sessionId, p.promptId).catch(() => {
|
|
7996
|
+
});
|
|
7997
|
+
}
|
|
7998
|
+
}
|
|
7999
|
+
}
|
|
8000
|
+
async drainHeldHookDeliveries(conversationId, decision, initialNumSteps) {
|
|
8001
|
+
const list = this.heldHookDeliveries.get(conversationId);
|
|
8002
|
+
if (!list || list.length === 0) return;
|
|
8003
|
+
for (const entry of list) {
|
|
8004
|
+
clearTimeout(entry.timer);
|
|
8005
|
+
}
|
|
8006
|
+
this.heldHookDeliveries.delete(conversationId);
|
|
8007
|
+
if (decision === "drop") {
|
|
8008
|
+
logger.info("Disposing held hook deliveries for foreign conversation", {
|
|
8009
|
+
conversationId,
|
|
8010
|
+
count: list.length
|
|
8011
|
+
});
|
|
8012
|
+
return;
|
|
8013
|
+
}
|
|
8014
|
+
logger.info("Replaying held hook deliveries after affirmative admission", {
|
|
8015
|
+
conversationId,
|
|
8016
|
+
count: list.length,
|
|
8017
|
+
decision
|
|
8018
|
+
});
|
|
8019
|
+
const now = Date.now();
|
|
8020
|
+
const HELD_TTL_MS = 3e4;
|
|
8021
|
+
const capturedGen = this.conversationGenerations.get(conversationId) ?? 0;
|
|
8022
|
+
for (const entry of list) {
|
|
8023
|
+
if (now - entry.receivedAt > HELD_TTL_MS) {
|
|
8024
|
+
logger.info("Discarding expired held hook delivery", {
|
|
8025
|
+
conversationId,
|
|
8026
|
+
toolName: entry.delivery.toolName,
|
|
8027
|
+
ageMs: now - entry.receivedAt
|
|
8028
|
+
});
|
|
8029
|
+
continue;
|
|
8030
|
+
}
|
|
8031
|
+
if (initialNumSteps !== void 0 && entry.delivery.stepIdx !== void 0 && entry.delivery.stepIdx < initialNumSteps - 1) {
|
|
8032
|
+
logger.info("Discarding held delivery from prior step before initialNumSteps", {
|
|
8033
|
+
conversationId,
|
|
8034
|
+
stepIdx: entry.delivery.stepIdx,
|
|
8035
|
+
initialNumSteps
|
|
8036
|
+
});
|
|
8037
|
+
continue;
|
|
8038
|
+
}
|
|
8039
|
+
if (capturedGen !== (this.conversationGenerations.get(conversationId) ?? 0)) {
|
|
8040
|
+
logger.info("Aborting drain of held hook delivery due to generation advance", {
|
|
8041
|
+
conversationId
|
|
8042
|
+
});
|
|
8043
|
+
break;
|
|
8044
|
+
}
|
|
8045
|
+
try {
|
|
8046
|
+
await this.handlePreToolUse(entry.delivery);
|
|
8047
|
+
} catch (err) {
|
|
8048
|
+
logger.warn("Failed replaying held hook delivery", { error: String(err) });
|
|
8049
|
+
}
|
|
8050
|
+
}
|
|
8051
|
+
}
|
|
8052
|
+
async handlePreToolUse(delivery) {
|
|
8053
|
+
if (!this.started || !delivery.toolName) return;
|
|
8054
|
+
logger.info("PreToolUse hook delivery received", {
|
|
8055
|
+
conversationId: delivery.conversationId,
|
|
8056
|
+
toolName: delivery.toolName,
|
|
8057
|
+
stepIdx: delivery.stepIdx
|
|
8058
|
+
});
|
|
8059
|
+
const tn = delivery.toolName.toLowerCase().trim();
|
|
8060
|
+
if (tn === "ask_question" || tn === "question" || tn === "ask") {
|
|
8061
|
+
const capturedTurnGen = this.conversationTurnGenerations.get(delivery.conversationId) ?? 0;
|
|
8062
|
+
const startedSeq = ++this.hookEventSeq;
|
|
8063
|
+
await this.ensureLaunchSession();
|
|
8064
|
+
if (!this.session && !this.sessionDisabled && !this.launchKeyUnavailable) {
|
|
8065
|
+
for (let attempt = 0; attempt < 2 && !this.session; attempt++) {
|
|
8066
|
+
await this.ensureLaunchSession();
|
|
8067
|
+
if (this.sessionDisabled || this.launchKeyUnavailable) break;
|
|
8068
|
+
if (!this.session) await new Promise((r) => setTimeout(r, 150));
|
|
8069
|
+
}
|
|
8070
|
+
}
|
|
8071
|
+
if (this.sessionDisabled || this.launchKeyUnavailable) {
|
|
8072
|
+
logger.warn("PreToolUse dropped due to terminal session state", {
|
|
8073
|
+
conversationId: delivery.conversationId
|
|
8074
|
+
});
|
|
8075
|
+
return;
|
|
8076
|
+
}
|
|
8077
|
+
const completedPosts = this.conversationCompletedPostDeliveries.get(delivery.conversationId) ?? [];
|
|
8078
|
+
const qText = extractQuestionFromArgs(delivery.toolName ?? "ask_question", delivery.toolArgs ?? {});
|
|
8079
|
+
const structQuestions = extractAllQuestionsFromArgs(delivery.toolName ?? "ask_question", delivery.toolArgs ?? {});
|
|
8080
|
+
const candidateCall = {
|
|
8081
|
+
intentStepIndex: delivery.stepIdx,
|
|
8082
|
+
questionText: qText,
|
|
8083
|
+
structuredQuestions: structQuestions,
|
|
8084
|
+
rawToolName: delivery.toolName ?? "ask_question"
|
|
8085
|
+
};
|
|
8086
|
+
const isAlreadyCompleted = completedPosts.some(
|
|
8087
|
+
(post) => post.seq > startedSeq && correlatesQuestionInvocation(candidateCall, post)
|
|
8088
|
+
);
|
|
8089
|
+
if (isAlreadyCompleted) {
|
|
8090
|
+
logger.info("PreToolUse already completed during async bootstrap await", {
|
|
8091
|
+
conversationId: delivery.conversationId,
|
|
8092
|
+
stepIdx: delivery.stepIdx
|
|
8093
|
+
});
|
|
8094
|
+
return;
|
|
8095
|
+
}
|
|
8096
|
+
const stoppedSeq = this.conversationStopSeq.get(delivery.conversationId) ?? 0;
|
|
8097
|
+
if (stoppedSeq > startedSeq) {
|
|
8098
|
+
logger.info("PreToolUse invalidated by Stop during async bootstrap await", {
|
|
8099
|
+
conversationId: delivery.conversationId,
|
|
8100
|
+
stepIdx: delivery.stepIdx
|
|
8101
|
+
});
|
|
8102
|
+
return;
|
|
8103
|
+
}
|
|
8104
|
+
const currentTurnGen = this.conversationTurnGenerations.get(delivery.conversationId) ?? 0;
|
|
8105
|
+
if (capturedTurnGen !== currentTurnGen) {
|
|
8106
|
+
const minStep2 = this.conversationInitialNumSteps.get(delivery.conversationId);
|
|
8107
|
+
const isCurrentTurn = minStep2 !== void 0 && delivery.stepIdx !== void 0 && delivery.stepIdx >= minStep2 - 1;
|
|
8108
|
+
if (!isCurrentTurn) {
|
|
8109
|
+
logger.info("PreToolUse invalidated by turn transition during async bootstrap await", {
|
|
8110
|
+
conversationId: delivery.conversationId,
|
|
8111
|
+
capturedTurnGen,
|
|
8112
|
+
currentTurnGen,
|
|
8113
|
+
stepIdx: delivery.stepIdx,
|
|
8114
|
+
minStep: minStep2
|
|
8115
|
+
});
|
|
8116
|
+
return;
|
|
8117
|
+
}
|
|
8118
|
+
logger.info("PreToolUse correlated with admitted current turn across bootstrap await", {
|
|
8119
|
+
conversationId: delivery.conversationId,
|
|
8120
|
+
stepIdx: delivery.stepIdx,
|
|
8121
|
+
minStep: minStep2
|
|
8122
|
+
});
|
|
8123
|
+
}
|
|
8124
|
+
const minStep = this.conversationInitialNumSteps.get(delivery.conversationId);
|
|
8125
|
+
if (minStep !== void 0 && delivery.stepIdx !== void 0 && delivery.stepIdx < minStep - 1) {
|
|
8126
|
+
logger.info("PreToolUse dropped as stale prior step before initialNumSteps", {
|
|
8127
|
+
conversationId: delivery.conversationId,
|
|
8128
|
+
stepIdx: delivery.stepIdx,
|
|
8129
|
+
minStep
|
|
8130
|
+
});
|
|
8131
|
+
return;
|
|
8132
|
+
}
|
|
8133
|
+
const decision = this.router.route(delivery.conversationId);
|
|
8134
|
+
if (decision === "drop") {
|
|
8135
|
+
logger.debug("PreToolUse dropped for conversation outside session", {
|
|
8136
|
+
conversationId: delivery.conversationId
|
|
8137
|
+
});
|
|
8138
|
+
return;
|
|
8139
|
+
}
|
|
8140
|
+
if (decision === "hold") {
|
|
8141
|
+
if (!this.heldHookDeliveries.has(delivery.conversationId) && this.heldHookDeliveries.size >= _McpServer.MAX_HELD_CONVERSATIONS) {
|
|
8142
|
+
const oldestConv = this.heldHookDeliveries.keys().next().value;
|
|
8143
|
+
if (oldestConv) {
|
|
8144
|
+
this.dropHeldHookDeliveries(oldestConv, "global-capacity-exceeded");
|
|
8145
|
+
}
|
|
8146
|
+
}
|
|
8147
|
+
let held = this.heldHookDeliveries.get(delivery.conversationId);
|
|
8148
|
+
if (!held) {
|
|
8149
|
+
held = [];
|
|
8150
|
+
this.heldHookDeliveries.set(delivery.conversationId, held);
|
|
8151
|
+
}
|
|
8152
|
+
if (held.length < 10) {
|
|
8153
|
+
const timer = setTimeout(() => {
|
|
8154
|
+
this.dropHeldHookDeliveries(delivery.conversationId, "hold-timeout");
|
|
8155
|
+
}, this.conversationHoldMs ?? 3e4);
|
|
8156
|
+
held.push({ delivery, receivedAt: Date.now(), timer });
|
|
8157
|
+
}
|
|
8158
|
+
this.registerHeldConversationBeforeFirstHook(delivery.conversationId);
|
|
8159
|
+
return;
|
|
8160
|
+
}
|
|
8161
|
+
const structuredQuestions = extractAllQuestionsFromArgs(delivery.toolName, delivery.toolArgs);
|
|
8162
|
+
const questionText = extractQuestionFromArgs(delivery.toolName, delivery.toolArgs);
|
|
8163
|
+
const questionOptions = extractQuestionOptionsFromArgs(delivery.toolName, delivery.toolArgs);
|
|
8164
|
+
logger.info("PreToolUse received ask_question hook", {
|
|
8165
|
+
conversationId: delivery.conversationId,
|
|
8166
|
+
questionText: questionText ? questionText.slice(0, 100) : null,
|
|
8167
|
+
optionCount: questionOptions ? questionOptions.length : 0,
|
|
8168
|
+
structuredQuestionsCount: structuredQuestions ? structuredQuestions.length : 0
|
|
8169
|
+
});
|
|
8170
|
+
const pendingCall = {
|
|
8171
|
+
intentStepIndex: delivery.stepIdx ?? -1,
|
|
8172
|
+
toolCallIndex: 0,
|
|
8173
|
+
intentByteOffset: 0,
|
|
8174
|
+
toolType: "ASK_QUESTION",
|
|
8175
|
+
conversationId: delivery.conversationId,
|
|
8176
|
+
rawToolName: delivery.toolName,
|
|
8177
|
+
questionText,
|
|
8178
|
+
questionOptions,
|
|
8179
|
+
structuredQuestions,
|
|
8180
|
+
startedAt: Date.now(),
|
|
8181
|
+
intentCreatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8182
|
+
};
|
|
8183
|
+
this.approvalDetector.registerPendingCall(pendingCall);
|
|
8184
|
+
this.emitQuestionPromptFromHook(delivery.conversationId, pendingCall);
|
|
8185
|
+
}
|
|
8186
|
+
}
|
|
8187
|
+
emitQuestionPromptFromHook(conversationId, pendingCall) {
|
|
8188
|
+
const decision = this.router.route(conversationId);
|
|
8189
|
+
if (decision !== "process" && decision !== "legacy") {
|
|
8190
|
+
logger.debug("emitQuestionPromptFromHook skipped for non-admitted conversation", { conversationId, decision });
|
|
8191
|
+
return;
|
|
8192
|
+
}
|
|
8193
|
+
let session = this.getSessionByConversation(conversationId);
|
|
8194
|
+
if (!session && this.session) {
|
|
8195
|
+
if (decision === "process" || decision === "legacy" && this.legacyAcceptsConversation(conversationId)) {
|
|
8196
|
+
this.observedMainConversationIds.add(conversationId);
|
|
8197
|
+
session = this.session;
|
|
8198
|
+
}
|
|
8199
|
+
}
|
|
8200
|
+
if (!session) {
|
|
8201
|
+
logger.warn("PreToolUse ask_question hook fired but conversation not owned or no session known", {
|
|
8202
|
+
conversationId,
|
|
8203
|
+
decision
|
|
8204
|
+
});
|
|
8205
|
+
return;
|
|
8206
|
+
}
|
|
8207
|
+
const e1rec = this.newE1Raise(session.sessionId, false);
|
|
8208
|
+
const emitted = this.approvalDetector.emitHookQuestionPrompt(pendingCall, e1rec.promptId);
|
|
8209
|
+
if (!emitted) {
|
|
8210
|
+
this.e1PromptRaises.delete(e1rec.promptId);
|
|
8211
|
+
} else if (emitted.promptId !== e1rec.promptId) {
|
|
8212
|
+
this.e1PromptRaises.delete(e1rec.promptId);
|
|
8213
|
+
}
|
|
7200
8214
|
}
|
|
7201
8215
|
/**
|
|
7202
8216
|
* A tool our agy just ran was blocked on its approval prompt, so a pending
|
|
@@ -7206,8 +8220,14 @@ var McpServer = class _McpServer {
|
|
|
7206
8220
|
async resolvePromptsForCompletedTool(delivery) {
|
|
7207
8221
|
const session = this.session;
|
|
7208
8222
|
if (!session) return;
|
|
7209
|
-
const pending = this.approvalDetector.getPendingPromptsForConversation();
|
|
7210
|
-
const matched = pending.filter((state) =>
|
|
8223
|
+
const pending = this.approvalDetector.getPendingPromptsForConversation(delivery.conversationId);
|
|
8224
|
+
const matched = pending.filter((state) => {
|
|
8225
|
+
if (state.conversationId !== delivery.conversationId) return false;
|
|
8226
|
+
if (state.kind === "question") {
|
|
8227
|
+
return correlatesQuestionInvocation(state.pendingCall, delivery);
|
|
8228
|
+
}
|
|
8229
|
+
return promptMatchesCompletedTool(state, delivery.toolName, delivery.toolArgs);
|
|
8230
|
+
});
|
|
7211
8231
|
logger.info("PostToolUse checked pending prompts", {
|
|
7212
8232
|
conversationId: delivery.conversationId,
|
|
7213
8233
|
toolName: delivery.toolName ?? null,
|
|
@@ -8235,11 +9255,14 @@ var McpServer = class _McpServer {
|
|
|
8235
9255
|
const e1rec = this.newE1Raise(session.sessionId, isActionable);
|
|
8236
9256
|
const emitted = this.approvalDetector.emitPaneOnlyPrompt(parsed, activeConvId, e1rec.promptId);
|
|
8237
9257
|
if (emitted) {
|
|
9258
|
+
if (emitted.promptId !== e1rec.promptId) {
|
|
9259
|
+
this.e1PromptRaises.delete(e1rec.promptId);
|
|
9260
|
+
}
|
|
8238
9261
|
if (cand.snapshotHash) {
|
|
8239
|
-
this.e1ContentKeyIndex.set(`${session.sessionId}::${cand.snapshotHash}`,
|
|
9262
|
+
this.e1ContentKeyIndex.set(`${session.sessionId}::${cand.snapshotHash}`, emitted.promptId);
|
|
8240
9263
|
}
|
|
8241
9264
|
logger.info("Pane prompt identity", {
|
|
8242
|
-
promptId:
|
|
9265
|
+
promptId: emitted.promptId,
|
|
8243
9266
|
conversationId: activeConvId,
|
|
8244
9267
|
commandDigest: commandDigest(parsed.command)
|
|
8245
9268
|
});
|
|
@@ -8283,8 +9306,16 @@ var McpServer = class _McpServer {
|
|
|
8283
9306
|
}
|
|
8284
9307
|
const isDisplayOnly = Object.keys(state.submitMap || {}).length === 0;
|
|
8285
9308
|
const e1existing = this.e1PromptRaises.get(state.promptId);
|
|
8286
|
-
|
|
8287
|
-
if (
|
|
9309
|
+
let e1rec;
|
|
9310
|
+
if (e1existing) {
|
|
9311
|
+
e1rec = e1existing.record;
|
|
9312
|
+
e1rec.mobileActionable = !isDisplayOnly;
|
|
9313
|
+
} else {
|
|
9314
|
+
e1rec = this.newE1Raise(session.sessionId, !isDisplayOnly);
|
|
9315
|
+
const oldPromptId = state.promptId;
|
|
9316
|
+
state.promptId = e1rec.promptId;
|
|
9317
|
+
this.approvalDetector.rekeyPrompt(oldPromptId, e1rec.promptId);
|
|
9318
|
+
}
|
|
8288
9319
|
const resolvedOptions = state.paneOptions && state.paneOptions.length > 0 ? state.paneOptions : state.pendingCall?.questionOptions && state.pendingCall.questionOptions.length > 0 ? state.pendingCall.questionOptions : void 0;
|
|
8289
9320
|
const optionsArr = isDisplayOnly ? [] : resolvedOptions ? resolvedOptions.map((o) => ({ number: o.number, text: o.text })) : Object.keys(state.submitMap).map((n) => ({ number: n }));
|
|
8290
9321
|
const diffParsed = parseFencedDiffFromBody(
|
|
@@ -8317,12 +9348,29 @@ var McpServer = class _McpServer {
|
|
|
8317
9348
|
file_path: filePath,
|
|
8318
9349
|
options: optionsArr,
|
|
8319
9350
|
...toolInput ? { tool_input: toolInput } : {},
|
|
8320
|
-
...state.kind === "question"
|
|
8321
|
-
questions:
|
|
8322
|
-
|
|
8323
|
-
|
|
8324
|
-
|
|
8325
|
-
|
|
9351
|
+
...state.kind === "question" ? {
|
|
9352
|
+
questions: (() => {
|
|
9353
|
+
if (!state.pendingCall?.structuredQuestions || state.pendingCall.structuredQuestions.length === 0) {
|
|
9354
|
+
return resolvedOptions && resolvedOptions.length > 0 ? [{
|
|
9355
|
+
question: state.pendingCall?.questionText || state.matchedPaneHeader || state.paneDisplayHeader || "Question",
|
|
9356
|
+
options: resolvedOptions,
|
|
9357
|
+
...isDisplayOnly ? { instructions: "Answer this question in your desktop terminal" } : {}
|
|
9358
|
+
}] : void 0;
|
|
9359
|
+
}
|
|
9360
|
+
const activeQuestionText = state.matchedPaneHeader || state.paneDisplayHeader || state.pendingCall?.questionText || "";
|
|
9361
|
+
let activeIdx = state.wizardStepIndex !== void 0 ? state.wizardStepIndex : state.pendingCall.structuredQuestions.findIndex((sq) => questionsMatch(sq.question, activeQuestionText));
|
|
9362
|
+
if (activeIdx < 0 || activeIdx >= state.pendingCall.structuredQuestions.length) activeIdx = 0;
|
|
9363
|
+
const activeQ = state.pendingCall.structuredQuestions[activeIdx] ?? {
|
|
9364
|
+
question: activeQuestionText || "Question",
|
|
9365
|
+
options: resolvedOptions || []
|
|
9366
|
+
};
|
|
9367
|
+
return [{
|
|
9368
|
+
question: activeQ.question,
|
|
9369
|
+
options: resolvedOptions && resolvedOptions.length > 0 ? resolvedOptions : activeQ.options,
|
|
9370
|
+
...activeQ.isMultiSelect !== void 0 ? { is_multi_select: activeQ.isMultiSelect } : {},
|
|
9371
|
+
...isDisplayOnly ? { instructions: "Answer this question in your desktop terminal" } : {}
|
|
9372
|
+
}];
|
|
9373
|
+
})()
|
|
8326
9374
|
} : {}
|
|
8327
9375
|
} : { promptId: state.promptId };
|
|
8328
9376
|
const content = state.paneDisplayHeader || state.matchedPaneHeader || state.pendingCall?.command || state.pendingCall?.filePath || "approval requested";
|
|
@@ -8545,22 +9593,119 @@ var McpServer = class _McpServer {
|
|
|
8545
9593
|
}
|
|
8546
9594
|
}
|
|
8547
9595
|
await this.trackDurableMobileEcho(session, evt, intent.option, "primary");
|
|
8548
|
-
const
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
9596
|
+
const isQuestionPrompt = activePrompt.kind === "question";
|
|
9597
|
+
if (isQuestionPrompt) {
|
|
9598
|
+
let armedWriteInEcho = false;
|
|
9599
|
+
const optionResult = intent.followUpText ? await this.promptResponder.sendApprovalReply(
|
|
9600
|
+
activePrompt.promptId,
|
|
9601
|
+
intent.option,
|
|
9602
|
+
intent.followUpText,
|
|
9603
|
+
async () => {
|
|
9604
|
+
armedWriteInEcho = true;
|
|
9605
|
+
await this.trackDurableMobileEcho(session, evt, intent.followUpText, "free_form");
|
|
9606
|
+
}
|
|
9607
|
+
) : await this.promptResponder.sendApprovalReply(
|
|
9608
|
+
activePrompt.promptId,
|
|
9609
|
+
intent.option
|
|
9610
|
+
);
|
|
9611
|
+
if (!optionResult.ok) {
|
|
9612
|
+
if (optionResult.reason === "write-in-text-required") {
|
|
9613
|
+
await this.emitPromptSafetyNotification(
|
|
9614
|
+
session,
|
|
9615
|
+
optionResult.details || 'Please provide your write-in text along with the option number (e.g. "4, your response").'
|
|
9616
|
+
);
|
|
9617
|
+
} else if (optionResult.reason === "write-in-already-active") {
|
|
9618
|
+
await this.emitPromptSafetyNotification(
|
|
9619
|
+
session,
|
|
9620
|
+
optionResult.details || "Write-in input field is already open on desktop; finish or cancel on desktop before selecting an option."
|
|
9621
|
+
);
|
|
9622
|
+
} else if (optionResult.reason === "follow-up-not-allowed") {
|
|
9623
|
+
await this.emitPromptSafetyNotification(
|
|
9624
|
+
session,
|
|
9625
|
+
optionResult.details || `Option ${intent.option} does not accept follow-up text.`
|
|
9626
|
+
);
|
|
9627
|
+
} else {
|
|
9628
|
+
const phase = optionResult.deliveryPhase ?? "none";
|
|
9629
|
+
let message;
|
|
9630
|
+
if (phase !== "none") {
|
|
9631
|
+
message = "Your answer was partially entered on desktop. Please complete or cancel the prompt directly on desktop.";
|
|
9632
|
+
} else if (optionResult.reason === "prompt-expired" || optionResult.reason === "prompt-superseded") {
|
|
9633
|
+
message = "The question prompt on desktop has expired or changed. Please review on desktop.";
|
|
9634
|
+
} else if (optionResult.reason === "prompt-probe-failed") {
|
|
9635
|
+
message = "Write-in input field was not ready on desktop. Please try again or respond on desktop.";
|
|
9636
|
+
} else if (optionResult.reason === "no-tmux-target" || optionResult.reason === "tmux-failed") {
|
|
9637
|
+
message = optionResult.details || "Desktop terminal session is unreachable. Please respond directly on desktop.";
|
|
9638
|
+
} else if (optionResult.reason === "display-only-question-active") {
|
|
9639
|
+
message = "This question requires desktop input. Please respond directly on desktop.";
|
|
9640
|
+
} else if (optionResult.reason === "invalid-option") {
|
|
9641
|
+
message = optionResult.details || `Option '${intent.option}' is not valid for the current question.`;
|
|
9642
|
+
} else {
|
|
9643
|
+
message = optionResult.details || `Failed to submit response to question prompt (${optionResult.reason}).`;
|
|
9644
|
+
}
|
|
9645
|
+
await this.emitPromptSafetyNotification(session, message);
|
|
9646
|
+
}
|
|
9647
|
+
if (!(0, import_codevibe_core7.requiresSyncInputDeliveryReceipt)(evt)) {
|
|
9648
|
+
const phase = optionResult.deliveryPhase ?? "none";
|
|
9649
|
+
if (phase === "none") {
|
|
9650
|
+
this.mobileDeduper.forget(session.sessionId, intent.option);
|
|
9651
|
+
if (armedWriteInEcho && intent.followUpText) {
|
|
9652
|
+
this.mobileDeduper.forget(session.sessionId, intent.followUpText);
|
|
9653
|
+
}
|
|
9654
|
+
} else if (phase === "option_sent" && armedWriteInEcho && intent.followUpText) {
|
|
9655
|
+
this.mobileDeduper.forget(session.sessionId, intent.followUpText);
|
|
9656
|
+
}
|
|
9657
|
+
}
|
|
9658
|
+
return;
|
|
9659
|
+
}
|
|
9660
|
+
await this.resolveE1PluginApproval(session.sessionId, activePrompt.promptId);
|
|
9661
|
+
await this.markExecuted(evt);
|
|
8554
9662
|
return;
|
|
8555
|
-
}
|
|
8556
|
-
|
|
8557
|
-
|
|
9663
|
+
} else {
|
|
9664
|
+
const optionResult = await this.promptResponder.sendApprovalReply(
|
|
9665
|
+
activePrompt.promptId,
|
|
9666
|
+
intent.option
|
|
9667
|
+
);
|
|
9668
|
+
if (!optionResult.ok) {
|
|
9669
|
+
if (!(0, import_codevibe_core7.requiresSyncInputDeliveryReceipt)(evt)) {
|
|
9670
|
+
const phase = optionResult.deliveryPhase ?? "none";
|
|
9671
|
+
if (phase === "none") {
|
|
9672
|
+
this.mobileDeduper.forget(session.sessionId, intent.option);
|
|
9673
|
+
}
|
|
9674
|
+
}
|
|
9675
|
+
return;
|
|
9676
|
+
}
|
|
9677
|
+
await this.resolveE1PluginApproval(session.sessionId, activePrompt.promptId);
|
|
9678
|
+
if (intent.followUpText) {
|
|
9679
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
9680
|
+
const freeFormResult = await this.promptResponder.sendFreeFormWhenNoApprovalActive(
|
|
9681
|
+
intent.followUpText,
|
|
9682
|
+
() => this.trackDurableMobileEcho(session, evt, intent.followUpText, "free_form")
|
|
9683
|
+
);
|
|
9684
|
+
if (!freeFormResult.ok) {
|
|
9685
|
+
if (!(0, import_codevibe_core7.requiresSyncInputDeliveryReceipt)(evt)) {
|
|
9686
|
+
this.mobileDeduper.forget(session.sessionId, intent.followUpText);
|
|
9687
|
+
}
|
|
9688
|
+
logger.warn("Follow-up text send failed for approval", { reason: freeFormResult.reason });
|
|
9689
|
+
return;
|
|
9690
|
+
}
|
|
9691
|
+
}
|
|
8558
9692
|
await this.markExecuted(evt);
|
|
8559
9693
|
return;
|
|
8560
9694
|
}
|
|
8561
|
-
promptContent = intent.followUpText;
|
|
8562
|
-
await new Promise((resolve2) => setTimeout(resolve2, 250));
|
|
8563
9695
|
} else {
|
|
9696
|
+
const activeQuestionPrompt = pendingPrompts.find((p) => p.kind === "question");
|
|
9697
|
+
if (activeQuestionPrompt) {
|
|
9698
|
+
const isDisplayOnly = Object.keys(activeQuestionPrompt.submitMap || {}).length === 0;
|
|
9699
|
+
logger.warn("Non-option input received while an interactive question prompt is active; rejecting dismissal", {
|
|
9700
|
+
sessionId: session.sessionId,
|
|
9701
|
+
promptId: activeQuestionPrompt.promptId,
|
|
9702
|
+
isDisplayOnly,
|
|
9703
|
+
content: promptContent.slice(0, 64)
|
|
9704
|
+
});
|
|
9705
|
+
const message = isDisplayOnly ? "The active terminal question prompt is display-only on mobile. Please select an option directly in your terminal." : "An interactive question prompt is active in your terminal. Please select one of the advertised options or answer directly in your terminal.";
|
|
9706
|
+
await this.emitPromptSafetyNotification(session, message);
|
|
9707
|
+
return;
|
|
9708
|
+
}
|
|
8564
9709
|
const hasDisplayOnlyPrompt = pendingPrompts.some(
|
|
8565
9710
|
(p) => Object.keys(p.submitMap || {}).length === 0
|
|
8566
9711
|
);
|
|
@@ -9230,6 +10375,7 @@ var __testing = {
|
|
|
9230
10375
|
0 && (module.exports = {
|
|
9231
10376
|
ApprovalDetector,
|
|
9232
10377
|
McpServer,
|
|
10378
|
+
PromptResponder,
|
|
9233
10379
|
SessionNotFoundError,
|
|
9234
10380
|
StoredAuthFailureError,
|
|
9235
10381
|
TERMINAL_SHUTDOWN_SIGNALS,
|