@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
package/src/question-decision.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { COORDINATOR_PROFILE_NAMES, errMessage, type OperatorDecisionFileInput, type HumwickHttpClient } from "@humwick/sdk";
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
// answer-as-message as
|
|
3
|
+
// a coordinator's AskUserQuestion becomes a durable operator decision (same store, dashboard card and
|
|
4
|
+
// answer-as-message as humwick_decision). Any other agent, or a failed file, keeps the live relayed question.
|
|
5
5
|
export async function fileQuestionDecision(
|
|
6
6
|
deps: { http: Pick<HumwickHttpClient, "fileDecision">; profile?: string; log(message: string): void },
|
|
7
7
|
input: OperatorDecisionFileInput,
|
package/src/quota.ts
CHANGED
|
@@ -1,21 +1,15 @@
|
|
|
1
1
|
export {
|
|
2
2
|
DEFAULT_PROVIDER_QUOTA_CONFIG,
|
|
3
3
|
QuotaRetryAfterError,
|
|
4
|
-
claudeRateLimitHeadersQuotaState,
|
|
5
4
|
claudeUsageQuotaState,
|
|
6
|
-
collectClaudeQuotaState,
|
|
7
5
|
collectCodexQuotaSample,
|
|
8
6
|
collectCodexQuotaState,
|
|
9
7
|
codexRateLimitsQuotaState,
|
|
10
8
|
credentialAccountKey,
|
|
11
|
-
defaultClaudeConfigDir,
|
|
12
9
|
defaultZaiCredentialsPath,
|
|
13
10
|
normalizeProviderQuotaConfig,
|
|
14
11
|
providerPayloadAccountKey,
|
|
15
|
-
resolveClaudeQuotaIdentity,
|
|
16
|
-
resolveClaudeStatuslineQuotaIdentity,
|
|
17
12
|
resolveCodexQuotaIdentity,
|
|
18
|
-
resolveStableClaudeQuotaIdentity,
|
|
19
13
|
resolveStableCodexQuotaIdentity,
|
|
20
14
|
resolveStableZaiQuotaIdentity,
|
|
21
15
|
type ProviderQuotaIdentity,
|
package/src/rate-limit.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// shared rate-limit hold construction + Claude pane detection.
|
|
2
2
|
//
|
|
3
3
|
// Two detection arms feed the SAME provider-neutral `blocked` hold:
|
|
4
4
|
// 1. The StopFailure hook (clean API-error turn-end: API-429/auth/billing) → control-server.
|
|
@@ -18,6 +18,7 @@ const MAX_RESET_AHEAD_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
18
18
|
|
|
19
19
|
interface RateLimitHoldInput {
|
|
20
20
|
errorType?: string;
|
|
21
|
+
usageLimitScope?: ProviderState["usageLimitScope"];
|
|
21
22
|
kind?: "usage_limit" | "transient_overload";
|
|
22
23
|
/** Unix ms the limit window resets, when known. */
|
|
23
24
|
resetAt?: number;
|
|
@@ -25,7 +26,7 @@ interface RateLimitHoldInput {
|
|
|
25
26
|
source?: string;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
//
|
|
29
|
+
// Claude reports a proxy that refused CONNECT as "Couldn't connect through your proxy
|
|
29
30
|
// (ERR_PROXY_TUNNEL)"; Agent Vault refuses an expired or revoked session with 407.
|
|
30
31
|
const PROXY_AUTH_FAILURE_RE = /ERR_PROXY_TUNNEL|proxy authentication required|\b407\b[^\n]*proxy|proxy[^\n]*\b407\b/i;
|
|
31
32
|
|
|
@@ -64,6 +65,7 @@ export function buildRateLimitProviderState(input: RateLimitHoldInput): Provider
|
|
|
64
65
|
? "Holding briefly for provider overload; Humwick will retry with bounded backoff."
|
|
65
66
|
: "Holding until the usage window resets; Humwick auto-resumes the agent.",
|
|
66
67
|
kind,
|
|
68
|
+
...(kind === "usage_limit" ? { usageLimitScope: input.usageLimitScope ?? "provider" } : {}),
|
|
67
69
|
source: input.source ?? "claude",
|
|
68
70
|
errorType: input.errorType ?? RATE_LIMIT_BLOCK_REASON,
|
|
69
71
|
enteredAt: now,
|
|
@@ -94,7 +96,7 @@ const LIMIT_PHRASE_RE = /(?:hit (?:your|the)\s*(?:[\w-]+\s+)*limit|usage limit|\
|
|
|
94
96
|
const RESET_AT_RE = /\bresets?\b(?:\s+at)?\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*(?:\(([^)]+)\))?/i;
|
|
95
97
|
const TRY_AGAIN_RE = /\btry again in\s+(\d+)\s*(second|minute|hour|day)s?/i;
|
|
96
98
|
|
|
97
|
-
//
|
|
99
|
+
// the limit phrase and its reset/retry must sit on the same line or within this many
|
|
98
100
|
// lines of each other. The pairing was originally allowed to span the WHOLE pane so a modal
|
|
99
101
|
// wrapping across rows still matched; that made the two halves collectable from unrelated
|
|
100
102
|
// places on screen. A modal wraps onto the very next row, so a tight window keeps the wrapped
|
|
@@ -123,7 +125,7 @@ export function parseClaudeRateLimitPane(text: string, nowMs: number = Date.now(
|
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
/**
|
|
126
|
-
*
|
|
128
|
+
* the usage-limit modal, distinguished from the agent's OWN pane output.
|
|
127
129
|
*
|
|
128
130
|
* The capture (`capture-pane -S -80`) contains the agent's rendered conversation, so a
|
|
129
131
|
* phrase test over the whole pane fires on any agent that WRITES about rate limits. It did:
|
|
@@ -144,7 +146,7 @@ export function parseClaudeRateLimitPane(text: string, nowMs: number = Date.now(
|
|
|
144
146
|
* a false negative, which costs one retry via the resume sweep. That is the cheap direction:
|
|
145
147
|
* a false positive marks a healthy agent blocked, and `isAgentUnavailable` (src/db/
|
|
146
148
|
* agent-predicates.ts) then QUEUES its inbound messages until the fabricated reset — real
|
|
147
|
-
* work idled on a fabricated signal.
|
|
149
|
+
* work idled on a fabricated signal.
|
|
148
150
|
*/
|
|
149
151
|
function parseClaudeUsageLimitModal(text: string, nowMs: number): PaneRateLimit | null {
|
|
150
152
|
const block = livePaneBlock(text);
|
|
@@ -171,25 +173,33 @@ function parseClaudeUsageLimitModal(text: string, nowMs: number): PaneRateLimit
|
|
|
171
173
|
return null;
|
|
172
174
|
}
|
|
173
175
|
|
|
174
|
-
//
|
|
176
|
+
// account-level usage/quota walls as providers word them in an API error message (zai:
|
|
175
177
|
// "Usage limit reached for 5 hour. Your limit will reset at 2026-09-24 03:47:08"). Narrower than
|
|
176
178
|
// LIMIT_PHRASE_RE on purpose: a bare "rate limit" is a per-request throttle, not a quota wall.
|
|
177
|
-
const USAGE_LIMIT_MESSAGE_RE = /usage limit|usage exceeded|quota|limit (?:will )?resets?\b
|
|
179
|
+
const USAGE_LIMIT_MESSAGE_RE = /usage limit|usage exceeded|quota|limit (?:will )?resets?\b/i;
|
|
180
|
+
// the money walls among them (Anthropic: "credit balance is too low", "You have reached your specified
|
|
181
|
+
// API usage limits. You will regain access on ..."). Only these zero a metered API budget; a throttle never does.
|
|
182
|
+
const CREDIT_WALL_MESSAGE_RE = /spend(?:ing)? limit|api usage limits?\b|credit balance[^\n]*too low|insufficient (?:balance|credits?)/i;
|
|
178
183
|
const RESET_AT_TIMESTAMP_RE = /\bresets?\s+(?:at\s+)?(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?\s*(?:Z|[+-]\d{2}:?\d{2}))/i;
|
|
179
184
|
|
|
180
185
|
type ProviderRefusal = { message: string; errorType?: string; statusCode?: number };
|
|
181
186
|
|
|
182
187
|
/** An account-level usage/quota wall: retrying in-session or re-rolling the session cannot clear it. */
|
|
183
188
|
export function isProviderUsageLimit(error: ProviderRefusal): boolean {
|
|
184
|
-
return USAGE_LIMIT_MESSAGE_RE.test(error.message) || /quota|usage_limit|tier_limit/i.test(error.errorType ?? "");
|
|
189
|
+
return isProviderCreditWall(error) || USAGE_LIMIT_MESSAGE_RE.test(error.message) || /quota|usage_limit|tier_limit/i.test(error.errorType ?? "");
|
|
185
190
|
}
|
|
186
191
|
|
|
187
|
-
/**
|
|
192
|
+
/** An account wall of money: exhausted credit or a reached spend limit. */
|
|
193
|
+
export function isProviderCreditWall(error: ProviderRefusal): boolean {
|
|
194
|
+
return CREDIT_WALL_MESSAGE_RE.test(error.message);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** a provider-wide overload (5xx): not a broken session, so never worth a re-roll. */
|
|
188
198
|
export function isProviderOverload(error: ProviderRefusal): boolean {
|
|
189
199
|
return (error.statusCode !== undefined && error.statusCode >= 500) || /overload/i.test(error.errorType ?? "");
|
|
190
200
|
}
|
|
191
201
|
|
|
192
|
-
//
|
|
202
|
+
// per-request throttle wording. Some providers send it on a non-429 status flagged
|
|
193
203
|
// non-retryable (zai: 403 "Rate limit exceeded"); it is still a throttle, never an auth/model failure.
|
|
194
204
|
const THROTTLE_MESSAGE_RE = /rate[\s_-]?limit|too many requests|throttl/i;
|
|
195
205
|
|
|
@@ -206,7 +216,7 @@ function isProviderRateLimited(error: ProviderRefusal): boolean {
|
|
|
206
216
|
}
|
|
207
217
|
|
|
208
218
|
/**
|
|
209
|
-
*
|
|
219
|
+
* the hold a RETRYABLE provider API refusal (429 quota wall, 5xx overload) ends a turn in,
|
|
210
220
|
* instead of a terminal failure: a terminal `error` status resolves runner-core's provider exit,
|
|
211
221
|
* which is how every zai/OpenCode session died ~10 min into a quota wall. The same
|
|
212
222
|
* `blocked/rate_limit` hold the Claude arms raise: runner-core keeps it across the idle, the relay
|
|
@@ -214,10 +224,11 @@ function isProviderRateLimited(error: ProviderRefusal): boolean {
|
|
|
214
224
|
*/
|
|
215
225
|
export function buildProviderRefusalHold(input: ProviderRefusal & { detail: string; source: string; resetAt?: number }, nowMs: number = Date.now()): ProviderState {
|
|
216
226
|
const usageLimit = isProviderRateLimited(input);
|
|
217
|
-
// A provider-stated reset
|
|
227
|
+
// A provider-stated reset beats parsing one out of the message.
|
|
218
228
|
const resetAt = usageLimit ? clampReset(input.resetAt, nowMs) ?? providerLimitResetAt(input.message, nowMs) : undefined;
|
|
219
229
|
return buildRateLimitProviderState({
|
|
220
230
|
kind: usageLimit ? "usage_limit" : "transient_overload",
|
|
231
|
+
usageLimitScope: isProviderCreditWall(input) ? "credit" : isProviderUsageLimit(input) || resetAt !== undefined ? "provider" : "request",
|
|
221
232
|
errorType: input.errorType ?? (input.statusCode !== undefined ? String(input.statusCode) : "provider_error"),
|
|
222
233
|
message: input.detail,
|
|
223
234
|
source: input.source,
|
|
@@ -258,7 +269,7 @@ function livePaneBlock(text: string): string[] {
|
|
|
258
269
|
// Unlike the usage-limit modal, Claude Code renders a transient API error as a CONVERSATION
|
|
259
270
|
// row (`● API Error: 529 Overloaded…`), i.e. in the body, above the composer — so the
|
|
260
271
|
// positional gate above cannot apply here without losing the detection entirely.
|
|
261
|
-
//
|
|
272
|
+
// but the arm had the same own-output hole: the phrase was matched anywhere on any
|
|
262
273
|
// line, so an agent rendering this file's diff or a quoted error tripped it (reproduced live:
|
|
263
274
|
// the fragment `+ane("● API Error: 529 Ove` in a rendered patch marked the agent blocked).
|
|
264
275
|
// Claude's own error row STARTS with the error, after nothing but its bullet/box chrome, so
|
|
@@ -5,12 +5,12 @@ import { join } from "node:path";
|
|
|
5
5
|
import { sanitizeFsName } from "@humwick/sdk/fs-name";
|
|
6
6
|
import { runnerOutboxDirFromEnv } from "./config";
|
|
7
7
|
|
|
8
|
-
/**
|
|
8
|
+
/** unconfirmed self-posts remain cancelled across restart, without an in-memory tombstone cap. */
|
|
9
9
|
export class RefusedPromptStore {
|
|
10
10
|
private readonly db: Database;
|
|
11
11
|
|
|
12
12
|
constructor(options: { agentId: string; dir?: string }) {
|
|
13
|
-
const dir = options.dir ?? runnerOutboxDirFromEnv() ?? join(tmpdir(), "
|
|
13
|
+
const dir = options.dir ?? runnerOutboxDirFromEnv() ?? join(tmpdir(), "humwick-outbox");
|
|
14
14
|
const name = sanitizeFsName(options.agentId, { replacement: "_", maxLen: 180, fallback: "agent" });
|
|
15
15
|
if (dir !== ":memory:") mkdirSync(dir, { recursive: true });
|
|
16
16
|
this.db = new Database(dir === ":memory:" ? dir : join(dir, `refused-prompts-${name}.sqlite`), { create: true });
|
|
@@ -51,7 +51,7 @@ function launchInjectionEvents(input: RunnerLaunchInjectionInput): KeyedRelayInj
|
|
|
51
51
|
: event,
|
|
52
52
|
}));
|
|
53
53
|
}
|
|
54
|
-
//
|
|
54
|
+
// P5.7 — the native-memory lever gets its OWN `memory`-category event (the channel
|
|
55
55
|
// established for the spawn-seam memory block), not a line in the tools summary: this decides
|
|
56
56
|
// whether the agent has one memory store or two, and a SUPPRESSED lever must be visible even on
|
|
57
57
|
// a launch that provisions nothing else and would otherwise return early below.
|
|
@@ -75,7 +75,7 @@ function launchInjectionEvents(input: RunnerLaunchInjectionInput): KeyedRelayInj
|
|
|
75
75
|
},
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
|
-
//
|
|
78
|
+
// a host without agent-redact still launches the agent; that launch must say so loudly.
|
|
79
79
|
// Appended last, so readers that find the first "tools" event still get the provisioning summary.
|
|
80
80
|
const redaction = assembled.redaction;
|
|
81
81
|
const redactionEvents: KeyedRelayInjectionEvent[] = redaction.state === "unavailable" ? [{
|
|
@@ -19,14 +19,14 @@ export function profileAllowsRelayFeature(config: Pick<RunnerSpawnConfig, "agent
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* First-turn briefing (availability + text together)
|
|
22
|
+
* First-turn briefing (availability + text together).: no core copy — the core is
|
|
23
23
|
* server-composed, and hand-started runners get no Relay instructions at all.
|
|
24
24
|
*/
|
|
25
25
|
export function resolveRelayContext(input: {
|
|
26
26
|
slug: string;
|
|
27
27
|
config?: Pick<RunnerSpawnConfig, "agentProfile">;
|
|
28
28
|
mcpServers?: Record<string, ProvisioningMcpServer>;
|
|
29
|
-
/**
|
|
29
|
+
/** the launch's Brave/Jina tools, named per provider in the steer. */
|
|
30
30
|
webTools?: { names: readonly WebToolName[]; claude: boolean };
|
|
31
31
|
}): RelayContextResolution {
|
|
32
32
|
if (input.config && !profileAllowsRelayFeature(input.config, "context")) {
|
|
@@ -47,7 +47,7 @@ export function claudeRelayManual(): string {
|
|
|
47
47
|
/** One line per capability: Brave searches; the local reader fetches, with keyless Jina fallback. */
|
|
48
48
|
export function webToolsSteer(names: readonly WebToolName[], claude: boolean): string {
|
|
49
49
|
return names.map(name => name === "brave"
|
|
50
|
-
? `Web search: use ${claude ? "web_search on the
|
|
50
|
+
? `Web search: use ${claude ? "web_search on the humwick MCP server (load with ToolSearch)" : "web_search on the humwick MCP server"}. Relay tries Tavily, then budget-capped Brave. If the monthly cap is reached, report it; do not retry in a loop.`
|
|
51
51
|
: renderRunnerTemplate("relay.web.fetch", {
|
|
52
52
|
tools: webToolSteerRefs(name, claude), fallback: webToolSteerRefs(name, claude, true),
|
|
53
53
|
})).join("\n");
|
|
@@ -62,7 +62,7 @@ export function claudeReadOnlyRelayContext(): string {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export function relayReplyActionText(messageId: number): string {
|
|
65
|
-
return `
|
|
65
|
+
return `humwick_reply (messageId: ${messageId}, body: "<your reply>"). CLI fallback: humwick /reply ${messageId} "<your reply>"`;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
export function workspaceDepsNote(input: { mode?: string | null; depsMode?: string | null; installCommand?: string | null }): string {
|
|
@@ -92,7 +92,7 @@ export function workspaceSymlinksNote(linked: string[]): string {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
export function workspaceDepsNoteFromEnv(env: Record<string, string | undefined> = process.env): string {
|
|
95
|
-
const json = relayEnv(env, "
|
|
95
|
+
const json = relayEnv(env, "HUMWICK_WORKSPACE_JSON");
|
|
96
96
|
if (!json) return "";
|
|
97
97
|
try {
|
|
98
98
|
const parsed = JSON.parse(json) as { mode?: string; branch?: string; baseRef?: string; deps?: { mode?: string; command?: string }; symlinks?: { linked?: string[] } };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* opt-in compressed long-turn E2E for the shared MCP tool-call forward.
|
|
3
3
|
*
|
|
4
|
-
* Spawned as a CHILD bun process by the
|
|
4
|
+
* Spawned as a CHILD bun process by the HUMWICK_LONG_TURN_E2E=1-gated test in
|
|
5
5
|
* relay-mcp-proxy.test.ts, with BUN_CONFIG_HTTP_IDLE_TIMEOUT=2 compressing Bun's native
|
|
6
6
|
* ~300s HTTP idle watchdog down to ~2s for this process. A stand-in relay withholds ALL
|
|
7
7
|
* response bytes for 6s (mimicking src/mcp/index.ts `tools/call`, which awaits the WHOLE
|
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
* CONTROL — a plain fetch (no `timeout` option) against the slow relay must die with
|
|
11
11
|
* TimeoutError: proves this process's idle watchdog is live and would abort
|
|
12
12
|
* any >idle-timeout forward, i.e. the pre-fix behavior of forward().
|
|
13
|
-
* REAL PATH — a long `
|
|
13
|
+
* REAL PATH — a long `humwick_run` `tools/call` through the REAL HumwickMcpProxy (whose
|
|
14
14
|
* forward() sets `timeout:false`) must complete with the usable
|
|
15
15
|
* `still-running` + `runId` envelope despite sitting silent well past the
|
|
16
16
|
* compressed idle timeout.
|
|
17
17
|
*
|
|
18
18
|
* Exits 0 and prints LONG-TURN E2E PASS on success; non-zero with diagnostics otherwise.
|
|
19
19
|
*/
|
|
20
|
-
import {
|
|
20
|
+
import { HumwickMcpProxy } from "./relay-mcp-proxy";
|
|
21
21
|
|
|
22
22
|
const WITHHOLD_MS = 6_000;
|
|
23
23
|
const SECRET = "long-turn-e2e-secret";
|
|
@@ -69,9 +69,9 @@ const slowRelay = Bun.serve({
|
|
|
69
69
|
// Keep the intentionally silent response shape even for malformed control input.
|
|
70
70
|
}
|
|
71
71
|
await Bun.sleep(WITHHOLD_MS);
|
|
72
|
-
if (!request || request.method !== "tools/call" || request.params?.name !== "
|
|
72
|
+
if (!request || request.method !== "tools/call" || request.params?.name !== "humwick_run") {
|
|
73
73
|
return Response.json(
|
|
74
|
-
{ jsonrpc: "2.0", id, error: { code: -32601, message: "expected
|
|
74
|
+
{ jsonrpc: "2.0", id, error: { code: -32601, message: "expected humwick_run tools/call" } },
|
|
75
75
|
{ status: 400 },
|
|
76
76
|
);
|
|
77
77
|
}
|
|
@@ -85,7 +85,7 @@ const slowRelay = Bun.serve({
|
|
|
85
85
|
jsonrpc: "2.0",
|
|
86
86
|
id,
|
|
87
87
|
result: {
|
|
88
|
-
content: [{ type: "text", text: "
|
|
88
|
+
content: [{ type: "text", text: "humwick_run still running" }],
|
|
89
89
|
structuredContent: {
|
|
90
90
|
run: {
|
|
91
91
|
runId: "long-turn-run-id",
|
|
@@ -121,7 +121,7 @@ async function controlPlainFetchMustTimeOut(relayUrl: string): Promise<void> {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
async function proxyForwardMustComplete(): Promise<void> {
|
|
124
|
-
const proxy = new
|
|
124
|
+
const proxy = new HumwickMcpProxy({
|
|
125
125
|
relayMcpEndpoint: `http://127.0.0.1:${slowRelay.port}/api/mcp`,
|
|
126
126
|
getToken: () => "long-turn-e2e-live-token",
|
|
127
127
|
authSecret: SECRET,
|
|
@@ -132,7 +132,7 @@ async function proxyForwardMustComplete(): Promise<void> {
|
|
|
132
132
|
const startedAt = Date.now();
|
|
133
133
|
// The agent-facing hop is NOT under test here (in production it does not run under the
|
|
134
134
|
// compressed env), so it also opts out of the watchdog to avoid a false failure; the hop
|
|
135
|
-
// being verified is forward
|
|
135
|
+
// being verified is forward inside the proxy, whose fetch carries the fix.
|
|
136
136
|
const agentSideInit: RequestInit & { timeout?: number | false } = {
|
|
137
137
|
method: "POST",
|
|
138
138
|
headers: { "content-type": "application/json", authorization: `Bearer ${SECRET}` },
|
|
@@ -141,7 +141,7 @@ async function proxyForwardMustComplete(): Promise<void> {
|
|
|
141
141
|
id: "long-turn-1",
|
|
142
142
|
method: "tools/call",
|
|
143
143
|
params: {
|
|
144
|
-
name: "
|
|
144
|
+
name: "humwick_run",
|
|
145
145
|
// A real maximum inline window is intentionally longer than this child's compressed
|
|
146
146
|
// watchdog. The stand-in withholds the envelope for 6s, while its unconfigured
|
|
147
147
|
// watchdog dies at ~2-4s, reproducing the pre-fix no-runId failure shape.
|
|
@@ -160,13 +160,13 @@ async function proxyForwardMustComplete(): Promise<void> {
|
|
|
160
160
|
};
|
|
161
161
|
if (!res.ok || !body.result) {
|
|
162
162
|
fail(`proxy forward returned status ${res.status} body ${JSON.stringify(body)} — forward() `
|
|
163
|
-
+ "did not survive the idle watchdog; is the
|
|
163
|
+
+ "did not survive the idle watchdog; is the `timeout:false` flag present?");
|
|
164
164
|
}
|
|
165
165
|
const run = body.result?.structuredContent?.run;
|
|
166
166
|
if (run?.runId !== "long-turn-run-id" || run.status !== "still-running" || run.exitCode !== null) {
|
|
167
|
-
fail(`
|
|
167
|
+
fail(`humwick_run did not return a usable still-running + runId envelope: ${JSON.stringify(body.result)}`);
|
|
168
168
|
}
|
|
169
|
-
console.log(`real path:
|
|
169
|
+
console.log(`real path: humwick_run returned still-running + runId after ${Date.now() - startedAt}ms`);
|
|
170
170
|
} finally {
|
|
171
171
|
proxy.stop();
|
|
172
172
|
}
|