Personal skill learning
Ownership and eligibility
Core offers automatic review only after a clean, completed human request in the account's own web or CLI conversation with autoLearnSkills enabled. Scheduled, automated, internal, shared/channel and delegated turns are excluded, as are failed, aborted, cut-off, steered or compacted exchanges. Proven successful direct and nested tool calls count; pending, failed and denied calls do not. Successfully completed SkillLoad calls supply loadedSkillNames, separately from the count.
The owner runner applies eligibility and the per-exchange minimum. settleTurn in src/brain/session/turnSettled.ts starts the effect detached, so settlement never waits for it, and aborts its signal after 120 seconds. The registry Skills plugin owns account serialization, cadence, the directive, JSON validation, personal skill writes and provenance. There is no MemoryCurator contribution.
skillLearningAllowed in src/plugins/skillLearningAccess.ts is the shared availability predicate for the runner and /auth/me/cli-settings. The live reviewer, reviewer plugin grant, CreateSkill tool and plugin grants and positive write budget are required, including for administrators. The fresh authorize() callback also checks account existence, opt-in, the exact registration and the daemon's live admitsNewWork flag before spending or writing. Shutdown/pause drain therefore forbids subsequent writes even before the abort signal is observed. No reviewer or skillReviewMaxOps=0 means no request.
Version 2 reviewer seam
The sole registration is registerSkillPostTurnReviewer({ contractVersion: 2, run, status }). Core explicitly refuses older registrations with a warning; it does not adapt their inference client. Only one reviewer may register, and a second is refused. The seam catalog and minimal JSON example describe the plugin contract.
run receives userId, successfulToolCalls: number, loadedSkillNames: readonly string[], budget: number, reviewEveryToolCalls: number, signal: AbortSignal, authorize: () => Promise<boolean>, currentBudget: () => Promise<number> and review: (directive: string) => Promise<SkillReviewResult>. Core supplies no reduced user/assistant text, argument summary, trajectory or helper inference client. The host-owned review closure admits at most one attempt for this exact settled source. The plugin cannot select a session, provider, model, connection or tools.
The result is either { status: 'completed', text: string } or { status: 'skipped', reason: SkillReviewSkipReason }. Reasons are exactly quota, unknown-quota, unsupported-route, hosted-tools, stale-source, runner-unavailable and revoked. A skipped attempt has no provider request and no writes; it is not an empty successful proposal. Typed skips are pre-dispatch policy/availability outcomes. Request errors and genuine AbortSignal cancellation reject, rather than being converted into skipped success; attempted failure consumes cadence and cancellation is recorded as aborted, preventing automatic retry on every turn.
Exact source and execution boundary
prepareSkillReviewAttempt in src/brain/service/skillReviewAttempt.ts owns route eligibility and mints the delegated review request, binding the captured source to the existing send, abort and release owners. src/brain/session/skillReviewFork.ts owns immutable source capture, validation, prefix reconstruction and the shared source-shape gate. Its capture entry point accepts only an optional captureSkillReviewSource callback, so session transport does not depend on live-session or delegated dispatch orchestration. The host captures the completed turn's session, journal leaf, last actual provider request and immutable provider state. The shared fork and PromptComposerRuntime seams copy that captured branch through that leaf, rather than reading the parent's later active branch. The child preserves provider/model, endpoint/API, connection/generation, cache identity, system and tool declarations, historical message/tool IDs, ordering and cache markers byte-for-byte.
The semantic suffix is the parent's final output when it was not already request input, followed by one review directive. Anthropic's native encoder may also emit its empty active-effort system frame with the source's unchanged effort settings; this adds no new instructions. The full final answer is retained and may be charged as fresh input. No worker briefing, turn-start recall, skill injection, clock/workdir frame or Scope/Result wrapper is added. Missing exact transient source, divergent reconstruction or unavailable captured credentials causes an explicit skip. There is no fresh-prompt, alternate-account or helper-model fallback.
Tool declarations stay in the request to preserve its prefix. Host execution denies every direct or nested tool before permission prompts, sandbox admission or side effects. A tool attempt invalidates the review and ends it without another paid model round. A provider-hosted tool that can execute without a host callback makes the source ineligible with hosted-tools.
BrainService.withDelegatedExecution supplies the execution-target preparation shared with ordinary delegation. For a captured local-computer scope it holds the account's live connection through withLocalExecution, resolves localTarget through Sandbox and includes that metadata in the runner request. The binding is released after success or failure; a lost connection aborts the child. This runs only when cadence and source/quota gates reach dispatch. Review still bypasses the parent settlement fence, while the runner retains its strict local-target validation and the review retains its tool-execution denial. Host and managed scopes need no local connection.
Review inference runs only in an opportunistically admitted runner. A disabled, full, unavailable or incompatible pool returns runner-unavailable; it does not queue ahead of interactive work or fall back into the daemon. Plugin counter/file operations stay under authoritative account serialization. The child has its own session, journal, activity, transport and mutable provider state. It cannot change parent chain IDs, affinity, cache markers, active leaf, tool activation, memory delivery or compaction state. Its directive and result are not appended to the parent and produce no title inference, notification, parent wake-up, curator or recursive skill review. Abort and shutdown cancel the child; interrupted review is not replayed after restart. See turn isolation and runner admission.
Cadence, live limits and writes
Runtime policy has three existing controls:
skillReviewEveryToolCalls: default 100, range 1–200, accumulated across eligible exchanges per account.skillReviewMinToolCalls: default 1, range 1–200, applied to each exchange before it counts.skillReviewMaxOps: default 1, range 0–6, proposed write budget; 0 disables automatic learning.
Explicit stored overrides survive the changed default. No migration rewrites an operator's saved interval. At the default, 99 eligible successful calls from zero cause no request; the hundredth offers at most one review after settlement. No child exists below the cadence. Skips retain pending cadence at the threshold and retry only from a later eligible exchange's new source, without a timer or replay of a cold prefix.
The plugin awaits authorize() in its account queue, adds the count and supplies only a JSON directive with a bounded visible catalog and permitted existing bodies. Before every create or patch, it awaits currentBudget() and live authority, then checks abort again; total writes may not exceed the captured budget or a newly lowered current budget. Completed writes still commit and reload if a later write is refused.
Writes use the existing file owner through files.reviewerStore, with strict proposal parsing, size/secret guards, name collision checks and expected-revision CAS. Creation makes an ordinary personal skill marked created_by: agent. Patching requires that personal agent-authored skill to have been successfully loaded in this exchange and remain unedited. Whole history can support a new procedure but grants no additional patch authority. Human edits, including automatic-discovery changes, clear agent provenance. Bundled, instance-wide, foreign and manually written/edited skills cannot be patched.
Subscription quota and accounting
Before admission and again before provider dispatch, the live skillReviewQuota host callback evaluates the pinned source connection. prepareSkillReviewAttempt performs admission; the spawner forwards the same callback into the session factory's provider-dispatch boundary. It reads current() on the usage repository's quota store (repositories.usage.quota, UsageRepositoryQuota) with the pinned { providerId, connectionId, generation } identity, through the existing authorized-dispatch usage dependency and without provider polling. The pure skillReviewQuotaReason helper evaluates OAuth usage only. A missing connection or changed generation is revoked.
For an OAuth subscription, the weekly window has no kind and exactly windowMinutes=10080. Its resetsAt is Unix seconds and is compared with now / 1000; fetchedAt and freshness remain milliseconds. usedPercent >= 90 returns quota. Missing, non-finite, future-dated, expired or stale evidence returns unknown-quota, using USAGE_COMPARABLE_MS for freshness. A stale reading already at or above 90% remains a ban while its reset is in the future. An expired reset requires fresh evidence. Unknown usage is never interpreted as zero or averaged across connections. API-key routes have no subscription-weekly guard; normal spend admission and grants still apply. See provider review guard.
The author pays review inference under explicit origin internal. The existing delegated request recorder charges it exactly once and retains the actual connection in provider-request and provider-day usage. Do not wrap an already charged child in a second origin-usage scope.
Cost and rollout
A cache hit still charges reading the full prefix. Cost includes cache reads/writes, fresh final-answer/directive/catalog input and review output at the pinned model's actual rates. Increasing the default to 100 reduces repeated full-context reads; it is not a monetary cap. Rollout measurements must use the real suffix and output, captured provider usage and prices, rather than treating estimated lengths as observed cost.
skillReviewRouteSkipReason delegates to the same skillReviewSourceSkipReason used at capture. Eligibility requires a supported exact source shape and host-controlled tools, without a separate measured-route allowlist. Supported APIs are anthropic-messages, openai-codex-responses, openai-responses and openai-completions. Responses APIs extend input; Anthropic and Completions extend messages, with Completions system instructions inside that history. Codex still requires its non-empty captured prompt_cache_key. Stateful sources with previous_response_id and APIs without a supported shape return unsupported-route; hosted tools return hosted-tools.
Reconstruction compares the inherited history and stable fields before appending the encoder's final-answer/directive suffix. Responses validates model, instructions and tools; Completions validates model and tools; Anthropic and Codex retain their existing model, instructions, system and tools checks. Every non-history field is copied exactly from the parent's actual wire body, including prompt_cache_key, retention, reasoning, sampling and provider metadata. Local child transport identity cannot change cache routing. New Responses and Completions encodings are compared as JSON, so PI's optional undefined fields match their absent wire representation. Only Codex's two leading developer IDs and native Anthropic cache markers are rebound. Completions' Anthropic cache mode also restores the last message's single text-block wrapper and marker, which PI moves when appending input. Changed content, reasoning signatures or historical IDs still return stale-source.
Real PI encoder tests cover inherited wire bytes, all non-history fields, signed reasoning and IDs, cache-marker movement, the exact suffix and runner serialization without network calls. The current provider registrations and OAuth catalogs use these four APIs. Azure endpoints use the ordinary openai-responses registration; standalone azure-openai-responses, google-generative-ai and mistral-conversations are not reachable through the current provider configuration and have no review source adapter.
The controlled Codex measurement retained an identical inherited wire prefix and next-parent request during overlapping review. Each child read 2816 of 2937 source tokens from cache, about 95.88% of the parent prefix; its total input was 2964 tokens, of which 95.01% was cached. The remaining 121 inherited tokens and 27 final-answer/directive tokens were uncached. This is measured cache reuse, not a full-prefix-cache or universal remote eviction guarantee. These observations do not establish remote cache reuse on other providers. Stateful Responses delta requests remain unsupported because a delta is not a complete inherited prefix; Codex evidence does not establish their chain behavior.
The controlled Anthropic measurement read 7921 of 7925 parent-prefix tokens from cache, about 99.95%; 4 inherited tokens remained uncached and 32 final-answer/directive/native-frame tokens were added. Native next-parent encoding was byte-identical to control while the first actual child delta was paused. The two-call limit covered warm and child inference; the parent-next payload was intercepted before network, so remote parent-next completion or cache eviction was not measured. Hosted-tool sources remain ineligible on this API.
Core and Skills must ship as one compatible release set. The Skills manifest's requiresCore must name the actual first released core containing version 2, assigned by the release mechanism. Do not preassign a candidate patch in documentation. Older reviewer registration fails closed in new core; the new plugin must be refused by old core's manifest check. Setting skillReviewMaxOps=0 disables learning without restoring the removed helper path.
Review status
The plugin stores pending calls, runs, saved, rejected, failed and the last attempted review atomically beside its per-account counter. A malformed state fails loudly. A skip retains cadence and does not count as a completed empty review. Actual attempts produce one local outcome log and status entry; aborted attempts count as failures. The plugin's .skill-reviewer/<userId>.json holds totals and only the latest attempt, not a run history. last.error contains the bounded, secret-redacted failure reason; older reasons are available in daemon outcome logs.
status(userId) remains a cheap synchronous local read returning SkillReviewStatus. Core reads and validates only the signed-in account's status while skill learning is available, adds reviewEveryToolCalls and serves skillReview through /auth/me/cli-settings. An exception or invalid answer omits only that field with a local warning. Account → Memory shows the last outcome, totals and remaining calls, but does not render last.error; the reason remains available in the authenticated settings response and plugin state. Availability of the preference does not promise that the current request has a supported source shape or that a review will run.