@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
package/src/adapters/claude.ts
CHANGED
|
@@ -43,7 +43,7 @@ type MonitorlessTurnWatchDeps = {
|
|
|
43
43
|
};
|
|
44
44
|
|
|
45
45
|
export class ClaudeAdapter implements ProviderAdapter {
|
|
46
|
-
//
|
|
46
|
+
// provider-id-dynamic (the id-parameterized OpenCodeAdapter pattern): the
|
|
47
47
|
// claude-b provider reuses this adapter whole, launching the same binary through the
|
|
48
48
|
// same wiring under its own id/home/credential. Everything that identified "claude"
|
|
49
49
|
// flows from this id; the launch assemblies/profile homes key off it the same way.
|
|
@@ -54,37 +54,37 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
54
54
|
readonly permissionPromptHandler = claudePermissionPromptHandler;
|
|
55
55
|
readonly compactSupportsInstructions = false;
|
|
56
56
|
readonly compactReportsSubmission = true;
|
|
57
|
-
//
|
|
57
|
+
// Claude's clearContext is a fire-and-forget tmux `/clear` keystroke with no structured
|
|
58
58
|
// completion barrier, so the self-resume continuation MUST be delivered in-process coupled to the
|
|
59
59
|
// clear command (after waitForClaudeInputReady settles it) instead of via a separate inject that
|
|
60
60
|
// would race the still-processing async clear. Codex omits this (atomic awaited threadStart).
|
|
61
61
|
readonly couplesClearInjectContinuation = true;
|
|
62
|
-
//
|
|
62
|
+
// F1 — Claude's /compact and /clear are blind tmux keystrokes with no delivery handshake:
|
|
63
63
|
// submitted mid-turn they queue behind the live turn and dequeue as literal chat (never parsed as
|
|
64
64
|
// slash commands), so the compaction silently no-ops. The runner gates the submit on the live
|
|
65
65
|
// provider turn clearing so the keystrokes always land on an idle prompt. Codex omits this
|
|
66
66
|
// (awaited app-server handshake — the await is the barrier).
|
|
67
67
|
readonly awaitsIdleBeforeContextCommand = true;
|
|
68
|
-
//
|
|
68
|
+
// initial prompt is seeded as Claude's positional launch arg (buildSpawnArgs) — reliable,
|
|
69
69
|
// no send-keys/onboarding race; tells the runner to skip the redundant post-launch delivery.
|
|
70
70
|
readonly seedsInitialPromptAtLaunch = true;
|
|
71
71
|
private readonly monitorlessTurnWatch: MonitorlessTurnWatchDeps;
|
|
72
72
|
private statusCb: (status: ProviderStatusUpdate) => void = () => {};
|
|
73
|
-
//
|
|
73
|
+
// the pane watcher's ONLY outbound channel for anything it inferred from pane text.
|
|
74
74
|
// Pane observations cannot end work or send input. A startup modal may carry only
|
|
75
75
|
// a display gate, which the runner publishes as a badge and timeline event.
|
|
76
76
|
private paneObservationCb: (observation: PaneObservation) => void = () => {};
|
|
77
77
|
private tmuxWatcher?: Timer; private turnWatcher?: Timer;
|
|
78
78
|
private turnWatchResume?: { process: ManagedProcess; sessionName: string; socketName?: string };
|
|
79
79
|
private statusWatchersStopped = false;
|
|
80
|
-
//
|
|
80
|
+
// review HIGH-1 — monotonic turn-watch generation. Captured when a monitorless
|
|
81
81
|
// turn-watch is armed and bumped by stopTurnWatch(); the async setup and every in-flight
|
|
82
82
|
// tick re-check it after each await and before any state mutation / status emit, so a
|
|
83
83
|
// shutdown or a replacement invalidates a predecessor's setup AND its outstanding ticks. The
|
|
84
84
|
// pane watcher fences on Timer identity (`this.tmuxWatcher !== watcher`) instead — the same
|
|
85
85
|
// guarantee, checked after every await there.
|
|
86
86
|
private turnWatchGen = 0;
|
|
87
|
-
//
|
|
87
|
+
// review MED — the pane pid resolved OFF the loop once per spawn and cached, so the
|
|
88
88
|
// synchronous resolveBusyPid() (called inline from the runner core on every provider-turn
|
|
89
89
|
// busy edge) can answer without a blocking tmux display-message spawn. undefined until the
|
|
90
90
|
// async resolve lands (graceful: the runner falls back to its other liveness signals).
|
|
@@ -94,13 +94,13 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
94
94
|
private startupReadyObserved = false;
|
|
95
95
|
private startupBypassDialogActive = false;
|
|
96
96
|
private startupOpenApproval = false;
|
|
97
|
-
//
|
|
97
|
+
// when relay LAST typed a turn-cancelling keystroke into the managed pane itself. A
|
|
98
98
|
// probe whose last write predates this cannot be describing the state after the cancellation,
|
|
99
|
-
// so its busy verdict stops being authoritative at once.
|
|
99
|
+
// so its busy verdict stops being authoritative at once. Removed the prompt gate's
|
|
100
100
|
// auto-Escape, so the sole remaining writer is the explicit `interrupt()` command — a
|
|
101
101
|
// deliberate, operator/runner-initiated cancel, not something inferred from pane text.
|
|
102
102
|
private paneInterruptAt?: number;
|
|
103
|
-
//
|
|
103
|
+
// provider epoch: bumped on every spawn, stamped into the ManagedProcess and
|
|
104
104
|
// into any shutdown intent minted for it. A shutdown request that outlives a restart
|
|
105
105
|
// therefore cannot authorize typing `/exit` into the replacement session.
|
|
106
106
|
private providerEpoch = 0;
|
|
@@ -124,7 +124,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
124
124
|
private readonly sessionCapture = new ClaudeSessionCapture();
|
|
125
125
|
private tokenUsageCapture?: ClaudeTranscriptTokenUsageCapture;
|
|
126
126
|
private tokenUsageReporter?: ProviderTokenUsageReporter;
|
|
127
|
-
//
|
|
127
|
+
// dedupes the empty-pass INFO log to once per turn: a turn's own capture pass (and
|
|
128
128
|
// any mid-turn permission pre-flush pass) can otherwise post 0 entries repeatedly for the
|
|
129
129
|
// same turn while its JSONL flush is still in flight.
|
|
130
130
|
private lastEmptyTokenUsageLogTurnKey?: string;
|
|
@@ -152,7 +152,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
152
152
|
const transcriptPath = input.transcriptPath;
|
|
153
153
|
const turnKey = ctx.turnId ?? input.promptId ?? transcriptPath;
|
|
154
154
|
void this.tokenUsageCapture.capture(transcriptPath).then((postedCount) => {
|
|
155
|
-
//
|
|
155
|
+
// an empty pass previously logged nothing at all; a turn's own one-shot
|
|
156
156
|
// capture racing the JSONL flush is exactly the condition this fix addresses, so it
|
|
157
157
|
// must be visible at INFO without flipping log level (see also the reportFailure
|
|
158
158
|
// wiring in runner-core.ts, moved off DEBUG for the same reason).
|
|
@@ -173,7 +173,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
async captureFinalTokenUsage(transcriptPath: string | undefined, ctx: ProviderSessionTurnContext): Promise<void> {
|
|
176
|
-
//
|
|
176
|
+
// round 2 review (MED-1) — this adapter owns the transcript-presence bail now
|
|
177
177
|
// (previously lived in the shared runner-core seam, which made it Claude-shaped for every
|
|
178
178
|
// adapter): Claude's capture tails a JSONL transcript file, so it has nothing to settle
|
|
179
179
|
// without a real path.
|
|
@@ -192,7 +192,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
192
192
|
this.paneObservationCb = cb;
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
// §4.1 contract verdict
|
|
195
|
+
// §4.1 contract verdict. Claude reference: Claude is token-silent during
|
|
196
196
|
// native compaction, but the runner publishes a `compacting` timeline transition while it
|
|
197
197
|
// runs — so `timelineEvent.timestamp` (folded into lastProgressAt) keeps a compacting Opus
|
|
198
198
|
// turn reading as progressing instead of wedged. Transport frames/acks + the server's
|
|
@@ -211,7 +211,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
211
211
|
return hydrateClaudeHostConfig(config, this.provider);
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
async spawn(config: RunnerSpawnConfig): Promise<ManagedProcess> {
|
|
214
|
+
async spawn(config: RunnerSpawnConfig, preparedArgs?: SpawnArgs): Promise<ManagedProcess> {
|
|
215
215
|
// A provider spawn is the adapter incarnation boundary on both tmux/headless and direct
|
|
216
216
|
// interactive paths. Invalidate predecessor watchers before reopening the new incarnation,
|
|
217
217
|
// but retain the stopped predecessor's resume descriptor if spawn itself throws: runner-core
|
|
@@ -223,7 +223,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
223
223
|
this.statusWatchersStopped = false;
|
|
224
224
|
try {
|
|
225
225
|
await verifyClaudeSubagentVersion(config);
|
|
226
|
-
const hydratedConfig = await this.hydrateSpawnConfig(config); const args = this.buildSpawnArgs(hydratedConfig, hydratedConfig.providerConfig as ProviderConfig);
|
|
226
|
+
const hydratedConfig = preparedArgs ? config : await this.hydrateSpawnConfig(config); const args = preparedArgs ?? this.buildSpawnArgs(hydratedConfig, hydratedConfig.providerConfig as ProviderConfig);
|
|
227
227
|
if (hydratedConfig.headless) return this.spawnHeadless(hydratedConfig, args);
|
|
228
228
|
|
|
229
229
|
const proc = Bun.spawn([args.command, ...args.args], {
|
|
@@ -261,25 +261,25 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
261
261
|
const session = process.meta?.tmuxSession as string | undefined;
|
|
262
262
|
const socket = process.meta?.tmuxSocket as string | undefined;
|
|
263
263
|
if (!session || !tmuxHasSession(session, socket)) throw new Error("no active tmux session for clear");
|
|
264
|
-
//
|
|
264
|
+
// snapshot the pane BEFORE submitting `/clear` so the clear-aware barrier can detect
|
|
265
265
|
// the loaded conversation body disappearing (positive clear-commit evidence), not just the
|
|
266
266
|
// ever-present footer. Best-effort: a capture failure just yields no anchor → clearConfirmed
|
|
267
267
|
// falls to false → backstop covers.
|
|
268
|
-
//
|
|
268
|
+
// VISIBLE screen only. The old `-S -80` capture snapshotted 80 lines of tmux
|
|
269
269
|
// scrollback too, and `/clear` repaints only the visible screen (it never emits the `ESC[3J`
|
|
270
270
|
// that would drop history), so the anchor picked out of scrollback was still there after the
|
|
271
|
-
// clear committed and the barrier below could never confirm it
|
|
271
|
+
// clear committed and the barrier below could never confirm it.
|
|
272
272
|
let preClearPane = "";
|
|
273
273
|
let preClearCaptured = false;
|
|
274
274
|
try { preClearPane = await CLAUDE_CLEAR_COMMIT_CAPTURE(session, socket); preClearCaptured = true; } catch {}
|
|
275
|
-
//
|
|
275
|
+
// the PRIMARY, machine-readable clear-commit signal: Claude mints a new session
|
|
276
276
|
// (new sessionId, new transcript file) the moment `/clear` commits. Snapshot the current one
|
|
277
277
|
// first, pid-pinned, so the barrier can watch for the rotation. Best-effort: an unavailable
|
|
278
278
|
// probe (pre-2.1.119, unresolvable config home) just leaves the barrier on its pane heuristic.
|
|
279
279
|
const readSession = await bindClaudeClearSessionReader(process, session, socket);
|
|
280
280
|
const preClearSession = readSession ? await readSession() : null;
|
|
281
281
|
await submitClaudeContextCommand(session, "/clear", socket);
|
|
282
|
-
//
|
|
282
|
+
// `/clear` is async in the Claude TUI: submitTextToTmux returns as soon as the keys
|
|
283
283
|
// are typed, before the conversation is actually cleared. The self-resume continuation used to
|
|
284
284
|
// race this still-processing clear (a separate command, ~8ms later, on a different transport)
|
|
285
285
|
// and get swallowed — a silent mind-wipe. The OLD barrier (waitForClaudeInputReady) accepted
|
|
@@ -287,13 +287,13 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
287
287
|
// could return before the clear committed. Use the CLEAR-AWARE barrier instead: it requires
|
|
288
288
|
// positive evidence the clear committed (pane idle + input-ready + the pre-clear conversation
|
|
289
289
|
// anchor gone). `clearConfirmed` is surfaced to the coupled deliver: only a genuine confirmation
|
|
290
|
-
// permits continuationDelivered:true; a timeout/uncertain settle → false → the relay's
|
|
290
|
+
// permits continuationDelivered:true; a timeout/uncertain settle → false → the relay's
|
|
291
291
|
// backstop re-injects. Never throws (the clear already ran); uncertainty resolves to false.
|
|
292
292
|
const clearConfirmed = await waitForClaudeClearCommitted(session, preClearPane, CLAUDE_TMUX_READY_TIMEOUT_MS, socket, { preClearSession, readSession, preClearCaptured });
|
|
293
293
|
return { method: "tmux-inject", command: "/clear", clearConfirmed };
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
-
//
|
|
296
|
+
// Claude reports context only through its out-of-band status-line probe,
|
|
297
297
|
// which freezes after an in-process compact/clear/native-resume. The transcript
|
|
298
298
|
// (handed to the runner every turn by the Stop hook) carries the same per-turn
|
|
299
299
|
// token usage, so the runner reads it here as a probe-independent fallback that
|
|
@@ -305,9 +305,9 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
305
305
|
const socket = process.meta?.tmuxSocket as string | undefined;
|
|
306
306
|
if (!session || !tmuxHasSession(session, socket)) throw new Error("no active tmux session to interrupt");
|
|
307
307
|
// The same ESC the web terminal's aux key sends: cancels the in-flight turn
|
|
308
|
-
// and drops Claude back to its input box without ending the session.
|
|
308
|
+
// and drops Claude back to its input box without ending the session. Mark the probe's
|
|
309
309
|
// pre-cancel busy verdict non-authoritative so the runner's post-interrupt reconcile can see
|
|
310
|
-
// the idle pane when the cancel writes no probe transition (a `shell` rewrite is idle
|
|
310
|
+
// the idle pane when the cancel writes no probe transition (a `shell` rewrite is idle).
|
|
311
311
|
this.paneInterruptAt = Date.now();
|
|
312
312
|
const result = spawnSyncBounded(tmuxCommand(socket, "send-keys", "-t", session, "Escape"), {
|
|
313
313
|
stdin: "ignore", stdout: "ignore", stderr: "pipe",
|
|
@@ -322,25 +322,25 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
322
322
|
async probeActivity(process: ManagedProcess): Promise<"busy" | "idle" | "unknown"> {
|
|
323
323
|
const session = process.meta?.tmuxSession as string | undefined;
|
|
324
324
|
const socket = process.meta?.tmuxSocket as string | undefined;
|
|
325
|
-
//
|
|
325
|
+
// row 14 — the busy reconciler drives this every 4s while busy; its has-session
|
|
326
326
|
// + probe read + pane fallback now run off the event loop. A non-`present` presence
|
|
327
327
|
// (absent OR an unreadable wedged read) yields "unknown", the safe hold value — the
|
|
328
|
-
// reconciler treats unknown as non-idle and never clears a live turn on it
|
|
328
|
+
// reconciler treats unknown as non-idle and never clears a live turn on it.
|
|
329
329
|
if (!session || (await tmuxSessionPresence(session, socket)) !== "present") return "unknown";
|
|
330
|
-
//
|
|
330
|
+
// U2 — the busy BACKSTOP consumer: when the intrinsic probe is unavailable, a bare
|
|
331
331
|
// input-ready pane is NOT proof the turn ended (a long foreground tool renders the same box),
|
|
332
332
|
// so it must read as `unknown` (hold), never `idle` (force-cancel). paneFallbackIsIdle:false
|
|
333
333
|
// opts this path out of the legacy pane-scrape idle the turn-watch below still relies on.
|
|
334
334
|
return this.activityFromProbeOrPane(process, session, socket, { paneFallbackIsIdle: false });
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
-
//
|
|
338
|
-
// (process.pid is undefined — only the tmux session/socket), so the runner's
|
|
337
|
+
// a tmux-launched Claude session's ManagedProcess carries no direct OS pid
|
|
338
|
+
// (process.pid is undefined — only the tmux session/socket), so the runner's
|
|
339
339
|
// pid-liveness fallback had nothing to check for an ordinary hook-driven turn. Resolve
|
|
340
340
|
// the tmux pane's real pid so a long, quiet (no subagents, no compaction) busy turn can
|
|
341
341
|
// prove it is still alive instead of being force-healed to idle by the runner's
|
|
342
342
|
// stale-progress backstop purely because 10 minutes passed with no interim signal.
|
|
343
|
-
//
|
|
343
|
+
// review MED — this is a SYNCHRONOUS adapter-interface method the runner core calls
|
|
344
344
|
// inline on every provider-turn `busy` edge, so it must never spawn: the old
|
|
345
345
|
// resolveClaudePid() shelled out to `tmux display-message` (up to a 10s block on a wedged
|
|
346
346
|
// socket) on that hot edge. It now returns the direct OS pid when there is one, else the pane
|
|
@@ -353,19 +353,19 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
353
353
|
return this.tmuxPanePid;
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
-
// Shared probe-primary / pane-fallback activity read
|
|
356
|
+
// Shared probe-primary / pane-fallback activity read, used by both the
|
|
357
357
|
// reconciler-facing probeActivity and the monitorless turn-watch. Primary: Claude
|
|
358
358
|
// Code's intrinsic session probe (busy→busy — the idle-while-working gap the
|
|
359
359
|
// pane-scrape misread; idle/shell→idle; waiting→unknown, a parked turn the backstop must
|
|
360
360
|
// not force-clear). Legacy fallback: scrape the tmux pane (pre-2.1.119 / probe missing).
|
|
361
361
|
private async activityFromProbeOrPane(process: ManagedProcess, session: string, socket?: string, opts: { paneFallbackIsIdle?: boolean } = {}): Promise<ClaudeProbeActivity> {
|
|
362
|
-
//
|
|
362
|
+
// rows 14/15 — every subprocess/fs read on this path (pane-pid resolve, probe JSON
|
|
363
363
|
// read, /proc liveness read, transcript stat, and the pane-scrape fallback) runs off the
|
|
364
364
|
// event loop now. The verdict logic itself (claudeProbeVerdict / claudeActivityFromProbeAndPane)
|
|
365
365
|
// is pure and unchanged.
|
|
366
366
|
const managed = await readManagedClaudeStatusAsync(claudeProbeArgsFor(process, session, socket));
|
|
367
|
-
//
|
|
368
|
-
// transcript mtime
|
|
367
|
+
// a busy probe holds the turn unless relay's own interrupt postdates it; the
|
|
368
|
+
// transcript mtime is only read then, to tell whether that cancel took.
|
|
369
369
|
const paneInterruptAt = this.paneInterruptAt;
|
|
370
370
|
const lastActivityAt = paneInterruptAt === undefined ? undefined : await claudeTranscriptMtimeMsAsync(managed?.reading, managed?.configHome);
|
|
371
371
|
const { activity: probe, busyStale } = claudeProbeVerdict(managed?.reading, { paneInterruptAt, lastActivityAt });
|
|
@@ -375,22 +375,22 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
375
375
|
return claudeActivityFromProbeAndPane(probe, pane, { probeBusyStale: busyStale, paneFallbackIsIdle: opts.paneFallbackIsIdle ?? true });
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
-
//
|
|
378
|
+
// P1-A: an independent pane-busy read consumed by the busy backstop — a probe-`idle`
|
|
379
379
|
// must never clear a live turn while the pane spinner still reads busy (a connection-retry/529
|
|
380
|
-
// can write status:idle mid-turn without tripping the rate-limit pane detector — the
|
|
380
|
+
// can write status:idle mid-turn without tripping the rate-limit pane detector — the gap).
|
|
381
381
|
// probeActivity stays probe-primary; this is a corroboration channel only.
|
|
382
382
|
//
|
|
383
|
-
//
|
|
383
|
+
// this used to answer `false` for all three of "captured, no spinner", "no tmux session"
|
|
384
384
|
// and "capture threw", and the backstop read that single `false` as permission to clear a live
|
|
385
385
|
// turn. A read that failed is a fact about the read, not about the provider — it is reported as
|
|
386
386
|
// such now, and the backstop refuses to act on it.
|
|
387
387
|
async probePaneBusy(process: ManagedProcess): Promise<PaneBusyReading> {
|
|
388
388
|
const session = process.meta?.tmuxSession as string | undefined;
|
|
389
389
|
const socket = process.meta?.tmuxSocket as string | undefined;
|
|
390
|
-
//
|
|
390
|
+
// row 14 — the reconciler's independent pane-busy corroboration read, off the loop.
|
|
391
391
|
// A wedged-but-live server (presence "unreadable") reports "unreadable" — which the
|
|
392
392
|
// reconciler already treats as no-evidence and HOLDS on — rather than the false "no-session"
|
|
393
|
-
// a boolean has-session would have produced
|
|
393
|
+
// a boolean has-session would have produced.
|
|
394
394
|
if (!session) return "no-session";
|
|
395
395
|
const presence = await tmuxSessionPresence(session, socket);
|
|
396
396
|
if (presence === "absent") return "no-session";
|
|
@@ -409,20 +409,20 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
409
409
|
// connected" forever); we inject via tmux instead. For monitored profiles we keep the monitor
|
|
410
410
|
// path (and its retry on transient startup races) to avoid double-delivering against a late monitor.
|
|
411
411
|
if (process.meta?.monitorless === true || !monitor?.deliver) return this.deliverAsUserTurn(process, messages, ctx);
|
|
412
|
-
//
|
|
412
|
+
// human chat goes in as a tmux user turn; without a tmux pane everything stays on Monitor.
|
|
413
413
|
const userTurn = process.meta?.tmuxSession ? (batch: Message[]) => this.deliverAsUserTurn(process, batch, ctx) : undefined;
|
|
414
414
|
return deliverClaudeByLane(messages, { userTurn, narrowTurnOpener: ctx?.narrowTurnOpener, monitor: (batch, reportsOffer) => this.deliverViaMonitor(process, monitor.deliver!.bind(monitor), batch, reportsOffer ? ctx : { ...ctx, inputObserver: undefined }) });
|
|
415
415
|
}
|
|
416
416
|
|
|
417
417
|
private async deliverViaMonitor(process: ManagedProcess, deliver: (messages: Message[]) => Promise<number[]>, messages: Message[], ctx?: ProviderDeliverContext): Promise<DeliverResult> {
|
|
418
|
-
//
|
|
418
|
+
// F2 edge 2 — for self-resume continuation deliveries, wait for the TUI to be input-ready
|
|
419
419
|
// first so the continuation never lands into a not-yet-settled post-clear context. The coupled
|
|
420
420
|
// clearContext clear-aware barrier is the authoritative clear-commit gate; this belt-and-
|
|
421
421
|
// suspenders settle also protects the SEPARATE relay-backstop inject. Best-effort: a settle
|
|
422
422
|
// timeout must not fail the delivery — the backstop path already IS the recovery.
|
|
423
423
|
const session = process.meta?.tmuxSession as string | undefined;
|
|
424
424
|
const socket = process.meta?.tmuxSocket as string | undefined;
|
|
425
|
-
//
|
|
425
|
+
// review — only skip the settle when the session is positively `absent`.
|
|
426
426
|
if (session && messagesAreSelfResumeContinuation(messages) && (await tmuxSessionPresence(session, socket)) !== "absent") {
|
|
427
427
|
await waitForClaudeInputReady(session, CLAUDE_TMUX_READY_TIMEOUT_MS, socket).catch(() => {});
|
|
428
428
|
}
|
|
@@ -434,7 +434,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
434
434
|
|
|
435
435
|
// Type the batch into the interactive tmux session as a real user turn — the transport
|
|
436
436
|
// deliverInitialPrompt uses. Keeps the agent on interactive Claude (the user's Max quota), never
|
|
437
|
-
// `claude -p`/API; carries human chat
|
|
437
|
+
// `claude -p`/API; carries human chat and every message of a monitorless profile.
|
|
438
438
|
private async deliverAsUserTurn(process: ManagedProcess, messages: Message[], ctx?: ProviderDeliverContext): Promise<DeliverResult> {
|
|
439
439
|
const monitorless = process.meta?.monitorless === true;
|
|
440
440
|
const { session, socket } = await this.paneReadyForInput(process);
|
|
@@ -442,8 +442,8 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
442
442
|
// scaffolding (reply reminder, claim-task instruction) — the agent has no way to /reply or /claim.
|
|
443
443
|
const text = claudeProviderMessageText(messages, { relaySurface: !monitorless }), imagePaths = providerAttachmentLocalPaths(messages);
|
|
444
444
|
ctx?.recordSubmittedPrompt?.(text, imagePaths);
|
|
445
|
-
//
|
|
446
|
-
// exit 0 is not proof of submission
|
|
445
|
+
// Wave 2a — any tmux failure throws and deliver propagates it unclassified; a tmux
|
|
446
|
+
// exit 0 is not proof of submission, which the submit confirmation settles.
|
|
447
447
|
await this.submitUserPromptToTmux(process, session, text, socket, imagePaths, ctx?.inputObserver);
|
|
448
448
|
// Monitorless = no Stop/UserPromptSubmit hooks, so the runner gets no turn
|
|
449
449
|
// lifecycle and never marks the claimed task done (the way Codex's app-server
|
|
@@ -465,22 +465,22 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
465
465
|
if (this.statusWatchersStopped) return;
|
|
466
466
|
this.stopTurnWatch();
|
|
467
467
|
this.turnWatchResume = { process, sessionName, ...(socketName ? { socketName } : {}) };
|
|
468
|
-
//
|
|
468
|
+
// review HIGH-1 — the generation this watch owns. stopTurnWatch just bumped it; a
|
|
469
469
|
// later stop/replace bumps it again, so every await below re-checks `gen === this.turnWatchGen`
|
|
470
470
|
// before touching shared state or emitting a status. A shutdown DURING the async setup, or an
|
|
471
471
|
// in-flight predecessor tick finishing after a successor is armed, is thereby fenced out.
|
|
472
472
|
const gen = this.turnWatchGen;
|
|
473
|
-
//
|
|
474
|
-
// so
|
|
473
|
+
// P2-C: feed the resolved claude pid into the provider-turn liveness pids
|
|
474
|
+
// so the pid-liveness reconcile arms for headless/monitorless turns too (the
|
|
475
475
|
// process itself isn't tracked here — pid is undefined for tmux-managed claude).
|
|
476
476
|
// A dead pid means the turn is definitively over, so this can't falsely clear.
|
|
477
|
-
//
|
|
477
|
+
// row 15 — the per-turn-start pane-pid resolve + probe read run off the loop.
|
|
478
478
|
const livenessPid = await this.monitorlessTurnWatch.resolvePanePid(process.pid, sessionName, socketName);
|
|
479
479
|
// A numeric pid is not a process identity: an unrelated Claude can reuse it after this pane
|
|
480
480
|
// exits. Pin /proc's start-time incarnation before accepting the asynchronous session probe.
|
|
481
481
|
const paneIncarnation = livenessPid === undefined ? undefined : await this.monitorlessTurnWatch.processIncarnation(livenessPid);
|
|
482
482
|
if (gen !== this.turnWatchGen) return;
|
|
483
|
-
//
|
|
483
|
+
// monitorless workers have no Stop/UserPromptSubmit hooks, so the transcript
|
|
484
484
|
// reasoning tail is never hook-triggered and chat stays empty. Resolve the live transcript
|
|
485
485
|
// path from the intrinsic session probe (sessionId + cwd) and hand it to the runner on the
|
|
486
486
|
// turn edges so it can drive the SAME reasoning tail natively. undefined on an old/absent
|
|
@@ -518,7 +518,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
518
518
|
let ticks = 0;
|
|
519
519
|
let idleStreak = 0;
|
|
520
520
|
let sawBusy = false;
|
|
521
|
-
//
|
|
521
|
+
// row 15 (shares row 14's fix) — the tick's activity read is now off-loop, so the
|
|
522
522
|
// body is an async IIFE. `inFlight` keeps a slow (now off-loop) probe from stacking a
|
|
523
523
|
// second concurrent tick on the 1.5s timer.
|
|
524
524
|
let inFlight = false;
|
|
@@ -529,7 +529,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
529
529
|
ticks += 1;
|
|
530
530
|
const presence = await this.monitorlessTurnWatch.sessionPresence(sessionName, socketName);
|
|
531
531
|
if (gen !== this.turnWatchGen) return; // superseded during the await — do not touch state
|
|
532
|
-
//
|
|
532
|
+
// HIGH-2: only a positive `absent` (or the safety cap) ends the watch. An
|
|
533
533
|
// `unreadable` read (wedged-but-live server) HOLDS — never abandon a live turn on it.
|
|
534
534
|
if (presence === "unreadable") return;
|
|
535
535
|
if (presence === "absent" || ticks > CLAUDE_TURN_MAX_TICKS) {
|
|
@@ -563,9 +563,9 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
563
563
|
this.statusCb({ status: "busy", reason: "provider-turn", id: CLAUDE_TURN_WATCH_ID, ...(livenessPid ? { pids: [livenessPid] } : {}), transcriptPath });
|
|
564
564
|
}
|
|
565
565
|
}
|
|
566
|
-
//
|
|
566
|
+
// drive turn boundaries off the intrinsic probe (busy→busy, idle/shell→idle),
|
|
567
567
|
// falling back to the pane-scrape when the probe is unavailable. A foreground Bash
|
|
568
|
-
// gate reads busy; `shell` is a finished turn with a background task alive
|
|
568
|
+
// gate reads busy; `shell` is a finished turn with a background task alive.
|
|
569
569
|
const activity = await this.monitorlessTurnWatch.activity(process, sessionName, socketName);
|
|
570
570
|
if (gen !== this.turnWatchGen) return; // superseded during the probe await
|
|
571
571
|
if (activity === "busy") { sawBusy = true; idleStreak = 0; return; }
|
|
@@ -576,7 +576,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
576
576
|
const turnObserved = sawBusy || ticks >= CLAUDE_TURN_QUICK_GRACE_TICKS;
|
|
577
577
|
if (confirmedIdle && turnObserved) {
|
|
578
578
|
this.stopTurnWatch();
|
|
579
|
-
// Carry the transcript path on the idle edge too
|
|
579
|
+
// Carry the transcript path on the idle edge too so the runner can capture the
|
|
580
580
|
// turn's final assistant response natively before it tears down the reasoning tail.
|
|
581
581
|
this.statusCb({ status: "idle", reason: "provider-turn", id: CLAUDE_TURN_WATCH_ID, ...(transcriptPath ? { transcriptPath } : {}) });
|
|
582
582
|
}
|
|
@@ -585,7 +585,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
585
585
|
}
|
|
586
586
|
|
|
587
587
|
private stopTurnWatch(): void {
|
|
588
|
-
//
|
|
588
|
+
// review HIGH-1 — bump the generation FIRST so any predecessor setup still awaiting,
|
|
589
589
|
// or an in-flight tick, sees itself invalidated after its next await and neither emits a
|
|
590
590
|
// status nor tears down a successor. Unconditional (not gated on this.turnWatcher) so a
|
|
591
591
|
// stop DURING setup (before the interval exists) still invalidates that setup.
|
|
@@ -602,7 +602,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
602
602
|
await this.submitUserPromptToTmux(process, session, prompt, socket, options?.imagePaths, options?.inputObserver);
|
|
603
603
|
}
|
|
604
604
|
|
|
605
|
-
//
|
|
605
|
+
// review — off-loop presence: only a positive `absent` proves there is no session; on
|
|
606
606
|
// `unreadable` the deadline-bounded waitForClaudeInputReady sorts it out rather than failing spuriously.
|
|
607
607
|
private async paneReadyForInput(process: ManagedProcess, readyTimeoutMs = CLAUDE_TMUX_READY_TIMEOUT_MS): Promise<{ session: string; socket?: string }> {
|
|
608
608
|
const session = process.meta?.tmuxSession as string | undefined;
|
|
@@ -622,17 +622,17 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
622
622
|
|
|
623
623
|
private async submitUserPromptToTmux(process: ManagedProcess, session: string, text: string, socket?: string, imagePaths: string[] = [], inputObserver?: ProviderDeliverContext["inputObserver"]): Promise<void> {
|
|
624
624
|
const resolveTranscriptPath = () => resolveManagedClaudeTranscriptPathAsync(claudeProbeArgsFor(process, session, socket));
|
|
625
|
-
//
|
|
625
|
+
// the UserPromptSubmit hook confirms the submit; monitorless launches have no hooks.
|
|
626
626
|
await submitTextToTmux(session, text, socket, { confirmSubmit: true, transcriptPath: await resolveTranscriptPath().catch(() => undefined), resolveTranscriptPath, imagePaths, inputObserver, ...(process.meta?.monitorless === true ? {} : { promptHooks: this.sessionCapture.promptHooks }) });
|
|
627
627
|
}
|
|
628
628
|
|
|
629
629
|
buildSpawnArgs(config: RunnerSpawnConfig, providerConfig: ProviderConfig): SpawnArgs {
|
|
630
630
|
// Create + bootstrap + auth-link the isolated config home (host base has none) so the
|
|
631
631
|
// assembler's materialize step has somewhere to write the provisioned skills/plugins.
|
|
632
|
-
//
|
|
632
|
+
// this also seeds folder trust into whichever config file THIS launch reads,
|
|
633
633
|
// including the shared host config on a host-base/profile-less spawn.
|
|
634
634
|
prepareClaudeProfileHome(config, this.provider);
|
|
635
|
-
//
|
|
635
|
+
// ONE assembler owns the provisioning/vanilla/instruction launch surface
|
|
636
636
|
// (--setting-sources, --plugin-dir, --mcp-config, status-line, --append-system-prompt);
|
|
637
637
|
// profile-projection describes the SAME assembled result, so report == launch. The
|
|
638
638
|
// adapter only adds the bits the assembler does not own: launch command resolution,
|
|
@@ -645,10 +645,10 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
645
645
|
const providerArgs = claudeCallerAppends(config.headless
|
|
646
646
|
? claudeLaunchArgs(defaultArgs, config.providerArgs, config.approvalMode)
|
|
647
647
|
: [...defaultArgs, ...config.providerArgs]).args;
|
|
648
|
-
//
|
|
648
|
+
// when the assembler merged the caller's own `--settings` into relay's (so the
|
|
649
649
|
// baseline hooks/projectSettings/statusLine survive), drop the caller's now-redundant
|
|
650
650
|
// `--settings` here so exactly ONE reaches the launch (relay's merged copy in assembled.args).
|
|
651
|
-
//
|
|
651
|
+
// the coding/review profiles' --disallowedTools trim unions in at this single
|
|
652
652
|
// claude-family launch seam (helpers + per-profile config in ./claude-tool-policy).
|
|
653
653
|
const args = claudeProfileDisallowedToolsArgs([
|
|
654
654
|
...assembled.args,
|
|
@@ -656,12 +656,12 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
656
656
|
], config);
|
|
657
657
|
if (config.model) args.push("--model", config.model);
|
|
658
658
|
const effortLaunch = claudeEffortLaunch(config.effort, config.env); args.push(...effortLaunch.args);
|
|
659
|
-
//
|
|
659
|
+
// seed the first turn at launch for ALL sessions; a transient headless oversized brief
|
|
660
660
|
// runs in Claude print mode and enters as stdin, not a racy tmux paste or system prompt.
|
|
661
|
-
const oneShot = config.prompt ? seedClaudeInitialPrompt(args, sanitizeLaunchText(String(config.prompt), "initial prompt"), { headless: config.headless, lifecycle: config.lifecycle, scratchDir: relayEnv(config.env, "
|
|
661
|
+
const oneShot = config.prompt ? seedClaudeInitialPrompt(args, sanitizeLaunchText(String(config.prompt), "initial prompt"), { headless: config.headless, lifecycle: config.lifecycle, scratchDir: relayEnv(config.env, "HUMWICK_SCRATCH_DIR") }) : undefined;
|
|
662
662
|
const env: Record<string, string> = writeChildEnv({}, {
|
|
663
663
|
...config.env,
|
|
664
|
-
//
|
|
664
|
+
// the assembler owns the isolated config-home env (CLAUDE_CONFIG_DIR).
|
|
665
665
|
...assembled.env,
|
|
666
666
|
// Bare `claude` runs its own CLAUDE_CONFIG_DIR and so never inherits the host's long-lived
|
|
667
667
|
// CLAUDE_CODE_OAUTH_TOKEN; without it claude falls back to the (often stale) file-based
|
|
@@ -677,20 +677,20 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
677
677
|
// carries a copy of the runner process env) wins, so an operator can still unset.
|
|
678
678
|
CLAUDE_CODE_DISABLE_BG_SHELL_PRESSURE_REAP:
|
|
679
679
|
config.env?.CLAUDE_CODE_DISABLE_BG_SHELL_PRESSURE_REAP ?? process.env.CLAUDE_CODE_DISABLE_BG_SHELL_PRESSURE_REAP ?? "1",
|
|
680
|
-
// Same treatment
|
|
680
|
+
// Same treatment: CLAUDE_CODE_SEND_FEEDBACK=0 is the
|
|
681
681
|
// documented removal of the SendFeedback tool — autonomous agents keep misfiling
|
|
682
682
|
// humwick bugs to Anthropic with no human to approve the draft. The managed
|
|
683
683
|
// settings.json additionally carries a permissions.deny belt (launch-assembly).
|
|
684
684
|
CLAUDE_CODE_SEND_FEEDBACK: config.env?.CLAUDE_CODE_SEND_FEEDBACK ?? process.env.CLAUDE_CODE_SEND_FEEDBACK ?? "0",
|
|
685
|
-
//
|
|
685
|
+
// a mid-fleet CC auto-update re-rendered startup gates on every restart; managed
|
|
686
686
|
// launches keep the installed version until an operator bump.
|
|
687
687
|
DISABLE_AUTOUPDATER: config.env?.DISABLE_AUTOUPDATER ?? process.env.DISABLE_AUTOUPDATER ?? "1",
|
|
688
|
-
//
|
|
688
|
+
// prompt suggestions ship conversation context to the provider to generate
|
|
689
689
|
// follow-up suggestions no Relay-agent human ever sees (pure quota burn). The bundle
|
|
690
690
|
// gate reads this env first; false outranks settings.json promptSuggestionEnabled.
|
|
691
691
|
CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION:
|
|
692
692
|
config.env?.CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION ?? process.env.CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION ?? "false",
|
|
693
|
-
//
|
|
693
|
+
// remote recap forks the FULL cached conversation through an extra model call at
|
|
694
694
|
// turn end to produce a recap pushed to a claude.ai remote UI (`notifyMetadataChanged({
|
|
695
695
|
// recap})`); no Relay agent ever renders it. The bundle gate reads
|
|
696
696
|
// CLAUDE_CODE_ENABLE_REMOTE_RECAP first (env outranks settings/statsig): "false" pins it
|
|
@@ -698,7 +698,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
698
698
|
// CCR surface; "true" still lets an operator force it on.
|
|
699
699
|
CLAUDE_CODE_ENABLE_REMOTE_RECAP:
|
|
700
700
|
config.env?.CLAUDE_CODE_ENABLE_REMOTE_RECAP ?? process.env.CLAUDE_CODE_ENABLE_REMOTE_RECAP ?? "false",
|
|
701
|
-
//
|
|
701
|
+
// CLI away summary fires the same recap generator when a pane regains terminal
|
|
702
702
|
// focus after ≥5min blur and injects the result into the live transcript. Relay panes
|
|
703
703
|
// routinely sit unfocused far longer than that, so every firing is a model call whose
|
|
704
704
|
// output no agent reads. The bundle gate reads CLAUDE_CODE_ENABLE_AWAY_SUMMARY before the
|
|
@@ -706,20 +706,20 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
706
706
|
// "true" forces it on.
|
|
707
707
|
CLAUDE_CODE_ENABLE_AWAY_SUMMARY:
|
|
708
708
|
config.env?.CLAUDE_CODE_ENABLE_AWAY_SUMMARY ?? process.env.CLAUDE_CODE_ENABLE_AWAY_SUMMARY ?? "false",
|
|
709
|
-
//
|
|
709
|
+
// a spawned session may not find the humwick CLI in the host login
|
|
710
710
|
// shell's PATH, so Relay's own bins lead (layout-agnostic for npm and git-checkout
|
|
711
|
-
// installs
|
|
712
|
-
// Homebrew's dirs
|
|
713
|
-
// and Claude's shell snapshot keeps this PATH, so no rc file restores it
|
|
714
|
-
//
|
|
711
|
+
// installs) while the inherited PATH keeps its own order behind them: leading
|
|
712
|
+
// Homebrew's dirs put Homebrew's node ahead of the operator's own on macOS,
|
|
713
|
+
// and Claude's shell snapshot keeps this PATH, so no rc file restores it.
|
|
714
|
+
// the profile's harness trim (Artifact, Workflow, bundled skills, Explore/Plan) outranks inherited env.
|
|
715
715
|
...claudeHarnessTrim(config).env,
|
|
716
716
|
...claudeSubagentEnv(config),
|
|
717
717
|
PATH: launchPath(config.env?.PATH ?? process.env.PATH),
|
|
718
|
-
|
|
718
|
+
HUMWICK_PROVIDER: this.provider,
|
|
719
719
|
...effortLaunch.env,
|
|
720
720
|
...claudeProjectShellEnv(config.env),
|
|
721
|
-
});
|
|
722
|
-
//
|
|
721
|
+
}, []);
|
|
722
|
+
// review HIGH-2 — credential isolation. config.env carries a copy of the whole
|
|
723
723
|
// runner process env, so ambient credentials of the PRIMARY subscription reach every
|
|
724
724
|
// launch — and each of these env vars OUTRANKS the file credential, so a
|
|
725
725
|
// second-subscription claude-family provider would silently authenticate as the
|
|
@@ -730,7 +730,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
730
730
|
// provider's own values can still ride its per-provider config env (threaded
|
|
731
731
|
// explicitly here, since runner-core only merges providerConfig.env into host-globals
|
|
732
732
|
// profiles). Primary subscription tokens survive; a host paid-key file does not. The key set is the SINGLE shared
|
|
733
|
-
// constant from @humwick/providers
|
|
733
|
+
// constant from @humwick/providers — the orchestrator's auth
|
|
734
734
|
// preflight scrubs the same list, and a local re-listing is blocked by the
|
|
735
735
|
// duplication ratchet.
|
|
736
736
|
const envUnset: string[] = [...effortLaunch.unset, ...(claudeSubagentPolicy(config) ? ["CLAUDE_CODE_AUTO_COMPACT_WINDOW", "DISABLE_AUTO_COMPACT"] : [])];
|
|
@@ -762,20 +762,20 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
762
762
|
this.startupOpenApproval = config.approvalMode === "open";
|
|
763
763
|
|
|
764
764
|
// Create the gate FIFO *before* the session exists — the launcher script blocks
|
|
765
|
-
// on reading it, so no output can be produced until we release it below
|
|
765
|
+
// on reading it, so no output can be produced until we release it below.
|
|
766
766
|
if (gateFifo) createLauncherGate(gateFifo);
|
|
767
767
|
|
|
768
768
|
await createTmuxSessionWithEnv(sessionName, socketName, tmuxArgs, launcherScript, spawnArgs.env, config.providerConfig.env, gateFifo, this.provider);
|
|
769
769
|
|
|
770
770
|
const exitCodeFile = spawnArgs.exitCodeFile ?? `${launcherScript}.exit-code`;
|
|
771
771
|
this.watchTmuxSession(sessionName, socketName, exitCodeFile, printMode);
|
|
772
|
-
//
|
|
772
|
+
// review MED — resolve the pane pid OFF the loop now that the session exists and cache
|
|
773
773
|
// it, so the synchronous resolveBusyPid() never has to shell out on a hot busy edge. Fenced
|
|
774
774
|
// on providerEpoch so a resolve that lands after a restart can't stamp a stale pid.
|
|
775
775
|
void resolveClaudePanePidAsync(undefined, sessionName, socketName)
|
|
776
776
|
.then((pid) => { if (this.providerEpoch === providerEpoch) this.tmuxPanePid = pid; })
|
|
777
777
|
.catch(() => {});
|
|
778
|
-
const logFile = relayEnv(spawnArgs.env, "
|
|
778
|
+
const logFile = relayEnv(spawnArgs.env, "HUMWICK_LOG_FILE");
|
|
779
779
|
if (logFile) {
|
|
780
780
|
spawnSyncBounded(tmuxCommand(socketName, "pipe-pane", "-o", "-t", sessionName, `cat >> ${shellQuote(logFile)}`), {
|
|
781
781
|
stdin: "ignore",
|
|
@@ -808,7 +808,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
808
808
|
|
|
809
809
|
private watchTmuxSession(sessionName: string, socketName?: string, exitCodeFile?: string, oneShot = false): void {
|
|
810
810
|
if (this.tmuxWatcher) clearInterval(this.tmuxWatcher);
|
|
811
|
-
//
|
|
811
|
+
// row 12 — the highest-frequency blocking site in the runner (every 2s for the
|
|
812
812
|
// agent's whole lifetime). Its two tmux subprocesses (has-session + capture-pane) now run
|
|
813
813
|
// OFF the event loop via the offload subprocess runner, so the tick is async. Guard against
|
|
814
814
|
// overlapping ticks: if a capture is still in flight when the timer fires again — a wedged
|
|
@@ -822,16 +822,16 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
822
822
|
tickInFlight = true;
|
|
823
823
|
void fireAndForget("[runner] claude tmux watch tick", (async () => {
|
|
824
824
|
const presence = await tmuxSessionPresence(sessionName, socketName);
|
|
825
|
-
//
|
|
825
|
+
// HIGH-1: re-fence after every await — a respawn/shutdown that replaced this
|
|
826
826
|
// watcher mid-await must not let a predecessor tick emit or tear down its successor.
|
|
827
827
|
if (this.tmuxWatcher !== watcher) return;
|
|
828
|
-
//
|
|
828
|
+
// HIGH-2: only a positive `absent` is evidence of death. An `unreadable` read
|
|
829
829
|
// (wedged-but-live server, deadline SIGKILL) HOLDS this tick — never emit `offline`.
|
|
830
830
|
if (presence === "unreadable") return;
|
|
831
831
|
if (presence === "absent") {
|
|
832
832
|
// The whole tick (including the delayed second confirmation) is serialized by the
|
|
833
833
|
// in-flight guard, so an off-loop confirm can no longer race a stacked tick. The
|
|
834
|
-
// confirm itself only returns true when BOTH reads are `absent
|
|
834
|
+
// confirm itself only returns true when BOTH reads are `absent`.
|
|
835
835
|
const gone = await confirmTmuxSessionGone(sessionName, socketName).catch(() => false);
|
|
836
836
|
if (this.tmuxWatcher !== watcher || !gone) return;
|
|
837
837
|
const exitStatus = await claudeLaunchExitStatus(exitCodeFile, this.startupReadyObserved, oneShot, lastPane);
|
|
@@ -845,7 +845,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
845
845
|
} catch {
|
|
846
846
|
return;
|
|
847
847
|
}
|
|
848
|
-
//
|
|
848
|
+
// HIGH-1: fence again after the capture await, before any observation/status emit.
|
|
849
849
|
if (this.tmuxWatcher !== watcher) return;
|
|
850
850
|
lastPane = pane;
|
|
851
851
|
// A reused tmux pane can leave an old ready footer in scrollback. Until the
|
|
@@ -867,7 +867,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
867
867
|
);
|
|
868
868
|
this.modelUnavailableObservationState = modelUnavailable.state;
|
|
869
869
|
if (modelUnavailable.observation) this.paneObservationCb(modelUnavailable.observation);
|
|
870
|
-
//
|
|
870
|
+
// connection-retry banner detection, advisory. BOTH edges are diagnostics.
|
|
871
871
|
// Appearance used to publish an idle + blocked hold, which the runner reads as the
|
|
872
872
|
// provider turn ENDING — completing the task claims of a turn that was still running.
|
|
873
873
|
// Disappearance used to publish a bare `idle`, an unconditional turn-end with no evidence
|
|
@@ -883,10 +883,10 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
883
883
|
this.connectionRetryActive = false;
|
|
884
884
|
this.paneObservationCb(claudeConnectionRetryClearedObservation(sessionName));
|
|
885
885
|
}
|
|
886
|
-
//
|
|
887
|
-
// this catches missed/new blocking panes with the same
|
|
886
|
+
// prompt-gate fallback. Known gates should be prevented before launch;
|
|
887
|
+
// this catches missed/new blocking panes with the same debounce + busy
|
|
888
888
|
// guard used for rate-limit holds.
|
|
889
|
-
//
|
|
889
|
+
// it NOTICES, it does not act. The five startup/modal gates are advisory and
|
|
890
890
|
// return no status at all, so the only thing this branch can do for them is publish a
|
|
891
891
|
// diagnostic. `status` is present solely for the usage-limit gate.
|
|
892
892
|
const promptGate = evaluateClaudePromptGatePane(livePane, this.promptGatePaneState, {
|
|
@@ -910,7 +910,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
910
910
|
private publishConfirmedTmuxExit(status: ProviderStatusUpdate): void {
|
|
911
911
|
// The pane and turn pollers are independent. Invalidate the turn generation BEFORE publishing
|
|
912
912
|
// the terminal edge so an in-flight incarnation/activity read cannot land a later idle and
|
|
913
|
-
// resurrect the row with its shutting-down lifecycle pill still latched
|
|
913
|
+
// resurrect the row with its shutting-down lifecycle pill still latched.
|
|
914
914
|
this.stopStatusWatchers();
|
|
915
915
|
this.statusCb(status);
|
|
916
916
|
}
|
|
@@ -935,7 +935,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
935
935
|
if (turnWatchResume) await this.beginMonitorlessTurnWatch(process, turnWatchResume.sessionName, turnWatchResume.socketName, false);
|
|
936
936
|
}
|
|
937
937
|
|
|
938
|
-
//
|
|
938
|
+
// the shutdown sequence (observe → correlate → maybe /exit) lives in
|
|
939
939
|
// ./claude-shutdown. Only the watcher teardown is the adapter's own business.
|
|
940
940
|
async shutdownTmux(
|
|
941
941
|
process: ManagedProcess,
|
|
@@ -949,7 +949,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
949
949
|
}
|
|
950
950
|
}
|
|
951
951
|
|
|
952
|
-
//
|
|
952
|
+
// a self-resume continuation delivery carries the resumeInjection/selfResume markers the
|
|
953
953
|
// runner stamps in injectContext. Used to scope the monitor-path settle re-check to ONLY those
|
|
954
954
|
// deliveries so ordinary message delivery is unchanged.
|
|
955
955
|
function messagesAreSelfResumeContinuation(messages: Message[]): boolean {
|
|
@@ -959,11 +959,11 @@ function messagesAreSelfResumeContinuation(messages: Message[]): boolean {
|
|
|
959
959
|
});
|
|
960
960
|
}
|
|
961
961
|
|
|
962
|
-
//
|
|
962
|
+
// the exit command and its two writers now live in ./claude-exit, behind the
|
|
963
963
|
// one-shot grant. Re-exported so existing `./claude` consumers + tests keep their path.
|
|
964
964
|
export { CLAUDE_EXIT_COMMAND } from "./claude-exit";
|
|
965
965
|
export { claudeShutdownGraceMs, type ClaudeShutdownDeps } from "./claude-shutdown";
|
|
966
|
-
// tmux transport plumbing moved to ./claude-tmux
|
|
966
|
+
// tmux transport plumbing moved to ./claude-tmux; re-exported so
|
|
967
967
|
// existing `./claude` consumers + tests keep their import paths.
|
|
968
968
|
export { CLAUDE_TMUX_SUBMIT_DELAY_MS, CLAUDE_TMUX_SUBMIT_KEY, CLAUDE_TMUX_READY_TIMEOUT_MS, tmuxSessionName, tmuxSocketName, assertTmuxSocketPathFits, tmuxEnvKeys, waitForClaudeClearCommitted } from "./claude-tmux";
|
|
969
969
|
// Monitorless turn detection (Fix: isolated agents reach task "done" like Codex).
|
|
@@ -973,7 +973,7 @@ const CLAUDE_TURN_IDLE_CONFIRM_TICKS = 3; // ~4.5s of stable idle before declar
|
|
|
973
973
|
const CLAUDE_TURN_QUICK_GRACE_TICKS = 8; // ~12s fallback when the spinner was never caught
|
|
974
974
|
const CLAUDE_TURN_MAX_TICKS = 3600; // ~90min safety cap so the timer never leaks
|
|
975
975
|
|
|
976
|
-
//
|
|
976
|
+
// sessionStatusLineSettingsArgs now lives in ../launch-assembly (the single
|
|
977
977
|
// launch-arg home); re-exported so existing ./claude consumers + tests keep their path.
|
|
978
978
|
export { sessionStatusLineSettingsArgs };
|
|
979
979
|
|
|
@@ -1034,7 +1034,7 @@ async function terminateSingleProcess(process: ManagedProcess, opts: { graceful:
|
|
|
1034
1034
|
try {
|
|
1035
1035
|
proc.kill(opts.graceful ? "SIGTERM" : "SIGKILL");
|
|
1036
1036
|
} catch {
|
|
1037
|
-
// Already gone (ESRCH) or unsignalable — re-observe rather than trust the throw.
|
|
1037
|
+
// Already gone (ESRCH) or unsignalable — re-observe rather than trust the throw.
|
|
1038
1038
|
return pid !== undefined && isPidAlive(pid) ? "failed-to-kill" : "killed";
|
|
1039
1039
|
}
|
|
1040
1040
|
const timeout = new Promise<"timeout">((resolve) => setTimeout(() => resolve("timeout"), opts.timeoutMs));
|
|
@@ -1045,6 +1045,6 @@ async function terminateSingleProcess(process: ManagedProcess, opts: { graceful:
|
|
|
1045
1045
|
} catch {}
|
|
1046
1046
|
await proc.exited.catch(() => {});
|
|
1047
1047
|
}
|
|
1048
|
-
// Confirm death rather than assume the kill took
|
|
1048
|
+
// Confirm death rather than assume the kill took.
|
|
1049
1049
|
return pid !== undefined && isPidAlive(pid) ? "failed-to-kill" : "killed";
|
|
1050
1050
|
}
|