@humwick/runner 0.0.0-stage → 0.130.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/LICENSE +167 -0
- package/agent-bin/agent-relay-browser +10 -0
- package/agent-bin/humwick +10 -0
- package/hooks/fff-search-redirect/fff-search-redirect.cjs +161 -0
- package/hooks/fff-search-redirect/hooks.json +13 -0
- package/hooks/rg-r-guard/hooks.json +18 -0
- package/hooks/rg-r-guard/rg-r-guard.cjs +144 -0
- package/hooks/web-tools-redirect/hooks.json +13 -0
- package/hooks/web-tools-redirect/web-tools-redirect.cjs +84 -0
- package/node_modules/@humwick/offload/LICENSE +167 -0
- package/node_modules/@humwick/offload/README.md +57 -0
- package/node_modules/@humwick/offload/dist/async-runner.d.ts +18 -0
- package/node_modules/@humwick/offload/dist/async-runner.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/async-runner.js +158 -0
- package/node_modules/@humwick/offload/dist/async-runner.js.map +1 -0
- package/node_modules/@humwick/offload/dist/envelope.d.ts +25 -0
- package/node_modules/@humwick/offload/dist/envelope.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/envelope.js +80 -0
- package/node_modules/@humwick/offload/dist/envelope.js.map +1 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts +10 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js +59 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js.map +1 -0
- package/node_modules/@humwick/offload/dist/index.d.ts +5 -0
- package/node_modules/@humwick/offload/dist/index.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/index.js +5 -0
- package/node_modules/@humwick/offload/dist/index.js.map +1 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts +62 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.js +295 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.js.map +1 -0
- package/node_modules/@humwick/offload/dist/types.d.ts +111 -0
- package/node_modules/@humwick/offload/dist/types.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/types.js +176 -0
- package/node_modules/@humwick/offload/dist/types.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts +23 -0
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-pool.js +435 -0
- package/node_modules/@humwick/offload/dist/worker-pool.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts +2 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.js +27 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker.d.ts +5 -0
- package/node_modules/@humwick/offload/dist/worker.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker.js +1 -0
- package/node_modules/@humwick/offload/dist/worker.js.map +1 -0
- package/node_modules/@humwick/offload/package.json +46 -0
- package/node_modules/@humwick/offload/src/async-runner.ts +170 -0
- package/node_modules/@humwick/offload/src/envelope.ts +92 -0
- package/node_modules/@humwick/offload/src/fixtures/worker-task.ts +49 -0
- package/node_modules/@humwick/offload/src/index.ts +4 -0
- package/node_modules/@humwick/offload/src/subprocess-runner.ts +334 -0
- package/node_modules/@humwick/offload/src/types.ts +235 -0
- package/node_modules/@humwick/offload/src/worker-pool.ts +463 -0
- package/node_modules/@humwick/offload/src/worker-runtime.ts +32 -0
- package/node_modules/@humwick/offload/src/worker.ts +5 -0
- package/package.json +47 -4
- package/plugins/browser/skills/playwright-cli/SKILL.md +53 -0
- package/plugins/claude/.claude-plugin/plugin.json +8 -0
- package/plugins/claude/claude-statusline-probe.cjs +144 -0
- package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +914 -0
- package/plugins/claude/hooks/gate-run-guard.sh +9 -0
- package/plugins/claude/hooks/gate-run-guard.ts +237 -0
- package/plugins/claude/hooks/hooks.json +158 -0
- package/plugins/claude/hooks/kill-guard.provisioned.mjs +7027 -0
- package/plugins/claude/hooks/kill-guard.sh +341 -0
- package/plugins/claude/hooks/kill-guard.ts +337 -0
- package/plugins/claude/hooks/permission-request.sh +22 -0
- package/plugins/claude/hooks/post-compact.sh +10 -0
- package/plugins/claude/hooks/post-tool-use.sh +8 -0
- package/plugins/claude/hooks/pre-compact.sh +12 -0
- package/plugins/claude/hooks/read-only-bash-guard.sh +587 -0
- package/plugins/claude/hooks/relay-status.sh +373 -0
- package/plugins/claude/hooks/session-end.sh +26 -0
- package/plugins/claude/hooks/session-start.sh +31 -0
- package/plugins/claude/hooks/shell-trust.ts +28 -0
- package/plugins/claude/hooks/stop-failure.sh +47 -0
- package/plugins/claude/hooks/stop.sh +48 -0
- package/plugins/claude/hooks/subagent-guard.cjs +26 -0
- package/plugins/claude/hooks/subagent-skill-policy.cjs +160 -0
- package/plugins/claude/hooks/subagent-start.sh +13 -0
- package/plugins/claude/hooks/subagent-stop.sh +19 -0
- package/plugins/claude/hooks/user-prompt-submit.sh +18 -0
- package/plugins/claude/hooks/wait-loop-guard.ts +120 -0
- package/plugins/claude/monitors/monitors.json +7 -0
- package/plugins/claude/monitors/relay-monitor.provisioned.mjs +786 -0
- package/plugins/claude/monitors/relay-monitor.ts +58 -0
- package/plugins/claude/skills/disconnect/SKILL.md +16 -0
- package/plugins/claude/skills/label/SKILL.md +23 -0
- package/plugins/claude/skills/message/SKILL.md +40 -0
- package/plugins/claude/skills/pair/SKILL.md +30 -0
- package/plugins/claude/skills/react/SKILL.md +18 -0
- package/plugins/claude/skills/read-message/SKILL.md +24 -0
- package/plugins/claude/skills/reply/SKILL.md +44 -0
- package/plugins/claude/skills/send-claimable/SKILL.md +39 -0
- package/plugins/claude/skills/slice-first/SKILL.md +32 -0
- package/plugins/claude/skills/slice-first/references/plan-recipe.md +46 -0
- package/plugins/claude/skills/status/SKILL.md +16 -0
- package/plugins/claude/skills/tags/SKILL.md +25 -0
- package/plugins/claude/skills/task/SKILL.md +28 -0
- package/plugins/claude-output-style/.claude-plugin/plugin.json +5 -0
- package/plugins/claude-output-style/output-styles/agent-relay.md +5 -0
- package/plugins/codex/skills/disconnect/SKILL.md +15 -0
- package/plugins/codex/skills/guide/SKILL.md +15 -0
- package/plugins/codex/skills/label/SKILL.md +22 -0
- package/plugins/codex/skills/message/SKILL.md +40 -0
- package/plugins/codex/skills/pair/SKILL.md +29 -0
- package/plugins/codex/skills/react/SKILL.md +17 -0
- package/plugins/codex/skills/read-message/SKILL.md +23 -0
- package/plugins/codex/skills/reply/SKILL.md +44 -0
- package/plugins/codex/skills/send-claimable/SKILL.md +39 -0
- package/plugins/codex/skills/slice-first/SKILL.md +32 -0
- package/plugins/codex/skills/slice-first/references/plan-recipe.md +46 -0
- package/plugins/codex/skills/status/SKILL.md +15 -0
- package/plugins/codex/skills/tags/SKILL.md +24 -0
- package/plugins/codex/skills/task/SKILL.md +28 -0
- package/src/adapter.ts +1061 -0
- package/src/adapters/__fixtures__/codex-app-server.ts +47 -0
- package/src/adapters/claude-api.ts +243 -0
- package/src/adapters/claude-context-command.ts +16 -0
- package/src/adapters/claude-credentials.ts +22 -0
- package/src/adapters/claude-delivery-lanes.ts +28 -0
- package/src/adapters/claude-delivery.ts +166 -0
- package/src/adapters/claude-effort.ts +94 -0
- package/src/adapters/claude-exit.ts +290 -0
- package/src/adapters/claude-headless-mirror.ts +88 -0
- package/src/adapters/claude-headless-refusal.ts +51 -0
- package/src/adapters/claude-headless.ts +617 -0
- package/src/adapters/claude-monitor-delivery-plan.ts +317 -0
- package/src/adapters/claude-permission.ts +243 -0
- package/src/adapters/claude-prompt-gates.ts +269 -0
- package/src/adapters/claude-prompt-hook.ts +145 -0
- package/src/adapters/claude-session-capture.ts +293 -0
- package/src/adapters/claude-session-probe.ts +414 -0
- package/src/adapters/claude-shutdown.ts +190 -0
- package/src/adapters/claude-status-detectors.ts +426 -0
- package/src/adapters/claude-subagents.ts +93 -0
- package/src/adapters/claude-submit-confirmation.ts +287 -0
- package/src/adapters/claude-tmux.ts +548 -0
- package/src/adapters/claude-token-usage-capture.ts +253 -0
- package/src/adapters/claude-tool-policy.ts +165 -0
- package/src/adapters/claude-transcript-tail.ts +193 -0
- package/src/adapters/claude-transcript.ts +812 -0
- package/src/adapters/claude.ts +1097 -0
- package/src/adapters/codex-agent-message-capture.ts +189 -0
- package/src/adapters/codex-app-server.ts +70 -0
- package/src/adapters/codex-client.ts +362 -0
- package/src/adapters/codex-effective-config.ts +87 -0
- package/src/adapters/codex-launch-config.ts +153 -0
- package/src/adapters/codex-rollout-context.ts +272 -0
- package/src/adapters/codex-skill-discovery.ts +18 -0
- package/src/adapters/codex-token-usage-capture.ts +163 -0
- package/src/adapters/codex-tool-output.ts +53 -0
- package/src/adapters/codex-turn-outcome.ts +27 -0
- package/src/adapters/codex-version.ts +64 -0
- package/src/adapters/codex.ts +1442 -0
- package/src/adapters/incremental-file.ts +110 -0
- package/src/adapters/opencode-api.ts +71 -0
- package/src/adapters/opencode-context.ts +277 -0
- package/src/adapters/opencode-model-registry.ts +126 -0
- package/src/adapters/opencode-provider.ts +169 -0
- package/src/adapters/opencode-server-retry.ts +50 -0
- package/src/adapters/opencode-soft-steer.ts +302 -0
- package/src/adapters/opencode-spawn.ts +145 -0
- package/src/adapters/opencode-subscribe.ts +62 -0
- package/src/adapters/opencode-token-usage-capture.ts +187 -0
- package/src/adapters/opencode-tool-events.ts +302 -0
- package/src/adapters/opencode-tools-config.ts +17 -0
- package/src/adapters/opencode.ts +2264 -0
- package/src/adapters/project-shell.ts +80 -0
- package/src/adapters/provider-launch-config.ts +21 -0
- package/src/adapters/provider-quota-harvest.ts +127 -0
- package/src/adapters/service-access.ts +153 -0
- package/src/adapters/tmux-offload.ts +207 -0
- package/src/adapters/tool-summary.ts +214 -0
- package/src/agent-redact.ts +75 -0
- package/src/agent-runner/commands/dispatch.ts +185 -0
- package/src/agent-runner/commands/lifecycle.ts +84 -0
- package/src/agent-runner/commands/prompt.ts +284 -0
- package/src/agent-runner/credentials.ts +208 -0
- package/src/agent-runner/http-liveness.ts +85 -0
- package/src/agent-runner/lifecycle-milestones.ts +207 -0
- package/src/agent-runner/lifecycle-phase.ts +80 -0
- package/src/agent-runner/registration.ts +163 -0
- package/src/agent-runner/runner-context.ts +158 -0
- package/src/agent-runner/session-mirror.ts +278 -0
- package/src/agent-runner/task-claims.ts +116 -0
- package/src/agent-runner/token-usage-coverage.ts +54 -0
- package/src/agent-runner/turn-report.ts +269 -0
- package/src/api-budget-accounting.ts +91 -0
- package/src/area-memory.ts +255 -0
- package/src/artifact-path-materialize.ts +52 -0
- package/src/async-guard.ts +35 -0
- package/src/attachment-cache-sweep.ts +31 -0
- package/src/attachment-cache.ts +195 -0
- package/src/background-script-reconcile.ts +106 -0
- package/src/bounded-log.ts +59 -0
- package/src/bounded-spawn-sync.ts +18 -0
- package/src/busy-reconciler.ts +282 -0
- package/src/claim-tracker.ts +140 -0
- package/src/claude-settings.ts +167 -0
- package/src/claude-workspace-trust.ts +289 -0
- package/src/config.ts +547 -0
- package/src/context-boundary.ts +101 -0
- package/src/context-command.ts +151 -0
- package/src/context-probe-state.ts +6 -0
- package/src/continuation-archive.ts +179 -0
- package/src/control-server.ts +730 -0
- package/src/delivery-policy.ts +9 -0
- package/src/delivery-result.ts +24 -0
- package/src/effort-command.ts +125 -0
- package/src/empty-turn.ts +816 -0
- package/src/free-port.ts +25 -0
- package/src/fs-offload.ts +26 -0
- package/src/gate-run-config.ts +35 -0
- package/src/gate-run-lease-probe.ts +38 -0
- package/src/host-asset-plan.ts +450 -0
- package/src/host-claude-config-worker.ts +34 -0
- package/src/host-sources.ts +40 -0
- package/src/inbox-carrier.ts +7 -0
- package/src/index.ts +386 -0
- package/src/launch-assembly.ts +955 -0
- package/src/liveness.ts +105 -0
- package/src/logger.ts +108 -0
- package/src/mcp-outbox.ts +204 -0
- package/src/mcp-preflight.ts +86 -0
- package/src/mcp-source-selection.ts +54 -0
- package/src/mcp-tool-deferral.ts +35 -0
- package/src/mcp-vault-env.ts +25 -0
- package/src/message-body-config/config.ts +16 -0
- package/src/message-body-config.ts +1 -0
- package/src/mid-turn-delivery.ts +43 -0
- package/src/native-memory-lever.ts +329 -0
- package/src/native-self-resume.ts +244 -0
- package/src/outbox.ts +453 -0
- package/src/park-post.ts +17 -0
- package/src/pending-permission-store.ts +148 -0
- package/src/process-meta.ts +9 -0
- package/src/profile-home.ts +419 -0
- package/src/profile-projection.ts +411 -0
- package/src/project-hooks.ts +65 -0
- package/src/project-instructions.ts +119 -0
- package/src/provider-double.ts +59 -0
- package/src/provider-hold.ts +228 -0
- package/src/provider-home-marker.ts +3 -0
- package/src/provider-home-tombstone.ts +4 -0
- package/src/provider-input-queue.ts +134 -0
- package/src/provider-integration.ts +93 -0
- package/src/provider-processes.ts +17 -0
- package/src/provider-submit-failure.ts +15 -0
- package/src/providers.ts +35 -0
- package/src/provisioning.ts +703 -0
- package/src/question-decision.ts +18 -0
- package/src/quota.ts +22 -0
- package/src/rate-limit.ts +351 -0
- package/src/refused-prompt-store.ts +36 -0
- package/src/relay-injection-events.ts +161 -0
- package/src/relay-instructions.ts +107 -0
- package/src/relay-mcp-proxy.long-turn-e2e.ts +183 -0
- package/src/relay-mcp-proxy.ts +704 -0
- package/src/relay-mcp.ts +583 -0
- package/src/reply-obligation-cache.ts +211 -0
- package/src/response-capture-report.ts +111 -0
- package/src/response-routing.ts +6 -0
- package/src/runner-core.ts +2955 -0
- package/src/runner-exit.ts +12 -0
- package/src/runner-helpers.ts +582 -0
- package/src/runner-insights.ts +105 -0
- package/src/runner-mcp-proxy.ts +46 -0
- package/src/runner-spawn-config.ts +68 -0
- package/src/runner-stale-scratch-sweep.ts +21 -0
- package/src/runner.ts +9 -0
- package/src/runtime-token-file.ts +45 -0
- package/src/runtime-token-remint.ts +56 -0
- package/src/service-access.ts +88 -0
- package/src/session-destroy.ts +27 -0
- package/src/session-insights.ts +118 -0
- package/src/session-scratch.ts +296 -0
- package/src/spawn-input.ts +147 -0
- package/src/startup-config-fetch.ts +48 -0
- package/src/subreaper.ts +113 -0
- package/src/template-resolver.ts +29 -0
- package/src/test-run-dedup.ts +435 -0
- package/src/transcript-context.ts +99 -0
- package/src/transport-errors.ts +48 -0
- package/src/turn-opener.ts +55 -0
- package/src/turn-reconcile-event.ts +88 -0
- package/src/vault-proxy-hop.ts +99 -0
- package/src/version.ts +44 -0
- package/src/web-tool-call-proxy.ts +128 -0
- package/src/web-tools.ts +59 -0
- package/README.md +0 -3
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { formatRunnerExitLine } from "@humwick/sdk";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* #2619 — the ONLY way the runner process exits. Every exit writes the structured exit record
|
|
5
|
+
* (code + cause) to stderr first: the orchestrator parses it back from the log tail, which
|
|
6
|
+
* outlives a garbage-collected systemd unit. A guard test forbids a direct process.exit anywhere
|
|
7
|
+
* else in the runner.
|
|
8
|
+
*/
|
|
9
|
+
export function exitRunner(code: number, reason?: string): never {
|
|
10
|
+
try { console.error(formatRunnerExitLine(code, reason)); } catch { /* best-effort: the exit must still happen */ }
|
|
11
|
+
process.exit(code);
|
|
12
|
+
}
|
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import { walkErrorChain } from "./transport-errors";
|
|
2
|
+
import { closeSync, openSync, readSync, statSync } from "node:fs";
|
|
3
|
+
import type { AgentProfile, ContextState, LaunchMode, Message, ProviderCapabilities, ProviderState, QuotaState } from "@humwick/sdk";
|
|
4
|
+
import { errMessage, isRelayInjectionCategory, isSpawnProvider } from "@humwick/sdk";
|
|
5
|
+
import { getManifest } from "@humwick/providers";
|
|
6
|
+
import type { BooleanCapabilityDefault, ProviderCapabilityCondition } from "@humwick/providers";
|
|
7
|
+
import { targetMatchesAgentIdentity } from "@humwick/sdk/agent-target";
|
|
8
|
+
import type { ProviderAdapter, ProviderConfig, RunnerSpawnConfig, SemanticStatus } from "./adapter";
|
|
9
|
+
import type { RunnerRelayInjectionEvent } from "./relay-injection-events";
|
|
10
|
+
import { agentProfileProjectionReport } from "./profile-projection";
|
|
11
|
+
import { assembleLaunch } from "./launch-assembly";
|
|
12
|
+
import { supportsActiveTurnSteer } from "./delivery-policy";
|
|
13
|
+
|
|
14
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
15
|
+
const LOG_TAIL_BYTES = 128 * 1024;
|
|
16
|
+
const RUNNER_CLAIMED_GUIDANCE_RE = /\s*Claim this task before working it, then update task status when finished\.\s*$/;
|
|
17
|
+
const PROVIDER_DISPLAY_NAMES: Record<string, string> = { claude: "Claude", codex: "Codex" };
|
|
18
|
+
|
|
19
|
+
interface RuntimeProviderOptions {
|
|
20
|
+
provider: string;
|
|
21
|
+
model?: string;
|
|
22
|
+
effort?: string;
|
|
23
|
+
approvalMode: string;
|
|
24
|
+
headless: boolean;
|
|
25
|
+
/** #2773 — a headless-pipe launch has no terminal to read, write, or attach. */
|
|
26
|
+
launchMode?: LaunchMode;
|
|
27
|
+
startedAt: number;
|
|
28
|
+
adapter: ProviderAdapter;
|
|
29
|
+
providerConfig?: ProviderConfig;
|
|
30
|
+
/** #2950 — the running process accepts an in-place effort change (see runnerEffortSwitch). */
|
|
31
|
+
effortSwitch?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ProbeModelInfo {
|
|
35
|
+
model?: string;
|
|
36
|
+
effort?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface RestartProviderOptions {
|
|
40
|
+
resumeId?: string;
|
|
41
|
+
suppressPrompt?: boolean;
|
|
42
|
+
systemPromptAppend?: string;
|
|
43
|
+
relayInjectionEvents?: RunnerRelayInjectionEvent[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function runnerMessageMatches(message: Pick<Message, "to" | "resolvedToAgent">, target: {
|
|
47
|
+
agentId: string;
|
|
48
|
+
label?: string;
|
|
49
|
+
provider: string;
|
|
50
|
+
tags: string[];
|
|
51
|
+
capabilities: string[];
|
|
52
|
+
defaultTags: string[];
|
|
53
|
+
defaultCapabilities: string[];
|
|
54
|
+
allowBroadTargets?: boolean;
|
|
55
|
+
}): boolean {
|
|
56
|
+
if (message.resolvedToAgent === target.agentId) return true;
|
|
57
|
+
if (message.to === target.agentId) return true;
|
|
58
|
+
if (target.label && message.to === `label:${target.label}`) return true;
|
|
59
|
+
if (target.allowBroadTargets === false) return false;
|
|
60
|
+
return targetMatchesAgentIdentity(message.to, {
|
|
61
|
+
id: target.agentId,
|
|
62
|
+
label: target.label,
|
|
63
|
+
tags: [...target.tags, ...target.defaultTags, target.provider],
|
|
64
|
+
capabilities: [...target.capabilities, ...target.defaultCapabilities],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function runnerMessageShouldDirtyObligations(message: Pick<Message, "from">, agentId: string): boolean {
|
|
69
|
+
return message.from !== agentId;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function taskIdFromMessage(message: Pick<Message, "payload">): number | undefined {
|
|
73
|
+
const taskId = message.payload?.taskId;
|
|
74
|
+
return Number.isSafeInteger(taskId) ? taskId as number : undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function stripRunnerClaimedGuidance(body: string): string {
|
|
78
|
+
return body.replace(RUNNER_CLAIMED_GUIDANCE_RE, "");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function relayInjectionEventsFromParam(value: unknown): RunnerRelayInjectionEvent[] | undefined {
|
|
82
|
+
if (!Array.isArray(value)) return undefined;
|
|
83
|
+
const events = value.flatMap((item): RunnerRelayInjectionEvent[] => {
|
|
84
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return [];
|
|
85
|
+
const record = item as Record<string, unknown>;
|
|
86
|
+
const detail = record.detail;
|
|
87
|
+
if (!isRelayInjectionCategory(record.category)) return [];
|
|
88
|
+
if (typeof record.summary !== "string" || !record.summary.trim()) return [];
|
|
89
|
+
if (!detail || typeof detail !== "object" || Array.isArray(detail)) return [];
|
|
90
|
+
return [{ category: record.category, summary: record.summary, detail: detail as Record<string, unknown> }];
|
|
91
|
+
});
|
|
92
|
+
return events.length ? events : undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function restartOptionsFromCommand(params: Record<string, unknown>): RestartProviderOptions {
|
|
96
|
+
return {
|
|
97
|
+
systemPromptAppend: typeof params.systemPromptAppend === "string" ? params.systemPromptAppend : undefined,
|
|
98
|
+
relayInjectionEvents: relayInjectionEventsFromParam(params.relayInjectionEvents),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Bound a runner's INITIAL relay registration. `connect` resolves on the first `registered`
|
|
103
|
+
// frame and otherwise loops forever, so a token the relay rejects (401 at the WS upgrade)
|
|
104
|
+
// hangs here silently. Race it against `timeoutMs`; on timeout, surface a fatal reason, tear
|
|
105
|
+
// down the bus (stop the retry loop) and throw so the process exits and the orchestrator
|
|
106
|
+
// observes it instead of a silent zombie. Reconnects after first registration stay infinite.
|
|
107
|
+
export async function registerWithinDeadline(
|
|
108
|
+
bus: { connect: () => Promise<void>; close: () => Promise<void> },
|
|
109
|
+
timeoutMs: number,
|
|
110
|
+
relayUrl: string,
|
|
111
|
+
onFatal: (reason: string) => void,
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
114
|
+
const deadline = new Promise<never>((_, reject) => {
|
|
115
|
+
timer = setTimeout(() => reject(new Error(
|
|
116
|
+
`runner failed to register with the relay within ${timeoutMs}ms — aborting so the orchestrator `
|
|
117
|
+
+ `observes the exit instead of a silent hang (relay ${relayUrl}; check the relay is reachable `
|
|
118
|
+
+ `and the runner token is valid).`,
|
|
119
|
+
)), timeoutMs);
|
|
120
|
+
});
|
|
121
|
+
try {
|
|
122
|
+
await Promise.race([bus.connect(), deadline]);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
onFatal(errMessage(error));
|
|
125
|
+
await bus.close().catch(() => {});
|
|
126
|
+
throw error;
|
|
127
|
+
} finally {
|
|
128
|
+
if (timer) clearTimeout(timer);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function csvTags(raw: string | undefined): string[] {
|
|
133
|
+
return (raw || "").split(",").map((tag) => tag.trim()).filter(Boolean);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function relayBusUrl(relayUrl: string): string {
|
|
137
|
+
const url = new URL(relayUrl);
|
|
138
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
139
|
+
url.pathname = `${url.pathname.replace(/\/+$/, "")}/bus`;
|
|
140
|
+
url.search = "";
|
|
141
|
+
return url.toString();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function runnerAgentStatus(status: "idle" | "busy" | "offline" | "error"): "idle" | "busy" | "offline" {
|
|
145
|
+
return status === "error" ? "offline" : status;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function runnerBusErrorAction(code: string, stopped: boolean): "ignore" | "log" | "stop" {
|
|
149
|
+
if (code === "STALE_SESSION") return stopped ? "ignore" : "stop";
|
|
150
|
+
return "log";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function runnerShouldResolveProviderExit(status: SemanticStatus, exitCommandInProgress: boolean): boolean {
|
|
154
|
+
return !exitCommandInProgress && (status === "offline" || status === "error");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function runnerShouldRestartUnexpectedProviderExit(
|
|
158
|
+
status: SemanticStatus,
|
|
159
|
+
input: {
|
|
160
|
+
exitCommandInProgress: boolean;
|
|
161
|
+
stopped: boolean;
|
|
162
|
+
restartInProgress: boolean;
|
|
163
|
+
provider?: string;
|
|
164
|
+
headless: boolean;
|
|
165
|
+
lifecycle?: "persistent" | "transient";
|
|
166
|
+
hasTerminalSession: boolean;
|
|
167
|
+
},
|
|
168
|
+
): boolean {
|
|
169
|
+
return status === "offline"
|
|
170
|
+
&& !input.exitCommandInProgress
|
|
171
|
+
&& !input.stopped
|
|
172
|
+
&& !input.restartInProgress
|
|
173
|
+
&& input.headless
|
|
174
|
+
&& input.lifecycle !== "transient"
|
|
175
|
+
&& input.hasTerminalSession;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// #1258 (L3): claude prints its resume banner ANSI-dimmed — the raw log bytes are
|
|
179
|
+
// `\x1b[2mclaude --resume <uuid>\x1b[22m`, so `claude` is immediately preceded by the SGR
|
|
180
|
+
// escape's trailing `m`. A leading `\b` in the extraction regex finds no word boundary
|
|
181
|
+
// (`m`→`c` are both word chars) and yields zero matches, stranding the resume id as null.
|
|
182
|
+
// Strip CSI escape sequences before matching so the banner reads as plain text.
|
|
183
|
+
const ANSI_ESCAPE_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
184
|
+
|
|
185
|
+
function stripAnsi(text: string): string {
|
|
186
|
+
return text.replace(ANSI_ESCAPE_RE, "");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function resumeLogExtractionRe(provider: string): RegExp | undefined {
|
|
190
|
+
const manifest = getManifest(provider);
|
|
191
|
+
const resume = manifest?.resume as { supported: boolean; idPattern?: string } | undefined;
|
|
192
|
+
if (!resume?.supported || !resume.idPattern || !manifest?.probe?.command) return undefined;
|
|
193
|
+
return new RegExp(`\\b${manifest.probe.command}\\s+--resume\\s+(${resume.idPattern})\\b`, "gi");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function latestResumeIdFromText(provider: string, text: string): string | undefined {
|
|
197
|
+
const re = resumeLogExtractionRe(provider);
|
|
198
|
+
if (!re) return undefined;
|
|
199
|
+
const cleaned = stripAnsi(text);
|
|
200
|
+
let latest: string | undefined;
|
|
201
|
+
for (let match = re.exec(cleaned); match; match = re.exec(cleaned)) {
|
|
202
|
+
latest = match[1];
|
|
203
|
+
}
|
|
204
|
+
return latest;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function latestResumeIdFromLogFile(provider: string, path: string): string | undefined {
|
|
208
|
+
let fd: number | undefined;
|
|
209
|
+
try {
|
|
210
|
+
const stat = statSync(path);
|
|
211
|
+
const length = Math.min(stat.size, LOG_TAIL_BYTES);
|
|
212
|
+
const offset = Math.max(0, stat.size - length);
|
|
213
|
+
const buffer = Buffer.alloc(length);
|
|
214
|
+
fd = openSync(path, "r");
|
|
215
|
+
readSync(fd, buffer, 0, length, offset);
|
|
216
|
+
return latestResumeIdFromText(provider, buffer.toString("utf8"));
|
|
217
|
+
} catch {
|
|
218
|
+
return undefined;
|
|
219
|
+
} finally {
|
|
220
|
+
if (fd !== undefined) closeSync(fd);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function providerExitDiagnostics(input: {
|
|
225
|
+
status: SemanticStatus;
|
|
226
|
+
runtimeMs: number;
|
|
227
|
+
processMeta?: Record<string, unknown>;
|
|
228
|
+
hasProcess: boolean;
|
|
229
|
+
exitCommandInProgress: boolean;
|
|
230
|
+
stopped: boolean;
|
|
231
|
+
restartInProgress: boolean;
|
|
232
|
+
restartPending: boolean;
|
|
233
|
+
headless: boolean;
|
|
234
|
+
provider: string;
|
|
235
|
+
logFile?: string;
|
|
236
|
+
}): Record<string, unknown> {
|
|
237
|
+
const tmuxSession = typeof input.processMeta?.tmuxSession === "string" ? input.processMeta.tmuxSession : undefined;
|
|
238
|
+
const tmuxSocket = typeof input.processMeta?.tmuxSocket === "string" ? input.processMeta.tmuxSocket : undefined;
|
|
239
|
+
const exitSource = tmuxSession ? "tmux-session-ended" : input.hasProcess ? "process-exit" : "provider-status";
|
|
240
|
+
const claudeResumeId = getManifest(input.provider)?.resume?.supported && input.logFile ? latestResumeIdFromLogFile(input.provider, input.logFile) : undefined;
|
|
241
|
+
return {
|
|
242
|
+
status: input.status,
|
|
243
|
+
runtimeMs: Number.isFinite(input.runtimeMs) ? input.runtimeMs : null,
|
|
244
|
+
exitSource,
|
|
245
|
+
exitCommandInProgress: input.exitCommandInProgress,
|
|
246
|
+
stopped: input.stopped,
|
|
247
|
+
restartInProgress: input.restartInProgress,
|
|
248
|
+
restartPending: input.restartPending,
|
|
249
|
+
headless: input.headless,
|
|
250
|
+
hasTerminalSession: Boolean(tmuxSession),
|
|
251
|
+
tmuxSession: tmuxSession ?? null,
|
|
252
|
+
tmuxSocket: tmuxSocket ?? null,
|
|
253
|
+
claudeResumeId: claudeResumeId ?? null,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function providerExitResumeId(diagnostics: Record<string, unknown>): string | undefined {
|
|
258
|
+
const value = diagnostics.claudeResumeId;
|
|
259
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function providerManualRecoveryReason(diagnostics: Record<string, unknown>): string {
|
|
263
|
+
return providerExitResumeId(diagnostics)
|
|
264
|
+
? "claude-exit-manual-resume-available"
|
|
265
|
+
: "claude-exit-manual-intervention-required";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function providerDisplayName(provider: string): string {
|
|
269
|
+
return PROVIDER_DISPLAY_NAMES[provider] ?? provider;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function providerMonitorUnavailableMessage(provider: string): string {
|
|
273
|
+
return `no ${providerDisplayName(provider)} monitor connected`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function providerManualRecoveryMessage(provider: string, diagnostics: Record<string, unknown>): string {
|
|
277
|
+
const displayName = providerDisplayName(provider);
|
|
278
|
+
return providerExitResumeId(diagnostics)
|
|
279
|
+
? `${displayName} exited; runner will not auto-resume. Resume id captured for manual recovery.`
|
|
280
|
+
: `${displayName} exited; runner will not restart automatically.`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function providerExitLastError(provider: string, marker: Record<string, unknown>): string {
|
|
284
|
+
const resumeId = providerExitResumeId(marker);
|
|
285
|
+
const displayName = providerDisplayName(provider);
|
|
286
|
+
return resumeId
|
|
287
|
+
? `${displayName} provider exited; resume id ${resumeId} captured for manual recovery`
|
|
288
|
+
: `${displayName} provider exited; manual intervention required`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function commandTimeoutMs(params: Record<string, unknown>, fallback = 10_000): number {
|
|
292
|
+
const raw = params.timeoutMs;
|
|
293
|
+
if (typeof raw !== "number" || !Number.isSafeInteger(raw) || raw <= 0) return fallback;
|
|
294
|
+
return Math.min(raw, 60_000);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// #1339: the Codex adapter rejects a delivery attempted while a turn is already in progress
|
|
298
|
+
// (Codex has no queue-while-busy the way Claude's input buffer does), so the runner re-queues
|
|
299
|
+
// and re-drains once the turn ends. That rejection is expected flow control, not a fault — the
|
|
300
|
+
// drain loop retries roughly once a second during a long turn, so it must not spam the log.
|
|
301
|
+
export const CODEX_TURN_IN_PROGRESS_MESSAGE = "codex turn in progress; deferring delivery until it completes";
|
|
302
|
+
|
|
303
|
+
export function shouldLogDeliveryFailure(error: unknown): boolean {
|
|
304
|
+
const message = errMessage(error);
|
|
305
|
+
return message !== providerMonitorUnavailableMessage("claude")
|
|
306
|
+
&& message !== CODEX_TURN_IN_PROGRESS_MESSAGE;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** The same rejection, seen from the COMMAND side (#1565 F1): a serialized boundary payload that
|
|
310
|
+
* met the turn the payload before it started is deferred until that turn ends, not failed. */
|
|
311
|
+
export function isTurnInProgressDeliveryError(error: unknown): boolean {
|
|
312
|
+
return errMessage(error) === CODEX_TURN_IN_PROGRESS_MESSAGE;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Command types that put a payload into the provider's single input channel, and therefore take a
|
|
316
|
+
* `ProviderInputQueue` ticket at dispatch so two of them can never interleave (#1565 F1). */
|
|
317
|
+
export const PROVIDER_INPUT_COMMAND_TYPES: ReadonlySet<string> = new Set(["agent.clearContext", "agent.injectContext", "prompt.inject"]);
|
|
318
|
+
|
|
319
|
+
export function runtimeTokenRenewDelayMs(expiresAtSeconds: number, nowMs = Date.now()): number | undefined {
|
|
320
|
+
const expiresAtMs = expiresAtSeconds * 1000;
|
|
321
|
+
const ttlMs = expiresAtMs - nowMs;
|
|
322
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0) return undefined;
|
|
323
|
+
const leadMs = Math.min(60 * 60 * 1000, Math.max(60_000, Math.floor(ttlMs * 0.2)));
|
|
324
|
+
const jitterMs = Math.floor(Math.random() * Math.min(30_000, Math.max(0, leadMs / 2)));
|
|
325
|
+
return Math.max(1_000, ttlMs - leadMs - jitterMs);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function runtimeTokenRenewTimerSchedule(delayMs: number): { delayMs: number; renew: boolean } | undefined {
|
|
329
|
+
if (!Number.isFinite(delayMs) || delayMs <= 0) return undefined;
|
|
330
|
+
if (delayMs > MAX_TIMER_DELAY_MS) return { delayMs: MAX_TIMER_DELAY_MS, renew: false };
|
|
331
|
+
return { delayMs, renew: true };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function lifecycleCapabilities(): Record<string, true> {
|
|
335
|
+
return {
|
|
336
|
+
shutdownHard: true,
|
|
337
|
+
restartHard: true,
|
|
338
|
+
semanticStatus: true,
|
|
339
|
+
transportReconnect: true,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function resolveCondition(condition: ProviderCapabilityCondition, options: RuntimeProviderOptions): boolean {
|
|
344
|
+
switch (condition) {
|
|
345
|
+
case "always": return true;
|
|
346
|
+
case "never": return false;
|
|
347
|
+
case "headless": return options.headless;
|
|
348
|
+
case "interactive": return !options.headless;
|
|
349
|
+
case "approval:read-only": return options.approvalMode === "read-only";
|
|
350
|
+
case "approval:not-read-only": return options.approvalMode !== "read-only";
|
|
351
|
+
case "adapter:compact-supports-instructions": return Boolean(options.adapter.compactSupportsInstructions);
|
|
352
|
+
case "adapter:clear-context": return Boolean(options.adapter.clearContext);
|
|
353
|
+
default: return false;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function resolveBooleanDefault(def: BooleanCapabilityDefault, options: RuntimeProviderOptions): boolean {
|
|
358
|
+
if (typeof def === "boolean") return def;
|
|
359
|
+
return resolveCondition(def.when, options) ? def.value : def.otherwise;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function runtimeProviderCapabilities(options: RuntimeProviderOptions, contextState?: ContextState, probeModel?: ProbeModelInfo, quotaState?: QuotaState): ProviderCapabilities {
|
|
363
|
+
const manifest = getManifest(options.provider);
|
|
364
|
+
const capDefaults = manifest?.capabilityDefaults;
|
|
365
|
+
const model = options.model ?? probeModel?.model;
|
|
366
|
+
const effort = options.effort ?? probeModel?.effort;
|
|
367
|
+
const modelSource = options.model ? "runtime" as const : probeModel?.model ? "provider" as const : "runtime" as const;
|
|
368
|
+
const shellMode = runtimeShellMode(options);
|
|
369
|
+
return {
|
|
370
|
+
lifecycle: {
|
|
371
|
+
managed: capDefaults?.lifecycle?.managed ?? true,
|
|
372
|
+
shutdownHard: capDefaults?.lifecycle?.shutdownHard !== undefined ? resolveBooleanDefault(capDefaults.lifecycle.shutdownHard, options) : true,
|
|
373
|
+
restartHard: capDefaults?.lifecycle?.restartHard !== undefined ? resolveBooleanDefault(capDefaults.lifecycle.restartHard, options) : true,
|
|
374
|
+
semanticStatus: capDefaults?.lifecycle?.semanticStatus ?? true,
|
|
375
|
+
reconnect: capDefaults?.lifecycle?.reconnect ?? true,
|
|
376
|
+
},
|
|
377
|
+
model: {
|
|
378
|
+
provider: options.provider,
|
|
379
|
+
alias: model,
|
|
380
|
+
id: model,
|
|
381
|
+
effort,
|
|
382
|
+
...(options.effortSwitch ? { effortSwitch: true } : {}),
|
|
383
|
+
source: modelSource,
|
|
384
|
+
confidence: options.model ? "reported" : probeModel?.model ? "reported" : "unknown",
|
|
385
|
+
lastUpdatedAt: options.startedAt,
|
|
386
|
+
},
|
|
387
|
+
session: {
|
|
388
|
+
approvalMode: options.approvalMode,
|
|
389
|
+
fileRead: true,
|
|
390
|
+
fileWrite: options.approvalMode !== "read-only",
|
|
391
|
+
shell: shellMode !== "none",
|
|
392
|
+
shellMode,
|
|
393
|
+
source: "runtime",
|
|
394
|
+
confidence: "reported",
|
|
395
|
+
lastUpdatedAt: options.startedAt,
|
|
396
|
+
},
|
|
397
|
+
...runtimeProviderContextCapabilities(options, contextState),
|
|
398
|
+
...runtimeProviderQuotaCapabilities(options, quotaState),
|
|
399
|
+
...runtimeProviderTerminalCapabilities(options),
|
|
400
|
+
liveSession: {
|
|
401
|
+
capture: true,
|
|
402
|
+
inject: Boolean(options.adapter.deliverInitialPrompt),
|
|
403
|
+
interrupt: Boolean(options.adapter.interrupt),
|
|
404
|
+
steer: supportsActiveTurnSteer(options.adapter, options.providerConfig),
|
|
405
|
+
promptEcho: true,
|
|
406
|
+
reasoning: true,
|
|
407
|
+
slashCommands: capDefaults?.liveSession?.slashCommands ?? false,
|
|
408
|
+
},
|
|
409
|
+
source: "runtime",
|
|
410
|
+
confidence: "reported",
|
|
411
|
+
lastUpdatedAt: options.startedAt,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function runtimeShellMode(options: RuntimeProviderOptions): NonNullable<NonNullable<ProviderCapabilities["session"]>["shellMode"]> {
|
|
416
|
+
const manifest = getManifest(options.provider);
|
|
417
|
+
const shellModeMap = manifest?.capabilityDefaults?.session?.shellMode;
|
|
418
|
+
if (options.approvalMode === "read-only") return (shellModeMap?.readOnly ?? "none") as NonNullable<NonNullable<ProviderCapabilities["session"]>["shellMode"]>;
|
|
419
|
+
if (options.approvalMode === "open") return (shellModeMap?.open ?? "unrestricted") as NonNullable<NonNullable<ProviderCapabilities["session"]>["shellMode"]>;
|
|
420
|
+
return (shellModeMap?.guarded ?? "guarded") as NonNullable<NonNullable<ProviderCapabilities["session"]>["shellMode"]>;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function runtimeProviderTerminalCapabilities(options: RuntimeProviderOptions): Pick<ProviderCapabilities, "terminal"> {
|
|
424
|
+
const manifest = getManifest(options.provider);
|
|
425
|
+
const termDefaults = manifest?.capabilityDefaults?.terminal;
|
|
426
|
+
if (!termDefaults || termDefaults.mode === "none" || options.launchMode === "headless") return {};
|
|
427
|
+
if (termDefaults.condition && !resolveCondition(termDefaults.condition, options)) return {};
|
|
428
|
+
if (termDefaults.mode === "live" && termDefaults.live) {
|
|
429
|
+
return { terminal: { live: termDefaults.live } };
|
|
430
|
+
}
|
|
431
|
+
if (termDefaults.mode === "attach" && termDefaults.attach) {
|
|
432
|
+
return { terminal: { attach: termDefaults.attach } };
|
|
433
|
+
}
|
|
434
|
+
return {};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// #557 — the agent-card projection now DESCRIBES the assembled launch (same source the
|
|
438
|
+
// adapter turns into args), so it can't drift from reality. `config`/`providerConfig` are
|
|
439
|
+
// the spawn inputs; when present we assemble (pure — no disk writes here) and report that.
|
|
440
|
+
export function appliedAgentProfileMetadata(
|
|
441
|
+
provider: string,
|
|
442
|
+
profile: AgentProfile,
|
|
443
|
+
config?: RunnerSpawnConfig,
|
|
444
|
+
providerConfig?: ProviderConfig,
|
|
445
|
+
): Record<string, unknown> {
|
|
446
|
+
const projection = isSpawnProvider(provider) && config && providerConfig
|
|
447
|
+
? agentProfileProjectionReport({ assembled: assembleLaunch(provider, config, providerConfig) })
|
|
448
|
+
: undefined;
|
|
449
|
+
return {
|
|
450
|
+
name: profile.name,
|
|
451
|
+
base: profile.base,
|
|
452
|
+
provider: profile.provider ?? "any",
|
|
453
|
+
relay: profile.relay,
|
|
454
|
+
instructions: {
|
|
455
|
+
repoInstructions: profile.instructions.repoInstructions,
|
|
456
|
+
globalInstructions: profile.instructions.globalInstructions,
|
|
457
|
+
appendCount: profile.instructions.append.length,
|
|
458
|
+
hasSystem: Boolean(profile.instructions.system),
|
|
459
|
+
},
|
|
460
|
+
skills: profile.skills.filter((item) => item.enabled).length,
|
|
461
|
+
plugins: profile.plugins.filter((item) => item.enabled).length,
|
|
462
|
+
mcp: { mode: profile.mcp.mode },
|
|
463
|
+
hooks: { mode: profile.hooks.mode, refs: profile.hooks.refs?.filter((item) => item.enabled).length ?? 0 },
|
|
464
|
+
permissions: profile.permissions,
|
|
465
|
+
harnessSubagents: profile.harnessSubagents === true,
|
|
466
|
+
projection,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function runtimeProviderContextCapabilities(options: RuntimeProviderOptions, contextState?: ContextState): Pick<ProviderCapabilities, "context"> {
|
|
471
|
+
const manifest = getManifest(options.provider);
|
|
472
|
+
const ctxDefaults = manifest?.capabilityDefaults?.context;
|
|
473
|
+
const context: NonNullable<ProviderCapabilities["context"]> = { resume: "none" };
|
|
474
|
+
context.compactInstructions = Boolean(options.adapter.compact && options.adapter.compactSupportsInstructions);
|
|
475
|
+
const supportsManagedContext = ctxDefaults?.managed ? resolveCondition(ctxDefaults.managed, options) : false;
|
|
476
|
+
if (contextState) {
|
|
477
|
+
context.stats = { source: contextState.source, confidence: contextState.confidence };
|
|
478
|
+
if (typeof contextState.tokensMax === "number" && Number.isFinite(contextState.tokensMax)) {
|
|
479
|
+
context.windowTokens = contextState.tokensMax;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (ctxDefaults) {
|
|
483
|
+
if (resolveBooleanDefault(ctxDefaults.compact, options)) context.compact = true;
|
|
484
|
+
if (resolveBooleanDefault(ctxDefaults.clear, options)) context.clear = true;
|
|
485
|
+
context.inject = ctxDefaults.inject;
|
|
486
|
+
}
|
|
487
|
+
if (supportsManagedContext && options.adapter.compact && (options.adapter.compactSupportsInstructions || options.adapter.compactReportsSubmission)) context.resume = "native";
|
|
488
|
+
else if (supportsManagedContext && options.adapter.clearContext) context.resume = "clear-inject";
|
|
489
|
+
return Object.keys(context).length ? { context } : {};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function runtimeProviderQuotaCapabilities(options: RuntimeProviderOptions, quotaState?: QuotaState): Pick<ProviderCapabilities, "quota"> {
|
|
493
|
+
const manifest = getManifest(options.provider);
|
|
494
|
+
const quotaDefaults = manifest?.capabilityDefaults?.quota;
|
|
495
|
+
if (!quotaDefaults?.supported) return {};
|
|
496
|
+
return {
|
|
497
|
+
quota: {
|
|
498
|
+
supported: true,
|
|
499
|
+
source: quotaState?.source ?? "probe",
|
|
500
|
+
unit: quotaState?.windows[0]?.unit ?? "%",
|
|
501
|
+
windows: quotaState?.windows.map((window) => ({
|
|
502
|
+
name: window.name,
|
|
503
|
+
unit: window.unit,
|
|
504
|
+
reset: window.resetsAt !== undefined,
|
|
505
|
+
})) ?? manifest!.quotaWindows.map(w => ({ name: w.name, unit: w.unit as "%" | "tokens" | "requests", reset: w.reset })),
|
|
506
|
+
},
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export function providerStateFromActiveWork(activeWork: Array<{ kind: string; metadata?: Record<string, unknown> }>): ProviderState | null {
|
|
511
|
+
const providerTurn = activeWork.find((item) => item.kind === "provider-turn");
|
|
512
|
+
const state = providerTurn?.metadata?.providerState;
|
|
513
|
+
return state && typeof state === "object" && !Array.isArray(state) && typeof (state as { state?: unknown }).state === "string"
|
|
514
|
+
? state as ProviderState
|
|
515
|
+
: null;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export function isHttpAuthError(error: unknown): boolean {
|
|
519
|
+
const status = typeof error === "object" && error !== null ? (error as { status?: unknown }).status : undefined;
|
|
520
|
+
return status === 401 || status === 403;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// #2433 — outbox delivery-error classification. The runner outboxes must never poison
|
|
524
|
+
// (permanently drop) an event over a CONNECTION-class failure: report-up chat rides the
|
|
525
|
+
// session outbox, so a poisoned row is real message loss. These predicates split a send()
|
|
526
|
+
// rejection into TRANSIENT (connect-phase transport failure, HTTP 5xx, or 429) versus
|
|
527
|
+
// DETERMINISTIC client rejection (400/413/422 — the relay parsed and refused this exact
|
|
528
|
+
// payload). Like the transport classifier in transport-errors.ts, the connect predicate walks
|
|
529
|
+
// the `cause` chain and inspects `code`, `name`, and message text: Bun surfaces undici's
|
|
530
|
+
// pre-send "Unable to connect" / "fetch failed" with the real code (ECONNREFUSED, ENOTFOUND…)
|
|
531
|
+
// on a nested cause.
|
|
532
|
+
// Connect-phase / mid-flight transport failure: refused, DNS, unreachable, socket lost, or a
|
|
533
|
+
// client-side timeout. Always safe to retry for outbox events — every row carries an
|
|
534
|
+
// idempotency key the relay dedups, so a resend can never double-apply.
|
|
535
|
+
const CONNECT_ERROR_CODE_RE = /^(?:ECONNREFUSED|ENOTFOUND|EAI_AGAIN|EHOSTUNREACH|ENETUNREACH|ETIMEDOUT|ECONNABORTED|ECONNRESET|EPIPE|ConnectionRefused)$/i;
|
|
536
|
+
const CONNECT_ERROR_MESSAGE_RE = /unable to connect|econnrefused|enotfound|eai_again|ehostunreach|enetunreach|etimedout|connection refused|fetch failed|failed to fetch|getaddrinfo|\bdns\b|network|timeout|timed out|socket hang up|socket connection was closed|connection was closed unexpectedly/i;
|
|
537
|
+
|
|
538
|
+
export function isConnectError(error: unknown): boolean {
|
|
539
|
+
return walkErrorChain(error, (code, _name, message) =>
|
|
540
|
+
CONNECT_ERROR_CODE_RE.test(code) || CONNECT_ERROR_MESSAGE_RE.test(message));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function httpErrorStatus(error: unknown): number | undefined {
|
|
544
|
+
const status = typeof error === "object" && error !== null ? (error as { status?: unknown }).status : undefined;
|
|
545
|
+
return typeof status === "number" ? status : undefined;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// TRANSIENT for the outbox retry policy (#2433): connection-class transport failures plus
|
|
549
|
+
// HTTP 5xx/429 (relay momentarily overloaded/restarting). Transient failures NEVER poison on
|
|
550
|
+
// attempt count — the outbox parks the head on a slow periodic re-drive until the relay returns.
|
|
551
|
+
export function isTransientDeliveryError(error: unknown): boolean {
|
|
552
|
+
if (isConnectError(error)) return true;
|
|
553
|
+
const status = httpErrorStatus(error);
|
|
554
|
+
return status !== undefined && (status >= 500 || status === 429);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// DETERMINISTIC client rejection (#2433): the relay is up, parsed the payload, and will refuse
|
|
558
|
+
// it every time. Poison immediately so one genuinely-bad row cannot stall the queue.
|
|
559
|
+
export function isDeterministicClientError(error: unknown): boolean {
|
|
560
|
+
const status = httpErrorStatus(error);
|
|
561
|
+
return status === 400 || status === 413 || status === 422;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export function isHttpStatusError(error: unknown, code: number): boolean {
|
|
565
|
+
const status = typeof error === "object" && error !== null ? (error as { status?: unknown }).status : undefined;
|
|
566
|
+
return status === code;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export function httpErrorKey(error: unknown): string {
|
|
570
|
+
const status = typeof error === "object" && error !== null ? (error as { status?: unknown }).status : undefined;
|
|
571
|
+
if (typeof status === "number") return `status:${status}`;
|
|
572
|
+
return String(error);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export function isContextState(value: unknown): value is ContextState {
|
|
576
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
577
|
+
const state = value as Record<string, unknown>;
|
|
578
|
+
return typeof state.utilization === "number" &&
|
|
579
|
+
typeof state.lifecycleState === "string" &&
|
|
580
|
+
typeof state.source === "string" &&
|
|
581
|
+
typeof state.confidence === "string";
|
|
582
|
+
}
|