@quantiya/codevibe-antigravity-plugin 2.0.62 → 2.0.64
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 +15 -0
- package/antigravity-plugin.json +1 -1
- package/dist/agy-hook.js +247 -0
- package/dist/installer-cli.js +227 -35
- package/dist/server.js +1256 -84
- package/libexec/agy-hook.sh +27 -0
- package/libexec/companion-launcher +29 -8
- package/package.json +3 -3
package/dist/server.js
CHANGED
|
@@ -37,16 +37,18 @@ __export(server_exports, {
|
|
|
37
37
|
__testing: () => __testing,
|
|
38
38
|
classifyEntrypoint: () => classifyEntrypoint,
|
|
39
39
|
classifyTmuxHasSessionError: () => classifyTmuxHasSessionError,
|
|
40
|
+
commandDigest: () => commandDigest,
|
|
40
41
|
generateLaunchSessionId: () => generateLaunchSessionId,
|
|
41
42
|
getActiveConversationFromCliLog: () => getActiveConversationFromCliLog,
|
|
42
43
|
parseMaybeJson: () => parseMaybeJson,
|
|
44
|
+
readTranscriptFirstType: () => readTranscriptFirstType,
|
|
43
45
|
runForeignSpawnIdle: () => runForeignSpawnIdle
|
|
44
46
|
});
|
|
45
47
|
module.exports = __toCommonJS(server_exports);
|
|
46
48
|
var crypto3 = __toESM(require("crypto"));
|
|
47
|
-
var
|
|
48
|
-
var
|
|
49
|
-
var
|
|
49
|
+
var path8 = __toESM(require("path"));
|
|
50
|
+
var fs7 = __toESM(require("fs"));
|
|
51
|
+
var os7 = __toESM(require("os"));
|
|
50
52
|
var import_child_process3 = require("child_process");
|
|
51
53
|
var import_util3 = require("util");
|
|
52
54
|
var import_codevibe_core6 = require("@quantiya/codevibe-core");
|
|
@@ -66,6 +68,60 @@ var fs = __toESM(require("fs"));
|
|
|
66
68
|
var crypto = __toESM(require("crypto"));
|
|
67
69
|
var http = __toESM(require("http"));
|
|
68
70
|
var import_codevibe_core2 = require("@quantiya/codevibe-core");
|
|
71
|
+
|
|
72
|
+
// src/agy-hook-paths.ts
|
|
73
|
+
var os2 = __toESM(require("os"));
|
|
74
|
+
var path2 = __toESM(require("path"));
|
|
75
|
+
var HOOK_ENTRY_NAME = "codevibe-antigravity";
|
|
76
|
+
var HOOK_ROUTE_PREFIX = "/agy-hook/v1/";
|
|
77
|
+
var HOOK_EVENTS = ["PreInvocation", "PostToolUse"];
|
|
78
|
+
var HOOK_SHELL_BASENAME = "agy-hook.sh";
|
|
79
|
+
var HOOK_SCRIPT_BASENAME = "agy-hook.js";
|
|
80
|
+
function isHookEvent(value) {
|
|
81
|
+
return typeof value === "string" && HOOK_EVENTS.includes(value);
|
|
82
|
+
}
|
|
83
|
+
function codevibeAgyDir(home = os2.homedir()) {
|
|
84
|
+
return path2.join(home, ".codevibe", "codevibe-agy");
|
|
85
|
+
}
|
|
86
|
+
function hooksDir(home = os2.homedir()) {
|
|
87
|
+
return path2.join(codevibeAgyDir(home), "hooks");
|
|
88
|
+
}
|
|
89
|
+
function globalHooksConfigPath(home = os2.homedir()) {
|
|
90
|
+
return path2.join(home, ".gemini", "config", "hooks.json");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/http-api.ts
|
|
94
|
+
var HOOK_BODY_MAX_BYTES = 64 * 1024;
|
|
95
|
+
var UUID_PATTERN = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
96
|
+
function parseAgyHookDelivery(event, raw) {
|
|
97
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
98
|
+
const body = raw;
|
|
99
|
+
if (typeof body.conversationId !== "string" || !UUID_PATTERN.test(body.conversationId)) return null;
|
|
100
|
+
const delivery = { event, conversationId: body.conversationId };
|
|
101
|
+
for (const key of ["invocationNum", "initialNumSteps", "stepIdx"]) {
|
|
102
|
+
const value = body[key];
|
|
103
|
+
if (value === void 0) continue;
|
|
104
|
+
if (!Number.isInteger(value) || value < 0) return null;
|
|
105
|
+
delivery[key] = value;
|
|
106
|
+
}
|
|
107
|
+
if (body.toolName !== void 0) {
|
|
108
|
+
if (typeof body.toolName !== "string") return null;
|
|
109
|
+
delivery.toolName = body.toolName;
|
|
110
|
+
}
|
|
111
|
+
if (body.toolArgs !== void 0) {
|
|
112
|
+
if (!body.toolArgs || typeof body.toolArgs !== "object" || Array.isArray(body.toolArgs)) return null;
|
|
113
|
+
const args = {};
|
|
114
|
+
for (const [key, value] of Object.entries(body.toolArgs)) {
|
|
115
|
+
if (typeof value === "string") args[key] = value;
|
|
116
|
+
}
|
|
117
|
+
delivery.toolArgs = args;
|
|
118
|
+
}
|
|
119
|
+
if (body.hasError !== void 0) {
|
|
120
|
+
if (typeof body.hasError !== "boolean") return null;
|
|
121
|
+
delivery.hasError = body.hasError;
|
|
122
|
+
}
|
|
123
|
+
return delivery;
|
|
124
|
+
}
|
|
69
125
|
var HttpApi = class {
|
|
70
126
|
constructor(options) {
|
|
71
127
|
this.server = null;
|
|
@@ -160,8 +216,50 @@ var HttpApi = class {
|
|
|
160
216
|
void this.handlePostEvent(req, res);
|
|
161
217
|
return;
|
|
162
218
|
}
|
|
219
|
+
if (req.method === "POST" && pathOnly.startsWith(HOOK_ROUTE_PREFIX)) {
|
|
220
|
+
void this.handleAgyHook(req, res, pathOnly.slice(HOOK_ROUTE_PREFIX.length));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
163
223
|
this.sendJson(res, 404, { success: false, error: "not found" });
|
|
164
224
|
}
|
|
225
|
+
async handleAgyHook(req, res, eventName) {
|
|
226
|
+
if (!isHookEvent(eventName)) {
|
|
227
|
+
this.sendJson(res, 404, { success: false, error: "unknown hook event" });
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const onAgyHook = this.handlers?.onAgyHook;
|
|
231
|
+
if (!onAgyHook) {
|
|
232
|
+
this.sendJson(res, 503, { success: false, error: "server not ready" });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
let body;
|
|
236
|
+
try {
|
|
237
|
+
body = await readBody(req, HOOK_BODY_MAX_BYTES);
|
|
238
|
+
} catch {
|
|
239
|
+
this.sendJson(res, 413, { success: false, error: "body too large or read failed" });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
let parsed;
|
|
243
|
+
try {
|
|
244
|
+
parsed = JSON.parse(body);
|
|
245
|
+
} catch {
|
|
246
|
+
this.sendJson(res, 400, { success: false, error: "invalid JSON" });
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const delivery = parseAgyHookDelivery(eventName, parsed);
|
|
250
|
+
if (!delivery) {
|
|
251
|
+
this.sendJson(res, 400, { success: false, error: "invalid hook delivery" });
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
onAgyHook(delivery);
|
|
256
|
+
} catch (err) {
|
|
257
|
+
logger.error("agy hook handler failed", { event: eventName, error: String(err) });
|
|
258
|
+
this.sendJson(res, 500, { success: false, error: "hook handling failed" });
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
this.sendJson(res, 200, { success: true });
|
|
262
|
+
}
|
|
165
263
|
checkBearer(req) {
|
|
166
264
|
const auth = req.headers["authorization"];
|
|
167
265
|
if (typeof auth !== "string") return false;
|
|
@@ -239,6 +337,7 @@ var HttpApi = class {
|
|
|
239
337
|
const content = JSON.stringify({
|
|
240
338
|
port: this.assignedPort,
|
|
241
339
|
token: this.bearerToken,
|
|
340
|
+
pid: process.pid,
|
|
242
341
|
writtenAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
243
342
|
});
|
|
244
343
|
const tmpPath = `${this.portFilePath}.tmp.${process.pid}`;
|
|
@@ -290,8 +389,8 @@ async function readBody(req, maxBytes) {
|
|
|
290
389
|
// src/transcript-tailer.ts
|
|
291
390
|
var import_events = require("events");
|
|
292
391
|
var fs2 = __toESM(require("fs"));
|
|
293
|
-
var
|
|
294
|
-
var
|
|
392
|
+
var os3 = __toESM(require("os"));
|
|
393
|
+
var path3 = __toESM(require("path"));
|
|
295
394
|
var import_chokidar = require("chokidar");
|
|
296
395
|
|
|
297
396
|
// src/shutdown-bounds.ts
|
|
@@ -405,10 +504,10 @@ function watchOneFilePolled(target, options) {
|
|
|
405
504
|
return watcher;
|
|
406
505
|
}
|
|
407
506
|
function defaultPaths() {
|
|
408
|
-
const home =
|
|
507
|
+
const home = os3.homedir();
|
|
409
508
|
return {
|
|
410
|
-
brainRoot:
|
|
411
|
-
offsetFilePath:
|
|
509
|
+
brainRoot: path3.join(home, ".gemini", "antigravity-cli", "brain"),
|
|
510
|
+
offsetFilePath: path3.join(home, ".codevibe", "codevibe-agy", "transcript-offsets.json")
|
|
412
511
|
};
|
|
413
512
|
}
|
|
414
513
|
var TranscriptTailer = class extends import_events.EventEmitter {
|
|
@@ -722,7 +821,7 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
722
821
|
onConversationDirAdded(conversationId) {
|
|
723
822
|
if (this.isShuttingDown) return;
|
|
724
823
|
logger.info("New brain directory discovered (pre-prompt)", {
|
|
725
|
-
dirPath:
|
|
824
|
+
dirPath: path3.join(this.brainRoot, conversationId),
|
|
726
825
|
conversationId
|
|
727
826
|
});
|
|
728
827
|
this.emit("conversation-discovered", conversationId);
|
|
@@ -914,7 +1013,7 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
914
1013
|
this.cursorResets.set(filePath, (this.cursorResets.get(filePath) ?? 0) + 1);
|
|
915
1014
|
}
|
|
916
1015
|
transcriptPathFor(conversationId) {
|
|
917
|
-
return
|
|
1016
|
+
return path3.join(this.brainRoot, conversationId, SYSTEM_GENERATED_DIR, LOGS_DIR, TRANSCRIPT_BASENAME);
|
|
918
1017
|
}
|
|
919
1018
|
/** UUID-named directories directly under the brain root; null on failure. */
|
|
920
1019
|
listConversationDirs() {
|
|
@@ -1022,8 +1121,8 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
1022
1121
|
this.snapshotTranscript(this.transcriptPathFor(conversationId))
|
|
1023
1122
|
);
|
|
1024
1123
|
}
|
|
1025
|
-
const dirPath =
|
|
1026
|
-
const transcriptPath =
|
|
1124
|
+
const dirPath = path3.join(this.brainRoot, conversationId);
|
|
1125
|
+
const transcriptPath = path3.join(
|
|
1027
1126
|
dirPath,
|
|
1028
1127
|
".system_generated",
|
|
1029
1128
|
"logs",
|
|
@@ -1349,7 +1448,7 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
1349
1448
|
return;
|
|
1350
1449
|
}
|
|
1351
1450
|
try {
|
|
1352
|
-
const offsetDir =
|
|
1451
|
+
const offsetDir = path3.dirname(this.offsetFilePath);
|
|
1353
1452
|
if (!fs2.existsSync(offsetDir)) {
|
|
1354
1453
|
fs2.mkdirSync(offsetDir, { recursive: true, mode: 448 });
|
|
1355
1454
|
}
|
|
@@ -1390,10 +1489,815 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
1390
1489
|
}
|
|
1391
1490
|
};
|
|
1392
1491
|
|
|
1393
|
-
// src/
|
|
1492
|
+
// src/conversation-router.ts
|
|
1493
|
+
var DEFAULT_HOLD_MS = 4e3;
|
|
1494
|
+
var DEFAULT_MAX_HELD_PER_CONVERSATION = 2e3;
|
|
1495
|
+
var DEFAULT_MAX_HELD_CONVERSATIONS = 16;
|
|
1496
|
+
var DEFAULT_MAX_PARKED_PER_CONVERSATION = 64;
|
|
1497
|
+
var VOUCHER_QUEUE_MS = 1e4;
|
|
1498
|
+
var PENDING_TURN_MS = 6e4;
|
|
1499
|
+
var ConversationRouter = class {
|
|
1500
|
+
constructor(options) {
|
|
1501
|
+
this.roles = /* @__PURE__ */ new Map();
|
|
1502
|
+
this.lastInvoked = /* @__PURE__ */ new Map();
|
|
1503
|
+
/** Reported by PostToolUse only, with an inconclusive transcript. Ours, not routed. */
|
|
1504
|
+
this.unclassified = /* @__PURE__ */ new Set();
|
|
1505
|
+
/** Held events of these were dropped as another session's (log dedupe + late-claim warning). */
|
|
1506
|
+
this.foreign = /* @__PURE__ */ new Set();
|
|
1507
|
+
this.legacy = /* @__PURE__ */ new Set();
|
|
1508
|
+
/** Model output arrived without our PreInvocation: not kept at expiry until our next PreInvocation. */
|
|
1509
|
+
this.unparkable = /* @__PURE__ */ new Set();
|
|
1510
|
+
/** Our turn-start PreInvocation that came before its turn line: that line's step. */
|
|
1511
|
+
this.turnTokens = /* @__PURE__ */ new Map();
|
|
1512
|
+
/** A turn line processed straight away (line first, unshared) and not yet seen by a turn start: that start leaves no voucher for it. */
|
|
1513
|
+
this.processedTurnSteps = /* @__PURE__ */ new Map();
|
|
1514
|
+
this.held = /* @__PURE__ */ new Map();
|
|
1515
|
+
/** `main` conversations, most recently invoked last; `bound` is the last. */
|
|
1516
|
+
this.mainOrder = [];
|
|
1517
|
+
this.hookReceived = false;
|
|
1518
|
+
this.options = options;
|
|
1519
|
+
this.holdMs = options.holdMs ?? DEFAULT_HOLD_MS;
|
|
1520
|
+
this.maxHeldPerConversation = options.maxHeldPerConversation ?? DEFAULT_MAX_HELD_PER_CONVERSATION;
|
|
1521
|
+
this.maxHeldConversations = options.maxHeldConversations ?? DEFAULT_MAX_HELD_CONVERSATIONS;
|
|
1522
|
+
this.maxParkedPerConversation = options.maxParkedPerConversation ?? DEFAULT_MAX_PARKED_PER_CONVERSATION;
|
|
1523
|
+
this.now = options.now ?? Date.now;
|
|
1524
|
+
}
|
|
1525
|
+
hasReceivedHook() {
|
|
1526
|
+
return this.hookReceived;
|
|
1527
|
+
}
|
|
1528
|
+
/** The `main` conversation our PreInvocation hook reported most recently. */
|
|
1529
|
+
getBound() {
|
|
1530
|
+
return this.mainOrder.length > 0 ? this.mainOrder[this.mainOrder.length - 1] : null;
|
|
1531
|
+
}
|
|
1532
|
+
roleOf(conversationId) {
|
|
1533
|
+
return this.roles.get(conversationId);
|
|
1534
|
+
}
|
|
1535
|
+
/** Reported by our own hooks, whatever its role. */
|
|
1536
|
+
isOwn(conversationId) {
|
|
1537
|
+
return this.roles.has(conversationId) || this.unclassified.has(conversationId);
|
|
1538
|
+
}
|
|
1539
|
+
route(conversationId, item) {
|
|
1540
|
+
const role = this.roles.get(conversationId);
|
|
1541
|
+
if (role === "main") {
|
|
1542
|
+
const step = item === void 0 ? null : this.turnStepOf(item);
|
|
1543
|
+
if (this.held.has(conversationId)) return "hold";
|
|
1544
|
+
const token = this.turnTokens.get(conversationId);
|
|
1545
|
+
if (item !== void 0 && token && this.now() - token.atMs <= VOUCHER_QUEUE_MS && (step === null || !this.tokenNames(token, step))) {
|
|
1546
|
+
return "hold";
|
|
1547
|
+
}
|
|
1548
|
+
if (this.movedElsewhere(conversationId)) return "hold";
|
|
1549
|
+
if (conversationId !== this.getBound()) return "hold";
|
|
1550
|
+
if (step !== null) {
|
|
1551
|
+
if (this.takeTurnToken(conversationId, step)) return "process";
|
|
1552
|
+
if (this.options.isClaimedElsewhere(conversationId)) return "hold";
|
|
1553
|
+
this.processedTurnSteps.set(conversationId, step);
|
|
1554
|
+
}
|
|
1555
|
+
return "process";
|
|
1556
|
+
}
|
|
1557
|
+
if (role === "subagent") return "drop";
|
|
1558
|
+
if (this.legacy.has(conversationId)) {
|
|
1559
|
+
if (this.options.isClaimedElsewhere(conversationId)) {
|
|
1560
|
+
this.markForeign(conversationId, "claimed by another CodeVibe session");
|
|
1561
|
+
return "drop";
|
|
1562
|
+
}
|
|
1563
|
+
return "legacy";
|
|
1564
|
+
}
|
|
1565
|
+
if (this.options.hooksArmed()) return "hold";
|
|
1566
|
+
if (this.options.isClaimedElsewhere(conversationId)) {
|
|
1567
|
+
this.markForeign(conversationId, "claimed by another CodeVibe session");
|
|
1568
|
+
return "drop";
|
|
1569
|
+
}
|
|
1570
|
+
return "legacy";
|
|
1571
|
+
}
|
|
1572
|
+
/** Buffer one event for a conversation route() answered `hold` for. */
|
|
1573
|
+
hold(conversationId, item) {
|
|
1574
|
+
let entry = this.held.get(conversationId);
|
|
1575
|
+
const itemStep = this.turnStepOf(item);
|
|
1576
|
+
if (itemStep !== null) {
|
|
1577
|
+
if (entry?.parked && !this.hasTurnLine(entry)) {
|
|
1578
|
+
this.dropParked(conversationId, "a new turn started");
|
|
1579
|
+
entry = void 0;
|
|
1580
|
+
}
|
|
1581
|
+
this.unparkable.delete(conversationId);
|
|
1582
|
+
}
|
|
1583
|
+
if (entry?.parked) {
|
|
1584
|
+
const reason = this.parkedDropReason(conversationId, entry, item);
|
|
1585
|
+
if (reason) {
|
|
1586
|
+
this.dropParked(conversationId, reason);
|
|
1587
|
+
entry = void 0;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
if (!entry) {
|
|
1591
|
+
if (this.held.size >= this.maxHeldConversations) {
|
|
1592
|
+
this.evict(this.evictionCandidate());
|
|
1593
|
+
}
|
|
1594
|
+
entry = { items: [], timer: this.startTimer(conversationId), parked: false, overflowed: 0, behindVoucher: false, shared: false, moved: false };
|
|
1595
|
+
this.held.set(conversationId, entry);
|
|
1596
|
+
if (!this.looksForeign(conversationId)) {
|
|
1597
|
+
this.options.onLog?.("Holding agy events until a hook claims the conversation", {
|
|
1598
|
+
conversationId,
|
|
1599
|
+
holdMs: this.holdMs
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
if (entry.items.length >= this.maxHeldPerConversation) {
|
|
1604
|
+
entry.items.shift();
|
|
1605
|
+
entry.overflowed += 1;
|
|
1606
|
+
}
|
|
1607
|
+
entry.items.push(item);
|
|
1608
|
+
if (!entry.shared && this.isShared(conversationId)) entry.shared = true;
|
|
1609
|
+
if (!entry.moved && this.movedElsewhere(conversationId)) entry.moved = true;
|
|
1610
|
+
if (itemStep !== null && this.roles.get(conversationId) === "main") {
|
|
1611
|
+
const token2 = this.takeTurnToken(conversationId, itemStep);
|
|
1612
|
+
if (token2) {
|
|
1613
|
+
this.releaseFromTurnLine(conversationId, itemStep, token2.earlierOwned);
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
if (itemStep !== null && entry.parked) this.armPendingTurnDeadline(conversationId, entry);
|
|
1618
|
+
const token = this.turnTokens.get(conversationId);
|
|
1619
|
+
if (token && this.now() - token.atMs <= VOUCHER_QUEUE_MS) entry.behindVoucher = true;
|
|
1620
|
+
}
|
|
1621
|
+
heldCount(conversationId) {
|
|
1622
|
+
return this.held.get(conversationId)?.items.length ?? 0;
|
|
1623
|
+
}
|
|
1624
|
+
/** Our PreInvocation hook reported this conversation. */
|
|
1625
|
+
onPreInvocation(report) {
|
|
1626
|
+
this.markHookReceived();
|
|
1627
|
+
const id = report.conversationId;
|
|
1628
|
+
const previous = this.roles.get(id);
|
|
1629
|
+
const role = this.classify(report, previous);
|
|
1630
|
+
const earlierOwned = previous === "main" && !this.movedElsewhere(id) && !this.held.get(id)?.moved;
|
|
1631
|
+
this.lastInvoked.set(id, this.now());
|
|
1632
|
+
this.unparkable.delete(id);
|
|
1633
|
+
const parked = this.held.get(id)?.parked ? this.heldCount(id) : 0;
|
|
1634
|
+
const turnStart = report.invocationNum === void 0 || report.invocationNum === 0;
|
|
1635
|
+
const turnLineStep = report.initialNumSteps === void 0 ? null : report.initialNumSteps - 1;
|
|
1636
|
+
const heldTurnSteps = this.heldTurnSteps(id);
|
|
1637
|
+
const ownLineHeld = turnStart && heldTurnSteps.some((step) => turnLineStep === null || step === turnLineStep);
|
|
1638
|
+
const releases = role !== "main" || heldTurnSteps.length === 0 || ownLineHeld;
|
|
1639
|
+
if (releases && parked > 0) {
|
|
1640
|
+
this.options.onLog?.("This session's hook arrived late; releasing the events kept for it", {
|
|
1641
|
+
conversationId: id,
|
|
1642
|
+
kept: parked
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
if (this.foreign.delete(id) && parked === 0) {
|
|
1646
|
+
this.options.onLog?.("This session now owns a conversation whose earlier events were dropped as another session's", {
|
|
1647
|
+
conversationId: id
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
this.setRole(id, role, previous, report);
|
|
1651
|
+
this.mainOrder = this.mainOrder.filter((other) => other !== id);
|
|
1652
|
+
if (role === "main") this.mainOrder.push(id);
|
|
1653
|
+
this.publishClaims();
|
|
1654
|
+
const lineAlreadyProcessed = turnStart && turnLineStep !== null && this.processedTurnSteps.get(id) === turnLineStep;
|
|
1655
|
+
if (turnStart) this.processedTurnSteps.delete(id);
|
|
1656
|
+
const vouches = role === "main" && turnStart && !ownLineHeld && !lineAlreadyProcessed;
|
|
1657
|
+
if (vouches) {
|
|
1658
|
+
this.turnTokens.set(id, { step: turnLineStep ?? Number.NaN, earlierOwned, atMs: this.now() });
|
|
1659
|
+
} else if (role !== "main" || ownLineHeld || lineAlreadyProcessed) {
|
|
1660
|
+
this.turnTokens.delete(id);
|
|
1661
|
+
}
|
|
1662
|
+
if (releases) {
|
|
1663
|
+
if (role !== "main") {
|
|
1664
|
+
this.releaseHeld(id, null);
|
|
1665
|
+
} else if (ownLineHeld) {
|
|
1666
|
+
this.releaseFromTurnLine(id, turnLineStep, earlierOwned);
|
|
1667
|
+
} else {
|
|
1668
|
+
const token = this.turnTokens.get(id);
|
|
1669
|
+
const owned = token && this.held.get(id)?.behindVoucher ? token.earlierOwned : earlierOwned || !turnStart;
|
|
1670
|
+
this.releaseHeld(id, owned ? "process" : null);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
const remaining = this.held.get(id);
|
|
1674
|
+
if (vouches && remaining) remaining.behindVoucher = true;
|
|
1675
|
+
return { role, bound: this.getBound() };
|
|
1676
|
+
}
|
|
1677
|
+
/** Our PostToolUse hook reported this conversation. */
|
|
1678
|
+
onPostToolUse(conversationId) {
|
|
1679
|
+
this.markHookReceived();
|
|
1680
|
+
if (this.roles.has(conversationId)) return;
|
|
1681
|
+
const firstType = this.options.readTranscriptFirstType(conversationId);
|
|
1682
|
+
const role = firstType === "USER_INPUT" ? "main" : firstType === "SYSTEM_MESSAGE" ? "subagent" : null;
|
|
1683
|
+
if (role) {
|
|
1684
|
+
this.setRole(conversationId, role, void 0, null);
|
|
1685
|
+
this.foreign.delete(conversationId);
|
|
1686
|
+
this.unparkable.delete(conversationId);
|
|
1687
|
+
this.publishClaims();
|
|
1688
|
+
this.releaseHeld(conversationId, role === "main" ? "process" : null);
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
if (this.unclassified.has(conversationId)) return;
|
|
1692
|
+
this.unclassified.add(conversationId);
|
|
1693
|
+
this.publishClaims();
|
|
1694
|
+
}
|
|
1695
|
+
/** Clear all state (daemon stop). */
|
|
1696
|
+
reset() {
|
|
1697
|
+
for (const entry of this.held.values()) {
|
|
1698
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
1699
|
+
}
|
|
1700
|
+
this.held.clear();
|
|
1701
|
+
this.roles.clear();
|
|
1702
|
+
this.lastInvoked.clear();
|
|
1703
|
+
this.unclassified.clear();
|
|
1704
|
+
this.foreign.clear();
|
|
1705
|
+
this.legacy.clear();
|
|
1706
|
+
this.unparkable.clear();
|
|
1707
|
+
this.turnTokens.clear();
|
|
1708
|
+
this.processedTurnSteps.clear();
|
|
1709
|
+
this.mainOrder = [];
|
|
1710
|
+
this.hookReceived = false;
|
|
1711
|
+
}
|
|
1712
|
+
setRole(id, role, previous, report) {
|
|
1713
|
+
this.roles.set(id, role);
|
|
1714
|
+
this.unclassified.delete(id);
|
|
1715
|
+
this.legacy.delete(id);
|
|
1716
|
+
if (previous !== role) {
|
|
1717
|
+
this.options.onLog?.("agy conversation classified by hook", {
|
|
1718
|
+
conversationId: id,
|
|
1719
|
+
role,
|
|
1720
|
+
previousRole: previous ?? null,
|
|
1721
|
+
via: report ? "PreInvocation" : "PostToolUse",
|
|
1722
|
+
invocationNum: report?.invocationNum ?? null,
|
|
1723
|
+
initialNumSteps: report?.initialNumSteps ?? null
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
/** Another session's (as far as we know): claimed elsewhere or already dropped as foreign. */
|
|
1728
|
+
looksForeign(conversationId) {
|
|
1729
|
+
if (this.roles.get(conversationId) === "main" && !this.movedElsewhere(conversationId)) return false;
|
|
1730
|
+
return this.foreign.has(conversationId) || this.options.isClaimedElsewhere(conversationId);
|
|
1731
|
+
}
|
|
1732
|
+
/** The held conversation to settle when the hold area is full. */
|
|
1733
|
+
evictionCandidate() {
|
|
1734
|
+
for (const id of this.held.keys()) {
|
|
1735
|
+
if (this.looksForeign(id)) return id;
|
|
1736
|
+
}
|
|
1737
|
+
return this.held.keys().next().value;
|
|
1738
|
+
}
|
|
1739
|
+
/** Only the exact line our turn start named uses its voucher (NaN: step unknown, any line). */
|
|
1740
|
+
tokenNames(token, step) {
|
|
1741
|
+
return Number.isNaN(token.step) || step === token.step;
|
|
1742
|
+
}
|
|
1743
|
+
takeTurnToken(conversationId, step) {
|
|
1744
|
+
const token = this.turnTokens.get(conversationId);
|
|
1745
|
+
if (token === void 0 || !this.tokenNames(token, step)) return null;
|
|
1746
|
+
this.turnTokens.delete(conversationId);
|
|
1747
|
+
return token;
|
|
1748
|
+
}
|
|
1749
|
+
/**
|
|
1750
|
+
* Release held events from our named turn line on. What came before it
|
|
1751
|
+
* belongs to earlier turns: released with it only if we owned the
|
|
1752
|
+
* conversation before this turn, dropped otherwise (Stage 2 R3-F1).
|
|
1753
|
+
*/
|
|
1754
|
+
releaseFromTurnLine(conversationId, step, earlierOwned) {
|
|
1755
|
+
const entry = this.held.get(conversationId);
|
|
1756
|
+
if (!entry) return;
|
|
1757
|
+
const named = (item) => {
|
|
1758
|
+
const itemStep = this.turnStepOf(item);
|
|
1759
|
+
return itemStep !== null && (step === null || itemStep === step);
|
|
1760
|
+
};
|
|
1761
|
+
let index = entry.items.findIndex(named);
|
|
1762
|
+
if (step !== null) {
|
|
1763
|
+
for (let i = entry.items.length - 1; i > index; i -= 1) {
|
|
1764
|
+
if (named(entry.items[i])) {
|
|
1765
|
+
index = i;
|
|
1766
|
+
break;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
if (index > 0 && !earlierOwned) {
|
|
1771
|
+
this.options.onLog?.("Dropping agy events held from an earlier turn of another session", {
|
|
1772
|
+
conversationId,
|
|
1773
|
+
dropped: index
|
|
1774
|
+
});
|
|
1775
|
+
entry.items = entry.items.slice(index);
|
|
1776
|
+
}
|
|
1777
|
+
this.releaseHeld(conversationId, "process");
|
|
1778
|
+
}
|
|
1779
|
+
turnStepOf(item) {
|
|
1780
|
+
return this.options.turnStep?.(item) ?? null;
|
|
1781
|
+
}
|
|
1782
|
+
hasTurnLine(entry) {
|
|
1783
|
+
return entry.items.some((item) => this.turnStepOf(item) !== null);
|
|
1784
|
+
}
|
|
1785
|
+
heldTurnSteps(conversationId) {
|
|
1786
|
+
const entry = this.held.get(conversationId);
|
|
1787
|
+
if (!entry) return [];
|
|
1788
|
+
return entry.items.map((item) => this.turnStepOf(item)).filter((step) => step !== null);
|
|
1789
|
+
}
|
|
1790
|
+
/** A turn line waiting for its owner (until PENDING_TURN_MS if the conversation was shared at any time while held; see Turn lines). */
|
|
1791
|
+
keepPendingTurn(conversationId, entry) {
|
|
1792
|
+
entry.parked = true;
|
|
1793
|
+
if (!entry.shared && this.isShared(conversationId)) entry.shared = true;
|
|
1794
|
+
if (!entry.moved && this.movedElsewhere(conversationId)) entry.moved = true;
|
|
1795
|
+
this.armPendingTurnDeadline(conversationId, entry);
|
|
1796
|
+
this.options.onLog?.("Waiting for the PreInvocation that owns this agy turn", {
|
|
1797
|
+
conversationId,
|
|
1798
|
+
held: entry.items.length
|
|
1799
|
+
});
|
|
1800
|
+
}
|
|
1801
|
+
/**
|
|
1802
|
+
* (Re)starts the deadline of a kept turn. It drops the batch only if another
|
|
1803
|
+
* live session shared the conversation at any time while the batch was held,
|
|
1804
|
+
* the only way a step can repeat (H23); a sharer that exits meanwhile does not
|
|
1805
|
+
* make its never-run input ours (Stage 1 R14-F1). Otherwise it checks again
|
|
1806
|
+
* later, so our own very late hook on an unshared conversation still finds its
|
|
1807
|
+
* line (Stage 1 R13-F1).
|
|
1808
|
+
*/
|
|
1809
|
+
armPendingTurnDeadline(conversationId, entry) {
|
|
1810
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
1811
|
+
const timer = setTimeout(() => {
|
|
1812
|
+
if (this.held.get(conversationId) !== entry) return;
|
|
1813
|
+
if (!entry.moved && this.movedElsewhere(conversationId)) entry.moved = true;
|
|
1814
|
+
if (!entry.shared && this.isShared(conversationId)) entry.shared = true;
|
|
1815
|
+
if (!entry.shared) {
|
|
1816
|
+
this.armPendingTurnDeadline(conversationId, entry);
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
this.options.onLog?.("Dropping an agy turn that no PreInvocation of this session claimed in time", {
|
|
1820
|
+
conversationId,
|
|
1821
|
+
dropped: entry.items.length,
|
|
1822
|
+
pendingTurnMs: PENDING_TURN_MS
|
|
1823
|
+
});
|
|
1824
|
+
this.releaseHeld(conversationId, null);
|
|
1825
|
+
}, PENDING_TURN_MS);
|
|
1826
|
+
timer.unref?.();
|
|
1827
|
+
entry.timer = timer;
|
|
1828
|
+
}
|
|
1829
|
+
/** Another live session claims or has invoked this conversation. */
|
|
1830
|
+
isShared(conversationId) {
|
|
1831
|
+
return this.options.isClaimedElsewhere(conversationId) || this.options.otherInvokedAt(conversationId) !== null;
|
|
1832
|
+
}
|
|
1833
|
+
/** Another live session invoked this conversation more recently than we did. */
|
|
1834
|
+
movedElsewhere(conversationId) {
|
|
1835
|
+
const other = this.options.otherInvokedAt(conversationId);
|
|
1836
|
+
if (other === null) return false;
|
|
1837
|
+
const own = this.lastInvoked.get(conversationId);
|
|
1838
|
+
return own === void 0 || other > own;
|
|
1839
|
+
}
|
|
1840
|
+
classify(report, previous) {
|
|
1841
|
+
const firstType = this.options.readTranscriptFirstType(report.conversationId);
|
|
1842
|
+
if (firstType === "USER_INPUT") return "main";
|
|
1843
|
+
if (firstType === "SYSTEM_MESSAGE") return "subagent";
|
|
1844
|
+
if (previous) return previous;
|
|
1845
|
+
return report.invocationNum === 0 && (report.initialNumSteps ?? 0) >= 1 ? "main" : "subagent";
|
|
1846
|
+
}
|
|
1847
|
+
markHookReceived() {
|
|
1848
|
+
if (this.hookReceived) return;
|
|
1849
|
+
this.hookReceived = true;
|
|
1850
|
+
this.legacy.clear();
|
|
1851
|
+
}
|
|
1852
|
+
markForeign(conversationId, reason) {
|
|
1853
|
+
if (this.foreign.has(conversationId)) return;
|
|
1854
|
+
this.foreign.add(conversationId);
|
|
1855
|
+
this.options.onLog?.("Ignoring agy conversation from another session", { conversationId, reason });
|
|
1856
|
+
}
|
|
1857
|
+
startTimer(conversationId) {
|
|
1858
|
+
const timer = setTimeout(() => this.expire(conversationId), this.holdMs);
|
|
1859
|
+
timer.unref?.();
|
|
1860
|
+
return timer;
|
|
1861
|
+
}
|
|
1862
|
+
/** Why kept events can no longer be ours, or null. */
|
|
1863
|
+
parkedDropReason(conversationId, entry, item) {
|
|
1864
|
+
if (this.options.isModelOutput?.(item)) return "the model ran without this session's PreInvocation";
|
|
1865
|
+
if (entry.items.length >= this.maxParkedPerConversation) return "more events than precede a model call";
|
|
1866
|
+
if (!this.roles.has(conversationId) && !this.hasTurnLine(entry) && this.options.isClaimedElsewhere(conversationId)) {
|
|
1867
|
+
return "claimed by another CodeVibe session";
|
|
1868
|
+
}
|
|
1869
|
+
return null;
|
|
1870
|
+
}
|
|
1871
|
+
dropParked(conversationId, reason) {
|
|
1872
|
+
this.unparkable.add(conversationId);
|
|
1873
|
+
this.options.onLog?.("Dropping agy events kept for a late hook", {
|
|
1874
|
+
conversationId,
|
|
1875
|
+
kept: this.heldCount(conversationId),
|
|
1876
|
+
reason
|
|
1877
|
+
});
|
|
1878
|
+
this.releaseHeld(conversationId, null);
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Foreign so far — no hook of ours, or another session invoked it more
|
|
1882
|
+
* recently. Keep its events for a late PreInvocation of ours unless they
|
|
1883
|
+
* already show an invocation that ran without it (Stage 2 R1-F3).
|
|
1884
|
+
*/
|
|
1885
|
+
settleForeign(conversationId, reason) {
|
|
1886
|
+
const entry = this.held.get(conversationId);
|
|
1887
|
+
if (!entry) return;
|
|
1888
|
+
const isModelOutput = this.options.isModelOutput;
|
|
1889
|
+
if (isModelOutput && entry.items.some((item) => isModelOutput(item))) {
|
|
1890
|
+
this.unparkable.add(conversationId);
|
|
1891
|
+
}
|
|
1892
|
+
if (!this.unparkable.has(conversationId) && entry.items.length <= this.maxParkedPerConversation) {
|
|
1893
|
+
this.markForeign(conversationId, `${reason}; keeping its first events in case this session's hook is late`);
|
|
1894
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
1895
|
+
entry.timer = null;
|
|
1896
|
+
entry.parked = true;
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
this.markForeign(conversationId, reason);
|
|
1900
|
+
this.releaseHeld(conversationId, null);
|
|
1901
|
+
}
|
|
1902
|
+
/** Free a slot when the hold area is full. */
|
|
1903
|
+
evict(conversationId) {
|
|
1904
|
+
const entry = this.held.get(conversationId);
|
|
1905
|
+
if (!entry) return;
|
|
1906
|
+
if (!entry.parked) this.expire(conversationId);
|
|
1907
|
+
if (this.held.get(conversationId)?.parked) {
|
|
1908
|
+
this.options.onLog?.("Hold area full; dropping agy events kept for a late hook", {
|
|
1909
|
+
conversationId,
|
|
1910
|
+
kept: this.heldCount(conversationId)
|
|
1911
|
+
});
|
|
1912
|
+
this.releaseHeld(conversationId, null);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
releaseHeld(conversationId, decision) {
|
|
1916
|
+
const entry = this.held.get(conversationId);
|
|
1917
|
+
if (!entry) return;
|
|
1918
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
1919
|
+
this.held.delete(conversationId);
|
|
1920
|
+
if (entry.overflowed > 0) {
|
|
1921
|
+
this.options.onLog?.("Held agy events overflowed; oldest dropped", {
|
|
1922
|
+
conversationId,
|
|
1923
|
+
dropped: entry.overflowed
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
if (decision) this.options.onRelease(entry.items, decision);
|
|
1927
|
+
}
|
|
1928
|
+
expire(conversationId) {
|
|
1929
|
+
const entry = this.held.get(conversationId);
|
|
1930
|
+
if (!entry || entry.parked) return;
|
|
1931
|
+
const token = this.turnTokens.get(conversationId);
|
|
1932
|
+
if (entry.behindVoucher && token && this.roles.get(conversationId) === "main") {
|
|
1933
|
+
if (token.earlierOwned) {
|
|
1934
|
+
this.releaseHeld(conversationId, "process");
|
|
1935
|
+
} else {
|
|
1936
|
+
this.options.onLog?.("Dropping agy events held from an earlier turn of another session", {
|
|
1937
|
+
conversationId,
|
|
1938
|
+
dropped: entry.items.length
|
|
1939
|
+
});
|
|
1940
|
+
this.releaseHeld(conversationId, null);
|
|
1941
|
+
}
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
if (this.hasTurnLine(entry) && this.options.hooksHealthy()) {
|
|
1945
|
+
const isModel = (item) => this.options.isModelOutput?.(item) ?? false;
|
|
1946
|
+
const turnIndexes = entry.items.flatMap((item, index) => this.turnStepOf(item) !== null ? [index] : []);
|
|
1947
|
+
const first = turnIndexes[0];
|
|
1948
|
+
let tailStart = turnIndexes[turnIndexes.length - 1];
|
|
1949
|
+
for (let k = turnIndexes.length - 2; k >= 0; k -= 1) {
|
|
1950
|
+
if (entry.items.slice(turnIndexes[k], tailStart).some(isModel)) break;
|
|
1951
|
+
tailStart = turnIndexes[k];
|
|
1952
|
+
}
|
|
1953
|
+
const earlier = entry.items.slice(0, first);
|
|
1954
|
+
const ranEarlier = entry.items.slice(first, tailStart);
|
|
1955
|
+
entry.items = entry.items.slice(tailStart);
|
|
1956
|
+
if (earlier.length > 0) {
|
|
1957
|
+
if (this.roles.get(conversationId) === "main" && !this.movedElsewhere(conversationId) && !entry.moved) {
|
|
1958
|
+
this.options.onRelease(earlier, "process");
|
|
1959
|
+
} else {
|
|
1960
|
+
this.options.onLog?.("Dropping agy events held from an earlier turn of another session", {
|
|
1961
|
+
conversationId,
|
|
1962
|
+
dropped: earlier.length
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
if (ranEarlier.length > 0) {
|
|
1967
|
+
this.options.onLog?.("Dropping earlier agy turns that ran without this session's PreInvocation", {
|
|
1968
|
+
conversationId,
|
|
1969
|
+
dropped: ranEarlier.length
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
if (entry.items.some(isModel)) {
|
|
1973
|
+
this.markForeign(conversationId, "the model ran this turn without this session's PreInvocation");
|
|
1974
|
+
this.releaseHeld(conversationId, null);
|
|
1975
|
+
} else {
|
|
1976
|
+
this.keepPendingTurn(conversationId, entry);
|
|
1977
|
+
}
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
const role = this.roles.get(conversationId);
|
|
1981
|
+
if (role === "main") {
|
|
1982
|
+
if (this.movedElsewhere(conversationId) || entry.moved) {
|
|
1983
|
+
this.settleForeign(conversationId, "another session invoked it more recently");
|
|
1984
|
+
} else {
|
|
1985
|
+
this.releaseHeld(conversationId, "process");
|
|
1986
|
+
}
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1989
|
+
if (role === "subagent") {
|
|
1990
|
+
this.releaseHeld(conversationId, null);
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
if (this.options.isClaimedElsewhere(conversationId)) {
|
|
1994
|
+
this.markForeign(conversationId, "claimed by another CodeVibe session");
|
|
1995
|
+
this.releaseHeld(conversationId, null);
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
if (this.options.hooksHealthy()) {
|
|
1999
|
+
this.settleForeign(conversationId, `no hook from this session within ${this.holdMs} ms`);
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
this.legacy.add(conversationId);
|
|
2003
|
+
this.options.onLog?.("No agy hook seen; routing conversation by the CLI log (legacy)", {
|
|
2004
|
+
conversationId,
|
|
2005
|
+
held: entry.items.length
|
|
2006
|
+
});
|
|
2007
|
+
this.releaseHeld(conversationId, "legacy");
|
|
2008
|
+
}
|
|
2009
|
+
publishClaims() {
|
|
2010
|
+
if (!this.options.onClaimsChanged) return;
|
|
2011
|
+
const claims = /* @__PURE__ */ new Map();
|
|
2012
|
+
for (const [id, role] of this.roles) {
|
|
2013
|
+
const lastInvokedMs = this.lastInvoked.get(id);
|
|
2014
|
+
claims.set(id, lastInvokedMs === void 0 ? { role } : { role, lastInvokedMs });
|
|
2015
|
+
}
|
|
2016
|
+
for (const id of this.unclassified) claims.set(id, { role: "unknown" });
|
|
2017
|
+
this.options.onClaimsChanged(claims);
|
|
2018
|
+
}
|
|
2019
|
+
};
|
|
2020
|
+
|
|
2021
|
+
// src/claim-registry.ts
|
|
1394
2022
|
var fs3 = __toESM(require("fs"));
|
|
1395
|
-
var
|
|
1396
|
-
var
|
|
2023
|
+
var os4 = __toESM(require("os"));
|
|
2024
|
+
var path4 = __toESM(require("path"));
|
|
2025
|
+
var CLAIMS_FILENAME = "claims.json";
|
|
2026
|
+
var SCAN_CACHE_MS = 1e3;
|
|
2027
|
+
var MAX_CLAIMS_FILE_BYTES = 256 * 1024;
|
|
2028
|
+
var MAX_CLAIMED_CONVERSATIONS = 500;
|
|
2029
|
+
var BOOT_TIME_SLACK_MS = 6e4;
|
|
2030
|
+
function isPidAlive(pid) {
|
|
2031
|
+
try {
|
|
2032
|
+
process.kill(pid, 0);
|
|
2033
|
+
return true;
|
|
2034
|
+
} catch (error) {
|
|
2035
|
+
return error.code === "EPERM";
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
var ClaimRegistry = class {
|
|
2039
|
+
constructor(options) {
|
|
2040
|
+
this.cache = null;
|
|
2041
|
+
this.runtimeDir = options.runtimeDir;
|
|
2042
|
+
this.wrapperPid = options.wrapperPid;
|
|
2043
|
+
this.daemonPid = options.daemonPid ?? process.pid;
|
|
2044
|
+
this.isPidAlive = options.isPidAlive ?? isPidAlive;
|
|
2045
|
+
this.now = options.now ?? Date.now;
|
|
2046
|
+
this.bootTimeMs = options.bootTimeMs ?? (() => Date.now() - os4.uptime() * 1e3);
|
|
2047
|
+
}
|
|
2048
|
+
isEnabled() {
|
|
2049
|
+
return this.runtimeDir !== null && this.wrapperPid !== null;
|
|
2050
|
+
}
|
|
2051
|
+
/** Replace this daemon's claims file with `conversations` (best-effort). */
|
|
2052
|
+
writeOwn(conversations) {
|
|
2053
|
+
if (!this.runtimeDir || this.wrapperPid === null) return;
|
|
2054
|
+
const entries = [...conversations.entries()].slice(-MAX_CLAIMED_CONVERSATIONS);
|
|
2055
|
+
const file = {
|
|
2056
|
+
version: 1,
|
|
2057
|
+
wrapperPid: this.wrapperPid,
|
|
2058
|
+
daemonPid: this.daemonPid,
|
|
2059
|
+
conversations: entries.map(([id, claim]) => claim.lastInvokedMs === void 0 ? { id, role: claim.role } : { id, role: claim.role, lastInvokedMs: claim.lastInvokedMs }),
|
|
2060
|
+
updatedAt: new Date(this.now()).toISOString()
|
|
2061
|
+
};
|
|
2062
|
+
const dest = path4.join(this.runtimeDir, CLAIMS_FILENAME);
|
|
2063
|
+
const tmp = `${dest}.tmp.${process.pid}`;
|
|
2064
|
+
try {
|
|
2065
|
+
fs3.writeFileSync(tmp, JSON.stringify(file), { mode: 384 });
|
|
2066
|
+
fs3.renameSync(tmp, dest);
|
|
2067
|
+
} catch (error) {
|
|
2068
|
+
try {
|
|
2069
|
+
fs3.unlinkSync(tmp);
|
|
2070
|
+
} catch {
|
|
2071
|
+
}
|
|
2072
|
+
logger.warn("Could not write conversation claims (other sessions may route by the CLI log)", {
|
|
2073
|
+
error: String(error)
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
/** Remove this daemon's claims file (on stop). */
|
|
2078
|
+
removeOwn() {
|
|
2079
|
+
if (!this.runtimeDir) return;
|
|
2080
|
+
try {
|
|
2081
|
+
fs3.unlinkSync(path4.join(this.runtimeDir, CLAIMS_FILENAME));
|
|
2082
|
+
} catch {
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
/** True when another live daemon has claimed this conversation. */
|
|
2086
|
+
isClaimedElsewhere(conversationId) {
|
|
2087
|
+
return this.scan()?.claimed.has(conversationId) ?? false;
|
|
2088
|
+
}
|
|
2089
|
+
/** Latest time another live daemon's agy invoked this conversation, or null. */
|
|
2090
|
+
otherInvokedAt(conversationId) {
|
|
2091
|
+
return this.scan()?.invokedAt.get(conversationId) ?? null;
|
|
2092
|
+
}
|
|
2093
|
+
scan() {
|
|
2094
|
+
if (!this.runtimeDir) return null;
|
|
2095
|
+
const now = this.now();
|
|
2096
|
+
if (!this.cache || now - this.cache.atMs >= SCAN_CACHE_MS) {
|
|
2097
|
+
this.cache = { atMs: now, ...this.scanOthers() };
|
|
2098
|
+
}
|
|
2099
|
+
return this.cache;
|
|
2100
|
+
}
|
|
2101
|
+
/** Drop the scan cache (tests, and right after a foreign claim is suspected). */
|
|
2102
|
+
invalidate() {
|
|
2103
|
+
this.cache = null;
|
|
2104
|
+
}
|
|
2105
|
+
scanOthers() {
|
|
2106
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
2107
|
+
const invokedAt = /* @__PURE__ */ new Map();
|
|
2108
|
+
if (!this.runtimeDir) return { claimed, invokedAt };
|
|
2109
|
+
const base = path4.dirname(this.runtimeDir);
|
|
2110
|
+
const own = path4.basename(this.runtimeDir);
|
|
2111
|
+
const uid = process.getuid?.();
|
|
2112
|
+
let names;
|
|
2113
|
+
try {
|
|
2114
|
+
names = fs3.readdirSync(base);
|
|
2115
|
+
} catch {
|
|
2116
|
+
return { claimed, invokedAt };
|
|
2117
|
+
}
|
|
2118
|
+
const bootedAt = this.bootTimeMs() - BOOT_TIME_SLACK_MS;
|
|
2119
|
+
for (const name of names) {
|
|
2120
|
+
if (name === own || !/^[0-9]{1,10}$/.test(name)) continue;
|
|
2121
|
+
const file = path4.join(base, name, CLAIMS_FILENAME);
|
|
2122
|
+
let parsed;
|
|
2123
|
+
try {
|
|
2124
|
+
const stat = fs3.lstatSync(file);
|
|
2125
|
+
if (!stat.isFile() || stat.size > MAX_CLAIMS_FILE_BYTES) continue;
|
|
2126
|
+
if (uid !== void 0 && stat.uid !== uid) continue;
|
|
2127
|
+
if (stat.mtimeMs < bootedAt) continue;
|
|
2128
|
+
parsed = JSON.parse(fs3.readFileSync(file, "utf8"));
|
|
2129
|
+
} catch {
|
|
2130
|
+
continue;
|
|
2131
|
+
}
|
|
2132
|
+
if (!parsed || typeof parsed !== "object") continue;
|
|
2133
|
+
const claims = parsed;
|
|
2134
|
+
if (claims.version !== 1 || claims.wrapperPid !== Number(name)) continue;
|
|
2135
|
+
if (!Number.isInteger(claims.daemonPid) || !Array.isArray(claims.conversations)) continue;
|
|
2136
|
+
if (!this.isPidAlive(claims.daemonPid)) continue;
|
|
2137
|
+
for (const entry of claims.conversations) {
|
|
2138
|
+
if (!entry || typeof entry.id !== "string") continue;
|
|
2139
|
+
claimed.add(entry.id);
|
|
2140
|
+
const at = entry.lastInvokedMs;
|
|
2141
|
+
if (entry.role === "main" && typeof at === "number" && Number.isFinite(at)) {
|
|
2142
|
+
invokedAt.set(entry.id, Math.max(at, invokedAt.get(entry.id) ?? 0));
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
return { claimed, invokedAt };
|
|
2147
|
+
}
|
|
2148
|
+
};
|
|
2149
|
+
|
|
2150
|
+
// src/installer.ts
|
|
2151
|
+
var fs4 = __toESM(require("fs"));
|
|
2152
|
+
var path5 = __toESM(require("path"));
|
|
2153
|
+
var HOOK_TIMEOUT_SECONDS = 5;
|
|
2154
|
+
function shellQuote(value) {
|
|
2155
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
2156
|
+
}
|
|
2157
|
+
function buildHookEntry(hooksFolder) {
|
|
2158
|
+
const shell = shellQuote(path5.join(hooksFolder, HOOK_SHELL_BASENAME));
|
|
2159
|
+
return {
|
|
2160
|
+
PreInvocation: [
|
|
2161
|
+
{ type: "command", command: `${shell} PreInvocation`, timeout: HOOK_TIMEOUT_SECONDS }
|
|
2162
|
+
],
|
|
2163
|
+
PostToolUse: [
|
|
2164
|
+
{
|
|
2165
|
+
matcher: "*",
|
|
2166
|
+
hooks: [
|
|
2167
|
+
{ type: "command", command: `${shell} PostToolUse`, timeout: HOOK_TIMEOUT_SECONDS }
|
|
2168
|
+
]
|
|
2169
|
+
}
|
|
2170
|
+
]
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
function readHooksConfigSync(hooksConfigPath) {
|
|
2174
|
+
let stat;
|
|
2175
|
+
try {
|
|
2176
|
+
stat = fs4.lstatSync(hooksConfigPath);
|
|
2177
|
+
} catch (err) {
|
|
2178
|
+
if (err.code === "ENOENT") return { kind: "absent" };
|
|
2179
|
+
return { kind: "unsupported", reason: `cannot stat: ${err.message}` };
|
|
2180
|
+
}
|
|
2181
|
+
if (stat.isSymbolicLink()) return { kind: "unsupported", reason: "is a symlink" };
|
|
2182
|
+
if (!stat.isFile()) return { kind: "unsupported", reason: "is not a regular file" };
|
|
2183
|
+
let raw;
|
|
2184
|
+
try {
|
|
2185
|
+
raw = fs4.readFileSync(hooksConfigPath, "utf8");
|
|
2186
|
+
} catch (err) {
|
|
2187
|
+
return { kind: "unsupported", reason: `cannot read: ${err.message}` };
|
|
2188
|
+
}
|
|
2189
|
+
if (raw.trim().length === 0) return { kind: "object", config: {}, mode: stat.mode & 511 };
|
|
2190
|
+
let parsed;
|
|
2191
|
+
try {
|
|
2192
|
+
parsed = JSON.parse(raw);
|
|
2193
|
+
} catch {
|
|
2194
|
+
return { kind: "unsupported", reason: "is not valid JSON" };
|
|
2195
|
+
}
|
|
2196
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2197
|
+
return { kind: "unsupported", reason: "is not a JSON object" };
|
|
2198
|
+
}
|
|
2199
|
+
return { kind: "object", config: parsed, mode: stat.mode & 511 };
|
|
2200
|
+
}
|
|
2201
|
+
function sameJson(a, b) {
|
|
2202
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
2203
|
+
}
|
|
2204
|
+
function isUserDisabled(entry) {
|
|
2205
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false;
|
|
2206
|
+
const named = entry;
|
|
2207
|
+
if (named.enabled === false) return true;
|
|
2208
|
+
const visit = (value) => {
|
|
2209
|
+
if (Array.isArray(value)) return value.some(visit);
|
|
2210
|
+
if (!value || typeof value !== "object") return false;
|
|
2211
|
+
const obj = value;
|
|
2212
|
+
return obj.enabled === false || visit(obj.hooks);
|
|
2213
|
+
};
|
|
2214
|
+
return Object.entries(named).some(([key, value]) => key !== "enabled" && visit(value));
|
|
2215
|
+
}
|
|
2216
|
+
function inspectHookRegistration(paths) {
|
|
2217
|
+
const read = readHooksConfigSync(paths.hooksConfigPath);
|
|
2218
|
+
if (read.kind === "unsupported") return "unsupported";
|
|
2219
|
+
if (read.kind === "absent") return "missing";
|
|
2220
|
+
if (isUserDisabled(read.config[HOOK_ENTRY_NAME])) return "missing";
|
|
2221
|
+
if (!sameJson(read.config[HOOK_ENTRY_NAME], buildHookEntry(paths.hooksDir))) return "missing";
|
|
2222
|
+
for (const name of [HOOK_SHELL_BASENAME, HOOK_SCRIPT_BASENAME]) {
|
|
2223
|
+
try {
|
|
2224
|
+
if (!fs4.statSync(path5.join(paths.hooksDir, name)).isFile()) return "missing";
|
|
2225
|
+
} catch {
|
|
2226
|
+
return "missing";
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
return "registered";
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
// src/tool-prompt-match.ts
|
|
2233
|
+
var TOOL_COMMAND_KEYS = ["CommandLine", "Command", "command"];
|
|
2234
|
+
var TOOL_PATH_KEYS = [
|
|
2235
|
+
"AbsolutePath",
|
|
2236
|
+
"TargetFile",
|
|
2237
|
+
"FilePath",
|
|
2238
|
+
"file_path",
|
|
2239
|
+
"filePath",
|
|
2240
|
+
"path",
|
|
2241
|
+
"DirectoryPath"
|
|
2242
|
+
];
|
|
2243
|
+
var EDIT_TOOLS = /* @__PURE__ */ new Set(["WRITE_TO_FILE", "REPLACE_FILE_CONTENT", "MULTI_REPLACE_FILE_CONTENT"]);
|
|
2244
|
+
function stripWrappingQuotes(value) {
|
|
2245
|
+
return value.length >= 2 && value.startsWith('"') && value.endsWith('"') ? value.slice(1, -1) : value;
|
|
2246
|
+
}
|
|
2247
|
+
function collapsePlainSpacing(value) {
|
|
2248
|
+
return /['"`$\\\r\n]/.test(value) ? value : value.replace(/[ \t]+/g, " ");
|
|
2249
|
+
}
|
|
2250
|
+
function normalizeCommand(value) {
|
|
2251
|
+
if (!value) return null;
|
|
2252
|
+
const normalized = collapsePlainSpacing(value.trim());
|
|
2253
|
+
return normalized.length > 0 ? normalized : null;
|
|
2254
|
+
}
|
|
2255
|
+
function normalizePath(value) {
|
|
2256
|
+
if (!value) return null;
|
|
2257
|
+
const normalized = stripWrappingQuotes(value.trim());
|
|
2258
|
+
return normalized.length > 0 ? normalized : null;
|
|
2259
|
+
}
|
|
2260
|
+
function fileLinks(text) {
|
|
2261
|
+
if (!text) return [];
|
|
2262
|
+
const found = [];
|
|
2263
|
+
for (const match of text.matchAll(/file:\/\/([^\s)\]]+)/g)) {
|
|
2264
|
+
try {
|
|
2265
|
+
found.push(decodeURIComponent(match[1]));
|
|
2266
|
+
} catch {
|
|
2267
|
+
found.push(match[1]);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
return found;
|
|
2271
|
+
}
|
|
2272
|
+
function isFileAccessPrompt(state) {
|
|
2273
|
+
return /Allow access to this file\?/i.test(state.paneDisplayHeader ?? "");
|
|
2274
|
+
}
|
|
2275
|
+
function promptMatchesCompletedTool(state, toolName, toolArgs) {
|
|
2276
|
+
if (!toolArgs || state.kind !== "approval") return false;
|
|
2277
|
+
const promptCommand = normalizeCommand(state.pendingCall?.command);
|
|
2278
|
+
if (promptCommand) {
|
|
2279
|
+
for (const key of TOOL_COMMAND_KEYS) {
|
|
2280
|
+
if (normalizeCommand(toolArgs[key]) === promptCommand) return true;
|
|
2281
|
+
}
|
|
2282
|
+
return false;
|
|
2283
|
+
}
|
|
2284
|
+
if (!isFileAccessPrompt(state) && !EDIT_TOOLS.has((toolName ?? "").toUpperCase())) return false;
|
|
2285
|
+
const promptPaths = /* @__PURE__ */ new Set();
|
|
2286
|
+
const declared = normalizePath(state.pendingCall?.filePath);
|
|
2287
|
+
if (declared) promptPaths.add(declared);
|
|
2288
|
+
for (const link of fileLinks(state.paneDisplayHeader)) promptPaths.add(link);
|
|
2289
|
+
if (promptPaths.size === 0) return false;
|
|
2290
|
+
for (const key of TOOL_PATH_KEYS) {
|
|
2291
|
+
const toolPath = normalizePath(toolArgs[key]);
|
|
2292
|
+
if (toolPath && promptPaths.has(toolPath)) return true;
|
|
2293
|
+
}
|
|
2294
|
+
return false;
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
// src/tmux-pane-observer.ts
|
|
2298
|
+
var fs5 = __toESM(require("fs"));
|
|
2299
|
+
var os5 = __toESM(require("os"));
|
|
2300
|
+
var path6 = __toESM(require("path"));
|
|
1397
2301
|
var import_crypto = require("crypto");
|
|
1398
2302
|
var import_child_process = require("child_process");
|
|
1399
2303
|
var import_events2 = require("events");
|
|
@@ -1439,7 +2343,7 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
|
|
|
1439
2343
|
* start()/stop() lifecycle and doesn't distinguish debounce
|
|
1440
2344
|
* cycles. (Stage 2 iter-4 R1 HIGH finding 2026-05-21.) */
|
|
1441
2345
|
this.debounceCycleId = 0;
|
|
1442
|
-
this.pipeFilePath = options.pipeFilePath ??
|
|
2346
|
+
this.pipeFilePath = options.pipeFilePath ?? path6.join(os5.tmpdir(), `codevibe-agy-pane-${process.pid}.log`);
|
|
1443
2347
|
this.skipTmuxCommands = options.skipTmuxCommands ?? false;
|
|
1444
2348
|
this.captureFn = options.captureFn ?? null;
|
|
1445
2349
|
}
|
|
@@ -1512,8 +2416,8 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
|
|
|
1512
2416
|
this.approvalUIActive = false;
|
|
1513
2417
|
this.currentApprovalHeader = null;
|
|
1514
2418
|
try {
|
|
1515
|
-
|
|
1516
|
-
|
|
2419
|
+
fs5.mkdirSync(path6.dirname(this.pipeFilePath), { recursive: true });
|
|
2420
|
+
fs5.writeFileSync(this.pipeFilePath, "");
|
|
1517
2421
|
if (!this.skipTmuxCommands) {
|
|
1518
2422
|
await this.enablePipePane();
|
|
1519
2423
|
}
|
|
@@ -1566,7 +2470,7 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
|
|
|
1566
2470
|
this.watcher = null;
|
|
1567
2471
|
}
|
|
1568
2472
|
try {
|
|
1569
|
-
|
|
2473
|
+
fs5.unlinkSync(this.pipeFilePath);
|
|
1570
2474
|
} catch {
|
|
1571
2475
|
}
|
|
1572
2476
|
logger.info("Tmux pane observer stopped", { sessionName: this.sessionName });
|
|
@@ -1668,7 +2572,7 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
|
|
|
1668
2572
|
await execAsync(cmd);
|
|
1669
2573
|
}
|
|
1670
2574
|
startFileWatcher() {
|
|
1671
|
-
this.watcher =
|
|
2575
|
+
this.watcher = fs5.watch(this.pipeFilePath, (eventType) => {
|
|
1672
2576
|
if (eventType !== "change") return;
|
|
1673
2577
|
void this.processFileChanges();
|
|
1674
2578
|
});
|
|
@@ -1888,17 +2792,17 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
|
|
|
1888
2792
|
}
|
|
1889
2793
|
}
|
|
1890
2794
|
readAppendedChunk() {
|
|
1891
|
-
const stat =
|
|
2795
|
+
const stat = fs5.statSync(this.pipeFilePath);
|
|
1892
2796
|
if (stat.size <= this.filePosition) return "";
|
|
1893
|
-
const fd =
|
|
2797
|
+
const fd = fs5.openSync(this.pipeFilePath, "r");
|
|
1894
2798
|
try {
|
|
1895
2799
|
const length = stat.size - this.filePosition;
|
|
1896
2800
|
const buffer = Buffer.alloc(length);
|
|
1897
|
-
|
|
2801
|
+
fs5.readSync(fd, buffer, 0, length, this.filePosition);
|
|
1898
2802
|
this.filePosition = stat.size;
|
|
1899
2803
|
return buffer.toString("utf-8");
|
|
1900
2804
|
} finally {
|
|
1901
|
-
|
|
2805
|
+
fs5.closeSync(fd);
|
|
1902
2806
|
}
|
|
1903
2807
|
}
|
|
1904
2808
|
};
|
|
@@ -2687,6 +3591,7 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
|
|
|
2687
3591
|
body: candidate.body,
|
|
2688
3592
|
paneOptions: candidate.options,
|
|
2689
3593
|
paneSemanticKey: buildApprovalSemanticKey(candidate),
|
|
3594
|
+
kind: candidate.kind,
|
|
2690
3595
|
emittedAt: Date.now(),
|
|
2691
3596
|
ttlMs: this.promptTtlMs,
|
|
2692
3597
|
// E1 (§6a-2b / F5) — carry the consumed pane hash so an emit-failure
|
|
@@ -2877,6 +3782,7 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
|
|
|
2877
3782
|
body: candidate.body,
|
|
2878
3783
|
paneOptions: candidate.options,
|
|
2879
3784
|
paneSemanticKey: buildApprovalSemanticKey(candidate),
|
|
3785
|
+
kind: candidate.kind,
|
|
2880
3786
|
emittedAt: Date.now(),
|
|
2881
3787
|
ttlMs: this.promptTtlMs
|
|
2882
3788
|
};
|
|
@@ -2902,19 +3808,37 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
|
|
|
2902
3808
|
* fresh-emit. Without this, mobile prompt state dangles until TTL
|
|
2903
3809
|
* AND the next identical approval is forever-suppressed by stale
|
|
2904
3810
|
* dedup. (Stage 2 R1 MED finding 2026-05-21.)
|
|
3811
|
+
*
|
|
3812
|
+
* `emittedBeforeMs` (F1, Stage 2 R1-F2): a desktop input that waited in the
|
|
3813
|
+
* routing hold proves only that prompts raised BEFORE it was typed are gone.
|
|
3814
|
+
* A prompt emitted after that moment stays pending, and so does its dedup
|
|
3815
|
+
* hash (clearing it would re-emit the still-visible chooser).
|
|
2905
3816
|
*/
|
|
2906
|
-
onPanePromptCleared() {
|
|
3817
|
+
onPanePromptCleared(emittedBeforeMs) {
|
|
2907
3818
|
if (this.pendingPrompts.size === 0 && this.paneOnlyEmittedHashes.size === 0) {
|
|
2908
3819
|
return [];
|
|
2909
3820
|
}
|
|
2910
3821
|
const now = Date.now();
|
|
2911
3822
|
const cleared = [];
|
|
2912
|
-
|
|
3823
|
+
let retained = false;
|
|
3824
|
+
for (const [promptId, state] of this.pendingPrompts.entries()) {
|
|
3825
|
+
if (emittedBeforeMs !== void 0 && state.emittedAt >= emittedBeforeMs) {
|
|
3826
|
+
retained = true;
|
|
3827
|
+
continue;
|
|
3828
|
+
}
|
|
2913
3829
|
this.resolvedPrompts.set(promptId, now);
|
|
2914
3830
|
cleared.push(promptId);
|
|
2915
3831
|
}
|
|
2916
|
-
|
|
2917
|
-
|
|
3832
|
+
if (!retained) {
|
|
3833
|
+
this.pendingPrompts.clear();
|
|
3834
|
+
this.paneOnlyEmittedHashes.clear();
|
|
3835
|
+
} else {
|
|
3836
|
+
for (const promptId of cleared) {
|
|
3837
|
+
const state = this.pendingPrompts.get(promptId);
|
|
3838
|
+
if (state?.paneHash) this.paneOnlyEmittedHashes.delete(state.paneHash);
|
|
3839
|
+
this.pendingPrompts.delete(promptId);
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
2918
3842
|
if (cleared.length > 0) {
|
|
2919
3843
|
logger.info("Pane prompt cleared \u2014 resolved pending prompts", {
|
|
2920
3844
|
count: cleared.length
|
|
@@ -3441,10 +4365,10 @@ var MobilePromptDeduper = class {
|
|
|
3441
4365
|
|
|
3442
4366
|
// src/daemon-telemetry.ts
|
|
3443
4367
|
var crypto2 = __toESM(require("crypto"));
|
|
3444
|
-
var
|
|
4368
|
+
var fs6 = __toESM(require("fs"));
|
|
3445
4369
|
var https = __toESM(require("https"));
|
|
3446
|
-
var
|
|
3447
|
-
var
|
|
4370
|
+
var os6 = __toESM(require("os"));
|
|
4371
|
+
var path7 = __toESM(require("path"));
|
|
3448
4372
|
var MEASUREMENT_ID = "G-GS74YEQTB8";
|
|
3449
4373
|
var API_SECRET = "lAfOF6OxRzSQ-NsLBRjhAg";
|
|
3450
4374
|
var GA4_HOSTNAME = "www.google-analytics.com";
|
|
@@ -3452,8 +4376,8 @@ var GA4_PATH = `/mp/collect?measurement_id=${MEASUREMENT_ID}&api_secret=${API_SE
|
|
|
3452
4376
|
var BEACON_TOTAL_TIMEOUT_MS = 800;
|
|
3453
4377
|
function readPluginVersion() {
|
|
3454
4378
|
try {
|
|
3455
|
-
const pkgPath =
|
|
3456
|
-
const raw =
|
|
4379
|
+
const pkgPath = path7.resolve(__dirname, "..", "package.json");
|
|
4380
|
+
const raw = fs6.readFileSync(pkgPath, "utf-8");
|
|
3457
4381
|
const pkg = JSON.parse(raw);
|
|
3458
4382
|
if (typeof pkg.version === "string" && pkg.version.length > 0 && pkg.version.length < 30) {
|
|
3459
4383
|
return pkg.version;
|
|
@@ -3465,14 +4389,14 @@ function readPluginVersion() {
|
|
|
3465
4389
|
var PLUGIN_VERSION = readPluginVersion();
|
|
3466
4390
|
function getClientId() {
|
|
3467
4391
|
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
3468
|
-
return crypto2.createHash("sha256").update(`${
|
|
4392
|
+
return crypto2.createHash("sha256").update(`${os6.hostname()}-${uid}`).digest("hex").substring(0, 36);
|
|
3469
4393
|
}
|
|
3470
4394
|
function sanitizeErrorMessage(msg) {
|
|
3471
4395
|
if (!msg) return "";
|
|
3472
|
-
const
|
|
4396
|
+
const homedir5 = os6.homedir();
|
|
3473
4397
|
let out = msg.replace(/[\n\r\t]/g, " ");
|
|
3474
|
-
if (
|
|
3475
|
-
const safeHome =
|
|
4398
|
+
if (homedir5 && homedir5.length > 0) {
|
|
4399
|
+
const safeHome = homedir5.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3476
4400
|
out = out.replace(new RegExp(safeHome, "g"), "~");
|
|
3477
4401
|
}
|
|
3478
4402
|
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, "");
|
|
@@ -3765,7 +4689,7 @@ function extractCommandFromArgs(toolName, args) {
|
|
|
3765
4689
|
for (const key of ["CommandLine", "Command", "command", "cmd", "exec"]) {
|
|
3766
4690
|
const v = a[key];
|
|
3767
4691
|
if (typeof v === "string" && v.trim().length > 0) {
|
|
3768
|
-
return
|
|
4692
|
+
return stripWrappingQuotes2(v.trim());
|
|
3769
4693
|
}
|
|
3770
4694
|
}
|
|
3771
4695
|
return void 0;
|
|
@@ -3785,7 +4709,7 @@ function extractFilePathFromArgs(toolName, args) {
|
|
|
3785
4709
|
]) {
|
|
3786
4710
|
const v = a[key];
|
|
3787
4711
|
if (typeof v === "string" && v.trim().length > 0) {
|
|
3788
|
-
return
|
|
4712
|
+
return stripWrappingQuotes2(v.trim());
|
|
3789
4713
|
}
|
|
3790
4714
|
}
|
|
3791
4715
|
return void 0;
|
|
@@ -3796,12 +4720,12 @@ function extractCwdFromArgs(args) {
|
|
|
3796
4720
|
for (const key of ["Cwd", "cwd", "workingDirectory", "WorkingDirectory"]) {
|
|
3797
4721
|
const v = a[key];
|
|
3798
4722
|
if (typeof v === "string" && v.trim().length > 0) {
|
|
3799
|
-
return
|
|
4723
|
+
return stripWrappingQuotes2(v.trim());
|
|
3800
4724
|
}
|
|
3801
4725
|
}
|
|
3802
4726
|
return void 0;
|
|
3803
4727
|
}
|
|
3804
|
-
function
|
|
4728
|
+
function stripWrappingQuotes2(s) {
|
|
3805
4729
|
if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
|
|
3806
4730
|
return s.slice(1, -1);
|
|
3807
4731
|
}
|
|
@@ -4106,6 +5030,53 @@ var StoredAuthFailureError = class extends Error {
|
|
|
4106
5030
|
this.name = "StoredAuthFailureError";
|
|
4107
5031
|
}
|
|
4108
5032
|
};
|
|
5033
|
+
var HOOK_REGISTRATION_CACHE_MS = 5e3;
|
|
5034
|
+
var TRANSCRIPT_FIRST_LINE_MAX_BYTES = 256 * 1024;
|
|
5035
|
+
var CONVERSATION_ID_PATTERN = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
5036
|
+
function defaultHookRouting() {
|
|
5037
|
+
if (process.env.JEST_WORKER_ID) return null;
|
|
5038
|
+
return {
|
|
5039
|
+
hooksDir: hooksDir(),
|
|
5040
|
+
hooksConfigPath: globalHooksConfigPath(),
|
|
5041
|
+
brainRoot: path8.join(os7.homedir(), ".gemini", "antigravity-cli", "brain")
|
|
5042
|
+
};
|
|
5043
|
+
}
|
|
5044
|
+
function commandDigest(command) {
|
|
5045
|
+
const normalized = normalizeCommand(command);
|
|
5046
|
+
return normalized ? crypto3.createHash("sha256").update(normalized).digest("hex").slice(0, 12) : null;
|
|
5047
|
+
}
|
|
5048
|
+
function readTranscriptFirstType(transcriptPath) {
|
|
5049
|
+
let fd = null;
|
|
5050
|
+
try {
|
|
5051
|
+
fd = fs7.openSync(transcriptPath, "r");
|
|
5052
|
+
const buffer = Buffer.alloc(TRANSCRIPT_FIRST_LINE_MAX_BYTES);
|
|
5053
|
+
let filled = 0;
|
|
5054
|
+
while (filled < buffer.length) {
|
|
5055
|
+
const read = fs7.readSync(fd, buffer, filled, buffer.length - filled, filled);
|
|
5056
|
+
if (read === 0) break;
|
|
5057
|
+
filled += read;
|
|
5058
|
+
if (buffer.subarray(0, filled).includes(10)) break;
|
|
5059
|
+
}
|
|
5060
|
+
const newline = buffer.subarray(0, filled).indexOf(10);
|
|
5061
|
+
if (newline < 0) {
|
|
5062
|
+
if (filled < buffer.length) return null;
|
|
5063
|
+
const head = buffer.subarray(0, 4096).toString("utf8");
|
|
5064
|
+
const m = head.match(/^\{[^{}]*?"type"\s*:\s*"(USER_INPUT|SYSTEM_MESSAGE)"/);
|
|
5065
|
+
return m ? m[1] : null;
|
|
5066
|
+
}
|
|
5067
|
+
const first = JSON.parse(buffer.subarray(0, newline).toString("utf8"));
|
|
5068
|
+
return typeof first?.type === "string" ? first.type : null;
|
|
5069
|
+
} catch {
|
|
5070
|
+
return null;
|
|
5071
|
+
} finally {
|
|
5072
|
+
if (fd !== null) {
|
|
5073
|
+
try {
|
|
5074
|
+
fs7.closeSync(fd);
|
|
5075
|
+
} catch {
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5078
|
+
}
|
|
5079
|
+
}
|
|
4109
5080
|
var MOBILE_PROMPT_FLOOR_RECENCY_MS = 12e4;
|
|
4110
5081
|
var LAUNCH_SETTLE_TIMEOUT_MS = 3e3;
|
|
4111
5082
|
var TMUX_LIFECYCLE_POLL_MS = 1e3;
|
|
@@ -4152,6 +5123,10 @@ var McpServer = class _McpServer {
|
|
|
4152
5123
|
this.nativeSources = /* @__PURE__ */ new Map();
|
|
4153
5124
|
this.nativeToolStart = null;
|
|
4154
5125
|
this.nativeAdmissionChain = Promise.resolve();
|
|
5126
|
+
/** When each transcript event first reached the router (F1, Stage 2 R1-F2):
|
|
5127
|
+
* a desktop input released from the hold only clears prompts raised before
|
|
5128
|
+
* it was typed. */
|
|
5129
|
+
this.transcriptObservedAt = /* @__PURE__ */ new WeakMap();
|
|
4155
5130
|
this.toolActivityKeyRefresh = null;
|
|
4156
5131
|
// E1 (§4/§6a-4) — per-promptId durable raise entries for the agy producer.
|
|
4157
5132
|
// Keyed by the E1 promptId (= derive(ownerToken)); each holds the SECRET
|
|
@@ -4226,7 +5201,8 @@ var McpServer = class _McpServer {
|
|
|
4226
5201
|
this.ensureLaunchInFlight = null;
|
|
4227
5202
|
/** Registry of agy conv UUIDs the wrapper has observed this lifecycle.
|
|
4228
5203
|
* Populated from handleConversationDiscovered + handleTranscriptEmit
|
|
4229
|
-
* (after
|
|
5204
|
+
* (after routing accepts the conversation) and from a PreInvocation hook
|
|
5205
|
+
* that classifies one as main (F1). Used by:
|
|
4230
5206
|
* (a) handlePromptCandidate — approvalDetector keys candidates by
|
|
4231
5207
|
* agy conv UUID, not backend session ID;
|
|
4232
5208
|
* (b) getSessionByConversation — defensive membership check before
|
|
@@ -4235,6 +5211,7 @@ var McpServer = class _McpServer {
|
|
|
4235
5211
|
* causes harmless extra candidate-buffer entries in approvalDetector
|
|
4236
5212
|
* bounded by TTL. (Codex Stage 2 R1 v12 HIGH.) */
|
|
4237
5213
|
this.observedMainConversationIds = /* @__PURE__ */ new Set();
|
|
5214
|
+
this.hookRegistrationCache = null;
|
|
4238
5215
|
this.started = false;
|
|
4239
5216
|
/** Monotonically-increasing counter incremented on every start(). Captured
|
|
4240
5217
|
* by async callbacks at entry; mismatch after an await means stop()→start()
|
|
@@ -4347,15 +5324,33 @@ var McpServer = class _McpServer {
|
|
|
4347
5324
|
paneObserver: this.paneObserver,
|
|
4348
5325
|
detector: this.approvalDetector
|
|
4349
5326
|
});
|
|
4350
|
-
const portFilePath = this.runtimeDir ?
|
|
5327
|
+
const portFilePath = this.runtimeDir ? path8.join(this.runtimeDir, "conn.json") : void 0;
|
|
4351
5328
|
this.httpApi = options.httpApi ?? new HttpApi({
|
|
4352
5329
|
bearerToken: this.bearerToken,
|
|
4353
5330
|
preferredPort: this.preferredPort,
|
|
4354
5331
|
portFilePath
|
|
4355
5332
|
});
|
|
5333
|
+
this.hookRouting = options.hookRouting !== void 0 ? options.hookRouting : defaultHookRouting();
|
|
5334
|
+
this.claims = new ClaimRegistry({ runtimeDir: this.runtimeDir, wrapperPid: this.wrapperPid });
|
|
5335
|
+
this.router = new ConversationRouter({
|
|
5336
|
+
hooksArmed: () => this.router.hasReceivedHook() || this.isHookRegistrationIntact(),
|
|
5337
|
+
hooksHealthy: () => this.router.hasReceivedHook() && (this.hookRouting === null || this.isHookRegistrationIntact()),
|
|
5338
|
+
isClaimedElsewhere: (conversationId) => this.claims.isClaimedElsewhere(conversationId),
|
|
5339
|
+
otherInvokedAt: (conversationId) => this.claims.otherInvokedAt(conversationId),
|
|
5340
|
+
readTranscriptFirstType: (conversationId) => this.readConversationFirstType(conversationId),
|
|
5341
|
+
onRelease: (emits, decision) => {
|
|
5342
|
+
for (const emit of emits) this.dispatchTranscriptEmit(emit, decision);
|
|
5343
|
+
},
|
|
5344
|
+
isModelOutput: (emit) => emit.event?.source === "MODEL",
|
|
5345
|
+
turnStep: (emit) => emit.event?.type === "USER_INPUT" ? typeof emit.event.step_index === "number" ? emit.event.step_index : -1 : null,
|
|
5346
|
+
onClaimsChanged: (claimed) => this.claims.writeOwn(claimed),
|
|
5347
|
+
onLog: (message, data) => logger.info(message, data),
|
|
5348
|
+
holdMs: options.conversationHoldMs
|
|
5349
|
+
});
|
|
4356
5350
|
this.httpApi.setHandlers({
|
|
4357
5351
|
pushEvent: (p) => this.handleHttpPushEvent(p),
|
|
4358
|
-
getActiveSession: () => this.getAnySession()
|
|
5352
|
+
getActiveSession: () => this.getAnySession(),
|
|
5353
|
+
onAgyHook: (delivery) => this.handleAgyHook(delivery)
|
|
4359
5354
|
});
|
|
4360
5355
|
}
|
|
4361
5356
|
static {
|
|
@@ -4453,14 +5448,7 @@ var McpServer = class _McpServer {
|
|
|
4453
5448
|
this.addListener(this.transcriptTailer, "error", () => {
|
|
4454
5449
|
});
|
|
4455
5450
|
this.addListener(this.transcriptTailer, "event", (emit) => {
|
|
4456
|
-
|
|
4457
|
-
logger.error("handleTranscriptEmit failed", { error: String(err) });
|
|
4458
|
-
});
|
|
4459
|
-
this.transcriptHandlers.add(task);
|
|
4460
|
-
void task.then(
|
|
4461
|
-
() => this.transcriptHandlers.delete(task),
|
|
4462
|
-
() => this.transcriptHandlers.delete(task)
|
|
4463
|
-
);
|
|
5451
|
+
this.dispatchTranscriptEmit(emit);
|
|
4464
5452
|
});
|
|
4465
5453
|
this.addListener(this.transcriptTailer, "conversation-discovered", (conversationId) => {
|
|
4466
5454
|
void this.handleConversationDiscovered(conversationId).catch((err) => {
|
|
@@ -4558,6 +5546,8 @@ var McpServer = class _McpServer {
|
|
|
4558
5546
|
}
|
|
4559
5547
|
}
|
|
4560
5548
|
this.listenerHandles = [];
|
|
5549
|
+
this.router.reset();
|
|
5550
|
+
this.claims.removeOwn();
|
|
4561
5551
|
await Promise.allSettled([...this.transcriptHandlers]);
|
|
4562
5552
|
await this.transcriptTailer.waitForReads();
|
|
4563
5553
|
await this.nativeAdmissionChain;
|
|
@@ -4591,6 +5581,7 @@ var McpServer = class _McpServer {
|
|
|
4591
5581
|
await this.boundedTeardownStep("paneObserver.stop", () => this.paneObserver.stop());
|
|
4592
5582
|
await this.boundedTeardownStep("approvalDetector.stop", () => this.approvalDetector.stop());
|
|
4593
5583
|
await this.boundedTeardownStep("httpApi.stop", () => this.httpApi.stop());
|
|
5584
|
+
this.removeOrphanedRuntimeDir();
|
|
4594
5585
|
try {
|
|
4595
5586
|
this.appSyncClient.cleanupSubscriptions();
|
|
4596
5587
|
} catch (err) {
|
|
@@ -4629,8 +5620,10 @@ var McpServer = class _McpServer {
|
|
|
4629
5620
|
if (this.sessionDisabled) return;
|
|
4630
5621
|
await this.ensureLaunchSession();
|
|
4631
5622
|
if (!this.session) return;
|
|
4632
|
-
|
|
4633
|
-
|
|
5623
|
+
const decision = this.router.route(conversationId);
|
|
5624
|
+
if (decision === "hold") this.registerHeldConversationBeforeFirstHook(conversationId);
|
|
5625
|
+
if (decision !== "process" && !(decision === "legacy" && this.legacyAcceptsConversation(conversationId))) {
|
|
5626
|
+
logger.debug("Ignoring discovery for a conversation not (yet) in this session", { conversationId, decision });
|
|
4634
5627
|
return;
|
|
4635
5628
|
}
|
|
4636
5629
|
this.observedMainConversationIds.add(conversationId);
|
|
@@ -4874,19 +5867,180 @@ var McpServer = class _McpServer {
|
|
|
4874
5867
|
if (this.ensureLaunchInFlight === attempt) this.ensureLaunchInFlight = null;
|
|
4875
5868
|
}
|
|
4876
5869
|
}
|
|
4877
|
-
|
|
5870
|
+
/**
|
|
5871
|
+
* The pre-F1 rule, now only the fallback for conversations no hook has
|
|
5872
|
+
* claimed when hooks do not fire (see conversation-router.ts): the newest
|
|
5873
|
+
* agy CLI log names the active conversation. F1 amendment: a conversation
|
|
5874
|
+
* another live CodeVibe daemon has claimed is never "the active one" here,
|
|
5875
|
+
* so another session's busy log cannot make us drop our own turn.
|
|
5876
|
+
*/
|
|
5877
|
+
legacyAcceptsConversation(conversationId) {
|
|
4878
5878
|
if (process.env.JEST_WORKER_ID && !this.cliLogPath) {
|
|
4879
5879
|
return true;
|
|
4880
5880
|
}
|
|
4881
|
-
|
|
5881
|
+
if (this.claims.isClaimedElsewhere(conversationId)) {
|
|
5882
|
+
return false;
|
|
5883
|
+
}
|
|
5884
|
+
const cliLogPath = this.cliLogPath ?? path8.join(os7.homedir(), ".gemini", "antigravity-cli", "cli.log");
|
|
4882
5885
|
const activeId = getActiveConversationFromCliLog(cliLogPath);
|
|
4883
|
-
if (!activeId) {
|
|
5886
|
+
if (!activeId || this.claims.isClaimedElsewhere(activeId)) {
|
|
4884
5887
|
return true;
|
|
4885
5888
|
}
|
|
4886
5889
|
return activeId === conversationId;
|
|
4887
5890
|
}
|
|
5891
|
+
/**
|
|
5892
|
+
* F1 moved the runtime dir under ~/.codevibe, so it outlives reboots. The
|
|
5893
|
+
* wrapper's EXIT trap removes it, except when the outer terminal closed while
|
|
5894
|
+
* agy kept running in tmux (the wrapper is gone; this daemon outlived it).
|
|
5895
|
+
* Then remove it here — only if empty: conn.json and claims.json are already
|
|
5896
|
+
* gone, and anything else is left alone.
|
|
5897
|
+
*/
|
|
5898
|
+
removeOrphanedRuntimeDir() {
|
|
5899
|
+
if (!this.runtimeDir || this.wrapperPid === null || isPidAlive(this.wrapperPid)) return;
|
|
5900
|
+
try {
|
|
5901
|
+
fs7.rmdirSync(this.runtimeDir);
|
|
5902
|
+
logger.info("Removed runtime dir left by a closed terminal", { runtimeDir: this.runtimeDir });
|
|
5903
|
+
} catch (error) {
|
|
5904
|
+
const code = error.code;
|
|
5905
|
+
if (code !== "ENOENT") {
|
|
5906
|
+
logger.warn("Could not remove runtime dir", { runtimeDir: this.runtimeDir, code: code ?? String(error) });
|
|
5907
|
+
}
|
|
5908
|
+
}
|
|
5909
|
+
}
|
|
5910
|
+
isHookRegistrationIntact() {
|
|
5911
|
+
if (!this.hookRouting) return false;
|
|
5912
|
+
const now = Date.now();
|
|
5913
|
+
if (!this.hookRegistrationCache || now - this.hookRegistrationCache.atMs >= HOOK_REGISTRATION_CACHE_MS) {
|
|
5914
|
+
this.hookRegistrationCache = {
|
|
5915
|
+
atMs: now,
|
|
5916
|
+
registered: inspectHookRegistration(this.hookRouting) === "registered"
|
|
5917
|
+
};
|
|
5918
|
+
}
|
|
5919
|
+
return this.hookRegistrationCache.registered;
|
|
5920
|
+
}
|
|
5921
|
+
readConversationFirstType(conversationId) {
|
|
5922
|
+
if (!this.hookRouting || !CONVERSATION_ID_PATTERN.test(conversationId)) return null;
|
|
5923
|
+
return readTranscriptFirstType(path8.join(
|
|
5924
|
+
this.hookRouting.brainRoot,
|
|
5925
|
+
conversationId,
|
|
5926
|
+
".system_generated",
|
|
5927
|
+
"logs",
|
|
5928
|
+
"transcript.jsonl"
|
|
5929
|
+
));
|
|
5930
|
+
}
|
|
5931
|
+
// ─── agy hooks (F1) ─────────────────────────────────────────────────────
|
|
5932
|
+
/**
|
|
5933
|
+
* One hook delivery from OUR agy (agy-hook.js only posts here when the
|
|
5934
|
+
* launcher's inline CODEVIBE_AGY_HOOK_OWNER names this daemon's runtime
|
|
5935
|
+
* dir). Updates routing state synchronously; network work runs afterwards so
|
|
5936
|
+
* the HTTP reply — which agy is waiting for — goes out at once.
|
|
5937
|
+
*/
|
|
5938
|
+
handleAgyHook(delivery) {
|
|
5939
|
+
if (!this.started) return;
|
|
5940
|
+
const { conversationId } = delivery;
|
|
5941
|
+
if (delivery.event === "PreInvocation") {
|
|
5942
|
+
const previousBound = this.router.getBound();
|
|
5943
|
+
const { role, bound } = this.router.onPreInvocation({
|
|
5944
|
+
conversationId,
|
|
5945
|
+
invocationNum: delivery.invocationNum,
|
|
5946
|
+
initialNumSteps: delivery.initialNumSteps
|
|
5947
|
+
});
|
|
5948
|
+
if (role === "main") {
|
|
5949
|
+
this.observedMainConversationIds.add(conversationId);
|
|
5950
|
+
} else {
|
|
5951
|
+
this.observedMainConversationIds.delete(conversationId);
|
|
5952
|
+
}
|
|
5953
|
+
if (bound !== previousBound) {
|
|
5954
|
+
logger.info("agy session bound to conversation (hook)", {
|
|
5955
|
+
conversationId: bound,
|
|
5956
|
+
previousConversationId: previousBound,
|
|
5957
|
+
launchSessionId: this.session?.sessionId ?? null
|
|
5958
|
+
});
|
|
5959
|
+
}
|
|
5960
|
+
return;
|
|
5961
|
+
}
|
|
5962
|
+
this.router.onPostToolUse(conversationId);
|
|
5963
|
+
void this.resolvePromptsForCompletedTool(delivery).catch((error) => {
|
|
5964
|
+
logger.warn("PostToolUse prompt resolution failed", { error: String(error) });
|
|
5965
|
+
});
|
|
5966
|
+
}
|
|
5967
|
+
/**
|
|
5968
|
+
* A tool our agy just ran was blocked on its approval prompt, so a pending
|
|
5969
|
+
* mobile card for that same command or file is answered: resolve it the way
|
|
5970
|
+
* a mobile reply does. Identity match only (tool-prompt-match.ts).
|
|
5971
|
+
*/
|
|
5972
|
+
async resolvePromptsForCompletedTool(delivery) {
|
|
5973
|
+
const session = this.session;
|
|
5974
|
+
if (!session) return;
|
|
5975
|
+
const pending = this.approvalDetector.getPendingPromptsForConversation();
|
|
5976
|
+
const matched = pending.filter((state) => promptMatchesCompletedTool(state, delivery.toolName, delivery.toolArgs));
|
|
5977
|
+
logger.info("PostToolUse checked pending prompts", {
|
|
5978
|
+
conversationId: delivery.conversationId,
|
|
5979
|
+
toolName: delivery.toolName ?? null,
|
|
5980
|
+
commandDigest: commandDigest(delivery.toolArgs?.CommandLine),
|
|
5981
|
+
pending: pending.length,
|
|
5982
|
+
matched: matched.length
|
|
5983
|
+
});
|
|
5984
|
+
for (const state of matched) {
|
|
5985
|
+
const stillPending = this.approvalDetector.resolvePrompt(state.promptId) !== null;
|
|
5986
|
+
const ownedByE1 = this.e1PromptRaises.has(state.promptId);
|
|
5987
|
+
const resolved = await this.resolveE1PluginApproval(session.sessionId, state.promptId);
|
|
5988
|
+
logger.info("Resolved interactive prompt answered on the desktop (PostToolUse)", {
|
|
5989
|
+
promptId: state.promptId,
|
|
5990
|
+
toolName: delivery.toolName ?? null,
|
|
5991
|
+
stillPending,
|
|
5992
|
+
ownedByE1,
|
|
5993
|
+
backendAcknowledged: resolved
|
|
5994
|
+
});
|
|
5995
|
+
}
|
|
5996
|
+
}
|
|
4888
5997
|
// ─── Transcript flow ────────────────────────────────────────────────────
|
|
4889
|
-
|
|
5998
|
+
/**
|
|
5999
|
+
* Route one transcript event synchronously, at arrival, so held events and
|
|
6000
|
+
* later ones keep their order. `released` carries the router's decision for
|
|
6001
|
+
* events leaving the hold buffer.
|
|
6002
|
+
*/
|
|
6003
|
+
dispatchTranscriptEmit(emit, released) {
|
|
6004
|
+
if (!this.started) return;
|
|
6005
|
+
if (!this.transcriptObservedAt.has(emit)) this.transcriptObservedAt.set(emit, Date.now());
|
|
6006
|
+
const decision = released ?? this.router.route(emit.conversationId, emit);
|
|
6007
|
+
if (decision === "drop") {
|
|
6008
|
+
logger.debug("Ignoring transcript event from a conversation outside this session", {
|
|
6009
|
+
conversationId: emit.conversationId
|
|
6010
|
+
});
|
|
6011
|
+
return;
|
|
6012
|
+
}
|
|
6013
|
+
if (decision === "hold") {
|
|
6014
|
+
this.router.hold(emit.conversationId, emit);
|
|
6015
|
+
this.registerHeldConversationBeforeFirstHook(emit.conversationId);
|
|
6016
|
+
return;
|
|
6017
|
+
}
|
|
6018
|
+
const task = this.handleTranscriptEmit(emit, decision).catch((err) => {
|
|
6019
|
+
logger.error("handleTranscriptEmit failed", { error: String(err) });
|
|
6020
|
+
});
|
|
6021
|
+
this.transcriptHandlers.add(task);
|
|
6022
|
+
void task.then(
|
|
6023
|
+
() => this.transcriptHandlers.delete(task),
|
|
6024
|
+
() => this.transcriptHandlers.delete(task)
|
|
6025
|
+
);
|
|
6026
|
+
}
|
|
6027
|
+
/**
|
|
6028
|
+
* Until this daemon's first hook arrives it cannot tell "hooks will claim
|
|
6029
|
+
* this" from "hooks never fire here" (older agy, or an account where agy's
|
|
6030
|
+
* hook gate is off). Keep the pre-F1 registration for a held conversation the
|
|
6031
|
+
* legacy rule accepts, so an approval prompt raised during the hold still has
|
|
6032
|
+
* an owning conversation and reaches the phone with its options. Registration
|
|
6033
|
+
* only makes the conversation eligible for prompt ownership; its events still
|
|
6034
|
+
* wait for the router. (Stage 1 R1-F2.)
|
|
6035
|
+
*/
|
|
6036
|
+
registerHeldConversationBeforeFirstHook(conversationId) {
|
|
6037
|
+
if (this.router.hasReceivedHook()) return;
|
|
6038
|
+
if (this.observedMainConversationIds.has(conversationId)) return;
|
|
6039
|
+
if (this.claims.isClaimedElsewhere(conversationId)) return;
|
|
6040
|
+
if (!this.legacyAcceptsConversation(conversationId)) return;
|
|
6041
|
+
this.observedMainConversationIds.add(conversationId);
|
|
6042
|
+
}
|
|
6043
|
+
async handleTranscriptEmit(emit, decision, admission) {
|
|
4890
6044
|
if (!this.started) return;
|
|
4891
6045
|
const { conversationId, byteOffset, event } = emit;
|
|
4892
6046
|
if (this.sessionDisabled) {
|
|
@@ -4896,7 +6050,7 @@ var McpServer = class _McpServer {
|
|
|
4896
6050
|
if (!this.session) {
|
|
4897
6051
|
return;
|
|
4898
6052
|
}
|
|
4899
|
-
if (!this.
|
|
6053
|
+
if (decision === "legacy" && !this.legacyAcceptsConversation(conversationId)) {
|
|
4900
6054
|
logger.debug("Ignoring transcript emit for non-main conversation (subagent)", { conversationId });
|
|
4901
6055
|
return;
|
|
4902
6056
|
}
|
|
@@ -4951,8 +6105,9 @@ var McpServer = class _McpServer {
|
|
|
4951
6105
|
}
|
|
4952
6106
|
}
|
|
4953
6107
|
if (input.type === import_codevibe_core6.EventType.USER_PROMPT && input.source === import_codevibe_core6.EventSource.DESKTOP) {
|
|
4954
|
-
const
|
|
4955
|
-
|
|
6108
|
+
const inputObservedAtMs = this.transcriptObservedAt.get(emit);
|
|
6109
|
+
const clearedPromptIds = this.approvalDetector.onPanePromptCleared(inputObservedAtMs);
|
|
6110
|
+
await this.resolveAcceptedInputPrompts(session, clearedPromptIds, inputObservedAtMs);
|
|
4956
6111
|
const dedupe = admission ? null : this.mobileDeduper.consumeIfDuplicate(session.sessionId, input.content);
|
|
4957
6112
|
if (dedupe) {
|
|
4958
6113
|
if (admission) admission.echoContent = input.content;
|
|
@@ -4998,10 +6153,13 @@ var McpServer = class _McpServer {
|
|
|
4998
6153
|
if (!this.started || this.sessionDisabled) throw Error("SYNC_SOURCE_STOPPED");
|
|
4999
6154
|
await this.ensureLaunchSession();
|
|
5000
6155
|
const session = this.session;
|
|
5001
|
-
if (!session
|
|
6156
|
+
if (!session) throw Error("SYNC_SOURCE_OWNERSHIP_UNAVAILABLE");
|
|
5002
6157
|
const selection = await this.selectNativeSource(session);
|
|
5003
6158
|
if (!this.started || this.session !== session) throw Error("SYNC_SOURCE_OWNER_CHANGED");
|
|
5004
6159
|
if (selection.protocol === "LEGACY") return null;
|
|
6160
|
+
if (!this.legacyAcceptsConversation(source.conversationId) || this.router.roleOf(source.conversationId) === "subagent") {
|
|
6161
|
+
throw Error("SYNC_SOURCE_OWNERSHIP_UNAVAILABLE");
|
|
6162
|
+
}
|
|
5005
6163
|
if (!this.toolActivity?.isActive(session.sessionId)) {
|
|
5006
6164
|
if (this.nativeToolStart && this.nativeToolStart.session !== session) throw Error("SYNC_SOURCE_OWNER_CHANGED");
|
|
5007
6165
|
if (!this.nativeToolStart) {
|
|
@@ -5058,7 +6216,7 @@ var McpServer = class _McpServer {
|
|
|
5058
6216
|
this.assertNativeOwner(owner);
|
|
5059
6217
|
if ((await owner.cursor.open(0)).byteOffset >= source.endOffset) return;
|
|
5060
6218
|
const admission = { outputs: [null, null] };
|
|
5061
|
-
await this.handleTranscriptEmit(emit, admission);
|
|
6219
|
+
await this.handleTranscriptEmit(emit, "legacy", admission);
|
|
5062
6220
|
this.assertNativeOwner(owner);
|
|
5063
6221
|
const echoFrame = {
|
|
5064
6222
|
streamSha256: owner.streamSha256,
|
|
@@ -5277,7 +6435,7 @@ var McpServer = class _McpServer {
|
|
|
5277
6435
|
agentType: "ANTIGRAVITY",
|
|
5278
6436
|
...title !== void 0 && { title }
|
|
5279
6437
|
});
|
|
5280
|
-
this.e1PromptRaises.set(record.promptId, { record });
|
|
6438
|
+
this.e1PromptRaises.set(record.promptId, { record, raisedAtMs: Date.now() });
|
|
5281
6439
|
if (!this.e1TtlRefreshTimer) this.startE1TtlRefresh();
|
|
5282
6440
|
return record;
|
|
5283
6441
|
}
|
|
@@ -5337,10 +6495,12 @@ var McpServer = class _McpServer {
|
|
|
5337
6495
|
}
|
|
5338
6496
|
}
|
|
5339
6497
|
}
|
|
5340
|
-
async resolveAcceptedInputPrompts(session, observedPromptIds = []) {
|
|
6498
|
+
async resolveAcceptedInputPrompts(session, observedPromptIds = [], inputObservedAtMs) {
|
|
5341
6499
|
const promptIds = new Set(observedPromptIds);
|
|
5342
6500
|
for (const entry of this.e1PromptRaises.values()) {
|
|
5343
|
-
if (entry.record.sessionId
|
|
6501
|
+
if (entry.record.sessionId !== session.sessionId) continue;
|
|
6502
|
+
if (inputObservedAtMs !== void 0 && (entry.raisedAtMs ?? 0) >= inputObservedAtMs) continue;
|
|
6503
|
+
promptIds.add(entry.record.promptId);
|
|
5344
6504
|
}
|
|
5345
6505
|
for (const promptId of promptIds) {
|
|
5346
6506
|
await this.resolveE1PluginApproval(session.sessionId, promptId);
|
|
@@ -5437,6 +6597,7 @@ var McpServer = class _McpServer {
|
|
|
5437
6597
|
snapshotOpenE1(sessionId) {
|
|
5438
6598
|
return [...this.e1PromptRaises.values()].filter((e) => e.record.sessionId === sessionId).map((e) => ({
|
|
5439
6599
|
record: { ...e.record },
|
|
6600
|
+
...e.raisedAtMs !== void 0 && { raisedAtMs: e.raisedAtMs },
|
|
5440
6601
|
...e.contentPlain !== void 0 && { contentPlain: e.contentPlain },
|
|
5441
6602
|
...e.metadataPlain !== void 0 && { metadataPlain: e.metadataPlain }
|
|
5442
6603
|
}));
|
|
@@ -5458,6 +6619,7 @@ var McpServer = class _McpServer {
|
|
|
5458
6619
|
for (const snap of snapshot) {
|
|
5459
6620
|
const entry = {
|
|
5460
6621
|
record: { ...snap.record, sessionId: replacementSession.sessionId },
|
|
6622
|
+
...snap.raisedAtMs !== void 0 && { raisedAtMs: snap.raisedAtMs },
|
|
5461
6623
|
...snap.contentPlain !== void 0 && { contentPlain: snap.contentPlain },
|
|
5462
6624
|
...snap.metadataPlain !== void 0 && { metadataPlain: snap.metadataPlain }
|
|
5463
6625
|
};
|
|
@@ -5831,6 +6993,11 @@ var McpServer = class _McpServer {
|
|
|
5831
6993
|
if (cand.snapshotHash) {
|
|
5832
6994
|
this.e1ContentKeyIndex.set(`${session.sessionId}::${cand.snapshotHash}`, e1rec.promptId);
|
|
5833
6995
|
}
|
|
6996
|
+
logger.info("Pane prompt identity", {
|
|
6997
|
+
promptId: e1rec.promptId,
|
|
6998
|
+
conversationId: activeConvId,
|
|
6999
|
+
commandDigest: commandDigest(parsed.command)
|
|
7000
|
+
});
|
|
5834
7001
|
} else {
|
|
5835
7002
|
this.e1PromptRaises.delete(e1rec.promptId);
|
|
5836
7003
|
}
|
|
@@ -5838,13 +7005,16 @@ var McpServer = class _McpServer {
|
|
|
5838
7005
|
/**
|
|
5839
7006
|
* Pick the agy conversation UUID that owns the current approval UI.
|
|
5840
7007
|
* Priority:
|
|
5841
|
-
* 1.
|
|
5842
|
-
* 2.
|
|
5843
|
-
* 3. Otherwise
|
|
7008
|
+
* 1. F1: the conversation our PreInvocation hook last bound.
|
|
7009
|
+
* 2. cli.log says it's a specific conv → return that one if observed.
|
|
7010
|
+
* 3. Otherwise, return the most recently observed main conv UUID.
|
|
7011
|
+
* 4. Otherwise null (no active conv).
|
|
5844
7012
|
*/
|
|
5845
7013
|
pickActiveConversationForPrompt() {
|
|
7014
|
+
const bound = this.router.getBound();
|
|
7015
|
+
if (bound && this.observedMainConversationIds.has(bound)) return bound;
|
|
5846
7016
|
if (this.observedMainConversationIds.size === 0) return null;
|
|
5847
|
-
const cliLogPath = this.cliLogPath ??
|
|
7017
|
+
const cliLogPath = this.cliLogPath ?? path8.join(os7.homedir(), ".gemini", "antigravity-cli", "cli.log");
|
|
5848
7018
|
const cliActive = getActiveConversationFromCliLog(cliLogPath);
|
|
5849
7019
|
if (cliActive && this.observedMainConversationIds.has(cliActive)) {
|
|
5850
7020
|
return cliActive;
|
|
@@ -5982,13 +7152,13 @@ var McpServer = class _McpServer {
|
|
|
5982
7152
|
} else if (shouldDecrypt && !sessionKey) {
|
|
5983
7153
|
logger.warn("Cannot decrypt attachment - no session key available", { id: attachment.id });
|
|
5984
7154
|
}
|
|
5985
|
-
const attachmentsDir =
|
|
5986
|
-
if (!
|
|
5987
|
-
|
|
7155
|
+
const attachmentsDir = path8.join(this.workingDirectory, ".codevibe-attachments");
|
|
7156
|
+
if (!fs7.existsSync(attachmentsDir)) {
|
|
7157
|
+
fs7.mkdirSync(attachmentsDir, { recursive: true });
|
|
5988
7158
|
}
|
|
5989
7159
|
let extension = "";
|
|
5990
7160
|
if (attachment.filename) {
|
|
5991
|
-
const ext =
|
|
7161
|
+
const ext = path8.extname(attachment.filename);
|
|
5992
7162
|
if (ext) extension = ext;
|
|
5993
7163
|
}
|
|
5994
7164
|
if (!extension) {
|
|
@@ -6003,8 +7173,8 @@ var McpServer = class _McpServer {
|
|
|
6003
7173
|
extension = mimeToExt[attachment.type] || ".bin";
|
|
6004
7174
|
}
|
|
6005
7175
|
const filename = `attachment-${attachment.id}${extension}`;
|
|
6006
|
-
const absolutePath =
|
|
6007
|
-
|
|
7176
|
+
const absolutePath = path8.join(attachmentsDir, filename);
|
|
7177
|
+
fs7.writeFileSync(absolutePath, buffer);
|
|
6008
7178
|
const relativePath = `./.codevibe-attachments/${filename}`;
|
|
6009
7179
|
logger.info("Attachment saved to working directory", {
|
|
6010
7180
|
id: attachment.id,
|
|
@@ -6520,7 +7690,7 @@ function getActiveConversationFromCliLog(cliLogPath) {
|
|
|
6520
7690
|
try {
|
|
6521
7691
|
let initialStat;
|
|
6522
7692
|
try {
|
|
6523
|
-
initialStat =
|
|
7693
|
+
initialStat = fs7.lstatSync(cliLogPath);
|
|
6524
7694
|
} catch {
|
|
6525
7695
|
return null;
|
|
6526
7696
|
}
|
|
@@ -6532,19 +7702,19 @@ function getActiveConversationFromCliLog(cliLogPath) {
|
|
|
6532
7702
|
if (cached && now - cached.lastScanMs < CLI_LOG_RESOLVE_TTL_MS) {
|
|
6533
7703
|
resolvedPath = cached.resolvedPath;
|
|
6534
7704
|
} else {
|
|
6535
|
-
const logDir =
|
|
7705
|
+
const logDir = path8.join(path8.dirname(cliLogPath), "log");
|
|
6536
7706
|
let dirOk = false;
|
|
6537
7707
|
try {
|
|
6538
|
-
dirOk =
|
|
7708
|
+
dirOk = fs7.statSync(logDir).isDirectory();
|
|
6539
7709
|
} catch {
|
|
6540
7710
|
}
|
|
6541
7711
|
if (dirOk) {
|
|
6542
7712
|
const entries = [];
|
|
6543
|
-
for (const name of
|
|
7713
|
+
for (const name of fs7.readdirSync(logDir)) {
|
|
6544
7714
|
if (!/^cli-.*\.log$/.test(name)) continue;
|
|
6545
|
-
const full =
|
|
7715
|
+
const full = path8.join(logDir, name);
|
|
6546
7716
|
try {
|
|
6547
|
-
const s =
|
|
7717
|
+
const s = fs7.statSync(full);
|
|
6548
7718
|
if (s.isFile()) entries.push({ full, mtimeMs: s.mtimeMs });
|
|
6549
7719
|
} catch {
|
|
6550
7720
|
}
|
|
@@ -6563,7 +7733,7 @@ function getActiveConversationFromCliLog(cliLogPath) {
|
|
|
6563
7733
|
resolvedPath = initialStat.isSymbolicLink() ? null : cliLogPath;
|
|
6564
7734
|
}
|
|
6565
7735
|
if (resolvedPath === null) return null;
|
|
6566
|
-
const content =
|
|
7736
|
+
const content = fs7.readFileSync(resolvedPath, "utf8");
|
|
6567
7737
|
const regex = /(?:Created conversation|Streaming conversation|Starting conversation update stream for)\s+([0-9a-fA-F-]{36})/g;
|
|
6568
7738
|
const matches = Array.from(content.matchAll(regex));
|
|
6569
7739
|
if (matches.length === 0) return null;
|
|
@@ -6765,8 +7935,10 @@ var __testing = {
|
|
|
6765
7935
|
__testing,
|
|
6766
7936
|
classifyEntrypoint,
|
|
6767
7937
|
classifyTmuxHasSessionError,
|
|
7938
|
+
commandDigest,
|
|
6768
7939
|
generateLaunchSessionId,
|
|
6769
7940
|
getActiveConversationFromCliLog,
|
|
6770
7941
|
parseMaybeJson,
|
|
7942
|
+
readTranscriptFirstType,
|
|
6771
7943
|
runForeignSpawnIdle
|
|
6772
7944
|
});
|