Răsfoiți Sursa

fix(session): retain fork tail until the first inbox change

Dudu-0223 1 săptămână în urmă
părinte
comite
0f8552df7f

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-09-11-session-controller-fork-turn-cut.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # 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:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-11-session-controller-fork-turn-cut.md
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-11-session-controller-fork-turn-cut.md
-2026-09-11-session-controller-fork-turn-cut.md: 0c8d45c861df964a62586827e80c613704ed17f0
-2026-09-11-session-controller-fork-turn-cut.zh.md: 729a42a596264ac618608d26cd52a1ad836b58db
+2026-09-11-session-controller-fork-turn-cut.md: 47f48e60fa3f34a766e5e3ecf21f3e95502384c3
+2026-09-11-session-controller-fork-turn-cut.zh.md: 1d109bfb33b2a8eadcad63feeb99a11562540afb

+ 7 - 7
.agents/notes/implemented/bug-fix/2026-09-11-session-controller-fork-turn-cut.md

@@ -1,4 +1,4 @@
-# Agent Note: Session Controller forks stop at the selected turn end
+# Agent Note: Session Controller forks exclude the next inbox change
 
 
 Status: implemented
 Status: implemented
 
 
@@ -10,20 +10,20 @@ A user input enters the durable inbox before its `turn/start`. Extending a compl
 
 
 ## Decision
 ## Decision
 
 
-The [Session Controller](../../../../packages/api/session-controller/README.md) copies the contiguous prefix through the selected `turn/end`, inclusive. Explicit anchors select the first closing event at or after the anchor; omitted and past-end anchors select the last closing event. No event after that closing event belongs to the seed, including queued input, titles, and model settings.
+The [Session Controller](../../../../packages/api/session-controller/README.md) selects a completed `turn/end` and copies its contiguous prefix plus following events up to, but excluding, the first `turn/start` or `agent/inbox/spliced`. Explicit anchors select the first closing event at or after the anchor; omitted and past-end anchors select the last closing event. Both next-turn and next-step inbox changes stop the extension.
 
 
-The lower-level `SessionStore.fork()` retains its explicit stable-event semantics from the [log-only event decision](../simplification/2026-07-28-remove-synthetic-log-only-turns.md). Selecting a completed turn in the controller does not request a later stable event.
+This retains title and model-setting events before the first inbox change, consistent with the [log-only event decision](../simplification/2026-07-28-remove-synthetic-log-only-turns.md). The lower-level `SessionStore.fork()` retains its explicit stable-event semantics.
 
 
 ## Alternatives considered
 ## Alternatives considered
 
 
-**Stop only at the next inbox event.** Event-type exceptions still copy unrelated state changes after the selected turn and require the controller to classify plugin-owned events.
+**Stop exactly at the selected turn end.** This drops standalone title and model-setting events recorded after the turn even when no input follows them.
 
 
-**Copy the tail and clear the child's inbox.** Clearing adds child events to cancel input that lies outside the requested prefix, while still inheriting other state from after the selected turn.
+**Copy the tail and clear the child's inbox.** Clearing adds child events to cancel input that the fork can exclude from its seed.
 
 
 ## Consequences
 ## Consequences
 
 
-A fork inherits the configuration recorded through its selected turn. Later configuration and title events are excluded. The client may independently assign the child's fork title. Events already inside the selected prefix retain their ordinary replay semantics; this decision does not redefine pending input inserted before the selected closing event.
+An asynchronous title or plugin event can occur after an inbox change; the fork excludes it along with the rest of that tail. Event ordering does not guarantee that every late plugin result is inherited. The client independently assigns a fork title from the displayed source title when available. Events already inside the selected completed-turn prefix retain their ordinary replay semantics; this decision does not redefine pending input inserted before the selected closing event.
 
 
 ## Verification
 ## Verification
 
 
-Controller tests execute the production loop and check that sending C after forking A excludes the parent's later B from child history and produces one model request. Message, closing-event, omitted, and past-end anchors share this assertion. Model-routing coverage excludes a later configuration change. The Web message-actions snapshot seeds a queued input after the completed turn and verifies the branch action creates a child without that input or its inbox insertion.
+Controller tests execute the production loop and check that sending C after forking A excludes the parent's later B from child history and produces one model request, while retaining a title between A's end and B's insertion. Message, closing-event, omitted, and past-end anchors share this assertion. Model-routing coverage retains settings before either inbox destination and excludes later titles and settings. The Web message-actions snapshot places B's inbox insertion between completed turns and verifies the branch action creates a child without B or its insertion.

+ 7 - 7
.agents/notes/implemented/bug-fix/2026-09-11-session-controller-fork-turn-cut.zh.md

@@ -1,4 +1,4 @@
-# Agent Note: Session Controller 分叉截到选中轮次的结束事件
+# Agent Note: Session Controller 分叉排除下一次 inbox 变更
 
 
 Status: implemented
 Status: implemented
 
 
@@ -10,20 +10,20 @@ English | [中文](2026-09-11-session-controller-fork-turn-cut.md)
 
 
 ## Decision
 ## Decision
 
 
-[Session Controller](../../../../packages/api/session-controller/README.zh.md) 复制截至选中 `turn/end` 的连续前缀,并包含该事件。显式锚点选择位于锚点或其后的第一条结束事件;省略锚点或锚点超出日志末尾时,选择最后一条结束事件。结束事件之后的所有事件均不属于种子,包括排队输入、标题和模型设置
+[Session Controller](../../../../packages/api/session-controller/README.zh.md) 选择已结束的 `turn/end`,复制其连续前缀及后续事件,直到第一条 `turn/start` 或 `agent/inbox/spliced` 之前,不包含该停止事件。显式锚点选择位于锚点或其后的第一条结束事件;省略锚点或锚点超出日志末尾时,选择最后一条结束事件。next-turn 和 next-step 两种 inbox 变更都会停止扩展
 
 
-底层 `SessionStore.fork()` 保留[纯日志事件决策](../simplification/2026-07-28-remove-synthetic-log-only-turns.zh.md)规定的显式稳定事件语义。在控制器中选择已结束轮次,不代表请求复制到更晚的稳定事件
+这会保留第一次 inbox 变更之前的标题和模型设置事件,与[纯日志事件决策](../simplification/2026-07-28-remove-synthetic-log-only-turns.zh.md)一致。底层 `SessionStore.fork()` 保留其显式稳定事件语义
 
 
 ## Alternatives considered
 ## Alternatives considered
 
 
-**仅在下一条 inbox 事件处停止。** 按事件类型添加例外仍会复制选中轮次之后的无关状态变化,并要求控制器分类插件所属的事件。
+**严格截到选中轮次的结束事件。** 即使后面没有输入,这也会丢弃轮次结束后记录的独立标题和模型设置事件。
 
 
-**复制尾部后清空子会话 inbox。** 清空操作会添加子会话事件,以取消本来就在所请求前缀之外的输入,同时仍然继承选中轮次之后的其他状态
+**复制尾部后清空子会话 inbox。** 清空操作会添加子会话事件,以取消本可从分叉种子中排除的输入
 
 
 ## Consequences
 ## Consequences
 
 
-分叉继承截至选中轮次记录的配置。之后的配置和标题事件被排除。客户端可以独立设置子会话的分叉标题。已经位于选中前缀内的事件保留通常的回放语义;本决策不重新定义在选中结束事件之前入队的待处理输入。
+异步标题或插件事件可能出现在 inbox 变更之后;分叉会将它与该尾部的其余事件一起排除。事件顺序不保证每个延迟的插件结果都会被继承。显示的源标题可用时,客户端会据此独立设置分叉标题。已经位于选中已结束轮次前缀内的事件保留通常的回放语义;本决策不重新定义在选中结束事件之前入队的待处理输入。
 
 
 ## Verification
 ## Verification
 
 
-控制器测试执行生产循环,检查从 A 分叉后发送 C 时,子会话历史不包含父会话后续的 B,并且只产生一次模型请求。消息、结束事件、省略及越界锚点共用此断言。模型路由覆盖排除了后续配置变更。Web 消息操作快照在已结束轮次后植入排队输入,验证分叉操作创建的子会话不包含该输入或其入队事件。
+控制器测试执行生产循环,检查从 A 分叉后发送 C 时,子会话历史不包含父会话后续的 B,并且只产生一次模型请求,同时保留 A 结束与 B 入队之间的标题。消息、结束事件、省略及越界锚点共用此断言。模型路由覆盖保留两种 inbox 目标变更之前的设置,并排除之后的标题和设置。Web 消息操作快照将 B 的入队事件放在已结束轮次之间,验证分叉操作创建的子会话不包含 B 或其入队事件。

+ 2 - 2
.agents/notes/implemented/simplification/2026-07-28-remove-synthetic-log-only-turns.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # 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:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-07-28-remove-synthetic-log-only-turns.md
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-07-28-remove-synthetic-log-only-turns.md
-2026-07-28-remove-synthetic-log-only-turns.md: b35045d036dbb3ea18431b76cec2c41674de5607
-2026-07-28-remove-synthetic-log-only-turns.zh.md: 3dbcd0d9b258f4ea9f4515ba009e75487a96c342
+2026-07-28-remove-synthetic-log-only-turns.md: ae935d625f6aecf5773d3069ea5b73278a94f5c7
+2026-07-28-remove-synthetic-log-only-turns.zh.md: c35431dcfbd863f147988ecbf2ccaedec6260bbc

+ 1 - 1
.agents/notes/implemented/simplification/2026-07-28-remove-synthetic-log-only-turns.md

@@ -20,7 +20,7 @@ Core session invariants continue to enforce core-owned execution relations: turn
 
 
 The title service appends `session/title` directly after its existing service, revision, cancellation, and live-session checks. The bundled model helper appends its literal `session/title-llm-request` record before dispatch. Persistence admits both through the bounded `session/event` path and drains them at ordinary checkpoints and lifecycle teardown; neither append forces a flush merely because it is between turns. A fallback, auxiliary request record, or accepted provider title may therefore appear after `turn/end` and before the next `turn/start`. Manual compaction uses the same between-turn capability for a `compaction/* { turn: null }` bracket, but explicitly flushes the closed attempt because `/compact` promises durability before releasing queued prompt admission.
 The title service appends `session/title` directly after its existing service, revision, cancellation, and live-session checks. The bundled model helper appends its literal `session/title-llm-request` record before dispatch. Persistence admits both through the bounded `session/event` path and drains them at ordinary checkpoints and lifecycle teardown; neither append forces a flush merely because it is between turns. A fallback, auxiliary request record, or accepted provider title may therefore appear after `turn/end` and before the next `turn/start`. Manual compaction uses the same between-turn capability for a `compaction/* { turn: null }` bracket, but explicitly flushes the closed attempt because `/compact` promises durability before releasing queued prompt admission.
 
 
-`SessionStore.fork()` may end at any stable event position outside an open turn, not only at `turn/end`. This preserves standalone title and other plugin-owned log-only records in a default store fork while still rejecting a prefix cut through active execution. The [Session Controller fork decision](../bug-fix/2026-09-11-session-controller-fork-turn-cut.md) limits its completed-turn operation to the selected closing event.
+`SessionStore.fork()` may end at any stable event position outside an open turn, not only at `turn/end`. This preserves standalone title and other plugin-owned log-only records in a default store fork while still rejecting a prefix cut through active execution. The [Session Controller fork decision](../bug-fix/2026-09-11-session-controller-fork-turn-cut.md) stops its between-turn extension at the first inbox change.
 
 
 The historical [universal turn-enclosure decision](../../archived/architecture/2026-06-15-turn-enclosure-invariant.md) remains useful only as the reason the synthetic mechanism was introduced. The [context-injection decision](../architecture/2026-07-24-separate-context-injection-from-turn-execution.md) established the current meaning: one turn represents one model-loop execution. The [queued manual compaction decision](../feature/2026-07-30-queued-manual-compaction.md) applies that rule to a durable multi-event bracket and owns its marker and admission semantics.
 The historical [universal turn-enclosure decision](../../archived/architecture/2026-06-15-turn-enclosure-invariant.md) remains useful only as the reason the synthetic mechanism was introduced. The [context-injection decision](../architecture/2026-07-24-separate-context-injection-from-turn-execution.md) established the current meaning: one turn represents one model-loop execution. The [queued manual compaction decision](../feature/2026-07-30-queued-manual-compaction.md) applies that rule to a durable multi-event bracket and owns its marker and admission semantics.
 
 

+ 1 - 1
.agents/notes/implemented/simplification/2026-07-28-remove-synthetic-log-only-turns.zh.md

@@ -20,7 +20,7 @@ Status: implemented
 
 
 标题服务会在完成既有的服务状态、修订、取消和活跃会话检查后,直接追加 `session/title`。随附模型辅助函数会在发起调用前追加其字面量 `session/title-llm-request` 记录。持久化通过有界 `session/event` 路径接纳两者,并在常规检查点与生命周期结束时排空;二者都不会仅因为位于轮次之间就强制刷写。因此,回退标题、辅助请求记录或已接受的提供方标题可以出现在 `turn/end` 之后、下一个 `turn/start` 之前。手动压缩(compaction)利用同一项轮次间能力记录 `compaction/* { turn: null }` 标记对,但会显式刷写已闭合的尝试,因为 `/compact` 承诺在放行排队中的提示词前完成持久化。
 标题服务会在完成既有的服务状态、修订、取消和活跃会话检查后,直接追加 `session/title`。随附模型辅助函数会在发起调用前追加其字面量 `session/title-llm-request` 记录。持久化通过有界 `session/event` 路径接纳两者,并在常规检查点与生命周期结束时排空;二者都不会仅因为位于轮次之间就强制刷写。因此,回退标题、辅助请求记录或已接受的提供方标题可以出现在 `turn/end` 之后、下一个 `turn/start` 之前。手动压缩(compaction)利用同一项轮次间能力记录 `compaction/* { turn: null }` 标记对,但会显式刷写已闭合的尝试,因为 `/compact` 承诺在放行排队中的提示词前完成持久化。
 
 
-`SessionStore.fork()` 可以结束于开放轮次之外的任意稳定事件位置,而不限于 `turn/end`。这样,默认的 store fork 会保留独立标题和其他插件所属的纯日志记录,同时仍拒绝在活跃执行过程中截断前缀。[Session Controller 分叉决策](../bug-fix/2026-09-11-session-controller-fork-turn-cut.zh.md)将其已结束轮次操作限制在选中的结束事件处
+`SessionStore.fork()` 可以结束于开放轮次之外的任意稳定事件位置,而不限于 `turn/end`。这样,默认的 store fork 会保留独立标题和其他插件所属的纯日志记录,同时仍拒绝在活跃执行过程中截断前缀。[Session Controller 分叉决策](../bug-fix/2026-09-11-session-controller-fork-turn-cut.zh.md)会在第一次 inbox 变更处停止轮次间扩展
 
 
 历史上的[通用轮次封闭决策](../../archived/architecture/2026-06-15-turn-enclosure-invariant.md)如今只适合用于解释为何曾引入合成机制。[上下文注入决策](../architecture/2026-07-24-separate-context-injection-from-turn-execution.zh.md)确立了当前语义:一个轮次表示一次模型循环执行。[排队手动压缩决策](../feature/2026-07-30-queued-manual-compaction.zh.md)将该规则应用于持久多事件标记对,并拥有其标记与接纳语义。
 历史上的[通用轮次封闭决策](../../archived/architecture/2026-06-15-turn-enclosure-invariant.md)如今只适合用于解释为何曾引入合成机制。[上下文注入决策](../architecture/2026-07-24-separate-context-injection-from-turn-execution.zh.md)确立了当前语义:一个轮次表示一次模型循环执行。[排队手动压缩决策](../feature/2026-07-30-queued-manual-compaction.zh.md)将该规则应用于持久多事件标记对,并拥有其标记与接纳语义。
 
 

+ 2 - 2
packages/api/session-controller/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # 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:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/api/session-controller/README.md
 #   pnpm run verify-translation-pairing --write packages/api/session-controller/README.md
-README.md: 8429b2f73199946944ca5a4e634b27d5d2f91f75
-README.zh.md: 3c20a2bfad7d29b764e9c47641e665721da8dce9
+README.md: f71b4e7f281caa0d1471dca04de4accee8e4c4be
+README.zh.md: daa4946e623a7cd9955744ce9477970462f339d7

+ 1 - 1
packages/api/session-controller/README.md

@@ -37,7 +37,7 @@ The Session object also carries local submission echoes: `session.beginSubmissio
 
 
 The user-invocable `skills/list` metadata includes the winning provider’s optional instruction-file `path`. The composer can preview that file without loading every skill body or activating a cold Agent.
 The user-invocable `skills/list` metadata includes the winning provider’s optional instruction-file `path`. The composer can preview that file without loading every skill body or activating a cold Agent.
 
 
-Fork copies history through the selected completed turn, including its `turn/end`. Events after that point, including queued input and model-setting changes, are excluded. An omitted or past-end anchor selects the last completed turn; an anchor inside an unfinished turn is rejected.
+Fork copies history through the selected completed turn and retains following events until the next turn starts or the inbox changes. The stopping event and all later events are excluded; titles and model settings are inherited only when recorded before that stop. An omitted or past-end anchor selects the last completed turn; an anchor inside an unfinished turn is rejected.
 
 
 
 
 <a id="session-media-references"></a>
 <a id="session-media-references"></a>

+ 1 - 1
packages/api/session-controller/README.zh.md

@@ -37,7 +37,7 @@ Session 对象还承载本地提交回显:`session.beginSubmission` 在调用
 
 
 面向用户调用的 `skills/list` 元数据包含胜出提供方可选的指令文件 `path`。输入框可据此预览文件,无需加载每个 skill 的正文或激活冷态 Agent。
 面向用户调用的 `skills/list` 元数据包含胜出提供方可选的指令文件 `path`。输入框可据此预览文件,无需加载每个 skill 的正文或激活冷态 Agent。
 
 
-分叉复制截至选中已结束轮次的历史,并包含其 `turn/end`。该位置之后的事件均被排除,包括排队输入和模型设置变更。省略锚点或锚点超出日志末尾时,选择最后一个已结束轮次;位于未结束轮次内的锚点会被拒绝。
+分叉复制截至选中已结束轮次的历史,并保留后续事件,直到下一轮开始或 inbox 发生变更。停止事件及其后的所有事件均被排除;标题和模型设置只有记录在该停止位置之前才会被继承。省略锚点或锚点超出日志末尾时,选择最后一个已结束轮次;位于未结束轮次内的锚点会被拒绝。
 
 
 
 
 <a id="session-media-references"></a>
 <a id="session-media-references"></a>

+ 6 - 1
packages/api/session-controller/src/commands.ts

@@ -240,7 +240,12 @@ export class SessionCommandController {
         { sessionId: request.sessionId },
         { sessionId: request.sessionId },
       )
       )
     }
     }
-    const cut = SessionLogOffset(boundary.seq + 1)
+    let cut = SessionLogOffset(boundary.seq + 1)
+    while (cut < source.events.length
+      && source.events[cut]?.type !== 'turn/start'
+      && source.events[cut]?.type !== 'agent/inbox/spliced') {
+      cut = SessionLogOffset(cut + 1)
+    }
     let workspace: Workspace | undefined
     let workspace: Workspace | undefined
     try {
     try {
       workspace = await this.forkWorkspace(source.header)
       workspace = await this.forkWorkspace(source.header)

+ 25 - 9
packages/api/session-controller/tests/session-fork.host.spec.ts

@@ -106,6 +106,9 @@ describe('sessions.fork', () => {
         source.followup(message('A'))
         source.followup(message('A'))
         await source.whenIdle()
         await source.whenIdle()
         const boundary = source.session.snapshotEvents().at(-1)!.seq
         const boundary = source.session.snapshotEvents().at(-1)!.seq
+        const title = source.session.append('session/title', {
+          title: 'Title after A', messageSeqs: [], source: { kind: 'user' },
+        })
         if (anchor === 'message' || anchor === 'turn-end') {
         if (anchor === 'message' || anchor === 'turn-end') {
           source.followup(message('B'))
           source.followup(message('B'))
           await source.whenIdle()
           await source.whenIdle()
@@ -129,7 +132,9 @@ describe('sessions.fork', () => {
           ? item.content.flatMap(part => part.type === 'text' ? [part.text] : []) : [])
           ? item.content.flatMap(part => part.type === 'text' ? [part.text] : []) : [])
         expect(userTexts).toEqual(['A', 'C'])
         expect(userTexts).toEqual(['A', 'C'])
         expect(adapter.requests.slice(requestCount)).toHaveLength(1)
         expect(adapter.requests.slice(requestCount)).toHaveLength(1)
-        expect(child.session.inheritedEventCount).toBe(boundary + 1)
+        expect(child.session.inheritedEventCount).toBe(title.seq + 1)
+        expect(child.session.snapshotEvents().slice(0, child.session.inheritedEventCount))
+          .toEqual(original.slice(0, title.seq + 1))
         expect(source.session.snapshotEvents()).toEqual(original)
         expect(source.session.snapshotEvents()).toEqual(original)
       } finally {
       } finally {
         await ctx.fiber.dispose()
         await ctx.fiber.dispose()
@@ -317,10 +322,9 @@ describe('sessions.fork', () => {
     await ctx.fiber.dispose()
     await ctx.fiber.dispose()
   })
   })
 
 
-  it('inherits model selection through the completed turn and excludes later changes', async () => {
+  it.each(['next-turn', 'next-step', null] as const)('inherits model settings before %s inbox changes', async (target) => {
     const ctx = await composed()
     const ctx = await composed()
-    const source = liveAgent(ctx, 'session-routed', 0)
-    source.append('turn/start', { turn: 1 })
+    const source = liveAgent(ctx, 'session-routed', 1)
     source.append('request/header', {
     source.append('request/header', {
       header: {
       header: {
         config: {
         config: {
@@ -331,16 +335,28 @@ describe('sessions.fork', () => {
       },
       },
       reason: 'initial',
       reason: 'initial',
     })
     })
-    source.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
-    source.append('request/header', {
-      header: { config: { provider: 'later-provider', model: 'later-model' } },
-      reason: 'change',
-    })
+    const inherited = source.snapshotEvents()
+    if (target !== null) {
+      source.append('agent/inbox/spliced', {
+        target, start: 0,
+        inserted: [createUserMessage({
+          content: [{ type: 'text', text: 'next input' }], source: { kind: 'user' },
+        })],
+      })
+      source.append('session/title', {
+        title: 'Title after inbox insertion', messageSeqs: [], source: { kind: 'user' },
+      })
+      source.append('request/header', {
+        header: { config: { provider: 'later-provider', model: 'later-model' } },
+        reason: 'change',
+      })
+    }
     const response = await remote(ctx).fork(request({ sessionId: source.id }))
     const response = await remote(ctx).fork(request({ sessionId: source.id }))
     expect(response.ok).toBe(true)
     expect(response.ok).toBe(true)
     if (!response.ok) return
     if (!response.ok) return
     const child = ctx.agents.get(response.value.sessionId)
     const child = ctx.agents.get(response.value.sessionId)
     if (child === undefined) throw new Error('fork did not publish the child agent')
     if (child === undefined) throw new Error('fork did not publish the child agent')
+    expect(child.session.snapshotEvents().slice(0, child.session.inheritedEventCount)).toEqual(inherited)
     const assembly = await child.ctx.systemPrompt.assemble()
     const assembly = await child.ctx.systemPrompt.assemble()
     expect(assembly.variables).toMatchObject({
     expect(assembly.variables).toMatchObject({
       provider: 'inherited-provider',
       provider: 'inherited-provider',