channels: bind restarted click decisions to the checkpointed session; approvals.get() reads the durable store - #388
Merged
Merged
Conversation
… approvals.get() reads the durable store (#279, #280) A channel button click after a restart resolved the approval but the continuation ran outside the session, so it never reached the transcript; and a function approvers saw only this process's pending list, so it failed closed after a restart. - mountChannels: a decision with inbound that this process did not pause on is checked against the conversation's checkpointed turn (exact approval id, matching kind, no sign-in), then bound via session.resume() (SessionAwaitingApprovalError) so agent.approvals.resolve() continues the turn inside the session and records the transcript. Mismatched, stale or replayed decisions are refused with LOUSHO_APPROVAL_NOT_FOUND before the approval is touched and without an audit entry. - ApprovalStore gains optional load(id) - resolve() without claiming - implemented by the in-memory, StorageService, file, SQLite, KV and agent-forge stores; agent.approvals.get(id) reads through it and ctx.approval() falls back to it, so a function approvers sees the recorded request (toolName, input, sessionId, principal) after a restart and fails closed only when no store knows the id.
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.
Summary
Closes #279
Closes #280
#279 - click after restart was not appended to the session transcript
mountChannelscalledagent.approvals.resolve()for a decision this process did not pause on, but nothing bound that approval to the restarted session, so the continuation ran outsideAgentSession.continueTurn()and was never committed to the transcript.Now a decision carrying
inbound(the conversation as the click names it) is checked against that conversation's checkpointed turn before the approval is touched: the pending checkpoint must beawaiting-approvalfor exactly this approval id and a matching kind (button = tool call, answer = question; sign-in pauses are refused).session.resume()then throws the expectedSessionAwaitingApprovalError, which binds the approval to the session, andagent.approvals.resolve()continues the turn inside it - so the continuation joins the transcript, a replayed click cannot run the tool twice, a click naming another conversation is refused withLOUSHO_APPROVAL_NOT_FOUND, and the next message in the thread continues the same session.onDecisionaudits only decisions that pass the check. Without a checkpoint store nothing can be checked, so the approval store alone decides, as before.#280 - function-form approvers failed closed after restart
ctx.approval(id)asked onlyagent.approvals.list()- the pauses this process made - so a durable pending record a restarted process did not create looked identical to a denial, and functionapproversfailed closed.ApprovalStoregains an optionalload(id)-resolve()without claiming or deleting the record - implemented byInMemoryApprovalStore,StorageServiceApprovalStore,fileStore,SqliteStore.approvals, the KV store and Agent Forge's store (a custom store without it keeps compiling and behaves as before). The newagent.approvals.get(id)reads through it (and normalizes question records viadescribeApproval), andctx.approval()falls back to it, so the approver function sees the recorded request (toolName,input,sessionId,principal) after a restart and fails closed only when no store knows the id.Tests
channels.test.ts): restarted click appends the continuation to the transcript and the next message continues the session; click in a conversation waiting on another approval, in one with nothing pending, a replay and a concurrent double-click are refused (404) without touching the approval; a button decision on a pendingask_questionis refused; an in-process click naming another conversation is refused;ctx.approval(id)answers from the durable store after a restart and isundefinedonce decided./approvecommand) after a restart appends the continuation to the transcript; Slack also covers a replayed click (tool runs once) and a functionapproversdeciding after a restart.createAgentApprovals.test.ts:agent.approvals.get()reads the durable store and stops answering once resolved.storeContracts.ts):load()reads a saved approval without resolving it and returns null after resolution.Validation
npm run build,npm run typecheck,npm run lint,npm run docs:llms, andnpx vitest run(4243 passed; the only 5 failures are pre-existing environmental ones inpackages/create-lousho-agent, which needs its owndist/build this worktree lacks).