Skip to content

feat(session): publish and score sessions per run mode, and drain them before an unattended job ends - #1478

Merged
SaulMoro merged 86 commits into
Tencent:mainfrom
hz89Dev:feat/1385-session-run-mode-policy
Oct 27, 2026
Merged

SaulMoro merged 86 commits into
Tencent:mainfrom
hz89Dev:feat/1385-session-run-mode-policy

Conversation

@hz89Dev

@hz89Dev hz89Dev commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Sessions of a CI job or a cloud VM were never published or judged: they disappeared with the runner, so a team could not see how its agents did there. A team can now set, per run mode, whether sessions are published and scored (off | ask | auto). Unattended runs default to auto for both, and attended runs stay as they were. Every unattended driver ends with one new step, teamai score --drain, which judges what this machine can, publishes, reports usage and says what is left. The session archive moves to teamai-reports for every git-kind team, and readers still find sessions archived on the default branch before.

teamai score --drain            (the local driver runs it at the end of an unattended run)
  unattended: every session captured unattended here counts as finished
  judge        open judge entries this machine can judge, within daily_limit
               nothing while another judge pass here holds its lock: left open, exit 0
  publish      pending labels, queued workflow run records,
               with publish: auto: finished captured sessions + step sessions, scrubbed, on teamai-reports
  report usage to stats/<member>.yaml, as pull does
  print        what is left for other drivers (judge_model_unavailable, isolation, no judge tool)
  exit 1       only when a judgment, a publish or a usage report failed; a failed publish is retried next drain
Setting File Default
sessions.publish: { attended, unattended } teamai.yaml, member config.yaml ask (acts as off until #1388), auto
sessions: { attended, unattended } (boolean still works) scorers/scoring.yaml, namespace scoring.yaml off, auto
scoring.sessions: { attended, unattended } member config.yaml inherits

Each mode resolves on its own: member, then the strictest active namespace, then root, then the default. A bad value is reported by teamai doctor and ignored, and teamai review --branch fails a teamai.yaml that has one.

Also in this change:

  • teamai session push --session <id> --source <platform> pushes exactly that session, always scrubbed. The drain and attended auto publishing use it.
  • Attended auto publishing runs in the background pass that judges finished sessions, once a session is six hours idle.
  • teamai doctor names, once per run mode, the enabled tools teamai reads no sessions from.
  • The dashboard overview gains a Session sharing panel. For each run mode it shows whether this machine publishes and scores sessions, which setting decided that, and what is waiting: sessions a drain must reach, step sessions to archive, and labels to publish. It also gives the command that drains them, in PowerShell form when the dashboard runs on Windows, where each command sets its mode once one does, since PowerShell keeps the variable for the session.
  • A batch session push says when a session it pushes is also archived on the default branch: session list and resume read the new copy on teamai-reports first.
  • A failed write to teamai-reports or teamai-learnings gives what went wrong on one line, the cause ssh prints included, without git's progress.
  • The dashboard Scorers page names a failing label's session from teamai-reports and from the archive on the default branch.
  • Scrubbing an archived session now masks the team's declared secrets (env/secrets.yaml) as well, as judge evidence and run records already did. Titles are redacted before they are cut to length, and a title cut inside a declared secret is masked from where that secret starts, so a secret split by the cut no longer leaves its start in the title, _index.json or the file name.

Dashboard

The built dashboard in headless Chrome, on a team with one attended session active and one finished, one unattended session waiting for a drain, one step session waiting to be archived and one pending label. No element in the panel overflows at any width, and the page is never wider than the viewport.

Desktop, 1440 Tablet, 834 Phone, 390
Dark zh-CN

Whole overview at 1440: the panel sits under the three summary cards, before the trends and the sessions table.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature causing existing behavior to change)
  • Documentation only
  • Refactor / internal cleanup

Breaking: in CI, on a cloud VM or in a hosted job (CI or TEAMAI_MODE=unattended), sessions are now archived on teamai-reports and judged by every scorer with sessions: true. To keep the old behavior, set sessions: { publish: { unattended: off } } in teamai.yaml and sessions: { unattended: off } in scorers/scoring.yaml. The changelog and the scorers and sharing guides say so.

Breaking: session push, session migrate and workflow publish --sessions archive on teamai-reports on every git-kind team. Members on 0.28.0 or earlier do not list, search or resume sessions archived there, and their workflow resume does not restore step sessions archived there, until they upgrade. Sessions on the default branch stay readable to both. The changelog lists it under Breaking Changes, and the dashboard guide says to upgrade every member.

Test Plan

  • npx tsc --noEmit, npm run lint: clean.
  • npx vitest run over the 181 test files this change touches or depends on: 4887 passed, 4 failed. The four are timeouts in workflow-reconciliation and workflow-settle-cost, which time out the same way on main on this machine; none is an assertion on this change.
  • npm run test:e2e -- dashboard-session-sharing score-sessions score-session session-publication workflow-publish-resume: 13 passed.
  • Real CLI, built from this branch: a git-kind team on a local bare origin, a stub claude that logs every call, writes a transcript whose prompt carries a ghp_ token and judges good, and sessions captured through teamai's own hooks with CI=true. Scenarios 1 to 9 ran before the last three commits, which change two messages, the Chinese colon in the panel and an e2e test; those were checked with their unit and e2e tests and the screenshots above.
  • Scenarios 10 and 11 ran on the built CLI at each title-cut fix, with session-title, score-drain, session-push, review-branch and agent-session-find passing.
  • The Windows drain commands are covered by dashboard-session-sharing unit tests (13 passed); npm run test:e2e -- dashboard-session-sharing passes on the rebuilt head. Windows itself is not available here.
  • After the rebase onto bd3adf9b (feat(reporting): record what sessions and workflow steps delivered, and count it per run mode #1471, session outcomes): npx tsc --noEmit and npm run lint clean; the tests of the files both sides touch pass (511), and npm run test:e2e -- dashboard-session-sharing init-keeps-member-settings-1458 score-session score-sessions session-publication workflow-publish-resume dashboard passes (26). Scenarios 12 and 13 ran on the built CLI before this rebase.
  • After the rebase onto 40b0093b (fix(init): keep the member's own settings on init --force #1476): npm run test:e2e -- init-keeps-member-settings-1458 passes with the member's sessions setting listed, and the init, init-projects and commands-reference unit tests pass.
  • After the rebase onto 3d253e45 (feat(dashboard): show scorer pass rates, what drives them, and whether a team change helped (#1182) #1466, the Scorers page): npx tsc --noEmit and npm run lint clean, and the tests of the files both sides touch (dashboard-scorers, dashboard-ui, dashboard-session-sharing-view, dashboard-workspaces, scorers-results, contribute-reminder-record, score-drain, pull-scope-isolation, update, hook-handlers, score-sessions) pass.
1. Unattended local run, no session settings
$ CI=true teamai workflow start review-and-fix --driver local --tools default=claude --auto-approve
ℹ Draining before exit (teamai score --drain)
✔ Judged 2 sampled passes here (review-quality)
✔ Judged 1 sampled session here (session-quality)
✔ Published 3 labels to teamai-reports
✔ Archived 1 captured session to teamai-reports (scrubbed)
✔ Archived 2 step sessions of r0363064d194cb54a to teamai-reports (scrubbed)
✔ Nothing left to do here
rc=0
$ git ls-tree -r --name-only teamai-reports
members/tester.yaml
scores/tester/2026-10.jsonl
sessions/_unattributed/_index.json
sessions/_unattributed/tester/claude-code_add-retries-to-the-client-with-token-redacted-gh-t_20261027.jsonl
sessions/_unattributed/tester/claude-code_add-retries-to-the-client-with-token-redacted-gh-t_20261027.meta.json
sessions/_unattributed/tester/claude-code_fix-what-the-review-found-token-redacted-gh-tok_20261027.jsonl
sessions/_unattributed/tester/claude-code_fix-what-the-review-found-token-redacted-gh-tok_20261027.meta.json
sessions/_unattributed/tester/claude-code_review-the-change-token-redacted-gh-tok_20261027.jsonl
sessions/_unattributed/tester/claude-code_review-the-change-token-redacted-gh-tok_20261027.meta.json
stats/tester.yaml
workflows/runs/r0363064d194cb54a.json
$ git ls-tree -r --name-only main
scorers/review-quality.md
scorers/session-quality.md
teamai.yaml
workflows/review-and-fix.yaml
$ git grep -c ghp_ teamai-reports
(no match)

All three labels carry runMode: unattended. _index.json titles end in <REDACTED:gh_tok>.

2. The same run with unattended: off for publishing and scoring
$ git show main:teamai.yaml | tail -2
sessions:
  publish: { unattended: off }
$ git show main:scorers/scoring.yaml
sessions: { unattended: off }
$ CI=true teamai workflow start review-and-fix --driver local --tools default=claude --auto-approve
ℹ Run r013a136fbff492a8 started. If it stops before it ends, continue it with: teamai workflow resume r013a136fbff492a8
rc=0
$ git ls-tree -r --name-only teamai-reports
members/tester.yaml
stats/tester.yaml
workflows/runs/r013a136fbff492a8.json
$ CI=true teamai score --drain
✔ Judged 2 sampled passes here (review-quality)
✔ Published 2 labels to teamai-reports
✔ Nothing left to do here
rc=0
$ git ls-tree -r --name-only teamai-reports | grep -c '^sessions/'
0

The run does not drain, and only two step calls reach claude. An explicit drain still judges the step passes under their own scorers; the captured session stays unjudged and unarchived.

3. score --drain with nothing to do, then a refused push, then again
$ CI=true teamai score --drain
✔ Nothing left to do here
rc=0
# origin refuses pushes (pre-receive hook); one unattended session captured
$ CI=true teamai score --drain
✔ Judged 1 sampled session here (session-quality)
✖ Could not publish 1 label and 1 session to teamai-reports: ! refs/heads/teamai-reports:refs/heads/teamai-reports [remote rejected] (pre-receive hook declined); remote: pushes are closed; error: failed to push some refs to '<origin>'. They stay pending; run teamai score --drain again.
✖ Could not report usage to stats/tester.yaml on teamai-reports. It stays on this machine; run teamai score --drain again.
rc=1
# pushes allowed again
$ CI=true teamai score --drain
✔ Published 1 label to teamai-reports
✔ Archived 1 captured session to teamai-reports (scrubbed)
✔ Reported usage to stats/tester.yaml
✔ Nothing left to do here
rc=0
$ CI=true teamai score --drain
✔ Nothing left to do here
rc=0

Origin is unchanged after the refused drain, and claude was called to judge once in total: the pending label was published without judging again.

4. session push --session on a git-kind team
$ teamai session push --session 03c12f08-d360-4fcf-8e6f-e66d7d34f811 --source claude-code
  ✓ Pushed claude-code session 03c12f08-d360-4fcf-8e6f-e66d7d34f811 to teamai-reports (redacted)
rc=0
$ git ls-tree -r --name-only main | grep -c sessions/
0
$ git ls-tree -r --name-only teamai-reports | grep review-this-project
sessions/_unattributed/tester/claude-code_review-this-project-token-redacted-gh-tok_20261027.jsonl
sessions/_unattributed/tester/claude-code_review-this-project-token-redacted-gh-tok_20261027.meta.json
$ git grep -c ghp_ teamai-reports
(no match)
5. Resuming a session the released CLI archived on the default branch
$ npx -y teamai-cli@latest --version
0.28.0
$ npx -y teamai-cli@latest session push --source claude-code --scrub --limit 1 -y
  ✓ Pushed 1 session(s) from claude-code
rc=0
$ git ls-tree -r --name-only main sessions/
sessions/_unattributed/_index.json
sessions/_unattributed/tester/claude-code_plan-the-release_20261027.jsonl
sessions/_unattributed/tester/claude-code_plan-the-release_20261027.meta.json
# local transcript deleted, then the built CLI
$ teamai session list --all
  claude-code_bump-the-client-timeou..  tester      claude-code        2  2026-10-27  _unattributed
  claude-code_review-this-project-to..  tester      claude-code        2  2026-10-27  _unattributed
  claude-code_plan-the-release_20261..  tester      claude-code        2  2026-10-27  _unattributed
  3 session(s)
$ teamai session resume claude-code_plan-the-release_20261027 --platform claude-code
  ✓ Session restored to claude-code
  Messages: 2
rc=0

session list merges both places: two sessions from teamai-reports and one from main.

6. teamai doctor with a bad value and tools teamai reads no sessions from
$ teamai doctor
ℹ teamai.yaml sessions.publish.unattended is "yes", not off, ask or auto, so it is ignored.
ℹ Sessions are offered for publishing in attended mode, but teamai reads no sessions from copilot, pi: they are not published.
ℹ Sessions are published and scored in unattended mode, but teamai reads no sessions from copilot, pi: they are neither published nor judged.

doctor exits 1 here because the fixture lists pi without installing it.

7. A secret the team declares, with a name that does not look like one
$ git show main:env/secrets.yaml
secrets:
  - key: WAREHOUSE_DSN
$ CI=true WAREHOUSE_DSN=mysql-prod-7Hq2xVb9Lk teamai score --drain
✔ Judged 1 sampled session here (session-quality)
✔ Published 1 label to teamai-reports
✔ Archived 1 captured session to teamai-reports (scrubbed)
✔ Nothing left to do here
rc=0
$ git log -p teamai-reports | grep -c mysql-prod-7Hq2xVb9Lk
0
$ git grep -h -o "[^\"]*REDACTED:WAREHOUSE_DSN[^\"]*" teamai-reports -- sessions/
Load the warehouse from <REDACTED:WAREHOUSE_DSN>
Load the warehouse from <REDACTED:WAREHOUSE_DSN>
Connected to <REDACTED:WAREHOUSE_DSN> and loaded it.
Load the warehouse from <REDACTED:WAREHOUSE_DSN>

The value is in no file, file name or commit on teamai-reports.

8. score --drain while another judge pass on this machine runs
# score-sessions.lock held by a live process
$ CI=true teamai score --drain
✔ Archived 1 captured session to teamai-reports (scrubbed)
ℹ Left open: 1 judge entry: another judge pass on this machine is running. Run teamai score --drain again once it ends.
rc=0
# no judge call; the other holder's lock is left alone. Holder ended:
$ CI=true teamai score --drain
✔ Judged 1 sampled session here (session-quality)
✔ Published 1 label to teamai-reports
✔ Nothing left to do here
rc=0
9. A token that crosses the title length
$ CI=true teamai score --drain
✔ Judged 2 sampled sessions here (session-quality)
✔ Published 2 labels to teamai-reports
✔ Archived 2 captured sessions to teamai-reports (scrubbed)
✔ Nothing left to do here
rc=0
$ git ls-tree -r --name-only teamai-reports | grep -ic ghp
0
$ grep '"title"' _index.json
"title": "Update the deploy pipeline settings with token <REDACTED:gh_"
"title": "Update the deployment pipeline configuration for staging and"

The first prompt's token starts at character 48, so the title cut falls inside it. Before this change that title kept 19 characters of the token.

10. A declared secret that crosses the title cut

The prompt is a 48-character start followed by the value of WAREHOUSE_DSN, declared in env/secrets.yaml, so the 60-character title cut falls 12 characters into it.

$ CI=true WAREHOUSE_DSN=mysql-prod-7Hq2xVb9Lk teamai score --drain
✔ Judged 1 sampled session here (session-quality)
✔ Published 1 label to teamai-reports
✔ Archived 1 captured session to teamai-reports (scrubbed)
✔ Nothing left to do here
rc=0
$ git ls-tree -r --name-only teamai-reports sessions/
sessions/_unattributed/_index.json
sessions/_unattributed/tester/claude-code_load-the-nightly-warehouse-snapshot-again-using-re_20261027.jsonl
sessions/_unattributed/tester/claude-code_load-the-nightly-warehouse-snapshot-again-using-re_20261027.meta.json
$ git grep -c mysql-prod teamai-reports
(no match)
$ git log -p teamai-reports | grep -c mysql-prod-7Hq2xVb9Lk
0
$ grep '"title"' _index.json
"title": "Load the nightly warehouse snapshot again using <REDACTED:WA",

Without the fix, the new unit test fails because the scrubbed title keeps the start of the value.

11. A declared secret in full, then again across the title cut

The team declares DSN. The prompt is Use mysql-prod-7Hq2xVb9Lk and compare with mysql-prod-7Hq2xVb9Lk, so the 60-character cut falls 17 characters into the second copy.

$ CI=true DSN=mysql-prod-7Hq2xVb9Lk teamai score --drain
✔ Judged 1 sampled session here (session-quality)
✔ Published 1 label to teamai-reports
✔ Archived 1 captured session to teamai-reports (scrubbed)
✔ Nothing left to do here
rc=0
$ git ls-tree -r --name-only teamai-reports sessions/
sessions/_unattributed/_index.json
sessions/_unattributed/tester/claude-code_use-redacted-dsn-and-compare-with-redacted-dsn_20261027.jsonl
sessions/_unattributed/tester/claude-code_use-redacted-dsn-and-compare-with-redacted-dsn_20261027.meta.json
$ git grep -c mysql-prod teamai-reports
(no match)
$ grep '"title"' _index.json
"title": "Use <REDACTED:DSN> and compare with <REDACTED:DSN>",

Without the fix, the new unit test fails because the title ends in mysql-prod-7Hq2xV.

12. Mixed releases: this build archives, 0.28.0 reads

One member HOME and one git-kind team.

$ teamai session push --session 7c1e0000-0000-4000-8000-0000000000f1 --source claude-code
  ✓ Pushed claude-code session 7c1e0000-0000-4000-8000-0000000000f1 to teamai-reports (redacted)
$ npx -y teamai-cli@latest --version
0.28.0
$ npx -y teamai-cli@latest session push --source claude-code --scrub --limit 1 -y
  ✓ Pushed 1 session(s) from claude-code
$ git ls-tree -r --name-only teamai-reports sessions/ | grep jsonl
sessions/_unattributed/tester/claude-code_ship-the-hotfix_20261027.jsonl
$ git ls-tree -r --name-only main sessions/ | grep jsonl
sessions/_unattributed/tester/claude-code_plan-the-release_20261027.jsonl
$ npx -y teamai-cli@latest session list --all
  claude-code_plan-the-release_20261..  tester      claude-code        2  2026-10-27  _unattributed
  1 session(s)
$ teamai session list --all
  claude-code_ship-the-hotfix_20261027  tester      claude-code        2  2026-10-27  _unattributed
  claude-code_plan-the-release_20261..  tester      claude-code        2  2026-10-27  _unattributed
  2 session(s)
# local transcript of the hotfix session deleted
$ npx -y teamai-cli@latest session resume claude-code_ship-the-hotfix_20261027 --platform claude-code
Error: Session claude-code_ship-the-hotfix_20261027 not found (searched all author directories)
rc=1
$ teamai session resume claude-code_ship-the-hotfix_20261027 --platform claude-code
  ✓ Session restored to claude-code
rc=0

This is the Breaking Changes entry: 0.28.0 neither lists nor resumes what this build archives, and both read the default branch.

13. A batch push over the default branch, doctor under ask, and a drain that cannot push over ssh
$ teamai session push --source claude-code --scrub -y --repo-root <team clone>
  ✓ Pushed 1 session(s) from claude-code
$ teamai session push --source claude-code --scrub -y
  · 1 session(s) also archived on the default branch: session list and resume read the copy pushed to teamai-reports first.
  ✓ Pushed 1 session(s) from claude-code
# pi enabled, attended publishing at its default ask
$ teamai doctor | grep "reads no sessions"
ℹ Sessions are published and scored in unattended mode, but teamai reads no sessions from pi: they are neither published nor judged.
# scorers/scoring.yaml has daily_limit: -1; origin's push URL is ssh, through a stand-in ssh that denies the key
$ CI=true GIT_SSH_COMMAND=<stand-in ssh> teamai score --drain
⚠ scorers/scoring.yaml: daily_limit is -1, not a whole number of 0 or more, so it is ignored.
✔ Judged 1 sampled session here (session-quality)
✖ Could not publish 1 label and 1 session to teamai-reports: git@example.invalid: Permission denied (publickey).; fatal: Could not read from remote repository.; Please make sure you have the correct access rights; and the repository exists. They stay pending; run teamai score --drain again.
rc=1

The ignored value is printed once, where the drain printed it once for each lease it took. The ssh cause is kept, where the reason kept only the fatal: line.

Not run here, left to CI: the gitlab and github providers, Windows, and real Claude Code, Codex, CodeBuddy, OpenCode and WorkBuddy. The WorkBuddy and CodeBuddy IDE title cut is covered by unit tests only.

Related Issues

Closes #1385. Builds on #1384 (run mode) and #1386 (the driver's own judge, #1470). ask offers nothing yet; #1388 adds it.

Notes for Reviewers

  • One drain implementation: src/score-drain.ts, called by score --drain and by the local driver at done.
  • A failed judgment is final, under the existing attempt rules. Only a failed publish or usage report stays pending for the next drain. teamai score --session asks for a new judgment.
  • A drain finishes and publishes only the sessions of its own run mode. To drain unattended sessions on a machine that is not unattended, run TEAMAI_MODE=unattended teamai score --drain.
  • Limitation, documented: runs, judge attempts and leases live in the runner's data home, so a runner killed before its drain loses that judging.
  • The batch session push -y without --scrub still archives as-is behind its existing confirmation, as before. Every policy path and push --session always scrub.
  • session push --scrub --repo-root <clone> masks secret-looking values but not the team's declared secrets: there is no teamai install to read env/secrets.yaml from. The changelog says so.

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Redact declared secrets before truncating titles — src/session-flow/title.ts:17
    cutTitle only uses redactWithEnv; declared secrets reach scrubSession after the adapter has truncated the title. With the test’s WAREHOUSE_DSN=mysql-prod-7Hq2xVb9Lk following a 48-character prompt prefix, the 60-character title retains mysql-prod-7. The later full-value replacement cannot mask it, so automatic publishing leaks that prefix into the archived title, index and filename. Apply declared-secret redaction before the cut.

  • [P2 non-blocking] Correct the invalid-YAML validation claim — docs/guide/sharing.md:323
    The guide says review --branch fails invalid teamai.yaml, but src/review-branch.ts:1211 catches YAML parse errors and returns no findings; the added test explicitly expects that behavior. A branch containing sessions: [ therefore passes this check. Either report a validation failure or correct both language versions of the guide.

The PR description documents sufficient testing, including a built real-CLI verification record. No missing-e2e finding. I did not run, build or install PR code.

@hz89Dev
hz89Dev force-pushed the feat/1385-session-run-mode-policy branch from b6b9993 to 363e663 Compare October 27, 2026 19:52
@github-actions

Copy link
Copy Markdown

Both earlier findings remain unresolved in the current diff:

  • [P1 blocking] Redact declared secrets before truncating titles — src/session-flow/title.ts:17
    cutTitle still uses only redactWithEnv; declared secrets are supplied later to scrubSession. With declared WAREHOUSE_DSN=mysql-prod-7Hq2xVb9Lk starting after a 48-character prompt prefix, the 60-character cut retains mysql-prod-7. Full-value replacement cannot mask that fragment, so automatic publishing leaks it into the archived title, index and filename. Apply declared-secret redaction before truncation.

  • [P2 non-blocking] Correct the invalid-YAML validation claim — docs/guide/sharing.md:323
    The guide still says review --branch fails invalid teamai.yaml, but the added sessionsFindings catches YAML parse errors and returns no findings. The added test explicitly expects sessions: [ to pass without findings. Either report a validation failure or correct both language versions of the guide.

The PR description documents sufficient testing, including built real-CLI verification; no missing-e2e finding. I found no additional confident issues and did not run, build or install PR code.

@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] Set the mode for every PowerShell drain command — src/dashboard/session-sharing.ts:117
    When the dashboard runs unattended and both modes have pending sessions, running the explicit attended command changes $env:TEAMAI_MODE persistently. The bare teamai score --drain offered for unattended mode then drains attended sessions again, leaving unattended sessions pending. Set the mode explicitly for every Windows command, or restore the previous environment value.

Both earlier findings are resolved:

  • Declared-secret fragments crossing the title cut are now masked.
  • Both sharing guides now describe invalid policy values, not invalid YAML.

The PR description documents sufficient testing, including built real-CLI verification; no missing-e2e finding. I did not run, build or install PR code.

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Check cut-secret fragments before redaction shortens the title — src/session-flow/scrub.ts:144
    With declared DSN=mysql-prod-7Hq2xVb9Lk, the prompt Use mysql-prod-7Hq2xVb9Lk and compare with mysql-prod-7Hq2xVb9Lk produces a 60-character title ending in mysql-prod-7Hq2xV. mask(session.title) replaces the first complete value and shortens the title to 53 characters; maskCutSecret then skips it because it is shorter than 60. Automatic publishing therefore leaks the second secret’s 17-character prefix into the archived title, index and filename. Check the cut fragment before full-value replacements change the title’s length.

The earlier single-secret title scenario, invalid-YAML wording, and PowerShell drain-command findings are resolved.

The PR description documents sufficient testing, including built real-CLI verification; no missing-e2e finding. I did not run, build or install PR code.

@SaulMoro SaulMoro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

Reviewed at 5cc5cf4. Every #1385 acceptance item works, most of them checked with the built CLI. Two things need a change before merge: one is the bar for a team repo layout change, the other a regression in git error messages.

Blocking

  • [P1 blocking] The archive move needs a Breaking Changes line and the reverse mixed-release run. On one member HOME and one git-kind team, 0.28.0 pushed one session to the default branch, then this build pushed two to teamai-reports. This build lists all three, finds and resumes the old one, and deletes nothing on main (git log --diff-filter=D main is empty). But session list on 0.28.0 and on main prints 1 session(s). A member who has not upgraded stops seeing every session an upgraded teammate archives, and by the code an older workflow resume does not restore step sessions archived there either. No data is lost, but every mixed-version team hits it, and the CHANGELOG lists the move only under ### Changed. Please add a ### 💥 Breaking Changes line, for example: "teamai session push, session migrate and workflow publish --sessions archive on teamai-reports on every git-kind team. Members on 0.28.0 or earlier do not list, search or resume sessions archived there until they upgrade; sessions on the default branch stay readable to both." Then add the reverse direction (this build writes, 0.28.0 reads) to the body's record.

  • [P2 non-blocking, but it fits this PR and should land with it] Git failures lose the ssh cause line. GIT_PROBLEM_LINE in src/utils/branch-worktree.ts keeps only remote:, !, error: and fatal: lines, and ssh prints its cause without a prefix. With a stand-in ssh that denies the key, teamai score --push prints only fatal: Could not read from remote repository.; main also prints git@example.invalid: Permission denied (publickey). By the regex, Host key verification failed. and ssh: Could not resolve hostname … are dropped too. This reaches every write to teamai-reports and teamai-learnings. Dropping progress lines instead of keeping a whitelist avoids it, for example:

    const GIT_PROGRESS_LINE = /^(?:To |Pushing to |hint:|Enumerating |Counting |Compressing |Writing |Total |remote: (?:Counting|Compressing|Resolving|Total)|$)/;
    const problems = message.split('\n').map((line) => line.trim().replace(/\s+/g, ' ')).filter((line) => line !== '' && !GIT_PROGRESS_LINE.test(line));

    plus a Permission denied (publickey) transcript in branch-worktree-failure-reason.test.ts.

Non-blocking

  • [P3 nit] Doctor says sessions are "offered for publishing in attended mode" under the default ask, which does nothing until #1388. Printing the attended line only for auto, or saying "set to ask (not offered yet)", would avoid confusion.
  • [P3 nit] score --drain prints each scoring-settings problem three times (judge, publish and usage paths each call scopeScoring).
  • [P3 nit] A batch session push writes a second copy of a session already on the default branch without saying so. It is still listed once.

CI review findings: all three are fixed at this head and we checked each: the declared-secret title leak (reproduced at b6b9993, zero secrets at 5cc5cf4), the guide's claim that review --branch rejects invalid YAML (corrected), and the Windows PowerShell drain command (fixed by reading the code; Windows not run).

What we ran (built CLI, git-kind team on a local bare origin, base = main 3d253e4)

  • score --drain twice (idempotent), with the judge lock held (entries left open, exit 0), a refused push then a retry, and at the end of an unattended local workflow run.
  • session push --session, scrubbed; bad values reported by doctor and refused by review --branch; member settings over the team's; attended auto archiving only the session idle over six hours.
  • npx tsc --noEmit and npm run lint exit 0; 35 changed unit files (995 tests) pass, also with a symlinked TMPDIR; 9 e2e files (20 tests) pass. On main, the PR's tests fail 26 cases, all new or changed by the PR.

Covered by unit tests only: namespace scoring files, the HTTP-team doctor note, a failed judgment, the judge-model-unavailable case.

@SaulMoro
SaulMoro self-requested a review October 27, 2026 20:18
@hz89Dev
hz89Dev force-pushed the feat/1385-session-run-mode-policy branch from ad4910f to 6c0604d Compare October 27, 2026 20:20
@github-actions

Copy link
Copy Markdown

No new confident findings in the specified diff.

All earlier findings are resolved:

  • Declared-secret fragments are masked before full-value redaction changes the title’s length, covering both reported truncation scenarios.
  • Both sharing guides now describe invalid policy values, not invalid YAML.
  • Every offered PowerShell drain command sets its mode when any command needs an explicit mode.

The PR description documents sufficient testing, including built real-CLI verification. No missing-e2e finding; deferred provider/agent coverage is not blocking.

I did not run, build, or install PR code.

scoring.yaml and config.yaml scoring.sessions take off, ask or auto per
run mode, or true/false for both. Each mode resolves on its own: member,
then the most restrictive active namespace, then the root file, then the
default (attended off, unattended auto). A bad value is reported with its
key and allowed values and ignored. A captured session is judge work
when the policy of the mode it was captured in is auto.
teamai.yaml and config.yaml take sessions.publish: { attended, unattended }
of off, ask or auto. Each mode resolves member, then team, then the default
(attended ask, unattended auto). A bad value is reported and ignored, and
never fails either file's load. An HTTP team publishes nothing.
doctor prints each sessions.publish value it ignores, and that an HTTP
team publishes no sessions.
session push, session migrate and workflow publish --sessions now commit to
teamai-reports on every git-kind team, as self mode already did. Readers look
there first; on a git-kind team they also read the archive kept on the default
branch before. session list and search show a session both hold once, from
teamai-reports.
teamai session push --session <id> --source <platform> archives exactly that
session, redacted and without prompting, on teamai-reports. pushSession is the
programmatic entry it calls, returning archived, busy or failed.
session list and search read both archives through one helper now, so the
SyncManager method has no caller. Its repo backfill is covered at the
session list --all seam.
An unattended capture becomes judge work when teamai score --drain marks
it drained, never after six idle hours. Attended sessions keep the
six-hour rule.
score --drain marks the unattended captures of each reached data home
finished, judges every judge entry a local tool here runs, settles a
failed pass lease as failed so no drain judges it again, prints what
failed and what is left for other drivers, and exits 1 when a judgment
failed.
The drain publishes the labels it left pending, as score --push does, and
exits 1 with the reason when they cannot reach teamai-reports. They stay
pending, so the next drain publishes them.
With sessions.publish auto for the current run mode, the drain archives
each finished session captured in that mode, scrubbed, on
teamai-reports. A per-data-home record keeps an archived session from
being pushed again; a push that fails records nothing, so the next drain
retries it, and the drain prints one line for what stayed pending.
…drain

With sessions.publish auto for the current run mode, the drain archives
the session of each pass settled here in the ended runs of that mode,
scrubbed, on teamai-reports, through the writer workflow publish
--sessions uses. A pass whose tool teamai reads no sessions from is
listed as skipped.
The drain reports the usage of each scope as pull does, honouring the
usage report opt-out, so the counts of an ephemeral runner reach
teamai-reports. A report that does not land exits 1.
Also covers that an attended drain archives only sessions idle six hours.
workflow start and resume with the local driver run the drain on the
run's scope once an unattended run is done, after printing its outcome.
The run's outcome keeps setting the exit code; what the drain could not
do is printed.
With sessions.publish auto for attended sessions, the pass a session start
begins archives the finished attended sessions of each scope before it
judges, and a session start begins that pass for such a scope even when it
scores no sessions.
Where sessions are published or scored in a run mode, doctor names each
enabled tool teamai reads no sessions from, since its sessions are never
captured and so are neither published nor judged.
…orts, as well as one archived on the default branch before

The Scorers page read session names only from the team clone's archive,
where a git-kind team no longer archives sessions. It now reads
teamai-reports and the old archive, as session list does.
… cut inside it

A title is cut to its length when its session is read, before the team's
declared secrets are known. A declared secret that crossed the cut left
its start in the archived title, _index.json and the file name, which
the whole-value mask no longer matched. Scrubbing now masks that start
as the cut would have had it known the secret.
…value

It reports nothing for a teamai.yaml that does not parse.
…f them sets it

PowerShell keeps $env:TEAMAI_MODE for the rest of the session, so after
the command for the other mode, the bare command for this machine's mode
drained the other mode's sessions again.
…g whole values shortens it

A title that held a declared secret in full before the cut came out of
the whole-value mask shorter than its cut length, so the start of a
second copy at the cut was no longer checked and stayed in the archived
title, _index.json and the file name.
…fails

A failed push kept only remote:, !, error: and fatal: lines, so the
line ssh prints without a prefix, such as Permission denied (publickey)
or Host key verification failed, was dropped. The reason now drops git's
progress lines and keeps every other one.
Members on 0.28.0 or earlier do not see sessions archived on
teamai-reports until they upgrade.
…t to ask

ask acts as off until sessions can be offered, so the attended note
said sessions were offered for publishing when nothing was.
…ings

Each lease a drain takes reads the scoring settings again, so a drain
that judged printed each ignored value once a lease.
…anch also holds

The push writes the session's current copy on teamai-reports, which
session list and resume read first, and left the copy on the default
branch unmentioned.
… in one

The cause ssh gives ends a sentence of its own, so the drain and init
printed two periods after it. A side-branch write reason drops its final
period, and the drain drops it from every reason it lists.
@hz89Dev
hz89Dev force-pushed the feat/1385-session-run-mode-policy branch from 01ac587 to e0c0c53 Compare October 27, 2026 20:54
@hz89Dev

hz89Dev commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

All five items are fixed at e0c0c53, which is rebased on main after #1471.

  • [P1] Breaking Changes line and the reverse run: 33747ac moves the archive line to ### 💥 Breaking Changes with your wording, and the dashboard guide (both languages) now says to upgrade every member. The reverse run is scenario 12 in the body. This build archived one session on teamai-reports and 0.28.0 one on main, from the same HOME. 0.28.0 lists 1 session and fails session resume of the teamai-reports one (not found, rc=1). This build lists 2 and resumes it.
  • [P2] ssh cause line: 0b19ead drops git's progress lines instead of keeping an allow-list, with the Permission denied (publickey) transcript in branch-worktree-failure-reason.test.ts. The reason now ends in ssh's own period, so 8440f7a drops that period from the reason, since the caller ends the sentence. Without it, the drain and init printed exists... With a stand-in ssh that denies the key, score --drain prints git@example.invalid: Permission denied (publickey).; fatal: Could not read from remote repository.; … and the repository exists. They stay pending; … (scenario 13).
  • [P3] doctor under ask: afd5dad treats ask as off there, so the attended line appears only for auto.
  • [P3] repeated scoring problems: each lease the drain took went back through requestedEntries and warned again. 005f9b8 uses warnOnce there, and a drain that judges now prints daily_limit is -1 once. The new test in score-drain.test.ts saw it three times before the fix.
  • [P3] batch push over the default branch: edce6ab keeps writing the current copy, which is the only place teamai writes now, and prints · 1 session(s) also archived on the default branch: session list and resume read the copy pushed to teamai-reports first.

The rebase onto #1471 needed one test change, 832762e. Its team-push-outcomes test used reportUsageToTeam, which this branch renamed to reportUsage with a named result. The tests of the files both PRs touch pass (511), and so do the e2e files for both features (26).

@github-actions

Copy link
Copy Markdown

No new confident findings in the specified diff.

All earlier findings are resolved:

  • Declared-secret fragments are masked before full-value redaction changes the title’s length, covering both reported truncation scenarios.
  • Both sharing guides now describe invalid policy values, not invalid YAML.
  • Every offered PowerShell drain command sets its mode when any command requires an explicit mode.

The PR description documents sufficient testing, including built real-CLI verification. No missing-e2e finding; coverage explicitly deferred to CI is not blocking.

I did not run, build, or install PR code.

@SaulMoro SaulMoro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

Approving at e0c0c53. All five items from our last review are resolved, each checked with the built CLI.

Item Evidence
[P1] Breaking Changes line and the reverse mixed-release run This build archives to teamai-reports; 0.28.0 and current main then list 1 session, find nothing with search and cannot resume the new one, while this build lists 2 and resumes it. Nothing on main is deleted. The Breaking line says exactly that.
[P2] ssh cause line With a stand-in ssh that denies the key, score --push and score --drain print Permission denied (publickey). A push refused by a pre-receive hook still gives a short reason.
[P3] doctor under attended ask The attended line is no longer printed for ask.
[P3] drain repeats settings warnings Each warning prints once.
[P3] batch push re-archives a default-branch session It now says so.

The CI review's 20:14Z finding (part of a second copy of a declared secret left in a cut title) reproduces at the earlier head and is fixed by 2e4007a, with a unit test for that case.

One cosmetic nit: the joined ssh reason reads (publickey).; fatal: …; Please make sure you have the correct access rights; and the repository exists, with an extra .; and git's two-line hint split in the middle.

npm run build, npx tsc --noEmit and npm run lint exit 0; the 36 changed unit files (1,019 tests) pass, also with a symlinked TMPDIR; CI is green at this head.

@jeff-r2026 Breaking for the next tag: on every git-kind team, sessions are now archived on teamai-reports, so members on 0.28.0 or earlier cannot list, search or resume them (including step-session restore in workflow resume) until they upgrade; unattended runs now publish and score their sessions by default. See CHANGELOG.

@SaulMoro
SaulMoro merged commit 59b2c07 into Tencent:main Oct 27, 2026
18 checks passed
hz89Dev added a commit to hz89Dev/teamai-cli that referenced this pull request Oct 27, 2026
Tencent#1478 added a member sessions setting to config.yaml, and Tencent#1473 made every
config key either an install key or a member setting. Keep sessions per
install, as scoring is, so the key ownership test passes again.
SaulMoro pushed a commit that referenced this pull request Oct 28, 2026
… no JSON to parse (#1482)

When a placeholder replaces a number or a key, the masked arguments are no longer valid JSON. The scrubber used to fall back to the original arguments, writing the secret into the archive; it now keeps the masked text as {redacted: ...}.

Follow-up to #1478
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] Publish and score sessions by run mode

3 participants