Repository navigation
feat(host): cap a team's model turns per team-local day - #2578
Merged
Merged
Conversation
… ones #645's C half, per the owner decision of 2026-11-07: the team counts model turns per team-local day and, at or over the cap, dispatches only a turn the user started themselves. A scheduled report, a tidy pass, a round a Bot started or a kickoff is refused before a model call — no request, no message to the user, one line in the host log. turns.js already records whose request a turn answers, so the gate reuses that verdict instead of inventing a second classification: `owner` passes, and everything else (scheduled, other, or no verdict) waits for the day to reset. The cap is state.prefs.turnBudget (200 for a fresh team, 0 = off, and an existing team is seeded off so upgrading changes nothing); the counter is state.turns = { day, count }, spent once at a turn's first step. Fixes #645
Collaborator
Author
|
Verdict: PASS at b5cc6d9 Ran at this tip: Review points
Not run here: the full core suite (this box aborts |
This was referenced Nov 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Written-by: Dev
Fixes #645
Owner: Dev
Reviewer: Reviewer
What changed
Before: every turn a team's system starts is dispatched, so a team keeps paying for scheduled reports, tidy passes and rounds a Bot starts. After: the team counts model turns per team-local day (
config.timeZone,Asia/Shanghaiwhen unset) and, at or over the cap, dispatches only a turn the user started themselves — their own message to a Bot, or a group message they wrote. A system-started turn is refused before a model call: no request, no message to the user, oneds-bot: turn budget:line in the host log. The counter resets at the team-local day boundary and0disables the budget.prefs.turnBudget(settings fieldturnBudgeton theapi/botendpoint): 200 for a fresh team,0for a team saved before the field existed (the owner's "existing teams get unlimited until it is set"),nullclears back to the default.state.turns = { day, count }, spent once at a turn's own first step, so a many-step turn costs one and a refused turn costs none.turns.js's recorded verdict:ownerpasses; everything else (scheduled,other, or no verdict) waits for the counter to reset. That is Connector tools: on by default for the owner's own use of every Bot, also on shared Bots; other people's use is a separate opt-in #369's classification, reused rather than re-invented.Where the change lives:
core/src/host/turn-budget.js(new, 87 lines) — the pure helpers (TEAM_TURNS_PER_DAY,TEAM_TURN_ZONE,turnBudgetOf:24,readTurnBudget:31,turnsUsed:39,teamDayKey:45) andinstall(): the spend atsession/eventstep/startstep 1 (:62) and the gate atagent/pre-stepwithprepend(:74).core/index.js:66—turnBudgetinMODULES, afterpartsso its prepend gate answers before the work-folder gate and a refused turn leaves no fresh part behind.core/src/host/store.js— the adopt path seedsprefs.turnBudget(200fresh,0migrated).core/src/host/channel.js—setPrefsreads the field throughreadTurnBudget.core/test/turn-budget.test.js— the four acceptance tests of the spec (a–d) plus the migration, the settings write and the zone/day-boundary cases.core/test/install.test.js,core/test/legacy-state.test.js,core/test/work-dir.test.js— the install order with a second gate registered, the migrated-team default, and a pre-step helper that runs the gates the way DSH does.docs/subsystems/core-host.md,docs/subsystems/team.md— the setting and the behaviour.Verification at the tip
Tip SHA: b5cc6d9 — parent
31d5c0b4(main's tip), so the branch is main plus these 17 files;git diff origin/main...HEAD --statlists exactly them and nothing else.Ran at that tip:
node --import ./test/harness-resolve.mjs --test --test-concurrency=1 test/turn-budget.test.js test/install.test.js test/legacy-state.test.js test/work-dir.test.js test/quiet-hours.test.js test/settings-readonly-pages.test.js→ 53/53 green.agent/pre-stepalways callsnext()),test/turn-budget.test.jsgives 11 pass / 4 fail — precisely the refusal cases, e.g.actual {kind:'enter'}vsexpected {kind:'reject'}(acceptance a), and the real-store case (a and d). Deletingcore/src/host/turn-budget.jsoutright fails the whole file at import (1 test, 0 pass).npm --prefix core run lint→ 0 errors (84 pre-existing warnings),arch-docs: OK (17 changed file(s)).node scripts/lint-plugins.mjs→ ok;node core/scripts/build-client.mjs --check→client.jsunchanged (nosrc/client/change);node scripts/ds-bot-patches.mjs check→ds-bot/matches the pin, 10 patches apply cleanly,core/equalsds-bot/+ patches;canonical→generateis a no-op..github/scripts/*.test.mjs+policy/*.test.mjs671/671, interop 37/37 with lint clean, live-e2e 34/34 with lint clean (bundled git'slibexec/git-coreonPATH, otherwise twofetch-basetests cannot findgit-upload-pack).tests-oksuccess (run 42344600807) and theplugins (tree),plugins (social),plugins (timeline),plugins (workflow),package,mockandjudgejobs success.Not run, and why:
pthread_create: Resource temporarily unavailable/SIGABRT/spawn node EAGAIN. Each such file passes when run alone — authz 21, automation-default 14, blocking-question 5, bot-messages 28, bot-tasks 15, busy-ack-ledger 10, capabilities 17, chat-link-open 13, claim-cue 8, composer-send-readonly 7, connector-access 25, connectors 37, agents 14, check-arch-docs 7 — all green. CI'stests-okgreen at this tip is the full-suite evidence.test/usage-fields.test.jsfails 1 of 7 only because this box has nopython3(the fixture check shells out to it); it does not touch this diff and is green in CI.index.jsandsrc/host/change).core/test/install.test.jspins the module list and listener order, and the mock-model path is exercised by CI'smockjob instead.Checkpoint
{ "pr": 2578, "claim": "Dev", "issue": 645, "summary": "A team now counts its model turns per team-local day and, at or over its cap, dispatches only a turn the user started themselves. Scheduled reports, tidy passes, kickoffs and rounds a Bot starts are refused before a model call, with one host log line and no message to the user. The cap is prefs.turnBudget: 200 for a fresh team, 0 (off) for a team saved before the field existed. The counter is state.turns and resets at the day boundary. Settings accept the field and the subsystem docs record the behaviour.", "files": ["core/src/host/turn-budget.js", "core/index.js", "core/src/host/store.js", "core/src/host/channel.js", "core/test/turn-budget.test.js", "core/test/install.test.js", "core/test/legacy-state.test.js", "core/test/work-dir.test.js", "docs/subsystems/team.md", "docs/subsystems/core-host.md", "changelog.d/645-team-turn-budget.changed.md", "patches/ds-bot/0002-flybot-api-host.patch"], "checks": { "ran": "affected core tests 53/53, red-check with the gate removed (4 fail), core lint + arch-docs (17 files), build-client --check, ds-bot-patches check + canonical, plugin suites 1299/1299, root+policy 671/671, interop 37/37, live-e2e 34/34; CI tests-ok green", "result": "green", "at": "b5cc6d9091c64dbf936c7dfe895d3e4c012c3e21" }, "next": "none — ready for review" }