@runuai/host 0.9.170 → 0.9.172
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/lib/agents/claude.ts +13 -0
- package/lib/agents/codex.ts +11 -5
- package/lib/agents/session-state.ts +77 -0
- package/lib/agents/transport.ts +2 -0
- package/lib/agents/types.ts +3 -0
- package/lib/agents/uai.ts +3 -25
- package/lib/assignment-evidence.ts +2 -1
- package/lib/capture-flow.ts +14 -1
- package/lib/cloud-credential-leases.ts +9 -3
- package/lib/cloud-credential-runtime.ts +8 -0
- package/lib/connection-secret-redaction.ts +25 -0
- package/lib/credential-cleanup-inventory.ts +2 -1
- package/lib/credential-consumers/engines.ts +29 -5
- package/lib/credentials-activation-authority.ts +158 -10
- package/lib/custom-connection-capture.ts +395 -0
- package/lib/custom-connection-launch.ts +299 -0
- package/lib/custom-connection-runtime.ts +70 -0
- package/lib/engine-accounts.ts +1 -1
- package/lib/engine-credential-env.ts +2 -0
- package/lib/internal-connections.ts +5 -0
- package/lib/legacy-runtime-retirement.ts +43 -8
- package/lib/obs.ts +2 -1
- package/lib/openrouter-budget.ts +247 -0
- package/lib/orchestrator.ts +308 -26
- package/lib/secrets-proxy/address.ts +23 -0
- package/lib/task-credential-cleanup.ts +13 -3
- package/package.json +1 -1
- package/runner/runner.mjs +231 -3
- package/src/capture-wire.ts +102 -0
- package/src/cloud-credentials.ts +36 -372
- package/src/credential-location.ts +248 -0
- package/src/credential-wire-validation.ts +25 -0
- package/src/custom-connections.ts +207 -0
- package/src/main.ts +116 -3
- package/src/protocol.ts +111 -0
package/lib/orchestrator.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
import { isOwnedInternalConnectionRuntime } from "./internal-connections";
|
|
2
|
+
import { registerConnectionSecret } from "./connection-secret-redaction";
|
|
3
|
+
import { buildCustomConnectionLaunch, tomlString, connectionNativeBlanks, connectionHomePreparation, newConnectionHome, connectionDnsLookupArgv, validateConnectionAddresses } from "./custom-connection-launch";
|
|
4
|
+
import { connectionFailureReason, ConnectionEventSanitizer } from "./custom-connection-runtime";
|
|
5
|
+
import { customConnectionEnabled, type CustomConnectionMaterial, type CustomConnectionLease, type CustomConnectionReason } from "../src/custom-connections";
|
|
1
6
|
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { rememberCredentialCleanupPath } from "./credential-cleanup-inventory";
|
|
2
8
|
import { AssignmentEvidenceJournal } from "./assignment-evidence";
|
|
3
9
|
import { durableAgentSessionId } from "./agents/transport";
|
|
4
10
|
import { agentOutputSource } from "./agents/output-source";
|
|
@@ -46,6 +52,7 @@ import { realAgentFactory } from "./agents/factory";
|
|
|
46
52
|
import { resolveAgentMode } from "./agents/mode";
|
|
47
53
|
import {
|
|
48
54
|
clearPersistedCodexThread,
|
|
55
|
+
purgeInternalAgentSessionState,
|
|
49
56
|
persistedAgentIds,
|
|
50
57
|
stopPersistedAgentSession,
|
|
51
58
|
} from "./agents/session-state";
|
|
@@ -633,7 +640,7 @@ function isEngineUsageKind(kind: string): kind is EngineUsageFrame["engine"] {
|
|
|
633
640
|
* or, for the Managed Engine, the task's open run alone (its agent's task
|
|
634
641
|
* token is its credential). */
|
|
635
642
|
type LeasedIdentity =
|
|
636
|
-
| { subject: "account"; runEpoch: number; credentialId: string; revision: string; version: number }
|
|
643
|
+
| { subject: "account"; runEpoch: number; credentialId: string; revision: string; version: number; connection?: boolean; settingsHash?: string; internal?: boolean }
|
|
637
644
|
| { subject: "task-token"; runEpoch: number };
|
|
638
645
|
|
|
639
646
|
type SpawnArgs = Omit<Parameters<AgentSessionFactory["create"]>[0], "environment">;
|
|
@@ -691,7 +698,7 @@ function sameLeasedIdentity(a: LeasedIdentity, b: LeasedIdentity): boolean {
|
|
|
691
698
|
a.runEpoch === b.runEpoch &&
|
|
692
699
|
a.credentialId === b.credentialId &&
|
|
693
700
|
a.revision === b.revision &&
|
|
694
|
-
a.version === b.version
|
|
701
|
+
a.version === b.version && a.settingsHash === b.settingsHash && a.internal === b.internal
|
|
695
702
|
);
|
|
696
703
|
}
|
|
697
704
|
return a.subject === b.subject && a.runEpoch === b.runEpoch;
|
|
@@ -815,6 +822,15 @@ export interface OrchestratorTaskStopResult {
|
|
|
815
822
|
}
|
|
816
823
|
|
|
817
824
|
export class Orchestrator {
|
|
825
|
+
private readonly connectionSessions = new WeakMap<AgentSession, { material: CustomConnectionMaterial; metadata: CustomConnectionLease; identity: Extract<LeasedIdentity, { subject: "account" }> }>();
|
|
826
|
+
private readonly connectionSecretReleases = new WeakMap<AgentSession, () => void>();
|
|
827
|
+
private readonly connectionRetries = new WeakMap<Channel, Map<string, {
|
|
828
|
+
timer: ReturnType<typeof setTimeout> | null;
|
|
829
|
+
identity: LeasedIdentity;
|
|
830
|
+
agent: RosterAgent;
|
|
831
|
+
}>>();
|
|
832
|
+
private readonly connectionExpiryTimers = new WeakMap<AgentSession, ReturnType<typeof setTimeout>>();
|
|
833
|
+
private readonly sessionIdentities = new WeakMap<AgentSession, LeasedIdentity>();
|
|
818
834
|
private readonly sessionEventHandlers = new WeakMap<AgentSession, AgentEventHandler>();
|
|
819
835
|
private readonly channels = new Map<string, Channel>();
|
|
820
836
|
private readonly channelSpecs = new Map<string, ChannelEnsureInput>();
|
|
@@ -1073,6 +1089,7 @@ export class Orchestrator {
|
|
|
1073
1089
|
// took effect: writeAgentCli kept seeing the old empty list). Replacing the
|
|
1074
1090
|
// array exactly also makes absence authoritative for live removal.
|
|
1075
1091
|
channel.roster = [...spec.agents];
|
|
1092
|
+
this.cancelStaleConnectionRetries(channel);
|
|
1076
1093
|
for (const agent of added) {
|
|
1077
1094
|
// Per-agent link/document skills, like getOrCreateChannel does at
|
|
1078
1095
|
// channel birth. Best-effort.
|
|
@@ -1213,6 +1230,7 @@ export class Orchestrator {
|
|
|
1213
1230
|
channel: Channel,
|
|
1214
1231
|
agentId: string,
|
|
1215
1232
|
): Promise<void> {
|
|
1233
|
+
this.cancelConnectionRetries(channel, agentId);
|
|
1216
1234
|
const session = channel.sessions.get(agentId);
|
|
1217
1235
|
const inFlightCreate = channel.factoryOperationByAgent.get(agentId);
|
|
1218
1236
|
const lifecycleAtRemoval = channel.rosterSessionChanges.get(agentId);
|
|
@@ -1775,7 +1793,7 @@ export class Orchestrator {
|
|
|
1775
1793
|
return lease.accounts
|
|
1776
1794
|
.filter(
|
|
1777
1795
|
(account): account is LeasedEngineAccount =>
|
|
1778
|
-
account.engineKind === kind && isLeasedEngineAccount(account),
|
|
1796
|
+
account.engineKind === kind && account.authKind !== "custom-connection" && isLeasedEngineAccount(account),
|
|
1779
1797
|
)
|
|
1780
1798
|
.map((account) => ({
|
|
1781
1799
|
id: account.credentialId,
|
|
@@ -1783,7 +1801,7 @@ export class Orchestrator {
|
|
|
1783
1801
|
label: account.label,
|
|
1784
1802
|
isDefault: false,
|
|
1785
1803
|
scope: account.scope,
|
|
1786
|
-
authKind: account.authKind,
|
|
1804
|
+
authKind: account.authKind as "env" | "config-dir",
|
|
1787
1805
|
execEnv: {},
|
|
1788
1806
|
}));
|
|
1789
1807
|
}
|
|
@@ -2886,12 +2904,41 @@ export class Orchestrator {
|
|
|
2886
2904
|
);
|
|
2887
2905
|
}
|
|
2888
2906
|
|
|
2907
|
+
private armConnectionExpiry(channel: Channel, agentId: string, session: AgentSession): void {
|
|
2908
|
+
const expiresAt = this.connectionSessions.get(session)?.material.expiresAt;
|
|
2909
|
+
if (expiresAt == null) return;
|
|
2910
|
+
const check = (): void => {
|
|
2911
|
+
this.connectionExpiryTimers.delete(session);
|
|
2912
|
+
if (!this.isActiveChannel(channel) || channel.sessions.get(agentId) !== session) return;
|
|
2913
|
+
const remaining = expiresAt - Date.now();
|
|
2914
|
+
if (remaining <= 0) { this.expireConnectionSession(channel, agentId, session); return; }
|
|
2915
|
+
const timer = setTimeout(check, Math.min(remaining, 2_147_483_647));
|
|
2916
|
+
timer.unref?.();
|
|
2917
|
+
this.connectionExpiryTimers.set(session, timer);
|
|
2918
|
+
};
|
|
2919
|
+
check();
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
private expireConnectionSession(channel: Channel, agentId: string, session: AgentSession): void {
|
|
2923
|
+
if (channel.sessions.get(agentId) !== session) return;
|
|
2924
|
+
const timer = this.connectionExpiryTimers.get(session);
|
|
2925
|
+
if (timer) clearTimeout(timer);
|
|
2926
|
+
this.connectionExpiryTimers.delete(session);
|
|
2927
|
+
this.retainRetiredTurnBoundary(channel, agentId, this.needsAbortedTurnBoundary(channel, agentId));
|
|
2928
|
+
this.endLeasedLaunches(channel, agentId, session);
|
|
2929
|
+
this.emitRetiredTurnAbort(channel, agentId);
|
|
2930
|
+
channel.sessions.delete(agentId);
|
|
2931
|
+
this.leasedUnavailable(channel, agentId, "expired");
|
|
2932
|
+
this.trackEventOperation(channel, session.close().catch(() => {}));
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2889
2935
|
private bindSession(
|
|
2890
2936
|
channel: Channel,
|
|
2891
2937
|
agentId: string,
|
|
2892
2938
|
session: AgentSession,
|
|
2893
2939
|
): void {
|
|
2894
2940
|
channel.sessions.set(agentId, session);
|
|
2941
|
+
this.armConnectionExpiry(channel, agentId, session);
|
|
2895
2942
|
channel.everBound.add(agentId);
|
|
2896
2943
|
const rosterChange = channel.rosterSessionChanges.get(agentId);
|
|
2897
2944
|
if (rosterChange) {
|
|
@@ -2922,7 +2969,9 @@ export class Orchestrator {
|
|
|
2922
2969
|
// transport emission order across that await; otherwise the later boundary
|
|
2923
2970
|
// can consume/reorder prompt state while the earlier error still owns it.
|
|
2924
2971
|
let eventTail: Promise<void> = Promise.resolve();
|
|
2925
|
-
const
|
|
2972
|
+
const connection = this.connectionSessions.get(session);
|
|
2973
|
+
const sanitizer = connection ? new ConnectionEventSanitizer(connection.material.apiKey) : null;
|
|
2974
|
+
const onSanitizedEvent: AgentEventHandler = (event) => {
|
|
2926
2975
|
this.markChannelActivity(channel.taskId);
|
|
2927
2976
|
// Recycle/teardown deletes the old session before calling close().
|
|
2928
2977
|
// Adapters deliberately emit a final exit event; without this identity
|
|
@@ -2936,6 +2985,9 @@ export class Orchestrator {
|
|
|
2936
2985
|
// ADR-154 §6: an engine that ended launches nothing more from now,
|
|
2937
2986
|
// whatever its terminal handling still awaits (the runtime's proof).
|
|
2938
2987
|
if (event.type === "error" || event.type === "exit") {
|
|
2988
|
+
const timer = this.connectionExpiryTimers.get(session);
|
|
2989
|
+
if (timer) clearTimeout(timer);
|
|
2990
|
+
this.connectionExpiryTimers.delete(session);
|
|
2939
2991
|
this.endLeasedLaunches(channel, agentId, session);
|
|
2940
2992
|
}
|
|
2941
2993
|
const operation = eventTail
|
|
@@ -2950,11 +3002,12 @@ export class Orchestrator {
|
|
|
2950
3002
|
return;
|
|
2951
3003
|
}
|
|
2952
3004
|
await this.handleAgentEvent(channel, agentId, session, event);
|
|
3005
|
+
if (connection && event.type === "error") await session.close().catch(() => {});
|
|
2953
3006
|
})
|
|
2954
3007
|
.catch((err: unknown) => {
|
|
2955
3008
|
console.warn(
|
|
2956
3009
|
`[orchestrator] ${channel.taskId}/${agentId}: event handling failed: ${
|
|
2957
|
-
err instanceof Error ? (err.stack ?? err.message) : String(err)
|
|
3010
|
+
connection ? connectionFailureReason(err) : err instanceof Error ? (err.stack ?? err.message) : String(err)
|
|
2958
3011
|
}`,
|
|
2959
3012
|
);
|
|
2960
3013
|
});
|
|
@@ -2965,6 +3018,9 @@ export class Orchestrator {
|
|
|
2965
3018
|
() => channel.eventOperations.delete(operation),
|
|
2966
3019
|
);
|
|
2967
3020
|
};
|
|
3021
|
+
const onEvent: AgentEventHandler = (event) => {
|
|
3022
|
+
for (const safe of sanitizer ? sanitizer.push(event) : [event]) onSanitizedEvent(safe);
|
|
3023
|
+
};
|
|
2968
3024
|
this.sessionEventHandlers.set(session, onEvent);
|
|
2969
3025
|
session.onEvent(onEvent);
|
|
2970
3026
|
}
|
|
@@ -3047,12 +3103,64 @@ export class Orchestrator {
|
|
|
3047
3103
|
if (!this.isActiveChannel(channel)) return null;
|
|
3048
3104
|
const account = this.leasedAccountFor(channel, agentId, kind, open);
|
|
3049
3105
|
if (!account) {
|
|
3050
|
-
reason =
|
|
3106
|
+
reason = args.agent.accountBinding === "required"
|
|
3107
|
+
? args.agent.accountId && open.connectionsUnsupported?.includes(args.agent.accountId) ? "unsupported" : "connection removed"
|
|
3108
|
+
: `no ${kind} account this task can use`;
|
|
3051
3109
|
break;
|
|
3052
3110
|
}
|
|
3053
3111
|
if (account.authKind === "env") {
|
|
3054
3112
|
return this.withLeasedAccount(channel, agentId, args, account, open.assignment.runEpoch);
|
|
3055
3113
|
}
|
|
3114
|
+
if (account.authKind === "custom-connection") {
|
|
3115
|
+
try {
|
|
3116
|
+
if (!account.connection || !account.connectionMaterial || args.agent.accountBinding !== "required") throw new Error("unsupported");
|
|
3117
|
+
const home = newConnectionHome(account.credentialId, account.revision, agentId, account.connection.settingsHash);
|
|
3118
|
+
const launch = buildCustomConnectionLaunch({
|
|
3119
|
+
material: account.connectionMaterial, connection: account.connection,
|
|
3120
|
+
model: args.agent.model, enabled: open.connectionsEnabled ?? [], home,
|
|
3121
|
+
runtimeProvider: environment.descriptor.locator.provider,
|
|
3122
|
+
});
|
|
3123
|
+
for (const name of ["", "auth.json", "config.toml", "aws-config", "aws-credentials"]) {
|
|
3124
|
+
rememberCredentialCleanupPath({
|
|
3125
|
+
taskId: channel.taskId, runEpoch: open.assignment.runEpoch,
|
|
3126
|
+
runtime: environment.descriptor.locator, lane: "engine-auth",
|
|
3127
|
+
path: name ? `${home}/${name}` : home, credentialId: account.credentialId,
|
|
3128
|
+
});
|
|
3129
|
+
}
|
|
3130
|
+
const prepared = await environment.exec({
|
|
3131
|
+
argv: connectionHomePreparation(home), projectEnvProfile: "maintenance",
|
|
3132
|
+
user: "node", timeoutMs: 20_000, maxOutputBytes: 1024,
|
|
3133
|
+
admit: () => {
|
|
3134
|
+
const current = leasedEnginesFor(channel.taskId);
|
|
3135
|
+
const held = current?.open && current.assignment.runEpoch === open.assignment.runEpoch &&
|
|
3136
|
+
current.accounts.some(a => isLeasedEngineAccount(a) &&
|
|
3137
|
+
a.credentialId === account.credentialId && a.revision === account.revision &&
|
|
3138
|
+
a.version === account.version && a.connection?.settingsHash === account.connection?.settingsHash);
|
|
3139
|
+
if (!this.isActiveChannel(channel) || !held || (account.connection?.internal === true && !isOwnedInternalConnectionRuntime(environment.descriptor.locator.provider))) throw new Error("unsupported");
|
|
3140
|
+
},
|
|
3141
|
+
});
|
|
3142
|
+
if (prepared.exitCode !== 0) throw new Error("unsupported");
|
|
3143
|
+
const lookup = await environment.exec({
|
|
3144
|
+
argv: agentCliArgv(environment, connectionDnsLookupArgv(account.connectionMaterial.baseUrl)),
|
|
3145
|
+
projectEnvProfile: "maintenance", user: "node", env: RUNTIME_AUTHORITY_ENV,
|
|
3146
|
+
timeoutMs: 10_000, maxOutputBytes: 8192,
|
|
3147
|
+
});
|
|
3148
|
+
if (lookup.exitCode !== 0) throw new Error("unreachable");
|
|
3149
|
+
validateConnectionAddresses(Buffer.from(lookup.stdout).toString("utf8"), account.connection.internal === true);
|
|
3150
|
+
if (!this.isActiveChannel(channel)) return null;
|
|
3151
|
+
const bound = this.withLeasedAccount(channel, agentId, {
|
|
3152
|
+
...args,
|
|
3153
|
+
agentEnv: { ...(args.agentEnv ?? {}), ...launch.agentEnv, UAI_CUSTOM_CONNECTION: "1" },
|
|
3154
|
+
cliConfigOverrides: [...launch.cliConfigOverrides,
|
|
3155
|
+
...(account.engineKind === "codex" ? [`model=${tomlString(launch.model)}`] : [])],
|
|
3156
|
+
...(launch.claudeSettings ? { claudeSettings: launch.claudeSettings } : {}),
|
|
3157
|
+
}, { ...account, authKind: "env", env: launch.agentEnv }, open.assignment.runEpoch);
|
|
3158
|
+
// The launch compiler, not the env-account path, owns this material.
|
|
3159
|
+
const identity: LeasedIdentity = { subject: "account", runEpoch: open.assignment.runEpoch, credentialId: account.credentialId, revision: account.revision, version: account.version, connection: true, settingsHash: account.connection.settingsHash, internal: account.connection.internal === true };
|
|
3160
|
+
channel.leasedIdentityByAgent.set(agentId, identity);
|
|
3161
|
+
return { args: bound.args, identity };
|
|
3162
|
+
} catch (error) { reason = connectionFailureReason(error); break; }
|
|
3163
|
+
}
|
|
3056
3164
|
// The account's home gets the task's managed config before anything
|
|
3057
3165
|
// can start in it, whenever the gate's opening first reveals it.
|
|
3058
3166
|
await this.ensureBrowserSetup(channel, kind === "codex");
|
|
@@ -3238,6 +3346,8 @@ export class Orchestrator {
|
|
|
3238
3346
|
* launch from this moment, before its terminal handling awaits anything.
|
|
3239
3347
|
* Every such handling retires or replaces the session. */
|
|
3240
3348
|
private endLeasedLaunches(channel: Channel, agentId: string, session: AgentSession): void {
|
|
3349
|
+
this.connectionSecretReleases.get(session)?.();
|
|
3350
|
+
this.connectionSecretReleases.delete(session);
|
|
3241
3351
|
const slot = channel.leasedLaunches.get(agentId);
|
|
3242
3352
|
if (slot?.session === session) void this.retireLeasedLaunch(channel, agentId, slot);
|
|
3243
3353
|
}
|
|
@@ -3255,6 +3365,7 @@ export class Orchestrator {
|
|
|
3255
3365
|
private retireLeasedLaunch(channel: Channel, agentId: string, slot: LeasedLaunchSlot | undefined): Promise<void> {
|
|
3256
3366
|
if (!slot || slot.retired) return Promise.resolve();
|
|
3257
3367
|
slot.retired = true;
|
|
3368
|
+
if (slot.session) { this.connectionSecretReleases.get(slot.session)?.(); this.connectionSecretReleases.delete(slot.session); }
|
|
3258
3369
|
if (channel.leasedLaunches.get(agentId) === slot) channel.leasedLaunches.delete(agentId);
|
|
3259
3370
|
// It never wrote an admission: nothing of it is in its runtime.
|
|
3260
3371
|
if (!slot.admitted) return Promise.resolve();
|
|
@@ -3406,6 +3517,10 @@ export class Orchestrator {
|
|
|
3406
3517
|
isLeasedEngineAccount(account) &&
|
|
3407
3518
|
account.revision === identity.revision &&
|
|
3408
3519
|
account.version === identity.version &&
|
|
3520
|
+
(account.connection?.settingsHash === identity.settingsHash) &&
|
|
3521
|
+
((account.connection?.internal === true) === (identity.internal === true)) &&
|
|
3522
|
+
(account.connection?.internal !== true || isOwnedInternalConnectionRuntime(channel.agentHandle?.descriptor.locator.provider)) &&
|
|
3523
|
+
(account.authKind !== "custom-connection" || ((account.expiresAt === null || account.expiresAt > Date.now()) && !!account.connectionMaterial && customConnectionEnabled(account.connectionMaterial, lease.connectionsEnabled ?? []))) &&
|
|
3409
3524
|
!engineVersionHeldBack(lease.assignment, identity)
|
|
3410
3525
|
);
|
|
3411
3526
|
}
|
|
@@ -3436,9 +3551,14 @@ export class Orchestrator {
|
|
|
3436
3551
|
kind: string,
|
|
3437
3552
|
lease: TaskEngineLease,
|
|
3438
3553
|
): LeasedEngineAccount | undefined {
|
|
3554
|
+
const agent = channel.roster.find(candidate => candidate.id === agentId);
|
|
3555
|
+
if (agent?.accountBinding === "required") {
|
|
3556
|
+
return lease.accounts.find((account): account is LeasedEngineAccount => account.credentialId === agent.accountId && account.engineKind === kind && account.authKind === "custom-connection" && isLeasedEngineAccount(account));
|
|
3557
|
+
}
|
|
3558
|
+
if (lease.accounts.some(account => account.credentialId === agent?.accountId && account.authKind === "custom-connection")) return undefined;
|
|
3439
3559
|
const accounts = lease.accounts.filter(
|
|
3440
3560
|
(account): account is LeasedEngineAccount =>
|
|
3441
|
-
account.engineKind === kind && isLeasedEngineAccount(account),
|
|
3561
|
+
account.engineKind === kind && account.authKind !== "custom-connection" && isLeasedEngineAccount(account),
|
|
3442
3562
|
);
|
|
3443
3563
|
const bound = channel.accountByAgent.get(agentId);
|
|
3444
3564
|
const preferredId = channel.roster.find(
|
|
@@ -3456,7 +3576,7 @@ export class Orchestrator {
|
|
|
3456
3576
|
label: account.label,
|
|
3457
3577
|
isDefault: false,
|
|
3458
3578
|
scope: account.scope,
|
|
3459
|
-
authKind: account.authKind,
|
|
3579
|
+
authKind: account.authKind as "env" | "config-dir",
|
|
3460
3580
|
execEnv: {},
|
|
3461
3581
|
})),
|
|
3462
3582
|
{ preferredId },
|
|
@@ -3481,6 +3601,8 @@ export class Orchestrator {
|
|
|
3481
3601
|
credentialId: account.credentialId,
|
|
3482
3602
|
revision: account.revision,
|
|
3483
3603
|
version: account.version,
|
|
3604
|
+
connection: account.authKind === "custom-connection",
|
|
3605
|
+
...(account.connection ? { settingsHash: account.connection.settingsHash, internal: account.connection.internal === true } : {}),
|
|
3484
3606
|
};
|
|
3485
3607
|
channel.accountByAgent.set(agentId, account.credentialId);
|
|
3486
3608
|
channel.leasedIdentityByAgent.set(agentId, identity);
|
|
@@ -3498,6 +3620,8 @@ export class Orchestrator {
|
|
|
3498
3620
|
identity.credentialId,
|
|
3499
3621
|
identity.revision,
|
|
3500
3622
|
identity.version,
|
|
3623
|
+
...(account.connection ? [account.connection.settingsHash] : []),
|
|
3624
|
+
...(account.connection?.internal === true ? ["custom-internal"] : []),
|
|
3501
3625
|
]),
|
|
3502
3626
|
agentEnv: {
|
|
3503
3627
|
...(args.agentEnv ?? {}),
|
|
@@ -3526,6 +3650,7 @@ export class Orchestrator {
|
|
|
3526
3650
|
account: LeasedEngineAccount,
|
|
3527
3651
|
environment: TaskEnvironmentHandle<unknown>,
|
|
3528
3652
|
): Promise<Record<string, string> | null> {
|
|
3653
|
+
if (account.authKind === "custom-connection") return null;
|
|
3529
3654
|
const marker = {
|
|
3530
3655
|
[LEASED_RUN_ENV]: String(lease.assignment.runEpoch),
|
|
3531
3656
|
[LEASED_ACCOUNT_ENV]: account.credentialId,
|
|
@@ -3562,6 +3687,7 @@ export class Orchestrator {
|
|
|
3562
3687
|
private leasedEnginesChanged(taskId: string): void {
|
|
3563
3688
|
const channel = this.channels.get(taskId);
|
|
3564
3689
|
if (!channel || !this.isActiveChannel(channel) || !this.isLeasedChannel(channel)) return;
|
|
3690
|
+
this.cancelStaleConnectionRetries(channel);
|
|
3565
3691
|
if (!leasedEnginesFor(taskId)?.open) return;
|
|
3566
3692
|
if (channel.leasedSyncRetry) {
|
|
3567
3693
|
clearTimeout(channel.leasedSyncRetry);
|
|
@@ -3746,6 +3872,7 @@ export class Orchestrator {
|
|
|
3746
3872
|
}
|
|
3747
3873
|
}
|
|
3748
3874
|
}
|
|
3875
|
+
await purgeInternalAgentSessionState(request, [...runtimes.values()]);
|
|
3749
3876
|
// Every admission of these runs not yet used is revoked in every runtime
|
|
3750
3877
|
// they were written in, or that runtime is stopped or gone and its gates
|
|
3751
3878
|
// with it: no revocation or slot retirement retained for them is left to
|
|
@@ -3763,6 +3890,7 @@ export class Orchestrator {
|
|
|
3763
3890
|
* so nothing launches on it again, its session is asked to stop, and it
|
|
3764
3891
|
* starts again on what the task holds once the gate opens. */
|
|
3765
3892
|
private quiesceLeasedAgent(channel: Channel, agentId: string): void {
|
|
3893
|
+
this.cancelConnectionRetries(channel, agentId);
|
|
3766
3894
|
channel.leasedIdentityByAgent.delete(agentId);
|
|
3767
3895
|
const session = channel.sessions.get(agentId);
|
|
3768
3896
|
if (this.isActiveChannel(channel)) channel.leasedRespawn.add(agentId);
|
|
@@ -3932,6 +4060,8 @@ export class Orchestrator {
|
|
|
3932
4060
|
}
|
|
3933
4061
|
const onOwn = ownHold !== undefined;
|
|
3934
4062
|
try {
|
|
4063
|
+
if (onOwn && args.agent.accountBinding === "required") { this.leasedUnavailable(channel, agentId, "unsupported"); return null; }
|
|
4064
|
+
args = { ...args, agentEnv: { ...connectionNativeBlanks(), UAI_CUSTOM_CONNECTION: "", ...(args.agentEnv ?? {}) } };
|
|
3935
4065
|
// ADR-076 + codex-cli 0.152.0: two codex agents that resolve to the SAME
|
|
3936
4066
|
// account share one CODEX_HOME, and codex's new sqlite state runtime
|
|
3937
4067
|
// cannot be opened by two concurrent processes — the second agent dies
|
|
@@ -4036,7 +4166,12 @@ export class Orchestrator {
|
|
|
4036
4166
|
void this.retireLeasedLaunch(channel, agentId, channel.leasedLaunches.get(agentId));
|
|
4037
4167
|
channel.leasedLaunches.set(agentId, slot);
|
|
4038
4168
|
}
|
|
4039
|
-
const
|
|
4169
|
+
const releaseConnectionSecret = identity?.subject === "account" && identity.connection
|
|
4170
|
+
? registerConnectionSecret(boundArgs.agentEnv?.UAI_CUSTOM_API_KEY || boundArgs.agentEnv?.AWS_BEARER_TOKEN_BEDROCK || boundArgs.agentEnv?.ANTHROPIC_AUTH_TOKEN || boundArgs.agentEnv?.ANTHROPIC_API_KEY || "")
|
|
4171
|
+
: () => {};
|
|
4172
|
+
let creating: Promise<AgentSession>;
|
|
4173
|
+
try {
|
|
4174
|
+
creating = this.factory.create({
|
|
4040
4175
|
...boundArgs,
|
|
4041
4176
|
environment:
|
|
4042
4177
|
identity && slot
|
|
@@ -4044,7 +4179,11 @@ export class Orchestrator {
|
|
|
4044
4179
|
: onOwn
|
|
4045
4180
|
? onOwnCredentialsEnvironment(environment)
|
|
4046
4181
|
: environment,
|
|
4047
|
-
|
|
4182
|
+
});
|
|
4183
|
+
} catch (error) {
|
|
4184
|
+
releaseConnectionSecret();
|
|
4185
|
+
throw identity?.subject === "account" && identity.connection ? new Error(connectionFailureReason(error)) : error;
|
|
4186
|
+
}
|
|
4048
4187
|
// Settled before this spawn's own continuation: from here until it
|
|
4049
4188
|
// binds, nothing launches on it.
|
|
4050
4189
|
if (slot) {
|
|
@@ -4074,14 +4213,22 @@ export class Orchestrator {
|
|
|
4074
4213
|
// after it launched, or its session is closed unbound) is retired at
|
|
4075
4214
|
// once, before anything else: a launch it handed over that has not
|
|
4076
4215
|
// started in its runtime never does.
|
|
4077
|
-
const unbound = (): Promise<void> =>
|
|
4078
|
-
|
|
4216
|
+
const unbound = (): Promise<void> => {
|
|
4217
|
+
releaseConnectionSecret();
|
|
4218
|
+
return slot ? this.retireLeasedLaunch(channel, agentId, slot) : Promise.resolve();
|
|
4219
|
+
};
|
|
4079
4220
|
let session: AgentSession;
|
|
4080
4221
|
try {
|
|
4081
4222
|
session = await creating;
|
|
4082
4223
|
} catch (error) {
|
|
4083
4224
|
await unbound();
|
|
4084
|
-
throw error;
|
|
4225
|
+
throw identity?.subject === "account" && identity.connection ? new Error(connectionFailureReason(error)) : error;
|
|
4226
|
+
}
|
|
4227
|
+
if (identity?.subject === "account" && identity.connection) this.connectionSecretReleases.set(session, releaseConnectionSecret);
|
|
4228
|
+
if (identity) this.sessionIdentities.set(session, { ...identity });
|
|
4229
|
+
if (identity?.subject === "account" && identity.connection) {
|
|
4230
|
+
const account = leasedEnginesFor(channel.taskId)?.accounts.find((a): a is LeasedEngineAccount => a.credentialId === identity.credentialId && a.revision === identity.revision && isLeasedEngineAccount(a));
|
|
4231
|
+
if (account?.connectionMaterial && account.connection) this.connectionSessions.set(session, { material: account.connectionMaterial, metadata: account.connection, identity: { ...identity } });
|
|
4085
4232
|
}
|
|
4086
4233
|
if (!this.isActiveChannel(channel)) {
|
|
4087
4234
|
await Promise.all([unbound(), session.close().catch(() => {})]);
|
|
@@ -4178,6 +4325,7 @@ export class Orchestrator {
|
|
|
4178
4325
|
scratchDir: string,
|
|
4179
4326
|
timeoutMs: number,
|
|
4180
4327
|
): Promise<{ exitCode: number | null; stdout: string } | null> {
|
|
4328
|
+
if (channel.roster.find(a => a.id === agentId)?.accountBinding === "required") return null;
|
|
4181
4329
|
const request = (env: Record<string, string>, admit: () => void) => ({
|
|
4182
4330
|
argv: agentCliArgv(handle, argv),
|
|
4183
4331
|
projectEnvProfile: "task" as const,
|
|
@@ -4205,7 +4353,7 @@ export class Orchestrator {
|
|
|
4205
4353
|
const lease = leasedEnginesFor(channel.taskId);
|
|
4206
4354
|
if (!lease?.open) return null;
|
|
4207
4355
|
const account = this.leasedAccountFor(channel, agentId, kind, lease);
|
|
4208
|
-
if (!account) return null;
|
|
4356
|
+
if (!account || account.authKind === "custom-connection") return null;
|
|
4209
4357
|
const identity: LeasedIdentity = {
|
|
4210
4358
|
subject: "account",
|
|
4211
4359
|
runEpoch: lease.assignment.runEpoch,
|
|
@@ -4740,6 +4888,8 @@ export class Orchestrator {
|
|
|
4740
4888
|
prompt: string | AcceptedPrompt,
|
|
4741
4889
|
assignment?: AssignmentDelivery,
|
|
4742
4890
|
): void {
|
|
4891
|
+
const connection = this.connectionSessions.get(session);
|
|
4892
|
+
if (connection?.material.expiresAt != null && connection.material.expiresAt <= Date.now()) { this.expireConnectionSession(channel, agentId, session); return; }
|
|
4743
4893
|
const replay = typeof prompt !== "string";
|
|
4744
4894
|
const text = typeof prompt === "string" ? prompt : prompt.text;
|
|
4745
4895
|
const leadRunId = replay ? (prompt as AcceptedPrompt).leadRunId ?? null : this.isSecretary(channel, agentId) ? channel.leadRunId ?? null : null;
|
|
@@ -4753,7 +4903,8 @@ export class Orchestrator {
|
|
|
4753
4903
|
const begun = journal.begin({ sessionId, engine: session.kind,
|
|
4754
4904
|
runEpoch: readTaskRunEpochState(channel.taskId)?.highestRunEpoch ?? 0,
|
|
4755
4905
|
recipientGeneration: channel.roster.find(a => a.id === agentId)?.tokenGeneration ?? null,
|
|
4756
|
-
runtimeVersion: evidenceRuntimeVersion, assignment: originalAssignment, replay, leadRunId
|
|
4906
|
+
runtimeVersion: evidenceRuntimeVersion, assignment: originalAssignment, replay, leadRunId,
|
|
4907
|
+
actualAccount: (() => { const identity = this.sessionIdentities.get(session); return identity?.subject === "account" ? { credentialId: identity.credentialId, credentialRevision: identity.revision, connection: identity.connection === true } : null; })() });
|
|
4757
4908
|
if (!begun) return; // stable assignment replay, already accepted
|
|
4758
4909
|
turn = begun;
|
|
4759
4910
|
retainedAssignment = begun.assignment;
|
|
@@ -4768,7 +4919,7 @@ export class Orchestrator {
|
|
|
4768
4919
|
this.incrementActiveTurns(channel, agentId);
|
|
4769
4920
|
void session.send(text).catch((err: unknown) => {
|
|
4770
4921
|
if (turn) journal?.failed(sessionId, turn.turnId);
|
|
4771
|
-
const diagnosticMessage = `Could not send the message to ${agentId}: ${err instanceof Error ? err.message : String(err)}`;
|
|
4922
|
+
const diagnosticMessage = this.connectionSessions.has(session) ? connectionFailureReason(err) : `Could not send the message to ${agentId}: ${err instanceof Error ? err.message : String(err)}`;
|
|
4772
4923
|
reportAgentError(diagnosticMessage, { taskId: channel.taskId, agentId, engine: session.kind },
|
|
4773
4924
|
{ capture: !(err instanceof ContainerRuntimeUnavailableError) });
|
|
4774
4925
|
if (channel.sessions.get(agentId) === session) {
|
|
@@ -5091,6 +5242,8 @@ export class Orchestrator {
|
|
|
5091
5242
|
if (!this.isActiveChannel(channel)) return;
|
|
5092
5243
|
switch (event.type) {
|
|
5093
5244
|
case "account_usage": {
|
|
5245
|
+
// Connection limits come from explicit provider budgets, never CLI login probes.
|
|
5246
|
+
if (this.connectionSessions.has(session)) break;
|
|
5094
5247
|
// Only the exact live session's cloud-leased ACCOUNT may be named on
|
|
5095
5248
|
// the wire. A runner default slot and the Managed Engine's task-token
|
|
5096
5249
|
// identity are deliberately unreportable.
|
|
@@ -5115,7 +5268,7 @@ export class Orchestrator {
|
|
|
5115
5268
|
credentialId: identity.credentialId,
|
|
5116
5269
|
revision: identity.revision,
|
|
5117
5270
|
usage: event.usage,
|
|
5118
|
-
leasedAccount,
|
|
5271
|
+
leasedAccount: leasedAccount?.authKind === "custom-connection" ? undefined : leasedAccount ? { ...leasedAccount, authKind: leasedAccount.authKind as "env" | "config-dir" } : undefined,
|
|
5119
5272
|
});
|
|
5120
5273
|
|
|
5121
5274
|
const coverage = event.usage.coverage;
|
|
@@ -5328,6 +5481,10 @@ export class Orchestrator {
|
|
|
5328
5481
|
);
|
|
5329
5482
|
break;
|
|
5330
5483
|
}
|
|
5484
|
+
if (agent?.accountBinding === "required") {
|
|
5485
|
+
await this.recoverConnectionAgent(channel, agent, connectionFailureReason(diagnosticMessage), abortBoundaryOnRosterChange, event.resetsAt);
|
|
5486
|
+
break;
|
|
5487
|
+
}
|
|
5331
5488
|
// ADR-076: a rate-limit/usage-cap error fails over to another account
|
|
5332
5489
|
// for this kind (cool the current one, recreate the session on the
|
|
5333
5490
|
// next, re-deliver the in-flight prompt). No-op with <2 accounts.
|
|
@@ -5508,14 +5665,16 @@ export class Orchestrator {
|
|
|
5508
5665
|
// ledger can roll up observed spend per credential (host 0.9.167+).
|
|
5509
5666
|
// Only a cloud-leased account identity is reportable; runner-default
|
|
5510
5667
|
// and Managed Engine identities stay unnamed (same rule as account_usage).
|
|
5511
|
-
const leasedIdentity = channel.leasedIdentityByAgent.get(agentId);
|
|
5668
|
+
const leasedIdentity = this.sessionIdentities.get(session) ?? channel.leasedIdentityByAgent.get(agentId);
|
|
5669
|
+
const connection = this.connectionSessions.get(session);
|
|
5670
|
+
const reportedUsage = connection && event.usage ? Object.fromEntries(Object.entries(event.usage).filter(([k]) => k !== "costUsd")) : event.usage;
|
|
5512
5671
|
const usage =
|
|
5513
|
-
|
|
5672
|
+
reportedUsage === undefined
|
|
5514
5673
|
? undefined
|
|
5515
5674
|
: {
|
|
5516
|
-
...
|
|
5675
|
+
...reportedUsage,
|
|
5517
5676
|
...(leasedIdentity?.subject === "account"
|
|
5518
|
-
? { credentialId: leasedIdentity.credentialId }
|
|
5677
|
+
? { credentialId: leasedIdentity.credentialId, credentialRevision: leasedIdentity.revision, connection: leasedIdentity.connection === true }
|
|
5519
5678
|
: {}),
|
|
5520
5679
|
};
|
|
5521
5680
|
this.emitHost({
|
|
@@ -5702,6 +5861,11 @@ export class Orchestrator {
|
|
|
5702
5861
|
): Promise<void> {
|
|
5703
5862
|
if (!this.isActiveChannel(channel) || channel.spawning.has(agentId)) return;
|
|
5704
5863
|
|
|
5864
|
+
const pinned = channel.roster.find(a => a.id === agentId);
|
|
5865
|
+
if (pinned?.accountBinding === "required") {
|
|
5866
|
+
await this.recoverConnectionAgent(channel, pinned, "upstream-error", abortBoundaryOnRosterChange);
|
|
5867
|
+
return;
|
|
5868
|
+
}
|
|
5705
5869
|
const tries = (channel.respawns.get(agentId) ?? 0) + 1;
|
|
5706
5870
|
channel.respawns.set(agentId, tries);
|
|
5707
5871
|
channel.respawnLastAt.set(agentId, Date.now());
|
|
@@ -5911,6 +6075,7 @@ export class Orchestrator {
|
|
|
5911
6075
|
// A concurrent terminal event for the same session is already being
|
|
5912
6076
|
// handled by the path that owns this spawn slot.
|
|
5913
6077
|
if (channel.spawning.has(agentId)) return true;
|
|
6078
|
+
if (agent.accountBinding === "required") return false;
|
|
5914
6079
|
if (this.engineAccounts(channel, agent.kind).length < 2) {
|
|
5915
6080
|
return false;
|
|
5916
6081
|
}
|
|
@@ -6133,10 +6298,11 @@ export class Orchestrator {
|
|
|
6133
6298
|
channel: Channel,
|
|
6134
6299
|
agentId: string,
|
|
6135
6300
|
agent: RosterAgent,
|
|
6136
|
-
account: ResolvedEngineAccount,
|
|
6301
|
+
account: ResolvedEngineAccount | null,
|
|
6137
6302
|
note: string,
|
|
6138
6303
|
abortBoundaryOnRosterChange = false,
|
|
6139
6304
|
): Promise<boolean> {
|
|
6305
|
+
if (!account && agent.accountBinding !== "required") return false;
|
|
6140
6306
|
if (channel.rosterSessionChanges.has(agentId)) {
|
|
6141
6307
|
this.reconcileErroredRosterChange(
|
|
6142
6308
|
channel,
|
|
@@ -6225,8 +6391,10 @@ export class Orchestrator {
|
|
|
6225
6391
|
cliSecret,
|
|
6226
6392
|
this.isSecretary(channel, agent.id) ? channel.leadRunId : null,
|
|
6227
6393
|
);
|
|
6228
|
-
|
|
6229
|
-
|
|
6394
|
+
if (account) {
|
|
6395
|
+
channel.accountByAgent.set(agentId, account.id);
|
|
6396
|
+
noteEngineAccountUsed(account.id);
|
|
6397
|
+
}
|
|
6230
6398
|
|
|
6231
6399
|
replacement = await this.createAndBindStableSession(
|
|
6232
6400
|
channel,
|
|
@@ -6243,7 +6411,7 @@ export class Orchestrator {
|
|
|
6243
6411
|
),
|
|
6244
6412
|
agentEnv: {
|
|
6245
6413
|
...this.machineProjectEnv(task),
|
|
6246
|
-
...account
|
|
6414
|
+
...(account?.execEnv ?? {}),
|
|
6247
6415
|
...base,
|
|
6248
6416
|
},
|
|
6249
6417
|
},
|
|
@@ -6325,6 +6493,113 @@ export class Orchestrator {
|
|
|
6325
6493
|
return true;
|
|
6326
6494
|
}
|
|
6327
6495
|
|
|
6496
|
+
private cancelConnectionRetries(channel: Channel, agentId?: string): void {
|
|
6497
|
+
const pending = this.connectionRetries.get(channel);
|
|
6498
|
+
if (!pending) return;
|
|
6499
|
+
for (const [id, entry] of pending) {
|
|
6500
|
+
if (agentId !== undefined && id !== agentId) continue;
|
|
6501
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
6502
|
+
pending.delete(id);
|
|
6503
|
+
channel.spawning.delete(id); // This timer owns the reservation.
|
|
6504
|
+
}
|
|
6505
|
+
}
|
|
6506
|
+
|
|
6507
|
+
private cancelStaleConnectionRetries(channel: Channel): void {
|
|
6508
|
+
const pending = this.connectionRetries.get(channel);
|
|
6509
|
+
if (!pending) return;
|
|
6510
|
+
for (const [id, entry] of pending) {
|
|
6511
|
+
if (this.hasDesiredLaunchSettings(channel, id, entry.agent) && this.leasedMaterialServed(channel, entry.identity)) continue;
|
|
6512
|
+
this.cancelConnectionRetries(channel, id);
|
|
6513
|
+
if (channel.roster.some(agent => agent.id === id)) channel.leasedRespawn.add(id);
|
|
6514
|
+
}
|
|
6515
|
+
}
|
|
6516
|
+
|
|
6517
|
+
private async deferConnectionRecovery(channel: Channel, agent: RosterAgent, dueAt: number): Promise<void> {
|
|
6518
|
+
const id = agent.id;
|
|
6519
|
+
const identity = channel.leasedIdentityByAgent.get(id);
|
|
6520
|
+
if (!identity) { this.leasedUnavailable(channel, id, "unsupported"); return; }
|
|
6521
|
+
channel.spawning.add(id);
|
|
6522
|
+
let scheduled = false;
|
|
6523
|
+
try {
|
|
6524
|
+
const old = channel.sessions.get(id);
|
|
6525
|
+
channel.sessions.delete(id);
|
|
6526
|
+
if (old) this.endLeasedLaunches(channel, id, old);
|
|
6527
|
+
this.retainRetiredTurnBoundary(channel, id, this.needsAbortedTurnBoundary(channel, id));
|
|
6528
|
+
this.retireRunnerGeneration(channel, id);
|
|
6529
|
+
this.emitRetiredTurnAbort(channel, id);
|
|
6530
|
+
this.emitSystemNote(channel.taskId, "rate-limited");
|
|
6531
|
+
if (old) await old.close().catch(() => {});
|
|
6532
|
+
if (!this.isActiveChannel(channel) || !this.hasDesiredLaunchSettings(channel, id, agent) || !this.leasedMaterialServed(channel, identity)) return;
|
|
6533
|
+
let pending = this.connectionRetries.get(channel);
|
|
6534
|
+
if (!pending) { pending = new Map(); this.connectionRetries.set(channel, pending); }
|
|
6535
|
+
const entry = { timer: null as ReturnType<typeof setTimeout> | null, agent, identity };
|
|
6536
|
+
pending.set(id, entry);
|
|
6537
|
+
const arm = (): void => {
|
|
6538
|
+
if (pending.get(id) !== entry) return;
|
|
6539
|
+
const remaining = dueAt - Date.now();
|
|
6540
|
+
if (remaining > 0) {
|
|
6541
|
+
entry.timer = setTimeout(arm, Math.min(remaining, 2_147_483_647));
|
|
6542
|
+
entry.timer.unref?.();
|
|
6543
|
+
return;
|
|
6544
|
+
}
|
|
6545
|
+
this.cancelConnectionRetries(channel, id);
|
|
6546
|
+
if (!this.isActiveChannel(channel) || !this.hasDesiredLaunchSettings(channel, id, agent) || !this.leasedMaterialServed(channel, identity)) return;
|
|
6547
|
+
this.trackEventOperation(channel, this.respawnAgentOnAccount(channel, id, agent, null, "rate-limited")
|
|
6548
|
+
.then(() => {})
|
|
6549
|
+
.catch(error => {
|
|
6550
|
+
if (this.isActiveChannel(channel) && this.hasDesiredLaunchSettings(channel, id, agent) &&
|
|
6551
|
+
this.leasedMaterialServed(channel, identity) && !channel.sessions.has(id)) {
|
|
6552
|
+
this.leasedUnavailable(channel, id, connectionFailureReason(error));
|
|
6553
|
+
}
|
|
6554
|
+
}));
|
|
6555
|
+
};
|
|
6556
|
+
scheduled = true;
|
|
6557
|
+
arm();
|
|
6558
|
+
} finally {
|
|
6559
|
+
if (!scheduled) {
|
|
6560
|
+
channel.spawning.delete(id);
|
|
6561
|
+
if (this.isActiveChannel(channel)) this.retryLeasedStart(channel, id);
|
|
6562
|
+
}
|
|
6563
|
+
}
|
|
6564
|
+
}
|
|
6565
|
+
|
|
6566
|
+
/** Retry a transport/runtime failure on exactly the same connection. Auth
|
|
6567
|
+
* failures stop; no native refresh, rotation or runner login is consulted. */
|
|
6568
|
+
private async recoverConnectionAgent(
|
|
6569
|
+
channel: Channel, agent: RosterAgent, reason: CustomConnectionReason,
|
|
6570
|
+
abortBoundaryOnRosterChange?: boolean, retryAt?: number,
|
|
6571
|
+
): Promise<void> {
|
|
6572
|
+
const agentId = agent.id;
|
|
6573
|
+
if (!this.isActiveChannel(channel) || channel.spawning.has(agentId)) return;
|
|
6574
|
+
if (!this.hasDesiredLaunchSettings(channel, agentId, agent)) return;
|
|
6575
|
+
if (agent.kind !== "claude" && agent.kind !== "codex") reason = "unsupported";
|
|
6576
|
+
const terminal = ["unauthorized", "forbidden", "expired", "unsupported", "private-address", "model-not-found", "wrong-protocol"].includes(reason);
|
|
6577
|
+
const tries = (channel.respawns.get(agentId) ?? 0) + 1;
|
|
6578
|
+
channel.respawns.set(agentId, tries);
|
|
6579
|
+
channel.respawnLastAt.set(agentId, Date.now());
|
|
6580
|
+
const providerRetryTooLate = reason === "rate-limited" &&
|
|
6581
|
+
typeof retryAt === "number" && Number.isSafeInteger(retryAt) && retryAt - Date.now() > 60 * 60_000;
|
|
6582
|
+
if (terminal || providerRetryTooLate || tries > MAX_RESPAWNS_PER_AGENT) {
|
|
6583
|
+
const old = channel.sessions.get(agentId);
|
|
6584
|
+
if (old) this.endLeasedLaunches(channel, agentId, old);
|
|
6585
|
+
this.retainRetiredTurnBoundary(channel, agentId, this.needsAbortedTurnBoundary(channel, agentId));
|
|
6586
|
+
this.retireRunnerGeneration(channel, agentId);
|
|
6587
|
+
channel.sessions.delete(agentId);
|
|
6588
|
+
this.emitRetiredTurnAbort(channel, agentId);
|
|
6589
|
+
this.leasedUnavailable(channel, agentId, reason);
|
|
6590
|
+
if (old) await old.close().catch(() => {});
|
|
6591
|
+
return;
|
|
6592
|
+
}
|
|
6593
|
+
if (reason === "rate-limited") {
|
|
6594
|
+
const delay = Math.max(Math.min(60_000, 3000 * 2 ** (tries - 1)),
|
|
6595
|
+
typeof retryAt === "number" && Number.isSafeInteger(retryAt) ? retryAt - Date.now() : 0);
|
|
6596
|
+
await this.deferConnectionRecovery(channel, agent, Date.now() + delay);
|
|
6597
|
+
return;
|
|
6598
|
+
}
|
|
6599
|
+
await this.respawnAgentOnAccount(channel, agentId, agent, null, reason,
|
|
6600
|
+
abortBoundaryOnRosterChange ?? this.needsAbortedTurnBoundary(channel, agentId));
|
|
6601
|
+
}
|
|
6602
|
+
|
|
6328
6603
|
/**
|
|
6329
6604
|
* Refresh ONE agent's credential by re-spawning it on its current
|
|
6330
6605
|
* (re-resolved) account — env accounts read process.env at spawn, so a
|
|
@@ -6335,8 +6610,13 @@ export class Orchestrator {
|
|
|
6335
6610
|
channel: Channel,
|
|
6336
6611
|
agent: RosterAgent,
|
|
6337
6612
|
abortBoundaryOnRosterChange?: boolean,
|
|
6613
|
+
reason: CustomConnectionReason = "upstream-error",
|
|
6338
6614
|
): Promise<void> {
|
|
6339
6615
|
const agentId = agent.id;
|
|
6616
|
+
if (agent.accountBinding === "required") {
|
|
6617
|
+
await this.recoverConnectionAgent(channel, agent, reason, abortBoundaryOnRosterChange);
|
|
6618
|
+
return;
|
|
6619
|
+
}
|
|
6340
6620
|
if (!this.isActiveChannel(channel) || channel.spawning.has(agentId)) return;
|
|
6341
6621
|
if (!this.hasDesiredLaunchSettings(channel, agentId, agent)) return;
|
|
6342
6622
|
const abortBoundary =
|
|
@@ -6481,6 +6761,7 @@ export class Orchestrator {
|
|
|
6481
6761
|
// start/reconcile factory continuations all check this identity, so none
|
|
6482
6762
|
// can repopulate an orphan channel while its sessions close.
|
|
6483
6763
|
ch.closed = true;
|
|
6764
|
+
this.cancelConnectionRetries(ch);
|
|
6484
6765
|
this.channels.delete(taskId);
|
|
6485
6766
|
// ADR-154 §6: nothing its agents' spawns were admitted for starts after:
|
|
6486
6767
|
// the close ends only once every retirement of the task under way, its
|
|
@@ -6798,6 +7079,7 @@ function rosterLaunchChangeReason(
|
|
|
6798
7079
|
if (previous.kind !== next.kind) changed.push("engine");
|
|
6799
7080
|
if ((previous.model ?? "") !== (next.model ?? "")) changed.push("model");
|
|
6800
7081
|
if ((previous.effort ?? "") !== (next.effort ?? "")) changed.push("effort");
|
|
7082
|
+
if ((previous.accountBinding ?? "preferred") !== (next.accountBinding ?? "preferred")) changed.push("binding");
|
|
6801
7083
|
if ((previous.accountId ?? "") !== (next.accountId ?? "")) {
|
|
6802
7084
|
changed.push("account");
|
|
6803
7085
|
}
|