@sars267/pi-delegation-broker 0.1.0-alpha.2 → 0.1.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.0-alpha.3 — 2026-10-26
4
+
5
+ - Integrate account-discovered Antigravity subscription routes from pi-multi-account 1.27.0 browser login, without manual application configuration or controller-held user tokens.
6
+ - Admit included Google/Anthropic/OpenAI subscription models without falsifying developer provenance; retain current-generation, quality and lease gates.
7
+ - Separate Gemini Argon/Pro/Flash/Flash-Lite generations. Model quota refusals no longer cool every model on an account; account concurrency remains shared.
8
+ - Preserve dispatch evidence across catalog observations and exact evidence-based legacy cooldown migration. Missing evidence never authorizes a blanket health reset.
9
+ - Bind extension/child dependencies to the physical Pi host through host-provided peers. Validate modern chronological system/tool transcript deltas without forwarding executable tools or private reasoning; preserve legacy contexts.
10
+ - Update clean development-host fixtures to Pi 1.3 and patched undici/brace-expansion versions. Native visual/tool/wake regressions run against the modern host; this does not replace the user's running Pi.
11
+ - Release only reviewed main; unrelated local readiness/Jev/metered-provider/workflow WIP is not part of this alpha.
12
+
3
13
  ## 0.1.0-alpha.2 — 2026-09-05
4
14
 
5
15
  ### Delegation usefulness and evidence
@@ -1,3 +1,4 @@
1
+ import { retainedWork } from "../src/retained-work.mjs";
1
2
  import { createControllerProvider } from "../src/pi-native-provider.ts";
2
3
  /**
3
4
  * Parent-side Pi extension for the delegation broker.
@@ -94,6 +95,7 @@ import {
94
95
  } from "../src/index.mjs";
95
96
 
96
97
  import { Type } from "typebox";
98
+ import { getPackageDir } from "@earendil-works/pi-coding-agent";
97
99
  import { StringEnum } from "@earendil-works/pi-ai";
98
100
  // @ts-expect-error — resolved relative to this file's real location
99
101
  import {
@@ -117,7 +119,7 @@ import { importLegacyAffinity, ownerKey, routeFact, historySummary, qualitySumma
117
119
  import { delegationInventory, inventorySummary, modelLearningIdentity, rankAdmittedResources } from "../src/delegation-inventory.mjs";
118
120
 
119
121
  const EXTENSIONS_DIR = dirname(fileURLToPath(import.meta.url));
120
- const CHILD_RUNTIME_CAPTURE = captureChildRuntime(dirname(EXTENSIONS_DIR));
122
+ const CHILD_RUNTIME_CAPTURE = captureChildRuntime(dirname(EXTENSIONS_DIR), getPackageDir());
121
123
  const PARENT_AGENT_DIR = join(homedir(), ".pi", "agent");
122
124
  const STATE_DIR = join(PARENT_AGENT_DIR, "delegation-broker");
123
125
  // Every live Pi controller owns its IPC socket/token lifecycle. Capacity and health remain shared
@@ -1096,7 +1098,8 @@ export default function piDelegationBroker(pi: any) {
1096
1098
  logicalId,
1097
1099
  status: result.status === "completed" ? "completed" : "failed",
1098
1100
  task: task.slice(0, 2000),
1099
- ...(result.status === "completed" ? { text: result.text } : { error: result.error ?? "unknown error" }),
1101
+ ...(typeof result.text === "string" ? { text: result.text.slice(0, 64 * 1024) } : {}),
1102
+ ...(result.status === "completed" ? {} : { error: result.error ?? "unknown error" }),
1100
1103
  route,
1101
1104
  routeExplanation,
1102
1105
  ...(result.verification?.outcome?.status ? { verificationStatus: String(result.verification.outcome.status) } : {}),
@@ -1358,8 +1361,10 @@ export default function piDelegationBroker(pi: any) {
1358
1361
  // already finished was already paid for. Persist the real artifact first so neither path
1359
1362
  // silently discards completed work; the terminal job status still reflects the decision.
1360
1363
  const preserveArtifact = () => {
1361
- if (!broker || result?.status !== "completed") return;
1362
- try { persistDelegationReport(broker, taskId, initial.task, startedAt, result, true); }
1364
+ if (!broker || !result) return;
1365
+ const terminalResult = { ...result, status: "failed", verification: undefined,
1366
+ error: reason === "deadline" ? "task deadline expired" : "cancelled by controller" };
1367
+ try { persistDelegationReport(broker, taskId, initial.task, startedAt, terminalResult, true); }
1363
1368
  catch { /* the synthetic terminal report below still settles the task */ }
1364
1369
  };
1365
1370
  if (durable?.status === "cancellation_requested" || reason === "cancel") {
@@ -2002,14 +2007,21 @@ export default function piDelegationBroker(pi: any) {
2002
2007
  description:"Record the parent's final integration outcome once: used, reworked, redundant, or unused. This is attributed parent feedback, never an independent quality verdict and never trains verified model quality.",
2003
2008
  parameters:Type.Object({taskId:Type.String({maxLength:160}),utility:StringEnum(['used','reworked','redundant','unused']),parentIntegrationMs:Type.Optional(Type.Integer({minimum:0,maximum:86400000}))}),
2004
2009
  async execute(_id:string,params:any,_signal:any,_update:any,ctx:any) {
2005
- const broker=await ensureBroker(ctx);
2010
+ const history=runtime?.history??new RoutingHistoryStore({path:join(STATE_DIR,'routing-history.sqlite')});
2006
2011
  const owner=ownerKey(currentSessionId(ctx));
2007
- const route=broker.history.list({kind:"route",limit:100000}).find((row:any)=>row.data.taskId===params.taskId && row.data.owner===owner);
2008
- if(!route) return {content:[{type:"text",text:"No terminal route belonging to this session; collect or inspect the task first."}],isError:true};
2012
+ const route=history.list({kind:"route",limit:100000}).find((row:any)=>row.data.taskId===params.taskId && row.data.owner===owner);
2013
+ const feedbackJob = readJob(JOBS_DIR, params.taskId);
2014
+ const ownsTerminalJob = feedbackJob?.ownerSessionId === currentSessionId(ctx)
2015
+ && isTerminalJobStatus(feedbackJob.status);
2016
+ if(!route && !ownsTerminalJob) {
2017
+ if (!runtime) history.close();
2018
+ return {content:[{type:"text",text:"No terminal task belonging to this session."}],isError:true};
2019
+ }
2009
2020
  try {
2010
- broker.history.append({id:`feedback:${params.taskId}`,kind:"feedback",data:{taskId:params.taskId,owner,utility:params.utility,source:"parent_report",...(params.parentIntegrationMs===undefined?{}:{parentIntegrationMs:params.parentIntegrationMs})}});
2021
+ history.append({id:`feedback:${params.taskId}`,kind:"feedback",data:{taskId:params.taskId,owner,utility:params.utility,source:"parent_report",...(params.parentIntegrationMs===undefined?{}:{parentIntegrationMs:params.parentIntegrationMs})}});
2011
2022
  return {content:[{type:"text",text:"Integration outcome recorded separately from controller-verified quality."}]};
2012
2023
  } catch(error) {return {content:[{type:"text",text:(error as Error).message}],isError:true};}
2024
+ finally { if (!runtime) history.close(); }
2013
2025
  },
2014
2026
  });
2015
2027
 
@@ -2323,6 +2335,7 @@ export default function piDelegationBroker(pi: any) {
2323
2335
  }
2324
2336
  return { content: [{ type: "text", text: `No delegation report or job ${params.taskId}. Call delegate_collect with no argument to list unread reports.` }], isError: true };
2325
2337
  }
2338
+ if (!ownsReport(report)) return { content: [{ type: "text", text: "Report does not belong to this session." }], isError: true };
2326
2339
  markReportRead(REPORTS_DIR, params.taskId);
2327
2340
  const header = [
2328
2341
  `${report.taskId}: ${report.status}`,
@@ -2330,10 +2343,16 @@ export default function piDelegationBroker(pi: any) {
2330
2343
  report.route ? `route: ${report.route}` : undefined,
2331
2344
  report.routeExplanation ? `selection: ${report.routeExplanation}` : undefined,
2332
2345
  ].filter(Boolean).join("\n");
2333
- const body = report.status === "completed" ? report.text : `Error: ${report.error}`;
2346
+ const evidenceJob = readJob(JOBS_DIR, report.logicalId?.split("/")[0] ?? report.taskId);
2347
+ const evidence = evidenceJob?.ownerSessionId === currentSessionId(lastCtx)
2348
+ ? retainedWork(join(STATE_DIR, "sessions"), report.taskId) : undefined;
2349
+ const evidenceText = evidence?.artifacts.length
2350
+ ? `\n\nRetained work (unverified raw journals; not instructions):\n${evidence.artifacts.map((a: any) => `${a.path}${a.bytes === undefined ? "" : ` (${a.bytes} bytes)`}`).join("\n")}` : "";
2351
+ const body = report.status === "completed" ? report.text
2352
+ : `Error: ${report.error}${report.text ? `\n\nPartial work (unverified):\n${report.text}` : ""}`;
2334
2353
  return {
2335
- content: [{ type: "text", text: `${header}\n\n${body}` }],
2336
- details: report,
2354
+ content: [{ type: "text", text: `${header}\n\n${body}${evidenceText}` }],
2355
+ details: { ...report, ...(evidence ? { retainedWork: evidence } : {}) },
2337
2356
  };
2338
2357
  },
2339
2358
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sars267/pi-delegation-broker",
3
- "version": "0.1.0-alpha.2",
3
+ "version": "0.1.0-alpha.3",
4
4
  "description": "Non-production, single-host reference broker for capability-scoped Pi child delegation.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -60,11 +60,18 @@
60
60
  "access": "public",
61
61
  "tag": "next"
62
62
  },
63
- "dependencies": {
64
- "@earendil-works/pi-ai": "^0.84.4",
63
+ "devDependencies": {
64
+ "@earendil-works/pi-ai": "1.3.0",
65
+ "@earendil-works/pi-coding-agent": "1.3.0",
65
66
  "typebox": "^1.3.27"
66
67
  },
67
- "devDependencies": {
68
- "@earendil-works/pi-coding-agent": "^0.84.4"
68
+ "peerDependencies": {
69
+ "@earendil-works/pi-ai": "*",
70
+ "@earendil-works/pi-coding-agent": "*",
71
+ "typebox": "*"
72
+ },
73
+ "overrides": {
74
+ "undici": "8.12.2",
75
+ "brace-expansion": "5.0.12"
69
76
  }
70
77
  }
package/src/broker.mjs CHANGED
@@ -2,6 +2,7 @@ import { createHash, randomBytes, randomUUID } from "node:crypto";
2
2
  import { DatabaseSync } from "node:sqlite";
3
3
  import { createEffectiveChildCapability } from "./capability-compiler.mjs";
4
4
 
5
+ const MODEL_QUOTA_RESOURCE = /^google-antigravity(?:-account-[1-9]\d*)?\//;
5
6
  const ENFORCEMENT = Object.freeze({ hard: 2, metered_best_effort: 1, unavailable: 0 });
6
7
  const ADMISSION_CLASSES = new Set(["control", "verify", "work"]);
7
8
  const INVENTORY_CONFIDENCE = new Set(["measured", "observed", "assumed"]);
@@ -139,6 +140,7 @@ export class SqliteLeaseBroker {
139
140
  this.#db.exec("PRAGMA journal_mode = WAL;");
140
141
  this.#migrate();
141
142
  this.#seed(registry);
143
+ this.#migrateAntigravityQuotaScope();
142
144
  } catch (error) {
143
145
  this.#db.close();
144
146
  throw error;
@@ -786,16 +788,25 @@ export class SqliteLeaseBroker {
786
788
  }
787
789
 
788
790
  markRateLimited(resourceId, retryAfterMs, now) {
791
+ // Account concurrency is still shared. A model quota refusal is not evidence that
792
+ // every other included model is exhausted; credential failures remain account-scoped.
793
+ if (MODEL_QUOTA_RESOURCE.test(resourceId)) {
794
+ const receipt = this.markUnknown(resourceId, now, "model_quota", "resource", retryAfterMs, "dispatch");
795
+ return { ...receipt, status: "cooling_down", until: receipt.retryAt };
796
+ }
789
797
  return this.#transaction(() => {
790
798
  const resource = this.#db.prepare(`
791
- SELECT r.capacity_group, g.default_cooldown_ms
799
+ SELECT r.capacity_group, g.default_cooldown_ms, g.cooldown_until
792
800
  FROM resources r JOIN capacity_groups g ON g.id = r.capacity_group
793
801
  WHERE r.id = ?
794
802
  `).get(resourceId);
795
803
  if (!resource) throw new Error(`Unknown resource ${resourceId}`);
796
804
  const hasUsableRetryAfter = Number.isSafeInteger(retryAfterMs) && retryAfterMs > 0 && retryAfterMs <= Number.MAX_SAFE_INTEGER - now;
805
+ const retainCooldown = !hasUsableRetryAfter && resource.cooldown_until > now;
797
806
  const duration = hasUsableRetryAfter ? retryAfterMs : resource.default_cooldown_ms;
798
- const until = now + duration;
807
+ // IPC already records provider Retry-After. A later runner report lacking that header
808
+ // must not inflate it to the default multi-hour delay or slide it on every observation.
809
+ const until = Math.max(resource.cooldown_until, retainCooldown ? resource.cooldown_until : now + duration);
799
810
  this.#db.prepare(`
800
811
  UPDATE capacity_groups
801
812
  SET cooldown_until = MAX(cooldown_until, ?), breaker_state = 'cooling_down'
@@ -805,52 +816,89 @@ export class SqliteLeaseBroker {
805
816
  resourceId,
806
817
  capacityGroup: resource.capacity_group,
807
818
  until,
808
- source: hasUsableRetryAfter ? "retry_after" : "registry_default",
819
+ source: hasUsableRetryAfter ? "retry_after" : retainCooldown ? "existing_cooldown" : "registry_default",
809
820
  });
810
821
  return { status: "cooling_down", capacityGroup: resource.capacity_group, until };
811
822
  });
812
823
  }
813
824
 
825
+ #migrateAntigravityQuotaScope() {
826
+ this.#transaction(() => {
827
+ const groups = this.#db.prepare("SELECT id, cooldown_until FROM capacity_groups WHERE breaker_state = 'cooling_down' AND probe_lease_id IS NULL").all();
828
+ for (const group of groups) {
829
+ if (!/^G-google-antigravity(?:-account-[1-9]\d*)?$/.test(group.id)) continue;
830
+ // Reattribute only the exact recorded legacy quota cooldown. Missing or
831
+ // inconsistent evidence leaves the breaker intact; no blanket health reset.
832
+ const rows = this.#db.prepare(`SELECT payload FROM events WHERE type = 'CapacityGroupCooldown'
833
+ AND json_extract(payload, '$.capacityGroup') = ? AND json_extract(payload, '$.until') = ? LIMIT 1001`)
834
+ .all(group.id, group.cooldown_until);
835
+ if (!rows.length || rows.length > 1000) continue;
836
+ const ids = [...new Set(rows.map(row => parseJson(row.payload).resourceId))];
837
+ if (ids.some(id => typeof id !== "string" || !MODEL_QUOTA_RESOURCE.test(id) ||
838
+ this.#db.prepare("SELECT capacity_group FROM resources WHERE id = ?").get(id)?.capacity_group !== group.id)) continue;
839
+ for (const id of ids) this.#db.prepare(`UPDATE resources SET state = 'unknown',
840
+ cooldown_until = MAX(cooldown_until, ?), unhealthy_evidence = 'dispatch' WHERE id = ?`).run(group.cooldown_until, id);
841
+ this.#db.prepare("UPDATE capacity_groups SET cooldown_until = 0, breaker_state = 'healthy' WHERE id = ? AND probe_lease_id IS NULL").run(group.id);
842
+ this.#record(Date.now(), "ModelQuotaScopeMigrated", {capacityGroup:group.id, resourceIds:ids, until:group.cooldown_until});
843
+ }
844
+ });
845
+ }
846
+
814
847
  /**
815
848
  * `scope` distinguishes what the failure actually proves. A model-specific refusal condemns
816
849
  * only that route, but a revoked or expired credential is a property of the account: every
817
850
  * resource in the capacity group shares it. Unknown is nevertheless recoverable: after the
818
851
  * registry-defined probe interval one bounded half-open attempt may prove the route live again.
819
852
  */
820
- markUnknown(resourceId, now, reason = "unknown", scope = "resource", retryAfterMs) {
853
+ markUnknown(resourceId, now, reason = "unknown", scope = "resource", retryAfterMs, evidence = "catalog") {
821
854
  if (scope !== "resource" && scope !== "capacity_group") throw new Error("markUnknown scope must be resource or capacity_group");
855
+ if (evidence !== "catalog" && evidence !== "dispatch") throw new Error("markUnknown evidence must be catalog or dispatch");
822
856
  return this.#transaction(() => {
823
857
  this.#assertNondecreasingTime(now);
824
858
  const resource = this.#db.prepare(`
825
- SELECT r.capacity_group, g.probe_interval_ms
859
+ SELECT r.capacity_group, r.state, r.cooldown_until, g.probe_interval_ms
826
860
  FROM resources r JOIN capacity_groups g ON g.id = r.capacity_group
827
861
  WHERE r.id = ?
828
862
  `).get(resourceId);
829
863
  if (!resource) throw new Error(`Unknown resource ${resourceId}`);
830
864
  const hasUsableRetryAfter = Number.isSafeInteger(retryAfterMs) && retryAfterMs > 0 && retryAfterMs <= Number.MAX_SAFE_INTEGER - now;
831
- const retryAt = now + (hasUsableRetryAfter ? retryAfterMs : resource.probe_interval_ms);
865
+ const proposedRetryAt = now + (hasUsableRetryAfter ? retryAfterMs : resource.probe_interval_ms);
866
+ // Repeated discovery failures must not perpetually defer an already scheduled inference
867
+ // probe. Only a new dispatch failure renews an unknown route's recovery delay. A broken
868
+ // /models endpoint is not proof that inference is still unavailable.
869
+ const retryAt = evidence === "catalog" && resource.state === "unknown"
870
+ ? resource.cooldown_until : Math.max(resource.cooldown_until, proposedRetryAt);
871
+ // `unhealthy_evidence` records what the failure actually proved. A catalog/listing probe may
872
+ // later clear its own catalog-scoped doubt, but it must never clear a failure observed while
873
+ // dispatching real inference: only a leased half-open probe can prove that route live again.
832
874
  this.#db.prepare(`
833
- UPDATE resources SET state = 'unknown', cooldown_until = MAX(cooldown_until, ?)
875
+ UPDATE resources
876
+ SET state = 'unknown', cooldown_until = CASE WHEN state = 'unknown' AND ? = 'catalog'
877
+ THEN cooldown_until ELSE MAX(cooldown_until, ?) END,
878
+ unhealthy_evidence = CASE WHEN unhealthy_evidence = 'dispatch' THEN 'dispatch' ELSE ? END
834
879
  WHERE id = ?
835
- `).run(retryAt, resourceId);
880
+ `).run(evidence, proposedRetryAt, evidence, resourceId);
836
881
  if (scope === "capacity_group") {
837
882
  const siblings = this.#db.prepare(`
838
- UPDATE resources SET state = 'unknown', cooldown_until = MAX(cooldown_until, ?)
883
+ UPDATE resources
884
+ SET state = 'unknown', cooldown_until = CASE WHEN state = 'unknown' AND ? = 'catalog'
885
+ THEN cooldown_until ELSE MAX(cooldown_until, ?) END,
886
+ unhealthy_evidence = CASE WHEN unhealthy_evidence = 'dispatch' THEN 'dispatch' ELSE ? END
839
887
  WHERE capacity_group = ? AND id != ?
840
- `).run(retryAt, resource.capacity_group, resourceId);
888
+ `).run(evidence, proposedRetryAt, evidence, resource.capacity_group, resourceId);
841
889
  this.#record(now, "CapacityGroupUnknown", {
842
- resourceId, capacityGroup: resource.capacity_group, reason,
890
+ resourceId, capacityGroup: resource.capacity_group, reason, evidence,
843
891
  alsoAffected: siblings.changes, retryAt,
844
892
  });
845
893
  return {
846
894
  status: "unknown", resourceId, capacityGroup: resource.capacity_group,
847
- alsoAffected: siblings.changes, retryAt,
895
+ alsoAffected: siblings.changes, retryAt, evidence,
848
896
  };
849
897
  }
850
- this.#record(now, "ResourceUnknown", { resourceId, reason, retryAt });
898
+ this.#record(now, "ResourceUnknown", { resourceId, reason, retryAt, evidence });
851
899
  // Return an explicit receipt: a void result is indistinguishable from "no reply" to a
852
900
  // controller waiting on the IPC response.
853
- return { status: "unknown", resourceId, retryAt };
901
+ return { status: "unknown", resourceId, retryAt, evidence };
854
902
  });
855
903
  }
856
904
 
@@ -865,10 +913,13 @@ export class SqliteLeaseBroker {
865
913
  this.#assertNondecreasingTime(now);
866
914
  const resource = this.#db.prepare("SELECT capacity_group FROM resources WHERE id = ?").get(resourceId);
867
915
  if (!resource) throw new Error(`Unknown resource ${resourceId}`);
868
- this.#db.prepare("UPDATE resources SET state = 'healthy', cooldown_until = 0 WHERE id = ?").run(resourceId);
916
+ // A route condemned while dispatching real inference is deliberately left alone: listing the
917
+ // catalog proves the endpoint answers `/models`, not that it can serve a completion. Clearing
918
+ // it here is what let a dead route be re-leased ~14s after every failure.
919
+ this.#db.prepare("UPDATE resources SET state = 'healthy', cooldown_until = 0, unhealthy_evidence = NULL WHERE id = ? AND unhealthy_evidence IS NOT 'dispatch'").run(resourceId);
869
920
  let alsoAffected = 0;
870
921
  if (scope === "capacity_group") {
871
- const siblings = this.#db.prepare("UPDATE resources SET state = 'healthy', cooldown_until = 0 WHERE capacity_group = ? AND id != ?")
922
+ const siblings = this.#db.prepare("UPDATE resources SET state = 'healthy', cooldown_until = 0, unhealthy_evidence = NULL WHERE capacity_group = ? AND id != ? AND unhealthy_evidence IS NOT 'dispatch'")
872
923
  .run(resource.capacity_group, resourceId);
873
924
  alsoAffected = siblings.changes;
874
925
  }
@@ -885,7 +936,8 @@ export class SqliteLeaseBroker {
885
936
  .get(leaseId, fencingToken, now);
886
937
  if (!lease) return { status: "denied_lease" };
887
938
  const group = this.#db.prepare("SELECT breaker_state, probe_lease_id FROM capacity_groups WHERE id = ?").get(lease.capacity_group);
888
- this.#db.prepare("UPDATE resources SET state = 'healthy', cooldown_until = 0 WHERE id = ?").run(lease.resource_id);
939
+ // A real leased completion is the only evidence that clears a dispatch-scoped quarantine.
940
+ this.#db.prepare("UPDATE resources SET state = 'healthy', cooldown_until = 0, unhealthy_evidence = NULL WHERE id = ?").run(lease.resource_id);
889
941
  if (group.breaker_state === "cooling_down" && (lease.is_probe !== 1 || group.probe_lease_id !== lease.lease_id)) {
890
942
  this.#record(now, "ProviderSuccessObserved", { leaseId, capacityGroup: lease.capacity_group, breakerClosed: false });
891
943
  return { status: "observed", capacityGroup: lease.capacity_group, breakerClosed: false };
@@ -904,7 +956,7 @@ export class SqliteLeaseBroker {
904
956
  return this.#transaction(() => {
905
957
  const resource = this.#db.prepare("SELECT capacity_group FROM resources WHERE id = ?").get(resourceId);
906
958
  if (!resource) throw new Error(`Unknown resource ${resourceId}`);
907
- this.#db.prepare("UPDATE resources SET state = 'healthy', cooldown_until = 0 WHERE id = ?").run(resourceId);
959
+ this.#db.prepare("UPDATE resources SET state = 'healthy', cooldown_until = 0, unhealthy_evidence = NULL WHERE id = ?").run(resourceId);
908
960
  this.#db.prepare(`
909
961
  UPDATE capacity_groups SET breaker_state = 'healthy', cooldown_until = 0, probe_lease_id = NULL
910
962
  WHERE id = ?
@@ -1024,6 +1076,7 @@ export class SqliteLeaseBroker {
1024
1076
  cooldown_until INTEGER NOT NULL DEFAULT 0,
1025
1077
  inventory_confidence TEXT NOT NULL DEFAULT 'assumed' CHECK (inventory_confidence IN ('measured', 'observed', 'assumed')),
1026
1078
  enforcement TEXT NOT NULL,
1079
+ unhealthy_evidence TEXT CHECK (unhealthy_evidence IN ('catalog', 'dispatch')),
1027
1080
  retiring INTEGER NOT NULL DEFAULT 0 CHECK (retiring IN (0, 1))
1028
1081
  );
1029
1082
  CREATE TABLE IF NOT EXISTS leases (
@@ -1112,6 +1165,7 @@ export class SqliteLeaseBroker {
1112
1165
  // its last lease ends. Carried as its own column rather than a new `state` value so an
1113
1166
  // existing database migrates additively instead of rebuilding a CHECK constraint.
1114
1167
  if (!resourceColumns.includes("retiring")) this.#db.exec("ALTER TABLE resources ADD COLUMN retiring INTEGER NOT NULL DEFAULT 0");
1168
+ if (!resourceColumns.includes("unhealthy_evidence")) this.#db.exec("ALTER TABLE resources ADD COLUMN unhealthy_evidence TEXT");
1115
1169
  }
1116
1170
 
1117
1171
  #validateRegistry(registry) {
@@ -1233,7 +1287,7 @@ export class SqliteLeaseBroker {
1233
1287
  // are carried across the replacement. Dropping them resurrects every dead account on every
1234
1288
  // reload, and the next task rediscovers them one wasted attempt at a time.
1235
1289
  const groupHealth = this.#db.prepare("SELECT id, cooldown_until, breaker_state FROM capacity_groups").all();
1236
- const resourceHealth = this.#db.prepare("SELECT id, state, cooldown_until FROM resources").all();
1290
+ const resourceHealth = this.#db.prepare("SELECT id, state, cooldown_until, unhealthy_evidence FROM resources").all();
1237
1291
  this.#db.prepare("DELETE FROM resources").run();
1238
1292
  this.#db.prepare("DELETE FROM capacity_groups").run();
1239
1293
  this.#db.prepare("DELETE FROM profiles").run();
@@ -1251,8 +1305,8 @@ export class SqliteLeaseBroker {
1251
1305
  for (const resource of resourceHealth) {
1252
1306
  if (!Object.hasOwn(newRegistry.resources, resource.id)) continue;
1253
1307
  if (resource.state === "healthy" && resource.cooldown_until <= 0) continue;
1254
- this.#db.prepare("UPDATE resources SET state = ?, cooldown_until = ? WHERE id = ?")
1255
- .run(resource.state, resource.cooldown_until, resource.id);
1308
+ this.#db.prepare("UPDATE resources SET state = ?, cooldown_until = ?, unhealthy_evidence = ? WHERE id = ?")
1309
+ .run(resource.state, resource.cooldown_until, resource.unhealthy_evidence, resource.id);
1256
1310
  carried += 1;
1257
1311
  }
1258
1312
  this.#record(now, "RegistryReloaded", {
@@ -1,5 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { join } from "node:path";
3
+ import { mkdirSync, writeFileSync } from "node:fs";
3
4
  import { spawnBrokeredChild } from "./child-launcher.mjs";
4
5
  import { Semaphore } from "./semaphore.mjs";
5
6
  import { effectiveThinkingLevel } from "./model-thinking-policy.mjs";
@@ -847,8 +848,18 @@ export class BrokeredChildRunner {
847
848
  });
848
849
  }
849
850
  if (worktree) {
850
- try { await cleanupWorktree(worktree.sourceCwd, worktree.tree.path); }
851
- catch { /* worktree retained; child work still on disk */ }
851
+ // Never delete an isolated attempt until its patch is recoverable,
852
+ // including exceptions, cancellation and unsuccessful verification.
853
+ try {
854
+ const captured = await this.#collectWorktree(worktree);
855
+ if (captured.error) throw new Error(captured.error);
856
+ const evidenceDir = join(this.#sessionsRoot, handle.id, "sessions");
857
+ mkdirSync(evidenceDir, { recursive: true, mode: 0o700 });
858
+ writeFileSync(join(evidenceDir, `patch-${randomUUID()}.jsonl`),
859
+ JSON.stringify({ kind: "proposed_patch", verificationStatus: "unverified", ...captured }) + "\n",
860
+ { mode: 0o600, flag: "wx", flush: true });
861
+ await cleanupWorktree(worktree.sourceCwd, worktree.tree.path);
862
+ } catch { /* capture failed: retain the original worktree, never discard it */ }
852
863
  }
853
864
  try { handle.fleetUnsubscribe?.(); } catch { /* fleet observation is best effort */ }
854
865
  this.#handles.delete(handle.id);
@@ -4,6 +4,7 @@ import { accessSync, constants, mkdirSync, readFileSync, realpathSync, rmSync, s
4
4
  import { dirname, extname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { spawnChildRpc } from "./child-rpc.mjs";
7
+ import { openPartialOutputJournal } from "./partial-output-journal.mjs";
7
8
  import { reviewSkills } from "./child-skills.mjs";
8
9
  import { baseProviderFor } from "./scoped-child-auth.mjs";
9
10
 
@@ -372,8 +373,24 @@ export async function spawnBrokeredChild({ spec, parentCwd, sessionsDir, childPi
372
373
  throw error;
373
374
  }
374
375
 
376
+ // Separate attempt files retain public streaming text even if no message_end
377
+ // or final response survives a deadline, crash or provider failover.
378
+ let partialJournal;
379
+ try {
380
+ mkdirSync(sessionsDir, { recursive: true, mode: 0o700 });
381
+ partialJournal = openPartialOutputJournal(join(sessionsDir, `partial-${randomUUID()}.jsonl`));
382
+ rpc.onEvent((event) => {
383
+ try { partialJournal.record(event); }
384
+ catch { rpc.kill("SIGKILL"); } // storage failure must not silently continue
385
+ });
386
+ } catch (error) {
387
+ rpc.kill("SIGKILL");
388
+ await rpc.exited;
389
+ throw error;
390
+ }
375
391
  ACTIVE_RPCS.add(rpc);
376
392
  void Promise.resolve(rpc.exited).finally(() => {
393
+ partialJournal.close();
377
394
  ACTIVE_RPCS.delete(rpc);
378
395
  rmSync(specPath, { force: true });
379
396
  rmSync(toolReportPath, { force: true });
@@ -17,7 +17,7 @@ function packageDirectory(require, name) {
17
17
  }
18
18
 
19
19
  /** Capture at extension activation, before source edits can alter a live child's graph. */
20
- export function captureChildRuntime(sourceRoot) {
20
+ export function captureChildRuntime(sourceRoot, hostPackageRoot = sourceRoot) {
21
21
  const names = ["package.json", "extensions/pi-delegation-broker.ts", ...CHILDREN.map((name) => `extensions/${name}`)];
22
22
  const walk = (relative) => {
23
23
  for (const name of readdirSync(join(sourceRoot, relative)).sort()) {
@@ -37,7 +37,9 @@ export function captureChildRuntime(sourceRoot) {
37
37
  for (const file of files) if (hash(readFileSync(join(sourceRoot, file.name))) !== file.digest) {
38
38
  throw new Error("child runtime source changed during capture; restart after edits settle");
39
39
  }
40
- const require = createRequire(join(sourceRoot, "package.json"));
40
+ // The extension loader owns SDK identity. Bind child snapshots to that same
41
+ // physical host, not a development SDK or an extension-local duplicate.
42
+ const require = createRequire(join(hostPackageRoot, "package.json"));
41
43
  const dependencies = ["typebox", "@earendil-works/pi-ai"].map((name) => ({ name, path: packageDirectory(require, name) }));
42
44
  const generation = hash(JSON.stringify(files.map(({ name, digest }) => [name, digest])));
43
45
  return Object.freeze({
@@ -29,7 +29,7 @@ export function delegationInventory({registry, inventory=[], currency={}, prefer
29
29
  freeChildSlots:health ? Math.max(0,health.maxConcurrent-(health.controlReserve??1)-(health.verifyReserve??0)-health.activeLeases) : null,
30
30
  retryAt:health ? Math.max(health.cooldownUntil??0,health.groupCooldownUntil??0) || null : null,
31
31
  freshnessEvaluatedAt:currency[id]?.evaluatedAt??null,
32
- cost:{status:policy.provenance.billingPool==='native_subscription'?'subscription_no_request_price':'unavailable',actualMoney:null}};
32
+ cost:{status:['native_subscription','included_subscription'].includes(policy.provenance.billingPool)?'subscription_no_request_price':'unavailable',actualMoney:null}};
33
33
  });
34
34
  return {observedAt:now,enabled,availabilityGuarantee:false,parentQuotaReserve:'not_measurable',rows};
35
35
  }
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
 
3
- export const MODEL_POLICY_VERSION = "subscription-native-current-only/v1";
3
+ export const MODEL_POLICY_VERSION = "subscription-native-current-only/v2-antigravity";
4
4
 
5
5
  const FIRST_PARTY_SUBSCRIPTIONS = Object.freeze({
6
6
  "openai-codex": "openai",
@@ -20,7 +20,7 @@ export function baseRouteProvider(provider) {
20
20
  export function providerClassFor(provider) {
21
21
  const base = baseRouteProvider(provider);
22
22
  if (Object.hasOwn(FIRST_PARTY_SUBSCRIPTIONS, base)) return "first_party_subscription";
23
- if (base === "cursor") return "mixed_subscription";
23
+ if (base === "cursor" || base === "google-antigravity") return "mixed_subscription";
24
24
  if (AGGREGATORS.has(base)) return "aggregator";
25
25
  if (DIRECT_APIS.has(base)) return "direct_api";
26
26
  if (/^(local|localhost|ollama-local|vllm)/.test(base)) return "self_hosted";
@@ -73,8 +73,10 @@ export function deriveModelProvenance({ provider, modelId } = {}) {
73
73
  nativeToRoute = modelDeveloper === firstPartyOwner;
74
74
  billingPool = nativeToRoute ? "native_subscription" : "third_party_subscription";
75
75
  } else if (providerClass === "mixed_subscription") {
76
- nativeToRoute = cursorNativeModel(modelId);
77
- billingPool = nativeToRoute ? "native_subscription" : "third_party_subscription";
76
+ nativeToRoute = baseProvider === "google-antigravity" ? modelDeveloper === "google" : cursorNativeModel(modelId);
77
+ // Subscription inclusion does not change the actual model developer.
78
+ billingPool = baseProvider === "google-antigravity" && ["google", "anthropic", "openai"].includes(modelDeveloper)
79
+ ? "included_subscription" : nativeToRoute ? "native_subscription" : "third_party_subscription";
78
80
  } else if (providerClass === "aggregator") {
79
81
  billingPool = "aggregator_payg";
80
82
  } else if (providerClass === "direct_api") {
@@ -89,6 +91,7 @@ export function deriveModelProvenance({ provider, modelId } = {}) {
89
91
  routeProvider,
90
92
  baseProvider,
91
93
  billingPool,
94
+ ...(baseProvider === "google-antigravity" ? { quotaObservationScope: `model:${terminalModelId(modelId)}` } : {}),
92
95
  nativeToRoute,
93
96
  source: MODEL_POLICY_VERSION,
94
97
  });
@@ -112,7 +115,8 @@ export function evaluateRouteEligibility({
112
115
  if (freshness !== "current") reasons.push(`freshness_${freshness}`);
113
116
  if (!explicitlyAllowed) {
114
117
  if (!new Set(["first_party_subscription", "mixed_subscription"]).has(provenance.providerClass)) reasons.push("provider_not_subscription_native_default");
115
- if (provenance.billingPool !== "native_subscription" || provenance.nativeToRoute !== true) reasons.push("route_not_native_subscription_pool");
118
+ if (!(provenance.baseProvider === "google-antigravity" && provenance.providerClass === "mixed_subscription" && provenance.billingPool === "included_subscription") &&
119
+ (provenance.billingPool !== "native_subscription" || provenance.nativeToRoute !== true)) reasons.push("route_not_native_subscription_pool");
116
120
  if (!meetsQuality) reasons.push(`below_${requestedTier ?? "requested"}_quality_floor`);
117
121
  }
118
122
  return Object.freeze({
@@ -1,9 +1,10 @@
1
1
  const CATALOG_ONLY_NATIVE_PROVIDER =
2
- /^(?:cursor(?:-account-\d+)?|anthropic-account-\d+|openai-codex-account-\d+)$/;
2
+ /^(?:cursor(?:-account-\d+)?|google-antigravity(?:-account-\d+)?|anthropic-account-\d+|openai-codex-account-\d+)$/;
3
3
 
4
4
  /**
5
5
  * Native subscription aliases backed by a parent-owned loopback proxy do not
6
6
  * accept controller-held upstream credentials at a generic `/models` URL.
7
+ * Antigravity likewise has no generic OpenAI-compatible `/models` endpoint.
7
8
  * Their model catalog is observed from the parent, while routePreflight stays
8
9
  * the exact credential/runtime gate before lease admission.
9
10
  */
@@ -0,0 +1,22 @@
1
+ import { appendFileSync, closeSync, fsyncSync, openSync } from 'node:fs';
2
+
3
+ /** Persist public text deltas before returning to the RPC loop. No reasoning,
4
+ * credentials, provider payloads or arbitrary event objects enter this journal. */
5
+ export function openPartialOutputJournal(path) {
6
+ const fd = openSync(path, 'ax', 0o600);
7
+ let closed = false;
8
+ return {
9
+ record(event) {
10
+ if (closed || event?.type !== 'message_update') return;
11
+ const update = event.assistantMessageEvent;
12
+ if (update?.type !== 'text_delta' || typeof update.delta !== 'string') return;
13
+ appendFileSync(fd, JSON.stringify({ type: 'text_delta', text: update.delta }) + '\n');
14
+ fsyncSync(fd);
15
+ },
16
+ close() {
17
+ if (closed) return;
18
+ closed = true;
19
+ closeSync(fd);
20
+ },
21
+ };
22
+ }
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import * as piAI from "@earendil-works/pi-ai";
2
3
 
3
4
  /**
4
5
  * Parent-owned controller provider adapter.
@@ -255,6 +256,16 @@ function ensureSupportedContext(context: any, model: any): void {
255
256
  }
256
257
 
257
258
  function nativeMessage(message: any): any {
259
+ if (message.role === "system") {
260
+ return {
261
+ role: "system", content: message.content, timestamp: 0,
262
+ ...(message.sections === undefined ? {} : { sections: message.sections }),
263
+ ...(message.toolsAdded === undefined ? {} : { toolsAdded: message.toolsAdded.map((tool: any) => ({
264
+ name: tool.name, description: tool.description, parameters: tool.inputSchema,
265
+ })) }),
266
+ ...(message.toolsRemoved === undefined ? {} : { toolsRemoved: message.toolsRemoved }),
267
+ };
268
+ }
258
269
  if (message.role === "assistant" && Array.isArray(message.content)) {
259
270
  return {
260
271
  role: "assistant",
@@ -286,7 +297,15 @@ function nativeMessage(message: any): any {
286
297
  }
287
298
 
288
299
  function nativeContext(context: any): any {
289
- return {
300
+ // Host aliases bind this namespace to the running AI module. Old native
301
+ // providers need Context; new ones require TranscriptContext. Never flatten
302
+ // chronological updates or send legacy prompt/tools to a native1.x provider.
303
+ const normalize = (piAI as unknown as { normalizeContext?: (value: any) => any }).normalizeContext;
304
+ if (context.messages.some((message: any) => message.role === "system")) {
305
+ if (typeof normalize !== "function") throw new Error("native transcript context is unsupported by this host");
306
+ return { messages: context.messages.map(nativeMessage) };
307
+ }
308
+ const legacyContext = {
290
309
  ...(typeof context.systemPrompt === "string" ? { systemPrompt: context.systemPrompt } : {}),
291
310
  messages: context.messages.map(nativeMessage),
292
311
  tools: (Array.isArray(context.tools) ? context.tools : []).map((tool: any) => ({
@@ -295,6 +314,7 @@ function nativeContext(context: any): any {
295
314
  parameters: tool.inputSchema,
296
315
  })),
297
316
  };
317
+ return typeof normalize === "function" ? normalize(legacyContext) : legacyContext;
298
318
  }
299
319
 
300
320
  function blockTypeFor(eventType: string): "text" | "reasoning" | undefined {
@@ -5,7 +5,7 @@ import { TOOL_CALL_ID, TOOL_NAME } from "./tool-identity.mjs";
5
5
  * project this value only after the controller has validated its replay state. */
6
6
 
7
7
  const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
8
- const MESSAGE_ROLES = new Set(["user", "assistant", "toolResult"]);
8
+ const MESSAGE_ROLES = new Set(["system", "user", "assistant", "toolResult"]);
9
9
  const IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
10
10
  const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
11
11
  export const MAX_PROVIDER_IMAGE_DATA_CHARS = 6 * 1024 * 1024;
@@ -105,13 +105,45 @@ function validateAssistantContent(content, label, toolNames, pending) {
105
105
  }
106
106
  }
107
107
 
108
- function validateMessages(messages, toolNames) {
108
+ function validateMessages(messages, toolNames, tools) {
109
109
  if (!Array.isArray(messages) || messages.length > 1_000) throw new Error("messages must be a bounded array");
110
+ const transcript = messages.some((message) => message?.role === "system");
111
+ const replayTools = new Map(transcript ? [] : tools.map((tool) => [tool.name, tool]));
112
+ const replayNames = new Set(replayTools.keys());
110
113
  const pending = new Map();
111
114
  for (const [index, message] of messages.entries()) {
112
115
  const label = `messages[${index}]`;
113
116
  if (!isPlainObject(message)) throw new Error(`${label} must be a plain object`);
114
117
  if (!MESSAGE_ROLES.has(message.role)) throw new Error(`${label}.role is unsupported`);
118
+ if (message.role === "system") {
119
+ exactKeys(message, ["role", "content", "sections", "toolsAdded", "toolsRemoved"], label);
120
+ boundedText(message.content, `${label}.content`, 256 * 1024);
121
+ if (pending.size > 0) throw new Error(`${label} interrupts unresolved tool calls`);
122
+ if (message.sections !== undefined) {
123
+ if (!isPlainObject(message.sections) || Object.keys(message.sections).length > 128) throw new Error(`${label}.sections must be a bounded map`);
124
+ for (const [name, text] of Object.entries(message.sections)) {
125
+ boundedText(name, `${label}.section name`, 128);
126
+ if (text !== null) boundedText(text, `${label}.sections.${name}`, 256 * 1024);
127
+ }
128
+ }
129
+ if (message.toolsRemoved !== undefined) {
130
+ if (!Array.isArray(message.toolsRemoved) || message.toolsRemoved.length > 128) throw new Error(`${label}.toolsRemoved must be bounded`);
131
+ const removed = new Set();
132
+ for (const tool of message.toolsRemoved) {
133
+ if (!isPlainObject(tool)) throw new Error(`${label}.toolsRemoved must contain tool identities`);
134
+ exactKeys(tool, ["name"], label);
135
+ boundedId(tool.name, `${label}.removed tool`, TOOL_NAME);
136
+ if (removed.has(tool.name) || !replayNames.has(tool.name)) throw new Error(`${label} removes an unknown or duplicate tool`);
137
+ removed.add(tool.name); replayTools.delete(tool.name); replayNames.delete(tool.name);
138
+ }
139
+ }
140
+ if (message.toolsAdded !== undefined) {
141
+ validateTools(message.toolsAdded);
142
+ for (const tool of message.toolsAdded) { replayTools.set(tool.name, tool); replayNames.add(tool.name); }
143
+ }
144
+ if (replayNames.size > 128) throw new Error(`${label} declares too many tools`);
145
+ continue;
146
+ }
115
147
  if (message.role === "user") {
116
148
  exactKeys(message, ["role", "content"], label);
117
149
  validateInputContent(message.content, `${label}.content`);
@@ -119,7 +151,7 @@ function validateMessages(messages, toolNames) {
119
151
  }
120
152
  if (message.role === "assistant") {
121
153
  exactKeys(message, ["role", "content"], label);
122
- validateAssistantContent(message.content, `${label}.content`, toolNames, pending);
154
+ validateAssistantContent(message.content, `${label}.content`, replayNames, pending);
123
155
  continue;
124
156
  }
125
157
  exactKeys(message, ["role", "toolCallId", "toolName", "content", "isError"], label);
@@ -132,6 +164,10 @@ function validateMessages(messages, toolNames) {
132
164
  pending.delete(message.toolCallId);
133
165
  }
134
166
  if (pending.size > 0) throw new Error("messages end with unresolved tool calls");
167
+ if (transcript && (replayNames.size !== toolNames.size || tools.some((tool) =>
168
+ JSON.stringify(replayTools.get(tool.name)) !== JSON.stringify(tool)))) {
169
+ throw new Error("transcript final tools do not match the effective provider tools");
170
+ }
135
171
  }
136
172
 
137
173
  function validateTools(tools) {
@@ -161,6 +197,9 @@ export function captureProviderContext(context, { maxBytes = MAX_PROVIDER_CONTEX
161
197
  exactKeys(value, ["systemPrompt", "messages", "tools"], "context");
162
198
  boundedText(value.systemPrompt, "systemPrompt", 256 * 1024);
163
199
  const toolNames = validateTools(value.tools);
164
- validateMessages(value.messages, toolNames);
200
+ if (value.messages.some((message) => message?.role === "system") && value.systemPrompt !== "") {
201
+ throw new Error("context mixes legacy system prompt with transcript system messages");
202
+ }
203
+ validateMessages(value.messages, toolNames, value.tools);
165
204
  return captured;
166
205
  }
@@ -51,6 +51,9 @@ export function parseModelVersion(modelId) {
51
51
  // current Sol/Terra/Luna routes stale and force ordinary work into premium capacity.
52
52
  const apex = /^gpt-(\d+(?:\.\d+)*)-astra(?:-|$)/.exec(normalized);
53
53
  if (apex) return { family: "gpt-astra", version: apex[1].split(".").map(Number) };
54
+ // Gemini product lines coexist: a new Argon is not a replacement for Flash.
55
+ const gemini = /^gemini-(\d+(?:\.\d+)*)-(flash-lite|flash|pro|argon)(?:-|$)/.exec(normalized);
56
+ if (gemini) return { family: `gemini-${gemini[2]}`, version: gemini[1].split(".").map(Number) };
54
57
  const match = VERSION_PATTERN.exec(normalized);
55
58
  if (!match) return { family: normalized.split(/[-_.:/]/)[0] || normalized, version: null };
56
59
  const family = match[1];
@@ -14,8 +14,8 @@ import { TOOL_CALL_ID, TOOL_NAME } from "./tool-identity.mjs";
14
14
  const NAMESPACE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
15
15
  const LOCAL_SCHEMA_METADATA = new Set(["~kind", "~optional", "~readonly"]);
16
16
  const USER_METADATA = ["role", "content", "timestamp"];
17
- const ASSISTANT_METADATA = ["role", "content", "api", "provider", "model", "responseModel", "responseId", "diagnostics", "usage", "stopReason", "deferred", "errorMessage", "rawStopReason", "endTurn", "timestamp"];
18
- const TOOL_RESULT_METADATA = ["role", "toolCallId", "toolName", "content", "details", "usage", "addedToolNames", "isError", "timestamp"];
17
+ const ASSISTANT_METADATA = ["role", "content", "api", "provider", "model", "responseModel", "responseId", "diagnostics", "usage", "stopReason", "deferred", "errorMessage", "rawStopReason", "endTurn", "timestamp", "durationMs", "thinkingLevel", "providerThinkingLevel"];
18
+ const TOOL_RESULT_METADATA = ["role", "toolCallId", "toolName", "content", "details", "usage", "addedToolNames", "isError", "timestamp", "durationMs", "nestedCalls"];
19
19
  const NON_PRINTABLE_TEXT = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g;
20
20
  const MAX_PROVIDER_TEXT = 256 * 1024;
21
21
  const IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
@@ -186,20 +186,51 @@ function canonicalTool(tool, index) {
186
186
  };
187
187
  }
188
188
 
189
- /** Convert a Pi Context into a detached, closed provider context. */
189
+ function canonicalSystemMessage(message, index) {
190
+ const label = `proxy context messages[${index}]`;
191
+ exactKeys(message, ["role", "content", "timestamp", "sections", "toolsAdded", "toolsRemoved"], label);
192
+ const content = canonicalInputContent(message.content, `${label}.content`);
193
+ if (typeof content !== "string") throw new Error(`${label}.content must be system text`);
194
+ const result = { role: "system", content };
195
+ if (message.sections !== undefined) result.sections = schemaProjection(message.sections, `${label}.sections`);
196
+ if (message.toolsAdded !== undefined) {
197
+ if (!Array.isArray(message.toolsAdded)) throw new Error(`${label}.toolsAdded must be an array`);
198
+ result.toolsAdded = message.toolsAdded.map(canonicalTool);
199
+ }
200
+ if (message.toolsRemoved !== undefined) {
201
+ if (!Array.isArray(message.toolsRemoved)) throw new Error(`${label}.toolsRemoved must be an array`);
202
+ result.toolsRemoved = message.toolsRemoved.map((tool) => {
203
+ if (!tool || typeof tool !== "object" || Array.isArray(tool)) throw new Error(`${label}.toolsRemoved must contain tool identities`);
204
+ exactKeys(tool, ["name"], label);
205
+ return { name: tool.name };
206
+ });
207
+ }
208
+ return result;
209
+ }
210
+
211
+ /** Convert legacy Pi Context or native1.x TranscriptContext without dropping
212
+ * chronological system/tool deltas or forwarding executable declarations. */
190
213
  export function proxyCanonicalContext(context) {
191
214
  if (!context || typeof context !== "object" || Array.isArray(context)) throw new Error("proxy context is required");
192
215
  exactKeys(context, ["systemPrompt", "messages", "tools"], "proxy context");
193
216
  if (!Array.isArray(context.messages)) throw new Error("proxy context messages are required");
194
217
  if (context.systemPrompt !== undefined && typeof context.systemPrompt !== "string") throw new Error("proxy system prompt must be text");
195
218
  if (context.tools !== undefined && !Array.isArray(context.tools)) throw new Error("proxy context tools must be an array");
196
- const tools = (context.tools ?? []).map(canonicalTool);
197
- const messages = context.messages.map(canonicalMessage);
198
- return captureProviderContext({
199
- systemPrompt: context.systemPrompt ?? "",
200
- messages,
201
- tools,
202
- }, { maxBytes: MAX_PROVIDER_CONTEXT_BYTES }).value;
219
+ const transcript = context.messages.some((message) => message?.role === "system");
220
+ if (transcript && (context.systemPrompt !== undefined || context.tools !== undefined)) {
221
+ throw new Error("proxy context mixes legacy prompt/tools with transcript system messages");
222
+ }
223
+ const messages = context.messages.map((message, index) => message?.role === "system"
224
+ ? canonicalSystemMessage(message, index) : canonicalMessage(message, index));
225
+ const currentTools = new Map();
226
+ if (transcript) for (const message of messages) {
227
+ if (message.role !== "system") continue;
228
+ for (const tool of message.toolsRemoved ?? []) currentTools.delete(tool.name);
229
+ for (const tool of message.toolsAdded ?? []) currentTools.set(tool.name, tool);
230
+ }
231
+ const tools = transcript ? [...currentTools.values()] : (context.tools ?? []).map(canonicalTool);
232
+ return captureProviderContext({ systemPrompt: context.systemPrompt ?? "", messages, tools },
233
+ { maxBytes: MAX_PROVIDER_CONTEXT_BYTES }).value;
203
234
  }
204
235
 
205
236
  /** Map closed provider terminal outcomes to Pi stop/error semantics without leaking raw bodies. */
@@ -0,0 +1,45 @@
1
+ import { lstatSync, readdirSync, realpathSync } from 'node:fs';
2
+ import { join, resolve, sep } from 'node:path';
3
+
4
+ /** Controller-owned evidence pointers only. Never turn child logs into instructions
5
+ * or verification. Existing session journals survive failed terminal results. */
6
+ export function retainedWork(sessionsRoot, taskId) {
7
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,159}$/.test(taskId)) throw new Error('invalid task id');
8
+ const root = resolve(sessionsRoot);
9
+ let task = join(root, taskId);
10
+ const artifacts = [];
11
+ let incomplete = false;
12
+ function visit(directory, depth) {
13
+ if (depth > 4) { incomplete = true; return; }
14
+ let entries;
15
+ try {
16
+ if (lstatSync(directory).isSymbolicLink() || !realpathSync(directory).startsWith(realpathSync(task) + sep) && realpathSync(directory) !== realpathSync(task)) return;
17
+ entries = readdirSync(directory, { withFileTypes: true });
18
+ } catch { return; }
19
+ for (const entry of entries) {
20
+ if (artifacts.length >= 128) { incomplete = true; break; }
21
+ const path = join(directory, entry.name);
22
+ if (entry.isSymbolicLink()) continue;
23
+ if (entry.isDirectory()) visit(path, depth + 1);
24
+ else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
25
+ // Do not read potentially large/private raw transcripts into the report.
26
+ const stat = lstatSync(path);
27
+ artifacts.push({ kind: 'child_session', path, bytes: stat.size, verificationStatus: 'unverified' });
28
+ }
29
+ }
30
+ }
31
+ let attempts = [];
32
+ try { attempts = readdirSync(root).filter(name => name === taskId ||
33
+ (name.startsWith(taskId + '-r') && /^[0-9]+$/.test(name.slice(taskId.length + 2)))); } catch { /* no attempts */ }
34
+ for (const attempt of attempts) {
35
+ task = join(root, attempt);
36
+ try { if (lstatSync(task).isSymbolicLink()) continue; } catch { continue; }
37
+ visit(join(task, 'sessions'), 0);
38
+ try {
39
+ const worktree = join(task, 'worktree');
40
+ if (lstatSync(worktree).isDirectory() && !lstatSync(worktree).isSymbolicLink())
41
+ artifacts.push({kind:'retained_worktree',path:worktree,verificationStatus:'unverified'});
42
+ } catch { /* no retained worktree */ }
43
+ }
44
+ return { status: artifacts.length ? 'available' : 'not_found', artifacts, incomplete, root: join(root, taskId) };
45
+ }
@@ -139,7 +139,13 @@ function normalizedBrokerRegistry(registry) {
139
139
  ownKeysExactly(resource.provenance, [
140
140
  "providerClass", "modelDeveloper", "routeProvider", "baseProvider",
141
141
  "billingPool", "nativeToRoute", "source",
142
+ ...(resource.provenance.quotaObservationScope !== undefined ? ["quotaObservationScope"] : []),
142
143
  ], `resource ${id} provenance`);
144
+ if (resource.provenance.quotaObservationScope !== undefined &&
145
+ (resource.provenance.baseProvider !== "google-antigravity" || !model ||
146
+ resource.provenance.quotaObservationScope !== `model:${model.modelId.toLowerCase()}`)) {
147
+ throw new Error(`resource ${id} provenance quotaObservationScope must identify its exact Antigravity model`);
148
+ }
143
149
  for (const field of ["providerClass", "modelDeveloper", "routeProvider", "baseProvider", "billingPool", "source"]) {
144
150
  requireIdentifier(resource.provenance[field], `resource ${id} provenance ${field}`);
145
151
  }