pending

package
v0.2.49 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 24, 2026 License: GPL-2.0, GPL-3.0 Imports: 6 Imported by: 0

Documentation

Overview

Package pending implements the staging store for Supervised mode.

When a chat has SupervisedMode=true, the fs handler doesn't apply file writes immediately. It registers a pending op in this store and blocks on the op's resume channel until the user resolves it via the UI. The UI posts resolve_pending_change / resolve_all_pending_changes (see hub/command_pending.go); that resolution closes the channel with an accept/reject signal, the fs handler unblocks, and the write either applies or returns an error to the agent.

Design points:

  • In-memory only. Pending ops don't survive a server restart because the blocked fs handlers are bound to the live ACP session; if the process dies, the kiro-cli turn is already lost and restoring pending state would point at callers that no longer exist.

  • Per-chat FIFO. Ops for a chat are tracked in insertion order so the UI can render "3 pending · oldest to newest" without re-sorting. The ordering also makes "accept all" predictable: files get applied in the order kiro-cli emitted them, so a create-then-edit sequence on the same path applies cleanly.

  • Concurrent writes to the same path are refused. If op A is already pending for path P and kiro-cli emits op B for P, B's Add returns ErrPathBusy and the fs handler returns an error to the agent without staging. Simpler than chained-op semantics and matches the "pause the agent" intent — the agent should wait on A to settle before queueing B.

  • Idempotent resolution. Resolving a non-existent or already- resolved op returns ErrUnknown (caller maps to 404/400); it never panics or double-delivers.

  • Text caps. OldText/NewText are stored verbatim because the SSE payload needs them for the inline diff. Callers MUST truncate above Cap (4 MiB per side) and set the Truncated flag so the UI can fetch full content from disk on demand.

Index

Constants

View Source
const (
	KindCreate = api.PendingKindCreate
	KindEdit   = api.PendingKindEdit
	KindDelete = api.PendingKindDelete
)

Kind constants — aliases for the canonical api.PendingChangeKind values.

View Source
const (
	ActionAccept = api.PendingActionAccept
	ActionReject = api.PendingActionReject
)

ActionAccept and ActionReject are retained as typed constants for callers that need the api.PendingAction value without importing api directly. Resolve now accepts api.PendingAction, making invalid actions a compile error.

View Source
const Cap = 4 << 20

Cap is the canonical per-side text cap for OldText/NewText in a pending op (4 MiB). This is the authoritative constant for the maximum file-write payload size across the application — both the Supervised staging path (this package) and the unstaged fs handler (hub/bridge_fs.go's fsWriteCap) MUST use this value so a staged write never holds more bytes than the unstaged path would accept. Callers that receive oversized content MUST truncate (marking Truncated=true) or reject; the store doesn't enforce.

View Source
const MaxPendingPerChat = 256

MaxPendingPerChat caps the number of staged ops per chat. A misbehaving agent that emits thousands of fs/write_text_file calls during a Supervised-mode turn would otherwise pin up to ~8 MiB per op (OldText + NewText at Cap each) × N ops in heap memory. 256 is generous for realistic supervised turns (typically <20 ops) and bounds the worst-case heap at ~2 GiB per chat.

View Source
const MaxPendingTotal = 2048

MaxPendingTotal caps the total number of staged ops across every chat. Belt-and-braces for the case where one quiet chat is under the per-chat cap but ten concurrent chats each accumulate a handful. Bounds worst-case heap at ~16 GiB across the process.

Variables

View Source
var (
	// ErrUnknown is returned by Resolve when the tool call id has
	// no pending op (never staged, or already settled).
	ErrUnknown = errors.New("pending: unknown tool call id")
	// ErrPathBusy is returned by Add when the chat already has a
	// pending op for the same path. The fs handler surfaces this to
	// the agent as a rejection, which is the cleanest signal; the
	// alternative (queueing) would require speculative application
	// order and is more work than the problem justifies.
	ErrPathBusy = errors.New("pending: path already staged")
	// ErrBadKind is returned when Add gets something other than
	// KindCreate / KindEdit / KindDelete.
	ErrBadKind = errors.New("pending: kind must be create, edit, or delete")
	// ErrEmptyID is returned when Add gets a blank ToolCallID.
	ErrEmptyID = errors.New("pending: tool_call_id is required")
	// ErrEmptyChatID is returned when Add gets a blank ChatID.
	ErrEmptyChatID = errors.New("pending: chat_id is required")
	// ErrEmptyPath is returned when Add gets a blank Path.
	ErrEmptyPath = errors.New("pending: path is required")
	// ErrTooManyPending is returned when the per-chat or total
	// pending-op caps are exhausted. The fs handler surfaces this
	// to the agent as a tool failure; the agent should wait for
	// existing ops to resolve before retrying.
	ErrTooManyPending = errors.New("pending: too many pending changes")
	// ErrMergeNotApplicable is returned by ResolveWithText when the op
	// kind is KindDelete. Merged text applies to edits AND creates (a
	// create's proposed content can be partially accepted hunk by hunk,
	// including down to an empty result); accepting a delete with a
	// merged body would resurrect content the delete was meant to
	// remove. See resolve.go for the enforced contract.
	ErrMergeNotApplicable = errors.New("pending: merged_text not applicable to delete ops")
)

Errors returned by the store. Callers map these to HTTP status codes in command_pending.go.

Functions

This section is empty.

Types

type AddParams

type AddParams struct {
	ToolCallID string
	ChatID     api.ChatID
	Path       string
	Kind       Kind
	OldText    string
	NewText    string
	Truncated  bool
}

AddParams is the caller's input to Add. Separate from op so the store controls the resume channel and timestamp.

type Kind

type Kind = api.PendingChangeKind

Kind is an alias for api.PendingChangeKind, establishing a single source of truth for pending-change kinds. The api package owns the canonical type; this alias avoids a mass rename of internal callers.

type Resolution

type Resolution struct {
	MergedText string
	Accepted   bool
	Merged     bool
}

Resolution is the outcome of a pending op after the resume channel closes. Returned atomically by the readResolution closure from Add, eliminating the need for separate MergedText/ClearMergedText calls.

Merged distinguishes a user-authored partial merge (via ResolveWithText) from a plain accept. It is the flag callers gate the write-override on — NOT MergedText != "" — so a partial merge that resolves to an empty file (e.g. a create whose only hunk was rejected) still overrides the agent's content with the empty result instead of silently writing the agent's original text.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store is the staging registry. One Store instance serves every chat in the hub.

func New

func New() *Store

New returns a ready-to-use Store.

func (*Store) AcceptAllForChat

func (s *Store) AcceptAllForChat(chatID api.ChatID) []api.PendingChange

AcceptAllForChat accepts every pending op for the chat in a single lock acquisition.

func (*Store) Add

func (s *Store) Add(ctx context.Context, p *AddParams) (waitCh <-chan struct{}, readResolution func() Resolution, err error)

Add stages a new pending op and returns the blocking channel the fs handler must read to learn the decision. The channel is closed when the user resolves (accept or reject), when the op is flushed via RejectAllForChat, or when ctx is cancelled. The returned readResolution func reports the final decision (including any merged text) after the channel has closed; reads take the store mutex so the mutex provides the happens-before barrier.

If ctx is cancelled before the op is resolved, the store auto-rejects the op (accepted=false) and closes the resume channel. Callers that don't need cancellation should pass context.Background().

Returns ErrPathBusy when the chat already has a pending op for the same path; callers should return an error to the agent in that case (no staging, no block).

Returns ErrTooManyPending when either the per-chat or total pending-op caps are exhausted.

func (*Store) ChatIDs

func (s *Store) ChatIDs() []api.ChatID

ChatIDs returns the set of chat IDs that currently have at least one pending op. O(1) bounded by pending ops (not total chats). Used by the hub's replay path to avoid scanning the entire chat directory.

func (*Store) CountForChat

func (s *Store) CountForChat(chatID api.ChatID) int

CountForChat returns the number of pending ops for the chat. Cheap O(1) read used by tests; not part of the hot path.

func (*Store) Get

func (s *Store) Get(toolCallID string) (api.PendingChange, bool)

Get returns the snapshot of one op, or ok=false if it's not pending. Used by the editor's pending-diff virtual path endpoint.

func (*Store) ListForChat

func (s *Store) ListForChat(chatID api.ChatID) []api.PendingChange

ListForChat returns snapshots of every pending op for the chat in insertion order. Used by SSE reconnect replay. Safe to call from any goroutine.

func (*Store) RejectAllForChat

func (s *Store) RejectAllForChat(chatID api.ChatID) []api.PendingChange

RejectAllForChat flushes every pending op for the chat, closing each resume channel with accepted=false.

func (*Store) Resolve

func (s *Store) Resolve(_ context.Context, chatID api.ChatID, toolCallID string, action api.PendingAction) (api.PendingChange, error)

Resolve settles a single op. chatID scopes the resolution: an op whose ChatID differs from chatID is treated as unknown, so a resolve command carrying a mismatched chat_id can never settle another chat's op. Returns ErrUnknown if the id isn't pending. Idempotent: a second Resolve for a closed op returns ErrUnknown (the op is gone by then). On success, returns the op's snapshot so the hub can broadcast the resolved event with the original path/kind (the caller may not have them handy).

func (*Store) ResolveWithText

func (s *Store) ResolveWithText(_ context.Context, chatID api.ChatID, toolCallID, mergedText string) (api.PendingChange, error)

ResolveWithText settles an op with accept semantics BUT overrides the agent's proposed NewText with caller-supplied merged content. Used by per-hunk Accept/Reject flows where the client computes a partial merge from OldText + only the accepted hunks' NewText.

chatID scopes the resolution: if the op's ChatID differs from chatID it is treated as unknown (ErrUnknown), so a resolve carrying a mismatched chat_id can never settle another chat's op. This is defense-in-depth alongside the chat-unique op keys.

KindDelete ops refuse merged text: there is no file content to override — the user-intended action is either "accept the delete" or "reject the delete". Accepting a delete with merged content would turn a deletion into a caller-controlled write, which is a data-integrity bug. KindCreate and KindEdit are both allowed; KindCreate means "the agent wanted to create this file with X; the user says write Y instead."

The merged text is stored on the op struct and returned atomically via the Resolution closure from Add, with Merged=true so the caller gates the write-override on the flag rather than on non-empty text (an empty merge must still override the agent's proposal). No separate cleanup step is needed — the Resolution read is the single retrieval point.

Returns ErrUnknown if the id isn't pending. Merged text must be under Cap bytes; callers validate before invoking. Accept semantics are implicit — to reject everything, use plain Resolve(…, "reject").

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL