* revert: roll back workspace trust-boundary hardening
Roll back the hardening added in #3964 that guarded against malicious
content inside an already-trusted repository:
- background git invocations no longer override repo-local git config
(fsmonitor, hooksPath, filters, drivers, gpg, editor)
- agent file tools no longer re-resolve paths through symlinks
- project-local local.toml applies without workspace-trust gating,
additional_dir entries are no longer rejected for resolving to the
home directory or filesystem root, and writing it is auto-approved
again inside a git worktree
Kept, since they only affect the pre-trust window or the trust prompt
itself: the fail-closed trust prompt and lazy footer git detection.
* Update revert-trust-boundary-hardening.md
Signed-off-by: 7Sageer <sag77r@hotmail.com>
---------
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* fix(remote-control): pick credential slot and relay from login region
Global login stores tokens in kimi-code-env-*.json and should use
code-rc.kimi.ai; Remote Control always read kimi-code.json and
code-rc.kimi.com. Resolve both from the persisted oauth ref.
* fix(telemetry): skip the dead global collector and keep shutdown alive
The global collector host has no DNS. Drop it from the region profile and
skip the send instead of falling back to the mainland endpoint. Keep the
shutdown timeout timer referenced so a failed flush cannot exit before the
TUI hands the process to the foreground server.
* fix(remote-control): require explicit relay origin and drop hardcoded .com defaults
* feat(agent-core-v2): add repeat_breaker config switch
Add a top-level repeat_breaker boolean (default true) to config.toml,
overridable with the KIMI_CODE_REPEAT_BREAKER env var. When disabled,
the agent skips repeat reminders and forced stops on consecutively
repeated identical tool calls; same-step dedupe is unaffected.
* Update repeat-breaker-config-switch.md
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* feat(agent-core-v2): add tool_dedupe config switch
* refactor(agent-core-v2): drop the tool_dedupe config switch
Same-step dedupe stays always on. repeat_breaker remains the only public switch.
* refactor(agent-core-v2): read the repeat breaker from the environment only
KIMI_CODE_REPEAT_BREAKER is the only switch. Unset keeps reminders and forced stops on.
---------
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* fix: restore swarm members from persisted lifecycle events
* test(agent-core-v2): register subagent lifecycle records in the wire vocabulary
* chore(agent-core-v2): regenerate the wire manifest for subagent lifecycle records
The manifest inventory drives the vis wire-renderer coverage check, so vis gains a renderer for each new record type and treats them as timeline-neutral in the context projector.
* fix(transcript): associate a folded subagent with the task registered after its spawn
Some emitters register the task after publishing the spawn, so the durable
spawn record carries no task id and the cold fold keyed the member by its
agent id. A later task.started then created a second task that no subagent
terminal event ever reached, leaving a duplicate stuck in the running state.
Folding task.started now records the agent-to-task association the live
projector keeps, and terminal subagent records update both representations
when both are retained.
* fix(transcript): restore subagent token usage when folding a completed member
The durable subagent.completed record carries the member's token usage and the
live projector copies it onto the task, but the cold fold dropped it, so a
reopened conversation showed a completed member without usage metadata. The
fold now validates the payload's four counters and restores them.
* fix(kap-server): base the lost-member decision on the turn that spawned it
The cold snapshot only terminalised an unfinished foreground member when the
parent agent was idle, so a member left behind by a restart stayed marked as
running forever once the user started a new parent turn before backfill.
The restored agent refs already name the tool call that spawned each member,
so the decision now uses the turn owning that call: a member is lost when its
own loop is not running and its spawning turn is not the turn the parent is
running now.
* fix(transcript): adopt the spawn placeholder into the task registered afterwards
A spawn published before its task is registered — the Tower worker path —
carries no task id, so the fold created a placeholder keyed by the agent id
and the later task.started added a second task under the registered id. Only
the registered task was ever terminalised, and the detached placeholder was
skipped by the lost-member pass, so a restored transcript kept a duplicate
worker shown as running forever. task.started now folds the placeholder into
the registered task instead of leaving it behind.
* fix(kap-server): drain the live wire before reading a cold snapshot
The append log buffers records and writes them in batches, which is why its
own reader flushes the log first. The cold transcript reads the wire file
directly and skipped that drain, so a snapshot could stop on a stale prefix.
When the transcript binding is created after a turn has already ended, the
whole store is built from that snapshot and nothing repairs it afterwards,
leaving the turn's last tool call without its result and its member marked
lost. Reading now drains the live agent's wire first, as session export
already does before it reads the same files.
* feat(tower): enforce protocol invariants and deny AgentSwarm in tower mode
- split missions by functional boundary and workload; simplify spawn briefings
- flip completed missions back to active on non-clean reviews and brief respawns with review history
- refuse completed with open tasks or zero-diff build branches; cap review rounds at 5 with a task_drop escape hatch
- name the next step in review/merge/status tool results; flag unspawned missions in TowerStatus
- veto AgentSwarm execution while tower mode is active
- deflake kap-server catalog test: waitForServerState timeout 3s to 10s
* fix(spawnTool): remove striction
---------
Co-authored-by: konghuanjun <konghuanjun@moonshot.ai>
* feat(cli): support audience targeting for startup banners
* feat(cli): rename the banner audience field to kfc_audience
* chore: revert unintended oxlint autofixes introduced by the merge commit
The lint-staged oxlint --fix pass applied warning-level unicorn autofixes to
main's files during the merge (prefer-at, prefer-string-replace-all,
toSorted, catch-error-name). The catch-error-name rename also renamed object
shorthand properties (reason -> error), which no longer matches the declared
types and fails tsc. Restore main's versions of these four files; committing
with --no-verify so the same autofix does not re-apply.
* refactor(cli): type the banner wire shape concretely
* refactor(cli): narrow the banner audience fields to literal unions
* fix(cli): classify 403 as anonymous and map REGION_OVERSEA in banner audience
* refactor(cli): require banner_id for banner entries
* fix(cli): drop the banner subtext indent when it cannot fit
* fix(cli): avoid cache filename collisions across config endpoints
* fix(cli): keep banner loading from rejecting when the token lookup fails
* fix(kap-server): reject malformed and null Origins
isOriginAllowed conflated an unparseable Origin header with an absent
one and allowed both, so a sandboxed document (Origin: null) passed the
WebSocket upgrade check and had its Origin echoed by the CORS hook.
An absent Origin is still allowed for non-browser clients, but a
present Origin that fails to parse, is null, or uses a non-http(s)
scheme is now rejected: the WS upgrade responds 403 and the CORS hook
withholds Access-Control-Allow-Origin.
* fix(kimi-code): sync web bundle for kimi_origin query gating
* fix(agent-core-v2): gate workspace local.toml behind trust and tighten additional_dir
* fix(agent-core-v2): block symlink escapes in agent file tools
* fix: harden background git invocations against repo-local config
* fix(agent-core-v2): close trust-gate gaps in add-dir and additional_dir validation
* fix: neutralize repo-configured clean and process filters in hardened git calls
* fix(agent-core-v2): fail closed when a path is too deep to verify
* fix: probe worktree-scoped filter config in hardened git calls
* fix: fail closed when git filter discovery fails and cache probes for repo subdirectories
* fix(agent-core-v2): block symlink aliases of the project-local config in write tools
* fix: neutralize smudge, merge drivers, and repo-controlled commit signing in hardened git calls
* fix: expand included git configs when discovering repo-defined drivers
* Revert "fix(kimi-code): sync web bundle for kimi_origin query gating"
This reverts commit cd6af49cf1dfebe505f0e890453c3207a74d3e80.
* fix: keep git hardening fail-closed on includes and probe failures in scanners
* fix: fail closed on uncodable driver names and disable repo-controlled signature verification
* fix: neutralize repo-controlled editors and match include sections case-insensitively
* fix: probe config names only and reject dangling symlinks in file tools
* fix: fail closed when core.worktree escapes the repository
* fix(agent-core-v2): match project config paths across separators and case
* fix: arm footer git only after workspace trust persists
* fix(agent-core-v2): check resolved sensitive targets for absolute outside paths
* fix: only skip git config caching for actual include sections
* refactor: share git config hardening primitives between agent-core-v2 and the CLI
* fix(agent-core-v2): clear trusted additional dirs synchronously on trust revocation
* fix(agent-core-v2): compare resolved write targets with Windows path semantics
* fix: fail closed when the git directory disappears during hardening probes
* refactor: move shared git hardening primitives into @moonshot-ai/git-hardening
* fix: close symlinked-gitdir and submodule-recurse gaps in git hardening
* fix(agent-core-v2): reject home-directory ancestors as additional_dir roots
* fix: compare core.worktree with Windows path semantics
* fix: skip git config probes on renders after a negative repo detection
* fix: resolve gitfile pointers against the .git file directory and match includeIf sections
* test: drop duplicated symlink and plain-file cases covered elsewhere
* fix: only treat project-config writes as aliased when the lexical path misses the config pattern
* test: move the realpath guard matrix into a module test
* test: cover git-hardening primitives in the shared package
* refactor: inline git hardening helpers and drop the shared package
Keep the constants and parsers next to the async engine spawn path and
the sync CLI spawn path instead of a third workspace package.
* refactor: drop footer git trust gate and config probing
Workspace trust is a process-level decision, so the TUI never mounts
the footer until the user has trusted. Footer git status now uses
static hardening flags only.
* refactor: route git execution through IGitService.runGit
- Add runGit(cwd, args, options) to IGitService as the public door for
running git with repo-local config hardening applied.
- GitService implements runGit using the existing hardenedGitConfigArgs
helper; status/diff now delegate to it.
- collectGitContext drops its own spawn/probe logic and calls
IGitService.runGit instead, receiving the service through the
promptPrefix context.
- Tower keeps its own git() function (accepted gap).
* refactor: move git hardening helpers to app/git/hardening
* Revert "fix(kap-server): reject malformed and null Origins"
This reverts commit b799a21d0390ff0e4e66437f0d7b90c0b8171efe.
* perf: cut CLI startup time and memory in the native binary and config reads
Ship the single-file binary with a V8 code cache and a 16 MB semi-space
(execArgv), lower dynamic import() to require() in the CommonJS bundle and
fail the native build if any survives, and stop the engine from redoing
work on every config read: env-bound sections are re-validated only when
their env values change, the tool policy service caches the global tools
section and refreshes it on section-change events, Ajv instances are built
lazily, the macOS product version is read from SystemVersion.plist instead
of spawning sw_vers, and the host time zone and date formatter are cached.
Measured on the 100-round mock benchmark: CU 4.1-4.4 -> 3.0-3.2,
CPU 2.6-2.8 -> 2.0-2.2 core-seconds, peak RSS 550-610 MB -> 370-465 MB,
`kimi --version` 0.48 s / 401 MB -> 0.33 s / 311 MB.
* fix(native): keep dynamic import() in the code-cached binary and cover plugin entries in smoke
Lowering import() to require() broke `__plugin_run_node` (the bundler dropped
the node:url binding, and require() cannot load file:// URLs or top-level-await
modules), so every plugin MCP server that runs on Node failed under the native
binary. The CommonJS SEA main script keeps a working import() with the V8 code
cache on Node 24, so the lowering and the lexer guard go away and the native
smoke now runs a plugin entry. Also assert that tool policy checks do not read
the tools section, and drop the host time zone cache.
* fix(agent-core-v2): key the env-bound config memo by section identity
A domain re-registered by a different section definition could hit the memo
entry left by the previous definition when the validated base and env values
were unchanged, returning a value parsed by the old schema.
* feat(agent-core-v2): report active plugins and plugin toggles in telemetry
* fix(agent-core-v2): refresh active plugin context before recording plugin_toggle
* refactor(agent-core-v2): publish plugin telemetry from committed state
* refactor(agent-core-v2): split plugin value import from type imports
* refactor(agent-core-v2): report enabled plugins on turn and toggle events
* feat(transcript): include step timing and usage in cold-folded steps
* refactor(transcript): carry only TTFT and stream duration in cold-folded steps
* refactor(transcript): rename step timing field to llmTiming
* fix(agent-core-v2): floor the human turn clock at the wire-wide max on engine journal reset
* fix(kap-server): fold the cold transcript snapshot over the restorable branch chain
* fix(agent-core-v2): advance the human turn clock when a turn starts
* fix(kap-server): split reused wire turn ids in the live transcript projector
* fix(kap-server): adopt the completed cold tip for mid-turn attach events
Continuation deltas after a lazy transcript attach belong on the last
cold turn, not a newly split export id.
This reverts 64505e36e3.
The v1 WS + legacy REST + transcript surfaces are alive on main and remain
the single protocol surface; kimi-inspect returns to the transcript-based
data model (keeping #3747's removal of the prompt input, which adapted to
the agent-core-v2 prompt-queue fold).
* fix(agent-core-v2): reuse the queued prompt id for a single steer
A follow-up sent during a turn minted a second context id, so cancel
while the model was still requesting could show the same text twice
and make the host prompt un-undoable. Reuse the queued prompt id when
one prompt is steered, mark that user as in-turn so undo targets the
host turn, and project it as a non-anchor origin for the client.
* fix(agent-core-v2): type in-turn steer origin on the human prompt
The human PromptOrigin type rejected the inTurn field, which failed
typecheck/build and left the generated state manifest stale.
Reserve the context message id when a prompt is steered, emit turn.steer only after that message is appended, and join live/cold transcript plus fork by the id instead of content.
* fix(agent-core-v2): avoid root watch inventory and add a kill switch
Watch concrete candidate paths so a flooded project root cannot fill the V8 heap. Allow disabling every watcher with [watch] enabled or KIMI_CODE_WATCH.
* fix(agent-core-v2): stop watching the whole .kimi-code directory
Watch missing config files on the file path itself so session writes do not reload.
* fix(agent-core-v2): keep session deletion from hanging
Deleting a live session closes it first, and three waits on that path
had no upper bound:
- AgentLifecycleService.remove() checked its 3s quiesce deadline only
after awaiting loop.settled() and the compaction promise, so a loop
that never settled after cancel() blocked agent removal, session
close and delete forever. The settle wait now races the deadline and
falls through to the existing forced-teardown path when it expires.
- HostProcess.kill() on Windows waited for taskkill to exit with no
bound. It now stops waiting after 5s.
- setSessionArchived() awaited a pending resume inside its lifecycle
critical section. A resume queued behind the archive then waited for
the archive while the archive waited for it, wedging the session's
lifecycle chain so that every later operation on the session,
delete included, never started. The resume is now settled before
the critical section is entered.
* fix(agent-core-v2): terminate a taskkill that outlives its timeout
When the 5s wait on taskkill timed out, kill() returned but the spawned
taskkill child stayed alive and referenced, so every hang leaked a
taskkill.exe process and its handle could keep the CLI or server
process from exiting. Keep the handle and, when the timeout wins, unref
it and terminate it.
* fix(agent-core-v2): never apply a compaction summary that arrives after abort
Cancelling a full compaction, or removing its agent, aborts the
compaction's signal, and the abort listener records the cancellation
and detaches the compaction right away. compactionRound() never checked
the signal again after the summary request returned, so a provider
request that finished despite the abort still went on to
applyCompaction(). That rewrote the context of a cancelled compaction,
and since agent removal now stops waiting for the compaction at its
quiesce deadline, it could also write records after the agent was torn
down and recreate files under a deleted session directory. Check the
signal after the last await and before applying the summary.
* feat(agent-core-v2): support api_key_env for provider credentials
Providers can declare api_key_env = "VAR" in config.toml to read their
Bearer token from an environment variable instead of storing it inline.
The value is resolved per request (rotation-friendly); an unset or empty
variable fails with config.invalid naming the provider and variable.
apiKey+apiKeyEnv and apiKeyEnv+oauth are rejected as conflicts.
The /models refresh path resolves the same chain (inline apiKey ->
apiKeyEnv -> env sub-table); a provider whose variable is unset lands in
failed[] without affecting others, and open-platform providers keep the
env declaration instead of persisting the resolved secret.
Custom registries: applyCustomRegistryProvider consumes the api.json env
field — imported providers get apiKeyEnv, the legacy inline apiKey is
dropped, and a hand-edited apiKeyEnv survives refreshes (tracked via
source.envKey). kimi provider add --api-key and the TUI registry token
field are now optional so public registries import without a key.
* docs: document api_key_env provider credentials
* fix(oauth): preserve hand-edited apiKeyEnv on registry refresh and address review nits
* fix(oauth): validate apiKeyEnv conflicts during refresh and address review nits
* fix(oauth): mirror apiKeyEnv in the klient provider contract and harden refresh validation
* fix(oauth): stop auto-binding registry env names to provider credentials
* fix(oauth): preserve hand-edited apiKeyEnv across registry re-imports
* fix(oauth): scope apiKeyEnv preservation to the owning registry
* fix(agent-core-v2): preserve apiKeyEnv across models.dev catalog re-imports
* fix(kap-server): drop apiKeyEnv when the provider update route receives a replacement key
* fix(agent-core-v2): replace provider credential fields on TOML writeback
* fix(kap-server): normalize whitespace-only api_key in the provider update route
* refactor(oauth): unify provider credential rules into shared primitives
A new provider-credential module in the oauth package owns the credential
precedence chain, the mutual-exclusion conflicts, the user-facing messages,
and the write-side tri-state reconciliation; the refresh orchestrator,
agent-core-v2 (model-auth, catalog, auth readiness), and the kap-server
provider routes compose it instead of re-deriving the rules per layer.
applyOpenPlatformConfig now takes credential: {apiKey}|{apiKeyEnv} and
merges onto the existing record, so open-platform refreshes no longer drop
hand-written fields (customHeaders, the env sub-table) on the inline-key
path either, and the refresh's write-then-undo block is gone.
Registry imports converge on removeCustomRegistryEntries +
applyCustomRegistryEntries (prior-snapshot provenance): the exported
capture/restore helpers are gone, the CLI persists a re-import with a
single setConfig instead of per-id removeProvider RPCs, and
default_model/default_provider survive a re-import whenever they still
resolve. kap-server's importCustomRegistry keeps the purge-then-rewrite
two-phase replace the config service's raw-merging writeback requires.
kap-server create/replace routes accept api_key_env (tri-state, mutually
exclusive with api_key), and GET /config plus the provider catalog item
expose the binding. A declared-but-unset api_key_env now fails readiness
checks with config.invalid (was auth.token_missing), matching request-time
behavior. Registry 401/403 without a key prints an authentication hint.
* fix(cli): persist registry re-import removals on both harness generations
The v2 harness's setConfig deep-merges per domain (retaining omitted keys
and skipping undefined defaults), so a shrunk re-import left removed
providers and aliases in config.toml and kept a cleared default dangling.
The CLI and TUI registry flows now share persistRegistryImport: on v2 the
removal phase and the rebuilt records go out as two atomic
replaceConfigSections writes (the writeback merges alias fields from raw
TOML, so the purge must be persisted first); on v1 the removeProvider RPC
cascades the removals before one merge write. defaultModel/defaultProvider
snapshots are captured before any removal so the restore works on both
paths.
* fix(oauth): reject oauth before the legacy env fallback and persist cleared thinking on re-import
refreshProviderModels' env sub-table fallback now throws the same
apiKey+oauth conflict the chat path rejects, instead of authenticating
with the fallback key and rewriting the provider to drop oauth.
persistRegistryImport writes the cleared thinking section through the
replacement payload, and drops the v1 fallback branch — registry imports
always persist through replaceConfigSections, so the removal phase no
longer needs per-id removeProvider RPCs.
* chore: consolidate provider credential changesets into one entry
* chore: trim changeset to the api_key_env entry
* fix: harden provider credential status and registry imports
* fix(oauth): expose browser-safe provider credential rules
* fix(node-sdk): replace serialized provider credentials
* fix(oauth): reject credential conflicts during refresh
* fix: harden custom registry re-imports
Reject reserved provider collisions and credential endpoint drift before mutation. Replace registry-owned sections atomically with compare-and-set guards, and carry the replacement contract through klient and node-sdk.
* refactor(registry): centralize custom registry import ownership
Route CLI and TUI imports through the engine via the typed SDK and Klient operation. Keep registry replacement policy inside the engine and preserve credential edits made while fetching the registry.
Cover the shared entry point with real configuration storage and memory/IPC transport tests.
* test: trim redundant api_key_env and registry-import coverage
* fix(cli): reuse the stored registry key on keyless re-import
Both first-party clients encoded "no key given" as an empty string, but
the engine's fallback chain (options.apiKey ?? registryKeyFromExisting)
only reuses the key parked in the provider's source blob when the option
is undefined. A keyless re-import of a private registry therefore went
out without an Authorization header and failed with 401 from the CLI
and the TUI dialog, while the kap-server REST route (which passes
undefined) reused the stored key fine.
Pass undefined from the CLI handler and the TUI import dialog instead
of normalizing to ''. The dialog's result type now marks apiKey
optional, matching the engine contract.
* test: pin CAS rejection and credential-conflict behavior
The coverage trim went past redundancy in three places, leaving the
PR's headline guards untested. Restore the compare-and-set rejection
path (guarded value changed on disk -> retry error, no write) and the
absent-section guard normalization in config.test.ts, the
apiKey+apiKeyEnv / apiKeyEnv+oauth conflict rejections at runtime
resolution (modelAuth) and at the refresh orchestrator (discovery,
with per-provider failure isolation), and the open-platform refresh
through api_key_env proving the resolved secret never lands in the
provider record. Also add the first direct unit tests for the shared
declaredProviderCredential / reconcileProviderCredentialUpdate
primitives.
* refactor(oauth): share reserved provider id rules between import and refresh
* Delete .changeset/registry-import-concurrent-credentials.md
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* test: trim cross-layer duplicate registry and credential coverage
---------
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* fix(agent-core-v2): mark MCP servers needs-auth when a tool call returns 401
* fix(agent-core-v2): cover retried MCP calls with unauthorized handling and recheck entry after close
* fix(agent-core-v2): bind MCP unauthorized reports to the originating client
* fix(agent-core-v2): invalidate rejected MCP OAuth tokens when entering needs-auth
* fix(agent-core-v2): preserve concurrent MCP OAuth grants and recheck entry after credential awaits
* fix(agent-core-v2): flip MCP servers to needs-auth before invalidating rejected tokens
* fix(agent-core-v2): compare-and-clear rejected MCP OAuth tokens and tolerate cleanup failures
* refactor(agent-core-v2): move rejected-grant handling into the MCP OAuth service
* fix(agent-core-v2): compare MCP OAuth grant age with the scheduler clock
* fix(agent-core-v2): reject future timestamps from the concurrent MCP grant window
* fix(agent-core-v2): distinguish concurrent MCP grants from freshly rejected ones
* fix(agent-core-v2): ignore application-level MCP errors in the unauthorized sniff
* test(agent-core-v2): trim MCP unauthorized tests to the core contract
* Delete .changeset/mcp-oauth-clear-if-current.md
Signed-off-by: 7Sageer <sag77r@hotmail.com>
---------
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* feat(agent-core-v2): add skill scopes and mark custom-theme as tui-only
Builtin skills can now declare a scopes whitelist (tui | web); skills
without scopes stay visible everywhere. The value is carried through
SkillSummary, the klient RPC contract, and the kap-server REST wire,
and the TUI slash commands filter by it. HTTP clients (code-app) drop
skills whose scopes exclude their own UI mode, so TUI-only skills like
/custom-theme no longer leak into app/web.
* fix: filter scoped skills from the acp and vscode palettes
ACP slash commands and the VS Code GetSlashCommands handler also
project SkillSummary lists; both now drop skills that carry a scopes
restriction so tui-scoped skills stay out of non-tui clients.
* fix(agent-core-v2): watch Windows 8.3 short paths through their long form
Watching a directory given in 8.3 short form (for example
C:\Users\RUNNER~1\...) makes libuv's Windows file watcher abort the process
with "Assertion failed: !_wcsnicmp(filename, dir, dirlen)" in fs-event.c,
because change notifications arrive under the long path.
On Windows, resolve a watch root containing a short-name segment to its long
form (the nearest existing ancestor when the root does not exist yet) and map
reported changes and ignore checks back to the requested path, so callers that
compare change paths with their own keep working.
* test(agent-core-v2): keep watch tests portable to Windows runners
* test(agent-core-v2): show received events when the Windows short path watch test fails
* test(agent-core-v2): derive the Windows short path without cmd quoting
* chore: add changeset for the Windows short path watch fix
* fix(agent-core-v2): keep watch children whose names start with two dots inside the root
relative() returns "..cache/x" for a child named "..cache", which the
startsWith('..') check treated as a path outside the watched root. Mapped
change paths then kept the resolved long form, and native signal events for
such children skipped the ignore predicate. Only a bare ".." segment, ".."
followed by a separator, or an absolute result is outside the root.
Store the optional metadata object on prompt submission and skill activation as opaque client metadata on the user or skill-activation origin, carry it through prompt events, the transcript projector, snapshots and history rebuilds, and echo it on prompt items without adding it to model content. Use display_text entries for session titles, undo labels and fork titles when every entry provides one. Project user turn payloads, live and rebuilt, through a path-free projection.