agent-relay-runner 0.97.0 → 0.98.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
- "version": "0.97.0",
3
+ "version": "0.98.0",
4
4
  "description": "Unified provider lifecycle runner for Agent Relay",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,7 +20,7 @@
20
20
  "directory": "runner"
21
21
  },
22
22
  "dependencies": {
23
- "agent-relay-sdk": "0.2.77"
23
+ "agent-relay-sdk": "0.2.78"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/bun": "latest",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
3
  "description": "Thin Agent Relay runner bridge for Claude Code",
4
- "version": "0.97.0",
4
+ "version": "0.98.0",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
package/src/adapter.ts CHANGED
@@ -1,5 +1,6 @@
1
- import type { AgentProfile, Message } from "agent-relay-sdk";
1
+ import type { AgentProfile, LivenessSignal, Message } from "agent-relay-sdk";
2
2
  import { isRecord } from "agent-relay-sdk";
3
+ import type { LivenessInputs } from "./liveness";
3
4
  import type { SessionEvent } from "./session-insights";
4
5
  import { messageBodyMaxCharsFromEnv } from "./config";
5
6
  import { relayReplyReminderText } from "./relay-instructions";
@@ -191,6 +192,13 @@ export interface ProviderAdapter {
191
192
  // (Codex app-server item events). Claude mirrors via hooks/transcript instead,
192
193
  // so it leaves this unimplemented.
193
194
  onSessionEvent?(cb: (event: ProviderSessionEvent) => void): void;
195
+ // §4.1 of the Runner↔provider contract (#725 / #746): fold the runner-tracked raw
196
+ // transport/timeline/token/work-kind signals into ONE normalized LivenessSignal the
197
+ // server reads instead of six raw `meta` fields. Each adapter owns what counts as
198
+ // progress for its provider (Claude #713 timeline transitions; Codex #724 live
199
+ // subprocess work). Optional so test/mock adapters fall back to the shared
200
+ // {@link computeLivenessSignal} fold — the runner always has a verdict to ship.
201
+ liveness?(inputs: LivenessInputs): LivenessSignal;
194
202
  // Headless providers with no tmux session (e.g. the Codex app-server) still
195
203
  // warrant an automatic restart on unexpected exit. Returning true opts the
196
204
  // provider into the runner's restart-with-backoff path.
@@ -7,6 +7,8 @@ import { shellEscape as shellQuote } from "agent-relay-sdk/shell-utils";
7
7
  import { tmuxCommand, tmuxHasSession } from "agent-relay-sdk/tmux-utils";
8
8
  import { sanitizeFsName } from "agent-relay-sdk/fs-name";
9
9
  import { profileAllowsRelayFeature, type ManagedProcess, type ProviderAdapter, type ProviderConfig, type ProviderStatusUpdate, type RunnerSpawnConfig, type SemanticStatus, type SpawnArgs } from "../adapter";
10
+ import { computeLivenessSignal, type LivenessInputs } from "../liveness";
11
+ import type { LivenessSignal } from "agent-relay-sdk";
10
12
  import { collectClaudeSessionEvents } from "./claude-transcript";
11
13
  import type { SessionEvent } from "../session-insights";
12
14
  import { prepareClaudeProfileHome, profileUsesClaudeHostProviderGlobals } from "../profile-home";
@@ -31,6 +33,16 @@ export class ClaudeAdapter implements ProviderAdapter {
31
33
  this.statusCb = cb;
32
34
  }
33
35
 
36
+ // §4.1 contract verdict (#746). Claude reference (#713): Claude is token-silent during
37
+ // native compaction, but the runner publishes a `compacting` timeline transition while it
38
+ // runs — so `timelineEvent.timestamp` (folded into lastProgressAt) keeps a compacting Opus
39
+ // turn reading as progressing instead of wedged. Transport frames/acks + the server's
40
+ // lastSeen floor cover long token-silent thinking turns. The fold itself is shared; this
41
+ // override exists to document and own Claude's progress sources behind the contract.
42
+ liveness(inputs: LivenessInputs): LivenessSignal {
43
+ return computeLivenessSignal(inputs);
44
+ }
45
+
34
46
  async spawn(config: RunnerSpawnConfig): Promise<ManagedProcess> {
35
47
  const args = this.buildSpawnArgs(config, config.providerConfig as ProviderConfig);
36
48
 
@@ -1,7 +1,7 @@
1
1
  import { accessSync, constants, existsSync, readFileSync, realpathSync, readdirSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { basename, join, resolve } from "node:path";
4
- import type { ContextState, Message } from "agent-relay-sdk";
4
+ import type { ContextState, LivenessSignal, Message } from "agent-relay-sdk";
5
5
  import { isRecord, stringValue } from "agent-relay-sdk";
6
6
  import { isPidAlive, killPid, processTreePids, processTreePidsFromTable, waitForPidsExit } from "agent-relay-sdk/process-utils";
7
7
  import { profileAllowsRelayFeature, providerMessageText, RELAY_CONTEXT, type ManagedProcess, type ProviderAdapter, type ProviderConfig, type ProviderPermissionDecisionInput, type ProviderSessionEvent, type ProviderStatusUpdate, type RunnerSpawnConfig, type SpawnArgs, type TerminalAttachSpec } from "../adapter";
@@ -11,6 +11,7 @@ import { assembleLaunch, bundledCodexSkillDirs, bundledSkillConfigArgs, material
11
11
  import { logger } from "../logger";
12
12
  import type { SessionEvent } from "../session-insights";
13
13
  import { CodexAgentMessageCapture } from "./codex-agent-message-capture";
14
+ import { computeLivenessSignal, type LivenessInputs } from "../liveness";
14
15
 
15
16
  /** Relay context prepended to a Codex agent's first turn: the standard relay
16
17
  * blurb plus, when running in an isolated workspace, the deps caveat (#159). */
@@ -68,6 +69,16 @@ export class CodexAdapter implements ProviderAdapter {
68
69
  this.statusCb = cb;
69
70
  }
70
71
 
72
+ // §4.1 contract verdict (#746). Codex reference (#724): a Codex provider-turn can wedge
73
+ // while a long-running subprocess child (a `bun test` / build gate) is still alive and
74
+ // making real progress. That subprocess is tracked as a non-`provider-turn` work-kind, so
75
+ // `hasLiveWork` stays true and the turn is never mis-read as stuck; app-server item events
76
+ // also refresh `lastProviderEventAt` → lastProgressAt. The fold is shared; this override
77
+ // documents and owns Codex's progress sources behind the contract.
78
+ liveness(inputs: LivenessInputs): LivenessSignal {
79
+ return computeLivenessSignal(inputs);
80
+ }
81
+
71
82
  onSessionEvent(cb: (event: ProviderSessionEvent) => void): void {
72
83
  this.sessionEventCb = cb;
73
84
  }
@@ -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
+ }
package/src/quota.ts CHANGED
@@ -17,6 +17,8 @@ export {
17
17
  collectCodexQuotaSample,
18
18
  collectCodexQuotaState,
19
19
  codexRateLimitsQuotaState,
20
+ codexWhamResetCredits,
21
+ codexWhamUsageQuotaState,
20
22
  credentialAccountKey,
21
23
  defaultClaudeConfigDir,
22
24
  defaultClaudeCredentialsPath,
@@ -7,6 +7,7 @@ import { errMessage, RelayBusClient, RelayHttpClient } from "agent-relay-sdk";
7
7
  import { contextStateFromProbeMetrics, quotaStateFromProbeMetrics } from "agent-relay-sdk/context-probe";
8
8
  import { isPidAlive } from "agent-relay-sdk/process-utils";
9
9
  import { providerAttachmentText, providerMessageText } from "./adapter";
10
+ import { computeLivenessSignal, type LivenessInputs } from "./liveness";
10
11
  import type { ManagedProcess, ProviderAdapter, ProviderConfig, ProviderPermissionDecision, ProviderPermissionDecisionInput, ProviderSessionEvent, ProviderStatusUpdate, RunnerSpawnConfig, SemanticStatus, TerminalAttachSpec } from "./adapter";
11
12
  import { messagesWithCachedAttachments } from "./attachment-cache";
12
13
  import { ClaimTracker } from "./claim-tracker";
@@ -1878,6 +1879,19 @@ export class AgentRunner {
1878
1879
  this.bus.setSemanticStatus(status === "offline" || status === "error" ? "idle" : status);
1879
1880
  const timelineEvent = this.pendingTimelineEvent;
1880
1881
  this.pendingTimelineEvent = undefined;
1882
+ // §4.1 Runner↔provider liveness contract (#725 / #746): fold the raw transport/timeline/
1883
+ // token/work-kind signals into ONE normalized verdict the server reads instead of six raw
1884
+ // `meta` fields. The adapter owns what counts as progress for its provider (#713/#724);
1885
+ // mock adapters fall back to the shared fold.
1886
+ const livenessInputs: LivenessInputs = {
1887
+ transport: this.bus.transportState,
1888
+ ...(this.lastProviderEventAt !== undefined ? { lastProviderEventAt: this.lastProviderEventAt } : {}),
1889
+ ...(timelineEvent ? { timelineEvent } : {}),
1890
+ activeWork,
1891
+ };
1892
+ const liveness = this.options.adapter.liveness
1893
+ ? this.options.adapter.liveness(livenessInputs)
1894
+ : computeLivenessSignal(livenessInputs);
1881
1895
  this.bus.status({
1882
1896
  agentStatus,
1883
1897
  ready: agentStatus !== "offline" && !this.stopped,
@@ -1922,6 +1936,9 @@ export class AgentRunner {
1922
1936
  },
1923
1937
  ...(timelineEvent ? { timelineEvent } : {}),
1924
1938
  transport: this.bus.transportState,
1939
+ // #746 — normalized liveness verdict; the server reads ONLY this for stuck-busy.
1940
+ // The raw fields above stay for other consumers (timeline rendering, diagnostics).
1941
+ liveness,
1925
1942
  },
1926
1943
  });
1927
1944
  }