agent-relay-runner 0.30.1 → 0.31.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 +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/src/config.ts +33 -0
- package/src/runner.ts +33 -3
package/package.json
CHANGED
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
|
-
|
|
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.
|
|
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.
|
|
1359
|
+
project: this.insightProject(),
|
|
1330
1360
|
agentId: this.agentId,
|
|
1331
1361
|
signal: "context_ratio",
|
|
1332
1362
|
value: { ...analysis.metric, endReason: reason },
|