fix(votes): keep votes with the scope they were cast in (#787) - #793
Merged
Merged
Conversation
Every scope recorded into one ~/.teamai/votes/<user>.yaml, so a vote cast in one project (recall feedback, a recall search, a Stop whose push failed) was pushed to the team of whichever scope synced next: the leak usage.jsonl had before Tencent#758. Votes now live in the data home of the scope that resolves for the session: <dataHome>/votes/ for a project, ~/.teamai/user-votes/ for the user scope. The Stop hook uses the config the dispatcher resolved; the pull report, recall search, `recall feedback` and the vote view read only that scope's votes, and the CLI readers resolve it with resolveConfigForDir, so an unreadable project config falls back to no other scope: `recall feedback` exits 1 and the vote view names the broken file. The shared ~/.teamai/votes/ is never read. Its V2 `votes` map is the last merged remote snapshot of whichever team synced, not this scope's history, and seeding a scope from it would let `recall feedback --negative` push a decrement and merged timestamps derived from another team. The remote votes/<user>.yaml format is unchanged.
No additional code defects found in the reviewed diff. No earlier findings were supplied to mark resolved. |
- recall search: in a project whose config cannot be read, detection falls back to another scope; record no recalled count there, so the vote cannot reach that scope's team. Which scope the search itself uses stays Tencent#796's. - recall feedback: with no project config and an empty or invalid user config, name the file and the fix (requireInit's error) instead of "not set up here". - CHANGELOG: note the recall search case; the shared directory is never read or pushed "by this release" (an earlier release still pushes it). - Design doc: getUserVotesDir() is the exception to "getters unchanged".
- recall feedback: name an unusable user config through throwMissingOrInvalid (now exported) instead of re-running requireInit, which loaded the config twice and printed its parse error twice. - recall search: a project detection that throws is treated like an unreadable config, so no recalled count lands in the fallback scope. - The recall-search test now asserts the search ran and that neither the shared directory nor the broken project's votes/ was written; it fails on origin/main too. - Design doc: re-wrap the edited paragraph.
|
Findings
|
…encent#787) The previous commit treated any throw from recall's project detection as an unreadable project, including a cwd that no longer exists. Such a cwd holds no project and resolves to the user scope everywhere else (resolveConfigForDir, detectTeam), so its recalled counts belong there. Tests pin that case and the single parse-error line for an invalid user config in recall feedback.
|
Findings
No earlier findings were resolved in the current diff. |
- getVotesDir: a historical project-scoped ~/.teamai/config.yaml with no projectRoot (schema-valid, not backfilled) made getDataHome throw, so recall, feedback and the Stop hook recorded no vote. It lives in ~/.teamai, as recall and viz already treat it, so its votes go to the user scope's user-votes/. - git-native-memory design doc: the local votes path is user-votes/.
|
Findings
Resolved
Note
|
|
Findings
Resolved
|
# Conflicts: # docs/designs/data-directory-layout.md # src/team-push.ts # src/votes.ts
- recall feedback --negative counts the upvotes the scope's own team already holds (its reports checkout's votes/<user>.yaml plus the deltas not yet pushed). A scope's file starts empty on upgrade, so a doc upvoted before it was rejected as not found, or as having no upvotes once a later recall counted it. The shared ~/.teamai/votes and other scopes' teams are never read. - The adoption judge (Tencent#723, merged meanwhile) recorded into and synced from the user scope's votes in every scope, which pushed the user scope's pending votes to the project's team. It uses the scope's votes like the Stop handler. - votes-scope tests: Stop transcripts prove adoption with a Read of the recalled file (Tencent#723), and the update.js mock keeps the real lock.
|
Review Result
|
|
Review Result
|
jeff-r2026
approved these changes
Sep 24, 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.
Fixes #787
Part of #752
Summary
getVotesDirfollows the same rule asgetUsagePathafter #758. A historical project-scoped~/.teamai/config.yamlwith noprojectRootlives in~/.teamai, as recall and viz already treat it, so its votes go touser-votes/.Shared directory: never read. In V2,
~/.teamai/votesholds the last merged snapshot of whichever team synced.--negativeinstead takes its baseline from the same scope's own team file (<reports checkout>/votes/<user>.yaml), because a scope's local file starts empty on upgrade. It takesmax(local count, team count + pending delta), so the first downvote after the upgrade lowers the team's count.Evidence
Real CLI on head
6e0c92c(dist/index.js) against a build of origin/maina84bf6f: 4 agents × 3 providers, 24 runs, all green. The agent axis goes throughhook-dispatch stop --tool <agent>payloads in a sandbox HOME, the provider axis through each team'steamai.yamlprovider. One member (tester) is in the user scope and in project A. Team A's reports branch already holdsdoc-oldwith 2 upvotes from before the upgrade.Steps: a pending vote left in
~/.teamai/votesby an earlier release →recall feedbackin A → Stop in the user scope (doc-u) → Stop in A (doc-a; the transcript recalls the doc and Reads its file, as #723 requires) →recall feedbackin the user scope →pullin each scope →recall feedback --negative doc-oldin A →pullin A.--negative doc-oldin Adoc-legacydoc-a-feedbackdoc-udoc-old=2doc-adoc-u-feedbackDocument not found, stays 2doc-udoc-u-feedbackdoc-old=2doc-a-feedbackdoc-aNegative signal recorded, 2 → 1doc-old=2doc-legacydoc-a-feedbackdoc-u-feedbackDocument not found, stays 2doc-u-feedbackdoc-old=2doc-a-feedbackNegative signal recorded, 2 → 1OpenCode's plugin sends only
{ cwd }onsession.idle(opencode-hooks.ts), so its Stop records no vote on either build. The adoption judge needs a local agent CLI, so it is covered by a test (below), not by the real-CLI run.Earlier heads, unchanged by later commits (none touches the Stop sync, the report push or config resolution):
597a84f, Claude × 3 providers): after rolling back to the earlier release with feedback in both scopes and re-upgrading, each team gains only its own new vote. Pending entries in the shared file are not pushed by this release.7a1ca7a):recall feedbackexits 1 withNo feedback recorded: <config path>: <parse error>. Fix the file, …and writes nothing;recallrecords no count; an empty or invalid~/.teamai/config.yamlis named (parse error printed once); a recall from a deleted cwd records inuser-votes/.Tests,
src/__tests__/votes-scope.test.ts(real dispatcher, report,recall feedback, viz). New on6e0c92c, both red on its parent:Test plan
On head
6e0c92c(branch = merge of origin/main8cee7ab+ fix, merged rather than rebased to avoid a force-push):npm run buildgit/gitlab/github(table above; 12 after-runs on6e0c92c, 12 baseline runs)npx vitest run: 4646 passed; 11 failed, all instats-scope.test.tsnpx tsc --noEmit: fails only insrc/stats.ts:250/251/347/370npm run test:e2e: 237 passed, 26 skippedRed checks come from main, not this PR.
origin/mainhas been broken on its own since352cfc4(#795 × #771:loadAllReportedEventsnow takes a config and returns a Promise). A clean archive of8cee7abgives the sametscerrors and the same 11stats-scopefailures. A separate hotfix PR fixes it. This PR does not touchstats.ts. Ona84bf6f, before that break, the same fix passed all four checks: tsc ok, vitest 4637 passed, test:e2e 237 passed, build ok.Merge Danger
Door: two-way
Rolling back is safe: the earlier release reads
~/.teamai/votesagain. While it runs, its first Stop or pull pushes whatever is pending there, including votes left from before this upgrade, to the team of that scope. This release cannot prevent that. What the earlier release wrote is never pushed after re-upgrading.Blast Radius: local-votes
~/.teamai/votesat upgrade are never pushed (the intent).recall feedback --negativereads the scope's reports checkout. If that checkout has not been created yet (no pull or push since setup), a doc that only the team holds still warns "not found".recall feedbacknow exits 1, a recall search records no recalled count (it still searches the fallback scope; recall searches nothing in a project whose config cannot be read #796 changes that), and the KB report route returns its error message instead of showing the user scope's data.~/.teamai/user-votes/, so it no longer shows history left in~/.teamai/votes/.<repo>/.teamai/config.yamlrecords into<repo>/.teamai/votes/, which self migration does not move (as withusage.jsonlafter fix(usage): discard pre-upgrade usage and stop falling back past an unreadable config (#748) #758). Deltas still pending at migration stay there and are not pushed. They are gitignored and not leaked.votes/<user>.yamlformat is unchanged.