@inixiative/agent-session 0.6.8 → 0.8.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/README.md +24 -10
- package/package.json +7 -6
- package/src/claude-code-session.ts +56 -0
- package/src/harness-session.ts +35 -5
- package/src/index.ts +20 -0
- package/src/limits.ts +38 -2
- package/src/muse-session.ts +2110 -0
- package/src/pool.ts +6 -1
- package/src/transport.ts +37 -2
- package/src/transports.ts +5 -1
package/README.md
CHANGED
|
@@ -74,24 +74,37 @@ arrive as `rate_limit` events.
|
|
|
74
74
|
```ts
|
|
75
75
|
import { createSession, TRANSPORTS } from "@inixiative/agent-session";
|
|
76
76
|
|
|
77
|
-
const kind = "codex-app-server"; // or claude-cli, claude-agent-sdk
|
|
77
|
+
const kind = "codex-app-server"; // or claude-cli, claude-agent-sdk, muse-serve
|
|
78
78
|
if (TRANSPORTS[kind].capabilities.interrupt === "acknowledged") { /* … */ }
|
|
79
79
|
const session = createSession(kind, { cwd, model: "gpt-6-luna", env: { CODEX_HOME: profile } });
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
| | claude-cli | codex-app-server | claude-agent-sdk | acp | api |
|
|
83
|
-
|
|
84
|
-
| Status | ✅ live | ✅ live | ✅ live | stub | stub |
|
|
85
|
-
| Resume / fork | native / native | native / – | native / native | – | – |
|
|
86
|
-
| Interrupt | acknowledged | acknowledged | acknowledged | – | – |
|
|
87
|
-
| Push | – | steer | – | – | – |
|
|
88
|
-
| Approvals | callback | callback | callback | – | – |
|
|
89
|
-
| Limits stream / poll | ✅ / ✅ | ✅ / ✅ | ✅ / – | – | – |
|
|
90
|
-
| Primed reset | fork | fork | – | – | – |
|
|
82
|
+
| | claude-cli | codex-app-server | muse-serve | claude-agent-sdk | acp | api |
|
|
83
|
+
|---|---|---|---|---|---|---|
|
|
84
|
+
| Status | ✅ live | ✅ live | ✅ live | ✅ live | stub | stub |
|
|
85
|
+
| Resume / fork | native / native | native / – | native / native | native / native | – | – |
|
|
86
|
+
| Interrupt | acknowledged | acknowledged | acknowledged | acknowledged | – | – |
|
|
87
|
+
| Push | – | steer | steer | – | – | – |
|
|
88
|
+
| Approvals | callback | callback | callback | callback | – | – |
|
|
89
|
+
| Limits stream / poll | ✅ / ✅ | ✅ / ✅ | ✅ / ✅ | ✅ / – | – | – |
|
|
90
|
+
| Primed reset | fork | fork | – | – | – | – |
|
|
91
91
|
|
|
92
92
|
Codex runs only over `codex app-server --listen stdio://` (`CodexAppServerSession`);
|
|
93
93
|
Codex CLI 0.155 has no `mcp-server` subcommand.
|
|
94
94
|
|
|
95
|
+
Muse Code runs over `muse serve` through Meta's `@muse-code/sdk` (`MuseSession`), pinned to
|
|
96
|
+
the CLI's version (1.4.14): a host serving another version is refused at start. It bills the
|
|
97
|
+
saved Muse login: metered keys and endpoints never reach it (nor Muse's own tool runs, which
|
|
98
|
+
inherit its environment: a repo command needing `OPENAI_API_KEY` will not see it under Muse),
|
|
99
|
+
and a resumed session must be on provider `meta`. Per-login isolation is `env: { XDG_CONFIG_HOME, XDG_DATA_HOME }` (login and
|
|
100
|
+
sessions). `musePermissions(mode)` maps the host modes to an explicit permission profile, because
|
|
101
|
+
the host default lets Muse's AI reviewer answer approvals instead of the gates. Questions arrive
|
|
102
|
+
through `onUserInput()`; unanswered ones are cancelled with a reason. An approval offering
|
|
103
|
+
no approve-once or reject choice is reported as an `error` event and left open (the turn ends
|
|
104
|
+
at its timeout). Work Muse starts itself
|
|
105
|
+
(cron firings, monitors, goals, reminder agents) is emitted too, marked
|
|
106
|
+
`unattributedReason: "task"` with a `task` lifecycle typed `muse-cron`, `muse-monitor`, ….
|
|
107
|
+
|
|
95
108
|
Full matrix, notes and what is stubbed: [docs/transports-pools-routing.md](docs/transports-pools-routing.md).
|
|
96
109
|
|
|
97
110
|
### Tool gate
|
|
@@ -254,6 +267,7 @@ snapshots in the artifact but do not fabricate a completed-turn total.
|
|
|
254
267
|
| Claude Code | ✅ shipped |
|
|
255
268
|
| Codex app-server | ✅ shipped (`CodexAppServerSession`, `CodexPrimedSessions`) |
|
|
256
269
|
| Claude Agent SDK | ✅ shipped (`ClaudeAgentSdkSession`; caller supplies `query`) |
|
|
270
|
+
| Muse Code | ✅ shipped (`MuseSession`, through `@muse-code/sdk`) |
|
|
257
271
|
| ACP agents / direct API | typed stubs |
|
|
258
272
|
| Gemini CLI | planned |
|
|
259
273
|
| Grok CLI | planned (when its headless/stream mode matures) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inixiative/agent-session",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Drive coding-agent CLIs as persistent, streaming, event-captured sessions over interchangeable transports, with primed decision sessions, subscription pools, routing, limit polling and continuity.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -36,18 +36,19 @@
|
|
|
36
36
|
],
|
|
37
37
|
"license": "MIT",
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@inixiative/json-rules": "^3.
|
|
39
|
+
"@inixiative/json-rules": "^3.16.0",
|
|
40
|
+
"@muse-code/sdk": "1.4.14"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"@biomejs/biome": "2.5.2",
|
|
43
|
-
"@inixiative/config": "^0.5.
|
|
44
|
-
"@types/bun": "1.4.
|
|
44
|
+
"@inixiative/config": "^0.5.173",
|
|
45
|
+
"@types/bun": "1.4.4",
|
|
45
46
|
"lefthook": "2.1.9",
|
|
46
47
|
"typescript": "6.0.3"
|
|
47
48
|
},
|
|
48
|
-
"packageManager": "bun@1.4.
|
|
49
|
+
"packageManager": "bun@1.4.4",
|
|
49
50
|
"engines": {
|
|
50
|
-
"bun": ">=1.4.
|
|
51
|
+
"bun": ">=1.4.4"
|
|
51
52
|
},
|
|
52
53
|
"repository": {
|
|
53
54
|
"type": "git",
|
|
@@ -59,6 +59,7 @@ import type {
|
|
|
59
59
|
SessionSendOptions,
|
|
60
60
|
SessionTokens,
|
|
61
61
|
SubagentLifecycle,
|
|
62
|
+
TaskLifecycle,
|
|
62
63
|
ToolDecision,
|
|
63
64
|
ToolGate,
|
|
64
65
|
ToolRequest,
|
|
@@ -385,6 +386,8 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
385
386
|
private _seenBoundaries = new Set<string>();
|
|
386
387
|
/** Task ids whose task_started was a subagent (local_agent); later task frames carry no type. */
|
|
387
388
|
private _agentTasks = new Set<string>();
|
|
389
|
+
/** Task ids whose task_started was another task type (a background shell, a monitor): id → type. */
|
|
390
|
+
private _otherTasks = new Map<string, string>();
|
|
388
391
|
|
|
389
392
|
// -- Turn queue --
|
|
390
393
|
private _queue: QueuedTurn[] = [];
|
|
@@ -829,6 +832,11 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
829
832
|
return turn.evidence.nativeOutcome === 'unknown' ? 'unacknowledged' : 'acknowledged';
|
|
830
833
|
}
|
|
831
834
|
|
|
835
|
+
/** Stop a background task via the CLI control protocol (`stop_task`); its end comes as lifecycle. */
|
|
836
|
+
async stopTask(taskId: string, opts?: { timeoutMs?: number }): Promise<void> {
|
|
837
|
+
await this._controlRequest({ subtype: 'stop_task', task_id: taskId }, opts?.timeoutMs ?? 5_000);
|
|
838
|
+
}
|
|
839
|
+
|
|
832
840
|
/** Account limits via the CLI control protocol (`get_usage`); no model turn. Requires a started session. */
|
|
833
841
|
async readLimits(opts?: { timeoutMs?: number }): Promise<LimitSnapshot | undefined> {
|
|
834
842
|
const response = await this._controlRequest(
|
|
@@ -1297,6 +1305,20 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
1297
1305
|
? raw.parent_tool_use_id
|
|
1298
1306
|
: undefined;
|
|
1299
1307
|
const lifecycle = this._subagentLifecycle(raw);
|
|
1308
|
+
// A background task (a shell, a monitor) outlives its turn like a subagent: never an admission's.
|
|
1309
|
+
const task = lifecycle || parentToolUseId ? undefined : this._taskLifecycle(raw);
|
|
1310
|
+
if (task) {
|
|
1311
|
+
this._emit({
|
|
1312
|
+
kind: 'task',
|
|
1313
|
+
timestamp: Date.now(),
|
|
1314
|
+
task,
|
|
1315
|
+
raw,
|
|
1316
|
+
correlation: 'unknown',
|
|
1317
|
+
unattributedReason: 'task',
|
|
1318
|
+
nativeSessionId: typeof raw.session_id === 'string' ? raw.session_id : undefined,
|
|
1319
|
+
});
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1300
1322
|
if (parentToolUseId || lifecycle) {
|
|
1301
1323
|
const message = raw.message as Record<string, unknown> | undefined;
|
|
1302
1324
|
const events: SessionEvent[] = lifecycle
|
|
@@ -1669,6 +1691,40 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
1669
1691
|
return claudeSubagentLifecycle(msg, phase, taskId);
|
|
1670
1692
|
}
|
|
1671
1693
|
|
|
1694
|
+
/**
|
|
1695
|
+
* A Claude Code `system` task frame for a task that is not a subagent (`task_type` other than
|
|
1696
|
+
* `local_agent`), as lifecycle. Later phases carry no task type: the session remembers it.
|
|
1697
|
+
*/
|
|
1698
|
+
private _taskLifecycle(msg: Record<string, unknown>): TaskLifecycle | undefined {
|
|
1699
|
+
if (msg.type !== 'system' || typeof msg.subtype !== 'string') return undefined;
|
|
1700
|
+
const phase = CLAUDE_TASK_PHASES[msg.subtype];
|
|
1701
|
+
const taskId = typeof msg.task_id === 'string' && msg.task_id ? msg.task_id : undefined;
|
|
1702
|
+
if (!phase || !taskId) return undefined;
|
|
1703
|
+
if (phase === 'started') {
|
|
1704
|
+
if (typeof msg.task_type !== 'string' || msg.task_type === 'local_agent') return undefined;
|
|
1705
|
+
this._otherTasks.set(taskId, msg.task_type);
|
|
1706
|
+
}
|
|
1707
|
+
const taskType = this._otherTasks.get(taskId);
|
|
1708
|
+
if (!taskType) return undefined;
|
|
1709
|
+
if (phase === 'ended') this._otherTasks.delete(taskId);
|
|
1710
|
+
const text = (value: unknown) => (typeof value === 'string' && value ? value : undefined);
|
|
1711
|
+
const patch =
|
|
1712
|
+
msg.patch && typeof msg.patch === 'object' && !Array.isArray(msg.patch)
|
|
1713
|
+
? (msg.patch as Record<string, unknown>)
|
|
1714
|
+
: undefined;
|
|
1715
|
+
const status = text(msg.status) ?? text(patch?.status);
|
|
1716
|
+
return {
|
|
1717
|
+
phase,
|
|
1718
|
+
taskId,
|
|
1719
|
+
taskType,
|
|
1720
|
+
...(text(msg.tool_use_id) ? { toolUseId: text(msg.tool_use_id) } : {}),
|
|
1721
|
+
...(text(msg.description) ? { description: text(msg.description) } : {}),
|
|
1722
|
+
...(status ? { status } : {}),
|
|
1723
|
+
...(text(msg.reason) ? { reason: text(msg.reason) } : {}),
|
|
1724
|
+
...(text(msg.summary) ? { summary: text(msg.summary) } : {}),
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1672
1728
|
private _classify(msg: Record<string, unknown>, parentToolUseId?: string): SessionEvent[] {
|
|
1673
1729
|
return this._classifyFrame(msg).map((event) =>
|
|
1674
1730
|
parentToolUseId ? { ...event, parentToolUseId } : event,
|
package/src/harness-session.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// captures every event for Oracle, and supports fork/interrupt.
|
|
12
12
|
//
|
|
13
13
|
// Implementations: ClaudeCodeSession, CodexAppServerSession,
|
|
14
|
-
// ClaudeAgentSdkSession. How a model is reached is the session's transport
|
|
14
|
+
// ClaudeAgentSdkSession, MuseSession. How a model is reached is the session's transport
|
|
15
15
|
// (see transport.ts); every transport implements this one contract.
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
17
|
|
|
@@ -84,12 +84,13 @@ export class SessionTurnError extends Error {
|
|
|
84
84
|
* session's cwd and config home: Claude found no transcript ("No conversation found with session
|
|
85
85
|
* ID": cleaned up, deleted, or kept under another project directory or `CLAUDE_CONFIG_DIR`), or
|
|
86
86
|
* Codex found no rollout ("no rollout found for thread id": deleted, or under another
|
|
87
|
-
* `CODEX_HOME`)
|
|
87
|
+
* `CODEX_HOME`), or Muse answered `sessionNotFound` (deleted, under another `XDG_DATA_HOME`, or
|
|
88
|
+
* an ephemeral host that died with it). No turn ran. The caller decides what comes next, typically a fresh session seeded
|
|
88
89
|
* from its own record of the conversation; `cwd` says where the lookup was made.
|
|
89
90
|
*/
|
|
90
91
|
export class NativeBindingMissingError extends Error {
|
|
91
92
|
constructor(
|
|
92
|
-
readonly runtime: 'claude-code' | 'codex-app-server',
|
|
93
|
+
readonly runtime: 'claude-code' | 'codex-app-server' | 'muse',
|
|
93
94
|
readonly binding: string,
|
|
94
95
|
message: string,
|
|
95
96
|
readonly cwd?: string,
|
|
@@ -122,7 +123,8 @@ export type SessionEventKind =
|
|
|
122
123
|
| 'rate_limit'
|
|
123
124
|
| 'result'
|
|
124
125
|
| 'error'
|
|
125
|
-
| 'subagent'
|
|
126
|
+
| 'subagent'
|
|
127
|
+
| 'task';
|
|
126
128
|
|
|
127
129
|
/**
|
|
128
130
|
* A subagent's lifecycle as the native runtime reports it (kind `subagent`). Claude Code: its
|
|
@@ -154,6 +156,26 @@ export interface SubagentLifecycle {
|
|
|
154
156
|
};
|
|
155
157
|
}
|
|
156
158
|
|
|
159
|
+
/**
|
|
160
|
+
* A background task that is not a subagent (kind `task`): Claude Code's `system` task frames for a
|
|
161
|
+
* background shell (`local_bash`), a monitor or a workflow. Like a subagent it can outlive the turn
|
|
162
|
+
* that started it, so its events belong to no admission (`unattributedReason: "task"`).
|
|
163
|
+
*/
|
|
164
|
+
export interface TaskLifecycle {
|
|
165
|
+
readonly phase: 'started' | 'progress' | 'updated' | 'ended';
|
|
166
|
+
/** The native task id (the id the agent's own task tools name). */
|
|
167
|
+
readonly taskId: string;
|
|
168
|
+
/** The runtime's task type, e.g. `local_bash`. */
|
|
169
|
+
readonly taskType: string;
|
|
170
|
+
/** The tool call that started it, in the session's stream (its command is that call's input). */
|
|
171
|
+
readonly toolUseId?: string;
|
|
172
|
+
readonly description?: string;
|
|
173
|
+
/** The native status at the end (completed, failed, stopped, killed). */
|
|
174
|
+
readonly status?: string;
|
|
175
|
+
readonly reason?: string;
|
|
176
|
+
readonly summary?: string;
|
|
177
|
+
}
|
|
178
|
+
|
|
157
179
|
/** Input excludes cache reads/writes when those disjoint counters are supplied.
|
|
158
180
|
* Thinking is a subset of output; TTL counters are subsets of cacheWrite.
|
|
159
181
|
* Missing optional counters mean the runtime did not report them.
|
|
@@ -191,7 +213,8 @@ export interface SessionEvent extends SessionIdentity {
|
|
|
191
213
|
| 'unmatched-tool'
|
|
192
214
|
| 'malformed-tool'
|
|
193
215
|
| 'account-status'
|
|
194
|
-
| 'subagent'
|
|
216
|
+
| 'subagent'
|
|
217
|
+
| 'task';
|
|
195
218
|
/** Local transport observation, not a native terminal acknowledgment. */
|
|
196
219
|
readonly transportOutcome?: SessionAttempt['transportOutcome'];
|
|
197
220
|
readonly nativeOutcome?: NativeOutcome;
|
|
@@ -243,6 +266,8 @@ export interface SessionEvent extends SessionIdentity {
|
|
|
243
266
|
readonly parentToolUseId?: string;
|
|
244
267
|
/** A subagent's lifecycle (kind `subagent`). */
|
|
245
268
|
readonly subagent?: SubagentLifecycle;
|
|
269
|
+
/** A background task's lifecycle (kind `task`). */
|
|
270
|
+
readonly task?: TaskLifecycle;
|
|
246
271
|
}
|
|
247
272
|
|
|
248
273
|
/** Result of a single send() — the turn's content plus all classified events. */
|
|
@@ -398,6 +423,11 @@ export interface HarnessSession {
|
|
|
398
423
|
interruptNative?(opts?: { timeoutMs?: number }): Promise<NativeInterruptOutcome>;
|
|
399
424
|
/** Poll account usage limits without a model turn. Present where the transport declares `limits.poll`. */
|
|
400
425
|
readLimits?(opts?: { timeoutMs?: number }): Promise<LimitSnapshot | undefined>;
|
|
426
|
+
/**
|
|
427
|
+
* Stop one background task (a `task` lifecycle's `taskId`) through the runtime's own control
|
|
428
|
+
* channel. Its end arrives as the task's `ended` lifecycle. Claude Code: the `stop_task` request.
|
|
429
|
+
*/
|
|
430
|
+
stopTask?(taskId: string, opts?: { timeoutMs?: number }): Promise<void>;
|
|
401
431
|
/** One immutable admission snapshot, without copying unrelated session history. */
|
|
402
432
|
inspectAttempt?(admissionId: string): SessionAttempt | undefined;
|
|
403
433
|
readonly admissionProtocol?: 'prewrite-v1';
|
package/src/index.ts
CHANGED
|
@@ -67,8 +67,28 @@ export {
|
|
|
67
67
|
type LimitWindow,
|
|
68
68
|
limitUtilization,
|
|
69
69
|
mergeLimits,
|
|
70
|
+
museLimitSnapshot,
|
|
70
71
|
} from './limits';
|
|
71
72
|
export { type LimitProbeOptions, probeClaudeLimits, probeCodexLimits } from './limits-probe';
|
|
73
|
+
// Muse Code adapter: one `muse serve` host per session through Meta's @muse-code/sdk.
|
|
74
|
+
export {
|
|
75
|
+
MUSE_SDK_VERSION,
|
|
76
|
+
type MuseApprovalMode,
|
|
77
|
+
type MuseConnect,
|
|
78
|
+
type MuseHost,
|
|
79
|
+
type MuseInitializeResult,
|
|
80
|
+
type MuseLaunch,
|
|
81
|
+
type MusePermissionProfile,
|
|
82
|
+
type MusePermissions,
|
|
83
|
+
type MuseReasoningEffort,
|
|
84
|
+
MuseSession,
|
|
85
|
+
type MuseSessionConfig,
|
|
86
|
+
type MuseUserInputAnswer,
|
|
87
|
+
type MuseUserInputHandler,
|
|
88
|
+
type MuseUserInputQuestion,
|
|
89
|
+
type MuseUserInputRequest,
|
|
90
|
+
musePermissions,
|
|
91
|
+
} from './muse-session';
|
|
72
92
|
export {
|
|
73
93
|
ContinuityError,
|
|
74
94
|
type ContinuityRefusal,
|
package/src/limits.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// Subscription usage limits, normalized across runtimes. Account identity
|
|
2
2
|
// (email, account/organization IDs) is never read into a snapshot.
|
|
3
3
|
|
|
4
|
-
export type LimitRuntime = 'claude' | 'codex';
|
|
4
|
+
export type LimitRuntime = 'claude' | 'codex' | 'muse';
|
|
5
5
|
|
|
6
6
|
export interface LimitWindow {
|
|
7
|
-
/** Stable per runtime: Claude `five_hour`/`seven_day`/…, Codex `primary`/`secondary`. */
|
|
7
|
+
/** Stable per runtime: Claude `five_hour`/`seven_day`/…, Codex `primary`/`secondary`, Muse `window`/`weekly`. */
|
|
8
8
|
readonly id: string;
|
|
9
9
|
/** 0–100. */
|
|
10
10
|
readonly usedPercent: number;
|
|
@@ -104,6 +104,42 @@ export function codexLimitSnapshot(
|
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Muse `usage/read` result (`{ usage }`) or `usage/changed` params (the usage itself): the plan's
|
|
109
|
+
* rolling `window` (five hours) and `weekly` windows, in percent used. Meta reports the windows from
|
|
110
|
+
* its latest model response, so a host that has not served a turn yet reports none. `tier` is not read:
|
|
111
|
+
* on the wire it is a numeric id (the CLI shows the plan's name), not a label known to be account-free.
|
|
112
|
+
*/
|
|
113
|
+
export function museLimitSnapshot(
|
|
114
|
+
value: unknown,
|
|
115
|
+
source: LimitSnapshot['source'],
|
|
116
|
+
observedAt = Date.now(),
|
|
117
|
+
): LimitSnapshot | undefined {
|
|
118
|
+
const outer = object(value);
|
|
119
|
+
const usage = object(outer?.usage) ?? outer;
|
|
120
|
+
const rolling = object(usage?.window),
|
|
121
|
+
weekly = object(usage?.weekly);
|
|
122
|
+
const ms = (v: unknown) => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : undefined);
|
|
123
|
+
const windows = [
|
|
124
|
+
...window(
|
|
125
|
+
'window',
|
|
126
|
+
percent(rolling?.usedPercent),
|
|
127
|
+
ms(rolling?.resetsAtMs),
|
|
128
|
+
typeof rolling?.windowDurationMins === 'number' ? rolling.windowDurationMins : undefined,
|
|
129
|
+
),
|
|
130
|
+
...window('weekly', percent(weekly?.usedPercent), ms(weekly?.resetsAtMs), 10_080),
|
|
131
|
+
];
|
|
132
|
+
if (!windows.length) return undefined;
|
|
133
|
+
return {
|
|
134
|
+
runtime: 'muse',
|
|
135
|
+
source,
|
|
136
|
+
observedAt: ms(usage?.observedAtMs) ?? observedAt,
|
|
137
|
+
windows,
|
|
138
|
+
// Explicit both ways, so a later report clears a block an earlier one (or a quota stop) set.
|
|
139
|
+
blocked: windows.some((w) => w.usedPercent >= 100),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
107
143
|
/** Claude CLI stream-json `rate_limit_event.rate_limit_info`. Utilization is a 0–1 fraction. */
|
|
108
144
|
export function claudeRateLimitSnapshot(
|
|
109
145
|
info: unknown,
|