toolroll 0.9.27 → 0.9.29
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 +28 -0
- package/dist/agentconfig.js +71 -3
- package/dist/browser/workspace.js +3 -3
- package/dist/build-review.js +3 -2
- package/dist/builder.js +10 -8
- package/dist/chat-actions.js +7 -4
- package/dist/chat-flow.js +10 -6
- package/dist/converse.js +3 -2
- package/dist/coordinator-proposals.js +2 -2
- package/dist/decision.d.ts +3 -3
- package/dist/decision.js +4 -3
- package/dist/desktop-update.d.ts +1 -1
- package/dist/evidence.js +1 -1
- package/dist/flow-actions.d.ts +1 -0
- package/dist/flow-actions.js +6 -7
- package/dist/flow-code.d.ts +3 -2
- package/dist/flow-code.js +6 -4
- package/dist/flow-draft.d.ts +10 -4
- package/dist/flow-draft.js +19 -8
- package/dist/flow-engine.d.ts +0 -3
- package/dist/flow-engine.js +17 -10
- package/dist/flow-people.d.ts +0 -1
- package/dist/flow-people.js +9 -1
- package/dist/flow-send.js +3 -2
- package/dist/flow-steps.js +33 -10
- package/dist/flows.d.ts +23 -8
- package/dist/flows.js +106 -52
- package/dist/guides.js +1 -1
- package/dist/lead-context.d.ts +3 -9
- package/dist/lead-context.js +21 -5
- package/dist/lead-status.js +17 -12
- package/dist/mate-doors.js +8 -2
- package/dist/mate-tools.js +27 -10
- package/dist/mate.js +13 -2
- package/dist/needs-you.d.ts +2 -2
- package/dist/needs-you.js +3 -0
- package/dist/operate.d.ts +3 -0
- package/dist/operate.js +144 -9
- package/dist/phase-routing.d.ts +86 -5
- package/dist/phase-routing.js +229 -17
- package/dist/process-liveness.d.ts +1 -0
- package/dist/process-liveness.js +5 -0
- package/dist/project-skills.d.ts +10 -0
- package/dist/proposal.d.ts +10 -2
- package/dist/proposal.js +25 -10
- package/dist/recipe-ui.js +2 -1
- package/dist/scout-report.d.ts +10 -4
- package/dist/scout-report.js +23 -6
- package/dist/scout.d.ts +2 -0
- package/dist/scout.js +83 -10
- package/dist/serve.js +71 -15
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +72 -8
- package/dist/store.js +230 -29
- package/dist/task-sizing.d.ts +78 -0
- package/dist/task-sizing.js +228 -0
- package/dist/task-text.d.ts +4 -4
- package/dist/task-text.js +10 -6
- package/dist/teammate-work.js +31 -7
- package/dist/teammates.d.ts +20 -1
- package/dist/teammates.js +18 -5
- package/dist/teams-chat.d.ts +4 -1
- package/dist/teams-chat.js +17 -6
- package/dist/telegram-flow.js +6 -2
- package/dist/test-temp.js +1 -1
- package/dist/text-limits.d.ts +67 -0
- package/dist/text-limits.js +73 -0
- package/dist/tier-report.d.ts +44 -0
- package/dist/tier-report.js +71 -0
- package/package.json +1 -1
package/dist/store.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Actor } from "./actor.js";
|
|
2
2
|
import { type CriterionMatrixRow, type CriterionJudgementWord } from "./proof.js";
|
|
3
3
|
import { type ExecutionProfile, type ChainEntry, type UnattendedPermissionMode, type AcceptanceCriterion } from "./scope.js";
|
|
4
|
-
import { type PhaseRoute, type PublicationAuthority, type RouteStamp, type ReadinessLookup, type ReadinessObservation, type RiskLevel, type RouteOverride } from "./phase-routing.js";
|
|
4
|
+
import { type ProviderRoom, type CandidateTier, type TaskSize, type TaskSizing, type PhaseRoute, type PublicationAuthority, type RouteStamp, type ReadinessLookup, type ReadinessObservation, type RiskLevel, type RouteOverride } from "./phase-routing.js";
|
|
5
5
|
import { type TerminalClass } from "./exhaustion.js";
|
|
6
6
|
import { type ProviderId } from "./provider.js";
|
|
7
7
|
import type { BoardFacts } from "./board.js";
|
|
@@ -26,7 +26,7 @@ import { type ApprovalGate, type ApprovalRules, type ApproverKind, type Filer }
|
|
|
26
26
|
* next; `channel` is where it reports (`chat`: the conversation it was promised in); a promise lapses at `expires_at`. */
|
|
27
27
|
export declare const LEAD_COMMITMENT_SCHEMA = "\nCREATE TABLE IF NOT EXISTS lead_commitment (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n owner TEXT NOT NULL,\n repo TEXT,\n thread INTEGER NOT NULL,\n turn INTEGER,\n channel TEXT NOT NULL DEFAULT 'chat',\n what TEXT NOT NULL,\n condition_json TEXT NOT NULL,\n check_at TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n state TEXT NOT NULL DEFAULT 'open' CHECK (state IN ('open', 'done', 'cancelled', 'expired')),\n created_at TEXT NOT NULL,\n checked_at TEXT,\n closed_at TEXT,\n closed_by TEXT,\n outcome TEXT\n);\nCREATE INDEX IF NOT EXISTS lead_commitment_due ON lead_commitment (state, check_at);\nCREATE INDEX IF NOT EXISTS lead_commitment_owner ON lead_commitment (owner, state, id);\n";
|
|
28
28
|
/** What stops a finished run reading stopped (Store.stopQuiescenceFact). */
|
|
29
|
-
export type StopFactKind = "open" | "alive" | "elsewhere" | "unprovable" | "unknown";
|
|
29
|
+
export type StopFactKind = "open" | "alive" | "elsewhere" | "unprovable" | "unknown" | "settling";
|
|
30
30
|
export type TaskAct = "filed" | "approved" | "cancelled" | "completed" | "asked";
|
|
31
31
|
/** Store.stopQuiescenceFact, read safely: a deploy proves completion with this code over the INSTALLED runtime's store,
|
|
32
32
|
* which may predate the method. There the older stopQuiescenceProblem decides, and its words give the kind. */
|
|
@@ -232,6 +232,12 @@ export type TaskRef = {
|
|
|
232
232
|
* list is malformed; the next filing goes unresolved rather than routing
|
|
233
233
|
* as if nobody had overridden anything. */
|
|
234
234
|
routeOverrides?: RouteOverride[] | null;
|
|
235
|
+
/** v2 routing: how big the change is (sized at filing by the classifier
|
|
236
|
+
* or the description, or set by a person); null when never sized. */
|
|
237
|
+
sizing?: TaskSizing | null;
|
|
238
|
+
/** The build tier the task's latest filed route chose (light, routine,
|
|
239
|
+
* strong — or override/pinned when a person or a pin chose the agent). */
|
|
240
|
+
routeTier?: string | null;
|
|
235
241
|
/** Plan pins (P2/C7): read ONLY by the plan phase; precedence plan pin
|
|
236
242
|
* > plan flags > plan config > installation > default. */
|
|
237
243
|
planProvider: string | null;
|
|
@@ -3849,6 +3855,8 @@ export declare class Store {
|
|
|
3849
3855
|
* exactly as the source's was, never over the installation's
|
|
3850
3856
|
* defaults of the day. */
|
|
3851
3857
|
riskLevel?: RiskLevel;
|
|
3858
|
+
/** How big the change is, sized at filing (v2 routing). */
|
|
3859
|
+
sizing?: TaskSizing;
|
|
3852
3860
|
routeOverridesJson?: string | null;
|
|
3853
3861
|
agentPin?: {
|
|
3854
3862
|
provider: string;
|
|
@@ -4012,7 +4020,7 @@ export declare class Store {
|
|
|
4012
4020
|
/** The operator's named STRONG candidate for a phase at a scope, or null.
|
|
4013
4021
|
* Read by the routing policy only — a phase_config row stays the routine
|
|
4014
4022
|
* tier, so every pre-v47 resolution is untouched. */
|
|
4015
|
-
phaseTierConfig(scope: string, phase: string, tier: "strong"): {
|
|
4023
|
+
phaseTierConfig(scope: string, phase: string, tier: "light" | "strong"): {
|
|
4016
4024
|
provider: string;
|
|
4017
4025
|
model: string | null;
|
|
4018
4026
|
updatedAt: string;
|
|
@@ -4020,14 +4028,36 @@ export declare class Store {
|
|
|
4020
4028
|
} | null;
|
|
4021
4029
|
listPhaseTierConfig(scope: string): {
|
|
4022
4030
|
phase: string;
|
|
4023
|
-
tier: "strong";
|
|
4031
|
+
tier: "light" | "strong";
|
|
4024
4032
|
provider: string;
|
|
4025
4033
|
model: string | null;
|
|
4026
4034
|
updatedAt: string;
|
|
4027
4035
|
updatedBy: string;
|
|
4028
4036
|
}[];
|
|
4029
|
-
setPhaseTierConfig(scope: string, phase: string, tier: "strong", provider: string, model: string | null, by: string, now: Date): void;
|
|
4030
|
-
clearPhaseTierConfig(scope: string, phase: string, tier: "strong"): boolean;
|
|
4037
|
+
setPhaseTierConfig(scope: string, phase: string, tier: "light" | "strong", provider: string, model: string | null, by: string, now: Date): void;
|
|
4038
|
+
clearPhaseTierConfig(scope: string, phase: string, tier: "light" | "strong"): boolean;
|
|
4039
|
+
/** A tier's candidates on other providers at a scope, in provider order (sized routing). */
|
|
4040
|
+
phaseTierAlternates(scope: string, phase: string, tier: CandidateTier): {
|
|
4041
|
+
provider: string;
|
|
4042
|
+
model: string;
|
|
4043
|
+
updatedAt: string;
|
|
4044
|
+
updatedBy: string;
|
|
4045
|
+
}[];
|
|
4046
|
+
listPhaseTierAlternates(scope: string): {
|
|
4047
|
+
phase: string;
|
|
4048
|
+
tier: CandidateTier;
|
|
4049
|
+
provider: string;
|
|
4050
|
+
model: string;
|
|
4051
|
+
updatedAt: string;
|
|
4052
|
+
updatedBy: string;
|
|
4053
|
+
}[];
|
|
4054
|
+
/** Name this provider's candidate on a tier, beside the tier's own (one per provider; a second replaces the first). */
|
|
4055
|
+
setPhaseTierAlternate(scope: string, phase: "plan" | "build", tier: CandidateTier, provider: string, model: string, by: string, now: Date): void;
|
|
4056
|
+
/** Remove one provider's candidate from a tier, or every one when no provider is named; how many went. */
|
|
4057
|
+
clearPhaseTierAlternates(scope: string, phase: string, tier: CandidateTier, provider: string | null): number;
|
|
4058
|
+
/** Each provider's plan headroom right now, from the latest readings of
|
|
4059
|
+
* its usage windows — the routing policy's headroom input at filing. */
|
|
4060
|
+
planHeadroom(now: Date): ProviderRoom[];
|
|
4031
4061
|
/** How far the plane may carry this repo's results unattended: a live
|
|
4032
4062
|
* automerge mode, a publication grant whose merges wait for a person, or
|
|
4033
4063
|
* nothing. A signed routing input, read at filing. */
|
|
@@ -4054,6 +4084,11 @@ export declare class Store {
|
|
|
4054
4084
|
reason: string;
|
|
4055
4085
|
};
|
|
4056
4086
|
risk?: RiskLevel;
|
|
4087
|
+
/** v2: a person's size for the task — it outranks the classifier's. */
|
|
4088
|
+
size?: {
|
|
4089
|
+
size: TaskSize;
|
|
4090
|
+
risky: boolean;
|
|
4091
|
+
};
|
|
4057
4092
|
override?: {
|
|
4058
4093
|
phase: RouteOverride["phase"];
|
|
4059
4094
|
provider: ProviderId;
|
|
@@ -4084,6 +4119,30 @@ export declare class Store {
|
|
|
4084
4119
|
model: string;
|
|
4085
4120
|
}[];
|
|
4086
4121
|
};
|
|
4122
|
+
/** Store a task's size (v2 routing); the next scope filing routes by it. */
|
|
4123
|
+
writeSizing(taskRef: number, sizing: TaskSizing): void;
|
|
4124
|
+
/** Planning follows the size and risk while nothing has run: a small
|
|
4125
|
+
* change at routine risk drops a plan that is only requested; a large or
|
|
4126
|
+
* risky one, or any task at elevated or high risk, asks for a plan. A
|
|
4127
|
+
* drafted plan is never discarded. */
|
|
4128
|
+
private planToSizing;
|
|
4129
|
+
/**
|
|
4130
|
+
* The classifier's answer, applied after filing (v2 routing): filing never
|
|
4131
|
+
* waits for it, so its size lands here — only while the task is still
|
|
4132
|
+
* unapproved, unclaimed, and not sized by a person. The scope is re-filed
|
|
4133
|
+
* under its own words so the route (and its digest) carries the size, and
|
|
4134
|
+
* planning follows it unless the filer chose planning explicitly.
|
|
4135
|
+
*/
|
|
4136
|
+
applySizing(taskId: string, sizing: TaskSizing, options: {
|
|
4137
|
+
followPlanning: boolean;
|
|
4138
|
+
}, now: Date): {
|
|
4139
|
+
ok: true;
|
|
4140
|
+
changed: boolean;
|
|
4141
|
+
planning: "requested" | "cleared" | null;
|
|
4142
|
+
} | {
|
|
4143
|
+
ok: false;
|
|
4144
|
+
reason: "no-task" | "person" | "running" | "approved";
|
|
4145
|
+
};
|
|
4087
4146
|
/** A runner's own non-spending observations, replacing its previous ones
|
|
4088
4147
|
* per provider. Callers authenticate the runner first (runner.ts). */
|
|
4089
4148
|
recordProviderReadiness(runner: string, observations: readonly Omit<ReadinessObservation, "runner" | "observedAt">[], now: Date): void;
|
|
@@ -7215,7 +7274,8 @@ export declare class Store {
|
|
|
7215
7274
|
* another computer can tell, `unprovable` when nothing is known to run
|
|
7216
7275
|
* but its exit was never recorded, `unknown` when this computer can't
|
|
7217
7276
|
* check at all (both are what `run settle` resolves; only the first may
|
|
7218
|
-
* be called not running).
|
|
7277
|
+
* be called not running). `settling` already meets the automatic
|
|
7278
|
+
* reconciliation proof, so no confirmation is needed. */
|
|
7219
7279
|
stopQuiescenceFact(runId: number): {
|
|
7220
7280
|
run: number;
|
|
7221
7281
|
kind: StopFactKind;
|
|
@@ -7247,7 +7307,11 @@ export declare class Store {
|
|
|
7247
7307
|
* process groups is alive, it settles as never started, with a ledger
|
|
7248
7308
|
* entry. A sole pid-less witness has no group to prove gone and stays for
|
|
7249
7309
|
* `run settle`. Native witnesses keep their OS-only road. */
|
|
7250
|
-
settleUnspawnedWitnesses(now: Date): number;
|
|
7310
|
+
settleUnspawnedWitnesses(now: Date, onlyRun?: number): number;
|
|
7311
|
+
/** The existing automatic settlement proof, shared with its read-only
|
|
7312
|
+
* status. Unknown-only, foreign, native and still-running custody keep
|
|
7313
|
+
* their fences; only an observed single-process exit avoids a new probe. */
|
|
7314
|
+
private settleableUnspawnedWitnesses;
|
|
7251
7315
|
/** `run settle` (an approver's last resort): every witness of a finished
|
|
7252
7316
|
* run that cannot be proven either way is recorded settled under the
|
|
7253
7317
|
* approver's reason, in the ledger. Refuses while anything of the run is
|
package/dist/store.js
CHANGED
|
@@ -7,6 +7,7 @@ import { KNOWLEDGE_SCHEMA } from "./project-knowledge.js";
|
|
|
7
7
|
import { MEMORY_SCHEMA } from "./project-memory.js";
|
|
8
8
|
import { MODELS_SCHEMA } from "./model-catalog.js";
|
|
9
9
|
import { validateTaskText } from "./task-text.js";
|
|
10
|
+
import { flowDefinitionForStore, flowDefinitionFromStore } from "./flows.js";
|
|
10
11
|
import { chatControlHref, chatResultHref } from "./chat-controls.js";
|
|
11
12
|
import { actorLabel, currentActor, leadSecretMatches, mintLeadToken, parseLeadToken } from "./actor.js";
|
|
12
13
|
import { scanForSecrets } from "./evidence.js";
|
|
@@ -52,7 +53,7 @@ import { parseReviewContext, reviewContextCustodyProblem } from "./review-contex
|
|
|
52
53
|
import { foldReview, manualReviewOnly } from "./proof.js";
|
|
53
54
|
import { approvalOf, digestOf, canonicalProfileJson, canonicalChainJson, chainFromJson, chainDigestOf, entryDigestOf, profileDigestOf, profileFromJson, scopeAuthorityOf, routeParityProblem, parseAcceptanceCriteria, exactAcceptance, exactStringList, exactSafeIntegerOrNull, exactKeys, CLAUDE_LIMITS, CODEX_SHAPED_LIMITS, GEMINI_LIMITS } from "./scope.js";
|
|
54
55
|
import { resolveScopeProfile, resolveScopeChain, resolveRouteCandidates, exactPinOf, routeOfTask, agentChoicesFor } from "./agentconfig.js";
|
|
55
|
-
import { canonicalOverridesJson, canonicalRouteJson, exactModelId, isRiskLevel, legOf, overridesFromJson, phaseOfRole, recommendRoute, routeDigestOf, routeFromJson, routeProblems, routeStampProblem, ROUTE_ERA, } from "./phase-routing.js";
|
|
56
|
+
import { canonicalOverridesJson, canonicalRouteJson, exactModelId, isRiskLevel, isTaskSize, headroomFrom, makesNoPlan, legOf, overridesFromJson, phaseOfRole, recommendRoute, routeDigestOf, routeFromJson, routeProblems, routeStampProblem, ROUTE_ERA, PROVIDER_ID_LIST, } from "./phase-routing.js";
|
|
56
57
|
import { readAuthModeStrict } from "./keys.js";
|
|
57
58
|
import { isFallbackEligible, recognizesEligible, classMatchesAuthMode } from "./exhaustion.js";
|
|
58
59
|
import { modeDigestOf, modeTermsFromJson } from "./modes.js";
|
|
@@ -655,7 +656,7 @@ function readTeammateQuestion(row) {
|
|
|
655
656
|
suggestion: row["suggestion"] === null || row["suggestion"] === undefined ? null : Number(row["suggestion"]) };
|
|
656
657
|
}
|
|
657
658
|
function readFlowRow(row) {
|
|
658
|
-
return { id: Number(row["id"]), repo: String(row["repo"]), name: String(row["name"]), definitionJson: String(row["definition_json"]), revision: Number(row["revision"]),
|
|
659
|
+
return { id: Number(row["id"]), repo: String(row["repo"]), name: String(row["name"]), definitionJson: flowDefinitionFromStore(String(row["definition_json"])), revision: Number(row["revision"]),
|
|
659
660
|
state: String(row["state"]), createdBy: String(row["created_by"]), createdAt: String(row["created_at"]), updatedBy: String(row["updated_by"]), updatedAt: String(row["updated_at"]),
|
|
660
661
|
owner: row["owner"] === null || row["owner"] === undefined ? String(row["created_by"]) : String(row["owner"]) };
|
|
661
662
|
}
|
|
@@ -1084,7 +1085,7 @@ CREATE TABLE IF NOT EXISTS fallback_config (
|
|
|
1084
1085
|
CREATE TABLE IF NOT EXISTS phase_tier_config (
|
|
1085
1086
|
scope TEXT NOT NULL,
|
|
1086
1087
|
phase TEXT NOT NULL CHECK (phase IN ('plan','build','repair','review')),
|
|
1087
|
-
tier TEXT NOT NULL CHECK (tier IN ('strong')),
|
|
1088
|
+
tier TEXT NOT NULL CHECK (tier IN ('light','strong')),
|
|
1088
1089
|
provider TEXT NOT NULL CHECK (provider IN ('claude','codex','openrouter','gemini')),
|
|
1089
1090
|
model TEXT,
|
|
1090
1091
|
updated_at TEXT NOT NULL,
|
|
@@ -3977,6 +3978,15 @@ function initializeStore(db, file) {
|
|
|
3977
3978
|
db.exec(PROVIDER_AUTH_SCHEMA);
|
|
3978
3979
|
// The sign-in pause the dispatch gate last left this task waiting on (null: none), for the work index.
|
|
3979
3980
|
addColumn(db, "task_ref", "auth_wait_pause", "INTEGER REFERENCES provider_auth_pause(id)");
|
|
3981
|
+
// Sized routing (no version bump: additive, and a wider tier check whose rows carry over): how big each task is,
|
|
3982
|
+
// who sized it and why, and the build tier its route chose; the light tier joins the strong one.
|
|
3983
|
+
addColumn(db, "task_ref", "size", "TEXT CHECK (size IN ('small','medium','large'))");
|
|
3984
|
+
addColumn(db, "task_ref", "size_risky", "INTEGER CHECK (size_risky IN (0, 1))");
|
|
3985
|
+
addColumn(db, "task_ref", "size_source", "TEXT CHECK (size_source IN ('classifier','heuristic','person'))");
|
|
3986
|
+
addColumn(db, "task_ref", "size_reason", "TEXT");
|
|
3987
|
+
addColumn(db, "task_ref", "route_tier", "TEXT");
|
|
3988
|
+
widenPhaseTierSchema(db);
|
|
3989
|
+
db.exec(PHASE_TIER_ALTERNATE_SCHEMA);
|
|
3980
3990
|
db.exec(REVIEW_SCHEMA);
|
|
3981
3991
|
db.exec(RETENTION_SCHEMA);
|
|
3982
3992
|
// 1-day evidence: an older file's retention_setting only allowed 7 days or more (no version bump: its rows carry over).
|
|
@@ -7099,6 +7109,57 @@ function hasColumn(db, table, column) {
|
|
|
7099
7109
|
.all()
|
|
7100
7110
|
.some(row => String(row["name"]) === column);
|
|
7101
7111
|
}
|
|
7112
|
+
/** A tier's candidates on other providers (sized routing): one row per provider, beside the tier's own candidate
|
|
7113
|
+
* (phase_config for routine, phase_tier_config for light and strong). At filing the one with more plan room runs. */
|
|
7114
|
+
const PHASE_TIER_ALTERNATE_SCHEMA = `
|
|
7115
|
+
CREATE TABLE IF NOT EXISTS phase_tier_alternate (
|
|
7116
|
+
scope TEXT NOT NULL,
|
|
7117
|
+
phase TEXT NOT NULL CHECK (phase IN ('plan','build')),
|
|
7118
|
+
tier TEXT NOT NULL CHECK (tier IN ('light','routine','strong')),
|
|
7119
|
+
provider TEXT NOT NULL CHECK (provider IN ('claude','codex','openrouter','gemini')),
|
|
7120
|
+
model TEXT NOT NULL,
|
|
7121
|
+
updated_at TEXT NOT NULL,
|
|
7122
|
+
updated_by TEXT NOT NULL,
|
|
7123
|
+
PRIMARY KEY (scope, phase, tier, provider)
|
|
7124
|
+
);
|
|
7125
|
+
`;
|
|
7126
|
+
/** A file from before the light tier keeps `tier IN ('strong')` in phase_tier_config's CHECK: rebuild the table with
|
|
7127
|
+
* the wider one, keeping every row, in one transaction. A file already widened is untouched. */
|
|
7128
|
+
function widenPhaseTierSchema(db) {
|
|
7129
|
+
const row = db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'phase_tier_config'").get();
|
|
7130
|
+
if (row === undefined || /'light'/.test(String(row["sql"])))
|
|
7131
|
+
return;
|
|
7132
|
+
db.exec("BEGIN IMMEDIATE");
|
|
7133
|
+
try {
|
|
7134
|
+
db.exec(`CREATE TABLE phase_tier_config_next (
|
|
7135
|
+
scope TEXT NOT NULL,
|
|
7136
|
+
phase TEXT NOT NULL CHECK (phase IN ('plan','build','repair','review')),
|
|
7137
|
+
tier TEXT NOT NULL CHECK (tier IN ('light','strong')),
|
|
7138
|
+
provider TEXT NOT NULL CHECK (provider IN ('claude','codex','openrouter','gemini')),
|
|
7139
|
+
model TEXT,
|
|
7140
|
+
updated_at TEXT NOT NULL,
|
|
7141
|
+
updated_by TEXT NOT NULL,
|
|
7142
|
+
PRIMARY KEY (scope, phase, tier)
|
|
7143
|
+
)`);
|
|
7144
|
+
db.exec("INSERT INTO phase_tier_config_next SELECT scope, phase, tier, provider, model, updated_at, updated_by FROM phase_tier_config");
|
|
7145
|
+
db.exec("DROP TABLE phase_tier_config");
|
|
7146
|
+
db.exec("ALTER TABLE phase_tier_config_next RENAME TO phase_tier_config");
|
|
7147
|
+
db.exec("COMMIT");
|
|
7148
|
+
}
|
|
7149
|
+
catch (error) {
|
|
7150
|
+
db.exec("ROLLBACK");
|
|
7151
|
+
throw error;
|
|
7152
|
+
}
|
|
7153
|
+
}
|
|
7154
|
+
/** A task's stored size, or null when it was never sized (or reads as nothing this code writes). */
|
|
7155
|
+
function sizingOfRow(row) {
|
|
7156
|
+
if (!isTaskSize(row["size"]))
|
|
7157
|
+
return null;
|
|
7158
|
+
const source = row["size_source"];
|
|
7159
|
+
if (source !== "classifier" && source !== "heuristic" && source !== "person")
|
|
7160
|
+
return null;
|
|
7161
|
+
return { size: row["size"], risky: Number(row["size_risky"] ?? 0) === 1, source, reason: row["size_reason"] == null ? "" : String(row["size_reason"]) };
|
|
7162
|
+
}
|
|
7102
7163
|
function addColumn(db, table, column, definition) {
|
|
7103
7164
|
if (hasColumn(db, table, column))
|
|
7104
7165
|
return;
|
|
@@ -8454,11 +8515,20 @@ export class Store {
|
|
|
8454
8515
|
candidates: candidatesResolved.candidates,
|
|
8455
8516
|
overrides,
|
|
8456
8517
|
pins: { plan: planPin.pin, build: buildPin.pin, repair: repairPin },
|
|
8518
|
+
// v2: the task's size and each provider's plan headroom, sealed
|
|
8519
|
+
// with the route like every other input.
|
|
8520
|
+
size: ref?.sizing ?? null,
|
|
8521
|
+
headroom: this.planHeadroom(new Date(scope.proposedAt)),
|
|
8457
8522
|
});
|
|
8458
8523
|
const problems = routeProblems(route);
|
|
8459
8524
|
if (problems.length > 0)
|
|
8460
8525
|
unresolvedReason = problems.join("; ");
|
|
8461
8526
|
}
|
|
8527
|
+
// The tier the build leg runs on, recorded per task for the spend report.
|
|
8528
|
+
if (route !== null && ref !== null) {
|
|
8529
|
+
const buildLeg = legOf(route, "build");
|
|
8530
|
+
this.db.prepare("UPDATE task_ref SET route_tier = ? WHERE id = ?").run(buildLeg.chosen === "recommended" ? buildLeg.tier : buildLeg.chosen, ref.id);
|
|
8531
|
+
}
|
|
8462
8532
|
if (unresolvedReason !== null) {
|
|
8463
8533
|
// Fail closed: an explicit profile does not outrank a route that
|
|
8464
8534
|
// cannot be made exact — the scope says why, and nothing runs.
|
|
@@ -13090,6 +13160,8 @@ export class Store {
|
|
|
13090
13160
|
if (spec.routeOverridesJson !== undefined && spec.routeOverridesJson !== null) {
|
|
13091
13161
|
this.db.prepare("UPDATE task_ref SET route_overrides_json = ? WHERE id = ?").run(spec.routeOverridesJson, ref.id);
|
|
13092
13162
|
}
|
|
13163
|
+
if (spec.sizing !== undefined)
|
|
13164
|
+
this.writeSizing(ref.id, spec.sizing);
|
|
13093
13165
|
if (spec.agentPin !== undefined && spec.agentPin !== null)
|
|
13094
13166
|
this.pinTaskAgent(ref.id, spec.agentPin.provider, spec.agentPin.model);
|
|
13095
13167
|
if (spec.planPin !== undefined && spec.planPin !== null) {
|
|
@@ -13467,7 +13539,7 @@ export class Store {
|
|
|
13467
13539
|
.all(scope)
|
|
13468
13540
|
.map(row => ({
|
|
13469
13541
|
phase: String(row["phase"]),
|
|
13470
|
-
tier: "strong",
|
|
13542
|
+
tier: row["tier"] === "light" ? "light" : "strong",
|
|
13471
13543
|
provider: String(row["provider"]),
|
|
13472
13544
|
model: row["model"] === null ? null : String(row["model"]),
|
|
13473
13545
|
updatedAt: String(row["updated_at"]),
|
|
@@ -13475,6 +13547,9 @@ export class Store {
|
|
|
13475
13547
|
}));
|
|
13476
13548
|
}
|
|
13477
13549
|
setPhaseTierConfig(scope, phase, tier, provider, model, by, now) {
|
|
13550
|
+
// A small change makes no plan and repairs resume the builder: only the build phase has a light agent.
|
|
13551
|
+
if (tier === "light" && phase !== "build")
|
|
13552
|
+
throw new Error(`a light agent is for the build phase only, not ${phase}`);
|
|
13478
13553
|
this.db
|
|
13479
13554
|
.prepare(`INSERT INTO phase_tier_config (scope, phase, tier, provider, model, updated_at, updated_by)
|
|
13480
13555
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
@@ -13486,6 +13561,50 @@ export class Store {
|
|
|
13486
13561
|
const { changes } = this.db.prepare("DELETE FROM phase_tier_config WHERE scope = ? AND phase = ? AND tier = ?").run(scope, phase, tier);
|
|
13487
13562
|
return Number(changes) > 0;
|
|
13488
13563
|
}
|
|
13564
|
+
/** A tier's candidates on other providers at a scope, in provider order (sized routing). */
|
|
13565
|
+
phaseTierAlternates(scope, phase, tier) {
|
|
13566
|
+
return this.db
|
|
13567
|
+
.prepare("SELECT * FROM phase_tier_alternate WHERE scope = ? AND phase = ? AND tier = ?")
|
|
13568
|
+
.all(scope, phase, tier)
|
|
13569
|
+
.map(row => ({ provider: String(row["provider"]), model: String(row["model"]), updatedAt: String(row["updated_at"]), updatedBy: String(row["updated_by"]) }))
|
|
13570
|
+
.sort((a, b) => PROVIDER_ID_LIST.indexOf(a.provider) - PROVIDER_ID_LIST.indexOf(b.provider));
|
|
13571
|
+
}
|
|
13572
|
+
listPhaseTierAlternates(scope) {
|
|
13573
|
+
return this.db
|
|
13574
|
+
.prepare("SELECT * FROM phase_tier_alternate WHERE scope = ? ORDER BY phase, tier, provider")
|
|
13575
|
+
.all(scope)
|
|
13576
|
+
.map(row => ({
|
|
13577
|
+
phase: String(row["phase"]),
|
|
13578
|
+
tier: (row["tier"] === "light" ? "light" : row["tier"] === "strong" ? "strong" : "routine"),
|
|
13579
|
+
provider: String(row["provider"]),
|
|
13580
|
+
model: String(row["model"]),
|
|
13581
|
+
updatedAt: String(row["updated_at"]),
|
|
13582
|
+
updatedBy: String(row["updated_by"]),
|
|
13583
|
+
}));
|
|
13584
|
+
}
|
|
13585
|
+
/** Name this provider's candidate on a tier, beside the tier's own (one per provider; a second replaces the first). */
|
|
13586
|
+
setPhaseTierAlternate(scope, phase, tier, provider, model, by, now) {
|
|
13587
|
+
if (tier === "light" && phase !== "build")
|
|
13588
|
+
throw new Error(`a light agent is for the build phase only, not ${phase}`);
|
|
13589
|
+
this.db
|
|
13590
|
+
.prepare(`INSERT INTO phase_tier_alternate (scope, phase, tier, provider, model, updated_at, updated_by)
|
|
13591
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
13592
|
+
ON CONFLICT (scope, phase, tier, provider) DO UPDATE SET model = excluded.model,
|
|
13593
|
+
updated_at = excluded.updated_at, updated_by = excluded.updated_by`)
|
|
13594
|
+
.run(scope, phase, tier, provider, model, now.toISOString(), by);
|
|
13595
|
+
}
|
|
13596
|
+
/** Remove one provider's candidate from a tier, or every one when no provider is named; how many went. */
|
|
13597
|
+
clearPhaseTierAlternates(scope, phase, tier, provider) {
|
|
13598
|
+
const { changes } = provider === null
|
|
13599
|
+
? this.db.prepare("DELETE FROM phase_tier_alternate WHERE scope = ? AND phase = ? AND tier = ?").run(scope, phase, tier)
|
|
13600
|
+
: this.db.prepare("DELETE FROM phase_tier_alternate WHERE scope = ? AND phase = ? AND tier = ? AND provider = ?").run(scope, phase, tier, provider);
|
|
13601
|
+
return Number(changes);
|
|
13602
|
+
}
|
|
13603
|
+
/** Each provider's plan headroom right now, from the latest readings of
|
|
13604
|
+
* its usage windows — the routing policy's headroom input at filing. */
|
|
13605
|
+
planHeadroom(now) {
|
|
13606
|
+
return headroomFrom(this.providerLimits(), now);
|
|
13607
|
+
}
|
|
13489
13608
|
/** How far the plane may carry this repo's results unattended: a live
|
|
13490
13609
|
* automerge mode, a publication grant whose merges wait for a person, or
|
|
13491
13610
|
* nothing. A signed routing input, read at filing. */
|
|
@@ -13518,7 +13637,7 @@ export class Store {
|
|
|
13518
13637
|
const ref = this.refForId(taskRef);
|
|
13519
13638
|
if (ref === null)
|
|
13520
13639
|
return { ok: false, reason: "no-task", detail: "no such task" };
|
|
13521
|
-
if (edit.risk === undefined && edit.override === undefined)
|
|
13640
|
+
if (edit.risk === undefined && edit.override === undefined && edit.size === undefined)
|
|
13522
13641
|
return { ok: false, reason: "nothing", detail: "nothing to change" };
|
|
13523
13642
|
if (this.hasLiveClaim(taskRef, now))
|
|
13524
13643
|
return { ok: false, reason: "live-claim", detail: "this task is running — the agents cannot change under a live claim" };
|
|
@@ -13552,6 +13671,9 @@ export class Store {
|
|
|
13552
13671
|
if (edit.risk !== undefined) {
|
|
13553
13672
|
this.db.prepare("UPDATE task_ref SET risk_level = ? WHERE id = ?").run(edit.risk, taskRef);
|
|
13554
13673
|
}
|
|
13674
|
+
if (edit.size !== undefined) {
|
|
13675
|
+
this.writeSizing(taskRef, { size: edit.size.size, risky: edit.size.risky, source: "person", reason: `set by ${edit.by}` });
|
|
13676
|
+
}
|
|
13555
13677
|
let overrides = ref.routeOverrides ?? [];
|
|
13556
13678
|
if (edit.override !== undefined) {
|
|
13557
13679
|
const kept = overrides.filter(one => one.phase !== edit.override.phase);
|
|
@@ -13570,6 +13692,10 @@ export class Store {
|
|
|
13570
13692
|
const wasApproved = before !== null && before.approvedAt !== null && before.approvedDigest === before.digest;
|
|
13571
13693
|
const refiled = before === null ? null : this.refileScope(ref.externalId, now);
|
|
13572
13694
|
const staled = wasApproved && refiled !== null && refiled.approvedDigest !== refiled.digest;
|
|
13695
|
+
// Planning follows a new size, and elevated or high risk always plans — after the re-file, so an approval
|
|
13696
|
+
// this edit staled does not refuse the plan request.
|
|
13697
|
+
if (edit.size !== undefined || (edit.risk !== undefined && edit.risk !== "routine"))
|
|
13698
|
+
this.planToSizing(taskRef, now);
|
|
13573
13699
|
// A plan drafted by the previous planner is not relabeled: a plan
|
|
13574
13700
|
// override (set or cleared) asks for a REAL re-plan.
|
|
13575
13701
|
let replanned = false;
|
|
@@ -13581,6 +13707,63 @@ export class Store {
|
|
|
13581
13707
|
return { ok: true, scope: refiled, staled, replanned, overrides: overridesFromJson(canonicalOverridesJson(overrides)) ?? [] };
|
|
13582
13708
|
});
|
|
13583
13709
|
}
|
|
13710
|
+
/** Store a task's size (v2 routing); the next scope filing routes by it. */
|
|
13711
|
+
writeSizing(taskRef, sizing) {
|
|
13712
|
+
this.db
|
|
13713
|
+
.prepare("UPDATE task_ref SET size = ?, size_risky = ?, size_source = ?, size_reason = ? WHERE id = ?")
|
|
13714
|
+
.run(sizing.size, sizing.risky ? 1 : 0, sizing.source, sizing.reason.slice(0, 300), taskRef);
|
|
13715
|
+
}
|
|
13716
|
+
/** Planning follows the size and risk while nothing has run: a small
|
|
13717
|
+
* change at routine risk drops a plan that is only requested; a large or
|
|
13718
|
+
* risky one, or any task at elevated or high risk, asks for a plan. A
|
|
13719
|
+
* drafted plan is never discarded. */
|
|
13720
|
+
planToSizing(taskRef, now) {
|
|
13721
|
+
const ref = this.refForId(taskRef);
|
|
13722
|
+
if (ref === null || ref.repo === null || ref.deliverable === "report")
|
|
13723
|
+
return null;
|
|
13724
|
+
const sizing = ref.sizing ?? null;
|
|
13725
|
+
if (makesNoPlan(sizing, ref.riskLevel ?? "routine")) {
|
|
13726
|
+
if (ref.plan !== "requested" || this.hasLiveClaim(taskRef, now))
|
|
13727
|
+
return null;
|
|
13728
|
+
this.db.prepare("UPDATE task_ref SET plan = NULL WHERE id = ? AND plan = 'requested'").run(taskRef);
|
|
13729
|
+
return "cleared";
|
|
13730
|
+
}
|
|
13731
|
+
const wanted = (ref.riskLevel ?? "routine") !== "routine" || (sizing !== null && (sizing.size === "large" || sizing.risky));
|
|
13732
|
+
if (wanted && ref.plan === null)
|
|
13733
|
+
return this.requestPlan(taskRef, now).ok ? "requested" : null;
|
|
13734
|
+
return null;
|
|
13735
|
+
}
|
|
13736
|
+
/**
|
|
13737
|
+
* The classifier's answer, applied after filing (v2 routing): filing never
|
|
13738
|
+
* waits for it, so its size lands here — only while the task is still
|
|
13739
|
+
* unapproved, unclaimed, and not sized by a person. The scope is re-filed
|
|
13740
|
+
* under its own words so the route (and its digest) carries the size, and
|
|
13741
|
+
* planning follows it unless the filer chose planning explicitly.
|
|
13742
|
+
*/
|
|
13743
|
+
applySizing(taskId, sizing, options, now) {
|
|
13744
|
+
return this.transact(() => {
|
|
13745
|
+
const ref = this.lookupRef(taskId);
|
|
13746
|
+
if (ref === null)
|
|
13747
|
+
return { ok: false, reason: "no-task" };
|
|
13748
|
+
if (ref.sizing?.source === "person")
|
|
13749
|
+
return { ok: false, reason: "person" };
|
|
13750
|
+
if (this.hasLiveClaim(ref.id, now))
|
|
13751
|
+
return { ok: false, reason: "running" };
|
|
13752
|
+
const scope = this.getScope(taskId);
|
|
13753
|
+
if (scope !== null && scope.approvedAt !== null && scope.approvedDigest === scope.digest)
|
|
13754
|
+
return { ok: false, reason: "approved" };
|
|
13755
|
+
const before = ref.sizing ?? null;
|
|
13756
|
+
if (before !== null && before.size === sizing.size && before.risky === sizing.risky && before.source === sizing.source && before.reason === sizing.reason) {
|
|
13757
|
+
return { ok: true, changed: false, planning: null };
|
|
13758
|
+
}
|
|
13759
|
+
this.writeSizing(ref.id, sizing);
|
|
13760
|
+
if (scope !== null)
|
|
13761
|
+
this.refileScope(taskId, now);
|
|
13762
|
+
const planning = options.followPlanning ? this.planToSizing(ref.id, now) : null;
|
|
13763
|
+
this.bumpWake();
|
|
13764
|
+
return { ok: true, changed: true, planning };
|
|
13765
|
+
});
|
|
13766
|
+
}
|
|
13584
13767
|
/** A runner's own non-spending observations, replacing its previous ones
|
|
13585
13768
|
* per provider. Callers authenticate the runner first (runner.ts). */
|
|
13586
13769
|
recordProviderReadiness(runner, observations, now) {
|
|
@@ -16296,6 +16479,9 @@ export class Store {
|
|
|
16296
16479
|
// retained witnesses and every owned run; recovery revisits a pending
|
|
16297
16480
|
// stop after an orphan or held supervisor finishes shutdown.
|
|
16298
16481
|
this.recordRunProcessExits(id, result.now);
|
|
16482
|
+
// Use the existing reconciliation proof now, while exits are fresh. In
|
|
16483
|
+
// particular, a returned --candidate check need not await the next scan.
|
|
16484
|
+
this.settleUnspawnedWitnesses(result.now, id);
|
|
16299
16485
|
this.settleRunStop(id, result.stopSettlement ?? "finished", result.now);
|
|
16300
16486
|
// The first Ready result is an installation fact the moment it lands,
|
|
16301
16487
|
// not only when something later looks for it. A run that changed
|
|
@@ -19096,7 +19282,7 @@ export class Store {
|
|
|
19096
19282
|
createFlow(flow, now) {
|
|
19097
19283
|
const stamp = now.toISOString();
|
|
19098
19284
|
return Number(this.db.prepare("INSERT INTO flow (repo, name, definition_json, revision, state, created_by, created_at, updated_by, updated_at) VALUES (?, ?, ?, 1, 'active', ?, ?, ?, ?)")
|
|
19099
|
-
.run(flow.repo, flow.name, flow.definitionJson, flow.by, stamp, flow.by, stamp).lastInsertRowid);
|
|
19285
|
+
.run(flow.repo, flow.name, flowDefinitionForStore(flow.definitionJson), flow.by, stamp, flow.by, stamp).lastInsertRowid);
|
|
19100
19286
|
}
|
|
19101
19287
|
/** Hand a flow to another person: its "the owner decides" zones ask them from now on. */
|
|
19102
19288
|
setFlowOwner(id, owner, now) {
|
|
@@ -19114,7 +19300,7 @@ export class Store {
|
|
|
19114
19300
|
/** Save a new drawing of the flow only over the revision the editor saw (two editors never overwrite each other silently). */
|
|
19115
19301
|
saveFlow(id, change, now) {
|
|
19116
19302
|
const { changes } = this.db.prepare("UPDATE flow SET name = ?, definition_json = ?, revision = revision + 1, updated_by = ?, updated_at = ? WHERE id = ? AND revision = ? AND state = 'active'")
|
|
19117
|
-
.run(change.name, change.definitionJson, change.by, now.toISOString(), id, change.sawRevision);
|
|
19303
|
+
.run(change.name, flowDefinitionForStore(change.definitionJson), change.by, now.toISOString(), id, change.sawRevision);
|
|
19118
19304
|
return Number(changes) === 1;
|
|
19119
19305
|
}
|
|
19120
19306
|
archiveFlow(id, by, now) {
|
|
@@ -21290,7 +21476,8 @@ export class Store {
|
|
|
21290
21476
|
* another computer can tell, `unprovable` when nothing is known to run
|
|
21291
21477
|
* but its exit was never recorded, `unknown` when this computer can't
|
|
21292
21478
|
* check at all (both are what `run settle` resolves; only the first may
|
|
21293
|
-
* be called not running).
|
|
21479
|
+
* be called not running). `settling` already meets the automatic
|
|
21480
|
+
* reconciliation proof, so no confirmation is needed. */
|
|
21294
21481
|
stopQuiescenceFact(runId) {
|
|
21295
21482
|
const fact = (run, kind, problem) => ({ run, kind, problem });
|
|
21296
21483
|
const ids = this.ownedRunsOf(runId);
|
|
@@ -21306,10 +21493,13 @@ export class Store {
|
|
|
21306
21493
|
if (held !== null && held.endedAt === null)
|
|
21307
21494
|
return fact(id, "alive", `run #${id}'s held supervisor has not finished shutdown`);
|
|
21308
21495
|
const witnesses = this.db.prepare("SELECT * FROM run_process WHERE run = ? ORDER BY id").all(id);
|
|
21496
|
+
let incompleteSpawn = false;
|
|
21309
21497
|
if (run.providerStartedAt !== null && witnesses.length === 0) {
|
|
21310
21498
|
return fact(id, "unprovable", `run #${id} may have spawned before its process witness was recorded; exit is unproven`);
|
|
21311
21499
|
}
|
|
21312
21500
|
for (const witness of witnesses) {
|
|
21501
|
+
// A recorded group exit proves the whole group ended, not just its
|
|
21502
|
+
// leader. Re-probing its number could mistake reuse for the old group.
|
|
21313
21503
|
if (witness["exited_at"] !== null)
|
|
21314
21504
|
continue;
|
|
21315
21505
|
const host = String(witness["host"]);
|
|
@@ -21339,10 +21529,12 @@ export class Store {
|
|
|
21339
21529
|
return fact(id, "unknown", `run #${id}'s ${backend} object ${container} cannot be proven empty from here`);
|
|
21340
21530
|
continue;
|
|
21341
21531
|
}
|
|
21342
|
-
if (witness["pid"] === null)
|
|
21343
|
-
|
|
21532
|
+
if (witness["pid"] === null) {
|
|
21533
|
+
incompleteSpawn = true;
|
|
21534
|
+
continue;
|
|
21535
|
+
}
|
|
21344
21536
|
const pid = Number(witness["pid"]);
|
|
21345
|
-
if (processMayBeAlive(pid, witness["process_group"] === 1, { observedAt: witness["observed_at"], finishedAt: run.finishedAt }))
|
|
21537
|
+
if (processMayBeAlive(pid, witness["process_group"] === 1, { observedAt: witness["observed_at"], finishedAt: run.finishedAt, exitedAt: witness["exited_at"] }))
|
|
21346
21538
|
return fact(id, "alive", `run #${id}'s process ${pid} may still be running`);
|
|
21347
21539
|
}
|
|
21348
21540
|
if (run.worktree !== null) {
|
|
@@ -21357,6 +21549,8 @@ export class Store {
|
|
|
21357
21549
|
return fact(id, "unknown", `run #${id}'s workspace occupancy could not be established`);
|
|
21358
21550
|
}
|
|
21359
21551
|
}
|
|
21552
|
+
if (incompleteSpawn)
|
|
21553
|
+
return fact(id, this.settleableUnspawnedWitnesses(run, witnesses).length > 0 ? "settling" : "unprovable", `run #${id} has an incomplete spawn witness; exit is unproven`);
|
|
21360
21554
|
}
|
|
21361
21555
|
return null;
|
|
21362
21556
|
}
|
|
@@ -21435,10 +21629,11 @@ export class Store {
|
|
|
21435
21629
|
* process groups is alive, it settles as never started, with a ledger
|
|
21436
21630
|
* entry. A sole pid-less witness has no group to prove gone and stays for
|
|
21437
21631
|
* `run settle`. Native witnesses keep their OS-only road. */
|
|
21438
|
-
settleUnspawnedWitnesses(now) {
|
|
21632
|
+
settleUnspawnedWitnesses(now, onlyRun) {
|
|
21439
21633
|
const candidates = this.db.prepare(`SELECT DISTINCT p.run FROM run_process p JOIN run r ON r.id = p.run
|
|
21440
21634
|
WHERE p.exited_at IS NULL AND p.pid IS NULL AND p.containment IS NULL AND p.container IS NULL
|
|
21441
|
-
AND p.host = ? AND r.outcome IS NOT NULL AND r.finished_at IS NOT NULL
|
|
21635
|
+
AND p.host = ? AND r.outcome IS NOT NULL AND r.finished_at IS NOT NULL
|
|
21636
|
+
AND (? IS NULL OR p.run = ?) ORDER BY p.run`).all(hostname(), onlyRun ?? null, onlyRun ?? null);
|
|
21442
21637
|
let settled = 0;
|
|
21443
21638
|
for (const candidate of candidates) {
|
|
21444
21639
|
const runId = Number(candidate["run"]);
|
|
@@ -21447,22 +21642,8 @@ export class Store {
|
|
|
21447
21642
|
const run = this.getRun(runId);
|
|
21448
21643
|
if (run === null || run.outcome === null)
|
|
21449
21644
|
return 0;
|
|
21450
|
-
if (ownedProcessCount(runOwnerTag(this, runId)) > 0)
|
|
21451
|
-
return 0;
|
|
21452
|
-
const held = this.heldSessionOf(runId);
|
|
21453
|
-
if (held !== null && held.endedAt === null)
|
|
21454
|
-
return 0;
|
|
21455
21645
|
const rows = this.db.prepare("SELECT * FROM run_process WHERE run = ? ORDER BY id").all(runId);
|
|
21456
|
-
const pidless =
|
|
21457
|
-
const others = rows.filter(row => !pidless.includes(row));
|
|
21458
|
-
if (pidless.length === 0 || others.length === 0)
|
|
21459
|
-
return 0;
|
|
21460
|
-
if (others.some(row => row["exited_at"] === null))
|
|
21461
|
-
return 0;
|
|
21462
|
-
// Exit was recorded once; the group's members are asked again now.
|
|
21463
|
-
const groups = others.filter(row => row["pid"] !== null && row["host"] === hostname());
|
|
21464
|
-
if (groups.length === 0 || groups.some(row => processMayBeAlive(Number(row["pid"]), row["process_group"] === 1, { observedAt: row["observed_at"], finishedAt: run.finishedAt })))
|
|
21465
|
-
return 0;
|
|
21646
|
+
const pidless = this.settleableUnspawnedWitnesses(run, rows);
|
|
21466
21647
|
const ref = this.refById(run.taskRef);
|
|
21467
21648
|
let count = 0;
|
|
21468
21649
|
for (const row of pidless) {
|
|
@@ -21481,6 +21662,24 @@ export class Store {
|
|
|
21481
21662
|
}
|
|
21482
21663
|
return settled;
|
|
21483
21664
|
}
|
|
21665
|
+
/** The existing automatic settlement proof, shared with its read-only
|
|
21666
|
+
* status. Unknown-only, foreign, native and still-running custody keep
|
|
21667
|
+
* their fences; only an observed single-process exit avoids a new probe. */
|
|
21668
|
+
settleableUnspawnedWitnesses(run, rows) {
|
|
21669
|
+
if (run.outcome === null || run.finishedAt === null || ownedProcessCount(runOwnerTag(this, run.id)) > 0)
|
|
21670
|
+
return [];
|
|
21671
|
+
const held = this.heldSessionOf(run.id);
|
|
21672
|
+
if (held !== null && held.endedAt === null)
|
|
21673
|
+
return [];
|
|
21674
|
+
const pidless = rows.filter(row => row["exited_at"] === null && row["pid"] === null && row["containment"] === null && row["container"] === null && row["host"] === hostname());
|
|
21675
|
+
const others = rows.filter(row => !pidless.includes(row));
|
|
21676
|
+
if (pidless.length === 0 || others.length === 0 || others.some(row => row["exited_at"] === null))
|
|
21677
|
+
return [];
|
|
21678
|
+
const known = others.filter(row => row["pid"] !== null && row["host"] === hostname());
|
|
21679
|
+
if (known.length === 0 || known.some(row => processMayBeAlive(Number(row["pid"]), row["process_group"] === 1, { observedAt: row["observed_at"], finishedAt: run.finishedAt, exitedAt: row["exited_at"] })))
|
|
21680
|
+
return [];
|
|
21681
|
+
return pidless;
|
|
21682
|
+
}
|
|
21484
21683
|
/** `run settle` (an approver's last resort): every witness of a finished
|
|
21485
21684
|
* run that cannot be proven either way is recorded settled under the
|
|
21486
21685
|
* approver's reason, in the ledger. Refuses while anything of the run is
|
|
@@ -21508,7 +21707,7 @@ export class Store {
|
|
|
21508
21707
|
return alive(`run #${args.runId}'s process ${String(row["pid"])} belongs to ${String(row["host"])}; settle it there`);
|
|
21509
21708
|
continue;
|
|
21510
21709
|
}
|
|
21511
|
-
if (processMayBeAlive(Number(row["pid"]), row["process_group"] === 1, { observedAt: row["observed_at"], finishedAt: run.finishedAt }))
|
|
21710
|
+
if (processMayBeAlive(Number(row["pid"]), row["process_group"] === 1, { observedAt: row["observed_at"], finishedAt: run.finishedAt, exitedAt: row["exited_at"] }))
|
|
21512
21711
|
return alive(`run #${args.runId}'s process ${String(row["pid"])} is still running`);
|
|
21513
21712
|
}
|
|
21514
21713
|
for (const row of open) {
|
|
@@ -23653,6 +23852,8 @@ function readTaskRef(row) {
|
|
|
23653
23852
|
: null,
|
|
23654
23853
|
riskLevel: isRiskLevel(row["risk_level"]) ? row["risk_level"] : null,
|
|
23655
23854
|
routeOverrides: overridesFromJson(row["route_overrides_json"] === null || row["route_overrides_json"] === undefined ? null : String(row["route_overrides_json"])),
|
|
23855
|
+
sizing: sizingOfRow(row),
|
|
23856
|
+
routeTier: row["route_tier"] === null || row["route_tier"] === undefined ? null : String(row["route_tier"]),
|
|
23656
23857
|
planProvider: row["plan_provider"] === null || row["plan_provider"] === undefined
|
|
23657
23858
|
? null
|
|
23658
23859
|
: String(row["plan_provider"]),
|