@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
@@ -13,7 +13,7 @@ import { logger } from "../logger";
13
13
  import { providerTerminalSession, providerTerminalSocket } from "../process-meta";
14
14
  import type { RunnerLaunchOptions } from "./runner-context";
15
15
 
16
- // #2095 test rig — fallback window used to derive tokensUsed for the forced synthetic context when
16
+ // test rig — fallback window used to derive tokensUsed for the forced synthetic context when
17
17
  // neither the process context nor the probe reports a tokensMax yet. The absolute value is immaterial
18
18
  // (only utilization drives the server thresholds); 200k matches the common Claude window.
19
19
  const FORCED_CONTEXT_DEFAULT_WINDOW_TOKENS = 200_000;
@@ -30,7 +30,7 @@ interface RegistrationDeps {
30
30
  }
31
31
 
32
32
  export class Registration {
33
- // #2095 self-compact e2e test rig — the one-shot forced-context latch. `forceContextPct` is the
33
+ // self-compact e2e test rig — the one-shot forced-context latch. `forceContextPct` is the
34
34
  // synthetic utilization to report (undefined = feature off, production behavior unchanged);
35
35
  // `forceContextArmed` is disarmed after the FIRST forced compaction/clear command this runner
36
36
  // handles, so exactly one trigger→compact→resume cycle runs (no repeat-compaction loop).
@@ -40,13 +40,13 @@ export class Registration {
40
40
  constructor(private readonly deps: RegistrationDeps) {}
41
41
 
42
42
  disarmForcedContext(type: string): void {
43
- // #2095 test rig — one-shot latch: the FIRST forced compaction/clear this runner handles disarms
43
+ // test rig — one-shot latch: the FIRST forced compaction/clear this runner handles disarms
44
44
  // the synthetic forced context, so the next heartbeat reports the real (post-clear, low)
45
45
  // utilization and no second cycle is triggered. Equivalent to "once lastCompactedAt advances".
46
- // No-op unless AGENT_RELAY_FORCE_CONTEXT_PCT armed the latch, so production is untouched.
46
+ // No-op unless HUMWICK_FORCE_CONTEXT_PCT armed the latch, so production is untouched.
47
47
  if (this.forceContextArmed && (type === "agent.compact" || type === "agent.clearContext")) {
48
48
  this.forceContextArmed = false;
49
- this.deps.sessionLog("AGENT_RELAY_FORCE_CONTEXT_PCT one-shot latch disarmed by forced compaction/clear command");
49
+ this.deps.sessionLog("HUMWICK_FORCE_CONTEXT_PCT one-shot latch disarmed by forced compaction/clear command");
50
50
  // Flush a heartbeat NOW so the server's stored context drops to the real (sub-threshold) value
51
51
  // immediately. Without this, the ~30s heartbeat cadence lets the stale forced utilization
52
52
  // survive several 10s lifecycle ticks and fire a SECOND forced compaction — breaking the strict
@@ -55,12 +55,12 @@ export class Registration {
55
55
  }
56
56
  }
57
57
 
58
- // #2095 self-compact e2e test rig — synthesize a ContextState at the forced utilization while the
58
+ // self-compact e2e test rig — synthesize a ContextState at the forced utilization while the
59
59
  // one-shot latch is armed. lifecycleState is pinned to "working" (a "compacting"/"hibernating"
60
60
  // state makes the server's handleContextPressure early-return, so the trigger would never fire),
61
61
  // and source/confidence marked estimate/estimated so the origin of the synthetic value is honest.
62
62
  // Everything downstream (advisory, forced clear-inject, clear-commit barrier, resume) is the REAL
63
- // path — only the reported utilization is fabricated. No-op unless AGENT_RELAY_FORCE_CONTEXT_PCT set.
63
+ // path — only the reported utilization is fabricated. No-op unless HUMWICK_FORCE_CONTEXT_PCT set.
64
64
  applyForcedContext(context: ContextState | undefined): ContextState | undefined {
65
65
  if (!this.forceContextArmed || this.forceContextPct === undefined) return context;
66
66
  const pct = this.forceContextPct;
@@ -147,7 +147,7 @@ export function runnerIdentityMeta(options: RunnerLaunchOptions, snapshot: Ident
147
147
  model: options.model ?? null,
148
148
  ...(isOpenAiApiModel(options.provider, options.model) ? { apiAuthMode: "apiKey", apiBackend: "openai", apiBudgetId: "openai-api", billing: "api" } : {}),
149
149
  effort: options.effort ?? null,
150
- // #557 — project the same assembled launch; the pure assembler ignores env/controlPort.
150
+ // project the same assembled launch; the pure assembler ignores env/controlPort.
151
151
  agentProfile: options.agentProfile ? appliedAgentProfileMetadata(options.provider, options.agentProfile, { ...options, ...options.launchConfig, agentId: snapshot.agentId, env: {}, controlPort: 0 } as RunnerSpawnConfig, options.providerConfig) : null,
152
152
  ...identity,
153
153
  runnerManaged: true,
@@ -1,6 +1,3 @@
1
- import { resolveEnvLayers, selectProviderConfigEnv } from "@humwick/sdk/child-env";
2
- import { runnerMcpGeneration } from "../relay-mcp";
3
- import { hostSourcesFor } from "../host-asset-plan";
4
1
  import type { AgentLifecycle, AgentProfile, LaunchMode, ProjectToolchain, WorkspaceMetadata } from "@humwick/sdk";
5
2
  import { errMessage, HumwickBusClient, HumwickHttpClient, type BusClientOptions, type HttpClientOptions } from "@humwick/sdk";
6
3
  import type { ProviderAdapter, ProviderConfig, RunnerSpawnConfig } from "../adapter";
@@ -13,9 +10,9 @@ export interface RunnerOptions {
13
10
  provider: string;
14
11
  model?: string;
15
12
  effort?: string;
16
- /** #2595 — effective-catalog window from the spawn payload; threaded into the adapter config. */
13
+ /** effective-catalog window from the spawn payload; threaded into the adapter config. */
17
14
  contextWindowTokens?: number;
18
- /** #2599 — the full server-resolved model record (protocol ≥ 2); threaded into the adapter config. */
15
+ /** the full server-resolved model record (protocol ≥ 2); threaded into the adapter config. */
19
16
  modelRecord?: import("@humwick/sdk/provider-catalog").ResolvedModelRecord;
20
17
  runnerId: string;
21
18
  instanceId: string;
@@ -40,7 +37,7 @@ export interface RunnerOptions {
40
37
  tags: string[];
41
38
  capabilities: string[];
42
39
  providerArgs: string[];
43
- /** #2773 — the interactive|headless driver this process was launched with. */
40
+ /** the interactive|headless driver this process was launched with. */
44
41
  launchMode?: LaunchMode;
45
42
  toolchain?: ProjectToolchain;
46
43
  policyName?: string;
@@ -54,7 +51,7 @@ export interface RunnerOptions {
54
51
  /** Provider-owned snapshot loaded before registration for truthful synchronous projection. */
55
52
  launchConfig?: Pick<RunnerSpawnConfig, "hostClaudeConfig" | "hostClaudeSettings">;
56
53
  adapter: ProviderAdapter;
57
- // #2443 — `reason`, when present, is a human-readable diagnosis of a terminal provider
54
+ // `reason`, when present, is a human-readable diagnosis of a terminal provider
58
55
  // failure (e.g. "model-unavailable: APIError 404 ..."), passed through so the caller can print
59
56
  // it before the process actually exits — see handleAdapterStatusChange's call site.
60
57
  onProviderExit?: (code: number | null, reason?: string) => void;
@@ -73,7 +70,6 @@ interface RunnerContextDeps {
73
70
  export class RunnerContext {
74
71
  readonly options: RunnerLaunchOptions;
75
72
  readonly agentId: string;
76
- readonly mcpToolGeneration: ReturnType<typeof runnerMcpGeneration>;
77
73
  http!: HumwickHttpClient;
78
74
  bus!: HumwickBusClient;
79
75
  private readonly sessionDebugVerbose = sessionDebugEnabled();
@@ -81,7 +77,6 @@ export class RunnerContext {
81
77
  constructor(options: RunnerOptions, private readonly deps: RunnerContextDeps) {
82
78
  const { token, tokenJti, tokenProfileId, tokenExpiresAt, ...launch } = options;
83
79
  this.options = Object.freeze(launch);
84
- this.mcpToolGeneration = runnerMcpGeneration(resolveEnvLayers([process.env, selectProviderConfigEnv(options.providerConfig.env, hostSourcesFor(options).settings)]));
85
80
  this.agentId = options.agentId ?? options.runnerId;
86
81
  }
87
82
 
@@ -96,7 +91,7 @@ export class RunnerContext {
96
91
 
97
92
  initializeHttp(token: string | undefined): void {
98
93
  // Enrolled over registration only; never part of launch options or provider config.
99
- this.http = (this.options.ports?.http ?? ((opts) => new HumwickHttpClient(opts)))({ baseUrl: this.options.relayUrl, token });
94
+ this.http = (this.options.ports?.http ?? ((opts) => new HumwickHttpClient(opts)))({ baseUrl: this.options.relayUrl, token, clientKind: "runner" });
100
95
  }
101
96
 
102
97
  initializeBus(options: BusClientOptions): void {
@@ -121,14 +116,14 @@ export class RunnerContext {
121
116
  this.deps.publishStatus();
122
117
  }
123
118
 
124
- // (#1617) — the reporting half of a guarded fire-and-forget call. Bun exits(1) on an unhandled
119
+ // the reporting half of a guarded fire-and-forget call. Bun exits(1) on an unhandled
125
120
  // rejection and the runner installs no `unhandledRejection` handler, so any `void this.foo()`
126
- // whose target can reject takes the whole agent down — and nothing respawns it (#1618).
121
+ // whose target can reject takes the whole agent down — and nothing respawns it.
127
122
  //
128
123
  // Deliberately NOT a fire-and-forget wrapper: the `.catch` still has to be written out at every
129
124
  // call site, so adding one stays a visible decision rather than something a helper hides. All
130
125
  // this fixes is the SHAPE of the report — the same logger.error + publishRunnerTimelineEvent
131
- // pair attemptInitialPromptDelivery and the #1614 obligation re-injection already use, so a
126
+ // pair attemptInitialPromptDelivery and the obligation re-injection already use, so a
132
127
  // background operation that dies is loud on the agent's own timeline instead of vanishing.
133
128
  reportBackgroundRejection(scope: string, status: string, title: string, error: unknown): void {
134
129
  const body = errMessage(error);
@@ -151,7 +146,7 @@ export class RunnerContext {
151
146
  }
152
147
 
153
148
  // Verbose, high-frequency lines (per-probe, per-emit) — surfaced only at log
154
- // level "debug" (AGENT_RELAY_LOG_LEVEL=debug, or flip live via /log-level).
149
+ // level "debug" (HUMWICK_LOG_LEVEL=debug, or flip live via /log-level).
155
150
  sessionDebug(message: string): void {
156
151
  logger.debug("mirror", message);
157
152
  }
@@ -28,17 +28,17 @@ interface SessionMirrorDeps {
28
28
  const SESSION_MIRROR_BATCH_MAX = 100;
29
29
 
30
30
  export class SessionMirror {
31
- // #332: a fast-lane outbox for display-only session-mirror trace events. Routing them through
31
+ // a fast-lane outbox for display-only session-mirror trace events. Routing them through
32
32
  // their own FIFO queue stops one transient trace failure from head-of-line blocking real-message
33
33
  // delivery (and vice versa); its backoff is capped low since a stale live-mirror frame is cheap.
34
34
  private readonly sessionOutbox: Outbox;
35
35
  /** In-flight asynchronous turn captures — the settle waits these out before judging a turn empty. */
36
36
  private turnCapturesInFlight = 0;
37
37
  private sessionBatchSeq = 0;
38
- // #1512: mtime-keyed cache of the last transcript-derived context usage, so the
38
+ // mtime-keyed cache of the last transcript-derived context usage, so the
39
39
  // heartbeat's fallback parse doesn't re-read a large transcript that hasn't changed.
40
40
  private transcriptContextCache?: TranscriptContextCache;
41
- // #1235 — true while a monitorless (hookless) provider turn is being mirrored natively:
41
+ // true while a monitorless (hookless) provider turn is being mirrored natively:
42
42
  // the adapter supplied a transcript path on the provider-turn busy edge, so the runner
43
43
  // drives the reasoning tail itself (start on busy, final-capture on idle) instead of via
44
44
  // Stop/UserPromptSubmit hooks the lean/isolated worker never installs.
@@ -49,11 +49,11 @@ export class SessionMirror {
49
49
  agentId: `${this.deps.agentId}-session`,
50
50
  dir: this.deps.outboxDir,
51
51
  send: (record) => this.deliverOutboxEvent(record),
52
- // Ephemeral trace frames: cap the per-row stall low. #2433 — transient (connect-class /
52
+ // Ephemeral trace frames: cap the per-row stall low. Transient (connect-class /
53
53
  // 5xx / 429) failures never burn this budget; it now only bounds unknown-class failures.
54
54
  maxBackoffMs: 5_000,
55
55
  maxAttempts: 12,
56
- // #2433 — a poisoned session row is REAL loss (report-up chat rides this queue), so
56
+ // a poisoned session row is REAL loss (report-up chat rides this queue), so
57
57
  // surface it in-session via the mirror-drop notice seam instead of only a FATAL host log.
58
58
  onPoison: (record, reason) => emitSessionPoisonNotice({ record, http: this.deps.http }, reason),
59
59
  });
@@ -82,10 +82,10 @@ export class SessionMirror {
82
82
  // the dashboard can render the live provider session faithfully. Display-only:
83
83
  // session messages are never delivered back into a provider.
84
84
  publishSessionEvent(input: { from: string; to: string; body: string; session: MessageSessionMeta; replyTo?: number; occurredAt?: number }): void {
85
- // Durable, ordered, timestamped (#196/#1115): the actual POST happens in
85
+ // Durable, ordered, timestamped: the actual POST happens in
86
86
  // deliverOutboxEvent, retried until it lands. An explicit occurredAt preserves the
87
87
  // provider-native event time; otherwise the outbox stamps enqueue time. Routed through
88
- // the fast-lane sessionOutbox (#332) so a transient trace failure can't head-of-line
88
+ // the fast-lane sessionOutbox so a transient trace failure can't head-of-line
89
89
  // block real messages.
90
90
  // A stepId-bearing step (tool running to completed, streamed reasoning/response) uses a
91
91
  // STABLE idempotency key so the server upserts the row in place instead of appending a dup.
@@ -221,7 +221,7 @@ export class SessionMirror {
221
221
  this.deps.adapter.stopSessionTrace?.();
222
222
  }
223
223
 
224
- // #1235 — start the transcript reasoning tail for a hookless (monitorless) turn. Reuses the
224
+ // start the transcript reasoning tail for a hookless (monitorless) turn. Reuses the
225
225
  // exact adapter capture path the UserPromptSubmit hook drives for hooked agents; only the
226
226
  // trigger differs (native turn-start vs hook). A missing/old transcript makes the tail a
227
227
  // graceful no-op (poll finds nothing), so this can never crash a turn.
@@ -234,11 +234,11 @@ export class SessionMirror {
234
234
  }
235
235
  }
236
236
 
237
- // #1235 — capture the final assistant response for a hookless turn (the Stop hook's job for
237
+ // capture the final assistant response for a hookless turn (the Stop hook's job for
238
238
  // hooked agents), then tear down the tail. captureSessionTurn drains the tail, reads the
239
239
  // closing turn text from the transcript, and emits the final `response` session event.
240
240
  async finishNativeSessionCapture(transcriptPath: string | undefined, ctx: ProviderSessionTurnContext): Promise<void> {
241
- // #2389 — this publishes the closing `response` event AFTER the idle edge; the empty-turn
241
+ // this publishes the closing `response` event AFTER the idle edge; the empty-turn
242
242
  // settle must not judge the turn until it has finished.
243
243
  this.turnCapturesInFlight += 1;
244
244
  try {
@@ -253,14 +253,14 @@ export class SessionMirror {
253
253
  }
254
254
  }
255
255
 
256
- // #2491 — the ONE terminal-settle seam for both capture paths: a monitorless (native) turn
256
+ // the ONE terminal-settle seam for both capture paths: a monitorless (native) turn
257
257
  // discovers its transcript on the provider-turn busy/idle edge; a hooked turn (Stop/
258
258
  // UserPromptSubmit) discovers it via publishSessionTurn/handleUserPrompt. Both write the
259
259
  // same `lastTranscriptPath`, so gating on that path alone (previously also required
260
260
  // `nativeSessionCaptureSeen`, set ONLY by the native busy/idle edge) is what makes this
261
261
  // fire for hooked agents too — without it every hooked turn's one-shot Stop-hook capture
262
262
  // pass raced Claude's own JSONL flush with nothing to settle it.
263
- // #2484 round 2 review (MED-1) — the transcript-presence bail used to live HERE, so an
263
+ // round 2 review (MED-1) — the transcript-presence bail used to live HERE, so an
264
264
  // adapter with no transcript file to tail (OpenCode's capture is event-sourced off the
265
265
  // message.updated SSE door, not a JSONL tail) silently never reached its
266
266
  // `captureFinalTokenUsage` hook on any of this seam's three call sites (stop(),
@@ -26,7 +26,7 @@ interface TaskClaimsDeps {
26
26
  export class TaskClaims {
27
27
  private claimRenewTimer?: Timer;
28
28
  private readonly activeTaskClaims = new Map<number, ActiveTaskClaim>();
29
- // #2619 — claimed Task messages whose provider-turn idle arrived while their own batch was still
29
+ // claimed Task messages whose provider-turn idle arrived while their own batch was still
30
30
  // delivering; completed at settle only if the provider accepted them (see completeObservedTaskClaims).
31
31
  private readonly completionDeferredTaskClaims = new Set<number>();
32
32
 
@@ -73,7 +73,7 @@ export class TaskClaims {
73
73
  if (claim.completionQueued) return false;
74
74
  if (acceptedIds) return acceptedIds.has(claim.messageId) && this.completionDeferredTaskClaims.delete(claim.messageId);
75
75
  if (!claim.observedProviderBusy) return false;
76
- // #2619 — a claim still inside its own delivery batch: an idle here may belong to a SIBLING
76
+ // a claim still inside its own delivery batch: an idle here may belong to a SIBLING
77
77
  // message's turn, and this one may yet be refused. Whether it was accepted is only known at
78
78
  // settle (settleMessageDelivery), so defer instead of marking its Task done now.
79
79
  if (this.deps.isDrainingMessage(claim.messageId)) {
@@ -1,4 +1,4 @@
1
- // #3351 — the runner's view of provider usage it has observed but Relay has not stored yet.
1
+ // the runner's view of provider usage it has observed but Relay has not stored yet.
2
2
  // Every adapter reports through this one seam, so a report that is in flight, or that failed
3
3
  // and was not yet replayed under the same dedupe key, is published as unreported spend
4
4
  // instead of looking like confirmed zero usage.
@@ -52,7 +52,7 @@ interface TurnReportDeps {
52
52
  sessionDebug(message: string): void;
53
53
  }
54
54
 
55
- // Bounded window to deliver the durable outbox before an exit-bound teardown (#183).
55
+ // Bounded window to deliver the durable outbox before an exit-bound teardown.
56
56
  const OUTBOX_FLUSH_TIMEOUT_MS = 3_000;
57
57
 
58
58
  export class TurnReport {
@@ -79,8 +79,8 @@ export class TurnReport {
79
79
  async publishProviderResponse(event: ProviderSessionEvent, body: string, turnId: string | undefined): Promise<void> {
80
80
  // Dashboard prompt injection is already answered by this captured App Server
81
81
  // response. User obligations still belong to the session mirror; spawner
82
- // obligations (#413) route to the spawn-parent with replyTo to clear the debt.
83
- // #1083: a turn-final capture is ALWAYS mirrored, regardless of reply route — an
82
+ // obligations route to the spawn-parent with replyTo to clear the debt.
83
+ // a turn-final capture is ALWAYS mirrored, regardless of reply route — an
84
84
  // autonomous provider turn with no pending prompt and no eligible obligation used to
85
85
  // hit `if (!route) return` here and vanish before ever reaching publishCapturedResponse,
86
86
  // so it was neither shown in the dashboard nor eligible for report-up. The route only
@@ -89,16 +89,16 @@ export class TurnReport {
89
89
  // response (enqueueCapturedResponseReport's own `!replyTo` gate then correctly skips
90
90
  // report-up — there's no spawner obligation to report up against).
91
91
  let replyToMessageId: number | undefined, replyTarget = "user";
92
- // #2335 — the route resolved to a channel-egress bridge; publishCapturedResponse re-addresses
92
+ // the route resolved to a channel-egress bridge; publishCapturedResponse re-addresses
93
93
  // the capture to the dashboard mirror rather than DM'ing this agent's narration to a human.
94
- let channelTarget = false, answersMessageId: number | undefined; // #2840 — the delivered opener this final answers
94
+ let channelTarget = false, answersMessageId: number | undefined; // the delivered opener this final answers
95
95
  // A hookless capture finishes after its idle edge, so its opener was snapshotted there (event.opener).
96
96
  const pendingPrompt = this.pendingPromptMessageId, opener = event.opener !== undefined ? event.opener ?? undefined : this.takeTurnOpener(turnId);
97
97
  if (event.replyRoute !== undefined) {
98
98
  this.takePendingPrompt();
99
99
  const route = await submittedResponseReplyRoute(event.replyRoute, id => this.deps.http.getThread(id), this.deps.agentId);
100
100
  if (route) { replyToMessageId = answersMessageId = route.replyToMessageId; replyTarget = route.to; channelTarget = route.channelTarget === true; }
101
- } else if (pendingPrompt && !this.deps.queuedTurnFollows()) { // #2919 — a prompt queued behind this turn is answered by its own turn
101
+ } else if (pendingPrompt && !this.deps.queuedTurnFollows()) { // a prompt queued behind this turn is answered by its own turn
102
102
  replyToMessageId = pendingPrompt;
103
103
  this.takePendingPrompt();
104
104
  } else {
@@ -109,13 +109,13 @@ export class TurnReport {
109
109
  answersMessageId = opener ? route?.replyToMessageId : undefined;
110
110
  if (route) { replyToMessageId = route.replyToMessageId; replyTarget = route.to; channelTarget = route.channelTarget === true; }
111
111
  }
112
- await publishCapturedResponse({ publishSessionEvent: (sessionEvent) => this.deps.publishSessionEvent({ ...sessionEvent, ...(event.occurredAt ? { occurredAt: event.occurredAt } : {}) }), outbox: this.deps.reportOutbox, provider: this.deps.provider, from: this.deps.agentId, to: replyTarget, body, replyTo: replyToMessageId, channelTarget, session: { type: "response", origin: event.origin ?? "provider", ...(turnId ? { turnId } : {}), ...(event.confidence ? { confidence: event.confidence } : {}), ...(event.endedBy ? { endedBy: event.endedBy } : {}), ...(answersMessageId ? { answersMessageId } : {}) } });
112
+ await publishCapturedResponse({ publishSessionEvent: (sessionEvent) => this.deps.publishSessionEvent({ ...sessionEvent, ...(event.occurredAt ? { occurredAt: event.occurredAt } : {}) }), outbox: this.deps.reportOutbox, provider: this.deps.provider, from: this.deps.agentId, to: replyTarget, body, replyTo: replyToMessageId, channelTarget, session: { type: "response", origin: event.origin ?? "provider", ...(turnId ? { turnId } : {}), ...(event.stepId ? { stepId: event.stepId, status: "completed" as const } : {}), ...(event.confidence ? { confidence: event.confidence } : {}), ...(event.endedBy ? { endedBy: event.endedBy } : {}), ...(answersMessageId ? { answersMessageId } : {}) } });
113
113
  if (replyToMessageId) this.deps.dirtyObligations();
114
114
  }
115
115
 
116
116
  async publishSessionTurn(input: ProviderSessionTurnInput): Promise<{ reasoningSettled: boolean } | void> {
117
117
  if (input.transcriptPath) this.deps.noteTranscriptPath(input.transcriptPath);
118
- // #2389 — a pre-flush capture IS a tool call in flight: independent proof the turn is doing
118
+ // a pre-flush capture IS a tool call in flight: independent proof the turn is doing
119
119
  // work, and the only such proof for a tools-only turn on an agent with reasoningCapture off.
120
120
  if (input.isPreFlush) this.deps.noteOutput();
121
121
  this.deps.beginTurnCapture();
@@ -166,7 +166,7 @@ export class TurnReport {
166
166
  this.deps.sessionDebug("user-prompt hook: skipped echo (runner-injected)");
167
167
  }
168
168
  await this.deps.adapter.handleUserPrompt?.(input, this.sessionTurnContext());
169
- // #2664 — a late UserPromptSubmit hook is what releases an unconfirmed Claude submit.
169
+ // a late UserPromptSubmit hook is what releases an unconfirmed Claude submit.
170
170
  if (this.deps.deliverySubmitHold()) this.deps.tryRecoverProviderSubmitHold(false);
171
171
  }
172
172
 
@@ -198,7 +198,7 @@ export class TurnReport {
198
198
  }
199
199
  }
200
200
 
201
- // The pre-session-destroy seam (#183): the single place end-of-session work runs before
201
+ // The pre-session-destroy seam: the single place end-of-session work runs before
202
202
  // an invasive transition (compact/clear/restart/shutdown/kill). Best-effort and
203
203
  // time-boxed so it never hangs teardown; concurrent calls for the same teardown coalesce
204
204
  // (a shutdown bus command and the SessionEnd hook can both fire). During the window the
@@ -214,7 +214,7 @@ export class TurnReport {
214
214
  Promise.all([
215
215
  this.deps.captureContextRatio(reason, opts),
216
216
  this.deps.captureContinuationArchive(reason, opts),
217
- // #2491 review MED-2 — prefer the already-tracked lastTranscriptPath (what the
217
+ // review MED-2 — prefer the already-tracked lastTranscriptPath (what the
218
218
  // capture instance's byte-offset watermark is actually keyed to) over the hook-
219
219
  // supplied opts.transcriptPath: a mismatch resets the watermark to 0 and re-reads
220
220
  // + re-posts the whole transcript serially (safe — dedupe-keyed — but wasteful).
@@ -238,7 +238,7 @@ export class TurnReport {
238
238
  }
239
239
  if (!reasonExitsRunner(reason)) return record.capture;
240
240
  // Exit flushing is a separate once-per-incarnation obligation from capture. A shutdown hook
241
- // coalesced onto a retained restart capture must still flush both durable outboxes (#1139),
241
+ // coalesced onto a retained restart capture must still flush both durable outboxes,
242
242
  // without repeating the expensive capture itself.
243
243
  record.exitFlush ??= record.capture.then(async () => {
244
244
  try {
@@ -1,5 +1,5 @@
1
1
  import { relayEnv } from "@humwick/sdk/env";
2
- import { RELAY_TOKEN_HEADER, resendOnRotatedToken, type ApiBudgetId, type ApiBudgetDecision, type ApiBudgetReservation, type ApiBudgetTokenUsage } from "@humwick/sdk";
2
+ import { HUMWICK_TOKEN_HEADER, resendOnRotatedToken, type ApiBudgetId, type ApiBudgetDecision, type ApiBudgetReservation, type ApiBudgetTokenUsage } from "@humwick/sdk";
3
3
  import type { RunnerSpawnConfig } from "./adapter";
4
4
  import { ProviderSubmitFailure } from "./provider-submit-failure";
5
5
 
@@ -64,8 +64,8 @@ export class ApiBudgetAccounting {
64
64
  async transport(config: RunnerSpawnConfig, action: "reserve" | "report", body: Record<string, unknown>): Promise<ApiBudgetDecision> {
65
65
  let status: number | undefined;
66
66
  try {
67
- const response = await resendOnRotatedToken(() => config.getRelayToken?.() ?? relayEnv(config.env, "AGENT_RELAY_TOKEN"), (token) => fetch(`${config.relayUrl}/api/agents/${encodeURIComponent(config.agentId)}/api-budget/${action}`, {
68
- method: "POST", headers: { "content-type": "application/json", ...(token ? { [RELAY_TOKEN_HEADER]: token } : {}) },
67
+ const response = await resendOnRotatedToken(() => config.getRelayToken?.() ?? relayEnv(config.env, "HUMWICK_TOKEN"), (token) => fetch(`${config.relayUrl}/api/agents/${encodeURIComponent(config.agentId)}/api-budget/${action}`, {
68
+ method: "POST", headers: { "content-type": "application/json", ...(token ? { [HUMWICK_TOKEN_HEADER]: token } : {}) },
69
69
  body: JSON.stringify(this.budgetId === "claude-api" ? body : { ...body, budgetId: this.budgetId }), signal: AbortSignal.timeout(3_000),
70
70
  })).catch(error => { throw new ProviderSubmitFailure("API budget transport unavailable", true, error); });
71
71
  status = response.status;
@@ -1,6 +1,6 @@
1
- // #3411 — the runner's half of on-touch area memory (epic #3406). Every file a normalized tool event
2
- // names is matched to the repo's path areas (`.agent-relay/areas.json`); an area's first touch in a
3
- // context generation is reported to POST /api/agents/:id/area-entered (#3410), which selects the
1
+ // the runner's half of on-touch area memory. Every file a normalized tool event
2
+ // names is matched to the repo's path areas (`.humwick/areas.json`); an area's first touch in a
3
+ // context generation is reported to POST /api/agents/:id/area-entered, which selects the
4
4
  // memories. Claude returns them through PostToolUse; other providers use `agent.injectContext`.
5
5
 
6
6
  import { relative, sep } from "node:path";
@@ -44,7 +44,7 @@ function checkoutGit(cwd: string): ProjectGit {
44
44
  }
45
45
 
46
46
  /** Never throws: the areas.json on the checkout's default branch, read through the same SDK reader
47
- * the relay uses (#3635), so a worktree older than the file still tracks its areas. */
47
+ * the relay uses, so a worktree older than the file still tracks its areas. */
48
48
  export async function loadAreas(cwd: string): Promise<AreasLookup> {
49
49
  try {
50
50
  const git = checkoutGit(cwd);
@@ -73,7 +73,7 @@ export class AreaMemoryTracker {
73
73
  private readonly servedAreas = new Set<string>();
74
74
  private readonly servedEntryIds = new Set<string>();
75
75
  private readonly failedReports = new Map<string, number>();
76
- /** This generation's areas, read from the default branch (#3635); unset until loaded. */
76
+ /** This generation's areas, read from the default branch; unset until loaded. */
77
77
  private lookup?: AreasLookup;
78
78
  /** The generation a load runs for. */
79
79
  private loadingFor?: number;
@@ -97,7 +97,7 @@ export class AreaMemoryTracker {
97
97
  }
98
98
  }
99
99
 
100
- /** Claude's hook owns file touches; transcript notifications must not claim them first (#3646). */
100
+ /** Claude's hook owns file touches; transcript notifications must not claim them first. */
101
101
  enableToolHook(): void { this.toolHook = true; this.prepare(); }
102
102
 
103
103
  /** Warm the default-branch snapshot while the provider starts, outside the hook budget. */
@@ -2,7 +2,7 @@ import { maxArtifactBytesFromEnv, maxPathUploadRawBytes, type TokenConstraints }
2
2
  import { openArtifactPathSource } from "@humwick/sdk/artifact-path-roots";
3
3
 
4
4
  // The relay server reads `path` uploads from its own filesystem, so a path that
5
- // exists on this agent host is "not found" on a remote relay (#2857). The runner
5
+ // exists on this agent host is "not found" on a remote relay. The runner
6
6
  // proxy runs beside the agent, reads the file here, and forwards bytes as base64
7
7
  // (agent→proxy stays a small path arg; only proxy→relay carries the bytes).
8
8
  export type PathMaterializeResult = { args?: Record<string, unknown>; error?: string };
@@ -1,12 +1,12 @@
1
- // #1665 — runner adapter over the SHARED async guards in `@humwick/sdk`.
1
+ // runner adapter over the SHARED async guards in `@humwick/sdk`.
2
2
  //
3
- // The primitives themselves live in `sdk/src/async-guard.ts` because #1676 is the same
3
+ // The primitives themselves live in `sdk/src/async-guard.ts` because is the same
4
4
  // defect class in the orchestrator: an unguarded `void <promise>` / async timer callback whose
5
5
  // rejection reaches the runtime and (in Bun) exits the process. One implementation, two
6
6
  // processes. Import from `@humwick/sdk` directly if you are outside this package.
7
7
  //
8
- // #2443 — `src/unguarded-async-ratchet.test.ts` has enforced the STRUCTURAL half of #1665 since
9
- // #1676 (no NEW unguarded site may appear in runner/src), but the actual process-level backstop
8
+ // `src/unguarded-async-ratchet.test.ts` has enforced the STRUCTURAL half of since
9
+ // (no NEW unguarded site may appear in runner/src), but the actual process-level backstop
10
10
  // (`installUnhandledRejectionGuard`) was never wired up here — only referenced by comments (see
11
11
  // runner-core.ts's `reportBackgroundRejection` doc). `runner/src/index.ts` installs it now.
12
12
  //
@@ -28,7 +28,7 @@ export function fireAndForget(label: string, work: Promise<unknown> | (() => Pro
28
28
  return sdkFireAndForget(`${PREFIX} ${label}`, work, log);
29
29
  }
30
30
 
31
- /** Install the non-fatal unhandled-rejection backstop for the runner daemon (#1665/#2443). See
31
+ /** Install the non-fatal unhandled-rejection backstop for the runner daemon. See
32
32
  * the SDK function for the "log and survive" reasoning and why `uncaughtException` stays fatal. */
33
33
  export function installUnhandledRejectionGuard(log?: GuardLog): () => void {
34
34
  return sdkInstallUnhandledRejectionGuard(PREFIX, log);
@@ -1,3 +1,4 @@
1
+ import { readHumwickRecord, withSharedHumwickRecord } from "@humwick/sdk/stored-names";
1
2
  import { mkdir, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
2
3
  import { homedir } from "node:os";
3
4
  import { basename, join } from "node:path";
@@ -40,7 +41,7 @@ function attachmentCacheRoot(agentId: string, rootDir = attachmentCacheDirFromEn
40
41
  }
41
42
 
42
43
  function attachmentCacheBase(rootDir = attachmentCacheDirFromEnv()): string {
43
- return rootDir || join(homedir(), ".agent-relay", "attachments");
44
+ return rootDir || join(homedir(), ".humwick", "attachments");
44
45
  }
45
46
 
46
47
  export async function messagesWithCachedAttachments(
@@ -112,36 +113,30 @@ async function cacheArtifact(client: AttachmentCacheClient, artifactId: string,
112
113
 
113
114
  function withCacheMetadata(ref: Record<string, unknown>, cached: CachedAttachment): Record<string, unknown> {
114
115
  const metadata = isRecord(ref.metadata) ? ref.metadata : {};
115
- const agentRelay = isRecord(metadata.agentRelay) ? metadata.agentRelay : {};
116
+ const humwick = readHumwickRecord(metadata) ?? {};
116
117
  return {
117
118
  ...ref,
118
- metadata: {
119
- ...metadata,
120
- agentRelay: {
121
- ...agentRelay,
122
- localPath: cached.path,
123
- filename: cached.filename,
124
- mediaType: cached.mediaType,
125
- size: cached.size,
126
- digest: cached.digest,
127
- cachedAt: new Date().toISOString(),
128
- },
129
- },
119
+ metadata: withSharedHumwickRecord(metadata, {
120
+ ...humwick,
121
+ localPath: cached.path,
122
+ filename: cached.filename,
123
+ mediaType: cached.mediaType,
124
+ size: cached.size,
125
+ digest: cached.digest,
126
+ cachedAt: new Date().toISOString(),
127
+ }),
130
128
  };
131
129
  }
132
130
 
133
131
  function withCacheError(ref: Record<string, unknown>, error: unknown): Record<string, unknown> {
134
132
  const metadata = isRecord(ref.metadata) ? ref.metadata : {};
135
- const agentRelay = isRecord(metadata.agentRelay) ? metadata.agentRelay : {};
133
+ const humwick = readHumwickRecord(metadata) ?? {};
136
134
  return {
137
135
  ...ref,
138
- metadata: {
139
- ...metadata,
140
- agentRelay: {
141
- ...agentRelay,
142
- cacheError: errMessage(error),
143
- },
144
- },
136
+ metadata: withSharedHumwickRecord(metadata, {
137
+ ...humwick,
138
+ cacheError: errMessage(error),
139
+ }),
145
140
  };
146
141
  }
147
142
 
@@ -1,21 +1,21 @@
1
- // #1856 — reconcile a leftover `background-script` busy that outlived its work.
1
+ // reconcile a leftover `background-script` busy that outlived its work.
2
2
  //
3
3
  // The Claude Stop hook (`plugins/claude/hooks/relay-status.sh` `relay_post_turn_end_status`)
4
4
  // posts a `background-script:background-tasks` busy record at turn-end whenever a
5
5
  // `run_in_background` shell is still running — but Claude's `background_tasks[]` payload carries
6
6
  // only a synthetic shell id, never an OS pid, so that record is posted WITHOUT `pids`. A record
7
- // with no pids can never be liveness-reconciled (#650 `reconcileLiveness` only touches pid-bearing
7
+ // with no pids can never be liveness-reconciled (`reconcileLiveness` only touches pid-bearing
8
8
  // records), and work records have no TTL, so the only thing that ever clears it is the NEXT Stop
9
9
  // hook — i.e. a fresh turn. If the background process finishes, or the operator kills it in the
10
10
  // Web terminal, nothing reconciles the belief: the agent reads `busy` forever, its turn-final
11
11
  // report is never flushed to the coordinator, and it holds a live-children quota slot doing nothing
12
- // (the #1856 wedge; #1068 is the sibling transient-reap symptom).
12
+ // (the wedge; is the sibling transient-reap symptom).
13
13
  //
14
14
  // This is the runner-side backstop that breaks the wedge WITHOUT depending on per-task pids the
15
15
  // hook cannot supply: once the agent is busy on background-script work ALONE (its provider turn has
16
16
  // already ended) and has made no forward progress for the stale window, the leftover record is
17
17
  // force-cleared and the agent reconciles to idle on its own. When pids ARE available (a caller that
18
- // can resolve them, or a future hook), the #650 pid reconcile still clears it the instant the
18
+ // can resolve them, or a future hook), the pid reconcile still clears it the instant the
19
19
  // process dies — this predicate only adds the no-pid backstop and is deliberately conservative:
20
20
  // - a live provider turn (or any non-background-script work) → NEVER stale (that work owns busy),
21
21
  // - a background-script record with a still-alive tracked pid → NEVER stale (demonstrably live),
@@ -49,7 +49,7 @@ export function isStaleBackgroundScriptOnlyBusy(input: StaleBackgroundScriptBusy
49
49
  const { activeWork } = input;
50
50
  // Something must be pinning busy, and it must be background-script work exclusively — a live
51
51
  // provider-turn, subagent, or any other work kind owns the busy state and must never be healed
52
- // here (the #1656/#1838 "never clear a live turn" invariant).
52
+ // here (the "never clear a live turn" invariant).
53
53
  if (activeWork.length === 0) return false;
54
54
  if (activeWork.some((work) => work.kind !== "background-script")) return false;
55
55
  // A tracked pid still alive is positive proof the background work is genuinely running — hold.
@@ -61,7 +61,7 @@ export function isStaleBackgroundScriptOnlyBusy(input: StaleBackgroundScriptBusy
61
61
  return input.now - lastProgressAt >= staleAfterMs;
62
62
  }
63
63
 
64
- // #2034 (#2031 L3) — build the per-task background-script LEDGER a watchdog (#2035/#2036)
64
+ // build the per-task background-script LEDGER a watchdog
65
65
  // reads to name a specific long-running script and its age, rather than just a count.
66
66
  //
67
67
  // The Claude Stop hook (`relay_background_task_list` in relay-status.sh) can only ever post
@@ -24,7 +24,7 @@ function size(path: string): number {
24
24
  }
25
25
 
26
26
  // Keep existing oversized logs intact for operator inspection; only bounded generations
27
- // participate in archive pruning (#2668).
27
+ // participate in archive pruning.
28
28
  function preserveLegacyFile(path: string, fileSize: number, maxBytes: number): void {
29
29
  if (fileSize <= maxBytes + Math.min(maxBytes, RUNNER_LOG_RECORD_MAX_BYTES)) return;
30
30
  try { renameSync(path, `${path}.legacy-${randomUUID()}`); } catch (error) {