@humwick/runner 0.0.0-stage → 0.130.1
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 +42 -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,43 @@
|
|
|
1
|
+
// #2539 Phase A — the shared soft-steer foundation.
|
|
2
|
+
//
|
|
3
|
+
// One adapter-owned eligibility decision (`partitionMidTurnDelivery`), one config seam
|
|
4
|
+
// (`resolveActiveTurnMessageDelivery`) and one acceptance-correlation id (`midTurnClientUserId`),
|
|
5
|
+
// shared by every adapter that owns an additive mid-turn steer channel (codex `turn/steer` today;
|
|
6
|
+
// opencode/zai build on this in Phase B). Kept in a neutral module so adapters never couple to
|
|
7
|
+
// each other and runner-core never couples to a specific provider.
|
|
8
|
+
import { isRecord, type Message } from "@humwick/sdk";
|
|
9
|
+
import { taskIdFromMessage } from "./runner-helpers";
|
|
10
|
+
|
|
11
|
+
export { resolveActiveTurnMessageDelivery } from "./delivery-policy";
|
|
12
|
+
|
|
13
|
+
/** Keep synthetic context inputs and claimed work on their own turn boundary. */
|
|
14
|
+
export function partitionMidTurnDelivery(messages: Message[]): { steerable: Message[]; boundaryOnly: Message[] } {
|
|
15
|
+
const steerable: Message[] = [];
|
|
16
|
+
const boundaryOnly: Message[] = [];
|
|
17
|
+
for (const message of messages) {
|
|
18
|
+
if (isSteerableMidTurnMessage(message)) steerable.push(message);
|
|
19
|
+
else boundaryOnly.push(message);
|
|
20
|
+
}
|
|
21
|
+
return { steerable, boundaryOnly };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isSteerableMidTurnMessage(message: Message): boolean {
|
|
25
|
+
// Synthetic boundary payloads: memory / self-resume / context injections carry id 0.
|
|
26
|
+
if (message.id === 0) return false;
|
|
27
|
+
const payload = isRecord(message.payload) ? message.payload : undefined;
|
|
28
|
+
if (payload?.memoryInjection === true || payload?.resumeInjection === true) return false;
|
|
29
|
+
// Claimable messages / task assignments keep their claim window semantics.
|
|
30
|
+
if (message.claimable === true || taskIdFromMessage(message) !== undefined) return false;
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Stable acceptance-correlation id for one steered composition (#2539 design C). Derived from
|
|
36
|
+
* the ordered batch ids, so a re-offer of the SAME subset reuses the id: the app-server echoes
|
|
37
|
+
* it back as the `userMessage.clientId` item field, and an echo observed after a lost steer
|
|
38
|
+
* response lets the next offer settle by correlation instead of re-steering (which would
|
|
39
|
+
* duplicate). Correlation, NOT idempotent dedupe — the upstream may accept the same id twice.
|
|
40
|
+
*/
|
|
41
|
+
export function midTurnClientUserId(messages: Message[]): string {
|
|
42
|
+
return `relay-steer-${messages.map((message) => message.id).join(",")}`;
|
|
43
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { writeChildEnv } from "@humwick/sdk/env";
|
|
2
|
+
// #1565 P5.7 (design §10.3) — the provider-NATIVE-memory lever.
|
|
3
|
+
//
|
|
4
|
+
// WHAT THIS IS FOR. Every agent today can carry two competing memory stores: relay memory (which
|
|
5
|
+
// survives a respawn) and the provider's own (which, for 8 of 11 built-in profiles, is destroyed
|
|
6
|
+
// at every spawn because the managed provider home is instanceId-keyed). This module renders one
|
|
7
|
+
// profile field into the concrete per-provider switches that make relay memory THE store.
|
|
8
|
+
//
|
|
9
|
+
// TWO INVARIANTS, both load-bearing:
|
|
10
|
+
//
|
|
11
|
+
// 1. ABSENT ⇒ OFF for Relay-spawned Claude. A profile without `nativeMemory` explicitly emits
|
|
12
|
+
// `CLAUDE_CODE_DISABLE_AUTO_MEMORY=1` and `autoMemoryEnabled:false`, so provider auto-memory
|
|
13
|
+
// cannot inject its own instructions into Relay-managed Claude sessions. Other providers keep
|
|
14
|
+
// their existing absent-policy behavior until they have an explicit global default.
|
|
15
|
+
//
|
|
16
|
+
// 2. A DECLARED policy renders EXPLICITLY IN BOTH DIRECTIONS — never as "omit the flag"
|
|
17
|
+
// (§10.3.5). `policy:"on"` emits `CLAUDE_CODE_DISABLE_AUTO_MEMORY=0` / `-c
|
|
18
|
+
// features.memories=true`, not an absence. An absence cannot override an `autoMemoryEnabled:
|
|
19
|
+
// false` or `features.memories = false` written into a reused managed home on an earlier
|
|
20
|
+
// launch, so a K3 kill-switch pull would be silently defeated. An explicit value cannot be.
|
|
21
|
+
//
|
|
22
|
+
// REACH, per §10.3.1's table — and the reason the env/argv lever is the one that must always be
|
|
23
|
+
// present: relay writes a managed `settings.json`/`config.toml` only into an ISOLATED home, and
|
|
24
|
+
// must never clobber a host-base agent's real `~/.claude`/`~/.codex`. Claude's env var sits ABOVE
|
|
25
|
+
// the settings read in the CLI's own predicate and Codex's `-c` sits above `config.toml`, so the
|
|
26
|
+
// portable lever is also the authoritative one; the file channels are a belt, not the trousers.
|
|
27
|
+
//
|
|
28
|
+
// NEVER THROWS. Resolution is total: any unexpected input degrades to "nothing applied, reported
|
|
29
|
+
// as suppressed" rather than propagating. A failure to apply this lever must not fail a spawn
|
|
30
|
+
// (the #1612 discipline, applied to the switch instead of the preamble) — an agent that launches
|
|
31
|
+
// with its native memory still on is a two-store agent, which is exactly today's state; an agent
|
|
32
|
+
// that fails to launch is an outage.
|
|
33
|
+
|
|
34
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
35
|
+
import { join } from "node:path";
|
|
36
|
+
import type { AgentProfileNativeMemoryPolicy, SpawnProvider } from "@humwick/sdk";
|
|
37
|
+
import { sanitizeFsName } from "@humwick/sdk/fs-name";
|
|
38
|
+
import type { RunnerSpawnConfig } from "./adapter";
|
|
39
|
+
import { providerHomeRootFromEnv } from "./config";
|
|
40
|
+
import { hostProviderDefaultArgs } from "./host-asset-plan";
|
|
41
|
+
import { upsertTomlTable } from "./relay-mcp";
|
|
42
|
+
|
|
43
|
+
export const CLAUDE_DISABLE_AUTO_MEMORY_ENV = "CLAUDE_CODE_DISABLE_AUTO_MEMORY";
|
|
44
|
+
export const CLAUDE_AUTO_MEMORY_ENABLED_SETTING = "autoMemoryEnabled";
|
|
45
|
+
export const CLAUDE_AUTO_MEMORY_DIRECTORY_SETTING = "autoMemoryDirectory";
|
|
46
|
+
export const CODEX_MEMORIES_FEATURE_KEY = "features.memories";
|
|
47
|
+
|
|
48
|
+
/** `"default"` = no effective policy applies, so no lever is emitted. */
|
|
49
|
+
export type ResolvedNativeMemoryPolicy = AgentProfileNativeMemoryPolicy | "default";
|
|
50
|
+
|
|
51
|
+
export interface NativeMemoryLever {
|
|
52
|
+
policy: ResolvedNativeMemoryPolicy;
|
|
53
|
+
/** Env additions for the launch. Empty when no effective policy applies. */
|
|
54
|
+
env: Record<string, string>;
|
|
55
|
+
/** Argv additions for the launch. Empty for `"default"`. */
|
|
56
|
+
args: string[];
|
|
57
|
+
/** Provider-settings keys relay folds into its OWN settings channel (Claude `--settings` and,
|
|
58
|
+
* for an isolated home, the managed `settings.json`). Empty when no effective policy applies
|
|
59
|
+
* and for Codex. */
|
|
60
|
+
settings: Record<string, unknown>;
|
|
61
|
+
/** Codex only: `config.toml` keys for an ISOLATED `CODEX_HOME`. Empty otherwise. */
|
|
62
|
+
configToml: Record<string, string>;
|
|
63
|
+
/** The levers that actually reach this launch, as short stable ids for the projection report. */
|
|
64
|
+
applied: string[];
|
|
65
|
+
/** Levers that were resolved but could NOT be delivered on this base/provider. Reported LOUDLY
|
|
66
|
+
* (the `hooksSuppressed` precedent, #1096) so a lever is never silently absent while the
|
|
67
|
+
* report claims it applied. */
|
|
68
|
+
suppressed?: { reason: string; levers: string[] };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const NONE: NativeMemoryLever = { policy: "default", env: {}, args: [], settings: {}, configToml: {}, applied: [] };
|
|
72
|
+
|
|
73
|
+
function none(): NativeMemoryLever {
|
|
74
|
+
return { ...NONE, env: {}, args: [], settings: {}, configToml: {}, applied: [] };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A STABLE, relay-owned, per-profile directory for `policy:"redirect"` (§10.3.4/F3). Deliberately
|
|
79
|
+
* NOT under the instanceId-keyed provider home: the entire point of the redirect is that a
|
|
80
|
+
* host-base profile's native store stops being destroyed every spawn and becomes durable and
|
|
81
|
+
* harvestable. Sibling to the per-instance homes, so one profile's native memory is one directory.
|
|
82
|
+
*/
|
|
83
|
+
export function defaultNativeMemoryDirectory(provider: SpawnProvider, config: RunnerSpawnConfig): string {
|
|
84
|
+
const profileName = sanitizeFsName(config.agentProfile?.name || config.profile || "profile", {
|
|
85
|
+
replacement: "-",
|
|
86
|
+
collapse: false,
|
|
87
|
+
maxLen: 120,
|
|
88
|
+
fallback: "profile",
|
|
89
|
+
});
|
|
90
|
+
return join(providerHomeRootFromEnv(), provider, profileName, "native-memory");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface ClaudeDeliveryChannels {
|
|
94
|
+
/** An isolated managed home exists, so relay owns a `settings.json` it may write. */
|
|
95
|
+
managedSettings: boolean;
|
|
96
|
+
/** Relay's inline `--settings` reaches this launch (false when a caller `--settings` is a file
|
|
97
|
+
* path or malformed JSON and therefore unmergeable — the same gate `claudeSettingsArgs` uses). */
|
|
98
|
+
inlineSettings: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resolveClaudeLever(
|
|
102
|
+
policy: AgentProfileNativeMemoryPolicy,
|
|
103
|
+
directory: string | undefined,
|
|
104
|
+
channels: ClaudeDeliveryChannels,
|
|
105
|
+
resolveDefaultDirectory: () => string,
|
|
106
|
+
): NativeMemoryLever {
|
|
107
|
+
// The env var is unconditional and authoritative — it works on EVERY base, host included, and
|
|
108
|
+
// outranks the settings read inside the CLI's own predicate.
|
|
109
|
+
const disable = policy === "off";
|
|
110
|
+
const env = writeChildEnv({}, { [CLAUDE_DISABLE_AUTO_MEMORY_ENV]: disable ? "1" : "0" });
|
|
111
|
+
const applied = [`env:${CLAUDE_DISABLE_AUTO_MEMORY_ENV}=${disable ? "1" : "0"}`];
|
|
112
|
+
|
|
113
|
+
const settings: Record<string, unknown> = { [CLAUDE_AUTO_MEMORY_ENABLED_SETTING]: !disable };
|
|
114
|
+
if (policy === "redirect") settings[CLAUDE_AUTO_MEMORY_DIRECTORY_SETTING] = directory ?? resolveDefaultDirectory();
|
|
115
|
+
|
|
116
|
+
const settingsLevers = Object.keys(settings).map((key) => `settings:${key}`);
|
|
117
|
+
const settingsReach = channels.managedSettings || channels.inlineSettings;
|
|
118
|
+
if (settingsReach) {
|
|
119
|
+
applied.push(...settingsLevers);
|
|
120
|
+
return { policy, env, args: [], settings, configToml: {}, applied };
|
|
121
|
+
}
|
|
122
|
+
// No settings channel: the env var still carries the on/off decision (and outranks settings
|
|
123
|
+
// anyway), so "off"/"on" lose nothing. A REDIRECT does lose something real — native memory
|
|
124
|
+
// stays on but writes wherever the provider defaults to — so say so instead of implying the
|
|
125
|
+
// redirect happened.
|
|
126
|
+
return {
|
|
127
|
+
policy,
|
|
128
|
+
env,
|
|
129
|
+
args: [],
|
|
130
|
+
settings: {},
|
|
131
|
+
configToml: {},
|
|
132
|
+
applied,
|
|
133
|
+
suppressed: {
|
|
134
|
+
reason: policy === "redirect"
|
|
135
|
+
? `relay has no settings channel for this launch (no isolated managed home, and the caller's --settings could not be merged), so ${CLAUDE_AUTO_MEMORY_DIRECTORY_SETTING} was NOT applied: native memory stays ON at the provider's default location`
|
|
136
|
+
: `relay has no settings channel for this launch (no isolated managed home, and the caller's --settings could not be merged), so the ${CLAUDE_AUTO_MEMORY_ENABLED_SETTING} belt was not applied; the ${CLAUDE_DISABLE_AUTO_MEMORY_ENV} env lever still applies and outranks settings`,
|
|
137
|
+
levers: settingsLevers,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function resolveCodexLever(policy: AgentProfileNativeMemoryPolicy, isolatedHome: boolean): NativeMemoryLever {
|
|
143
|
+
// §10.3.4: Codex memories live in a fixed `CODEX_HOME` sqlite with no redirect knob, so
|
|
144
|
+
// `redirect` degrades to an EXPLICIT "on" — never to "off". Losing a redirect must not be a
|
|
145
|
+
// path that silently deletes a store.
|
|
146
|
+
const enable = policy !== "off";
|
|
147
|
+
const value = enable ? "true" : "false";
|
|
148
|
+
const args = ["-c", `${CODEX_MEMORIES_FEATURE_KEY}=${value}`];
|
|
149
|
+
const applied = [`arg:${CODEX_MEMORIES_FEATURE_KEY}=${value}`];
|
|
150
|
+
const configToml: Record<string, string> = isolatedHome ? { [CODEX_MEMORIES_FEATURE_KEY]: value } : {};
|
|
151
|
+
if (isolatedHome) applied.push(`config.toml:${CODEX_MEMORIES_FEATURE_KEY}=${value}`);
|
|
152
|
+
|
|
153
|
+
const suppressedLevers: string[] = [];
|
|
154
|
+
const reasons: string[] = [];
|
|
155
|
+
if (policy === "redirect") {
|
|
156
|
+
suppressedLevers.push("redirect");
|
|
157
|
+
reasons.push("Codex has no autoMemoryDirectory equivalent (memories live in a fixed CODEX_HOME sqlite), so the redirect could not be applied; native memory was left EXPLICITLY ON instead");
|
|
158
|
+
}
|
|
159
|
+
if (!isolatedHome) {
|
|
160
|
+
suppressedLevers.push(`config.toml:${CODEX_MEMORIES_FEATURE_KEY}`);
|
|
161
|
+
reasons.push(`this host-base launch reuses the real ~/.codex, which relay must not write, so the config.toml belt was not applied; the ${CODEX_MEMORIES_FEATURE_KEY} argv lever still applies and outranks config.toml`);
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
policy,
|
|
165
|
+
env: {},
|
|
166
|
+
args,
|
|
167
|
+
settings: {},
|
|
168
|
+
configToml,
|
|
169
|
+
applied,
|
|
170
|
+
...(suppressedLevers.length ? { suppressed: { reason: reasons.join("; "), levers: suppressedLevers } } : {}),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Per-provider lever renderers, dispatched by NAME rather than by an identity branch — the same
|
|
175
|
+
// registry shape `launch-assembly`'s ASSEMBLERS/MATERIALIZERS use, so adding a provider is a table
|
|
176
|
+
// entry (and an unregistered provider degrades to "no lever, reported", never to a throw).
|
|
177
|
+
type NativeMemoryLeverRenderer = (
|
|
178
|
+
policy: AgentProfileNativeMemoryPolicy,
|
|
179
|
+
input: ResolveNativeMemoryLeverInput,
|
|
180
|
+
) => NativeMemoryLever;
|
|
181
|
+
|
|
182
|
+
// #2513 — the claude renderer is shared by every claude-family provider id (claude-b reuses
|
|
183
|
+
// the claude adapter/launch path whole), so the absent⇒off invariant holds for it too.
|
|
184
|
+
const claudeLeverRenderer: NativeMemoryLeverRenderer = (policy, input) => resolveClaudeLever(
|
|
185
|
+
policy,
|
|
186
|
+
input.config.agentProfile?.nativeMemory?.directory,
|
|
187
|
+
{ managedSettings: Boolean(input.channels?.managedSettings), inlineSettings: Boolean(input.channels?.inlineSettings) },
|
|
188
|
+
() => defaultNativeMemoryDirectory(input.provider, input.config),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const LEVER_RENDERERS: Record<string, NativeMemoryLeverRenderer> = {
|
|
192
|
+
claude: claudeLeverRenderer,
|
|
193
|
+
"claude-b": claudeLeverRenderer,
|
|
194
|
+
"claude-api": claudeLeverRenderer,
|
|
195
|
+
codex: (policy, input) => resolveCodexLever(policy, Boolean(input.isolatedHome)),
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Global defaults belong beside the renderer registry so adding a provider does not create an
|
|
199
|
+
// identity branch in the resolver. Profile declarations still take precedence over this table.
|
|
200
|
+
const PROVIDER_DEFAULT_POLICIES: Partial<Record<SpawnProvider, AgentProfileNativeMemoryPolicy>> = {
|
|
201
|
+
claude: "off",
|
|
202
|
+
"claude-b": "off",
|
|
203
|
+
"claude-api": "off",
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
export interface ResolveNativeMemoryLeverInput {
|
|
207
|
+
provider: SpawnProvider;
|
|
208
|
+
config: RunnerSpawnConfig;
|
|
209
|
+
/** Claude only — see {@link ClaudeDeliveryChannels}. Ignored for Codex. */
|
|
210
|
+
channels?: Partial<ClaudeDeliveryChannels>;
|
|
211
|
+
/** Codex only — true when this launch has an isolated `CODEX_HOME` relay may write. */
|
|
212
|
+
isolatedHome?: boolean;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Render the profile's `nativeMemory` field into concrete launch levers. Total: absent profiles
|
|
217
|
+
* resolve to the runner-level Claude default (`"off"`); providers without such a default return
|
|
218
|
+
* the empty application (policy `"default"`). Any unexpected failure becomes a reported
|
|
219
|
+
* suppression rather than a throw.
|
|
220
|
+
*/
|
|
221
|
+
export function resolveNativeMemoryLever(input: ResolveNativeMemoryLeverInput): NativeMemoryLever {
|
|
222
|
+
try {
|
|
223
|
+
const declared = input.config.agentProfile?.nativeMemory;
|
|
224
|
+
const declaredPolicy = declared?.policy ?? (declared?.directory ? "redirect" : undefined);
|
|
225
|
+
const policy = declaredPolicy ?? PROVIDER_DEFAULT_POLICIES[input.provider];
|
|
226
|
+
if (!policy) return none();
|
|
227
|
+
if (policy !== "on" && policy !== "off" && policy !== "redirect") {
|
|
228
|
+
return {
|
|
229
|
+
...none(),
|
|
230
|
+
suppressed: { reason: `unrecognized nativeMemory.policy ${JSON.stringify(policy)}; no lever was applied and the provider default (native memory ON) stands`, levers: ["nativeMemory"] },
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const render = LEVER_RENDERERS[input.provider];
|
|
234
|
+
if (render) return render(policy, input);
|
|
235
|
+
return {
|
|
236
|
+
...none(),
|
|
237
|
+
policy,
|
|
238
|
+
suppressed: { reason: `provider ${input.provider} has no known native-memory lever; nothing was applied`, levers: ["nativeMemory"] },
|
|
239
|
+
};
|
|
240
|
+
} catch (err) {
|
|
241
|
+
// The fail-safe (#1612's discipline applied to the switch). A lever we could not compute must
|
|
242
|
+
// degrade to "not applied, loudly reported" — never to a failed spawn.
|
|
243
|
+
return {
|
|
244
|
+
...none(),
|
|
245
|
+
suppressed: { reason: `native-memory lever could not be resolved: ${(err as Error)?.message ?? String(err)}; no lever was applied and the provider default (native memory ON) stands`, levers: ["nativeMemory"] },
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── Launch-seam adapters ───────────────────────────────────────────────────────
|
|
251
|
+
//
|
|
252
|
+
// The two entry points `launch-assembly` calls, kept here so the lever's whole surface — what it
|
|
253
|
+
// emits, where each half is delivered, and what happens when a delivery fails — reads as one file.
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Resolve the Claude lever with its REAL delivery channels. The `inlineSettings` reach uses the
|
|
257
|
+
* same caller-`--settings` mergeability gate `claudeSettingsArgs` applies, so the lever's
|
|
258
|
+
* applied/suppressed report cannot claim a settings key the merge then drops. Both `assembleClaude`
|
|
259
|
+
* (for the launch) and `materializeClaudeAssets` (for the managed settings.json) call this, so the
|
|
260
|
+
* launch and the disk agree — the `claudeManagedHookSettings` pattern.
|
|
261
|
+
*/
|
|
262
|
+
export function resolveClaudeNativeMemoryLever(
|
|
263
|
+
config: RunnerSpawnConfig,
|
|
264
|
+
configHome: string | undefined,
|
|
265
|
+
hostDefaultArgs: string[],
|
|
266
|
+
callerSettingsMergeable: (argLists: string[][]) => boolean,
|
|
267
|
+
provider: SpawnProvider = "claude",
|
|
268
|
+
): NativeMemoryLever {
|
|
269
|
+
return resolveNativeMemoryLever({
|
|
270
|
+
provider,
|
|
271
|
+
config,
|
|
272
|
+
channels: {
|
|
273
|
+
managedSettings: Boolean(configHome),
|
|
274
|
+
inlineSettings: callerSettingsMergeable([hostDefaultArgs, config.providerArgs]),
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* The native-memory keys `materializeClaudeAssets` writes into an ISOLATED managed home's
|
|
281
|
+
* settings.json. Returns `{}` for host base (which reuses the real `~/.claude` — relay must not
|
|
282
|
+
* write it); that base is covered by the env lever plus the inline `--settings`.
|
|
283
|
+
*/
|
|
284
|
+
export function claudeManagedNativeMemorySettings(
|
|
285
|
+
config: RunnerSpawnConfig,
|
|
286
|
+
configHome: string | undefined,
|
|
287
|
+
callerSettingsMergeable: (argLists: string[][]) => boolean,
|
|
288
|
+
provider: SpawnProvider = "claude",
|
|
289
|
+
): Record<string, unknown> {
|
|
290
|
+
if (!configHome) return {};
|
|
291
|
+
const hostDefaultArgs = hostProviderDefaultArgs(config);
|
|
292
|
+
return resolveClaudeNativeMemoryLever(config, configHome, hostDefaultArgs, callerSettingsMergeable, provider).settings;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* The fail-safe at the APPLY seam (#1612's discipline, moved from the preamble to the switch). The
|
|
297
|
+
* file-channel belt is best-effort by construction: the authoritative lever is the env var / `-c`
|
|
298
|
+
* arg, computed into the launch and touching no disk, so a failed belt write costs nothing an
|
|
299
|
+
* operator can observe — while a THROW here would fail the whole spawn. So: never propagate, and
|
|
300
|
+
* never swallow either. Returns the reason on failure (also logged), `undefined` on success.
|
|
301
|
+
*/
|
|
302
|
+
export function applyNativeMemoryBelt(label: string, apply: () => void): string | undefined {
|
|
303
|
+
try {
|
|
304
|
+
apply();
|
|
305
|
+
return undefined;
|
|
306
|
+
} catch (err) {
|
|
307
|
+
const reason = `native-memory lever: ${label} could not be written: ${(err as Error)?.message ?? String(err)}. `
|
|
308
|
+
+ "The authoritative env/argv lever still applies (it outranks the file channel), and the spawn continues — design #1565 §10.3.";
|
|
309
|
+
console.warn(reason);
|
|
310
|
+
return reason;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Idempotently upsert the `[features]` table into an ISOLATED `CODEX_HOME/config.toml`. Values are
|
|
316
|
+
* already rendered TOML literals (`"true"`/`"false"`) and are emitted BARE, not quoted:
|
|
317
|
+
* `features.memories` is a boolean, and a quoted string would not parse as one.
|
|
318
|
+
*/
|
|
319
|
+
export function writeCodexFeaturesToml(codexHome: string, features: Record<string, string>): void {
|
|
320
|
+
const bodyLines = Object.entries(features)
|
|
321
|
+
.map(([key, value]) => [key.startsWith("features.") ? key.slice("features.".length) : key, value] as const)
|
|
322
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
323
|
+
.map(([key, value]) => `${key} = ${value}`);
|
|
324
|
+
if (!bodyLines.length) return;
|
|
325
|
+
mkdirSync(codexHome, { recursive: true });
|
|
326
|
+
const configPath = join(codexHome, "config.toml");
|
|
327
|
+
const existing = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
|
|
328
|
+
writeFileSync(configPath, upsertTomlTable(existing, "features", bodyLines), { mode: 0o600 });
|
|
329
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { compactExecutionBudgetMs, errMessage, isRecord } from "@humwick/sdk";
|
|
2
|
+
import type { ProviderStatusEvent, SemanticStatus } from "./adapter";
|
|
3
|
+
|
|
4
|
+
interface RunnerTimelineEvent {
|
|
5
|
+
status: string;
|
|
6
|
+
id?: string;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
title?: string;
|
|
9
|
+
body?: string;
|
|
10
|
+
icon?: string;
|
|
11
|
+
metadata?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface NativeSelfResumeHandoff {
|
|
15
|
+
commandId: string;
|
|
16
|
+
generation?: number;
|
|
17
|
+
compactedAt?: number;
|
|
18
|
+
submittedAt?: number;
|
|
19
|
+
abort: AbortController;
|
|
20
|
+
timer: ReturnType<typeof setTimeout>;
|
|
21
|
+
resolve: (result: Record<string, unknown>) => void;
|
|
22
|
+
reject: (error: Error) => void;
|
|
23
|
+
promise: Promise<Record<string, unknown>>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isNativeSelfResumeCompact(type: string, params: Record<string, unknown>): boolean {
|
|
27
|
+
if (type !== "agent.compact") return false;
|
|
28
|
+
const selfResume = params.selfResume;
|
|
29
|
+
return Boolean(selfResume && typeof selfResume === "object" && !Array.isArray(selfResume) && (selfResume as Record<string, unknown>).mode === "native");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class NativeSelfResumeTracker {
|
|
33
|
+
private current?: NativeSelfResumeHandoff;
|
|
34
|
+
|
|
35
|
+
get active(): NativeSelfResumeHandoff | undefined {
|
|
36
|
+
return this.current;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
begin(commandId: string, params: Record<string, unknown>): NativeSelfResumeHandoff {
|
|
40
|
+
if (this.current) this.reject(this.current, new Error("native compact superseded"));
|
|
41
|
+
let resolve!: (result: Record<string, unknown>) => void;
|
|
42
|
+
let reject!: (error: Error) => void;
|
|
43
|
+
const promise = new Promise<Record<string, unknown>>((res, rej) => {
|
|
44
|
+
resolve = res;
|
|
45
|
+
reject = rej;
|
|
46
|
+
});
|
|
47
|
+
const selfResume = params.selfResume && typeof params.selfResume === "object" && !Array.isArray(params.selfResume)
|
|
48
|
+
? params.selfResume as Record<string, unknown>
|
|
49
|
+
: undefined;
|
|
50
|
+
const generation = typeof selfResume?.generation === "number" ? selfResume.generation : undefined;
|
|
51
|
+
void promise.catch(() => {});
|
|
52
|
+
const timeoutMs = compactExecutionBudgetMs(params);
|
|
53
|
+
const abort = new AbortController();
|
|
54
|
+
const handoff: NativeSelfResumeHandoff = {
|
|
55
|
+
commandId,
|
|
56
|
+
abort,
|
|
57
|
+
generation,
|
|
58
|
+
promise,
|
|
59
|
+
resolve,
|
|
60
|
+
reject,
|
|
61
|
+
timer: setTimeout(() => {
|
|
62
|
+
if (this.current !== handoff) return;
|
|
63
|
+
this.reject(handoff, new Error(`native self-resume timed out after ${Math.round(timeoutMs / 1000)}s`));
|
|
64
|
+
}, timeoutMs),
|
|
65
|
+
};
|
|
66
|
+
this.current = handoff;
|
|
67
|
+
return handoff;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
finish(handoff?: NativeSelfResumeHandoff): void {
|
|
71
|
+
if (!handoff) return;
|
|
72
|
+
clearTimeout(handoff.timer);
|
|
73
|
+
if (this.current === handoff) this.current = undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
resolve(handoff: NativeSelfResumeHandoff, result: Record<string, unknown>): void {
|
|
77
|
+
this.finish(handoff);
|
|
78
|
+
handoff.resolve(result);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
reject(handoff: NativeSelfResumeHandoff, error: Error): void {
|
|
82
|
+
this.finish(handoff);
|
|
83
|
+
handoff.abort.abort(error);
|
|
84
|
+
handoff.reject(error);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async run<T>(handoff: NativeSelfResumeHandoff | undefined, operation: () => Promise<T>): Promise<T> {
|
|
88
|
+
if (!handoff) return operation();
|
|
89
|
+
handoff.abort.signal.throwIfAborted();
|
|
90
|
+
const result = await Promise.race([operation(), handoff.promise.then(() => new Promise<T>(() => {}))]);
|
|
91
|
+
handoff.abort.signal.throwIfAborted();
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
checkAcknowledgement(ack: unknown, status: string): void {
|
|
96
|
+
if (!isRecord(ack) || !isRecord(ack.command)) return;
|
|
97
|
+
this.settleCommand(ack.command);
|
|
98
|
+
if ((status === "accepted" || status === "running") && ["canceled", "timed_out", "failed", "rejected", "succeeded"].includes(String(ack.command.status))) throw new Error(`command already ${ack.command.status}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
settleCommand(command: Record<string, unknown>): void {
|
|
102
|
+
if (command.id !== this.current?.commandId) return;
|
|
103
|
+
if (["canceled", "timed_out", "failed", "rejected"].includes(String(command.status))) {
|
|
104
|
+
this.reject(this.current!, new Error(`compact command ${command.status}: ${command.error ?? "server settlement"}`));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async reconcile(fetchCommand: (id: string) => Promise<Response>): Promise<void> {
|
|
109
|
+
const handoff = this.current;
|
|
110
|
+
if (!handoff) return;
|
|
111
|
+
const response = await fetchCommand(handoff.commandId);
|
|
112
|
+
if (!response.ok) throw new Error(`compact settlement lookup failed: ${response.status}`);
|
|
113
|
+
const command: unknown = await response.json();
|
|
114
|
+
if (isRecord(command)) this.settleCommand(command);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
notePrompt(prompt: string, timestamp = Date.now()): void {
|
|
118
|
+
const active = this.current;
|
|
119
|
+
if (!active || active.submittedAt === undefined || timestamp < active.submittedAt) return;
|
|
120
|
+
if (/^(?:<pasted_content[^>]*>\s*)?\/compact(?:\s|$)/.test(prompt.trim())) {
|
|
121
|
+
this.reject(active, new Error("compact was submitted as a user prompt; boundary did not apply"));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
noteCompacted(timestamp: number = Date.now()): void {
|
|
126
|
+
if (!this.current || this.current.submittedAt === undefined || timestamp < this.current.submittedAt) return;
|
|
127
|
+
this.current.compactedAt = timestamp;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface NativeSelfResumeRecoveryOptions {
|
|
132
|
+
handoff: NativeSelfResumeHandoff;
|
|
133
|
+
tracker: NativeSelfResumeTracker;
|
|
134
|
+
providerSessionId: string;
|
|
135
|
+
status: SemanticStatus;
|
|
136
|
+
diagnostics: Record<string, unknown>;
|
|
137
|
+
now: number;
|
|
138
|
+
publishTimeline(event: RunnerTimelineEvent): void;
|
|
139
|
+
setTerminalProviderExit(marker: Record<string, unknown>): void;
|
|
140
|
+
setProviderStatus(update: ProviderStatusEvent): void;
|
|
141
|
+
restartProvider(resumeId?: string): Promise<void>;
|
|
142
|
+
publishStatus(): void;
|
|
143
|
+
scheduleDrain(): void;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function recoverNativeSelfResumeExit(opts: NativeSelfResumeRecoveryOptions): Promise<void> {
|
|
147
|
+
const resumeId = typeof opts.diagnostics.claudeResumeId === "string" && opts.diagnostics.claudeResumeId.length > 0
|
|
148
|
+
? opts.diagnostics.claudeResumeId
|
|
149
|
+
: undefined;
|
|
150
|
+
if (opts.handoff.compactedAt && resumeId) {
|
|
151
|
+
await recoverWithClaudeResume(opts, resumeId);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
await recoverWithFreshLaunch(opts, resumeId);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function recoverWithClaudeResume(opts: NativeSelfResumeRecoveryOptions, resumeId: string): Promise<void> {
|
|
158
|
+
opts.publishTimeline({
|
|
159
|
+
status: "provider.restart_decision",
|
|
160
|
+
id: `provider-self-resume-relaunch-${opts.providerSessionId}-${opts.now}`,
|
|
161
|
+
timestamp: Date.now(),
|
|
162
|
+
title: "Self-resume relaunching",
|
|
163
|
+
body: `Claude exited during native self-resume; runner is relaunching with claude --resume ${resumeId}.`,
|
|
164
|
+
icon: "ti-refresh",
|
|
165
|
+
metadata: selfResumeMetadata(opts.handoff, opts.diagnostics, {
|
|
166
|
+
decision: "resume-native-self-resume",
|
|
167
|
+
reason: "self-resume-exit-after-compact",
|
|
168
|
+
claudeResumeId: resumeId,
|
|
169
|
+
compactedAt: opts.handoff.compactedAt,
|
|
170
|
+
}),
|
|
171
|
+
});
|
|
172
|
+
try {
|
|
173
|
+
await opts.restartProvider(resumeId);
|
|
174
|
+
opts.publishStatus();
|
|
175
|
+
opts.scheduleDrain();
|
|
176
|
+
opts.tracker.resolve(opts.handoff, { recovered: true, via: "claude-resume", claudeResumeId: resumeId });
|
|
177
|
+
} catch (error) {
|
|
178
|
+
const message = errMessage(error);
|
|
179
|
+
opts.setTerminalProviderExit(selfResumeMetadata(opts.handoff, opts.diagnostics, {
|
|
180
|
+
reason: "self-resume-relaunch-failed",
|
|
181
|
+
claudeResumeId: resumeId,
|
|
182
|
+
compactedAt: opts.handoff.compactedAt,
|
|
183
|
+
lastError: message,
|
|
184
|
+
}));
|
|
185
|
+
opts.setProviderStatus({
|
|
186
|
+
status: "offline",
|
|
187
|
+
reason: "provider-turn",
|
|
188
|
+
id: `provider-self-resume-relaunch-failed-${opts.providerSessionId}`,
|
|
189
|
+
clear: ["provider-turn", "subagent", "background-script"],
|
|
190
|
+
});
|
|
191
|
+
opts.tracker.reject(opts.handoff, new Error(`self-resume relaunch failed: ${message}`));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function recoverWithFreshLaunch(opts: NativeSelfResumeRecoveryOptions, resumeId?: string): Promise<void> {
|
|
196
|
+
opts.publishTimeline({
|
|
197
|
+
status: "provider.restart_decision",
|
|
198
|
+
id: `provider-self-resume-fresh-launch-${opts.providerSessionId}-${opts.now}`,
|
|
199
|
+
timestamp: Date.now(),
|
|
200
|
+
title: "Self-resume fresh-launching",
|
|
201
|
+
body: "Claude exited during native self-resume before a compacted marker; runner is fresh-launching so Relay can inject the persisted continuation.",
|
|
202
|
+
icon: "ti-refresh",
|
|
203
|
+
metadata: selfResumeMetadata(opts.handoff, opts.diagnostics, {
|
|
204
|
+
decision: "fresh-launch-native-self-resume",
|
|
205
|
+
reason: "self-resume-exit-before-compact",
|
|
206
|
+
...(resumeId ? { claudeResumeId: resumeId } : {}),
|
|
207
|
+
}),
|
|
208
|
+
});
|
|
209
|
+
try {
|
|
210
|
+
await opts.restartProvider();
|
|
211
|
+
opts.publishStatus();
|
|
212
|
+
opts.scheduleDrain();
|
|
213
|
+
opts.tracker.resolve(opts.handoff, {
|
|
214
|
+
recovered: true,
|
|
215
|
+
via: "fresh-launch",
|
|
216
|
+
...(resumeId ? { claudeResumeId: resumeId } : {}),
|
|
217
|
+
});
|
|
218
|
+
} catch (error) {
|
|
219
|
+
const message = errMessage(error);
|
|
220
|
+
opts.setTerminalProviderExit(selfResumeMetadata(opts.handoff, opts.diagnostics, {
|
|
221
|
+
reason: "self-resume-fresh-launch-failed",
|
|
222
|
+
...(resumeId ? { claudeResumeId: resumeId } : {}),
|
|
223
|
+
lastError: message,
|
|
224
|
+
}));
|
|
225
|
+
opts.setProviderStatus({
|
|
226
|
+
status: "offline",
|
|
227
|
+
reason: "provider-turn",
|
|
228
|
+
id: `provider-self-resume-fresh-launch-failed-${opts.providerSessionId}`,
|
|
229
|
+
clear: ["provider-turn", "subagent", "background-script"],
|
|
230
|
+
});
|
|
231
|
+
opts.tracker.reject(opts.handoff, new Error(`self-resume fresh launch failed: ${message}`));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function selfResumeMetadata(handoff: NativeSelfResumeHandoff, diagnostics: Record<string, unknown>, extra: Record<string, unknown>): Record<string, unknown> {
|
|
236
|
+
return {
|
|
237
|
+
eventType: "provider.restart_decision",
|
|
238
|
+
selfResume: true,
|
|
239
|
+
selfResumeCommandId: handoff.commandId,
|
|
240
|
+
selfResumeGeneration: handoff.generation ?? null,
|
|
241
|
+
...extra,
|
|
242
|
+
...diagnostics,
|
|
243
|
+
};
|
|
244
|
+
}
|