agent-relay-runner 0.129.11 → 0.129.13

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.
@@ -299,6 +299,16 @@ export function shouldLogDeliveryFailure(error: unknown): boolean {
299
299
  && message !== CODEX_TURN_IN_PROGRESS_MESSAGE;
300
300
  }
301
301
 
302
+ /** The same rejection, seen from the COMMAND side (#1565 F1): a serialized boundary payload that
303
+ * met the turn the payload before it started is deferred until that turn ends, not failed. */
304
+ export function isTurnInProgressDeliveryError(error: unknown): boolean {
305
+ return errMessage(error) === CODEX_TURN_IN_PROGRESS_MESSAGE;
306
+ }
307
+
308
+ /** Command types that put a payload into the provider's single input channel, and therefore take a
309
+ * `ProviderInputQueue` ticket at dispatch so two of them can never interleave (#1565 F1). */
310
+ export const PROVIDER_INPUT_COMMAND_TYPES: ReadonlySet<string> = new Set(["agent.injectContext", "prompt.inject"]);
311
+
302
312
  export function runtimeTokenRenewDelayMs(expiresAtSeconds: number, nowMs = Date.now()): number | undefined {
303
313
  const expiresAtMs = expiresAtSeconds * 1000;
304
314
  const ttlMs = expiresAtMs - nowMs;
@@ -0,0 +1,83 @@
1
+ import { readFileSync, statSync } from "node:fs";
2
+ import type { ContextState } from "agent-relay-sdk";
3
+ import type { ProviderContextUsage } from "./adapter";
4
+
5
+ // #1512: Claude reports context only through its out-of-band status-line probe,
6
+ // which freezes after an in-process compact/clear/native-resume (no new process,
7
+ // so the probe file stops advancing while the agent keeps taking turns). The
8
+ // transcript — handed to the runner every turn by the Stop hook — carries the
9
+ // same per-turn token usage, so the runner derives fresh context from it here as
10
+ // a probe-independent fallback that keeps context.lastUpdatedAt advancing across
11
+ // those in-process generation bumps. Kept out of runner-core to hold that giant's
12
+ // line budget (epic #291); runner-core just threads its per-session cache through.
13
+
14
+ export interface TranscriptContextCache {
15
+ path: string;
16
+ mtimeMs: number;
17
+ /** Byte size alongside mtime: a same-second rewrite (coarse mtime granularity) still changes length. */
18
+ size: number;
19
+ usage: ProviderContextUsage | undefined;
20
+ }
21
+
22
+ export interface TranscriptContextInput {
23
+ /** Latest transcript path the Stop hook reported this session. */
24
+ transcriptPath: string | undefined;
25
+ /** The transcript-backed adapter's per-turn usage parser (absent for others). */
26
+ derive: ((jsonl: string) => ProviderContextUsage | undefined) | undefined;
27
+ /** Context-window size — stable for the session, so reuse what the probe saw. */
28
+ tokensMax: number | undefined;
29
+ /** Prior parse, reused when the transcript's (mtime, size) is unchanged (large files). */
30
+ cache: TranscriptContextCache | undefined;
31
+ now: number;
32
+ }
33
+
34
+ // Returns the derived context (or undefined when it can't be computed) and the
35
+ // cache to carry forward. The context-window size is stable for a session, so we
36
+ // reuse the tokensMax the status-line probe reported at least once rather than
37
+ // re-deriving it from a model catalog the runner doesn't hold; the transcript
38
+ // supplies the per-turn tokensUsed. The parse is cached by transcript (mtime, size)
39
+ // so a large log isn't re-read every 30s heartbeat while nothing new has been
40
+ // written. Size is paired with mtime because an in-process compact/clear/resume can
41
+ // rewrite the transcript within the same coarse mtime tick (1s on some filesystems),
42
+ // which mtime alone would miss — leaving stale context; the length still changes.
43
+ export function transcriptDerivedContext(input: TranscriptContextInput): {
44
+ context: ContextState | undefined;
45
+ cache: TranscriptContextCache | undefined;
46
+ } {
47
+ const { transcriptPath, derive, tokensMax, cache, now } = input;
48
+ if (!transcriptPath || !derive) return { context: undefined, cache };
49
+ if (typeof tokensMax !== "number" || !Number.isFinite(tokensMax) || tokensMax <= 0) return { context: undefined, cache };
50
+
51
+ let nextCache = cache;
52
+ let usage: ProviderContextUsage | undefined;
53
+ try {
54
+ const stat = statSync(transcriptPath);
55
+ const mtimeMs = stat.mtimeMs;
56
+ const size = stat.size;
57
+ if (cache?.path === transcriptPath && cache.mtimeMs === mtimeMs && cache.size === size) {
58
+ usage = cache.usage;
59
+ } else {
60
+ usage = derive(readFileSync(transcriptPath, "utf8"));
61
+ nextCache = { path: transcriptPath, mtimeMs, size, usage };
62
+ }
63
+ } catch {
64
+ return { context: undefined, cache };
65
+ }
66
+ if (!usage) return { context: undefined, cache: nextCache };
67
+
68
+ return {
69
+ context: {
70
+ utilization: Math.max(0, Math.min(1, usage.tokensUsed / tokensMax)),
71
+ tokensUsed: usage.tokensUsed,
72
+ tokensMax,
73
+ lifecycleState: "working",
74
+ warmTopics: [],
75
+ activeMemories: [],
76
+ tasksSinceCompact: 0,
77
+ lastUpdatedAt: usage.occurredAt ?? now,
78
+ source: "api",
79
+ confidence: "reported",
80
+ },
81
+ cache: nextCache,
82
+ };
83
+ }
@@ -0,0 +1,48 @@
1
+ import { RECONCILED_TURN_TIMELINE_STATUS } from "agent-relay-sdk";
2
+
3
+ // Same shape the runner publishes (structurally identical to the local declarations in
4
+ // runner-core.ts / native-self-resume.ts, which is how the other timeline producers do it).
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
+ /**
16
+ * #1621 — the announcement for a provider-turn claim the busy backstop force-cleared.
17
+ *
18
+ * Reconciliation is a real state change, not bookkeeping repair: the turn the coordinator is
19
+ * waiting on is over and produced no reply, and `busy` is exactly the signal it used to decide
20
+ * NOT to intervene. So the clear is EMITTED (the relay turns this event into an activity row plus
21
+ * an `agent.turn_interrupted` notification for the spawn parent) instead of silently correcting
22
+ * the card — a quietly-fixed card still leaves the coordinator's plan stalled on a turn that
23
+ * ended. Pure builder so the payload the parent's notice is derived from is unit-testable without
24
+ * standing up an AgentRunner.
25
+ */
26
+ export function reconciledTurnTimelineEvent(input: {
27
+ turnId: string;
28
+ reason: string;
29
+ startedAt: number;
30
+ now?: number;
31
+ }): RunnerTimelineEvent {
32
+ const now = input.now ?? Date.now();
33
+ return {
34
+ status: RECONCILED_TURN_TIMELINE_STATUS,
35
+ id: `provider-turn-reconciled-${input.turnId}`,
36
+ timestamp: now,
37
+ title: "Provider turn cleared by reconciliation",
38
+ body: `The provider turn ended without a turn-end signal (${input.reason}); the agent was observed idle at its prompt and is no longer busy.`,
39
+ icon: "ti-alert-triangle",
40
+ metadata: {
41
+ eventType: RECONCILED_TURN_TIMELINE_STATUS,
42
+ reconcileReason: input.reason,
43
+ turnId: input.turnId,
44
+ turnStartedAt: input.startedAt,
45
+ turnAgeMs: Math.max(0, now - input.startedAt),
46
+ },
47
+ };
48
+ }