@runuai/host 0.9.162 → 0.9.163
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/lib/agents/claude.ts +110 -4
- package/lib/agents/codex.ts +175 -8
- package/lib/agents/opencode.ts +35 -12
- package/lib/agents/rate-limit.ts +299 -3
- package/lib/agents/types.ts +29 -1
- package/lib/engine-accounts.ts +82 -4
- package/lib/orchestrator.ts +184 -9
- package/package.json +1 -1
- package/src/main.ts +8 -0
- package/src/protocol.ts +55 -0
package/lib/orchestrator.ts
CHANGED
|
@@ -31,6 +31,7 @@ import { dirname, join } from "node:path";
|
|
|
31
31
|
import { inArray } from "drizzle-orm";
|
|
32
32
|
|
|
33
33
|
import { getDb, schema } from "./db";
|
|
34
|
+
import { newId } from "./ulid";
|
|
34
35
|
import { mockAgentFactory } from "./agents/mock";
|
|
35
36
|
import { realAgentFactory } from "./agents/factory";
|
|
36
37
|
import { resolveAgentMode } from "./agents/mode";
|
|
@@ -152,7 +153,12 @@ import {
|
|
|
152
153
|
codexHomeIsolatable,
|
|
153
154
|
codexHomeSeedCommand,
|
|
154
155
|
codexPerAgentHome,
|
|
156
|
+
clearEngineAccountCooldowns,
|
|
157
|
+
clearEngineAccountCooldownWindow,
|
|
155
158
|
cooldownEngineAccount,
|
|
159
|
+
cooldownEngineAccountUntil,
|
|
160
|
+
cooldownEngineAccountWindow,
|
|
161
|
+
cooldownEngineAccountWindowUntil,
|
|
156
162
|
noteEngineAccountUsed,
|
|
157
163
|
pickEngineAccount,
|
|
158
164
|
provisionEngineAccounts,
|
|
@@ -227,6 +233,7 @@ import {
|
|
|
227
233
|
import { env } from "./env";
|
|
228
234
|
import type {
|
|
229
235
|
ChannelEnsureInput,
|
|
236
|
+
EngineUsageFrame,
|
|
230
237
|
ChannelHuman,
|
|
231
238
|
HostEvent,
|
|
232
239
|
} from "../src/protocol";
|
|
@@ -247,6 +254,17 @@ import {
|
|
|
247
254
|
import { forgetTaskEnvSnapshot } from "./task-env-source";
|
|
248
255
|
|
|
249
256
|
export type HostEventSubscriber = (event: HostEvent) => void;
|
|
257
|
+
/** True means the frame reached the current cloud transport generation. */
|
|
258
|
+
export type EngineUsageSubscriber = (frame: EngineUsageFrame) => boolean;
|
|
259
|
+
|
|
260
|
+
export const ENGINE_USAGE_REFRESH_MS = 15 * 60_000;
|
|
261
|
+
|
|
262
|
+
interface DeliveredEngineUsage {
|
|
263
|
+
payload: string;
|
|
264
|
+
sentAt: number;
|
|
265
|
+
taskId: string;
|
|
266
|
+
agentId: string;
|
|
267
|
+
}
|
|
250
268
|
|
|
251
269
|
// Rolling upgrade fence: v0 Secretary runners were launched under the
|
|
252
270
|
// communicator tool-denial profile and may survive a host restart. Durable
|
|
@@ -568,6 +586,18 @@ const LEASED_CREDENTIAL_BLANKS: Readonly<Record<string, string>> = Object.freeze
|
|
|
568
586
|
* again. */
|
|
569
587
|
const LEASED_SYNC_RETRY_MS = 30_000;
|
|
570
588
|
|
|
589
|
+
const ENGINE_USAGE_KINDS = new Set<EngineUsageFrame["engine"]>([
|
|
590
|
+
"claude",
|
|
591
|
+
"codex",
|
|
592
|
+
"grok",
|
|
593
|
+
"cursor",
|
|
594
|
+
"opencode",
|
|
595
|
+
]);
|
|
596
|
+
|
|
597
|
+
function isEngineUsageKind(kind: string): kind is EngineUsageFrame["engine"] {
|
|
598
|
+
return ENGINE_USAGE_KINDS.has(kind as EngineUsageFrame["engine"]);
|
|
599
|
+
}
|
|
600
|
+
|
|
571
601
|
/** ADR-154 §6: the exact lease material a spawn starts on: an AI account's,
|
|
572
602
|
* or, for the Managed Engine, the task's open run alone (its agent's task
|
|
573
603
|
* token is its credential). */
|
|
@@ -762,6 +792,10 @@ export class Orchestrator {
|
|
|
762
792
|
* later command cannot overtake an earlier one after channel close. */
|
|
763
793
|
private readonly taskLifecycleTails = new Map<string, Promise<void>>();
|
|
764
794
|
private readonly hostSubscribers = new Set<HostEventSubscriber>();
|
|
795
|
+
private readonly engineUsageSubscribers = new Set<EngineUsageSubscriber>();
|
|
796
|
+
/** Last payload (excluding report id/time) for each leased account + agent.
|
|
797
|
+
* Structured Claude events can repeat without any state change. */
|
|
798
|
+
private readonly lastEngineUsage = new Map<string, DeliveredEngineUsage>();
|
|
765
799
|
/** ADR-154 §6: launch slots retired whose retirement has not yet reached
|
|
766
800
|
* their runtime, per task, whatever channel they were in: an agent's next
|
|
767
801
|
* spawn launches there only once they have (`retireLeasedLaunch`). */
|
|
@@ -779,6 +813,11 @@ export class Orchestrator {
|
|
|
779
813
|
return () => this.hostSubscribers.delete(fn);
|
|
780
814
|
}
|
|
781
815
|
|
|
816
|
+
subscribeEngineUsageFrames(fn: EngineUsageSubscriber): () => void {
|
|
817
|
+
this.engineUsageSubscribers.add(fn);
|
|
818
|
+
return () => this.engineUsageSubscribers.delete(fn);
|
|
819
|
+
}
|
|
820
|
+
|
|
782
821
|
/**
|
|
783
822
|
* The cloud credential composer changed the MCP view it serves for a task.
|
|
784
823
|
* Its committed lease view, not the independently delivered channel spec,
|
|
@@ -896,6 +935,30 @@ export class Orchestrator {
|
|
|
896
935
|
}
|
|
897
936
|
}
|
|
898
937
|
|
|
938
|
+
private emitEngineUsage(frame: EngineUsageFrame): boolean {
|
|
939
|
+
let delivered = false;
|
|
940
|
+
for (const fn of this.engineUsageSubscribers) {
|
|
941
|
+
try {
|
|
942
|
+
delivered = fn(frame) || delivered;
|
|
943
|
+
} catch {
|
|
944
|
+
// Usage is best-effort; a broken bridge subscriber cannot affect a
|
|
945
|
+
// running agent turn.
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return delivered;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
private forgetEngineUsage(taskId: string, agentId?: string): void {
|
|
952
|
+
for (const [key, delivered] of this.lastEngineUsage) {
|
|
953
|
+
if (
|
|
954
|
+
delivered.taskId === taskId &&
|
|
955
|
+
(agentId === undefined || delivered.agentId === agentId)
|
|
956
|
+
) {
|
|
957
|
+
this.lastEngineUsage.delete(key);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
899
962
|
// -- channel lifecycle ----------------------------------------------------
|
|
900
963
|
|
|
901
964
|
registerChannelSpec(spec: ChannelEnsureInput): void {
|
|
@@ -4586,6 +4649,7 @@ export class Orchestrator {
|
|
|
4586
4649
|
* could silently restart canceled work on the new generation.
|
|
4587
4650
|
*/
|
|
4588
4651
|
private retireRunnerGeneration(channel: Channel, agentId: string): void {
|
|
4652
|
+
this.forgetEngineUsage(channel.taskId, agentId);
|
|
4589
4653
|
const aborted = channel.interrupted.delete(agentId);
|
|
4590
4654
|
channel.activeTurns.delete(agentId);
|
|
4591
4655
|
channel.acceptedPrompts.delete(agentId);
|
|
@@ -4929,6 +4993,105 @@ export class Orchestrator {
|
|
|
4929
4993
|
): Promise<void> {
|
|
4930
4994
|
if (!this.isActiveChannel(channel)) return;
|
|
4931
4995
|
switch (event.type) {
|
|
4996
|
+
case "account_usage": {
|
|
4997
|
+
// Only the exact live session's cloud-leased ACCOUNT may be named on
|
|
4998
|
+
// the wire. A runner default slot and the Managed Engine's task-token
|
|
4999
|
+
// identity are deliberately unreportable.
|
|
5000
|
+
if (channel.sessions.get(agentId) !== session) break;
|
|
5001
|
+
const identity = channel.leasedIdentityByAgent.get(agentId);
|
|
5002
|
+
if (!identity || identity.subject !== "account") break;
|
|
5003
|
+
const agent = channel.roster.find((candidate) => candidate.id === agentId);
|
|
5004
|
+
if (!agent || !isEngineUsageKind(agent.kind)) break;
|
|
5005
|
+
|
|
5006
|
+
const coverage = event.usage.coverage;
|
|
5007
|
+
if (event.usage.status === "limited") {
|
|
5008
|
+
const limitedWindows =
|
|
5009
|
+
coverage?.kind === "all-windows"
|
|
5010
|
+
? event.usage.windows
|
|
5011
|
+
.filter((window) => (window.usedPercent ?? 0) >= 100)
|
|
5012
|
+
.map((window) => window.window)
|
|
5013
|
+
: coverage?.kind === "window"
|
|
5014
|
+
? [coverage.window]
|
|
5015
|
+
: [];
|
|
5016
|
+
const causes = limitedWindows.length > 0 ? limitedWindows : [null];
|
|
5017
|
+
for (const window of causes) {
|
|
5018
|
+
if (window === null) {
|
|
5019
|
+
if (event.usage.resetsAt === undefined) {
|
|
5020
|
+
cooldownEngineAccount(identity.credentialId);
|
|
5021
|
+
} else {
|
|
5022
|
+
cooldownEngineAccountUntil(
|
|
5023
|
+
identity.credentialId,
|
|
5024
|
+
event.usage.resetsAt,
|
|
5025
|
+
);
|
|
5026
|
+
}
|
|
5027
|
+
} else if (event.usage.resetsAt === undefined) {
|
|
5028
|
+
cooldownEngineAccountWindow(identity.credentialId, window);
|
|
5029
|
+
} else {
|
|
5030
|
+
cooldownEngineAccountWindowUntil(
|
|
5031
|
+
identity.credentialId,
|
|
5032
|
+
window,
|
|
5033
|
+
event.usage.resetsAt,
|
|
5034
|
+
);
|
|
5035
|
+
}
|
|
5036
|
+
}
|
|
5037
|
+
} else if (coverage?.kind === "window") {
|
|
5038
|
+
clearEngineAccountCooldownWindow(
|
|
5039
|
+
identity.credentialId,
|
|
5040
|
+
coverage.window,
|
|
5041
|
+
);
|
|
5042
|
+
} else if (coverage?.kind === "all-windows") {
|
|
5043
|
+
clearEngineAccountCooldowns(identity.credentialId);
|
|
5044
|
+
}
|
|
5045
|
+
|
|
5046
|
+
const key = `${identity.credentialId}\0${agentId}`;
|
|
5047
|
+
const payload = JSON.stringify({
|
|
5048
|
+
taskId: channel.taskId,
|
|
5049
|
+
engine: agent.kind,
|
|
5050
|
+
credentialId: identity.credentialId,
|
|
5051
|
+
status: event.usage.status,
|
|
5052
|
+
resetsAt: event.usage.resetsAt,
|
|
5053
|
+
usingOverage: event.usage.usingOverage,
|
|
5054
|
+
windows: event.usage.windows,
|
|
5055
|
+
source: event.usage.source,
|
|
5056
|
+
});
|
|
5057
|
+
const now = Date.now();
|
|
5058
|
+
const previous = this.lastEngineUsage.get(key);
|
|
5059
|
+
if (
|
|
5060
|
+
previous?.payload === payload &&
|
|
5061
|
+
now - previous.sentAt < ENGINE_USAGE_REFRESH_MS
|
|
5062
|
+
) {
|
|
5063
|
+
break;
|
|
5064
|
+
}
|
|
5065
|
+
const delivered = this.emitEngineUsage({
|
|
5066
|
+
kind: "engine.usage",
|
|
5067
|
+
reportId: newId(),
|
|
5068
|
+
taskId: channel.taskId,
|
|
5069
|
+
agentId,
|
|
5070
|
+
engine: agent.kind,
|
|
5071
|
+
credentialId: identity.credentialId,
|
|
5072
|
+
status: event.usage.status,
|
|
5073
|
+
...(event.usage.resetsAt === undefined
|
|
5074
|
+
? {}
|
|
5075
|
+
: { resetsAt: event.usage.resetsAt }),
|
|
5076
|
+
...(event.usage.usingOverage === undefined
|
|
5077
|
+
? {}
|
|
5078
|
+
: { usingOverage: event.usage.usingOverage }),
|
|
5079
|
+
windows: event.usage.windows,
|
|
5080
|
+
source: event.usage.source,
|
|
5081
|
+
observedAt: event.usage.observedAt,
|
|
5082
|
+
});
|
|
5083
|
+
// A disconnected bridge returns false. Do not suppress the next
|
|
5084
|
+
// identical observation unless this one actually reached the socket.
|
|
5085
|
+
if (delivered) {
|
|
5086
|
+
this.lastEngineUsage.set(key, {
|
|
5087
|
+
payload,
|
|
5088
|
+
sentAt: now,
|
|
5089
|
+
taskId: channel.taskId,
|
|
5090
|
+
agentId,
|
|
5091
|
+
});
|
|
5092
|
+
}
|
|
5093
|
+
break;
|
|
5094
|
+
}
|
|
4932
5095
|
case "message_delta": {
|
|
4933
5096
|
this.markActiveTurn(channel, agentId);
|
|
4934
5097
|
channel.openTurns.add(agentId);
|
|
@@ -5063,6 +5226,7 @@ export class Orchestrator {
|
|
|
5063
5226
|
agentId,
|
|
5064
5227
|
agent,
|
|
5065
5228
|
abortBoundaryOnRosterChange,
|
|
5229
|
+
event.resetsAt,
|
|
5066
5230
|
))
|
|
5067
5231
|
) {
|
|
5068
5232
|
break;
|
|
@@ -5583,6 +5747,7 @@ export class Orchestrator {
|
|
|
5583
5747
|
agentId: string,
|
|
5584
5748
|
agent: RosterAgent,
|
|
5585
5749
|
abortBoundaryOnRosterChange = false,
|
|
5750
|
+
resetsAt?: number,
|
|
5586
5751
|
): Promise<boolean> {
|
|
5587
5752
|
if (!this.isActiveChannel(channel)) return true;
|
|
5588
5753
|
if (channel.rosterSessionChanges.has(agentId)) {
|
|
@@ -5606,7 +5771,10 @@ export class Orchestrator {
|
|
|
5606
5771
|
if (budget > MAX_ROTATIONS_PER_AGENT) return false;
|
|
5607
5772
|
|
|
5608
5773
|
const current = channel.accountByAgent.get(agentId);
|
|
5609
|
-
if (current)
|
|
5774
|
+
if (current) {
|
|
5775
|
+
if (resetsAt === undefined) cooldownEngineAccount(current);
|
|
5776
|
+
else cooldownEngineAccountUntil(current, resetsAt);
|
|
5777
|
+
}
|
|
5610
5778
|
const next = this.pickAccount(
|
|
5611
5779
|
channel,
|
|
5612
5780
|
agent.kind,
|
|
@@ -6156,6 +6324,7 @@ export class Orchestrator {
|
|
|
6156
6324
|
this.channelSpecs.delete(taskId);
|
|
6157
6325
|
const ch = this.channels.get(taskId);
|
|
6158
6326
|
if (!ch) return Promise.resolve();
|
|
6327
|
+
this.forgetEngineUsage(taskId);
|
|
6159
6328
|
// Invalidate the generation BEFORE the first await. Browser setup and the
|
|
6160
6329
|
// start/reconcile factory continuations all check this identity, so none
|
|
6161
6330
|
// can repopulate an orphan channel while its sessions close.
|
|
@@ -6888,8 +7057,8 @@ export function buildSystemPreamble(
|
|
|
6888
7057
|
" (at least one of the three; `default` as the model or effort clears it to",
|
|
6889
7058
|
" the engine's own default)",
|
|
6890
7059
|
"- `crew remove @agent --why \"…\"`",
|
|
6891
|
-
"Use the smallest crew the job needs, often none
|
|
6892
|
-
"
|
|
7060
|
+
"Use the smallest crew the job needs, often none, and hire only when",
|
|
7061
|
+
"parallel work or a different skill or model pays for itself.",
|
|
6893
7062
|
"A new agent is idle until you `dispatch` to it, so brief it then. Remove an",
|
|
6894
7063
|
"agent when its part is done; its handle is not reused, so hiring that",
|
|
6895
7064
|
"persona again gives it a new one. A model change restarts that agent and drops",
|
|
@@ -6914,18 +7083,24 @@ export function buildSystemPreamble(
|
|
|
6914
7083
|
"connections and persona permissions of any task agent, except native",
|
|
6915
7084
|
"subagents: helpers your engine starts on its own would work outside the",
|
|
6916
7085
|
"crew, unpriced and unseen by the humans, so they are switched off. Work",
|
|
6917
|
-
"for another agent goes to a crew agent through `dispatch`.
|
|
6918
|
-
|
|
7086
|
+
"for another agent goes to a crew agent through `dispatch`.",
|
|
7087
|
+
// One rule for who does the work, the plan's, so this section and
|
|
7088
|
+
// "Your crew" cannot disagree: the crew may be nobody.
|
|
7089
|
+
"Who does each step is your plan's call: do it yourself when one agent",
|
|
7090
|
+
"is enough, hand it to the crew when parallel work or another skill or",
|
|
7091
|
+
"model pays for itself. A human's request for you to do something",
|
|
7092
|
+
"yourself overrides the plan.",
|
|
7093
|
+
"",
|
|
6919
7094
|
]
|
|
6920
7095
|
: [
|
|
6921
7096
|
"Your Secretary role is prompt-guided, not tool-restricted. You have the",
|
|
6922
7097
|
"same normal tools, approved MCP connections, model choice, and persona",
|
|
6923
7098
|
"permissions as any other task agent. Your default job is to communicate,",
|
|
7099
|
+
"coordinate, and dispatch implementation work rather than doing it yourself.",
|
|
7100
|
+
"If the human explicitly asks YOU to make a change, you may use those tools",
|
|
7101
|
+
"and do it directly; otherwise hand mutations to a crew agent.",
|
|
7102
|
+
"",
|
|
6924
7103
|
]),
|
|
6925
|
-
"coordinate, and dispatch implementation work rather than doing it yourself.",
|
|
6926
|
-
"If the human explicitly asks YOU to make a change, you may use those tools",
|
|
6927
|
-
"and do it directly; otherwise hand mutations to a crew agent.",
|
|
6928
|
-
"",
|
|
6929
7104
|
]
|
|
6930
7105
|
: [];
|
|
6931
7106
|
const groupMessageBrief = isSecretary
|
package/package.json
CHANGED
package/src/main.ts
CHANGED
|
@@ -285,6 +285,7 @@ import { hostCommands, hostEvents, setManagedHostRestartHook } from "./index";
|
|
|
285
285
|
import { EventOutbox, transmitEventOutbox } from "./event-outbox";
|
|
286
286
|
import {
|
|
287
287
|
HostErrorCode,
|
|
288
|
+
ENGINE_USAGE_PROTOCOL_FEATURE,
|
|
288
289
|
EVENT_REPLAY_PROTOCOL_FEATURE,
|
|
289
290
|
GITHUB_CREDENTIAL_GENERATION_PROTOCOL_FEATURE,
|
|
290
291
|
GITHUB_INSTALLATION_VERIFICATION_PROTOCOL_FEATURE,
|
|
@@ -1486,6 +1487,7 @@ function buildCapabilities(): HostCapabilities {
|
|
|
1486
1487
|
HOST_ENGINE_ACCOUNT_SCOPES_PROTOCOL_FEATURE,
|
|
1487
1488
|
HOST_ENGINE_ACCOUNT_MANAGE_PROTOCOL_FEATURE,
|
|
1488
1489
|
HOST_ENGINE_DISCONNECT_PROTOCOL_FEATURE,
|
|
1490
|
+
ENGINE_USAGE_PROTOCOL_FEATURE,
|
|
1489
1491
|
LIVE_ROSTER_EDIT_PROTOCOL_FEATURE,
|
|
1490
1492
|
AGENT_TOKEN_GENERATION_PROTOCOL_FEATURE,
|
|
1491
1493
|
// The echo adapter cannot execute the in-task CLI. Advertising typed
|
|
@@ -2874,6 +2876,11 @@ async function connect(): Promise<void> {
|
|
|
2874
2876
|
return false;
|
|
2875
2877
|
}
|
|
2876
2878
|
};
|
|
2879
|
+
const stopEngineUsageSubscription = getOrchestrator().subscribeEngineUsageFrames(
|
|
2880
|
+
(frame) => {
|
|
2881
|
+
return sendRemoteOperationFrame(frame);
|
|
2882
|
+
},
|
|
2883
|
+
);
|
|
2877
2884
|
|
|
2878
2885
|
let projectEnvOperationTail = Promise.resolve();
|
|
2879
2886
|
const projectEnvGenerationIsCurrent = (): boolean =>
|
|
@@ -2916,6 +2923,7 @@ async function connect(): Promise<void> {
|
|
|
2916
2923
|
};
|
|
2917
2924
|
|
|
2918
2925
|
const cleanup = (): void => {
|
|
2926
|
+
stopEngineUsageSubscription();
|
|
2919
2927
|
// Connection loss revokes the serving gate before deferred transport
|
|
2920
2928
|
// cleanup yields. A late close from a superseded socket must not revoke a
|
|
2921
2929
|
// replacement generation that has already committed its fresh snapshot.
|
package/src/protocol.ts
CHANGED
|
@@ -156,6 +156,60 @@ export const HOST_ENGINE_ACCOUNT_MANAGE_PROTOCOL_FEATURE =
|
|
|
156
156
|
"host-engine-account-manage-v1";
|
|
157
157
|
export const HOST_ENGINE_ACCOUNT_SCOPES_PROTOCOL_FEATURE =
|
|
158
158
|
"host-engine-account-scopes-v1";
|
|
159
|
+
/** AI-account usage (ADR-157 slice 3, amended 2026-10-21): the host reports
|
|
160
|
+
* what an engine CLI said about the account an agent runs on — Claude Code's
|
|
161
|
+
* `rate_limit_event`, Codex's `account/rateLimits` — and the reset time of a
|
|
162
|
+
* cut-off, as `engine.usage` frames. Reports are best-effort and
|
|
163
|
+
* self-superseding: a lost one is replaced by the next. The cloud keeps the
|
|
164
|
+
* latest per account and never asks the provider itself (subscription
|
|
165
|
+
* sign-ins are for the engine CLIs only). A host advertises this once it
|
|
166
|
+
* sends the frames; the cloud needs nothing from a host without it. */
|
|
167
|
+
export const ENGINE_USAGE_PROTOCOL_FEATURE = "engine-usage-v1";
|
|
168
|
+
export const MAX_ENGINE_USAGE_WINDOWS = 8;
|
|
169
|
+
|
|
170
|
+
/** One usage window of an AI account, in the engine's own terms. Every
|
|
171
|
+
* measurement is optional: an engine reports what it reports. */
|
|
172
|
+
export interface EngineUsageWindow {
|
|
173
|
+
/** The engine's name for the window: Claude's `rateLimitType`
|
|
174
|
+
* (`five_hour`, `seven_day`, `seven_day_opus`, …) or Codex's `primary` /
|
|
175
|
+
* `secondary`. At most 64 characters. */
|
|
176
|
+
window: string;
|
|
177
|
+
/** Window length in minutes. */
|
|
178
|
+
windowMinutes?: number;
|
|
179
|
+
/** Share of the window used, 0–100. */
|
|
180
|
+
usedPercent?: number;
|
|
181
|
+
/** When the window resets, epoch ms. */
|
|
182
|
+
resetsAt?: number;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** One usage report for one leased AI account (ADR-154 `credentialId`).
|
|
186
|
+
* Accounts that are not cloud credentials (a host's own default slot) are
|
|
187
|
+
* never reported. */
|
|
188
|
+
export interface EngineUsageFrame {
|
|
189
|
+
kind: "engine.usage";
|
|
190
|
+
/** Host-minted, unique per report; a repeat is ignored. */
|
|
191
|
+
reportId: string;
|
|
192
|
+
taskId: string;
|
|
193
|
+
agentId: string;
|
|
194
|
+
engine: "claude" | "codex" | "grok" | "cursor" | "opencode";
|
|
195
|
+
/** The `uai_credentials` id the agent's session runs on. */
|
|
196
|
+
credentialId: string;
|
|
197
|
+
/** `allowed`: working; `warning`: the engine says it is close to a limit;
|
|
198
|
+
* `limited`: cut off until `resetsAt` (or an unknown time). */
|
|
199
|
+
status: "allowed" | "warning" | "limited";
|
|
200
|
+
/** When a `limited` account should work again, epoch ms, if known. */
|
|
201
|
+
resetsAt?: number;
|
|
202
|
+
/** Whether the engine says it is now billing past the plan (Claude
|
|
203
|
+
* "overage"), when it says. */
|
|
204
|
+
usingOverage?: boolean;
|
|
205
|
+
windows: EngineUsageWindow[];
|
|
206
|
+
/** What the report was read from. */
|
|
207
|
+
source: "claude-rate-limit-event" | "codex-rate-limits" | "error-text";
|
|
208
|
+
/** When the engine said it, epoch ms (host clock). The cloud keeps the
|
|
209
|
+
* newest per account, so a late report never overwrites a newer one. */
|
|
210
|
+
observedAt: number;
|
|
211
|
+
}
|
|
212
|
+
|
|
159
213
|
/** ADR-049 addendum: live roster edits. On channel.ensure the host diffs the
|
|
160
214
|
* live roster against the spec — an agent whose kind, model or effort changed
|
|
161
215
|
* has its session recycled so the reconciler respawns it on the new settings,
|
|
@@ -4806,6 +4860,7 @@ export type HostToCloud =
|
|
|
4806
4860
|
// cloud's replay watermark only applies within a matching boot. Optional
|
|
4807
4861
|
// for pre-103 hosts (which also never send seq'd events).
|
|
4808
4862
|
| { kind: "auth"; token: string; hostId: string; bootId?: string }
|
|
4863
|
+
| EngineUsageFrame
|
|
4809
4864
|
| { kind: "host.capabilities"; capabilities: HostCapabilities }
|
|
4810
4865
|
| {
|
|
4811
4866
|
kind: "host.inventory.page";
|