@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 +10 -0
- package/extensions/pi-delegation-broker.ts +30 -11
- package/package.json +12 -5
- package/src/broker.mjs +75 -21
- package/src/brokered-runner.mjs +13 -2
- package/src/child-launcher.mjs +17 -0
- package/src/child-runtime-snapshot.mjs +4 -2
- package/src/delegation-inventory.mjs +1 -1
- package/src/model-provenance-policy.mjs +9 -5
- package/src/native-provider-routing.mjs +2 -1
- package/src/partial-output-journal.mjs +22 -0
- package/src/pi-native-provider.ts +21 -1
- package/src/provider-context.mjs +43 -4
- package/src/provider-probe.mjs +3 -0
- package/src/proxy-context.mjs +41 -10
- package/src/retained-work.mjs +45 -0
- package/src/signed-registry.mjs +6 -0
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.
|
|
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
|
|
1362
|
-
|
|
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
|
|
2010
|
+
const history=runtime?.history??new RoutingHistoryStore({path:join(STATE_DIR,'routing-history.sqlite')});
|
|
2006
2011
|
const owner=ownerKey(currentSessionId(ctx));
|
|
2007
|
-
const route=
|
|
2008
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
64
|
-
"@earendil-works/pi-ai": "
|
|
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
|
-
"
|
|
68
|
-
"@earendil-works/pi-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
880
|
+
`).run(evidence, proposedRetryAt, evidence, resourceId);
|
|
836
881
|
if (scope === "capacity_group") {
|
|
837
882
|
const siblings = this.#db.prepare(`
|
|
838
|
-
UPDATE resources
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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", {
|
package/src/brokered-runner.mjs
CHANGED
|
@@ -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
|
-
|
|
851
|
-
|
|
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);
|
package/src/child-launcher.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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/
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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 {
|
package/src/provider-context.mjs
CHANGED
|
@@ -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`,
|
|
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
|
-
|
|
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
|
}
|
package/src/provider-probe.mjs
CHANGED
|
@@ -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];
|
package/src/proxy-context.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
+
}
|
package/src/signed-registry.mjs
CHANGED
|
@@ -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
|
}
|