@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.
Files changed (217) hide show
  1. package/LICENSE +661 -167
  2. package/agent-bin/humwick +2 -2
  3. package/agent-bin/humwick-browser +4 -0
  4. package/hooks/fff-search-redirect/fff-search-redirect.cjs +1 -1
  5. package/hooks/web-tools-redirect/web-tools-redirect.cjs +2 -2
  6. package/package.json +8 -11
  7. package/plugins/browser/skills/playwright-cli/SKILL.md +8 -6
  8. package/plugins/claude/.claude-plugin/plugin.json +3 -3
  9. package/plugins/claude/claude-statusline-probe.cjs +26 -16
  10. package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +47 -17
  11. package/plugins/claude/hooks/gate-run-guard.sh +3 -3
  12. package/plugins/claude/hooks/gate-run-guard.ts +5 -5
  13. package/plugins/claude/hooks/kill-guard.provisioned.mjs +1 -1
  14. package/plugins/claude/hooks/kill-guard.sh +4 -4
  15. package/plugins/claude/hooks/kill-guard.ts +2 -2
  16. package/plugins/claude/hooks/permission-request.sh +1 -1
  17. package/plugins/claude/hooks/post-compact.sh +1 -1
  18. package/plugins/claude/hooks/post-tool-use.sh +3 -3
  19. package/plugins/claude/hooks/pre-compact.sh +2 -2
  20. package/plugins/claude/hooks/read-only-bash-guard.sh +4 -4
  21. package/plugins/claude/hooks/relay-status.sh +32 -32
  22. package/plugins/claude/hooks/session-end.sh +2 -2
  23. package/plugins/claude/hooks/session-start.sh +3 -3
  24. package/plugins/claude/hooks/shell-trust.ts +1 -1
  25. package/plugins/claude/hooks/stop-failure.sh +2 -2
  26. package/plugins/claude/hooks/stop.sh +6 -6
  27. package/plugins/claude/hooks/subagent-skill-policy.cjs +8 -6
  28. package/plugins/claude/hooks/subagent-stop.sh +3 -3
  29. package/plugins/claude/hooks/user-prompt-submit.sh +1 -1
  30. package/plugins/claude/hooks/wait-loop-guard.ts +3 -3
  31. package/plugins/claude/monitors/monitors.json +2 -2
  32. package/plugins/claude/monitors/relay-monitor.provisioned.mjs +96 -62
  33. package/plugins/claude/monitors/relay-monitor.ts +5 -5
  34. package/plugins/claude/skills/label/SKILL.md +3 -3
  35. package/plugins/claude/skills/message/SKILL.md +5 -5
  36. package/plugins/claude/skills/react/SKILL.md +1 -1
  37. package/plugins/claude/skills/read-message/SKILL.md +4 -4
  38. package/plugins/claude/skills/reply/SKILL.md +5 -5
  39. package/plugins/claude/skills/send-claimable/SKILL.md +5 -5
  40. package/plugins/claude/skills/slice-first/SKILL.md +5 -3
  41. package/plugins/claude/skills/slice-first/references/plan-recipe.md +30 -29
  42. package/plugins/claude/skills/status/SKILL.md +3 -3
  43. package/plugins/claude/skills/tags/SKILL.md +5 -5
  44. package/plugins/claude/skills/task/SKILL.md +10 -10
  45. package/plugins/claude-output-style/.claude-plugin/plugin.json +1 -1
  46. package/plugins/claude-output-style/output-styles/{agent-relay.md → humwick.md} +1 -1
  47. package/plugins/codex/skills/guide/SKILL.md +3 -3
  48. package/plugins/codex/skills/label/SKILL.md +3 -3
  49. package/plugins/codex/skills/message/SKILL.md +5 -5
  50. package/plugins/codex/skills/react/SKILL.md +1 -1
  51. package/plugins/codex/skills/read-message/SKILL.md +4 -4
  52. package/plugins/codex/skills/reply/SKILL.md +5 -5
  53. package/plugins/codex/skills/send-claimable/SKILL.md +5 -5
  54. package/plugins/codex/skills/slice-first/SKILL.md +5 -3
  55. package/plugins/codex/skills/slice-first/references/plan-recipe.md +30 -29
  56. package/plugins/codex/skills/status/SKILL.md +3 -3
  57. package/plugins/codex/skills/tags/SKILL.md +5 -5
  58. package/plugins/codex/skills/task/SKILL.md +11 -10
  59. package/src/adapter.ts +95 -92
  60. package/src/adapters/claude-api.ts +8 -11
  61. package/src/adapters/claude-context-command.ts +1 -1
  62. package/src/adapters/claude-credentials.ts +7 -3
  63. package/src/adapters/claude-delivery-lanes.ts +2 -2
  64. package/src/adapters/claude-delivery.ts +16 -16
  65. package/src/adapters/claude-effort.ts +2 -2
  66. package/src/adapters/claude-exit.ts +2 -2
  67. package/src/adapters/claude-headless-mirror.ts +18 -2
  68. package/src/adapters/claude-headless-refusal.ts +3 -3
  69. package/src/adapters/claude-headless.ts +11 -10
  70. package/src/adapters/claude-launch-exit.ts +1 -1
  71. package/src/adapters/claude-monitor-delivery-plan.ts +36 -16
  72. package/src/adapters/claude-permission.ts +5 -5
  73. package/src/adapters/claude-prompt-gates.ts +9 -9
  74. package/src/adapters/claude-prompt-hook.ts +2 -2
  75. package/src/adapters/claude-session-capture.ts +6 -6
  76. package/src/adapters/claude-session-probe.ts +25 -25
  77. package/src/adapters/claude-shutdown.ts +11 -11
  78. package/src/adapters/claude-status-detectors.ts +31 -31
  79. package/src/adapters/claude-subagents.ts +7 -7
  80. package/src/adapters/claude-submit-confirmation.ts +6 -6
  81. package/src/adapters/claude-tmux.ts +43 -43
  82. package/src/adapters/claude-token-usage-capture.ts +5 -5
  83. package/src/adapters/claude-tool-policy.ts +7 -7
  84. package/src/adapters/claude-transcript.ts +24 -24
  85. package/src/adapters/claude.ts +100 -100
  86. package/src/adapters/codex-agent-message-capture.ts +26 -63
  87. package/src/adapters/codex-app-server.ts +1 -1
  88. package/src/adapters/codex-client.ts +9 -9
  89. package/src/adapters/codex-effective-config.ts +1 -1
  90. package/src/adapters/codex-launch-config.ts +5 -5
  91. package/src/adapters/codex-rollout-context.ts +2 -2
  92. package/src/adapters/codex-skill-discovery.ts +1 -1
  93. package/src/adapters/codex-token-usage-capture.ts +16 -16
  94. package/src/adapters/codex-tool-output.ts +4 -4
  95. package/src/adapters/codex-version.ts +3 -3
  96. package/src/adapters/codex.ts +64 -64
  97. package/src/adapters/opencode-api.ts +1 -1
  98. package/src/adapters/opencode-context.ts +20 -22
  99. package/src/adapters/opencode-model-registry.ts +7 -7
  100. package/src/adapters/opencode-provider.ts +8 -8
  101. package/src/adapters/opencode-provisioning.ts +30 -0
  102. package/src/adapters/opencode-server-retry.ts +1 -1
  103. package/src/adapters/opencode-soft-steer.ts +5 -5
  104. package/src/adapters/opencode-spawn.ts +18 -14
  105. package/src/adapters/opencode-token-usage-capture.ts +12 -12
  106. package/src/adapters/opencode-tool-events.ts +15 -15
  107. package/src/adapters/opencode.ts +232 -232
  108. package/src/adapters/project-shell.ts +11 -11
  109. package/src/adapters/service-access.ts +6 -6
  110. package/src/adapters/tmux-offload.ts +9 -9
  111. package/src/adapters/tool-summary.ts +5 -5
  112. package/src/agent-redact.ts +1 -1
  113. package/src/agent-runner/commands/dispatch.ts +10 -10
  114. package/src/agent-runner/commands/lifecycle.ts +2 -2
  115. package/src/agent-runner/commands/prompt.ts +19 -19
  116. package/src/agent-runner/credentials.ts +5 -5
  117. package/src/agent-runner/inbox.ts +1 -1
  118. package/src/agent-runner/lifecycle-milestones.ts +3 -3
  119. package/src/agent-runner/lifecycle-phase.ts +2 -2
  120. package/src/agent-runner/liveness-exchange.ts +4 -10
  121. package/src/agent-runner/registration.ts +8 -8
  122. package/src/agent-runner/runner-context.ts +9 -14
  123. package/src/agent-runner/session-mirror.ts +12 -12
  124. package/src/agent-runner/task-claims.ts +2 -2
  125. package/src/agent-runner/token-usage-coverage.ts +1 -1
  126. package/src/agent-runner/turn-report.ts +12 -12
  127. package/src/api-budget-accounting.ts +3 -3
  128. package/src/area-memory.ts +6 -6
  129. package/src/artifact-path-materialize.ts +1 -1
  130. package/src/async-guard.ts +5 -5
  131. package/src/attachment-cache.ts +17 -22
  132. package/src/background-script-reconcile.ts +6 -6
  133. package/src/bounded-log.ts +1 -1
  134. package/src/busy-reconciler.ts +22 -22
  135. package/src/claim-tracker.ts +3 -3
  136. package/src/claude-settings.ts +8 -8
  137. package/src/claude-system-prompt.ts +1 -1
  138. package/src/claude-workspace-trust.ts +1 -1
  139. package/src/config.ts +62 -61
  140. package/src/context-boundary.ts +5 -5
  141. package/src/context-command.ts +3 -3
  142. package/src/control-server.ts +32 -32
  143. package/src/delivery-result.ts +2 -2
  144. package/src/effort-command.ts +2 -2
  145. package/src/empty-turn.ts +22 -14
  146. package/src/free-port.ts +1 -1
  147. package/src/gate-run-config.ts +3 -3
  148. package/src/host-asset-plan.ts +24 -17
  149. package/src/host-claude-config-worker.ts +2 -2
  150. package/src/host-sources.ts +2 -2
  151. package/src/index.ts +16 -16
  152. package/src/launch-assembly.ts +94 -96
  153. package/src/liveness.ts +15 -15
  154. package/src/logger.ts +3 -3
  155. package/src/mcp-outbox.ts +3 -3
  156. package/src/mcp-preflight.ts +2 -2
  157. package/src/mcp-source-selection.ts +2 -2
  158. package/src/mcp-tool-deferral.ts +1 -1
  159. package/src/mcp-vault-env.ts +2 -2
  160. package/src/message-body-config/config.ts +4 -4
  161. package/src/mid-turn-delivery.ts +2 -2
  162. package/src/native-memory-lever.ts +8 -8
  163. package/src/outbox.ts +15 -15
  164. package/src/pending-permission-store.ts +1 -1
  165. package/src/profile-home.ts +19 -19
  166. package/src/profile-projection.ts +26 -24
  167. package/src/project-instructions.ts +5 -5
  168. package/src/provider-auth-warning.ts +17 -0
  169. package/src/provider-double.ts +1 -1
  170. package/src/provider-hold.ts +3 -3
  171. package/src/provider-home-marker.ts +1 -1
  172. package/src/provider-home-tombstone.ts +1 -1
  173. package/src/provider-input-queue.ts +2 -2
  174. package/src/provider-integration.ts +15 -13
  175. package/src/provider-setup.ts +15 -3
  176. package/src/providers.ts +1 -1
  177. package/src/provisioning.ts +37 -40
  178. package/src/question-decision.ts +2 -2
  179. package/src/quota.ts +0 -6
  180. package/src/rate-limit.ts +24 -13
  181. package/src/refused-prompt-store.ts +2 -2
  182. package/src/relay-injection-events.ts +2 -2
  183. package/src/relay-instructions.ts +5 -5
  184. package/src/relay-mcp-proxy.long-turn-e2e.ts +13 -13
  185. package/src/relay-mcp-proxy.ts +57 -58
  186. package/src/relay-mcp.ts +55 -69
  187. package/src/reply-obligation-cache.ts +9 -9
  188. package/src/response-capture-report.ts +9 -9
  189. package/src/response-routing.ts +1 -1
  190. package/src/runner-core.ts +209 -222
  191. package/src/runner-exit.ts +1 -1
  192. package/src/runner-helpers.ts +10 -10
  193. package/src/runner-insights.ts +4 -2
  194. package/src/runner-mcp-proxy.ts +3 -3
  195. package/src/runner-spawn-config.ts +3 -3
  196. package/src/runtime-token-file.ts +1 -1
  197. package/src/runtime-token-remint.ts +1 -1
  198. package/src/service-access.ts +2 -2
  199. package/src/session-destroy.ts +1 -1
  200. package/src/session-insights.ts +1 -1
  201. package/src/session-scratch.ts +24 -37
  202. package/src/spawn-input.ts +6 -6
  203. package/src/startup-config-fetch.ts +4 -4
  204. package/src/subreaper.ts +3 -5
  205. package/src/test-run-dedup.ts +13 -13
  206. package/src/transcript-context.ts +2 -2
  207. package/src/transport-errors.ts +1 -1
  208. package/src/turn-opener.ts +1 -1
  209. package/src/turn-reconcile-event.ts +2 -2
  210. package/src/vault-proxy-hop.ts +2 -2
  211. package/src/web-tools.ts +2 -2
  212. package/agent-bin/agent-relay-browser +0 -10
  213. package/plugins/claude/skills/disconnect/SKILL.md +0 -16
  214. package/plugins/claude/skills/pair/SKILL.md +0 -30
  215. package/plugins/codex/skills/disconnect/SKILL.md +0 -15
  216. package/plugins/codex/skills/pair/SKILL.md +0 -29
  217. package/src/adapters/provider-quota-harvest.ts +0 -76
@@ -1,7 +1,7 @@
1
1
  import { formatRunnerExitLine } from "@humwick/sdk";
2
2
 
3
3
  /**
4
- * #2619 — the ONLY way the runner process exits. Every exit writes the structured exit record
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.
@@ -22,12 +22,12 @@ interface RuntimeProviderOptions {
22
22
  effort?: string;
23
23
  approvalMode: string;
24
24
  headless: boolean;
25
- /** #2773 — a headless-pipe launch has no terminal to read, write, or attach. */
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
- /** #2950 — the running process accepts an in-place effort change (see runnerEffortSwitch). */
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
- // #1258 (L3): claude prints its resume banner ANSI-dimmed — the raw log bytes are
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
- // #1339: the Codex adapter rejects a delivery attempted while a turn is already in progress
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 (#1565 F1): a serialized boundary payload that
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 (#1565 F1). */
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
- // #557 — the agent-card projection now DESCRIBES the assembled launch (same source the
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
- // #2433 — outbox delivery-error classification. The runner outboxes must never poison
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 (#2433): connection-class transport failures plus
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 (#2433): the relay is up, parsed the payload, and will refuse
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);
@@ -1,4 +1,4 @@
1
- import type { WorkspaceMetadata } from "@humwick/sdk";
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 bounded = boundContinuationArchiveSegment(segment);
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",
@@ -1,5 +1,5 @@
1
1
  import { sharedCallmuxToolCallTimeoutMsFromEnv, type HumwickHttpClient, webSearchEnabledFromEnv, webCallLogEnabledFromEnv, errMessage, type WebToolCall, type AgentProfile } from "@humwick/sdk";
2
- import { RelayMcpProxy, type RelayMcpProxyOptions } from "./relay-mcp-proxy";
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?: RelayMcpProxy; endpoint?: string } {
12
+ export function startRunnerMcpProxy(options: RelayMcpProxyOptions): { proxy?: HumwickMcpProxy; endpoint?: string } {
13
13
  try {
14
- const proxy = new RelayMcpProxy(options);
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
- // #2595 — positive-only gate mirrors the CLI parse; an unset/invalid value stays absent.
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
- // #2599 — the record arrives pre-parsed by the shared parser at the CLI seam; thread it
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 (#2318).
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
- * #1245 — re-derive the runner's token from the agent's CURRENT profile at a clear/compact boundary and
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(
@@ -34,7 +34,7 @@ export async function withServiceAccessLaunch<T>(
34
34
  }
35
35
 
36
36
  /**
37
- * #3577: the provider gets a loopback proxy URL and local credential for its whole life; the session
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(`[agent-relay] ${error instanceof Error ? error.message : "Vault access renewal failed"}; retrying in ${RENEW_RETRY_MS / 1000}s`);
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
  };
@@ -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
- // #1746 — a TERMINAL marker: the graceful+force-kill path ran and the provider process
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.
@@ -1,4 +1,4 @@
1
- // Provider-agnostic core for the #184 context-gathering signal (epic #183).
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`
@@ -1,23 +1,22 @@
1
- import { accessSync, appendFileSync, constants, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
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 = ".agent-relay";
9
- // The local-ignore entry. Leading + trailing slash scopes it to the dir at the
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 .agent-relay (cwd, or fallback base dir)
17
- rootDir: string; // <base>/.agent-relay
18
- sessionsDir: string; // <base>/.agent-relay/sessions
19
- sessionDir: string; // <base>/.agent-relay/sessions/<id>
20
- tmpDir: string; // <base>/.agent-relay/sessions/<id>/tmp
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, SCRATCH_DIR_NAME);
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
- // Idempotently append EXCLUDE_ENTRY to an ignore file (grep-before-write).
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
- appendIgnoreEntry(excludePath);
90
+ appendIgnoreEntries(excludePath, entries);
104
91
  return "exclude";
105
92
  }
106
- appendIgnoreEntry(join(baseDir, ".gitignore"));
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
- // #2310 P1 — this dir can hold sensitive spilled content (an over-cap memory/continuation
114
- // injection's full text, #1935's monitor over-limit handling), so it's session-private: 0700, not
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 (#1317) — lets the caller attach `tmux pipe-pane` while the pane is still
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
- // AGENT_RELAY_ID, so a stale sweep can safely remove only scripts whose agent
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 AGENT_RELAY_ID=(.+)$/m);
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, SCRATCH_DIR_NAME, "sessions");
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)) {
@@ -8,13 +8,13 @@ export interface RunnerCliOptions {
8
8
  provider: SpawnProvider;
9
9
  model?: string;
10
10
  effort?: string;
11
- /** #2595 — effective-catalog window resolved server-side; preferred over the static manifest. */
11
+ /** effective-catalog window resolved server-side; preferred over the static manifest. */
12
12
  contextWindowTokens?: number;
13
- /** #2599 — the full server-resolved model record (protocol ≥ 2); precedence payload > manifest. */
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
- /** #2773 — interactive|headless driver; absent keeps the provider's legacy selection. */
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
- // #2599 — raw --model-record-* fields, assembled then parsed ONCE through the shared parser
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
- // #2595 — server-resolved effective-catalog window; positive-integer gate keeps a malformed
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
- // #2599 — full-record fields (protocol ≥ 2). Values ride raw; the shared parser at the end
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, RELAY_TOKEN_HEADER, errMessage } from "@humwick/sdk";
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 (#3326).
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 ? { [RELAY_TOKEN_HEADER]: token } : undefined,
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(`[agent-relay] provider config relay read failed (${errMessage(error)}); retrying in ${delay}ms.`);
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 { SYSTEMD_MANAGED_SESSION_PREFIX } from "@humwick/sdk/systemd-unit";
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.startsWith(SYSTEMD_MANAGED_SESSION_PREFIX)
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 (#2858); only unsupervised runners need
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;
@@ -1,5 +1,5 @@
1
1
  import { relayEnv } from "@humwick/sdk/env";
2
- // #3754 — the lease owns launch-time replacement. The sweep only deduplicates
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 #2461 because the orchestrator (a separate workspace that cannot import
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 #2142 a no-op for exactly those Codex sessions. Prefer
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 (#2461). A `relay_run` gate run is a child of the orchestrator daemon, not of this
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: `AGENT_RELAY_ID=x` must not match agent `xy`.
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 (#2461).
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 (#2461).
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 (#3754).
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 (#3754). */
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 (#2461). Absent ⇒ ancestry-only, as before. */
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
- // #2461 — either proof of ownership is enough, but at least one is required. With neither, the
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 (#2142): a newer test run started, killing ${plan.staleRoots.length} superseded run(s) ` +
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, "AGENT_RELAY_TEST_DEDUP") ?? "").trim().toLowerCase();
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
- // #1512: Claude reports context only through its out-of-band status-line probe,
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 (epic #291); runner-core just threads its per-session cache through.
12
+ // line budget; runner-core just threads its per-session cache through.
13
13
 
14
14
  export interface TranscriptContextCache {
15
15
  path: string;
@@ -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 #1790 exposed). The retry DECISION now also weighs idempotency for the
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";
@@ -6,7 +6,7 @@ import { receivesTurnResponseAutomatically } from "./response-routing";
6
6
 
7
7
  type ReplyRoute = NonNullable<ProviderSessionEvent["replyRoute"]>;
8
8
 
9
- // #2840 — binds the delivered message that OPENS a provider turn to that turn; only its final answers it.
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
- * #1621 — the announcement for a provider-turn claim the busy backstop force-cleared.
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
- * #1980 — the announcement for a live provider turn the busy backstop is HOLDING (not cancelling)
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