@claudexor/orchestrator 3.5.0 → 3.8.0
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/dist/account-pool.d.ts +62 -0
- package/dist/account-pool.d.ts.map +1 -0
- package/dist/account-pool.js +103 -0
- package/dist/account-pool.js.map +1 -0
- package/dist/account-resolution.d.ts +88 -0
- package/dist/account-resolution.d.ts.map +1 -0
- package/dist/account-resolution.js +327 -0
- package/dist/account-resolution.js.map +1 -0
- package/dist/attemptOutputMarkers.d.ts +30 -0
- package/dist/attemptOutputMarkers.d.ts.map +1 -0
- package/dist/attemptOutputMarkers.js +31 -0
- package/dist/attemptOutputMarkers.js.map +1 -0
- package/dist/attemptTelemetry.d.ts +5 -4
- package/dist/attemptTelemetry.d.ts.map +1 -1
- package/dist/attemptTelemetry.js +4 -2
- package/dist/attemptTelemetry.js.map +1 -1
- package/dist/credential-cooldown.d.ts +131 -0
- package/dist/credential-cooldown.d.ts.map +1 -0
- package/dist/credential-cooldown.js +169 -0
- package/dist/credential-cooldown.js.map +1 -0
- package/dist/credential-differential.d.ts +81 -0
- package/dist/credential-differential.d.ts.map +1 -0
- package/dist/credential-differential.js +110 -0
- package/dist/credential-differential.js.map +1 -0
- package/dist/credential-preflight.d.ts +36 -0
- package/dist/credential-preflight.d.ts.map +1 -0
- package/dist/credential-preflight.js +78 -0
- package/dist/credential-preflight.js.map +1 -0
- package/dist/credential-profile-rotation.d.ts +80 -54
- package/dist/credential-profile-rotation.d.ts.map +1 -1
- package/dist/credential-profile-rotation.js +191 -163
- package/dist/credential-profile-rotation.js.map +1 -1
- package/dist/credential-profiles.d.ts +3 -49
- package/dist/credential-profiles.d.ts.map +1 -1
- package/dist/credential-profiles.js +2 -46
- package/dist/credential-profiles.js.map +1 -1
- package/dist/deepScanReducer.d.ts.map +1 -1
- package/dist/deepScanReducer.js +2 -1
- package/dist/deepScanReducer.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/laneStreamEvents.d.ts +54 -0
- package/dist/laneStreamEvents.d.ts.map +1 -0
- package/dist/laneStreamEvents.js +81 -0
- package/dist/laneStreamEvents.js.map +1 -0
- package/dist/modelGovernance.d.ts +35 -4
- package/dist/modelGovernance.d.ts.map +1 -1
- package/dist/modelGovernance.js +71 -27
- package/dist/modelGovernance.js.map +1 -1
- package/dist/orchestrator-credentials.d.ts +93 -0
- package/dist/orchestrator-credentials.d.ts.map +1 -0
- package/dist/orchestrator-credentials.js +162 -0
- package/dist/orchestrator-credentials.js.map +1 -0
- package/dist/orchestrator.d.ts +17 -30
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +293 -344
- package/dist/orchestrator.js.map +1 -1
- package/dist/plannerAttempt.d.ts.map +1 -1
- package/dist/plannerAttempt.js +2 -1
- package/dist/plannerAttempt.js.map +1 -1
- package/dist/rotation-predicate.d.ts +71 -0
- package/dist/rotation-predicate.d.ts.map +1 -0
- package/dist/rotation-predicate.js +29 -0
- package/dist/rotation-predicate.js.map +1 -0
- package/dist/routeContext.d.ts +4 -1
- package/dist/routeContext.d.ts.map +1 -1
- package/dist/routeContext.js +8 -3
- package/dist/routeContext.js.map +1 -1
- package/package.json +17 -17
package/dist/orchestrator.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { effectiveAuthPreference, observeNativeSessionEvent,
|
|
2
|
+
import { effectiveAuthPreference, observeNativeSessionEvent, resumeSessionForProfile, rotateSpecOnTypedLimit, selectedProfileAvailability, } from "./credential-profiles.js";
|
|
3
|
+
import { OrchestratorCredentials, rotatedSpecInLaneHome } from "./orchestrator-credentials.js";
|
|
4
|
+
import { accountPoolRows } from "./account-pool.js";
|
|
3
5
|
import { writeRunTelemetryArtifact } from "./runTelemetryWriter.js";
|
|
4
6
|
import { buildFileBackedSynthesisInput, materializeWinnerOutputs, stageFileBackedContext, writeCandidateAttemptArtifacts, } from "./candidateOutputs.js";
|
|
5
7
|
import { processAttemptUsage } from "./attemptUsage.js";
|
|
@@ -13,8 +15,8 @@ import { finalizePlanRun, runCouncilPlan, writePlanHarnessFailure, } from "./pla
|
|
|
13
15
|
import { runPlannerAttempt as executePlannerAttempt, } from "./plannerAttempt.js";
|
|
14
16
|
import { HarnessRunSpec, ModeKind as ModeKindSchema, QuotaSnapshot as QuotaSnapshotSchema, isBlocking, makeOutcomeFacts, normalizeUserOutputSchema, strictifyOutputSchema, estimateEffectiveAuthRoute, } from "@claudexor/schema";
|
|
15
17
|
import { globalConfigDir, loadConfig } from "@claudexor/config";
|
|
16
|
-
import { AnswerAssembly, CLAUDEXOR_BROWSER_ARTIFACT_SUBDIR, countsAsAgentProgress, HarnessUnavailableError, summarizeDiffPaths as diffStats, withInactivityWatchdog, } from "@claudexor/core";
|
|
17
|
-
import { assertRouteModelsAllowed } from "./modelGovernance.js";
|
|
18
|
+
import { acceptedTryOutput, AnswerAssembly, CLAUDEXOR_BROWSER_ARTIFACT_SUBDIR, countsAsAgentProgress, HarnessUnavailableError, summarizeDiffPaths as diffStats, withInactivityWatchdog, } from "@claudexor/core";
|
|
19
|
+
import { assertRouteModelsAllowed, runModelGovernedRoute } from "./modelGovernance.js";
|
|
18
20
|
import { authModeForCredentialRoute, authModeForPreference } from "./auth-route-classification.js";
|
|
19
21
|
import { governRouteEffort } from "./effortGovernance.js";
|
|
20
22
|
import { isFullAccess, RequestRequirementsResolver } from "./requestRequirements.js";
|
|
@@ -31,7 +33,8 @@ import { buildRevisePrompt } from "./revisePrompt.js";
|
|
|
31
33
|
import { cancelledResult, declaredFailure, failTerminally, guardAnnouncedRun, writeFailure, } from "./runTerminals.js";
|
|
32
34
|
import { budgetFailureRecord, classifyBudgetFailure } from "./budgetFailure.js";
|
|
33
35
|
import { assertOutputSchemaCompiles, finalizeStructuredOutput } from "./structuredOutput.js";
|
|
34
|
-
import {
|
|
36
|
+
import { dropDeltaPastBudget, emitPlanProgress, emitTransientExhausted, emitTransientRetryPlan, observeReadonlySpend, } from "./laneStreamEvents.js";
|
|
37
|
+
import { promptWithEngineConstraints, sleep, redactHarnessEvent, harnessEventPayload, safeErrorMessage, renderSummary, observeBudgetSignals, rotateOnStall, recordCleanAttemptMetrics, envInheritance, transientRetryPolicy, reviewerTimeoutMs, harnessInactivityTimeoutMs, observeAuthSwitch, emitPrimaryDivergence, emitPoolDegraded, deliveryRefusalFailure, winnerNeedsHuman, writeRaceDeliveryDecision, } from "./runSupport.js";
|
|
35
38
|
import { candidateStatusInRouteContext, resolveReadOnlyRouteContext, } from "./routeContext.js";
|
|
36
39
|
import { resolveAutoReviewerPanel, resolveExplicitReviewerPanel } from "./reviewerPanel.js";
|
|
37
40
|
import { ensureWriteModeGitBoundary } from "./git-precondition.js";
|
|
@@ -40,6 +43,7 @@ import { activePlanPointer, resolveContinuitySummary, workspaceAnchor, } from ".
|
|
|
40
43
|
import { runDiffReview } from "./diffReview.js";
|
|
41
44
|
import { rawScoutBundle, resolveDeepScanSynthesis, } from "./deepScanReducer.js";
|
|
42
45
|
import { classifyAdapterThrow, createAttemptTelemetry, observeAttemptTelemetry, setAttemptOutcome, telemetrySummary, toolWarnings, unrecoveredToolErrors, webUnsatisfied, } from "./attemptTelemetry.js";
|
|
46
|
+
import { newAttemptOutputMarkers } from "./attemptOutputMarkers.js";
|
|
43
47
|
import * as delegateFailure from "./delegationFailure.js";
|
|
44
48
|
import * as secretDiff from "./secretDiff.js";
|
|
45
49
|
import { dominantHarnessFailureCategory, harnessFailureNextActions } from "./harnessFailure.js";
|
|
@@ -332,11 +336,21 @@ export class Orchestrator {
|
|
|
332
336
|
const cfg = this.config(input.repoRoot)?.global;
|
|
333
337
|
const profile = quotaAdmission?.model === model
|
|
334
338
|
? quotaAdmission.profile
|
|
335
|
-
: await this.preflightProfile(input, harnessId, model, log, defaultRoute);
|
|
339
|
+
: await this.credentials.preflightProfile(input, harnessId, model, log, defaultRoute);
|
|
340
|
+
// Q3=A: the PAID api_key route serves ONLY the EXPLICIT api_key
|
|
341
|
+
// preference (the user's paid election, or the sole permitted service of
|
|
342
|
+
// an empty/exhausted pool) — never silently under auto, which terminalizes
|
|
343
|
+
// typed instead; quotaAdmission.route carries the admission verdict.
|
|
344
|
+
const forcedApiKeyRoute = profile === null &&
|
|
345
|
+
(quotaAdmission?.model === model
|
|
346
|
+
? quotaAdmission.route === "managed_api_key"
|
|
347
|
+
: this.credentials.poolApiKeyRoute(input, harnessId));
|
|
336
348
|
return {
|
|
337
349
|
// "auto" at ANY level falls through (thread turns send the thread default
|
|
338
350
|
// "auto" as a per-run value; it must not shadow a configured preference).
|
|
339
|
-
auth_preference:
|
|
351
|
+
auth_preference: forcedApiKeyRoute
|
|
352
|
+
? "api_key"
|
|
353
|
+
: effectiveAuthPreference(input.authPreference, cfg?.harnesses?.[harnessId]?.auth_preference, cfg?.routing?.auth_preference),
|
|
340
354
|
resume_session_id: resumeSessionForProfile(input.resumeSessions?.[harnessId], profile),
|
|
341
355
|
credential_profile: profile,
|
|
342
356
|
};
|
|
@@ -350,138 +364,26 @@ export class Orchestrator {
|
|
|
350
364
|
* Keyed by the run's REQUESTED credential profile — the same key the daemon's
|
|
351
365
|
* `resumeMap` lookup uses (INV-135), so record and resume land in one home.
|
|
352
366
|
*/
|
|
353
|
-
laneHomeEnvFor(input, harnessId
|
|
367
|
+
laneHomeEnvFor(input, harnessId,
|
|
368
|
+
// Keyed by the RESOLVED account (INV-135/137): the same key the recorded
|
|
369
|
+
// native session carries, so record and resume share one home.
|
|
370
|
+
resolvedProfileId) {
|
|
354
371
|
if (!input.threadId)
|
|
355
372
|
return null;
|
|
356
|
-
return new WorkspaceManager(input.repoRoot).laneHomeEnv(input.threadId, harnessId,
|
|
357
|
-
// The lane is keyed by the EFFECTIVE account (INV-135): an explicit pin,
|
|
358
|
-
// else null resolves the same home the recorded native session lives in.
|
|
359
|
-
this.effectiveProfileId(input, harnessId)).env;
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* The per-harness EFFECTIVE credential profile id (INV-135 accounts
|
|
363
|
-
* authority): an explicit per-run/per-thread pin wins; else null — POOL AUTO,
|
|
364
|
-
* the native/CLI login default subject (enabled profiles route only by
|
|
365
|
-
* explicit pin or quota rotation, never as a silent Active default).
|
|
366
|
-
*/
|
|
367
|
-
effectiveProfileId(input, _harnessId) {
|
|
368
|
-
return input.credentialProfileId ?? null;
|
|
369
|
-
}
|
|
370
|
-
/** Whether the native/CLI login is EXCLUDED from this harness's credential
|
|
371
|
-
* ladder (INV-135). When excluded, a harness with no effective profile has
|
|
372
|
-
* nothing routable and must refuse — never silently fall back into it. */
|
|
373
|
-
nativeCredentialsDisabled(repoRoot, harnessId) {
|
|
374
|
-
return (this.config(repoRoot)?.global.harnesses?.[harnessId]?.native_credentials_enabled === false);
|
|
375
|
-
}
|
|
376
|
-
resolveCredentialProfile(input, harnessId) {
|
|
377
|
-
const explicit = input.credentialProfileId ?? null;
|
|
378
|
-
const wanted = this.effectiveProfileId(input, harnessId);
|
|
379
|
-
if (!wanted)
|
|
380
|
-
return null;
|
|
381
|
-
const registry = this.config(input.repoRoot)?.global.credential_profiles ?? [];
|
|
382
|
-
try {
|
|
383
|
-
return resolveCredentialProfile(registry, wanted, harnessId);
|
|
384
|
-
}
|
|
385
|
-
catch (err) {
|
|
386
|
-
// With Active removed, `wanted` is always the explicit pin; keep the
|
|
387
|
-
// fail-closed guard so any future non-pin source still refuses loudly.
|
|
388
|
-
if (!explicit) {
|
|
389
|
-
throw new Error(`harness "${harnessId}" credential profile "${wanted}" is unusable: ${err instanceof Error ? err.message : String(err)}`);
|
|
390
|
-
}
|
|
391
|
-
throw err;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
/** The typed effective auth route for a SELECTED credential profile
|
|
395
|
-
* (round-18 #2): adapters execute strictly by credential_kind, so routing,
|
|
396
|
-
* billing classification, model truth, and quota lookup must share this
|
|
397
|
-
* one fact — never the default store's sources or a previous default-route
|
|
398
|
-
* metric. null = no profile selected or it does not resolve here. */
|
|
399
|
-
profileAuthRoute(input, harnessId) {
|
|
400
|
-
try {
|
|
401
|
-
const profile = this.resolveCredentialProfile(input, harnessId);
|
|
402
|
-
if (!profile)
|
|
403
|
-
return null;
|
|
404
|
-
return profile.credential_kind === "api_key" ? "api_key" : "local_session";
|
|
405
|
-
}
|
|
406
|
-
catch {
|
|
407
|
-
return null;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
profilePolicy(repoRoot, harnessId) {
|
|
411
|
-
const policy = this.config(repoRoot)?.global.harnesses?.[harnessId]?.profile_policy;
|
|
412
|
-
return policy ?? { limit_action: "fail", rotation_eligible: [], headroom_threshold: 0.9 };
|
|
413
|
-
}
|
|
414
|
-
/** The quota poller's authenticated vendor evidence for THIS decision epoch,
|
|
415
|
-
* read once so every profile in the epoch is judged against one observation
|
|
416
|
-
* set. This is what makes profile readiness at admission mean the same thing
|
|
417
|
-
* it means on the Accounts card (INV-135 honesty). */
|
|
418
|
-
vendorQuotaObservations() {
|
|
419
|
-
return {
|
|
420
|
-
snapshots: this.deps.quotaSnapshots?.() ?? [],
|
|
421
|
-
absences: this.deps.quotaAbsences?.() ?? [],
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
/** Fresh profile readiness for one rotation decision epoch. Accounts uses
|
|
425
|
-
* the same probe wrapper + vendor overlay + admission predicate when
|
|
426
|
-
* projecting next_up. */
|
|
427
|
-
async readyProfileIdsForRotation(input, harnessId, current, excluded = new Set()) {
|
|
428
|
-
const profiles = staticRotationCandidates({
|
|
429
|
-
registry: this.config(input.repoRoot)?.global.credential_profiles ?? [],
|
|
430
|
-
harnessId,
|
|
431
|
-
policy: this.profilePolicy(input.repoRoot, harnessId),
|
|
432
|
-
current,
|
|
433
|
-
excluded,
|
|
434
|
-
});
|
|
435
|
-
const adapter = this.deps.registry.get(harnessId);
|
|
436
|
-
const quota = this.vendorQuotaObservations();
|
|
437
|
-
const entries = await Promise.all(profiles.map(async (profile) => ({
|
|
438
|
-
profile,
|
|
439
|
-
// Rotating INTO a profile the vendor has already rejected would spend a
|
|
440
|
-
// whole attempt to rediscover the 401 the poller reported a minute ago.
|
|
441
|
-
status: vendorVerifiedProfileStatus(await probeCredentialProfileStatus(profile, adapter?.probeCredentialProfile?.bind(adapter)), quota),
|
|
442
|
-
})));
|
|
443
|
-
return new Set(entries
|
|
444
|
-
.filter(({ profile, status }) => profileStatusAdmits(profile, status))
|
|
445
|
-
.map(({ profile }) => profile.profile_id));
|
|
446
|
-
}
|
|
447
|
-
async preflightProfile(input, harnessId, model, log, defaultRoute) {
|
|
448
|
-
const profile = this.resolveCredentialProfile(input, harnessId);
|
|
449
|
-
const policy = this.profilePolicy(input.repoRoot, harnessId);
|
|
450
|
-
const registry = this.config(input.repoRoot)?.global.credential_profiles ?? [];
|
|
451
|
-
const snapshots = this.deps.quotaSnapshots?.() ?? [];
|
|
452
|
-
const emit = (type, payload) => log?.emit(type, payload);
|
|
453
|
-
const breach = profileHeadroomBreach(snapshots, harnessId, profile?.profile_id ?? null, policy.headroom_threshold, model);
|
|
454
|
-
const readyProfileIds = policy.limit_action === "rotate" &&
|
|
455
|
-
breach !== null &&
|
|
456
|
-
(profile !== null || defaultRoute === "local_session")
|
|
457
|
-
? await this.readyProfileIdsForRotation(input, harnessId, profile)
|
|
458
|
-
: new Set();
|
|
459
|
-
if (!profile) {
|
|
460
|
-
// Unpinned runs (INV-135 auto-balance): under `rotate`, a fresh
|
|
461
|
-
// default-subject headroom breach starts on the next eligible
|
|
462
|
-
// subscription profile instead; `fail`/`ask` change nothing.
|
|
463
|
-
return preflightDefaultSubject({
|
|
464
|
-
harnessId,
|
|
465
|
-
policy,
|
|
466
|
-
registry,
|
|
467
|
-
snapshots,
|
|
468
|
-
readyProfileIds,
|
|
469
|
-
defaultRoute,
|
|
470
|
-
model,
|
|
471
|
-
emit,
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
return preflightCredentialProfile({
|
|
475
|
-
profile,
|
|
476
|
-
harnessId,
|
|
477
|
-
policy,
|
|
478
|
-
registry,
|
|
479
|
-
snapshots,
|
|
480
|
-
readyProfileIds,
|
|
481
|
-
model,
|
|
482
|
-
emit,
|
|
483
|
-
});
|
|
373
|
+
return new WorkspaceManager(input.repoRoot).laneHomeEnv(input.threadId, harnessId, resolvedProfileId).env;
|
|
484
374
|
}
|
|
375
|
+
/** Credential-resolution cluster (INV-135), split to orchestrator-credentials.ts. */
|
|
376
|
+
credentials = new OrchestratorCredentials({
|
|
377
|
+
// Accessor functions: the field initializer runs before the constructor
|
|
378
|
+
// assigns this.deps, so every read defers to call time.
|
|
379
|
+
config: (repoRoot) => this.config(repoRoot),
|
|
380
|
+
registry: () => this.deps.registry,
|
|
381
|
+
quotaSnapshots: () => this.deps.quotaSnapshots?.() ?? [],
|
|
382
|
+
quotaAbsences: () => this.deps.quotaAbsences?.() ?? [],
|
|
383
|
+
credentialUnusable: () => this.deps.credentialUnusable?.() ?? [],
|
|
384
|
+
recordCredentialUnusable: (obs) => this.deps.recordCredentialUnusable?.(obs),
|
|
385
|
+
authPreferenceForHarness: (repoRoot, harnessId, runPreference) => this.authPreferenceForHarness(repoRoot, harnessId, runPreference),
|
|
386
|
+
});
|
|
485
387
|
/**
|
|
486
388
|
* Resolve candidate adapters: explicit `--harness`, else available real harnesses, then
|
|
487
389
|
* **capability-gate** to those that can actually produce work for `intent` (e.g. a
|
|
@@ -547,10 +449,18 @@ export class Orchestrator {
|
|
|
547
449
|
: `credential profile "${input.credentialProfileId}" belongs to unavailable harness(es): ${registered.map((p) => p.harness_id).join(", ")}`);
|
|
548
450
|
}
|
|
549
451
|
else {
|
|
550
|
-
// Auto-pools take
|
|
551
|
-
// readiness; a key string or degraded route is visible but not
|
|
452
|
+
// Auto-pools take doctor-OK harnesses (BIBLE §2: doctor decides
|
|
453
|
+
// readiness; a key string or degraded route is visible but not
|
|
454
|
+
// routable) PLUS harnesses with enabled account rows: the default
|
|
455
|
+
// doctor only sees the default store, and a signed-in registry row is
|
|
456
|
+
// invisible to it (cursor after the host-Keychain retirement, or a
|
|
457
|
+
// named-rows-only claude/codex install). Row-bearing lanes still pass
|
|
458
|
+
// the per-lane row probe below before they are admitted.
|
|
459
|
+
const registry = this.config(input.repoRoot)?.global.credential_profiles ?? [];
|
|
552
460
|
ids = statuses
|
|
553
|
-
.filter((s) => s.manifest?.kind !== "fake" &&
|
|
461
|
+
.filter((s) => s.manifest?.kind !== "fake" &&
|
|
462
|
+
((s.status === "ok" && s.enabledIntents.includes(intent)) ||
|
|
463
|
+
accountPoolRows(registry, s.id).length > 0))
|
|
554
464
|
.map((s) => s.id);
|
|
555
465
|
if (ids.length === 0) {
|
|
556
466
|
throw new HarnessUnavailableError("no doctor-ok harness for this mode; install/login codex/claude/cursor/opencode (see `claudexor doctor`), or pass --harness explicitly");
|
|
@@ -598,7 +508,7 @@ export class Orchestrator {
|
|
|
598
508
|
};
|
|
599
509
|
// One observation set for the whole admission pass, so two lanes cannot be
|
|
600
510
|
// judged against different vendor epochs.
|
|
601
|
-
const vendorQuota = this.vendorQuotaObservations();
|
|
511
|
+
const vendorQuota = this.credentials.vendorQuotaObservations();
|
|
602
512
|
for (const id of ids) {
|
|
603
513
|
const adapter = this.deps.registry.get(id);
|
|
604
514
|
if (!adapter) {
|
|
@@ -621,13 +531,22 @@ export class Orchestrator {
|
|
|
621
531
|
dropLane(id, "settings", why);
|
|
622
532
|
continue;
|
|
623
533
|
}
|
|
624
|
-
//
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
534
|
+
// Incomplete unified-accounts migration refuses THIS harness only.
|
|
535
|
+
const migrationBlock = this.deps.accountsMigrationGate?.(id) ?? null;
|
|
536
|
+
if (migrationBlock) {
|
|
537
|
+
dropLane(id, "credential", migrationBlock.reason);
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
// INV-135: native login excluded + no rows + no pin ⇒ only the
|
|
541
|
+
// EXPLICITLY-requested paid route can serve (Q3=A — `auto` never
|
|
542
|
+
// silently takes the API key); everything else refuses/drops here,
|
|
543
|
+
// never a silent spawn INTO the disabled login.
|
|
544
|
+
if (this.credentials.effectiveProfileId(input, id) === null &&
|
|
545
|
+
this.credentials.nativeCredentialsDisabled(input.repoRoot, id) &&
|
|
546
|
+
accountPoolRows(this.config(input.repoRoot)?.global.credential_profiles ?? [], id)
|
|
547
|
+
.length === 0 &&
|
|
548
|
+
this.authPreferenceForHarness(input.repoRoot, id, input.authPreference) !== "api_key") {
|
|
549
|
+
const why = `${id} has no routable credential: the CLI login is disabled (harnesses.${id}.native_credentials_enabled=false), no account is signed in, and the paid API-key route requires the explicit api_key preference (pin an account with --profile or connect one)`;
|
|
631
550
|
dropLane(id, "credential", why);
|
|
632
551
|
continue;
|
|
633
552
|
}
|
|
@@ -656,37 +575,64 @@ export class Orchestrator {
|
|
|
656
575
|
// routing truth). Capability/manifest gating above still applies.
|
|
657
576
|
let profileAdmitted = false;
|
|
658
577
|
const profileAdapter = this.deps.registry.get(id);
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
578
|
+
const profileProbe = profileAdapter?.probeCredentialProfile?.bind(profileAdapter);
|
|
579
|
+
const explicitPin = this.credentials.effectiveProfileId(input, id);
|
|
580
|
+
// The account candidates whose readiness may overlay the default-store
|
|
581
|
+
// verdict: the explicit pin when set; otherwise — row-aware UNPINNED
|
|
582
|
+
// admission — the thread-bound row followed by the harness's enabled
|
|
583
|
+
// pool rows, consulted only when the default doctor did not already
|
|
584
|
+
// admit the lane. A signed-in registry row is invisible to the default
|
|
585
|
+
// doctor (cursor after the host-Keychain retirement, or a named-rows-
|
|
586
|
+
// only claude/codex install), so an unpinned run must be admitted on
|
|
587
|
+
// row readiness exactly the way explicit pins already are.
|
|
588
|
+
const rowCandidateIds = [];
|
|
589
|
+
if (explicitPin) {
|
|
590
|
+
rowCandidateIds.push(explicitPin);
|
|
591
|
+
}
|
|
592
|
+
else if (status.status !== "ok") {
|
|
593
|
+
const pool = accountPoolRows(this.config(input.repoRoot)?.global.credential_profiles ?? [], id);
|
|
594
|
+
const bound = input.threadAccountBindings?.[id] ?? null;
|
|
595
|
+
rowCandidateIds.push(...(bound && pool.some((row) => row.profile_id === bound) ? [bound] : []), ...pool.map((row) => row.profile_id).filter((rowId) => rowId !== bound));
|
|
596
|
+
}
|
|
597
|
+
let lastRowVerdict = null;
|
|
598
|
+
for (const candidateId of rowCandidateIds) {
|
|
599
|
+
const verdict = await selectedProfileAvailability({
|
|
600
|
+
registry: this.config(input.repoRoot)?.global.credential_profiles ?? [],
|
|
601
|
+
profileId: candidateId,
|
|
602
|
+
harnessId: id,
|
|
603
|
+
probe: profileProbe,
|
|
604
|
+
// `verification: passed` from the local store only means a login file
|
|
605
|
+
// is present. The poller's authenticated vendor call is the only
|
|
606
|
+
// liveness evidence we have, and admission is the surface that must
|
|
607
|
+
// act on it — otherwise the run dispatches into a revoked token.
|
|
608
|
+
quota: vendorQuota,
|
|
609
|
+
});
|
|
610
|
+
if (verdict === "available") {
|
|
673
611
|
profileAdmitted = true;
|
|
674
|
-
|
|
675
|
-
if (status.status !== "ok") {
|
|
676
|
-
status = {
|
|
677
|
-
...status,
|
|
678
|
-
status: "degraded",
|
|
679
|
-
enabledIntents: capabilityIntents(manifest.capabilities),
|
|
680
|
-
};
|
|
681
|
-
statusById.set(id, status);
|
|
682
|
-
}
|
|
612
|
+
break;
|
|
683
613
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
614
|
+
lastRowVerdict = verdict;
|
|
615
|
+
}
|
|
616
|
+
if (profileAdmitted) {
|
|
617
|
+
// A valid account row restores manifest intent truth when the default store failed.
|
|
618
|
+
if (status.status !== "ok") {
|
|
619
|
+
status = {
|
|
620
|
+
...status,
|
|
621
|
+
status: "degraded",
|
|
622
|
+
enabledIntents: capabilityIntents(manifest.capabilities),
|
|
623
|
+
};
|
|
624
|
+
statusById.set(id, status);
|
|
688
625
|
}
|
|
689
626
|
}
|
|
627
|
+
else if (explicitPin) {
|
|
628
|
+
// An explicit pin that is not ready refuses/drops the lane; a
|
|
629
|
+
// not-ready UNPINNED row merely leaves the default doctor verdict in
|
|
630
|
+
// charge (the gates below keep the refusal honest when neither a row
|
|
631
|
+
// nor a default login exists).
|
|
632
|
+
const why = `${id} credential profile is not ready: ${lastRowVerdict}`;
|
|
633
|
+
dropLane(id, "credential", why);
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
690
636
|
if (status.status === "unavailable" && !profileAdmitted) {
|
|
691
637
|
const why = `${id} is unavailable${status.reasons.length ? `: ${status.reasons.join("; ")}` : ""}`;
|
|
692
638
|
dropLane(id, "doctor", why);
|
|
@@ -749,7 +695,7 @@ export class Orchestrator {
|
|
|
749
695
|
knownModels: manifest.capabilities.known_models,
|
|
750
696
|
// A selected profile's credential_kind IS the route (round-18 #2);
|
|
751
697
|
// the default store's sources apply only to profile-less runs.
|
|
752
|
-
authRouteEstimate: this.profileAuthRoute(input, id) ??
|
|
698
|
+
authRouteEstimate: this.credentials.profileAuthRoute(input, id) ??
|
|
753
699
|
estimateEffectiveAuthRoute(this.authPreferenceForHarness(input.repoRoot, id, input.authPreference), status.authSources),
|
|
754
700
|
quotaAdmission: { model: null, profile: null, route: null },
|
|
755
701
|
supportsSynthesize: manifest.capabilities.synthesize,
|
|
@@ -790,20 +736,41 @@ export class Orchestrator {
|
|
|
790
736
|
// particular, an opt-in default-subject rotation has to select its ready
|
|
791
737
|
// profile before the budget router filters the exhausted default away.
|
|
792
738
|
// The same resolved profile is reused by the first spec build below.
|
|
793
|
-
const
|
|
739
|
+
const quotaPrepared = await Promise.all(pool.map(async (routed) => {
|
|
794
740
|
const model = input.models?.[routed.adapter.id] ?? routed.settings?.defaultModel ?? null;
|
|
795
|
-
|
|
741
|
+
let profile;
|
|
742
|
+
try {
|
|
743
|
+
profile = await this.credentials.preflightProfile(input, routed.adapter.id, model, log, routed.authRouteEstimate);
|
|
744
|
+
}
|
|
745
|
+
catch (err) {
|
|
746
|
+
// Q3=A: ONE harness's exhausted account pool must not kill an AUTO
|
|
747
|
+
// multi-harness run — the lane drops with the typed disclosure and a
|
|
748
|
+
// surviving sibling serves. An EXPLICIT pool (and the last remaining
|
|
749
|
+
// auto lane) keeps the typed `credential_pool_exhausted` terminal so
|
|
750
|
+
// its code + earliest reset reach failure.yaml intact.
|
|
751
|
+
if (!explicitPool &&
|
|
752
|
+
pool.length > 1 &&
|
|
753
|
+
err.code === "credential_pool_exhausted") {
|
|
754
|
+
dropLane(routed.adapter.id, "credential", safeErrorMessage(err));
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
throw err;
|
|
758
|
+
}
|
|
796
759
|
const route = profile
|
|
797
760
|
? profile.credential_kind === "api_key"
|
|
798
761
|
? "managed_api_key"
|
|
799
762
|
: "vendor_native"
|
|
800
|
-
:
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
763
|
+
: // The explicitly-opted PAID route (Q3=A) classifies as managed_api_key.
|
|
764
|
+
this.credentials.poolApiKeyRoute(input, routed.adapter.id)
|
|
765
|
+
? "managed_api_key"
|
|
766
|
+
: routed.authRouteEstimate === "api_key"
|
|
767
|
+
? "managed_api_key"
|
|
768
|
+
: routed.authRouteEstimate === "local_session"
|
|
769
|
+
? "vendor_native"
|
|
770
|
+
: null;
|
|
805
771
|
return { ...routed, quotaAdmission: { model, profile, route } };
|
|
806
772
|
}));
|
|
773
|
+
const quotaPreparedPool = quotaPrepared.filter((routed) => routed !== null);
|
|
807
774
|
const ordered = this.orderPool(quotaPreparedPool, input, intent, statusById, ledger, runId);
|
|
808
775
|
if (ordered.length === 0) {
|
|
809
776
|
throw new HarnessUnavailableError(`no harness remains eligible for '${intent}' after budget and quota routing`);
|
|
@@ -883,7 +850,7 @@ export class Orchestrator {
|
|
|
883
850
|
}
|
|
884
851
|
}
|
|
885
852
|
// Strict pre-run model gate (INV-104) — see modelGovernance.ts.
|
|
886
|
-
await assertRouteModelsAllowed(out, input.models, this.execRootOf(input));
|
|
853
|
+
await assertRouteModelsAllowed(out, input.models, this.execRootOf(input), (id) => routeContext ? (routeContext.envForHarness?.(id) ?? routeContext.env) : undefined);
|
|
887
854
|
return out;
|
|
888
855
|
}
|
|
889
856
|
/**
|
|
@@ -1347,7 +1314,7 @@ export class Orchestrator {
|
|
|
1347
1314
|
adapter: this.deps.registry.get(harnessId),
|
|
1348
1315
|
credentialProfile: sessionFields.credential_profile,
|
|
1349
1316
|
authPreference: sessionFields.auth_preference ?? "auto",
|
|
1350
|
-
laneEnv: this.laneHomeEnvFor(runInput, harnessId) ?? {},
|
|
1317
|
+
laneEnv: this.laneHomeEnvFor(runInput, harnessId, profileId) ?? {},
|
|
1351
1318
|
envInheritance: envInheritance(this.config(runInput.repoRoot)),
|
|
1352
1319
|
signal: runInput.signal,
|
|
1353
1320
|
});
|
|
@@ -1501,13 +1468,12 @@ export class Orchestrator {
|
|
|
1501
1468
|
let cost = 0;
|
|
1502
1469
|
let costEstimated = false;
|
|
1503
1470
|
let harnessErrored = false;
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
let deltaCutoffDisclosed = false;
|
|
1471
|
+
let poolExhausted = null; // A5: typed pool-exhausted refusal
|
|
1472
|
+
const deltaFlood = { count: 0, disclosed: false }; // W-C4 per-attempt delta budget
|
|
1507
1473
|
// QA-024: emit the belt-failure disclosure event at most once per attempt.
|
|
1508
1474
|
let beltFailureDisclosed = false;
|
|
1509
1475
|
const errors = [];
|
|
1510
|
-
|
|
1476
|
+
let answer = new AnswerAssembly();
|
|
1511
1477
|
const retryPolicy = transientRetryPolicy(this.config(contract.repo.root));
|
|
1512
1478
|
// QA-024: the delegation belt is the ONLY engine-owned extra MCP server
|
|
1513
1479
|
// injected into an agent lane (the browser MCP rides its own field), so its
|
|
@@ -1533,6 +1499,11 @@ export class Orchestrator {
|
|
|
1533
1499
|
}
|
|
1534
1500
|
try {
|
|
1535
1501
|
for (let nativeTry = 0; !signal?.aborted; nativeTry += 1) {
|
|
1502
|
+
// A3 per-try isolation: neither output nor progress markers leak across tries.
|
|
1503
|
+
if (nativeTry > 0) {
|
|
1504
|
+
answer = new AnswerAssembly();
|
|
1505
|
+
telemetry.outputMarkers = newAttemptOutputMarkers();
|
|
1506
|
+
}
|
|
1536
1507
|
const clearFileBackedContext = stageFileBackedContext(envelope.worktree_path, fileBackedContext);
|
|
1537
1508
|
const runSpec = nativeTry === 0
|
|
1538
1509
|
? spec
|
|
@@ -1552,7 +1523,7 @@ export class Orchestrator {
|
|
|
1552
1523
|
const rateLimitStart = telemetry.rateLimits.length;
|
|
1553
1524
|
let rawPatch = null;
|
|
1554
1525
|
try {
|
|
1555
|
-
const watched = withInactivityWatchdog(
|
|
1526
|
+
const watched = withInactivityWatchdog(runModelGovernedRoute(routed, runSpec), {
|
|
1556
1527
|
timeoutMs: inactivityMs,
|
|
1557
1528
|
countsAsProgress: countsAsAgentProgress,
|
|
1558
1529
|
onTimeout: () => {
|
|
@@ -1572,27 +1543,8 @@ export class Orchestrator {
|
|
|
1572
1543
|
if (ev.type === "patch_produced")
|
|
1573
1544
|
continue;
|
|
1574
1545
|
const safeEv = redactHarnessEvent(ev);
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
// bound. Delta messages are DISPLAY-only (the complete message
|
|
1578
|
-
// still follows and carries the authoritative text), so past a
|
|
1579
|
-
// per-attempt budget we DROP further deltas and disclose the
|
|
1580
|
-
// cutoff ONCE — the final answer is unaffected.
|
|
1581
|
-
if (safeEv.type === "message" && safeEv.payload?.["delta"] === true) {
|
|
1582
|
-
deltaCount += 1;
|
|
1583
|
-
if (deltaCount > Orchestrator.MAX_DELTAS_PER_ATTEMPT) {
|
|
1584
|
-
if (!deltaCutoffDisclosed) {
|
|
1585
|
-
deltaCutoffDisclosed = true;
|
|
1586
|
-
log?.emit("harness.event", {
|
|
1587
|
-
harness_id: adapter.id,
|
|
1588
|
-
attempt_id: attemptId,
|
|
1589
|
-
type: "status",
|
|
1590
|
-
title: `live delta stream capped at ${Orchestrator.MAX_DELTAS_PER_ATTEMPT} chunks; the complete message still lands`,
|
|
1591
|
-
});
|
|
1592
|
-
}
|
|
1593
|
-
continue; // drop this delta; never journal past the budget
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1546
|
+
if (dropDeltaPastBudget(safeEv, deltaFlood, Orchestrator.MAX_DELTAS_PER_ATTEMPT, (t, p) => log?.emit(t, p), adapter.id, attemptId))
|
|
1547
|
+
continue;
|
|
1596
1548
|
safeInvoke(onHarnessEvent, safeEv);
|
|
1597
1549
|
// In-place turns run in the live tree under the native environment, so
|
|
1598
1550
|
// the session they emit IS reachable for the next turn: record it. An
|
|
@@ -1619,15 +1571,7 @@ export class Orchestrator {
|
|
|
1619
1571
|
reason: "mcp_server_failed_to_start",
|
|
1620
1572
|
});
|
|
1621
1573
|
}
|
|
1622
|
-
|
|
1623
|
-
// progress as a run event (LAST WINS; the UI renders the latest).
|
|
1624
|
-
if (safeEv.plan_progress) {
|
|
1625
|
-
log?.emit("plan.progress", {
|
|
1626
|
-
attempt_id: attemptId,
|
|
1627
|
-
harness_id: adapter.id,
|
|
1628
|
-
items: safeEv.plan_progress.items,
|
|
1629
|
-
});
|
|
1630
|
-
}
|
|
1574
|
+
emitPlanProgress((t, p) => log?.emit(t, p), adapter.id, attemptId, safeEv);
|
|
1631
1575
|
if (safeEv.type === "usage") {
|
|
1632
1576
|
const usage = processAttemptUsage({
|
|
1633
1577
|
event: safeEv,
|
|
@@ -1683,38 +1627,53 @@ export class Orchestrator {
|
|
|
1683
1627
|
}
|
|
1684
1628
|
const newTransients = telemetry.transientFailures.slice(transientStart);
|
|
1685
1629
|
const transient = newTransients.at(-1) ?? null;
|
|
1686
|
-
// #31: the centralized retry gate reads the classified `retryable
|
|
1687
|
-
// bare "saw any transient" boolean.
|
|
1630
|
+
// #31: the centralized retry gate reads the classified `retryable` verdict.
|
|
1688
1631
|
const sawRetryable = newTransients.some((f) => f.retryable);
|
|
1689
1632
|
const sawTypedLimit = telemetry.rateLimits.length > rateLimitStart;
|
|
1690
1633
|
const currentDiff = await wsm.diff(envelope);
|
|
1691
|
-
const
|
|
1692
|
-
|
|
1693
|
-
//
|
|
1694
|
-
// session under the next profile with provenance (vendor_limit_rejected).
|
|
1634
|
+
const deliverableEmpty = currentDiff.trim().length === 0 && answer.text().length === 0;
|
|
1635
|
+
// W5.4 + A2 failover: a typed-limit hit OR a structural pre-progress
|
|
1636
|
+
// death rebuilds the spec on a NEW session under the next profile.
|
|
1695
1637
|
if (harnessErrored && runInput && !signal?.aborted) {
|
|
1696
|
-
const
|
|
1697
|
-
const readyProfileIds = sawTypedLimit && deliverableEmpty && rotationPolicy.limit_action === "rotate"
|
|
1698
|
-
? await this.readyProfileIdsForRotation(runInput, adapter.id, spec.credential_profile ?? null, triedProfiles)
|
|
1699
|
-
: new Set();
|
|
1700
|
-
const rotated = rotateSpecOnTypedLimit({
|
|
1638
|
+
const rotated = await rotateSpecOnTypedLimit({
|
|
1701
1639
|
spec,
|
|
1702
1640
|
harnessId: adapter.id,
|
|
1703
1641
|
attemptId,
|
|
1704
|
-
policy:
|
|
1642
|
+
policy: this.credentials.profilePolicy(contract.repo.root, adapter.id),
|
|
1705
1643
|
registry: this.config(contract.repo.root)?.global.credential_profiles ?? [],
|
|
1706
1644
|
snapshots: this.deps.quotaSnapshots?.() ?? [],
|
|
1707
|
-
|
|
1645
|
+
probeReadyProfiles: () => this.credentials.readyProfileIdsForRotation(runInput, adapter.id, spec.credential_profile ?? null, triedProfiles, spec.model_hint ?? null),
|
|
1646
|
+
...this.credentials.rotationObservations(adapter, spec, newTransients),
|
|
1708
1647
|
triedProfiles,
|
|
1648
|
+
markers: telemetry.outputMarkers,
|
|
1709
1649
|
sawTypedLimit,
|
|
1710
|
-
|
|
1650
|
+
sawRetryable,
|
|
1651
|
+
attemptErrored: harnessErrored,
|
|
1652
|
+
// Rotation evidence reads the POLICY-accepted try output: refusal
|
|
1653
|
+
// prose arriving as mid-stream MESSAGE events (claude org-disabled)
|
|
1654
|
+
// is no deliverable; the transient gate keeps RAW deliverableEmpty.
|
|
1655
|
+
deliverableEmpty: currentDiff.trim().length === 0 &&
|
|
1656
|
+
acceptedTryOutput(answer, harnessErrored).length === 0,
|
|
1657
|
+
workspaceDiffNonEmpty: currentDiff.trim().length > 0,
|
|
1711
1658
|
lastLimit: telemetry.rateLimits.at(-1) ?? null,
|
|
1712
1659
|
emit: (type, payload) => log?.emit(type, payload),
|
|
1713
1660
|
newSessionId: () => newId("ses"),
|
|
1714
1661
|
defaultRouteWasVendorNative: routed.authRouteEstimate === "local_session",
|
|
1662
|
+
// D-U6: an explicit pin never rotates; pool-selected rows do.
|
|
1663
|
+
pinned: runInput.credentialProfileId != null,
|
|
1715
1664
|
});
|
|
1665
|
+
// A5 ordering: an exhausted pool terminalizes TYPED here, BEFORE the
|
|
1666
|
+
// transient gate below burns same-profile retries (limits classify
|
|
1667
|
+
// as retryable) on the already-refused subject.
|
|
1668
|
+
if (rotated && "poolExhausted" in rotated) {
|
|
1669
|
+
poolExhausted = rotated.poolExhausted;
|
|
1670
|
+
errors.push(safeErrorMessage(poolExhausted));
|
|
1671
|
+
break;
|
|
1672
|
+
}
|
|
1716
1673
|
if (rotated) {
|
|
1717
|
-
|
|
1674
|
+
// INV-137: the rotated row runs in ITS OWN lane home (no-op for
|
|
1675
|
+
// isolated envelopes and native-env in-place turns).
|
|
1676
|
+
spec = rotatedSpecInLaneHome(spec, rotated, (id) => this.laneHomeEnvFor(runInput, adapter.id, id), runInput.credentialProfileId ?? null);
|
|
1718
1677
|
applied = appliedNow();
|
|
1719
1678
|
errors.length = 0;
|
|
1720
1679
|
harnessErrored = false;
|
|
@@ -1727,21 +1686,7 @@ export class Orchestrator {
|
|
|
1727
1686
|
nativeTry >= retryPolicy.maxRetries ||
|
|
1728
1687
|
signal?.aborted)
|
|
1729
1688
|
break;
|
|
1730
|
-
const
|
|
1731
|
-
const delayMs = transientRetryDelayMs(transient?.retryDelayMs ?? null, retryPolicy, nativeTry);
|
|
1732
|
-
log?.emit("route.transient.detected", {
|
|
1733
|
-
harness_id: adapter.id,
|
|
1734
|
-
attempt_id: attemptId,
|
|
1735
|
-
kind: transient?.kind ?? "unknown",
|
|
1736
|
-
category: transient?.category ?? "unknown_harness_error",
|
|
1737
|
-
native_try: nativeTry + 1,
|
|
1738
|
-
});
|
|
1739
|
-
log?.emit("route.transient.retry_scheduled", {
|
|
1740
|
-
harness_id: adapter.id,
|
|
1741
|
-
attempt_id: attemptId,
|
|
1742
|
-
retry: nextTry,
|
|
1743
|
-
delay_ms: delayMs,
|
|
1744
|
-
});
|
|
1689
|
+
const delayMs = emitTransientRetryPlan((t, p) => log?.emit(t, p), adapter.id, attemptId, transient, nativeTry, retryPolicy);
|
|
1745
1690
|
errors.length = 0;
|
|
1746
1691
|
harnessErrored = false;
|
|
1747
1692
|
await sleep(delayMs);
|
|
@@ -1750,22 +1695,18 @@ export class Orchestrator {
|
|
|
1750
1695
|
finally {
|
|
1751
1696
|
signal?.removeEventListener("abort", onAbort);
|
|
1752
1697
|
}
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
category: telemetry.transientFailures.at(-1)?.category ?? "unknown_harness_error",
|
|
1758
|
-
retries: retryPolicy.maxRetries,
|
|
1759
|
-
});
|
|
1698
|
+
// A pool-exhausted terminal is rotation's verdict, not the transient
|
|
1699
|
+
// machinery's — no `route.transient.exhausted` rides along with it.
|
|
1700
|
+
if (harnessErrored && !poolExhausted) {
|
|
1701
|
+
emitTransientExhausted((t, p) => log?.emit(t, p), adapter.id, attemptId, telemetry, retryPolicy.maxRetries);
|
|
1760
1702
|
}
|
|
1761
1703
|
const attemptStreamEndedMs = Date.now();
|
|
1762
1704
|
if (webUnsatisfied(telemetry)) {
|
|
1763
1705
|
errors.push(webEvidenceFailure(telemetry.web));
|
|
1764
1706
|
}
|
|
1765
|
-
// D-16:
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
});
|
|
1707
|
+
// D-16: answer.md persists only the deliverable. A3: an errored attempt
|
|
1708
|
+
// with no typed final contributes no answer material (acceptedTryOutput).
|
|
1709
|
+
const unwrapped = unwrapWorkReportEnvelope(acceptedTryOutput(answer, harnessErrored), workReportMode, { sideToolReport: telemetry.sideToolWorkReport ?? undefined });
|
|
1769
1710
|
const redacted = redactSecrets(unwrapped.deliverable);
|
|
1770
1711
|
const candidateAnswer = redacted.trim().length > 0 ? redacted : undefined;
|
|
1771
1712
|
const { diff, refusal: secretDiffRefusal } = await secretDiff.quarantineCandidateWorkspace(wsm, envelope, inPlaceEnvelope, candidateAnswer);
|
|
@@ -1892,6 +1833,8 @@ export class Orchestrator {
|
|
|
1892
1833
|
errors: errors.slice(0, 8),
|
|
1893
1834
|
telemetry,
|
|
1894
1835
|
...(secretDiffRefusal ? { secretDiffRefusal } : {}),
|
|
1836
|
+
// A5: the typed refusal survives NORMAL attempt finalization (no throw).
|
|
1837
|
+
...(poolExhausted ? { declaredFailure: declaredFailure(poolExhausted) } : {}),
|
|
1895
1838
|
outcomeClass: finalized.outcomeClass,
|
|
1896
1839
|
applied,
|
|
1897
1840
|
};
|
|
@@ -2313,7 +2256,7 @@ export class Orchestrator {
|
|
|
2313
2256
|
error: message,
|
|
2314
2257
|
phase: infraPhase,
|
|
2315
2258
|
});
|
|
2316
|
-
store.writeYaml(join(paths.attemptsDir, slot.attemptId, "attempt.yaml"), AC.attemptFailureRecord(slot.attemptId, adapter.id, failureCost, infraPhase, message, appliedAttemptFacts(harnessHome, slot.routed.adapterAccess, this.effectiveProfileId(input, adapter.id))));
|
|
2259
|
+
store.writeYaml(join(paths.attemptsDir, slot.attemptId, "attempt.yaml"), AC.attemptFailureRecord(slot.attemptId, adapter.id, failureCost, infraPhase, message, appliedAttemptFacts(harnessHome, slot.routed.adapterAccess, this.credentials.effectiveProfileId(input, adapter.id))));
|
|
2317
2260
|
runsBySlot[slotIdx] = {
|
|
2318
2261
|
attemptId: slot.attemptId,
|
|
2319
2262
|
harnessId: adapter.id,
|
|
@@ -2334,7 +2277,7 @@ export class Orchestrator {
|
|
|
2334
2277
|
// alive past this catch; `message` alone would force the terminal to
|
|
2335
2278
|
// read prose back out.
|
|
2336
2279
|
...(declared.code ? { declaredFailure: declared } : {}),
|
|
2337
|
-
applied: appliedAttemptFacts(harnessHome, slot.routed.adapterAccess, this.effectiveProfileId(input, adapter.id)),
|
|
2280
|
+
applied: appliedAttemptFacts(harnessHome, slot.routed.adapterAccess, this.credentials.effectiveProfileId(input, adapter.id)),
|
|
2338
2281
|
};
|
|
2339
2282
|
}
|
|
2340
2283
|
finally {
|
|
@@ -3466,6 +3409,8 @@ export class Orchestrator {
|
|
|
3466
3409
|
// carry their route-specific settlement from runCandidateInEnvelope.
|
|
3467
3410
|
const failureCost = AC.attemptFailureCost(err, "attempt-error");
|
|
3468
3411
|
const message = safeErrorMessage(err);
|
|
3412
|
+
// A5: keep a TYPED refusal alive past this catch (race-lane parity).
|
|
3413
|
+
const declared = declaredFailure(err);
|
|
3469
3414
|
ledger.settle(lease.lease?.lease_id ?? "", failureCost.settlement);
|
|
3470
3415
|
log.emit("harness.completed", {
|
|
3471
3416
|
harness_id: adapter.id,
|
|
@@ -3473,9 +3418,9 @@ export class Orchestrator {
|
|
|
3473
3418
|
status: "failed",
|
|
3474
3419
|
error: message,
|
|
3475
3420
|
});
|
|
3476
|
-
store.writeYaml(join(paths.attemptsDir, attemptId, "attempt.yaml"), AC.attemptFailureRecord(attemptId, adapter.id, failureCost, "harness", message, appliedAttemptFacts(harnessHome, routed.adapterAccess, this.effectiveProfileId(input, adapter.id))));
|
|
3421
|
+
store.writeYaml(join(paths.attemptsDir, attemptId, "attempt.yaml"), AC.attemptFailureRecord(attemptId, adapter.id, failureCost, "harness", message, appliedAttemptFacts(harnessHome, routed.adapterAccess, this.credentials.effectiveProfileId(input, adapter.id))));
|
|
3477
3422
|
run = {
|
|
3478
|
-
applied: appliedAttemptFacts(harnessHome, routed.adapterAccess, this.effectiveProfileId(input, adapter.id)),
|
|
3423
|
+
applied: appliedAttemptFacts(harnessHome, routed.adapterAccess, this.credentials.effectiveProfileId(input, adapter.id)),
|
|
3479
3424
|
attemptId,
|
|
3480
3425
|
harnessId: adapter.id,
|
|
3481
3426
|
label: `Attempt ${attempt}`,
|
|
@@ -3485,6 +3430,7 @@ export class Orchestrator {
|
|
|
3485
3430
|
errored: true,
|
|
3486
3431
|
costEstimated: failureCost.estimated,
|
|
3487
3432
|
errors: [message],
|
|
3433
|
+
...(declared.code ? { declaredFailure: declared } : {}),
|
|
3488
3434
|
telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [routed.browserRequirement, routed.denyRequirement, routed.delegationRequirement], knobs.model),
|
|
3489
3435
|
};
|
|
3490
3436
|
}
|
|
@@ -3884,6 +3830,11 @@ export class Orchestrator {
|
|
|
3884
3830
|
// failure record + fires run.blocked even though the lifecycle succeeded.
|
|
3885
3831
|
const convIsFailureTerminal = facts.lifecycle !== "succeeded" || convNeedsDecision;
|
|
3886
3832
|
if (convIsFailureTerminal) {
|
|
3833
|
+
// A5 last-result lift: the LAST attempt's typed mid-attempt refusal
|
|
3834
|
+
// survives onto the terminal; budget/cancel/needs-decision outrank it.
|
|
3835
|
+
const convDeclared = !convNeedsDecision && facts.lifecycle === "failed" && !isBudgetTerminal(facts.reason)
|
|
3836
|
+
? lastRun?.declaredFailure
|
|
3837
|
+
: undefined;
|
|
3887
3838
|
writeFailure(store, paths, {
|
|
3888
3839
|
phase: convNeedsDecision ? "review" : "convergence",
|
|
3889
3840
|
category: isBudgetTerminal(facts.reason)
|
|
@@ -3892,7 +3843,9 @@ export class Orchestrator {
|
|
|
3892
3843
|
? "cancelled"
|
|
3893
3844
|
: convNeedsDecision
|
|
3894
3845
|
? "policy"
|
|
3895
|
-
: "internal",
|
|
3846
|
+
: (convDeclared?.category ?? "internal"),
|
|
3847
|
+
code: convDeclared?.code ?? null,
|
|
3848
|
+
resetsAt: convDeclared?.resetsAt ?? null,
|
|
3896
3849
|
safeMessage: convNeedsDecision
|
|
3897
3850
|
? `review escalated to a human decision after ${attempt} attempt(s)`
|
|
3898
3851
|
: facts.reason === "stuck_no_progress"
|
|
@@ -4030,7 +3983,9 @@ export class Orchestrator {
|
|
|
4030
3983
|
...sessionFields,
|
|
4031
3984
|
...this.harnessSpecKnobs(contract, knobs, args.intent),
|
|
4032
3985
|
env_inheritance: envInheritance(this.config(input.repoRoot)),
|
|
4033
|
-
env: (args.laneRun
|
|
3986
|
+
env: (args.laneRun
|
|
3987
|
+
? this.laneHomeEnvFor(input, adapter.id, sessionFields.credential_profile?.profile_id ?? input.credentialProfileId ?? null)
|
|
3988
|
+
: null) ?? args.fallbackHome,
|
|
4034
3989
|
});
|
|
4035
3990
|
const plannerAbort = new AbortController();
|
|
4036
3991
|
spec.extra["abortSignal"] = input.signal
|
|
@@ -4077,15 +4032,14 @@ export class Orchestrator {
|
|
|
4077
4032
|
store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
|
|
4078
4033
|
log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
|
|
4079
4034
|
// W3.3: ONE resolved read-only context — the routing point-probe and every
|
|
4080
|
-
// planner spawn consume the SAME scoped env (see routeContext.ts).
|
|
4081
|
-
// probe home stays a disposable throwaway even for a thread lane turn (auth
|
|
4082
|
-
// truth is home-independent); only the planner spawn swaps in the durable
|
|
4083
|
-
// per-lane home below so its recorded native session survives.
|
|
4084
|
-
const roHome = resolveReadOnlyRouteContext(this.execRootOf(input));
|
|
4035
|
+
// planner spawn consume the SAME scoped env (see routeContext.ts).
|
|
4085
4036
|
// A thread PLAN turn is a chat turn (INV-034): plan candidates are distinct
|
|
4086
|
-
// harnesses run sequentially,
|
|
4087
|
-
//
|
|
4037
|
+
// harnesses run sequentially. Doctor, model inventory, and spawn share each
|
|
4038
|
+
// lane's durable HOME; the disposable context remains the non-thread fallback.
|
|
4088
4039
|
const laneRun = Boolean(input.threadId);
|
|
4040
|
+
const roHome = resolveReadOnlyRouteContext(this.execRootOf(input), laneRun
|
|
4041
|
+
? (id) => this.laneHomeEnvFor(input, id, input.credentialProfileId ?? null)
|
|
4042
|
+
: undefined);
|
|
4089
4043
|
let adapters;
|
|
4090
4044
|
try {
|
|
4091
4045
|
adapters = await this.resolveCandidateAdapters({ ...input, n: undefined }, "plan", ledger, log, roHome, runId);
|
|
@@ -4348,7 +4302,7 @@ export class Orchestrator {
|
|
|
4348
4302
|
}
|
|
4349
4303
|
routeBillingKnowledge(input, harnessId) {
|
|
4350
4304
|
// A selected profile's credential_kind decides billing (round-18 #2).
|
|
4351
|
-
const profileRoute = this.profileAuthRoute(input, harnessId);
|
|
4305
|
+
const profileRoute = this.credentials.profileAuthRoute(input, harnessId);
|
|
4352
4306
|
if (profileRoute)
|
|
4353
4307
|
return profileRoute === "api_key" ? "metered" : "unknown";
|
|
4354
4308
|
// Deps-closure site: no selected route exists yet, so the RESOLVED
|
|
@@ -4446,17 +4400,13 @@ export class Orchestrator {
|
|
|
4446
4400
|
: Math.min(Math.max(input.n ?? 2, 1), 3);
|
|
4447
4401
|
// W3.3: ONE resolved read-only context — the routing point-probe and every
|
|
4448
4402
|
// read-only attempt spawn consume the SAME scoped env (see routeContext.ts).
|
|
4449
|
-
// The point-probe home is a disposable throwaway even for a thread lane
|
|
4450
|
-
// turn: readiness auth truth is home-INDEPENDENT (credentials come from the
|
|
4451
|
-
// profile/keychain/default store, never the scoped home), so the probe and
|
|
4452
|
-
// the run share the same auth source; only the ACTUAL spawn swaps in the
|
|
4453
|
-
// durable per-lane home below so the recorded native session survives.
|
|
4454
|
-
const roHome = resolveReadOnlyRouteContext(this.execRootOf(input));
|
|
4455
4403
|
// A thread ASK turn is a chat turn: its native session is recorded per lane
|
|
4456
|
-
// and the next lane turn resumes it (INV-034).
|
|
4457
|
-
//
|
|
4458
|
-
// NOT lane chat turns — they keep the disposable home and record nothing.
|
|
4404
|
+
// and the next lane turn resumes it (INV-034). Its doctor/model/spawn path
|
|
4405
|
+
// shares that lane HOME; deep-scan and one-shot asks keep the disposable one.
|
|
4459
4406
|
const laneRun = Boolean(input.threadId) && opts.mode === "ask" && !opts.deepScan;
|
|
4407
|
+
const roHome = resolveReadOnlyRouteContext(this.execRootOf(input), laneRun
|
|
4408
|
+
? (id) => this.laneHomeEnvFor(input, id, input.credentialProfileId ?? null)
|
|
4409
|
+
: undefined);
|
|
4460
4410
|
let adapters;
|
|
4461
4411
|
try {
|
|
4462
4412
|
adapters = await this.resolveCandidateAdapters({ ...input, prompt, n: width }, opts.intent, ledger, log, roHome, runId,
|
|
@@ -4640,7 +4590,9 @@ export class Orchestrator {
|
|
|
4640
4590
|
// A thread lane turn spawns in its DURABLE per-lane home so the native
|
|
4641
4591
|
// session it records is reachable for resume next turn; everything else
|
|
4642
4592
|
// uses the disposable route-context home.
|
|
4643
|
-
env: (laneRun
|
|
4593
|
+
env: (laneRun
|
|
4594
|
+
? this.laneHomeEnvFor(input, adapter.id, sessionFields.credential_profile?.profile_id ?? input.credentialProfileId ?? null)
|
|
4595
|
+
: null) ?? roHome.env,
|
|
4644
4596
|
});
|
|
4645
4597
|
const reportAbort = new AbortController();
|
|
4646
4598
|
spec.extra["abortSignal"] = input.signal
|
|
@@ -4687,6 +4639,7 @@ export class Orchestrator {
|
|
|
4687
4639
|
harnessErrored: true,
|
|
4688
4640
|
webRequiredUnsatisfied: false,
|
|
4689
4641
|
});
|
|
4642
|
+
const preDeclared = declaredFailure(preparation.error);
|
|
4690
4643
|
attempts.push({
|
|
4691
4644
|
attemptId,
|
|
4692
4645
|
harnessId: adapter.id,
|
|
@@ -4694,6 +4647,7 @@ export class Orchestrator {
|
|
|
4694
4647
|
report: "",
|
|
4695
4648
|
error: message,
|
|
4696
4649
|
telemetry,
|
|
4650
|
+
...(preDeclared.code ? { declaredFailure: preDeclared } : {}), // typed pre-spawn refusal
|
|
4697
4651
|
});
|
|
4698
4652
|
attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry });
|
|
4699
4653
|
if (opts.deepScan) {
|
|
@@ -4701,7 +4655,8 @@ export class Orchestrator {
|
|
|
4701
4655
|
}
|
|
4702
4656
|
return { status: "launched" };
|
|
4703
4657
|
}
|
|
4704
|
-
const { knobs, spec: preparedSpec, reportAbort, reportInteraction, readonlyWorkMode, attemptEventsPath,
|
|
4658
|
+
const { knobs, spec: preparedSpec, reportAbort, reportInteraction, readonlyWorkMode, attemptEventsPath, telemetry, } = preparation.value;
|
|
4659
|
+
let { answer } = preparation.value;
|
|
4705
4660
|
let spec = preparedSpec;
|
|
4706
4661
|
const retryPolicy = transientRetryPolicy(this.config(input.repoRoot));
|
|
4707
4662
|
let activeSessionId = spec.session_id;
|
|
@@ -4717,9 +4672,16 @@ export class Orchestrator {
|
|
|
4717
4672
|
let cost = 0;
|
|
4718
4673
|
let costEstimated = false;
|
|
4719
4674
|
let harnessError = null;
|
|
4675
|
+
let poolExhausted = null; // A5: typed pool-exhausted refusal
|
|
4720
4676
|
try {
|
|
4721
4677
|
const triedProfiles = new Set(); // W5.4 failover: each profile at most once
|
|
4722
4678
|
for (let nativeTry = 0; !input.signal?.aborted; nativeTry += 1) {
|
|
4679
|
+
// A3 per-try isolation (candidate-lane parity): neither a failed try's
|
|
4680
|
+
// output nor its progress markers leak into the next try's evidence.
|
|
4681
|
+
if (nativeTry > 0) {
|
|
4682
|
+
answer = new AnswerAssembly();
|
|
4683
|
+
telemetry.outputMarkers = newAttemptOutputMarkers();
|
|
4684
|
+
}
|
|
4723
4685
|
const runSpec = nativeTry === 0
|
|
4724
4686
|
? spec
|
|
4725
4687
|
: HarnessRunSpec.parse({
|
|
@@ -4740,7 +4702,7 @@ export class Orchestrator {
|
|
|
4740
4702
|
...(knobs.ignored.length > 0 ? { ignored_settings: knobs.ignored } : {}),
|
|
4741
4703
|
});
|
|
4742
4704
|
try {
|
|
4743
|
-
const watchedReport = withInactivityWatchdog(
|
|
4705
|
+
const watchedReport = withInactivityWatchdog(runModelGovernedRoute(routed, runSpec), {
|
|
4744
4706
|
timeoutMs: harnessInactivityTimeoutMs(this.config(input.repoRoot)),
|
|
4745
4707
|
countsAsProgress: countsAsAgentProgress,
|
|
4746
4708
|
onTimeout: () => {
|
|
@@ -4767,29 +4729,14 @@ export class Orchestrator {
|
|
|
4767
4729
|
log.emit("harness.event", harnessEventPayload(adapter.id, attemptId, safeEv));
|
|
4768
4730
|
appendLine(attemptEventsPath, JSON.stringify(safeEv));
|
|
4769
4731
|
observeAttemptTelemetry(telemetry, safeEv);
|
|
4770
|
-
|
|
4771
|
-
log.emit("plan.progress", {
|
|
4772
|
-
attempt_id: attemptId,
|
|
4773
|
-
harness_id: adapter.id,
|
|
4774
|
-
items: safeEv.plan_progress.items,
|
|
4775
|
-
});
|
|
4776
|
-
}
|
|
4732
|
+
emitPlanProgress((t, p) => log.emit(t, p), adapter.id, attemptId, safeEv);
|
|
4777
4733
|
// read-only routes burn quota too (the orchestrate PLANNER is
|
|
4778
4734
|
// the loudest) — same single owner as the agent loop.
|
|
4779
4735
|
observeBudgetSignals(ledger, log, adapter.id, attemptId, safeEv, budgetSignalState);
|
|
4780
4736
|
this.deps.quotaEventSink?.(adapter.id, safeEv);
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
costEstimated = true;
|
|
4785
|
-
log.emit("budget.observation", {
|
|
4786
|
-
harness_id: adapter.id,
|
|
4787
|
-
attempt_id: attemptId,
|
|
4788
|
-
kind: "spend",
|
|
4789
|
-
usd: safeEv.usage.cost_usd,
|
|
4790
|
-
estimated: safeEv.usage.estimated === true,
|
|
4791
|
-
});
|
|
4792
|
-
}
|
|
4737
|
+
const spend = observeReadonlySpend(safeEv, (t, p) => log.emit(t, p), adapter.id, attemptId);
|
|
4738
|
+
cost += spend.costUsd;
|
|
4739
|
+
costEstimated ||= spend.estimated;
|
|
4793
4740
|
// A TYPED final message wins verbatim over joined narration.
|
|
4794
4741
|
answer.observe(safeEv);
|
|
4795
4742
|
if (safeEv.type === "error")
|
|
@@ -4800,8 +4747,7 @@ export class Orchestrator {
|
|
|
4800
4747
|
}
|
|
4801
4748
|
catch (err) {
|
|
4802
4749
|
harnessError = safeErrorMessage(err);
|
|
4803
|
-
// #31: classify the throw
|
|
4804
|
-
// a typed category (watchdog timeout vs process crash).
|
|
4750
|
+
// #31: classify the throw (watchdog timeout vs process crash) as typed.
|
|
4805
4751
|
telemetry.transientFailures.push(classifyAdapterThrow({ errorName: err instanceof Error ? err.name : null }));
|
|
4806
4752
|
}
|
|
4807
4753
|
const newTransients = telemetry.transientFailures.slice(transientStart);
|
|
@@ -4809,31 +4755,48 @@ export class Orchestrator {
|
|
|
4809
4755
|
const sawRetryable = newTransients.some((f) => f.retryable);
|
|
4810
4756
|
const sawTypedLimit = telemetry.rateLimits.length > rateLimitStart;
|
|
4811
4757
|
const reportSoFar = answer.text();
|
|
4812
|
-
// W5.4 reactive failover, READ-ONLY lane (same contract as the
|
|
4813
|
-
// candidate lane
|
|
4758
|
+
// W5.4 + A2 reactive failover, READ-ONLY lane (same contract as the
|
|
4759
|
+
// candidate lane: typed limit or structural pre-progress death).
|
|
4814
4760
|
if (harnessError && !input.signal?.aborted) {
|
|
4815
|
-
const
|
|
4816
|
-
const readyProfileIds = sawTypedLimit && reportSoFar.length === 0 && rotationPolicy.limit_action === "rotate"
|
|
4817
|
-
? await this.readyProfileIdsForRotation(input, adapter.id, spec.credential_profile ?? null, triedProfiles)
|
|
4818
|
-
: new Set();
|
|
4819
|
-
const rotated = rotateSpecOnTypedLimit({
|
|
4761
|
+
const rotated = await rotateSpecOnTypedLimit({
|
|
4820
4762
|
spec,
|
|
4821
4763
|
harnessId: adapter.id,
|
|
4822
4764
|
attemptId,
|
|
4823
|
-
policy:
|
|
4765
|
+
policy: this.credentials.profilePolicy(input.repoRoot, adapter.id),
|
|
4824
4766
|
registry: this.config(input.repoRoot)?.global.credential_profiles ?? [],
|
|
4825
4767
|
snapshots: this.deps.quotaSnapshots?.() ?? [],
|
|
4826
|
-
|
|
4768
|
+
probeReadyProfiles: () => this.credentials.readyProfileIdsForRotation(input, adapter.id, spec.credential_profile ?? null, triedProfiles, spec.model_hint ?? null),
|
|
4769
|
+
...this.credentials.rotationObservations(adapter, spec, newTransients),
|
|
4827
4770
|
triedProfiles,
|
|
4771
|
+
markers: telemetry.outputMarkers,
|
|
4828
4772
|
sawTypedLimit,
|
|
4829
|
-
|
|
4773
|
+
sawRetryable,
|
|
4774
|
+
attemptErrored: harnessError !== null,
|
|
4775
|
+
// Same policy owner as the candidate lane: an errored try's
|
|
4776
|
+
// narration is not a report; the transient gate keeps reportSoFar.
|
|
4777
|
+
deliverableEmpty: acceptedTryOutput(answer, harnessError !== null).length === 0,
|
|
4830
4778
|
lastLimit: telemetry.rateLimits.at(-1) ?? null,
|
|
4831
4779
|
emit: (type, payload) => log.emit(type, payload),
|
|
4832
4780
|
newSessionId: () => newId("ses"),
|
|
4833
4781
|
defaultRouteWasVendorNative: routed.authRouteEstimate === "local_session",
|
|
4782
|
+
// D-U6: an explicit pin never rotates; pool-selected rows do.
|
|
4783
|
+
pinned: input.credentialProfileId != null,
|
|
4834
4784
|
});
|
|
4785
|
+
// A5 ordering: same as the candidate lane — terminalize typed
|
|
4786
|
+
// before the transient gate burns same-profile retries. The pool
|
|
4787
|
+
// refusal APPENDS to the original harness error (candidate-lane
|
|
4788
|
+
// parity: errors.push) instead of erasing the true failure.
|
|
4789
|
+
if (rotated && "poolExhausted" in rotated) {
|
|
4790
|
+
poolExhausted = rotated.poolExhausted;
|
|
4791
|
+
harnessError = harnessError
|
|
4792
|
+
? `${harnessError}; ${safeErrorMessage(poolExhausted)}`
|
|
4793
|
+
: safeErrorMessage(poolExhausted);
|
|
4794
|
+
break;
|
|
4795
|
+
}
|
|
4835
4796
|
if (rotated) {
|
|
4836
|
-
|
|
4797
|
+
// INV-137: the rotated row's session must land in ITS OWN lane
|
|
4798
|
+
// home — never the previous row's lane store.
|
|
4799
|
+
spec = rotatedSpecInLaneHome(spec, rotated, (id) => this.laneHomeEnvFor(input, adapter.id, id), input.credentialProfileId ?? null);
|
|
4837
4800
|
harnessError = null;
|
|
4838
4801
|
continue;
|
|
4839
4802
|
}
|
|
@@ -4844,21 +4807,7 @@ export class Orchestrator {
|
|
|
4844
4807
|
nativeTry >= retryPolicy.maxRetries ||
|
|
4845
4808
|
input.signal?.aborted)
|
|
4846
4809
|
break;
|
|
4847
|
-
const
|
|
4848
|
-
const delayMs = transientRetryDelayMs(transient?.retryDelayMs ?? null, retryPolicy, nativeTry);
|
|
4849
|
-
log.emit("route.transient.detected", {
|
|
4850
|
-
harness_id: adapter.id,
|
|
4851
|
-
attempt_id: attemptId,
|
|
4852
|
-
kind: transient?.kind ?? "unknown",
|
|
4853
|
-
category: transient?.category ?? "unknown_harness_error",
|
|
4854
|
-
native_try: nativeTry + 1,
|
|
4855
|
-
});
|
|
4856
|
-
log.emit("route.transient.retry_scheduled", {
|
|
4857
|
-
harness_id: adapter.id,
|
|
4858
|
-
attempt_id: attemptId,
|
|
4859
|
-
retry: nextTry,
|
|
4860
|
-
delay_ms: delayMs,
|
|
4861
|
-
});
|
|
4810
|
+
const delayMs = emitTransientRetryPlan((t, p) => log.emit(t, p), adapter.id, attemptId, transient, nativeTry, retryPolicy);
|
|
4862
4811
|
harnessError = null;
|
|
4863
4812
|
await sleep(delayMs);
|
|
4864
4813
|
}
|
|
@@ -4877,19 +4826,13 @@ export class Orchestrator {
|
|
|
4877
4826
|
preStreamFailureSource: "readonly-pre-stream",
|
|
4878
4827
|
});
|
|
4879
4828
|
}
|
|
4880
|
-
if (harnessError &&
|
|
4881
|
-
log.emit(
|
|
4882
|
-
harness_id: adapter.id,
|
|
4883
|
-
attempt_id: attemptId,
|
|
4884
|
-
category: telemetry.transientFailures.at(-1)?.category ?? "unknown_harness_error",
|
|
4885
|
-
retries: retryPolicy.maxRetries,
|
|
4886
|
-
});
|
|
4829
|
+
if (harnessError && !poolExhausted) {
|
|
4830
|
+
emitTransientExhausted((t, p) => log.emit(t, p), adapter.id, attemptId, telemetry, retryPolicy.maxRetries);
|
|
4887
4831
|
}
|
|
4888
4832
|
attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry });
|
|
4889
|
-
// D-16:
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
});
|
|
4833
|
+
// D-16: the deliverable is the report. A3: an errored attempt with no
|
|
4834
|
+
// typed final contributes no report material (acceptedTryOutput).
|
|
4835
|
+
const roUnwrapped = unwrapWorkReportEnvelope(acceptedTryOutput(answer, harnessError !== null), readonlyWorkMode, { sideToolReport: telemetry.sideToolWorkReport ?? undefined });
|
|
4893
4836
|
// Trim symmetrically with the plan path: a whitespace-only answer is not
|
|
4894
4837
|
// a delivered report (the final-artifact wrapper heading would otherwise
|
|
4895
4838
|
// make it read as present content by construction).
|
|
@@ -4944,6 +4887,7 @@ export class Orchestrator {
|
|
|
4944
4887
|
report,
|
|
4945
4888
|
error: harnessError,
|
|
4946
4889
|
telemetry,
|
|
4890
|
+
...(poolExhausted ? { declaredFailure: declaredFailure(poolExhausted) } : {}),
|
|
4947
4891
|
});
|
|
4948
4892
|
if (opts.deepScan) {
|
|
4949
4893
|
store.writeText(join(paths.findingsDir, `${attemptId}-error.md`), `# Explorer ${attemptId} failed\n\n${harnessError}\n`);
|
|
@@ -5169,14 +5113,19 @@ export class Orchestrator {
|
|
|
5169
5113
|
// #31: classify the harness cause across the read-only attempts so auth
|
|
5170
5114
|
// guidance appears only on a real auth failure.
|
|
5171
5115
|
const roCategory = dominantHarnessFailureCategory(attemptTelemetries.flatMap((a) => a.telemetry.transientFailures));
|
|
5116
|
+
// A5: the terminal already speaks with the LAST attempt's message; its
|
|
5117
|
+
// typed refusal rides along machine-readably instead of reducing to it.
|
|
5118
|
+
const roDeclared = webBlocked ? undefined : last?.declaredFailure;
|
|
5172
5119
|
writeFailure(store, paths, {
|
|
5173
5120
|
phase: "harness",
|
|
5174
|
-
category: webBlocked ? "policy" : "harness_error",
|
|
5121
|
+
category: webBlocked ? "policy" : (roDeclared?.category ?? "harness_error"),
|
|
5122
|
+
code: roDeclared?.code ?? null,
|
|
5175
5123
|
harnessId: last?.harnessId,
|
|
5176
5124
|
attemptId: last?.attemptId,
|
|
5177
5125
|
safeMessage: singleError,
|
|
5178
5126
|
eventRefs: roEventRefs,
|
|
5179
5127
|
runDir: paths.root,
|
|
5128
|
+
resetsAt: roDeclared?.resetsAt ?? null,
|
|
5180
5129
|
nextActions: harnessFailureNextActions(roCategory),
|
|
5181
5130
|
});
|
|
5182
5131
|
}
|