@humwick/runner 0.0.0-stage → 0.130.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +167 -0
- package/agent-bin/agent-relay-browser +10 -0
- package/agent-bin/humwick +10 -0
- package/hooks/fff-search-redirect/fff-search-redirect.cjs +161 -0
- package/hooks/fff-search-redirect/hooks.json +13 -0
- package/hooks/rg-r-guard/hooks.json +18 -0
- package/hooks/rg-r-guard/rg-r-guard.cjs +144 -0
- package/hooks/web-tools-redirect/hooks.json +13 -0
- package/hooks/web-tools-redirect/web-tools-redirect.cjs +84 -0
- package/node_modules/@humwick/offload/LICENSE +167 -0
- package/node_modules/@humwick/offload/README.md +57 -0
- package/node_modules/@humwick/offload/dist/async-runner.d.ts +18 -0
- package/node_modules/@humwick/offload/dist/async-runner.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/async-runner.js +158 -0
- package/node_modules/@humwick/offload/dist/async-runner.js.map +1 -0
- package/node_modules/@humwick/offload/dist/envelope.d.ts +25 -0
- package/node_modules/@humwick/offload/dist/envelope.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/envelope.js +80 -0
- package/node_modules/@humwick/offload/dist/envelope.js.map +1 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts +10 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js +59 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js.map +1 -0
- package/node_modules/@humwick/offload/dist/index.d.ts +5 -0
- package/node_modules/@humwick/offload/dist/index.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/index.js +5 -0
- package/node_modules/@humwick/offload/dist/index.js.map +1 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts +62 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.js +295 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.js.map +1 -0
- package/node_modules/@humwick/offload/dist/types.d.ts +111 -0
- package/node_modules/@humwick/offload/dist/types.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/types.js +176 -0
- package/node_modules/@humwick/offload/dist/types.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts +23 -0
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-pool.js +435 -0
- package/node_modules/@humwick/offload/dist/worker-pool.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts +2 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.js +27 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker.d.ts +5 -0
- package/node_modules/@humwick/offload/dist/worker.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker.js +1 -0
- package/node_modules/@humwick/offload/dist/worker.js.map +1 -0
- package/node_modules/@humwick/offload/package.json +46 -0
- package/node_modules/@humwick/offload/src/async-runner.ts +170 -0
- package/node_modules/@humwick/offload/src/envelope.ts +92 -0
- package/node_modules/@humwick/offload/src/fixtures/worker-task.ts +49 -0
- package/node_modules/@humwick/offload/src/index.ts +4 -0
- package/node_modules/@humwick/offload/src/subprocess-runner.ts +334 -0
- package/node_modules/@humwick/offload/src/types.ts +235 -0
- package/node_modules/@humwick/offload/src/worker-pool.ts +463 -0
- package/node_modules/@humwick/offload/src/worker-runtime.ts +32 -0
- package/node_modules/@humwick/offload/src/worker.ts +5 -0
- package/package.json +47 -4
- package/plugins/browser/skills/playwright-cli/SKILL.md +53 -0
- package/plugins/claude/.claude-plugin/plugin.json +8 -0
- package/plugins/claude/claude-statusline-probe.cjs +144 -0
- package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +914 -0
- package/plugins/claude/hooks/gate-run-guard.sh +9 -0
- package/plugins/claude/hooks/gate-run-guard.ts +237 -0
- package/plugins/claude/hooks/hooks.json +158 -0
- package/plugins/claude/hooks/kill-guard.provisioned.mjs +7027 -0
- package/plugins/claude/hooks/kill-guard.sh +341 -0
- package/plugins/claude/hooks/kill-guard.ts +337 -0
- package/plugins/claude/hooks/permission-request.sh +22 -0
- package/plugins/claude/hooks/post-compact.sh +10 -0
- package/plugins/claude/hooks/post-tool-use.sh +8 -0
- package/plugins/claude/hooks/pre-compact.sh +12 -0
- package/plugins/claude/hooks/read-only-bash-guard.sh +587 -0
- package/plugins/claude/hooks/relay-status.sh +373 -0
- package/plugins/claude/hooks/session-end.sh +26 -0
- package/plugins/claude/hooks/session-start.sh +31 -0
- package/plugins/claude/hooks/shell-trust.ts +28 -0
- package/plugins/claude/hooks/stop-failure.sh +47 -0
- package/plugins/claude/hooks/stop.sh +48 -0
- package/plugins/claude/hooks/subagent-guard.cjs +26 -0
- package/plugins/claude/hooks/subagent-skill-policy.cjs +160 -0
- package/plugins/claude/hooks/subagent-start.sh +13 -0
- package/plugins/claude/hooks/subagent-stop.sh +19 -0
- package/plugins/claude/hooks/user-prompt-submit.sh +18 -0
- package/plugins/claude/hooks/wait-loop-guard.ts +120 -0
- package/plugins/claude/monitors/monitors.json +7 -0
- package/plugins/claude/monitors/relay-monitor.provisioned.mjs +786 -0
- package/plugins/claude/monitors/relay-monitor.ts +58 -0
- package/plugins/claude/skills/disconnect/SKILL.md +16 -0
- package/plugins/claude/skills/label/SKILL.md +23 -0
- package/plugins/claude/skills/message/SKILL.md +40 -0
- package/plugins/claude/skills/pair/SKILL.md +30 -0
- package/plugins/claude/skills/react/SKILL.md +18 -0
- package/plugins/claude/skills/read-message/SKILL.md +24 -0
- package/plugins/claude/skills/reply/SKILL.md +44 -0
- package/plugins/claude/skills/send-claimable/SKILL.md +39 -0
- package/plugins/claude/skills/slice-first/SKILL.md +32 -0
- package/plugins/claude/skills/slice-first/references/plan-recipe.md +46 -0
- package/plugins/claude/skills/status/SKILL.md +16 -0
- package/plugins/claude/skills/tags/SKILL.md +25 -0
- package/plugins/claude/skills/task/SKILL.md +28 -0
- package/plugins/claude-output-style/.claude-plugin/plugin.json +5 -0
- package/plugins/claude-output-style/output-styles/agent-relay.md +5 -0
- package/plugins/codex/skills/disconnect/SKILL.md +15 -0
- package/plugins/codex/skills/guide/SKILL.md +15 -0
- package/plugins/codex/skills/label/SKILL.md +22 -0
- package/plugins/codex/skills/message/SKILL.md +40 -0
- package/plugins/codex/skills/pair/SKILL.md +29 -0
- package/plugins/codex/skills/react/SKILL.md +17 -0
- package/plugins/codex/skills/read-message/SKILL.md +23 -0
- package/plugins/codex/skills/reply/SKILL.md +44 -0
- package/plugins/codex/skills/send-claimable/SKILL.md +39 -0
- package/plugins/codex/skills/slice-first/SKILL.md +32 -0
- package/plugins/codex/skills/slice-first/references/plan-recipe.md +46 -0
- package/plugins/codex/skills/status/SKILL.md +15 -0
- package/plugins/codex/skills/tags/SKILL.md +24 -0
- package/plugins/codex/skills/task/SKILL.md +28 -0
- package/src/adapter.ts +1061 -0
- package/src/adapters/__fixtures__/codex-app-server.ts +47 -0
- package/src/adapters/claude-api.ts +243 -0
- package/src/adapters/claude-context-command.ts +16 -0
- package/src/adapters/claude-credentials.ts +22 -0
- package/src/adapters/claude-delivery-lanes.ts +28 -0
- package/src/adapters/claude-delivery.ts +166 -0
- package/src/adapters/claude-effort.ts +94 -0
- package/src/adapters/claude-exit.ts +290 -0
- package/src/adapters/claude-headless-mirror.ts +88 -0
- package/src/adapters/claude-headless-refusal.ts +51 -0
- package/src/adapters/claude-headless.ts +617 -0
- package/src/adapters/claude-monitor-delivery-plan.ts +317 -0
- package/src/adapters/claude-permission.ts +243 -0
- package/src/adapters/claude-prompt-gates.ts +269 -0
- package/src/adapters/claude-prompt-hook.ts +145 -0
- package/src/adapters/claude-session-capture.ts +293 -0
- package/src/adapters/claude-session-probe.ts +414 -0
- package/src/adapters/claude-shutdown.ts +190 -0
- package/src/adapters/claude-status-detectors.ts +426 -0
- package/src/adapters/claude-subagents.ts +93 -0
- package/src/adapters/claude-submit-confirmation.ts +287 -0
- package/src/adapters/claude-tmux.ts +548 -0
- package/src/adapters/claude-token-usage-capture.ts +253 -0
- package/src/adapters/claude-tool-policy.ts +165 -0
- package/src/adapters/claude-transcript-tail.ts +193 -0
- package/src/adapters/claude-transcript.ts +812 -0
- package/src/adapters/claude.ts +1097 -0
- package/src/adapters/codex-agent-message-capture.ts +189 -0
- package/src/adapters/codex-app-server.ts +70 -0
- package/src/adapters/codex-client.ts +362 -0
- package/src/adapters/codex-effective-config.ts +87 -0
- package/src/adapters/codex-launch-config.ts +153 -0
- package/src/adapters/codex-rollout-context.ts +272 -0
- package/src/adapters/codex-skill-discovery.ts +18 -0
- package/src/adapters/codex-token-usage-capture.ts +163 -0
- package/src/adapters/codex-tool-output.ts +53 -0
- package/src/adapters/codex-turn-outcome.ts +27 -0
- package/src/adapters/codex-version.ts +64 -0
- package/src/adapters/codex.ts +1442 -0
- package/src/adapters/incremental-file.ts +110 -0
- package/src/adapters/opencode-api.ts +71 -0
- package/src/adapters/opencode-context.ts +277 -0
- package/src/adapters/opencode-model-registry.ts +126 -0
- package/src/adapters/opencode-provider.ts +169 -0
- package/src/adapters/opencode-server-retry.ts +50 -0
- package/src/adapters/opencode-soft-steer.ts +302 -0
- package/src/adapters/opencode-spawn.ts +145 -0
- package/src/adapters/opencode-subscribe.ts +62 -0
- package/src/adapters/opencode-token-usage-capture.ts +187 -0
- package/src/adapters/opencode-tool-events.ts +302 -0
- package/src/adapters/opencode-tools-config.ts +17 -0
- package/src/adapters/opencode.ts +2264 -0
- package/src/adapters/project-shell.ts +80 -0
- package/src/adapters/provider-launch-config.ts +21 -0
- package/src/adapters/provider-quota-harvest.ts +127 -0
- package/src/adapters/service-access.ts +153 -0
- package/src/adapters/tmux-offload.ts +207 -0
- package/src/adapters/tool-summary.ts +214 -0
- package/src/agent-redact.ts +75 -0
- package/src/agent-runner/commands/dispatch.ts +185 -0
- package/src/agent-runner/commands/lifecycle.ts +84 -0
- package/src/agent-runner/commands/prompt.ts +284 -0
- package/src/agent-runner/credentials.ts +208 -0
- package/src/agent-runner/http-liveness.ts +85 -0
- package/src/agent-runner/lifecycle-milestones.ts +207 -0
- package/src/agent-runner/lifecycle-phase.ts +80 -0
- package/src/agent-runner/registration.ts +163 -0
- package/src/agent-runner/runner-context.ts +158 -0
- package/src/agent-runner/session-mirror.ts +278 -0
- package/src/agent-runner/task-claims.ts +116 -0
- package/src/agent-runner/token-usage-coverage.ts +54 -0
- package/src/agent-runner/turn-report.ts +269 -0
- package/src/api-budget-accounting.ts +91 -0
- package/src/area-memory.ts +255 -0
- package/src/artifact-path-materialize.ts +52 -0
- package/src/async-guard.ts +35 -0
- package/src/attachment-cache-sweep.ts +31 -0
- package/src/attachment-cache.ts +195 -0
- package/src/background-script-reconcile.ts +106 -0
- package/src/bounded-log.ts +59 -0
- package/src/bounded-spawn-sync.ts +18 -0
- package/src/busy-reconciler.ts +282 -0
- package/src/claim-tracker.ts +140 -0
- package/src/claude-settings.ts +167 -0
- package/src/claude-workspace-trust.ts +289 -0
- package/src/config.ts +547 -0
- package/src/context-boundary.ts +101 -0
- package/src/context-command.ts +151 -0
- package/src/context-probe-state.ts +6 -0
- package/src/continuation-archive.ts +179 -0
- package/src/control-server.ts +730 -0
- package/src/delivery-policy.ts +9 -0
- package/src/delivery-result.ts +24 -0
- package/src/effort-command.ts +125 -0
- package/src/empty-turn.ts +816 -0
- package/src/free-port.ts +25 -0
- package/src/fs-offload.ts +26 -0
- package/src/gate-run-config.ts +35 -0
- package/src/gate-run-lease-probe.ts +38 -0
- package/src/host-asset-plan.ts +450 -0
- package/src/host-claude-config-worker.ts +34 -0
- package/src/host-sources.ts +40 -0
- package/src/inbox-carrier.ts +7 -0
- package/src/index.ts +386 -0
- package/src/launch-assembly.ts +955 -0
- package/src/liveness.ts +105 -0
- package/src/logger.ts +108 -0
- package/src/mcp-outbox.ts +204 -0
- package/src/mcp-preflight.ts +86 -0
- package/src/mcp-source-selection.ts +54 -0
- package/src/mcp-tool-deferral.ts +35 -0
- package/src/mcp-vault-env.ts +25 -0
- package/src/message-body-config/config.ts +16 -0
- package/src/message-body-config.ts +1 -0
- package/src/mid-turn-delivery.ts +43 -0
- package/src/native-memory-lever.ts +329 -0
- package/src/native-self-resume.ts +244 -0
- package/src/outbox.ts +453 -0
- package/src/park-post.ts +17 -0
- package/src/pending-permission-store.ts +148 -0
- package/src/process-meta.ts +9 -0
- package/src/profile-home.ts +419 -0
- package/src/profile-projection.ts +411 -0
- package/src/project-hooks.ts +65 -0
- package/src/project-instructions.ts +119 -0
- package/src/provider-double.ts +59 -0
- package/src/provider-hold.ts +228 -0
- package/src/provider-home-marker.ts +3 -0
- package/src/provider-home-tombstone.ts +4 -0
- package/src/provider-input-queue.ts +134 -0
- package/src/provider-integration.ts +93 -0
- package/src/provider-processes.ts +17 -0
- package/src/provider-submit-failure.ts +15 -0
- package/src/providers.ts +35 -0
- package/src/provisioning.ts +703 -0
- package/src/question-decision.ts +18 -0
- package/src/quota.ts +22 -0
- package/src/rate-limit.ts +351 -0
- package/src/refused-prompt-store.ts +36 -0
- package/src/relay-injection-events.ts +161 -0
- package/src/relay-instructions.ts +107 -0
- package/src/relay-mcp-proxy.long-turn-e2e.ts +183 -0
- package/src/relay-mcp-proxy.ts +704 -0
- package/src/relay-mcp.ts +583 -0
- package/src/reply-obligation-cache.ts +211 -0
- package/src/response-capture-report.ts +111 -0
- package/src/response-routing.ts +6 -0
- package/src/runner-core.ts +2955 -0
- package/src/runner-exit.ts +12 -0
- package/src/runner-helpers.ts +582 -0
- package/src/runner-insights.ts +105 -0
- package/src/runner-mcp-proxy.ts +46 -0
- package/src/runner-spawn-config.ts +68 -0
- package/src/runner-stale-scratch-sweep.ts +21 -0
- package/src/runner.ts +9 -0
- package/src/runtime-token-file.ts +45 -0
- package/src/runtime-token-remint.ts +56 -0
- package/src/service-access.ts +88 -0
- package/src/session-destroy.ts +27 -0
- package/src/session-insights.ts +118 -0
- package/src/session-scratch.ts +296 -0
- package/src/spawn-input.ts +147 -0
- package/src/startup-config-fetch.ts +48 -0
- package/src/subreaper.ts +113 -0
- package/src/template-resolver.ts +29 -0
- package/src/test-run-dedup.ts +435 -0
- package/src/transcript-context.ts +99 -0
- package/src/transport-errors.ts +48 -0
- package/src/turn-opener.ts +55 -0
- package/src/turn-reconcile-event.ts +88 -0
- package/src/vault-proxy-hop.ts +99 -0
- package/src/version.ts +44 -0
- package/src/web-tool-call-proxy.ts +128 -0
- package/src/web-tools.ts +59 -0
- package/README.md +0 -3
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// A tmux command exiting 0 only proves that tmux accepted a key. Claude can still
|
|
2
|
+
// have the pasted prompt in its composer (for example when Enter lands in copy mode).
|
|
3
|
+
// Claude's UserPromptSubmit hook proves acceptance; the composer only decides whether
|
|
4
|
+
// another Enter is safe, and confirms alone only on hookless launches (#2664).
|
|
5
|
+
import { ProviderSubmitFailure } from "../provider-submit-failure";
|
|
6
|
+
import type { ClaudePromptHookExpectation } from "./claude-prompt-hook";
|
|
7
|
+
import { claudeHookPromptMatches } from "./claude-prompt-hook";
|
|
8
|
+
import { CLAUDE_COMPOSER_RULE_RE } from "./claude-status-detectors";
|
|
9
|
+
import { stat } from "node:fs/promises";
|
|
10
|
+
import { IncrementalClaudeTranscriptTail, type IncrementalTranscriptPollResult } from "./claude-transcript-tail";
|
|
11
|
+
|
|
12
|
+
export const CLAUDE_SUBMIT_MAX_ENTERS = 3;
|
|
13
|
+
export const CLAUDE_SUBMIT_HOOK_WAIT_MS = 3_000;
|
|
14
|
+
/** Extra hook wait once our Enter left the composer: a slow hook is still proof (#3081). */
|
|
15
|
+
export const CLAUDE_SUBMIT_LATE_HOOK_WAIT_MS = 10_000;
|
|
16
|
+
const PASTE_RENDER_TIMEOUT_MS = 3_000;
|
|
17
|
+
const SUBMIT_SETTLE_MS = 1_200;
|
|
18
|
+
const POLL_MS = 100;
|
|
19
|
+
|
|
20
|
+
/** Watch the current transcript, resolving it again if the first user turn creates the file. */
|
|
21
|
+
export class ClaudeSubmitTranscriptProbe {
|
|
22
|
+
private readonly tail = new IncrementalClaudeTranscriptTail();
|
|
23
|
+
private path?: string;
|
|
24
|
+
private ready = false;
|
|
25
|
+
private knownMissing = false;
|
|
26
|
+
private readonly startedAt: number;
|
|
27
|
+
private recoveryPoll?: IncrementalTranscriptPollResult;
|
|
28
|
+
|
|
29
|
+
constructor(private readonly resolvePath: () => Promise<string | undefined>, now: () => number = Date.now) {
|
|
30
|
+
this.startedAt = now();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async start(path?: string): Promise<void> {
|
|
34
|
+
this.path = path;
|
|
35
|
+
this.ready = path ? await this.tail.seekToEnd(path).catch(() => false) : false;
|
|
36
|
+
this.knownMissing = Boolean(path && !this.ready && await stat(path).then(() => false, (error: NodeJS.ErrnoException) => error.code === "ENOENT"));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async poll(): Promise<IncrementalTranscriptPollResult | null> {
|
|
40
|
+
const fresh = await this.pollTail();
|
|
41
|
+
const retained = this.recoveryPoll;
|
|
42
|
+
this.recoveryPoll = undefined;
|
|
43
|
+
if (!retained) return fresh;
|
|
44
|
+
if (!fresh) return retained;
|
|
45
|
+
return { ...fresh, reset: retained.reset || fresh.reset,
|
|
46
|
+
userPrompts: retained.userPrompts.concat(fresh.userPrompts),
|
|
47
|
+
userPromptOccurredAts: retained.userPromptOccurredAts.concat(fresh.userPromptOccurredAts) };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private async pollTail(): Promise<IncrementalTranscriptPollResult | null> {
|
|
51
|
+
const path = this.path ?? await this.resolvePath().catch(() => undefined);
|
|
52
|
+
if (!path) return null;
|
|
53
|
+
if (!this.ready) {
|
|
54
|
+
let missing = false;
|
|
55
|
+
const file = await stat(path).catch((error: NodeJS.ErrnoException) => {
|
|
56
|
+
missing = error.code === "ENOENT";
|
|
57
|
+
return null;
|
|
58
|
+
});
|
|
59
|
+
if (!file) {
|
|
60
|
+
if (missing && this.path === path) this.knownMissing = true;
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
this.path = path;
|
|
64
|
+
// A late resolver may reveal an old conversation. Seek past clearly old
|
|
65
|
+
// files without loading their potentially large histories into memory.
|
|
66
|
+
if (!this.knownMissing && file.birthtimeMs < this.startedAt - 10_000) {
|
|
67
|
+
this.ready = await this.tail.seekToEnd(path).catch(() => false);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const first = await this.tail.poll(path).catch(() => null);
|
|
71
|
+
if (!first?.available) return null;
|
|
72
|
+
this.ready = true;
|
|
73
|
+
if (this.knownMissing || first.reset) return first;
|
|
74
|
+
// File timestamps can lag Date.now even when a file was created after
|
|
75
|
+
// submit. Use Claude's user-entry timestamp to distinguish a fresh turn
|
|
76
|
+
// from old history when the path was unknown at submit time.
|
|
77
|
+
const recent = first.userPrompts.flatMap((prompt, index) =>
|
|
78
|
+
(first.userPromptOccurredAts[index] ?? -Infinity) >= this.startedAt ? [{ prompt, occurredAt: first.userPromptOccurredAts[index] }] : []);
|
|
79
|
+
if (recent.length === 0) return null;
|
|
80
|
+
return { ...first, userPrompts: recent.map((item) => item.prompt), userPromptOccurredAts: recent.map((item) => item.occurredAt) };
|
|
81
|
+
}
|
|
82
|
+
return this.tail.poll(path).catch(() => null);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async confirms(text: string, offeredAt: number, imagePaths: readonly string[] = []): Promise<boolean> {
|
|
86
|
+
// A consumption check must leave its read available to submit recovery (#2691).
|
|
87
|
+
const result = this.recoveryPoll = (await this.poll()) ?? undefined;
|
|
88
|
+
return result?.userPrompts.some((prompt, i) =>
|
|
89
|
+
(result.userPromptOccurredAts[i] ?? -Infinity) >= offeredAt && claudeHookPromptMatches(prompt, text, imagePaths)) === true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Hold an ambiguous submission until a new tmux incarnation is created. */
|
|
94
|
+
export class ClaudeSubmitFence {
|
|
95
|
+
private readonly uncertain = new Map<string, (turnStarted: boolean) => Promise<boolean>>();
|
|
96
|
+
|
|
97
|
+
async run(session: string, submit: (markPasteStarted: () => void) => Promise<unknown>, recovery: (turnStarted: boolean) => Promise<boolean>): Promise<void> {
|
|
98
|
+
// Probe here too: a dashboard prompt reaches the fence directly, and the turn that would
|
|
99
|
+
// otherwise trigger recovery may have started before the failure was recorded (#2664).
|
|
100
|
+
if (this.uncertain.has(session) && !await this.recover(session)) {
|
|
101
|
+
throw new ProviderSubmitFailure("Claude prompt submit remains unconfirmed from an earlier attempt; inspect the TUI before retrying", false);
|
|
102
|
+
}
|
|
103
|
+
let pasteStarted = false;
|
|
104
|
+
try { await submit(() => { pasteStarted = true; }); }
|
|
105
|
+
catch (error) {
|
|
106
|
+
const retrySafe = !pasteStarted && (!(error instanceof ProviderSubmitFailure) || error.retrySafe);
|
|
107
|
+
const failure = new ProviderSubmitFailure(String(error), retrySafe, error);
|
|
108
|
+
if (!failure.retrySafe) this.uncertain.set(session, recovery);
|
|
109
|
+
throw failure;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async recover(session: string, turnStarted = false): Promise<boolean> {
|
|
114
|
+
const probe = this.uncertain.get(session);
|
|
115
|
+
if (!probe) return true; // nothing unconfirmed is outstanding
|
|
116
|
+
if (!await probe(turnStarted).catch(() => false) || this.uncertain.get(session) !== probe) return false;
|
|
117
|
+
this.uncertain.delete(session);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
clear(session: string): void { this.uncertain.delete(session); }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Composer recovery: a settled empty composer plus turn or transcript evidence. A matching hook also recovers. */
|
|
125
|
+
export async function claudeSubmitRecoveryConfirmed(deps: ClaudeComposerReadDeps & {
|
|
126
|
+
readNewUserPrompts?: () => Promise<{ reset: boolean; userPrompts: string[] } | null>;
|
|
127
|
+
paneShowsTurnStarted: (pane: string) => boolean;
|
|
128
|
+
}, turnStarted: boolean): Promise<boolean> {
|
|
129
|
+
// Turn evidence is only required on top of a settled empty composer, never instead of it.
|
|
130
|
+
const settled = await settleClaudeComposer(deps, null);
|
|
131
|
+
if (settled.verdict !== "empty") return false;
|
|
132
|
+
if (turnStarted || deps.paneShowsTurnStarted(plainPaneText(settled.pane))) return true;
|
|
133
|
+
const transcript = await deps.readNewUserPrompts?.();
|
|
134
|
+
return transcript?.reset === false && transcript.userPrompts.length > 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface ClaudeComposerReadDeps {
|
|
138
|
+
readPane: (timeoutMs: number) => Promise<string>;
|
|
139
|
+
sleep?: (ms: number) => Promise<unknown>;
|
|
140
|
+
now?: () => number;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Hookless (degraded) composer acceptance (#2664): empty only after a readable empty composer
|
|
144
|
+
// has lasted SUBMIT_SETTLE_MS from its FIRST empty frame, with no input re-shown meanwhile.
|
|
145
|
+
// A torn frame neither confirms nor withdraws, but the window must end on a readable empty
|
|
146
|
+
// frame; an unsettled or unreadable end is "unreadable", so no key goes into a dialog.
|
|
147
|
+
export async function settleClaudeComposer(deps: ClaudeComposerReadDeps, expected: string | null): Promise<{ verdict: "empty" | "retained" | "changed" | "unreadable"; pane: string }> {
|
|
148
|
+
const now = deps.now ?? Date.now;
|
|
149
|
+
const sleep = deps.sleep ?? Bun.sleep;
|
|
150
|
+
const deadline = now() + 2 * SUBMIT_SETTLE_MS;
|
|
151
|
+
let emptySince: number | undefined, text: string | null = null, pane = "";
|
|
152
|
+
for (;;) {
|
|
153
|
+
// A full capture budget per read: a read clipped to the window's remainder would end every
|
|
154
|
+
// window on a timed-out, "unreadable" frame.
|
|
155
|
+
try { pane = await deps.readPane(PASTE_RENDER_TIMEOUT_MS); text = claudeComposerText(pane); }
|
|
156
|
+
catch { text = null; }
|
|
157
|
+
if (text === "") emptySince ??= now();
|
|
158
|
+
else if (text !== null) {
|
|
159
|
+
emptySince = undefined;
|
|
160
|
+
if (expected === null) return { verdict: "retained", pane };
|
|
161
|
+
if (text !== expected) return { verdict: "changed", pane };
|
|
162
|
+
}
|
|
163
|
+
if (text === "" && now() - emptySince! >= SUBMIT_SETTLE_MS) return { verdict: "empty", pane };
|
|
164
|
+
if (now() >= deadline) break;
|
|
165
|
+
await sleep(Math.min(POLL_MS, Math.max(0, deadline - now())));
|
|
166
|
+
}
|
|
167
|
+
return { verdict: text !== null && emptySince === undefined ? "retained" : "unreadable", pane };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function readComposer(deps: ClaudeComposerReadDeps, timeoutMs: number): Promise<string | null> {
|
|
171
|
+
try { return claudeComposerText(await deps.readPane(timeoutMs)); }
|
|
172
|
+
catch { return null; }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Claude renders composer placeholders ("Press up to edit queued messages" after a
|
|
176
|
+
// mid-turn submit, "Try …") dim; typed and pasted input never is. Reading a styled
|
|
177
|
+
// capture keeps a placeholder from looking like an unsubmitted prompt (#2664).
|
|
178
|
+
const TERMINAL_ESCAPE = /\x1b\[([0-9;:]*)m|\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
179
|
+
|
|
180
|
+
function sgrDim(params: string, dim: boolean): boolean {
|
|
181
|
+
const codes = params === "" ? ["0"] : params.split(";");
|
|
182
|
+
for (let i = 0; i < codes.length; i++) {
|
|
183
|
+
const code = Number(codes[i]);
|
|
184
|
+
if (code === 0 || code === 22) dim = false;
|
|
185
|
+
else if (code === 2) dim = true;
|
|
186
|
+
else if ((code === 38 || code === 48 || code === 58) && !codes[i]!.includes(":")) i += codes[i + 1] === "5" ? 2 : codes[i + 1] === "2" ? 4 : 0;
|
|
187
|
+
}
|
|
188
|
+
return dim;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Visible text per line plus the undimmed part of it. SGR state carries across tmux lines. */
|
|
192
|
+
function paneLines(pane: string): { text: string; input: string }[] {
|
|
193
|
+
let dim = false;
|
|
194
|
+
return pane.split("\n").map((line) => {
|
|
195
|
+
let text = "", input = "", last = 0;
|
|
196
|
+
for (const match of line.matchAll(TERMINAL_ESCAPE)) {
|
|
197
|
+
const chunk = line.slice(last, match.index);
|
|
198
|
+
text += chunk;
|
|
199
|
+
if (!dim) input += chunk;
|
|
200
|
+
last = match.index + match[0].length;
|
|
201
|
+
if (match[1] !== undefined) dim = sgrDim(match[1], dim);
|
|
202
|
+
}
|
|
203
|
+
const tail = line.slice(last);
|
|
204
|
+
return { text: text + tail, input: dim ? input : input + tail };
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function plainPaneText(pane: string): string {
|
|
209
|
+
return pane.includes("\x1b") ? pane.replace(TERMINAL_ESCAPE, "") : pane;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const COMPOSER_PROMPT = /^\s*❯[\u00a0 ]?(.*)$/;
|
|
213
|
+
|
|
214
|
+
/** null = no recognizable Claude composer, "" = empty composer (or only a dim placeholder). */
|
|
215
|
+
export function claudeComposerText(pane: string): string | null {
|
|
216
|
+
const lines = paneLines(pane);
|
|
217
|
+
for (let i = lines.length - 2; i >= 0; i--) {
|
|
218
|
+
const match = COMPOSER_PROMPT.exec(lines[i]!.text);
|
|
219
|
+
if (!match) continue;
|
|
220
|
+
const border = lines.findIndex((line, index) => index > i && CLAUDE_COMPOSER_RULE_RE.test(line.text));
|
|
221
|
+
if (border < 0) continue;
|
|
222
|
+
const body = lines.slice(i + 1, border);
|
|
223
|
+
const input = [COMPOSER_PROMPT.exec(lines[i]!.input)?.[1] ?? "", ...body.map((line) => line.input)].join("").trim();
|
|
224
|
+
return input === "" ? "" : [match[1], ...body.map((line) => line.text)].join("\n").trim();
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface ClaudeSubmitConfirmationDeps extends ClaudeComposerReadDeps {
|
|
230
|
+
paste: () => Promise<void>;
|
|
231
|
+
sendEnter: () => Promise<void>;
|
|
232
|
+
cancelCopyMode: () => Promise<void>;
|
|
233
|
+
delayBeforeFirstEnterMs: number;
|
|
234
|
+
/** Matching UserPromptSubmit hook; absent on hookless (monitorless) launches. */
|
|
235
|
+
hook?: Pick<ClaudePromptHookExpectation, "arm" | "accepted" | "wait">;
|
|
236
|
+
onRetry?: (attempt: number) => void;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function submitClaudePromptConfirmed(deps: ClaudeSubmitConfirmationDeps): Promise<"hook" | "degraded"> {
|
|
240
|
+
// A failure before our Enter still arms the hook, so a later operator Enter can release the fence.
|
|
241
|
+
try { return await submitConfirmed(deps); }
|
|
242
|
+
catch (error) { deps.hook?.arm(); throw error; }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function submitConfirmed(deps: ClaudeSubmitConfirmationDeps): Promise<"hook" | "degraded"> {
|
|
246
|
+
const now = deps.now ?? Date.now;
|
|
247
|
+
const sleep = deps.sleep ?? Bun.sleep;
|
|
248
|
+
// A fresh read right before the paste; no remembered frame can vouch for an empty composer.
|
|
249
|
+
const before = await readComposer(deps, PASTE_RENDER_TIMEOUT_MS);
|
|
250
|
+
if (before === null) throw new Error("Claude prompt submit failed: input composer is not visible before paste");
|
|
251
|
+
if (before !== "") throw new Error("Claude prompt submit failed: input composer already contains text");
|
|
252
|
+
|
|
253
|
+
await deps.paste();
|
|
254
|
+
const pasteDeadline = now() + PASTE_RENDER_TIMEOUT_MS;
|
|
255
|
+
let pasted: string | null = null;
|
|
256
|
+
while (now() < pasteDeadline) {
|
|
257
|
+
pasted = claudeComposerText(await deps.readPane(Math.max(1, pasteDeadline - now())));
|
|
258
|
+
if (pasted) break;
|
|
259
|
+
await sleep(Math.min(POLL_MS, Math.max(0, pasteDeadline - now())));
|
|
260
|
+
}
|
|
261
|
+
if (!pasted) throw new Error("Claude prompt submit failed: pasted text did not appear in the input composer");
|
|
262
|
+
await sleep(deps.delayBeforeFirstEnterMs);
|
|
263
|
+
|
|
264
|
+
for (let attempt = 1; attempt <= CLAUDE_SUBMIT_MAX_ENTERS; attempt++) {
|
|
265
|
+
if (attempt > 1) {
|
|
266
|
+
deps.onRetry?.(attempt);
|
|
267
|
+
// A user scrolling the web terminal can leave tmux in copy mode. An Enter
|
|
268
|
+
// sent there succeeds at the tmux layer but never reaches Claude.
|
|
269
|
+
await deps.cancelCopyMode();
|
|
270
|
+
}
|
|
271
|
+
deps.hook?.arm(); // only hooks fired from here on can vouch for this Enter
|
|
272
|
+
await deps.sendEnter();
|
|
273
|
+
if (deps.hook) {
|
|
274
|
+
if (await deps.hook.wait(CLAUDE_SUBMIT_HOOK_WAIT_MS)) return "hook";
|
|
275
|
+
const text = await readComposer(deps, PASTE_RENDER_TIMEOUT_MS);
|
|
276
|
+
if (deps.hook.accepted()) return "hook";
|
|
277
|
+
if (text === pasted) continue; // provably still pending: another Enter is safe
|
|
278
|
+
if (await deps.hook.wait(CLAUDE_SUBMIT_LATE_HOOK_WAIT_MS)) return "hook";
|
|
279
|
+
throw new Error(`Claude prompt submit unconfirmed: no UserPromptSubmit hook after Enter and the composer ${text === null ? "is unreadable" : text === "" ? "emptied" : "changed"}`);
|
|
280
|
+
}
|
|
281
|
+
const { verdict } = await settleClaudeComposer(deps, pasted);
|
|
282
|
+
if (verdict === "empty") return "degraded";
|
|
283
|
+
if (verdict === "changed") throw new Error("Claude prompt submit unconfirmed: composer changed after Enter; refusing another key");
|
|
284
|
+
if (verdict === "unreadable") throw new Error("Claude prompt submit unconfirmed: input composer did not settle after Enter");
|
|
285
|
+
}
|
|
286
|
+
throw new Error(`Claude prompt submit failed: pasted text remained after ${CLAUDE_SUBMIT_MAX_ENTERS} Enter attempts`);
|
|
287
|
+
}
|