Skip to content

feat(lanes): private, aggregatable store for lane transcripts #7676

Closed

Description

@kyle-sexton

Refs: #6958

Context

The claude.dev post "Building effective agent automations" (2026-10-08, https://claude.dev/blog/building-effective-agent-automations/) recommends reading each run's transcript to find what went wrong. This repo's lanes publish telemetry but keep no transcript store, so runs cannot be pulled and compared to find errors and bottlenecks or to measure improvement.

Decision

  • Q25, C18: the store is swappable. CI always emits an encrypted, gzipped artifact kept 7 days; a puller copies it to a sink set by one config value. The laptop is the first sink; others (object storage, another machine) are a config change.
  • Q28, C23: transcripts stay private and easy to aggregate. Published per-run fields go to the telemetry issues below, not here.
  • Q33 is open for planning: where the encryption key lives, and the puller's host, schedule and repo.

Acceptance criteria

Related

Out of scope

  • The published per-run fields and their format.

Activity

  1. added
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Oct 16, 2026
  2. github-actions commented on Oct 16, 2026

    @github-actions
    Contributor

    Proposed work class: work-class: scoped
    Basis: automated intake triage; the lanes App applied this class.

    Size: Medium
    Complexity: Medium
    Uncertainty: Knowable
    Flags: intent
    Blocked by: none

    Decision recorded. The owner answered Q33 on 2026-10-18: where the key lives and where the puller runs. Nothing new is decided here. Intake posted no refinement comment on this issue, so no refinement questions are open. The decision does not name the puller's repository.
    [quote truncated; the full decision is at the link]

    #7676 (comment)

    I understand this as: CI uploads each lane transcript as an encrypted, gzipped, 7-day artifact. It encrypts with a public key held as a repo variable. A daily laptop task pulls the artifacts, with the private key in Key Vault read through vault-exec.

    The decision settles the key location and the puller host. One question is still open:

    1. Which repository holds the puller? RECOMMENDED: this repository, next to the lane workflows. Basis: the artifacts and the one config value for the sink live here, so the puller then needs no cross-repo access.

    Still needed from a person: create the key pair, store the private key in Key Vault, and set the public-key repo variable. The laptop scheduling and the retention and alert checks also need a person.

    Automated triage, applied by the lanes App.

  3. added
    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.
    and removed
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Oct 16, 2026
  4. kyle-sexton commented on Oct 18, 2026

    @kyle-sexton
    MemberAuthor

    Decision recorded.

    Question: Every lane run would upload its transcript, encrypted, as a 7-day artifact, and a puller copies it to one sink. This is a public repository, so artifacts must be encrypted before upload. Q33 asks where the decryption key lives and where the puller runs. Options: (a) public key in CI, private key on the laptop, puller a scheduled task on the laptop; (b) key in Azure Key Vault, puller on a fleet host; (c) defer the store until #6973/#6974 show it is needed.

    Owner's answer: asymmetric encryption, with the private key in Key Vault, read through vault-exec, and the puller as a daily task on the laptop. The estate already resolves secrets through vault-exec from Key Vault, so a key kept only on the laptop would be a second, unbacked-up way of holding secrets. The public key is not secret, so CI can hold it as a repo variable. (c) does not fit: #6973 and #6974 read the published fields, not transcripts (#7676 "Related"). This is a security decision, so the owner must answer it. Basis: ~/.config/estate/ESTATE.md:34-36; #7676 body.

    The owner accepted this recommendation as item D3 of a 21-decision batch in an attended backlog-shaping session on 2026-10-18. The batch question: "Accept all 21 batch 7 decisions, with my change to D13 (#6008 goes to the lane, but you merge its PR because it edits AGENTS.md)? I post a 'Decision recorded.' comment on each issue." The owner answered: "Accept, with my D13 change (Recommended)".

    Posted by AI from the owner's answer in that session.

  5. removed
    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.
    on Oct 25, 2026
  6. kyle-sexton commented on Oct 25, 2026

    @kyle-sexton
    MemberAuthor

    Released for re-triage under the owner's 2026-10-24 rule: an issue stops for a person only for an intent or vision question, a security call, a host or attended run, or spend outside prepaid credit. The hold label came off and intake triage runs again now; it re-applies a hold, with a numbered question, when one of those applies.

    To hold this issue again, add needs-human (or status: needs-decision) and say why in a comment.

  7. added
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.
    priority: mediumReal value, no hard deadline; normal backlog flow.
    and removed
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Oct 25, 2026
  8. melodic-automation-lanes commented on Oct 25, 2026

    @melodic-automation-lanes
    Contributor

    The triage verdict left this issue at status: needs-decision:

    • flagged: intent
    • held by status: needs-decision
    • held by needs-human
    • no lanes App verdict comment shows whether an earlier verdict was flagged
  9. 15 remaining items

  10. removed
    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.
    on Oct 27, 2026
  11. kyle-sexton commented on Oct 28, 2026

    @kyle-sexton
    MemberAuthor

    Re-triage. This is not an owner stop. The owner decided the security question, where the key lives and where the puller runs, as D3 on 2026-10-18 (comment). The criteria are recorded in this comment.

    The one gap that record left, which repository holds the puller, is decided on the facts: this repository. The artifacts and the one sink config value live here, so the puller needs no cross-repo access. The laptop task calls the puller from this repository's checkout.

    Cost: artifacts on a public repository with 7-day retention fall within the free Actions storage for public repositories, so nothing here can bill.

    Acceptance criteria (unchanged from the 2026-10-26 record):

    • Every lane run uploads its transcript, encrypted and gzipped, as a CI artifact with 7-day retention, encrypted with the public key CI holds as a repo variable.
    • No transcript, key or decrypted content lands in a tracked file or a public artifact.
    • The puller lives in this repository and copies artifacts to the sink named by one config value. Changing the sink changes only that value.

    Builder: the Automation Lanes session builds this from the owner's machine, because the upload step edits .github/workflows/**, which the lanes App cannot push. Before the build: create the key pair, put the private key in Key Vault, and set the public key as the repo variable CI reads. agent-ready stays off.

    Checks a person runs: the private key is read through vault-exec; the puller runs as a daily task on the laptop; sink retention (30 days for transcripts, 1 year for metrics, a 5 GB cap, an alert after 5 days with no pull); stored transcripts are queryable by lane, model, outcome and date.

  12. added
    host-runNeeds an agent run or builder session on a host; not a person or the issue lane.
    and removed
    status: needs-decisionAwaiting a human or maintainer judgment call.
    on Oct 28, 2026
  13. kyle-sexton commented on Nov 2, 2026

    @kyle-sexton
    MemberAuthor

    Status: what landed and what is left.

    Status check by the issue-triage session against main at 0ed6022913, on the criteria in the 2026-10-28 re-triage comment.

    Met:

    Remaining:

    • Every lane run uploads its transcript, encrypted and gzipped, as a CI artifact with 7-day retention, encrypted with the public key CI holds as a repo variable. Partly met: upload-lane-transcript (gzip, seal to LANE_TRANSCRIPT_PUBLIC_KEY, retention-days: 7) is wired into intake-triage, pr-verify, pr-unblock, pr-review-main, maintenance-audit-lane-runs and the read runner's reuse summary. Not covered, per feat(lanes): upload each model leg's transcript as a sealed 7-day artifact #10749: the read runner's skill job, the write runner's skill job (pr-run-activity-write.yml has no call), the mutation audit, and the review lanes, which run in melodic-software/ci-workflows. No run with LANE_TRANSCRIPT_PUBLIC_KEY set is recorded.
    • The puller lives in this repository and copies artifacts to the sink named by one config value; changing the sink changes only that value. Main has no puller and no sink config value; only upload-lane-transcript/README.md describes it.
    • Checks a person runs (from the 2026-10-28 comment): the private key is read through vault-exec; the puller runs as a daily task on the laptop; sink retention (30 days for transcripts, 1 year for metrics, a 5 GB cap, an alert after 5 days with no pull); stored transcripts are queryable by lane, model, outcome and date.

    Route: the remaining uploads edit .github/workflows files or a ci-workflows reusable, so they go to a builder session. Creating the key pair, the Key Vault secret and the repository variable is a person's step, and the puller's laptop task and retention checks are host runs.

  14. kyle-sexton commented on Nov 2, 2026

    @kyle-sexton
    MemberAuthor

    Decision (Lane 5, under the standing rule: least friction that keeps the boundary): the three jobs exempted from transcript upload stay exempt. Each is an accepted gap against "every lane run".

    The jobs are pr-run-activity-read.yml#run, pr-run-activity-write.yml#run and post-merge-test-mutation.yml#audit. #10749 exempted them because code the model can rewrite runs before a later step that holds a token (id-token: write on the write runner, the runtime token in the mutation audit). The reasons are recorded at .github/actions/resolve-config/callers.test.mjs:7453-7464. Uploading from those jobs would run an upload step after code that may have been tampered with, while it holds that token, which loosens a boundary. Keeping the exemption changes nothing.

    If transcripts from these lanes prove necessary, the route is a separate clean job that receives an already sealed record. That is a new design.

    The puller is #12428. The model legs inside ci-workflows reusables are ci-workflows#964.

    This was generated by AI.

  15. kyle-sexton commented on Nov 2, 2026

    @kyle-sexton
    MemberAuthor

    Status: live on melo-lap-001, first real pulls verified. This stays open only for ci-workflows#964.

    • Key: kv-melo-github-prod/lane-transcript-private-key. The key id derived from the vault's private key matches LANE_TRANSCRIPT_PUBLIC_KEY (b9a83392502f6984).
    • Puller: #12428, merged.
    • Timer: dotfiles#1414, merged and enabled. It runs daily from a fresh origin/main export. A failed run raises a desktop toast through unit-failure-toast@.
    • First pull (systemctl --user start lane-transcript-pull.service): run 1 stored 65 transcripts, then failed on one gh call. Run 2 succeeded: pulled 153 transcript(s); skipped {"pulled":153,"known":67,...,"too_large":0,"bad_zip":0,"sealed_mismatch":0}. Total: 218 transcripts, 6.0 MB, none sealed to another key.
    • Query: query --since 2026-11-02 --outcome success returns rows by lane, model, outcome and date.
    • First use of the store: #12526 (the CI API key is out of credit).

    UNVERIFIED: that a toast visibly appears on the Windows desktop (exit 0 only); a midnight timer firing, due 2026-11-03 00:00 EST.

    This was generated by AI.

  16. kyle-sexton commented on Nov 3, 2026

    @kyle-sexton
    MemberAuthor

    ci-workflows#969 merged (328b0c15), so the reusable review and intake legs can now seal and upload transcripts once a caller passes lane-transcript-public-key. Remaining before this closes: the runner policy allowlist and hosted review callers in melodic-software/standards (PR in progress), the intake-triage.yml caller here, then a real sealed reusable-leg artifact through the puller. Lane 5 holds this.

  17. kyle-sexton commented on Nov 3, 2026

    @kyle-sexton
    MemberAuthor

    Done. #12579 merged (71d28c0), so the reusable review and intake callers now pass the transcript key.

    Proof: one run of the lane-transcript-pull unit at 2026-11-03T01:48Z pulled 246 transcripts with sealed_mismatch 0 and fetch_failed 0. The sink holds 464 rows and 14.5 MB. The reusable-leg runs 41559009992 (code-review-subscription) and 41559010015 (security-review-subscription) are stored and returned by pull.mjs query with their lane, model, outcome and date.

    Left elsewhere: hardening of the job that follows main is #12534. The first scheduled timer run is due at midnight EST; Lane 5 checks it and reports any failure here.

  18. kyle-sexton commented on Nov 3, 2026

    @kyle-sexton
    MemberAuthor

    Scheduled pull failed: the 2026-11-03T05:00:10Z timer run exited 1 at 05:00:45Z with pull failed: artifact-list: gh exited 1 (journalctl, lane-transcript-pull.service). A manual run at 14:25Z failed the same way.

    Cause: GitHub returned HTTP 500 with an empty body on repos/melodic-software/claude-code-plugins/actions/artifacts?per_page=100&page=1 (request id E139:12AAB4:2788507:820BC81:6AE9EED9). The 500s were intermittent: at 14:25Z, per_page 100 and 50 failed while 75, 30, 10 and 1 succeeded, and a minute later 8 of 8 calls at per_page 100 returned 200. The repo holds 15,555 artifacts. ghRetried makes 3 attempts with 2 s and 4 s waits (scripts/lane-transcripts/pull.mjs:45-46, :88-99), so a burst of 500s longer than about 6 s fails the whole pull. The rate limit was not involved (core 4,762 of 5,000 left).

    A rerun started at 14:29Z is in progress; its result follows here. The unit-failure toast fired as designed (OnFailure).

  19. kyle-sexton commented on Nov 3, 2026

    @kyle-sexton
    MemberAuthor

    Rerun result: exited 0 at 14:52:46Z after 25 min 25 s wall clock (2 min 35 s CPU). Pulled 1,125 transcripts, 442 already known, fetch_failed 0, sealed_mismatch 0, sink 52.5 MB, nothing pruned. The 05:00Z failure was GitHub's transient 500s, not the puller. The next scheduled run is tonight; the 3-attempt, 6 s retry window is short against bursts like this one, and a longer backoff on 5xx would have saved this run.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    host-runNeeds an agent run or builder session on a host; not a person or the issue lane.priority: mediumReal value, no hard deadline; normal backlog flow.status: confirmedA maintainer confirmed the item is real and in scope; ready to be worked.work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.

    Fields

    No fields configured for Feature.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions