@humwick/runner 0.0.0-stage → 0.130.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +167 -0
- package/agent-bin/agent-relay-browser +10 -0
- package/agent-bin/humwick +10 -0
- package/hooks/fff-search-redirect/fff-search-redirect.cjs +161 -0
- package/hooks/fff-search-redirect/hooks.json +13 -0
- package/hooks/rg-r-guard/hooks.json +18 -0
- package/hooks/rg-r-guard/rg-r-guard.cjs +144 -0
- package/hooks/web-tools-redirect/hooks.json +13 -0
- package/hooks/web-tools-redirect/web-tools-redirect.cjs +84 -0
- package/node_modules/@humwick/offload/LICENSE +167 -0
- package/node_modules/@humwick/offload/README.md +57 -0
- package/node_modules/@humwick/offload/dist/async-runner.d.ts +18 -0
- package/node_modules/@humwick/offload/dist/async-runner.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/async-runner.js +158 -0
- package/node_modules/@humwick/offload/dist/async-runner.js.map +1 -0
- package/node_modules/@humwick/offload/dist/envelope.d.ts +25 -0
- package/node_modules/@humwick/offload/dist/envelope.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/envelope.js +80 -0
- package/node_modules/@humwick/offload/dist/envelope.js.map +1 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts +10 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js +59 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js.map +1 -0
- package/node_modules/@humwick/offload/dist/index.d.ts +5 -0
- package/node_modules/@humwick/offload/dist/index.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/index.js +5 -0
- package/node_modules/@humwick/offload/dist/index.js.map +1 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts +62 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.js +295 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.js.map +1 -0
- package/node_modules/@humwick/offload/dist/types.d.ts +111 -0
- package/node_modules/@humwick/offload/dist/types.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/types.js +176 -0
- package/node_modules/@humwick/offload/dist/types.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts +23 -0
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-pool.js +435 -0
- package/node_modules/@humwick/offload/dist/worker-pool.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts +2 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.js +27 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker.d.ts +5 -0
- package/node_modules/@humwick/offload/dist/worker.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker.js +1 -0
- package/node_modules/@humwick/offload/dist/worker.js.map +1 -0
- package/node_modules/@humwick/offload/package.json +42 -0
- package/node_modules/@humwick/offload/src/async-runner.ts +170 -0
- package/node_modules/@humwick/offload/src/envelope.ts +92 -0
- package/node_modules/@humwick/offload/src/fixtures/worker-task.ts +49 -0
- package/node_modules/@humwick/offload/src/index.ts +4 -0
- package/node_modules/@humwick/offload/src/subprocess-runner.ts +334 -0
- package/node_modules/@humwick/offload/src/types.ts +235 -0
- package/node_modules/@humwick/offload/src/worker-pool.ts +463 -0
- package/node_modules/@humwick/offload/src/worker-runtime.ts +32 -0
- package/node_modules/@humwick/offload/src/worker.ts +5 -0
- package/package.json +47 -4
- package/plugins/browser/skills/playwright-cli/SKILL.md +53 -0
- package/plugins/claude/.claude-plugin/plugin.json +8 -0
- package/plugins/claude/claude-statusline-probe.cjs +144 -0
- package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +914 -0
- package/plugins/claude/hooks/gate-run-guard.sh +9 -0
- package/plugins/claude/hooks/gate-run-guard.ts +237 -0
- package/plugins/claude/hooks/hooks.json +158 -0
- package/plugins/claude/hooks/kill-guard.provisioned.mjs +7027 -0
- package/plugins/claude/hooks/kill-guard.sh +341 -0
- package/plugins/claude/hooks/kill-guard.ts +337 -0
- package/plugins/claude/hooks/permission-request.sh +22 -0
- package/plugins/claude/hooks/post-compact.sh +10 -0
- package/plugins/claude/hooks/post-tool-use.sh +8 -0
- package/plugins/claude/hooks/pre-compact.sh +12 -0
- package/plugins/claude/hooks/read-only-bash-guard.sh +587 -0
- package/plugins/claude/hooks/relay-status.sh +373 -0
- package/plugins/claude/hooks/session-end.sh +26 -0
- package/plugins/claude/hooks/session-start.sh +31 -0
- package/plugins/claude/hooks/shell-trust.ts +28 -0
- package/plugins/claude/hooks/stop-failure.sh +47 -0
- package/plugins/claude/hooks/stop.sh +48 -0
- package/plugins/claude/hooks/subagent-guard.cjs +26 -0
- package/plugins/claude/hooks/subagent-skill-policy.cjs +160 -0
- package/plugins/claude/hooks/subagent-start.sh +13 -0
- package/plugins/claude/hooks/subagent-stop.sh +19 -0
- package/plugins/claude/hooks/user-prompt-submit.sh +18 -0
- package/plugins/claude/hooks/wait-loop-guard.ts +120 -0
- package/plugins/claude/monitors/monitors.json +7 -0
- package/plugins/claude/monitors/relay-monitor.provisioned.mjs +786 -0
- package/plugins/claude/monitors/relay-monitor.ts +58 -0
- package/plugins/claude/skills/disconnect/SKILL.md +16 -0
- package/plugins/claude/skills/label/SKILL.md +23 -0
- package/plugins/claude/skills/message/SKILL.md +40 -0
- package/plugins/claude/skills/pair/SKILL.md +30 -0
- package/plugins/claude/skills/react/SKILL.md +18 -0
- package/plugins/claude/skills/read-message/SKILL.md +24 -0
- package/plugins/claude/skills/reply/SKILL.md +44 -0
- package/plugins/claude/skills/send-claimable/SKILL.md +39 -0
- package/plugins/claude/skills/slice-first/SKILL.md +32 -0
- package/plugins/claude/skills/slice-first/references/plan-recipe.md +46 -0
- package/plugins/claude/skills/status/SKILL.md +16 -0
- package/plugins/claude/skills/tags/SKILL.md +25 -0
- package/plugins/claude/skills/task/SKILL.md +28 -0
- package/plugins/claude-output-style/.claude-plugin/plugin.json +5 -0
- package/plugins/claude-output-style/output-styles/agent-relay.md +5 -0
- package/plugins/codex/skills/disconnect/SKILL.md +15 -0
- package/plugins/codex/skills/guide/SKILL.md +15 -0
- package/plugins/codex/skills/label/SKILL.md +22 -0
- package/plugins/codex/skills/message/SKILL.md +40 -0
- package/plugins/codex/skills/pair/SKILL.md +29 -0
- package/plugins/codex/skills/react/SKILL.md +17 -0
- package/plugins/codex/skills/read-message/SKILL.md +23 -0
- package/plugins/codex/skills/reply/SKILL.md +44 -0
- package/plugins/codex/skills/send-claimable/SKILL.md +39 -0
- package/plugins/codex/skills/slice-first/SKILL.md +32 -0
- package/plugins/codex/skills/slice-first/references/plan-recipe.md +46 -0
- package/plugins/codex/skills/status/SKILL.md +15 -0
- package/plugins/codex/skills/tags/SKILL.md +24 -0
- package/plugins/codex/skills/task/SKILL.md +28 -0
- package/src/adapter.ts +1061 -0
- package/src/adapters/__fixtures__/codex-app-server.ts +47 -0
- package/src/adapters/claude-api.ts +243 -0
- package/src/adapters/claude-context-command.ts +16 -0
- package/src/adapters/claude-credentials.ts +22 -0
- package/src/adapters/claude-delivery-lanes.ts +28 -0
- package/src/adapters/claude-delivery.ts +166 -0
- package/src/adapters/claude-effort.ts +94 -0
- package/src/adapters/claude-exit.ts +290 -0
- package/src/adapters/claude-headless-mirror.ts +88 -0
- package/src/adapters/claude-headless-refusal.ts +51 -0
- package/src/adapters/claude-headless.ts +617 -0
- package/src/adapters/claude-monitor-delivery-plan.ts +317 -0
- package/src/adapters/claude-permission.ts +243 -0
- package/src/adapters/claude-prompt-gates.ts +269 -0
- package/src/adapters/claude-prompt-hook.ts +145 -0
- package/src/adapters/claude-session-capture.ts +293 -0
- package/src/adapters/claude-session-probe.ts +414 -0
- package/src/adapters/claude-shutdown.ts +190 -0
- package/src/adapters/claude-status-detectors.ts +426 -0
- package/src/adapters/claude-subagents.ts +93 -0
- package/src/adapters/claude-submit-confirmation.ts +287 -0
- package/src/adapters/claude-tmux.ts +548 -0
- package/src/adapters/claude-token-usage-capture.ts +253 -0
- package/src/adapters/claude-tool-policy.ts +165 -0
- package/src/adapters/claude-transcript-tail.ts +193 -0
- package/src/adapters/claude-transcript.ts +812 -0
- package/src/adapters/claude.ts +1097 -0
- package/src/adapters/codex-agent-message-capture.ts +189 -0
- package/src/adapters/codex-app-server.ts +70 -0
- package/src/adapters/codex-client.ts +362 -0
- package/src/adapters/codex-effective-config.ts +87 -0
- package/src/adapters/codex-launch-config.ts +153 -0
- package/src/adapters/codex-rollout-context.ts +272 -0
- package/src/adapters/codex-skill-discovery.ts +18 -0
- package/src/adapters/codex-token-usage-capture.ts +163 -0
- package/src/adapters/codex-tool-output.ts +53 -0
- package/src/adapters/codex-turn-outcome.ts +27 -0
- package/src/adapters/codex-version.ts +64 -0
- package/src/adapters/codex.ts +1442 -0
- package/src/adapters/incremental-file.ts +110 -0
- package/src/adapters/opencode-api.ts +71 -0
- package/src/adapters/opencode-context.ts +277 -0
- package/src/adapters/opencode-model-registry.ts +126 -0
- package/src/adapters/opencode-provider.ts +169 -0
- package/src/adapters/opencode-server-retry.ts +50 -0
- package/src/adapters/opencode-soft-steer.ts +302 -0
- package/src/adapters/opencode-spawn.ts +145 -0
- package/src/adapters/opencode-subscribe.ts +62 -0
- package/src/adapters/opencode-token-usage-capture.ts +187 -0
- package/src/adapters/opencode-tool-events.ts +302 -0
- package/src/adapters/opencode-tools-config.ts +17 -0
- package/src/adapters/opencode.ts +2264 -0
- package/src/adapters/project-shell.ts +80 -0
- package/src/adapters/provider-launch-config.ts +21 -0
- package/src/adapters/provider-quota-harvest.ts +127 -0
- package/src/adapters/service-access.ts +153 -0
- package/src/adapters/tmux-offload.ts +207 -0
- package/src/adapters/tool-summary.ts +214 -0
- package/src/agent-redact.ts +75 -0
- package/src/agent-runner/commands/dispatch.ts +185 -0
- package/src/agent-runner/commands/lifecycle.ts +84 -0
- package/src/agent-runner/commands/prompt.ts +284 -0
- package/src/agent-runner/credentials.ts +208 -0
- package/src/agent-runner/http-liveness.ts +85 -0
- package/src/agent-runner/lifecycle-milestones.ts +207 -0
- package/src/agent-runner/lifecycle-phase.ts +80 -0
- package/src/agent-runner/registration.ts +163 -0
- package/src/agent-runner/runner-context.ts +158 -0
- package/src/agent-runner/session-mirror.ts +278 -0
- package/src/agent-runner/task-claims.ts +116 -0
- package/src/agent-runner/token-usage-coverage.ts +54 -0
- package/src/agent-runner/turn-report.ts +269 -0
- package/src/api-budget-accounting.ts +91 -0
- package/src/area-memory.ts +255 -0
- package/src/artifact-path-materialize.ts +52 -0
- package/src/async-guard.ts +35 -0
- package/src/attachment-cache-sweep.ts +31 -0
- package/src/attachment-cache.ts +195 -0
- package/src/background-script-reconcile.ts +106 -0
- package/src/bounded-log.ts +59 -0
- package/src/bounded-spawn-sync.ts +18 -0
- package/src/busy-reconciler.ts +282 -0
- package/src/claim-tracker.ts +140 -0
- package/src/claude-settings.ts +167 -0
- package/src/claude-workspace-trust.ts +289 -0
- package/src/config.ts +547 -0
- package/src/context-boundary.ts +101 -0
- package/src/context-command.ts +151 -0
- package/src/context-probe-state.ts +6 -0
- package/src/continuation-archive.ts +179 -0
- package/src/control-server.ts +730 -0
- package/src/delivery-policy.ts +9 -0
- package/src/delivery-result.ts +24 -0
- package/src/effort-command.ts +125 -0
- package/src/empty-turn.ts +816 -0
- package/src/free-port.ts +25 -0
- package/src/fs-offload.ts +26 -0
- package/src/gate-run-config.ts +35 -0
- package/src/gate-run-lease-probe.ts +38 -0
- package/src/host-asset-plan.ts +450 -0
- package/src/host-claude-config-worker.ts +34 -0
- package/src/host-sources.ts +40 -0
- package/src/inbox-carrier.ts +7 -0
- package/src/index.ts +386 -0
- package/src/launch-assembly.ts +955 -0
- package/src/liveness.ts +105 -0
- package/src/logger.ts +108 -0
- package/src/mcp-outbox.ts +204 -0
- package/src/mcp-preflight.ts +86 -0
- package/src/mcp-source-selection.ts +54 -0
- package/src/mcp-tool-deferral.ts +35 -0
- package/src/mcp-vault-env.ts +25 -0
- package/src/message-body-config/config.ts +16 -0
- package/src/message-body-config.ts +1 -0
- package/src/mid-turn-delivery.ts +43 -0
- package/src/native-memory-lever.ts +329 -0
- package/src/native-self-resume.ts +244 -0
- package/src/outbox.ts +453 -0
- package/src/park-post.ts +17 -0
- package/src/pending-permission-store.ts +148 -0
- package/src/process-meta.ts +9 -0
- package/src/profile-home.ts +419 -0
- package/src/profile-projection.ts +411 -0
- package/src/project-hooks.ts +65 -0
- package/src/project-instructions.ts +119 -0
- package/src/provider-double.ts +59 -0
- package/src/provider-hold.ts +228 -0
- package/src/provider-home-marker.ts +3 -0
- package/src/provider-home-tombstone.ts +4 -0
- package/src/provider-input-queue.ts +134 -0
- package/src/provider-integration.ts +93 -0
- package/src/provider-processes.ts +17 -0
- package/src/provider-submit-failure.ts +15 -0
- package/src/providers.ts +35 -0
- package/src/provisioning.ts +703 -0
- package/src/question-decision.ts +18 -0
- package/src/quota.ts +22 -0
- package/src/rate-limit.ts +351 -0
- package/src/refused-prompt-store.ts +36 -0
- package/src/relay-injection-events.ts +161 -0
- package/src/relay-instructions.ts +107 -0
- package/src/relay-mcp-proxy.long-turn-e2e.ts +183 -0
- package/src/relay-mcp-proxy.ts +704 -0
- package/src/relay-mcp.ts +583 -0
- package/src/reply-obligation-cache.ts +211 -0
- package/src/response-capture-report.ts +111 -0
- package/src/response-routing.ts +6 -0
- package/src/runner-core.ts +2955 -0
- package/src/runner-exit.ts +12 -0
- package/src/runner-helpers.ts +582 -0
- package/src/runner-insights.ts +105 -0
- package/src/runner-mcp-proxy.ts +46 -0
- package/src/runner-spawn-config.ts +68 -0
- package/src/runner-stale-scratch-sweep.ts +21 -0
- package/src/runner.ts +9 -0
- package/src/runtime-token-file.ts +45 -0
- package/src/runtime-token-remint.ts +56 -0
- package/src/service-access.ts +88 -0
- package/src/session-destroy.ts +27 -0
- package/src/session-insights.ts +118 -0
- package/src/session-scratch.ts +296 -0
- package/src/spawn-input.ts +147 -0
- package/src/startup-config-fetch.ts +48 -0
- package/src/subreaper.ts +113 -0
- package/src/template-resolver.ts +29 -0
- package/src/test-run-dedup.ts +435 -0
- package/src/transcript-context.ts +99 -0
- package/src/transport-errors.ts +48 -0
- package/src/turn-opener.ts +55 -0
- package/src/turn-reconcile-event.ts +88 -0
- package/src/vault-proxy-hop.ts +99 -0
- package/src/version.ts +44 -0
- package/src/web-tool-call-proxy.ts +128 -0
- package/src/web-tools.ts +59 -0
- package/README.md +0 -3
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { errMessage, type ContextState, type MessageSessionMeta, type HumwickHttpClient, type SendMessageInput } from "@humwick/sdk";
|
|
2
|
+
import type { ProviderAdapter, ProviderSessionEvent, ProviderSessionEventBatch, ProviderSessionTurnContext } from "../adapter";
|
|
3
|
+
import { Outbox, type OutboxRecord } from "../outbox";
|
|
4
|
+
import { deliverRunnerOutboxEvent, emitSessionPoisonNotice } from "../mcp-outbox";
|
|
5
|
+
import { transcriptDerivedContext, type TranscriptContextCache } from "../transcript-context";
|
|
6
|
+
|
|
7
|
+
interface SessionMirrorDeps {
|
|
8
|
+
agentId: string;
|
|
9
|
+
provider: string;
|
|
10
|
+
instanceId: string;
|
|
11
|
+
startedAt: number;
|
|
12
|
+
relayUrl: string;
|
|
13
|
+
outboxDir: string | undefined;
|
|
14
|
+
http: HumwickHttpClient;
|
|
15
|
+
adapter: ProviderAdapter;
|
|
16
|
+
getToken(): string | undefined;
|
|
17
|
+
updateOutboxPayload(seq: number, payload: unknown): void;
|
|
18
|
+
recoverRuntimeTokenAfterAuthFailure(source: string): void;
|
|
19
|
+
sessionLog(message: string): void;
|
|
20
|
+
sessionTurnContext(): ProviderSessionTurnContext;
|
|
21
|
+
lastTranscriptPath(): string | undefined;
|
|
22
|
+
currentTurnId(): string | undefined;
|
|
23
|
+
reasoningCapture(): boolean | undefined;
|
|
24
|
+
isRunnerInjectedPrompt(text: string): boolean;
|
|
25
|
+
publishProviderResponse(event: ProviderSessionEvent, body: string, turnId: string | undefined): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const SESSION_MIRROR_BATCH_MAX = 100;
|
|
29
|
+
|
|
30
|
+
export class SessionMirror {
|
|
31
|
+
// #332: a fast-lane outbox for display-only session-mirror trace events. Routing them through
|
|
32
|
+
// their own FIFO queue stops one transient trace failure from head-of-line blocking real-message
|
|
33
|
+
// delivery (and vice versa); its backoff is capped low since a stale live-mirror frame is cheap.
|
|
34
|
+
private readonly sessionOutbox: Outbox;
|
|
35
|
+
/** In-flight asynchronous turn captures — the settle waits these out before judging a turn empty. */
|
|
36
|
+
private turnCapturesInFlight = 0;
|
|
37
|
+
private sessionBatchSeq = 0;
|
|
38
|
+
// #1512: mtime-keyed cache of the last transcript-derived context usage, so the
|
|
39
|
+
// heartbeat's fallback parse doesn't re-read a large transcript that hasn't changed.
|
|
40
|
+
private transcriptContextCache?: TranscriptContextCache;
|
|
41
|
+
// #1235 — true while a monitorless (hookless) provider turn is being mirrored natively:
|
|
42
|
+
// the adapter supplied a transcript path on the provider-turn busy edge, so the runner
|
|
43
|
+
// drives the reasoning tail itself (start on busy, final-capture on idle) instead of via
|
|
44
|
+
// Stop/UserPromptSubmit hooks the lean/isolated worker never installs.
|
|
45
|
+
private nativeSessionCaptureActive = false;
|
|
46
|
+
|
|
47
|
+
constructor(private readonly deps: SessionMirrorDeps) {
|
|
48
|
+
this.sessionOutbox = new Outbox({
|
|
49
|
+
agentId: `${this.deps.agentId}-session`,
|
|
50
|
+
dir: this.deps.outboxDir,
|
|
51
|
+
send: (record) => this.deliverOutboxEvent(record),
|
|
52
|
+
// Ephemeral trace frames: cap the per-row stall low. #2433 — transient (connect-class /
|
|
53
|
+
// 5xx / 429) failures never burn this budget; it now only bounds unknown-class failures.
|
|
54
|
+
maxBackoffMs: 5_000,
|
|
55
|
+
maxAttempts: 12,
|
|
56
|
+
// #2433 — a poisoned session row is REAL loss (report-up chat rides this queue), so
|
|
57
|
+
// surface it in-session via the mirror-drop notice seam instead of only a FATAL host log.
|
|
58
|
+
onPoison: (record, reason) => emitSessionPoisonNotice({ record, http: this.deps.http }, reason),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
enqueueResponseReport(record: Parameters<Outbox["enqueue"]>[0]): void { this.sessionOutbox.enqueue(record); }
|
|
63
|
+
|
|
64
|
+
start(): void { this.sessionOutbox.start(); }
|
|
65
|
+
close(): void { this.sessionOutbox.close(); }
|
|
66
|
+
flushSessionEvents(timeoutMs: number): Promise<boolean> { return this.sessionOutbox.flush(timeoutMs); }
|
|
67
|
+
pendingSessionEventCount(): number { return this.sessionOutbox.pendingCount(); }
|
|
68
|
+
capturesInFlight(): boolean { return this.turnCapturesInFlight > 0; }
|
|
69
|
+
beginTurnCapture(): void { this.turnCapturesInFlight += 1; }
|
|
70
|
+
endTurnCapture(): void { this.turnCapturesInFlight -= 1; }
|
|
71
|
+
nativeCaptureActive(): boolean { return this.nativeSessionCaptureActive; }
|
|
72
|
+
clearNativeSessionCapture(): void { this.nativeSessionCaptureActive = false; }
|
|
73
|
+
|
|
74
|
+
transcriptContext(tokensMax: number | undefined): ContextState | undefined {
|
|
75
|
+
const derived = transcriptDerivedContext({ transcriptPath: this.deps.lastTranscriptPath(), derive: this.deps.adapter.contextUsageFromTranscript, tokensMax, cache: this.transcriptContextCache, now: Date.now() });
|
|
76
|
+
this.transcriptContextCache = derived.cache;
|
|
77
|
+
return derived.context;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Post one session-mirror event (prompt echo, assistant response, reasoning or
|
|
81
|
+
// tool step) as a `kind: "session"` relay message tagged with payload.session so
|
|
82
|
+
// the dashboard can render the live provider session faithfully. Display-only:
|
|
83
|
+
// session messages are never delivered back into a provider.
|
|
84
|
+
publishSessionEvent(input: { from: string; to: string; body: string; session: MessageSessionMeta; replyTo?: number; occurredAt?: number }): void {
|
|
85
|
+
// Durable, ordered, timestamped (#196/#1115): the actual POST happens in
|
|
86
|
+
// deliverOutboxEvent, retried until it lands. An explicit occurredAt preserves the
|
|
87
|
+
// provider-native event time; otherwise the outbox stamps enqueue time. Routed through
|
|
88
|
+
// the fast-lane sessionOutbox (#332) so a transient trace failure can't head-of-line
|
|
89
|
+
// block real messages.
|
|
90
|
+
// A stepId-bearing step (tool running to completed, streamed reasoning/response) uses a
|
|
91
|
+
// STABLE idempotency key so the server upserts the row in place instead of appending a dup.
|
|
92
|
+
const stepId = input.session.stepId;
|
|
93
|
+
this.sessionOutbox.enqueue({
|
|
94
|
+
kind: "session-message",
|
|
95
|
+
...(input.occurredAt ? { occurredAt: input.occurredAt } : {}),
|
|
96
|
+
...(stepId ? { idempotencyKey: `session-step:${input.from}:${input.session.turnId ?? ""}:${stepId}` } : {}),
|
|
97
|
+
payload: {
|
|
98
|
+
from: input.from,
|
|
99
|
+
to: input.to,
|
|
100
|
+
...(input.replyTo ? { replyTo: input.replyTo } : {}),
|
|
101
|
+
kind: "session",
|
|
102
|
+
body: input.body,
|
|
103
|
+
payload: { session: { provider: this.deps.provider, ...input.session } },
|
|
104
|
+
} satisfies SendMessageInput,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
publishSessionEventsBatch(inputs: Array<{ from: string; to: string; body: string; session: MessageSessionMeta; replyTo?: number; occurredAt?: number }>): void {
|
|
109
|
+
for (let i = 0; i < inputs.length; i += SESSION_MIRROR_BATCH_MAX) {
|
|
110
|
+
const chunk = inputs.slice(i, i + SESSION_MIRROR_BATCH_MAX);
|
|
111
|
+
if (chunk.length === 1) {
|
|
112
|
+
this.publishSessionEvent(chunk[0]!);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const messages = chunk.map((input) => {
|
|
116
|
+
const stepId = input.session.stepId;
|
|
117
|
+
return {
|
|
118
|
+
from: input.from,
|
|
119
|
+
to: input.to,
|
|
120
|
+
...(input.replyTo ? { replyTo: input.replyTo } : {}),
|
|
121
|
+
kind: "session",
|
|
122
|
+
body: input.body,
|
|
123
|
+
...(input.occurredAt ? { occurredAt: input.occurredAt } : {}),
|
|
124
|
+
...(stepId ? { idempotencyKey: `session-step:${input.from}:${input.session.turnId ?? ""}:${stepId}` } : {}),
|
|
125
|
+
payload: { session: { provider: this.deps.provider, ...input.session } },
|
|
126
|
+
} satisfies SendMessageInput;
|
|
127
|
+
});
|
|
128
|
+
this.sessionOutbox.enqueue({
|
|
129
|
+
kind: "session-message-batch",
|
|
130
|
+
idempotencyKey: `session-batch:${this.deps.agentId}:${this.deps.instanceId}:${this.deps.startedAt}:${++this.sessionBatchSeq}:${messages.length}`,
|
|
131
|
+
payload: { messages },
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Map queued records to HTTP calls. Throw to retry, return to ack/delete.
|
|
137
|
+
async deliverOutboxEvent(record: OutboxRecord): Promise<void> {
|
|
138
|
+
await deliverRunnerOutboxEvent({
|
|
139
|
+
record,
|
|
140
|
+
http: this.deps.http,
|
|
141
|
+
relayUrl: this.deps.relayUrl,
|
|
142
|
+
getToken: () => this.deps.getToken(),
|
|
143
|
+
updatePayload: (seq, payload) => this.deps.updateOutboxPayload(seq, payload),
|
|
144
|
+
sessionLog: (message) => this.deps.sessionLog(message),
|
|
145
|
+
recoverRuntimeTokenAfterAuthFailure: (source) => this.deps.recoverRuntimeTokenAfterAuthFailure(source),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Route a provider-emitted session event into the chat mirror.
|
|
150
|
+
// Mirrors provider session events into chat. Prompts are echoed with dedup, and
|
|
151
|
+
// response routing clears only the obligations the adapter says its capture can satisfy.
|
|
152
|
+
async publishProviderSessionEvent(event: ProviderSessionEvent): Promise<void> {
|
|
153
|
+
const body = event.body.trim();
|
|
154
|
+
if (!body) return;
|
|
155
|
+
const turnId = event.turnId ?? this.deps.currentTurnId();
|
|
156
|
+
if (event.type === "prompt") {
|
|
157
|
+
if (this.deps.isRunnerInjectedPrompt(body)) return;
|
|
158
|
+
await this.publishSessionEvent({
|
|
159
|
+
from: "user",
|
|
160
|
+
to: this.deps.agentId,
|
|
161
|
+
body,
|
|
162
|
+
session: { type: "prompt", origin: event.origin ?? "terminal", ...(turnId ? { turnId } : {}) },
|
|
163
|
+
...(event.occurredAt ? { occurredAt: event.occurredAt } : {}),
|
|
164
|
+
});
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (event.type === "response" && event.final === false) {
|
|
168
|
+
await this.publishSessionEvent({
|
|
169
|
+
from: this.deps.agentId,
|
|
170
|
+
to: "user",
|
|
171
|
+
body,
|
|
172
|
+
...(event.occurredAt ? { occurredAt: event.occurredAt } : {}),
|
|
173
|
+
session: { type: "response", origin: event.origin ?? "provider", ...(turnId ? { turnId } : {}) },
|
|
174
|
+
});
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (event.type === "response") {
|
|
178
|
+
return this.deps.publishProviderResponse(event, body, turnId);
|
|
179
|
+
}
|
|
180
|
+
if (this.deps.reasoningCapture() === false) return;
|
|
181
|
+
this.publishSessionEvent(this.providerStepSessionInput(event, body, turnId));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async publishProviderSessionEvents(events: ProviderSessionEventBatch): Promise<void> {
|
|
185
|
+
const inputs: Array<{ from: string; to: string; body: string; session: MessageSessionMeta; replyTo?: number; occurredAt?: number }> = [];
|
|
186
|
+
for (const event of events) {
|
|
187
|
+
const body = event.body.trim();
|
|
188
|
+
if (!body) continue;
|
|
189
|
+
if (event.type === "prompt" || event.type === "response") {
|
|
190
|
+
await this.publishProviderSessionEvent(event);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (this.deps.reasoningCapture() === false) continue;
|
|
194
|
+
inputs.push(this.providerStepSessionInput(event, body, event.turnId ?? this.deps.currentTurnId()));
|
|
195
|
+
}
|
|
196
|
+
if (inputs.length) this.publishSessionEventsBatch(inputs);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private providerStepSessionInput(event: ProviderSessionEvent, body: string, turnId?: string): { from: string; to: string; body: string; session: MessageSessionMeta; occurredAt?: number } {
|
|
200
|
+
return {
|
|
201
|
+
from: this.deps.agentId,
|
|
202
|
+
to: "user",
|
|
203
|
+
body,
|
|
204
|
+
...(event.occurredAt ? { occurredAt: event.occurredAt } : {}),
|
|
205
|
+
session: {
|
|
206
|
+
type: event.type,
|
|
207
|
+
origin: event.origin ?? "provider",
|
|
208
|
+
...(turnId ? { turnId } : {}),
|
|
209
|
+
...(event.label ? { label: event.label } : {}),
|
|
210
|
+
...(event.category ? { toolCategory: event.category } : {}),
|
|
211
|
+
...(event.status ? { status: event.status } : {}),
|
|
212
|
+
...(event.streaming !== undefined ? { streaming: event.streaming } : {}),
|
|
213
|
+
...(event.stepId ? { stepId: event.stepId } : {}),
|
|
214
|
+
...(event.final ? { final: true } : {}),
|
|
215
|
+
...(event.params !== undefined ? { params: event.params } : {}),
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
stopReasoningTail(): void {
|
|
221
|
+
this.deps.adapter.stopSessionTrace?.();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// #1235 — start the transcript reasoning tail for a hookless (monitorless) turn. Reuses the
|
|
225
|
+
// exact adapter capture path the UserPromptSubmit hook drives for hooked agents; only the
|
|
226
|
+
// trigger differs (native turn-start vs hook). A missing/old transcript makes the tail a
|
|
227
|
+
// graceful no-op (poll finds nothing), so this can never crash a turn.
|
|
228
|
+
async startNativeSessionCapture(transcriptPath: string): Promise<void> {
|
|
229
|
+
this.nativeSessionCaptureActive = true;
|
|
230
|
+
try {
|
|
231
|
+
await this.deps.adapter.handleUserPrompt?.({ prompt: "", transcriptPath }, this.deps.sessionTurnContext());
|
|
232
|
+
} catch (error) {
|
|
233
|
+
this.deps.sessionLog(`native session capture start failed: ${errMessage(error)}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// #1235 — capture the final assistant response for a hookless turn (the Stop hook's job for
|
|
238
|
+
// hooked agents), then tear down the tail. captureSessionTurn drains the tail, reads the
|
|
239
|
+
// closing turn text from the transcript, and emits the final `response` session event.
|
|
240
|
+
async finishNativeSessionCapture(transcriptPath: string | undefined, ctx: ProviderSessionTurnContext): Promise<void> {
|
|
241
|
+
// #2389 — this publishes the closing `response` event AFTER the idle edge; the empty-turn
|
|
242
|
+
// settle must not judge the turn until it has finished.
|
|
243
|
+
this.turnCapturesInFlight += 1;
|
|
244
|
+
try {
|
|
245
|
+
if (transcriptPath) {
|
|
246
|
+
await this.deps.adapter.captureSessionTurn?.({ transcriptPath, ...(ctx.turnId ? { promptId: ctx.turnId } : {}) }, ctx);
|
|
247
|
+
}
|
|
248
|
+
} catch (error) {
|
|
249
|
+
this.deps.sessionLog(`native session capture finish failed: ${errMessage(error)}`);
|
|
250
|
+
} finally {
|
|
251
|
+
this.turnCapturesInFlight -= 1;
|
|
252
|
+
this.stopReasoningTail();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// #2491 — the ONE terminal-settle seam for both capture paths: a monitorless (native) turn
|
|
257
|
+
// discovers its transcript on the provider-turn busy/idle edge; a hooked turn (Stop/
|
|
258
|
+
// UserPromptSubmit) discovers it via publishSessionTurn/handleUserPrompt. Both write the
|
|
259
|
+
// same `lastTranscriptPath`, so gating on that path alone (previously also required
|
|
260
|
+
// `nativeSessionCaptureSeen`, set ONLY by the native busy/idle edge) is what makes this
|
|
261
|
+
// fire for hooked agents too — without it every hooked turn's one-shot Stop-hook capture
|
|
262
|
+
// pass raced Claude's own JSONL flush with nothing to settle it.
|
|
263
|
+
// #2484 round 2 review (MED-1) — the transcript-presence bail used to live HERE, so an
|
|
264
|
+
// adapter with no transcript file to tail (OpenCode's capture is event-sourced off the
|
|
265
|
+
// message.updated SSE door, not a JSONL tail) silently never reached its
|
|
266
|
+
// `captureFinalTokenUsage` hook on any of this seam's three call sites (stop(),
|
|
267
|
+
// restartUnexpectedProviderExit, runPreSessionDestroy) — a Claude-shaped precondition baked
|
|
268
|
+
// into the shared seam. The bail is now adapter-owned: a transcript-tailing adapter checks
|
|
269
|
+
// `transcriptPath` itself (see claude.ts), an event-sourced one ignores it.
|
|
270
|
+
async settleTerminalTokenUsage(transcriptPath: string | undefined = this.deps.lastTranscriptPath()): Promise<void> {
|
|
271
|
+
try {
|
|
272
|
+
await this.deps.adapter.captureFinalTokenUsage?.(transcriptPath, this.deps.sessionTurnContext());
|
|
273
|
+
} catch (error) {
|
|
274
|
+
this.deps.sessionLog(`native final token-usage capture failed: ${errMessage(error)}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { Message, HumwickHttpClient, TaskStatusInput } from "@humwick/sdk";
|
|
2
|
+
import type { Outbox, OutboxRecord } from "../outbox";
|
|
3
|
+
import type { ClaimTracker } from "../claim-tracker";
|
|
4
|
+
import { taskIdFromMessage } from "../runner-helpers";
|
|
5
|
+
|
|
6
|
+
interface ActiveTaskClaim {
|
|
7
|
+
messageId: number;
|
|
8
|
+
taskId: number;
|
|
9
|
+
observedProviderBusy: boolean;
|
|
10
|
+
completionQueued?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const CLAIM_RENEW_INTERVAL_MS = 5 * 60 * 1000;
|
|
14
|
+
|
|
15
|
+
interface TaskClaimsDeps {
|
|
16
|
+
http: Pick<HumwickHttpClient, "renewMessageClaim">;
|
|
17
|
+
agentId: string;
|
|
18
|
+
startClaim: ClaimTracker["startClaim"];
|
|
19
|
+
finishClaim: ClaimTracker["finishClaim"];
|
|
20
|
+
isDrainingMessage(messageId: number): boolean;
|
|
21
|
+
updateTaskStatus(taskId: number, input: TaskStatusInput): Promise<void>;
|
|
22
|
+
publishStatus(): void;
|
|
23
|
+
enqueueCompletion: Outbox["enqueue"];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class TaskClaims {
|
|
27
|
+
private claimRenewTimer?: Timer;
|
|
28
|
+
private readonly activeTaskClaims = new Map<number, ActiveTaskClaim>();
|
|
29
|
+
// #2619 — claimed Task messages whose provider-turn idle arrived while their own batch was still
|
|
30
|
+
// delivering; completed at settle only if the provider accepted them (see completeObservedTaskClaims).
|
|
31
|
+
private readonly completionDeferredTaskClaims = new Set<number>();
|
|
32
|
+
|
|
33
|
+
constructor(private readonly deps: TaskClaimsDeps) {}
|
|
34
|
+
|
|
35
|
+
stop(): void {
|
|
36
|
+
if (this.claimRenewTimer) clearInterval(this.claimRenewTimer);
|
|
37
|
+
this.claimRenewTimer = undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
noteClaim(messageId: number, taskId: number, providerAlreadyBusy: boolean, claimExpiresAt?: number): void {
|
|
41
|
+
this.activeTaskClaims.set(messageId, { messageId, taskId, observedProviderBusy: providerAlreadyBusy });
|
|
42
|
+
this.deps.startClaim("task", String(taskId), claimExpiresAt);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
observeProviderBusy(): void {
|
|
46
|
+
for (const claim of this.activeTaskClaims.values()) claim.observedProviderBusy = true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
hasDeferredCompletion(): boolean { return this.completionDeferredTaskClaims.size > 0; }
|
|
50
|
+
forgetDeferredCompletion(messageId: number): void { this.completionDeferredTaskClaims.delete(messageId); }
|
|
51
|
+
|
|
52
|
+
start(): void {
|
|
53
|
+
if (this.claimRenewTimer) return;
|
|
54
|
+
this.claimRenewTimer = setInterval(() => {
|
|
55
|
+
void this.renewActiveTaskClaims();
|
|
56
|
+
}, CLAIM_RENEW_INTERVAL_MS);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private async renewActiveTaskClaims(): Promise<void> {
|
|
60
|
+
for (const claim of this.activeTaskClaims.values()) {
|
|
61
|
+
const renewed = await this.deps.http.renewMessageClaim(claim.messageId, this.deps.agentId).catch(() => ({ ok: false, claimExpiresAt: undefined }));
|
|
62
|
+
if (renewed.ok) {
|
|
63
|
+
this.deps.startClaim("message", String(claim.messageId), renewed.claimExpiresAt);
|
|
64
|
+
this.deps.startClaim("task", String(claim.taskId), renewed.claimExpiresAt);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
this.deps.publishStatus();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async completeObservedTaskClaims(acceptedIds?: ReadonlySet<number>): Promise<void> {
|
|
71
|
+
const completed = [...this.activeTaskClaims.values()].filter((claim) => {
|
|
72
|
+
// Settle-time pass: complete only the deferred claims whose message the provider ACCEPTED.
|
|
73
|
+
if (claim.completionQueued) return false;
|
|
74
|
+
if (acceptedIds) return acceptedIds.has(claim.messageId) && this.completionDeferredTaskClaims.delete(claim.messageId);
|
|
75
|
+
if (!claim.observedProviderBusy) return false;
|
|
76
|
+
// #2619 — a claim still inside its own delivery batch: an idle here may belong to a SIBLING
|
|
77
|
+
// message's turn, and this one may yet be refused. Whether it was accepted is only known at
|
|
78
|
+
// settle (settleMessageDelivery), so defer instead of marking its Task done now.
|
|
79
|
+
if (this.deps.isDrainingMessage(claim.messageId)) {
|
|
80
|
+
this.completionDeferredTaskClaims.add(claim.messageId);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
84
|
+
});
|
|
85
|
+
for (const claim of completed) {
|
|
86
|
+
// Persist the observed completion before yielding; read/delivery state alone cannot prove it.
|
|
87
|
+
this.deps.enqueueCompletion({
|
|
88
|
+
kind: "task-completion",
|
|
89
|
+
payload: { taskId: claim.taskId, messageId: claim.messageId },
|
|
90
|
+
});
|
|
91
|
+
claim.completionQueued = true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async deliverCompletion(record: OutboxRecord): Promise<void> {
|
|
96
|
+
const { taskId, messageId } = record.payload as { taskId: number; messageId: number };
|
|
97
|
+
await this.deps.updateTaskStatus(taskId, {
|
|
98
|
+
status: "done",
|
|
99
|
+
agentId: this.deps.agentId,
|
|
100
|
+
body: `Runner observed provider completion for message #${messageId}`,
|
|
101
|
+
metadata: { messageId, completedBy: "runner" },
|
|
102
|
+
});
|
|
103
|
+
this.activeTaskClaims.delete(messageId);
|
|
104
|
+
this.deps.finishClaim("message", String(messageId));
|
|
105
|
+
this.deps.finishClaim("task", String(taskId));
|
|
106
|
+
this.deps.publishStatus();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
clearActiveClaim(message: Message): void {
|
|
110
|
+
const taskId = taskIdFromMessage(message);
|
|
111
|
+
this.deps.finishClaim("message", String(message.id));
|
|
112
|
+
if (!taskId) return;
|
|
113
|
+
this.activeTaskClaims.delete(message.id);
|
|
114
|
+
this.deps.finishClaim("task", String(taskId));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// #3351 — the runner's view of provider usage it has observed but Relay has not stored yet.
|
|
2
|
+
// Every adapter reports through this one seam, so a report that is in flight, or that failed
|
|
3
|
+
// and was not yet replayed under the same dedupe key, is published as unreported spend
|
|
4
|
+
// instead of looking like confirmed zero usage.
|
|
5
|
+
import type { ProviderTokenUsageRecord } from "../adapter";
|
|
6
|
+
|
|
7
|
+
export class TokenUsageReportCoverage {
|
|
8
|
+
private readonly inFlight = new Map<string, number>();
|
|
9
|
+
private readonly failed = new Set<string>();
|
|
10
|
+
private readonly captures = new Map<string, object>();
|
|
11
|
+
|
|
12
|
+
constructor(private readonly onChange: () => void) {}
|
|
13
|
+
|
|
14
|
+
/** Distinct reports not yet acknowledged by Relay. */
|
|
15
|
+
get unreported(): number {
|
|
16
|
+
let count = this.failed.size + this.captures.size;
|
|
17
|
+
for (const key of this.inFlight.keys()) if (!this.failed.has(key)) count++;
|
|
18
|
+
return count;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async capture(key: string, read: () => Promise<number>, pending = true): Promise<void> {
|
|
22
|
+
if (!pending && !this.captures.has(key)) { await read(); return; }
|
|
23
|
+
const generation = {};
|
|
24
|
+
const before = this.unreported;
|
|
25
|
+
this.captures.set(key, generation);
|
|
26
|
+
if (this.unreported !== before) this.onChange();
|
|
27
|
+
const reported = await read();
|
|
28
|
+
if (reported > 0 && this.captures.get(key) === generation) {
|
|
29
|
+
this.captures.delete(key);
|
|
30
|
+
this.onChange();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async report(record: ProviderTokenUsageRecord, send: (record: ProviderTokenUsageRecord) => Promise<unknown>): Promise<unknown> {
|
|
35
|
+
const key = record.dedupeKey;
|
|
36
|
+
const before = this.unreported;
|
|
37
|
+
this.inFlight.set(key, (this.inFlight.get(key) ?? 0) + 1);
|
|
38
|
+
if (this.unreported !== before) this.onChange();
|
|
39
|
+
let acknowledged = false;
|
|
40
|
+
try {
|
|
41
|
+
const result = await send(record);
|
|
42
|
+
acknowledged = true;
|
|
43
|
+
return result;
|
|
44
|
+
} finally {
|
|
45
|
+
const settledBefore = this.unreported;
|
|
46
|
+
const remaining = (this.inFlight.get(key) ?? 1) - 1;
|
|
47
|
+
if (remaining > 0) this.inFlight.set(key, remaining);
|
|
48
|
+
else this.inFlight.delete(key);
|
|
49
|
+
if (acknowledged) this.failed.delete(key);
|
|
50
|
+
else this.failed.add(key);
|
|
51
|
+
if (this.unreported !== settledBefore) this.onChange();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|