fix(workspace-context): deduplicate baseline on resume
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-24-workspace-context.md
|
||||
2026-06-24-workspace-context.md: d58dde38b9851bc1cad2e6b4c679343dedfbc139
|
||||
2026-06-24-workspace-context.zh.md: 983b6a82df78cb865cf9db367cba0fd839c247e9
|
||||
2026-06-24-workspace-context.md: a9d11f88ab9525a40f9bc58f817a088edc43105a
|
||||
2026-06-24-workspace-context.zh.md: 1273bd9b460055a4b0e193267c5e9ad37bbeb0a2
|
||||
|
||||
@@ -28,11 +28,11 @@ The user-global file is fixed at `$DSH_HOME/AGENTS.md`, is not affected by eithe
|
||||
|
||||
### Baseline Injection
|
||||
|
||||
At the first `agent/step` of an agent-loop instance, the plugin injects one sourced user-role message before the request is derived. It loads the user-global file first, then finds the project root by walking upward from `agent.session.header.cwd` to a configured root marker (default `.git`), then loads one candidate from each directory from the root to the cwd. A `.git` file and a `.git` directory are both valid markers, covering linked worktrees and submodules. Without a marker, the cwd itself is the root.
|
||||
At the first `agent/step` of a fresh session, the plugin injects one sourced user-role message before the request is derived. It loads the user-global file first, then finds the project root by walking upward from `agent.session.header.cwd` to a configured root marker (default `.git`), then loads one candidate from each directory from the root to the cwd. A `.git` file and a `.git` directory are both valid markers, covering linked worktrees and submodules. Without a marker, the cwd itself is the root.
|
||||
|
||||
The injection becomes a durable `user/message` with a typed `workspace-instructions` source. Its `baseline: true` marker distinguishes the complete startup or resume baseline from later deltas, and its change list persists the included scopes and content digests. In the product spine workspace instructions are registered before the skills catalog, so their `agent/step` listener injects first. The loop drains both messages before deriving the first request.
|
||||
The injection becomes a durable `user/message` with a typed `workspace-instructions` source. Its `baseline: true` marker distinguishes a complete baseline from later deltas, and its change list persists the included scopes and content digests. In the product spine workspace instructions are registered before the skills catalog, so their `agent/step` listener injects first. The loop drains both messages before deriving the first request.
|
||||
|
||||
A resumed agent creates a new loop instance and injects a baseline composed from current files before its first request. This permits current baseline content on resume without mutating an earlier history event. A resume and a hot plugin remount both face a log that may already hold a baseline; they are told apart by `agent/session-start`, which a startup or resume emits before the first step while a remount attaches to an already-live session and never sees it. A remount retains the existing baseline only when its typed event remains in the current visible surface, and still rebuilds scope and provider-version tracking from current files. If compaction has shadowed that event, the remount injects a current baseline. A resume always re-composes.
|
||||
A resumed agent creates a new loop instance over persisted history. If a typed baseline remains in the visible surface, the loop retains that event and reconciles baseline plus dynamic scopes against current files before its first request. Unchanged files append nothing; files added, edited, or removed while the agent was offline append `set`, `replace`, or `remove` transitions without mutating or duplicating the original baseline. A hot plugin remount follows the same visibility rule. If no typed baseline remains visible, as after compaction shadows it, the loop composes and injects one complete current baseline.
|
||||
|
||||
Compaction can shadow the baseline after this plugin's guarded `agent/step` listener has already run for the session. The `system-prompt/assemble` waterfall therefore delegates first, but restores only for an assembly explicitly marked for the loop's next model request; diagnostic assemblies remain read-only. When a prior typed baseline exists but none remains visible, the listener recomposes the current chain, rechecks cancellation and the current surface generation after every asynchronous probe, and injects before the loop drains its outbox and snapshots derived request history. A per-session settled marker prevents repeated preparation when the current generation produced no baseline; a separate queued marker plus the synchronous commit-time recheck lets concurrent preparations scan without queuing duplicate baselines.
|
||||
|
||||
@@ -54,9 +54,9 @@ Every workspace context event stores versioned metadata with `{ action, scope, p
|
||||
|
||||
At reconciliation time the plugin scans workspace-sourced `user/message` events and derives the latest state for each visible scope. A short per-session pending map begins only after the immutable top-level `tools/result` proves an `additionalContexts` entry survived every post-execute listener, then covers the interval before the loop appends that context to the log. Each entry records the open `{ turn, step }`: an equal durable `user/message` at or after its sequence boundary confirms and removes it, while a matching `step/end` arriving first means the loop discarded its context buffer, so the plugin removes both the pending entry and its version-cache fast path. A nested Code Mode result stages its changes under the parent's opaque execution token so repeated sub-dispatches in one run do not duplicate them; the parent result rolls that provisional state back and commits only contexts retained by outer policy.
|
||||
|
||||
An unchanged path and digest is suppressed. A logged removal is a tombstone, so a reappearing candidate becomes a new `set`. Resume works from persisted metadata. If compaction removes a dynamic instruction event from the visible surface, that state no longer suppresses a later tool-triggered load; if it removes the baseline, prompt assembly restores the complete current chain before the next request. Only changes actually included under the byte budget enter metadata or pending state, so an omitted file remains eligible on a later touch.
|
||||
An unchanged path and digest is suppressed. A logged removal is a tombstone, so a reappearing candidate becomes a new `set`. Resume works from persisted metadata: a visible baseline is comparison state for current-file reconciliation rather than a reason to append another complete baseline. If compaction removes a dynamic instruction event from the visible surface, that state no longer suppresses a later tool-triggered load; if it removes the baseline, prompt assembly restores the complete current chain before the next request. Only changes actually included under the byte budget enter metadata or pending state, so an omitted file remains eligible on a later touch.
|
||||
|
||||
The initial baseline's typed changes are comparison state only while its event remains in the visible session surface. Model-request prompt assembly recomposes a shadowed baseline for the current replacement generation and appends it before the first post-replacement request. It rechecks the caller's signal before injection, so an aborted preparation publishes no pending baseline; a queued marker remains until the corresponding durable event confirms delivery. Later successful filesystem touches can append edits or removals as dynamic messages. The plugin never rewrites the original event. The in-memory scope marker and provider-version cache only select and accelerate probes, so neither can suppress context the model no longer sees. During resumed or post-replacement baseline preparation the plugin also reconciles visible dynamic scopes, so nested changes made while the agent was offline can append an update before the next request.
|
||||
The initial baseline's typed changes are comparison state only while its event remains in the visible session surface. A resumed loop retains that baseline and reconciles current baseline and visible dynamic scopes, so changes made while the agent was offline append transitions before the next request. Model-request prompt assembly instead recomposes a shadowed baseline for the current replacement generation and appends it before the first post-replacement request. It rechecks the caller's signal before injection, so an aborted preparation publishes no pending baseline; a queued marker remains until the corresponding durable event confirms delivery. The plugin never rewrites the original event. The in-memory scope marker and provider-version cache only select and accelerate probes, so neither can suppress context the model no longer sees.
|
||||
|
||||
There is intentionally no watcher. Detection occurs at the next successful structured filesystem touch, post-replacement prompt assembly, or resumed baseline preparation. A provider failure produces no removal; absence is only accepted when all configured candidates in that scope were probed successfully.
|
||||
|
||||
|
||||
@@ -28,11 +28,11 @@ Status: implemented
|
||||
|
||||
### 基线注入
|
||||
|
||||
在 agent loop(智能体循环)实例的第一个 `agent/step`,插件会在派生请求前注入一条带来源的 user 角色消息。它先加载用户全局文件,再从 `agent.session.header.cwd` 向上遍历至配置的根标记(默认为 `.git`)以确定项目根目录,随后从根目录至 cwd 的每级目录各加载一个候选项。`.git` 文件与 `.git` 目录都是有效标记,因而能覆盖链接 worktree 和 submodule。找不到标记时,cwd 本身就是根目录。
|
||||
在全新会话的第一个 `agent/step`,插件会在派生请求前注入一条带来源的 user 角色消息。它先加载用户全局文件,再从 `agent.session.header.cwd` 向上遍历至配置的根标记(默认为 `.git`)以确定项目根目录,随后从根目录至 cwd 的每级目录各加载一个候选项。`.git` 文件与 `.git` 目录都是有效标记,因而能覆盖链接 worktree 和 submodule。找不到标记时,cwd 本身就是根目录。
|
||||
|
||||
该注入成为一条持久 `user/message`,并携带带类型的 `workspace-instructions` 来源。其 `baseline: true` 标记将完整的启动或恢复基线与后续增量区分开来,变更列表则持久保存已纳入的作用域和内容 digest。在产品主干中,工作区指令的注册先于 skill 目录,所以其 `agent/step` 监听器先注入。循环会在派生第一次请求前 drain 这两条消息。
|
||||
该注入成为一条持久 `user/message`,并携带带类型的 `workspace-instructions` 来源。其 `baseline: true` 标记将完整基线与后续增量区分开来,变更列表则持久保存已纳入的作用域和内容 digest。在产品主干中,工作区指令的注册先于 skill 目录,所以其 `agent/step` 监听器先注入。循环会在派生第一次请求前 drain 这两条消息。
|
||||
|
||||
恢复 agent 会创建新的循环实例,并在其第一次请求前注入由当前文件组合的基线。这样,恢复时可以使用当前基线内容,而无需修改先前的历史事件。恢复与插件热重挂都会面对日志中可能已存在基线的情况;二者通过 `agent/session-start` 区分:启动或恢复会在第一步前发出该事件,而热重挂附着到一个已存活的会话、永远不会看到它。只有当基线的类型化事件仍在当前可见表层中时,热重挂才保留既有基线,同时仍会根据当前文件重建 scope 与提供方版本跟踪。如果压缩(compaction)已遮蔽该事件,热重挂会注入当前基线。恢复则始终重新组合。
|
||||
恢复 agent 会基于持久化历史创建新的 loop 实例。如果带类型的基线仍位于可见表层,loop 会保留该事件,并在第一个请求前根据当前文件对账基线与动态 scope。未变文件不追加任何内容;agent 离线期间新增、编辑或移除的文件会追加 `set`、`replace` 或 `remove` 转换,既不改写也不重复追加原始基线。插件热重挂遵循相同的可见性规则。如果已无带类型的基线可见(例如压缩(compaction)将其遮蔽后),loop 会组合并注入一条完整的当前基线。
|
||||
|
||||
在本插件带防护的 `agent/step` 监听器已经为该会话运行后,压缩仍可能遮蔽基线。因此,`system-prompt/assemble` waterfall(瀑布式事件)会先委托,但只有当组装被明确标记为供 loop 的下一个模型请求使用时才恢复;诊断组装保持只读。如果此前存在带类型的基线、但已无基线可见,该监听器会重新组合当前文件链,在每次异步探测后重新检查取消状态和当前表层代次,并在 loop 排空 outbox 和对派生请求历史创建快照之前注入。逐会话的已结算标记会在当前代次没有产生基线时避免重复准备;单独的排队标记加上提交时同步复查,使并发准备可以扫描而不会排入重复基线。
|
||||
|
||||
@@ -54,9 +54,9 @@ shell 命令不会触发发现。本地 bash 调用会启动全新的 shell,
|
||||
|
||||
协调时,插件扫描带工作区来源的 `user/message` 事件,并派生每个可见作用域的最新状态。一个简短的逐会话待处理映射只会在不可变的顶层 `tools/result` 证明某个 `additionalContexts` 条目经过所有 post-execute 监听器后仍然保留时开始记录;随后,它覆盖循环将该上下文追加到日志之前的间隔。每个条目记录开启状态的 `{ turn, step }`:如果相同的持久 `user/message` 出现在其序列边界或之后,该条目得到确认并被移除;如果匹配的 `step/end` 先到达,则说明循环丢弃了上下文缓冲区,插件会同时移除待处理条目及其版本缓存快速路径。嵌套的 Code Mode 结果会把变更暂存在父级的不透明执行 token 下,确保一次运行中的重复子分发不会产生重复项;父级结果会回滚这份临时状态,并且只提交外层策略保留的上下文。
|
||||
|
||||
路径和 digest 均未变化时会被抑制。日志中的移除操作是一条墓碑记录,因此重新出现的候选项会成为新的 `set`。恢复操作从持久化元数据继续工作。如果压缩从可见表面移除动态指令事件,该状态不再抑制之后由工具触发的加载;如果移除的是基线,提示词组装会在下一个请求前恢复完整的当前指令链。只有真正纳入字节预算的变更才会进入元数据或待处理状态,因此被省略的文件在之后的触碰中仍有资格加载。
|
||||
路径和 digest 均未变化时会被抑制。日志中的移除操作是一条墓碑记录,因此重新出现的候选项会成为新的 `set`。恢复从持久化元数据继续工作:可见基线是当前文件对账的比较状态,而不是追加另一条完整基线的理由。如果压缩从可见表面移除动态指令事件,该状态不再抑制之后由工具触发的加载;如果移除的是基线,提示词组装会在下一个请求前恢复完整的当前指令链。只有真正纳入字节预算的变更才会进入元数据或待处理状态,因此被省略的文件在之后的触碰中仍有资格加载。
|
||||
|
||||
只有当初始基线事件仍在可见会话表层中时,其类型化变更才用作比较状态。面向模型请求的提示词组装会为当前替换代次重新组合被遮蔽的基线,并在替换后的第一个请求前追加它。它会在注入前重新检查调用方的 signal,因此已中止的准备不会发布待处理基线;排队标记会保留,直到相应的持久事件确认投递。之后成功的文件系统触碰仍可把编辑或移除作为动态消息追加。插件绝不重写原始事件。内存中的 scope 标记和提供方版本 cache 只用于选择探测对象并加速探测,因此二者都不能抑制模型已无法看见的上下文。在恢复或替换后准备基线的过程中,插件还会协调可见的动态作用域,因此 agent 离线期间发生的嵌套变更可以在下一个请求前追加更新。
|
||||
只有当初始基线事件仍在可见会话表层中时,其类型化变更才用作比较状态。恢复的 loop 会保留该基线,并对账当前基线与可见动态 scope,因此 agent 离线期间的变更会在下一个请求前追加为转换。而面向模型请求的提示词组装会为当前替换代次重新组合被遮蔽的基线,并在替换后的第一个请求前追加它。它会在注入前重新检查调用方的 signal,因此已中止的准备不会发布待处理基线;排队标记会保留,直到相应的持久事件确认投递。插件绝不重写原始事件。内存中的 scope 标记和提供方版本 cache 只用于选择探测对象并加速探测,因此二者都不能抑制模型已无法看见的上下文。
|
||||
|
||||
系统刻意不使用文件监视器。检测发生在下一次成功的结构化文件系统触碰、替换后的提示词组装或恢复时的基线准备。提供方失败不会产生移除;只有该作用域中的全部已配置候选项都成功完成探测后,系统才接受「不存在」这一结论。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user