paseo-acp-agy 1.2.4 → 1.2.6

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.
@@ -85,12 +85,14 @@ export class ACPServer {
85
85
  }
86
86
  publishUsageUpdate(session) {
87
87
  const costUsd = roundUsageCostUsd(session.usage.totalCostUsd);
88
+ const maxTokens = session.usage.contextWindowMaxTokens;
89
+ const usedTokens = Math.min(session.usage.contextWindowUsedTokens, maxTokens);
88
90
  this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
89
91
  sessionId: session.id,
90
92
  update: {
91
93
  sessionUpdate: "usage_update",
92
- size: session.usage.contextWindowMaxTokens,
93
- used: session.usage.contextWindowUsedTokens,
94
+ size: maxTokens,
95
+ used: usedTokens,
94
96
  cost: { amount: costUsd, currency: "USD" },
95
97
  inputTokens: session.usage.inputTokens,
96
98
  outputTokens: session.usage.outputTokens,
@@ -98,8 +100,8 @@ export class ACPServer {
98
100
  cachedInputTokens: session.usage.cachedInputTokens,
99
101
  totalTokens: session.usage.totalTokens,
100
102
  totalCostUsd: costUsd,
101
- contextWindowMaxTokens: session.usage.contextWindowMaxTokens,
102
- contextWindowUsedTokens: session.usage.contextWindowUsedTokens,
103
+ contextWindowMaxTokens: maxTokens,
104
+ contextWindowUsedTokens: usedTokens,
103
105
  },
104
106
  });
105
107
  }
@@ -138,7 +140,7 @@ export class ACPServer {
138
140
  const thought = turnUsage?.thinking_tokens;
139
141
  const total = turnUsage?.total_tokens ?? (input + output);
140
142
  const maxTokens = getModelContextWindow(executingModel || session.model);
141
- const usedTokens = session.usage.contextWindowUsedTokens;
143
+ const usedTokens = Math.min(session.usage.contextWindowUsedTokens, maxTokens);
142
144
  const costUsd = roundUsageCostUsd(session.usage.totalCostUsd);
143
145
  return {
144
146
  inputTokens: input,
@@ -356,7 +358,10 @@ export class ACPServer {
356
358
  break;
357
359
  }
358
360
  let hasSentNonNarration = false;
361
+ let latestStepContextTokens;
362
+ let stepCount = 0;
359
363
  const onStepUpdate = (event) => {
364
+ stepCount++;
360
365
  const step = event.step_update;
361
366
  if (!step)
362
367
  return;
@@ -437,15 +442,18 @@ export class ACPServer {
437
442
  }
438
443
  }
439
444
  if (step.usage) {
440
- const turnCost = calculateUsageCostUsd(executingModel, step.usage.input_tokens || 0, step.usage.output_tokens || 0, step.usage.cache_read_tokens || 0);
441
445
  const inputTokens = step.usage.input_tokens ?? 0;
442
446
  const outputTokens = step.usage.output_tokens ?? 0;
443
447
  const cachedReadTokens = step.usage.cache_read_tokens ?? 0;
448
+ if (inputTokens > 0 || cachedReadTokens > 0) {
449
+ latestStepContextTokens = inputTokens + cachedReadTokens + outputTokens;
450
+ }
451
+ const turnCost = calculateUsageCostUsd(executingModel, inputTokens, outputTokens, cachedReadTokens);
444
452
  const thoughtTokens = step.usage.thinking_tokens;
445
453
  const totalTokens = step.usage.total_tokens ?? (inputTokens + outputTokens);
446
454
  const totalCostUsd = roundUsageCostUsd(session.usage.totalCostUsd + turnCost);
447
455
  const contextWindowMaxTokens = getModelContextWindow(executingModel);
448
- const contextWindowUsedTokens = inputTokens + outputTokens;
456
+ const contextWindowUsedTokens = Math.min(latestStepContextTokens ?? (inputTokens + cachedReadTokens + outputTokens), contextWindowMaxTokens);
449
457
  this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
450
458
  sessionId: session.id,
451
459
  update: {
@@ -468,9 +476,10 @@ export class ACPServer {
468
476
  try {
469
477
  const resultEvent = await session.process.sendPrompt(promptText, onStepUpdate);
470
478
  const turnUsage = resultEvent.result?.usage;
479
+ const numTurns = resultEvent.result?.num_turns || stepCount || 1;
471
480
  // Usage is billable even when the turn terminates with ERROR
472
481
  // after model/tool work. Record it before branching on status.
473
- session.recordTurnUsage(turnUsage, executingModel);
482
+ session.recordTurnUsage(turnUsage, executingModel, latestStepContextTokens, numTurns);
474
483
  const usagePayload = this.turnUsagePayload(session, turnUsage, executingModel);
475
484
  this.publishUsageUpdate(session);
476
485
  if (session.isCancelled) {
package/dist/index.js CHANGED
@@ -160,7 +160,7 @@ if (args.includes("doctor") || args.includes("--doctor")) {
160
160
  process.stdout.write(" Then launch Paseo again.\n");
161
161
  }
162
162
  else {
163
- process.stdout.write(" Tip: Run 'paseo daemon restart' or close the Paseo app to reload.\n");
163
+ process.stdout.write(" Tip: Run 'paseo restart' or close the Paseo app/daemon to reload with updated patches.\n");
164
164
  }
165
165
  }
166
166
  else {
@@ -16,10 +16,16 @@ export declare function findPaseoServerInstallations(): string[];
16
16
  * injected into Paseo server's quota-fetcher providers.
17
17
  */
18
18
  export declare function generateAntigravityQuotaProviderJs(): string;
19
+ /**
20
+ * Generates the TypeScript source for the Paseo 0.11+ Plugin Usage Source.
21
+ * This integrates Antigravity quota and balance metrics into Paseo 0.11+'s
22
+ * new Plugin UsageSource architecture (Settings -> Usage).
23
+ */
24
+ export declare function generateAntigravityPluginUsageTs(): string;
19
25
  /**
20
26
  * Patches a Paseo server installation directory to enable Antigravity:
21
- * 1. Patches quota-fetcher/manifest.js to register Antigravity
22
- * 2. Writes quota-fetcher/providers/antigravity.js
27
+ * 1. For Paseo 0.11+: patches builtin-plugins/antigravity-provider with usage source
28
+ * 2. For Paseo <= 0.10: patches quota-fetcher/manifest.js and writes providers/antigravity.js
23
29
  * 3. Patches acp-agent.js to map context window tokens and emit usage updates
24
30
  */
25
31
  export declare function patchPaseoServer(serverDir: string): {
@@ -222,6 +222,9 @@ const execFileAsync = promisify(execFile);
222
222
  let cachedAgyBin = null;
223
223
  let cachedAgyBinTime = 0;
224
224
  const BIN_CACHE_TTL_MS = 86400000; // 24 hours
225
+ let lastSuccessfulQuota = null;
226
+ let lastSuccessfulQuotaTime = 0;
227
+ const QUOTA_CACHE_TTL_MS = 60000; // 60 seconds
225
228
 
226
229
  function resolveAgyBinary() {
227
230
  if (process.env.AGY_BIN_PATH) return process.env.AGY_BIN_PATH;
@@ -346,6 +349,11 @@ export class AntigravityQuotaProvider {
346
349
  }
347
350
 
348
351
  async fetchUsage() {
352
+ const now = Date.now();
353
+ if (lastSuccessfulQuota && (now - lastSuccessfulQuotaTime < QUOTA_CACHE_TTL_MS)) {
354
+ return lastSuccessfulQuota;
355
+ }
356
+
349
357
  try {
350
358
  const isWin = process.platform === "win32";
351
359
  const bin = resolveAgyBinary();
@@ -388,14 +396,23 @@ export class AntigravityQuotaProvider {
388
396
  env.Path = newPath;
389
397
  }
390
398
 
391
- const [usageRes, creditsRes] = await Promise.allSettled([
392
- execFileAsync(bin, ["--print-timeout", "24h", "--print", "/usage"], { timeout: 15000, env, shell: isBatch, windowsHide: true }),
393
- execFileAsync(bin, ["--print-timeout", "24h", "--print", "/credits"], { timeout: 15000, env, shell: isBatch, windowsHide: true }),
394
- ]);
395
-
396
- if (usageRes.status === "rejected") {
397
- const reason = usageRes.reason;
398
- const msg = reason instanceof Error ? reason.message : String(reason);
399
+ let rawUsageOut = "";
400
+ try {
401
+ const usageRes = await execFileAsync(bin, ["--print-timeout", "24h", "--print", "/usage"], {
402
+ timeout: 30000,
403
+ env,
404
+ shell: isBatch,
405
+ windowsHide: true,
406
+ });
407
+ rawUsageOut = usageRes.stdout || usageRes.stderr || "";
408
+ } catch (usageErr) {
409
+ if (lastSuccessfulQuota) {
410
+ if (this.logger && typeof this.logger.warn === "function") {
411
+ this.logger.warn("Transient failure fetching agy /usage; returning cached quota", { error: usageErr.message });
412
+ }
413
+ return lastSuccessfulQuota;
414
+ }
415
+ const msg = usageErr instanceof Error ? usageErr.message : String(usageErr);
399
416
  return unavailableUsage({
400
417
  providerId: this.providerId,
401
418
  displayName: "Antigravity",
@@ -403,8 +420,16 @@ export class AntigravityQuotaProvider {
403
420
  });
404
421
  }
405
422
 
406
- const rawUsageOut = usageRes.value.stdout || usageRes.value.stderr || "";
407
- const rawCreditsOut = creditsRes.status === "fulfilled" ? creditsRes.value.stdout || creditsRes.value.stderr : "";
423
+ let rawCreditsOut = "";
424
+ try {
425
+ const creditsRes = await execFileAsync(bin, ["--print-timeout", "24h", "--print", "/credits"], {
426
+ timeout: 6000,
427
+ env,
428
+ shell: isBatch,
429
+ windowsHide: true,
430
+ });
431
+ rawCreditsOut = creditsRes.stdout || creditsRes.stderr || "";
432
+ } catch {}
408
433
 
409
434
  const usageOut = (rawUsageOut || "").replace(/\\r\\n/g, "\\n");
410
435
  const creditsOut = (rawCreditsOut || "").replace(/\\r\\n/g, "\\n");
@@ -496,7 +521,7 @@ export class AntigravityQuotaProvider {
496
521
  tone: remainingCredits > 0 ? "ok" : "default",
497
522
  });
498
523
 
499
- return {
524
+ const result = {
500
525
  providerId: this.providerId,
501
526
  displayName: "Antigravity",
502
527
  status: "available",
@@ -506,7 +531,12 @@ export class AntigravityQuotaProvider {
506
531
  details: [],
507
532
  error: null,
508
533
  };
534
+
535
+ lastSuccessfulQuota = result;
536
+ lastSuccessfulQuotaTime = Date.now();
537
+ return result;
509
538
  } catch (err) {
539
+ if (lastSuccessfulQuota) return lastSuccessfulQuota;
510
540
  return unavailableUsage({
511
541
  providerId: this.providerId,
512
542
  displayName: "Antigravity",
@@ -517,16 +547,387 @@ export class AntigravityQuotaProvider {
517
547
  }
518
548
  `;
519
549
  }
550
+ /**
551
+ * Generates the TypeScript source for the Paseo 0.11+ Plugin Usage Source.
552
+ * This integrates Antigravity quota and balance metrics into Paseo 0.11+'s
553
+ * new Plugin UsageSource architecture (Settings -> Usage).
554
+ */
555
+ export function generateAntigravityPluginUsageTs() {
556
+ return `import { execFileSync } from "node:child_process";
557
+ import fs from "node:fs";
558
+ import os from "node:os";
559
+ import path from "node:path";
560
+ import { z } from "zod";
561
+
562
+ let cachedAgyBin = null;
563
+ let lastAgyBinCheck = 0;
564
+ const RESOLVE_TTL_MS = 30000;
565
+ let lastSuccessfulPluginQuota = null;
566
+ let lastSuccessfulPluginQuotaTime = 0;
567
+ const PLUGIN_QUOTA_CACHE_TTL_MS = 60000;
568
+
569
+ function resolveAgyBinary() {
570
+ const now = Date.now();
571
+ if (cachedAgyBin && (now - lastAgyBinCheck) < RESOLVE_TTL_MS) {
572
+ return cachedAgyBin;
573
+ }
574
+
575
+ if (process.env.AGY_BIN_PATH && fs.existsSync(process.env.AGY_BIN_PATH)) {
576
+ cachedAgyBin = process.env.AGY_BIN_PATH;
577
+ lastAgyBinCheck = now;
578
+ return cachedAgyBin;
579
+ }
580
+
581
+ const home = os.homedir();
582
+ const isWin = process.platform === "win32";
583
+
584
+ if (isWin) {
585
+ const localAppData = process.env.LOCALAPPDATA || (home ? path.join(home, "AppData", "Local") : "");
586
+ const programFiles = process.env.ProgramFiles || "C:\\\\Program Files";
587
+ const programFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\\\Program Files (x86)";
588
+
589
+ const winCandidates = [
590
+ path.join(localAppData, "Programs", "Antigravity", "bin", "agy.exe"),
591
+ path.join(localAppData, "Programs", "antigravity", "bin", "agy.exe"),
592
+ path.join(localAppData, "Programs", "Antigravity", "agy.exe"),
593
+ path.join(localAppData, "Programs", "antigravity", "agy.exe"),
594
+ path.join(localAppData, "antigravity", "agy.exe"),
595
+ path.join(programFiles, "Antigravity", "bin", "agy.exe"),
596
+ path.join(programFiles, "antigravity", "bin", "agy.exe"),
597
+ path.join(programFiles, "Antigravity", "agy.exe"),
598
+ path.join(programFiles, "antigravity", "agy.exe"),
599
+ path.join(programFilesX86, "Antigravity", "agy.exe"),
600
+ path.join(localAppData, "Microsoft", "WindowsApps", "agy.exe"),
601
+ ];
602
+
603
+ for (const cand of winCandidates) {
604
+ if (cand && fs.existsSync(cand)) {
605
+ cachedAgyBin = cand;
606
+ lastAgyBinCheck = now;
607
+ return cand;
608
+ }
609
+ }
610
+
611
+ try {
612
+ const out = execFileSync("where.exe", ["agy"], {
613
+ encoding: "utf-8",
614
+ timeout: 3000,
615
+ windowsHide: true,
616
+ });
617
+ const lines = out.split(/\\r?\\n/).map(l => l.trim()).filter(Boolean);
618
+ const exeMatch = lines.find(l => /\\.exe$/i.test(l) && fs.existsSync(l));
619
+ if (exeMatch) {
620
+ cachedAgyBin = exeMatch;
621
+ lastAgyBinCheck = now;
622
+ return exeMatch;
623
+ }
624
+ const scriptMatch = lines.find(l => /\\.(cmd|bat)$/i.test(l) && fs.existsSync(l));
625
+ if (scriptMatch) {
626
+ cachedAgyBin = scriptMatch;
627
+ lastAgyBinCheck = now;
628
+ return scriptMatch;
629
+ }
630
+ if (lines.length > 0 && fs.existsSync(lines[0])) {
631
+ cachedAgyBin = lines[0];
632
+ lastAgyBinCheck = now;
633
+ return lines[0];
634
+ }
635
+ } catch {}
636
+
637
+ cachedAgyBin = "agy.exe";
638
+ lastAgyBinCheck = now;
639
+ return cachedAgyBin;
640
+ } else {
641
+ const posixCandidates = [
642
+ path.join(home, ".local", "bin", "agy"),
643
+ "/usr/local/bin/agy",
644
+ "/usr/bin/agy",
645
+ "/opt/homebrew/bin/agy",
646
+ ];
647
+ for (const cand of posixCandidates) {
648
+ if (fs.existsSync(cand)) {
649
+ cachedAgyBin = cand;
650
+ lastAgyBinCheck = now;
651
+ return cand;
652
+ }
653
+ }
654
+ try {
655
+ const out = execFileSync("which", ["agy"], {
656
+ encoding: "utf-8",
657
+ timeout: 3000,
658
+ }).trim();
659
+ if (out && fs.existsSync(out)) {
660
+ cachedAgyBin = out;
661
+ lastAgyBinCheck = now;
662
+ return out;
663
+ }
664
+ } catch {}
665
+
666
+ cachedAgyBin = "agy";
667
+ lastAgyBinCheck = now;
668
+ return cachedAgyBin;
669
+ }
670
+ }
671
+
672
+ function toneFromUsedPct(usedPct) {
673
+ if (usedPct >= 90) return "critical";
674
+ if (usedPct >= 75) return "warn";
675
+ return "ok";
676
+ }
677
+
678
+ export async function fetchAntigravityUsage() {
679
+ const now = Date.now();
680
+ if (lastSuccessfulPluginQuota && (now - lastSuccessfulPluginQuotaTime < PLUGIN_QUOTA_CACHE_TTL_MS)) {
681
+ return lastSuccessfulPluginQuota;
682
+ }
683
+
684
+ try {
685
+ const bin = resolveAgyBinary();
686
+ const isWin = process.platform === "win32";
687
+ const isBatch = isWin && /\\.(cmd|bat)$/i.test(bin);
688
+
689
+ let usageOut = "";
690
+ try {
691
+ usageOut = execFileSync(bin, ["--print-timeout", "24h", "--print", "/usage"], {
692
+ encoding: "utf-8",
693
+ timeout: 30000,
694
+ windowsHide: true,
695
+ shell: isBatch,
696
+ });
697
+ } catch (usageErr) {
698
+ if (lastSuccessfulPluginQuota) return lastSuccessfulPluginQuota;
699
+ throw usageErr;
700
+ }
701
+
702
+ let creditsOut = "";
703
+ try {
704
+ creditsOut = execFileSync(bin, ["--print-timeout", "24h", "--print", "/credits"], {
705
+ encoding: "utf-8",
706
+ timeout: 6000,
707
+ windowsHide: true,
708
+ shell: isBatch,
709
+ });
710
+ } catch {}
711
+
712
+ const normalizedUsage = usageOut.replace(/\\r\\n/g, "\\n");
713
+ const lines = normalizedUsage.split("\\n");
714
+ const rawWindows = [];
715
+ let currentModel = "";
716
+
717
+ for (const rawLine of lines) {
718
+ const line = rawLine.trim();
719
+ if (!line) continue;
720
+
721
+ if (line.endsWith(":") && !line.includes("%")) {
722
+ currentModel = line.slice(0, -1).trim();
723
+ continue;
724
+ }
725
+
726
+ const isGemini = /gemini/i.test(currentModel) || !/claude/i.test(currentModel);
727
+ const match = line.match(/^([A-Za-z0-9\\s_-]+):\\s*(\\d+)%\\s*(?:used)?(?:\\s*\\((?:resets?\\s+in\\s+)?([^)]+)\\))?/i);
728
+ if (match) {
729
+ const limitType = match[1].trim();
730
+ const usedPct = parseInt(match[2], 10);
731
+ const resetStr = match[3] ? match[3].trim() : null;
732
+
733
+ let resetsAt = null;
734
+ if (resetStr) {
735
+ const durMatch = resetStr.match(/(?:(\\d+)h)?\\s*(?:(\\d+)m)?/i);
736
+ if (durMatch && (durMatch[1] || durMatch[2])) {
737
+ const h = parseInt(durMatch[1] || "0", 10);
738
+ const m = parseInt(durMatch[2] || "0", 10);
739
+ resetsAt = new Date(Date.now() + (h * 3600 + m * 60) * 1000).toISOString();
740
+ } else {
741
+ const parsed = Date.parse(resetStr);
742
+ if (!isNaN(parsed)) resetsAt = new Date(parsed).toISOString();
743
+ }
744
+ }
745
+
746
+ const isFiveHour = /session|5-?hour|5h/i.test(limitType);
747
+ const isWeekly = /week|7-?day/i.test(limitType);
748
+
749
+ let id = isFiveHour ? "session" : isWeekly ? "weekly" : "quota";
750
+ let label = isFiveHour ? "Session" : isWeekly ? "Weekly" : limitType.replace(/\\s+Remaining$/i, "");
751
+ if (!isGemini) {
752
+ id = \`claude_\${id}\`;
753
+ label = \`Claude \${label}\`;
754
+ }
755
+
756
+ rawWindows.push({
757
+ id,
758
+ label,
759
+ shortLabel: isFiveHour ? "5h" : isWeekly ? "wk" : "",
760
+ summary: true,
761
+ usedPct,
762
+ remainingPct: Math.max(0, 100 - usedPct),
763
+ resetsAt,
764
+ tone: toneFromUsedPct(usedPct),
765
+ isFiveHour,
766
+ isGemini,
767
+ });
768
+ }
769
+ }
770
+
771
+ rawWindows.sort((a, b) => {
772
+ if (a.isGemini && !b.isGemini) return -1;
773
+ if (!a.isGemini && b.isGemini) return 1;
774
+ if (a.isFiveHour && !b.isFiveHour) return -1;
775
+ if (!a.isFiveHour && b.isFiveHour) return 1;
776
+ return 0;
777
+ });
778
+
779
+ const windows = rawWindows.map(w => ({
780
+ id: w.id,
781
+ label: w.label,
782
+ shortLabel: w.shortLabel,
783
+ summary: w.summary,
784
+ usedPct: w.usedPct,
785
+ remainingPct: w.remainingPct,
786
+ resetsAt: w.resetsAt,
787
+ tone: w.tone,
788
+ }));
789
+
790
+ const balances = [];
791
+ const credMatch = creditsOut.match(/Remaining\\s+credits\\s+([\\d.]+)/i);
792
+ const remainingCredits = credMatch ? parseFloat(credMatch[1]) : 0;
793
+ balances.push({
794
+ id: "credits",
795
+ label: "Credits",
796
+ remaining: remainingCredits,
797
+ unit: "usd",
798
+ tone: remainingCredits > 0 ? "ok" : "default",
799
+ });
800
+
801
+ const result = {
802
+ status: "available",
803
+ planLabel: "Google Gemini",
804
+ windows,
805
+ balances,
806
+ details: [],
807
+ };
808
+ lastSuccessfulPluginQuota = result;
809
+ lastSuccessfulPluginQuotaTime = Date.now();
810
+ return result;
811
+ } catch (err) {
812
+ if (lastSuccessfulPluginQuota) return lastSuccessfulPluginQuota;
813
+ return {
814
+ status: "error",
815
+ error: err instanceof Error ? err.message : String(err),
816
+ };
817
+ }
818
+ }
819
+
820
+ export function registerAntigravityUsageSource(server) {
821
+ if (!server || typeof server.registerUsageSource !== "function") return;
822
+ try {
823
+ server.registerUsageSource({
824
+ id: "antigravity",
825
+ label: "Antigravity",
826
+ icon: "icon.svg",
827
+ input: z.object({}).passthrough(),
828
+ discover: async (scope) => {
829
+ if (scope && scope.kind === "session" && scope.provider && scope.provider !== "antigravity") {
830
+ return [];
831
+ }
832
+ return [{ key: "default", label: "Antigravity", input: {} }];
833
+ },
834
+ fetch: async () => {
835
+ return fetchAntigravityUsage();
836
+ },
837
+ });
838
+ } catch (err) {
839
+ console.warn("[antigravity-provider] Failed to register usage source:", err);
840
+ }
841
+ }
842
+ `;
843
+ }
520
844
  /**
521
845
  * Patches a Paseo server installation directory to enable Antigravity:
522
- * 1. Patches quota-fetcher/manifest.js to register Antigravity
523
- * 2. Writes quota-fetcher/providers/antigravity.js
846
+ * 1. For Paseo 0.11+: patches builtin-plugins/antigravity-provider with usage source
847
+ * 2. For Paseo <= 0.10: patches quota-fetcher/manifest.js and writes providers/antigravity.js
524
848
  * 3. Patches acp-agent.js to map context window tokens and emit usage updates
525
849
  */
526
850
  export function patchPaseoServer(serverDir) {
527
851
  const changes = [];
528
852
  try {
529
- // 1. Locate manifest.js in quota-fetcher
853
+ // 1. Check for Paseo 0.11+ builtin-plugins/antigravity-provider
854
+ const pluginDirCandidates = [
855
+ path.join(serverDir, "dist", "server", "builtin-plugins", "antigravity-provider"),
856
+ path.join(serverDir, "dist", "builtin-plugins", "antigravity-provider"),
857
+ path.join(serverDir, "builtin-plugins", "antigravity-provider"),
858
+ ];
859
+ let pluginDir = pluginDirCandidates.find((d) => fs.existsSync(d));
860
+ if (!pluginDir && fs.existsSync(path.join(serverDir, "dist"))) {
861
+ const findPlugin = (dir, depth = 0) => {
862
+ if (depth > 5)
863
+ return null;
864
+ try {
865
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
866
+ for (const e of entries) {
867
+ const full = path.join(dir, e.name);
868
+ if (e.isDirectory() && e.name !== "node_modules") {
869
+ if (e.name === "antigravity-provider")
870
+ return full;
871
+ const found = findPlugin(full, depth + 1);
872
+ if (found)
873
+ return found;
874
+ }
875
+ }
876
+ }
877
+ catch { }
878
+ return null;
879
+ };
880
+ pluginDir = findPlugin(path.join(serverDir, "dist")) || undefined;
881
+ }
882
+ if (pluginDir) {
883
+ const serverSubdir = path.join(pluginDir, "server");
884
+ if (!fs.existsSync(serverSubdir)) {
885
+ fs.mkdirSync(serverSubdir, { recursive: true });
886
+ }
887
+ // Write / update usage.ts
888
+ const usageTsPath = path.join(serverSubdir, "usage.ts");
889
+ fs.writeFileSync(usageTsPath, generateAntigravityPluginUsageTs(), "utf-8");
890
+ changes.push(`Created/Updated ${usageTsPath}`);
891
+ // Locate index.server.ts and/or index.server.js
892
+ const indexCandidates = [
893
+ path.join(pluginDir, "index.server.ts"),
894
+ path.join(pluginDir, "index.server.js"),
895
+ ].filter((f) => fs.existsSync(f));
896
+ for (const indexFile of indexCandidates) {
897
+ let indexCode = fs.readFileSync(indexFile, "utf-8");
898
+ let indexModified = false;
899
+ if (!indexCode.includes("registerAntigravityUsageSource")) {
900
+ // Add import
901
+ if (!indexCode.includes('from "./server/usage.js"')) {
902
+ indexCode = `import { registerAntigravityUsageSource } from "./server/usage.js";\n` + indexCode;
903
+ indexModified = true;
904
+ }
905
+ // Inject registration call into contribute function
906
+ const contributeMatch = indexCode.match(/(export\s+default\s+function\s+contribute\s*\([^)]*\)\s*\{)([\s\S]*?)(\})/);
907
+ if (contributeMatch) {
908
+ const before = contributeMatch[1];
909
+ const body = contributeMatch[2];
910
+ const after = contributeMatch[3];
911
+ if (!body.includes("registerAntigravityUsageSource")) {
912
+ const patchedBody = body.replace(/return\s+\(\)\s*=>\s*\{[^}]*\};?/, (m) => `server.registerUsageSource ? registerAntigravityUsageSource(server) : null;\n ${m}`);
913
+ if (patchedBody !== body) {
914
+ indexCode = indexCode.replace(contributeMatch[0], `${before}${patchedBody}${after}`);
915
+ indexModified = true;
916
+ }
917
+ else {
918
+ indexCode = indexCode.replace(contributeMatch[0], `${before}${body}\n server.registerUsageSource ? registerAntigravityUsageSource(server) : null;\n${after}`);
919
+ indexModified = true;
920
+ }
921
+ }
922
+ }
923
+ }
924
+ if (indexModified) {
925
+ fs.writeFileSync(indexFile, indexCode, "utf-8");
926
+ changes.push(`Patched ${indexFile} with registerAntigravityUsageSource`);
927
+ }
928
+ }
929
+ }
930
+ // 2. Check for Paseo <= 0.10 legacy quota-fetcher/manifest.js
530
931
  const manifestCandidates = [
531
932
  path.join(serverDir, "dist", "server", "services", "quota-fetcher", "manifest.js"),
532
933
  path.join(serverDir, "dist", "services", "quota-fetcher", "manifest.js"),
@@ -580,7 +981,7 @@ export function patchPaseoServer(serverDir) {
580
981
  changes.push(`Patched ${manifestFile} with AntigravityQuotaProvider`);
581
982
  }
582
983
  }
583
- // 2. Locate acp-agent.js
984
+ // 3. Locate acp-agent.js
584
985
  const acpCandidates = [
585
986
  path.join(serverDir, "dist", "server", "server", "agent", "providers", "acp-agent.js"),
586
987
  path.join(serverDir, "dist", "server", "agent", "providers", "acp-agent.js"),
@@ -609,19 +1010,26 @@ export function patchPaseoServer(serverDir) {
609
1010
  let acpCode = fs.readFileSync(acpFile, "utf-8");
610
1011
  let acpModified = false;
611
1012
  // Patch mapACPUsage
612
- if (!acpCode.includes("contextWindowMaxTokens: usage.contextWindowMaxTokens")) {
1013
+ if (!acpCode.includes("contextWindowMaxTokens: usage.contextWindowMaxTokens") || !acpCode.includes("Math.min(usedTokens, maxTokens)")) {
613
1014
  const oldMapRegex = /export\s+function\s+mapACPUsage\s*\([^)]*\)\s*\{[\s\S]*?return\s*\{[\s\S]*?\};\s*\}/m;
614
1015
  const newMap = `export function mapACPUsage(usage) {
615
1016
  if (!usage) {
616
1017
  return undefined;
617
1018
  }
1019
+ const maxTokens = usage.contextWindowMaxTokens ?? usage.size ?? undefined;
1020
+ let usedTokens = usage.contextWindowUsedTokens ?? usage.used ?? undefined;
1021
+ if (typeof usedTokens === "number" && typeof maxTokens === "number" && maxTokens > 0) {
1022
+ if (usedTokens > maxTokens) {
1023
+ usedTokens = Math.min(usedTokens, maxTokens);
1024
+ }
1025
+ }
618
1026
  return {
619
1027
  inputTokens: usage.inputTokens ?? undefined,
620
1028
  outputTokens: usage.outputTokens ?? undefined,
621
1029
  cachedInputTokens: usage.cachedReadTokens ?? usage.cachedInputTokens ?? undefined,
622
1030
  totalCostUsd: usage.totalCostUsd ?? (usage.cost?.amount !== undefined ? Number(usage.cost.amount) : undefined),
623
1031
  contextWindowMaxTokens: usage.contextWindowMaxTokens ?? usage.size ?? undefined,
624
- contextWindowUsedTokens: usage.contextWindowUsedTokens ?? usage.used ?? undefined,
1032
+ contextWindowUsedTokens: usedTokens,
625
1033
  };
626
1034
  }`;
627
1035
  if (oldMapRegex.test(acpCode)) {
@@ -666,7 +1074,7 @@ export function patchPaseoServer(serverDir) {
666
1074
  }
667
1075
  if (depth === 0) {
668
1076
  const currentMethodBody = acpCode.slice(startIdx, i);
669
- const needsPatch = !currentMethodBody.includes('type: "usage_updated"') || currentMethodBody.includes("this.notifySubscribers");
1077
+ const needsPatch = !currentMethodBody.includes("this.currentTurnUsage =") || currentMethodBody.includes("this.notifySubscribers");
670
1078
  if (needsPatch) {
671
1079
  const indent = methodMatch[1] || " ";
672
1080
  const newHandler = `${indent}handleUsageUpdate(update) {
@@ -984,43 +1392,15 @@ export function isPaseoServerPatched(serverDir) {
984
1392
  try {
985
1393
  if (!fs.existsSync(serverDir))
986
1394
  return false;
987
- // 1. Antigravity quota provider file
988
- const antigravityJsCandidates = [
989
- path.join(serverDir, "dist", "server", "services", "quota-fetcher", "providers", "antigravity.js"),
990
- path.join(serverDir, "dist", "services", "quota-fetcher", "providers", "antigravity.js"),
1395
+ // 1. Check for Paseo 0.11+ builtin-plugins/antigravity-provider
1396
+ const pluginDirCandidates = [
1397
+ path.join(serverDir, "dist", "server", "builtin-plugins", "antigravity-provider"),
1398
+ path.join(serverDir, "dist", "builtin-plugins", "antigravity-provider"),
1399
+ path.join(serverDir, "builtin-plugins", "antigravity-provider"),
991
1400
  ];
992
- let hasProvider = antigravityJsCandidates.some((p) => fs.existsSync(p));
993
- if (!hasProvider && fs.existsSync(path.join(serverDir, "dist"))) {
994
- const checkRecursive = (dir, depth = 0) => {
995
- if (depth > 5)
996
- return false;
997
- try {
998
- const entries = fs.readdirSync(dir, { withFileTypes: true });
999
- for (const e of entries) {
1000
- if (e.isDirectory() && e.name !== "node_modules") {
1001
- if (checkRecursive(path.join(dir, e.name), depth + 1))
1002
- return true;
1003
- }
1004
- else if (e.isFile() && e.name === "antigravity.js" && dir.replace(/\\/g, "/").includes("quota-fetcher")) {
1005
- return true;
1006
- }
1007
- }
1008
- }
1009
- catch { }
1010
- return false;
1011
- };
1012
- hasProvider = checkRecursive(path.join(serverDir, "dist"));
1013
- }
1014
- if (!hasProvider)
1015
- return false;
1016
- // 2. Manifest check
1017
- const manifestCandidates = [
1018
- path.join(serverDir, "dist", "server", "services", "quota-fetcher", "manifest.js"),
1019
- path.join(serverDir, "dist", "services", "quota-fetcher", "manifest.js"),
1020
- ];
1021
- let manifestFile = manifestCandidates.find((f) => fs.existsSync(f));
1022
- if (!manifestFile && fs.existsSync(path.join(serverDir, "dist"))) {
1023
- const findManifest = (dir, depth = 0) => {
1401
+ let pluginDir = pluginDirCandidates.find((d) => fs.existsSync(d));
1402
+ if (!pluginDir && fs.existsSync(path.join(serverDir, "dist"))) {
1403
+ const findPlugin = (dir, depth = 0) => {
1024
1404
  if (depth > 5)
1025
1405
  return null;
1026
1406
  try {
@@ -1028,27 +1408,104 @@ export function isPaseoServerPatched(serverDir) {
1028
1408
  for (const e of entries) {
1029
1409
  const full = path.join(dir, e.name);
1030
1410
  if (e.isDirectory() && e.name !== "node_modules") {
1031
- const found = findManifest(full, depth + 1);
1411
+ if (e.name === "antigravity-provider")
1412
+ return full;
1413
+ const found = findPlugin(full, depth + 1);
1032
1414
  if (found)
1033
1415
  return found;
1034
1416
  }
1035
- else if (e.isFile() && e.name === "manifest.js" && dir.replace(/\\/g, "/").includes("quota-fetcher")) {
1036
- return full;
1037
- }
1038
1417
  }
1039
1418
  }
1040
1419
  catch { }
1041
1420
  return null;
1042
1421
  };
1043
- manifestFile = findManifest(path.join(serverDir, "dist")) || undefined;
1422
+ pluginDir = findPlugin(path.join(serverDir, "dist")) || undefined;
1044
1423
  }
1045
- if (manifestFile) {
1046
- const content = fs.readFileSync(manifestFile, "utf-8");
1047
- if (!content.includes('providerId: "antigravity"')) {
1424
+ if (pluginDir) {
1425
+ // Paseo 0.11+ architecture
1426
+ const usageFile = [
1427
+ path.join(pluginDir, "server", "usage.ts"),
1428
+ path.join(pluginDir, "server", "usage.js"),
1429
+ ].find((f) => fs.existsSync(f));
1430
+ if (!usageFile)
1431
+ return false;
1432
+ const indexFile = [
1433
+ path.join(pluginDir, "index.server.ts"),
1434
+ path.join(pluginDir, "index.server.js"),
1435
+ ].find((f) => fs.existsSync(f));
1436
+ if (!indexFile)
1437
+ return false;
1438
+ const indexContent = fs.readFileSync(indexFile, "utf-8");
1439
+ if (!indexContent.includes("registerAntigravityUsageSource"))
1440
+ return false;
1441
+ }
1442
+ else {
1443
+ // Paseo <= 0.10 legacy architecture
1444
+ const antigravityJsCandidates = [
1445
+ path.join(serverDir, "dist", "server", "services", "quota-fetcher", "providers", "antigravity.js"),
1446
+ path.join(serverDir, "dist", "services", "quota-fetcher", "providers", "antigravity.js"),
1447
+ ];
1448
+ let hasProvider = antigravityJsCandidates.some((p) => fs.existsSync(p));
1449
+ if (!hasProvider && fs.existsSync(path.join(serverDir, "dist"))) {
1450
+ const checkRecursive = (dir, depth = 0) => {
1451
+ if (depth > 5)
1452
+ return false;
1453
+ try {
1454
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
1455
+ for (const e of entries) {
1456
+ if (e.isDirectory() && e.name !== "node_modules") {
1457
+ if (checkRecursive(path.join(dir, e.name), depth + 1))
1458
+ return true;
1459
+ }
1460
+ else if (e.isFile() && e.name === "antigravity.js" && dir.replace(/\\/g, "/").includes("quota-fetcher")) {
1461
+ return true;
1462
+ }
1463
+ }
1464
+ }
1465
+ catch { }
1466
+ return false;
1467
+ };
1468
+ hasProvider = checkRecursive(path.join(serverDir, "dist"));
1469
+ }
1470
+ if (!hasProvider)
1048
1471
  return false;
1472
+ // Manifest check
1473
+ const manifestCandidates = [
1474
+ path.join(serverDir, "dist", "server", "services", "quota-fetcher", "manifest.js"),
1475
+ path.join(serverDir, "dist", "services", "quota-fetcher", "manifest.js"),
1476
+ ];
1477
+ let manifestFile = manifestCandidates.find((f) => fs.existsSync(f));
1478
+ if (!manifestFile && fs.existsSync(path.join(serverDir, "dist"))) {
1479
+ const findManifest = (dir, depth = 0) => {
1480
+ if (depth > 5)
1481
+ return null;
1482
+ try {
1483
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
1484
+ for (const e of entries) {
1485
+ const full = path.join(dir, e.name);
1486
+ if (e.isDirectory() && e.name !== "node_modules") {
1487
+ const found = findManifest(full, depth + 1);
1488
+ if (found)
1489
+ return found;
1490
+ }
1491
+ else if (e.isFile() && e.name === "manifest.js" && dir.replace(/\\/g, "/").includes("quota-fetcher")) {
1492
+ return full;
1493
+ }
1494
+ }
1495
+ }
1496
+ catch { }
1497
+ return null;
1498
+ };
1499
+ manifestFile = findManifest(path.join(serverDir, "dist")) || undefined;
1500
+ }
1501
+ if (manifestFile) {
1502
+ const content = fs.readFileSync(manifestFile, "utf-8");
1503
+ if (!content.includes('providerId: "antigravity"')) {
1504
+ return false;
1505
+ }
1049
1506
  }
1050
1507
  }
1051
- // 3. ACP Agent check (context window telemetry & usage updates)
1508
+ // 2. ACP Agent check (common to both architectures)
1052
1509
  const acpCandidates = [
1053
1510
  path.join(serverDir, "dist", "server", "server", "agent", "providers", "acp-agent.js"),
1054
1511
  path.join(serverDir, "dist", "server", "agent", "providers", "acp-agent.js"),
@@ -1080,7 +1537,7 @@ export function isPaseoServerPatched(serverDir) {
1080
1537
  }
1081
1538
  if (acpFile) {
1082
1539
  const content = fs.readFileSync(acpFile, "utf-8");
1083
- if (!content.includes('type: "usage_updated"') || !content.includes("contextWindowMaxTokens")) {
1540
+ if (!content.includes("this.currentTurnUsage =") || !content.includes("contextWindowMaxTokens")) {
1084
1541
  return false;
1085
1542
  }
1086
1543
  }
@@ -1098,7 +1555,11 @@ export async function isPaseoAsarPatched(asarPath) {
1098
1555
  if (!fs.existsSync(asarPath))
1099
1556
  return false;
1100
1557
  const files = listPackage(asarPath);
1101
- return files.some((f) => f.replace(/\\/g, "/").includes("quota-fetcher/providers/antigravity.js"));
1558
+ return files.some((f) => {
1559
+ const norm = f.replace(/\\/g, "/");
1560
+ return norm.includes("quota-fetcher/providers/antigravity.js") ||
1561
+ norm.includes("antigravity-provider/server/usage.");
1562
+ });
1102
1563
  }
1103
1564
  catch {
1104
1565
  return false;
@@ -1118,15 +1579,28 @@ export function isPaseoRunning() {
1118
1579
  return /paseo/i.test(out) && !out.includes("INFO:") && !out.includes("No tasks");
1119
1580
  }
1120
1581
  else {
1121
- const out = execFileSync("pgrep", ["-i", "-x", "paseo"], {
1582
+ try {
1583
+ const out = execFileSync("pgrep", ["-i", "-f", "paseo (daemon|supervisor)|/paseo$|@getpaseo/server"], {
1584
+ encoding: "utf-8",
1585
+ timeout: 2000,
1586
+ });
1587
+ const pids = out
1588
+ .split(/\r?\n/)
1589
+ .map((s) => parseInt(s.trim(), 10))
1590
+ .filter((p) => !isNaN(p) && p !== process.pid && p !== process.ppid);
1591
+ if (pids.length > 0)
1592
+ return true;
1593
+ }
1594
+ catch { }
1595
+ const out2 = execFileSync("pgrep", ["-i", "-x", "paseo"], {
1122
1596
  encoding: "utf-8",
1123
1597
  timeout: 2000,
1124
1598
  });
1125
- const pids = out
1599
+ const pids2 = out2
1126
1600
  .split(/\r?\n/)
1127
1601
  .map((s) => parseInt(s.trim(), 10))
1128
- .filter((p) => !isNaN(p) && p !== process.pid);
1129
- return pids.length > 0;
1602
+ .filter((p) => !isNaN(p) && p !== process.pid && p !== process.ppid);
1603
+ return pids2.length > 0;
1130
1604
  }
1131
1605
  }
1132
1606
  catch {
package/dist/protocol.js CHANGED
@@ -308,14 +308,14 @@ export async function fetchAntigravityUsage(binaryPath = "agy", force = false) {
308
308
  try {
309
309
  const [usageResult, creditsResult] = await Promise.allSettled([
310
310
  execFileAsync(cmd, ["--print", "/usage"], {
311
- timeout: 8_000,
311
+ timeout: 30_000,
312
312
  maxBuffer: 1024 * 1024,
313
313
  env: process.env,
314
314
  shell: isBatch,
315
315
  windowsHide: true,
316
316
  }),
317
317
  execFileAsync(cmd, ["--print", "/credits"], {
318
- timeout: 8_000,
318
+ timeout: 6_000,
319
319
  maxBuffer: 1024 * 1024,
320
320
  env: process.env,
321
321
  shell: isBatch,
@@ -331,6 +331,10 @@ export async function fetchAntigravityUsage(binaryPath = "agy", force = false) {
331
331
  // /usage is the primary provider-availability probe. allSettled never
332
332
  // throws for an exec failure, so rejected results must be handled here.
333
333
  if (usageResult.status === "rejected") {
334
+ if (cachedProviderUsage && cachedProviderUsage.status === "available" && cachedProviderUsageBinaryPath === binaryPath) {
335
+ logger.warn("Transient failure in /usage; returning cached provider usage", { error: errorMessage(usageResult.reason) });
336
+ return cachedProviderUsage;
337
+ }
334
338
  const error = `Unable to fetch Antigravity quota: ${errorMessage(usageResult.reason)}`;
335
339
  logger.warn("Failed to fetch Antigravity provider usage", { error });
336
340
  return cacheProviderUsage(binaryPath, {
package/dist/session.d.ts CHANGED
@@ -47,7 +47,7 @@ export declare class Session {
47
47
  output_tokens?: number;
48
48
  cache_read_tokens?: number;
49
49
  total_tokens?: number;
50
- }, executingModel?: string): void;
50
+ }, executingModel?: string, latestStepContextTokens?: number, numSteps?: number): void;
51
51
  touch(): void;
52
52
  cancelTurn(): boolean;
53
53
  close(): Promise<void>;
package/dist/session.js CHANGED
@@ -30,15 +30,22 @@ export class Session {
30
30
  this.createdAt = new Date();
31
31
  this.lastActivity = new Date();
32
32
  this.store = options.store || sessionStore;
33
- this.usage = options.usage || {
34
- inputTokens: 0,
35
- outputTokens: 0,
36
- cachedInputTokens: 0,
37
- totalTokens: 0,
38
- totalCostUsd: 0,
39
- contextWindowUsedTokens: 0,
40
- contextWindowMaxTokens: getModelContextWindow(this.model),
41
- };
33
+ const maxTokens = getModelContextWindow(this.model);
34
+ this.usage = options.usage
35
+ ? { ...options.usage }
36
+ : {
37
+ inputTokens: 0,
38
+ outputTokens: 0,
39
+ cachedInputTokens: 0,
40
+ totalTokens: 0,
41
+ totalCostUsd: 0,
42
+ contextWindowUsedTokens: 0,
43
+ contextWindowMaxTokens: maxTokens,
44
+ };
45
+ this.usage.contextWindowMaxTokens = maxTokens;
46
+ if (this.usage.contextWindowUsedTokens > maxTokens) {
47
+ this.usage.contextWindowUsedTokens = Math.min(this.usage.contextWindowUsedTokens, maxTokens);
48
+ }
42
49
  const isSandbox = this.permission === "sandbox" || options.sandbox;
43
50
  const isBypass = this.permission === "bypass" || options.dangerouslySkipPermissions;
44
51
  const permissions = resolvePermissionSettings({
@@ -146,7 +153,7 @@ export class Session {
146
153
  throw err;
147
154
  }
148
155
  }
149
- recordTurnUsage(turnUsage, executingModel = this.model) {
156
+ recordTurnUsage(turnUsage, executingModel = this.model, latestStepContextTokens, numSteps = 1) {
150
157
  if (!turnUsage)
151
158
  return;
152
159
  const input = turnUsage.input_tokens || 0;
@@ -159,10 +166,28 @@ export class Session {
159
166
  this.usage.totalTokens = this.usage.inputTokens + this.usage.outputTokens;
160
167
  // Keep the accumulator unrounded. Micro-costs must survive across turns.
161
168
  this.usage.totalCostUsd += turnCost;
162
- this.usage.contextWindowUsedTokens = input + output;
163
- // The selected model may have changed while this turn was running. Keep
164
- // the stored meter aligned with the model currently selected for next turn.
169
+ // Keep the stored meter aligned with the model currently selected for next turn,
170
+ // but evaluate turn occupancy against the model that executed this turn.
171
+ const executingMaxTokens = getModelContextWindow(executingModel);
165
172
  this.usage.contextWindowMaxTokens = getModelContextWindow(this.model);
173
+ // Context window represents the active prompt memory buffer of the model.
174
+ // If we observed the latest step's context window size, use it directly.
175
+ // Otherwise, if input + output exceeds executingMaxTokens, it is an accumulation
176
+ // across multiple steps in the turn; derive the single-step context footprint.
177
+ let activeContextTokens = latestStepContextTokens;
178
+ if (!activeContextTokens || activeContextTokens <= 0) {
179
+ const rawTurnTokens = input + output;
180
+ if (rawTurnTokens > executingMaxTokens && numSteps > 1) {
181
+ activeContextTokens = Math.round(rawTurnTokens / numSteps);
182
+ }
183
+ else if (rawTurnTokens > executingMaxTokens) {
184
+ activeContextTokens = Math.min(Math.round(rawTurnTokens / 10), executingMaxTokens);
185
+ }
186
+ else {
187
+ activeContextTokens = rawTurnTokens;
188
+ }
189
+ }
190
+ this.usage.contextWindowUsedTokens = Math.min(activeContextTokens, executingMaxTokens);
166
191
  this.persist();
167
192
  }
168
193
  touch() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paseo-acp-agy",
3
- "version": "1.2.4",
3
+ "version": "1.2.6",
4
4
  "description": "ACP (Agent Client Protocol) provider for Google Antigravity in Paseo and Zed",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",