@estebanforge/pi-ask-antigravity 1.2.0 → 1.2.1

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.
Files changed (2) hide show
  1. package/extensions/index.ts +147 -5
  2. package/package.json +14 -5
@@ -500,13 +500,124 @@ function snapshotConversations(dir: string): Set<string> {
500
500
  return out;
501
501
  }
502
502
 
503
- /** Find the single new conversation id created since `before`. Returns null if
504
- * none, or if several appeared (can't safely pick which one is ours). */
505
- function newConversationId(dir: string, before: Set<string>): string | null {
503
+ /** Resolve which of the `candidates` DB ids is held open by the process tree
504
+ * rooted at `rootPid`. Used to disambiguate concurrent agy runs. Returns the
505
+ * single matching id, or null when none/several are open or /proc is
506
+ * unavailable. Ported from pi-antigravity-bridge src/discovery.ts; keep in
507
+ * sync. */
508
+ type OpenDbResolver = (
509
+ rootPid: number,
510
+ dir: string,
511
+ candidates: Set<string>,
512
+ ) => string | null;
513
+
514
+ function readProcStat(pid: number): { pid: number; ppid: number } | null {
515
+ let raw: string;
516
+ try {
517
+ raw = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
518
+ } catch {
519
+ return null;
520
+ }
521
+ const closeParen = raw.lastIndexOf(")");
522
+ if (closeParen < 0) return null;
523
+ const fields = raw.slice(closeParen + 2).trim().split(/\s+/);
524
+ const ppid = Number(fields[1]);
525
+ if (!Number.isFinite(ppid)) return null;
526
+ return { pid, ppid };
527
+ }
528
+
529
+ function collectDescendants(rootPid: number): Set<number> {
530
+ const out = new Set<number>([rootPid]);
531
+ let entries: string[];
532
+ try {
533
+ entries = fs.readdirSync("/proc");
534
+ } catch {
535
+ return out;
536
+ }
537
+ const ppidOf = new Map<number, number>();
538
+ for (const e of entries) {
539
+ if (!/^\d+$/.test(e)) continue;
540
+ const s = readProcStat(Number(e));
541
+ if (s) ppidOf.set(s.pid, s.ppid);
542
+ }
543
+ let changed = true;
544
+ while (changed) {
545
+ changed = false;
546
+ for (const [pid, ppid] of ppidOf) {
547
+ if (out.has(pid)) continue;
548
+ if (out.has(ppid)) {
549
+ out.add(pid);
550
+ changed = true;
551
+ }
552
+ }
553
+ }
554
+ return out;
555
+ }
556
+
557
+ function safeRealpath(p: string): string | null {
558
+ try {
559
+ return fs.realpathSync(p);
560
+ } catch {
561
+ return null;
562
+ }
563
+ }
564
+
565
+ const procTreeOpenDbResolver: OpenDbResolver = (rootPid, dir, candidates) => {
566
+ if (candidates.size <= 1) return null;
567
+ if (process.platform !== "linux") return null;
568
+ const dirResolved = safeRealpath(dir);
569
+ const tree = collectDescendants(rootPid);
570
+ const found = new Set<string>();
571
+ for (const pid of tree) {
572
+ let fds: string[];
573
+ try {
574
+ fds = fs.readdirSync(`/proc/${pid}/fd`);
575
+ } catch {
576
+ continue;
577
+ }
578
+ for (const fd of fds) {
579
+ let target: string;
580
+ try {
581
+ target = fs.readlinkSync(`/proc/${pid}/fd/${fd}`);
582
+ } catch {
583
+ continue;
584
+ }
585
+ const base = path.basename(target);
586
+ if (!base.endsWith(".db")) continue;
587
+ if (dirResolved && safeRealpath(path.dirname(target)) !== dirResolved) continue;
588
+ const id = base.slice(0, -3);
589
+ if (candidates.has(id)) found.add(id);
590
+ }
591
+ }
592
+ if (found.size === 1) return [...found][0] ?? null;
593
+ return null;
594
+ };
595
+
596
+ interface BindOptions {
597
+ pid?: number;
598
+ resolveOpenDb?: OpenDbResolver;
599
+ }
600
+
601
+ /** Find the conversation id created since `before`. Returns null when none
602
+ * appeared, or when several appeared and we cannot tie one to our process.
603
+ * Pass `opts.pid` (the spawned agy) to enable concurrent-run disambiguation
604
+ * via the process-tree FD scan. */
605
+ function newConversationId(
606
+ dir: string,
607
+ before: Set<string>,
608
+ opts: BindOptions = {},
609
+ ): string | null {
506
610
  const created = [...snapshotConversations(dir)].filter((id) => !before.has(id));
507
611
  if (created.length === 0) return null;
508
- if (created.length > 1) return null; // ambiguous; refuse to bind
509
- return created[0] ?? null;
612
+ if (created.length === 1) return created[0] ?? null;
613
+ // Ambiguous: try to authoritatively identify ours via the spawned
614
+ // process's open files. Fail safe to null when we can't pick exactly one.
615
+ if (opts.pid !== undefined) {
616
+ const resolve = opts.resolveOpenDb ?? procTreeOpenDbResolver;
617
+ const hit = resolve(opts.pid, dir, new Set(created));
618
+ if (hit && created.includes(hit)) return hit;
619
+ }
620
+ return null;
510
621
  }
511
622
 
512
623
  // --- Extension -------------------------------------------------------------
@@ -859,6 +970,11 @@ export default async function (pi: ExtensionAPI) {
859
970
  : null;
860
971
 
861
972
  try {
973
+ // Bind the conversation id DURING the run (agy is alive then) so the
974
+ // pid-based /proc FD resolver can disambiguate when a concurrent agy
975
+ // also drops a new .db. Awaited after the run; the post-exit loop
976
+ // below is the fallback for runs that exit before the poll binds.
977
+ let bindDuringRun: Promise<void> = Promise.resolve();
862
978
  const outcome = await new Promise<{
863
979
  exitCode: number;
864
980
  aborted: boolean;
@@ -886,6 +1002,28 @@ export default async function (pi: ExtensionAPI) {
886
1002
  details.stderr += d;
887
1003
  });
888
1004
 
1005
+ // Concurrent bind: poll for the new id while agy is alive. The FD
1006
+ // resolver needs a live process tree, so this stops (and the
1007
+ // post-exit fallback below takes over) once agy has exited.
1008
+ if (!isContinuation && snapshot && proc.pid) {
1009
+ bindDuringRun = (async () => {
1010
+ for (let attempt = 0; attempt < DISCOVERY_POLL_ATTEMPTS; attempt++) {
1011
+ if (details.conversationId) return;
1012
+ if (proc.exitCode !== null) return; // agy gone: scan useless now
1013
+ const found = newConversationId(CONVERSATIONS_DIR, snapshot, {
1014
+ pid: proc.pid,
1015
+ });
1016
+ if (found) {
1017
+ details.conversationId = found;
1018
+ return;
1019
+ }
1020
+ await sleep(DISCOVERY_POLL_MS);
1021
+ }
1022
+ })().catch(() => {
1023
+ /* best-effort: a bind error must never fail an otherwise-OK turn */
1024
+ });
1025
+ }
1026
+
889
1027
  let sigkillTimer: ReturnType<typeof setTimeout> | undefined;
890
1028
  let watchdog: ReturnType<typeof setTimeout> | undefined;
891
1029
  let settled = false;
@@ -952,6 +1090,10 @@ export default async function (pi: ExtensionAPI) {
952
1090
 
953
1091
  if (statusInterval) clearInterval(statusInterval);
954
1092
 
1093
+ // Let the during-run bind poll finish (it bails immediately once agy
1094
+ // has exited, so this rarely blocks).
1095
+ await bindDuringRun;
1096
+
955
1097
  details.exitCode = outcome.exitCode;
956
1098
  details.aborted = outcome.aborted;
957
1099
  details.timedOut = outcome.timedOut;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estebanforge/pi-ask-antigravity",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Pi tool that delegates a self-contained sub-task to Google Antigravity's agy CLI (the CLI for Gemini). Friendly model aliases (flash/pro/gemini), configurable default model + thinking, and the AskAntigravity delegation tool.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -22,7 +22,15 @@
22
22
  "type": "git",
23
23
  "url": "git+https://github.com/EstebanForge/pi-ask-antigravity.git"
24
24
  },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
25
28
  "type": "module",
29
+ "scripts": {
30
+ "typecheck": "tsc --noEmit",
31
+ "test": "vitest run",
32
+ "prepublishOnly": "npm run typecheck && npm test"
33
+ },
26
34
  "files": [
27
35
  "extensions",
28
36
  "README.md"
@@ -41,11 +49,12 @@
41
49
  }
42
50
  },
43
51
  "devDependencies": {
44
- "@earendil-works/pi-coding-agent": "^0.80.2",
45
- "@earendil-works/pi-ai": "^0.80.2",
46
- "@earendil-works/pi-tui": "^0.80.2",
52
+ "@earendil-works/pi-coding-agent": "^0.84.0",
53
+ "@earendil-works/pi-ai": "^0.84.0",
54
+ "@earendil-works/pi-tui": "^0.84.0",
47
55
  "@types/node": "^22.0.0",
48
56
  "typebox": "^1.1.38",
49
- "typescript": "^5.8.0"
57
+ "typescript": "^5.8.0",
58
+ "vitest": "^4.1.7"
50
59
  }
51
60
  }