@humwick/runner 0.0.0-stage → 0.130.1
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 +42 -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,207 @@
|
|
|
1
|
+
import { errMessage } from "@humwick/sdk";
|
|
2
|
+
import type { ManagedProcess } from "../adapter";
|
|
3
|
+
import { orchestratorBaseDirFromEnv } from "../config";
|
|
4
|
+
import { logger } from "../logger";
|
|
5
|
+
import { PRE_DESTROY_TIMEOUT_MS } from "../session-destroy";
|
|
6
|
+
import { reapSessionLauncher, reapSessionScratch } from "../session-scratch";
|
|
7
|
+
import type { RunnerLaunchOptions } from "./runner-context";
|
|
8
|
+
|
|
9
|
+
export type StopMilestone = "teardown-started" | "background-stopped" | "provider-shut-down" | "offline-published" | "timers-stopped" | "closed";
|
|
10
|
+
export type RestartMilestone = "work-cleared" | "replacement-adopted" | "launch-seeded" | "teardown-failed";
|
|
11
|
+
|
|
12
|
+
interface LifecycleMilestoneDeps {
|
|
13
|
+
options: RunnerLaunchOptions;
|
|
14
|
+
agentId: string;
|
|
15
|
+
process(): ManagedProcess | undefined;
|
|
16
|
+
stopAttachmentCacheSweep(): void;
|
|
17
|
+
stopTestRunDedupSweep(): void;
|
|
18
|
+
stopGateRunLeaseProbe(): void;
|
|
19
|
+
disposeEmptyTurns(): void;
|
|
20
|
+
resetEmptyTurns(): void;
|
|
21
|
+
resetLateClearRecovery(): void;
|
|
22
|
+
clearLifecycleAction(): void;
|
|
23
|
+
setOffline(): void;
|
|
24
|
+
publishStatus(): void;
|
|
25
|
+
stopDrainTimer(): void;
|
|
26
|
+
stopSubmitRecoveryPoll(): void;
|
|
27
|
+
stopTaskClaims(): void;
|
|
28
|
+
stopHttpLiveness(): void;
|
|
29
|
+
stopCredentials(): void;
|
|
30
|
+
disarmBusyLivenessReconcile(): void;
|
|
31
|
+
disarmBusyReconciler(): void;
|
|
32
|
+
disposeProviderInput(): void;
|
|
33
|
+
disposeParkPosts(): void;
|
|
34
|
+
stopReasoningTail(): void;
|
|
35
|
+
clearNativeSessionCapture(): void;
|
|
36
|
+
stopObligationCache(): void;
|
|
37
|
+
closeStores(): void;
|
|
38
|
+
stopProxy(): void;
|
|
39
|
+
stopControl(): void;
|
|
40
|
+
clearTerminalStatus(): void;
|
|
41
|
+
clearProviderTurnWork(): void;
|
|
42
|
+
clearSubagentWork(): void;
|
|
43
|
+
clearBackgroundScriptWork(): void;
|
|
44
|
+
endLateClearGrace(): void;
|
|
45
|
+
clearDeliverySubmitHold(): void;
|
|
46
|
+
clearSubmitFence(): void;
|
|
47
|
+
clearHeldSubmit(): void;
|
|
48
|
+
clearSubmitDeliveryAttempts(): void;
|
|
49
|
+
clearPreDestroyRun(): void;
|
|
50
|
+
reopenProviderStatus(): void;
|
|
51
|
+
resumeHeartbeat(): void;
|
|
52
|
+
settleTerminalTokenUsage(): Promise<void>;
|
|
53
|
+
closeBus(): Promise<void>;
|
|
54
|
+
terminalStatusLatched(): boolean;
|
|
55
|
+
restoreProviderSessionId(value: ReturnType<typeof crypto.randomUUID>): void;
|
|
56
|
+
restoreLastTranscriptPath(value: string | undefined): void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class LifecycleMilestones {
|
|
60
|
+
constructor(private readonly deps: LifecycleMilestoneDeps) {}
|
|
61
|
+
|
|
62
|
+
// Synchronous milestones stay in the caller's stack; only existing I/O boundaries yield.
|
|
63
|
+
onStop(milestone: "teardown-started" | "offline-published", alreadyStopped: boolean): void;
|
|
64
|
+
onStop(milestone: "background-stopped" | "timers-stopped"): void;
|
|
65
|
+
onStop(milestone: "provider-shut-down" | "closed"): Promise<void>;
|
|
66
|
+
onStop(milestone: StopMilestone, alreadyStopped = false): void | Promise<void> {
|
|
67
|
+
switch (milestone) {
|
|
68
|
+
case "teardown-started": return this.teardownStarted(alreadyStopped);
|
|
69
|
+
case "background-stopped": return this.backgroundStopped();
|
|
70
|
+
case "provider-shut-down": return this.providerShutDown();
|
|
71
|
+
case "offline-published": return this.offlinePublished(alreadyStopped);
|
|
72
|
+
case "timers-stopped": return this.timersStopped();
|
|
73
|
+
case "closed": return this.closed();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
onRestart(milestone: Exclude<RestartMilestone, "teardown-failed">): void;
|
|
78
|
+
onRestart(milestone: "teardown-failed", priorProviderSessionId: ReturnType<typeof crypto.randomUUID>, priorLastTranscriptPath: string | undefined): Promise<void>;
|
|
79
|
+
onRestart(milestone: RestartMilestone, priorProviderSessionId?: ReturnType<typeof crypto.randomUUID>, priorLastTranscriptPath?: string): void | Promise<void> {
|
|
80
|
+
switch (milestone) {
|
|
81
|
+
case "work-cleared": return this.workCleared();
|
|
82
|
+
case "replacement-adopted": return this.replacementAdopted();
|
|
83
|
+
case "launch-seeded": return this.launchSeeded();
|
|
84
|
+
case "teardown-failed": return this.teardownFailed(priorProviderSessionId!, priorLastTranscriptPath);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private teardownStarted(alreadyStopped: boolean): void {
|
|
89
|
+
if (!alreadyStopped) this.deps.publishStatus();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private backgroundStopped(): void {
|
|
93
|
+
this.deps.stopAttachmentCacheSweep();
|
|
94
|
+
this.deps.stopTestRunDedupSweep();
|
|
95
|
+
this.deps.stopGateRunLeaseProbe();
|
|
96
|
+
// #2389 — before the shutdown await, not after: an escalation or settle timer firing during a
|
|
97
|
+
// graceful provider shutdown would publish a "produced nothing" verdict about a turn the
|
|
98
|
+
// shutdown itself ended.
|
|
99
|
+
this.deps.disposeEmptyTurns();
|
|
100
|
+
this.deps.resetLateClearRecovery();
|
|
101
|
+
const process = this.deps.process();
|
|
102
|
+
const launcherScript = typeof process?.meta?.launcherScript === "string" ? process.meta.launcherScript : undefined; const tmuxSession = typeof process?.meta?.tmuxSession === "string" ? process.meta.tmuxSession : undefined;
|
|
103
|
+
reapSessionScratch({ agentId: this.deps.agentId, cwd: this.deps.options.cwd, fallbackBaseDir: orchestratorBaseDirFromEnv() });
|
|
104
|
+
reapSessionLauncher({ launcherScript, tmuxSession });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private async providerShutDown(): Promise<void> {
|
|
108
|
+
// #2491 review MED-1 — time-boxed like the other two settleTerminalTokenUsage call
|
|
109
|
+
// sites (restartUnexpectedProviderExit, runPreSessionDestroy): an unsettled capture
|
|
110
|
+
// must not eat into the graceful-shutdown grace window and push the provider toward a
|
|
111
|
+
// forced SIGKILL.
|
|
112
|
+
try {
|
|
113
|
+
await Promise.race([
|
|
114
|
+
this.deps.settleTerminalTokenUsage(),
|
|
115
|
+
new Promise<void>((resolve) => setTimeout(resolve, PRE_DESTROY_TIMEOUT_MS)),
|
|
116
|
+
]);
|
|
117
|
+
await this.deps.options.adapter.shutdown(this.deps.process()!, {
|
|
118
|
+
graceful: true,
|
|
119
|
+
timeoutMs: this.deps.options.providerConfig.headless.shutdownTimeoutMs,
|
|
120
|
+
});
|
|
121
|
+
} catch (error) {
|
|
122
|
+
// stop() is already the runner's terminal path. A provider teardown throw is still a
|
|
123
|
+
// completed attempt: keep going so the terminal frame clears the lifecycle pill and every
|
|
124
|
+
// timer/outbox/control/bus resource below is closed instead of leaving an orange ghost.
|
|
125
|
+
logger.error("lifecycle", `provider shutdown during runner stop failed for ${this.deps.agentId}: ${errMessage(error)}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private offlinePublished(alreadyStopped: boolean): void {
|
|
130
|
+
if (!alreadyStopped) {
|
|
131
|
+
// The terminal edge belongs at teardown completion, not intent. Keeping claims live until
|
|
132
|
+
// here preserves the dashboard's in-flight pill and the shutdown watchdog. The shared
|
|
133
|
+
// status projection includes lifecycleAction:null, clearing the server-owned pill on the
|
|
134
|
+
// same frame that pins the row offline.
|
|
135
|
+
this.deps.clearLifecycleAction();
|
|
136
|
+
this.deps.setOffline();
|
|
137
|
+
this.deps.publishStatus();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private timersStopped(): void {
|
|
142
|
+
this.deps.stopDrainTimer();
|
|
143
|
+
this.deps.stopSubmitRecoveryPoll();
|
|
144
|
+
this.deps.stopTaskClaims();
|
|
145
|
+
this.deps.stopHttpLiveness();
|
|
146
|
+
this.deps.stopCredentials();
|
|
147
|
+
this.deps.disarmBusyLivenessReconcile();
|
|
148
|
+
this.deps.disarmBusyReconciler();
|
|
149
|
+
this.deps.disposeProviderInput();
|
|
150
|
+
this.deps.disposeParkPosts();
|
|
151
|
+
this.deps.stopReasoningTail();
|
|
152
|
+
this.deps.clearNativeSessionCapture();
|
|
153
|
+
this.deps.stopObligationCache();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private closed(): Promise<void> {
|
|
157
|
+
this.deps.closeStores();
|
|
158
|
+
this.deps.stopProxy();
|
|
159
|
+
this.deps.stopControl();
|
|
160
|
+
return this.deps.closeBus();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private workCleared(): void {
|
|
164
|
+
this.deps.clearTerminalStatus();
|
|
165
|
+
this.deps.clearProviderTurnWork();
|
|
166
|
+
this.deps.clearSubagentWork();
|
|
167
|
+
this.deps.clearBackgroundScriptWork();
|
|
168
|
+
// #2389 — a pre-restart obligation must never be charged against an unrelated post-restart
|
|
169
|
+
// turn. Covers every restart, including the suppressPrompt one recoverNativeSelfResumeExit
|
|
170
|
+
// takes, where the agent comes back with no prompt at all.
|
|
171
|
+
this.deps.resetEmptyTurns();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private replacementAdopted(): void {
|
|
175
|
+
this.deps.endLateClearGrace(); // The installed replacement cannot receive the old /clear.
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private launchSeeded(): void {
|
|
179
|
+
// A replacement tmux incarnation can accept new work. The failed inbox row stays
|
|
180
|
+
// server-side as failed until an operator explicitly retries it.
|
|
181
|
+
this.deps.clearDeliverySubmitHold();
|
|
182
|
+
this.deps.stopSubmitRecoveryPoll();
|
|
183
|
+
this.deps.clearSubmitFence();
|
|
184
|
+
this.deps.clearHeldSubmit();
|
|
185
|
+
this.deps.clearSubmitDeliveryAttempts();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private async teardownFailed(priorProviderSessionId: ReturnType<typeof crypto.randomUUID>, priorLastTranscriptPath: string | undefined): Promise<void> {
|
|
189
|
+
const heartbeatNeedsResume = this.deps.terminalStatusLatched();
|
|
190
|
+
// Restore the surviving process's identity and transcript watermark so its hook frames are
|
|
191
|
+
// not rejected as stale after the failed teardown. The already-completed restart capture
|
|
192
|
+
// must NOT survive: this incarnation can produce new content before its eventual shutdown
|
|
193
|
+
// boundary, which therefore needs a fresh capture.
|
|
194
|
+
this.deps.restoreProviderSessionId(priorProviderSessionId);
|
|
195
|
+
this.deps.clearPreDestroyRun();
|
|
196
|
+
this.deps.restoreLastTranscriptPath(priorLastTranscriptPath);
|
|
197
|
+
this.deps.reopenProviderStatus();
|
|
198
|
+
if (heartbeatNeedsResume) this.deps.resumeHeartbeat();
|
|
199
|
+
if (this.deps.process()) {
|
|
200
|
+
try {
|
|
201
|
+
await this.deps.options.adapter.resumeStatusWatchers?.(this.deps.process()!);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
logger.error("lifecycle", `failed to resume provider status watchers after restart failure for ${this.deps.agentId}: ${errMessage(error)}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { LifecycleAction } from "../session-destroy";
|
|
2
|
+
|
|
3
|
+
interface LifecyclePhaseDeps {
|
|
4
|
+
suspendHeartbeat(): void;
|
|
5
|
+
stopStatusWatchers(): void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class LifecyclePhase {
|
|
9
|
+
private stoppedValue = false;
|
|
10
|
+
// Provider status is one-way once teardown intent is armed. This fences adapter callbacks,
|
|
11
|
+
// hooks, watchers and semantic heartbeats, but deliberately does NOT pin the runner projection
|
|
12
|
+
// offline: runner-owned lifecycle pills remain visible until teardown actually completes.
|
|
13
|
+
private terminalStatusLatchedValue = false;
|
|
14
|
+
// #2414: signal intent must suppress restart without skipping stop() teardown.
|
|
15
|
+
private shuttingDownValue = false;
|
|
16
|
+
private exitCommandInProgressValue = false;
|
|
17
|
+
private restartInProgressValue = false;
|
|
18
|
+
// Set for the whole unexpected-exit restart, including the pre-restart backoff
|
|
19
|
+
// sleep, so a second exit during that window can't launch a parallel restart.
|
|
20
|
+
private restartPendingValue = false;
|
|
21
|
+
private lifecycleActionValue?: LifecycleAction;
|
|
22
|
+
private lifecycleActionAtValue?: number;
|
|
23
|
+
|
|
24
|
+
get stopped(): boolean { return this.stoppedValue; }
|
|
25
|
+
get shuttingDown(): boolean { return this.shuttingDownValue; }
|
|
26
|
+
get terminalStatusLatched(): boolean { return this.terminalStatusLatchedValue; }
|
|
27
|
+
get exitCommandInProgress(): boolean { return this.exitCommandInProgressValue; }
|
|
28
|
+
get restartInProgress(): boolean { return this.restartInProgressValue; }
|
|
29
|
+
get restartPending(): boolean { return this.restartPendingValue; }
|
|
30
|
+
get lifecycleAction(): LifecycleAction | undefined { return this.lifecycleActionValue; }
|
|
31
|
+
get lifecycleActionAt(): number | undefined { return this.lifecycleActionAtValue; }
|
|
32
|
+
|
|
33
|
+
constructor(private readonly deps: LifecyclePhaseDeps) {}
|
|
34
|
+
|
|
35
|
+
// #2414: latch synchronously in the signal handler, before child exit callbacks.
|
|
36
|
+
markShuttingDown(): void {
|
|
37
|
+
this.shuttingDownValue = true;
|
|
38
|
+
if (!this.lifecycleActionValue) this.setLifecycleAction("shutting-down");
|
|
39
|
+
this.armTerminal();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
beginStop(): boolean {
|
|
43
|
+
const alreadyStopped = this.stoppedValue;
|
|
44
|
+
this.stoppedValue = true;
|
|
45
|
+
if (!alreadyStopped && !this.lifecycleActionValue) this.setLifecycleAction("shutting-down");
|
|
46
|
+
this.armTerminal();
|
|
47
|
+
return alreadyStopped;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
markStopped(): void { this.stoppedValue = true; }
|
|
51
|
+
beginExitCommand(): void { this.exitCommandInProgressValue = true; }
|
|
52
|
+
beginRestart(): void { this.restartInProgressValue = true; }
|
|
53
|
+
finishRestart(): void { this.restartInProgressValue = false; }
|
|
54
|
+
beginUnexpectedExitRestart(): void { this.restartPendingValue = true; }
|
|
55
|
+
finishUnexpectedExitRestart(): void { this.restartPendingValue = false; }
|
|
56
|
+
reopenProviderStatus(): void { this.terminalStatusLatchedValue = false; }
|
|
57
|
+
|
|
58
|
+
notePublishedFinalizing(action: LifecycleAction, at: number): void {
|
|
59
|
+
this.lifecycleActionValue = action;
|
|
60
|
+
this.lifecycleActionAtValue = at;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
armTerminal(): void {
|
|
64
|
+
// One synchronous operation closes every provider-originated status source before any teardown
|
|
65
|
+
// await opens a resurrection window. Claims stay unpinned until confirmed completion so the
|
|
66
|
+
// runner's own shutting-down/killing/kill-failed projection remains visible.
|
|
67
|
+
const alreadyLatched = this.terminalStatusLatchedValue;
|
|
68
|
+
this.terminalStatusLatchedValue = true;
|
|
69
|
+
if (!alreadyLatched) {
|
|
70
|
+
this.deps.suspendHeartbeat();
|
|
71
|
+
this.deps.stopStatusWatchers();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
setLifecycleAction(action: LifecycleAction | undefined): void {
|
|
76
|
+
if (this.lifecycleActionValue === action) return;
|
|
77
|
+
this.lifecycleActionValue = action;
|
|
78
|
+
this.lifecycleActionAtValue = action ? Date.now() : undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { writeChildEnv } from "@humwick/sdk/env";
|
|
2
|
+
import { isOpenAiApiModel } from "@humwick/sdk";
|
|
3
|
+
import { harnessSubagentsEnabled } from "@humwick/sdk";
|
|
4
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { dirname } from "node:path";
|
|
6
|
+
import type { ContextProbeMetrics, ContextState } from "@humwick/sdk";
|
|
7
|
+
import type { ManagedProcess, RunnerSpawnConfig } from "../adapter";
|
|
8
|
+
import type { ControlServer } from "../control-server";
|
|
9
|
+
import { forceContextPctFromEnv, runnerInfoFileFromEnv, runtimeTokenFileFromEnv, workspaceModeFromEnv } from "../config";
|
|
10
|
+
import { appliedAgentProfileMetadata, lifecycleCapabilities, runtimeProviderCapabilities } from "../runner-helpers";
|
|
11
|
+
import type { runtimeMetadata } from "../version";
|
|
12
|
+
import { logger } from "../logger";
|
|
13
|
+
import { providerTerminalSession, providerTerminalSocket } from "../process-meta";
|
|
14
|
+
import type { RunnerLaunchOptions } from "./runner-context";
|
|
15
|
+
|
|
16
|
+
// #2095 test rig — fallback window used to derive tokensUsed for the forced synthetic context when
|
|
17
|
+
// neither the process context nor the probe reports a tokensMax yet. The absolute value is immaterial
|
|
18
|
+
// (only utilization drives the server thresholds); 200k matches the common Claude window.
|
|
19
|
+
const FORCED_CONTEXT_DEFAULT_WINDOW_TOKENS = 200_000;
|
|
20
|
+
|
|
21
|
+
interface RegistrationDeps {
|
|
22
|
+
options: RunnerLaunchOptions;
|
|
23
|
+
agentId: string;
|
|
24
|
+
process(): ManagedProcess | undefined;
|
|
25
|
+
control(): ControlServer | undefined;
|
|
26
|
+
registeredAt(): number | undefined;
|
|
27
|
+
latestProbeMetrics(): ContextProbeMetrics | null;
|
|
28
|
+
sessionLog(message: string): void;
|
|
29
|
+
flushHeartbeat(): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class Registration {
|
|
33
|
+
// #2095 self-compact e2e test rig — the one-shot forced-context latch. `forceContextPct` is the
|
|
34
|
+
// synthetic utilization to report (undefined = feature off, production behavior unchanged);
|
|
35
|
+
// `forceContextArmed` is disarmed after the FIRST forced compaction/clear command this runner
|
|
36
|
+
// handles, so exactly one trigger→compact→resume cycle runs (no repeat-compaction loop).
|
|
37
|
+
private readonly forceContextPct = forceContextPctFromEnv();
|
|
38
|
+
private forceContextArmed = this.forceContextPct !== undefined;
|
|
39
|
+
|
|
40
|
+
constructor(private readonly deps: RegistrationDeps) {}
|
|
41
|
+
|
|
42
|
+
disarmForcedContext(type: string): void {
|
|
43
|
+
// #2095 test rig — one-shot latch: the FIRST forced compaction/clear this runner handles disarms
|
|
44
|
+
// the synthetic forced context, so the next heartbeat reports the real (post-clear, low)
|
|
45
|
+
// utilization and no second cycle is triggered. Equivalent to "once lastCompactedAt advances".
|
|
46
|
+
// No-op unless AGENT_RELAY_FORCE_CONTEXT_PCT armed the latch, so production is untouched.
|
|
47
|
+
if (this.forceContextArmed && (type === "agent.compact" || type === "agent.clearContext")) {
|
|
48
|
+
this.forceContextArmed = false;
|
|
49
|
+
this.deps.sessionLog("AGENT_RELAY_FORCE_CONTEXT_PCT one-shot latch disarmed by forced compaction/clear command");
|
|
50
|
+
// Flush a heartbeat NOW so the server's stored context drops to the real (sub-threshold) value
|
|
51
|
+
// immediately. Without this, the ~30s heartbeat cadence lets the stale forced utilization
|
|
52
|
+
// survive several 10s lifecycle ticks and fire a SECOND forced compaction — breaking the strict
|
|
53
|
+
// one-shot guarantee. applyForcedContext is already disarmed above, so this beat carries real context.
|
|
54
|
+
this.deps.flushHeartbeat();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// #2095 self-compact e2e test rig — synthesize a ContextState at the forced utilization while the
|
|
59
|
+
// one-shot latch is armed. lifecycleState is pinned to "working" (a "compacting"/"hibernating"
|
|
60
|
+
// state makes the server's handleContextPressure early-return, so the trigger would never fire),
|
|
61
|
+
// and source/confidence marked estimate/estimated so the origin of the synthetic value is honest.
|
|
62
|
+
// Everything downstream (advisory, forced clear-inject, clear-commit barrier, resume) is the REAL
|
|
63
|
+
// path — only the reported utilization is fabricated. No-op unless AGENT_RELAY_FORCE_CONTEXT_PCT set.
|
|
64
|
+
applyForcedContext(context: ContextState | undefined): ContextState | undefined {
|
|
65
|
+
if (!this.forceContextArmed || this.forceContextPct === undefined) return context;
|
|
66
|
+
const pct = this.forceContextPct;
|
|
67
|
+
const tokensMax = context?.tokensMax ?? this.deps.latestProbeMetrics()?.tokensMax ?? FORCED_CONTEXT_DEFAULT_WINDOW_TOKENS;
|
|
68
|
+
return {
|
|
69
|
+
warmTopics: context?.warmTopics ?? [],
|
|
70
|
+
tasksSinceCompact: context?.tasksSinceCompact ?? 0,
|
|
71
|
+
...(context?.lastCompactedAt !== undefined ? { lastCompactedAt: context.lastCompactedAt } : {}),
|
|
72
|
+
utilization: pct,
|
|
73
|
+
tokensMax,
|
|
74
|
+
tokensUsed: Math.round(pct * tokensMax),
|
|
75
|
+
lifecycleState: "working",
|
|
76
|
+
source: "estimate",
|
|
77
|
+
confidence: "estimated",
|
|
78
|
+
lastUpdatedAt: Date.now(),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
writeRunnerInfoFile(): void {
|
|
83
|
+
const file = runnerInfoFileFromEnv();
|
|
84
|
+
const control = this.deps.control();
|
|
85
|
+
if (!file || !control) return;
|
|
86
|
+
try {
|
|
87
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
88
|
+
writeFileSync(file, JSON.stringify({
|
|
89
|
+
agentId: this.deps.agentId,
|
|
90
|
+
runnerId: this.deps.options.runnerId,
|
|
91
|
+
provider: this.deps.options.provider,
|
|
92
|
+
controlUrl: control.url,
|
|
93
|
+
runtimeTokenFile: runtimeTokenFileFromEnv(),
|
|
94
|
+
pid: process.pid,
|
|
95
|
+
...(providerTerminalSession(this.deps.process()) ? { tmuxSession: providerTerminalSession(this.deps.process()) } : {}),
|
|
96
|
+
...(providerTerminalSocket(this.deps.process()) ? { tmuxSocket: providerTerminalSocket(this.deps.process()) } : {}),
|
|
97
|
+
startedAt: this.deps.options.startedAt,
|
|
98
|
+
...(this.deps.registeredAt() ? { registeredAt: this.deps.registeredAt() } : {}),
|
|
99
|
+
}, null, 2) + "\n", { mode: 0o600 });
|
|
100
|
+
} catch (error) {
|
|
101
|
+
logger.error("runner", `failed to write runner info file: ${error}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface IdentitySnapshot {
|
|
107
|
+
tmuxSession?: string;
|
|
108
|
+
tmuxSocket?: string;
|
|
109
|
+
auth?: { profileId?: string; jti?: string; expiresAt?: number };
|
|
110
|
+
runtime?: ReturnType<typeof runtimeMetadata>;
|
|
111
|
+
agentId?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Preserve the registration, status and heartbeat merge-patch key sets. */
|
|
115
|
+
export function runnerIdentityMeta(options: RunnerLaunchOptions, snapshot: IdentitySnapshot, surface: "registration" | "status" | "heartbeat"): Record<string, unknown> {
|
|
116
|
+
const { tmuxSession, tmuxSocket } = snapshot;
|
|
117
|
+
if (surface === "heartbeat") return {
|
|
118
|
+
...(tmuxSession ? { tmuxSession } : {}),
|
|
119
|
+
...(tmuxSocket ? { tmuxSocket } : {}),
|
|
120
|
+
};
|
|
121
|
+
const identity = {
|
|
122
|
+
runnerId: options.runnerId,
|
|
123
|
+
startedAt: options.startedAt,
|
|
124
|
+
tmuxSession: tmuxSession ?? options.tmuxSession ?? null,
|
|
125
|
+
tmuxSocket: tmuxSocket ?? null,
|
|
126
|
+
policyName: options.policyName ?? null,
|
|
127
|
+
spawnRequestId: options.spawnRequestId ?? null,
|
|
128
|
+
taskId: options.taskId ?? null,
|
|
129
|
+
automationId: options.automationId ?? null,
|
|
130
|
+
automationRunId: options.automationRunId ?? null,
|
|
131
|
+
workspace: options.workspace ?? null,
|
|
132
|
+
lifecycle: options.lifecycle ?? "persistent",
|
|
133
|
+
workspaceMode: options.workspace?.requestedMode ?? options.workspace?.mode ?? workspaceModeFromEnv() ?? null,
|
|
134
|
+
profile: options.profile ?? null,
|
|
135
|
+
auth: {
|
|
136
|
+
profileId: snapshot.auth?.profileId ?? null,
|
|
137
|
+
jti: snapshot.auth?.jti ?? null,
|
|
138
|
+
expiresAt: snapshot.auth?.expiresAt ?? null,
|
|
139
|
+
rootTokenFallback: options.rootTokenFallback === true,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
if (surface === "status") return identity;
|
|
143
|
+
return {
|
|
144
|
+
...snapshot.runtime,
|
|
145
|
+
version: snapshot.runtime!.package.version,
|
|
146
|
+
provider: options.provider,
|
|
147
|
+
model: options.model ?? null,
|
|
148
|
+
...(isOpenAiApiModel(options.provider, options.model) ? { apiAuthMode: "apiKey", apiBackend: "openai", apiBudgetId: "openai-api", billing: "api" } : {}),
|
|
149
|
+
effort: options.effort ?? null,
|
|
150
|
+
// #557 — project the same assembled launch; the pure assembler ignores env/controlPort.
|
|
151
|
+
agentProfile: options.agentProfile ? appliedAgentProfileMetadata(options.provider, options.agentProfile, { ...options, ...options.launchConfig, agentId: snapshot.agentId, env: {}, controlPort: 0 } as RunnerSpawnConfig, options.providerConfig) : null,
|
|
152
|
+
...identity,
|
|
153
|
+
runnerManaged: true,
|
|
154
|
+
harnessSubagents: harnessSubagentsEnabled(options),
|
|
155
|
+
cwd: options.cwd,
|
|
156
|
+
approvalMode: options.approvalMode,
|
|
157
|
+
launchMode: options.launchMode ?? null,
|
|
158
|
+
toolchain: options.toolchain ?? null,
|
|
159
|
+
label: options.label ?? null,
|
|
160
|
+
lifecycleCapabilities: lifecycleCapabilities(),
|
|
161
|
+
providerCapabilities: runtimeProviderCapabilities(options),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { resolveEnvLayers, selectProviderConfigEnv } from "@humwick/sdk/child-env";
|
|
2
|
+
import { runnerMcpGeneration } from "../relay-mcp";
|
|
3
|
+
import { hostSourcesFor } from "../host-asset-plan";
|
|
4
|
+
import type { AgentLifecycle, AgentProfile, LaunchMode, ProjectToolchain, WorkspaceMetadata } from "@humwick/sdk";
|
|
5
|
+
import { errMessage, HumwickBusClient, HumwickHttpClient, type BusClientOptions, type HttpClientOptions } from "@humwick/sdk";
|
|
6
|
+
import type { ProviderAdapter, ProviderConfig, RunnerSpawnConfig } from "../adapter";
|
|
7
|
+
import type { RunnerRelayInjectionEvent } from "../relay-injection-events";
|
|
8
|
+
import type { RunnerTimelineEvent } from "../turn-reconcile-event";
|
|
9
|
+
import { logLevelFromEnv, sessionDebugEnabled } from "../config";
|
|
10
|
+
import { logger, parseLogLevel } from "../logger";
|
|
11
|
+
|
|
12
|
+
export interface RunnerOptions {
|
|
13
|
+
provider: string;
|
|
14
|
+
model?: string;
|
|
15
|
+
effort?: string;
|
|
16
|
+
/** #2595 — effective-catalog window from the spawn payload; threaded into the adapter config. */
|
|
17
|
+
contextWindowTokens?: number;
|
|
18
|
+
/** #2599 — the full server-resolved model record (protocol ≥ 2); threaded into the adapter config. */
|
|
19
|
+
modelRecord?: import("@humwick/sdk/provider-catalog").ResolvedModelRecord;
|
|
20
|
+
runnerId: string;
|
|
21
|
+
instanceId: string;
|
|
22
|
+
agentId?: string;
|
|
23
|
+
relayUrl: string;
|
|
24
|
+
token?: string;
|
|
25
|
+
tokenJti?: string;
|
|
26
|
+
tokenProfileId?: string;
|
|
27
|
+
tokenExpiresAt?: number;
|
|
28
|
+
rootTokenFallback?: boolean;
|
|
29
|
+
cwd: string;
|
|
30
|
+
headless: boolean;
|
|
31
|
+
approvalMode: string;
|
|
32
|
+
label?: string;
|
|
33
|
+
rig?: string;
|
|
34
|
+
profile?: string;
|
|
35
|
+
harnessSubagents?: boolean;
|
|
36
|
+
agentProfile?: AgentProfile;
|
|
37
|
+
prompt?: string;
|
|
38
|
+
systemPromptAppend?: string;
|
|
39
|
+
relayInjectionEvents?: RunnerRelayInjectionEvent[];
|
|
40
|
+
tags: string[];
|
|
41
|
+
capabilities: string[];
|
|
42
|
+
providerArgs: string[];
|
|
43
|
+
/** #2773 — the interactive|headless driver this process was launched with. */
|
|
44
|
+
launchMode?: LaunchMode;
|
|
45
|
+
toolchain?: ProjectToolchain;
|
|
46
|
+
policyName?: string;
|
|
47
|
+
spawnRequestId?: string;
|
|
48
|
+
taskId?: number;
|
|
49
|
+
automationId?: string; automationRunId?: string;
|
|
50
|
+
workspace?: WorkspaceMetadata; lifecycle?: AgentLifecycle;
|
|
51
|
+
tmuxSession?: string;
|
|
52
|
+
startedAt: number;
|
|
53
|
+
providerConfig: ProviderConfig;
|
|
54
|
+
/** Provider-owned snapshot loaded before registration for truthful synchronous projection. */
|
|
55
|
+
launchConfig?: Pick<RunnerSpawnConfig, "hostClaudeConfig" | "hostClaudeSettings">;
|
|
56
|
+
adapter: ProviderAdapter;
|
|
57
|
+
// #2443 — `reason`, when present, is a human-readable diagnosis of a terminal provider
|
|
58
|
+
// failure (e.g. "model-unavailable: APIError 404 ..."), passed through so the caller can print
|
|
59
|
+
// it before the process actually exits — see handleAdapterStatusChange's call site.
|
|
60
|
+
onProviderExit?: (code: number | null, reason?: string) => void;
|
|
61
|
+
exitProcessOnShutdown?: boolean;
|
|
62
|
+
ports?: { http?: (options: HttpClientOptions) => HumwickHttpClient; bus?: (options: BusClientOptions) => HumwickBusClient };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type RunnerLaunchOptions = Readonly<Omit<RunnerOptions, "token" | "tokenJti" | "tokenProfileId" | "tokenExpiresAt">>;
|
|
66
|
+
|
|
67
|
+
interface RunnerContextDeps {
|
|
68
|
+
providerSessionId(): string;
|
|
69
|
+
setTimelineEvent(event: RunnerTimelineEvent): void;
|
|
70
|
+
publishStatus(): void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class RunnerContext {
|
|
74
|
+
readonly options: RunnerLaunchOptions;
|
|
75
|
+
readonly agentId: string;
|
|
76
|
+
readonly mcpToolGeneration: ReturnType<typeof runnerMcpGeneration>;
|
|
77
|
+
http!: HumwickHttpClient;
|
|
78
|
+
bus!: HumwickBusClient;
|
|
79
|
+
private readonly sessionDebugVerbose = sessionDebugEnabled();
|
|
80
|
+
|
|
81
|
+
constructor(options: RunnerOptions, private readonly deps: RunnerContextDeps) {
|
|
82
|
+
const { token, tokenJti, tokenProfileId, tokenExpiresAt, ...launch } = options;
|
|
83
|
+
this.options = Object.freeze(launch);
|
|
84
|
+
this.mcpToolGeneration = runnerMcpGeneration(resolveEnvLayers([process.env, selectProviderConfigEnv(options.providerConfig.env, hostSourcesFor(options).settings)]));
|
|
85
|
+
this.agentId = options.agentId ?? options.runnerId;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
configureLogger(): void {
|
|
89
|
+
// Keep the verbose session alias; an explicit log level still wins.
|
|
90
|
+
logger.configure({
|
|
91
|
+
agentId: this.agentId,
|
|
92
|
+
headless: this.options.headless,
|
|
93
|
+
...(this.sessionDebugVerbose && !parseLogLevel(logLevelFromEnv()) ? { level: "debug" as const } : {}),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
initializeHttp(token: string | undefined): void {
|
|
98
|
+
// Enrolled over registration only; never part of launch options or provider config.
|
|
99
|
+
this.http = (this.options.ports?.http ?? ((opts) => new HumwickHttpClient(opts)))({ baseUrl: this.options.relayUrl, token });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
initializeBus(options: BusClientOptions): void {
|
|
103
|
+
this.bus = (this.options.ports?.bus ?? ((opts) => new HumwickBusClient(opts)))(options);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
publishRunnerTimelineEvent(event: RunnerTimelineEvent): void {
|
|
107
|
+
this.deps.setTimelineEvent({
|
|
108
|
+
...event,
|
|
109
|
+
metadata: {
|
|
110
|
+
source: "runner",
|
|
111
|
+
provider: this.options.provider,
|
|
112
|
+
runnerId: this.options.runnerId,
|
|
113
|
+
agentId: this.agentId,
|
|
114
|
+
policyName: this.options.policyName ?? null,
|
|
115
|
+
spawnRequestId: this.options.spawnRequestId ?? null,
|
|
116
|
+
label: this.options.label ?? null,
|
|
117
|
+
providerSessionId: this.deps.providerSessionId(),
|
|
118
|
+
...(event.metadata ?? {}),
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
this.deps.publishStatus();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// (#1617) — the reporting half of a guarded fire-and-forget call. Bun exits(1) on an unhandled
|
|
125
|
+
// rejection and the runner installs no `unhandledRejection` handler, so any `void this.foo()`
|
|
126
|
+
// whose target can reject takes the whole agent down — and nothing respawns it (#1618).
|
|
127
|
+
//
|
|
128
|
+
// Deliberately NOT a fire-and-forget wrapper: the `.catch` still has to be written out at every
|
|
129
|
+
// call site, so adding one stays a visible decision rather than something a helper hides. All
|
|
130
|
+
// this fixes is the SHAPE of the report — the same logger.error + publishRunnerTimelineEvent
|
|
131
|
+
// pair attemptInitialPromptDelivery and the #1614 obligation re-injection already use, so a
|
|
132
|
+
// background operation that dies is loud on the agent's own timeline instead of vanishing.
|
|
133
|
+
reportBackgroundRejection(scope: string, status: string, title: string, error: unknown): void {
|
|
134
|
+
const body = errMessage(error);
|
|
135
|
+
logger.error("runner", `${scope} failed: ${body}`);
|
|
136
|
+
this.publishRunnerTimelineEvent({ status, timestamp: Date.now(), title, body });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Runner operational diagnostics (HTTP liveness, token renewal failures). Routed
|
|
140
|
+
// through the leveled logger at warn — see logger.ts. Kept as a thin wrapper so
|
|
141
|
+
// the existing call sites and their `[runner]` framing stay put.
|
|
142
|
+
logRunnerDiagnostic(message: string): void {
|
|
143
|
+
logger.warn("runner", message.replace(/^\[runner\]\s*/, ""));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Session-mirror diagnostics → the leveled logger (component "mirror"), written
|
|
147
|
+
// to the dashboard-surfaced session-mirror-<agent>.log. Key transitions log at
|
|
148
|
+
// info; the single place to look when chat/terminal sync misbehaves.
|
|
149
|
+
sessionLog(message: string): void {
|
|
150
|
+
logger.info("mirror", message);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Verbose, high-frequency lines (per-probe, per-emit) — surfaced only at log
|
|
154
|
+
// level "debug" (AGENT_RELAY_LOG_LEVEL=debug, or flip live via /log-level).
|
|
155
|
+
sessionDebug(message: string): void {
|
|
156
|
+
logger.debug("mirror", message);
|
|
157
|
+
}
|
|
158
|
+
}
|