@humwick/runner 0.130.1 → 0.130.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +12 -16
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +31 -1
- package/src/adapters/claude-effort.ts +2 -4
- package/src/adapters/claude-headless.ts +1 -1
- package/src/adapters/claude-launch-exit.ts +26 -0
- package/src/adapters/claude-session-probe.ts +1 -1
- package/src/adapters/claude-tmux.ts +38 -10
- package/src/adapters/claude.ts +28 -75
- package/src/adapters/codex-launch-config.ts +4 -5
- package/src/adapters/codex.ts +1 -1
- package/src/adapters/opencode-provider.ts +7 -22
- package/src/adapters/opencode.ts +4 -4
- package/src/adapters/provider-quota-harvest.ts +20 -71
- package/src/agent-runner/credentials.ts +42 -12
- package/src/agent-runner/inbox.ts +27 -0
- package/src/agent-runner/lifecycle-milestones.ts +2 -4
- package/src/agent-runner/liveness-exchange.ts +104 -0
- package/src/bin/claude.ts +4 -0
- package/src/bin/codex.ts +4 -0
- package/src/bin/opencode.ts +4 -0
- package/src/claude-system-prompt.ts +49 -0
- package/src/index.ts +5 -2
- package/src/launch-assembly.ts +14 -10
- package/src/profile-home.ts +1 -1
- package/src/project-instructions.ts +2 -2
- package/src/provider-command.ts +18 -0
- package/src/provider-setup.ts +35 -0
- package/src/providers.ts +2 -1
- package/src/runner-core.ts +32 -38
- package/src/runner-helpers.ts +0 -8
- package/src/session-scratch.ts +6 -2
- package/src/spawn-input.ts +2 -1
- package/src/test-run-dedup.ts +59 -56
- package/node_modules/@humwick/offload/LICENSE +0 -167
- package/node_modules/@humwick/offload/README.md +0 -57
- package/node_modules/@humwick/offload/dist/async-runner.d.ts +0 -18
- package/node_modules/@humwick/offload/dist/async-runner.d.ts.map +0 -1
- package/node_modules/@humwick/offload/dist/async-runner.js +0 -158
- package/node_modules/@humwick/offload/dist/async-runner.js.map +0 -1
- package/node_modules/@humwick/offload/dist/envelope.d.ts +0 -25
- package/node_modules/@humwick/offload/dist/envelope.d.ts.map +0 -1
- package/node_modules/@humwick/offload/dist/envelope.js +0 -80
- package/node_modules/@humwick/offload/dist/envelope.js.map +0 -1
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts +0 -10
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts.map +0 -1
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js +0 -59
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js.map +0 -1
- package/node_modules/@humwick/offload/dist/index.d.ts +0 -5
- package/node_modules/@humwick/offload/dist/index.d.ts.map +0 -1
- package/node_modules/@humwick/offload/dist/index.js +0 -5
- package/node_modules/@humwick/offload/dist/index.js.map +0 -1
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts +0 -62
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts.map +0 -1
- package/node_modules/@humwick/offload/dist/subprocess-runner.js +0 -295
- package/node_modules/@humwick/offload/dist/subprocess-runner.js.map +0 -1
- package/node_modules/@humwick/offload/dist/types.d.ts +0 -111
- package/node_modules/@humwick/offload/dist/types.d.ts.map +0 -1
- package/node_modules/@humwick/offload/dist/types.js +0 -176
- package/node_modules/@humwick/offload/dist/types.js.map +0 -1
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts +0 -23
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts.map +0 -1
- package/node_modules/@humwick/offload/dist/worker-pool.js +0 -435
- package/node_modules/@humwick/offload/dist/worker-pool.js.map +0 -1
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts +0 -2
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts.map +0 -1
- package/node_modules/@humwick/offload/dist/worker-runtime.js +0 -27
- package/node_modules/@humwick/offload/dist/worker-runtime.js.map +0 -1
- package/node_modules/@humwick/offload/dist/worker.d.ts +0 -5
- package/node_modules/@humwick/offload/dist/worker.d.ts.map +0 -1
- package/node_modules/@humwick/offload/dist/worker.js +0 -1
- package/node_modules/@humwick/offload/dist/worker.js.map +0 -1
- package/node_modules/@humwick/offload/package.json +0 -42
- package/node_modules/@humwick/offload/src/async-runner.ts +0 -170
- package/node_modules/@humwick/offload/src/envelope.ts +0 -92
- package/node_modules/@humwick/offload/src/fixtures/worker-task.ts +0 -49
- package/node_modules/@humwick/offload/src/index.ts +0 -4
- package/node_modules/@humwick/offload/src/subprocess-runner.ts +0 -334
- package/node_modules/@humwick/offload/src/types.ts +0 -235
- package/node_modules/@humwick/offload/src/worker-pool.ts +0 -463
- package/node_modules/@humwick/offload/src/worker-runtime.ts +0 -32
- package/node_modules/@humwick/offload/src/worker.ts +0 -5
- package/src/agent-runner/http-liveness.ts +0 -85
- package/src/gate-run-lease-probe.ts +0 -38
|
@@ -1,61 +1,47 @@
|
|
|
1
1
|
import { writeChildEnv } from "@humwick/sdk/env";
|
|
2
|
-
import type { QuotaState,
|
|
2
|
+
import type { QuotaState, HeartbeatReport, HeartbeatResult } from "@humwick/sdk";
|
|
3
3
|
import { getManifest } from "@humwick/providers";
|
|
4
4
|
import { quotaStateFromProbeMetrics } from "@humwick/sdk/context-probe";
|
|
5
5
|
import { claudeQuotaGeneration, claudeQuotaUnavailableWindows } from "@humwick/sdk/provider-quota-order";
|
|
6
6
|
import { readRunnerContextProbeState } from "../context-probe-state";
|
|
7
7
|
import { providerHostConfigHome } from "../profile-home";
|
|
8
|
-
import {
|
|
8
|
+
import { resolveClaudeStatuslineQuotaIdentity, type ProviderQuotaIdentity } from "../quota";
|
|
9
9
|
|
|
10
|
-
const PASSIVE_QUOTA_CONFIG_REFRESH_MS = 60_000;
|
|
11
10
|
const PASSIVE_QUOTA_SAMPLE_STALE_MS = 10 * 60_000;
|
|
12
|
-
const PASSIVE_QUOTA_LEASE_MAX_TTL_MS = 10 * 60_000;
|
|
13
|
-
const PASSIVE_QUOTA_LEASE_PADDING_MS = 60_000;
|
|
14
11
|
|
|
15
12
|
interface ProviderQuotaHarvestOptions {
|
|
16
13
|
agentId: string;
|
|
17
14
|
provider: string;
|
|
18
15
|
/** The runner's per-provider config env (central provider-config values), read lazily each publish. */
|
|
19
16
|
providerConfigEnv: () => Record<string, string>;
|
|
20
|
-
http: () => Pick<HumwickHttpClient, "acquireProviderQuotaPublisherLease" | "getProviderQuotaConfig" | "upsertProviderQuota">;
|
|
21
17
|
}
|
|
22
18
|
|
|
23
19
|
export class ProviderQuotaHarvest {
|
|
24
|
-
private config = { ...DEFAULT_PROVIDER_QUOTA_CONFIG };
|
|
25
|
-
private configRefreshAt = 0;
|
|
26
|
-
private lastReportAt?: number;
|
|
27
20
|
private lastReportedObservedAt?: number;
|
|
28
|
-
private
|
|
29
|
-
private
|
|
21
|
+
private nextAttemptAt = 0;
|
|
22
|
+
private lease?: { accountKey: string; leaseToken: string };
|
|
30
23
|
|
|
31
24
|
constructor(private readonly options: ProviderQuotaHarvestOptions) {}
|
|
32
25
|
|
|
33
|
-
async
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (getManifest(this.options.provider)?.shim?.kind !== "claude-relay") return;
|
|
38
|
-
const probeMetrics = readRunnerContextProbeState(this.options.agentId);
|
|
39
|
-
const now = Date.now();
|
|
40
|
-
const quota = probeMetrics ? freshQuotaFromProbeMetrics(probeMetrics, now) : undefined;
|
|
41
|
-
// An idle session re-sends nothing new; skipping lets its lease lapse to an active session (#3082).
|
|
26
|
+
async sample(): Promise<Pick<HeartbeatReport, "providerQuota" | "quotaLeaseToken"> | undefined> {
|
|
27
|
+
if (getManifest(this.options.provider)?.shim?.kind !== "claude-relay" || Date.now() < this.nextAttemptAt) return;
|
|
28
|
+
const metrics = readRunnerContextProbeState(this.options.agentId);
|
|
29
|
+
const quota = metrics ? freshQuotaFromProbeMetrics(metrics, Date.now()) : undefined;
|
|
42
30
|
if (!quota || (this.lastReportedObservedAt !== undefined && quota.updatedAt <= this.lastReportedObservedAt)) return;
|
|
43
|
-
const config = await this.refreshConfig();
|
|
44
|
-
if (!config.enabled) return;
|
|
45
|
-
if (this.lastReportAt !== undefined && now - this.lastReportAt < config.pollIntervalMs) return;
|
|
46
31
|
const identity = await this.resolveIdentity();
|
|
47
32
|
if (!identity) return;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
accountKey
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
this.
|
|
58
|
-
this.lastReportedObservedAt = quota
|
|
33
|
+
return {
|
|
34
|
+
providerQuota: { provider: this.options.provider, accountKey: identity.accountKey, quota,
|
|
35
|
+
unavailableWindows: claudeQuotaUnavailableWindows(quota), lastAttemptAt: Date.now(), sourceAgentId: this.options.agentId },
|
|
36
|
+
...(this.lease?.accountKey === identity.accountKey ? { quotaLeaseToken: this.lease.leaseToken } : {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
accept(report: HeartbeatReport, result: HeartbeatResult["providerQuota"]): void {
|
|
41
|
+
if (!result || !report.providerQuota) return;
|
|
42
|
+
this.nextAttemptAt = Date.now() + result.retryAfterMs;
|
|
43
|
+
if (result.reported) this.lastReportedObservedAt = report.providerQuota.quota?.updatedAt;
|
|
44
|
+
if (result.leaseToken) this.lease = { accountKey: report.providerQuota.accountKey, leaseToken: result.leaseToken };
|
|
59
45
|
}
|
|
60
46
|
|
|
61
47
|
// #2514 — quota identity resolves exactly as the LAUNCH authenticates. The primary claude
|
|
@@ -77,43 +63,6 @@ export class ProviderQuotaHarvest {
|
|
|
77
63
|
});
|
|
78
64
|
}
|
|
79
65
|
|
|
80
|
-
private async refreshConfig() {
|
|
81
|
-
const now = Date.now();
|
|
82
|
-
if (now < this.configRefreshAt) return this.config;
|
|
83
|
-
this.configRefreshAt = now + PASSIVE_QUOTA_CONFIG_REFRESH_MS;
|
|
84
|
-
try {
|
|
85
|
-
const map = await this.options.http().getProviderQuotaConfig();
|
|
86
|
-
const providerCfg = map[this.options.provider];
|
|
87
|
-
this.config = providerCfg
|
|
88
|
-
? normalizeProviderQuotaConfig(providerCfg)
|
|
89
|
-
: { ...DEFAULT_PROVIDER_QUOTA_CONFIG };
|
|
90
|
-
} catch {
|
|
91
|
-
// Keep the previous config; defaults preserve collection if the relay is older
|
|
92
|
-
// or temporarily unreachable during a liveness tick.
|
|
93
|
-
}
|
|
94
|
-
return this.config;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
private async ensurePublisherLease(accountKey: string, pollIntervalMs: number, now: number): Promise<boolean> {
|
|
98
|
-
if (this.lease?.accountKey === accountKey && this.lease.expiresAt > now) return true;
|
|
99
|
-
if (this.nextLeaseAttemptAt !== undefined && now < this.nextLeaseAttemptAt) return false;
|
|
100
|
-
const leaseToken = this.lease?.accountKey === accountKey ? this.lease.leaseToken : undefined;
|
|
101
|
-
const result = await this.options.http().acquireProviderQuotaPublisherLease({
|
|
102
|
-
provider: this.options.provider,
|
|
103
|
-
accountKey,
|
|
104
|
-
sourceAgentId: this.options.agentId,
|
|
105
|
-
...(leaseToken ? { leaseToken } : {}),
|
|
106
|
-
ttlMs: Math.min(PASSIVE_QUOTA_LEASE_MAX_TTL_MS, pollIntervalMs + PASSIVE_QUOTA_LEASE_PADDING_MS),
|
|
107
|
-
});
|
|
108
|
-
if (!result.acquired || !result.lease) {
|
|
109
|
-
this.lease = undefined;
|
|
110
|
-
this.nextLeaseAttemptAt = now + Math.min(result.retryAfterMs ?? pollIntervalMs, pollIntervalMs);
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
this.lease = { accountKey, leaseToken: result.lease.leaseToken, expiresAt: result.lease.expiresAt };
|
|
114
|
-
this.nextLeaseAttemptAt = undefined;
|
|
115
|
-
return true;
|
|
116
|
-
}
|
|
117
66
|
}
|
|
118
67
|
|
|
119
68
|
function freshQuotaFromProbeMetrics(metrics: NonNullable<ReturnType<typeof readRunnerContextProbeState>>, now: number): QuotaState | undefined {
|
|
@@ -14,13 +14,26 @@ interface CredentialsDeps {
|
|
|
14
14
|
publishRenewalFailure(): void;
|
|
15
15
|
logRunnerDiagnostic(message: string): void;
|
|
16
16
|
reportBackgroundRejection(scope: string, status: string, title: string, error: unknown): void;
|
|
17
|
+
/** Recovery gave up: the runner must exit so Relay sees a terminal failure, not a mint loop (#3775). */
|
|
18
|
+
onTerminalAuthFailure(reason: string): void;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
const TOKEN_RENEW_LOG_INTERVAL_MS = 5 * 60 * 1000;
|
|
20
22
|
const TOKEN_RENEW_RETRY_MS = 60_000;
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
23
|
+
// #3775 — a successful mint does not prove the hub accepts the runner: an orphaned runner kept
|
|
24
|
+
// getting 401 on its fresh token and re-minted ~2/s. Auth recoveries back off (1 s doubling to
|
|
25
|
+
// 5 min, jittered) and stop after a bounded count; only a quiet spell resets the count.
|
|
26
|
+
export const AUTH_RECOVERY_MAX_ATTEMPTS = 10;
|
|
27
|
+
const AUTH_RECOVERY_BASE_MS = 1_000;
|
|
28
|
+
const AUTH_RECOVERY_MAX_DELAY_MS = 5 * 60_000;
|
|
29
|
+
const AUTH_RECOVERY_QUIET_RESET_MS = 30 * 60_000;
|
|
30
|
+
|
|
31
|
+
/** Delay before auth recovery `attempt` (0-based): the first heals at once, later ones back off with equal jitter. */
|
|
32
|
+
export function authRecoveryDelayMs(attempt: number, random: () => number = Math.random): number {
|
|
33
|
+
if (attempt <= 0) return 0;
|
|
34
|
+
const ceiling = Math.min(AUTH_RECOVERY_MAX_DELAY_MS, AUTH_RECOVERY_BASE_MS * 2 ** (attempt - 1));
|
|
35
|
+
return Math.round(ceiling / 2 + random() * (ceiling / 2));
|
|
36
|
+
}
|
|
24
37
|
|
|
25
38
|
export class Credentials {
|
|
26
39
|
private currentToken?: string;
|
|
@@ -30,7 +43,10 @@ export class Credentials {
|
|
|
30
43
|
private tokenRenewTimer?: Timer;
|
|
31
44
|
private readonly tokenRotation = new TokenRotation();
|
|
32
45
|
private tokenRenewLastLog?: { key: string; at: number };
|
|
33
|
-
private
|
|
46
|
+
private authRecoveryAttempts = 0;
|
|
47
|
+
private authRecoveryLastAt?: number;
|
|
48
|
+
private authRecoveryTimer?: Timer;
|
|
49
|
+
private authRecoveryExhausted = false;
|
|
34
50
|
|
|
35
51
|
constructor(options: { token?: string; tokenJti?: string; tokenProfileId?: string; tokenExpiresAt?: number }, private readonly deps: CredentialsDeps) {
|
|
36
52
|
this.currentToken = options.token;
|
|
@@ -47,6 +63,8 @@ export class Credentials {
|
|
|
47
63
|
stop(): void {
|
|
48
64
|
if (this.tokenRenewTimer) clearTimeout(this.tokenRenewTimer);
|
|
49
65
|
this.tokenRenewTimer = undefined;
|
|
66
|
+
if (this.authRecoveryTimer) clearTimeout(this.authRecoveryTimer);
|
|
67
|
+
this.authRecoveryTimer = undefined;
|
|
50
68
|
}
|
|
51
69
|
|
|
52
70
|
// Timer/edge callers cannot await renewal; report an escaping rejection so it cannot
|
|
@@ -55,22 +73,35 @@ export class Credentials {
|
|
|
55
73
|
void this.renewRuntimeToken().catch((error) => this.deps.reportBackgroundRejection("runtime token renewal", "runtime-token-renewal-failed", "Runtime token renewal aborted", error));
|
|
56
74
|
}
|
|
57
75
|
|
|
58
|
-
// The TTL timer cannot detect revocation: recover on auth failure, coalescing a burst
|
|
76
|
+
// The TTL timer cannot detect revocation: recover on auth failure, coalescing a burst into the
|
|
77
|
+
// one recovery in flight or scheduled.
|
|
59
78
|
// Orchestrator re-mint heals stale/expired tokens but preserves hard revocation (#484).
|
|
60
79
|
recoverRuntimeTokenAfterAuthFailure(source: string): void {
|
|
61
|
-
if (this.deps.isStopped() || this.tokenRotation.inFlight) return;
|
|
80
|
+
if (this.deps.isStopped() || this.authRecoveryExhausted || this.tokenRotation.inFlight || this.authRecoveryTimer) return;
|
|
62
81
|
if (!this.isRuntimeTokenRenewable() && !this.canRemintViaOrchestrator()) return;
|
|
63
82
|
const now = Date.now();
|
|
64
|
-
if (this.
|
|
65
|
-
this.
|
|
66
|
-
|
|
67
|
-
|
|
83
|
+
if (this.authRecoveryLastAt !== undefined && now - this.authRecoveryLastAt >= AUTH_RECOVERY_QUIET_RESET_MS) this.authRecoveryAttempts = 0;
|
|
84
|
+
if (this.authRecoveryAttempts >= AUTH_RECOVERY_MAX_ATTEMPTS) {
|
|
85
|
+
this.authRecoveryExhausted = true;
|
|
86
|
+
this.stop();
|
|
87
|
+
this.deps.onTerminalAuthFailure(`runtime-token-auth-failed: relay still rejects the runner (last on ${source}) after ${AUTH_RECOVERY_MAX_ATTEMPTS} token recoveries`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const delayMs = authRecoveryDelayMs(this.authRecoveryAttempts);
|
|
91
|
+
this.authRecoveryAttempts += 1;
|
|
92
|
+
this.authRecoveryLastAt = now;
|
|
93
|
+
this.deps.logRunnerDiagnostic(`[runner] relay auth failure on ${source}; recovering runtime token (attempt ${this.authRecoveryAttempts}/${AUTH_RECOVERY_MAX_ATTEMPTS}${delayMs ? ` in ${Math.ceil(delayMs / 1000)}s` : ""})`);
|
|
94
|
+
if (!delayMs) return this.renewRuntimeTokenInBackground();
|
|
95
|
+
this.authRecoveryTimer = setTimeout(() => {
|
|
96
|
+
this.authRecoveryTimer = undefined;
|
|
97
|
+
this.renewRuntimeTokenInBackground();
|
|
98
|
+
}, delayMs);
|
|
68
99
|
}
|
|
69
100
|
|
|
70
101
|
scheduleRuntimeTokenRenewal(delayMs?: number): void {
|
|
71
102
|
if (this.tokenRenewTimer) clearTimeout(this.tokenRenewTimer);
|
|
72
103
|
this.tokenRenewTimer = undefined;
|
|
73
|
-
if (this.deps.isStopped()) return;
|
|
104
|
+
if (this.deps.isStopped() || this.authRecoveryExhausted) return;
|
|
74
105
|
const canSelfRenew = this.isRuntimeTokenRenewable();
|
|
75
106
|
const canRemint = this.canRemintViaOrchestrator();
|
|
76
107
|
// Keep the renewal clock ticking as long as the session can recover its token
|
|
@@ -152,7 +183,6 @@ export class Credentials {
|
|
|
152
183
|
this.currentTokenProfileId = record.profileId ?? this.currentTokenProfileId;
|
|
153
184
|
this.currentTokenExpiresAt = record.expiresAt;
|
|
154
185
|
this.deps.onTokenChanged(token, record);
|
|
155
|
-
this.reactiveTokenRecoveryAt = undefined;
|
|
156
186
|
this.deps.resumeLivenessAndPublishRotation(status, record.jti);
|
|
157
187
|
this.scheduleRuntimeTokenRenewal();
|
|
158
188
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Message } from "@humwick/sdk";
|
|
2
|
+
import type { HumwickHttpClient } from "@humwick/sdk/http-client";
|
|
3
|
+
import { isNotificationOnlyInput } from "../inbox-carrier";
|
|
4
|
+
import { logger } from "../logger";
|
|
5
|
+
|
|
6
|
+
export async function bootstrapUnreadMessages(http: Pick<HumwickHttpClient, "pollMessages">, agentId: string, enqueue: (message: Message) => void): Promise<void> {
|
|
7
|
+
try {
|
|
8
|
+
const messages = await http.pollMessages({ for: agentId, unread: true, limit: 100, turnDelivery: true });
|
|
9
|
+
for (const message of messages) enqueue(message);
|
|
10
|
+
} catch (error) { logger.error("runner", `inbox bootstrap failed: ${error}`); }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Keep startup failure visible to each sender, including mail queued before the check (#3751). */
|
|
14
|
+
export async function reportUnavailableMessages(
|
|
15
|
+
http: Pick<HumwickHttpClient, "sendMessage" | "markRead">, agentId: string,
|
|
16
|
+
pending: Map<number, Message>, settled: Set<number>, reason: string,
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
for (const [id, message] of pending) {
|
|
19
|
+
try {
|
|
20
|
+
if (!isNotificationOnlyInput(message)) await http.sendMessage({ from: agentId, to: message.from, replyTo: id,
|
|
21
|
+
body: reason, idempotencyKey: `provider-startup-${agentId}-${id}`, payload: { kind: "notification" } });
|
|
22
|
+
await http.markRead(id, agentId);
|
|
23
|
+
pending.delete(id);
|
|
24
|
+
settled.add(id);
|
|
25
|
+
} catch (error) { logger.error("runner", `startup failure reply failed: ${error}`); }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -15,7 +15,6 @@ interface LifecycleMilestoneDeps {
|
|
|
15
15
|
process(): ManagedProcess | undefined;
|
|
16
16
|
stopAttachmentCacheSweep(): void;
|
|
17
17
|
stopTestRunDedupSweep(): void;
|
|
18
|
-
stopGateRunLeaseProbe(): void;
|
|
19
18
|
disposeEmptyTurns(): void;
|
|
20
19
|
resetEmptyTurns(): void;
|
|
21
20
|
resetLateClearRecovery(): void;
|
|
@@ -25,7 +24,7 @@ interface LifecycleMilestoneDeps {
|
|
|
25
24
|
stopDrainTimer(): void;
|
|
26
25
|
stopSubmitRecoveryPoll(): void;
|
|
27
26
|
stopTaskClaims(): void;
|
|
28
|
-
|
|
27
|
+
stopLiveness(): void;
|
|
29
28
|
stopCredentials(): void;
|
|
30
29
|
disarmBusyLivenessReconcile(): void;
|
|
31
30
|
disarmBusyReconciler(): void;
|
|
@@ -92,7 +91,6 @@ export class LifecycleMilestones {
|
|
|
92
91
|
private backgroundStopped(): void {
|
|
93
92
|
this.deps.stopAttachmentCacheSweep();
|
|
94
93
|
this.deps.stopTestRunDedupSweep();
|
|
95
|
-
this.deps.stopGateRunLeaseProbe();
|
|
96
94
|
// #2389 — before the shutdown await, not after: an escalation or settle timer firing during a
|
|
97
95
|
// graceful provider shutdown would publish a "produced nothing" verdict about a turn the
|
|
98
96
|
// shutdown itself ended.
|
|
@@ -142,7 +140,7 @@ export class LifecycleMilestones {
|
|
|
142
140
|
this.deps.stopDrainTimer();
|
|
143
141
|
this.deps.stopSubmitRecoveryPoll();
|
|
144
142
|
this.deps.stopTaskClaims();
|
|
145
|
-
this.deps.
|
|
143
|
+
this.deps.stopLiveness();
|
|
146
144
|
this.deps.stopCredentials();
|
|
147
145
|
this.deps.disarmBusyLivenessReconcile();
|
|
148
146
|
this.deps.disarmBusyReconciler();
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { errMessage, type HumwickBusClient, type HumwickHttpClient, type HeartbeatReport, type HeartbeatResult, type Message } from "@humwick/sdk";
|
|
2
|
+
import { httpErrorKey, isHttpAuthError, runnerAgentStatus } from "../runner-helpers";
|
|
3
|
+
import type { ClaimTracker } from "../claim-tracker";
|
|
4
|
+
|
|
5
|
+
const LIVENESS_INTERVAL_MS = 20_000;
|
|
6
|
+
// Shorter than the interval, so a wedged bus exchange falls back to HTTP before the next tick.
|
|
7
|
+
const BUS_LIVENESS_TIMEOUT_MS = 15_000;
|
|
8
|
+
|
|
9
|
+
interface LivenessExchangeDeps {
|
|
10
|
+
http: Pick<HumwickHttpClient, "heartbeat">;
|
|
11
|
+
bus(): Pick<HumwickBusClient, "livenessAvailable" | "reportLiveness">;
|
|
12
|
+
agentId: string;
|
|
13
|
+
instanceId: string;
|
|
14
|
+
logIntervalMs: number;
|
|
15
|
+
isStopped(): boolean;
|
|
16
|
+
isTerminalStatusLatched(): boolean;
|
|
17
|
+
currentStatus: ClaimTracker["currentStatus"];
|
|
18
|
+
sampleQuota(): Promise<Pick<HeartbeatReport, "providerQuota" | "quotaLeaseToken"> | undefined>;
|
|
19
|
+
acceptQuota(report: HeartbeatReport, result: HeartbeatResult["providerQuota"]): void;
|
|
20
|
+
enqueueMessage(message: Message): void;
|
|
21
|
+
triggerEffort(): void;
|
|
22
|
+
recoverRuntimeTokenAfterAuthFailure(source: string): void;
|
|
23
|
+
logRunnerDiagnostic(message: string): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The runner's periodic liveness exchange (#3608). It rides the registered bus socket and falls
|
|
27
|
+
* back to HTTP while the socket is down or the exchange fails on it (#3618). */
|
|
28
|
+
export class LivenessExchange {
|
|
29
|
+
private timer?: Timer;
|
|
30
|
+
private inFlight = false;
|
|
31
|
+
private authFailed = false;
|
|
32
|
+
private lastLog?: { key: string; at: number };
|
|
33
|
+
|
|
34
|
+
constructor(private readonly deps: LivenessExchangeDeps) {}
|
|
35
|
+
|
|
36
|
+
stop(): void {
|
|
37
|
+
if (this.timer) clearInterval(this.timer);
|
|
38
|
+
this.timer = undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
resetAuthFailure(): void { this.authFailed = false; }
|
|
42
|
+
|
|
43
|
+
start(): void {
|
|
44
|
+
if (this.timer) clearInterval(this.timer);
|
|
45
|
+
void this.publish();
|
|
46
|
+
this.timer = setInterval(() => {
|
|
47
|
+
void this.publish();
|
|
48
|
+
}, LIVENESS_INTERVAL_MS);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private async publish(): Promise<void> {
|
|
52
|
+
if (this.deps.isStopped() || this.deps.isTerminalStatusLatched() || this.inFlight || this.authFailed) return;
|
|
53
|
+
this.inFlight = true;
|
|
54
|
+
const status = this.deps.currentStatus();
|
|
55
|
+
const agentStatus = runnerAgentStatus(status);
|
|
56
|
+
try {
|
|
57
|
+
const quota = await this.deps.sampleQuota().catch((error) => {
|
|
58
|
+
this.deps.logRunnerDiagnostic(`passive provider quota sample failed: ${errMessage(error)}`);
|
|
59
|
+
});
|
|
60
|
+
// #3608 — inbox recovery and fresh quota samples ride the single liveness exchange.
|
|
61
|
+
const report: HeartbeatReport = { status: agentStatus, ready: agentStatus !== "offline", unread: true, ...quota };
|
|
62
|
+
const result = await this.exchange(report);
|
|
63
|
+
this.deps.acceptQuota(report, result?.providerQuota);
|
|
64
|
+
for (const message of result?.messages ?? []) this.deps.enqueueMessage(message);
|
|
65
|
+
this.deps.triggerEffort();
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (!this.deps.isStopped()) this.handleHttpFailure(error);
|
|
68
|
+
} finally {
|
|
69
|
+
this.inFlight = false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private async exchange(report: HeartbeatReport): Promise<HeartbeatResult> {
|
|
74
|
+
const bus = this.deps.bus();
|
|
75
|
+
if (bus.livenessAvailable) {
|
|
76
|
+
try {
|
|
77
|
+
return await bus.reportLiveness(report, BUS_LIVENESS_TIMEOUT_MS);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
// The report is idempotent (status, readiness, unread by id), so a retry over HTTP is safe.
|
|
80
|
+
this.logFailure(`bus:${errMessage(error)}`, `[runner] bus liveness exchange failed, using HTTP: ${errMessage(error)}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return await this.deps.http.heartbeat(this.deps.agentId, this.deps.instanceId, report);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private handleHttpFailure(error: unknown): void {
|
|
87
|
+
const authFailed = isHttpAuthError(error);
|
|
88
|
+
const suffix = authFailed ? "auth failed; stopping HTTP liveness retries until restart" : String(error);
|
|
89
|
+
this.logFailure(httpErrorKey(error), `[runner] HTTP liveness update failed: ${suffix}`);
|
|
90
|
+
if (!authFailed) return;
|
|
91
|
+
this.authFailed = true;
|
|
92
|
+
this.stop();
|
|
93
|
+
// A 401/403 here is the only timely signal that the token died — stopping the
|
|
94
|
+
// liveness timer means there is no second chance, so recover from THIS failure.
|
|
95
|
+
this.deps.recoverRuntimeTokenAfterAuthFailure("http-liveness");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private logFailure(key: string, message: string): void {
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
if (this.lastLog?.key === key && now - this.lastLog.at < this.deps.logIntervalMs) return;
|
|
101
|
+
this.lastLog = { key, at: now };
|
|
102
|
+
this.deps.logRunnerDiagnostic(message);
|
|
103
|
+
}
|
|
104
|
+
}
|
package/src/bin/codex.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { dirname, join, resolve } from "node:path";
|
|
2
|
+
import { HARNESS_SUBAGENTS_DISABLED, harnessSubagentsEnabled } from "@humwick/sdk";
|
|
3
|
+
import type { RunnerSpawnConfig } from "./adapter";
|
|
4
|
+
import { bundledIntegrationHome } from "./provider-integration";
|
|
5
|
+
import { composeSystemPromptAppend } from "./project-instructions";
|
|
6
|
+
import { CLAUDE_NO_ATTRIBUTION_RULE } from "./claude-settings";
|
|
7
|
+
import { claudeWebToolsSteer } from "./web-tools";
|
|
8
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
|
|
10
|
+
const APPEND_FLAGS = ["--append-system-prompt", "--append-system-prompt-file"];
|
|
11
|
+
|
|
12
|
+
// #3830 — caller/default appends share the same file channel as Relay context.
|
|
13
|
+
export function claudeCallerAppends(args: string[]): { args: string[]; appends: Array<{ value: string; file: boolean }> } {
|
|
14
|
+
const remaining: string[] = [];
|
|
15
|
+
const appends: Array<{ value: string; file: boolean }> = [];
|
|
16
|
+
for (let i = 0; i < args.length; i++) {
|
|
17
|
+
const arg = args[i]!;
|
|
18
|
+
if (arg === "--") { remaining.push(...args.slice(i)); break; }
|
|
19
|
+
const flag = APPEND_FLAGS.find(flag => arg === flag || arg.startsWith(`${flag}=`));
|
|
20
|
+
if (!flag) { remaining.push(arg); continue; }
|
|
21
|
+
const value = arg === flag ? args[++i] : arg.slice(flag.length + 1);
|
|
22
|
+
if (value === undefined) throw new Error(`${flag} requires a value`);
|
|
23
|
+
appends.push({ value, file: flag.endsWith("-file") });
|
|
24
|
+
}
|
|
25
|
+
return { args: remaining, appends };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function resolveClaudeSystemPrompt(config: RunnerSpawnConfig, defaultArgs: string[], assetHome?: string) {
|
|
29
|
+
const composed = composeSystemPromptAppend(config);
|
|
30
|
+
const parts: Array<{ text: string } | { file: string }> = [{ text: composed.text }];
|
|
31
|
+
for (const append of claudeCallerAppends([...defaultArgs, ...config.providerArgs]).appends) {
|
|
32
|
+
parts.push(append.file ? { file: resolve(config.cwd, append.value) } : { text: append.value });
|
|
33
|
+
}
|
|
34
|
+
parts.push(...[...(harnessSubagentsEnabled(config) ? [] : [HARNESS_SUBAGENTS_DISABLED]), claudeWebToolsSteer(config), CLAUDE_NO_ATTRIBUTION_RULE].map(text => ({ text })));
|
|
35
|
+
return {
|
|
36
|
+
path: join(assetHome ?? bundledIntegrationHome(config), "system-prompt-append.md"),
|
|
37
|
+
reinjected: composed.reinjected,
|
|
38
|
+
parts,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function materializeClaudeSystemPrompt(file?: ReturnType<typeof resolveClaudeSystemPrompt>): void {
|
|
43
|
+
if (!file) return;
|
|
44
|
+
// Report-only assembly never opens caller files; launch materialization owns that failure.
|
|
45
|
+
const text = file.parts.map(part => "text" in part ? part.text : readFileSync(part.file, "utf8")).filter(Boolean).join("\n\n");
|
|
46
|
+
mkdirSync(dirname(file.path), { recursive: true, mode: 0o700 });
|
|
47
|
+
writeFileSync(file.path, text, { mode: 0o600 });
|
|
48
|
+
chmodSync(file.path, 0o600);
|
|
49
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -375,8 +375,9 @@ export function detectApprovalModeFromArgs(provider: string, defaultArgs: string
|
|
|
375
375
|
return detector ? detector([...defaultArgs, ...providerArgs]) : undefined;
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
-
|
|
379
|
-
|
|
378
|
+
export function runCli(provider?: string): void {
|
|
379
|
+
const argv = provider ? [...process.argv.slice(0, 2), provider, ...process.argv.slice(2)] : process.argv;
|
|
380
|
+
main(argv).catch((error) => {
|
|
380
381
|
// A fatal startup failure (e.g. the runner could not register with the relay within the
|
|
381
382
|
// deadline) must exit non-zero, not hang or swallow — the orchestrator's exit detection
|
|
382
383
|
// then reports it and the spawn-parent is notified instead of waiting on a silent zombie.
|
|
@@ -384,3 +385,5 @@ if (import.meta.main) {
|
|
|
384
385
|
exitRunner(1, `runner fatal: ${errMessage(error)}`);
|
|
385
386
|
});
|
|
386
387
|
}
|
|
388
|
+
|
|
389
|
+
if (import.meta.main) runCli();
|
package/src/launch-assembly.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { bundledIntegrationAssets, bundledIntegrationHome, suppressOwnedClaudePlugins, selectClaudePluginDirs } from "./provider-integration";
|
|
2
2
|
import { relayEnv, writeChildEnv } from "@humwick/sdk/env";
|
|
3
3
|
import { codexRetiredMcpConfigArgs } from "./adapters/codex-effective-config";
|
|
4
|
-
import { activeSharedMcpServers
|
|
4
|
+
import { activeSharedMcpServers } from "@humwick/sdk";
|
|
5
5
|
import { claudeSubagentHooks, claudeSubagentSettings } from "./adapters/claude-subagents";
|
|
6
6
|
import { serviceAccessLaunchEnv, serviceAccessMcpServers } from "./adapters/service-access";
|
|
7
|
-
import { claudeSettingsArgs,
|
|
7
|
+
import { claudeSettingsArgs, extractCallerSettings, stripSettingsArgs, mergeHookSets, hasSettingsArg, sessionStatusLineSettingsArgs } from "./claude-settings";
|
|
8
8
|
export { stripSettingsArgs, sessionStatusLineSettingsArgs } from "./claude-settings";
|
|
9
9
|
import { rmSync, writeFileSync } from "node:fs";
|
|
10
10
|
import { basename, relative, resolve, join } from "node:path";
|
|
@@ -45,8 +45,8 @@ import type { ResolvedProjectClaudeSettings } from "./provisioning";
|
|
|
45
45
|
import { resolveMcpSources, type McpSourceSelection } from "./mcp-source-selection";
|
|
46
46
|
import { resolveProjectHooks } from "./project-hooks";
|
|
47
47
|
import { agentRedactEnv, agentRedactOn, isAgentRedactInstall, resolveAgentRedact, type AgentRedactState } from "./agent-redact";
|
|
48
|
-
import {
|
|
49
|
-
import {
|
|
48
|
+
import { resolveClaudeSystemPrompt, materializeClaudeSystemPrompt } from "./claude-system-prompt";
|
|
49
|
+
import { disabledSharedWebTools, webToolsEnv, withDisabledWebTools } from "./web-tools";
|
|
50
50
|
import {
|
|
51
51
|
claudeMcpConfigPayload,
|
|
52
52
|
claudeMcpLaunchArgs,
|
|
@@ -160,6 +160,7 @@ export interface AssembledMcp {
|
|
|
160
160
|
|
|
161
161
|
export interface AssembledLaunch {
|
|
162
162
|
provider: SpawnProvider;
|
|
163
|
+
systemPromptFile?: ReturnType<typeof resolveClaudeSystemPrompt>;
|
|
163
164
|
/** The resolved profile this launch was assembled from; undefined for a profile-less
|
|
164
165
|
* (legacy default) spawn, which behaves as host base with relay features default-on. */
|
|
165
166
|
profile?: AgentProfile;
|
|
@@ -546,8 +547,8 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
|
|
|
546
547
|
// #2086 — when P.hooks forces the project scope off but P.instructions is on, re-inject the repo
|
|
547
548
|
// CLAUDE.md/AGENTS.md through the controlled append-system-prompt channel so the docs are honored
|
|
548
549
|
// rather than silently dropped (see composeSystemPromptAppend / resolveProjectInstructions).
|
|
549
|
-
const
|
|
550
|
-
const
|
|
550
|
+
const promptFile = resolveClaudeSystemPrompt(config, hostDefaultArgs, assetHome);
|
|
551
|
+
const repoInstructionsReinjected = promptFile.reinjected;
|
|
551
552
|
const instructions = assembleInstructions(config, {
|
|
552
553
|
projectScopeDropped,
|
|
553
554
|
isolatedHome: Boolean(configHome),
|
|
@@ -570,11 +571,12 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
|
|
|
570
571
|
...(mcpConfigFilePath ? { configFilePath: mcpConfigFilePath } : {}),
|
|
571
572
|
}),
|
|
572
573
|
...settings.args,
|
|
573
|
-
|
|
574
|
+
"--append-system-prompt-file", promptFile.path,
|
|
574
575
|
];
|
|
575
576
|
|
|
576
577
|
return {
|
|
577
578
|
provider,
|
|
579
|
+
systemPromptFile: promptFile,
|
|
578
580
|
...(config.agentProfile ? { profile: config.agentProfile } : {}),
|
|
579
581
|
base: config.agentProfile?.base ?? "host",
|
|
580
582
|
vanilla,
|
|
@@ -938,10 +940,12 @@ export function assembleLaunch(provider: SpawnProvider, config: RunnerSpawnConfi
|
|
|
938
940
|
/**
|
|
939
941
|
* Write the provisioned assets the assembly describes (skills/plugins/hooks and Codex
|
|
940
942
|
* hooks.json). Driven by the SAME resolvers `assembleLaunch` uses, so disk == launch ==
|
|
941
|
-
* report. Idempotent; the isolated home must already exist (prepare*ProfileHome).
|
|
942
|
-
*
|
|
943
|
+
* report. Idempotent; the isolated home must already exist (prepare*ProfileHome). Claude
|
|
944
|
+
* system context also uses a private file for host-base and profile-less launches.
|
|
943
945
|
*/
|
|
944
|
-
export function materializeLaunchAssembly(provider: SpawnProvider, config: RunnerSpawnConfig): void {
|
|
946
|
+
export function materializeLaunchAssembly(provider: SpawnProvider, config: RunnerSpawnConfig, assembled?: AssembledLaunch): void {
|
|
947
|
+
// #3830 — every Claude path, including host-base/profile-less, uses a private file.
|
|
948
|
+
if (getManifest(provider)?.shim?.kind === "claude-relay") materializeClaudeSystemPrompt((assembled ?? assembleLaunch(provider, config, config.providerConfig)).systemPromptFile);
|
|
945
949
|
if (!config.agentProfile && runnerMcpGeneration(config.env) === "humwick") {
|
|
946
950
|
const feature = getManifest(provider)?.provisioning?.bundledSkills ? "skills" : "plugins";
|
|
947
951
|
if (profileAllowsRelayFeature(config, feature)) {
|
package/src/profile-home.ts
CHANGED
|
@@ -78,7 +78,7 @@ export function providerHomePathFor(provider: SpawnProvider, config: RunnerSpawn
|
|
|
78
78
|
// the repo's CLAUDE.md / AGENTS.md as behavioral instructions, NOT "don't
|
|
79
79
|
// operate here" — an isolated agent must still read/edit/execute in its own cwd.
|
|
80
80
|
//
|
|
81
|
-
// #2254 —
|
|
81
|
+
// #2254 — host-base launches skip the onboarding/theme bootstrap (an
|
|
82
82
|
// onboarded host config already answers those), but they do NOT dodge folder trust:
|
|
83
83
|
// --dangerously-skip-permissions does not bypass the trust gate, and a cwd the host
|
|
84
84
|
// config has never seen still renders the dialog and wedges the spawn. Trust is
|
|
@@ -11,8 +11,8 @@ import { projectSettingSourcesOn } from "./profile-home";
|
|
|
11
11
|
// the Claude re-injection channel and lists both names the cwd may carry.
|
|
12
12
|
const REPO_INSTRUCTION_FILES = ["CLAUDE.md", "AGENTS.md"] as const;
|
|
13
13
|
|
|
14
|
-
// Bound each re-injected file so a pathological repo doc
|
|
15
|
-
//
|
|
14
|
+
// Bound each re-injected file so a pathological repo doc cannot balloon the context.
|
|
15
|
+
// The composed append travels by private file (#3830). Mirrors Codex's
|
|
16
16
|
// project_doc_max_bytes default; oversized content is truncated with a loud marker
|
|
17
17
|
// rather than silently dropped.
|
|
18
18
|
const REPO_INSTRUCTIONS_MAX_BYTES = 64 * 1024;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { logger } from "./logger";
|
|
3
|
+
|
|
4
|
+
const warned = new Set<string>();
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The ONE place a provider's launch command is resolved. The launch and the pre-spawn login check
|
|
8
|
+
* both call it, so the check can never probe a different binary than the launch runs.
|
|
9
|
+
* A stored `claude-rig` command (the retired wrapper) is treated as plain `claude`, with one warning.
|
|
10
|
+
*/
|
|
11
|
+
export function resolveLaunchCommand(command: string): string {
|
|
12
|
+
if (basename(command) !== "claude-rig") return command;
|
|
13
|
+
if (!warned.has(command)) {
|
|
14
|
+
warned.add(command);
|
|
15
|
+
logger.warn("provider-command", `stored provider command "${command}" is the retired claude-rig wrapper; launching plain "claude" instead. Set the provider command to "claude" in the dashboard.`);
|
|
16
|
+
}
|
|
17
|
+
return "claude";
|
|
18
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { errMessage } from "@humwick/sdk";
|
|
2
|
+
import { checkProviderAuth } from "@humwick/sdk/provider-probe";
|
|
3
|
+
import { selectProviderConfigEnv } from "@humwick/sdk/child-env";
|
|
4
|
+
import type { ProviderAdapter, ProviderStatusEvent } from "./adapter";
|
|
5
|
+
import { logger } from "./logger";
|
|
6
|
+
import { hostSourcesFor } from "./host-asset-plan";
|
|
7
|
+
import { resolveLaunchCommand } from "./provider-command";
|
|
8
|
+
|
|
9
|
+
/** All real adapters share the managed-spawn login check, including replacements (#3751). */
|
|
10
|
+
export function withProviderSetup(adapter: ProviderAdapter): ProviderAdapter {
|
|
11
|
+
const spawn = adapter.spawn.bind(adapter);
|
|
12
|
+
adapter.spawn = async (config) => {
|
|
13
|
+
const env = selectProviderConfigEnv(config.providerConfig.env, hostSourcesFor(config).settings);
|
|
14
|
+
const auth = await checkProviderAuth(config.provider, undefined, env, resolveLaunchCommand(config.providerConfig.command));
|
|
15
|
+
if (!auth.ok) throw new Error(auth.reason);
|
|
16
|
+
return spawn(config);
|
|
17
|
+
};
|
|
18
|
+
return adapter;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function providerStartupFailure(error: unknown): ProviderStatusEvent {
|
|
22
|
+
const message = `Provider startup failed: ${errMessage(error)}`;
|
|
23
|
+
return { status: "error", reason: "provider-turn", providerState: {
|
|
24
|
+
state: "blocked", terminal: true, reason: "providerSetupRequired", label: "Provider setup required", message,
|
|
25
|
+
recommendedAction: "Complete the provider login and first-run setup on this host, then restart the runner.",
|
|
26
|
+
} };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function attemptProviderStartup(start: () => Promise<void>, failed: (status: ProviderStatusEvent) => void): Promise<void> {
|
|
30
|
+
try { await start(); }
|
|
31
|
+
catch (error) {
|
|
32
|
+
logger.error("startup", errMessage(error));
|
|
33
|
+
failed(providerStartupFailure(error));
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/providers.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withProviderSetup } from "./provider-setup";
|
|
1
2
|
import { ClaudeApiAdapter } from "./adapters/claude-api";
|
|
2
3
|
import type { LaunchMode, SpawnProvider } from "@humwick/sdk";
|
|
3
4
|
import { getAllManifests, supportedLaunchModesForProvider } from "@humwick/providers";
|
|
@@ -31,5 +32,5 @@ export function createProviderAdapter(provider: SpawnProvider, providerArgs: str
|
|
|
31
32
|
if (launchMode && !supportedLaunchModesForProvider(provider).includes(launchMode)) {
|
|
32
33
|
throw new Error(`${provider} does not support launchMode=${launchMode}`);
|
|
33
34
|
}
|
|
34
|
-
return registration.createAdapter(providerArgs, launchMode);
|
|
35
|
+
return withProviderSetup(registration.createAdapter(providerArgs, launchMode));
|
|
35
36
|
}
|