@intentic/sandbox-contract 1.344.0 → 1.345.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 +6 -0
- package/dist/contracts/accounts.contract.d.ts +24 -0
- package/dist/contracts/accounts.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.d.ts +214 -6
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.js +13 -2
- package/dist/contracts/agent.contract.js.map +1 -1
- package/dist/contracts/agents.contract.d.ts +656 -0
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js +5 -3
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/device.contract.d.ts +242 -0
- package/dist/contracts/device.contract.d.ts.map +1 -1
- package/dist/contracts/device.contract.js +5 -1
- package/dist/contracts/device.contract.js.map +1 -1
- package/dist/contracts/endpoints.contract.d.ts +2 -0
- package/dist/contracts/endpoints.contract.d.ts.map +1 -1
- package/dist/contracts/endpoints.contract.js +1 -0
- package/dist/contracts/endpoints.contract.js.map +1 -1
- package/dist/contracts/push.contract.d.ts +11 -0
- package/dist/contracts/push.contract.d.ts.map +1 -1
- package/dist/contracts/push.contract.js +10 -1
- package/dist/contracts/push.contract.js.map +1 -1
- package/dist/contracts/runner.contract.d.ts +15 -0
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/secrets.contract.d.ts +169 -0
- package/dist/contracts/secrets.contract.d.ts.map +1 -1
- package/dist/contracts/secrets.contract.js +35 -0
- package/dist/contracts/secrets.contract.js.map +1 -1
- package/dist/contracts/sessions.contract.d.ts +2 -0
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +33 -0
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +133 -30
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/translator.contract.d.ts +24 -0
- package/dist/contracts/translator.contract.d.ts.map +1 -1
- package/dist/contracts/workspace.contract.d.ts +11 -0
- package/dist/contracts/workspace.contract.d.ts.map +1 -1
- package/dist/contracts/workspace.contract.js +11 -1
- package/dist/contracts/workspace.contract.js.map +1 -1
- package/dist/events/agent-events.d.ts +17 -0
- package/dist/events/agent-events.d.ts.map +1 -1
- package/dist/events/agent-events.js +2 -1
- package/dist/events/agent-events.js.map +1 -1
- package/dist/events/agent-transcript.d.ts +727 -0
- package/dist/events/agent-transcript.d.ts.map +1 -0
- package/dist/events/agent-transcript.js +34 -0
- package/dist/events/agent-transcript.js.map +1 -0
- package/dist/events/agent-words.d.ts +5 -0
- package/dist/events/agent-words.d.ts.map +1 -1
- package/dist/events/agent-words.js +9 -0
- package/dist/events/agent-words.js.map +1 -1
- package/dist/events/requests.d.ts +143 -0
- package/dist/events/requests.d.ts.map +1 -1
- package/dist/events/requests.js +35 -1
- package/dist/events/requests.js.map +1 -1
- package/dist/events/sandbox-notice.d.ts +7 -0
- package/dist/events/sandbox-notice.d.ts.map +1 -1
- package/dist/events/sandbox-notice.js +1 -0
- package/dist/events/sandbox-notice.js.map +1 -1
- package/dist/events/system-events.d.ts +88 -0
- package/dist/events/system-events.d.ts.map +1 -1
- package/dist/events/transcript.d.ts +20 -720
- package/dist/events/transcript.d.ts.map +1 -1
- package/dist/events/transcript.js +17 -19
- package/dist/events/transcript.js.map +1 -1
- package/dist/events/undelivered.d.ts +4 -0
- package/dist/events/undelivered.d.ts.map +1 -1
- package/dist/events/undelivered.js +13 -0
- package/dist/events/undelivered.js.map +1 -1
- package/dist/ids/conversation-ids.d.ts +2 -0
- package/dist/ids/conversation-ids.d.ts.map +1 -1
- package/dist/ids/conversation-ids.js +2 -0
- package/dist/ids/conversation-ids.js.map +1 -1
- package/dist/ids/row-ids.d.ts +3 -0
- package/dist/ids/row-ids.d.ts.map +1 -0
- package/dist/ids/row-ids.js +11 -0
- package/dist/ids/row-ids.js.map +1 -0
- package/dist/index.d.ts +1281 -39
- package/dist/index.d.ts.map +1 -1
- package/dist/models/agent-lanes.d.ts.map +1 -1
- package/dist/models/agent-lanes.js +3 -2
- package/dist/models/agent-lanes.js.map +1 -1
- package/dist/models/agent-standing.d.ts +4 -0
- package/dist/models/agent-standing.d.ts.map +1 -1
- package/dist/models/agent-standing.js +2 -1
- package/dist/models/agent-standing.js.map +1 -1
- package/dist/models/plan-pools.d.ts +5 -1
- package/dist/models/plan-pools.d.ts.map +1 -1
- package/dist/models/plan-pools.js +57 -21
- package/dist/models/plan-pools.js.map +1 -1
- package/dist/netd/generated/wire.d.ts +5 -0
- package/dist/netd/generated/wire.d.ts.map +1 -1
- package/dist/policy/command-classes.d.ts +1 -1
- package/dist/policy/command-classes.d.ts.map +1 -1
- package/dist/policy/command-classes.js +2 -2
- package/dist/policy/command-classes.js.map +1 -1
- package/dist/policy/keyring-origins.d.ts +27 -0
- package/dist/policy/keyring-origins.d.ts.map +1 -0
- package/dist/policy/keyring-origins.js +213 -0
- package/dist/policy/keyring-origins.js.map +1 -0
- package/dist/policy/keyring-seal.d.ts +9 -0
- package/dist/policy/keyring-seal.d.ts.map +1 -1
- package/dist/policy/keyring-seal.js +18 -6
- package/dist/policy/keyring-seal.js.map +1 -1
- package/dist/policy/safety-policy.d.ts +1 -1
- package/dist/policy/safety-policy.d.ts.map +1 -1
- package/dist/policy/safety-policy.js +1 -1
- package/dist/policy/safety-policy.js.map +1 -1
- package/dist/policy/turned-away.d.ts +1 -0
- package/dist/policy/turned-away.d.ts.map +1 -1
- package/dist/policy/turned-away.js +10 -0
- package/dist/policy/turned-away.js.map +1 -1
- package/dist/protocol/doors/host-protocol.d.ts +1 -0
- package/dist/protocol/doors/host-protocol.d.ts.map +1 -1
- package/dist/protocol/doors/host-protocol.js +1 -0
- package/dist/protocol/doors/host-protocol.js.map +1 -1
- package/dist/protocol/doors/webext-links.d.ts +1 -0
- package/dist/protocol/doors/webext-links.d.ts.map +1 -1
- package/dist/protocol/doors/webext-links.js +1 -0
- package/dist/protocol/doors/webext-links.js.map +1 -1
- package/dist/protocol/peer-dial.d.ts +2 -0
- package/dist/protocol/peer-dial.d.ts.map +1 -1
- package/dist/protocol/peer-dial.js +62 -29
- package/dist/protocol/peer-dial.js.map +1 -1
- package/dist/protocol/raw/raw-json-routes.d.ts +176 -0
- package/dist/protocol/raw/raw-json-routes.d.ts.map +1 -1
- package/dist/protocol/raw/raw-json-routes.js +5 -0
- package/dist/protocol/raw/raw-json-routes.js.map +1 -1
- package/dist/protocol/raw/raw-routes.d.ts +31 -0
- package/dist/protocol/raw/raw-routes.d.ts.map +1 -1
- package/dist/protocol/raw/raw-routes.js +8 -0
- package/dist/protocol/raw/raw-routes.js.map +1 -1
- package/dist/schemas/agent-place.d.ts +10 -0
- package/dist/schemas/agent-place.d.ts.map +1 -0
- package/dist/schemas/agent-place.js +15 -0
- package/dist/schemas/agent-place.js.map +1 -0
- package/dist/schemas/agent.d.ts +116 -83
- package/dist/schemas/agent.d.ts.map +1 -1
- package/dist/schemas/agent.js +26 -39
- package/dist/schemas/agent.js.map +1 -1
- package/dist/schemas/agents-list.d.ts +38 -0
- package/dist/schemas/agents-list.d.ts.map +1 -1
- package/dist/schemas/agents.d.ts +117 -16
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/agents.js +19 -21
- package/dist/schemas/agents.js.map +1 -1
- package/dist/schemas/attach.d.ts +8 -0
- package/dist/schemas/attach.d.ts.map +1 -0
- package/dist/schemas/attach.js +14 -0
- package/dist/schemas/attach.js.map +1 -0
- package/dist/schemas/command-admission.d.ts +71 -0
- package/dist/schemas/command-admission.d.ts.map +1 -0
- package/dist/schemas/command-admission.js +27 -0
- package/dist/schemas/command-admission.js.map +1 -0
- package/dist/schemas/conversation-links.d.ts +44 -0
- package/dist/schemas/conversation-links.d.ts.map +1 -0
- package/dist/schemas/conversation-links.js +35 -0
- package/dist/schemas/conversation-links.js.map +1 -0
- package/dist/schemas/data-protection.d.ts +1 -1
- package/dist/schemas/data-protection.d.ts.map +1 -1
- package/dist/schemas/devices.d.ts +4 -1
- package/dist/schemas/devices.d.ts.map +1 -1
- package/dist/schemas/devices.js +1 -0
- package/dist/schemas/devices.js.map +1 -1
- package/dist/schemas/hosts.d.ts +2 -0
- package/dist/schemas/hosts.d.ts.map +1 -1
- package/dist/schemas/hosts.js +2 -0
- package/dist/schemas/hosts.js.map +1 -1
- package/dist/schemas/keyring-native.d.ts +137 -0
- package/dist/schemas/keyring-native.d.ts.map +1 -0
- package/dist/schemas/keyring-native.js +69 -0
- package/dist/schemas/keyring-native.js.map +1 -0
- package/dist/schemas/keyring.d.ts +413 -0
- package/dist/schemas/keyring.d.ts.map +1 -1
- package/dist/schemas/keyring.js +139 -4
- package/dist/schemas/keyring.js.map +1 -1
- package/dist/schemas/phone-passwords.d.ts +186 -0
- package/dist/schemas/phone-passwords.d.ts.map +1 -0
- package/dist/schemas/phone-passwords.js +25 -0
- package/dist/schemas/phone-passwords.js.map +1 -0
- package/dist/schemas/providers/plan-limits.d.ts +74 -0
- package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
- package/dist/schemas/providers/plan-limits.js +31 -0
- package/dist/schemas/providers/plan-limits.js.map +1 -1
- package/dist/schemas/providers/provider-oauth.d.ts +24 -0
- package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
- package/dist/schemas/providers/usage.d.ts +1 -0
- package/dist/schemas/providers/usage.d.ts.map +1 -1
- package/dist/schemas/providers/usage.js +1 -0
- package/dist/schemas/providers/usage.js.map +1 -1
- package/dist/schemas/push.d.ts +13 -0
- package/dist/schemas/push.d.ts.map +1 -1
- package/dist/schemas/push.js +11 -1
- package/dist/schemas/push.js.map +1 -1
- package/dist/schemas/sandbox-passwords.d.ts +288 -0
- package/dist/schemas/sandbox-passwords.d.ts.map +1 -0
- package/dist/schemas/sandbox-passwords.js +66 -0
- package/dist/schemas/sandbox-passwords.js.map +1 -0
- package/dist/schemas/secrets.d.ts +2 -0
- package/dist/schemas/secrets.d.ts.map +1 -1
- package/dist/schemas/secrets.js +4 -0
- package/dist/schemas/secrets.js.map +1 -1
- package/dist/schemas/settings-savings.d.ts +709 -0
- package/dist/schemas/settings-savings.d.ts.map +1 -0
- package/dist/schemas/settings-savings.js +53 -0
- package/dist/schemas/settings-savings.js.map +1 -0
- package/dist/schemas/settings.d.ts +3 -671
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +3 -50
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/terminal.d.ts +5 -0
- package/dist/schemas/terminal.d.ts.map +1 -1
- package/dist/schemas/terminal.js +4 -0
- package/dist/schemas/terminal.js.map +1 -1
- package/dist/schemas/work-assurance.d.ts +24 -0
- package/dist/schemas/work-assurance.d.ts.map +1 -0
- package/dist/schemas/work-assurance.js +23 -0
- package/dist/schemas/work-assurance.js.map +1 -0
- package/dist/schemas/workspace/workspace-tree.d.ts +23 -0
- package/dist/schemas/workspace/workspace-tree.d.ts.map +1 -1
- package/dist/schemas/workspace/workspace-tree.js +20 -0
- package/dist/schemas/workspace/workspace-tree.js.map +1 -1
- package/dist/state/auth-state.d.ts +4 -0
- package/dist/state/auth-state.d.ts.map +1 -0
- package/dist/state/auth-state.js +37 -0
- package/dist/state/auth-state.js.map +1 -0
- package/dist/state/definition.d.ts +4 -0
- package/dist/state/definition.d.ts.map +1 -1
- package/dist/state/history-state.d.ts.map +1 -1
- package/dist/state/history-state.js +17 -1
- package/dist/state/history-state.js.map +1 -1
- package/dist/state/runtime-state.d.ts.map +1 -1
- package/dist/state/runtime-state.js.map +1 -1
- package/dist/text/transcript-fold.js +5 -5
- package/dist/text/transcript-fold.js.map +1 -1
- package/dist/webext/index.d.ts +1 -0
- package/dist/webext/index.d.ts.map +1 -1
- package/dist/webext/index.js +1 -0
- package/dist/webext/index.js.map +1 -1
- package/package.json +6 -5
- package/src/contracts/agent.contract.ts +13 -1
- package/src/contracts/agents.contract.ts +4 -3
- package/src/contracts/device.contract.ts +16 -0
- package/src/contracts/endpoints.contract.ts +3 -0
- package/src/contracts/push.contract.ts +23 -1
- package/src/contracts/secrets.contract.ts +39 -0
- package/src/contracts/workspace.contract.ts +16 -0
- package/src/events/agent-events.ts +4 -1
- package/src/events/agent-transcript.ts +48 -0
- package/src/events/agent-words.ts +13 -0
- package/src/events/requests.ts +41 -3
- package/src/events/sandbox-notice.ts +3 -0
- package/src/events/transcript.ts +30 -28
- package/src/events/undelivered.ts +18 -0
- package/src/ids/conversation-ids.ts +7 -0
- package/src/ids/row-ids.ts +20 -0
- package/src/models/agent-lanes.ts +9 -3
- package/src/models/agent-standing.ts +9 -1
- package/src/models/plan-pools.test.ts +128 -20
- package/src/models/plan-pools.ts +114 -31
- package/src/netd/generated/netd-wire.json +25 -0
- package/src/netd/generated/wire.ts +1 -1
- package/src/policy/command-classes.ts +2 -2
- package/src/policy/keyring-origins.test.ts +132 -0
- package/src/policy/keyring-origins.ts +287 -0
- package/src/policy/keyring-seal.test.ts +37 -2
- package/src/policy/keyring-seal.ts +33 -7
- package/src/policy/safety-policy.ts +1 -1
- package/src/policy/tests/safety-policy.test.ts +1 -1
- package/src/policy/turned-away.ts +15 -0
- package/src/protocol/doors/host-protocol.ts +3 -0
- package/src/protocol/doors/webext-links.ts +3 -0
- package/src/protocol/peer-dial.test.ts +90 -4
- package/src/protocol/peer-dial.ts +83 -39
- package/src/protocol/raw/raw-json-routes.ts +13 -0
- package/src/protocol/raw/raw-routes.ts +12 -1
- package/src/schemas/agent-place.ts +22 -0
- package/src/schemas/agent.ts +42 -78
- package/src/schemas/agents.ts +41 -46
- package/src/schemas/attach.ts +23 -0
- package/src/schemas/command-admission.ts +57 -0
- package/src/schemas/conversation-links.ts +62 -0
- package/src/schemas/data-protection.ts +1 -1
- package/src/schemas/devices.ts +4 -4
- package/src/schemas/hosts.ts +4 -0
- package/src/schemas/keyring-native.test.ts +13 -0
- package/src/schemas/keyring-native.ts +101 -0
- package/src/schemas/keyring.ts +202 -4
- package/src/schemas/phone-passwords.ts +51 -0
- package/src/schemas/providers/plan-limits.ts +49 -2
- package/src/schemas/providers/usage.ts +2 -0
- package/src/schemas/push.ts +19 -3
- package/src/schemas/sandbox-passwords.ts +88 -0
- package/src/schemas/secrets.ts +4 -0
- package/src/schemas/settings-savings.ts +125 -0
- package/src/schemas/settings.ts +8 -116
- package/src/schemas/terminal.ts +8 -0
- package/src/schemas/work-assurance.ts +36 -0
- package/src/schemas/workspace/workspace-tree.ts +32 -0
- package/src/state/auth-state.ts +89 -0
- package/src/state/history-state.ts +44 -1
- package/src/state/runtime-state.ts +5 -4
- package/src/text/transcript-fold.test.ts +67 -0
- package/src/text/transcript-fold.ts +6 -6
- package/src/webext/index.ts +2 -0
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
|
+
answerCovers,
|
|
2
3
|
bindingWindow,
|
|
3
4
|
currentWindows,
|
|
4
5
|
gatesModel,
|
|
5
6
|
gatingWindows,
|
|
6
|
-
|
|
7
|
+
pinRefused,
|
|
8
|
+
readingHeadroom,
|
|
7
9
|
preferredAccount,
|
|
8
10
|
refusalVerdict,
|
|
11
|
+
refusedHeadroom,
|
|
9
12
|
roomiestAccount,
|
|
10
13
|
scopedWindow,
|
|
14
|
+
uncoveredModels,
|
|
11
15
|
serviceState,
|
|
12
16
|
serviceStates,
|
|
13
17
|
windowLive,
|
|
@@ -155,14 +159,14 @@ const at = (measuredAt: number, ...windows: UsageWindow[]): AccountUsage => ({ w
|
|
|
155
159
|
const refusal = (over: Partial<ProviderRefusal> & Pick<ProviderRefusal, "kind">): ProviderRefusal => ({ at: 1_000, message: "no.", ...over });
|
|
156
160
|
|
|
157
161
|
test("spent is the contract's one line: 99.5% still has room, 100% is spent until its last full pool reopens", () => {
|
|
158
|
-
expect(
|
|
162
|
+
expect(readingHeadroom(usage(window({ kind: "five_hour", utilization: 99.5 })))).toEqual({ kind: "ready", room: 0.5 });
|
|
159
163
|
expect(
|
|
160
|
-
|
|
164
|
+
readingHeadroom(usage(window({ kind: "five_hour", utilization: 100, resetsAt: 50 }), window({ kind: "seven_day", utilization: 100, resetsAt: 900 }))),
|
|
161
165
|
).toEqual({ kind: "spent", reopensAt: 900 });
|
|
162
|
-
expect(
|
|
163
|
-
expect(
|
|
164
|
-
expect(
|
|
165
|
-
expect(
|
|
166
|
+
expect(readingHeadroom(usage(window({ kind: "five_hour", utilization: 100 })))).toEqual({ kind: "spent" });
|
|
167
|
+
expect(readingHeadroom(undefined)).toEqual({ kind: "unknown" });
|
|
168
|
+
expect(readingHeadroom(CLAUDE, { id: "claude-haiku-4-5" })).toEqual({ kind: "ready", room: 70 });
|
|
169
|
+
expect(readingHeadroom(CLAUDE, { id: "claude-opus-4-6" })).toEqual({ kind: "spent" });
|
|
166
170
|
});
|
|
167
171
|
|
|
168
172
|
// Anthropic reports whole percents: an account read at 100 kept serving turns for hours, so that reading is the line,
|
|
@@ -170,10 +174,10 @@ test("spent is the contract's one line: 99.5% still has room, 100% is spent unti
|
|
|
170
174
|
test("a pool read at 100 in whole percents is at the line with no room, until a refusal proves it empty", () => {
|
|
171
175
|
const week = window({ kind: "seven_day", utilization: 100, resetsAt: 900, rounded: true });
|
|
172
176
|
const atLine = usage(window({ kind: "five_hour", utilization: 75, rounded: true }), week);
|
|
173
|
-
expect(
|
|
174
|
-
expect(
|
|
177
|
+
expect(readingHeadroom(atLine)).toEqual({ kind: "ready", room: 0 });
|
|
178
|
+
expect(readingHeadroom(atLine, { id: "claude-opus-5-5" })).toEqual({ kind: "ready", room: 0 });
|
|
175
179
|
// A precise 100 is still spent.
|
|
176
|
-
expect(
|
|
180
|
+
expect(readingHeadroom(usage(window({ kind: "seven_day", utilization: 100, resetsAt: 900 })))).toEqual({ kind: "spent", reopensAt: 900 });
|
|
177
181
|
// The provider refused it: spent until the pool reopens, and a reading still at the line does not answer that.
|
|
178
182
|
const facts = { account: "a", usage: { ...atLine, measuredAt: 2_000 } };
|
|
179
183
|
const refused = refusal({ kind: "limit", account: "a" });
|
|
@@ -183,16 +187,16 @@ test("a pool read at 100 in whole percents is at the line with no room, until a
|
|
|
183
187
|
|
|
184
188
|
test("with no model named, an account is spent only when no model can run on it", () => {
|
|
185
189
|
// A full Opus slice leaves every other model the room of the fullest pool still open.
|
|
186
|
-
expect(
|
|
187
|
-
expect(
|
|
190
|
+
expect(readingHeadroom(CLAUDE)).toEqual({ kind: "ready", room: 70 });
|
|
191
|
+
expect(readingHeadroom(GOOGLE)).toEqual({ kind: "ready", room: 73 });
|
|
188
192
|
// A pool gating every model, full: nothing runs, and it reopens with that pool.
|
|
189
|
-
expect(
|
|
193
|
+
expect(readingHeadroom(usage(window({ kind: "seven_day", utilization: 100, resetsAt: 900 }), window({ kind: "model:Opus", utilization: 10, gates: { models: ["Opus"] } })))).toEqual({
|
|
190
194
|
kind: "spent",
|
|
191
195
|
reopensAt: 900,
|
|
192
196
|
});
|
|
193
197
|
// Every slice full: the first to reopen lets some model run again.
|
|
194
198
|
expect(
|
|
195
|
-
|
|
199
|
+
readingHeadroom(
|
|
196
200
|
usage(
|
|
197
201
|
window({ kind: "g", utilization: 100, resetsAt: 500, gates: { models: ["gemini"] } }),
|
|
198
202
|
window({ kind: "c", utilization: 100, resetsAt: 800, gates: { models: ["claude"] } }),
|
|
@@ -258,17 +262,42 @@ test("a refusal stands until a later reading contradicts it, and never past its
|
|
|
258
262
|
expect(refusalVerdict(refusal({ kind: "limit", account: "a" }), [])).toBe("standing");
|
|
259
263
|
});
|
|
260
264
|
|
|
261
|
-
//
|
|
262
|
-
test("an allowance no reading measures stands against
|
|
265
|
+
// An admin's allowance (Cursor's team spend limit) is one the refused account's own reading does not measure.
|
|
266
|
+
test("an allowance no reading measures stands against its own account's reading, until the provider's own reset", () => {
|
|
263
267
|
const room = at(Date.now() + 60_000, window({ kind: "seven_day", utilization: 43 }));
|
|
264
268
|
const credit = refusal({ kind: "limit", unmetered: true, at: Date.now(), resetsAt: Math.floor(Date.now() / 1000) + 3_600 });
|
|
265
|
-
expect(refusalVerdict(credit, [{ account: "a", usage: room }, { account: "b", usage: room }])).toBe("standing");
|
|
266
|
-
expect(refusalVerdict(
|
|
267
|
-
|
|
268
|
-
);
|
|
269
|
+
expect(refusalVerdict({ ...credit, account: "a" }, [{ account: "a", usage: room }, { account: "b", usage: room }])).toBe("standing");
|
|
270
|
+
expect(refusalVerdict(credit, [{ account: "a", usage: room }])).toBe("standing");
|
|
271
|
+
expect(refusalVerdict({ ...credit, account: "a", resetsAt: Math.floor(Date.now() / 1000) - 1 }, [{ account: "a", usage: room }])).toBe("answered");
|
|
269
272
|
expect(refusalVerdict({ ...credit, resetsAt: undefined }, [{ account: "a", usage: room }])).toBe("standing");
|
|
270
273
|
});
|
|
271
274
|
|
|
275
|
+
// 2026-10-19: one routed refusal ("try again at 1:53 PM", filed a day out, as an admin's credit) named no account and
|
|
276
|
+
// held all three Codex accounts spent for that day, two free ones reading 0% of their month used among them.
|
|
277
|
+
test("a nameless refusal over several accounts is answered by any of them reading room since, whatever it named", () => {
|
|
278
|
+
const filed = Date.parse("2026-10-19T13:53:35Z");
|
|
279
|
+
const now = Date.parse("2026-10-20T09:22:10Z");
|
|
280
|
+
const monthly = at(Date.parse("2026-10-20T09:22:03Z"), window({ kind: "monthly", label: "Monthly · all models", utilization: 0 }));
|
|
281
|
+
const fiveHour = at(Date.parse("2026-10-20T09:22:03Z"), window({ kind: "five_hour", utilization: 0 }), window({ kind: "seven_day", utilization: 0 }));
|
|
282
|
+
const accounts = [
|
|
283
|
+
{ account: "free-1", usage: monthly },
|
|
284
|
+
{ account: "free-2", usage: monthly },
|
|
285
|
+
{ account: "team", usage: fiveHour },
|
|
286
|
+
];
|
|
287
|
+
const codex = refusal({ kind: "limit", at: filed, model: "gpt-6.1-sol", resetsAt: Date.parse("2026-10-20T13:53:00Z") / 1000, unmetered: true });
|
|
288
|
+
expect(refusalVerdict(codex, accounts, now)).toBe("answered");
|
|
289
|
+
expect(Object.fromEntries(serviceStates(accounts, codex, undefined, now))).toEqual({
|
|
290
|
+
"free-1": { kind: "ready", room: 100 },
|
|
291
|
+
"free-2": { kind: "ready", room: 100 },
|
|
292
|
+
team: { kind: "ready", room: 100 },
|
|
293
|
+
});
|
|
294
|
+
// Until one of them is read with room after it, it stands over all of them.
|
|
295
|
+
const before = accounts.map((entry) => ({ ...entry, usage: { ...entry.usage, measuredAt: filed - 1 } }));
|
|
296
|
+
expect(refusalVerdict(codex, before, now)).toBe("standing");
|
|
297
|
+
// And never past the instant the provider named.
|
|
298
|
+
expect(refusalVerdict(codex, before, Date.parse("2026-10-20T13:53:00Z"))).toBe("answered");
|
|
299
|
+
});
|
|
300
|
+
|
|
272
301
|
|
|
273
302
|
// Codex's usage endpoint kept reading room while every turn was refused "try again at 3:38 PM": the provider's own
|
|
274
303
|
// instant is what ends the refusal, where it is known which account it refused.
|
|
@@ -340,3 +369,82 @@ test("an account at the line is tried after an unmeasured one and before a spent
|
|
|
340
369
|
expect(roomiestAccount([entry("line", atLine), entry("spent", { kind: "spent" })])).toBeUndefined();
|
|
341
370
|
expect(roomiestAccount([entry("line", atLine), entry("low", { kind: "ready", room: 2 })])?.id).toBe("low");
|
|
342
371
|
});
|
|
372
|
+
|
|
373
|
+
// A reading covers what its pools gate, and nothing more: a model no pool names is unknown, never covered by the rest.
|
|
374
|
+
test("names the served models no pool covers, and none when a pool gates every model", () => {
|
|
375
|
+
const sliced = usage(window({ kind: "google:3p-weekly", utilization: 100, gates: { models: ["claude", "gpt"] } }));
|
|
376
|
+
expect(uncoveredModels(sliced.windows, [{ id: "claude-opus-4-6-thinking" }, { id: "gemini-3.8-flash-high" }, { id: "gpt-oss-120b-medium" }])).toEqual([
|
|
377
|
+
"gemini-3.8-flash-high",
|
|
378
|
+
]);
|
|
379
|
+
expect(uncoveredModels([window({ kind: "five_hour" })], [{ id: "opus" }, { id: "sonnet" }])).toEqual([]);
|
|
380
|
+
// A pool gated `none` is shown, never a cover.
|
|
381
|
+
expect(uncoveredModels([window({ kind: "surface:web", gates: "none" })], [{ id: "opus" }])).toEqual(["opus"]);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// 2026-10-19: Google's summary stopped naming the Gemini pool, and every account read spent off its full Claude pool
|
|
385
|
+
// while its Gemini models served.
|
|
386
|
+
test("an account whose only named pools are full, with models no pool covers, is unknown rather than spent", () => {
|
|
387
|
+
const thirdPartySpent = window({ kind: "google:3p-weekly", utilization: 100, resetsAt: 9_000, gates: { models: ["claude", "gpt"] } });
|
|
388
|
+
expect(readingHeadroom({ ...usage(thirdPartySpent), uncovered: ["gemini-3.8-flash-high"] })).toEqual({ kind: "unknown" });
|
|
389
|
+
// Without the gap it is the spent account it always was, and a model the full pool gates is spent either way.
|
|
390
|
+
expect(readingHeadroom(usage(thirdPartySpent))).toEqual({ kind: "spent", reopensAt: 9_000 });
|
|
391
|
+
expect(readingHeadroom({ ...usage(thirdPartySpent), uncovered: ["gemini-3.8-flash-high"] }, { id: "claude-opus-4-6-thinking" })).toEqual({
|
|
392
|
+
kind: "spent",
|
|
393
|
+
reopensAt: 9_000,
|
|
394
|
+
});
|
|
395
|
+
// A full pool gating every model still holds every model, covered or not.
|
|
396
|
+
expect(readingHeadroom({ ...usage(window({ kind: "seven_day", utilization: 100, resetsAt: 7_000 })), uncovered: ["x"] })).toEqual({
|
|
397
|
+
kind: "spent",
|
|
398
|
+
reopensAt: 7_000,
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test("a reading contradicted by an answered turn holds that model's full pools at the line, not spent", () => {
|
|
403
|
+
const full = usage(window({ kind: "google:gemini-weekly", utilization: 100, resetsAt: 9_000, gates: { models: ["gemini"] } }));
|
|
404
|
+
const contradicted: AccountUsage = { ...full, contradicted: { at: 1, model: "gemini-3.8-flash-high" } };
|
|
405
|
+
expect(readingHeadroom(contradicted, { id: "gemini-3.8-flash-high" })).toEqual({ kind: "ready", room: 0 });
|
|
406
|
+
expect(readingHeadroom(contradicted)).toEqual({ kind: "ready", room: 0 });
|
|
407
|
+
// Another family's pool is not what the turn proved anything about.
|
|
408
|
+
const other: AccountUsage = { ...full, contradicted: { at: 1, model: "claude-opus-4-6-thinking" } };
|
|
409
|
+
expect(readingHeadroom(other, { id: "gemini-3.8-flash-high" })).toEqual({ kind: "spent", reopensAt: 9_000 });
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test("a pool the provider stopped reporting is dropped once its window closes, not kept at a floor", () => {
|
|
413
|
+
const missing = window({ kind: "google:gemini-weekly", utilization: 60, resetsAt: 1_000, missingSince: 500 });
|
|
414
|
+
const live = window({ kind: "google:3p-weekly", utilization: 20, resetsAt: 9_000 });
|
|
415
|
+
expect(currentWindows(usage(missing, live), 2_000_000)).toEqual([live]);
|
|
416
|
+
expect(currentWindows(usage(missing, live), 900_000)).toEqual([missing, live]);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// The one rule the daemon's verdict and the editor's live re-reading both judge a standing refusal by (2026-10-20: the
|
|
420
|
+
// editor drew "spent until 19:55", the pinned pool's own reset, over a daemon verdict of 15:53).
|
|
421
|
+
test("a standing refusal pins the pool its model spends, reopening when the provider said, for every surface alike", () => {
|
|
422
|
+
const reading = at(2_000, window({ kind: "five_hour", utilization: 0, resetsAt: 20_000 }), window({ kind: "seven_day", utilization: 0, resetsAt: 600_000 }));
|
|
423
|
+
const said = refusal({ kind: "limit", model: "gpt-6.1-sol", resetsAt: 9_000 });
|
|
424
|
+
expect(pinRefused(reading, said)?.windows.map((entry) => [entry.kind, entry.utilization, entry.resetsAt])).toEqual([
|
|
425
|
+
["five_hour", 100, 9_000],
|
|
426
|
+
["seven_day", 0, 600_000],
|
|
427
|
+
]);
|
|
428
|
+
expect(refusedHeadroom(reading, said)).toEqual({ kind: "spent", reopensAt: 9_000 });
|
|
429
|
+
// Where it named no instant, the pinned pool keeps its own reset, and that is when it reopens.
|
|
430
|
+
expect(refusedHeadroom(reading, { ...said, resetsAt: undefined })).toEqual({ kind: "spent", reopensAt: 20_000 });
|
|
431
|
+
// No pool to pin: the refusal itself is the evidence for its own model, and none for another's.
|
|
432
|
+
expect(pinRefused(undefined, said)).toBeUndefined();
|
|
433
|
+
expect(refusedHeadroom(undefined, said)).toEqual({ kind: "spent", reopensAt: 9_000 });
|
|
434
|
+
expect(refusedHeadroom(undefined, said, { id: "gpt-6-luna" })).toEqual({ kind: "unknown" });
|
|
435
|
+
// The daemon's verdict is this rule.
|
|
436
|
+
expect(serviceState({ account: "a", usage: reading }, said, undefined, 0)).toEqual(refusedHeadroom(reading, said));
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test("an answer on one model covers another only where every pool the other spends gates it too", () => {
|
|
440
|
+
const codex = usage(window({ kind: "five_hour", utilization: 0 }), window({ kind: "seven_day", utilization: 0 }));
|
|
441
|
+
expect(answerCovers(codex, { id: "gpt-6-luna" }, { id: "gpt-6.1-sol" })).toBe(true);
|
|
442
|
+
// A plan with no entitlement to a model meters it by a pool of its own.
|
|
443
|
+
const free = usage(window({ kind: "monthly", utilization: 0 }), window({ kind: "entitlement:gpt-6.1-sol", utilization: 100, gates: { models: ["gpt-6.1-sol"] } }));
|
|
444
|
+
expect(answerCovers(free, { id: "gpt-6-luna" }, { id: "gpt-6.1-sol" })).toBe(false);
|
|
445
|
+
// Haiku's answer says nothing of Opus's own slice, and Opus's says everything of Haiku's pools.
|
|
446
|
+
expect(answerCovers(CLAUDE, { id: "claude-haiku-4-5" }, { id: "claude-opus-4-6" })).toBe(false);
|
|
447
|
+
expect(answerCovers(CLAUDE, { id: "claude-opus-4-6" }, { id: "claude-haiku-4-5" })).toBe(true);
|
|
448
|
+
// Nothing read proves nothing.
|
|
449
|
+
expect(answerCovers(undefined, { id: "gpt-6-luna" }, { id: "gpt-6.1-sol" })).toBe(false);
|
|
450
|
+
});
|
package/src/models/plan-pools.ts
CHANGED
|
@@ -47,6 +47,11 @@ const nameMatches = (name: string, model: ModelRef): boolean => {
|
|
|
47
47
|
export const gatesModel = (gates: WindowGates, model: ModelRef): boolean =>
|
|
48
48
|
gates === "all" ? true : gates === "none" ? false : gates.models.some((name) => nameMatches(name, model));
|
|
49
49
|
|
|
50
|
+
// Served models no pool in `windows` gates: what a reading cannot say anything about. A pool gated `none` covers
|
|
51
|
+
// nothing, and one gated `all` covers every model.
|
|
52
|
+
export const uncoveredModels = (windows: readonly UsageWindow[], served: readonly ModelRef[]): string[] =>
|
|
53
|
+
served.filter((model) => !windows.some((window) => gatesModel(window.gates, model))).map((model) => model.id);
|
|
54
|
+
|
|
50
55
|
// Pools that gate this model, or, with no model given, every pool that gates anything.
|
|
51
56
|
export const gatingWindows = (usage: AccountUsage | undefined, model?: ModelRef): readonly UsageWindow[] =>
|
|
52
57
|
(usage?.windows ?? []).filter((window) => (model === undefined ? window.gates !== "none" : gatesModel(window.gates, model)));
|
|
@@ -58,6 +63,17 @@ const fullest = (windows: readonly UsageWindow[]): UsageWindow | undefined =>
|
|
|
58
63
|
// nothing was measured) — distinct from a measured 0%.
|
|
59
64
|
export const bindingWindow = (usage: AccountUsage | undefined, model?: ModelRef): UsageWindow | undefined => fullest(gatingWindows(usage, model));
|
|
60
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Whether a turn answered on `served` proves `other` had room on this account too: every pool `other` spends gates
|
|
68
|
+
* `served` as well, so the answer went through each of them. False where nothing is read, and where `other` spends a pool
|
|
69
|
+
* of its own (Claude's Opus slice, a model the plan has no entitlement to), which another model's answer says nothing
|
|
70
|
+
* about. Codex meters every model from one allowance, so a Luna turn going through proves Sol's allowance open as well.
|
|
71
|
+
*/
|
|
72
|
+
export const answerCovers = (usage: AccountUsage | undefined, served: ModelRef, other: ModelRef): boolean => {
|
|
73
|
+
const pools = gatingWindows(usage, other);
|
|
74
|
+
return pools.length > 0 && pools.every((window) => gatesModel(window.gates, served));
|
|
75
|
+
};
|
|
76
|
+
|
|
61
77
|
// The model's own metered pool, by most specific name match; distinct from bindingWindow, which may answer with an
|
|
62
78
|
// all-models pool. Two matches tied in specificity return undefined rather than guess.
|
|
63
79
|
export const scopedWindow = (usage: AccountUsage | undefined, model: ModelRef): UsageWindow | undefined => {
|
|
@@ -160,12 +176,19 @@ const lapsedWindow = (window: UsageWindow, measuredAt: number): UsageWindow => {
|
|
|
160
176
|
// while the weekly rows stayed, a partial card that reads as complete. The floor of 0 is what dropping it already meant
|
|
161
177
|
// to every verdict (an absent pool binds nothing). A reading with no window still current says nothing about now and
|
|
162
178
|
// answers none, so the account reads as unmeasured. Idempotent: a pool already lapsed stays as it is.
|
|
179
|
+
// A pool the provider stopped reporting (`missingSince`) is dropped instead once its window closes: nothing will read it
|
|
180
|
+
// again, so its floor of 0 would be a pool that is not there.
|
|
163
181
|
export const currentWindows = (usage: AccountUsage, now: number): UsageWindow[] => {
|
|
164
182
|
const live = (window: UsageWindow): boolean => window.lapsedAt === undefined && windowLive(window, usage.measuredAt, now);
|
|
165
183
|
if (!usage.windows.some(live)) {
|
|
166
184
|
return [];
|
|
167
185
|
}
|
|
168
|
-
return usage.windows.
|
|
186
|
+
return usage.windows.flatMap((window) => {
|
|
187
|
+
if (window.lapsedAt !== undefined || live(window)) {
|
|
188
|
+
return [window];
|
|
189
|
+
}
|
|
190
|
+
return window.missingSince === undefined ? [lapsedWindow(window, usage.measuredAt)] : [];
|
|
191
|
+
});
|
|
169
192
|
};
|
|
170
193
|
|
|
171
194
|
// Whether an account can serve a turn: the one rule every picker and surface reads (AccountState). The daemon runs it and
|
|
@@ -188,10 +211,12 @@ export interface ServiceFacts {
|
|
|
188
211
|
|
|
189
212
|
// A spent allowance is over once a reading with room lands after it; a refused credential once any reading does (the
|
|
190
213
|
// token worked). Nothing a reading says answers an entitlement refusal: a seatless account authenticates and reads fine.
|
|
191
|
-
// Nor does
|
|
192
|
-
|
|
214
|
+
// Nor does the refused account's own reading answer an allowance it does not measure (`unmetered`: an admin's credit).
|
|
215
|
+
// `measured` says whether this reading can speak to the allowance at all: false for that account's own, true for any
|
|
216
|
+
// account's where nothing says which one refused (refusalVerdict).
|
|
217
|
+
const answers = (refusal: ProviderRefusal, facts: ServiceFacts, measured: boolean = refusal.unmetered !== true): boolean => {
|
|
193
218
|
const measuredAt = facts.usage?.measuredAt;
|
|
194
|
-
if (refusal.kind === "entitlement" ||
|
|
219
|
+
if (refusal.kind === "entitlement" || !measured || measuredAt === undefined || measuredAt <= refusal.at) {
|
|
195
220
|
return false;
|
|
196
221
|
}
|
|
197
222
|
if (refusal.kind === "auth") {
|
|
@@ -203,8 +228,9 @@ const answers = (refusal: ProviderRefusal, facts: ServiceFacts): boolean => {
|
|
|
203
228
|
|
|
204
229
|
/**
|
|
205
230
|
* How a provider's last refusal stands against everything read since, over every account the provider holds: `gone` once
|
|
206
|
-
* the account it named is disconnected, `answered` once
|
|
207
|
-
* later reading that contradicts it, else `standing`. An empty list is one not
|
|
231
|
+
* the account it named is disconnected, `answered` once the instant it named has passed or once that account (or, for a
|
|
232
|
+
* refusal naming none, any account) has a later reading that contradicts it, else `standing`. An empty list is one not
|
|
233
|
+
* loaded yet, so the refusal stands.
|
|
208
234
|
*/
|
|
209
235
|
export const refusalVerdict = (
|
|
210
236
|
refusal: ProviderRefusal,
|
|
@@ -215,14 +241,24 @@ export const refusalVerdict = (
|
|
|
215
241
|
if (refusal.account !== undefined && named.length === 0) {
|
|
216
242
|
return accounts.length === 0 ? "standing" : "gone";
|
|
217
243
|
}
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
// translator benches the one it refused and routes on.
|
|
222
|
-
if (refusal.kind === "limit" && refusal.resetsAt !== undefined && (refusal.account !== undefined || accounts.length <= 1 || refusal.unmetered === true)) {
|
|
223
|
-
return refusal.resetsAt * 1000 <= now ? "answered" : "standing";
|
|
244
|
+
// The provider's own "try again at" is over at that instant, whichever account it refused.
|
|
245
|
+
if (refusal.kind === "limit" && refusal.resetsAt !== undefined && refusal.resetsAt * 1000 <= now) {
|
|
246
|
+
return "answered";
|
|
224
247
|
}
|
|
225
|
-
|
|
248
|
+
// Where it is known which account it refused (it named one, or there is only one), the provider's own instant
|
|
249
|
+
// outranks a usage endpoint that kept showing room while it refused: over at that instant, and not before.
|
|
250
|
+
const known = refusal.account !== undefined || accounts.length <= 1;
|
|
251
|
+
if (refusal.kind === "limit" && refusal.resetsAt !== undefined && known) {
|
|
252
|
+
return "standing";
|
|
253
|
+
}
|
|
254
|
+
if (known) {
|
|
255
|
+
return (refusal.account === undefined ? accounts : named).some((facts) => answers(refusal, facts)) ? "answered" : "standing";
|
|
256
|
+
}
|
|
257
|
+
/* A nameless refusal over several accounts is one of them saying no, and nothing says which: the translator benches
|
|
258
|
+
the one it refused and routes on, so any of them reading room since answers it, whatever kind of allowance it was
|
|
259
|
+
and whatever instant it named. Held to its instant, or to an allowance no reading measures, one team account's
|
|
260
|
+
refusal held every Codex account spent for a day, the two free ones reading 0% used included (2026-10-19). */
|
|
261
|
+
return accounts.some((facts) => answers(refusal, facts, true)) ? "answered" : "standing";
|
|
226
262
|
};
|
|
227
263
|
|
|
228
264
|
// The account reopens for this question when the pools holding it shut do: all of them for a model's own question (or
|
|
@@ -234,41 +270,88 @@ const reopening = (full: readonly UsageWindow[], first: boolean): number | undef
|
|
|
234
270
|
|
|
235
271
|
const spent = (reopensAt: number | undefined): AccountState => (reopensAt === undefined ? { kind: "spent" } : { kind: "spent", reopensAt });
|
|
236
272
|
|
|
273
|
+
// A turn on the model this reading was contradicted for went through while the pool read full: the pool is at the line,
|
|
274
|
+
// not proven empty, exactly as a rounded one is (provenFull).
|
|
275
|
+
const answeredDespite = (usage: AccountUsage | undefined, window: UsageWindow): boolean =>
|
|
276
|
+
usage?.contradicted !== undefined && gatesModel(window.gates, { id: usage.contradicted.model });
|
|
277
|
+
|
|
278
|
+
// The pools holding the account shut: for a model, every full pool it spends; with none, the full pools gating every
|
|
279
|
+
// model, else every per-model slice once all of them are full; nothing while one is open.
|
|
280
|
+
const holdingPools = (full: readonly UsageWindow[], open: readonly UsageWindow[], model: ModelRef | undefined): readonly UsageWindow[] => {
|
|
281
|
+
if (model !== undefined) {
|
|
282
|
+
return full;
|
|
283
|
+
}
|
|
284
|
+
const wholePlan = full.filter((window) => window.gates === "all");
|
|
285
|
+
if (wholePlan.length > 0) {
|
|
286
|
+
return wholePlan;
|
|
287
|
+
}
|
|
288
|
+
return open.length === 0 ? full : [];
|
|
289
|
+
};
|
|
290
|
+
|
|
237
291
|
/**
|
|
292
|
+
* What the polled reading ALONE says, never whether the account can serve: that is `serviceState`, which also reads the
|
|
293
|
+
* refusals on file, ask it through the daemon's `serviceability`/`allowanceWall` or the editor's `accountState`. A
|
|
294
|
+
* provider that rounds its percentages (Claude does, every pool) is never proven spent here, so a caller reading this
|
|
295
|
+
* as the verdict sees every spent account as open: a pick between two spent accounts re-booked a held turn for "now"
|
|
296
|
+
* that way (2026-10-19). Imported only by the few modules lint allows (.oxlintrc.json, no-restricted-imports), and was
|
|
297
|
+
* named `headroomState` until then.
|
|
298
|
+
*
|
|
238
299
|
* The plan-limit half of the verdict, at the contract's one spent line. With a model, the pools that model spends: spent
|
|
239
300
|
* once any is full, else the room the fullest leaves. With none, whether the account can serve SOME turn: spent once a
|
|
240
301
|
* pool gating every model is full, or every per-model slice is; a full slice alone (Opus at 100%) leaves the other
|
|
241
302
|
* models their room, which is then read off the fullest pool still open.
|
|
242
303
|
*/
|
|
243
|
-
export const
|
|
244
|
-
const windows = gatingWindows(usage, model);
|
|
304
|
+
export const readingHeadroom = (usage: AccountUsage | undefined, model?: ModelRef): AccountState => {
|
|
305
|
+
const windows = gatingWindows(usage, model).map((window) => (answeredDespite(usage, window) ? { ...window, rounded: true } : window));
|
|
245
306
|
if (windows.length === 0) {
|
|
246
307
|
return { kind: "unknown" };
|
|
247
308
|
}
|
|
248
309
|
const full = windows.filter(provenFull);
|
|
249
310
|
const open = windows.filter((window) => !provenFull(window));
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
311
|
+
const holding = holdingPools(full, open, model);
|
|
312
|
+
if (holding.length === 0) {
|
|
313
|
+
return { kind: "ready", room: Math.max(0, SPENT_UTILIZATION - Math.max(...open.map((window) => window.utilization))) };
|
|
314
|
+
}
|
|
315
|
+
const sliced = model === undefined && !holding.some((window) => window.gates === "all");
|
|
316
|
+
// Every per-model slice this reading names is full, but it names none for some models the account serves: whether
|
|
317
|
+
// those can run is unknown, not spent. Reading the account spent on this is how every Google account read out of
|
|
318
|
+
// allowance while its Gemini models, whose pool the provider had stopped reporting, still served.
|
|
319
|
+
if (sliced && (usage?.uncovered?.length ?? 0) > 0) {
|
|
320
|
+
return { kind: "unknown" };
|
|
255
321
|
}
|
|
256
|
-
return
|
|
322
|
+
return spent(reopening(holding, sliced));
|
|
257
323
|
};
|
|
258
324
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
325
|
+
/**
|
|
326
|
+
* A reading as a standing limit refusal leaves it, for every surface that draws or judges it: the pool the refused turn's
|
|
327
|
+
* model spends (else the one asked about, else the tightest) read full, since a polled reading freezes the moment a pool
|
|
328
|
+
* actually empties, and reopening when the provider said to try again, where it said, over whatever the polled pool last
|
|
329
|
+
* said. Never another model's allowance. The reading as it was where it has no such pool.
|
|
330
|
+
*/
|
|
331
|
+
export const pinRefused = (usage: AccountUsage | undefined, refusal: ProviderRefusal, model?: ModelRef): AccountUsage | undefined => {
|
|
332
|
+
const binding = bindingWindow(usage, refusal.model === undefined ? model : { id: refusal.model });
|
|
264
333
|
if (usage === undefined || binding === undefined) {
|
|
334
|
+
return usage;
|
|
335
|
+
}
|
|
336
|
+
const pinned = (window: UsageWindow): UsageWindow =>
|
|
337
|
+
refusal.resetsAt === undefined ? pinnedFull(window) : { ...pinnedFull(window), resetsAt: refusal.resetsAt };
|
|
338
|
+
return { ...usage, windows: usage.windows.map((window) => (window === binding ? pinned(window) : window)) };
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* The plan-limit verdict on a reading a standing limit refusal covers (pinRefused), the one rule the daemon's verdict and
|
|
343
|
+
* the editor's live re-reading both judge by. Spent until the provider's own "try again at" where it named one: the
|
|
344
|
+
* editor once re-judged the pinned pool by its own reset alone, and drew "spent until 19:55" over a daemon verdict, and a
|
|
345
|
+
* model picker, both saying 15:53 (2026-10-20).
|
|
346
|
+
*/
|
|
347
|
+
export const refusedHeadroom = (usage: AccountUsage | undefined, refusal: ProviderRefusal, model?: ModelRef): AccountState => {
|
|
348
|
+
const pinned = pinRefused(usage, refusal, model);
|
|
349
|
+
if (pinned === usage) {
|
|
265
350
|
// No pool to pin: the refusal itself is the evidence, unless it was about a different model than this question.
|
|
266
351
|
const sameModel = refusal.model === undefined || model === undefined || refusal.model === model.id;
|
|
267
|
-
return sameModel ? spent(refusal.resetsAt) :
|
|
352
|
+
return sameModel ? spent(refusal.resetsAt) : readingHeadroom(usage, model);
|
|
268
353
|
}
|
|
269
|
-
const
|
|
270
|
-
const state = headroomState({ ...usage, windows: pinned }, model);
|
|
271
|
-
// The provider's own "try again at" is when it reopens, over whatever the polled pool last said.
|
|
354
|
+
const state = readingHeadroom(pinned, model);
|
|
272
355
|
return state.kind === "spent" && refusal.resetsAt !== undefined ? spent(refusal.resetsAt) : state;
|
|
273
356
|
};
|
|
274
357
|
|
|
@@ -303,7 +386,7 @@ export const serviceState = (facts: ServiceFacts, refusal?: ProviderRefusal, mod
|
|
|
303
386
|
if (cooling?.until !== undefined && cooling.until * 1000 > now) {
|
|
304
387
|
return { kind: "blocked", fix: "wait", reason: cooling.reason ?? "cooling down", until: cooling.until };
|
|
305
388
|
}
|
|
306
|
-
return refusal?.kind === "limit" ?
|
|
389
|
+
return refusal?.kind === "limit" ? refusedHeadroom(facts.usage, refusal, model) : readingHeadroom(facts.usage, model);
|
|
307
390
|
};
|
|
308
391
|
|
|
309
392
|
/** The refusals a provider's accounts are judged against: its last one, or every one on file (each account's own beside it). */
|
|
@@ -874,6 +874,31 @@
|
|
|
874
874
|
"connected"
|
|
875
875
|
],
|
|
876
876
|
"type": "object"
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
"description": "A line the person typed with echo off in one of the editor's own terminal sessions (`web-…`), after a\npassword-ish question: told once, on Enter, for the daemon to hand Passwords if the owner turned remembering on\nfor sandbox terminals (2026-10-19). Never logged on either side; netd keeps nothing of it after sending.",
|
|
880
|
+
"properties": {
|
|
881
|
+
"kind": {
|
|
882
|
+
"const": "hiddenLine",
|
|
883
|
+
"type": "string"
|
|
884
|
+
},
|
|
885
|
+
"prompt": {
|
|
886
|
+
"type": "string"
|
|
887
|
+
},
|
|
888
|
+
"session": {
|
|
889
|
+
"type": "string"
|
|
890
|
+
},
|
|
891
|
+
"value": {
|
|
892
|
+
"type": "string"
|
|
893
|
+
}
|
|
894
|
+
},
|
|
895
|
+
"required": [
|
|
896
|
+
"kind",
|
|
897
|
+
"session",
|
|
898
|
+
"prompt",
|
|
899
|
+
"value"
|
|
900
|
+
],
|
|
901
|
+
"type": "object"
|
|
877
902
|
}
|
|
878
903
|
],
|
|
879
904
|
"title": "ToNode"
|
|
@@ -73,7 +73,7 @@ export type TerminalPlan = { "plan": "session", name: string, createIn?: string,
|
|
|
73
73
|
/**
|
|
74
74
|
* Everything netd sends Node.
|
|
75
75
|
*/
|
|
76
|
-
export type ToNode = { "kind": "ask", id: number, question: Question, } | { "kind": "answer", id: number, answer: NetdAnswer, } | { "kind": "refused", id: number, message: string, } | { "kind": "tunnel", connected: boolean, reason?: string, refused?: TunnelRefusal, };
|
|
76
|
+
export type ToNode = { "kind": "ask", id: number, question: Question, } | { "kind": "answer", id: number, answer: NetdAnswer, } | { "kind": "refused", id: number, message: string, } | { "kind": "tunnel", connected: boolean, reason?: string, refused?: TunnelRefusal, } | { "kind": "hiddenLine", session: string, prompt: string, value: string, };
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
79
|
* The ingress tunnel's door, the reachability grant it presents there, and the daemon's transfer routes it announces
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HISTORY_ROOT, WORKSPACE_ROOT } from "@intentic/constants";
|
|
2
|
-
import { type CommandClass, CommandClassSchema, type CommandLocus } from "../schemas/
|
|
2
|
+
import { type CommandClass, CommandClassSchema, type CommandLocus } from "../schemas/command-admission.js";
|
|
3
3
|
import { SECRET_REFERENCE } from "../schemas/needs.js";
|
|
4
4
|
import { inertRegions, isLive } from "../text/shell-regions.js";
|
|
5
5
|
|
|
@@ -413,7 +413,7 @@ const findAndXargsDeletes = (command: string): CommandSpan[] => [
|
|
|
413
413
|
const POWERSHELL_RECURSE = String.raw`\s-r(?:e(?:c(?:u(?:r(?:s(?:e)?)?)?)?)?)?(?::\$true)?(?![\w-])`;
|
|
414
414
|
const WINDOWS_RECURSIVE_DELETES = [
|
|
415
415
|
new RegExp(String.raw`(?<![\w-])(?:Remove-Item|ri|rd|rmdir|del|erase)\b[^|;&\n]*?${POWERSHELL_RECURSE}`, "i"),
|
|
416
|
-
|
|
416
|
+
/(?<![\w-])rm\b[^|;&\n]*?\s-recurse(?![\w-])/i,
|
|
417
417
|
/(?<![\w-])(?:rd|rmdir|del|erase)\b[^|;&\n]*?\s\/s\b/i,
|
|
418
418
|
/\[(?:System\.)?IO\.Directory\]::Delete\s*\([^)]*,\s*\$true\s*\)/i,
|
|
419
419
|
];
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { KEYRING_NAME_RE, type KeyringOrigin } from "../schemas/keyring.js";
|
|
2
|
+
import {
|
|
3
|
+
type CaptureCandidate,
|
|
4
|
+
entriesFor,
|
|
5
|
+
entryNameFor,
|
|
6
|
+
findCaptureTarget,
|
|
7
|
+
gitOrigin,
|
|
8
|
+
mergeRules,
|
|
9
|
+
originKey,
|
|
10
|
+
originsOfRules,
|
|
11
|
+
rememberSwitchFor,
|
|
12
|
+
rulesForOrigin,
|
|
13
|
+
rulesServe,
|
|
14
|
+
titleForOrigin,
|
|
15
|
+
} from "./keyring-origins.js";
|
|
16
|
+
|
|
17
|
+
const NONE = { programs: [], packages: [], hosts: [], askpass: [] };
|
|
18
|
+
|
|
19
|
+
const entry = (name: string, origin: KeyringOrigin, username?: string): CaptureCandidate => ({ name, rules: rulesForOrigin(origin), username, title: titleForOrigin(origin) });
|
|
20
|
+
|
|
21
|
+
describe("origins and rules", () => {
|
|
22
|
+
test("a site keeps its host, without www and case", () => {
|
|
23
|
+
expect(rulesForOrigin({ kind: "site", host: "WWW.GitHub.com" })).toEqual({ ...NONE, hosts: ["github.com"] });
|
|
24
|
+
expect(originKey({ kind: "site", host: "www.github.com." })).toBe(originKey({ kind: "site", host: "github.com" }));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("terminal places become askpass matchers, and read back as the same places", () => {
|
|
28
|
+
const places: KeyringOrigin[] = [
|
|
29
|
+
{ kind: "ssh", target: "radar@build-01" },
|
|
30
|
+
{ kind: "ssh-key", path: "/home/radar/.ssh/id_ed25519" },
|
|
31
|
+
{ kind: "sudo" },
|
|
32
|
+
{ kind: "sudo", user: "root" },
|
|
33
|
+
];
|
|
34
|
+
for (const place of places) {
|
|
35
|
+
expect(originsOfRules({ ...NONE, ...rulesForOrigin(place) })).toEqual([place]);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("git answers for every repository on its host", () => {
|
|
40
|
+
expect(gitOrigin("https://github.com/intentic/intentic.git")).toBe("https://github.com");
|
|
41
|
+
const rules = rulesForOrigin({ kind: "git", url: "https://github.com/intentic/intentic" });
|
|
42
|
+
expect(rules).toEqual({ ...NONE, askpass: ["git:https://github.com"], hosts: ["github.com"] });
|
|
43
|
+
expect(rulesServe(rules, { kind: "git", url: "https://github.com/other/repo" })).toBe(true);
|
|
44
|
+
// Its site is said once, by the git chip.
|
|
45
|
+
expect(originsOfRules(rules)).toEqual([{ kind: "git", url: "https://github.com" }]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("a sandbox terminal's prompt places nothing on this computer", () => {
|
|
49
|
+
expect(rulesForOrigin({ kind: "prompt", text: "Password:", command: "npm login", sandbox: "https://x.intentic.dev" })).toEqual(NONE);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("merging adds a place without repeating one", () => {
|
|
53
|
+
const a = rulesForOrigin({ kind: "site", host: "github.com" });
|
|
54
|
+
const b = rulesForOrigin({ kind: "git", url: "https://github.com" });
|
|
55
|
+
expect(mergeRules(a, b)).toEqual({ ...NONE, hosts: ["github.com"], askpass: ["git:https://github.com"] });
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("serving a place", () => {
|
|
60
|
+
test("a saved site fills its subdomains, and a wildcard its domain", () => {
|
|
61
|
+
expect(rulesServe({ ...NONE, hosts: ["github.com"] }, { kind: "site", host: "gist.github.com" })).toBe(true);
|
|
62
|
+
expect(rulesServe({ ...NONE, hosts: ["*.corp.example"] }, { kind: "site", host: "corp.example" })).toBe(true);
|
|
63
|
+
expect(rulesServe({ ...NONE, hosts: ["github.com"] }, { kind: "site", host: "notgithub.com" })).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("a program matches by its file name, without case or folder", () => {
|
|
67
|
+
expect(rulesServe({ ...NONE, programs: ["C:\\Program Files\\Fortinet\\FortiClient.exe"] }, { kind: "program", program: "forticlient.EXE" })).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("the entry a capture updates", () => {
|
|
72
|
+
const github = entry("GITHUB_COM", { kind: "site", host: "github.com" }, "radarsu");
|
|
73
|
+
const work = entry("GITHUB_COM_WORK", { kind: "site", host: "github.com" }, "radar-work");
|
|
74
|
+
|
|
75
|
+
test("the same place and account is an update", () => {
|
|
76
|
+
expect(findCaptureTarget([github, work], { kind: "site", host: "www.github.com" }, "Radarsu")?.name).toBe("GITHUB_COM");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("another account at the same place is a new entry", () => {
|
|
80
|
+
expect(findCaptureTarget([github], { kind: "site", host: "github.com" }, "someone-else")).toBeUndefined();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("a sandbox prompt is found again by its title", () => {
|
|
84
|
+
const origin: KeyringOrigin = { kind: "prompt", text: "Password:", command: "npm login", sandbox: "https://x.intentic.dev" };
|
|
85
|
+
expect(findCaptureTarget([entry("NPM_LOGIN", origin)], origin, undefined)?.name).toBe("NPM_LOGIN");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("a need offers the asked-for account first", () => {
|
|
89
|
+
expect(entriesFor([github, work], { kind: "site", host: "github.com" }, "radar-work").map((found) => found.name)).toEqual(["GITHUB_COM_WORK", "GITHUB_COM"]);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("titles and names", () => {
|
|
94
|
+
test("each place reads as a person would say it", () => {
|
|
95
|
+
expect(titleForOrigin({ kind: "site", host: "www.github.com" })).toBe("github.com");
|
|
96
|
+
expect(titleForOrigin({ kind: "program", program: "C:\\x\\FortiClient.exe" })).toBe("FortiClient");
|
|
97
|
+
expect(titleForOrigin({ kind: "program", program: "FortiClient.exe", label: "FortiClient VPN" })).toBe("FortiClient VPN");
|
|
98
|
+
expect(titleForOrigin({ kind: "ssh", target: "radar@build-01" })).toBe("ssh radar@build-01");
|
|
99
|
+
expect(titleForOrigin({ kind: "git", url: "https://github.com/a/b" })).toBe("git github.com");
|
|
100
|
+
expect(titleForOrigin({ kind: "prompt", text: "Password for npm: ", command: "npm login" })).toBe("npm login");
|
|
101
|
+
expect(titleForOrigin({ kind: "site", host: "github.com" }, " GitHub ")).toBe("GitHub");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("a name takes the title, then the account, then a number", () => {
|
|
105
|
+
const taken = new Set<string>();
|
|
106
|
+
const name = (title: string, user?: string): string => {
|
|
107
|
+
const made = entryNameFor(title, user, (candidate) => taken.has(candidate));
|
|
108
|
+
taken.add(made);
|
|
109
|
+
return made;
|
|
110
|
+
};
|
|
111
|
+
expect(name("github.com", "radarsu@example.com")).toBe("GITHUB_COM");
|
|
112
|
+
expect(name("github.com", "radarsu@example.com")).toBe("GITHUB_COM_RADARSU");
|
|
113
|
+
expect(name("github.com", "radarsu@example.com")).toBe("GITHUB_COM_RADARSU_2");
|
|
114
|
+
expect(name("ssh radar@build-01")).toBe("SSH_RADAR_BUILD_01");
|
|
115
|
+
expect(name("1Password")).toBe("_1PASSWORD");
|
|
116
|
+
expect(name("Zürich Bank")).toBe("ZURICH_BANK");
|
|
117
|
+
expect(name("···")).toBe("PASSWORD");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("every name fits a reference", () => {
|
|
121
|
+
for (const title of ["a".repeat(400), "ąę", "--", "x y z", "9"]) {
|
|
122
|
+
expect(entryNameFor(title, "u", () => false)).toMatch(KEYRING_NAME_RE);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("deliberate acts need no remember switch", () => {
|
|
128
|
+
expect(rememberSwitchFor("browser")).toBe("browser");
|
|
129
|
+
expect(rememberSwitchFor("git")).toBe("terminal");
|
|
130
|
+
expect(rememberSwitchFor("terminal")).toBe("sandbox");
|
|
131
|
+
expect(rememberSwitchFor("import")).toBeUndefined();
|
|
132
|
+
});
|