@humwick/runner 0.130.4 → 0.130.6
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 +661 -167
- package/agent-bin/humwick +2 -2
- package/agent-bin/humwick-browser +4 -0
- package/hooks/fff-search-redirect/fff-search-redirect.cjs +1 -1
- package/hooks/web-tools-redirect/web-tools-redirect.cjs +2 -2
- package/package.json +8 -11
- package/plugins/browser/skills/playwright-cli/SKILL.md +8 -6
- package/plugins/claude/.claude-plugin/plugin.json +3 -3
- package/plugins/claude/claude-statusline-probe.cjs +26 -16
- package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +47 -17
- package/plugins/claude/hooks/gate-run-guard.sh +3 -3
- package/plugins/claude/hooks/gate-run-guard.ts +5 -5
- package/plugins/claude/hooks/kill-guard.provisioned.mjs +1 -1
- package/plugins/claude/hooks/kill-guard.sh +4 -4
- package/plugins/claude/hooks/kill-guard.ts +2 -2
- package/plugins/claude/hooks/permission-request.sh +1 -1
- package/plugins/claude/hooks/post-compact.sh +1 -1
- package/plugins/claude/hooks/post-tool-use.sh +3 -3
- package/plugins/claude/hooks/pre-compact.sh +2 -2
- package/plugins/claude/hooks/read-only-bash-guard.sh +4 -4
- package/plugins/claude/hooks/relay-status.sh +32 -32
- package/plugins/claude/hooks/session-end.sh +2 -2
- package/plugins/claude/hooks/session-start.sh +3 -3
- package/plugins/claude/hooks/shell-trust.ts +1 -1
- package/plugins/claude/hooks/stop-failure.sh +2 -2
- package/plugins/claude/hooks/stop.sh +6 -6
- package/plugins/claude/hooks/subagent-skill-policy.cjs +8 -6
- package/plugins/claude/hooks/subagent-stop.sh +3 -3
- package/plugins/claude/hooks/user-prompt-submit.sh +1 -1
- package/plugins/claude/hooks/wait-loop-guard.ts +3 -3
- package/plugins/claude/monitors/monitors.json +2 -2
- package/plugins/claude/monitors/relay-monitor.provisioned.mjs +96 -62
- package/plugins/claude/monitors/relay-monitor.ts +5 -5
- package/plugins/claude/skills/label/SKILL.md +3 -3
- package/plugins/claude/skills/message/SKILL.md +5 -5
- package/plugins/claude/skills/react/SKILL.md +1 -1
- package/plugins/claude/skills/read-message/SKILL.md +4 -4
- package/plugins/claude/skills/reply/SKILL.md +5 -5
- package/plugins/claude/skills/send-claimable/SKILL.md +5 -5
- package/plugins/claude/skills/slice-first/SKILL.md +5 -3
- package/plugins/claude/skills/slice-first/references/plan-recipe.md +30 -29
- package/plugins/claude/skills/status/SKILL.md +3 -3
- package/plugins/claude/skills/tags/SKILL.md +5 -5
- package/plugins/claude/skills/task/SKILL.md +10 -10
- package/plugins/claude-output-style/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-output-style/output-styles/{agent-relay.md → humwick.md} +1 -1
- package/plugins/codex/skills/guide/SKILL.md +3 -3
- package/plugins/codex/skills/label/SKILL.md +3 -3
- package/plugins/codex/skills/message/SKILL.md +5 -5
- package/plugins/codex/skills/react/SKILL.md +1 -1
- package/plugins/codex/skills/read-message/SKILL.md +4 -4
- package/plugins/codex/skills/reply/SKILL.md +5 -5
- package/plugins/codex/skills/send-claimable/SKILL.md +5 -5
- package/plugins/codex/skills/slice-first/SKILL.md +5 -3
- package/plugins/codex/skills/slice-first/references/plan-recipe.md +30 -29
- package/plugins/codex/skills/status/SKILL.md +3 -3
- package/plugins/codex/skills/tags/SKILL.md +5 -5
- package/plugins/codex/skills/task/SKILL.md +11 -10
- package/src/adapter.ts +95 -92
- package/src/adapters/claude-api.ts +8 -11
- package/src/adapters/claude-context-command.ts +1 -1
- package/src/adapters/claude-credentials.ts +7 -3
- package/src/adapters/claude-delivery-lanes.ts +2 -2
- package/src/adapters/claude-delivery.ts +16 -16
- package/src/adapters/claude-effort.ts +2 -2
- package/src/adapters/claude-exit.ts +2 -2
- package/src/adapters/claude-headless-mirror.ts +18 -2
- package/src/adapters/claude-headless-refusal.ts +3 -3
- package/src/adapters/claude-headless.ts +11 -10
- package/src/adapters/claude-launch-exit.ts +1 -1
- package/src/adapters/claude-monitor-delivery-plan.ts +36 -16
- package/src/adapters/claude-permission.ts +5 -5
- package/src/adapters/claude-prompt-gates.ts +9 -9
- package/src/adapters/claude-prompt-hook.ts +2 -2
- package/src/adapters/claude-session-capture.ts +6 -6
- package/src/adapters/claude-session-probe.ts +25 -25
- package/src/adapters/claude-shutdown.ts +11 -11
- package/src/adapters/claude-status-detectors.ts +31 -31
- package/src/adapters/claude-subagents.ts +7 -7
- package/src/adapters/claude-submit-confirmation.ts +6 -6
- package/src/adapters/claude-tmux.ts +43 -43
- package/src/adapters/claude-token-usage-capture.ts +5 -5
- package/src/adapters/claude-tool-policy.ts +7 -7
- package/src/adapters/claude-transcript.ts +24 -24
- package/src/adapters/claude.ts +100 -100
- package/src/adapters/codex-agent-message-capture.ts +26 -63
- package/src/adapters/codex-app-server.ts +1 -1
- package/src/adapters/codex-client.ts +9 -9
- package/src/adapters/codex-effective-config.ts +1 -1
- package/src/adapters/codex-launch-config.ts +5 -5
- package/src/adapters/codex-rollout-context.ts +2 -2
- package/src/adapters/codex-skill-discovery.ts +1 -1
- package/src/adapters/codex-token-usage-capture.ts +16 -16
- package/src/adapters/codex-tool-output.ts +4 -4
- package/src/adapters/codex-version.ts +3 -3
- package/src/adapters/codex.ts +64 -64
- package/src/adapters/opencode-api.ts +1 -1
- package/src/adapters/opencode-context.ts +20 -22
- package/src/adapters/opencode-model-registry.ts +7 -7
- package/src/adapters/opencode-provider.ts +8 -8
- package/src/adapters/opencode-provisioning.ts +30 -0
- package/src/adapters/opencode-server-retry.ts +1 -1
- package/src/adapters/opencode-soft-steer.ts +5 -5
- package/src/adapters/opencode-spawn.ts +18 -14
- package/src/adapters/opencode-token-usage-capture.ts +12 -12
- package/src/adapters/opencode-tool-events.ts +15 -15
- package/src/adapters/opencode.ts +232 -232
- package/src/adapters/project-shell.ts +11 -11
- package/src/adapters/service-access.ts +6 -6
- package/src/adapters/tmux-offload.ts +9 -9
- package/src/adapters/tool-summary.ts +5 -5
- package/src/agent-redact.ts +1 -1
- package/src/agent-runner/commands/dispatch.ts +10 -10
- package/src/agent-runner/commands/lifecycle.ts +2 -2
- package/src/agent-runner/commands/prompt.ts +19 -19
- package/src/agent-runner/credentials.ts +5 -5
- package/src/agent-runner/inbox.ts +1 -1
- package/src/agent-runner/lifecycle-milestones.ts +3 -3
- package/src/agent-runner/lifecycle-phase.ts +2 -2
- package/src/agent-runner/liveness-exchange.ts +4 -10
- package/src/agent-runner/registration.ts +8 -8
- package/src/agent-runner/runner-context.ts +9 -14
- package/src/agent-runner/session-mirror.ts +12 -12
- package/src/agent-runner/task-claims.ts +2 -2
- package/src/agent-runner/token-usage-coverage.ts +1 -1
- package/src/agent-runner/turn-report.ts +12 -12
- package/src/api-budget-accounting.ts +3 -3
- package/src/area-memory.ts +6 -6
- package/src/artifact-path-materialize.ts +1 -1
- package/src/async-guard.ts +5 -5
- package/src/attachment-cache.ts +17 -22
- package/src/background-script-reconcile.ts +6 -6
- package/src/bounded-log.ts +1 -1
- package/src/busy-reconciler.ts +22 -22
- package/src/claim-tracker.ts +3 -3
- package/src/claude-settings.ts +8 -8
- package/src/claude-system-prompt.ts +1 -1
- package/src/claude-workspace-trust.ts +1 -1
- package/src/config.ts +62 -61
- package/src/context-boundary.ts +5 -5
- package/src/context-command.ts +3 -3
- package/src/control-server.ts +32 -32
- package/src/delivery-result.ts +2 -2
- package/src/effort-command.ts +2 -2
- package/src/empty-turn.ts +22 -14
- package/src/free-port.ts +1 -1
- package/src/gate-run-config.ts +3 -3
- package/src/host-asset-plan.ts +24 -17
- package/src/host-claude-config-worker.ts +2 -2
- package/src/host-sources.ts +2 -2
- package/src/index.ts +16 -16
- package/src/launch-assembly.ts +94 -96
- package/src/liveness.ts +15 -15
- package/src/logger.ts +3 -3
- package/src/mcp-outbox.ts +3 -3
- package/src/mcp-preflight.ts +2 -2
- package/src/mcp-source-selection.ts +2 -2
- package/src/mcp-tool-deferral.ts +1 -1
- package/src/mcp-vault-env.ts +2 -2
- package/src/message-body-config/config.ts +4 -4
- package/src/mid-turn-delivery.ts +2 -2
- package/src/native-memory-lever.ts +8 -8
- package/src/outbox.ts +15 -15
- package/src/pending-permission-store.ts +1 -1
- package/src/profile-home.ts +19 -19
- package/src/profile-projection.ts +26 -24
- package/src/project-instructions.ts +5 -5
- package/src/provider-auth-warning.ts +17 -0
- package/src/provider-double.ts +1 -1
- package/src/provider-hold.ts +3 -3
- package/src/provider-home-marker.ts +1 -1
- package/src/provider-home-tombstone.ts +1 -1
- package/src/provider-input-queue.ts +2 -2
- package/src/provider-integration.ts +15 -13
- package/src/provider-setup.ts +15 -3
- package/src/providers.ts +1 -1
- package/src/provisioning.ts +37 -40
- package/src/question-decision.ts +2 -2
- package/src/quota.ts +0 -6
- package/src/rate-limit.ts +24 -13
- package/src/refused-prompt-store.ts +2 -2
- package/src/relay-injection-events.ts +2 -2
- package/src/relay-instructions.ts +5 -5
- package/src/relay-mcp-proxy.long-turn-e2e.ts +13 -13
- package/src/relay-mcp-proxy.ts +57 -58
- package/src/relay-mcp.ts +55 -69
- package/src/reply-obligation-cache.ts +9 -9
- package/src/response-capture-report.ts +9 -9
- package/src/response-routing.ts +1 -1
- package/src/runner-core.ts +209 -222
- package/src/runner-exit.ts +1 -1
- package/src/runner-helpers.ts +10 -10
- package/src/runner-insights.ts +4 -2
- package/src/runner-mcp-proxy.ts +3 -3
- package/src/runner-spawn-config.ts +3 -3
- package/src/runtime-token-file.ts +1 -1
- package/src/runtime-token-remint.ts +1 -1
- package/src/service-access.ts +2 -2
- package/src/session-destroy.ts +1 -1
- package/src/session-insights.ts +1 -1
- package/src/session-scratch.ts +24 -37
- package/src/spawn-input.ts +6 -6
- package/src/startup-config-fetch.ts +4 -4
- package/src/subreaper.ts +3 -5
- package/src/test-run-dedup.ts +13 -13
- package/src/transcript-context.ts +2 -2
- package/src/transport-errors.ts +1 -1
- package/src/turn-opener.ts +1 -1
- package/src/turn-reconcile-event.ts +2 -2
- package/src/vault-proxy-hop.ts +2 -2
- package/src/web-tools.ts +2 -2
- package/agent-bin/agent-relay-browser +0 -10
- package/plugins/claude/skills/disconnect/SKILL.md +0 -16
- package/plugins/claude/skills/pair/SKILL.md +0 -30
- package/plugins/codex/skills/disconnect/SKILL.md +0 -15
- package/plugins/codex/skills/pair/SKILL.md +0 -29
- package/src/adapters/provider-quota-harvest.ts +0 -76
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { relayEnv, writeChildEnv } from "@humwick/sdk/env";
|
|
2
|
-
//
|
|
2
|
+
// the orchestrator hands the runner the project's toolchain PATH plus shell hooks that re-lead
|
|
3
3
|
// it after the user's shell init. The provider process keeps the host PATH; only its shell tool re-leads.
|
|
4
|
-
// The same hooks keep the agent's private TMPDIR when a login profile resets it
|
|
5
|
-
// A hook the user already configured is chained behind ours, never dropped; an inherited Relay hook never is
|
|
4
|
+
// The same hooks keep the agent's private TMPDIR when a login profile resets it.
|
|
5
|
+
// A hook the user already configured is chained behind ours, never dropped; an inherited Relay hook never is.
|
|
6
6
|
import { resolve } from "node:path";
|
|
7
7
|
import { browserToolsBinDir, isRecord, PROJECT_ENV_FILE_ENV, PROJECT_PATH_ENV, PROJECT_ZDOTDIR_ENV, RELAY_BIN_PATH_ENV } from "@humwick/sdk";
|
|
8
8
|
import { AGENT_TMPDIR_ENV } from "@humwick/sdk/host-tmpdir";
|
|
@@ -14,13 +14,13 @@ import { providerSourceHome } from "../profile-home";
|
|
|
14
14
|
import { readCodexConfig } from "./codex-effective-config";
|
|
15
15
|
import { serviceAccessEnvKeys } from "./service-access";
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* it (and
|
|
17
|
+
/** every launch, orchestrator-spawned or direct: the agent's private TMPDIR plus the hooks that keep
|
|
18
|
+
* it (and the toolchain PATH) after shell init. */
|
|
19
19
|
export async function agentShellLaunchEnv(agentId: string, operatorEnv?: Record<string, string>): Promise<Record<string, string>> {
|
|
20
|
-
return writeChildEnv({}, { ...await providerScratchEnv(agentId, undefined, operatorEnv), [PROJECT_ENV_FILE_ENV]: PROJECT_ENV_SCRIPT, [PROJECT_ZDOTDIR_ENV]: PROJECT_ZDOTDIR, [RELAY_BIN_PATH_ENV]: relayBinPath() });
|
|
20
|
+
return writeChildEnv({}, { ...await providerScratchEnv(agentId, undefined, operatorEnv), [PROJECT_ENV_FILE_ENV]: PROJECT_ENV_SCRIPT, [PROJECT_ZDOTDIR_ENV]: PROJECT_ZDOTDIR, [RELAY_BIN_PATH_ENV]: relayBinPath() }, []);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
/**
|
|
23
|
+
/** this runner's own `humwick-browser` and the host's provisioned `playwright-cli`: a login shell's
|
|
24
24
|
* PATH (an older global install) can never pick which Relay release or browser tools an agent runs. */
|
|
25
25
|
export function relayBinPath(): string {
|
|
26
26
|
return [resolve(import.meta.dir, "../../agent-bin"), browserToolsBinDir(humwickHome())].join(":");
|
|
@@ -40,7 +40,7 @@ export function codexProjectShellConfigArgs(env: Record<string, string>, configA
|
|
|
40
40
|
const path = relayEnv(env, PROJECT_PATH_ENV);
|
|
41
41
|
if (!shellHookNeeded(env)) return [];
|
|
42
42
|
const configured = codexShellPolicySet(configArgs, codexHome || providerSourceHome("codex"));
|
|
43
|
-
if (relayEnv(env, "
|
|
43
|
+
if (relayEnv(env, "HUMWICK_SERVICE_ACCESS_ENV_FILE")) {
|
|
44
44
|
for (const key of serviceAccessEnvKeys(env)) if (relayEnv(configured, key) !== undefined && relayEnv(configured, key) !== relayEnv(env, key)) throw new Error("vault_env_conflict:shell:" + key);
|
|
45
45
|
const policy = codexShellPolicy(configArgs, codexHome || providerSourceHome("codex"));
|
|
46
46
|
if ((Array.isArray(policy.exclude) && policy.exclude.length) || (Array.isArray(policy.include_only) && policy.include_only.length) || (policy.inherit && policy.inherit !== "all")) throw new Error("vault_env_conflict:shell_environment_policy");
|
|
@@ -50,9 +50,9 @@ export function codexProjectShellConfigArgs(env: Record<string, string>, configA
|
|
|
50
50
|
[PROJECT_PATH_ENV]: path,
|
|
51
51
|
[RELAY_BIN_PATH_ENV]: relayEnv(env, RELAY_BIN_PATH_ENV),
|
|
52
52
|
...composeShellHooks(user, { BASH_ENV: relayEnv(env, PROJECT_ENV_FILE_ENV), ZDOTDIR: relayEnv(env, PROJECT_ZDOTDIR_ENV) }),
|
|
53
|
-
});
|
|
53
|
+
}, []);
|
|
54
54
|
// An empty chain variable is set too: it clears a stale one the app-server env would hand down.
|
|
55
|
-
return [...(relayEnv(env, "
|
|
55
|
+
return [...(relayEnv(env, "HUMWICK_SERVICE_ACCESS_ENV_FILE") ? ["-c", 'shell_environment_policy.inherit="all"', "-c", "shell_environment_policy.ignore_default_excludes=true"] : []),
|
|
56
56
|
...Object.entries(set).flatMap(([key, value]) => value === undefined ? [] : ["-c", `shell_environment_policy.set.${key}=${tomlString(value)}`])];
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -64,7 +64,7 @@ export function openCodeProjectShellEnv(env: Record<string, string>): Record<str
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
function shellHookNeeded(env: Record<string, string>): boolean {
|
|
67
|
-
return Boolean(relayEnv(env, PROJECT_PATH_ENV) || relayEnv(env, AGENT_TMPDIR_ENV) || relayEnv(env, "
|
|
67
|
+
return Boolean(relayEnv(env, PROJECT_PATH_ENV) || relayEnv(env, AGENT_TMPDIR_ENV) || relayEnv(env, "HUMWICK_SERVICE_ACCESS_ENV_FILE"));
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/** Codex's own `shell_environment_policy.set` without us: CODEX_HOME/config.toml, then `-c` overrides in order. */
|
|
@@ -12,7 +12,7 @@ import { extractCallerSettings } from "../claude-settings";
|
|
|
12
12
|
import { codexConfigOverrides, isRecord } from "@humwick/sdk";
|
|
13
13
|
import { projectSettingSources, providerHomePathFor, providerSourceHome } from "../profile-home";
|
|
14
14
|
|
|
15
|
-
export const SERVICE_ACCESS_ENV_FILE = "
|
|
15
|
+
export const SERVICE_ACCESS_ENV_FILE = "HUMWICK_SERVICE_ACCESS_ENV_FILE";
|
|
16
16
|
const PROXY_CA_ENV_KEYS = ["SSL_CERT_FILE", "CURL_CA_BUNDLE", "GIT_SSL_CAINFO", "NODE_EXTRA_CA_CERTS", "REQUESTS_CA_BUNDLE"] as const;
|
|
17
17
|
export const PROXY_TRUST_ENV_KEYS = ["HTTPS_PROXY", "HTTP_PROXY", "https_proxy", "http_proxy", "NO_PROXY", "no_proxy", "NODE_USE_ENV_PROXY", ...PROXY_CA_ENV_KEYS] as const;
|
|
18
18
|
|
|
@@ -42,7 +42,7 @@ export function prepareServiceAccessConfig(config: RunnerSpawnConfig): RunnerSpa
|
|
|
42
42
|
if (!cert.ca || cert.fingerprint256.replaceAll(":", "").toLowerCase() !== access.caFingerprint || Date.parse(cert.validTo) <= Date.now()) fail("vault_ca_mismatch");
|
|
43
43
|
} catch { fail("vault_ca_mismatch"); }
|
|
44
44
|
if (access.expiresAt <= Date.now()) fail("vault_token_expired");
|
|
45
|
-
const inherited = writeChildEnv({}, { ...config.env, ...config.providerConfig.env });
|
|
45
|
+
const inherited = writeChildEnv({}, { ...config.env, ...config.providerConfig.env }, []);
|
|
46
46
|
if (inherited.NODE_TLS_REJECT_UNAUTHORIZED === "0" || inherited.GIT_SSL_NO_VERIFY === "true") fail("vault_tls_verification_disabled");
|
|
47
47
|
// Ambient routing cannot silently bypass a brokered host, even through ALL_PROXY.
|
|
48
48
|
for (const key of ["HTTPS_PROXY", "HTTP_PROXY", "https_proxy", "http_proxy", "ALL_PROXY", "all_proxy", "NO_PROXY", "no_proxy"]) {
|
|
@@ -82,9 +82,9 @@ export function prepareServiceAccessConfig(config: RunnerSpawnConfig): RunnerSpa
|
|
|
82
82
|
...dummy, HTTPS_PROXY: proxy.href, HTTP_PROXY: proxy.href, https_proxy: proxy.href, http_proxy: proxy.href,
|
|
83
83
|
NO_PROXY: [...bypass].join(","), no_proxy: [...bypass].join(","),
|
|
84
84
|
...Object.fromEntries(PROXY_CA_ENV_KEYS.map((key) => [key, bundle])), NODE_USE_ENV_PROXY: "1", [SERVICE_ACCESS_ENV_FILE]: script,
|
|
85
|
-
});
|
|
85
|
+
}, []);
|
|
86
86
|
writeFileSync(script, Object.entries(env).map(([key, value]) => `export ${key}=${shellQuote(value)}`).join("\n") + "\n", { mode: 0o600 });
|
|
87
|
-
return { ...config, providerConfig: { ...config.providerConfig, env: writeChildEnv({}, { ...config.providerConfig.env, ...env }) }, serviceAccessEnv: env, env: writeChildEnv({}, { ...config.env, ...env, [PROJECT_ENV_FILE_ENV]: PROJECT_ENV_SCRIPT, [PROJECT_ZDOTDIR_ENV]: PROJECT_ZDOTDIR }) };
|
|
87
|
+
return { ...config, providerConfig: { ...config.providerConfig, env: writeChildEnv({}, { ...config.providerConfig.env, ...env }, []) }, serviceAccessEnv: env, env: writeChildEnv({}, { ...config.env, ...env, [PROJECT_ENV_FILE_ENV]: PROJECT_ENV_SCRIPT, [PROJECT_ZDOTDIR_ENV]: PROJECT_ZDOTDIR }, []) };
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/** References carry the child's env into native MCP config without writing the proxy token. */
|
|
@@ -107,11 +107,11 @@ export function serviceAccessMcpServers(config: RunnerSpawnConfig, servers: Reco
|
|
|
107
107
|
if (value !== undefined && value !== `\${${key}}` && value !== config.serviceAccessEnv![key]) fail("vault_env_conflict:mcp:" + name + ":" + key);
|
|
108
108
|
}
|
|
109
109
|
if (config.provider === "codex") {
|
|
110
|
-
const env = writeChildEnv({}, { ...server.env }), configEnv = writeChildEnv({}, { ...server.configEnv });
|
|
110
|
+
const env = writeChildEnv({}, { ...server.env }, []), configEnv = writeChildEnv({}, { ...server.configEnv }, []);
|
|
111
111
|
for (const key of keys) { writeChildEnv(env, {}, [key]); writeChildEnv(configEnv, {}, [key]); }
|
|
112
112
|
return [name, { ...server, env, configEnv, env_vars: [...new Set([...(Array.isArray(server.env_vars) ? server.env_vars : Object.keys(server.env_vars ?? {})), ...keys])] }];
|
|
113
113
|
}
|
|
114
|
-
return [name, { ...server, env: writeChildEnv({}, { ...server.env, ...Object.fromEntries(keys.map((key) => [key, `\${${key}}`])) }) }];
|
|
114
|
+
return [name, { ...server, env: writeChildEnv({}, { ...server.env, ...Object.fromEntries(keys.map((key) => [key, `\${${key}}`])) }, []) }];
|
|
115
115
|
}));
|
|
116
116
|
}
|
|
117
117
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Off-loop tmux subprocess transport (
|
|
2
|
-
// adoption of the
|
|
1
|
+
// Off-loop tmux subprocess transport (Wave 2, rows 12/14/15/16 — first real
|
|
2
|
+
// adoption of the offload primitive).
|
|
3
3
|
//
|
|
4
4
|
// Every tmux subprocess the runner spawns from a HOT PATH — the 2s pane watcher, the 4s
|
|
5
5
|
// busy reconciler, the 1.5s monitorless turn watch, and the per-payload input transport —
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// `Bun.spawn`, bounded by a hard deadline (SIGTERM → SIGKILL of the whole process group
|
|
8
8
|
// after a grace window), instead of a synchronous `Bun.spawnSync` that pins the runner's
|
|
9
9
|
// single event loop for the entire 10s deadline while a wedged tmux socket hangs (the
|
|
10
|
-
//
|
|
10
|
+
// class; findings 1/6/7/10). A blocked tmux call here would otherwise freeze
|
|
11
11
|
// this agent's bus connection, control server, heartbeat, and every timer at once.
|
|
12
12
|
//
|
|
13
13
|
// AT-MOST-ONCE — a property of the backend, not a per-call flag. The offload envelope's
|
|
@@ -45,7 +45,7 @@ export async function destroyTmuxRunner(): Promise<void> {
|
|
|
45
45
|
if (current) await current.destroy();
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
/** Per-call bounding for a read that sits inside a caller's OWN deadline loop (
|
|
48
|
+
/** Per-call bounding for a read that sits inside a caller's OWN deadline loop (round-3
|
|
49
49
|
* MED): pass the REMAINING outer budget as `timeoutMs` so the subprocess deadline shrinks with
|
|
50
50
|
* the loop, and/or a `signal` the caller aborts at its absolute deadline. Without this each run
|
|
51
51
|
* would get the runner's full ~10s default and blow the caller's (e.g. 500ms) deadline. */
|
|
@@ -75,7 +75,7 @@ export const TMUX_CAPTURE_SCROLLBACK_LINES = 80;
|
|
|
75
75
|
/**
|
|
76
76
|
* The `tmux capture-pane` argv, with the scrollback window as the single explicit knob.
|
|
77
77
|
* `scrollbackLines` omitted/0 ⇒ the VISIBLE SCREEN ONLY (no `-S`). Exported so the two capture
|
|
78
|
-
* scopes are assertable in a unit test — the
|
|
78
|
+
* scopes are assertable in a unit test — the regression was exactly a caller reading the
|
|
79
79
|
* wrong one.
|
|
80
80
|
*/
|
|
81
81
|
export function tmuxCapturePaneArgv(sessionName: string, socketName?: string, scrollbackLines = 0, styled = false): string[] {
|
|
@@ -88,7 +88,7 @@ export function tmuxCapturePaneArgv(sessionName: string, socketName?: string, sc
|
|
|
88
88
|
/**
|
|
89
89
|
* Off-loop `tmux capture-pane` of the VISIBLE SCREEN ONLY — no `-S` scrollback window.
|
|
90
90
|
*
|
|
91
|
-
*
|
|
91
|
+
* the difference matters for anything that reads a repaint as evidence. Claude Code's
|
|
92
92
|
* `/clear` repaints only the visible screen (`ESC[H` + a per-line `ESC[2K`; it never emits the
|
|
93
93
|
* `ESC[3J` that would drop tmux's history), so the pre-clear conversation SURVIVES verbatim in
|
|
94
94
|
* scrollback. A `-S -80` capture therefore still contains the wiped conversation long after the
|
|
@@ -98,7 +98,7 @@ export function tmuxCapturePaneArgv(sessionName: string, socketName?: string, sc
|
|
|
98
98
|
*/
|
|
99
99
|
export async function captureTmuxVisiblePaneAsync(sessionName: string, socketName?: string, opts: TmuxReadOpts & { styled?: boolean } = {}): Promise<string> {
|
|
100
100
|
const { styled, ...readOpts } = opts;
|
|
101
|
-
// `styled` keeps SGR escapes (`-e`) for readers that must tell dim placeholder text from input
|
|
101
|
+
// `styled` keeps SGR escapes (`-e`) for readers that must tell dim placeholder text from input.
|
|
102
102
|
const result = await runTmux("tmux-capture-pane", tmuxCapturePaneArgv(sessionName, socketName, 0, styled), readOpts);
|
|
103
103
|
return result.stdout;
|
|
104
104
|
}
|
|
@@ -107,7 +107,7 @@ export async function captureTmuxVisiblePaneAsync(sessionName: string, socketNam
|
|
|
107
107
|
export type TmuxSessionPresence = "present" | "absent" | "unreadable";
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
|
-
* Off-loop `tmux has-session` (read-only, idempotent), as a TRI-STATE
|
|
110
|
+
* Off-loop `tmux has-session` (read-only, idempotent), as a TRI-STATE.
|
|
111
111
|
*
|
|
112
112
|
* A boolean `present`/`!present` collapses "the session is gone" together with "I could not
|
|
113
113
|
* find out", so a wedged-but-LIVE tmux server (has-session SIGKILLed at its deadline) would
|
|
@@ -162,7 +162,7 @@ export async function runTmuxInput(taskName: string, argv: string[], signal?: Ab
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
/**
|
|
165
|
-
* Off-loop `tmux load-buffer -b <name> -`, streaming the payload on STDIN
|
|
165
|
+
* Off-loop `tmux load-buffer -b <name> -`, streaming the payload on STDIN.
|
|
166
166
|
*
|
|
167
167
|
* This replaces the old `set-buffer -b <name> <text>` whose payload rode in argv — an
|
|
168
168
|
* ARG_MAX exposure for a large delivered message (up to the platform's arg limit, beyond
|
|
@@ -45,7 +45,7 @@ export function oneLineText(value: unknown): string {
|
|
|
45
45
|
* in the chat mirror / a report-up. This is NOT a proof that no secret can ever leak — a one-line
|
|
46
46
|
* free-text summary of arbitrary tool input cannot make that guarantee — so it layers three
|
|
47
47
|
* conservative, structural nets and errs toward over-redaction (a real secret surfacing is always
|
|
48
|
-
* the worse outcome
|
|
48
|
+
* the worse outcome):
|
|
49
49
|
* 1. positional shapes — URL basic-auth, `Authorization: Bearer/Basic …`;
|
|
50
50
|
* 2. known provider key prefixes — OpenRouter/Anthropic/OpenAI `sk-…`, GitHub `gh?_`, Slack
|
|
51
51
|
* `xox…`, AWS `AKIA…`;
|
|
@@ -142,7 +142,7 @@ export function summarizeToolInput(input: Record<string, unknown> | undefined, e
|
|
|
142
142
|
return "";
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
//
|
|
145
|
+
// the structured-params sibling of the one-line summary above: the FULL tool-call
|
|
146
146
|
// input/arguments, redacted then size-capped for the dashboard trace's expandable detail (the
|
|
147
147
|
// one-line `body` stays a short label; this is the "show me everything" view). Own budget, own
|
|
148
148
|
// key list — deliberately separate constants from the one-line summary's so tuning one never
|
|
@@ -170,7 +170,7 @@ function redactParamsValue(value: unknown, depth: number): unknown {
|
|
|
170
170
|
|
|
171
171
|
/**
|
|
172
172
|
* Redact then size-cap a tool call's raw input/arguments for the dashboard trace's expandable
|
|
173
|
-
* detail
|
|
173
|
+
* detail. Redaction runs FIRST, over the whole structured value — every string passes
|
|
174
174
|
* through the shared `redactSecrets` net, and any object key matching
|
|
175
175
|
* `/token|secret|password|key|authorization/i` is replaced wholesale — so a later byte-budget cut
|
|
176
176
|
* can never land mid-secret. Only THEN is the redacted value serialized and size-capped: within
|
|
@@ -193,7 +193,7 @@ export function redactAndCapParams(input: unknown, maxBytes: number = PARAMS_DEF
|
|
|
193
193
|
return `${truncated}${PARAMS_TRUNCATION_MARKER}`;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
/**
|
|
196
|
+
/** the absolute file paths among a tool's raw input values: a relative path resolves
|
|
197
197
|
* against `cwd` when one is known and is dropped otherwise. Undefined when none remain. */
|
|
198
198
|
export function absoluteToolPaths(candidates: readonly unknown[], cwd?: string): string[] | undefined {
|
|
199
199
|
const paths: string[] = [];
|
|
@@ -205,7 +205,7 @@ export function absoluteToolPaths(candidates: readonly unknown[], cwd?: string):
|
|
|
205
205
|
return paths.length ? paths : undefined;
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
/**
|
|
208
|
+
/** the files a Codex shell command reads: Codex parses each read into `commandActions`
|
|
209
209
|
* ({ type: "read", path }), so a read touches its file's area just as an edit does. */
|
|
210
210
|
export function codexReadPaths(item: Record<string, unknown>, cwd?: string): string[] | undefined {
|
|
211
211
|
const actions = Array.isArray(item.commandActions) ? item.commandActions.filter(isRecord) : [];
|
package/src/agent-redact.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { AGENT_REDACT_FILES, AGENT_REDACT_VERSION, agentRedactInstallDir, agentR
|
|
|
4
4
|
import { humwickHome, redactionEnabledFromEnv } from "./config";
|
|
5
5
|
import { parseHookSet } from "./project-hooks";
|
|
6
6
|
|
|
7
|
-
//
|
|
7
|
+
// agent-redact post-tool hooks for every runner-managed launch. The orchestrator installs
|
|
8
8
|
// the pinned binary and the hook entries its `install` command writes (agent-redact-provisioning.ts).
|
|
9
9
|
// A host without them launches agents without redaction and says so: an agent must always run.
|
|
10
10
|
|
|
@@ -41,9 +41,9 @@ interface CommandDispatchDeps {
|
|
|
41
41
|
deleteAgent(): Promise<unknown>;
|
|
42
42
|
stop(): Promise<void>;
|
|
43
43
|
updateCommand(commandId: string, input: { status: string; result?: Record<string, unknown>; error?: string }): Promise<unknown>;
|
|
44
|
-
/** `GET /api/commands/:id` — the full record behind a bounded bus event
|
|
44
|
+
/** `GET /api/commands/:id` — the full record behind a bounded bus event. */
|
|
45
45
|
fetchCommand(commandId: string): Promise<Response>;
|
|
46
|
-
/**
|
|
46
|
+
/** the command's destructive input reached the provider and no reset has settled it. */
|
|
47
47
|
destructiveInputUnsettled(commandId: string): boolean;
|
|
48
48
|
checkAcknowledgement(ack: unknown, status: string): void;
|
|
49
49
|
}
|
|
@@ -52,7 +52,7 @@ export class CommandDispatch {
|
|
|
52
52
|
constructor(private readonly deps: CommandDispatchDeps) {}
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
* The bus dispatch seam
|
|
55
|
+
* The bus dispatch seam.
|
|
56
56
|
*
|
|
57
57
|
* Commands that put a payload into the provider's input channel take their place in the
|
|
58
58
|
* `providerInput` queue HERE, synchronously, before the first `await` — so the order the relay
|
|
@@ -72,7 +72,7 @@ export class CommandDispatch {
|
|
|
72
72
|
const target = typeof command?.target === "string" ? command.target : this.deps.agentId;
|
|
73
73
|
if (target !== this.deps.agentId && target !== this.deps.options.runnerId) return;
|
|
74
74
|
if (type !== "agent.shutdown" && type !== "agent.restart" && type !== "agent.reconnect" && type !== "agent.kill" && type !== "agent.compact" && type !== "agent.clearContext" && type !== "agent.injectContext" && type !== "agent.permissionDecision" && type !== "agent.interrupt" && type !== "agent.releaseSubmitHold" && type !== "prompt.inject") return;
|
|
75
|
-
//
|
|
75
|
+
// a bus event carries params whole only up to a size budget; read the rest by id.
|
|
76
76
|
if (command?.paramsOmitted === true) {
|
|
77
77
|
try { params = await this.loadCommandParams(commandId); } catch (error) {
|
|
78
78
|
await this.updateCommand(commandId, "failed", undefined, `command params unavailable: ${errMessage(error)}`).catch(() => {});
|
|
@@ -84,7 +84,7 @@ export class CommandDispatch {
|
|
|
84
84
|
|
|
85
85
|
const exitAfterCommand = type === "agent.shutdown" || type === "agent.kill";
|
|
86
86
|
if (exitAfterCommand) this.deps.beginExitCommand();
|
|
87
|
-
//
|
|
87
|
+
// the confirmed terminal outcome of an exit command's provider teardown. Stays
|
|
88
88
|
// undefined if shutdownProvider never ran or threw; only a positive `killed` authorizes
|
|
89
89
|
// deregistration (a roster removal must imply the process is gone).
|
|
90
90
|
let killOutcome: ShutdownOutcome | undefined;
|
|
@@ -94,8 +94,8 @@ export class CommandDispatch {
|
|
|
94
94
|
try {
|
|
95
95
|
await this.deps.runNativeSelfResume(nativeSelfResume, () => this.updateCommand(commandId, "accepted"));
|
|
96
96
|
await this.deps.runNativeSelfResume(nativeSelfResume, () => this.updateCommand(commandId, "running"));
|
|
97
|
-
// Pre-session-destroy seam
|
|
98
|
-
// work
|
|
97
|
+
// Pre-session-destroy seam: for destructive transitions, run end-of-session
|
|
98
|
+
// work BEFORE the invasive operation, surfaced as a
|
|
99
99
|
// non-addressable "finalizing" state so the agent isn't mistaken for merely busy.
|
|
100
100
|
const destroyReason = boundaryReasonForCommand(type);
|
|
101
101
|
if (destroyReason) await this.deps.runNativeSelfResume(nativeSelfResume, () => this.deps.runPreSessionDestroy(destroyReason));
|
|
@@ -138,8 +138,8 @@ export class CommandDispatch {
|
|
|
138
138
|
if (nativeSelfResume) this.deps.rejectNativeSelfResume(nativeSelfResume, error instanceof Error ? error : new Error(errMessage(error)));
|
|
139
139
|
this.deps.finishCommandClaim(commandId);
|
|
140
140
|
this.deps.publishStatus();
|
|
141
|
-
// An unsettled destructive input can still reset the context: Relay replaces the process
|
|
142
|
-
// One that never reached the provider says so: nothing can still apply, and Relay closes its obligation
|
|
141
|
+
// An unsettled destructive input can still reset the context: Relay replaces the process.
|
|
142
|
+
// One that never reached the provider says so: nothing can still apply, and Relay closes its obligation.
|
|
143
143
|
const unsettled = this.deps.destructiveInputUnsettled(commandId) ? { destructiveInputUnsettled: true }
|
|
144
144
|
: error instanceof ContextSubmitAborted ? { destructiveInputSubmitted: false } : undefined;
|
|
145
145
|
await this.updateCommand(commandId, "failed", unsettled, errMessage(error)).catch(() => {});
|
|
@@ -150,7 +150,7 @@ export class CommandDispatch {
|
|
|
150
150
|
if (params.preserveRegistration === true) {
|
|
151
151
|
await this.deps.setOffline().catch(() => {});
|
|
152
152
|
} else if (killOutcome === "killed") {
|
|
153
|
-
//
|
|
153
|
+
// CONFIRM-DEAD-BEFORE-DEREGISTER. Only remove the roster row once the
|
|
154
154
|
// provider is positively confirmed gone. If the force-kill could not confirm death
|
|
155
155
|
// (killOutcome "failed-to-kill", or undefined because shutdownProvider threw), leave
|
|
156
156
|
// the agent registered so it stays visible (as "kill-failed") rather than becoming a
|
|
@@ -47,7 +47,7 @@ export class LifecycleCommands {
|
|
|
47
47
|
const hasProviderTurn = this.deps.hasProviderTurn();
|
|
48
48
|
if (hasProviderTurn || !redirect) {
|
|
49
49
|
this.deps.sessionLog(`interrupt requested via ${typeof params.requestedVia === "string" ? params.requestedVia : "dashboard"}`);
|
|
50
|
-
//
|
|
50
|
+
// an interrupted turn always ends with no output, and that state already has its
|
|
51
51
|
// own notice (`agent.turn_interrupted`). It must not also draw an empty-turn strike.
|
|
52
52
|
this.deps.resetEmptyTurns();
|
|
53
53
|
providerResult = await this.deps.options.adapter.interrupt(this.deps.process()!);
|
|
@@ -63,7 +63,7 @@ export class LifecycleCommands {
|
|
|
63
63
|
try {
|
|
64
64
|
killOutcome = await this.deps.shutdownProvider(type === "agent.kill", commandTimeoutMs(params));
|
|
65
65
|
} catch (shutdownError) {
|
|
66
|
-
//
|
|
66
|
+
// fix-forward — a throw INSIDE shutdownProvider (e.g. tmux missing from PATH ⇒
|
|
67
67
|
// defaultKillSession's Bun.spawnSync throws ENOENT) previously left killOutcome undefined
|
|
68
68
|
// and the "kill-failed" pill unset (that pill is set inside shutdownProvider, which never
|
|
69
69
|
// completed) — so the confirm-dead gate below deleted nothing AND published nothing, wedging
|
|
@@ -34,7 +34,7 @@ interface PromptCommandsDeps {
|
|
|
34
34
|
publishStatus(): void;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
//
|
|
37
|
+
// a cold isolated profile (first-run trust/onboarding/install gates) can take far longer
|
|
38
38
|
// than the warm-restart window to render an input-ready TUI, so the first prompt delivery gets a
|
|
39
39
|
// generous timeout. If it still misses, the prompt is re-attempted on the next provider ready
|
|
40
40
|
// signal (short timeout, since it just went idle), up to a bounded number of attempts.
|
|
@@ -42,11 +42,11 @@ const INITIAL_PROMPT_FIRST_READY_TIMEOUT_MS = 30_000;
|
|
|
42
42
|
const INITIAL_PROMPT_RETRY_READY_TIMEOUT_MS = 10_000;
|
|
43
43
|
const MAX_INITIAL_PROMPT_ATTEMPTS = 6;
|
|
44
44
|
const PROMPT_NOT_DURABLE = "prompt refused under a provider hold and could not be parked durably; not delivered";
|
|
45
|
-
/** Who a prompt.inject came from (every producer stamps its session message's sender); parked copies keep it
|
|
45
|
+
/** Who a prompt.inject came from (every producer stamps its session message's sender); parked copies keep it. */
|
|
46
46
|
export const promptOrigin = (params: Record<string, unknown>): string => [params.from, params.requestedBy].find((id): id is string => typeof id === "string" && id !== "") ?? "user";
|
|
47
47
|
|
|
48
48
|
export class PromptCommands {
|
|
49
|
-
//
|
|
49
|
+
// a spawn-time initial prompt whose first delivery timed out before the TUI was
|
|
50
50
|
// ready. Held (not dropped) for a ready-signal-driven re-attempt; cleared on success.
|
|
51
51
|
private pendingInitialPrompt?: string;
|
|
52
52
|
private deliveringInitialPrompt = false;
|
|
@@ -75,7 +75,7 @@ export class PromptCommands {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
// Deliver the spawn-time first prompt, surviving a cold-start TUI that isn't input-ready yet
|
|
78
|
-
//
|
|
78
|
+
// Timeout stashes it for ready-signal retry and surfaces the stall on the timeline.
|
|
79
79
|
private async attemptInitialPromptDelivery(prompt: string, readyTimeoutMs: number): Promise<void> {
|
|
80
80
|
if (this.deliveringInitialPrompt || this.deps.isStopped() || !this.deps.process() || !this.deps.options.adapter.deliverInitialPrompt) return;
|
|
81
81
|
this.deliveringInitialPrompt = true;
|
|
@@ -83,20 +83,20 @@ export class PromptCommands {
|
|
|
83
83
|
let arm: EmptyTurnArm | undefined;
|
|
84
84
|
try {
|
|
85
85
|
const startedAt = Date.now();
|
|
86
|
-
//
|
|
86
|
+
// armed BEFORE the delivery is awaited. On OpenCode `deliverInitialPrompt` awaits the
|
|
87
87
|
// synchronous /message POST, which returns only when the assistant message is complete; an arm
|
|
88
88
|
// taken on resolution lands at or after that turn's own idle edge and misses it entirely.
|
|
89
89
|
arm = this.deps.armInput(Date.now(), "ladder", true);
|
|
90
90
|
this.deps.recordInjectedPrompt(prompt);
|
|
91
91
|
const outcome = await this.deps.offerPromptOrPark(prompt, { readyTimeoutMs, isInitial: true, inputObserver: this.deps.inputObserver(arm) });
|
|
92
|
-
//
|
|
92
|
+
// a refused launch prompt that could not be made durable stays pending (re-offered
|
|
93
93
|
// on the next ready edge); a durably parked one redelivers from the inbox.
|
|
94
94
|
this.pendingInitialPrompt = outcome === "not-durable" ? prompt : undefined;
|
|
95
95
|
if (outcome !== "accepted") {
|
|
96
96
|
this.deps.sessionLog(`initial prompt attempt ${attempt} refused under a provider hold (${prompt.length} chars); ${outcome === "parked" ? "parked for redelivery" : "not durable, kept pending"}`);
|
|
97
97
|
return;
|
|
98
98
|
}
|
|
99
|
-
//
|
|
99
|
+
// round 3 review (LOW-5r) — a synchronous-endpoint adapter (OpenCode) can resolve this
|
|
100
100
|
// call NORMALLY after running its own bounded retry+reroll loop into a TERMINAL provider
|
|
101
101
|
// failure (see failTurn) rather than throwing — throwing here would route into this method's
|
|
102
102
|
// own retry-and-give-up path, re-sending the same prompt on top of the budget the adapter
|
|
@@ -104,11 +104,11 @@ export class PromptCommands {
|
|
|
104
104
|
// the adapter's own `error` status and cleared by the `busy` edge THIS delivery started with
|
|
105
105
|
// (setProviderStatus, ~line 1841), so a set value here can only have been raised during this
|
|
106
106
|
// very call, never stale. Without this check, a freshly spawned agent's terminally-failed
|
|
107
|
-
// first turn — the exact incident
|
|
107
|
+
// first turn — the exact incident was filed on — was still logged "delivered".
|
|
108
108
|
if (this.deps.terminalFailure()) {
|
|
109
109
|
this.deps.sessionLog(`initial prompt attempt ${attempt} ended in a terminal provider failure (${prompt.length} chars, ${Date.now() - startedAt}ms): ${this.deps.terminalFailure()!.message}`);
|
|
110
110
|
} else {
|
|
111
|
-
//
|
|
111
|
+
// delivery used to be logged ONLY on exception, so a spawn that idled forever left no
|
|
112
112
|
// evidence of whether its first prompt ever reached the provider; establishing that took
|
|
113
113
|
// SQLite archaeology on retained provider state. Say so on success too.
|
|
114
114
|
this.deps.sessionLog(`initial prompt delivered on attempt ${attempt} (${prompt.length} chars, ${Date.now() - startedAt}ms)`);
|
|
@@ -117,8 +117,8 @@ export class PromptCommands {
|
|
|
117
117
|
// Nothing reached the provider, so nothing is owed — unless a turn started meanwhile, which
|
|
118
118
|
// disarmInput checks: a synchronous-endpoint transport can throw after the model already ran.
|
|
119
119
|
if (arm) this.deps.disarmInput(arm);
|
|
120
|
-
//
|
|
121
|
-
// outbound request identity) before its completion await failed (e.g. the
|
|
120
|
+
// the adapter proved the provider ACCEPTED and persisted this prompt (matched to the
|
|
121
|
+
// outbound request identity) before its completion await failed (e.g. the whole-turn
|
|
122
122
|
// abort on a long first turn). The provider is already working on it, so requeueing would
|
|
123
123
|
// run the same assignment a second time — and with the acceptance-keyed markers retained,
|
|
124
124
|
// the retry would also no longer carry the launch briefing. Drop it like a normal success.
|
|
@@ -187,13 +187,13 @@ export class PromptCommands {
|
|
|
187
187
|
// Mark so the matching UserPromptSubmit echo isn't double-posted: a chat-box
|
|
188
188
|
// prompt already created its own session message shown in the dashboard.
|
|
189
189
|
this.deps.recordInjectedPrompt(prompt.trim(), imagePaths);
|
|
190
|
-
//
|
|
190
|
+
// prompt injection is a manual dashboard "continue"/chat-box send, not the
|
|
191
191
|
// agent's genuine first turn — never label it as the initial prompt.
|
|
192
|
-
//
|
|
192
|
+
// offer through the ONE provider-input seam: arm the empty-turn watch at
|
|
193
193
|
// hand-off, serialize through the FIFO behind any reserved ticket, disarm on rejection.
|
|
194
|
-
//
|
|
194
|
+
// a busy recipient (the adapter answered with the shared turn-in-progress signal
|
|
195
195
|
// instead of an unguarded start) DEFERS: bounded wait for the provider turn to clear, then
|
|
196
|
-
// one re-offer on the idle path through a fresh FIFO slot — the same shape as the
|
|
196
|
+
// one re-offer on the idle path through a fresh FIFO slot — the same shape as the
|
|
197
197
|
// context-injection defer. A timed-out wait (or any other failure) throws so the command
|
|
198
198
|
// fails visibly — never a silent drop.
|
|
199
199
|
const submitHeld = (outcome: string) => outcome === "submit-held";
|
|
@@ -216,7 +216,7 @@ export class PromptCommands {
|
|
|
216
216
|
return { injected: true, messageId, attachmentCount: attachments.length };
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
//
|
|
219
|
+
// the resolution side of the deny-then-hold AskUserQuestion flow (see control-server.ts's
|
|
220
220
|
// handlePermissionRequest). By the time this fires, the original PreToolUse hook has long since
|
|
221
221
|
// been answered with `deny` — there's no live HTTP request to respond to — so the human's answer
|
|
222
222
|
// (or dismissal) is delivered by clearing the pending-question hold and injecting a normal
|
|
@@ -234,9 +234,9 @@ export class PromptCommands {
|
|
|
234
234
|
return;
|
|
235
235
|
}
|
|
236
236
|
this.deps.recordInjectedPrompt(text.trim());
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
237
|
+
// this resolves a held AskUserQuestion mid-session — never the initial prompt.
|
|
238
|
+
// the answer unblocks a turn that is waiting on it, so it arms.
|
|
239
|
+
// offered through the same seam with the same busy policy: the adapter's shared
|
|
240
240
|
// turn-in-progress refusal defers (bounded turn-clear wait + one re-offer) instead of an
|
|
241
241
|
// unguarded start; any other failure stays best-effort logged, exactly as before.
|
|
242
242
|
const deliver = async (inputObserver?: ProviderInputObserver) => {
|
|
@@ -14,13 +14,13 @@ interface CredentialsDeps {
|
|
|
14
14
|
publishRenewalFailure(): void;
|
|
15
15
|
logRunnerDiagnostic(message: string): void;
|
|
16
16
|
reportBackgroundRejection(scope: string, status: string, title: string, error: unknown): void;
|
|
17
|
-
/** Recovery gave up: the runner must exit so Relay sees a terminal failure, not a mint loop
|
|
17
|
+
/** Recovery gave up: the runner must exit so Relay sees a terminal failure, not a mint loop. */
|
|
18
18
|
onTerminalAuthFailure(reason: string): void;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const TOKEN_RENEW_LOG_INTERVAL_MS = 5 * 60 * 1000;
|
|
22
22
|
const TOKEN_RENEW_RETRY_MS = 60_000;
|
|
23
|
-
//
|
|
23
|
+
// a successful mint does not prove the hub accepts the runner: an orphaned runner kept
|
|
24
24
|
// getting 401 on its fresh token and re-minted ~2/s. Auth recoveries back off (1 s doubling to
|
|
25
25
|
// 5 min, jittered) and stop after a bounded count; only a quiet spell resets the count.
|
|
26
26
|
export const AUTH_RECOVERY_MAX_ATTEMPTS = 10;
|
|
@@ -68,14 +68,14 @@ export class Credentials {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
// Timer/edge callers cannot await renewal; report an escaping rejection so it cannot
|
|
71
|
-
// silently strand the runner off the bus
|
|
71
|
+
// silently strand the runner off the bus.
|
|
72
72
|
private renewRuntimeTokenInBackground(): void {
|
|
73
73
|
void this.renewRuntimeToken().catch((error) => this.deps.reportBackgroundRejection("runtime token renewal", "runtime-token-renewal-failed", "Runtime token renewal aborted", error));
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
// The TTL timer cannot detect revocation: recover on auth failure, coalescing a burst into the
|
|
77
77
|
// one recovery in flight or scheduled.
|
|
78
|
-
// Orchestrator re-mint heals stale/expired tokens but preserves hard revocation
|
|
78
|
+
// Orchestrator re-mint heals stale/expired tokens but preserves hard revocation.
|
|
79
79
|
recoverRuntimeTokenAfterAuthFailure(source: string): void {
|
|
80
80
|
if (this.deps.isStopped() || this.authRecoveryExhausted || this.tokenRotation.inFlight || this.authRecoveryTimer) return;
|
|
81
81
|
if (!this.isRuntimeTokenRenewable() && !this.canRemintViaOrchestrator()) return;
|
|
@@ -212,7 +212,7 @@ export class Credentials {
|
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
//
|
|
215
|
+
// a newly allowed tool group or scope reaches the next generation without a restart.
|
|
216
216
|
async remintRuntimeTokenAtBoundary(reason: "clear" | "compact"): Promise<Record<string, unknown>> {
|
|
217
217
|
try {
|
|
218
218
|
// Queued behind a renewal in flight, so it re-derives from the token that renewal installs.
|
|
@@ -10,7 +10,7 @@ export async function bootstrapUnreadMessages(http: Pick<HumwickHttpClient, "pol
|
|
|
10
10
|
} catch (error) { logger.error("runner", `inbox bootstrap failed: ${error}`); }
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
/** Keep startup failure visible to each sender, including mail queued before the check
|
|
13
|
+
/** Keep startup failure visible to each sender, including mail queued before the check. */
|
|
14
14
|
export async function reportUnavailableMessages(
|
|
15
15
|
http: Pick<HumwickHttpClient, "sendMessage" | "markRead">, agentId: string,
|
|
16
16
|
pending: Map<number, Message>, settled: Set<number>, reason: string,
|
|
@@ -91,7 +91,7 @@ export class LifecycleMilestones {
|
|
|
91
91
|
private backgroundStopped(): void {
|
|
92
92
|
this.deps.stopAttachmentCacheSweep();
|
|
93
93
|
this.deps.stopTestRunDedupSweep();
|
|
94
|
-
//
|
|
94
|
+
// before the shutdown await, not after: an escalation or settle timer firing during a
|
|
95
95
|
// graceful provider shutdown would publish a "produced nothing" verdict about a turn the
|
|
96
96
|
// shutdown itself ended.
|
|
97
97
|
this.deps.disposeEmptyTurns();
|
|
@@ -103,7 +103,7 @@ export class LifecycleMilestones {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
private async providerShutDown(): Promise<void> {
|
|
106
|
-
//
|
|
106
|
+
// review MED-1 — time-boxed like the other two settleTerminalTokenUsage call
|
|
107
107
|
// sites (restartUnexpectedProviderExit, runPreSessionDestroy): an unsettled capture
|
|
108
108
|
// must not eat into the graceful-shutdown grace window and push the provider toward a
|
|
109
109
|
// forced SIGKILL.
|
|
@@ -163,7 +163,7 @@ export class LifecycleMilestones {
|
|
|
163
163
|
this.deps.clearProviderTurnWork();
|
|
164
164
|
this.deps.clearSubagentWork();
|
|
165
165
|
this.deps.clearBackgroundScriptWork();
|
|
166
|
-
//
|
|
166
|
+
// a pre-restart obligation must never be charged against an unrelated post-restart
|
|
167
167
|
// turn. Covers every restart, including the suppressPrompt one recoverNativeSelfResumeExit
|
|
168
168
|
// takes, where the agent comes back with no prompt at all.
|
|
169
169
|
this.deps.resetEmptyTurns();
|
|
@@ -11,7 +11,7 @@ export class LifecyclePhase {
|
|
|
11
11
|
// hooks, watchers and semantic heartbeats, but deliberately does NOT pin the runner projection
|
|
12
12
|
// offline: runner-owned lifecycle pills remain visible until teardown actually completes.
|
|
13
13
|
private terminalStatusLatchedValue = false;
|
|
14
|
-
//
|
|
14
|
+
// signal intent must suppress restart without skipping stop teardown.
|
|
15
15
|
private shuttingDownValue = false;
|
|
16
16
|
private exitCommandInProgressValue = false;
|
|
17
17
|
private restartInProgressValue = false;
|
|
@@ -32,7 +32,7 @@ export class LifecyclePhase {
|
|
|
32
32
|
|
|
33
33
|
constructor(private readonly deps: LifecyclePhaseDeps) {}
|
|
34
34
|
|
|
35
|
-
//
|
|
35
|
+
// latch synchronously in the signal handler, before child exit callbacks.
|
|
36
36
|
markShuttingDown(): void {
|
|
37
37
|
this.shuttingDownValue = true;
|
|
38
38
|
if (!this.lifecycleActionValue) this.setLifecycleAction("shutting-down");
|
|
@@ -15,16 +15,14 @@ interface LivenessExchangeDeps {
|
|
|
15
15
|
isStopped(): boolean;
|
|
16
16
|
isTerminalStatusLatched(): boolean;
|
|
17
17
|
currentStatus: ClaimTracker["currentStatus"];
|
|
18
|
-
sampleQuota(): Promise<Pick<HeartbeatReport, "providerQuota" | "quotaLeaseToken"> | undefined>;
|
|
19
|
-
acceptQuota(report: HeartbeatReport, result: HeartbeatResult["providerQuota"]): void;
|
|
20
18
|
enqueueMessage(message: Message): void;
|
|
21
19
|
triggerEffort(): void;
|
|
22
20
|
recoverRuntimeTokenAfterAuthFailure(source: string): void;
|
|
23
21
|
logRunnerDiagnostic(message: string): void;
|
|
24
22
|
}
|
|
25
23
|
|
|
26
|
-
/** The runner's periodic liveness exchange
|
|
27
|
-
* back to HTTP while the socket is down or the exchange fails on it
|
|
24
|
+
/** The runner's periodic liveness exchange. It rides the registered bus socket and falls
|
|
25
|
+
* back to HTTP while the socket is down or the exchange fails on it. */
|
|
28
26
|
export class LivenessExchange {
|
|
29
27
|
private timer?: Timer;
|
|
30
28
|
private inFlight = false;
|
|
@@ -54,13 +52,9 @@ export class LivenessExchange {
|
|
|
54
52
|
const status = this.deps.currentStatus();
|
|
55
53
|
const agentStatus = runnerAgentStatus(status);
|
|
56
54
|
try {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
});
|
|
60
|
-
// #3608 — inbox recovery and fresh quota samples ride the single liveness exchange.
|
|
61
|
-
const report: HeartbeatReport = { status: agentStatus, ready: agentStatus !== "offline", unread: true, ...quota };
|
|
55
|
+
// inbox recovery rides the single liveness exchange.
|
|
56
|
+
const report: HeartbeatReport = { status: agentStatus, ready: agentStatus !== "offline", unread: true };
|
|
62
57
|
const result = await this.exchange(report);
|
|
63
|
-
this.deps.acceptQuota(report, result?.providerQuota);
|
|
64
58
|
for (const message of result?.messages ?? []) this.deps.enqueueMessage(message);
|
|
65
59
|
this.deps.triggerEffort();
|
|
66
60
|
} catch (error) {
|