shariq-pi-extensions 0.2.4 → 0.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.
@@ -6,7 +6,7 @@ Last verified: 2026-08-20
6
6
 
7
7
  ### [Antigravity Provider](../extensions/antigravity-provider/README.md)
8
8
 
9
- Registers the `antigravity` provider and `/login antigravity` flow for supported Google Antigravity models. Repeating login adds or updates accounts in the secure `<agent-dir>/antigravity/accounts.json` pool. Requests use quota-aware least-recently-used balancing, rotate before streaming on account-specific auth/rate/quota/capacity failures, honor known reset times, and refresh stored OAuth tokens. `/antigravity` shows account state and live model quotas and can enable or disable accounts; `/antigravity.doctor` reports sanitized provider and rotation diagnostics.
9
+ Registers the `antigravity` provider and `/login antigravity` flow for the latest Gemini Flash, Gemini Pro, Claude Sonnet, and Claude Opus families. Repeating login adds or updates accounts in the secure `<agent-dir>/antigravity/accounts.json` pool. Requests use quota-aware least-recently-used balancing, rotate before streaming on account-specific auth/rate/quota/capacity failures, honor known reset times, and refresh stored OAuth tokens. `/antigravity` shows account state plus the shared five-hour and weekly limits for the Gemini and Claude pools and can enable or disable accounts; `/antigravity.doctor` reports sanitized provider and rotation diagnostics.
10
10
 
11
11
  ### [Cursor provider](../extensions/cursor-provider/README.md)
12
12
 
@@ -38,7 +38,7 @@ The goal extension adds persistent, branch-safe objectives, progress evidence, b
38
38
 
39
39
  The subagent extension runs flat Pi child agents with profiles, capability policies, continuation, result delivery, optional worktrees, and a dashboard. Configuration lives in `<agent-dir>/subagents.json`; trusted projects may override it through their Pi config directory. The configured concurrency ceiling is 50.
40
40
 
41
- The extension supplies tools including `spawn_agent`, `task`, `check_agent`, `list_agents`, `wait_agent`, `send_message`, `close_agent`, `reply_question`, and `apply_agent_changes`. Child settlement automatically sends a follow-up that starts the next parent turn; status tools are for explicit inspection, not waiting.
41
+ The extension supplies tools including `spawn_agent`, `task`, `check_agent`, `list_agents`, `wait_agent`, `send_message`, `close_agent`, `reply_question`, and `apply_agent_changes`. Child settlement is handed to Pi immediately as a turn-triggering follow-up, so Pi queues it while the parent is active or starts a new parent turn when idle; status tools are for explicit inspection, not waiting.
42
42
 
43
43
  ### [Orchestration](../extensions/orchestration/README.md)
44
44
 
@@ -50,7 +50,7 @@ The model-facing `create_orchestration` tool starts planning only after an expli
50
50
 
51
51
  Managed PTYs support servers, watchers, long builds, downloads, and interactive processes. The extension tracks up to eight concurrent terminals, retains bounded output, stores full logs in restrictive temporary directories, and stops process groups during shutdown or reload.
52
52
 
53
- Its tools are `start_terminal`, `read_terminal`, `write_terminal`, `list_terminals`, and `stop_terminal`. A model-started terminal automatically sends a completion or failure follow-up and starts the next parent turn. Reading a terminal no longer suppresses that delivery; agents should inspect only for explicit progress requests or immediate interaction.
53
+ Its tools are `start_terminal`, `read_terminal`, `write_terminal`, `list_terminals`, and `stop_terminal`. A model-started terminal immediately hands its completion or failure to Pi as a turn-triggering follow-up, which Pi queues while the parent is active or uses to start a new parent turn when idle. Reading a terminal does not suppress that delivery; agents should inspect only for explicit progress requests or immediate interaction.
54
54
 
55
55
  ## Web access
56
56
 
@@ -8,19 +8,18 @@ Local persistent Pi provider for Google Antigravity-compatible models.
8
8
  - Account and quota dashboard: `/antigravity`
9
9
  - Doctor command: `/antigravity.doctor`
10
10
 
11
- The provider includes an IPv4 OAuth token-exchange fallback for Node environments where the default request fails. Its deterministic catalog follows Antigravity CLI 1.1.13 model identifiers and runtime behavior.
11
+ The provider includes an IPv4 OAuth token-exchange fallback for Node environments where the default request fails. Its curated catalog follows current Antigravity model identifiers and runtime behavior.
12
12
 
13
13
  Successful logins are added to `<agent-dir>/antigravity/accounts.json`, written with owner-only permissions. Existing Pi OAuth credentials are migrated into that pool without exposing token values. Requests select the least recently used eligible account, skip disabled/cooling/exhausted accounts, refresh expiring OAuth tokens, and rotate to another account when an auth, rate, quota, or capacity failure occurs before response streaming begins. Cached per-model remaining quota and reset times guide selection; `/antigravity` refreshes the authoritative catalog and can reversibly enable or disable accounts.
14
14
 
15
15
  Current public model IDs:
16
16
  - `antigravity/gemini-3.7-flash`
17
- - `antigravity/gemini-3.6-flash`
18
- - `antigravity/gemini-3.5-flash`
19
17
  - `antigravity/gemini-3.1-pro`
20
18
  - `antigravity/claude-sonnet-4-6`
21
19
  - `antigravity/claude-opus-4-6`
22
- - `antigravity/gpt-oss-120b`
23
20
 
24
- Pi's thinking selector is normalized to Antigravity's runtime IDs: Flash models clamp `off`/`minimal` to `low` and `xhigh`/`max` to `high`; Gemini 3.1 Pro clamps to its `low`/`high` pair; Claude and GPT-OSS use their single fixed-thinking runtime regardless of the selected Pi level.
21
+ The provider intentionally exposes only the latest Gemini Flash and Pro generations and the latest Claude Sonnet and Opus family. `/antigravity` shows the shared five-hour and weekly limits for the Gemini and Claude pools instead of listing every upstream runtime variant.
22
+
23
+ Pi's thinking selector is normalized to Antigravity's runtime IDs: Gemini 3.7 Flash clamps `off`/`minimal` to `low` and `xhigh`/`max` to `high`; Gemini 3.1 Pro clamps to its `low`/`high` pair; Claude models use their fixed-thinking runtime regardless of the selected Pi level.
25
24
 
26
25
  The provider id intentionally remains `antigravity`; existing `~/.pi/agent/auth.json` credentials continue to work without moving secret values. After updating `agy`, compare `agy models` with `antigravity/models.ts` before changing this deterministic catalog.
@@ -14,11 +14,13 @@ const CAPACITY_COOLDOWN_MS = 60 * 1_000;
14
14
  const REFRESH_SKEW_MS = 60 * 1_000;
15
15
 
16
16
  export type AntigravityQuotaGroup = "gemini" | "non-gemini";
17
+ export type AntigravityQuotaWindow = "five-hour" | "weekly";
17
18
 
18
19
  export interface AntigravityQuotaEntry {
19
20
  modelId: string;
20
21
  displayName?: string;
21
22
  group: AntigravityQuotaGroup;
23
+ window?: AntigravityQuotaWindow;
22
24
  remainingFraction: number;
23
25
  resetTime?: string;
24
26
  }
@@ -78,6 +80,7 @@ function normalizeAccount(value: unknown): AntigravityAccount | undefined {
78
80
  modelId: entry.modelId,
79
81
  displayName: typeof entry.displayName === "string" ? entry.displayName : undefined,
80
82
  group: entry.group,
83
+ window: entry.window === "five-hour" || entry.window === "weekly" ? entry.window : undefined,
81
84
  remainingFraction: Math.max(0, Math.min(1, remainingFraction)),
82
85
  resetTime: typeof entry.resetTime === "string" ? entry.resetTime : undefined,
83
86
  }];
@@ -281,7 +284,74 @@ export function updateAntigravityAccountQuota(id: string, quota: AntigravityQuot
281
284
  }
282
285
 
283
286
  export function parseAntigravityQuota(value: unknown): AntigravityQuotaEntry[] {
284
- const entries: AntigravityQuotaEntry[] = [];
287
+ const findGroups = (node: unknown): unknown[] | undefined => {
288
+ if (!node || typeof node !== "object") return undefined;
289
+ if (Array.isArray(node)) {
290
+ for (const item of node) {
291
+ const found = findGroups(item);
292
+ if (found) return found;
293
+ }
294
+ return undefined;
295
+ }
296
+ const raw = node as Record<string, unknown>;
297
+ if (Array.isArray(raw.groups)) return raw.groups;
298
+ for (const child of Object.values(raw)) {
299
+ const found = findGroups(child);
300
+ if (found) return found;
301
+ }
302
+ return undefined;
303
+ };
304
+
305
+ const summaryEntries: AntigravityQuotaEntry[] = [];
306
+ for (const groupValue of findGroups(value) ?? []) {
307
+ if (!groupValue || typeof groupValue !== "object") continue;
308
+ const group = groupValue as Record<string, unknown>;
309
+ const groupLabel = [group.displayName, group.name, group.id].find((item): item is string => typeof item === "string") ?? "";
310
+ const quotaGroup: AntigravityQuotaGroup | undefined = /gemini/i.test(groupLabel)
311
+ ? "gemini"
312
+ : /claude|third.party|3p|gpt/i.test(groupLabel)
313
+ ? "non-gemini"
314
+ : undefined;
315
+ if (!quotaGroup || !Array.isArray(group.buckets)) continue;
316
+ for (const bucketValue of group.buckets) {
317
+ if (!bucketValue || typeof bucketValue !== "object") continue;
318
+ const bucket = bucketValue as Record<string, unknown>;
319
+ const remaining = bucket.remaining && typeof bucket.remaining === "object"
320
+ ? bucket.remaining as Record<string, unknown>
321
+ : undefined;
322
+ const remainingFraction = finiteNumber(bucket.remainingFraction) ?? finiteNumber(remaining?.remainingFraction);
323
+ const windowLabel = [bucket.window, bucket.bucketId, bucket.displayName].find((item): item is string => typeof item === "string") ?? "";
324
+ const window: AntigravityQuotaWindow | undefined = /week/i.test(windowLabel)
325
+ ? "weekly"
326
+ : /5h|five.?hour/i.test(windowLabel)
327
+ ? "five-hour"
328
+ : undefined;
329
+ if (remainingFraction === undefined || !window) continue;
330
+ summaryEntries.push({
331
+ modelId: typeof bucket.bucketId === "string" ? bucket.bucketId : `${quotaGroup}-${window}`,
332
+ displayName: window === "weekly" ? "Weekly" : "5-Hour",
333
+ group: quotaGroup,
334
+ window,
335
+ remainingFraction: Math.max(0, Math.min(1, remainingFraction)),
336
+ resetTime: typeof bucket.resetTime === "string"
337
+ ? bucket.resetTime
338
+ : typeof remaining?.resetTime === "string"
339
+ ? remaining.resetTime
340
+ : undefined,
341
+ });
342
+ }
343
+ }
344
+ if (summaryEntries.length) return summaryEntries;
345
+
346
+ const supportedRuntimeIds = new Set(
347
+ Object.entries(ANTIGRAVITY_ROUTING).flatMap(([modelId, routing]) => [
348
+ modelId,
349
+ routing.off,
350
+ routing.defaultRequestId,
351
+ ...Object.values(routing.routing ?? {}),
352
+ ]).filter((item): item is string => typeof item === "string"),
353
+ );
354
+ const modelEntries: AntigravityQuotaEntry[] = [];
285
355
  const seen = new Set<string>();
286
356
  const visit = (node: unknown, keyHint?: string) => {
287
357
  if (!node || typeof node !== "object") return;
@@ -291,17 +361,17 @@ export function parseAntigravityQuota(value: unknown): AntigravityQuotaEntry[] {
291
361
  }
292
362
  const raw = node as Record<string, unknown>;
293
363
  const quota = raw.quotaInfo && typeof raw.quotaInfo === "object" ? raw.quotaInfo as Record<string, unknown> : raw;
294
- const remaining = finiteNumber(quota.remainingFraction);
295
- const modelId = [raw.modelId, raw.id, raw.name, raw.model, keyHint].find((item): item is string => typeof item === "string" && /gemini|claude|gpt-oss/i.test(item));
296
- if (modelId && remaining !== undefined) {
364
+ const remainingFraction = finiteNumber(quota.remainingFraction);
365
+ const modelId = [raw.modelId, raw.id, raw.name, raw.model, keyHint].find((item): item is string => typeof item === "string");
366
+ if (modelId && remainingFraction !== undefined) {
297
367
  const normalized = modelId.replace(/^models\//, "");
298
- if (!seen.has(normalized)) {
368
+ if (supportedRuntimeIds.has(normalized) && !seen.has(normalized)) {
299
369
  seen.add(normalized);
300
- entries.push({
370
+ modelEntries.push({
301
371
  modelId: normalized,
302
- displayName: typeof raw.displayName === "string" ? raw.displayName : typeof raw.label === "string" ? raw.label : undefined,
303
372
  group: normalized.startsWith("gemini-") ? "gemini" : "non-gemini",
304
- remainingFraction: Math.max(0, Math.min(1, remaining)),
373
+ window: "five-hour",
374
+ remainingFraction: Math.max(0, Math.min(1, remainingFraction)),
305
375
  resetTime: typeof quota.resetTime === "string" ? quota.resetTime : undefined,
306
376
  });
307
377
  }
@@ -311,7 +381,16 @@ export function parseAntigravityQuota(value: unknown): AntigravityQuotaEntry[] {
311
381
  }
312
382
  };
313
383
  visit(value);
314
- return entries;
384
+
385
+ return (["gemini", "non-gemini"] as const).flatMap((group) => {
386
+ const entries = modelEntries.filter((entry) => entry.group === group).sort((left, right) => left.remainingFraction - right.remainingFraction);
387
+ const mostConstrained = entries[0];
388
+ return mostConstrained ? [{
389
+ ...mostConstrained,
390
+ modelId: group === "gemini" ? "gemini-5h" : "claude-5h",
391
+ displayName: "5-Hour",
392
+ }] : [];
393
+ });
315
394
  }
316
395
 
317
396
  const refreshes = new Map<string, Promise<AntigravityAccount>>();
@@ -29,7 +29,22 @@ function until(timestamp: number | string | undefined) {
29
29
  }
30
30
 
31
31
  function groupQuota(quota: AntigravityQuotaEntry[] | undefined, group: "gemini" | "non-gemini") {
32
- return (quota ?? []).filter((entry) => entry.group === group).sort((left, right) => left.remainingFraction - right.remainingFraction);
32
+ const byWindow = new Map<"five-hour" | "weekly", AntigravityQuotaEntry>();
33
+ for (const entry of (quota ?? []).filter((candidate) => candidate.group === group)) {
34
+ const window = entry.window ?? "five-hour";
35
+ const existing = byWindow.get(window);
36
+ if (!existing || entry.remainingFraction < existing.remainingFraction) {
37
+ byWindow.set(window, {
38
+ ...entry,
39
+ window,
40
+ displayName: window === "weekly" ? "Weekly" : "5-Hour",
41
+ });
42
+ }
43
+ }
44
+ return (["five-hour", "weekly"] as const).flatMap((window) => {
45
+ const entry = byWindow.get(window);
46
+ return entry ? [entry] : [];
47
+ });
33
48
  }
34
49
 
35
50
  function accountState(account: AntigravityAccountStatus) {
@@ -50,8 +65,7 @@ function stateColor(state: string) {
50
65
 
51
66
  function compactGroup(label: string, entries: AntigravityQuotaEntry[]) {
52
67
  if (!entries.length) return `${label}: unavailable`;
53
- const worst = entries[0]!;
54
- return `${label}: ${remaining(worst.remainingFraction)} remaining${worst.resetTime ? ` · resets ${until(worst.resetTime)}` : ""}`;
68
+ return `${label}: ${entries.map((entry) => `${entry.displayName} ${remaining(entry.remainingFraction)}${entry.resetTime ? ` · resets ${until(entry.resetTime)}` : ""}`).join(" · ")}`;
55
69
  }
56
70
 
57
71
  export class AntigravityDashboard implements Component {
@@ -159,7 +173,7 @@ export class AntigravityDashboard implements Component {
159
173
  const lines = width >= 64
160
174
  ? [joinSides(title, `${right} `, width)]
161
175
  : [truncateToWidth(title, width, ""), truncateToWidth(` ${right}`, width, "")];
162
- lines.push(frameTop(this.theme, width, `ACCOUNTS ${accounts.length} · QUOTA REMAINING`));
176
+ lines.push(frameTop(this.theme, width, `ACCOUNTS ${accounts.length} · 5-HOUR + WEEKLY LIMITS REMAINING`));
163
177
  const inner = Math.max(1, width - 2);
164
178
  if (width >= 104 && selected) {
165
179
  const leftWidth = Math.max(46, Math.floor((inner - 1) * 0.45));
@@ -195,7 +209,7 @@ export class AntigravityDashboard implements Component {
195
209
  lines.push(selected ? this.theme.bg("selectedBg", padLine(first, width)) : first);
196
210
  if (!compact) {
197
211
  lines.push(` ${this.theme.fg("muted", compactGroup("Gemini", groupQuota(account.quota, "gemini")))}`);
198
- lines.push(` ${this.theme.fg("muted", compactGroup("Claude / GPT", groupQuota(account.quota, "non-gemini")))}`);
212
+ lines.push(` ${this.theme.fg("muted", compactGroup("Claude", groupQuota(account.quota, "non-gemini")))}`);
199
213
  lines.push("");
200
214
  }
201
215
  }
@@ -211,8 +225,8 @@ export class AntigravityDashboard implements Component {
211
225
  if (account.cooldownUntil && account.cooldownUntil > Date.now()) lines.push(` ${this.theme.fg("warning", `${account.cooldownReason || "rotation"} cooldown · ${until(account.cooldownUntil)}`)}`);
212
226
  if (account.quotaError) lines.push(` ${this.theme.fg("error", oneLine(account.quotaError))}`);
213
227
  if (account.lastError) lines.push(` ${this.theme.fg("warning", oneLine(account.lastError))}`);
214
- lines.push("", ...this.renderQuotaGroup("Gemini", groupQuota(account.quota, "gemini"), width));
215
- lines.push("", ...this.renderQuotaGroup("Claude / GPT-OSS", groupQuota(account.quota, "non-gemini"), width));
228
+ lines.push("", ...this.renderQuotaGroup("Gemini models", groupQuota(account.quota, "gemini"), width));
229
+ lines.push("", ...this.renderQuotaGroup("Claude models", groupQuota(account.quota, "non-gemini"), width));
216
230
  if (account.lastUsedAt) lines.push("", ` ${this.theme.fg("muted", "LAST USED")} ${this.theme.fg("text", new Date(account.lastUsedAt).toLocaleString())}`);
217
231
  return lines.slice(0, height);
218
232
  }
@@ -221,7 +235,7 @@ export class AntigravityDashboard implements Component {
221
235
  const lines = [` ${this.theme.fg("accent", this.theme.bold(`${name.toUpperCase()} · REMAINING`))}`];
222
236
  if (!entries.length) return [...lines, ` ${this.theme.fg("muted", "Unavailable")}`];
223
237
  const meterWidth = Math.max(10, width - 29);
224
- for (const entry of entries.slice(0, 8)) {
238
+ for (const entry of entries) {
225
239
  const percent = entry.remainingFraction * 100;
226
240
  const color = percent <= 0 ? "error" : percent <= 15 ? "warning" : "active";
227
241
  const label = oneLine(entry.displayName || entry.modelId).slice(0, 16).padEnd(16);
@@ -59,42 +59,6 @@ export const ANTIGRAVITY_ROUTING: Record<string, AntigravityRouting> = {
59
59
  },
60
60
  defaultRequestId: "gemini-3.7-flash-low",
61
61
  },
62
- "gemini-3.6-flash": {
63
- off: "gemini-3.6-flash-low",
64
- routing: {
65
- minimal: "gemini-3.6-flash-low",
66
- low: "gemini-3.6-flash-low",
67
- medium: "gemini-3.6-flash-medium",
68
- high: "gemini-3.6-flash-high",
69
- xhigh: "gemini-3.6-flash-high",
70
- max: "gemini-3.6-flash-high",
71
- },
72
- defaultRequestId: "gemini-3.6-flash-low",
73
- },
74
- "gemini-3.5-flash": {
75
- off: "gemini-3.5-flash-extra-low",
76
- routing: {
77
- minimal: "gemini-3.5-flash-extra-low",
78
- low: "gemini-3.5-flash-extra-low",
79
- medium: "gemini-3.5-flash-low",
80
- high: "gemini-3-flash-agent",
81
- xhigh: "gemini-3-flash-agent",
82
- max: "gemini-3-flash-agent",
83
- },
84
- defaultRequestId: "gemini-3.5-flash-low",
85
- },
86
- "gpt-oss-120b": {
87
- off: "gpt-oss-120b-medium",
88
- routing: {
89
- minimal: "gpt-oss-120b-medium",
90
- low: "gpt-oss-120b-medium",
91
- medium: "gpt-oss-120b-medium",
92
- high: "gpt-oss-120b-medium",
93
- xhigh: "gpt-oss-120b-medium",
94
- max: "gpt-oss-120b-medium",
95
- },
96
- defaultRequestId: "gpt-oss-120b-medium",
97
- },
98
62
  };
99
63
 
100
64
  export const ANTIGRAVITY_MODELS: ProviderModelConfig[] = [
@@ -138,36 +102,6 @@ export const ANTIGRAVITY_MODELS: ProviderModelConfig[] = [
138
102
  contextWindow: 1048576,
139
103
  maxTokens: 65536,
140
104
  },
141
- {
142
- id: "gemini-3.6-flash",
143
- name: "Gemini 3.6 Flash (Antigravity)",
144
- reasoning: true,
145
- thinkingLevelMap: { off: null, minimal: "LOW", low: "LOW", medium: "MEDIUM", high: "HIGH", xhigh: "HIGH", max: "HIGH" } as any,
146
- input: ["text", "image"],
147
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
148
- contextWindow: 1048576,
149
- maxTokens: 65536,
150
- },
151
- {
152
- id: "gemini-3.5-flash",
153
- name: "Gemini 3.5 Flash (Antigravity)",
154
- reasoning: true,
155
- thinkingLevelMap: { off: null, minimal: "LOW", low: "LOW", medium: "MEDIUM", high: "HIGH", xhigh: "HIGH", max: "HIGH" } as any,
156
- input: ["text", "image"],
157
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
158
- contextWindow: 1048576,
159
- maxTokens: 65536,
160
- },
161
- {
162
- id: "gpt-oss-120b",
163
- name: "GPT-OSS 120B (Antigravity)",
164
- reasoning: true,
165
- thinkingLevelMap: { off: null, minimal: "MEDIUM", low: "MEDIUM", medium: "MEDIUM", high: "MEDIUM", xhigh: "MEDIUM", max: "MEDIUM" } as any,
166
- input: ["text"],
167
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
168
- contextWindow: 131072,
169
- maxTokens: 32768,
170
- },
171
105
  ];
172
106
 
173
107
  export function getAntigravityRequestModelId(modelId: string, effort: string | undefined): string {
@@ -322,8 +322,28 @@ function findDynamicModel(value: any, requestedId: string): DynamicModelInfo | u
322
322
 
323
323
  export async function fetchAntigravityQuotaCatalog(token: string, projectId: string, signal?: AbortSignal): Promise<unknown> {
324
324
  let lastFailure = "Antigravity quota catalog is unavailable.";
325
- const bodies = [{}, { cloudaicompanionProject: projectId }, { project: projectId }];
326
325
  for (const endpoint of endpointCandidates()) {
326
+ try {
327
+ const res = await fetch(`${endpoint}/v1internal:retrieveUserQuotaSummary`, {
328
+ method: "POST",
329
+ headers: antigravityHeaders(token),
330
+ body: "{}",
331
+ signal: boundedFetchSignal(signal),
332
+ });
333
+ lastStatus = res.status;
334
+ lastEndpoint = endpoint;
335
+ if (res.ok) {
336
+ const summary = await res.json() as { groups?: unknown[] };
337
+ if (Array.isArray(summary.groups) && summary.groups.length) return summary;
338
+ }
339
+ else lastFailure = `HTTP ${res.status}: ${jsonOrTextError(await res.text())}`;
340
+ } catch (error) {
341
+ if (signal?.aborted) throw error;
342
+ lastFailure = safeError(error);
343
+ lastError = lastFailure;
344
+ }
345
+
346
+ const bodies = [{}, { cloudaicompanionProject: projectId }, { project: projectId }];
327
347
  for (const candidateBody of bodies) {
328
348
  try {
329
349
  const res = await fetch(`${endpoint}/v1internal:fetchAvailableModels`, {
@@ -20,7 +20,9 @@ export function refreshAntigravityQuotas(options: { force?: boolean; signal?: Ab
20
20
  const accounts = antigravityAccountStatuses(now);
21
21
  for (const account of accounts) {
22
22
  if (account.disabled) continue;
23
- if (!options.force && account.quotaUpdatedAt && now - account.quotaUpdatedAt < ANTIGRAVITY_QUOTA_TTL_MS) continue;
23
+ const hasBothWindows = account.quota?.some((entry) => entry.window === "five-hour") &&
24
+ account.quota.some((entry) => entry.window === "weekly");
25
+ if (!options.force && hasBothWindows && account.quotaUpdatedAt && now - account.quotaUpdatedAt < ANTIGRAVITY_QUOTA_TTL_MS) continue;
24
26
  try {
25
27
  const resolved = await resolveAntigravityAccount(account, refreshAntigravityToken, now);
26
28
  const catalog = await fetchAntigravityQuotaCatalog(resolved.access, resolved.projectId, options.signal);
@@ -10,7 +10,7 @@ Session-scoped background pseudo-terminals for Pi. The extension combines Codex-
10
10
  - `list_terminals` — list running and settled terminals.
11
11
  - `stop_terminal` — stop complete process groups with TERM-to-KILL escalation.
12
12
 
13
- Each output response carries a byte cursor. Pass it to the next read/write operation to avoid repeating output. Long or uncertain commands should use `start_terminal` instead of a large blocking `bash` timeout. Completion wakes the parent automatically, so it can continue other work or end its turn rather than poll.
13
+ Each output response carries a byte cursor. Pass it to the next read/write operation to avoid repeating output. Long or uncertain commands should use `start_terminal` instead of a large blocking `bash` timeout. Settlement is handed to Pi immediately: it queues a follow-up while the parent is active or starts a new parent turn when idle, so the parent can continue other work or end its turn rather than poll.
14
14
 
15
15
  ## User interface
16
16
 
@@ -34,7 +34,7 @@ Each output response carries a byte cursor. Pass it to the next read/write opera
34
34
  - Output is sanitized before TUI or model rendering.
35
35
  - Processes run in their own PTY process group and are stopped on session shutdown, replacement, or reload.
36
36
  - Shutdown and stop operations are bounded and escalate from SIGTERM to SIGKILL.
37
- - Model-started terminals automatically deliver one completion/failure follow-up that starts the next parent turn.
37
+ - Model-started terminals immediately hand one completion/failure follow-up to Pi; Pi queues it while the parent is active or starts the next parent turn when idle.
38
38
  - Reading settled output does not consume or suppress the automatic completion delivery.
39
39
  - Completion delivery is keyed by terminal id to prevent duplicate follow-ups.
40
40
 
@@ -3,12 +3,12 @@ import * as path from "node:path";
3
3
  import type {
4
4
  ExtensionAPI,
5
5
  ExtensionCommandContext,
6
- ExtensionContext,
7
6
  ExtensionUIContext,
8
7
  } from "@earendil-works/pi-coding-agent";
9
8
  import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
10
9
  import { Markdown, Text } from "@earendil-works/pi-tui";
11
10
  import { Type } from "typebox";
11
+ import { deliverSettlement } from "../shared/settlement-delivery.ts";
12
12
  import { oneLine, sanitizeTerminalText, stateLabel } from "../shared/tui-dashboard.ts";
13
13
  import { TerminalManager, MAX_RUNNING_TERMINALS } from "./src/manager.ts";
14
14
  import {
@@ -37,12 +37,12 @@ function resolveCwd(base: string, requested?: string): string {
37
37
 
38
38
  export default function backgroundTerminals(pi: ExtensionAPI) {
39
39
  let manager: TerminalManager | undefined;
40
- let sessionContext: ExtensionContext | undefined;
41
40
  let ui: ExtensionUIContext | undefined;
42
41
  let unsubscribe: (() => void) | undefined;
43
42
  let lastStatus = "";
44
43
  const pendingResults = new Map<string, TerminalSnapshot>();
45
44
  const modelOwned = new Set<string>();
45
+ const starting = new Set<string>();
46
46
 
47
47
  const getManager = (): TerminalManager => {
48
48
  if (manager) return manager;
@@ -56,7 +56,7 @@ export default function backgroundTerminals(pi: ExtensionAPI) {
56
56
  return;
57
57
  }
58
58
  pendingResults.set(snapshot.id, snapshot);
59
- if (sessionContext?.isIdle()) flushResults();
59
+ if (!starting.has(snapshot.id)) flushResult(snapshot.id);
60
60
  });
61
61
  unsubscribe = manager.view.subscribe(updateStatus);
62
62
  updateStatus();
@@ -85,40 +85,31 @@ export default function backgroundTerminals(pi: ExtensionAPI) {
85
85
  }
86
86
  }
87
87
 
88
- function deliver(snapshot: TerminalSnapshot): void {
89
- pi.sendMessage(
90
- {
91
- customType: "background-terminal-result",
92
- content: formatCompletion(snapshot),
93
- display: true,
94
- details: {
95
- id: snapshot.id,
96
- title: snapshot.title,
97
- status: snapshot.status,
98
- exitCode: snapshot.exitCode,
99
- },
88
+ function flushResult(id: string): void {
89
+ const snapshot = pendingResults.get(id);
90
+ if (!snapshot) return;
91
+ pendingResults.delete(id);
92
+ deliverSettlement(pi, {
93
+ customType: "background-terminal-result",
94
+ content: formatCompletion(snapshot),
95
+ display: true,
96
+ details: {
97
+ id: snapshot.id,
98
+ title: snapshot.title,
99
+ status: snapshot.status,
100
+ exitCode: snapshot.exitCode,
100
101
  },
101
- { deliverAs: "followUp", triggerTurn: true },
102
- );
103
- }
104
-
105
- function flushResults(): void {
106
- const results = [...pendingResults.values()];
107
- pendingResults.clear();
108
- for (const snapshot of results) deliver(snapshot);
102
+ });
109
103
  }
110
104
 
111
105
  pi.on("session_start", (_event, ctx) => {
112
- sessionContext = ctx;
113
106
  if (ctx.hasUI) ui = ctx.ui;
114
107
  });
115
108
 
116
- pi.on("agent_settled", flushResults);
117
-
118
109
  pi.on("session_shutdown", async () => {
119
- sessionContext = undefined;
120
110
  pendingResults.clear();
121
111
  modelOwned.clear();
112
+ starting.clear();
122
113
  unsubscribe?.();
123
114
  unsubscribe = undefined;
124
115
  ui?.setStatus(STATUS_KEY, undefined);
@@ -140,7 +131,7 @@ export default function backgroundTerminals(pi: ExtensionAPI) {
140
131
  promptSnippet: "Start an interactive or long-running command in a managed background PTY.",
141
132
  promptGuidelines: [
142
133
  "Use start_terminal by default for servers, watchers, downloads, long or uncertain builds and tests, interactive shells, and any command that should not occupy the main turn; reserve bash for short commands whose result is needed immediately. Never use a large bash timeout merely to wait for long work.",
143
- "After start_terminal returns, continue only genuinely independent work. If none remains, end the turn immediately. The terminal completion automatically sends a follow-up and starts the next parent turn; do not call read_terminal, list_terminals, or start a timer merely to check whether it finished.",
134
+ "After start_terminal returns, continue only genuinely independent work. If none remains, end the turn immediately. Terminal settlement is handed to Pi immediately: it queues a follow-up while the parent is active or starts the next parent turn when idle. When that result invokes the parent, continue the original task immediately without waiting for the user or rereading the same terminal; do not call read_terminal, list_terminals, or start a timer merely to check whether it finished.",
144
135
  "Use stop_terminal when a managed process is no longer needed. Background terminals are session-scoped and are stopped during session shutdown or reload.",
145
136
  ],
146
137
  parameters: Type.Object({
@@ -158,6 +149,7 @@ export default function backgroundTerminals(pi: ExtensionAPI) {
158
149
  cwd: resolveCwd(ctx.cwd, params.working_dir),
159
150
  });
160
151
  modelOwned.add(terminal.id);
152
+ starting.add(terminal.id);
161
153
  let result: TerminalReadResult;
162
154
  try {
163
155
  result = await getManager().read(terminal.id, {
@@ -166,12 +158,15 @@ export default function backgroundTerminals(pi: ExtensionAPI) {
166
158
  signal,
167
159
  });
168
160
  } catch (error) {
169
- await getManager().kill([terminal.id]).catch(() => undefined);
161
+ starting.delete(terminal.id);
170
162
  modelOwned.delete(terminal.id);
163
+ await getManager().kill([terminal.id]).catch(() => undefined);
171
164
  consume(terminal.id);
172
165
  throw error;
173
166
  }
167
+ starting.delete(terminal.id);
174
168
  if (result.snapshot.status !== "running") consume(terminal.id);
169
+ else flushResult(terminal.id);
175
170
  let text = `Started background terminal ${terminal.id} "${oneLine(terminal.title)}" (pid ${terminal.pid}). Use cursor ${result.cursor} for incremental reads.`;
176
171
  if (result.text || result.snapshot.status !== "running") {
177
172
  text += `\n\n${formatReadResult(result)}`;
@@ -8,6 +8,7 @@ Internal runtime utilities used by more than one extension. This directory is no
8
8
  - `child-session.ts` owns trust-aware child resources and bounded session shutdown.
9
9
  - `context-utilization.ts` formats model-context usage and capacity.
10
10
  - `dashboard-state.ts` keeps list selection stable as live rows change.
11
+ - `settlement-delivery.ts` immediately hands asynchronous results to Pi as turn-triggering follow-ups.
11
12
  - `tool-call-timeout.ts` applies cancellation-aware execution limits to registered tools.
12
13
  - `tui-dashboard.ts` provides bounded, sanitized terminal-dashboard rendering helpers.
13
14
 
@@ -0,0 +1,11 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
3
+ type SettlementMessage = Parameters<ExtensionAPI["sendMessage"]>[0];
4
+
5
+ /**
6
+ * Hand settlement to Pi immediately. Pi starts a turn when idle and queues the
7
+ * same message as a follow-up when the parent is still running.
8
+ */
9
+ export function deliverSettlement(pi: ExtensionAPI, message: SettlementMessage): void {
10
+ pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
11
+ }
@@ -19,7 +19,7 @@ The system is deliberately flat. Only the main Pi thread can spawn subagents. Ch
19
19
  - `reply_question` — answer a child’s blocking `ask_parent` request
20
20
  - `task` — atomically reserve capacity for up to the configured limit (maximum 50), start the fan-out in the background, and return child ids immediately
21
21
 
22
- Child sessions receive `message_parent`, `ask_parent`, `list_peers`, and `message_peer`. Peer messages are routed through the main-thread manager and can steer a running child or continue a settled one; they cannot create agents. Background completions wake the parent automatically, so the main turn can continue independent work or end and remain available to the user.
22
+ Child sessions receive `message_parent`, `ask_parent`, `list_peers`, and `message_peer`. Peer messages are routed through the main-thread manager and can steer a running child or continue a settled one; they cannot create agents. Child settlement is handed to Pi immediately: it queues a follow-up while the parent is active or starts a new parent turn when idle, so the main turn can continue independent work or end and remain available to the user.
23
23
 
24
24
  ## Profiles and capabilities
25
25
 
@@ -86,4 +86,3 @@ Children share the requested workspace by default. Use `isolation: "worktree"` o
86
86
  - `src/worktree.ts` — isolated worktree creation, inspection, conflict preflight, integration, and cleanup
87
87
  - `src/runtime.ts` — managed runtime and Pi backend registry
88
88
  - `src/ui/` — dashboard, transcript, and takeover components
89
- - `src/result-delivery.ts` — deferred automatic result delivery
@@ -26,6 +26,7 @@ import {
26
26
  } from "@earendil-works/pi-coding-agent";
27
27
  import { Markdown, Text } from "@earendil-works/pi-tui";
28
28
  import { Type } from "typebox";
29
+ import { deliverSettlement } from "../shared/settlement-delivery.ts";
29
30
  import {
30
31
  formatElapsed,
31
32
  latestText,
@@ -56,7 +57,6 @@ import {
56
57
  SUBAGENT_WAIT_TOOL_DESCRIPTION,
57
58
  WORKTREE_ISOLATION_DESCRIPTION,
58
59
  } from "./src/prompt.ts";
59
- import { createDeferredResultDelivery } from "./src/result-delivery.ts";
60
60
  import {
61
61
  CAPABILITY_MODES,
62
62
  ISOLATION_MODES,
@@ -197,10 +197,8 @@ function resolveChildProjectTrust(options: {
197
197
  export default function (pi: ExtensionAPI) {
198
198
  let runtime: SubagentRuntime | undefined;
199
199
  let managerPromise: Promise<SubagentManagerShape> | undefined;
200
- let sessionContext: ExtensionContext | undefined;
201
200
  let ui: ExtensionUIContext | undefined;
202
201
  let unsubStatus: (() => void) | undefined;
203
- const resultDelivery = createDeferredResultDelivery<SubagentSnapshot>();
204
202
  const archived = new Map<string, ArchivedSubagent>();
205
203
  const peerHistory: PeerMessage[] = [];
206
204
  const pendingQuestions = new Map<
@@ -524,35 +522,24 @@ export default function (pi: ExtensionAPI) {
524
522
  };
525
523
 
526
524
  const deliverResult = (snap: SubagentSnapshot) => {
527
- pi.sendMessage(
528
- {
529
- customType: "subagent-result",
530
- content: buildSubagentResultMessage({
531
- id: snap.id,
532
- title: snap.title,
533
- status: snap.status,
534
- errorText: snap.errorText,
535
- output: truncatedOutput(snap),
536
- }),
537
- display: true,
538
- details: { id: snap.id, title: snap.title, status: snap.status },
539
- },
540
- { deliverAs: "followUp", triggerTurn: true },
541
- );
542
- };
543
-
544
- const flushResults = () => {
545
- for (const snap of resultDelivery.drain()) deliverResult(snap);
525
+ deliverSettlement(pi, {
526
+ customType: "subagent-result",
527
+ content: buildSubagentResultMessage({
528
+ id: snap.id,
529
+ title: snap.title,
530
+ status: snap.status,
531
+ errorText: snap.errorText,
532
+ output: truncatedOutput(snap),
533
+ }),
534
+ display: true,
535
+ details: { id: snap.id, title: snap.title, status: snap.status },
536
+ });
546
537
  };
547
538
 
548
539
  const onSettled = (snap: SubagentSnapshot, consumed: boolean) => {
549
540
  persistSnapshot(snap);
550
- if (snap.meta.origin === "orchestration") {
551
- resultDelivery.consume([snap.id]);
552
- return;
553
- }
541
+ if (snap.meta.origin === "orchestration") return;
554
542
  if (snap.meta.origin === "btw") {
555
- resultDelivery.consume([snap.id]);
556
543
  pi.appendEntry<BtwEntryData>(BTW_ENTRY_TYPE, {
557
544
  id: snap.id,
558
545
  title: snap.title,
@@ -562,16 +549,10 @@ export default function (pi: ExtensionAPI) {
562
549
  });
563
550
  return;
564
551
  }
565
- if (consumed) {
566
- resultDelivery.consume([snap.id]);
567
- return;
568
- }
569
- // Keep the result retractable while the parent is working. A later
570
- // wait_agent can consume it before agent_settled flushes follow-ups.
571
- // Defer a copy: the live snapshot keeps mutating if the subagent is
572
- // restarted before the deferred result flushes.
573
- resultDelivery.defer({ ...snap, meta: { ...snap.meta } });
574
- if (sessionContext?.isIdle()) flushResults();
552
+ if (consumed) return;
553
+ // Hand the immutable settlement to Pi immediately. Pi queues it when the
554
+ // parent is active and starts a new parent turn when idle.
555
+ deliverResult({ ...snap, meta: { ...snap.meta } });
575
556
  };
576
557
 
577
558
  const coordinator: SubagentCoordinator = {
@@ -641,7 +622,6 @@ export default function (pi: ExtensionAPI) {
641
622
  );
642
623
 
643
624
  pi.on("session_start", (_event, ctx) => {
644
- sessionContext = ctx;
645
625
  archived.clear();
646
626
  peerHistory.length = 0;
647
627
  for (const record of loadSubagentCatalog().values()) archived.set(record.id, record);
@@ -662,12 +642,8 @@ export default function (pi: ExtensionAPI) {
662
642
  if (ctx.hasUI) ui = ctx.ui;
663
643
  });
664
644
 
665
- pi.on("agent_settled", flushResults);
666
-
667
645
  pi.on("session_shutdown", async () => {
668
- sessionContext = undefined;
669
646
  unsubscribeCoordinator();
670
- resultDelivery.clear();
671
647
  for (const pending of pendingQuestions.values()) {
672
648
  pending.reject(new Error("Parent Pi session shut down before replying."));
673
649
  }
@@ -692,7 +668,7 @@ export default function (pi: ExtensionAPI) {
692
668
  promptSnippet: SUBAGENT_SPAWN_PROMPT_SNIPPET,
693
669
  promptGuidelines: [
694
670
  ...SUBAGENT_SPAWN_PROMPT_GUIDELINES,
695
- "After spawn_agent starts a child, continue only independent parent work or end the turn immediately. Do not call wait_agent, list_agents, or check_agent merely to watch it run; completion automatically starts the next parent turn.",
671
+ "After spawn_agent starts a child, continue only independent parent work or end the turn immediately. Do not call wait_agent, list_agents, or check_agent merely to watch it run. Settlement is handed to Pi immediately; when its attached summary invokes the parent, continue the original task without waiting for another user message.",
696
672
  ],
697
673
  parameters: Type.Object({
698
674
  message: Type.String({
@@ -832,7 +808,6 @@ export default function (pi: ExtensionAPI) {
832
808
  const snapshots = ids.map((id) => manager.view.get(id)!);
833
809
  const settled = snapshots.filter((snap) => snap.status !== "running");
834
810
  const pending = snapshots.filter((snap) => snap.status === "running");
835
- resultDelivery.consume(settled.map((snap) => snap.id));
836
811
 
837
812
  const sections: string[] = [];
838
813
  let remainingBytes = WAIT_OUTPUT_MAX_BYTES;
@@ -1121,7 +1096,7 @@ export default function (pi: ExtensionAPI) {
1121
1096
  label: "Start Pi Subagent Tasks",
1122
1097
  description: "Start independent subagent tasks together in the background and return their ids immediately. Completion notices automatically start the next parent turn, so the parent should end its current turn when no independent work remains instead of checking status.",
1123
1098
  promptGuidelines: [
1124
- "After task starts children, continue only independent parent work or end the turn immediately. Do not call wait_agent, list_agents, or check_agent merely to watch them run; completion automatically starts the next parent turn.",
1099
+ "After task starts children, continue only independent parent work or end the turn immediately. Do not call wait_agent, list_agents, or check_agent merely to watch them run. Settlements are handed to Pi immediately; when their attached summaries invoke the parent, continue the original task without waiting for another user message.",
1125
1100
  ],
1126
1101
  parameters: Type.Object({
1127
1102
  tasks: Type.Array(
@@ -19,6 +19,7 @@ export const SUBAGENT_SPAWN_PROMPT_GUIDELINES = [
19
19
  "Use resume_from to continue a completed child's existing context instead of restating its original task.",
20
20
  "After spawn_agent or task starts background work, continue useful parent work; otherwise end the turn so Pi remains available to the user.",
21
21
  "Do not poll background agents. Completion notices arrive automatically and wake the parent; wait_agent only collects results already available and reports running agents without blocking.",
22
+ "When a completion notice invokes the parent, use its attached summary and continue the original task immediately; do not wait for another user message or call a status tool for the same result.",
22
23
  ];
23
24
 
24
25
  export const SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS = {
@@ -51,7 +52,7 @@ export function buildSubagentSpawnResult(options: {
51
52
  ].filter(Boolean);
52
53
  return (
53
54
  `Started Pi subagent ${options.id} "${options.title}" (${attributes.join(", ")}).\n` +
54
- "It will report and wake the parent when finished. Continue useful work or end the turn instead of polling."
55
+ "It will report and wake the parent when finished. Continue useful work or end the turn instead of polling; when invoked by the result, continue the original task immediately."
55
56
  );
56
57
  }
57
58
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shariq-pi-extensions",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Cross-platform extension suite for the Pi coding agent.",
5
5
  "license": "MIT",
6
6
  "author": "Shariq Riaz",
@@ -24,7 +24,9 @@ Provide:
24
24
  - the working directory when it differs from the current directory;
25
25
  - an initial wait only when startup output is needed for the next decision.
26
26
 
27
- After startup, continue only genuinely useful independent work. If none remains, end the turn immediately. Terminal settlement automatically sends a follow-up and starts the next parent turn with the final status and bounded output. Do not keep the current turn alive to wait, invent monitoring work, or call terminal tools merely to see whether the process finished.
27
+ After startup, continue only genuinely useful independent work. If none remains, end the turn immediately. Ending the turn is the waiting mechanism: terminal settlement is handed to Pi immediately, queued as a follow-up if the parent is still active, and otherwise starts the next parent turn with the final status and bounded output. Do not keep the current turn alive to wait, invent monitoring work, or call terminal tools merely to see whether the process finished.
28
+
29
+ When that completion follow-up invokes the next turn, treat its attached output as the terminal result and continue the original task immediately. Do not wait for another user message, announce that you are still waiting, or call `read_terminal` to retrieve the same result again. If `start_terminal` itself returns a settled result, the output is already synchronous and no second completion notice is needed.
28
30
 
29
31
  ## Inspect and interact only when necessary
30
32
 
@@ -24,14 +24,14 @@ This skill governs temporary Pi child agents. The `codex-thread-orchestrator` sk
24
24
 
25
25
  ## Wait by notification; inspect progress only when justified
26
26
 
27
- A successful `spawn_agent` or `task` call starts asynchronous work and returns control to the parent. When a child finishes, its completion notice automatically starts the next main-agent turn. The parent does not need to remain active or check once before ending its turn.
27
+ A successful `spawn_agent` or `task` call starts asynchronous work and returns control to the parent. When a child finishes, its settlement is handed to Pi immediately, queued as a follow-up if the parent is still active, and otherwise starts the next main-agent turn. The parent does not need to remain active or check once before ending its turn.
28
28
 
29
29
  After dispatch:
30
30
 
31
31
  1. Continue only parent work that is independently useful to the requested result.
32
32
  2. If no such work remains, end the turn immediately. A short progress note is enough when the user needs one.
33
33
  3. Do not call `wait_agent`, `list_agents`, or `check_agent` in the same turn merely because the child was just launched. Ending the turn is the waiting mechanism.
34
- 4. Resume when a child completion notice invokes the main agent. Collect the completed result, launch any intentionally queued work if capacity requires waves, and otherwise keep waiting through notifications.
34
+ 4. When a child completion notice invokes the main agent, treat the attached summary as the child result and continue the original task immediately. Reconcile completed results, launch any intentionally queued work if capacity requires waves, and otherwise keep waiting through notifications. Do not wait for the user to prompt you again or call a status tool to retrieve the same result.
35
35
 
36
36
  A progress check is reasonable when the user asks for status, a child has run materially longer than expected for its task and model, an interruption left its state unclear, or current status will change an immediate coordination decision. Prefer `check_agent` for one known child and `list_agents` for a batch overview. Use `wait_agent` to collect results already expected to be available, not as a running-status probe.
37
37
 
@@ -1,20 +0,0 @@
1
- export function createDeferredResultDelivery<T extends { id: string }>() {
2
- const pending = new Map<string, T>();
3
-
4
- return {
5
- defer(result: T) {
6
- pending.set(result.id, result);
7
- },
8
- consume(ids: Iterable<string>) {
9
- for (const id of ids) pending.delete(id);
10
- },
11
- drain() {
12
- const results = [...pending.values()];
13
- pending.clear();
14
- return results;
15
- },
16
- clear() {
17
- pending.clear();
18
- },
19
- };
20
- }