@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/runner-exit.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { formatRunnerExitLine } from "@humwick/sdk";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* the ONLY way the runner process exits. Every exit writes the structured exit record
|
|
5
5
|
* (code + cause) to stderr first: the orchestrator parses it back from the log tail, which
|
|
6
6
|
* outlives a garbage-collected systemd unit. A guard test forbids a direct process.exit anywhere
|
|
7
7
|
* else in the runner.
|
package/src/runner-helpers.ts
CHANGED
|
@@ -22,12 +22,12 @@ interface RuntimeProviderOptions {
|
|
|
22
22
|
effort?: string;
|
|
23
23
|
approvalMode: string;
|
|
24
24
|
headless: boolean;
|
|
25
|
-
/**
|
|
25
|
+
/** a headless-pipe launch has no terminal to read, write, or attach. */
|
|
26
26
|
launchMode?: LaunchMode;
|
|
27
27
|
startedAt: number;
|
|
28
28
|
adapter: ProviderAdapter;
|
|
29
29
|
providerConfig?: ProviderConfig;
|
|
30
|
-
/**
|
|
30
|
+
/** the running process accepts an in-place effort change (see runnerEffortSwitch). */
|
|
31
31
|
effortSwitch?: boolean;
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -167,7 +167,7 @@ export function runnerShouldRestartUnexpectedProviderExit(
|
|
|
167
167
|
&& input.hasTerminalSession;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
//
|
|
170
|
+
// (L3): claude prints its resume banner ANSI-dimmed — the raw log bytes are
|
|
171
171
|
// `\x1b[2mclaude --resume <uuid>\x1b[22m`, so `claude` is immediately preceded by the SGR
|
|
172
172
|
// escape's trailing `m`. A leading `\b` in the extraction regex finds no word boundary
|
|
173
173
|
// (`m`→`c` are both word chars) and yields zero matches, stranding the resume id as null.
|
|
@@ -286,7 +286,7 @@ export function commandTimeoutMs(params: Record<string, unknown>, fallback = 10_
|
|
|
286
286
|
return Math.min(raw, 60_000);
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
-
//
|
|
289
|
+
// the Codex adapter rejects a delivery attempted while a turn is already in progress
|
|
290
290
|
// (Codex has no queue-while-busy the way Claude's input buffer does), so the runner re-queues
|
|
291
291
|
// and re-drains once the turn ends. That rejection is expected flow control, not a fault — the
|
|
292
292
|
// drain loop retries roughly once a second during a long turn, so it must not spam the log.
|
|
@@ -298,14 +298,14 @@ export function shouldLogDeliveryFailure(error: unknown): boolean {
|
|
|
298
298
|
&& message !== CODEX_TURN_IN_PROGRESS_MESSAGE;
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
-
/** The same rejection, seen from the COMMAND side
|
|
301
|
+
/** The same rejection, seen from the COMMAND side: a serialized boundary payload that
|
|
302
302
|
* met the turn the payload before it started is deferred until that turn ends, not failed. */
|
|
303
303
|
export function isTurnInProgressDeliveryError(error: unknown): boolean {
|
|
304
304
|
return errMessage(error) === CODEX_TURN_IN_PROGRESS_MESSAGE;
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
/** Command types that put a payload into the provider's single input channel, and therefore take a
|
|
308
|
-
* `ProviderInputQueue` ticket at dispatch so two of them can never interleave
|
|
308
|
+
* `ProviderInputQueue` ticket at dispatch so two of them can never interleave. */
|
|
309
309
|
export const PROVIDER_INPUT_COMMAND_TYPES: ReadonlySet<string> = new Set(["agent.clearContext", "agent.injectContext", "prompt.inject"]);
|
|
310
310
|
|
|
311
311
|
export function runtimeTokenRenewDelayMs(expiresAtSeconds: number, nowMs = Date.now()): number | undefined {
|
|
@@ -426,7 +426,7 @@ function runtimeProviderTerminalCapabilities(options: RuntimeProviderOptions): P
|
|
|
426
426
|
return {};
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
-
//
|
|
429
|
+
// the agent-card projection now DESCRIBES the assembled launch (same source the
|
|
430
430
|
// adapter turns into args), so it can't drift from reality. `config`/`providerConfig` are
|
|
431
431
|
// the spawn inputs; when present we assemble (pure — no disk writes here) and report that.
|
|
432
432
|
export function appliedAgentProfileMetadata(
|
|
@@ -512,7 +512,7 @@ export function isHttpAuthError(error: unknown): boolean {
|
|
|
512
512
|
return status === 401 || status === 403;
|
|
513
513
|
}
|
|
514
514
|
|
|
515
|
-
//
|
|
515
|
+
// outbox delivery-error classification. The runner outboxes must never poison
|
|
516
516
|
// (permanently drop) an event over a CONNECTION-class failure: report-up chat rides the
|
|
517
517
|
// session outbox, so a poisoned row is real message loss. These predicates split a send()
|
|
518
518
|
// rejection into TRANSIENT (connect-phase transport failure, HTTP 5xx, or 429) versus
|
|
@@ -537,7 +537,7 @@ function httpErrorStatus(error: unknown): number | undefined {
|
|
|
537
537
|
return typeof status === "number" ? status : undefined;
|
|
538
538
|
}
|
|
539
539
|
|
|
540
|
-
// TRANSIENT for the outbox retry policy
|
|
540
|
+
// TRANSIENT for the outbox retry policy: connection-class transport failures plus
|
|
541
541
|
// HTTP 5xx/429 (relay momentarily overloaded/restarting). Transient failures NEVER poison on
|
|
542
542
|
// attempt count — the outbox parks the head on a slow periodic re-drive until the relay returns.
|
|
543
543
|
export function isTransientDeliveryError(error: unknown): boolean {
|
|
@@ -546,7 +546,7 @@ export function isTransientDeliveryError(error: unknown): boolean {
|
|
|
546
546
|
return status !== undefined && (status >= 500 || status === 429);
|
|
547
547
|
}
|
|
548
548
|
|
|
549
|
-
// DETERMINISTIC client rejection
|
|
549
|
+
// DETERMINISTIC client rejection: the relay is up, parsed the payload, and will refuse
|
|
550
550
|
// it every time. Poison immediately so one genuinely-bad row cannot stall the queue.
|
|
551
551
|
export function isDeterministicClientError(error: unknown): boolean {
|
|
552
552
|
const status = httpErrorStatus(error);
|
package/src/runner-insights.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { renderContinuationArchive, type WorkspaceMetadata } from "@humwick/sdk";
|
|
2
2
|
import type { ManagedProcess, ProviderAdapter } from "./adapter";
|
|
3
3
|
import type { Outbox } from "./outbox";
|
|
4
4
|
import type { SessionDestroyReason } from "./session-destroy";
|
|
@@ -60,7 +60,9 @@ export class RunnerInsights {
|
|
|
60
60
|
const segment = archive.slice(this.archiveObservedChars).trim();
|
|
61
61
|
this.archiveObservedChars = archive.length;
|
|
62
62
|
if (!segment) return;
|
|
63
|
-
const
|
|
63
|
+
const rendered = renderContinuationArchive(segment);
|
|
64
|
+
if (!rendered.trim()) return;
|
|
65
|
+
const bounded = boundContinuationArchiveSegment(rendered);
|
|
64
66
|
if (bounded.droppedBytes > 0) this.deps.sessionLog(`continuation archive truncated at ${bounded.keptBytes} bytes; dropped ${bounded.droppedBytes} bytes (${reason})`);
|
|
65
67
|
this.deps.getOutbox().enqueue({
|
|
66
68
|
kind: "continuation-archive",
|
package/src/runner-mcp-proxy.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { sharedCallmuxToolCallTimeoutMsFromEnv, type HumwickHttpClient, webSearchEnabledFromEnv, webCallLogEnabledFromEnv, errMessage, type WebToolCall, type AgentProfile } from "@humwick/sdk";
|
|
2
|
-
import {
|
|
2
|
+
import { HumwickMcpProxy, type RelayMcpProxyOptions } from "./relay-mcp-proxy";
|
|
3
3
|
import { runnerManagedRelayOnDemandTools } from "./mcp-tool-deferral";
|
|
4
4
|
import { logger } from "./logger";
|
|
5
5
|
import type { WebToolLogOptions } from "./web-tool-call-proxy";
|
|
@@ -9,9 +9,9 @@ export function runnerMcpHiddenTools(provider: string, profile: AgentProfile | u
|
|
|
9
9
|
return profile?.webTools?.brave === false ? new Set([...(hidden ?? []), "web_search"]) : hidden;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export function startRunnerMcpProxy(options: RelayMcpProxyOptions): { proxy?:
|
|
12
|
+
export function startRunnerMcpProxy(options: RelayMcpProxyOptions): { proxy?: HumwickMcpProxy; endpoint?: string } {
|
|
13
13
|
try {
|
|
14
|
-
const proxy = new
|
|
14
|
+
const proxy = new HumwickMcpProxy(options);
|
|
15
15
|
const endpoint = proxy.start().url;
|
|
16
16
|
logger.info("mcp-proxy", `runner MCP proxy listening at ${endpoint} (worktree=${options.initialContext?.isolatedWorktree ?? false})`);
|
|
17
17
|
return { proxy, endpoint };
|
|
@@ -34,9 +34,9 @@ export function buildRunnerSpawnConfig(
|
|
|
34
34
|
provider: source.provider,
|
|
35
35
|
...(source.model ? { model: source.model } : {}),
|
|
36
36
|
...(source.effort ? { effort: source.effort } : {}),
|
|
37
|
-
//
|
|
37
|
+
// positive-only gate mirrors the CLI parse; an unset/invalid value stays absent.
|
|
38
38
|
...(typeof source.contextWindowTokens === "number" && Number.isSafeInteger(source.contextWindowTokens) && source.contextWindowTokens > 0 ? { contextWindowTokens: source.contextWindowTokens } : {}),
|
|
39
|
-
//
|
|
39
|
+
// the record arrives pre-parsed by the shared parser at the CLI seam; thread it
|
|
40
40
|
// verbatim. The ADAPTER applies precedence (payload > manifest > undefined), not this builder.
|
|
41
41
|
...(source.modelRecord ? { modelRecord: source.modelRecord } : {}),
|
|
42
42
|
runnerId: source.runnerId,
|
|
@@ -58,7 +58,7 @@ export function buildRunnerSpawnConfig(
|
|
|
58
58
|
...(source.tmuxSession ? { tmuxSession: source.tmuxSession } : {}),
|
|
59
59
|
providerArgs: runtime.resumeId ? [...source.providerArgs, "--resume", runtime.resumeId] : [...source.providerArgs],
|
|
60
60
|
providerConfig: source.providerConfig,
|
|
61
|
-
env: writeChildEnv({}, { ...runtime.env }),
|
|
61
|
+
env: writeChildEnv({}, { ...runtime.env }, []),
|
|
62
62
|
controlPort: runtime.controlPort,
|
|
63
63
|
...(runtime.relayMcpEndpoint ? { relayMcpEndpoint: runtime.relayMcpEndpoint } : {}),
|
|
64
64
|
...(runtime.mcpProxySecret ? { mcpProxySecret: runtime.mcpProxySecret } : {}),
|
|
@@ -19,7 +19,7 @@ function replaceFile(path: string, value: string): void {
|
|
|
19
19
|
* Publish the runner's live credential to CLI children. A provider's process
|
|
20
20
|
* environment is immutable after it starts, but each CLI invocation can read
|
|
21
21
|
* these stable paths afresh. The orchestrator creates the initial files when
|
|
22
|
-
* it materializes the session launch script
|
|
22
|
+
* it materializes the session launch script.
|
|
23
23
|
*/
|
|
24
24
|
export function persistRuntimeTokenFiles(token: string, expiresAt?: number): void {
|
|
25
25
|
const tokenFile = runtimeTokenFileFromEnv();
|
|
@@ -28,7 +28,7 @@ export class TokenRotation {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
31
|
+
* re-derive the runner's token from the agent's CURRENT profile at a clear/compact boundary and
|
|
32
32
|
* report the outcome for the command result. Never throws: a failed re-mint keeps the still-valid token.
|
|
33
33
|
*/
|
|
34
34
|
export async function rederiveRuntimeToken(
|
package/src/service-access.ts
CHANGED
|
@@ -34,7 +34,7 @@ export async function withServiceAccessLaunch<T>(
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
37
|
+
* the provider gets a loopback proxy URL and local credential for its whole life; the session
|
|
38
38
|
* token behind it renews at half its remaining lifetime (retrying each minute) until the launch ends.
|
|
39
39
|
* A 409 means the launch is no longer live, so exit cleanup owns the session from there.
|
|
40
40
|
*/
|
|
@@ -57,7 +57,7 @@ export async function startServiceAccessRenewal(http: LaunchHttp, identity: Laun
|
|
|
57
57
|
hop.setToken(renewed.token);
|
|
58
58
|
schedule((renewed.expiresAt - Date.now()) / 2);
|
|
59
59
|
} catch (error) {
|
|
60
|
-
console.warn(`[
|
|
60
|
+
console.warn(`[humwick] ${error instanceof Error ? error.message : "Vault access renewal failed"}; retrying in ${RENEW_RETRY_MS / 1000}s`);
|
|
61
61
|
schedule(RENEW_RETRY_MS);
|
|
62
62
|
}
|
|
63
63
|
};
|
package/src/session-destroy.ts
CHANGED
|
@@ -2,7 +2,7 @@ export type SessionDestroyReason = "compact" | "clear" | "restart" | "shutdown"
|
|
|
2
2
|
|
|
3
3
|
export type LifecycleAction =
|
|
4
4
|
| "shutting-down" | "killing" | "restarting"
|
|
5
|
-
//
|
|
5
|
+
// a TERMINAL marker: the graceful+force-kill path ran and the provider process
|
|
6
6
|
// still could not be confirmed dead. The runner deliberately keeps the agent registered
|
|
7
7
|
// (a roster removal must imply the process is gone), so this pill replaces an indefinite
|
|
8
8
|
// "shutting-down" with an operator-visible "the kill did not take" state.
|
package/src/session-insights.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Provider-agnostic core for the
|
|
1
|
+
// Provider-agnostic core for the context-gathering signal.
|
|
2
2
|
//
|
|
3
3
|
// The transcript *format* is provider-specific (Claude JSONL, Codex app-server items,
|
|
4
4
|
// future providers), so each adapter normalizes its session into the same `SessionEvent`
|
package/src/session-scratch.ts
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { appendIgnoreEntries, resolveRepoConfigPath } from "@humwick/sdk/repo-config-file";
|
|
2
|
+
import { accessSync, constants, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
2
3
|
import { chmod, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
3
4
|
import { tmpdir } from "node:os";
|
|
4
5
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
6
|
import { sanitizeFsName } from "@humwick/sdk/fs-name";
|
|
6
7
|
import { spawnSyncBounded } from "./bounded-spawn-sync";
|
|
7
8
|
|
|
8
|
-
const SCRATCH_DIR_NAME = ".
|
|
9
|
-
|
|
10
|
-
// base, matching git's gitignore semantics.
|
|
11
|
-
const EXCLUDE_ENTRY = "/.agent-relay/";
|
|
9
|
+
const SCRATCH_DIR_NAME = ".humwick";
|
|
10
|
+
const SCRATCH_GENERATIONS = [SCRATCH_DIR_NAME, ".agent-relay"];
|
|
12
11
|
const LAUNCHER_DIR = join(tmpdir(), `agent-relay-launchers-${typeof process.getuid === "function" ? process.getuid() : "user"}`);
|
|
13
12
|
const DEFAULT_LAUNCHER_SWEEP_MIN_AGE_MS = 24 * 60 * 60 * 1000;
|
|
14
13
|
|
|
15
14
|
export interface SessionScratchLayout {
|
|
16
|
-
baseDir: string; // dir that contains .
|
|
17
|
-
rootDir: string; // <base>/.
|
|
18
|
-
sessionsDir: string; // <base>/.
|
|
19
|
-
sessionDir: string; // <base>/.
|
|
20
|
-
tmpDir: string; // <base>/.
|
|
15
|
+
baseDir: string; // dir that contains .humwick (cwd, or fallback base dir)
|
|
16
|
+
rootDir: string; // <base>/.humwick
|
|
17
|
+
sessionsDir: string; // <base>/.humwick/sessions
|
|
18
|
+
sessionDir: string; // <base>/.humwick/sessions/<id>
|
|
19
|
+
tmpDir: string; // <base>/.humwick/sessions/<id>/tmp
|
|
21
20
|
replyFile: string; // <tmp>/reply.md
|
|
22
21
|
}
|
|
23
22
|
|
|
@@ -55,8 +54,8 @@ export function resolveScratchBase(cwd: string, fallbackBaseDir?: string): strin
|
|
|
55
54
|
return cwd;
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
function sessionScratchLayout(baseDir: string, agentId: string): SessionScratchLayout {
|
|
59
|
-
const rootDir = join(baseDir,
|
|
57
|
+
function sessionScratchLayout(baseDir: string, agentId: string, directory = dirname(resolveRepoConfigPath(baseDir, `${SCRATCH_DIR_NAME}/sessions`))): SessionScratchLayout {
|
|
58
|
+
const rootDir = join(baseDir, directory);
|
|
60
59
|
const sessionsDir = join(rootDir, "sessions");
|
|
61
60
|
const sessionDir = join(sessionsDir, agentId);
|
|
62
61
|
const tmpDir = join(sessionDir, "tmp");
|
|
@@ -80,38 +79,26 @@ function gitExcludePath(baseDir: string): string | null {
|
|
|
80
79
|
}
|
|
81
80
|
}
|
|
82
81
|
|
|
83
|
-
//
|
|
84
|
-
function appendIgnoreEntry(file: string): void {
|
|
85
|
-
let existing = "";
|
|
86
|
-
try {
|
|
87
|
-
existing = readFileSync(file, "utf8");
|
|
88
|
-
} catch {}
|
|
89
|
-
const wanted = EXCLUDE_ENTRY.trim();
|
|
90
|
-
if (existing.split("\n").some((line) => line.trim() === wanted)) return;
|
|
91
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
92
|
-
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
93
|
-
appendFileSync(file, `${prefix}${EXCLUDE_ENTRY}\n`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Ensure `.agent-relay/` is locally ignored. Prefers .git/info/exclude (git's
|
|
82
|
+
// Ensure session and browser tool scratch are locally ignored. Prefers .git/info/exclude (git's
|
|
97
83
|
// local, uncommitted ignore list) so no tracked file is ever modified; falls
|
|
98
84
|
// back to .gitignore only when baseDir is not a git repo. Returns which file was
|
|
99
85
|
// used (for logging/tests).
|
|
100
86
|
export function ensureScratchIgnored(baseDir: string): "exclude" | "gitignore" {
|
|
87
|
+
const entries = [`/${resolveRepoConfigPath(baseDir, `${SCRATCH_DIR_NAME}/sessions`)}/`, ".playwright-cli/"];
|
|
101
88
|
const excludePath = gitExcludePath(baseDir);
|
|
102
89
|
if (excludePath) {
|
|
103
|
-
|
|
90
|
+
appendIgnoreEntries(excludePath, entries);
|
|
104
91
|
return "exclude";
|
|
105
92
|
}
|
|
106
|
-
|
|
93
|
+
appendIgnoreEntries(join(baseDir, ".gitignore"), entries);
|
|
107
94
|
return "gitignore";
|
|
108
95
|
}
|
|
109
96
|
|
|
110
97
|
// SessionStart: create the session tmp dir and ensure local ignore. Dir
|
|
111
98
|
// creation is the load-bearing part; ignore setup is best-effort.
|
|
112
99
|
//
|
|
113
|
-
//
|
|
114
|
-
// injection's full text,
|
|
100
|
+
// P1 — this dir can hold sensitive spilled content (an over-cap memory/continuation
|
|
101
|
+
// injection's full text, the monitor over-limit handling), so it's session-private: 0700, not
|
|
115
102
|
// the default (umask-dependent, typically 0755/0775). `mode` here is safe from umask stripping it
|
|
116
103
|
// back down: umask can only CLEAR bits from the requested mode, and 0700 already has none of the
|
|
117
104
|
// group/other bits a realistic umask would clear — so this always lands exactly on 0700, with no
|
|
@@ -139,7 +126,7 @@ export function launcherScriptPathForSession(sessionName: string, launcherDir?:
|
|
|
139
126
|
}
|
|
140
127
|
|
|
141
128
|
// Named-pipe path the launcher script blocks on before exec'ing the real provider
|
|
142
|
-
// command
|
|
129
|
+
// command — lets the caller attach `tmux pipe-pane` while the pane is still
|
|
143
130
|
// idle, so a process that exits within milliseconds can't slip output past it.
|
|
144
131
|
export function launcherGatePathForSession(sessionName: string, launcherDir?: string): string {
|
|
145
132
|
return `${launcherScriptPathForSession(sessionName, launcherDir)}.gate`;
|
|
@@ -181,8 +168,8 @@ function dedupePaths(paths: Array<string | undefined>): string[] {
|
|
|
181
168
|
|
|
182
169
|
// SessionEnd: remove this session's dir from every candidate base.
|
|
183
170
|
export function reapSessionScratch(target: SessionScratchTarget): void {
|
|
184
|
-
for (const base of dedupeBases([target.cwd, target.fallbackBaseDir])) {
|
|
185
|
-
const { sessionDir } = sessionScratchLayout(base, target.agentId);
|
|
171
|
+
for (const { base, generation } of dedupeBases([target.cwd, target.fallbackBaseDir]).flatMap(base => SCRATCH_GENERATIONS.map(generation => ({ base, generation })))) {
|
|
172
|
+
const { sessionDir } = sessionScratchLayout(base, target.agentId, generation);
|
|
186
173
|
try {
|
|
187
174
|
rmSync(sessionDir, { recursive: true, force: true });
|
|
188
175
|
} catch {}
|
|
@@ -226,7 +213,7 @@ interface SweepOptions {
|
|
|
226
213
|
|
|
227
214
|
interface LauncherSweepOptions {
|
|
228
215
|
// Agent ids to keep (currently-known agents + self). Launcher scripts embed
|
|
229
|
-
//
|
|
216
|
+
// HUMWICK_ID, so a stale sweep can safely remove only scripts whose agent
|
|
230
217
|
// is no longer live.
|
|
231
218
|
keepAgentIds: Set<string>;
|
|
232
219
|
launcherDir?: string;
|
|
@@ -243,7 +230,7 @@ function maybeShellUnquote(value: string): string {
|
|
|
243
230
|
|
|
244
231
|
async function launcherAgentId(scriptPath: string): Promise<string | null> {
|
|
245
232
|
try {
|
|
246
|
-
const match = (await Bun.file(scriptPath).text()).match(/^export
|
|
233
|
+
const match = (await Bun.file(scriptPath).text()).match(/^export (?:HUMWICK|AGENT_RELAY)_ID=(.+)$/m);
|
|
247
234
|
if (!match) return null;
|
|
248
235
|
const agentId = maybeShellUnquote(match[1] ?? "");
|
|
249
236
|
return agentId || null;
|
|
@@ -259,8 +246,8 @@ export async function sweepStaleSessionsAsync(opts: SweepOptions): Promise<strin
|
|
|
259
246
|
const minAge = opts.minAgeMs ?? 60 * 60 * 1000;
|
|
260
247
|
const maxEntries = opts.maxEntries ?? 100;
|
|
261
248
|
const removed: string[] = [];
|
|
262
|
-
for (const base of dedupeBases([opts.cwd, opts.fallbackBaseDir])) {
|
|
263
|
-
const sessionsDir = join(base,
|
|
249
|
+
for (const { base, generation } of dedupeBases([opts.cwd, opts.fallbackBaseDir]).flatMap(base => SCRATCH_GENERATIONS.map(generation => ({ base, generation })))) {
|
|
250
|
+
const sessionsDir = join(base, generation, "sessions");
|
|
264
251
|
let ids: string[];
|
|
265
252
|
try { ids = await readdir(sessionsDir); } catch { continue; }
|
|
266
253
|
for (const id of ids.slice(0, maxEntries)) {
|
package/src/spawn-input.ts
CHANGED
|
@@ -8,13 +8,13 @@ export interface RunnerCliOptions {
|
|
|
8
8
|
provider: SpawnProvider;
|
|
9
9
|
model?: string;
|
|
10
10
|
effort?: string;
|
|
11
|
-
/**
|
|
11
|
+
/** effective-catalog window resolved server-side; preferred over the static manifest. */
|
|
12
12
|
contextWindowTokens?: number;
|
|
13
|
-
/**
|
|
13
|
+
/** the full server-resolved model record (protocol ≥ 2); precedence payload > manifest. */
|
|
14
14
|
modelRecord?: ResolvedModelRecord;
|
|
15
15
|
headless: boolean;
|
|
16
16
|
interactive: boolean;
|
|
17
|
-
/**
|
|
17
|
+
/** interactive|headless driver; absent keeps the provider's legacy selection. */
|
|
18
18
|
launchMode?: LaunchMode;
|
|
19
19
|
cwd?: string;
|
|
20
20
|
rig?: string;
|
|
@@ -64,7 +64,7 @@ export function parseRunnerCliArgs(
|
|
|
64
64
|
let model: string | undefined;
|
|
65
65
|
let effort: string | undefined;
|
|
66
66
|
let contextWindowTokens: number | undefined;
|
|
67
|
-
//
|
|
67
|
+
// raw --model-record-* fields, assembled then parsed ONCE through the shared parser
|
|
68
68
|
// (the same parser the orchestrator uses), so CLI parsing can never drift from the payload seam.
|
|
69
69
|
const modelRecordRaw: Record<string, unknown> = {};
|
|
70
70
|
let cwd: string | undefined;
|
|
@@ -91,13 +91,13 @@ export function parseRunnerCliArgs(
|
|
|
91
91
|
}
|
|
92
92
|
else if (arg === "--model" && ownArgs[i + 1]) model = ownArgs[++i];
|
|
93
93
|
else if (arg === "--effort" && ownArgs[i + 1]) effort = ownArgs[++i];
|
|
94
|
-
//
|
|
94
|
+
// server-resolved effective-catalog window; positive-integer gate keeps a malformed
|
|
95
95
|
// value from ever becoming a fabricated denominator (openCodeContextState would clamp anyway).
|
|
96
96
|
else if (arg === "--context-window-tokens" && ownArgs[i + 1]) {
|
|
97
97
|
const parsed = Number(ownArgs[++i]);
|
|
98
98
|
contextWindowTokens = Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
99
99
|
}
|
|
100
|
-
//
|
|
100
|
+
// full-record fields (protocol ≥ 2). Values ride raw; the shared parser at the end
|
|
101
101
|
// applies the SAME gates as the orchestrator seam. --model-record-threshold carries a JSON
|
|
102
102
|
// threshold object; a malformed JSON is untrusted and drops the whole record field set to
|
|
103
103
|
// the parser's verdict (never a fabricated threshold).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ReconnectionManager,
|
|
1
|
+
import { ReconnectionManager, HUMWICK_TOKEN_HEADER, errMessage } from "@humwick/sdk";
|
|
2
2
|
import type { SettingEntry } from "@humwick/sdk";
|
|
3
3
|
import { isRetryableTransportError } from "./transport-errors";
|
|
4
4
|
|
|
@@ -16,7 +16,7 @@ function transient(error: unknown): boolean {
|
|
|
16
16
|
|| (error instanceof Error && error.name === "TimeoutError");
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
// Use registration's backoff and initial-handshake budget for managed bootstrap reads
|
|
19
|
+
// Use registration's backoff and initial-handshake budget for managed bootstrap reads.
|
|
20
20
|
export async function fetchStartupConfig(url: URL, token: string | undefined, timeoutMs: number, deps: Partial<StartupFetchDeps> = {}) {
|
|
21
21
|
const d = { now: Date.now, sleep: Bun.sleep, reconnect: new ReconnectionManager(), ...deps };
|
|
22
22
|
const deadline = d.deadlineAt ?? d.now() + timeoutMs;
|
|
@@ -24,7 +24,7 @@ export async function fetchStartupConfig(url: URL, token: string | undefined, ti
|
|
|
24
24
|
if (d.deadlineAt !== undefined && d.now() >= deadline) throw new Error("provider config retry budget expired");
|
|
25
25
|
try {
|
|
26
26
|
const response = await fetch(url, {
|
|
27
|
-
headers: token ? { [
|
|
27
|
+
headers: token ? { [HUMWICK_TOKEN_HEADER]: token } : undefined,
|
|
28
28
|
signal: AbortSignal.timeout(timeoutMs > 0 ? Math.max(1, Math.min(5_000, deadline - d.now())) : 5_000),
|
|
29
29
|
});
|
|
30
30
|
if (response.status >= 500) {
|
|
@@ -40,7 +40,7 @@ export async function fetchStartupConfig(url: URL, token: string | undefined, ti
|
|
|
40
40
|
const remaining = deadline - d.now();
|
|
41
41
|
if (!transient(error) || remaining <= 0) throw error;
|
|
42
42
|
const delay = Math.min(d.reconnect.nextDelay(), remaining);
|
|
43
|
-
console.warn(`[
|
|
43
|
+
console.warn(`[humwick] provider config relay read failed (${errMessage(error)}); retrying in ${delay}ms.`);
|
|
44
44
|
await d.sleep(delay);
|
|
45
45
|
if (d.now() >= deadline) throw error;
|
|
46
46
|
}
|
package/src/subreaper.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
2
|
import { dlopen } from "bun:ffi";
|
|
3
|
-
import {
|
|
3
|
+
import { isManagedSessionUnit } from "@humwick/sdk/systemd-unit";
|
|
4
4
|
|
|
5
5
|
const REAP_INTERVAL_MS = 5_000;
|
|
6
6
|
const ZOMBIE_GRACE_MS = 5_000;
|
|
@@ -9,9 +9,7 @@ let reaperStarted = false;
|
|
|
9
9
|
export function hasSystemdSessionOwner(cgroup: string, parentComm: string): boolean {
|
|
10
10
|
if (parentComm.trim() !== "systemd") return false;
|
|
11
11
|
return cgroup.split("\n").some((line) => line.startsWith("0::/") && line.slice(4).split("/").some((part) =>
|
|
12
|
-
part
|
|
13
|
-
&& part.length > SYSTEMD_MANAGED_SESSION_PREFIX.length + ".service".length
|
|
14
|
-
&& part.endsWith(".service")));
|
|
12
|
+
isManagedSessionUnit(part)));
|
|
15
13
|
}
|
|
16
14
|
|
|
17
15
|
function zombieChildren(): Map<number, string> {
|
|
@@ -39,7 +37,7 @@ function zombieChildren(): Map<number, string> {
|
|
|
39
37
|
}
|
|
40
38
|
|
|
41
39
|
// Dedicated systemd sessions already own descendant teardown via KillMode=control-group.
|
|
42
|
-
// Let their manager promptly reap orphans
|
|
40
|
+
// Let their manager promptly reap orphans; only unsupervised runners need
|
|
43
41
|
// to adopt them here. Darwin has no process-local equivalent; launchd remains the reaper.
|
|
44
42
|
export function enableRunnerSubreaper(): boolean {
|
|
45
43
|
if (process.platform !== "linux") return false;
|
package/src/test-run-dedup.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { relayEnv } from "@humwick/sdk/env";
|
|
2
|
-
//
|
|
2
|
+
// the lease owns launch-time replacement. The sweep only deduplicates
|
|
3
3
|
// unleased runs with the same captured checkout/revision; ancestry proves ownership,
|
|
4
4
|
// but cannot prove two commands tested the same tree. Live leases protect whole runs.
|
|
5
5
|
import { execFile } from "node:child_process";
|
|
@@ -12,7 +12,7 @@ import { GATE_RUN_AGENT_ENV, GATE_RUN_CONTEXT_ENV, GATE_RUN_HELD_ENV, readLiveGa
|
|
|
12
12
|
import type { ManagedProcess } from "./adapter";
|
|
13
13
|
|
|
14
14
|
// Re-exported so the sweep and both lease front doors share ONE definition of "is this a test run".
|
|
15
|
-
// It moved to the sdk in
|
|
15
|
+
// It moved to the sdk in because the orchestrator (a separate workspace that cannot import
|
|
16
16
|
// runner/) needs the same rule; keeping the name exported here keeps every existing caller intact.
|
|
17
17
|
export { isTestRunCommand };
|
|
18
18
|
|
|
@@ -55,7 +55,7 @@ function validPid(pid: unknown): pid is number {
|
|
|
55
55
|
* Most providers expose that child as both `process.pid` and `pid`. Interactive
|
|
56
56
|
* Codex is intentionally different: `pid` is the visible remote TUI, while
|
|
57
57
|
* `process` is the App Server that owns Codex turns and their command children.
|
|
58
|
-
* Rooting at the TUI made
|
|
58
|
+
* Rooting at the TUI made a no-op for exactly those Codex sessions. Prefer
|
|
59
59
|
* the runner-owned execution child, then retain the historical managed-pid and
|
|
60
60
|
* adapter-specific fallbacks for tmux-backed providers.
|
|
61
61
|
*/
|
|
@@ -104,7 +104,7 @@ export function parseProcessTable(text: string): ProcessRow[] {
|
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
106
|
* Pids whose ENVIRONMENT names `agentId` as their owner — the half of ownership that ancestry
|
|
107
|
-
* cannot express
|
|
107
|
+
* cannot express. A `humwick_run` gate run is a child of the orchestrator daemon, not of this
|
|
108
108
|
* agent's provider pane, so it is reachable only through the tag the orchestrator stamps on it.
|
|
109
109
|
*
|
|
110
110
|
* Linux reads `/proc/<pid>/environ` directly: no subprocess, and unreadable entries (another user's
|
|
@@ -144,7 +144,7 @@ export async function collectAgentTaggedPids(agentId: string, platform: string =
|
|
|
144
144
|
const match = line.match(/^\s*(\d+)\s+(.*)$/);
|
|
145
145
|
if (!match) continue;
|
|
146
146
|
// `ps -E` renders the environment space-separated, so a whole-token test is the analogue of
|
|
147
|
-
// the NUL split above: `
|
|
147
|
+
// the NUL split above: `HUMWICK_ID=x` must not match agent `xy`.
|
|
148
148
|
if (!match[2]!.split(/\s+/).includes(needle)) continue;
|
|
149
149
|
const pid = Number(match[1]);
|
|
150
150
|
if (Number.isInteger(pid) && pid > 1) pids.push(pid);
|
|
@@ -177,7 +177,7 @@ const EMPTY_PLAN: TestRunDedupPlan = { staleRoots: [], killPids: [] };
|
|
|
177
177
|
* single healthy run presents exactly one root and is never touched.
|
|
178
178
|
*/
|
|
179
179
|
export interface TestRunDedupScope {
|
|
180
|
-
/** Pids proven to belong to this agent by their ENVIRONMENT rather than by ancestry
|
|
180
|
+
/** Pids proven to belong to this agent by their ENVIRONMENT rather than by ancestry.
|
|
181
181
|
* These extend the scope; they are never used as tree roots, because a tagged process is
|
|
182
182
|
* usually INSIDE the run rather than above it. */
|
|
183
183
|
ownedPids?: readonly number[];
|
|
@@ -227,7 +227,7 @@ export function planTestRunDedup(
|
|
|
227
227
|
};
|
|
228
228
|
|
|
229
229
|
// Ownership is the UNION of the two proofs: descended from this session's provider process, or
|
|
230
|
-
// carrying this agent's id in its environment. Neither alone covers both launch paths
|
|
230
|
+
// carrying this agent's id in its environment. Neither alone covers both launch paths.
|
|
231
231
|
const sessionPids = new Set<number>(byPid.has(rootPid) ? subtree(rootPid) : []);
|
|
232
232
|
for (const pid of ownedInTable) sessionPids.add(pid);
|
|
233
233
|
const candidates = new Set<number>();
|
|
@@ -255,7 +255,7 @@ export function planTestRunDedup(
|
|
|
255
255
|
if (runRoots.length < 2) return EMPTY_PLAN;
|
|
256
256
|
|
|
257
257
|
// A lease may belong to a child of the outer shell. Spare the WHOLE run, including
|
|
258
|
-
// ancestors and descendants, rather than signalling its owner indirectly
|
|
258
|
+
// ancestors and descendants, rather than signalling its owner indirectly.
|
|
259
259
|
const leased = new Set(scope.leasedPids ?? []);
|
|
260
260
|
const groups = new Map<string, number[]>();
|
|
261
261
|
const leasedRoots = new Set<number>();
|
|
@@ -315,7 +315,7 @@ export async function readProcessTable(timeoutMs: number = PROCESS_TABLE_TIMEOUT
|
|
|
315
315
|
return stdout;
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
-
/** Only launch-time metadata can distinguish revisions; today's HEAD cannot
|
|
318
|
+
/** Only launch-time metadata can distinguish revisions; today's HEAD cannot. */
|
|
319
319
|
export function readTestRunContext(pid: number): { context?: string; leased?: boolean } {
|
|
320
320
|
try {
|
|
321
321
|
const env = Object.fromEntries(readFileSync(`/proc/${pid}/environ`, "utf8").split("\0").map((entry) => {
|
|
@@ -329,7 +329,7 @@ export function readTestRunContext(pid: number): { context?: string; leased?: bo
|
|
|
329
329
|
export interface TestRunDedupDeps {
|
|
330
330
|
/** Resolve this session's provider pid. undefined ⇒ ancestry proves nothing; the env tag may. */
|
|
331
331
|
rootPid: () => number | undefined;
|
|
332
|
-
/** This agent's id, for the env-tag ownership proof
|
|
332
|
+
/** This agent's id, for the env-tag ownership proof. Absent ⇒ ancestry-only, as before. */
|
|
333
333
|
agentId?: () => string | undefined;
|
|
334
334
|
/** The repo's configured gate commands, so a foreign gate runner is classified exactly rather
|
|
335
335
|
* than left to the argv-shape heuristic. */
|
|
@@ -353,7 +353,7 @@ export async function runTestRunDedupSweep(deps: TestRunDedupDeps): Promise<numb
|
|
|
353
353
|
const resolvedRoot = deps.rootPid();
|
|
354
354
|
const rootPid = resolvedRoot !== undefined && Number.isInteger(resolvedRoot) && resolvedRoot > 1 ? resolvedRoot : 0;
|
|
355
355
|
const agentId = deps.agentId?.() ?? "";
|
|
356
|
-
//
|
|
356
|
+
// either proof of ownership is enough, but at least one is required. With neither, the
|
|
357
357
|
// sweep cannot show any process is ours and must do nothing at all.
|
|
358
358
|
const ownedPids = agentId ? await (deps.collectOwnedPids ?? collectAgentTaggedPids)(agentId) : [];
|
|
359
359
|
if (rootPid === 0 && ownedPids.length === 0) return [];
|
|
@@ -387,7 +387,7 @@ export async function runTestRunDedupSweep(deps: TestRunDedupDeps): Promise<numb
|
|
|
387
387
|
const kill = deps.kill ?? killPid;
|
|
388
388
|
const waitForExit = deps.waitForExit ?? waitForPidsExit;
|
|
389
389
|
deps.log(
|
|
390
|
-
`test-run dedup
|
|
390
|
+
`test-run dedup: a newer test run started, killing ${plan.staleRoots.length} superseded run(s) ` +
|
|
391
391
|
`[roots ${plan.staleRoots.join(", ")}; ${plan.killPids.length} pid(s)]` +
|
|
392
392
|
`${plan.keptPid !== undefined ? `; keeping ${plan.keptPid}` : ""}`,
|
|
393
393
|
);
|
|
@@ -401,7 +401,7 @@ export async function runTestRunDedupSweep(deps: TestRunDedupDeps): Promise<numb
|
|
|
401
401
|
|
|
402
402
|
/** True unless the operator has explicitly turned dedup off for this session. */
|
|
403
403
|
export function testRunDedupEnabled(env: Record<string, string | undefined> = process.env): boolean {
|
|
404
|
-
const raw = (relayEnv(env, "
|
|
404
|
+
const raw = (relayEnv(env, "HUMWICK_TEST_DEDUP") ?? "").trim().toLowerCase();
|
|
405
405
|
return raw !== "0" && raw !== "false" && raw !== "off";
|
|
406
406
|
}
|
|
407
407
|
|
|
@@ -2,14 +2,14 @@ import { readFileSync, statSync } from "node:fs";
|
|
|
2
2
|
import type { ContextState } from "@humwick/sdk";
|
|
3
3
|
import type { ProviderContextUsage } from "./adapter";
|
|
4
4
|
|
|
5
|
-
//
|
|
5
|
+
// Claude reports context only through its out-of-band status-line probe,
|
|
6
6
|
// which freezes after an in-process compact/clear/native-resume (no new process,
|
|
7
7
|
// so the probe file stops advancing while the agent keeps taking turns). The
|
|
8
8
|
// transcript — handed to the runner every turn by the Stop hook — carries the
|
|
9
9
|
// same per-turn token usage, so the runner derives fresh context from it here as
|
|
10
10
|
// a probe-independent fallback that keeps context.lastUpdatedAt advancing across
|
|
11
11
|
// those in-process generation bumps. Kept out of runner-core to hold that giant's
|
|
12
|
-
// line budget
|
|
12
|
+
// line budget; runner-core just threads its per-session cache through.
|
|
13
13
|
|
|
14
14
|
export interface TranscriptContextCache {
|
|
15
15
|
path: string;
|
package/src/transport-errors.ts
CHANGED
|
@@ -41,7 +41,7 @@ export function classifyTransportError(error: unknown): TransportErrorClass {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// Back-compat: a failure is "retryable transport" if it is either connect-phase or ambiguous (the old
|
|
44
|
-
// single-class predicate
|
|
44
|
+
// single-class predicate exposed). The retry DECISION now also weighs idempotency for the
|
|
45
45
|
// ambiguous class — see forwardWithConnectionRetry.
|
|
46
46
|
export function isRetryableTransportError(error: unknown): boolean {
|
|
47
47
|
return classifyTransportError(error) !== "fatal";
|
package/src/turn-opener.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { receivesTurnResponseAutomatically } from "./response-routing";
|
|
|
6
6
|
|
|
7
7
|
type ReplyRoute = NonNullable<ProviderSessionEvent["replyRoute"]>;
|
|
8
8
|
|
|
9
|
-
//
|
|
9
|
+
// binds the delivered message that OPENS a provider turn to that turn; only its final answers it.
|
|
10
10
|
// Armed before the input is offered (OpenCode runs the whole turn inside deliver, Codex starts it
|
|
11
11
|
// synchronously), bound at the next turn-start edge, consumed by the final with that turn id. A turn
|
|
12
12
|
// that starts with nothing armed clears the binding, so a later timer turn never inherits a question.
|
|
@@ -13,7 +13,7 @@ export interface RunnerTimelineEvent {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* the announcement for a provider-turn claim the busy backstop force-cleared.
|
|
17
17
|
*
|
|
18
18
|
* Reconciliation is a real state change, not bookkeeping repair: the turn the coordinator is
|
|
19
19
|
* waiting on is over and produced no reply, and `busy` is exactly the signal it used to decide
|
|
@@ -48,7 +48,7 @@ export function reconciledTurnTimelineEvent(input: {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
*
|
|
51
|
+
* the announcement for a live provider turn the busy backstop is HOLDING (not cancelling)
|
|
52
52
|
* past its prolonged-hold threshold: a frozen-active `shell` gate, or a pane-scrape-only long tool
|
|
53
53
|
* the runner cannot positively prove idle. In contrast to {@link reconciledTurnTimelineEvent} the
|
|
54
54
|
* turn is NOT ended and the card stays busy — this only surfaces "still working, held by the
|