crewly 1.20.187 → 1.20.188
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/config/skills/agent/core/project-tickets/SKILL.md +3 -2
- package/config/skills/agent/core/project-tickets/execute.sh +4 -4
- package/config/skills/agent/core/project-tickets/execute.test.sh +2 -2
- package/dist/backend/backend/src/constants.d.ts +70 -12
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +70 -12
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.d.ts +1 -1
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.js +5 -1
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.routes.d.ts +1 -1
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.routes.js +1 -1
- package/dist/backend/backend/src/controllers/system/usage.controller.d.ts +46 -0
- package/dist/backend/backend/src/controllers/system/usage.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/system/usage.controller.js +181 -0
- package/dist/backend/backend/src/controllers/system/usage.controller.js.map +1 -0
- package/dist/backend/backend/src/index.d.ts +7 -0
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +59 -4
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/routes/modules/system.routes.js +3 -3
- package/dist/backend/backend/src/routes/modules/system.routes.js.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js +7 -7
- package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/owner-message-watchdog.service.d.ts +4 -2
- package/dist/backend/backend/src/services/messaging/owner-message-watchdog.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/owner-message-watchdog.service.js +6 -1
- package/dist/backend/backend/src/services/messaging/owner-message-watchdog.service.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/queue-processor.service.js +2 -2
- package/dist/backend/backend/src/services/monitoring/activity-monitor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/monitoring/activity-monitor.service.js +4 -2
- package/dist/backend/backend/src/services/monitoring/activity-monitor.service.js.map +1 -1
- package/dist/backend/backend/src/services/monitoring/antigravity-usage-sync.service.d.ts +130 -0
- package/dist/backend/backend/src/services/monitoring/antigravity-usage-sync.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/monitoring/antigravity-usage-sync.service.js +274 -0
- package/dist/backend/backend/src/services/monitoring/antigravity-usage-sync.service.js.map +1 -0
- package/dist/backend/backend/src/services/monitoring/codex-rollout-sync.service.d.ts +152 -0
- package/dist/backend/backend/src/services/monitoring/codex-rollout-sync.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/monitoring/codex-rollout-sync.service.js +311 -0
- package/dist/backend/backend/src/services/monitoring/codex-rollout-sync.service.js.map +1 -0
- package/dist/backend/backend/src/services/monitoring/token-usage.service.d.ts +53 -1
- package/dist/backend/backend/src/services/monitoring/token-usage.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/monitoring/token-usage.service.js +51 -3
- package/dist/backend/backend/src/services/monitoring/token-usage.service.js.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-decision.d.ts +4 -4
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-decision.d.ts.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-decision.js +1 -1
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-decision.js.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-messages.d.ts +4 -3
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-messages.d.ts.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-messages.js +7 -4
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-messages.js.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.d.ts +40 -9
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.js +68 -20
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.js.map +1 -1
- package/dist/backend/backend/src/services/spend/spend-cap-command.d.ts +48 -18
- package/dist/backend/backend/src/services/spend/spend-cap-command.d.ts.map +1 -1
- package/dist/backend/backend/src/services/spend/spend-cap-command.js +195 -62
- package/dist/backend/backend/src/services/spend/spend-cap-command.js.map +1 -1
- package/dist/backend/backend/src/services/spend/spend-cap.gate.d.ts +11 -15
- package/dist/backend/backend/src/services/spend/spend-cap.gate.d.ts.map +1 -1
- package/dist/backend/backend/src/services/spend/spend-cap.gate.js +8 -13
- package/dist/backend/backend/src/services/spend/spend-cap.gate.js.map +1 -1
- package/dist/backend/backend/src/services/spend/spend-cap.service.d.ts +179 -64
- package/dist/backend/backend/src/services/spend/spend-cap.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/spend/spend-cap.service.js +406 -149
- package/dist/backend/backend/src/services/spend/spend-cap.service.js.map +1 -1
- package/dist/backend/backend/src/services/spend/spend-cap.store.d.ts +52 -11
- package/dist/backend/backend/src/services/spend/spend-cap.store.d.ts.map +1 -1
- package/dist/backend/backend/src/services/spend/spend-cap.store.js +67 -21
- package/dist/backend/backend/src/services/spend/spend-cap.store.js.map +1 -1
- package/dist/backend/backend/src/services/spend/spend-cap.wiring.d.ts +3 -1
- package/dist/backend/backend/src/services/spend/spend-cap.wiring.d.ts.map +1 -1
- package/dist/backend/backend/src/services/spend/spend-cap.wiring.js +21 -7
- package/dist/backend/backend/src/services/spend/spend-cap.wiring.js.map +1 -1
- package/dist/backend/backend/src/services/spend/spend-ledger.service.d.ts +57 -36
- package/dist/backend/backend/src/services/spend/spend-ledger.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/spend/spend-ledger.service.js +80 -55
- package/dist/backend/backend/src/services/spend/spend-ledger.service.js.map +1 -1
- package/dist/backend/backend/src/services/task-pool/work-item-usage.d.ts +10 -1
- package/dist/backend/backend/src/services/task-pool/work-item-usage.d.ts.map +1 -1
- package/dist/backend/backend/src/services/task-pool/work-item-usage.js +2 -0
- package/dist/backend/backend/src/services/task-pool/work-item-usage.js.map +1 -1
- package/dist/backend/backend/src/services/usage/token-format.d.ts +49 -0
- package/dist/backend/backend/src/services/usage/token-format.d.ts.map +1 -0
- package/dist/backend/backend/src/services/usage/token-format.js +81 -0
- package/dist/backend/backend/src/services/usage/token-format.js.map +1 -0
- package/dist/backend/backend/src/services/usage/usage-stats.service.d.ts +132 -0
- package/dist/backend/backend/src/services/usage/usage-stats.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/usage/usage-stats.service.js +226 -0
- package/dist/backend/backend/src/services/usage/usage-stats.service.js.map +1 -0
- package/dist/backend/backend/src/types/decision.types.d.ts +2 -2
- package/dist/backend/backend/src/types/ticket-autopilot.types.d.ts +24 -7
- package/dist/backend/backend/src/types/ticket-autopilot.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/ticket-autopilot.types.js +29 -13
- package/dist/backend/backend/src/types/ticket-autopilot.types.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +70 -12
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +70 -12
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.js +7 -7
- package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/cli/backend/src/services/messaging/owner-message-watchdog.service.d.ts +4 -2
- package/dist/cli/backend/src/services/messaging/owner-message-watchdog.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/messaging/owner-message-watchdog.service.js +6 -1
- package/dist/cli/backend/src/services/messaging/owner-message-watchdog.service.js.map +1 -1
- package/dist/cli/backend/src/services/messaging/queue-processor.service.js +2 -2
- package/dist/cli/backend/src/services/monitoring/activity-monitor.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/monitoring/activity-monitor.service.js +4 -2
- package/dist/cli/backend/src/services/monitoring/activity-monitor.service.js.map +1 -1
- package/dist/cli/backend/src/services/monitoring/token-usage.service.d.ts +53 -1
- package/dist/cli/backend/src/services/monitoring/token-usage.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/monitoring/token-usage.service.js +51 -3
- package/dist/cli/backend/src/services/monitoring/token-usage.service.js.map +1 -1
- package/dist/cli/backend/src/services/spend/spend-cap.gate.d.ts +11 -15
- package/dist/cli/backend/src/services/spend/spend-cap.gate.d.ts.map +1 -1
- package/dist/cli/backend/src/services/spend/spend-cap.gate.js +8 -13
- package/dist/cli/backend/src/services/spend/spend-cap.gate.js.map +1 -1
- package/dist/cli/backend/src/services/task-pool/work-item-usage.d.ts +10 -1
- package/dist/cli/backend/src/services/task-pool/work-item-usage.d.ts.map +1 -1
- package/dist/cli/backend/src/services/task-pool/work-item-usage.js +2 -0
- package/dist/cli/backend/src/services/task-pool/work-item-usage.js.map +1 -1
- package/dist/cli/backend/src/services/usage/token-format.d.ts +49 -0
- package/dist/cli/backend/src/services/usage/token-format.d.ts.map +1 -0
- package/dist/cli/backend/src/services/usage/token-format.js +81 -0
- package/dist/cli/backend/src/services/usage/token-format.js.map +1 -0
- package/dist/cli/backend/src/types/decision.types.d.ts +2 -2
- package/dist/cli/backend/src/types/ticket-autopilot.types.d.ts +24 -7
- package/dist/cli/backend/src/types/ticket-autopilot.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/ticket-autopilot.types.js +29 -13
- package/dist/cli/backend/src/types/ticket-autopilot.types.js.map +1 -1
- package/frontend/dist/assets/{index-15c2faf2.js → index-49f50b45.js} +276 -276
- package/frontend/dist/index.html +1 -1
- package/package.json +1 -1
- package/dist/backend/backend/src/controllers/system/spend.controller.d.ts +0 -32
- package/dist/backend/backend/src/controllers/system/spend.controller.d.ts.map +0 -1
- package/dist/backend/backend/src/controllers/system/spend.controller.js +0 -100
- package/dist/backend/backend/src/controllers/system/spend.controller.js.map +0 -1
|
@@ -1,33 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Daily token caps with a hard stop: per agent, per team and all agents
|
|
3
|
+
* together, plus temporary boosts.
|
|
3
4
|
*
|
|
5
|
+
* - The unit is tokens (input incl. cached + output, see `eventTokens`).
|
|
4
6
|
* - Caps are the owner's own limits (unlike the runtime fallback, which
|
|
5
7
|
* reacts to a provider's usage limit). All are OFF until the owner sets
|
|
6
|
-
* one: a default per-agent cap, per-agent overrides, and an
|
|
7
|
-
* all-agents
|
|
8
|
+
* one: a default per-agent cap, per-agent overrides, per-team caps, and an
|
|
9
|
+
* optional all-agents total.
|
|
8
10
|
* - At {@link SPEND_CAP_CONSTANTS.WARN_FRACTION} of a cap the owner gets ONE
|
|
9
11
|
* heads-up.
|
|
10
12
|
* - At 100% the agent is hard-stopped: {@link stopOf} answers the delivery
|
|
11
13
|
* and wake gates, so no new turn starts (the current one may finish) and
|
|
12
|
-
* messages stay queued. The owner gets ONE decision card: "
|
|
13
|
-
* today" / "Keep stopped".
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
14
|
+
* messages stay queued. The owner gets ONE decision card: "Boost +X
|
|
15
|
+
* today" / "Unlimited today" / "Keep stopped".
|
|
16
|
+
* - A boost covers an agent, a team (its members) or everyone. For every
|
|
17
|
+
* agent it covers, it adds `extraTokens` to EVERY cap checked for that
|
|
18
|
+
* agent (its own, its team's, the total), or lifts them all
|
|
19
|
+
* (`unlimited`). It ends at `until` — by default the next local midnight.
|
|
20
|
+
* - Local midnight resets the day; when a stop lifts (midnight, boost, cap
|
|
21
|
+
* change) the queued messages are released.
|
|
20
22
|
*
|
|
21
23
|
* specs/2026-10-02-spend-cap.md
|
|
22
24
|
*
|
|
23
25
|
* @module services/spend/spend-cap.service
|
|
24
26
|
*/
|
|
27
|
+
import { randomUUID } from 'crypto';
|
|
25
28
|
import { ORCHESTRATOR_SESSION_NAME, SPEND_CAP_CONSTANTS as C } from '../../constants.js';
|
|
26
29
|
import { localDateKey, localMidnight } from '../project-tickets/ticket-autopilot-decision.js';
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
30
|
+
import { compactTokens, formatTokens, parseTokenAmount } from '../usage/token-format.js';
|
|
31
|
+
import { spendCapReason } from './spend-cap.gate.js';
|
|
29
32
|
import { emptyDay, emptySpendCapFile } from './spend-cap.store.js';
|
|
30
|
-
/** A rejected
|
|
33
|
+
/** A rejected change. */
|
|
31
34
|
export class SpendCapError extends Error {
|
|
32
35
|
status;
|
|
33
36
|
/**
|
|
@@ -52,42 +55,57 @@ function isCap(v) {
|
|
|
52
55
|
/**
|
|
53
56
|
* Validate a cap from the API.
|
|
54
57
|
*
|
|
55
|
-
* @param v - Value
|
|
58
|
+
* @param v - Value (number or "20M")
|
|
56
59
|
* @param what - Field name for the error
|
|
57
|
-
* @returns The cap (whole
|
|
60
|
+
* @returns The cap (whole tokens) or null
|
|
58
61
|
* @throws SpendCapError
|
|
59
62
|
*/
|
|
60
63
|
function parseCap(v, what) {
|
|
61
|
-
if (v === null)
|
|
64
|
+
if (v === null || v === '')
|
|
62
65
|
return null;
|
|
63
|
-
const n =
|
|
64
|
-
if (
|
|
65
|
-
throw new SpendCapError(400, `${what} must be a positive
|
|
66
|
-
return
|
|
66
|
+
const n = parseTokenAmount(v);
|
|
67
|
+
if (n === null)
|
|
68
|
+
throw new SpendCapError(400, `${what} must be a positive number of tokens (e.g. 5000000 or "5M"), or null to turn it off`);
|
|
69
|
+
return n;
|
|
67
70
|
}
|
|
68
71
|
/**
|
|
69
|
-
*
|
|
70
|
-
* whole dollars, and always above what was already spent.
|
|
72
|
+
* Round up to a whole million (at least 1M).
|
|
71
73
|
*
|
|
72
|
-
* @param
|
|
73
|
-
* @
|
|
74
|
-
* @returns USD
|
|
74
|
+
* @param tokens - Tokens
|
|
75
|
+
* @returns Rounded tokens
|
|
75
76
|
*/
|
|
76
|
-
export function
|
|
77
|
-
return Math.max(Math.ceil(
|
|
77
|
+
export function roundUpMillions(tokens) {
|
|
78
|
+
return Math.max(1_000_000, Math.ceil(tokens / 1_000_000) * 1_000_000);
|
|
78
79
|
}
|
|
79
80
|
/**
|
|
80
|
-
*
|
|
81
|
+
* The "Boost +X today" amount on a cap card: the cap times BOOST_FACTOR,
|
|
82
|
+
* rounded up to a whole million.
|
|
83
|
+
*
|
|
84
|
+
* @param capTokens - Cap in force
|
|
85
|
+
* @returns Tokens
|
|
86
|
+
*/
|
|
87
|
+
export function suggestedBoost(capTokens) {
|
|
88
|
+
return roundUpMillions(capTokens * C.BOOST_FACTOR);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Read the amount out of a "Boost +X tokens today" option label.
|
|
81
92
|
*
|
|
82
93
|
* @param label - Option label
|
|
83
|
-
* @returns
|
|
94
|
+
* @returns Tokens, `unlimited`, or null when the label is not a boost
|
|
84
95
|
*/
|
|
85
|
-
export function
|
|
86
|
-
const
|
|
87
|
-
|
|
96
|
+
export function boostOfLabel(label) {
|
|
97
|
+
const text = (label ?? '').trim();
|
|
98
|
+
if (text.toLowerCase().startsWith(C.OPTIONS.UNLIMITED.toLowerCase()))
|
|
99
|
+
return 'unlimited';
|
|
100
|
+
const m = /^Boost \+(\S+?)(?: tokens)? today/i.exec(text);
|
|
101
|
+
return m ? parseTokenAmount(m[1]) : null;
|
|
102
|
+
}
|
|
103
|
+
/** Team target key. */
|
|
104
|
+
export function teamTarget(teamId) {
|
|
105
|
+
return `${C.TEAM_TARGET_PREFIX}${teamId}`;
|
|
88
106
|
}
|
|
89
107
|
/**
|
|
90
|
-
*
|
|
108
|
+
* Daily token cap service.
|
|
91
109
|
*/
|
|
92
110
|
export class SpendCapService {
|
|
93
111
|
deps;
|
|
@@ -97,9 +115,11 @@ export class SpendCapService {
|
|
|
97
115
|
timer = null;
|
|
98
116
|
/** Sessions stopped at the last evaluation (to release them when the stop lifts) */
|
|
99
117
|
lastStopped = new Set();
|
|
100
|
-
/** Evaluation passes run one after another */
|
|
101
118
|
chain = Promise.resolve();
|
|
102
119
|
queuedPass = null;
|
|
120
|
+
/** Team index, refreshed on every evaluation (the gate is synchronous) */
|
|
121
|
+
teamList = [];
|
|
122
|
+
teamsOfSession = new Map();
|
|
103
123
|
/**
|
|
104
124
|
* @param deps - Collaborators
|
|
105
125
|
*/
|
|
@@ -123,6 +143,17 @@ export class SpendCapService {
|
|
|
123
143
|
clearInterval(this.timer);
|
|
124
144
|
this.timer = null;
|
|
125
145
|
}
|
|
146
|
+
/** Re-read the team list now (tests; the tick does it too). */
|
|
147
|
+
async refreshTeams() {
|
|
148
|
+
const teams = (await this.deps.teams?.().catch(() => [])) ?? [];
|
|
149
|
+
this.teamList = teams;
|
|
150
|
+
const index = new Map();
|
|
151
|
+
for (const t of teams) {
|
|
152
|
+
for (const s of t.members)
|
|
153
|
+
index.set(s, [...(index.get(s) ?? []), t.id]);
|
|
154
|
+
}
|
|
155
|
+
this.teamsOfSession = index;
|
|
156
|
+
}
|
|
126
157
|
// ---------------------------------------------------------------- caps
|
|
127
158
|
/** @returns The owner's caps (a copy) */
|
|
128
159
|
getConfig() {
|
|
@@ -139,56 +170,165 @@ export class SpendCapService {
|
|
|
139
170
|
if (!patch || typeof patch !== 'object')
|
|
140
171
|
throw new SpendCapError(400, 'Send the caps to change as JSON');
|
|
141
172
|
const next = this.getConfig();
|
|
142
|
-
if ('
|
|
143
|
-
next.
|
|
144
|
-
if ('
|
|
145
|
-
next.
|
|
173
|
+
if ('defaultAgentCapTokens' in patch)
|
|
174
|
+
next.defaultAgentCapTokens = parseCap(patch.defaultAgentCapTokens, 'defaultAgentCapTokens');
|
|
175
|
+
if ('totalCapTokens' in patch)
|
|
176
|
+
next.totalCapTokens = parseCap(patch.totalCapTokens, 'totalCapTokens');
|
|
146
177
|
if (patch.agents !== undefined) {
|
|
147
178
|
if (!patch.agents || typeof patch.agents !== 'object')
|
|
148
|
-
throw new SpendCapError(400, 'agents must be an object: { "<session>":
|
|
179
|
+
throw new SpendCapError(400, 'agents must be an object: { "<session>": 5000000 | null | "default" }');
|
|
149
180
|
for (const [session, value] of Object.entries(patch.agents)) {
|
|
150
181
|
if (!session.trim())
|
|
151
182
|
throw new SpendCapError(400, 'agents keys must be agent session names');
|
|
152
183
|
if (value === 'default')
|
|
153
|
-
delete next.
|
|
184
|
+
delete next.agentCapsTokens[session];
|
|
154
185
|
else
|
|
155
|
-
next.
|
|
186
|
+
next.agentCapsTokens[session] = parseCap(value, `agents.${session}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (patch.teams !== undefined) {
|
|
190
|
+
if (!patch.teams || typeof patch.teams !== 'object')
|
|
191
|
+
throw new SpendCapError(400, 'teams must be an object: { "<teamId>": 50000000 | null }');
|
|
192
|
+
for (const [teamId, value] of Object.entries(patch.teams)) {
|
|
193
|
+
if (!teamId.trim())
|
|
194
|
+
throw new SpendCapError(400, 'teams keys must be team ids');
|
|
195
|
+
const cap = parseCap(value, `teams.${teamId}`);
|
|
196
|
+
if (cap === null)
|
|
197
|
+
delete next.teamCapsTokens[teamId];
|
|
198
|
+
else
|
|
199
|
+
next.teamCapsTokens[teamId] = cap;
|
|
156
200
|
}
|
|
157
201
|
}
|
|
158
202
|
next.updatedAt = this.now().toISOString();
|
|
159
203
|
this.file.config = next;
|
|
160
204
|
this.persist();
|
|
161
|
-
this.logger?.info('
|
|
205
|
+
this.logger?.info('Token caps changed', { caps: next });
|
|
162
206
|
await this.evaluate();
|
|
163
207
|
return this.getConfig();
|
|
164
208
|
}
|
|
209
|
+
// ---------------------------------------------------------------- boosts
|
|
210
|
+
/** @returns Boosts in force (expired ones are dropped) */
|
|
211
|
+
activeBoosts() {
|
|
212
|
+
this.pruneBoosts();
|
|
213
|
+
return this.file.boosts.map((b) => ({ ...b }));
|
|
214
|
+
}
|
|
165
215
|
/**
|
|
166
|
-
*
|
|
216
|
+
* Add a temporary boost.
|
|
167
217
|
*
|
|
168
|
-
* @param
|
|
169
|
-
* @
|
|
170
|
-
* @
|
|
171
|
-
* @throws SpendCapError when the amount is not above today's spend
|
|
218
|
+
* @param input - Scope, target, extra tokens or unlimited, optional end
|
|
219
|
+
* @returns The boost
|
|
220
|
+
* @throws SpendCapError on a bad request
|
|
172
221
|
*/
|
|
173
|
-
async
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
if (
|
|
180
|
-
throw new SpendCapError(400,
|
|
222
|
+
async boost(input) {
|
|
223
|
+
if (!input || typeof input !== 'object')
|
|
224
|
+
throw new SpendCapError(400, 'Send the boost as JSON');
|
|
225
|
+
const target = await this.resolveBoostTarget(input.scope, input.id);
|
|
226
|
+
const unlimited = input.unlimited === true;
|
|
227
|
+
const extra = unlimited ? null : parseTokenAmount(input.extraTokens);
|
|
228
|
+
if (!unlimited && extra === null)
|
|
229
|
+
throw new SpendCapError(400, 'Send extraTokens (a positive number of tokens, e.g. 20000000 or "20M") or unlimited: true');
|
|
230
|
+
const now = this.now();
|
|
231
|
+
let until = this.nextMidnight();
|
|
232
|
+
if (input.until !== undefined && input.until !== null && input.until !== '') {
|
|
233
|
+
const t = new Date(String(input.until));
|
|
234
|
+
if (Number.isNaN(t.getTime()) || t.getTime() <= now.getTime())
|
|
235
|
+
throw new SpendCapError(400, 'until must be a future time (ISO 8601)');
|
|
236
|
+
until = t;
|
|
181
237
|
}
|
|
182
|
-
|
|
238
|
+
const boost = {
|
|
239
|
+
id: randomUUID(),
|
|
240
|
+
target,
|
|
241
|
+
...(unlimited ? { unlimited: true } : { extraTokens: extra }),
|
|
242
|
+
until: until.toISOString(),
|
|
243
|
+
createdAt: now.toISOString(),
|
|
244
|
+
...(input.by ? { by: input.by } : {}),
|
|
245
|
+
};
|
|
246
|
+
this.pruneBoosts();
|
|
247
|
+
this.file.boosts.push(boost);
|
|
183
248
|
this.persist();
|
|
184
|
-
this.logger?.info('
|
|
249
|
+
this.logger?.info('Usage boost added', { target, extraTokens: boost.extraTokens, unlimited: boost.unlimited, until: boost.until });
|
|
185
250
|
await this.evaluate();
|
|
186
|
-
return
|
|
251
|
+
return { ...boost };
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* End a boost early.
|
|
255
|
+
*
|
|
256
|
+
* @param id - Boost id
|
|
257
|
+
* @returns True when it existed
|
|
258
|
+
*/
|
|
259
|
+
async removeBoost(id) {
|
|
260
|
+
const before = this.file.boosts.length;
|
|
261
|
+
this.file.boosts = this.file.boosts.filter((b) => b.id !== id);
|
|
262
|
+
if (this.file.boosts.length === before)
|
|
263
|
+
return false;
|
|
264
|
+
this.persist();
|
|
265
|
+
await this.evaluate();
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Owner-facing description of a boost target.
|
|
270
|
+
*
|
|
271
|
+
* @param target - Boost target
|
|
272
|
+
* @returns e.g. `everyone`, `team CE`, `Ella`
|
|
273
|
+
*/
|
|
274
|
+
describeTarget(target) {
|
|
275
|
+
if (target === C.TOTAL_TARGET)
|
|
276
|
+
return 'everyone';
|
|
277
|
+
if (target.startsWith(C.TEAM_TARGET_PREFIX)) {
|
|
278
|
+
const id = target.slice(C.TEAM_TARGET_PREFIX.length);
|
|
279
|
+
return `team ${this.teamList.find((t) => t.id === id)?.name ?? id}`;
|
|
280
|
+
}
|
|
281
|
+
return this.nameOf(target);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Extra tokens / unlimited a boost set gives for one target set.
|
|
285
|
+
*
|
|
286
|
+
* @param covers - Which boost targets apply
|
|
287
|
+
* @returns Sum of extras and whether any is unlimited
|
|
288
|
+
*/
|
|
289
|
+
boostFor(covers) {
|
|
290
|
+
let extra = 0;
|
|
291
|
+
let unlimited = false;
|
|
292
|
+
for (const b of this.activeBoostsRaw()) {
|
|
293
|
+
if (!covers(b.target))
|
|
294
|
+
continue;
|
|
295
|
+
if (b.unlimited)
|
|
296
|
+
unlimited = true;
|
|
297
|
+
else
|
|
298
|
+
extra += b.extraTokens ?? 0;
|
|
299
|
+
}
|
|
300
|
+
return { extra, unlimited };
|
|
301
|
+
}
|
|
302
|
+
/** Boosts covering an agent: its own, its teams', everyone's. */
|
|
303
|
+
boostForSession(session) {
|
|
304
|
+
const teams = new Set((this.teamsOfSession.get(session) ?? []).map(teamTarget));
|
|
305
|
+
return this.boostFor((t) => t === C.TOTAL_TARGET || t === session || teams.has(t));
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Boosts covering a team (team and everyone boosts) — what the ticket
|
|
309
|
+
* autopilot budget of the team's projects honours.
|
|
310
|
+
*
|
|
311
|
+
* @param teamId - Team id
|
|
312
|
+
* @returns Extra tokens and unlimited
|
|
313
|
+
*/
|
|
314
|
+
boostForTeam(teamId) {
|
|
315
|
+
return this.boostForTeams([teamId]);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Boosts covering any of several teams, each boost counted once (a
|
|
319
|
+
* project worked on by two teams).
|
|
320
|
+
*
|
|
321
|
+
* @param teamIds - Team ids
|
|
322
|
+
* @returns Extra tokens and unlimited
|
|
323
|
+
*/
|
|
324
|
+
boostForTeams(teamIds) {
|
|
325
|
+
const keys = new Set(teamIds.map(teamTarget));
|
|
326
|
+
return this.boostFor((t) => t === C.TOTAL_TARGET || keys.has(t));
|
|
187
327
|
}
|
|
188
328
|
// ---------------------------------------------------------------- gate
|
|
189
329
|
/**
|
|
190
330
|
* The stop in force for a session (the delivery / wake gate). Synchronous
|
|
191
|
-
* and live: it reads today's
|
|
331
|
+
* and live: it reads today's usage now, not at the last tick.
|
|
192
332
|
*
|
|
193
333
|
* @param session - Agent session
|
|
194
334
|
* @returns Stop, or null when the agent may run
|
|
@@ -210,72 +350,98 @@ export class SpendCapService {
|
|
|
210
350
|
return this.nameOf(session);
|
|
211
351
|
}
|
|
212
352
|
/**
|
|
213
|
-
*
|
|
353
|
+
* An agent's own cap before boosts, and where it comes from.
|
|
214
354
|
*
|
|
215
|
-
* @param
|
|
355
|
+
* @param session - Agent session
|
|
216
356
|
* @returns Cap (null = none) and source
|
|
217
357
|
*/
|
|
218
|
-
|
|
219
|
-
const raised = this.file.day.raised[target];
|
|
220
|
-
if (isCap(raised))
|
|
221
|
-
return { capUsd: raised, source: 'raised' };
|
|
358
|
+
ownCapOf(session) {
|
|
222
359
|
const cfg = this.file.config;
|
|
223
|
-
if (
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const v = cfg.agentCapsUsd[target];
|
|
227
|
-
return isCap(v) ? { capUsd: v, source: 'override' } : { capUsd: null, source: 'exempt' };
|
|
360
|
+
if (Object.prototype.hasOwnProperty.call(cfg.agentCapsTokens, session)) {
|
|
361
|
+
const v = cfg.agentCapsTokens[session];
|
|
362
|
+
return isCap(v) ? { capTokens: v, source: 'override' } : { capTokens: null, source: 'exempt' };
|
|
228
363
|
}
|
|
229
|
-
return isCap(cfg.
|
|
364
|
+
return isCap(cfg.defaultAgentCapTokens) ? { capTokens: cfg.defaultAgentCapTokens, source: 'default' } : { capTokens: null, source: 'none' };
|
|
230
365
|
}
|
|
231
366
|
// ---------------------------------------------------------------- view
|
|
232
367
|
/**
|
|
233
|
-
*
|
|
368
|
+
* Usage plus caps for the API / Settings panel.
|
|
234
369
|
*
|
|
235
370
|
* @param days - Window
|
|
236
371
|
* @returns View
|
|
237
372
|
*/
|
|
238
373
|
async view(days = C.DEFAULT_DAYS) {
|
|
239
374
|
this.rollDay();
|
|
375
|
+
await this.refreshTeams();
|
|
240
376
|
const summary = this.deps.ledger.summarize(days);
|
|
241
377
|
const known = (await this.deps.knownSessions?.().catch(() => [])) ?? [];
|
|
242
378
|
const rows = new Map(summary.agents.map((a) => [a.session, a]));
|
|
243
|
-
for (const s of known) {
|
|
379
|
+
for (const s of [...known, ...this.teamList.flatMap((t) => t.members)]) {
|
|
244
380
|
if (!rows.has(s))
|
|
245
|
-
rows.set(s, { session: s, runtimes: [],
|
|
381
|
+
rows.set(s, { session: s, runtimes: [], todayTokens: 0, windowTokens: 0, windowCachedTokens: 0, daily: summary.days.map(() => 0) });
|
|
246
382
|
}
|
|
247
383
|
const agents = [...rows.values()].map((a) => {
|
|
248
|
-
const
|
|
384
|
+
const own = this.ownCapOf(a.session);
|
|
385
|
+
const b = this.boostForSession(a.session);
|
|
249
386
|
const stop = this.computeStop(a.session);
|
|
250
387
|
return {
|
|
251
|
-
|
|
388
|
+
session: a.session,
|
|
252
389
|
name: this.nameOf(a.session),
|
|
253
|
-
|
|
254
|
-
|
|
390
|
+
...(this.teamsOfSession.get(a.session)?.[0] ? { teamId: this.teamsOfSession.get(a.session)?.[0] } : {}),
|
|
391
|
+
runtimes: a.runtimes,
|
|
392
|
+
todayTokens: a.todayTokens,
|
|
393
|
+
windowTokens: a.windowTokens,
|
|
394
|
+
daily: a.daily,
|
|
395
|
+
baseCapTokens: own.capTokens,
|
|
396
|
+
capTokens: own.capTokens === null || b.unlimited ? null : own.capTokens + b.extra,
|
|
397
|
+
capSource: own.source,
|
|
398
|
+
boosted: b.unlimited || b.extra > 0,
|
|
399
|
+
unlimited: b.unlimited,
|
|
255
400
|
stopped: !!stop,
|
|
256
401
|
...(stop ? { stopReason: spendCapReason(stop, this.nameOf(a.session)) } : {}),
|
|
257
402
|
};
|
|
258
403
|
});
|
|
259
|
-
const
|
|
404
|
+
const teams = this.teamList.map((t) => {
|
|
405
|
+
const base = this.file.config.teamCapsTokens[t.id];
|
|
406
|
+
const b = this.boostForTeam(t.id);
|
|
407
|
+
const cap = isCap(base) && !b.unlimited ? base + b.extra : null;
|
|
408
|
+
const used = this.deps.ledger.groupToday(t.members);
|
|
409
|
+
return {
|
|
410
|
+
teamId: t.id,
|
|
411
|
+
name: t.name,
|
|
412
|
+
members: t.members,
|
|
413
|
+
todayTokens: used,
|
|
414
|
+
baseCapTokens: isCap(base) ? base : null,
|
|
415
|
+
capTokens: cap,
|
|
416
|
+
extraTokens: b.extra,
|
|
417
|
+
unlimited: b.unlimited,
|
|
418
|
+
boosts: this.activeBoostsRaw().filter((x) => x.target === teamTarget(t.id)),
|
|
419
|
+
stopped: cap !== null && used >= cap,
|
|
420
|
+
};
|
|
421
|
+
});
|
|
422
|
+
const totalBoost = this.boostFor((t) => t === C.TOTAL_TARGET);
|
|
423
|
+
const total = isCap(this.file.config.totalCapTokens) && !totalBoost.unlimited ? this.file.config.totalCapTokens + totalBoost.extra : null;
|
|
424
|
+
const { agents: _omit, ...rest } = summary;
|
|
425
|
+
void _omit;
|
|
260
426
|
return {
|
|
261
|
-
...
|
|
427
|
+
...rest,
|
|
262
428
|
agents,
|
|
429
|
+
teams,
|
|
263
430
|
caps: this.getConfig(),
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
431
|
+
boosts: this.activeBoosts(),
|
|
432
|
+
totalCapTodayTokens: total,
|
|
433
|
+
suggestedAgentCapTokens: summary.p90AgentDayTokens > 0 ? roundUpMillions(summary.p90AgentDayTokens) : null,
|
|
267
434
|
totalStopped: total !== null && this.deps.ledger.totalToday() >= total,
|
|
268
435
|
};
|
|
269
436
|
}
|
|
270
437
|
// ---------------------------------------------------------------- enforcement
|
|
271
438
|
/**
|
|
272
|
-
* One enforcement pass: day roll, 80% notices, stop
|
|
273
|
-
* card per stop), and releasing agents whose stop
|
|
274
|
-
* one at a time; a call during a pass gets another
|
|
439
|
+
* One enforcement pass: day roll, boost expiry, 80% notices, stop
|
|
440
|
+
* announcements (one card per stop), and releasing agents whose stop
|
|
441
|
+
* lifted. Passes run one at a time; a call during a pass gets another
|
|
442
|
+
* pass after it.
|
|
275
443
|
*/
|
|
276
444
|
evaluate() {
|
|
277
|
-
// Coalesce: a pass not yet started is shared; a call during a running
|
|
278
|
-
// pass gets a fresh pass after it (so it sees the latest change).
|
|
279
445
|
if (this.queuedPass)
|
|
280
446
|
return this.queuedPass;
|
|
281
447
|
const pass = this.chain.then(async () => {
|
|
@@ -284,7 +450,7 @@ export class SpendCapService {
|
|
|
284
450
|
await this.evaluateOnce();
|
|
285
451
|
}
|
|
286
452
|
catch (err) {
|
|
287
|
-
this.logger?.warn('
|
|
453
|
+
this.logger?.warn('Token cap evaluation failed', { error: err instanceof Error ? err.message : String(err) });
|
|
288
454
|
}
|
|
289
455
|
});
|
|
290
456
|
this.queuedPass = pass;
|
|
@@ -307,100 +473,135 @@ export class SpendCapService {
|
|
|
307
473
|
await this.reply(d.id, 'This card is from an earlier day; the cap has already reset at midnight.');
|
|
308
474
|
return null;
|
|
309
475
|
}
|
|
310
|
-
const
|
|
476
|
+
const who = this.describeTarget(target);
|
|
311
477
|
if (d.status !== 'resolved')
|
|
312
478
|
return null;
|
|
313
479
|
const label = d.options.find((o) => o.key === d.chosenKey)?.label;
|
|
314
|
-
const
|
|
315
|
-
if (
|
|
316
|
-
await this.reply(d.id, `OK, ${
|
|
480
|
+
const boost = boostOfLabel(label);
|
|
481
|
+
if (boost === null) {
|
|
482
|
+
await this.reply(d.id, `OK, ${who} stays stopped until midnight. Messages stay queued.`);
|
|
317
483
|
return null;
|
|
318
484
|
}
|
|
319
485
|
try {
|
|
320
|
-
const
|
|
321
|
-
|
|
486
|
+
const scope = target === C.TOTAL_TARGET ? 'all' : target.startsWith(C.TEAM_TARGET_PREFIX) ? 'team' : 'agent';
|
|
487
|
+
const id = scope === 'team' ? target.slice(C.TEAM_TARGET_PREFIX.length) : scope === 'agent' ? target : undefined;
|
|
488
|
+
await this.boost(boost === 'unlimited' ? { scope, id, unlimited: true, by: 'card' } : { scope, id, extraTokens: boost, by: 'card' });
|
|
489
|
+
const what = boost === 'unlimited' ? 'no cap' : `+${formatTokens(boost)}`;
|
|
490
|
+
await this.reply(d.id, `Done: ${what} for ${who} until midnight. Queued messages are being delivered.`);
|
|
322
491
|
}
|
|
323
492
|
catch (err) {
|
|
324
|
-
await this.reply(d.id, `Could not
|
|
493
|
+
await this.reply(d.id, `Could not boost: ${err instanceof Error ? err.message : String(err)}`);
|
|
325
494
|
}
|
|
326
495
|
return null;
|
|
327
496
|
}
|
|
328
497
|
// ---------------------------------------------------------------- internals
|
|
329
498
|
async evaluateOnce() {
|
|
330
499
|
this.rollDay();
|
|
500
|
+
const hadBoosts = this.file.boosts.length;
|
|
501
|
+
this.pruneBoosts();
|
|
502
|
+
if (hadBoosts !== this.file.boosts.length)
|
|
503
|
+
this.persist();
|
|
331
504
|
if (!this.anyCap() && this.lastStopped.size === 0)
|
|
332
505
|
return;
|
|
506
|
+
await this.refreshTeams();
|
|
333
507
|
this.deps.ledger.invalidate();
|
|
334
508
|
const summary = this.deps.ledger.summarize(1);
|
|
335
509
|
const known = (await this.deps.knownSessions?.().catch(() => [])) ?? [];
|
|
336
|
-
const sessions = new Set([...known, ...summary.agents.map((a) => a.session), ...
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
sessions.add(s);
|
|
340
|
-
const stoppedNow = new Set();
|
|
510
|
+
const sessions = new Set([...known, ...summary.agents.map((a) => a.session), ...this.teamList.flatMap((t) => t.members)]);
|
|
511
|
+
for (const s of this.lastStopped)
|
|
512
|
+
sessions.add(s);
|
|
341
513
|
// The all-agents total first: its notice names the whole crew once.
|
|
342
|
-
const
|
|
343
|
-
if (
|
|
344
|
-
|
|
345
|
-
|
|
514
|
+
const totalCap = this.capForTarget(C.TOTAL_TARGET);
|
|
515
|
+
if (totalCap !== null)
|
|
516
|
+
await this.checkThresholds(C.TOTAL_TARGET, totalCap, this.deps.ledger.totalToday());
|
|
517
|
+
for (const t of this.teamList) {
|
|
518
|
+
const cap = this.capForTarget(teamTarget(t.id));
|
|
519
|
+
if (cap !== null)
|
|
520
|
+
await this.checkThresholds(teamTarget(t.id), cap, this.deps.ledger.groupToday(t.members));
|
|
346
521
|
}
|
|
522
|
+
const stoppedNow = new Set();
|
|
347
523
|
for (const session of sessions) {
|
|
348
|
-
const cap = this.
|
|
524
|
+
const cap = this.capForTarget(session);
|
|
349
525
|
if (cap !== null)
|
|
350
|
-
await this.checkThresholds(session, cap, this.deps.ledger.
|
|
526
|
+
await this.checkThresholds(session, cap, this.deps.ledger.usedToday(session));
|
|
351
527
|
if (this.computeStop(session))
|
|
352
528
|
stoppedNow.add(session);
|
|
353
529
|
}
|
|
354
530
|
const released = [...this.lastStopped].filter((s) => !stoppedNow.has(s));
|
|
355
531
|
this.lastStopped = stoppedNow;
|
|
356
532
|
if (released.length > 0) {
|
|
357
|
-
this.logger?.info('
|
|
533
|
+
this.logger?.info('Token cap stop lifted — releasing queued messages', { sessions: released });
|
|
358
534
|
await this.deps.onReleased?.(released).catch((err) => this.logger?.warn('Releasing queued messages failed', { sessions: released, error: err instanceof Error ? err.message : String(err) }));
|
|
359
535
|
}
|
|
360
536
|
}
|
|
361
|
-
|
|
537
|
+
/**
|
|
538
|
+
* The cap in force for a cap target, with the boosts that cover the
|
|
539
|
+
* target itself (for notices and cards).
|
|
540
|
+
*/
|
|
541
|
+
capForTarget(target) {
|
|
542
|
+
const cfg = this.file.config;
|
|
543
|
+
if (target === C.TOTAL_TARGET) {
|
|
544
|
+
const b = this.boostFor((t) => t === C.TOTAL_TARGET);
|
|
545
|
+
return isCap(cfg.totalCapTokens) && !b.unlimited ? cfg.totalCapTokens + b.extra : null;
|
|
546
|
+
}
|
|
547
|
+
if (target.startsWith(C.TEAM_TARGET_PREFIX)) {
|
|
548
|
+
const id = target.slice(C.TEAM_TARGET_PREFIX.length);
|
|
549
|
+
const base = cfg.teamCapsTokens[id];
|
|
550
|
+
const b = this.boostForTeam(id);
|
|
551
|
+
return isCap(base) && !b.unlimited ? base + b.extra : null;
|
|
552
|
+
}
|
|
553
|
+
const own = this.ownCapOf(target).capTokens;
|
|
554
|
+
const b = this.boostForSession(target);
|
|
555
|
+
return own !== null && !b.unlimited ? own + b.extra : null;
|
|
556
|
+
}
|
|
557
|
+
async checkThresholds(target, cap, used) {
|
|
362
558
|
const key = `${target}@${cap}`;
|
|
363
559
|
const day = this.file.day;
|
|
364
|
-
const
|
|
365
|
-
if (
|
|
560
|
+
const who = this.describeTarget(target);
|
|
561
|
+
if (used >= cap) {
|
|
366
562
|
if (day.stopped.includes(key))
|
|
367
563
|
return;
|
|
368
564
|
day.stopped.push(key);
|
|
369
565
|
if (!day.warned.includes(key))
|
|
370
566
|
day.warned.push(key);
|
|
371
567
|
this.persist();
|
|
372
|
-
this.logger?.warn('Daily
|
|
373
|
-
await this.announceStop(target,
|
|
568
|
+
this.logger?.warn('Daily token cap reached — hard stop (no new turns until midnight)', { target, capTokens: cap, usedTokens: used });
|
|
569
|
+
await this.announceStop(target, who, cap, used);
|
|
374
570
|
return;
|
|
375
571
|
}
|
|
376
|
-
if (
|
|
572
|
+
if (used >= cap * C.WARN_FRACTION && !day.warned.includes(key)) {
|
|
377
573
|
day.warned.push(key);
|
|
378
574
|
this.persist();
|
|
379
|
-
const pct = Math.floor((
|
|
380
|
-
const
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
await this.notify(`Heads-up: ${who} spent ${formatUsd(spent)} of ${its} ${formatUsd(cap)} daily spend cap today (${pct}%). At ${formatUsd(cap)} ${stops} taking new turns until midnight.`);
|
|
575
|
+
const pct = Math.floor((used / cap) * 100);
|
|
576
|
+
const subject = target === C.TOTAL_TARGET ? 'All agents together have' : target.startsWith(C.TEAM_TARGET_PREFIX) ? `Team ${who.replace(/^team /, '')} has` : `${who} has`;
|
|
577
|
+
const stops = target === C.TOTAL_TARGET ? 'every agent stops' : target.startsWith(C.TEAM_TARGET_PREFIX) ? 'its members stop' : 'it stops';
|
|
578
|
+
await this.notify(`Heads-up: ${subject} used ${formatTokens(used)} of the ${formatTokens(cap)} daily token cap today (${pct}%). At ${compactTokens(cap)} ${stops} taking new turns until midnight.`);
|
|
384
579
|
}
|
|
385
580
|
}
|
|
386
|
-
async announceStop(target,
|
|
387
|
-
const
|
|
388
|
-
const
|
|
581
|
+
async announceStop(target, who, cap, used) {
|
|
582
|
+
const extra = suggestedBoost(cap);
|
|
583
|
+
const dmWho = target === C.TOTAL_TARGET ? 'everyone' : target === ORCHESTRATOR_SESSION_NAME ? 'orc' : who.replace(/^team /, '');
|
|
389
584
|
const headline = target === C.TOTAL_TARGET
|
|
390
|
-
? `All agents together hit the daily
|
|
391
|
-
:
|
|
585
|
+
? `All agents together hit the daily token cap (${formatTokens(cap)}); every agent is stopped until midnight.`
|
|
586
|
+
: target.startsWith(C.TEAM_TARGET_PREFIX)
|
|
587
|
+
? `Team ${who.replace(/^team /, '')} hit its daily token cap (${formatTokens(cap)}); its members are stopped until midnight.`
|
|
588
|
+
: `${who} hit its daily token cap (${formatTokens(cap)}) and is stopped until midnight.`;
|
|
392
589
|
const decisions = this.deps.decisions?.() ?? null;
|
|
393
590
|
if (decisions) {
|
|
394
591
|
try {
|
|
395
592
|
const d = await decisions.askSystem({
|
|
396
593
|
kind: C.DECISION_KIND,
|
|
397
594
|
system: { key: target, defaultIsDecline: true },
|
|
398
|
-
title: 'Daily
|
|
399
|
-
question: `${headline}
|
|
595
|
+
title: 'Daily token cap reached',
|
|
596
|
+
question: `${headline} Boost it for today?`,
|
|
400
597
|
body: [
|
|
401
|
-
`
|
|
598
|
+
`Used today: ${formatTokens(used)}. The current turn was allowed to finish; no new turns start. Messages are kept and delivered when it runs again.`,
|
|
599
|
+
],
|
|
600
|
+
options: [
|
|
601
|
+
`Boost +${compactTokens(extra)} tokens today — runs again until midnight`,
|
|
602
|
+
`${C.OPTIONS.UNLIMITED} — no cap until midnight`,
|
|
603
|
+
`${C.OPTIONS.KEEP} — resets at midnight`,
|
|
402
604
|
],
|
|
403
|
-
options: [`Raise to $${raise} today — runs again until midnight`, `${C.OPTIONS.KEEP} — resets at midnight`],
|
|
404
605
|
default: C.OPTIONS.KEEP,
|
|
405
606
|
deadline: this.nextMidnight(),
|
|
406
607
|
sensitive: 'spend',
|
|
@@ -410,50 +611,106 @@ export class SpendCapService {
|
|
|
410
611
|
return;
|
|
411
612
|
}
|
|
412
613
|
catch (err) {
|
|
413
|
-
this.logger?.warn('Could not post the
|
|
614
|
+
this.logger?.warn('Could not post the token cap card — sending a notice instead', { target, error: err instanceof Error ? err.message : String(err) });
|
|
414
615
|
}
|
|
415
616
|
}
|
|
416
|
-
await this.notify(`${headline}
|
|
617
|
+
await this.notify(`${headline} Messages are queued. Reply \`boost ${dmWho} by ${compactTokens(extra)} today\` or \`unlimited today for ${dmWho}\`.`);
|
|
417
618
|
}
|
|
418
619
|
anyCap() {
|
|
419
620
|
const cfg = this.file.config;
|
|
420
|
-
return (isCap(cfg.
|
|
421
|
-
isCap(cfg.
|
|
422
|
-
Object.values(cfg.
|
|
423
|
-
Object.values(
|
|
621
|
+
return (isCap(cfg.defaultAgentCapTokens) ||
|
|
622
|
+
isCap(cfg.totalCapTokens) ||
|
|
623
|
+
Object.values(cfg.agentCapsTokens).some(isCap) ||
|
|
624
|
+
Object.values(cfg.teamCapsTokens).some(isCap));
|
|
424
625
|
}
|
|
425
626
|
computeStop(session) {
|
|
426
|
-
const
|
|
627
|
+
const b = this.boostForSession(session);
|
|
628
|
+
if (b.unlimited)
|
|
629
|
+
return null;
|
|
630
|
+
const cfg = this.file.config;
|
|
631
|
+
const own = this.ownCapOf(session).capTokens;
|
|
427
632
|
if (own !== null) {
|
|
428
|
-
const
|
|
429
|
-
if (
|
|
430
|
-
return { session, scope: 'agent',
|
|
633
|
+
const used = this.deps.ledger.usedToday(session);
|
|
634
|
+
if (used >= own + b.extra)
|
|
635
|
+
return { session, scope: 'agent', capTokens: own + b.extra, usedTokens: used };
|
|
636
|
+
}
|
|
637
|
+
for (const teamId of this.teamsOfSession.get(session) ?? []) {
|
|
638
|
+
const base = cfg.teamCapsTokens[teamId];
|
|
639
|
+
if (!isCap(base))
|
|
640
|
+
continue;
|
|
641
|
+
const team = this.teamList.find((t) => t.id === teamId);
|
|
642
|
+
const used = this.deps.ledger.groupToday(team?.members ?? [session]);
|
|
643
|
+
if (used >= base + b.extra)
|
|
644
|
+
return { session, scope: 'team', capTokens: base + b.extra, usedTokens: used, teamId, teamName: team?.name ?? teamId };
|
|
431
645
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
return { session, scope: 'total', capUsd: total, spentUsd: spent };
|
|
646
|
+
if (isCap(cfg.totalCapTokens)) {
|
|
647
|
+
const used = this.deps.ledger.totalToday();
|
|
648
|
+
if (used >= cfg.totalCapTokens + b.extra)
|
|
649
|
+
return { session, scope: 'total', capTokens: cfg.totalCapTokens + b.extra, usedTokens: used };
|
|
437
650
|
}
|
|
438
651
|
return null;
|
|
439
652
|
}
|
|
440
653
|
stopKey(stop) {
|
|
441
|
-
|
|
654
|
+
const target = stop.scope === 'total' ? C.TOTAL_TARGET : stop.scope === 'team' ? teamTarget(stop.teamId ?? '') : stop.session;
|
|
655
|
+
return `${target}@${stop.capTokens}`;
|
|
442
656
|
}
|
|
443
657
|
scheduleEvaluate() {
|
|
444
658
|
void this.evaluate();
|
|
445
659
|
}
|
|
660
|
+
activeBoostsRaw() {
|
|
661
|
+
const now = this.now().getTime();
|
|
662
|
+
return this.file.boosts.filter((b) => new Date(b.until).getTime() > now);
|
|
663
|
+
}
|
|
664
|
+
pruneBoosts() {
|
|
665
|
+
const live = this.activeBoostsRaw();
|
|
666
|
+
if (live.length !== this.file.boosts.length) {
|
|
667
|
+
const ended = this.file.boosts.filter((b) => !live.includes(b));
|
|
668
|
+
this.file.boosts = live;
|
|
669
|
+
this.logger?.info('Usage boosts ended', { boosts: ended.map((b) => ({ target: b.target, until: b.until })) });
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
async resolveBoostTarget(scope, id) {
|
|
673
|
+
if (scope === 'all')
|
|
674
|
+
return C.TOTAL_TARGET;
|
|
675
|
+
const raw = typeof id === 'string' ? id.trim() : '';
|
|
676
|
+
if (scope === 'team') {
|
|
677
|
+
if (!raw)
|
|
678
|
+
throw new SpendCapError(400, 'id is required for a team boost (team id or name)');
|
|
679
|
+
await this.refreshTeams();
|
|
680
|
+
const w = raw.toLowerCase();
|
|
681
|
+
const hit = this.teamList.find((t) => t.id === raw) ?? this.teamList.filter((t) => t.name.toLowerCase() === w).at(0);
|
|
682
|
+
if (!hit)
|
|
683
|
+
throw new SpendCapError(404, `No team called "${raw}"`);
|
|
684
|
+
return teamTarget(hit.id);
|
|
685
|
+
}
|
|
686
|
+
if (scope === 'agent') {
|
|
687
|
+
if (!raw)
|
|
688
|
+
throw new SpendCapError(400, 'id is required for an agent boost (session or name)');
|
|
689
|
+
await this.refreshTeams();
|
|
690
|
+
const known = new Set([...((await this.deps.knownSessions?.().catch(() => [])) ?? []), ...this.teamList.flatMap((t) => t.members)]);
|
|
691
|
+
if (known.has(raw))
|
|
692
|
+
return raw;
|
|
693
|
+
const byName = [...known].filter((s) => this.nameOf(s).toLowerCase() === raw.toLowerCase());
|
|
694
|
+
if (byName.length === 1)
|
|
695
|
+
return byName[0];
|
|
696
|
+
if (byName.length > 1)
|
|
697
|
+
throw new SpendCapError(400, `More than one agent is called "${raw}"; use its session name`);
|
|
698
|
+
return raw;
|
|
699
|
+
}
|
|
700
|
+
throw new SpendCapError(400, 'scope must be team, agent or all');
|
|
701
|
+
}
|
|
446
702
|
/** Reset today's bookkeeping when the local day changed. */
|
|
447
703
|
rollDay() {
|
|
448
704
|
const today = localDateKey(this.now());
|
|
449
705
|
if (this.file.day.date === today)
|
|
450
706
|
return;
|
|
451
|
-
const had =
|
|
707
|
+
const had = this.file.day.stopped.length;
|
|
452
708
|
this.file.day = emptyDay(today);
|
|
709
|
+
this.pruneBoosts();
|
|
453
710
|
this.deps.ledger.invalidate();
|
|
454
711
|
this.persist();
|
|
455
712
|
if (had > 0)
|
|
456
|
-
this.logger?.info('
|
|
713
|
+
this.logger?.info('Token caps reset for the new day', { date: today });
|
|
457
714
|
}
|
|
458
715
|
nextMidnight() {
|
|
459
716
|
const next = localMidnight(this.now());
|
|
@@ -476,7 +733,7 @@ export class SpendCapService {
|
|
|
476
733
|
await this.deps.notifyOwner(text);
|
|
477
734
|
}
|
|
478
735
|
catch (err) {
|
|
479
|
-
this.logger?.warn('Could not send the
|
|
736
|
+
this.logger?.warn('Could not send the token cap notice', { error: err instanceof Error ? err.message : String(err) });
|
|
480
737
|
}
|
|
481
738
|
}
|
|
482
739
|
async reply(decisionId, text) {
|
|
@@ -487,7 +744,7 @@ export class SpendCapService {
|
|
|
487
744
|
this.deps.store.write(this.file);
|
|
488
745
|
}
|
|
489
746
|
catch (err) {
|
|
490
|
-
this.logger?.warn('Could not save
|
|
747
|
+
this.logger?.warn('Could not save token caps', { error: err instanceof Error ? err.message : String(err) });
|
|
491
748
|
}
|
|
492
749
|
}
|
|
493
750
|
}
|