@quantiya/codevibe-antigravity-plugin 2.0.51 → 2.0.53
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/dist/server.js +109 -11
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -350,6 +350,10 @@ var OFFSET_FLUSH_INTERVAL_MS = 3e4;
|
|
|
350
350
|
var ERROR_LOG_MIN_INTERVAL_MS = 1e4;
|
|
351
351
|
var LRU_CAP = 50;
|
|
352
352
|
var TRANSCRIPT_FILE_REGEX = /\/brain\/([0-9a-fA-F-]{36})\/\.system_generated\/logs\/transcript\.jsonl$/;
|
|
353
|
+
var CONVERSATION_DIR_REGEX = /^[0-9a-fA-F-]{36}$/;
|
|
354
|
+
var SYSTEM_GENERATED_DIR = ".system_generated";
|
|
355
|
+
var LOGS_DIR = "logs";
|
|
356
|
+
var TRANSCRIPT_BASENAME = "transcript.jsonl";
|
|
353
357
|
function defaultPaths() {
|
|
354
358
|
const home = os2.homedir();
|
|
355
359
|
return {
|
|
@@ -428,17 +432,69 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
428
432
|
// partial-line carryover handles partial writes
|
|
429
433
|
usePolling: this.usePolling,
|
|
430
434
|
interval: this.usePolling ? 50 : void 0,
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
435
|
+
// R24 — admit ONLY the exact path shape that can lead to a transcript.
|
|
436
|
+
//
|
|
437
|
+
// THE DEFECT THIS REPLACES: the previous predicate returned `false` for
|
|
438
|
+
// EVERY directory ("allow directories so chokidar can descend into
|
|
439
|
+
// them"), so chokidar descended into the whole brain. Measured on a real
|
|
440
|
+
// machine 2026-09-22: **40,345 directories across 234 conversations**
|
|
441
|
+
// (7.8 GB), on chokidar's kqueue backend which needs roughly one file
|
|
442
|
+
// descriptor per watched directory. The watcher exhausted its descriptors
|
|
443
|
+
// and threw EMFILE continuously — 77,359 errors in a single 28-minute
|
|
444
|
+
// daemon start, 89% of the whole log file across the day.
|
|
445
|
+
//
|
|
446
|
+
// That was not merely wasteful, it was a CORRECTNESS failure, and the
|
|
447
|
+
// exact shape matters because it is worse than a slow start:
|
|
448
|
+
//
|
|
449
|
+
// 1. The initial scan storms for tens of minutes.
|
|
450
|
+
// 2. When it finally completes, `ready` fires and
|
|
451
|
+
// backfillActiveTranscripts() does a ONE-SHOT catch-up read. That
|
|
452
|
+
// uses its own readdirSync, NOT the watcher, so it succeeds — mobile
|
|
453
|
+
// shows a sudden burst of backlog.
|
|
454
|
+
// 3. But the per-file WATCH was never established, because the
|
|
455
|
+
// descriptors were spent on old conversations during the scan. So no
|
|
456
|
+
// `change` event ever arrives again and the tailer STALLS PERMANENTLY
|
|
457
|
+
// at whatever offset the backfill reached.
|
|
458
|
+
//
|
|
459
|
+
// Measured live on the owner's machine while this fix was being written,
|
|
460
|
+
// and RETAINED — every figure here comes from that capture, not from
|
|
461
|
+
// recollection:
|
|
462
|
+
// .hard-gate/r24-watch-scope/evidence/prefix-descriptor-accounting.log
|
|
463
|
+
// The daemon held file descriptors for 231 OLD conversations and **zero**
|
|
464
|
+
// for the active one, while its offset sat frozen at 987,484 bytes
|
|
465
|
+
// against a transcript of 1,656,028 on disk — 668 KB behind and not
|
|
466
|
+
// advancing, with the EMFILE rate back to 0/sec and the daemon otherwise
|
|
467
|
+
// idle at 0.3% CPU. The gap WIDENED between two samplings twenty minutes
|
|
468
|
+
// apart (231 KB, then 668 KB), which is what distinguishes a permanent
|
|
469
|
+
// stall from slow catch-up.
|
|
470
|
+
//
|
|
471
|
+
// User-visible result: a burst of backlog, then silence. Interactive
|
|
472
|
+
// prompts degrade to "Prompt candidate fired but no active conversation
|
|
473
|
+
// known {observedConvCount:0}", which renders on mobile as "a prompt is
|
|
474
|
+
// waiting in your desktop terminal" with NO selectable options — the user
|
|
475
|
+
// can see that a prompt exists but cannot answer it, which is the entire
|
|
476
|
+
// purpose of the mobile companion.
|
|
477
|
+
//
|
|
478
|
+
// NOTE this description was corrected twice. It was first written as
|
|
479
|
+
// "permanent", then weakened to "a bounded dead window" after observing a
|
|
480
|
+
// recovery, then corrected again once the descriptor accounting above was
|
|
481
|
+
// actually measured. The recovery was the one-shot backfill of step 2, not
|
|
482
|
+
// the watcher healing. Stage 1 NB-4 predicted this sub-case before it was
|
|
483
|
+
// measured.
|
|
484
|
+
//
|
|
485
|
+
// THE FIX: the transcript lives at exactly one shape, four levels deep:
|
|
486
|
+
// brain/<uuid>/.system_generated/logs/transcript.jsonl
|
|
487
|
+
// so decide purely LEXICALLY on that shape and refuse everything else.
|
|
488
|
+
// Same machine, after: **699** directories (1 root + 234 conversations +
|
|
489
|
+
// 232 .system_generated + 232 logs) — a 57x reduction that also excludes
|
|
490
|
+
// the actual bulk: scratch/ (230 dirs), .user_uploaded/ (232) and the
|
|
491
|
+
// thousands of .system_generated/tasks/ dirs, none of which can ever hold
|
|
492
|
+
// a transcript.
|
|
493
|
+
//
|
|
494
|
+
// Deciding lexically also removes the per-path `fs.statSync` the old
|
|
495
|
+
// predicate ran during traversal, which was its own CPU cost on a tree
|
|
496
|
+
// this size.
|
|
497
|
+
ignored: (filePath) => this.isOutsideTranscriptPath(filePath)
|
|
442
498
|
});
|
|
443
499
|
this.watcher.on("add", (filePath) => this.onFileAdded(filePath));
|
|
444
500
|
this.watcher.on("addDir", (dirPath) => this.onDirAdded(dirPath));
|
|
@@ -568,6 +624,48 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
568
624
|
this.emit("transcript-add", filePath, conversationId);
|
|
569
625
|
void this.scheduleReadNewLines(filePath, conversationId);
|
|
570
626
|
}
|
|
627
|
+
/**
|
|
628
|
+
* R24 traversal filter: true = chokidar must NOT descend into / watch this.
|
|
629
|
+
*
|
|
630
|
+
* Purely lexical — no filesystem call — so it is cheap enough to run on every
|
|
631
|
+
* traversed path and cannot be fooled by a stat race. The only paths admitted
|
|
632
|
+
* are the ones that can lead to a transcript:
|
|
633
|
+
*
|
|
634
|
+
* <brainRoot> depth 0
|
|
635
|
+
* <brainRoot>/<uuid> depth 1
|
|
636
|
+
* <brainRoot>/<uuid>/.system_generated depth 2
|
|
637
|
+
* <brainRoot>/<uuid>/.system_generated/logs depth 3
|
|
638
|
+
* <brainRoot>/<uuid>/.system_generated/logs/transcript.jsonl depth 4
|
|
639
|
+
*
|
|
640
|
+
* Anything else — scratch/, evidence/, .user_uploaded/, the task logs, and
|
|
641
|
+
* every file that is not the transcript — is refused, which stops chokidar
|
|
642
|
+
* descending into it at all.
|
|
643
|
+
*
|
|
644
|
+
* NOTE the depth-1 UUID check is what keeps this bounded as the brain grows:
|
|
645
|
+
* a non-conversation entry at the top level can never be a conversation, so
|
|
646
|
+
* it is never traversed. `depth: 4` on the watcher remains as a second,
|
|
647
|
+
* independent bound.
|
|
648
|
+
*/
|
|
649
|
+
isOutsideTranscriptPath(filePath) {
|
|
650
|
+
const rel = path2.relative(this.brainRoot, path2.resolve(filePath));
|
|
651
|
+
if (rel === "") return false;
|
|
652
|
+
if (rel === ".." || rel.startsWith(".." + path2.sep)) return true;
|
|
653
|
+
const seg = rel.split(path2.sep);
|
|
654
|
+
if (!CONVERSATION_DIR_REGEX.test(seg[0])) return true;
|
|
655
|
+
switch (seg.length) {
|
|
656
|
+
case 1:
|
|
657
|
+
return false;
|
|
658
|
+
// a well-formed conversation directory
|
|
659
|
+
case 2:
|
|
660
|
+
return seg[1] !== SYSTEM_GENERATED_DIR;
|
|
661
|
+
case 3:
|
|
662
|
+
return seg[2] !== LOGS_DIR;
|
|
663
|
+
case 4:
|
|
664
|
+
return seg[3] !== TRANSCRIPT_BASENAME;
|
|
665
|
+
default:
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
571
669
|
onDirAdded(dirPath) {
|
|
572
670
|
if (this.isShuttingDown) return;
|
|
573
671
|
const base = path2.basename(dirPath);
|
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.53",
|
|
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": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"node": ">=22.0.0"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@quantiya/codevibe-core": "^2.0.
|
|
49
|
+
"@quantiya/codevibe-core": "^2.0.49",
|
|
50
50
|
"chokidar": "^5.0.0",
|
|
51
51
|
"dotenv": "^16.6.1",
|
|
52
52
|
"express": "^5.1.0",
|