@humwick/runner 0.130.4 → 0.130.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -167
- package/agent-bin/humwick +2 -2
- package/agent-bin/humwick-browser +4 -0
- package/hooks/fff-search-redirect/fff-search-redirect.cjs +1 -1
- package/hooks/web-tools-redirect/web-tools-redirect.cjs +2 -2
- package/package.json +8 -11
- package/plugins/browser/skills/playwright-cli/SKILL.md +8 -6
- package/plugins/claude/.claude-plugin/plugin.json +3 -3
- package/plugins/claude/claude-statusline-probe.cjs +26 -16
- package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +47 -17
- package/plugins/claude/hooks/gate-run-guard.sh +3 -3
- package/plugins/claude/hooks/gate-run-guard.ts +5 -5
- package/plugins/claude/hooks/kill-guard.provisioned.mjs +1 -1
- package/plugins/claude/hooks/kill-guard.sh +4 -4
- package/plugins/claude/hooks/kill-guard.ts +2 -2
- package/plugins/claude/hooks/permission-request.sh +1 -1
- package/plugins/claude/hooks/post-compact.sh +1 -1
- package/plugins/claude/hooks/post-tool-use.sh +3 -3
- package/plugins/claude/hooks/pre-compact.sh +2 -2
- package/plugins/claude/hooks/read-only-bash-guard.sh +4 -4
- package/plugins/claude/hooks/relay-status.sh +32 -32
- package/plugins/claude/hooks/session-end.sh +2 -2
- package/plugins/claude/hooks/session-start.sh +3 -3
- package/plugins/claude/hooks/shell-trust.ts +1 -1
- package/plugins/claude/hooks/stop-failure.sh +2 -2
- package/plugins/claude/hooks/stop.sh +6 -6
- package/plugins/claude/hooks/subagent-skill-policy.cjs +8 -6
- package/plugins/claude/hooks/subagent-stop.sh +3 -3
- package/plugins/claude/hooks/user-prompt-submit.sh +1 -1
- package/plugins/claude/hooks/wait-loop-guard.ts +3 -3
- package/plugins/claude/monitors/monitors.json +2 -2
- package/plugins/claude/monitors/relay-monitor.provisioned.mjs +96 -62
- package/plugins/claude/monitors/relay-monitor.ts +5 -5
- package/plugins/claude/skills/label/SKILL.md +3 -3
- package/plugins/claude/skills/message/SKILL.md +5 -5
- package/plugins/claude/skills/react/SKILL.md +1 -1
- package/plugins/claude/skills/read-message/SKILL.md +4 -4
- package/plugins/claude/skills/reply/SKILL.md +5 -5
- package/plugins/claude/skills/send-claimable/SKILL.md +5 -5
- package/plugins/claude/skills/slice-first/SKILL.md +5 -3
- package/plugins/claude/skills/slice-first/references/plan-recipe.md +30 -29
- package/plugins/claude/skills/status/SKILL.md +3 -3
- package/plugins/claude/skills/tags/SKILL.md +5 -5
- package/plugins/claude/skills/task/SKILL.md +10 -10
- package/plugins/claude-output-style/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-output-style/output-styles/{agent-relay.md → humwick.md} +1 -1
- package/plugins/codex/skills/guide/SKILL.md +3 -3
- package/plugins/codex/skills/label/SKILL.md +3 -3
- package/plugins/codex/skills/message/SKILL.md +5 -5
- package/plugins/codex/skills/react/SKILL.md +1 -1
- package/plugins/codex/skills/read-message/SKILL.md +4 -4
- package/plugins/codex/skills/reply/SKILL.md +5 -5
- package/plugins/codex/skills/send-claimable/SKILL.md +5 -5
- package/plugins/codex/skills/slice-first/SKILL.md +5 -3
- package/plugins/codex/skills/slice-first/references/plan-recipe.md +30 -29
- package/plugins/codex/skills/status/SKILL.md +3 -3
- package/plugins/codex/skills/tags/SKILL.md +5 -5
- package/plugins/codex/skills/task/SKILL.md +11 -10
- package/src/adapter.ts +95 -92
- package/src/adapters/claude-api.ts +8 -11
- package/src/adapters/claude-context-command.ts +1 -1
- package/src/adapters/claude-credentials.ts +7 -3
- package/src/adapters/claude-delivery-lanes.ts +2 -2
- package/src/adapters/claude-delivery.ts +16 -16
- package/src/adapters/claude-effort.ts +2 -2
- package/src/adapters/claude-exit.ts +2 -2
- package/src/adapters/claude-headless-mirror.ts +18 -2
- package/src/adapters/claude-headless-refusal.ts +3 -3
- package/src/adapters/claude-headless.ts +11 -10
- package/src/adapters/claude-launch-exit.ts +1 -1
- package/src/adapters/claude-monitor-delivery-plan.ts +36 -16
- package/src/adapters/claude-permission.ts +5 -5
- package/src/adapters/claude-prompt-gates.ts +9 -9
- package/src/adapters/claude-prompt-hook.ts +2 -2
- package/src/adapters/claude-session-capture.ts +6 -6
- package/src/adapters/claude-session-probe.ts +25 -25
- package/src/adapters/claude-shutdown.ts +11 -11
- package/src/adapters/claude-status-detectors.ts +31 -31
- package/src/adapters/claude-subagents.ts +7 -7
- package/src/adapters/claude-submit-confirmation.ts +6 -6
- package/src/adapters/claude-tmux.ts +43 -43
- package/src/adapters/claude-token-usage-capture.ts +5 -5
- package/src/adapters/claude-tool-policy.ts +7 -7
- package/src/adapters/claude-transcript.ts +24 -24
- package/src/adapters/claude.ts +100 -100
- package/src/adapters/codex-agent-message-capture.ts +26 -63
- package/src/adapters/codex-app-server.ts +1 -1
- package/src/adapters/codex-client.ts +9 -9
- package/src/adapters/codex-effective-config.ts +1 -1
- package/src/adapters/codex-launch-config.ts +5 -5
- package/src/adapters/codex-rollout-context.ts +2 -2
- package/src/adapters/codex-skill-discovery.ts +1 -1
- package/src/adapters/codex-token-usage-capture.ts +16 -16
- package/src/adapters/codex-tool-output.ts +4 -4
- package/src/adapters/codex-version.ts +3 -3
- package/src/adapters/codex.ts +64 -64
- package/src/adapters/opencode-api.ts +1 -1
- package/src/adapters/opencode-context.ts +20 -22
- package/src/adapters/opencode-model-registry.ts +7 -7
- package/src/adapters/opencode-provider.ts +8 -8
- package/src/adapters/opencode-provisioning.ts +30 -0
- package/src/adapters/opencode-server-retry.ts +1 -1
- package/src/adapters/opencode-soft-steer.ts +5 -5
- package/src/adapters/opencode-spawn.ts +18 -14
- package/src/adapters/opencode-token-usage-capture.ts +12 -12
- package/src/adapters/opencode-tool-events.ts +15 -15
- package/src/adapters/opencode.ts +232 -232
- package/src/adapters/project-shell.ts +11 -11
- package/src/adapters/service-access.ts +6 -6
- package/src/adapters/tmux-offload.ts +9 -9
- package/src/adapters/tool-summary.ts +5 -5
- package/src/agent-redact.ts +1 -1
- package/src/agent-runner/commands/dispatch.ts +10 -10
- package/src/agent-runner/commands/lifecycle.ts +2 -2
- package/src/agent-runner/commands/prompt.ts +19 -19
- package/src/agent-runner/credentials.ts +5 -5
- package/src/agent-runner/inbox.ts +1 -1
- package/src/agent-runner/lifecycle-milestones.ts +3 -3
- package/src/agent-runner/lifecycle-phase.ts +2 -2
- package/src/agent-runner/liveness-exchange.ts +4 -10
- package/src/agent-runner/registration.ts +8 -8
- package/src/agent-runner/runner-context.ts +9 -14
- package/src/agent-runner/session-mirror.ts +12 -12
- package/src/agent-runner/task-claims.ts +2 -2
- package/src/agent-runner/token-usage-coverage.ts +1 -1
- package/src/agent-runner/turn-report.ts +12 -12
- package/src/api-budget-accounting.ts +3 -3
- package/src/area-memory.ts +6 -6
- package/src/artifact-path-materialize.ts +1 -1
- package/src/async-guard.ts +5 -5
- package/src/attachment-cache.ts +17 -22
- package/src/background-script-reconcile.ts +6 -6
- package/src/bounded-log.ts +1 -1
- package/src/busy-reconciler.ts +22 -22
- package/src/claim-tracker.ts +3 -3
- package/src/claude-settings.ts +8 -8
- package/src/claude-system-prompt.ts +1 -1
- package/src/claude-workspace-trust.ts +1 -1
- package/src/config.ts +62 -61
- package/src/context-boundary.ts +5 -5
- package/src/context-command.ts +3 -3
- package/src/control-server.ts +32 -32
- package/src/delivery-result.ts +2 -2
- package/src/effort-command.ts +2 -2
- package/src/empty-turn.ts +22 -14
- package/src/free-port.ts +1 -1
- package/src/gate-run-config.ts +3 -3
- package/src/host-asset-plan.ts +24 -17
- package/src/host-claude-config-worker.ts +2 -2
- package/src/host-sources.ts +2 -2
- package/src/index.ts +16 -16
- package/src/launch-assembly.ts +94 -96
- package/src/liveness.ts +15 -15
- package/src/logger.ts +3 -3
- package/src/mcp-outbox.ts +3 -3
- package/src/mcp-preflight.ts +2 -2
- package/src/mcp-source-selection.ts +2 -2
- package/src/mcp-tool-deferral.ts +1 -1
- package/src/mcp-vault-env.ts +2 -2
- package/src/message-body-config/config.ts +4 -4
- package/src/mid-turn-delivery.ts +2 -2
- package/src/native-memory-lever.ts +8 -8
- package/src/outbox.ts +15 -15
- package/src/pending-permission-store.ts +1 -1
- package/src/profile-home.ts +19 -19
- package/src/profile-projection.ts +26 -24
- package/src/project-instructions.ts +5 -5
- package/src/provider-auth-warning.ts +17 -0
- package/src/provider-double.ts +1 -1
- package/src/provider-hold.ts +3 -3
- package/src/provider-home-marker.ts +1 -1
- package/src/provider-home-tombstone.ts +1 -1
- package/src/provider-input-queue.ts +2 -2
- package/src/provider-integration.ts +15 -13
- package/src/provider-setup.ts +15 -3
- package/src/providers.ts +1 -1
- package/src/provisioning.ts +37 -40
- package/src/question-decision.ts +2 -2
- package/src/quota.ts +0 -6
- package/src/rate-limit.ts +24 -13
- package/src/refused-prompt-store.ts +2 -2
- package/src/relay-injection-events.ts +2 -2
- package/src/relay-instructions.ts +5 -5
- package/src/relay-mcp-proxy.long-turn-e2e.ts +13 -13
- package/src/relay-mcp-proxy.ts +57 -58
- package/src/relay-mcp.ts +55 -69
- package/src/reply-obligation-cache.ts +9 -9
- package/src/response-capture-report.ts +9 -9
- package/src/response-routing.ts +1 -1
- package/src/runner-core.ts +209 -222
- package/src/runner-exit.ts +1 -1
- package/src/runner-helpers.ts +10 -10
- package/src/runner-insights.ts +4 -2
- package/src/runner-mcp-proxy.ts +3 -3
- package/src/runner-spawn-config.ts +3 -3
- package/src/runtime-token-file.ts +1 -1
- package/src/runtime-token-remint.ts +1 -1
- package/src/service-access.ts +2 -2
- package/src/session-destroy.ts +1 -1
- package/src/session-insights.ts +1 -1
- package/src/session-scratch.ts +24 -37
- package/src/spawn-input.ts +6 -6
- package/src/startup-config-fetch.ts +4 -4
- package/src/subreaper.ts +3 -5
- package/src/test-run-dedup.ts +13 -13
- package/src/transcript-context.ts +2 -2
- package/src/transport-errors.ts +1 -1
- package/src/turn-opener.ts +1 -1
- package/src/turn-reconcile-event.ts +2 -2
- package/src/vault-proxy-hop.ts +2 -2
- package/src/web-tools.ts +2 -2
- package/agent-bin/agent-relay-browser +0 -10
- package/plugins/claude/skills/disconnect/SKILL.md +0 -16
- package/plugins/claude/skills/pair/SKILL.md +0 -30
- package/plugins/codex/skills/disconnect/SKILL.md +0 -15
- package/plugins/codex/skills/pair/SKILL.md +0 -29
- package/src/adapters/provider-quota-harvest.ts +0 -76
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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("
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
/**
|
|
13
|
+
/** effective-catalog window from the spawn payload; threaded into the adapter config. */
|
|
17
14
|
contextWindowTokens?: number;
|
|
18
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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" (
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
|
83
|
-
//
|
|
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
|
-
//
|
|
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; //
|
|
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()) { //
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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 {
|
|
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, "
|
|
68
|
-
method: "POST", headers: { "content-type": "application/json", ...(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;
|
package/src/area-memory.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
//
|
|
2
|
-
// names is matched to the repo's path areas (`.
|
|
3
|
-
// context generation is reported to POST /api/agents/:id/area-entered
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 };
|
package/src/async-guard.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
//
|
|
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
|
|
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
|
-
//
|
|
9
|
-
//
|
|
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
|
|
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);
|
package/src/attachment-cache.ts
CHANGED
|
@@ -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(), ".
|
|
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
|
|
116
|
+
const humwick = readHumwickRecord(metadata) ?? {};
|
|
116
117
|
return {
|
|
117
118
|
...ref,
|
|
118
|
-
metadata: {
|
|
119
|
-
...
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
133
|
+
const humwick = readHumwickRecord(metadata) ?? {};
|
|
136
134
|
return {
|
|
137
135
|
...ref,
|
|
138
|
-
metadata: {
|
|
139
|
-
...
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
//
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
package/src/bounded-log.ts
CHANGED
|
@@ -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
|
|
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) {
|