agent-relay-server 0.122.0 → 0.123.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/docs/openapi.json +1 -1
- package/package.json +7 -8
- package/public/assets/{MessageBubble-DRdUD-kW.js → MessageBubble-BTeFExjj.js} +2 -2
- package/public/assets/{MessageBubble-DRdUD-kW.js.map → MessageBubble-BTeFExjj.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-siKMPCln.js → PlanGraphPanel-jU1DXg7w.js} +2 -2
- package/public/assets/{PlanGraphPanel-siKMPCln.js.map → PlanGraphPanel-jU1DXg7w.js.map} +1 -1
- package/public/assets/{activity-D3MNcSaV.js → activity-DUig2iYW.js} +2 -2
- package/public/assets/{activity-D3MNcSaV.js.map → activity-DUig2iYW.js.map} +1 -1
- package/public/assets/{agent-profiles-CKzttq6G.js → agent-profiles-D-oFmb6S.js} +2 -2
- package/public/assets/{agent-profiles-CKzttq6G.js.map → agent-profiles-D-oFmb6S.js.map} +1 -1
- package/public/assets/{agent-type-icon-0ZJRP1Vy.js → agent-type-icon-Dan2l3Tm.js} +2 -2
- package/public/assets/{agent-type-icon-0ZJRP1Vy.js.map → agent-type-icon-Dan2l3Tm.js.map} +1 -1
- package/public/assets/{agents-CuVj_rfG.js → agents-BkC6mcTh.js} +2 -2
- package/public/assets/{agents-CuVj_rfG.js.map → agents-BkC6mcTh.js.map} +1 -1
- package/public/assets/{analytics-iob6C27d.js → analytics-C3Dlmqe9.js} +2 -2
- package/public/assets/{analytics-iob6C27d.js.map → analytics-C3Dlmqe9.js.map} +1 -1
- package/public/assets/{automation-B2ixxs4q.js → automation-AeMZ70WQ.js} +2 -2
- package/public/assets/{automation-B2ixxs4q.js.map → automation-AeMZ70WQ.js.map} +1 -1
- package/public/assets/{branch-state-badge-CafBJWKo.js → branch-state-badge-CNW10MTC.js} +2 -2
- package/public/assets/{branch-state-badge-CafBJWKo.js.map → branch-state-badge-CNW10MTC.js.map} +1 -1
- package/public/assets/{channels-C-WsbL2H.js → channels-Bc_mEd56.js} +2 -2
- package/public/assets/{channels-C-WsbL2H.js.map → channels-Bc_mEd56.js.map} +1 -1
- package/public/assets/{chat-DeoVTVob.js → chat-BCNCuTgw.js} +2 -2
- package/public/assets/{chat-DeoVTVob.js.map → chat-BCNCuTgw.js.map} +1 -1
- package/public/assets/{connectors-C2Ac03LJ.js → connectors-h0d5Mc6e.js} +2 -2
- package/public/assets/{connectors-C2Ac03LJ.js.map → connectors-h0d5Mc6e.js.map} +1 -1
- package/public/assets/{display-CJzSoTMq.js → display-GFbGUG0O.js} +2 -2
- package/public/assets/{display-CJzSoTMq.js.map → display-GFbGUG0O.js.map} +1 -1
- package/public/assets/{formatted-body-35XBOY5L.js → formatted-body-CVr3Iev9.js} +3 -3
- package/public/assets/{formatted-body-35XBOY5L.js.map → formatted-body-CVr3Iev9.js.map} +1 -1
- package/public/assets/{formatted-body-impl-RmDxI5Ux.js → formatted-body-impl-Cq5SRGpm.js} +2 -2
- package/public/assets/{formatted-body-impl-RmDxI5Ux.js.map → formatted-body-impl-Cq5SRGpm.js.map} +1 -1
- package/public/assets/{index-D0xMQQb_.js → index--9UMzCFf.js} +6 -6
- package/public/assets/{index-D0xMQQb_.js.map → index--9UMzCFf.js.map} +1 -1
- package/public/assets/{insights-B643SFvm.js → insights-C8lIBXg3.js} +2 -2
- package/public/assets/{insights-B643SFvm.js.map → insights-C8lIBXg3.js.map} +1 -1
- package/public/assets/{integrations-hFIHeCF4.js → integrations-7OFsJtY7.js} +2 -2
- package/public/assets/{integrations-hFIHeCF4.js.map → integrations-7OFsJtY7.js.map} +1 -1
- package/public/assets/{maintenance-CURvdp_Z.js → maintenance-HDpg20OY.js} +2 -2
- package/public/assets/{maintenance-CURvdp_Z.js.map → maintenance-HDpg20OY.js.map} +1 -1
- package/public/assets/{managed-agents-DrZtdlSn.js → managed-agents-muCIMxng.js} +2 -2
- package/public/assets/{managed-agents-DrZtdlSn.js.map → managed-agents-muCIMxng.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-CVDO--ek.js → markdown-preview-impl-Cx7pCfA6.js} +2 -2
- package/public/assets/{markdown-preview-impl-CVDO--ek.js.map → markdown-preview-impl-Cx7pCfA6.js.map} +1 -1
- package/public/assets/{memory-Bm7dt_Qf.js → memory-CTO1FfJ-.js} +2 -2
- package/public/assets/{memory-Bm7dt_Qf.js.map → memory-CTO1FfJ-.js.map} +1 -1
- package/public/assets/{messages-DTq4i2KZ.js → messages-RWSXG2UM.js} +2 -2
- package/public/assets/{messages-DTq4i2KZ.js.map → messages-RWSXG2UM.js.map} +1 -1
- package/public/assets/{orchestrators-Dra318Ap.js → orchestrators-5aiYw3yx.js} +2 -2
- package/public/assets/{orchestrators-Dra318Ap.js.map → orchestrators-5aiYw3yx.js.map} +1 -1
- package/public/assets/{overview-yr1Vmk64.js → overview-Du6XLt5l.js} +2 -2
- package/public/assets/{overview-yr1Vmk64.js.map → overview-Du6XLt5l.js.map} +1 -1
- package/public/assets/{pairs-mnlKDBGp.js → pairs-BDRnlEta.js} +2 -2
- package/public/assets/{pairs-mnlKDBGp.js.map → pairs-BDRnlEta.js.map} +1 -1
- package/public/assets/{projects-Dzdf-gkK.js → projects-DwsWE9es.js} +2 -2
- package/public/assets/{projects-Dzdf-gkK.js.map → projects-DwsWE9es.js.map} +1 -1
- package/public/assets/{prompt-settings-ByL8SNCi.js → prompt-settings-CaJX_LbA.js} +2 -2
- package/public/assets/{prompt-settings-ByL8SNCi.js.map → prompt-settings-CaJX_LbA.js.map} +1 -1
- package/public/assets/{registry-CXWyOtpr.js → registry-C-uIL0Nl.js} +2 -2
- package/public/assets/{registry-CXWyOtpr.js.map → registry-C-uIL0Nl.js.map} +1 -1
- package/public/assets/{security-0SBsc_4W.js → security-BpVrdYhJ.js} +2 -2
- package/public/assets/{security-0SBsc_4W.js.map → security-BpVrdYhJ.js.map} +1 -1
- package/public/assets/{select-Bw5z2KJ_.js → select-DallbU93.js} +2 -2
- package/public/assets/{select-Bw5z2KJ_.js.map → select-DallbU93.js.map} +1 -1
- package/public/assets/{settings-78Cyq_tA.js → settings-BUQirdyI.js} +2 -2
- package/public/assets/{settings-78Cyq_tA.js.map → settings-BUQirdyI.js.map} +1 -1
- package/public/assets/{store-BPrVuKiv.js → store-DMZSTozC.js} +2 -2
- package/public/assets/{store-BPrVuKiv.js.map → store-DMZSTozC.js.map} +1 -1
- package/public/assets/{tasks-Cg2MMeri.js → tasks-k8mQDfKy.js} +2 -2
- package/public/assets/{tasks-Cg2MMeri.js.map → tasks-k8mQDfKy.js.map} +1 -1
- package/public/assets/{teams-DWv6UwFV.js → teams-DI-Lhhbl.js} +2 -2
- package/public/assets/{teams-DWv6UwFV.js.map → teams-DI-Lhhbl.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-DBLDjbIg.js → terminal-viewer-impl-BL853Fpi.js} +2 -2
- package/public/assets/{terminal-viewer-impl-DBLDjbIg.js.map → terminal-viewer-impl-BL853Fpi.js.map} +1 -1
- package/public/assets/types-CRptbSYh.js +2 -0
- package/public/assets/types-CRptbSYh.js.map +1 -0
- package/public/assets/{user-avatar-BsOLyVJj.js → user-avatar-D9xyGi3u.js} +2 -2
- package/public/assets/{user-avatar-BsOLyVJj.js.map → user-avatar-D9xyGi3u.js.map} +1 -1
- package/public/assets/{work-queue-DmwvnF6F.js → work-queue-DrLIdvxB.js} +2 -2
- package/public/assets/{work-queue-DmwvnF6F.js.map → work-queue-DrLIdvxB.js.map} +1 -1
- package/public/assets/{workspaces-dIBivJN8.js → workspaces-CnNL6T8Q.js} +2 -2
- package/public/assets/{workspaces-dIBivJN8.js.map → workspaces-CnNL6T8Q.js.map} +1 -1
- package/public/index.html +6 -6
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/runner/src/adapters/claude-delivery.ts +127 -0
- package/runner/src/adapters/claude-quota-harvest.ts +92 -0
- package/runner/src/adapters/claude-session-probe.ts +225 -0
- package/runner/src/adapters/claude-status-detectors.ts +147 -0
- package/runner/src/adapters/claude-transcript-tail.ts +128 -0
- package/runner/src/adapters/claude-transcript.ts +414 -0
- package/runner/src/adapters/claude.ts +675 -0
- package/runner/src/adapters/codex-agent-message-capture.ts +139 -0
- package/runner/src/adapters/codex-client.ts +279 -0
- package/runner/src/adapters/codex.ts +1345 -0
- package/runner/src/attachment-cache.ts +189 -0
- package/runner/src/busy-reconciler.ts +102 -0
- package/runner/src/claim-tracker.ts +140 -0
- package/runner/src/claude-prompt-gates.ts +268 -0
- package/runner/src/codex-version.ts +63 -0
- package/runner/src/config.ts +7 -0
- package/runner/src/context-probe-state.ts +6 -0
- package/runner/src/continuation-archive.ts +179 -0
- package/runner/src/control-server.ts +639 -0
- package/runner/src/index.ts +396 -0
- package/runner/src/launch-assembly.ts +674 -0
- package/runner/src/liveness.ts +50 -0
- package/runner/src/logger.ts +99 -0
- package/runner/src/mcp-outbox.ts +120 -0
- package/runner/src/message-body-config/config.ts +3 -0
- package/runner/src/message-body-config.ts +1 -0
- package/runner/src/native-self-resume.ts +202 -0
- package/runner/src/outbox.ts +342 -0
- package/runner/src/process-meta.ts +9 -0
- package/runner/src/profile-home.ts +260 -0
- package/runner/src/profile-projection.ts +324 -0
- package/runner/src/providers.ts +20 -0
- package/runner/src/provisioning.ts +368 -0
- package/runner/src/quota.ts +40 -0
- package/runner/src/rate-limit.ts +189 -0
- package/runner/src/relay-injection-events.ts +105 -0
- package/runner/src/relay-instructions.ts +60 -0
- package/runner/src/relay-mcp-proxy.ts +383 -0
- package/runner/src/relay-mcp.ts +156 -0
- package/runner/src/reply-obligation-cache.ts +136 -0
- package/runner/src/response-capture-report.ts +63 -0
- package/runner/src/runner-core.ts +2409 -0
- package/runner/src/runner-helpers.ts +435 -0
- package/runner/src/runner-insights.ts +105 -0
- package/runner/src/runner.ts +14 -0
- package/runner/src/session-destroy.ts +22 -0
- package/runner/src/session-insights.ts +118 -0
- package/runner/src/session-scratch.ts +271 -0
- package/runner/src/template-resolver.ts +29 -0
- package/runner/src/version.ts +44 -0
- package/src/config-store.ts +2 -0
- package/src/db/provisioning-registry.ts +40 -9
- package/src/db/provisioning-resolve.ts +66 -5
- package/src/gate-resolver.ts +7 -12
- package/src/routes/provisioning-registry.ts +1 -1
- package/src/services/spawn-agent.ts +1 -0
- package/public/assets/types-BeJUSfDj.js +0 -2
- package/public/assets/types-BeJUSfDj.js.map +0 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { LivenessSignal } from "agent-relay-sdk";
|
|
2
|
+
|
|
3
|
+
// The raw runner-tracked signals an adapter folds into a normalized LivenessSignal.
|
|
4
|
+
// This is the INPUT to the contract computation (#746): the runner owns this state
|
|
5
|
+
// (transport frames, the last provider event, the pending timeline transition, the
|
|
6
|
+
// active-work set), passes it to the adapter's `liveness()` verdict, and ships the
|
|
7
|
+
// resulting LivenessSignal on the agent card. Provider-raw interpretation stops here —
|
|
8
|
+
// nothing above the runner ever reads these fields again.
|
|
9
|
+
export interface LivenessInputs {
|
|
10
|
+
/** Runner→server transport frame/ack timestamps (sdk BusClient.transportState). */
|
|
11
|
+
transport?: { lastServerFrameAt?: number; lastHeartbeatAckAt?: number };
|
|
12
|
+
/** Epoch-ms of the most recent provider status/token event the runner observed. */
|
|
13
|
+
lastProviderEventAt?: number;
|
|
14
|
+
/** The most recent lifecycle/timeline transition the runner published (status + ts). */
|
|
15
|
+
timelineEvent?: { status?: string; timestamp?: number };
|
|
16
|
+
/** Active work records. The adapter classifies its own kinds; `provider-turn` is the
|
|
17
|
+
* bare turn, anything else (subagent, background-script, tool, subprocess) is live work. */
|
|
18
|
+
activeWork: Array<{ kind?: string; startedAt?: number }>;
|
|
19
|
+
/** Fallback progress floor when no other progress evidence exists yet (e.g. work start). */
|
|
20
|
+
fallbackProgressAt?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function num(value: unknown): number {
|
|
24
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The shared liveness fold (#725 / #746). Both reference adapters (Claude #713, Codex #724)
|
|
29
|
+
* route through this so the verdict is computed in ONE place; each adapter's `liveness()`
|
|
30
|
+
* decides WHICH raw signals it feeds in (Claude surfaces a `compacting` timeline transition
|
|
31
|
+
* as progress; Codex surfaces live subprocess work-kinds). The fold itself is provider-
|
|
32
|
+
* agnostic, which is the point of the contract: a new provider only has to feed its signals.
|
|
33
|
+
*/
|
|
34
|
+
export function computeLivenessSignal(inputs: LivenessInputs): LivenessSignal {
|
|
35
|
+
const transportLiveAt = Math.max(
|
|
36
|
+
num(inputs.transport?.lastServerFrameAt),
|
|
37
|
+
num(inputs.transport?.lastHeartbeatAckAt),
|
|
38
|
+
);
|
|
39
|
+
// Progress = provider tokens OR a lifecycle/timeline transition (e.g. native compaction,
|
|
40
|
+
// #713) — whichever is latest, with the caller-supplied floor as the baseline.
|
|
41
|
+
const lastProgressAt = Math.max(
|
|
42
|
+
num(inputs.lastProviderEventAt),
|
|
43
|
+
num(inputs.timelineEvent?.timestamp),
|
|
44
|
+
num(inputs.fallbackProgressAt),
|
|
45
|
+
);
|
|
46
|
+
// Any work-kind that is NOT the bare provider turn is live tool/subprocess work (#724):
|
|
47
|
+
// a wedged provider turn with a live `bun test` child must read as progressing.
|
|
48
|
+
const hasLiveWork = inputs.activeWork.some((work) => Boolean(work.kind && work.kind !== "provider-turn"));
|
|
49
|
+
return { transportLiveAt, lastProgressAt, hasLiveWork };
|
|
50
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
4
|
+
import { logLevelFromEnv } from "./config";
|
|
5
|
+
|
|
6
|
+
// Phase 1 observability (#198): one leveled, runtime-togglable logger for the
|
|
7
|
+
// Runner and the provider adapters below it. Replaces the ad-hoc scatter of
|
|
8
|
+
// `console.error`, `logRunnerDiagnostic` (-> runner-<agent>.log) and
|
|
9
|
+
// `sessionLog`/`sessionDebug` (-> session-mirror-<agent>.log) with a single
|
|
10
|
+
// switch and a single greppable, ANSI-free sink.
|
|
11
|
+
//
|
|
12
|
+
// Sink: the per-agent `session-mirror-<agent>.log` — the file the orchestrator
|
|
13
|
+
// already surfaces to the dashboard log-viewer (captureSessionMirror). One place
|
|
14
|
+
// to look when anything in the Runner misbehaves.
|
|
15
|
+
//
|
|
16
|
+
// Level is read once from AGENT_RELAY_LOG_LEVEL (default "info") and can be
|
|
17
|
+
// flipped at runtime via the control port (no restart) — so a phase refactor can
|
|
18
|
+
// be watched at debug without bouncing the agent.
|
|
19
|
+
|
|
20
|
+
type LogLevel = "debug" | "info" | "warn" | "error" | "fatal";
|
|
21
|
+
|
|
22
|
+
const ORDER: Record<LogLevel, number> = { debug: 10, info: 20, warn: 30, error: 40, fatal: 50 };
|
|
23
|
+
export const LOG_LEVELS = Object.keys(ORDER) as LogLevel[];
|
|
24
|
+
|
|
25
|
+
export function parseLogLevel(value: string | undefined | null): LogLevel | undefined {
|
|
26
|
+
if (!value) return undefined;
|
|
27
|
+
const v = value.trim().toLowerCase();
|
|
28
|
+
return (LOG_LEVELS as string[]).includes(v) ? (v as LogLevel) : undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Shared with the orchestrator (session-mirror reader) + runner outbox via the
|
|
32
|
+
// SDK helper, so all sites resolve the identical filename for a given agent id.
|
|
33
|
+
function safeLogName(value: string): string {
|
|
34
|
+
return sanitizeFsName(value, { replacement: "_", maxLen: 180 });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface LoggerConfig {
|
|
38
|
+
agentId?: string;
|
|
39
|
+
level?: LogLevel;
|
|
40
|
+
headless?: boolean;
|
|
41
|
+
logDir?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class Logger {
|
|
45
|
+
private level: LogLevel;
|
|
46
|
+
private agentId: string;
|
|
47
|
+
private headless: boolean;
|
|
48
|
+
private logDir: string;
|
|
49
|
+
|
|
50
|
+
constructor(config: LoggerConfig = {}) {
|
|
51
|
+
this.level = config.level ?? parseLogLevel(logLevelFromEnv()) ?? "info";
|
|
52
|
+
this.agentId = config.agentId ?? "runner";
|
|
53
|
+
this.headless = config.headless ?? false;
|
|
54
|
+
this.logDir = config.logDir ?? join(process.env.HOME || ".", ".agent-relay", "logs");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Bind the logger to a concrete agent once the runner knows its id. Preserves a
|
|
58
|
+
// level already set via env/runtime unless an explicit level is passed.
|
|
59
|
+
configure(config: LoggerConfig): void {
|
|
60
|
+
if (config.agentId !== undefined) this.agentId = config.agentId;
|
|
61
|
+
if (config.headless !== undefined) this.headless = config.headless;
|
|
62
|
+
if (config.logDir !== undefined) this.logDir = config.logDir;
|
|
63
|
+
if (config.level !== undefined) this.level = config.level;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setLevel(level: LogLevel): void { this.level = level; }
|
|
67
|
+
getLevel(): LogLevel { return this.level; }
|
|
68
|
+
isEnabled(level: LogLevel): boolean { return ORDER[level] >= ORDER[this.level]; }
|
|
69
|
+
|
|
70
|
+
debug(component: string, message: string): void { this.log("debug", component, message); }
|
|
71
|
+
info(component: string, message: string): void { this.log("info", component, message); }
|
|
72
|
+
warn(component: string, message: string): void { this.log("warn", component, message); }
|
|
73
|
+
error(component: string, message: string): void { this.log("error", component, message); }
|
|
74
|
+
fatal(component: string, message: string): void { this.log("fatal", component, message); }
|
|
75
|
+
|
|
76
|
+
log(level: LogLevel, component: string, message: string): void {
|
|
77
|
+
if (!this.isEnabled(level)) return;
|
|
78
|
+
const line = `[${new Date().toISOString()}] ${level.toUpperCase().padEnd(5)} [${component}] ${oneLine(message)}\n`;
|
|
79
|
+
try {
|
|
80
|
+
mkdirSync(this.logDir, { recursive: true });
|
|
81
|
+
appendFileSync(join(this.logDir, `session-mirror-${safeLogName(this.agentId)}.log`), line);
|
|
82
|
+
} catch {
|
|
83
|
+
// Best-effort. If the per-agent file can't be written, surface error/fatal to
|
|
84
|
+
// stderr so it is not lost entirely (headless: lands in the orchestrator log).
|
|
85
|
+
if (ORDER[level] >= ORDER.error) { try { console.error(line.trimEnd()); } catch { /* give up */ } }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Newlines would split one record across several log lines and break greppability;
|
|
91
|
+
// collapse them so a multi-line message stays one line.
|
|
92
|
+
function oneLine(message: string): string {
|
|
93
|
+
return message.replace(/\r?\n/g, " ⏎ ");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Process-global logger. A runner process serves exactly one agent, so a singleton
|
|
97
|
+
// is the right scope; the runner calls configure() once it knows its id, and
|
|
98
|
+
// adapters import this instance directly (no constructor threading).
|
|
99
|
+
export const logger = new Logger();
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { SendMessageInput } from "agent-relay-sdk";
|
|
2
|
+
import type { RelayHttpClient } from "agent-relay-sdk";
|
|
3
|
+
import { errMessage } from "agent-relay-sdk";
|
|
4
|
+
import { relayMcpEndpoint } from "./relay-mcp";
|
|
5
|
+
import type { OutboxRecord } from "./outbox";
|
|
6
|
+
import { deliverContinuationArchiveRecord } from "./continuation-archive";
|
|
7
|
+
import { logger } from "./logger";
|
|
8
|
+
import { isHttpAuthError, isHttpStatusError } from "./runner-helpers";
|
|
9
|
+
|
|
10
|
+
interface RunnerOutboxDelivery {
|
|
11
|
+
record: OutboxRecord;
|
|
12
|
+
http: RelayHttpClient;
|
|
13
|
+
relayUrl: string;
|
|
14
|
+
token?: string;
|
|
15
|
+
updatePayload(seq: number, payload: unknown): void;
|
|
16
|
+
sessionLog(message: string): void;
|
|
17
|
+
recoverRuntimeTokenAfterAuthFailure(source: string): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function deliverRunnerOutboxEvent(input: RunnerOutboxDelivery): Promise<void> {
|
|
21
|
+
const { record, http } = input;
|
|
22
|
+
try {
|
|
23
|
+
if (record.kind === "session-message") {
|
|
24
|
+
await http.sendMessage({
|
|
25
|
+
...(record.payload as SendMessageInput),
|
|
26
|
+
occurredAt: record.occurredAt,
|
|
27
|
+
idempotencyKey: record.idempotencyKey,
|
|
28
|
+
});
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (record.kind === "session-message-batch") {
|
|
32
|
+
const messages = Array.isArray((record.payload as { messages?: unknown }).messages)
|
|
33
|
+
? (record.payload as { messages: SendMessageInput[] }).messages
|
|
34
|
+
: [];
|
|
35
|
+
const batch = messages.map((message, index) => ({
|
|
36
|
+
...message,
|
|
37
|
+
occurredAt: typeof message.occurredAt === "number" ? message.occurredAt : record.occurredAt,
|
|
38
|
+
idempotencyKey: message.idempotencyKey ?? `${record.idempotencyKey}:${index}`,
|
|
39
|
+
}));
|
|
40
|
+
try {
|
|
41
|
+
await http.sendMessages(batch);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (!isDeterministicValidationError(error)) throw error;
|
|
44
|
+
await deliverSessionBatchItems(input, batch);
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (record.kind === "insight") {
|
|
49
|
+
await http.recordInsightObservation({
|
|
50
|
+
...(record.payload as Parameters<RelayHttpClient["recordInsightObservation"]>[0]),
|
|
51
|
+
occurredAt: record.occurredAt,
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (record.kind === "continuation-archive") {
|
|
56
|
+
await deliverContinuationArchiveRecord({
|
|
57
|
+
record,
|
|
58
|
+
http,
|
|
59
|
+
updatePayload: input.updatePayload,
|
|
60
|
+
sessionLog: input.sessionLog,
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (record.kind === "mcp-tool-call") {
|
|
65
|
+
await deliverBufferedMcpCall(input);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
logger.warn("outbox", `dropping event with unknown kind: ${record.kind}`);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (isHttpStatusError(error, 409)) return;
|
|
71
|
+
if (isHttpAuthError(error)) input.recoverRuntimeTokenAfterAuthFailure("outbox");
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function deliverSessionBatchItems(input: RunnerOutboxDelivery, messages: SendMessageInput[]): Promise<void> {
|
|
77
|
+
for (const message of messages) {
|
|
78
|
+
try {
|
|
79
|
+
await input.http.sendMessage(message);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (!isDeterministicValidationError(error)) throw error;
|
|
82
|
+
logger.warn("outbox", `dropping invalid session batch item: ${errMessage(error)}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isDeterministicValidationError(error: unknown): boolean {
|
|
88
|
+
const status = typeof error === "object" && error !== null ? (error as { status?: unknown }).status : undefined;
|
|
89
|
+
return status === 400 || status === 413 || status === 422;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function deliverBufferedMcpCall(input: {
|
|
93
|
+
record: OutboxRecord;
|
|
94
|
+
relayUrl: string;
|
|
95
|
+
token?: string;
|
|
96
|
+
recoverRuntimeTokenAfterAuthFailure(source: string): void;
|
|
97
|
+
}): Promise<void> {
|
|
98
|
+
const payload = input.record.payload as { tool: string; arguments: Record<string, unknown> };
|
|
99
|
+
const headers: Record<string, string> = { "content-type": "application/json" };
|
|
100
|
+
if (input.token) headers.authorization = `Bearer ${input.token}`;
|
|
101
|
+
const response = await fetch(relayMcpEndpoint(input.relayUrl), {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers,
|
|
104
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: payload.tool, arguments: payload.arguments } }),
|
|
105
|
+
});
|
|
106
|
+
if (response.status === 401 || response.status === 403) {
|
|
107
|
+
input.recoverRuntimeTokenAfterAuthFailure("mcp-outbox");
|
|
108
|
+
throw new Error(`relay rejected buffered ${payload.tool} with ${response.status}`);
|
|
109
|
+
}
|
|
110
|
+
if (response.status >= 500) throw new Error(`relay ${response.status} on buffered ${payload.tool}`);
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
const body = await response.text().catch(() => "");
|
|
113
|
+
logger.warn("mcp-outbox", `buffered ${payload.tool} permanently rejected (${response.status}); dropping: ${body.slice(0, 200)}`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const json = await response.json().catch(() => null) as { error?: { message?: string } } | null;
|
|
117
|
+
if (json?.error) {
|
|
118
|
+
logger.warn("mcp-outbox", `buffered ${payload.tool} returned a tool error; dropping: ${json.error.message ?? "(no detail)"}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { messageBodyMaxCharsFromEnv } from "./message-body-config/config";
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { errMessage } from "agent-relay-sdk";
|
|
2
|
+
import type { ProviderStatusEvent, SemanticStatus } from "./adapter";
|
|
3
|
+
import { nativeSelfResumeTimeoutMsFromEnv } from "./config";
|
|
4
|
+
|
|
5
|
+
interface RunnerTimelineEvent {
|
|
6
|
+
status: string;
|
|
7
|
+
id?: string;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
title?: string;
|
|
10
|
+
body?: string;
|
|
11
|
+
icon?: string;
|
|
12
|
+
metadata?: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface NativeSelfResumeHandoff {
|
|
16
|
+
commandId: string;
|
|
17
|
+
generation?: number;
|
|
18
|
+
compactedAt?: number;
|
|
19
|
+
timer: ReturnType<typeof setTimeout>;
|
|
20
|
+
resolve: (result: Record<string, unknown>) => void;
|
|
21
|
+
reject: (error: Error) => void;
|
|
22
|
+
promise: Promise<Record<string, unknown>>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isNativeSelfResumeCompact(type: string, params: Record<string, unknown>): boolean {
|
|
26
|
+
if (type !== "agent.compact") return false;
|
|
27
|
+
const selfResume = params.selfResume;
|
|
28
|
+
return Boolean(selfResume && typeof selfResume === "object" && !Array.isArray(selfResume) && (selfResume as Record<string, unknown>).mode === "native");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class NativeSelfResumeTracker {
|
|
32
|
+
private current?: NativeSelfResumeHandoff;
|
|
33
|
+
|
|
34
|
+
get active(): NativeSelfResumeHandoff | undefined {
|
|
35
|
+
return this.current;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
begin(commandId: string, params: Record<string, unknown>): NativeSelfResumeHandoff {
|
|
39
|
+
this.finish(this.current);
|
|
40
|
+
let resolve!: (result: Record<string, unknown>) => void;
|
|
41
|
+
let reject!: (error: Error) => void;
|
|
42
|
+
const promise = new Promise<Record<string, unknown>>((res, rej) => {
|
|
43
|
+
resolve = res;
|
|
44
|
+
reject = rej;
|
|
45
|
+
});
|
|
46
|
+
const selfResume = params.selfResume && typeof params.selfResume === "object" && !Array.isArray(params.selfResume)
|
|
47
|
+
? params.selfResume as Record<string, unknown>
|
|
48
|
+
: undefined;
|
|
49
|
+
const generation = typeof selfResume?.generation === "number" ? selfResume.generation : undefined;
|
|
50
|
+
const timeoutMs = nativeSelfResumeTimeoutMsFromEnv();
|
|
51
|
+
const handoff: NativeSelfResumeHandoff = {
|
|
52
|
+
commandId,
|
|
53
|
+
generation,
|
|
54
|
+
promise,
|
|
55
|
+
resolve,
|
|
56
|
+
reject,
|
|
57
|
+
timer: setTimeout(() => {
|
|
58
|
+
if (this.current !== handoff) return;
|
|
59
|
+
this.current = undefined;
|
|
60
|
+
reject(new Error(`native self-resume timed out after ${Math.round(timeoutMs / 1000)}s`));
|
|
61
|
+
}, timeoutMs),
|
|
62
|
+
};
|
|
63
|
+
this.current = handoff;
|
|
64
|
+
return handoff;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
finish(handoff?: NativeSelfResumeHandoff): void {
|
|
68
|
+
if (!handoff) return;
|
|
69
|
+
clearTimeout(handoff.timer);
|
|
70
|
+
if (this.current === handoff) this.current = undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
resolve(handoff: NativeSelfResumeHandoff, result: Record<string, unknown>): void {
|
|
74
|
+
this.finish(handoff);
|
|
75
|
+
handoff.resolve(result);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
reject(handoff: NativeSelfResumeHandoff, error: Error): void {
|
|
79
|
+
this.finish(handoff);
|
|
80
|
+
handoff.reject(error);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
noteCompacted(timestamp: number = Date.now()): void {
|
|
84
|
+
if (!this.current) return;
|
|
85
|
+
this.current.compactedAt = timestamp;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface NativeSelfResumeRecoveryOptions {
|
|
90
|
+
handoff: NativeSelfResumeHandoff;
|
|
91
|
+
tracker: NativeSelfResumeTracker;
|
|
92
|
+
providerSessionId: string;
|
|
93
|
+
status: SemanticStatus;
|
|
94
|
+
diagnostics: Record<string, unknown>;
|
|
95
|
+
now: number;
|
|
96
|
+
publishTimeline(event: RunnerTimelineEvent): void;
|
|
97
|
+
setTerminalProviderExit(marker: Record<string, unknown>): void;
|
|
98
|
+
setProviderStatus(update: ProviderStatusEvent): void;
|
|
99
|
+
restartProvider(resumeId?: string): Promise<void>;
|
|
100
|
+
publishStatus(): void;
|
|
101
|
+
scheduleDrain(): void;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function recoverNativeSelfResumeExit(opts: NativeSelfResumeRecoveryOptions): Promise<void> {
|
|
105
|
+
const resumeId = typeof opts.diagnostics.claudeResumeId === "string" && opts.diagnostics.claudeResumeId.length > 0
|
|
106
|
+
? opts.diagnostics.claudeResumeId
|
|
107
|
+
: undefined;
|
|
108
|
+
if (opts.handoff.compactedAt && resumeId) {
|
|
109
|
+
await recoverWithClaudeResume(opts, resumeId);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
await recoverWithFreshLaunch(opts, resumeId);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function recoverWithClaudeResume(opts: NativeSelfResumeRecoveryOptions, resumeId: string): Promise<void> {
|
|
116
|
+
opts.publishTimeline({
|
|
117
|
+
status: "provider.restart_decision",
|
|
118
|
+
id: `provider-self-resume-relaunch-${opts.providerSessionId}-${opts.now}`,
|
|
119
|
+
timestamp: Date.now(),
|
|
120
|
+
title: "Self-resume relaunching",
|
|
121
|
+
body: `Claude exited during native self-resume; runner is relaunching with claude --resume ${resumeId}.`,
|
|
122
|
+
icon: "ti-refresh",
|
|
123
|
+
metadata: selfResumeMetadata(opts.handoff, opts.diagnostics, {
|
|
124
|
+
decision: "resume-native-self-resume",
|
|
125
|
+
reason: "self-resume-exit-after-compact",
|
|
126
|
+
claudeResumeId: resumeId,
|
|
127
|
+
compactedAt: opts.handoff.compactedAt,
|
|
128
|
+
}),
|
|
129
|
+
});
|
|
130
|
+
try {
|
|
131
|
+
await opts.restartProvider(resumeId);
|
|
132
|
+
opts.publishStatus();
|
|
133
|
+
opts.scheduleDrain();
|
|
134
|
+
opts.tracker.resolve(opts.handoff, { recovered: true, via: "claude-resume", claudeResumeId: resumeId });
|
|
135
|
+
} catch (error) {
|
|
136
|
+
const message = errMessage(error);
|
|
137
|
+
opts.setTerminalProviderExit(selfResumeMetadata(opts.handoff, opts.diagnostics, {
|
|
138
|
+
reason: "self-resume-relaunch-failed",
|
|
139
|
+
claudeResumeId: resumeId,
|
|
140
|
+
compactedAt: opts.handoff.compactedAt,
|
|
141
|
+
lastError: message,
|
|
142
|
+
}));
|
|
143
|
+
opts.setProviderStatus({
|
|
144
|
+
status: "offline",
|
|
145
|
+
reason: "provider-turn",
|
|
146
|
+
id: `provider-self-resume-relaunch-failed-${opts.providerSessionId}`,
|
|
147
|
+
clear: ["provider-turn", "subagent", "background-script"],
|
|
148
|
+
});
|
|
149
|
+
opts.tracker.reject(opts.handoff, new Error(`self-resume relaunch failed: ${message}`));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function recoverWithFreshLaunch(opts: NativeSelfResumeRecoveryOptions, resumeId?: string): Promise<void> {
|
|
154
|
+
opts.publishTimeline({
|
|
155
|
+
status: "provider.restart_decision",
|
|
156
|
+
id: `provider-self-resume-fresh-launch-${opts.providerSessionId}-${opts.now}`,
|
|
157
|
+
timestamp: Date.now(),
|
|
158
|
+
title: "Self-resume fresh-launching",
|
|
159
|
+
body: "Claude exited during native self-resume before a compacted marker; runner is fresh-launching so Relay can inject the persisted continuation.",
|
|
160
|
+
icon: "ti-refresh",
|
|
161
|
+
metadata: selfResumeMetadata(opts.handoff, opts.diagnostics, {
|
|
162
|
+
decision: "fresh-launch-native-self-resume",
|
|
163
|
+
reason: "self-resume-exit-before-compact",
|
|
164
|
+
...(resumeId ? { claudeResumeId: resumeId } : {}),
|
|
165
|
+
}),
|
|
166
|
+
});
|
|
167
|
+
try {
|
|
168
|
+
await opts.restartProvider();
|
|
169
|
+
opts.publishStatus();
|
|
170
|
+
opts.scheduleDrain();
|
|
171
|
+
opts.tracker.resolve(opts.handoff, {
|
|
172
|
+
recovered: true,
|
|
173
|
+
via: "fresh-launch",
|
|
174
|
+
...(resumeId ? { claudeResumeId: resumeId } : {}),
|
|
175
|
+
});
|
|
176
|
+
} catch (error) {
|
|
177
|
+
const message = errMessage(error);
|
|
178
|
+
opts.setTerminalProviderExit(selfResumeMetadata(opts.handoff, opts.diagnostics, {
|
|
179
|
+
reason: "self-resume-fresh-launch-failed",
|
|
180
|
+
...(resumeId ? { claudeResumeId: resumeId } : {}),
|
|
181
|
+
lastError: message,
|
|
182
|
+
}));
|
|
183
|
+
opts.setProviderStatus({
|
|
184
|
+
status: "offline",
|
|
185
|
+
reason: "provider-turn",
|
|
186
|
+
id: `provider-self-resume-fresh-launch-failed-${opts.providerSessionId}`,
|
|
187
|
+
clear: ["provider-turn", "subagent", "background-script"],
|
|
188
|
+
});
|
|
189
|
+
opts.tracker.reject(opts.handoff, new Error(`self-resume fresh launch failed: ${message}`));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function selfResumeMetadata(handoff: NativeSelfResumeHandoff, diagnostics: Record<string, unknown>, extra: Record<string, unknown>): Record<string, unknown> {
|
|
194
|
+
return {
|
|
195
|
+
eventType: "provider.restart_decision",
|
|
196
|
+
selfResume: true,
|
|
197
|
+
selfResumeCommandId: handoff.commandId,
|
|
198
|
+
selfResumeGeneration: handoff.generation ?? null,
|
|
199
|
+
...extra,
|
|
200
|
+
...diagnostics,
|
|
201
|
+
};
|
|
202
|
+
}
|