@humwick/runner 0.0.0-stage → 0.130.0
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 +167 -0
- package/agent-bin/agent-relay-browser +10 -0
- package/agent-bin/humwick +10 -0
- package/hooks/fff-search-redirect/fff-search-redirect.cjs +161 -0
- package/hooks/fff-search-redirect/hooks.json +13 -0
- package/hooks/rg-r-guard/hooks.json +18 -0
- package/hooks/rg-r-guard/rg-r-guard.cjs +144 -0
- package/hooks/web-tools-redirect/hooks.json +13 -0
- package/hooks/web-tools-redirect/web-tools-redirect.cjs +84 -0
- package/node_modules/@humwick/offload/LICENSE +167 -0
- package/node_modules/@humwick/offload/README.md +57 -0
- package/node_modules/@humwick/offload/dist/async-runner.d.ts +18 -0
- package/node_modules/@humwick/offload/dist/async-runner.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/async-runner.js +158 -0
- package/node_modules/@humwick/offload/dist/async-runner.js.map +1 -0
- package/node_modules/@humwick/offload/dist/envelope.d.ts +25 -0
- package/node_modules/@humwick/offload/dist/envelope.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/envelope.js +80 -0
- package/node_modules/@humwick/offload/dist/envelope.js.map +1 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts +10 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js +59 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js.map +1 -0
- package/node_modules/@humwick/offload/dist/index.d.ts +5 -0
- package/node_modules/@humwick/offload/dist/index.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/index.js +5 -0
- package/node_modules/@humwick/offload/dist/index.js.map +1 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts +62 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.js +295 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.js.map +1 -0
- package/node_modules/@humwick/offload/dist/types.d.ts +111 -0
- package/node_modules/@humwick/offload/dist/types.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/types.js +176 -0
- package/node_modules/@humwick/offload/dist/types.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts +23 -0
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-pool.js +435 -0
- package/node_modules/@humwick/offload/dist/worker-pool.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts +2 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.js +27 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker.d.ts +5 -0
- package/node_modules/@humwick/offload/dist/worker.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker.js +1 -0
- package/node_modules/@humwick/offload/dist/worker.js.map +1 -0
- package/node_modules/@humwick/offload/package.json +46 -0
- package/node_modules/@humwick/offload/src/async-runner.ts +170 -0
- package/node_modules/@humwick/offload/src/envelope.ts +92 -0
- package/node_modules/@humwick/offload/src/fixtures/worker-task.ts +49 -0
- package/node_modules/@humwick/offload/src/index.ts +4 -0
- package/node_modules/@humwick/offload/src/subprocess-runner.ts +334 -0
- package/node_modules/@humwick/offload/src/types.ts +235 -0
- package/node_modules/@humwick/offload/src/worker-pool.ts +463 -0
- package/node_modules/@humwick/offload/src/worker-runtime.ts +32 -0
- package/node_modules/@humwick/offload/src/worker.ts +5 -0
- package/package.json +47 -4
- package/plugins/browser/skills/playwright-cli/SKILL.md +53 -0
- package/plugins/claude/.claude-plugin/plugin.json +8 -0
- package/plugins/claude/claude-statusline-probe.cjs +144 -0
- package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +914 -0
- package/plugins/claude/hooks/gate-run-guard.sh +9 -0
- package/plugins/claude/hooks/gate-run-guard.ts +237 -0
- package/plugins/claude/hooks/hooks.json +158 -0
- package/plugins/claude/hooks/kill-guard.provisioned.mjs +7027 -0
- package/plugins/claude/hooks/kill-guard.sh +341 -0
- package/plugins/claude/hooks/kill-guard.ts +337 -0
- package/plugins/claude/hooks/permission-request.sh +22 -0
- package/plugins/claude/hooks/post-compact.sh +10 -0
- package/plugins/claude/hooks/post-tool-use.sh +8 -0
- package/plugins/claude/hooks/pre-compact.sh +12 -0
- package/plugins/claude/hooks/read-only-bash-guard.sh +587 -0
- package/plugins/claude/hooks/relay-status.sh +373 -0
- package/plugins/claude/hooks/session-end.sh +26 -0
- package/plugins/claude/hooks/session-start.sh +31 -0
- package/plugins/claude/hooks/shell-trust.ts +28 -0
- package/plugins/claude/hooks/stop-failure.sh +47 -0
- package/plugins/claude/hooks/stop.sh +48 -0
- package/plugins/claude/hooks/subagent-guard.cjs +26 -0
- package/plugins/claude/hooks/subagent-skill-policy.cjs +160 -0
- package/plugins/claude/hooks/subagent-start.sh +13 -0
- package/plugins/claude/hooks/subagent-stop.sh +19 -0
- package/plugins/claude/hooks/user-prompt-submit.sh +18 -0
- package/plugins/claude/hooks/wait-loop-guard.ts +120 -0
- package/plugins/claude/monitors/monitors.json +7 -0
- package/plugins/claude/monitors/relay-monitor.provisioned.mjs +786 -0
- package/plugins/claude/monitors/relay-monitor.ts +58 -0
- package/plugins/claude/skills/disconnect/SKILL.md +16 -0
- package/plugins/claude/skills/label/SKILL.md +23 -0
- package/plugins/claude/skills/message/SKILL.md +40 -0
- package/plugins/claude/skills/pair/SKILL.md +30 -0
- package/plugins/claude/skills/react/SKILL.md +18 -0
- package/plugins/claude/skills/read-message/SKILL.md +24 -0
- package/plugins/claude/skills/reply/SKILL.md +44 -0
- package/plugins/claude/skills/send-claimable/SKILL.md +39 -0
- package/plugins/claude/skills/slice-first/SKILL.md +32 -0
- package/plugins/claude/skills/slice-first/references/plan-recipe.md +46 -0
- package/plugins/claude/skills/status/SKILL.md +16 -0
- package/plugins/claude/skills/tags/SKILL.md +25 -0
- package/plugins/claude/skills/task/SKILL.md +28 -0
- package/plugins/claude-output-style/.claude-plugin/plugin.json +5 -0
- package/plugins/claude-output-style/output-styles/agent-relay.md +5 -0
- package/plugins/codex/skills/disconnect/SKILL.md +15 -0
- package/plugins/codex/skills/guide/SKILL.md +15 -0
- package/plugins/codex/skills/label/SKILL.md +22 -0
- package/plugins/codex/skills/message/SKILL.md +40 -0
- package/plugins/codex/skills/pair/SKILL.md +29 -0
- package/plugins/codex/skills/react/SKILL.md +17 -0
- package/plugins/codex/skills/read-message/SKILL.md +23 -0
- package/plugins/codex/skills/reply/SKILL.md +44 -0
- package/plugins/codex/skills/send-claimable/SKILL.md +39 -0
- package/plugins/codex/skills/slice-first/SKILL.md +32 -0
- package/plugins/codex/skills/slice-first/references/plan-recipe.md +46 -0
- package/plugins/codex/skills/status/SKILL.md +15 -0
- package/plugins/codex/skills/tags/SKILL.md +24 -0
- package/plugins/codex/skills/task/SKILL.md +28 -0
- package/src/adapter.ts +1061 -0
- package/src/adapters/__fixtures__/codex-app-server.ts +47 -0
- package/src/adapters/claude-api.ts +243 -0
- package/src/adapters/claude-context-command.ts +16 -0
- package/src/adapters/claude-credentials.ts +22 -0
- package/src/adapters/claude-delivery-lanes.ts +28 -0
- package/src/adapters/claude-delivery.ts +166 -0
- package/src/adapters/claude-effort.ts +94 -0
- package/src/adapters/claude-exit.ts +290 -0
- package/src/adapters/claude-headless-mirror.ts +88 -0
- package/src/adapters/claude-headless-refusal.ts +51 -0
- package/src/adapters/claude-headless.ts +617 -0
- package/src/adapters/claude-monitor-delivery-plan.ts +317 -0
- package/src/adapters/claude-permission.ts +243 -0
- package/src/adapters/claude-prompt-gates.ts +269 -0
- package/src/adapters/claude-prompt-hook.ts +145 -0
- package/src/adapters/claude-session-capture.ts +293 -0
- package/src/adapters/claude-session-probe.ts +414 -0
- package/src/adapters/claude-shutdown.ts +190 -0
- package/src/adapters/claude-status-detectors.ts +426 -0
- package/src/adapters/claude-subagents.ts +93 -0
- package/src/adapters/claude-submit-confirmation.ts +287 -0
- package/src/adapters/claude-tmux.ts +548 -0
- package/src/adapters/claude-token-usage-capture.ts +253 -0
- package/src/adapters/claude-tool-policy.ts +165 -0
- package/src/adapters/claude-transcript-tail.ts +193 -0
- package/src/adapters/claude-transcript.ts +812 -0
- package/src/adapters/claude.ts +1097 -0
- package/src/adapters/codex-agent-message-capture.ts +189 -0
- package/src/adapters/codex-app-server.ts +70 -0
- package/src/adapters/codex-client.ts +362 -0
- package/src/adapters/codex-effective-config.ts +87 -0
- package/src/adapters/codex-launch-config.ts +153 -0
- package/src/adapters/codex-rollout-context.ts +272 -0
- package/src/adapters/codex-skill-discovery.ts +18 -0
- package/src/adapters/codex-token-usage-capture.ts +163 -0
- package/src/adapters/codex-tool-output.ts +53 -0
- package/src/adapters/codex-turn-outcome.ts +27 -0
- package/src/adapters/codex-version.ts +64 -0
- package/src/adapters/codex.ts +1442 -0
- package/src/adapters/incremental-file.ts +110 -0
- package/src/adapters/opencode-api.ts +71 -0
- package/src/adapters/opencode-context.ts +277 -0
- package/src/adapters/opencode-model-registry.ts +126 -0
- package/src/adapters/opencode-provider.ts +169 -0
- package/src/adapters/opencode-server-retry.ts +50 -0
- package/src/adapters/opencode-soft-steer.ts +302 -0
- package/src/adapters/opencode-spawn.ts +145 -0
- package/src/adapters/opencode-subscribe.ts +62 -0
- package/src/adapters/opencode-token-usage-capture.ts +187 -0
- package/src/adapters/opencode-tool-events.ts +302 -0
- package/src/adapters/opencode-tools-config.ts +17 -0
- package/src/adapters/opencode.ts +2264 -0
- package/src/adapters/project-shell.ts +80 -0
- package/src/adapters/provider-launch-config.ts +21 -0
- package/src/adapters/provider-quota-harvest.ts +127 -0
- package/src/adapters/service-access.ts +153 -0
- package/src/adapters/tmux-offload.ts +207 -0
- package/src/adapters/tool-summary.ts +214 -0
- package/src/agent-redact.ts +75 -0
- package/src/agent-runner/commands/dispatch.ts +185 -0
- package/src/agent-runner/commands/lifecycle.ts +84 -0
- package/src/agent-runner/commands/prompt.ts +284 -0
- package/src/agent-runner/credentials.ts +208 -0
- package/src/agent-runner/http-liveness.ts +85 -0
- package/src/agent-runner/lifecycle-milestones.ts +207 -0
- package/src/agent-runner/lifecycle-phase.ts +80 -0
- package/src/agent-runner/registration.ts +163 -0
- package/src/agent-runner/runner-context.ts +158 -0
- package/src/agent-runner/session-mirror.ts +278 -0
- package/src/agent-runner/task-claims.ts +116 -0
- package/src/agent-runner/token-usage-coverage.ts +54 -0
- package/src/agent-runner/turn-report.ts +269 -0
- package/src/api-budget-accounting.ts +91 -0
- package/src/area-memory.ts +255 -0
- package/src/artifact-path-materialize.ts +52 -0
- package/src/async-guard.ts +35 -0
- package/src/attachment-cache-sweep.ts +31 -0
- package/src/attachment-cache.ts +195 -0
- package/src/background-script-reconcile.ts +106 -0
- package/src/bounded-log.ts +59 -0
- package/src/bounded-spawn-sync.ts +18 -0
- package/src/busy-reconciler.ts +282 -0
- package/src/claim-tracker.ts +140 -0
- package/src/claude-settings.ts +167 -0
- package/src/claude-workspace-trust.ts +289 -0
- package/src/config.ts +547 -0
- package/src/context-boundary.ts +101 -0
- package/src/context-command.ts +151 -0
- package/src/context-probe-state.ts +6 -0
- package/src/continuation-archive.ts +179 -0
- package/src/control-server.ts +730 -0
- package/src/delivery-policy.ts +9 -0
- package/src/delivery-result.ts +24 -0
- package/src/effort-command.ts +125 -0
- package/src/empty-turn.ts +816 -0
- package/src/free-port.ts +25 -0
- package/src/fs-offload.ts +26 -0
- package/src/gate-run-config.ts +35 -0
- package/src/gate-run-lease-probe.ts +38 -0
- package/src/host-asset-plan.ts +450 -0
- package/src/host-claude-config-worker.ts +34 -0
- package/src/host-sources.ts +40 -0
- package/src/inbox-carrier.ts +7 -0
- package/src/index.ts +386 -0
- package/src/launch-assembly.ts +955 -0
- package/src/liveness.ts +105 -0
- package/src/logger.ts +108 -0
- package/src/mcp-outbox.ts +204 -0
- package/src/mcp-preflight.ts +86 -0
- package/src/mcp-source-selection.ts +54 -0
- package/src/mcp-tool-deferral.ts +35 -0
- package/src/mcp-vault-env.ts +25 -0
- package/src/message-body-config/config.ts +16 -0
- package/src/message-body-config.ts +1 -0
- package/src/mid-turn-delivery.ts +43 -0
- package/src/native-memory-lever.ts +329 -0
- package/src/native-self-resume.ts +244 -0
- package/src/outbox.ts +453 -0
- package/src/park-post.ts +17 -0
- package/src/pending-permission-store.ts +148 -0
- package/src/process-meta.ts +9 -0
- package/src/profile-home.ts +419 -0
- package/src/profile-projection.ts +411 -0
- package/src/project-hooks.ts +65 -0
- package/src/project-instructions.ts +119 -0
- package/src/provider-double.ts +59 -0
- package/src/provider-hold.ts +228 -0
- package/src/provider-home-marker.ts +3 -0
- package/src/provider-home-tombstone.ts +4 -0
- package/src/provider-input-queue.ts +134 -0
- package/src/provider-integration.ts +93 -0
- package/src/provider-processes.ts +17 -0
- package/src/provider-submit-failure.ts +15 -0
- package/src/providers.ts +35 -0
- package/src/provisioning.ts +703 -0
- package/src/question-decision.ts +18 -0
- package/src/quota.ts +22 -0
- package/src/rate-limit.ts +351 -0
- package/src/refused-prompt-store.ts +36 -0
- package/src/relay-injection-events.ts +161 -0
- package/src/relay-instructions.ts +107 -0
- package/src/relay-mcp-proxy.long-turn-e2e.ts +183 -0
- package/src/relay-mcp-proxy.ts +704 -0
- package/src/relay-mcp.ts +583 -0
- package/src/reply-obligation-cache.ts +211 -0
- package/src/response-capture-report.ts +111 -0
- package/src/response-routing.ts +6 -0
- package/src/runner-core.ts +2955 -0
- package/src/runner-exit.ts +12 -0
- package/src/runner-helpers.ts +582 -0
- package/src/runner-insights.ts +105 -0
- package/src/runner-mcp-proxy.ts +46 -0
- package/src/runner-spawn-config.ts +68 -0
- package/src/runner-stale-scratch-sweep.ts +21 -0
- package/src/runner.ts +9 -0
- package/src/runtime-token-file.ts +45 -0
- package/src/runtime-token-remint.ts +56 -0
- package/src/service-access.ts +88 -0
- package/src/session-destroy.ts +27 -0
- package/src/session-insights.ts +118 -0
- package/src/session-scratch.ts +296 -0
- package/src/spawn-input.ts +147 -0
- package/src/startup-config-fetch.ts +48 -0
- package/src/subreaper.ts +113 -0
- package/src/template-resolver.ts +29 -0
- package/src/test-run-dedup.ts +435 -0
- package/src/transcript-context.ts +99 -0
- package/src/transport-errors.ts +48 -0
- package/src/turn-opener.ts +55 -0
- package/src/turn-reconcile-event.ts +88 -0
- package/src/vault-proxy-hop.ts +99 -0
- package/src/version.ts +44 -0
- package/src/web-tool-call-proxy.ts +128 -0
- package/src/web-tools.ts +59 -0
- package/README.md +0 -3
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { maxArtifactBytesFromEnv, maxPathUploadRawBytes, type TokenConstraints } from "@humwick/sdk";
|
|
2
|
+
import { openArtifactPathSource } from "@humwick/sdk/artifact-path-roots";
|
|
3
|
+
|
|
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
|
|
6
|
+
// proxy runs beside the agent, reads the file here, and forwards bytes as base64
|
|
7
|
+
// (agent→proxy stays a small path arg; only proxy→relay carries the bytes).
|
|
8
|
+
export type PathMaterializeResult = { args?: Record<string, unknown>; error?: string };
|
|
9
|
+
|
|
10
|
+
type TokenClaims = { constraints?: TokenConstraints; scope?: unknown };
|
|
11
|
+
|
|
12
|
+
function decodeClaims(token: string | undefined): TokenClaims | undefined {
|
|
13
|
+
const payload = token?.split(".")[1];
|
|
14
|
+
if (!payload) return undefined;
|
|
15
|
+
try {
|
|
16
|
+
const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
17
|
+
return JSON.parse(Buffer.from(normalized, "base64").toString("utf8")) as TokenClaims;
|
|
18
|
+
} catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function hasWildcardScope(claims: TokenClaims): boolean {
|
|
24
|
+
return Array.isArray(claims.scope) && claims.scope.includes("*");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function materializeArtifactPathUpload(
|
|
28
|
+
args: Record<string, unknown>,
|
|
29
|
+
getToken: () => string | undefined,
|
|
30
|
+
): Promise<PathMaterializeResult> {
|
|
31
|
+
if (typeof args.path !== "string") return {};
|
|
32
|
+
if (args.content !== undefined || args.base64 !== undefined) return {};
|
|
33
|
+
const claims = decodeClaims(getToken());
|
|
34
|
+
if (!claims) return {};
|
|
35
|
+
const rawLimit = maxPathUploadRawBytes(maxArtifactBytesFromEnv());
|
|
36
|
+
let source: Awaited<ReturnType<typeof openArtifactPathSource>> | undefined;
|
|
37
|
+
try {
|
|
38
|
+
source = await openArtifactPathSource(args.path.trim(), {
|
|
39
|
+
constraints: claims.constraints,
|
|
40
|
+
unrestricted: hasWildcardScope(claims),
|
|
41
|
+
maxBytes: rawLimit,
|
|
42
|
+
});
|
|
43
|
+
const bytes = Buffer.from(await new Response(source.stream).arrayBuffer());
|
|
44
|
+
if (bytes.length > rawLimit) return { error: `artifact exceeds ${rawLimit} bytes` };
|
|
45
|
+
const { path: _dropped, ...rest } = args;
|
|
46
|
+
return { args: { ...rest, base64: bytes.toString("base64"), ...(typeof args.filename === "string" ? {} : { filename: source.filename }) } };
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return { error: (error as Error).message };
|
|
49
|
+
} finally {
|
|
50
|
+
await source?.cleanup();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// #1665 — runner adapter over the SHARED async guards in `@humwick/sdk`.
|
|
2
|
+
//
|
|
3
|
+
// The primitives themselves live in `sdk/src/async-guard.ts` because #1676 is the same
|
|
4
|
+
// defect class in the orchestrator: an unguarded `void <promise>` / async timer callback whose
|
|
5
|
+
// rejection reaches the runtime and (in Bun) exits the process. One implementation, two
|
|
6
|
+
// processes. Import from `@humwick/sdk` directly if you are outside this package.
|
|
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
|
|
10
|
+
// (`installUnhandledRejectionGuard`) was never wired up here — only referenced by comments (see
|
|
11
|
+
// runner-core.ts's `reportBackgroundRejection` doc). `runner/src/index.ts` installs it now.
|
|
12
|
+
//
|
|
13
|
+
// All this file adds is the `[runner]` log prefix, so the daemon's log format is unchanged and
|
|
14
|
+
// call sites pass a bare label ("opencode server exit callback", not the prefix).
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
fireAndForget as sdkFireAndForget,
|
|
18
|
+
installUnhandledRejectionGuard as sdkInstallUnhandledRejectionGuard,
|
|
19
|
+
type GuardLog,
|
|
20
|
+
} from "@humwick/sdk";
|
|
21
|
+
|
|
22
|
+
export { type GuardLog } from "@humwick/sdk";
|
|
23
|
+
|
|
24
|
+
const PREFIX = "[runner]";
|
|
25
|
+
|
|
26
|
+
/** See {@link import("@humwick/sdk").fireAndForget}. Replaces bare `void promise()`. */
|
|
27
|
+
export function fireAndForget(label: string, work: Promise<unknown> | (() => Promise<unknown> | unknown), log?: GuardLog): Promise<void> {
|
|
28
|
+
return sdkFireAndForget(`${PREFIX} ${label}`, work, log);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Install the non-fatal unhandled-rejection backstop for the runner daemon (#1665/#2443). See
|
|
32
|
+
* the SDK function for the "log and survive" reasoning and why `uncaughtException` stays fatal. */
|
|
33
|
+
export function installUnhandledRejectionGuard(log?: GuardLog): () => void {
|
|
34
|
+
return sdkInstallUnhandledRejectionGuard(PREFIX, log);
|
|
35
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { errMessage } from "@humwick/sdk";
|
|
2
|
+
import { pruneAttachmentCache } from "./attachment-cache";
|
|
3
|
+
import { runnerFsOffload } from "./fs-offload";
|
|
4
|
+
|
|
5
|
+
const SWEEP_INTERVAL_MS = 60 * 60 * 1000;
|
|
6
|
+
|
|
7
|
+
/** Process-local maintenance: intentionally separate from message delivery. */
|
|
8
|
+
export class AttachmentCacheSweep {
|
|
9
|
+
private timer?: Timer;
|
|
10
|
+
private inFlight = false;
|
|
11
|
+
|
|
12
|
+
constructor(private readonly agentId: string, private readonly log: (message: string) => void) {}
|
|
13
|
+
|
|
14
|
+
start(): void {
|
|
15
|
+
if (this.timer) return;
|
|
16
|
+
const sweep = () => {
|
|
17
|
+
if (this.inFlight) return;
|
|
18
|
+
this.inFlight = true;
|
|
19
|
+
void runnerFsOffload().run("runner-attachment-cache-sweep", () => pruneAttachmentCache(this.agentId))
|
|
20
|
+
.catch((error) => this.log(`attachment cache sweep failed: ${errMessage(error)}`))
|
|
21
|
+
.finally(() => { this.inFlight = false; });
|
|
22
|
+
};
|
|
23
|
+
sweep();
|
|
24
|
+
this.timer = setInterval(sweep, SWEEP_INTERVAL_MS);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
stop(): void {
|
|
28
|
+
if (this.timer) clearInterval(this.timer);
|
|
29
|
+
this.timer = undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { mkdir, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, join } from "node:path";
|
|
4
|
+
import type { Artifact, Message } from "@humwick/sdk";
|
|
5
|
+
import { errMessage, isRecord } from "@humwick/sdk";
|
|
6
|
+
import { sanitizeFsName } from "@humwick/sdk/fs-name";
|
|
7
|
+
import { attachmentCacheDirFromEnv } from "./config";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
10
|
+
|
|
11
|
+
interface AttachmentCacheClient {
|
|
12
|
+
downloadArtifact(id: string): Promise<{ stream: ReadableStream<Uint8Array>; meta: Artifact }>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface AttachmentCacheOptions {
|
|
16
|
+
agentId: string;
|
|
17
|
+
rootDir?: string;
|
|
18
|
+
maxAgeMs?: number;
|
|
19
|
+
now?: number;
|
|
20
|
+
onError?: (message: string) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface CachedAttachment {
|
|
24
|
+
path: string;
|
|
25
|
+
filename: string;
|
|
26
|
+
mediaType: string;
|
|
27
|
+
size: number;
|
|
28
|
+
digest: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function attachmentRefs(message: Message): Record<string, unknown>[] {
|
|
32
|
+
const payloadRefs = message.payload?.attachments;
|
|
33
|
+
const topLevelRefs = (message as Message & { attachments?: unknown }).attachments;
|
|
34
|
+
const refs = Array.isArray(payloadRefs) ? payloadRefs : Array.isArray(topLevelRefs) ? topLevelRefs : [];
|
|
35
|
+
return refs.filter(isRecord);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function attachmentCacheRoot(agentId: string, rootDir = attachmentCacheDirFromEnv()): string {
|
|
39
|
+
return join(attachmentCacheBase(rootDir), safePathPart(agentId));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function attachmentCacheBase(rootDir = attachmentCacheDirFromEnv()): string {
|
|
43
|
+
return rootDir || join(homedir(), ".agent-relay", "attachments");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function messagesWithCachedAttachments(
|
|
47
|
+
messages: Message[],
|
|
48
|
+
client: AttachmentCacheClient,
|
|
49
|
+
options: AttachmentCacheOptions,
|
|
50
|
+
): Promise<Message[]> {
|
|
51
|
+
const enriched: Message[] = [];
|
|
52
|
+
for (const message of messages) enriched.push(await messageWithCachedAttachments(message, client, options));
|
|
53
|
+
return enriched;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function messageWithCachedAttachments(
|
|
57
|
+
message: Message,
|
|
58
|
+
client: AttachmentCacheClient,
|
|
59
|
+
options: AttachmentCacheOptions,
|
|
60
|
+
): Promise<Message> {
|
|
61
|
+
const refs = attachmentRefs(message);
|
|
62
|
+
if (!refs.length) return message;
|
|
63
|
+
const nextRefs = [];
|
|
64
|
+
let changed = false;
|
|
65
|
+
for (const ref of refs) {
|
|
66
|
+
const artifactId = typeof ref.artifactId === "string" ? ref.artifactId.trim() : "";
|
|
67
|
+
if (!artifactId) {
|
|
68
|
+
nextRefs.push(ref);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const cached = await cacheArtifact(client, artifactId, options);
|
|
73
|
+
nextRefs.push(withCacheMetadata(ref, cached));
|
|
74
|
+
changed = true;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
options.onError?.(`attachment ${artifactId} cache failed: ${errMessage(error)}`);
|
|
77
|
+
nextRefs.push(withCacheError(ref, error));
|
|
78
|
+
changed = true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!changed) return message;
|
|
82
|
+
return {
|
|
83
|
+
...message,
|
|
84
|
+
payload: {
|
|
85
|
+
...message.payload,
|
|
86
|
+
attachments: nextRefs,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function cacheArtifact(client: AttachmentCacheClient, artifactId: string, options: AttachmentCacheOptions): Promise<CachedAttachment> {
|
|
92
|
+
const { stream, meta } = await client.downloadArtifact(artifactId);
|
|
93
|
+
const root = attachmentCacheRoot(options.agentId, options.rootDir);
|
|
94
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
95
|
+
const filename = safeFilename(meta.filename || artifactId);
|
|
96
|
+
const digestPart = safePathPart((meta.digest || meta.blobDigest || artifactId).replace(/^sha256:/, "")).slice(0, 24) || safePathPart(artifactId);
|
|
97
|
+
const path = join(root, `${digestPart}-${filename}`);
|
|
98
|
+
try {
|
|
99
|
+
const current = await stat(path);
|
|
100
|
+
if (current.isFile() && current.size === meta.size) {
|
|
101
|
+
return { path, filename, mediaType: meta.mediaType, size: meta.size, digest: meta.digest };
|
|
102
|
+
}
|
|
103
|
+
} catch {}
|
|
104
|
+
const bytes = new Uint8Array(await new Response(stream).arrayBuffer());
|
|
105
|
+
if (bytes.byteLength !== meta.size) throw new Error(`artifact size mismatch: expected ${meta.size}, got ${bytes.byteLength}`);
|
|
106
|
+
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
107
|
+
await writeFile(temp, bytes, { mode: 0o600 });
|
|
108
|
+
await rm(path, { force: true });
|
|
109
|
+
await rename(temp, path);
|
|
110
|
+
return { path, filename, mediaType: meta.mediaType, size: meta.size, digest: meta.digest };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function withCacheMetadata(ref: Record<string, unknown>, cached: CachedAttachment): Record<string, unknown> {
|
|
114
|
+
const metadata = isRecord(ref.metadata) ? ref.metadata : {};
|
|
115
|
+
const agentRelay = isRecord(metadata.agentRelay) ? metadata.agentRelay : {};
|
|
116
|
+
return {
|
|
117
|
+
...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
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function withCacheError(ref: Record<string, unknown>, error: unknown): Record<string, unknown> {
|
|
134
|
+
const metadata = isRecord(ref.metadata) ? ref.metadata : {};
|
|
135
|
+
const agentRelay = isRecord(metadata.agentRelay) ? metadata.agentRelay : {};
|
|
136
|
+
return {
|
|
137
|
+
...ref,
|
|
138
|
+
metadata: {
|
|
139
|
+
...metadata,
|
|
140
|
+
agentRelay: {
|
|
141
|
+
...agentRelay,
|
|
142
|
+
cacheError: errMessage(error),
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function pruneAttachmentCache(
|
|
149
|
+
agentId: string,
|
|
150
|
+
rootDir = attachmentCacheDirFromEnv(),
|
|
151
|
+
maxAgeMs = DEFAULT_CACHE_MAX_AGE_MS,
|
|
152
|
+
now = Date.now(),
|
|
153
|
+
): Promise<void> {
|
|
154
|
+
const base = attachmentCacheBase(rootDir);
|
|
155
|
+
let entries: string[];
|
|
156
|
+
try {
|
|
157
|
+
entries = await readdir(base);
|
|
158
|
+
} catch {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const roots = [attachmentCacheRoot(agentId, rootDir), ...entries.map((entry) => join(base, entry))];
|
|
162
|
+
await Promise.all([...new Set(roots)].map((root) => pruneAttachmentCacheDir(root, maxAgeMs, now)));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function pruneAttachmentCacheDir(root: string, maxAgeMs: number, now: number): Promise<void> {
|
|
166
|
+
let entries: string[];
|
|
167
|
+
try {
|
|
168
|
+
entries = await readdir(root);
|
|
169
|
+
} catch {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
await Promise.all(entries.map(async (entry) => {
|
|
173
|
+
const path = join(root, entry);
|
|
174
|
+
try {
|
|
175
|
+
const current = await stat(path);
|
|
176
|
+
if (current.isFile() && now - current.mtimeMs > maxAgeMs) await rm(path, { force: true });
|
|
177
|
+
} catch {
|
|
178
|
+
// Best-effort cache cleanup only.
|
|
179
|
+
}
|
|
180
|
+
}));
|
|
181
|
+
try {
|
|
182
|
+
if ((await readdir(root)).length === 0) await rm(root, { recursive: true, force: true });
|
|
183
|
+
} catch {
|
|
184
|
+
// Best-effort cache cleanup only.
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function safeFilename(value: string): string {
|
|
189
|
+
const name = safePathPart(basename(value));
|
|
190
|
+
return name || "artifact";
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function safePathPart(value: string): string {
|
|
194
|
+
return sanitizeFsName(value, { replacement: "_", collapseReplacement: true, maxLen: 180 });
|
|
195
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// #1856 — reconcile a leftover `background-script` busy that outlived its work.
|
|
2
|
+
//
|
|
3
|
+
// The Claude Stop hook (`plugins/claude/hooks/relay-status.sh` `relay_post_turn_end_status`)
|
|
4
|
+
// posts a `background-script:background-tasks` busy record at turn-end whenever a
|
|
5
|
+
// `run_in_background` shell is still running — but Claude's `background_tasks[]` payload carries
|
|
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
|
|
8
|
+
// records), and work records have no TTL, so the only thing that ever clears it is the NEXT Stop
|
|
9
|
+
// hook — i.e. a fresh turn. If the background process finishes, or the operator kills it in the
|
|
10
|
+
// Web terminal, nothing reconciles the belief: the agent reads `busy` forever, its turn-final
|
|
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).
|
|
13
|
+
//
|
|
14
|
+
// This is the runner-side backstop that breaks the wedge WITHOUT depending on per-task pids the
|
|
15
|
+
// hook cannot supply: once the agent is busy on background-script work ALONE (its provider turn has
|
|
16
|
+
// already ended) and has made no forward progress for the stale window, the leftover record is
|
|
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
|
|
19
|
+
// process dies — this predicate only adds the no-pid backstop and is deliberately conservative:
|
|
20
|
+
// - a live provider turn (or any non-background-script work) → NEVER stale (that work owns busy),
|
|
21
|
+
// - a background-script record with a still-alive tracked pid → NEVER stale (demonstrably live),
|
|
22
|
+
// - no forward-progress timestamp yet → NEVER stale (nothing to age against).
|
|
23
|
+
|
|
24
|
+
/** Default stale window: mirror the provider-turn backstop (runner-core STALE_PROVIDER_TURN_BUSY_MS).
|
|
25
|
+
* A background task that legitimately runs this long with no intervening turn stops PINNING the
|
|
26
|
+
* agent busy — the task keeps running, but the worker's report is allowed to flush. */
|
|
27
|
+
export const STALE_BACKGROUND_SCRIPT_BUSY_MS = 10 * 60_000;
|
|
28
|
+
|
|
29
|
+
export interface StaleBackgroundScriptBusyInput {
|
|
30
|
+
/** The runner's current active-work records (kind + optional tracked pids). */
|
|
31
|
+
activeWork: Array<{ kind: string; pids?: number[] }>;
|
|
32
|
+
/** Latest forward-progress epoch-ms (the runner's lastProviderEventAt). 0/undefined ⇒ none yet. */
|
|
33
|
+
lastProgressAt: number | undefined;
|
|
34
|
+
/** Now, injectable for tests. */
|
|
35
|
+
now: number;
|
|
36
|
+
/** Liveness probe for a tracked pid. */
|
|
37
|
+
isPidAlive: (pid: number) => boolean;
|
|
38
|
+
/** Stale window; defaults to {@link STALE_BACKGROUND_SCRIPT_BUSY_MS}. */
|
|
39
|
+
staleAfterMs?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* True when the agent's ENTIRE busy state is a leftover `background-script` record that has aged
|
|
44
|
+
* past the stale window with no live tracked process and no forward progress — i.e. it should be
|
|
45
|
+
* force-cleared so the agent reconciles busy→idle without a human nudge. Pure so the decision is
|
|
46
|
+
* unit-testable without standing up an AgentRunner.
|
|
47
|
+
*/
|
|
48
|
+
export function isStaleBackgroundScriptOnlyBusy(input: StaleBackgroundScriptBusyInput): boolean {
|
|
49
|
+
const { activeWork } = input;
|
|
50
|
+
// Something must be pinning busy, and it must be background-script work exclusively — a live
|
|
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).
|
|
53
|
+
if (activeWork.length === 0) return false;
|
|
54
|
+
if (activeWork.some((work) => work.kind !== "background-script")) return false;
|
|
55
|
+
// A tracked pid still alive is positive proof the background work is genuinely running — hold.
|
|
56
|
+
// (No pids at all falls through: the hook can't supply them, so time is the only signal left.)
|
|
57
|
+
if (activeWork.some((work) => work.pids?.some((pid) => input.isPidAlive(pid)))) return false;
|
|
58
|
+
const lastProgressAt = input.lastProgressAt ?? 0;
|
|
59
|
+
if (!lastProgressAt) return false;
|
|
60
|
+
const staleAfterMs = input.staleAfterMs ?? STALE_BACKGROUND_SCRIPT_BUSY_MS;
|
|
61
|
+
return input.now - lastProgressAt >= staleAfterMs;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// #2034 (#2031 L3) — build the per-task background-script LEDGER a watchdog (#2035/#2036)
|
|
65
|
+
// reads to name a specific long-running script and its age, rather than just a count.
|
|
66
|
+
//
|
|
67
|
+
// The Claude Stop hook (`relay_background_task_list` in relay-status.sh) can only ever post
|
|
68
|
+
// the CURRENT snapshot of running tasks as {id, command} — each hook invocation is a fresh
|
|
69
|
+
// process with no memory of when a task first appeared. This function is where startedAt
|
|
70
|
+
// actually gets tracked: called on every background-script busy report with the freshly
|
|
71
|
+
// reported tasks and the PRIOR ledger (the existing work record's metadata.backgroundTasks,
|
|
72
|
+
// if any), it stamps startedAt=now for a task id seen for the first time and carries the
|
|
73
|
+
// original startedAt forward for a task id that was already in the prior ledger — so a task
|
|
74
|
+
// that keeps running across turns keeps its true start time instead of resetting every turn.
|
|
75
|
+
// A task id absent from the fresh report (completed, or killed) is simply not in the
|
|
76
|
+
// returned ledger; the caller's own work-record lifecycle (finishWork on idle) clears the
|
|
77
|
+
// record entirely once no task is left running at all.
|
|
78
|
+
export interface BackgroundTask {
|
|
79
|
+
id: string;
|
|
80
|
+
command: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface BackgroundLedgerTask extends BackgroundTask {
|
|
84
|
+
startedAt: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isBackgroundTask(value: unknown): value is BackgroundTask {
|
|
88
|
+
return typeof value === "object" && value !== null
|
|
89
|
+
&& typeof (value as Record<string, unknown>).id === "string"
|
|
90
|
+
&& typeof (value as Record<string, unknown>).command === "string";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function buildBackgroundTaskLedger(freshTasks: unknown, priorLedger: unknown, now: number): BackgroundLedgerTask[] {
|
|
94
|
+
const priorStartedAt = new Map<string, number>();
|
|
95
|
+
if (Array.isArray(priorLedger)) {
|
|
96
|
+
for (const entry of priorLedger) {
|
|
97
|
+
if (isBackgroundTask(entry) && typeof (entry as { startedAt?: unknown }).startedAt === "number") {
|
|
98
|
+
priorStartedAt.set(entry.id, (entry as BackgroundLedgerTask).startedAt);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (!Array.isArray(freshTasks)) return [];
|
|
103
|
+
return freshTasks
|
|
104
|
+
.filter(isBackgroundTask)
|
|
105
|
+
.map((task) => ({ id: task.id, command: task.command, startedAt: priorStartedAt.get(task.id) ?? now }));
|
|
106
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { appendFileSync, renameSync, rmSync, statSync } from "node:fs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
|
|
4
|
+
export const RUNNER_LOG_MAX_BYTES = 5 * 1024 * 1024;
|
|
5
|
+
export const RUNNER_LOG_ARCHIVES = 2;
|
|
6
|
+
export const RUNNER_LOG_RECORD_MAX_BYTES = 64 * 1024;
|
|
7
|
+
const TRUNCATED = " …[truncated]\n";
|
|
8
|
+
|
|
9
|
+
/** Bound a record before encoding it, including pathological multi-megabyte diagnostics. */
|
|
10
|
+
export function boundedLogRecord(line: string, maxBytes: number): Buffer {
|
|
11
|
+
const bytes = Buffer.from(line.slice(0, maxBytes), "utf8");
|
|
12
|
+
if (bytes.length <= maxBytes && line.length <= maxBytes) return bytes;
|
|
13
|
+
const suffix = Buffer.from(TRUNCATED);
|
|
14
|
+
let end = maxBytes - suffix.length;
|
|
15
|
+
while (end > 0 && (bytes[end]! & 0xc0) === 0x80) end--;
|
|
16
|
+
return Buffer.concat([bytes.subarray(0, end), suffix]);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function size(path: string): number {
|
|
20
|
+
try { return statSync(path).size; } catch (error) {
|
|
21
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return 0;
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Keep existing oversized logs intact for operator inspection; only bounded generations
|
|
27
|
+
// participate in archive pruning (#2668).
|
|
28
|
+
function preserveLegacyFile(path: string, fileSize: number, maxBytes: number): void {
|
|
29
|
+
if (fileSize <= maxBytes + Math.min(maxBytes, RUNNER_LOG_RECORD_MAX_BYTES)) return;
|
|
30
|
+
try { renameSync(path, `${path}.legacy-${randomUUID()}`); } catch (error) {
|
|
31
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The runner's single diagnostics sink: active file + two archives, each <= maxBytes. */
|
|
36
|
+
export function appendBoundedLog(path: string, line: string, maxBytes = RUNNER_LOG_MAX_BYTES, firstWrite = true): void {
|
|
37
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 128 || maxBytes > RUNNER_LOG_MAX_BYTES) {
|
|
38
|
+
throw new Error("invalid runner log size limit");
|
|
39
|
+
}
|
|
40
|
+
const bytes = boundedLogRecord(line, Math.min(maxBytes, RUNNER_LOG_RECORD_MAX_BYTES));
|
|
41
|
+
const activeSize = size(path);
|
|
42
|
+
if (firstWrite || activeSize + bytes.length > maxBytes) {
|
|
43
|
+
preserveLegacyFile(path, activeSize, maxBytes);
|
|
44
|
+
for (let i = 1; i <= RUNNER_LOG_ARCHIVES; i++) {
|
|
45
|
+
const archive = `${path}.${i}`;
|
|
46
|
+
preserveLegacyFile(archive, size(archive), maxBytes);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (activeSize + bytes.length > maxBytes) {
|
|
50
|
+
rmSync(`${path}.${RUNNER_LOG_ARCHIVES}`, { force: true });
|
|
51
|
+
for (let i = RUNNER_LOG_ARCHIVES - 1; i >= 0; i--) {
|
|
52
|
+
const from = i === 0 ? path : `${path}.${i}`;
|
|
53
|
+
try { renameSync(from, `${path}.${i + 1}`); } catch (error) {
|
|
54
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
appendFileSync(path, bytes, { mode: 0o600 });
|
|
59
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Synchronous subprocesses run on the runner's event-loop thread. A hung helper
|
|
2
|
+
// must therefore be deadline-killed rather than pinning the whole runner forever.
|
|
3
|
+
// Callers deliberately receive Bun's result unchanged: several commands use a
|
|
4
|
+
// non-zero exit as an ordinary answer (for example, tmux has-session when absent).
|
|
5
|
+
export const SYNC_SUBPROCESS_TIMEOUT_MS = 10_000;
|
|
6
|
+
|
|
7
|
+
export function spawnSyncBounded(
|
|
8
|
+
cmd: string[],
|
|
9
|
+
options = {},
|
|
10
|
+
timeoutMs = SYNC_SUBPROCESS_TIMEOUT_MS,
|
|
11
|
+
) {
|
|
12
|
+
return Bun.spawnSync({
|
|
13
|
+
cmd,
|
|
14
|
+
...options,
|
|
15
|
+
timeout: timeoutMs,
|
|
16
|
+
killSignal: "SIGKILL",
|
|
17
|
+
});
|
|
18
|
+
}
|