@quantiya/codevibe-antigravity-plugin 2.0.60 → 2.0.61
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 +1 -1
- package/dist/server.js +422 -164
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ install the unified `codevibe --agent` interface yet.
|
|
|
51
51
|
|
|
52
52
|
## How it works
|
|
53
53
|
|
|
54
|
-
Antigravity CLI writes session transcripts to `~/.gemini/antigravity-cli/brain/<uuid>/.system_generated/logs/transcript.jsonl`. CodeVibe
|
|
54
|
+
Antigravity CLI writes session transcripts to `~/.gemini/antigravity-cli/brain/<uuid>/.system_generated/logs/transcript.jsonl`. CodeVibe follows the active conversation's transcript file, parses every line of the JSONL stream, and pushes events through E2E-encrypted AWS AppSync to your phone.
|
|
55
55
|
|
|
56
56
|
Approval prompts and multi-choice questions aren't in the transcript at the moment they appear — agy renders them directly into the terminal. CodeVibe observes the live tmux pane to detect both UI variants, parses the option labels, and surfaces them to mobile. Your mobile reply is delivered back via `tmux send-keys`.
|
|
57
57
|
|
package/dist/server.js
CHANGED
|
@@ -95,14 +95,14 @@ var HttpApi = class {
|
|
|
95
95
|
if (this.server) return this.assignedPort;
|
|
96
96
|
this.server = http.createServer((req, res) => this.dispatch(req, res));
|
|
97
97
|
try {
|
|
98
|
-
await new Promise((
|
|
98
|
+
await new Promise((resolve2, reject) => {
|
|
99
99
|
const onError = (err) => {
|
|
100
100
|
this.server?.off("listening", onListening);
|
|
101
101
|
reject(err);
|
|
102
102
|
};
|
|
103
103
|
const onListening = () => {
|
|
104
104
|
this.server?.off("error", onError);
|
|
105
|
-
|
|
105
|
+
resolve2();
|
|
106
106
|
};
|
|
107
107
|
this.server.once("error", onError);
|
|
108
108
|
this.server.once("listening", onListening);
|
|
@@ -135,7 +135,7 @@ var HttpApi = class {
|
|
|
135
135
|
const srv = this.server;
|
|
136
136
|
if (!srv) return;
|
|
137
137
|
this.server = null;
|
|
138
|
-
await new Promise((
|
|
138
|
+
await new Promise((resolve2) => srv.close(() => resolve2()));
|
|
139
139
|
if (this.portFilePath) {
|
|
140
140
|
try {
|
|
141
141
|
await fs.promises.unlink(this.portFilePath);
|
|
@@ -268,7 +268,7 @@ function isValidEventSource(value) {
|
|
|
268
268
|
return Object.values(import_codevibe_core2.EventSource).includes(value);
|
|
269
269
|
}
|
|
270
270
|
async function readBody(req, maxBytes) {
|
|
271
|
-
return new Promise((
|
|
271
|
+
return new Promise((resolve2, reject) => {
|
|
272
272
|
let received = 0;
|
|
273
273
|
const chunks = [];
|
|
274
274
|
req.on("data", (chunk) => {
|
|
@@ -280,7 +280,7 @@ async function readBody(req, maxBytes) {
|
|
|
280
280
|
}
|
|
281
281
|
chunks.push(chunk);
|
|
282
282
|
});
|
|
283
|
-
req.on("end", () =>
|
|
283
|
+
req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
|
|
284
284
|
req.on("error", reject);
|
|
285
285
|
});
|
|
286
286
|
}
|
|
@@ -306,8 +306,8 @@ async function boundedStep(work, ms, onTimeout) {
|
|
|
306
306
|
return Promise.reject(err);
|
|
307
307
|
}
|
|
308
308
|
})();
|
|
309
|
-
const timeout = new Promise((
|
|
310
|
-
timer = setTimeout(() =>
|
|
309
|
+
const timeout = new Promise((resolve2) => {
|
|
310
|
+
timer = setTimeout(() => resolve2("timeout"), ms);
|
|
311
311
|
if (timer.unref) timer.unref();
|
|
312
312
|
});
|
|
313
313
|
const outcome = await Promise.race([
|
|
@@ -355,6 +355,30 @@ var CONVERSATION_DIR_REGEX = /^[0-9a-fA-F-]{36}$/;
|
|
|
355
355
|
var SYSTEM_GENERATED_DIR = ".system_generated";
|
|
356
356
|
var LOGS_DIR = "logs";
|
|
357
357
|
var TRANSCRIPT_BASENAME = "transcript.jsonl";
|
|
358
|
+
var DISCOVERY_TICK_MS = 250;
|
|
359
|
+
var RESUME_SWEEP_EVERY_TICKS = 4;
|
|
360
|
+
var TRANSCRIPT_POLL_INTERVAL_MS = 100;
|
|
361
|
+
var MAX_FOLLOWED_TRANSCRIPTS = 4;
|
|
362
|
+
var FOLLOW_CATCH_UP_READ_DELAY_MS = 250;
|
|
363
|
+
var CHOKIDAR_ENV_OVERRIDES = ["CHOKIDAR_USEPOLLING", "CHOKIDAR_INTERVAL"];
|
|
364
|
+
function watchOneFilePolled(target, options) {
|
|
365
|
+
const saved = CHOKIDAR_ENV_OVERRIDES.map((name) => [name, process.env[name]]);
|
|
366
|
+
for (const [name] of saved) delete process.env[name];
|
|
367
|
+
let watcher;
|
|
368
|
+
try {
|
|
369
|
+
watcher = (0, import_chokidar.watch)(target, { ...options, usePolling: true });
|
|
370
|
+
} finally {
|
|
371
|
+
for (const [name, value] of saved) {
|
|
372
|
+
if (value === void 0) delete process.env[name];
|
|
373
|
+
else process.env[name] = value;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (watcher.options?.usePolling !== true) {
|
|
377
|
+
void watcher.close().catch(() => void 0);
|
|
378
|
+
throw new Error("chokidar did not honour usePolling; refusing a native fs.watch");
|
|
379
|
+
}
|
|
380
|
+
return watcher;
|
|
381
|
+
}
|
|
358
382
|
function defaultPaths() {
|
|
359
383
|
const home = os2.homedir();
|
|
360
384
|
return {
|
|
@@ -365,9 +389,39 @@ function defaultPaths() {
|
|
|
365
389
|
var TranscriptTailer = class extends import_events.EventEmitter {
|
|
366
390
|
constructor(options = {}) {
|
|
367
391
|
super();
|
|
368
|
-
this.watcher = null;
|
|
369
392
|
this.isWatching = false;
|
|
370
393
|
this.isShuttingDown = false;
|
|
394
|
+
/** Transcripts currently followed (stat-polled), keyed by conversation. */
|
|
395
|
+
this.followed = /* @__PURE__ */ new Map();
|
|
396
|
+
/** Conversations released by the follow cap during this lifetime. Their
|
|
397
|
+
* offsets stay protected from LRU eviction (as activeConversations are), so
|
|
398
|
+
* a later re-follow resumes exactly where it left off instead of replaying
|
|
399
|
+
* history. Cleared when its transcript goes away (unlink, confirmed
|
|
400
|
+
* absence, directory removal), when the sweep resets a never-read cursor
|
|
401
|
+
* for a replaced file, and on start(). A replacement found by the read
|
|
402
|
+
* chain keeps it: that read consumes the new file (Stage 2 R1-F6). */
|
|
403
|
+
this.releasedConversations = /* @__PURE__ */ new Set();
|
|
404
|
+
/** Last inode seen per transcript path by readNewLines: a different inode
|
|
405
|
+
* is a replacement (delete-then-recreate or atomic rename) and resets the
|
|
406
|
+
* cursor, as the former tree watch's `unlink` did. */
|
|
407
|
+
this.fileInodes = /* @__PURE__ */ new Map();
|
|
408
|
+
/** Per-path count of cursor resets (replacement, deletion, directory
|
|
409
|
+
* removal, unlink). A read in flight that sees the count change discards
|
|
410
|
+
* its stale cursor instead of writing it back, and re-queues itself so the
|
|
411
|
+
* new file is read from byte 0 (Stage 1 r7 S1-F14 / NB-K). */
|
|
412
|
+
this.cursorResets = /* @__PURE__ */ new Map();
|
|
413
|
+
/** Conversation directories seen in the brain root (snapshot at start, then
|
|
414
|
+
* maintained by the discovery tick). */
|
|
415
|
+
this.knownConversationDirs = /* @__PURE__ */ new Set();
|
|
416
|
+
/** Conversations created after start whose transcript does not exist yet;
|
|
417
|
+
* checked on every discovery tick. */
|
|
418
|
+
this.pendingTranscripts = /* @__PURE__ */ new Map();
|
|
419
|
+
/** Last swept stat of each unfollowed conversation's transcript (null when
|
|
420
|
+
* absent), for the resume sweep. */
|
|
421
|
+
this.transcriptSnapshots = /* @__PURE__ */ new Map();
|
|
422
|
+
this.rootMtimeMs = 0;
|
|
423
|
+
this.discoveryTimer = null;
|
|
424
|
+
this.discoveryTickCount = 0;
|
|
371
425
|
/** Error-log rate limiting (incident 2026-09-22 — a wedged watcher wrote
|
|
372
426
|
* 356,298 identical EMFILE errors / 96 MB before the daemon was killed). */
|
|
373
427
|
this.lastErrorSignature = null;
|
|
@@ -394,11 +448,11 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
394
448
|
const defaults = defaultPaths();
|
|
395
449
|
this.brainRoot = options.brainRoot ?? defaults.brainRoot;
|
|
396
450
|
this.offsetFilePath = options.offsetFilePath ?? defaults.offsetFilePath;
|
|
397
|
-
this.usePolling = options.usePolling ?? false;
|
|
398
451
|
}
|
|
399
452
|
/**
|
|
400
|
-
* Start
|
|
401
|
-
* then
|
|
453
|
+
* Start following transcripts. Loads persisted offsets, prunes orphans,
|
|
454
|
+
* snapshots the brain, then runs the discovery tick. Opens no directory
|
|
455
|
+
* watch (see the file header).
|
|
402
456
|
*/
|
|
403
457
|
start() {
|
|
404
458
|
if (this.isWatching) {
|
|
@@ -420,97 +474,35 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
420
474
|
});
|
|
421
475
|
}
|
|
422
476
|
}
|
|
477
|
+
this.knownConversationDirs.clear();
|
|
478
|
+
this.pendingTranscripts.clear();
|
|
479
|
+
this.transcriptSnapshots.clear();
|
|
480
|
+
this.releasedConversations.clear();
|
|
481
|
+
this.discoveryTickCount = 0;
|
|
482
|
+
this.rootMtimeMs = this.statMtimeMs(this.brainRoot) ?? 0;
|
|
483
|
+
for (const conversationId of this.listConversationDirs() ?? []) {
|
|
484
|
+
this.knownConversationDirs.add(conversationId);
|
|
485
|
+
this.transcriptSnapshots.set(
|
|
486
|
+
conversationId,
|
|
487
|
+
this.snapshotTranscript(this.transcriptPathFor(conversationId))
|
|
488
|
+
);
|
|
489
|
+
}
|
|
423
490
|
logger.info("Starting transcript tailer", {
|
|
424
491
|
brainRoot: this.brainRoot,
|
|
425
492
|
offsetFile: this.offsetFilePath,
|
|
426
|
-
loadedOffsets: this.offsets.size
|
|
427
|
-
|
|
428
|
-
this.watcher = (0, import_chokidar.watch)(this.brainRoot, {
|
|
429
|
-
persistent: true,
|
|
430
|
-
ignoreInitial: true,
|
|
431
|
-
depth: 4,
|
|
432
|
-
awaitWriteFinish: false,
|
|
433
|
-
// partial-line carryover handles partial writes
|
|
434
|
-
usePolling: this.usePolling,
|
|
435
|
-
interval: this.usePolling ? 50 : void 0,
|
|
436
|
-
// R24 — admit ONLY the exact path shape that can lead to a transcript.
|
|
437
|
-
//
|
|
438
|
-
// THE DEFECT THIS REPLACES: the previous predicate returned `false` for
|
|
439
|
-
// EVERY directory ("allow directories so chokidar can descend into
|
|
440
|
-
// them"), so chokidar descended into the whole brain. Measured on a real
|
|
441
|
-
// machine 2026-09-22: **40,345 directories across 234 conversations**
|
|
442
|
-
// (7.8 GB), on chokidar's kqueue backend which needs roughly one file
|
|
443
|
-
// descriptor per watched directory. The watcher exhausted its descriptors
|
|
444
|
-
// and threw EMFILE continuously — 77,359 errors in a single 28-minute
|
|
445
|
-
// daemon start, 89% of the whole log file across the day.
|
|
446
|
-
//
|
|
447
|
-
// That was not merely wasteful, it was a CORRECTNESS failure, and the
|
|
448
|
-
// exact shape matters because it is worse than a slow start:
|
|
449
|
-
//
|
|
450
|
-
// 1. The initial scan storms for tens of minutes.
|
|
451
|
-
// 2. When it finally completes, `ready` fires and
|
|
452
|
-
// backfillActiveTranscripts() does a ONE-SHOT catch-up read. That
|
|
453
|
-
// uses its own readdirSync, NOT the watcher, so it succeeds — mobile
|
|
454
|
-
// shows a sudden burst of backlog.
|
|
455
|
-
// 3. But the per-file WATCH was never established, because the
|
|
456
|
-
// descriptors were spent on old conversations during the scan. So no
|
|
457
|
-
// `change` event ever arrives again and the tailer STALLS PERMANENTLY
|
|
458
|
-
// at whatever offset the backfill reached.
|
|
459
|
-
//
|
|
460
|
-
// Measured live on the owner's machine while this fix was being written,
|
|
461
|
-
// and RETAINED — every figure here comes from that capture, not from
|
|
462
|
-
// recollection:
|
|
463
|
-
// .hard-gate/r24-watch-scope/evidence/prefix-descriptor-accounting.log
|
|
464
|
-
// The daemon held file descriptors for 231 OLD conversations and **zero**
|
|
465
|
-
// for the active one, while its offset sat frozen at 987,484 bytes
|
|
466
|
-
// against a transcript of 1,656,028 on disk — 668 KB behind and not
|
|
467
|
-
// advancing, with the EMFILE rate back to 0/sec and the daemon otherwise
|
|
468
|
-
// idle at 0.3% CPU. The gap WIDENED between two samplings twenty minutes
|
|
469
|
-
// apart (231 KB, then 668 KB), which is what distinguishes a permanent
|
|
470
|
-
// stall from slow catch-up.
|
|
471
|
-
//
|
|
472
|
-
// User-visible result: a burst of backlog, then silence. Interactive
|
|
473
|
-
// prompts degrade to "Prompt candidate fired but no active conversation
|
|
474
|
-
// known {observedConvCount:0}", which renders on mobile as "a prompt is
|
|
475
|
-
// waiting in your desktop terminal" with NO selectable options — the user
|
|
476
|
-
// can see that a prompt exists but cannot answer it, which is the entire
|
|
477
|
-
// purpose of the mobile companion.
|
|
478
|
-
//
|
|
479
|
-
// NOTE this description was corrected twice. It was first written as
|
|
480
|
-
// "permanent", then weakened to "a bounded dead window" after observing a
|
|
481
|
-
// recovery, then corrected again once the descriptor accounting above was
|
|
482
|
-
// actually measured. The recovery was the one-shot backfill of step 2, not
|
|
483
|
-
// the watcher healing. Stage 1 NB-4 predicted this sub-case before it was
|
|
484
|
-
// measured.
|
|
485
|
-
//
|
|
486
|
-
// THE FIX: the transcript lives at exactly one shape, four levels deep:
|
|
487
|
-
// brain/<uuid>/.system_generated/logs/transcript.jsonl
|
|
488
|
-
// so decide purely LEXICALLY on that shape and refuse everything else.
|
|
489
|
-
// Same machine, after: **699** directories (1 root + 234 conversations +
|
|
490
|
-
// 232 .system_generated + 232 logs) — a 57x reduction that also excludes
|
|
491
|
-
// the actual bulk: scratch/ (230 dirs), .user_uploaded/ (232) and the
|
|
492
|
-
// thousands of .system_generated/tasks/ dirs, none of which can ever hold
|
|
493
|
-
// a transcript.
|
|
494
|
-
//
|
|
495
|
-
// Deciding lexically also removes the per-path `fs.statSync` the old
|
|
496
|
-
// predicate ran during traversal, which was its own CPU cost on a tree
|
|
497
|
-
// this size.
|
|
498
|
-
ignored: (filePath) => this.isOutsideTranscriptPath(filePath)
|
|
499
|
-
});
|
|
500
|
-
this.watcher.on("add", (filePath) => this.onFileAdded(filePath));
|
|
501
|
-
this.watcher.on("addDir", (dirPath) => this.onDirAdded(dirPath));
|
|
502
|
-
this.watcher.on("change", (filePath) => this.onFileChanged(filePath));
|
|
503
|
-
this.watcher.on("unlink", (filePath) => this.onFileUnlinked(filePath));
|
|
504
|
-
this.watcher.on("error", (error) => {
|
|
505
|
-
this.emitError(error, "Chokidar error");
|
|
506
|
-
});
|
|
507
|
-
this.watcher.on("ready", () => {
|
|
508
|
-
logger.info("Transcript tailer ready (chokidar initial scan complete)");
|
|
509
|
-
this.backfillActiveTranscripts();
|
|
493
|
+
loadedOffsets: this.offsets.size,
|
|
494
|
+
knownConversations: this.knownConversationDirs.size
|
|
510
495
|
});
|
|
496
|
+
this.discoveryTimer = setInterval(() => this.discoveryTick(), DISCOVERY_TICK_MS);
|
|
497
|
+
if (this.discoveryTimer.unref) this.discoveryTimer.unref();
|
|
511
498
|
this.flushTimer = setInterval(() => this.flushOffsets(), OFFSET_FLUSH_INTERVAL_MS);
|
|
512
499
|
if (this.flushTimer.unref) this.flushTimer.unref();
|
|
513
500
|
this.isWatching = true;
|
|
501
|
+
const generation = this.readGeneration;
|
|
502
|
+
setImmediate(() => {
|
|
503
|
+
if (!this.isWatching || this.isShuttingDown || generation !== this.readGeneration) return;
|
|
504
|
+
this.backfillActiveTranscripts();
|
|
505
|
+
});
|
|
514
506
|
}
|
|
515
507
|
/**
|
|
516
508
|
* (#638 G4) SAFE 'error' emission. `EventEmitter.emit('error', …)` with NO
|
|
@@ -559,10 +551,18 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
559
551
|
} catch (error) {
|
|
560
552
|
logger.warn("Final offset flush failed", { error });
|
|
561
553
|
}
|
|
562
|
-
if (this.
|
|
563
|
-
|
|
554
|
+
if (this.discoveryTimer) {
|
|
555
|
+
clearInterval(this.discoveryTimer);
|
|
556
|
+
this.discoveryTimer = null;
|
|
557
|
+
}
|
|
558
|
+
const watchers = Array.from(this.followed.values(), (f) => f.watcher);
|
|
559
|
+
this.followed.clear();
|
|
560
|
+
this.pendingTranscripts.clear();
|
|
561
|
+
this.knownConversationDirs.clear();
|
|
562
|
+
this.transcriptSnapshots.clear();
|
|
563
|
+
if (watchers.length > 0) {
|
|
564
564
|
const outcome = await boundedStep(
|
|
565
|
-
() =>
|
|
565
|
+
() => Promise.all(watchers.map((w) => w.close())),
|
|
566
566
|
WATCHER_CLOSE_TIMEOUT_MS,
|
|
567
567
|
() => logger.warn("chokidar watcher.close() timed out \u2014 abandoning it", {
|
|
568
568
|
timeoutMs: WATCHER_CLOSE_TIMEOUT_MS
|
|
@@ -571,10 +571,10 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
571
571
|
if (outcome === "timeout") {
|
|
572
572
|
logger.warn("Transcript tailer watcher abandoned; relying on shutdown watchdog");
|
|
573
573
|
}
|
|
574
|
-
this.watcher = null;
|
|
575
574
|
}
|
|
576
575
|
this.activeReads.clear();
|
|
577
576
|
this.pendingReads.clear();
|
|
577
|
+
this.cursorResets.clear();
|
|
578
578
|
this.isWatching = false;
|
|
579
579
|
this.isShuttingDown = false;
|
|
580
580
|
logger.info(
|
|
@@ -603,7 +603,7 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
603
603
|
getActiveConversations() {
|
|
604
604
|
return Array.from(this.activeConversations);
|
|
605
605
|
}
|
|
606
|
-
// ───
|
|
606
|
+
// ─── follower event handlers ─────────────────────────────────────────────
|
|
607
607
|
onFileAdded(filePath) {
|
|
608
608
|
if (this.isShuttingDown) return;
|
|
609
609
|
const conversationId = this.extractConversationId(filePath);
|
|
@@ -625,57 +625,292 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
625
625
|
this.emit("transcript-add", filePath, conversationId);
|
|
626
626
|
void this.scheduleReadNewLines(filePath, conversationId);
|
|
627
627
|
}
|
|
628
|
+
// ─── Discovery (no directory watch) ─────────────────────────────────────
|
|
628
629
|
/**
|
|
629
|
-
*
|
|
630
|
-
*
|
|
631
|
-
* Purely lexical — no filesystem call — so it is cheap enough to run on every
|
|
632
|
-
* traversed path and cannot be fooled by a stat race. The only paths admitted
|
|
633
|
-
* are the ones that can lead to a transcript:
|
|
634
|
-
*
|
|
635
|
-
* <brainRoot> depth 0
|
|
636
|
-
* <brainRoot>/<uuid> depth 1
|
|
637
|
-
* <brainRoot>/<uuid>/.system_generated depth 2
|
|
638
|
-
* <brainRoot>/<uuid>/.system_generated/logs depth 3
|
|
639
|
-
* <brainRoot>/<uuid>/.system_generated/logs/transcript.jsonl depth 4
|
|
640
|
-
*
|
|
641
|
-
* Anything else — scratch/, evidence/, .user_uploaded/, the task logs, and
|
|
642
|
-
* every file that is not the transcript — is refused, which stops chokidar
|
|
643
|
-
* descending into it at all.
|
|
644
|
-
*
|
|
645
|
-
* NOTE the depth-1 UUID check is what keeps this bounded as the brain grows:
|
|
646
|
-
* a non-conversation entry at the top level can never be a conversation, so
|
|
647
|
-
* it is never traversed. `depth: 4` on the watcher remains as a second,
|
|
648
|
-
* independent bound.
|
|
630
|
+
* One discovery tick. Never throws: a throw from a timer callback is an
|
|
631
|
+
* uncaughtException, which the daemon's process guard may treat as fatal.
|
|
649
632
|
*/
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
633
|
+
discoveryTick() {
|
|
634
|
+
if (!this.isWatching || this.isShuttingDown) return;
|
|
635
|
+
try {
|
|
636
|
+
this.discoveryTickCount++;
|
|
637
|
+
const rootMtimeMs = this.statMtimeMs(this.brainRoot);
|
|
638
|
+
if (rootMtimeMs !== null && rootMtimeMs !== this.rootMtimeMs) {
|
|
639
|
+
this.rootMtimeMs = rootMtimeMs;
|
|
640
|
+
this.reconcileConversationDirs();
|
|
641
|
+
}
|
|
642
|
+
for (const [conversationId, transcriptPath] of Array.from(this.pendingTranscripts)) {
|
|
643
|
+
if (fs2.existsSync(transcriptPath)) {
|
|
644
|
+
this.pendingTranscripts.delete(conversationId);
|
|
645
|
+
this.followTranscript(conversationId, transcriptPath, "added");
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (this.discoveryTickCount % RESUME_SWEEP_EVERY_TICKS === 0) {
|
|
649
|
+
this.reconcileConversationDirs();
|
|
650
|
+
this.sweepForResumedTranscripts();
|
|
651
|
+
}
|
|
652
|
+
} catch (error) {
|
|
653
|
+
this.emitError(error, "Transcript discovery tick failed (non-fatal)");
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
/** Bring knownConversationDirs in line with the brain root's listing. */
|
|
657
|
+
reconcileConversationDirs() {
|
|
658
|
+
const present = this.listConversationDirs();
|
|
659
|
+
if (present === null) return;
|
|
660
|
+
const presentSet = new Set(present);
|
|
661
|
+
for (const conversationId of present) {
|
|
662
|
+
if (this.knownConversationDirs.has(conversationId)) continue;
|
|
663
|
+
this.knownConversationDirs.add(conversationId);
|
|
664
|
+
this.onConversationDirAdded(conversationId);
|
|
665
|
+
}
|
|
666
|
+
const tracked = /* @__PURE__ */ new Set([
|
|
667
|
+
...this.knownConversationDirs,
|
|
668
|
+
...this.pendingTranscripts.keys(),
|
|
669
|
+
...this.followed.keys()
|
|
670
|
+
]);
|
|
671
|
+
for (const conversationId of tracked) {
|
|
672
|
+
if (presentSet.has(conversationId)) continue;
|
|
673
|
+
this.knownConversationDirs.delete(conversationId);
|
|
674
|
+
this.pendingTranscripts.delete(conversationId);
|
|
675
|
+
this.transcriptSnapshots.delete(conversationId);
|
|
676
|
+
this.unfollow(conversationId, "conversation directory removed");
|
|
677
|
+
this.resetCursorForReplacement(
|
|
678
|
+
this.transcriptPathFor(conversationId),
|
|
679
|
+
conversationId,
|
|
680
|
+
"conversation directory removed"
|
|
681
|
+
);
|
|
682
|
+
this.activeConversations.delete(conversationId);
|
|
668
683
|
}
|
|
669
684
|
}
|
|
670
|
-
|
|
685
|
+
onConversationDirAdded(conversationId) {
|
|
671
686
|
if (this.isShuttingDown) return;
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
if (path2.resolve(expectedParent) !== path2.resolve(this.brainRoot)) return;
|
|
677
|
-
logger.info("New brain directory discovered (pre-prompt)", { dirPath, conversationId });
|
|
687
|
+
logger.info("New brain directory discovered (pre-prompt)", {
|
|
688
|
+
dirPath: path2.join(this.brainRoot, conversationId),
|
|
689
|
+
conversationId
|
|
690
|
+
});
|
|
678
691
|
this.emit("conversation-discovered", conversationId);
|
|
692
|
+
const transcriptPath = this.transcriptPathFor(conversationId);
|
|
693
|
+
this.transcriptSnapshots.set(conversationId, null);
|
|
694
|
+
if (fs2.existsSync(transcriptPath)) {
|
|
695
|
+
this.followTranscript(conversationId, transcriptPath, "added");
|
|
696
|
+
} else {
|
|
697
|
+
this.pendingTranscripts.set(conversationId, transcriptPath);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
sweepForResumedTranscripts() {
|
|
701
|
+
for (const conversationId of this.knownConversationDirs) {
|
|
702
|
+
if (this.followed.has(conversationId)) continue;
|
|
703
|
+
if (this.pendingTranscripts.has(conversationId)) continue;
|
|
704
|
+
const transcriptPath = this.transcriptPathFor(conversationId);
|
|
705
|
+
const current = this.snapshotTranscript(transcriptPath);
|
|
706
|
+
const previous = this.transcriptSnapshots.get(conversationId) ?? null;
|
|
707
|
+
if (current === null) {
|
|
708
|
+
if (previous !== null && this.isAbsent(transcriptPath)) {
|
|
709
|
+
this.transcriptSnapshots.set(conversationId, null);
|
|
710
|
+
this.resetCursorForReplacement(transcriptPath, conversationId, "deleted while unfollowed");
|
|
711
|
+
}
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
this.transcriptSnapshots.set(conversationId, current);
|
|
715
|
+
if (previous !== null && current.ino !== previous.ino && !this.fileInodes.has(transcriptPath)) {
|
|
716
|
+
this.resetCursorForReplacement(transcriptPath, conversationId, "inode changed while unfollowed");
|
|
717
|
+
this.emit("transcript-rotated", transcriptPath, conversationId);
|
|
718
|
+
}
|
|
719
|
+
if (previous === null || current.size !== previous.size || current.mtimeMs !== previous.mtimeMs || current.ino !== previous.ino || this.hasUnreadTail(conversationId, transcriptPath, current.size)) {
|
|
720
|
+
this.followTranscript(conversationId, transcriptPath, "resumed");
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* A conversation released by the follow cap in this lifetime whose file
|
|
726
|
+
* holds bytes past its cursor: its eviction drain did not finish (e.g. a
|
|
727
|
+
* transient stat/open error), so the final message would otherwise wait for
|
|
728
|
+
* the next write (Stage 2 R1-F8). Limited to released conversations — the
|
|
729
|
+
* offsets file also holds other daemons' cursors (F2), and following those
|
|
730
|
+
* on stale offsets would pull another session's history in.
|
|
731
|
+
*/
|
|
732
|
+
hasUnreadTail(conversationId, transcriptPath, size) {
|
|
733
|
+
if (!this.releasedConversations.has(conversationId)) return false;
|
|
734
|
+
if (this.activeReads.has(transcriptPath)) return false;
|
|
735
|
+
const entry = this.offsets.get(transcriptPath);
|
|
736
|
+
if (!entry) return false;
|
|
737
|
+
return size > entry.byteOffset + Buffer.byteLength(entry.carryover, "utf-8");
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Start following one transcript by stat-polling that file (no `fs.watch`
|
|
741
|
+
* handle), then route it through the same handler a filesystem event would
|
|
742
|
+
* have: `add` for a transcript created after start, `change` for an existing
|
|
743
|
+
* one (first sight of it is treated as a resume).
|
|
744
|
+
*/
|
|
745
|
+
followTranscript(conversationId, transcriptPath, how) {
|
|
746
|
+
if (this.isShuttingDown || !this.isWatching) return;
|
|
747
|
+
if (this.followed.has(conversationId)) return;
|
|
748
|
+
let watcher;
|
|
749
|
+
try {
|
|
750
|
+
watcher = watchOneFilePolled(transcriptPath, {
|
|
751
|
+
persistent: true,
|
|
752
|
+
ignoreInitial: true,
|
|
753
|
+
usePolling: true,
|
|
754
|
+
interval: TRANSCRIPT_POLL_INTERVAL_MS,
|
|
755
|
+
awaitWriteFinish: false
|
|
756
|
+
// partial-line carryover handles partial writes
|
|
757
|
+
});
|
|
758
|
+
} catch (error) {
|
|
759
|
+
this.emitError(error, "Could not follow transcript by polling");
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
while (this.followed.size >= MAX_FOLLOWED_TRANSCRIPTS) {
|
|
763
|
+
let oldestId = null;
|
|
764
|
+
let oldestMs = Number.POSITIVE_INFINITY;
|
|
765
|
+
for (const [id, f] of this.followed) {
|
|
766
|
+
if (f.lastActivityMs < oldestMs) {
|
|
767
|
+
oldestMs = f.lastActivityMs;
|
|
768
|
+
oldestId = id;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
if (oldestId === null) break;
|
|
772
|
+
this.unfollow(oldestId, "follow cap reached", { drainTail: true });
|
|
773
|
+
}
|
|
774
|
+
const followed = {
|
|
775
|
+
watcher,
|
|
776
|
+
transcriptPath,
|
|
777
|
+
lastActivityMs: Date.now()
|
|
778
|
+
};
|
|
779
|
+
this.followed.set(conversationId, followed);
|
|
780
|
+
const touch = () => {
|
|
781
|
+
followed.lastActivityMs = Date.now();
|
|
782
|
+
};
|
|
783
|
+
watcher.on("add", (filePath) => {
|
|
784
|
+
touch();
|
|
785
|
+
this.onFileAdded(filePath);
|
|
786
|
+
});
|
|
787
|
+
watcher.on("change", (filePath) => {
|
|
788
|
+
touch();
|
|
789
|
+
this.onFileChanged(filePath);
|
|
790
|
+
});
|
|
791
|
+
watcher.on("unlink", (filePath) => this.onFileUnlinked(filePath));
|
|
792
|
+
watcher.on("error", (error) => {
|
|
793
|
+
this.emitError(error, "Chokidar error");
|
|
794
|
+
});
|
|
795
|
+
watcher.on("ready", () => {
|
|
796
|
+
const timer = setTimeout(() => {
|
|
797
|
+
if (this.followed.get(conversationId)?.watcher !== watcher) return;
|
|
798
|
+
if (!this.activeConversations.has(conversationId)) return;
|
|
799
|
+
void this.scheduleReadNewLines(transcriptPath, conversationId);
|
|
800
|
+
}, FOLLOW_CATCH_UP_READ_DELAY_MS);
|
|
801
|
+
if (timer.unref) timer.unref();
|
|
802
|
+
});
|
|
803
|
+
logger.info("Following transcript", {
|
|
804
|
+
conversationId,
|
|
805
|
+
how,
|
|
806
|
+
followedCount: this.followed.size,
|
|
807
|
+
pollIntervalMs: TRANSCRIPT_POLL_INTERVAL_MS
|
|
808
|
+
});
|
|
809
|
+
if (how === "added") this.onFileAdded(transcriptPath);
|
|
810
|
+
else this.onFileChanged(transcriptPath);
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Stop following one transcript. Its offset is kept, its swept stat is
|
|
814
|
+
* refreshed so the resume sweep only re-follows it on a NEW write, and it
|
|
815
|
+
* leaves activeConversations so a re-follow is a resume.
|
|
816
|
+
*
|
|
817
|
+
* drainTail (eviction): read whatever is past the offset now. Otherwise a
|
|
818
|
+
* write that landed within the last poll interval would sit under the
|
|
819
|
+
* refreshed snapshot, unread until that conversation writes again — for a
|
|
820
|
+
* final message, indefinitely. An evicted conversation also joins
|
|
821
|
+
* releasedConversations, which protects its offset from LRU eviction so a
|
|
822
|
+
* re-follow never replays history. Without drainTail (its directory was
|
|
823
|
+
* removed) the offset is LRU-evictable.
|
|
824
|
+
*/
|
|
825
|
+
unfollow(conversationId, reason, opts = {}) {
|
|
826
|
+
const followed = this.followed.get(conversationId);
|
|
827
|
+
if (!followed) return;
|
|
828
|
+
if (this.knownConversationDirs.has(conversationId)) {
|
|
829
|
+
this.transcriptSnapshots.set(conversationId, this.snapshotTranscript(followed.transcriptPath));
|
|
830
|
+
}
|
|
831
|
+
if (opts.drainTail) {
|
|
832
|
+
this.releasedConversations.add(conversationId);
|
|
833
|
+
void this.scheduleReadNewLines(followed.transcriptPath, conversationId);
|
|
834
|
+
} else {
|
|
835
|
+
this.releasedConversations.delete(conversationId);
|
|
836
|
+
}
|
|
837
|
+
this.followed.delete(conversationId);
|
|
838
|
+
this.activeConversations.delete(conversationId);
|
|
839
|
+
followed.watcher.close().catch((error) => {
|
|
840
|
+
this.emitError(error, "Transcript follower close failed (non-fatal)");
|
|
841
|
+
});
|
|
842
|
+
logger.info("Stopped following transcript", { conversationId, reason });
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* The file at this path is no longer the one the cursor describes (deleted,
|
|
846
|
+
* its directory removed, or replaced by a different inode). Restart it from
|
|
847
|
+
* byte 0, as the former tree watch's `unlink` → onFileUnlinked did. The
|
|
848
|
+
* released-follower offset protection ends with the file it protected —
|
|
849
|
+
* except when the reset is made by the read that is about to consume the
|
|
850
|
+
* new file (`keepProtection`): that read advances the cursor over content it
|
|
851
|
+
* delivers, so the cursor must stay protected or a later re-follow would
|
|
852
|
+
* replay it (Stage 2 R1-F6).
|
|
853
|
+
*/
|
|
854
|
+
resetCursorForReplacement(filePath, conversationId, reason, opts = {}) {
|
|
855
|
+
const entry = this.offsets.get(filePath);
|
|
856
|
+
if (entry) {
|
|
857
|
+
logger.warn("Transcript replaced or removed; resetting offset", {
|
|
858
|
+
filePath,
|
|
859
|
+
conversationId,
|
|
860
|
+
reason,
|
|
861
|
+
previousOffset: entry.byteOffset
|
|
862
|
+
});
|
|
863
|
+
entry.byteOffset = 0;
|
|
864
|
+
entry.carryover = "";
|
|
865
|
+
entry.lastKnownSize = 0;
|
|
866
|
+
entry.lastModifiedMs = Date.now();
|
|
867
|
+
}
|
|
868
|
+
this.fileInodes.delete(filePath);
|
|
869
|
+
if (!opts.keepProtection) this.releasedConversations.delete(conversationId);
|
|
870
|
+
this.bumpCursorResets(filePath);
|
|
871
|
+
}
|
|
872
|
+
/** Only a read chain in flight can hold a stale cursor, so only then is the
|
|
873
|
+
* count kept; the chain's end prunes it (Stage 2 R1-F7). Outside resets
|
|
874
|
+
* land at an `await`, by which time the chain is registered in activeReads. */
|
|
875
|
+
bumpCursorResets(filePath) {
|
|
876
|
+
if (!this.activeReads.has(filePath)) return;
|
|
877
|
+
this.cursorResets.set(filePath, (this.cursorResets.get(filePath) ?? 0) + 1);
|
|
878
|
+
}
|
|
879
|
+
transcriptPathFor(conversationId) {
|
|
880
|
+
return path2.join(this.brainRoot, conversationId, SYSTEM_GENERATED_DIR, LOGS_DIR, TRANSCRIPT_BASENAME);
|
|
881
|
+
}
|
|
882
|
+
/** UUID-named directories directly under the brain root; null on failure. */
|
|
883
|
+
listConversationDirs() {
|
|
884
|
+
try {
|
|
885
|
+
return fs2.readdirSync(this.brainRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && CONVERSATION_DIR_REGEX.test(entry.name)).map((entry) => entry.name);
|
|
886
|
+
} catch {
|
|
887
|
+
return null;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
snapshotTranscript(transcriptPath) {
|
|
891
|
+
try {
|
|
892
|
+
const stat = fs2.statSync(transcriptPath);
|
|
893
|
+
return stat.isFile() ? { size: stat.size, mtimeMs: stat.mtimeMs, ino: stat.ino } : null;
|
|
894
|
+
} catch {
|
|
895
|
+
return null;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
/** True only when the path is confirmed absent (ENOENT / ENOTDIR). */
|
|
899
|
+
isAbsent(filePath) {
|
|
900
|
+
try {
|
|
901
|
+
fs2.statSync(filePath);
|
|
902
|
+
return false;
|
|
903
|
+
} catch (error) {
|
|
904
|
+
const code = error.code;
|
|
905
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
statMtimeMs(dirPath) {
|
|
909
|
+
try {
|
|
910
|
+
return fs2.statSync(dirPath).mtimeMs;
|
|
911
|
+
} catch {
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
679
914
|
}
|
|
680
915
|
/**
|
|
681
916
|
* Fires when chokidar reports a transcript.jsonl was deleted or moved
|
|
@@ -709,6 +944,9 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
709
944
|
entry.carryover = "";
|
|
710
945
|
entry.lastKnownSize = 0;
|
|
711
946
|
entry.lastModifiedMs = Date.now();
|
|
947
|
+
this.fileInodes.delete(filePath);
|
|
948
|
+
this.releasedConversations.delete(conversationId);
|
|
949
|
+
this.bumpCursorResets(filePath);
|
|
712
950
|
this.activeConversations.delete(conversationId);
|
|
713
951
|
}
|
|
714
952
|
onFileChanged(filePath) {
|
|
@@ -728,7 +966,7 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
728
966
|
}
|
|
729
967
|
// ─── Backfill at startup ────────────────────────────────────────────────
|
|
730
968
|
/**
|
|
731
|
-
* Walk the brain root once at startup and
|
|
969
|
+
* Walk the brain root once at startup and follow (as a resume) any
|
|
732
970
|
* transcript.jsonl that already exists and shows recent activity. This
|
|
733
971
|
* covers the case where agy is already running when codevibe-agy starts.
|
|
734
972
|
*/
|
|
@@ -740,6 +978,13 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
740
978
|
if (!dir.isDirectory()) continue;
|
|
741
979
|
const conversationId = dir.name;
|
|
742
980
|
if (!/^[0-9a-fA-F-]{36}$/.test(conversationId)) continue;
|
|
981
|
+
if (!this.knownConversationDirs.has(conversationId)) {
|
|
982
|
+
this.knownConversationDirs.add(conversationId);
|
|
983
|
+
this.transcriptSnapshots.set(
|
|
984
|
+
conversationId,
|
|
985
|
+
this.snapshotTranscript(this.transcriptPathFor(conversationId))
|
|
986
|
+
);
|
|
987
|
+
}
|
|
743
988
|
const dirPath = path2.join(this.brainRoot, conversationId);
|
|
744
989
|
const transcriptPath = path2.join(
|
|
745
990
|
dirPath,
|
|
@@ -777,16 +1022,16 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
777
1022
|
transcriptPath,
|
|
778
1023
|
conversationId
|
|
779
1024
|
});
|
|
780
|
-
this.
|
|
781
|
-
this.ensureOffset(transcriptPath);
|
|
782
|
-
this.emit("transcript-resume", transcriptPath, conversationId);
|
|
783
|
-
void this.scheduleReadNewLines(transcriptPath, conversationId);
|
|
1025
|
+
this.followTranscript(conversationId, transcriptPath, "resumed");
|
|
784
1026
|
} else {
|
|
785
1027
|
logger.info("Backfilling recent transcriptless directory at startup", {
|
|
786
1028
|
dirPath,
|
|
787
1029
|
conversationId
|
|
788
1030
|
});
|
|
789
1031
|
this.emit("conversation-discovered", conversationId);
|
|
1032
|
+
if (!this.followed.has(conversationId)) {
|
|
1033
|
+
this.pendingTranscripts.set(conversationId, transcriptPath);
|
|
1034
|
+
}
|
|
790
1035
|
}
|
|
791
1036
|
}
|
|
792
1037
|
} catch (error) {
|
|
@@ -813,6 +1058,7 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
813
1058
|
if (this.activeReads.get(filePath) === read) {
|
|
814
1059
|
this.activeReads.delete(filePath);
|
|
815
1060
|
this.pendingReads.delete(filePath);
|
|
1061
|
+
this.cursorResets.delete(filePath);
|
|
816
1062
|
}
|
|
817
1063
|
});
|
|
818
1064
|
return read;
|
|
@@ -837,6 +1083,12 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
837
1083
|
logger.warn("stat failed; file may have been deleted", { filePath, error });
|
|
838
1084
|
return;
|
|
839
1085
|
}
|
|
1086
|
+
const lastInode = this.fileInodes.get(filePath);
|
|
1087
|
+
if (lastInode !== void 0 && lastInode !== stat.ino) {
|
|
1088
|
+
this.resetCursorForReplacement(filePath, conversationId, "inode changed", { keepProtection: true });
|
|
1089
|
+
this.emit("transcript-rotated", filePath, conversationId);
|
|
1090
|
+
}
|
|
1091
|
+
this.fileInodes.set(filePath, stat.ino);
|
|
840
1092
|
if (stat.size < entry.lastKnownSize) {
|
|
841
1093
|
logger.warn("Transcript file rotated (size shrunk); resetting offset", {
|
|
842
1094
|
filePath,
|
|
@@ -849,6 +1101,12 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
849
1101
|
entry.lastKnownSize = stat.size;
|
|
850
1102
|
this.emit("transcript-rotated", filePath, conversationId);
|
|
851
1103
|
}
|
|
1104
|
+
const resetsAtStart = this.cursorResets.get(filePath) ?? 0;
|
|
1105
|
+
const staleRead = () => {
|
|
1106
|
+
if ((this.cursorResets.get(filePath) ?? 0) === resetsAtStart) return false;
|
|
1107
|
+
this.pendingReads.add(filePath);
|
|
1108
|
+
return true;
|
|
1109
|
+
};
|
|
852
1110
|
const carryoverBytes = Buffer.byteLength(entry.carryover, "utf-8");
|
|
853
1111
|
const readStart = entry.byteOffset + carryoverBytes;
|
|
854
1112
|
if (stat.size <= readStart) {
|
|
@@ -871,14 +1129,14 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
871
1129
|
let lineStartOffset = entry.byteOffset;
|
|
872
1130
|
try {
|
|
873
1131
|
for await (const chunk of stream) {
|
|
874
|
-
if (generation !== this.readGeneration) {
|
|
1132
|
+
if (generation !== this.readGeneration || staleRead()) {
|
|
875
1133
|
stream.destroy();
|
|
876
1134
|
return;
|
|
877
1135
|
}
|
|
878
1136
|
buffered += chunk;
|
|
879
1137
|
let newlineIndex = buffered.indexOf("\n");
|
|
880
1138
|
while (newlineIndex !== -1) {
|
|
881
|
-
if (generation !== this.readGeneration) {
|
|
1139
|
+
if (generation !== this.readGeneration || staleRead()) {
|
|
882
1140
|
stream.destroy();
|
|
883
1141
|
return;
|
|
884
1142
|
}
|
|
@@ -900,7 +1158,7 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
900
1158
|
this.emitError(error);
|
|
901
1159
|
return;
|
|
902
1160
|
}
|
|
903
|
-
if (generation !== this.readGeneration) return;
|
|
1161
|
+
if (generation !== this.readGeneration || staleRead()) return;
|
|
904
1162
|
entry.byteOffset = lineStartOffset;
|
|
905
1163
|
entry.carryover = buffered;
|
|
906
1164
|
entry.lastKnownSize = stat.size;
|
|
@@ -964,7 +1222,7 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
964
1222
|
let oldestMtime = Number.POSITIVE_INFINITY;
|
|
965
1223
|
for (const [p, entry] of this.offsets.entries()) {
|
|
966
1224
|
const cid = this.extractConversationId(p);
|
|
967
|
-
if (cid && this.activeConversations.has(cid)) {
|
|
1225
|
+
if (cid && (this.activeConversations.has(cid) || this.releasedConversations.has(cid))) {
|
|
968
1226
|
continue;
|
|
969
1227
|
}
|
|
970
1228
|
if (entry.lastModifiedMs < oldestMtime) {
|
|
@@ -1962,8 +2220,8 @@ function escapeShellArg(value) {
|
|
|
1962
2220
|
return value.replace(/'/g, `'\\''`);
|
|
1963
2221
|
}
|
|
1964
2222
|
function sleep(ms) {
|
|
1965
|
-
return new Promise((
|
|
1966
|
-
const t = setTimeout(
|
|
2223
|
+
return new Promise((resolve2) => {
|
|
2224
|
+
const t = setTimeout(resolve2, ms);
|
|
1967
2225
|
if (t.unref) t.unref();
|
|
1968
2226
|
});
|
|
1969
2227
|
}
|
|
@@ -3135,14 +3393,14 @@ function sanitizeErrorMessage(msg) {
|
|
|
3135
3393
|
return out.trim().substring(0, 100);
|
|
3136
3394
|
}
|
|
3137
3395
|
function fireDaemonBeacon(name, params) {
|
|
3138
|
-
return new Promise((
|
|
3396
|
+
return new Promise((resolve2) => {
|
|
3139
3397
|
let req;
|
|
3140
3398
|
let settled = false;
|
|
3141
3399
|
const settle = () => {
|
|
3142
3400
|
if (settled) return;
|
|
3143
3401
|
settled = true;
|
|
3144
3402
|
clearTimeout(overall);
|
|
3145
|
-
|
|
3403
|
+
resolve2();
|
|
3146
3404
|
};
|
|
3147
3405
|
const overall = setTimeout(() => {
|
|
3148
3406
|
try {
|
|
@@ -4126,8 +4384,8 @@ var McpServer = class _McpServer {
|
|
|
4126
4384
|
let hostedRetirementError = null;
|
|
4127
4385
|
if (launchSettlement) {
|
|
4128
4386
|
let timer;
|
|
4129
|
-
const timeout = new Promise((
|
|
4130
|
-
timer = setTimeout(
|
|
4387
|
+
const timeout = new Promise((resolve2) => {
|
|
4388
|
+
timer = setTimeout(resolve2, this.launchSettleTimeoutMs);
|
|
4131
4389
|
timer.unref?.();
|
|
4132
4390
|
});
|
|
4133
4391
|
try {
|
|
@@ -4280,7 +4538,7 @@ var McpServer = class _McpServer {
|
|
|
4280
4538
|
error: String(err)
|
|
4281
4539
|
});
|
|
4282
4540
|
if (attempt < SESSION_RETIRE_MAX_ATTEMPTS) {
|
|
4283
|
-
await new Promise((
|
|
4541
|
+
await new Promise((resolve2) => setTimeout(resolve2, SESSION_RETIRE_RETRY_MS * attempt));
|
|
4284
4542
|
}
|
|
4285
4543
|
}
|
|
4286
4544
|
}
|
|
@@ -4940,7 +5198,7 @@ var McpServer = class _McpServer {
|
|
|
4940
5198
|
if (this.reRaiseSuperseded(gen)) return;
|
|
4941
5199
|
const slice = Math.min(50, deadline - Date.now());
|
|
4942
5200
|
if (slice <= 0) return;
|
|
4943
|
-
await new Promise((
|
|
5201
|
+
await new Promise((resolve2) => setTimeout(resolve2, slice));
|
|
4944
5202
|
}
|
|
4945
5203
|
}
|
|
4946
5204
|
/**
|
|
@@ -5036,7 +5294,7 @@ var McpServer = class _McpServer {
|
|
|
5036
5294
|
);
|
|
5037
5295
|
await this.interruptibleBackoff(delay2, gen);
|
|
5038
5296
|
} else {
|
|
5039
|
-
await new Promise((
|
|
5297
|
+
await new Promise((resolve2) => setTimeout(resolve2, 250 * attempt));
|
|
5040
5298
|
}
|
|
5041
5299
|
}
|
|
5042
5300
|
}
|
|
@@ -5541,7 +5799,7 @@ var McpServer = class _McpServer {
|
|
|
5541
5799
|
return;
|
|
5542
5800
|
}
|
|
5543
5801
|
promptContent = intent.followUpText;
|
|
5544
|
-
await new Promise((
|
|
5802
|
+
await new Promise((resolve2) => setTimeout(resolve2, 250));
|
|
5545
5803
|
} else {
|
|
5546
5804
|
const dismissal = await this.promptResponder.dismissActivePrompt();
|
|
5547
5805
|
if (!dismissal.ok) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quantiya/codevibe-antigravity-plugin",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.61",
|
|
4
4
|
"description": "Control Antigravity CLI from your iPhone and Android — real-time sync, approve file edits, send prompts by voice. Part of CodeVibe.",
|
|
5
5
|
"main": "dist/server.js",
|
|
6
6
|
"codevibe": {
|