agent-relay-server 0.99.1 → 0.100.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 +5 -2
- package/public/assets/{MessageBubble-DiorZXyB.js → MessageBubble-DZ3cUoHO.js} +2 -2
- package/public/assets/{MessageBubble-DiorZXyB.js.map → MessageBubble-DZ3cUoHO.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-DY1p8v4p.js → PlanGraphPanel-B6G_RSZ3.js} +2 -2
- package/public/assets/{PlanGraphPanel-DY1p8v4p.js.map → PlanGraphPanel-B6G_RSZ3.js.map} +1 -1
- package/public/assets/{activity-rIxqUtFC.js → activity-BefDMYiE.js} +2 -2
- package/public/assets/{activity-rIxqUtFC.js.map → activity-BefDMYiE.js.map} +1 -1
- package/public/assets/{agent-profiles-Cu-VkJm4.js → agent-profiles-D5VW943m.js} +2 -2
- package/public/assets/agent-profiles-D5VW943m.js.map +1 -0
- package/public/assets/agents-CtsNY05B.js +2 -0
- package/public/assets/agents-CtsNY05B.js.map +1 -0
- package/public/assets/{automation-B0cyCT5l.js → automation-B5TFhCGs.js} +2 -2
- package/public/assets/{automation-B0cyCT5l.js.map → automation-B5TFhCGs.js.map} +1 -1
- package/public/assets/branch-state-badge-BLF9LPtu.js +2 -0
- package/public/assets/branch-state-badge-BLF9LPtu.js.map +1 -0
- package/public/assets/chat-CoZcF6dA.js +2 -0
- package/public/assets/chat-CoZcF6dA.js.map +1 -0
- package/public/assets/{formatted-body-impl-ZwgTH3jh.js → formatted-body-impl-Zk5WjWS7.js} +2 -2
- package/public/assets/{formatted-body-impl-ZwgTH3jh.js.map → formatted-body-impl-Zk5WjWS7.js.map} +1 -1
- package/public/assets/{index-GoC6YrKn.js → index-B4cTW4A9.js} +5 -5
- package/public/assets/index-B4cTW4A9.js.map +1 -0
- package/public/assets/{insights-C3mlXZdd.js → insights-C8iC-IB1.js} +2 -2
- package/public/assets/{insights-C3mlXZdd.js.map → insights-C8iC-IB1.js.map} +1 -1
- package/public/assets/{maintenance-DL1lnqmG.js → maintenance-pl6U2a_y.js} +2 -2
- package/public/assets/{maintenance-DL1lnqmG.js.map → maintenance-pl6U2a_y.js.map} +1 -1
- package/public/assets/{managed-agents-7A6WH4Rn.js → managed-agents-1CynkXEy.js} +2 -2
- package/public/assets/{managed-agents-7A6WH4Rn.js.map → managed-agents-1CynkXEy.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-THat8xv0.js → markdown-preview-impl-Dj6MExTR.js} +2 -2
- package/public/assets/{markdown-preview-impl-THat8xv0.js.map → markdown-preview-impl-Dj6MExTR.js.map} +1 -1
- package/public/assets/{memory-C349UJe9.js → memory-DyWT5mwx.js} +2 -2
- package/public/assets/{memory-C349UJe9.js.map → memory-DyWT5mwx.js.map} +1 -1
- package/public/assets/{messages-DXoLFj2P.js → messages-LqXFX2yc.js} +2 -2
- package/public/assets/{messages-DXoLFj2P.js.map → messages-LqXFX2yc.js.map} +1 -1
- package/public/assets/{orchestrators-CTXu4jAn.js → orchestrators-xdpBJOt1.js} +2 -2
- package/public/assets/{orchestrators-CTXu4jAn.js.map → orchestrators-xdpBJOt1.js.map} +1 -1
- package/public/assets/{overview-dWiN6TyN.js → overview-BDetOVf7.js} +2 -2
- package/public/assets/{overview-dWiN6TyN.js.map → overview-BDetOVf7.js.map} +1 -1
- package/public/assets/{pairs-KIXektDv.js → pairs-BxVd-YVM.js} +2 -2
- package/public/assets/{pairs-KIXektDv.js.map → pairs-BxVd-YVM.js.map} +1 -1
- package/public/assets/{registry-C-Y41yGB.js → registry-BeoPmyhz.js} +2 -2
- package/public/assets/{registry-C-Y41yGB.js.map → registry-BeoPmyhz.js.map} +1 -1
- package/public/assets/{security-BUymkwuj.js → security-DKDgxf96.js} +2 -2
- package/public/assets/{security-BUymkwuj.js.map → security-DKDgxf96.js.map} +1 -1
- package/public/assets/{select-DV25WRP5.js → select-DpRN9C_1.js} +2 -2
- package/public/assets/{select-DV25WRP5.js.map → select-DpRN9C_1.js.map} +1 -1
- package/public/assets/{settings-C1En58bx.js → settings-D_mLzA5O.js} +2 -2
- package/public/assets/{settings-C1En58bx.js.map → settings-D_mLzA5O.js.map} +1 -1
- package/public/assets/{tasks-BkpnKA63.js → tasks-D5OVfd-K.js} +2 -2
- package/public/assets/{tasks-BkpnKA63.js.map → tasks-D5OVfd-K.js.map} +1 -1
- package/public/assets/{teams-D-UYp7yH.js → teams-CFJM0XlG.js} +2 -2
- package/public/assets/{teams-D-UYp7yH.js.map → teams-CFJM0XlG.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-Bif-tkvD.js → terminal-viewer-impl-B-2tqoEB.js} +2 -2
- package/public/assets/{terminal-viewer-impl-Bif-tkvD.js.map → terminal-viewer-impl-B-2tqoEB.js.map} +1 -1
- package/public/assets/{user-avatar--czLv_AJ.js → user-avatar-BGXTBaME.js} +2 -2
- package/public/assets/{user-avatar--czLv_AJ.js.map → user-avatar-BGXTBaME.js.map} +1 -1
- package/public/assets/{work-queue-DN4oTe7w.js → work-queue-DMwtDbBg.js} +2 -2
- package/public/assets/{work-queue-DN4oTe7w.js.map → work-queue-DMwtDbBg.js.map} +1 -1
- package/public/index.html +1 -1
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/runner/plugins/claude/hooks/kill-guard.sh +300 -0
- package/runner/plugins/claude/hooks/read-only-bash-guard.sh +38 -140
- package/runner/src/adapter.ts +2 -2
- package/src/agent-death-diagnosis.ts +12 -3
- package/src/bus.ts +6 -45
- package/src/config-store.ts +13 -9
- package/src/db/provider-quota-burn.ts +2 -1
- package/src/issue-tracker/adapter.ts +11 -1
- package/src/issue-tracker/github.ts +83 -2
- package/src/issue-tracker/index.ts +1 -0
- package/src/lifecycle-manager.ts +9 -22
- package/src/provider-state.ts +9 -18
- package/src/provider-timeline-event.ts +70 -0
- package/src/quota-advisory.ts +4 -7
- package/src/routes/agent-sessions.ts +4 -2
- package/src/services/issue-lifecycle.ts +54 -8
- package/src/services/provider-state-audit.ts +3 -3
- package/src/services/rate-limit-resume.ts +6 -4
- package/src/services/register-agent.ts +2 -1
- package/src/spawn-targets.ts +1 -0
- package/src/sse.ts +5 -4
- package/public/assets/agent-profiles-Cu-VkJm4.js.map +0 -1
- package/public/assets/agents-dRHOBDUe.js +0 -2
- package/public/assets/agents-dRHOBDUe.js.map +0 -1
- package/public/assets/branch-state-badge-CNko2_BL.js +0 -2
- package/public/assets/branch-state-badge-CNko2_BL.js.map +0 -1
- package/public/assets/chat-jit-e6S_.js +0 -2
- package/public/assets/chat-jit-e6S_.js.map +0 -1
- package/public/assets/index-GoC6YrKn.js.map +0 -1
package/runner/src/adapter.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentProfile, LivenessSignal, Message } from "agent-relay-sdk";
|
|
1
|
+
import type { AgentProfile, LivenessSignal, Message, ProviderState } from "agent-relay-sdk";
|
|
2
2
|
import { isRecord } from "agent-relay-sdk";
|
|
3
3
|
import type { LivenessInputs } from "./liveness";
|
|
4
4
|
import type { SessionEvent } from "./session-insights";
|
|
@@ -30,7 +30,7 @@ export interface ProviderStatusEvent {
|
|
|
30
30
|
// runner reconciles them against actual liveness and clears the busy marker if
|
|
31
31
|
// every pid is dead — recovering from a tracked process killed out-of-band.
|
|
32
32
|
pids?: number[];
|
|
33
|
-
providerState?:
|
|
33
|
+
providerState?: ProviderState;
|
|
34
34
|
metadata?: Record<string, unknown>;
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
// deterministic and unit-testable per failure mode. The output rides the #633 terminal event and
|
|
7
7
|
// is persisted as a CrashReport so the WHY survives the reaper.
|
|
8
8
|
|
|
9
|
-
import { extractClaudeModelUnavailableMessage } from "agent-relay-sdk";
|
|
10
9
|
import type { AgentCard, AgentDeathCause, AgentDeathDiagnosis, CrashReport, ManagedSessionExitDiagnostics } from "./types";
|
|
11
10
|
|
|
12
11
|
export interface DeathEvidence {
|
|
@@ -87,6 +86,16 @@ function haystack(ev: DeathEvidence): string {
|
|
|
87
86
|
return parts.join("\n");
|
|
88
87
|
}
|
|
89
88
|
|
|
89
|
+
function terminalModelUnavailableMessage(meta: Record<string, unknown> | undefined): string | undefined {
|
|
90
|
+
const terminalReason = str(meta?.terminalFailureReason);
|
|
91
|
+
const terminalMessage = str(meta?.terminalFailureMessage);
|
|
92
|
+
if (terminalReason === "model-unavailable" && terminalMessage) return terminalMessage;
|
|
93
|
+
const providerState = record(meta, "providerState");
|
|
94
|
+
const stateReason = str(providerState?.reason);
|
|
95
|
+
const stateMessage = str(providerState?.message);
|
|
96
|
+
return stateReason === "model-unavailable" && stateMessage ? stateMessage : undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
90
99
|
function logTailOf(ev: DeathEvidence): string[] | undefined {
|
|
91
100
|
if (ev.exit?.logTail?.length) return ev.exit.logTail;
|
|
92
101
|
const lastExit = record(ev.agentMeta, "lastExit");
|
|
@@ -159,8 +168,8 @@ export function classifyAgentDeath(ev: DeathEvidence): AgentDeathDiagnosis {
|
|
|
159
168
|
return verdict("onboarding-gate", fast ? "high" : "medium", "Stuck at a first-run onboarding/trust gate");
|
|
160
169
|
}
|
|
161
170
|
|
|
162
|
-
// 4) provider-crash — CLI panic/segfault/model-unavailable, or a crash signal.
|
|
163
|
-
const modelUnavailable =
|
|
171
|
+
// 4) provider-crash — CLI panic/segfault/adapter-classified model-unavailable, or a crash signal.
|
|
172
|
+
const modelUnavailable = terminalModelUnavailableMessage(ev.agentMeta);
|
|
164
173
|
if (modelUnavailable) {
|
|
165
174
|
pushUnique(evidence, modelUnavailable);
|
|
166
175
|
return verdict("provider-crash", "high", "Provider unavailable (model/CLI error)");
|
package/src/bus.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Server, ServerWebSocket } from "bun";
|
|
2
|
-
import { ValidationError,
|
|
2
|
+
import { ValidationError, getAgent, getDb, heartbeat, markReady, mergeAgentMeta, orphanTasksForAgent, revokeRuntimeTokensForAgent, setStatus, validateAgentSession } from "./db";
|
|
3
3
|
import { getOldestOutboxCursor, getOutboxCursor, replayEvents, type BusEvent } from "./bus-outbox";
|
|
4
4
|
import { projectAgentStatusData } from "./agent-card-projection";
|
|
5
5
|
import { emitRelayEvent, subscribeRelayEvents, type RelayEvent } from "./events";
|
|
@@ -11,6 +11,7 @@ import { registerAgent } from "./services/register-agent";
|
|
|
11
11
|
import { authContextFromBus } from "./services/auth-context";
|
|
12
12
|
import { commandAuthorizationResource, dispatchCommand } from "./services/dispatch-command";
|
|
13
13
|
import { auditProviderStateTransition } from "./services/provider-state-audit";
|
|
14
|
+
import { agentProviderTimelineEvent, auditRunnerTimelineEvent } from "./provider-timeline-event";
|
|
14
15
|
import { ServiceAuthError } from "./services/errors";
|
|
15
16
|
import { ShutdownAuthError, ShutdownTargetError, shutdownAgent, shutdownInputFromBusFrame } from "./services/shutdown-agent";
|
|
16
17
|
import { flushQueuedDirectMessages } from "./services/managed-running";
|
|
@@ -28,7 +29,7 @@ import {
|
|
|
28
29
|
type BusFrame,
|
|
29
30
|
type RegisterFrame,
|
|
30
31
|
} from "agent-relay-sdk/protocol";
|
|
31
|
-
import { errMessage, isRecord
|
|
32
|
+
import { errMessage, isRecord } from "agent-relay-sdk";
|
|
32
33
|
import { getComponentAuth, isComponentAuthorizedFor, isAuthorized, isOriginAllowed, unauthorized } from "./security";
|
|
33
34
|
import type { Command, ComponentToken, ContextState, Message, ProviderCapabilities, QuotaState, RegisterAgentInput, Task } from "./types";
|
|
34
35
|
|
|
@@ -189,10 +190,11 @@ function handleFrame(ws: BusWebSocket, frame: ReturnType<typeof validateClientFr
|
|
|
189
190
|
}
|
|
190
191
|
const after = getAgent(conn.agentId);
|
|
191
192
|
auditProviderStateTransition(conn.agentId, before, after);
|
|
192
|
-
|
|
193
|
+
const timelineEvent = agentProviderTimelineEvent(after);
|
|
194
|
+
auditRunnerTimelineEvent(conn.agentId, timelineEvent);
|
|
193
195
|
// A real PreCompact/SessionStart hook arrives as a timelineEvent in the
|
|
194
196
|
// merged meta — clears any pending stall watch (stale events ignored).
|
|
195
|
-
noteAgentTimelineEvent(conn.agentId,
|
|
197
|
+
noteAgentTimelineEvent(conn.agentId, timelineEvent);
|
|
196
198
|
emitAgentStatusEvent(conn.agentId);
|
|
197
199
|
// #237 stop-hook → instant ⚪↔🟡 changes detection on the turn-end edge.
|
|
198
200
|
probeWorkspaceOnTurnEnd(before, after);
|
|
@@ -568,47 +570,6 @@ function emitAgentStatusEvent(agentId: string): void {
|
|
|
568
570
|
});
|
|
569
571
|
}
|
|
570
572
|
|
|
571
|
-
function auditRunnerTimelineEvent(agentId: string, timelineEvent: unknown): void {
|
|
572
|
-
if (!isRecord(timelineEvent)) return;
|
|
573
|
-
const metadata = isRecord(timelineEvent.metadata) ? timelineEvent.metadata : {};
|
|
574
|
-
if (metadata.source !== "runner") return;
|
|
575
|
-
const status = stringValue(timelineEvent.status);
|
|
576
|
-
if (!status) return;
|
|
577
|
-
const eventType = stringValue(metadata.eventType) ?? status;
|
|
578
|
-
const timestamp = numberValue(timelineEvent.timestamp) ?? Date.now();
|
|
579
|
-
const id = stringValue(timelineEvent.id) ?? `${eventType}-${timestamp}`;
|
|
580
|
-
const title = stringValue(timelineEvent.title) ?? status.replace(/[._-]+/g, " ");
|
|
581
|
-
const body = stringValue(timelineEvent.body);
|
|
582
|
-
const icon = stringValue(timelineEvent.icon) ?? "ti-activity";
|
|
583
|
-
try {
|
|
584
|
-
const event = createActivityEvent({
|
|
585
|
-
clientId: `runner-timeline-${agentId}-${id}`,
|
|
586
|
-
kind: "state",
|
|
587
|
-
title,
|
|
588
|
-
body,
|
|
589
|
-
meta: agentId,
|
|
590
|
-
icon,
|
|
591
|
-
view: "agents",
|
|
592
|
-
agentId,
|
|
593
|
-
metadata: {
|
|
594
|
-
...metadata,
|
|
595
|
-
eventType,
|
|
596
|
-
timelineStatus: status,
|
|
597
|
-
timelineId: id,
|
|
598
|
-
timelineTimestamp: timestamp,
|
|
599
|
-
},
|
|
600
|
-
});
|
|
601
|
-
emitRelayEvent({
|
|
602
|
-
type: "activity.created",
|
|
603
|
-
source: "server",
|
|
604
|
-
subject: String(event.id),
|
|
605
|
-
data: event as unknown as Record<string, unknown>,
|
|
606
|
-
});
|
|
607
|
-
} catch {
|
|
608
|
-
// Timeline writes must never block bus status updates.
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
573
|
function sendCommandResult(
|
|
613
574
|
ws: BusWebSocket,
|
|
614
575
|
commandId: string,
|
package/src/config-store.ts
CHANGED
|
@@ -72,15 +72,15 @@ const BUILT_IN_AGENT_PROFILE_NAMES = new Set([DEFAULT_SPAWN_PROFILE_NAME, "minim
|
|
|
72
72
|
const WORKER_TOOL_GROUPS = ["core", "workspace", "memory", "artifacts", "blackboard", "subscriptions"];
|
|
73
73
|
|
|
74
74
|
const BUILT_IN_AGENT_PROFILES: AgentProfile[] = [
|
|
75
|
-
agentProfileDefaults({ name: DEFAULT_SPAWN_PROFILE_NAME, description: "Relay-aware spawned agent with bundled Relay context, skills, plugins, and status integration.", base: "host", mcp: { mode: "none" }, relay: { tools: { groups: WORKER_TOOL_GROUPS } } }),
|
|
76
|
-
agentProfileDefaults({ name: "minimal", description: "Repo-focused spawned agent with Relay affordances disabled and host/global extras avoided where supported.", base: "minimal" }),
|
|
77
|
-
agentProfileDefaults({ name: "isolated-research", description: "Strict profile for autonomous research runs: repo context only, no Relay prompt surface, no global extras where supported.", base: "isolated" }),
|
|
78
|
-
agentProfileDefaults({ name: "vanilla", description: "Zero host bleed (#552): isolated provider home with host plugins, skills, hooks, MCP, and the CLAUDE.md/AGENTS.md cascade (repo + global) all suppressed at launch. Relay provisions any surface explicitly per profile.", base: "vanilla" }),
|
|
79
|
-
agentProfileDefaults({ name: "lean-worker", description: "Lean worker profile (#570): vanilla host-dependency drop with only Relay context/MCP restored and Project knowledge injected from the spawn cwd.", base: "vanilla", relay: { context: true, skills: false, plugins: false, statusLine: false, mcp: true, tools: { groups: WORKER_TOOL_GROUPS } }, projectInstructions: { policy: "all" } }),
|
|
80
|
-
agentProfileDefaults({ name: "coordinator", description: "Relay-aware coordination lead for spawning worker teams, maintaining shared state, and driving review and release.", base: "host", instructions: { append: [COORDINATION_CHARTER], repoInstructions: "allow", globalInstructions: "allow" }, maxSpawnedAgents: 8 }),
|
|
81
|
-
agentProfileDefaults({ name: "ops-assistant", description: "Relay-aware ops lieutenant for a coordinator: runs tests, releases, smoke probes, gates, and host checks, then reports crisp outcomes back up.", provider: "codex", base: "host", instructions: { append: [OPS_ASSISTANT_BRIEF], repoInstructions: "allow", globalInstructions: "allow" }, permissions: { mode: "open", filesystem: "host" } }),
|
|
82
|
-
agentProfileDefaults({ name: "worker", description: "Relay-aware focused implementer with open approval posture and no delegated spawn capability.", base: "host", mcp: { mode: "none" }, permissions: { mode: "open", filesystem: "host" }, relay: { tools: { groups: WORKER_TOOL_GROUPS } } }),
|
|
83
|
-
agentProfileDefaults({ name: "reviewer", description: "Relay-aware read-only reviewer for audit and review passes without file writes.", base: "host", mcp: { mode: "none" }, permissions: { mode: "read-only", filesystem: "host" }, relay: { tools: { groups: WORKER_TOOL_GROUPS } } }),
|
|
75
|
+
agentProfileDefaults({ name: DEFAULT_SPAWN_PROFILE_NAME, icon: "🤖", description: "Relay-aware spawned agent with bundled Relay context, skills, plugins, and status integration.", base: "host", mcp: { mode: "none" }, relay: { tools: { groups: WORKER_TOOL_GROUPS } } }),
|
|
76
|
+
agentProfileDefaults({ name: "minimal", icon: "▫️", description: "Repo-focused spawned agent with Relay affordances disabled and host/global extras avoided where supported.", base: "minimal" }),
|
|
77
|
+
agentProfileDefaults({ name: "isolated-research", icon: "🔬", description: "Strict profile for autonomous research runs: repo context only, no Relay prompt surface, no global extras where supported.", base: "isolated" }),
|
|
78
|
+
agentProfileDefaults({ name: "vanilla", icon: "🫥", description: "Zero host bleed (#552): isolated provider home with host plugins, skills, hooks, MCP, and the CLAUDE.md/AGENTS.md cascade (repo + global) all suppressed at launch. Relay provisions any surface explicitly per profile.", base: "vanilla" }),
|
|
79
|
+
agentProfileDefaults({ name: "lean-worker", icon: "🪶", description: "Lean worker profile (#570): vanilla host-dependency drop with only Relay context/MCP restored and Project knowledge injected from the spawn cwd.", base: "vanilla", relay: { context: true, skills: false, plugins: false, statusLine: false, mcp: true, tools: { groups: WORKER_TOOL_GROUPS } }, projectInstructions: { policy: "all" } }),
|
|
80
|
+
agentProfileDefaults({ name: "coordinator", icon: "🧭", description: "Relay-aware coordination lead for spawning worker teams, maintaining shared state, and driving review and release.", base: "host", instructions: { append: [COORDINATION_CHARTER], repoInstructions: "allow", globalInstructions: "allow" }, maxSpawnedAgents: 8 }),
|
|
81
|
+
agentProfileDefaults({ name: "ops-assistant", icon: "🛠️", description: "Relay-aware ops lieutenant for a coordinator: runs tests, releases, smoke probes, gates, and host checks, then reports crisp outcomes back up.", provider: "codex", base: "host", instructions: { append: [OPS_ASSISTANT_BRIEF], repoInstructions: "allow", globalInstructions: "allow" }, permissions: { mode: "open", filesystem: "host" } }),
|
|
82
|
+
agentProfileDefaults({ name: "worker", icon: "🔨", description: "Relay-aware focused implementer with open approval posture and no delegated spawn capability.", base: "host", mcp: { mode: "none" }, permissions: { mode: "open", filesystem: "host" }, relay: { tools: { groups: WORKER_TOOL_GROUPS } } }),
|
|
83
|
+
agentProfileDefaults({ name: "reviewer", icon: "🔍", description: "Relay-aware read-only reviewer for audit and review passes without file writes.", base: "host", mcp: { mode: "none" }, permissions: { mode: "read-only", filesystem: "host" }, relay: { tools: { groups: WORKER_TOOL_GROUPS } } }),
|
|
84
84
|
].map((profile) => ({ ...profile, builtIn: true }));
|
|
85
85
|
|
|
86
86
|
interface ConfigRow {
|
|
@@ -286,6 +286,7 @@ function agentProfileDefaults(input: Pick<AgentProfile, "name" | "base"> & Omit<
|
|
|
286
286
|
return {
|
|
287
287
|
name: input.name,
|
|
288
288
|
description: input.description,
|
|
289
|
+
...(input.icon === undefined ? {} : { icon: input.icon }),
|
|
289
290
|
provider: input.provider ?? "any",
|
|
290
291
|
base: input.base,
|
|
291
292
|
builtIn: input.builtIn,
|
|
@@ -361,6 +362,9 @@ function validateAgentProfile(key: string, value: unknown, opts: { allowBuiltIn?
|
|
|
361
362
|
return agentProfileDefaults({
|
|
362
363
|
...defaults,
|
|
363
364
|
description: cleanString(value.description, "description", { max: 1000 }),
|
|
365
|
+
// #751: per-profile emoji icon. Short free string (single emoji incl. ZWJ
|
|
366
|
+
// sequences); cap length so it can't smuggle arbitrary text. Empty → undefined.
|
|
367
|
+
icon: cleanString(value.icon, "icon", { max: 8 }) || undefined,
|
|
364
368
|
provider: value.provider === undefined || value.provider === null
|
|
365
369
|
? defaults.provider
|
|
366
370
|
: cleanEnum(value.provider, "provider", VALID_PROFILE_PROVIDERS),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { quotaWindowMetrics } from "agent-relay-sdk/provider-quota";
|
|
1
2
|
import type { ProviderQuotaBurn, ProviderQuotaBurnRole, ProviderQuotaBurnWindow, ProviderQuotaSnapshot, QuotaState } from "../types";
|
|
2
3
|
|
|
3
4
|
const QUOTA_BURN_MIN_SAMPLE_SPAN_MS = 10 * 60_000;
|
|
@@ -78,7 +79,7 @@ function deriveProviderQuotaBurnWindow(input: {
|
|
|
78
79
|
return low;
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
const remainingQuota =
|
|
82
|
+
const remainingQuota = quotaWindowMetrics(input.currentWindow).remaining;
|
|
82
83
|
const timeToResetMs = resetAt - input.now;
|
|
83
84
|
if (remainingQuota <= 0 || timeToResetMs <= 0) return low;
|
|
84
85
|
const observedRatePerMs = Math.max(0, (utilizationNow - oldest.utilization) / (current.capturedAt - oldest.capturedAt));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IssueRef, IssueRefStatus } from "../types";
|
|
1
|
+
import type { IssueProvider, IssueRef, IssueRefStatus } from "../types";
|
|
2
2
|
|
|
3
3
|
export interface IssueTrackerStatus {
|
|
4
4
|
status: IssueRefStatus;
|
|
@@ -14,9 +14,19 @@ export interface IssueTrackerReopenOptions {
|
|
|
14
14
|
comment?: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export interface IssueTrackerIssueSummary {
|
|
18
|
+
provider: IssueProvider;
|
|
19
|
+
repo: string;
|
|
20
|
+
externalKey: string;
|
|
21
|
+
url?: string;
|
|
22
|
+
title?: string;
|
|
23
|
+
status: IssueRefStatus;
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
export interface IssueTrackerAdapter {
|
|
18
27
|
resolveLink(ref: IssueRef): string;
|
|
19
28
|
getStatus(ref: IssueRef): Promise<IssueTrackerStatus>;
|
|
29
|
+
listOpenSubIssues?(ref: IssueRef): Promise<IssueTrackerIssueSummary[]>;
|
|
20
30
|
close(ref: IssueRef, options?: IssueTrackerCloseOptions): Promise<void>;
|
|
21
31
|
reopen(ref: IssueRef, options?: IssueTrackerReopenOptions): Promise<void>;
|
|
22
32
|
comment(ref: IssueRef, body: string): Promise<void>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IssueRef, IssueRefStatus } from "../types";
|
|
2
|
-
import type { IssueTrackerAdapter, IssueTrackerCloseOptions, IssueTrackerReopenOptions, IssueTrackerStatus } from "./adapter";
|
|
2
|
+
import type { IssueTrackerAdapter, IssueTrackerCloseOptions, IssueTrackerIssueSummary, IssueTrackerReopenOptions, IssueTrackerStatus } from "./adapter";
|
|
3
3
|
|
|
4
4
|
export const GITHUB_FIXED_LABEL = "fixed";
|
|
5
5
|
const GITHUB_CREDENTIAL_REQUIREMENT = "GitHub issue tracker adapter requires gh authenticated with a token that has issues:write scope";
|
|
@@ -20,6 +20,50 @@ interface GitHubIssuePayload {
|
|
|
20
20
|
html_url?: unknown;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
interface GitHubSubIssueNode {
|
|
24
|
+
number?: unknown;
|
|
25
|
+
title?: unknown;
|
|
26
|
+
state?: unknown;
|
|
27
|
+
url?: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface GitHubSubIssuesPayload {
|
|
31
|
+
data?: {
|
|
32
|
+
repository?: {
|
|
33
|
+
issue?: {
|
|
34
|
+
subIssues?: {
|
|
35
|
+
nodes?: GitHubSubIssueNode[];
|
|
36
|
+
pageInfo?: {
|
|
37
|
+
hasNextPage?: unknown;
|
|
38
|
+
endCursor?: unknown;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const SUB_ISSUES_QUERY = `
|
|
47
|
+
query($owner: String!, $repo: String!, $number: Int!, $after: String) {
|
|
48
|
+
repository(owner: $owner, name: $repo) {
|
|
49
|
+
issue(number: $number) {
|
|
50
|
+
subIssues(first: 100, after: $after) {
|
|
51
|
+
nodes {
|
|
52
|
+
number
|
|
53
|
+
title
|
|
54
|
+
state
|
|
55
|
+
url
|
|
56
|
+
}
|
|
57
|
+
pageInfo {
|
|
58
|
+
hasNextPage
|
|
59
|
+
endCursor
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
`;
|
|
66
|
+
|
|
23
67
|
function textFromBytes(value: unknown): string {
|
|
24
68
|
if (!value) return "";
|
|
25
69
|
if (typeof value === "string") return value;
|
|
@@ -78,7 +122,10 @@ function assertGitHubIssueRef(ref: IssueRef): void {
|
|
|
78
122
|
}
|
|
79
123
|
|
|
80
124
|
function statusFromState(state: unknown): IssueRefStatus {
|
|
81
|
-
if (
|
|
125
|
+
if (typeof state === "string") {
|
|
126
|
+
const normalized = state.toLowerCase();
|
|
127
|
+
if (normalized === "open" || normalized === "closed") return normalized;
|
|
128
|
+
}
|
|
82
129
|
return "unknown";
|
|
83
130
|
}
|
|
84
131
|
|
|
@@ -105,6 +152,40 @@ class GitHubIssueTrackerAdapter implements IssueTrackerAdapter {
|
|
|
105
152
|
return { status: statusFromState(issue.state), title: titleFromIssue(issue) };
|
|
106
153
|
}
|
|
107
154
|
|
|
155
|
+
async listOpenSubIssues(ref: IssueRef): Promise<IssueTrackerIssueSummary[]> {
|
|
156
|
+
assertGitHubIssueRef(ref);
|
|
157
|
+
const [owner, repo] = ref.repo.split("/") as [string, string];
|
|
158
|
+
const open: IssueTrackerIssueSummary[] = [];
|
|
159
|
+
let after: string | undefined;
|
|
160
|
+
|
|
161
|
+
do {
|
|
162
|
+
const response = await this.api({
|
|
163
|
+
method: "POST",
|
|
164
|
+
path: "graphql",
|
|
165
|
+
body: {
|
|
166
|
+
query: SUB_ISSUES_QUERY,
|
|
167
|
+
owner,
|
|
168
|
+
repo,
|
|
169
|
+
number: Number(ref.externalKey),
|
|
170
|
+
after,
|
|
171
|
+
},
|
|
172
|
+
}) as GitHubSubIssuesPayload;
|
|
173
|
+
const subIssues = response.data?.repository?.issue?.subIssues;
|
|
174
|
+
for (const node of subIssues?.nodes ?? []) {
|
|
175
|
+
if (statusFromState(node.state) !== "open") continue;
|
|
176
|
+
const number = typeof node.number === "number" || typeof node.number === "string" ? String(node.number) : "";
|
|
177
|
+
if (!number) continue;
|
|
178
|
+
const title = typeof node.title === "string" ? node.title : undefined;
|
|
179
|
+
const url = typeof node.url === "string" ? node.url : `https://github.com/${ref.repo}/issues/${number}`;
|
|
180
|
+
open.push({ provider: "github", repo: ref.repo, externalKey: number, url, title, status: "open" });
|
|
181
|
+
}
|
|
182
|
+
const pageInfo = subIssues?.pageInfo;
|
|
183
|
+
after = pageInfo?.hasNextPage === true && typeof pageInfo.endCursor === "string" ? pageInfo.endCursor : undefined;
|
|
184
|
+
} while (after);
|
|
185
|
+
|
|
186
|
+
return open;
|
|
187
|
+
}
|
|
188
|
+
|
|
108
189
|
async close(ref: IssueRef, options: IssueTrackerCloseOptions = {}): Promise<void> {
|
|
109
190
|
await this.api({ method: "PATCH", path: githubIssuePath(ref), body: { state: "closed" } });
|
|
110
191
|
await this.applyLabels(ref, [GITHUB_FIXED_LABEL, ...(options.labels ?? [])]);
|
package/src/lifecycle-manager.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createCommand } from "./commands-db";
|
|
2
|
-
import { extractClaudeModelUnavailableMessage } from "agent-relay-sdk";
|
|
3
2
|
import { isPathWithinBase } from "./utils";
|
|
3
|
+
import { agentProviderState } from "./provider-state";
|
|
4
4
|
import { createActivityEvent, deleteWorkspace, getAgent, getDb, getOrchestrator, listOrchestrators, listWorkspaces } from "./db";
|
|
5
5
|
import {
|
|
6
6
|
getManagedAgentState,
|
|
@@ -413,7 +413,10 @@ export class LifecycleManager {
|
|
|
413
413
|
|
|
414
414
|
private markPolicyFailure(policy: SpawnPolicy, state: ManagedAgentState, error: string, exited?: ManagedSessionExitDiagnostics): void {
|
|
415
415
|
if (state.status === "stopping") return;
|
|
416
|
-
|
|
416
|
+
// Read the terminal reason from the adapter-surfaced typed provider state rather
|
|
417
|
+
// than re-parsing raw provider log text (#745 / #725 §4.4 leak #13).
|
|
418
|
+
const agentId = state.agentId || exited?.agentId;
|
|
419
|
+
const modelUnavailable = agentId ? agentTerminalFailureMessage(getAgent(agentId)) : null;
|
|
417
420
|
if (modelUnavailable) {
|
|
418
421
|
this.markFailed(policy, state, modelUnavailable);
|
|
419
422
|
return;
|
|
@@ -622,34 +625,18 @@ export function __resetLifecycleManager(): void {
|
|
|
622
625
|
singleton = null;
|
|
623
626
|
}
|
|
624
627
|
|
|
625
|
-
function managedModelUnavailableMessage(_policy: SpawnPolicy, error: string, exited?: ManagedSessionExitDiagnostics): string | null {
|
|
626
|
-
// extractClaudeModelUnavailableMessage returns null for text that doesn't match
|
|
627
|
-
// the Claude-specific error pattern, so this is safe to call unconditionally —
|
|
628
|
-
// no provider=== branch needed here; the parser is the gate (#743).
|
|
629
|
-
return extractClaudeModelUnavailableMessage([
|
|
630
|
-
error,
|
|
631
|
-
exited?.lastError,
|
|
632
|
-
...(exited?.logTail ?? []),
|
|
633
|
-
...(exited?.unavailable ?? []),
|
|
634
|
-
].join("\n"));
|
|
635
|
-
}
|
|
636
|
-
|
|
637
628
|
function isFastFailStorm(failures: number, exited?: ManagedSessionExitDiagnostics): boolean {
|
|
638
629
|
return failures >= MAX_FAST_FAILURES && exited !== undefined && exited.runtimeMs < FAST_FAIL_RUNTIME_MS;
|
|
639
630
|
}
|
|
640
631
|
|
|
632
|
+
// Returns the model-unavailable message when the agent's adapter-surfaced typed provider
|
|
633
|
+
// state indicates a terminal model-unavailable failure (#745 / #725 §4.4 leak #13).
|
|
641
634
|
function agentTerminalFailureMessage(agent: ReturnType<typeof getAgent> | null): string | null {
|
|
642
635
|
if (!agent?.meta) return null;
|
|
643
636
|
const reason = typeof agent.meta.terminalFailureReason === "string" ? agent.meta.terminalFailureReason : undefined;
|
|
644
637
|
const message = typeof agent.meta.terminalFailureMessage === "string" ? agent.meta.terminalFailureMessage : undefined;
|
|
645
638
|
if (reason === "model-unavailable" && message) return message;
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
const stateMessage = typeof providerState?.message === "string" ? providerState.message : undefined;
|
|
649
|
-
if (stateReason === "model-unavailable" && stateMessage) return stateMessage;
|
|
639
|
+
const ps = agentProviderState(agent);
|
|
640
|
+
if (ps?.reason === "model-unavailable" && ps.message) return ps.message;
|
|
650
641
|
return null;
|
|
651
642
|
}
|
|
652
|
-
|
|
653
|
-
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
654
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
655
|
-
}
|
package/src/provider-state.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isRecord } from "agent-relay-sdk";
|
|
2
|
+
import type { ProviderState } from "agent-relay-sdk";
|
|
2
3
|
import { rateLimitFallbackMs } from "./config";
|
|
3
4
|
|
|
4
5
|
// One server-side home for reading an agent's provider runtime state (the
|
|
@@ -7,28 +8,18 @@ import { rateLimitFallbackMs } from "./config";
|
|
|
7
8
|
// the transient-agent reaper (don't reap a held agent), and the rate-limit
|
|
8
9
|
// resume sweep. The dashboard has its own client-side copy in display.ts.
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
reason?: string;
|
|
13
|
-
label?: string;
|
|
14
|
-
/** Unix ms the provider's limit window resets (rate-limit holds). */
|
|
15
|
-
resetAt?: number;
|
|
16
|
-
/** Unix ms the hold was entered (used for the resume fallback when resetAt is absent). */
|
|
17
|
-
enteredAt?: number;
|
|
18
|
-
/** Raw provider error_type, kept for empirical observation of which type a real limit fires. */
|
|
19
|
-
errorType?: string;
|
|
20
|
-
[key: string]: unknown;
|
|
21
|
-
}
|
|
11
|
+
// Re-export so callers don't need to import from two places.
|
|
12
|
+
export type { ProviderState };
|
|
22
13
|
|
|
23
14
|
interface HasMeta {
|
|
24
15
|
meta?: Record<string, unknown> | null;
|
|
25
16
|
}
|
|
26
17
|
|
|
27
18
|
/** The agent's provider runtime state, or null when none is set / malformed. */
|
|
28
|
-
export function agentProviderState(agent: HasMeta | null | undefined):
|
|
19
|
+
export function agentProviderState(agent: HasMeta | null | undefined): ProviderState | null {
|
|
29
20
|
const value = agent?.meta?.providerState;
|
|
30
21
|
if (!isRecord(value) || typeof value.state !== "string") return null;
|
|
31
|
-
return value as
|
|
22
|
+
return value as ProviderState;
|
|
32
23
|
}
|
|
33
24
|
|
|
34
25
|
/** True when the agent is in any blocked provider state (rate-limit, approval, model-unavailable). */
|
|
@@ -47,16 +38,16 @@ export function isRateLimitHold(agent: HasMeta | null | undefined): boolean {
|
|
|
47
38
|
return state?.state === "blocked" && state.reason === RATE_LIMIT_BLOCK_REASON;
|
|
48
39
|
}
|
|
49
40
|
|
|
50
|
-
export function isTransientRateLimitHold(state:
|
|
41
|
+
export function isTransientRateLimitHold(state: ProviderState | null): boolean {
|
|
51
42
|
return state?.kind === TRANSIENT_RATE_LIMIT_KIND;
|
|
52
43
|
}
|
|
53
44
|
|
|
54
|
-
export function transientRateLimitAttempt(state:
|
|
45
|
+
export function transientRateLimitAttempt(state: ProviderState | null): number {
|
|
55
46
|
const raw = state?.resumeAttempt;
|
|
56
47
|
return typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 0;
|
|
57
48
|
}
|
|
58
49
|
|
|
59
|
-
export function transientRateLimitExhausted(state:
|
|
50
|
+
export function transientRateLimitExhausted(state: ProviderState | null): boolean {
|
|
60
51
|
return isTransientRateLimitHold(state) && transientRateLimitAttempt(state) >= TRANSIENT_RATE_LIMIT_BACKOFF_MS.length;
|
|
61
52
|
}
|
|
62
53
|
|
|
@@ -69,7 +60,7 @@ export function transientRateLimitExhausted(state: ProviderStateView | null): bo
|
|
|
69
60
|
* authoritative; otherwise fall back to `enteredAt + fallback window`. Null only
|
|
70
61
|
* when neither is known (shouldn't happen — the runner always stamps enteredAt).
|
|
71
62
|
*/
|
|
72
|
-
export function rateLimitResumeAt(state:
|
|
63
|
+
export function rateLimitResumeAt(state: ProviderState | null): number | null {
|
|
73
64
|
if (!state) return null;
|
|
74
65
|
if (isTransientRateLimitHold(state)) {
|
|
75
66
|
if (typeof state.nextResumeAt === "number" && Number.isFinite(state.nextResumeAt)) return state.nextResumeAt;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { isRecord, stringValue } from "agent-relay-sdk";
|
|
2
|
+
import { createActivityEvent } from "./db";
|
|
3
|
+
import { emitRelayEvent } from "./events";
|
|
4
|
+
|
|
5
|
+
interface HasMeta {
|
|
6
|
+
meta?: Record<string, unknown> | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ProviderTimelineEvent extends Record<string, unknown> {
|
|
10
|
+
status?: string;
|
|
11
|
+
timestamp?: number;
|
|
12
|
+
metadata?: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The provider timeline event latched on the agent card, or null when absent/malformed. */
|
|
16
|
+
export function agentProviderTimelineEvent(agent: HasMeta | null | undefined): ProviderTimelineEvent | null {
|
|
17
|
+
const value = agent?.meta?.timelineEvent;
|
|
18
|
+
if (!isRecord(value)) return null;
|
|
19
|
+
return {
|
|
20
|
+
...value,
|
|
21
|
+
...(typeof value.status === "string" ? { status: value.status } : {}),
|
|
22
|
+
...(typeof value.timestamp === "number" && Number.isFinite(value.timestamp) ? { timestamp: value.timestamp } : {}),
|
|
23
|
+
...(isRecord(value.metadata) ? { metadata: value.metadata } : {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function numberValue(value: unknown): number | undefined {
|
|
28
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function auditRunnerTimelineEvent(agentId: string, timelineEvent: ProviderTimelineEvent | null): void {
|
|
32
|
+
if (!timelineEvent) return;
|
|
33
|
+
const metadata = isRecord(timelineEvent.metadata) ? timelineEvent.metadata : {};
|
|
34
|
+
if (metadata.source !== "runner") return;
|
|
35
|
+
const status = stringValue(timelineEvent.status);
|
|
36
|
+
if (!status) return;
|
|
37
|
+
const eventType = stringValue(metadata.eventType) ?? status;
|
|
38
|
+
const timestamp = numberValue(timelineEvent.timestamp) ?? Date.now();
|
|
39
|
+
const id = stringValue(timelineEvent.id) ?? `${eventType}-${timestamp}`;
|
|
40
|
+
const title = stringValue(timelineEvent.title) ?? status.replace(/[._-]+/g, " ");
|
|
41
|
+
const body = stringValue(timelineEvent.body);
|
|
42
|
+
const icon = stringValue(timelineEvent.icon) ?? "ti-activity";
|
|
43
|
+
try {
|
|
44
|
+
const event = createActivityEvent({
|
|
45
|
+
clientId: `runner-timeline-${agentId}-${id}`,
|
|
46
|
+
kind: "state",
|
|
47
|
+
title,
|
|
48
|
+
body,
|
|
49
|
+
meta: agentId,
|
|
50
|
+
icon,
|
|
51
|
+
view: "agents",
|
|
52
|
+
agentId,
|
|
53
|
+
metadata: {
|
|
54
|
+
...metadata,
|
|
55
|
+
eventType,
|
|
56
|
+
timelineStatus: status,
|
|
57
|
+
timelineId: id,
|
|
58
|
+
timelineTimestamp: timestamp,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
emitRelayEvent({
|
|
62
|
+
type: "activity.created",
|
|
63
|
+
source: "server",
|
|
64
|
+
subject: String(event.id),
|
|
65
|
+
data: event as unknown as Record<string, unknown>,
|
|
66
|
+
});
|
|
67
|
+
} catch {
|
|
68
|
+
// Timeline writes must never block status updates.
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/quota-advisory.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isRecord } from "agent-relay-sdk";
|
|
2
|
+
import { quotaWindowMetrics } from "agent-relay-sdk/provider-quota";
|
|
2
3
|
import { getDb } from "./db/connection.ts";
|
|
3
4
|
import { emitRelayEvent } from "./events";
|
|
4
5
|
import { routeNotification } from "./notification-router";
|
|
@@ -46,7 +47,7 @@ function handleProviderQuotaAdvisory(input: ProviderQuotaAdvisoryInput): string
|
|
|
46
47
|
|
|
47
48
|
for (const window of input.quota.windows) {
|
|
48
49
|
const previousWindow = input.previousQuota?.windows.find((candidate) => candidate.name === window.name);
|
|
49
|
-
const remaining =
|
|
50
|
+
const remaining = quotaWindowMetrics(window).remaining;
|
|
50
51
|
const remainingPercent = percent(remaining);
|
|
51
52
|
let previous = nextState[window.name];
|
|
52
53
|
const reset = hasLastQuotaAdvisory(previous) && isQuotaReset(remaining, previousWindow, window, config.resetRemaining);
|
|
@@ -279,7 +280,7 @@ function crossProviderHeadroomHint(provider: string): string {
|
|
|
279
280
|
return quota.windows.map((window) => ({
|
|
280
281
|
provider: row.provider,
|
|
281
282
|
window: window.name,
|
|
282
|
-
remainingPercent: percent(
|
|
283
|
+
remainingPercent: percent(quotaWindowMetrics(window).remaining),
|
|
283
284
|
}));
|
|
284
285
|
}).sort((a, b) => b.remainingPercent - a.remainingPercent);
|
|
285
286
|
const best = options[0];
|
|
@@ -295,17 +296,13 @@ function isQuotaReset(
|
|
|
295
296
|
): boolean {
|
|
296
297
|
if (remaining >= resetRemaining) return true;
|
|
297
298
|
if (!previousWindow?.resetsAt || !window.resetsAt || window.resetsAt <= previousWindow.resetsAt) return false;
|
|
298
|
-
return remaining -
|
|
299
|
+
return remaining - quotaWindowMetrics(previousWindow).remaining >= 0.5 && remaining >= 0.8;
|
|
299
300
|
}
|
|
300
301
|
|
|
301
302
|
function quotaRemainingBand(remaining: number, bandSize: number): number {
|
|
302
303
|
return Math.min(100, Math.max(0, percent(Math.ceil((remaining / bandSize) - 1e-9) * bandSize)));
|
|
303
304
|
}
|
|
304
305
|
|
|
305
|
-
function quotaRemaining(utilization: number): number {
|
|
306
|
-
return Math.max(0, Math.min(1, 1 - utilization));
|
|
307
|
-
}
|
|
308
|
-
|
|
309
306
|
function percent(value: number): number {
|
|
310
307
|
return Math.round(value * 100);
|
|
311
308
|
}
|
|
@@ -13,6 +13,7 @@ import { emitAgentStatus, emitDashboardPresence, emitNewMessage } from "../sse";
|
|
|
13
13
|
import { getAgentProfile, getSpawnPolicy } from "../config-store";
|
|
14
14
|
import { listManagedOrchestratorsForAgent } from "../orchestrator-lookup";
|
|
15
15
|
import { runnerRuntimeTokenEnv } from "../runtime-tokens";
|
|
16
|
+
import { agentProviderState } from "../provider-state";
|
|
16
17
|
import { authContextFromRequest } from "../services/auth-context";
|
|
17
18
|
import { ShutdownAuthError, ShutdownTargetError, shutdownAgent } from "../services/shutdown-agent";
|
|
18
19
|
import { type AgentCard, type SpawnApprovalMode } from "../types";
|
|
@@ -414,8 +415,9 @@ export const postAgentPermissionDecision: Handler = async (req, params) => {
|
|
|
414
415
|
const arb = arbitrateDrive(requestUserId(req), agent.id);
|
|
415
416
|
if (!arb.allowed) return driveBlockedResponse(arb);
|
|
416
417
|
if (!agentIsControlEligible(agent)) return error("agent cannot receive permission decisions", 400);
|
|
417
|
-
const
|
|
418
|
-
|
|
418
|
+
const providerState = agentProviderState(agent);
|
|
419
|
+
const pendingApproval = isRecord(providerState?.pendingApproval)
|
|
420
|
+
? providerState.pendingApproval
|
|
419
421
|
: null;
|
|
420
422
|
const pendingId = typeof pendingApproval?.id === "string" ? pendingApproval.id : "";
|
|
421
423
|
if (pendingId && pendingId !== approvalId) return error("approval request is no longer current", 409);
|