@humwick/runner 0.130.5 → 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/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 +7 -10
- 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.sh +4 -4
- package/plugins/claude/hooks/kill-guard.ts +1 -1
- 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 +1 -1
- 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 +1 -1
- 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 +1 -1
- 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/src/adapters/provider-quota-harvest.ts +0 -76
package/src/relay-mcp.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { MCP_SERVER_INFO, isRelayMcpServerName } from "@humwick/sdk";
|
|
2
2
|
import { relayEnv, writeChildEnv } from "@humwick/sdk/env";
|
|
3
3
|
import { codexVaultMcpEnv } from "./mcp-vault-env";
|
|
4
4
|
// Single home for the relay HTTP MCP endpoint descriptor + per-provider injection
|
|
@@ -6,35 +6,35 @@ import { codexVaultMcpEnv } from "./mcp-vault-env";
|
|
|
6
6
|
// server name, and token-handling rules live in exactly one place.
|
|
7
7
|
//
|
|
8
8
|
// Token handling: the bearer token is NEVER placed in argv (it would leak via `ps`
|
|
9
|
-
// and the agent's own process inspection). Claude expands `${
|
|
9
|
+
// and the agent's own process inspection). Claude expands `${HUMWICK_SESSION_TOKEN}`
|
|
10
10
|
// from the env at MCP-config parse time; Codex reads it from the named env var.
|
|
11
11
|
//
|
|
12
|
-
// Why a dedicated var and NOT `
|
|
12
|
+
// Why a dedicated var and NOT `HUMWICK_TOKEN`: a managed session inherits the
|
|
13
13
|
// runner's scoped, identity-bearing session token, but Claude Code applies a rig/user
|
|
14
14
|
// `settings.json` `env` block OVER the inherited env. A rig that sets
|
|
15
|
-
// `env.
|
|
15
|
+
// `env.HUMWICK_TOKEN` (e.g. to the admin token, for interactive CLI use) would
|
|
16
16
|
// clobber the scoped token at parse time, so the relay MCP connection authenticates as
|
|
17
|
-
// the `server` actor with no agent identity — `
|
|
18
|
-
// `
|
|
17
|
+
// the `server` actor with no agent identity — `humwick_whoami` returns null and
|
|
18
|
+
// `humwick_send_message` demands a `from` it can't supply. The runner exports the
|
|
19
19
|
// scoped token under this dedicated name that rigs don't set, so nothing on the host can
|
|
20
20
|
// hijack the managed agent's identity.
|
|
21
|
-
export const RELAY_MCP_SERVER_NAME =
|
|
21
|
+
export const RELAY_MCP_SERVER_NAME = MCP_SERVER_INFO.name;
|
|
22
22
|
export { isRelayMcpServerName };
|
|
23
23
|
export const RELAY_MCP_PATH = "/api/mcp";
|
|
24
|
-
export const RELAY_MCP_TOKEN_ENV = "
|
|
24
|
+
export const RELAY_MCP_TOKEN_ENV = "HUMWICK_SESSION_TOKEN";
|
|
25
25
|
|
|
26
|
-
//
|
|
26
|
+
// shared host-listener MCP. The agent connects to ONE orchestrator-owned shared
|
|
27
27
|
// `callmux` listener via a per-agent `callmux bridge`, fronting the host's NON-IDENTITY MCP
|
|
28
28
|
// (tokenlean, github). The bridge is a stdio child (auto-reconnect + retry-once on a
|
|
29
29
|
// listener/downstream hiccup; `--cwd` injects `x-callmux-cwd` for per-worktree session-cwd
|
|
30
30
|
// isolation), so it rides the SAME provider injection as any provisioned stdio server. The
|
|
31
|
-
// identity-bearing relay endpoint is NEVER routed here — it stays per-agent
|
|
31
|
+
// identity-bearing relay endpoint is NEVER routed here — it stays per-agent.
|
|
32
32
|
export { SHARED_MCP_SERVER_NAME };
|
|
33
33
|
// callmux's documented manual-listener endpoint (`callmux --listen 4860`, Streamable HTTP at
|
|
34
34
|
// /mcp). In managed launches the orchestrator owns the real listener URL and passes it through
|
|
35
|
-
//
|
|
35
|
+
// HUMWICK_SHARED_MCP_URL; this fallback is for direct runner/manual-listener use.
|
|
36
36
|
export const DEFAULT_SHARED_MCP_URL = "http://localhost:4860/mcp";
|
|
37
|
-
export const SHARED_MCP_URL_ENV = "
|
|
37
|
+
export const SHARED_MCP_URL_ENV = "HUMWICK_SHARED_MCP_URL";
|
|
38
38
|
export { runnerManagedToolDeferralRoute } from "./mcp-tool-deferral";
|
|
39
39
|
|
|
40
40
|
/** Resolve the shared-listener URL: explicit runner option → env → documented default. */
|
|
@@ -42,22 +42,11 @@ export function resolveSharedMcpUrl(explicit?: string): string {
|
|
|
42
42
|
return explicit ?? relayEnv(process.env, SHARED_MCP_URL_ENV) ?? DEFAULT_SHARED_MCP_URL;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
export function
|
|
46
|
-
return
|
|
45
|
+
export function relayMcpEndpoint(relayUrl: string): string {
|
|
46
|
+
return `${relayUrl.replace(/\/+$/, "")}${RELAY_MCP_PATH}`;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
const url = new URL(endpoint);
|
|
51
|
-
if (generation === "humwick") url.searchParams.set("tools", generation);
|
|
52
|
-
else url.searchParams.delete("tools");
|
|
53
|
-
return url.toString();
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function relayMcpEndpoint(relayUrl: string, generation: ToolGeneration = "relay"): string {
|
|
57
|
-
return generationEndpoint(`${relayUrl.replace(/\/+$/, "")}${RELAY_MCP_PATH}`, generation);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// #1248 — per-server Codex MCP overrides. Default `tool_timeout_sec` (60s) and
|
|
49
|
+
// per-server Codex MCP overrides. Default `tool_timeout_sec` (60s) and
|
|
61
50
|
// `startup_timeout_ms` are too short for our long-running tools (tests/releases run
|
|
62
51
|
// 5+ min), forcing agents to poll/sleep instead of just waiting on the call. Verified
|
|
63
52
|
// against the installed codex-cli binary (0.144.0): both fields live on the
|
|
@@ -77,12 +66,12 @@ function codexTimeoutConfigArgs(key: string): string[] {
|
|
|
77
66
|
|
|
78
67
|
// Codex: `-c mcp_servers.<name>.*` overrides. `bearer_token_env_var` tells Codex to
|
|
79
68
|
// read the token from the env var itself → transport resolves to streamable_http.
|
|
80
|
-
// `endpoint` overrides the target URL (runner-local proxy, Stage 2
|
|
81
|
-
export function relayMcpCodexConfigArgs(relayUrl: string, endpoint?: string
|
|
82
|
-
const key = `mcp_servers.${
|
|
69
|
+
// `endpoint` overrides the target URL (runner-local proxy, Stage 2) — see above.
|
|
70
|
+
export function relayMcpCodexConfigArgs(relayUrl: string, endpoint?: string): string[] {
|
|
71
|
+
const key = `mcp_servers.${MCP_SERVER_INFO.name}`;
|
|
83
72
|
return [
|
|
84
73
|
"-c",
|
|
85
|
-
`${key}.url=${tomlString(
|
|
74
|
+
`${key}.url=${tomlString(endpoint ?? relayMcpEndpoint(relayUrl))}`,
|
|
86
75
|
"-c",
|
|
87
76
|
`${key}.bearer_token_env_var=${tomlString(RELAY_MCP_TOKEN_ENV)}`,
|
|
88
77
|
...codexTimeoutConfigArgs(key),
|
|
@@ -154,8 +143,8 @@ function codexNativeMcpToml(server: ProvisioningMcpServer): { root: string[]; ne
|
|
|
154
143
|
return { root, nested };
|
|
155
144
|
}
|
|
156
145
|
|
|
157
|
-
//
|
|
158
|
-
// profile's relay-provisioned servers, PLUS
|
|
146
|
+
// compose the launch MCP set from (the relay endpoint, if relay.mcp) PLUS the
|
|
147
|
+
// profile's relay-provisioned servers, PLUS the host USER-scope servers for an
|
|
159
148
|
// `mcp.mode:"host"` profile. One `--mcp-config` JSON carries them all. `strict` adds
|
|
160
149
|
// `--strict-mcp-config` so ONLY these load — Claude reads no on-disk config, so the
|
|
161
150
|
// project-cwd `.mcp.json` is never discovered (its enable modal can't wedge a headless
|
|
@@ -168,11 +157,11 @@ import { SHARED_MCP_SERVER_NAME, sharedCallmuxToolCallTimeoutMsFromEnv, type Pro
|
|
|
168
157
|
|
|
169
158
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
170
159
|
|
|
171
|
-
//
|
|
160
|
+
// a bare `command: "callmux"` resolves off PATH, which can drift from the version
|
|
172
161
|
// this package pins (root/runner/orchestrator all pin the same `callmux` dependency). npm
|
|
173
162
|
// (and bun/pnpm workspaces) always link a dependency's declared bin to `node_modules/.bin/<name>`
|
|
174
163
|
// at whichever ancestor directory it resolves/hoists to — the same layout-agnostic-bin
|
|
175
|
-
// invariant
|
|
164
|
+
// invariant rely on for the humwick CLI itself — so walk up from this
|
|
176
165
|
// module's own location to find the PINNED bridge binary instead of trusting PATH. Falls
|
|
177
166
|
// back to the bare name (old behavior) only if no bundled bin is found anywhere above.
|
|
178
167
|
export function resolveCallmuxBin(startDir: string = __dirname): string {
|
|
@@ -188,7 +177,7 @@ export function resolveCallmuxBin(startDir: string = __dirname): string {
|
|
|
188
177
|
|
|
189
178
|
// The shared-listener bridge as a provider-neutral stdio server descriptor: a `callmux bridge`
|
|
190
179
|
// child fronting the host's shared listener, with per-agent `--cwd "$WORKTREE"` for session-cwd
|
|
191
|
-
// isolation
|
|
180
|
+
// isolation. It flows through the existing claude/codex stdio injection unchanged.
|
|
192
181
|
export function sharedMcpBridgeServer(url: string, cwd: string): ProvisioningMcpServer {
|
|
193
182
|
return {
|
|
194
183
|
command: resolveCallmuxBin(),
|
|
@@ -196,7 +185,7 @@ export function sharedMcpBridgeServer(url: string, cwd: string): ProvisioningMcp
|
|
|
196
185
|
};
|
|
197
186
|
}
|
|
198
187
|
|
|
199
|
-
//
|
|
188
|
+
// the per-agent metaOnly callmux for Codex workers. The header callmux reads to scope
|
|
200
189
|
// session-cwd stdio downstreams (tokenlean) per worktree; the shared bridge injects it dynamically,
|
|
201
190
|
// and here we pin it statically because a stdio callmux has no incoming request headers to forward.
|
|
202
191
|
const CALLMUX_CWD_HEADER = "x-callmux-cwd";
|
|
@@ -205,20 +194,20 @@ const CALLMUX_CWD_HEADER = "x-callmux-cwd";
|
|
|
205
194
|
// materializer (writes it) so disk == launch.
|
|
206
195
|
export const SHARED_MCP_METAONLY_CONFIG_FILENAME = "callmux-metaonly.json";
|
|
207
196
|
|
|
208
|
-
//
|
|
197
|
+
// the request header the per-agent metaOnly callmux sets on its relay downstream. The runner
|
|
209
198
|
// proxy reads it to decide which relay tools/list view to serve: WITHOUT it (the agent's DIRECT relay
|
|
210
199
|
// client) the proxy hides the on-demand set so only the eager hot-path set boots; WITH it (this
|
|
211
200
|
// callmux, fronting the on-demand set) the proxy serves the FULL set the relay already returned so
|
|
212
201
|
// callmux can route callmux_call to any of them. It can only ever widen the LIST back to that
|
|
213
|
-
// relay-returned set — never authorization: tools/call stays scope-gated regardless (
|
|
202
|
+
// relay-returned set — never authorization: tools/call stays scope-gated regardless (/ capability
|
|
214
203
|
// neutrality). Kept out of `forward` so the relay itself never sees it.
|
|
215
|
-
const RELAY_MCP_ONDEMAND_SURFACE_HEADER = "x-
|
|
204
|
+
const RELAY_MCP_ONDEMAND_SURFACE_HEADER = "x-humwick-ondemand-front";
|
|
216
205
|
export const RELAY_MCP_ONDEMAND_SURFACE_VALUE = "1";
|
|
217
206
|
|
|
218
|
-
//
|
|
207
|
+
// a Codex worker's relay downstream inside the per-agent metaOnly callmux. It points at the
|
|
219
208
|
// runner-LOCAL proxy (never the relay directly): the proxy injects the live relay token, so identity
|
|
220
|
-
// rides through and the agent/callmux never hold the real token on disk
|
|
221
|
-
// per-session PROXY secret (which the agent already holds in
|
|
209
|
+
// rides through and the agent/callmux never hold the real token on disk. `authSecret` is the
|
|
210
|
+
// per-session PROXY secret (which the agent already holds in HUMWICK_SESSION_TOKEN — writing it to
|
|
222
211
|
// this 0600 config adds no exposure, and it is NOT the relay token). cachePolicy denies every tool:
|
|
223
212
|
// relay ops are identity-bearing/mutating and must never be served from cache.
|
|
224
213
|
export interface MetaOnlyRelayDownstream {
|
|
@@ -226,18 +215,17 @@ export interface MetaOnlyRelayDownstream {
|
|
|
226
215
|
endpoint: string;
|
|
227
216
|
/** The per-session proxy secret the agent authenticates to the local proxy with. */
|
|
228
217
|
authSecret: string;
|
|
229
|
-
generation?: ToolGeneration;
|
|
230
218
|
}
|
|
231
219
|
|
|
232
220
|
function metaOnlyRelayServerConfig(relay: MetaOnlyRelayDownstream): Record<string, unknown> {
|
|
233
221
|
return {
|
|
234
|
-
url:
|
|
222
|
+
url: relay.endpoint,
|
|
235
223
|
transport: "streamable-http",
|
|
236
224
|
headers: {
|
|
237
225
|
Authorization: `Bearer ${relay.authSecret}`,
|
|
238
226
|
[RELAY_MCP_ONDEMAND_SURFACE_HEADER]: RELAY_MCP_ONDEMAND_SURFACE_VALUE,
|
|
239
227
|
},
|
|
240
|
-
//
|
|
228
|
+
// never cache relay ops (identity-bearing/mutating). Wildcard-deny is explicit belt-and-
|
|
241
229
|
// suspenders on top of the config's disabled global cache TTL.
|
|
242
230
|
cachePolicy: { denyTools: ["*"] },
|
|
243
231
|
callTimeoutMs: sharedCallmuxToolCallTimeoutMsFromEnv(),
|
|
@@ -246,13 +234,13 @@ function metaOnlyRelayServerConfig(relay: MetaOnlyRelayDownstream): Record<strin
|
|
|
246
234
|
|
|
247
235
|
// The per-agent metaOnly callmux config: streamable-http downstreams exposing ONLY the callmux
|
|
248
236
|
// meta-tools (callmux_search_tools/callmux_call/…). A Codex worker boots with ~11 meta-tool schemas
|
|
249
|
-
// instead of the flat downstream schemas
|
|
237
|
+
// instead of the flat downstream schemas and reaches the real tools on demand
|
|
250
238
|
// through callmux_call.
|
|
251
239
|
// • The shared listener (SHARED_MCP_SERVER_NAME): the worktree cwd is a STATIC downstream header —
|
|
252
240
|
// the per-agent callmux runs over stdio (no incoming headers to forwardHeaders) and the worktree
|
|
253
241
|
// is fixed per agent, so a fixed x-callmux-cwd routes the shared listener's session-cwd tools to
|
|
254
242
|
// this worktree exactly as the dynamic bridge header did.
|
|
255
|
-
// •
|
|
243
|
+
// • — when `relay` is given, the identity-bearing relay endpoint is added as a SECOND
|
|
256
244
|
// downstream (via the runner-local proxy) so the on-demand relay set loads through callmux_call.
|
|
257
245
|
// Timeouts mirror the bridge's SHARED_CALLMUX_TOOL_CALL_TIMEOUT_MS floor so nested calls keep the same
|
|
258
246
|
// call-timeout budget.
|
|
@@ -272,7 +260,7 @@ export function sharedMcpMetaOnlyConfig(url: string, cwd: string, relay?: MetaOn
|
|
|
272
260
|
cachePolicy: { denyTools: ["*"] },
|
|
273
261
|
callTimeoutMs,
|
|
274
262
|
},
|
|
275
|
-
...(relay ? { [
|
|
263
|
+
...(relay ? { [MCP_SERVER_INFO.name]: metaOnlyRelayServerConfig(relay) } : {}),
|
|
276
264
|
},
|
|
277
265
|
};
|
|
278
266
|
}
|
|
@@ -288,22 +276,22 @@ export function sharedMcpMetaOnlyServer(configPath: string): ProvisioningMcpServ
|
|
|
288
276
|
}
|
|
289
277
|
|
|
290
278
|
// Materialize the per-agent metaOnly config `sharedMcpMetaOnlyServer(configPath)` points at. The
|
|
291
|
-
// shared-listener URL is non-secret.
|
|
292
|
-
// the per-session PROXY secret (NOT the relay token — the proxy injects that
|
|
293
|
-
// already holds the proxy secret in
|
|
279
|
+
// shared-listener URL is non-secret. When a `relay` downstream is given, the config carries
|
|
280
|
+
// the per-session PROXY secret (NOT the relay token — the proxy injects that). The agent
|
|
281
|
+
// already holds the proxy secret in HUMWICK_SESSION_TOKEN, so the 0600 file adds no exposure.
|
|
294
282
|
export function writeSharedMcpMetaOnlyConfig(configPath: string, url: string, cwd: string, relay?: MetaOnlyRelayDownstream): void {
|
|
295
283
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
296
284
|
writeFileSync(configPath, JSON.stringify(sharedMcpMetaOnlyConfig(url, cwd, relay), null, 2) + "\n", { mode: 0o600 });
|
|
297
285
|
}
|
|
298
286
|
|
|
299
|
-
//
|
|
287
|
+
// shared by the Claude JSON payload builder and the Codex TOML materializer, so
|
|
300
288
|
// both projections merge `configEnv` (non-secret, allowlisted literals) into `env` the
|
|
301
289
|
// same way instead of re-deriving the merge twice.
|
|
302
290
|
export function mergedConfigEnv(server: ProvisioningMcpServer): ProvisioningMcpServer {
|
|
303
291
|
if (!server.configEnv || Object.keys(server.configEnv).length === 0) return server;
|
|
304
292
|
return {
|
|
305
293
|
...server,
|
|
306
|
-
env: writeChildEnv({}, { ...server.configEnv, ...(server.env ?? {}) }),
|
|
294
|
+
env: writeChildEnv({}, { ...server.configEnv, ...(server.env ?? {}) }, []),
|
|
307
295
|
};
|
|
308
296
|
}
|
|
309
297
|
|
|
@@ -323,16 +311,15 @@ export interface ClaudeMcpConfigOpts {
|
|
|
323
311
|
relayUrl: string;
|
|
324
312
|
endpoint?: string;
|
|
325
313
|
includeRelay: boolean;
|
|
326
|
-
generation?: ToolGeneration;
|
|
327
314
|
servers?: Record<string, ProvisioningMcpServer>;
|
|
328
|
-
/**
|
|
315
|
+
/** host USER-scope MCP servers (`~/.claude.json` `mcpServers`), re-injected for
|
|
329
316
|
* `mcp.mode:"host"` so `--strict-mcp-config` PRESERVES them instead of dropping the
|
|
330
317
|
* ambient host MCP a host-base agent loaded before strict. Lowest precedence: the relay
|
|
331
318
|
* endpoint and relay-provisioned servers win on a name collision (relay decides). */
|
|
332
319
|
hostServers?: Record<string, ProvisioningMcpServer>;
|
|
333
320
|
}
|
|
334
321
|
|
|
335
|
-
//
|
|
322
|
+
// the `{ mcpServers: {...} }` object Claude parses, extracted so both the launch
|
|
336
323
|
// assembler (decides whether it's written to argv or a file) and the materializer (writes
|
|
337
324
|
// the file) build the IDENTICAL payload from the same inputs.
|
|
338
325
|
export function claudeMcpConfigPayload(opts: ClaudeMcpConfigOpts): Record<string, unknown> {
|
|
@@ -345,9 +332,9 @@ export function claudeMcpConfigPayload(opts: ClaudeMcpConfigOpts): Record<string
|
|
|
345
332
|
if (Object.keys(entry).length) mcpServers[name] = entry;
|
|
346
333
|
}
|
|
347
334
|
if (opts.includeRelay) {
|
|
348
|
-
mcpServers[
|
|
335
|
+
mcpServers[MCP_SERVER_INFO.name] = {
|
|
349
336
|
type: "http",
|
|
350
|
-
url:
|
|
337
|
+
url: opts.endpoint ?? relayMcpEndpoint(opts.relayUrl),
|
|
351
338
|
headers: { Authorization: `Bearer \${${RELAY_MCP_TOKEN_ENV}}` },
|
|
352
339
|
};
|
|
353
340
|
}
|
|
@@ -400,7 +387,6 @@ export interface OpenCodeMcpConfigOpts {
|
|
|
400
387
|
relayUrl: string;
|
|
401
388
|
endpoint?: string;
|
|
402
389
|
includeRelay: boolean;
|
|
403
|
-
generation?: ToolGeneration;
|
|
404
390
|
servers?: Record<string, ProvisioningMcpServer>;
|
|
405
391
|
}
|
|
406
392
|
|
|
@@ -408,9 +394,9 @@ export interface OpenCodeMcpConfigOpts {
|
|
|
408
394
|
export function openCodeMcpConfigPayload(opts: OpenCodeMcpConfigOpts): Record<string, unknown> {
|
|
409
395
|
const mcp: Record<string, unknown> = {};
|
|
410
396
|
if (opts.includeRelay) {
|
|
411
|
-
mcp[
|
|
397
|
+
mcp[MCP_SERVER_INFO.name] = {
|
|
412
398
|
type: "remote",
|
|
413
|
-
url:
|
|
399
|
+
url: opts.endpoint ?? relayMcpEndpoint(opts.relayUrl),
|
|
414
400
|
headers: { Authorization: `Bearer {env:${RELAY_MCP_TOKEN_ENV}}` },
|
|
415
401
|
oauth: false,
|
|
416
402
|
timeout: OPENCODE_MCP_TIMEOUT_MS,
|
|
@@ -426,7 +412,7 @@ export function openCodeMcpConfigPayload(opts: OpenCodeMcpConfigOpts): Record<st
|
|
|
426
412
|
|
|
427
413
|
export function claudeMcpLaunchArgs(opts: ClaudeMcpConfigOpts & {
|
|
428
414
|
strict?: boolean;
|
|
429
|
-
/**
|
|
415
|
+
/** when set, `--mcp-config` points at this path instead of carrying the JSON
|
|
430
416
|
* inline. Inline JSON lands verbatim in argv, which is world-readable via `ps`/
|
|
431
417
|
* `/proc/<pid>/cmdline`; a provisioned server's `env`/`headers` can carry a literal
|
|
432
418
|
* secret, so any launch with a relay-owned scratch dir to write into (see
|
|
@@ -442,8 +428,8 @@ export function claudeMcpLaunchArgs(opts: ClaudeMcpConfigOpts & {
|
|
|
442
428
|
return ["--mcp-config", configArg, ...(opts.strict ? ["--strict-mcp-config"] : [])];
|
|
443
429
|
}
|
|
444
430
|
|
|
445
|
-
//
|
|
446
|
-
// same shape relayMcpCodexConfigArgs uses for the relay endpoint.
|
|
431
|
+
// (Codex) — `-c mcp_servers.<name>.*` overrides for each relay-provisioned server, the
|
|
432
|
+
// same shape relayMcpCodexConfigArgs uses for the relay endpoint. This puts a
|
|
447
433
|
// provisioned server's literal `env`/`headers` secret material in argv (world-readable via
|
|
448
434
|
// `ps`/`/proc/<pid>/cmdline`), so it's ONLY safe to use when there is no isolated CODEX_HOME
|
|
449
435
|
// to materialize `codexProvisionedMcpToml` into instead (host-base Codex reuses the real
|
|
@@ -464,12 +450,12 @@ export function codexProvisionedMcpArgs(servers?: Record<string, ProvisioningMcp
|
|
|
464
450
|
args.push("-c", `${key}.env.${envKey}=${tomlString(envVal)}`);
|
|
465
451
|
}
|
|
466
452
|
if (server.disabled_tools?.length) args.push("-c", `${key}.disabled_tools=${tomlValue(server.disabled_tools)}`);
|
|
467
|
-
args.push(...codexTimeoutConfigArgs(key));
|
|
453
|
+
args.push(...codexTimeoutConfigArgs(key));
|
|
468
454
|
}
|
|
469
455
|
return args;
|
|
470
456
|
}
|
|
471
457
|
|
|
472
|
-
//
|
|
458
|
+
// the non-secret half of the provisioned-server `-c` overrides (per-server
|
|
473
459
|
// tool/startup timeouts only). Pairs with `codexProvisionedMcpToml`, which carries
|
|
474
460
|
// command/args/url/env into CODEX_HOME/config.toml instead of argv.
|
|
475
461
|
export function codexProvisionedMcpTimeoutArgs(servers?: Record<string, ProvisioningMcpServer>): string[] {
|
|
@@ -481,7 +467,7 @@ export function codexProvisionedMcpTimeoutArgs(servers?: Record<string, Provisio
|
|
|
481
467
|
return args;
|
|
482
468
|
}
|
|
483
469
|
|
|
484
|
-
//
|
|
470
|
+
// renders provisioned servers as `[mcp_servers.<name>]` / `[mcp_servers.<name>.env]`
|
|
485
471
|
// TOML tables for CODEX_HOME/config.toml, mirroring the `-c mcp_servers.<name>.*` dotted-key
|
|
486
472
|
// shape `codexProvisionedMcpArgs` uses, so command/args/url/env never touch argv. Table/key
|
|
487
473
|
// names are always TOML-quoted (server + env-var names aren't guaranteed bare-key safe).
|
|
@@ -544,7 +530,7 @@ export function upsertTomlTable(config: string, header: string, bodyLines: strin
|
|
|
544
530
|
return lines.join("\n");
|
|
545
531
|
}
|
|
546
532
|
|
|
547
|
-
//
|
|
533
|
+
// idempotently upsert provisioned MCP servers' command/args/url/env into
|
|
548
534
|
// CODEX_HOME/config.toml, the isolated per-instance home Codex actually reads. Pairs with
|
|
549
535
|
// `codexProvisionedMcpTimeoutArgs` (the non-secret `-c` overrides that stay in argv). Only
|
|
550
536
|
// safe to call for an ISOLATED CODEX_HOME (host-base reuses the real `~/.codex`, which relay
|
|
@@ -553,7 +539,7 @@ export function writeCodexMcpServersToml(codexHome: string, servers: Record<stri
|
|
|
553
539
|
const relevant = Object.entries(servers ?? {}).filter(([name]) => !isRelayMcpServerName(name));
|
|
554
540
|
const configPath = join(codexHome, "config.toml");
|
|
555
541
|
const existing = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
|
|
556
|
-
//
|
|
542
|
+
// every mcp_servers table in a managed CODEX_HOME is Relay-written: drop the previous
|
|
557
543
|
// launch's set first, so a host MCP class switched off on reuse leaves no stale server behind.
|
|
558
544
|
let config = withoutTomlTables(existing, "mcp_servers.");
|
|
559
545
|
if (!relevant.length) {
|
|
@@ -4,9 +4,9 @@ import { latestReplyableMessage, type ProviderSessionEvent } from "./adapter";
|
|
|
4
4
|
import { logger } from "./logger";
|
|
5
5
|
import { receivesTurnResponseAutomatically } from "./response-routing";
|
|
6
6
|
|
|
7
|
-
// Phase 2
|
|
7
|
+
// Phase 2 — the crux. The Claude Stop hook used to ask the server, synchronously
|
|
8
8
|
// and in the hot path, "does this agent owe a reply?" before clearing the turn. A slow
|
|
9
|
-
// server answer (the unindexed reply_to scan
|
|
9
|
+
// server answer (the unindexed reply_to scan) blew the hook's timeout and wedged the
|
|
10
10
|
// agent in `busy` forever. The fix: the hook asks the Runner, the Runner answers instantly
|
|
11
11
|
// from this local snapshot, and the snapshot is refreshed from the server only in the
|
|
12
12
|
// background — never on the path that ends a turn.
|
|
@@ -23,13 +23,13 @@ export function obligationRequiresExplicitReply(obligation: ReplyObligation): bo
|
|
|
23
23
|
return !receivesTurnResponseAutomatically(obligation.from);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
//
|
|
27
|
-
// deferred: its owner is waiting on its own
|
|
26
|
+
// automatic nags (Stop-hook block, idle re-injection) skip an obligation the server marked
|
|
27
|
+
// deferred: its owner is waiting on its own humwick_run. The obligation itself stays open.
|
|
28
28
|
export function obligationNagEligible(obligation: ReplyObligation): boolean {
|
|
29
29
|
return obligationRequiresExplicitReply(obligation) && obligation.nagDeferral === undefined;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
//
|
|
32
|
+
// (P1 privacy) — does this obligation's originator sit behind a CHANNEL-EGRESS bridge
|
|
33
33
|
// (`telegram:<account>`, …)? A bridge forwards what it receives to a real human's DM, so the runner's
|
|
34
34
|
// auto-captured turn narration must never be ADDRESSED at one; the obligation itself still stands and
|
|
35
35
|
// still requires an EXPLICIT reply (obligationRequiresExplicitReply is deliberately untouched).
|
|
@@ -72,14 +72,14 @@ export function responseCaptureReplyRoute(
|
|
|
72
72
|
|
|
73
73
|
type ReplyObligationFetch = () => Promise<ReplyObligation[]>;
|
|
74
74
|
|
|
75
|
-
/** The route a turn opened by this delivered batch answers
|
|
75
|
+
/** The route a turn opened by this delivered batch answers: its latest replyable message that could
|
|
76
76
|
* carry an obligation — never the agent's own run/timer wake (sent to itself) nor a system/control notice. */
|
|
77
77
|
export function deliveredBatchReplyRoute(messages: Message[], selfId?: string): NonNullable<ProviderSessionEvent["replyRoute"]> | null {
|
|
78
78
|
const replyable = latestReplyableMessage(messages.filter((m) => m.from !== m.to && m.from !== selfId && !isMechanicalMessageKind(m.kind)));
|
|
79
79
|
return replyable ? { to: replyable.from, replyToMessageId: replyable.id, channelTarget: obligationOriginatesFromChannel(replyable), replyExpected: replyable.replyExpected === true } : null;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
/**
|
|
82
|
+
/** the server's "answered" rule (annotateAnsweredMessages): the agent replied to it or later in
|
|
83
83
|
* its thread with a live (non-suppressed) message, or acknowledged it with an ack reaction. */
|
|
84
84
|
export function threadAnswers(thread: Message[], askedId: number, agentId: string): boolean {
|
|
85
85
|
const asked = thread.find((m) => m.id === askedId);
|
|
@@ -93,7 +93,7 @@ function repliesTo(thread: Message[], askedId: number, agentId: string): boolean
|
|
|
93
93
|
return thread.some((m) => m.from === agentId && m.kind !== "session" && m.replyTo === askedId && (isHumanAgentId(m.to) || m.payload?.reportSuppression === undefined));
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
// Peer chat has no obligation row and can be acknowledged without a reply, so the thread decides:
|
|
96
|
+
// Peer chat has no obligation row and can be acknowledged without a reply, so the thread decides: humwick_reply
|
|
97
97
|
// (or 👍) followed by a native final stays single. A route backed by an obligation row (a reply-expected
|
|
98
98
|
// message, or `fromObligationRow`) follows the server's settle rule and is already proven open when unreadable.
|
|
99
99
|
export async function submittedResponseReplyRoute(
|
|
@@ -175,7 +175,7 @@ export class ReplyObligationCache {
|
|
|
175
175
|
this.dirtyTimer.unref?.();
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
//
|
|
178
|
+
// a nag (Stop-hook block, idle re-injection) decided on a stale snapshot can miss a humwick_run
|
|
179
179
|
// started this turn, or one that just settled. Re-read once, bounded well under the Stop hook's 2s
|
|
180
180
|
// curl budget, but only when an explicit-reply obligation is in play; otherwise serve the snapshot.
|
|
181
181
|
async freshForNag(timeoutMs = NAG_REFRESH_TIMEOUT_MS): Promise<ReplyObligation[]> {
|
|
@@ -14,7 +14,7 @@ type SessionPublisher = (input: {
|
|
|
14
14
|
replyTo?: number;
|
|
15
15
|
}) => void | Promise<void>;
|
|
16
16
|
|
|
17
|
-
/** The turn-final capture's addressing, after the
|
|
17
|
+
/** The turn-final capture's addressing, after the channel guard has had its say. */
|
|
18
18
|
interface CaptureRoute {
|
|
19
19
|
to: string;
|
|
20
20
|
replyTo?: number;
|
|
@@ -29,13 +29,13 @@ interface CapturedResponseInput {
|
|
|
29
29
|
session: MessageSessionMeta;
|
|
30
30
|
replyTo?: number;
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
32
|
+
* the reply route resolved to a CHANNEL-EGRESS bridge (see obligationOriginatesFromChannel,
|
|
33
33
|
* runner/src/reply-obligation-cache.ts). Set by the caller; absent means "not a channel".
|
|
34
34
|
*/
|
|
35
35
|
channelTarget?: boolean;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
//
|
|
38
|
+
// dual-stamp the Part A turn-finality field alongside the legacy
|
|
39
39
|
// `final` alias so old consumers keep working while new ones can key off the
|
|
40
40
|
// typed `turnFinal` flag directly instead of every writer's own `final: true`.
|
|
41
41
|
function finalSession(session: MessageSessionMeta): MessageSessionMeta {
|
|
@@ -43,7 +43,7 @@ function finalSession(session: MessageSessionMeta): MessageSessionMeta {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
46
|
+
* (P1 privacy) — the runner-side half of "auto-captured narration NEVER reaches a channel".
|
|
47
47
|
*
|
|
48
48
|
* What this function captures is the agent's raw terminal narration at turn end: its own running
|
|
49
49
|
* commentary, written for its operator's scrollback. Whenever a reply obligation was open, that text
|
|
@@ -59,10 +59,10 @@ function finalSession(session: MessageSessionMeta): MessageSessionMeta {
|
|
|
59
59
|
* stamp and the channel-recipient poll (isDisallowedChannelContent, src/db/channel-egress-allowlist.ts),
|
|
60
60
|
* which is what still holds when the sender is a stale runner, an automation, or a raw POST.
|
|
61
61
|
*
|
|
62
|
-
* The capture is not discarded —
|
|
62
|
+
* The capture is not discarded — requires a turn-final capture to ALWAYS be mirrored. It is
|
|
63
63
|
* re-addressed to the `user` dashboard-mirror sink with the reply link dropped, exactly the shape an
|
|
64
64
|
* autonomous turn with no eligible obligation already produces. Dropping `replyTo` is the point, not a
|
|
65
|
-
* side effect: the channel obligation stays OPEN and still demands an explicit `
|
|
65
|
+
* side effect: the channel obligation stays OPEN and still demands an explicit `humwick_reply` /
|
|
66
66
|
* `relay_egress_to_channel`, so a channel gets a deliberate, addressed answer or none at all.
|
|
67
67
|
*/
|
|
68
68
|
function captureRoute(input: CapturedResponseInput): CaptureRoute {
|
|
@@ -74,10 +74,10 @@ export async function publishCapturedResponse(input: CapturedResponseInput & {
|
|
|
74
74
|
publishSessionEvent: SessionPublisher;
|
|
75
75
|
}): Promise<void> {
|
|
76
76
|
const route = captureRoute(input);
|
|
77
|
-
//
|
|
77
|
+
// the enqueued chat report IS the parent delivery, so the mirror stays
|
|
78
78
|
// transcript-only (`user` sink): at most one parent-addressed row per turn-final. (Before,
|
|
79
79
|
// the mirror ALSO went to the parent, doubling opener-answered turns and splitting the
|
|
80
|
-
//
|
|
80
|
+
// suppression decision.) The mirror keeps replyTo + turn stamp for lineage.
|
|
81
81
|
const reported = input.replyTo !== undefined && !receivesTurnResponseAutomatically(input.to) && input.channelTarget !== true;
|
|
82
82
|
await input.publishSessionEvent({
|
|
83
83
|
from: input.from,
|
|
@@ -90,7 +90,7 @@ export async function publishCapturedResponse(input: CapturedResponseInput & {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
export function enqueueCapturedResponseReport(input: CapturedResponseInput): void {
|
|
93
|
-
//
|
|
93
|
+
// `to === "user"` is kept (the dashboard mirror already carries the turn; a report would
|
|
94
94
|
// double it) but is NO LONGER the only skip: a channel-egress target is dropped outright, whether
|
|
95
95
|
// it arrived here pre-rewritten by captureRoute or straight from a caller that never consulted it.
|
|
96
96
|
if (!input.replyTo || receivesTurnResponseAutomatically(input.to) || input.channelTarget === true) return;
|
package/src/response-routing.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isHumanAgentId } from "@humwick/sdk/types/users";
|
|
2
2
|
// The runner mirrors every completed turn to the dashboard's user sink, regardless of role.
|
|
3
|
-
// Keep header/nag policy and capture-report routing on the same recipient rule
|
|
3
|
+
// Keep header/nag policy and capture-report routing on the same recipient rule.
|
|
4
4
|
export function receivesTurnResponseAutomatically(recipient: string): boolean {
|
|
5
5
|
return isHumanAgentId(recipient);
|
|
6
6
|
}
|