agent-relay-runner 0.30.1 → 0.31.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
- "version": "0.30.1",
3
+ "version": "0.31.1",
4
4
  "description": "Unified provider lifecycle runner for Agent Relay",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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.30.1",
4
+ "version": "0.31.1",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
package/src/config.ts CHANGED
@@ -1,7 +1,9 @@
1
+ import { execFileSync } from "node:child_process";
1
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
3
  import { homedir, hostname } from "node:os";
3
4
  import { join, resolve } from "node:path";
4
5
  import { DEFAULT_RELAY_URL, stringValue } from "agent-relay-sdk";
6
+ import type { WorkspaceMetadata } from "agent-relay-sdk";
5
7
  import { sanitizeFsName } from "agent-relay-sdk/fs-name";
6
8
  import type { ProviderConfig } from "./adapter";
7
9
 
@@ -101,6 +103,37 @@ export function runnerId(provider: string, cwd: string, label?: string): string
101
103
  return `${hostname()}-${provider}-${cleanLabel}-${crypto.randomUUID().slice(0, 8)}`;
102
104
  }
103
105
 
106
+ /**
107
+ * Stable project identifier for insights aggregation: the repo NAME, never a
108
+ * per-branch/per-session worktree dir or full path. Isolated workspace agents run
109
+ * from a session-specific worktree (…/workspaces/<repo>/<id>) whose basename is
110
+ * unique per session — using it would scatter one repo's data across many "projects"
111
+ * and break per-project rollups. So we resolve up to the main repo root, then take
112
+ * its basename. Falls back to the git toplevel of cwd (handles a direct agent
113
+ * launched in a subdir), then to the cwd basename for a non-git directory.
114
+ */
115
+ export function resolveProjectName(cwd: string, workspace?: WorkspaceMetadata): string {
116
+ const root =
117
+ workspace?.repoRoot ||
118
+ workspace?.probe?.repoRoot ||
119
+ gitToplevel(cwd) ||
120
+ workspace?.sourceCwd ||
121
+ cwd;
122
+ return root.split("/").filter(Boolean).at(-1) || "unknown";
123
+ }
124
+
125
+ function gitToplevel(cwd: string): string | undefined {
126
+ try {
127
+ const out = execFileSync("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
128
+ encoding: "utf8",
129
+ stdio: ["ignore", "pipe", "ignore"],
130
+ }).trim();
131
+ return out || undefined;
132
+ } catch {
133
+ return undefined;
134
+ }
135
+ }
136
+
104
137
  export function resolveCwd(value: string | undefined, fallback: string): string {
105
138
  return resolve(value || fallback);
106
139
  }
package/src/runner.ts CHANGED
@@ -7,6 +7,7 @@ import { errMessage, RelayBusClient, RelayHttpClient } from "agent-relay-sdk";
7
7
  import { contextStateFromProbeMetrics, readContextProbeState } from "agent-relay-sdk/context-probe";
8
8
  import type { ManagedProcess, ProviderAdapter, ProviderConfig, ProviderPermissionDecision, ProviderPermissionDecisionInput, ProviderSessionEvent, ProviderStatusUpdate, RunnerSpawnConfig, SemanticStatus, TerminalAttachSpec } from "./adapter";
9
9
  import { messagesWithCachedAttachments } from "./attachment-cache";
10
+ import { resolveProjectName } from "./config";
10
11
  import { ClaimTracker } from "./claim-tracker";
11
12
  import { startControlServer, type ControlServer } from "./control-server";
12
13
  import { ReplyObligationCache } from "./reply-obligation-cache";
@@ -129,7 +130,11 @@ const INTERRUPT_RECONCILE_DELAY_MS = 1_500;
129
130
  // Relay-injected content (delivered messages, memory context) is wrapped with
130
131
  // these markers; a UserPromptSubmit echo starting with one is a runner injection,
131
132
  // not a human typing into the terminal, so it must not be mirrored as a prompt.
132
- const RELAY_INJECTION_MARKERS = ["[relay message #", "[agent-relay"];
133
+ // `<task-notification>` is the harness wrapper for async monitor/task wake-ups
134
+ // (which embed a `[relay message #…]` deeper inside, defeating a plain prefix
135
+ // match) — it is emitted only by the harness, never typed by a human, so any
136
+ // echo starting with it is a system injection, not a user prompt (#289).
137
+ const RELAY_INJECTION_MARKERS = ["[relay message #", "[agent-relay", "<task-notification>"];
133
138
  // Reasoning tailer poll cadence (item 5). Coarse on purpose — reasoning is a
134
139
  // discreet progress signal, not a token stream, so ~1.2s keeps it light.
135
140
  const REASONING_POLL_MS = 1_200;
@@ -202,6 +207,9 @@ export class AgentRunner {
202
207
  // (transcript rotated, Codex buffer trimmed) resets the cursor.
203
208
  private insightsObserved = 0;
204
209
  private insightsCursorKey = "";
210
+ // Memoized repo-name project id for insight observations (resolved once; involves a
211
+ // git toplevel lookup for direct agents). Aggregates by repo, not per-session worktree.
212
+ private insightProjectName?: string;
205
213
  // Coalesces concurrent pre-session-destroy runs (e.g. the shutdown bus command and the
206
214
  // SessionEnd hook both fire for the same teardown) so the cursor isn't raced.
207
215
  private preDestroyPromise?: Promise<void>;
@@ -453,6 +461,10 @@ export class AgentRunner {
453
461
  AGENT_RELAY_RUNNER_ID: this.options.runnerId,
454
462
  AGENT_RELAY_PROVIDER_SESSION_ID: this.providerSessionId,
455
463
  AGENT_RELAY_ID: this.agentId,
464
+ // The repo-name project id the runner uses for insight observations. Exposed so
465
+ // agent-side signals (e.g. /introspect) tag the SAME project and aggregate together,
466
+ // instead of the agent's worktree cwd splitting one repo into many "projects".
467
+ AGENT_RELAY_PROJECT: this.insightProject(),
456
468
  ...(this.scratch ? { AGENT_RELAY_SCRATCH_DIR: this.scratch.tmpDir } : {}),
457
469
  AGENT_RELAY_URL: this.options.relayUrl,
458
470
  AGENT_RELAY_APPROVAL: this.options.approvalMode,
@@ -1217,7 +1229,7 @@ export class AgentRunner {
1217
1229
  kind: "insight",
1218
1230
  payload: {
1219
1231
  sessionId: this.providerSessionId,
1220
- project: this.options.cwd,
1232
+ project: this.insightProject(),
1221
1233
  agentId: this.agentId,
1222
1234
  signal: "hook_fatal",
1223
1235
  value: { hook: report.hook, error: report.error },
@@ -1266,6 +1278,17 @@ export class AgentRunner {
1266
1278
  : input.reason === "clear" ? "clear"
1267
1279
  : "shutdown";
1268
1280
  await this.runPreSessionDestroy(reason, { transcriptPath: input.transcriptPath });
1281
+ // clear/compact CONTINUE the session — the finalizing window is transient. The bus-command
1282
+ // path (handleCommand) restores the addressable status in its finally; the hook path has no
1283
+ // such teardown, so without this the dashboard stays stuck on "wrapping up — messaging
1284
+ // paused" with the composer disabled forever. Only restore if we're still in the exact
1285
+ // finalizing state we published — a concurrent bus command (kill/restart/shutdown) may have
1286
+ // transitioned lifecycleAction since, and that must win. "shutdown" reasons end the session,
1287
+ // so their finalizing state is superseded by going offline; leave it be.
1288
+ if ((reason === "clear" || reason === "compact") && this.lifecycleAction === `finalizing-${reason}` && !this.stopped) {
1289
+ this.lifecycleAction = undefined;
1290
+ this.publishStatus();
1291
+ }
1269
1292
  }
1270
1293
 
1271
1294
  // The pre-session-destroy seam (#183): the single place end-of-session work runs before
@@ -1305,6 +1328,13 @@ export class AgentRunner {
1305
1328
  // into the shared SessionEvent stream; the math + classifier live in session-insights.ts.
1306
1329
  // Per-segment via a runner-side cursor, so each datapoint is one work chunk between
1307
1330
  // context resets. Mechanical, model-free → zero agent tokens, un-gameable.
1331
+ // Repo-name project id for insight observations, resolved once. Aggregating by the raw
1332
+ // cwd would split one repo across many "projects" (each isolated agent runs from a
1333
+ // unique per-session worktree dir). See resolveProjectName.
1334
+ private insightProject(): string {
1335
+ return (this.insightProjectName ??= resolveProjectName(this.options.cwd, this.options.workspace));
1336
+ }
1337
+
1308
1338
  private async captureContextRatio(reason: SessionDestroyReason, opts?: { transcriptPath?: string }): Promise<void> {
1309
1339
  const adapter = this.options.adapter;
1310
1340
  if (!adapter.collectSessionEvents || !this.process) return;
@@ -1326,7 +1356,7 @@ export class AgentRunner {
1326
1356
  kind: "insight",
1327
1357
  payload: {
1328
1358
  sessionId: this.providerSessionId,
1329
- project: this.options.cwd,
1359
+ project: this.insightProject(),
1330
1360
  agentId: this.agentId,
1331
1361
  signal: "context_ratio",
1332
1362
  value: { ...analysis.metric, endReason: reason },