Merge remote-tracking branch 'upstream/master' into feat/produced-files-folder
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 packages/client/runtime/README.md
|
||||
README.md: bcc1b070535046ab2af1878f763c806aada49ba0
|
||||
README.zh.md: 7a5d651c862d9f9682688f71a9bdf7d6b22f3e63
|
||||
README.md: f4823f58ec79df0cbccfff0a08d9bb59b9a3ac8d
|
||||
README.zh.md: ce8117fc4c95071a6db8592302030a8a63b5478b
|
||||
|
||||
@@ -4,6 +4,8 @@ English | [中文](README.zh.md)
|
||||
|
||||
Client cordis boot and React-free object services: SlotsService wraps SlotCore and supplies renderer data sources; SessionsService owns Session objects, list and scope state, and the shared event window and history paging used by registered conversation view targets. WorkspacesService depends on SessionsService and owns Workspace objects, list/actions, default-target derivation, and the New Session blank-reuse entry (`connectWorkspace`). The runtime fans the shared Host stream into Session and Workspace owners and hands each generic `host/remote-event` frame to `ctx.remote.$dispatch`; domain packages subscribe to their owner events through `ctx.remote.$on` and decide which caches or session rows they invalidate. Client sessions are always Host-born (Session+Agent+cwd in one `session.create`); the client holds no pre-entity session state — a session's Agent scope (the client mirror of host dsh-scope, keyed by the shared agent/session id) is born when its row enters the list mirror and dies with the prune. Contract: api-contracts v3 §4. Each `Session` holds a generic `ProjectionValueStore` seeded from the history-tail `projections` block and updated by `session/projection` frames under higher-seq-wins; domain keys (including `todos`) are read via `projections.faceOf` / `useProjection`, not via `ConversationSnapshot`. The store also publishes one reference-stable whole-value map through `SessionSummary.projectionValues`, allowing global list consumers to reuse the same projections without creating per-session subscriptions.
|
||||
|
||||
For each prompt that can reach a local root or continuable child Agent, the runtime samples the browser's current `Intl.DateTimeFormat().resolvedOptions().timeZone` and attaches it to that one Session or subagent prompt RPC. It is neither cached nor included in Session creation or fork state, so travel and concurrent tabs keep message-local provenance. A browser that cannot provide a non-empty zone fails the prompt locally instead of silently substituting deployment state.
|
||||
|
||||
`bindSettingsScope` is the browser mirror of the Host-side settings owner seam for one domain-owned namespace. It subscribes before starting a nonblocking initial read, publishes a uSES snapshot (status, section value, the composition `base` and raw `user` layers, revision, writability, host/memory mode), serializes `set` and `unset` writes with the latest known namespace revision, suppresses stale publications, recovers a rejected latest write from Host state, and reaches quiescence on plugin disposal. The default decoder validates each section against the namespace's own serialized wire schema (rehydrated through dsh-client-schema-form), so a domain adds a decoder only to narrow beyond that schema. Loopback pages use the Host settings API; remote pages stay in memory mode. A field is overridden when it is PRESENT in `user` — an override equal to the composition default is still an override, which comparing values could not see — and `unset` is how a form clears one back to `base`. Domain packages own the namespace schema, default, and live service rather than putting product policy in runtime.
|
||||
|
||||
## Slot declaration injection
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
客户端 cordis 启动与不依赖 React 的对象服务:SlotsService 包装 SlotCore 并提供 renderer 数据源;SessionsService 拥有 Session 对象、列表与 scope 状态,以及供已注册 conversation view target 共用的事件窗口与历史分页。WorkspacesService 依赖 SessionsService,拥有 Workspace 对象、列表/操作、默认目标派生,以及 New Session 空会话复用入口(`connectWorkspace`)。运行时把共享 Host 流分发给 Session 与 Workspace 所有者,并把每个通用 `host/remote-event` 帧交给 `ctx.remote.$dispatch`;各领域包通过 `ctx.remote.$on` 订阅自身 owner 事件,并自行决定使哪些缓存或会话行失效。客户端会话一律由 Host 创建(一次 `session.create` 同时产生 Session、agent(智能体)和 cwd);客户端不持有任何实体化之前的会话状态——agent scope(host dsh-scope 的客户端镜像,以 agent/session 共用 id 为键)在会话行进入列表镜像时创建,并随 prune 销毁。约定:api-contracts v3 §4。每个 `Session` 持有一个通用的 `ProjectionValueStore`,由历史记录尾部的 `projections` 块播种,并经 `session/projection` 帧按 seq 高者胜更新;领域键(含 `todos`)经 `projections.faceOf`/`useProjection` 读取,不经 `ConversationSnapshot`。该 store 还会通过 `SessionSummary.projectionValues` 发布一份引用稳定的完整值映射,使全局列表消费方无需为每个会话创建订阅,即可复用同一组投影。
|
||||
|
||||
对于每条可到达本地根 Agent 或可继续子 Agent 的提示词,运行时都会采样浏览器当前的 `Intl.DateTimeFormat().resolvedOptions().timeZone`,并只把该值附加到这一次 Session 或 subagent 提示词 RPC。该值既不缓存,也不包含在 Session 创建或 fork 状态中,因此旅行与并发标签页都能保留消息本地的来源信息。浏览器若无法提供非空时区,会在本地拒绝该提示词,而不会悄然使用部署状态代替。
|
||||
|
||||
`bindSettingsScope` 面向单个由领域持有的 namespace,是 Host 侧 settings owner seam 的浏览器镜像。它在开始非阻塞初始读取前建立订阅,发布 uSES 快照(状态、分节值、组装 `base` 层与原始 `user` 层、revision、可写性、host/内存模式),使用已知最新 namespace revision 串行执行 `set` 与 `unset` 写入,抑制陈旧发布,并在最新写入被拒时从 Host 状态恢复;插件释放时,它会达到完全停稳。默认解码器会对照该 namespace 自身的序列化 wire schema(经 dsh-client-schema-form 还原)校验每个分节,因此领域只有在需要比该 schema 进一步收窄时才添加解码器。回环页面使用 Host settings API,远程页面则停留在内存模式。字段是否被覆盖,取决于它是否**出现**在 `user` 中——与组装默认值相同的覆盖仍然是覆盖,比较值是看不出来的——而 `unset` 就是表单把某个字段清回 `base` 的方式。namespace schema、默认值与实时服务归领域包所有,而非把产品政策放入运行时。
|
||||
|
||||
## Slot 声明注入
|
||||
|
||||
@@ -23,6 +23,7 @@ import { PendingWait } from './pending.ts'
|
||||
import { Notifier } from './notifier.ts'
|
||||
import { ProjectionValueStore } from './projection-store.ts'
|
||||
import type { ProjectionsBaseline } from './projection-store.ts'
|
||||
import { resolvedClientTimeZone } from '../time-zone.ts'
|
||||
import { SessionQueueMirror } from './queue-mirror.ts'
|
||||
|
||||
/** Messages requested per history page. */
|
||||
@@ -194,7 +195,12 @@ export class Session implements SessionFace {
|
||||
let result: RpcResult<{ accepted: true }>
|
||||
try {
|
||||
if (this.address === undefined) {
|
||||
result = (await this.api.sessions.prompt({ sessionId: this.sessionId, mode, content })).result
|
||||
result = (await this.api.sessions.prompt({
|
||||
sessionId: this.sessionId,
|
||||
mode,
|
||||
content,
|
||||
clientTimeZone: resolvedClientTimeZone(),
|
||||
})).result
|
||||
} else if (this.address.mode === 'one-shot') {
|
||||
result = {
|
||||
ok: false,
|
||||
@@ -220,6 +226,7 @@ export class Session implements SessionFace {
|
||||
content: content.flatMap(part => part.type === 'text'
|
||||
? [{ type: 'text' as const, text: part.text }]
|
||||
: []),
|
||||
clientTimeZone: resolvedClientTimeZone(),
|
||||
})).result
|
||||
result = routed.ok ? { ok: true, value: { accepted: true } } : routed
|
||||
}
|
||||
|
||||
14
packages/client/runtime/src/client/time-zone.ts
Normal file
14
packages/client/runtime/src/client/time-zone.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/** Browser-owned time-zone sampling for prompt RPC provenance. */
|
||||
|
||||
/**
|
||||
* Resolve the current browser IANA zone for one outbound operation.
|
||||
* @returns The browser-provided canonical zone.
|
||||
* @throws when the runtime cannot provide a non-empty zone.
|
||||
*/
|
||||
export function resolvedClientTimeZone(): string {
|
||||
const timeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
if (typeof timeZone !== 'string' || timeZone.length === 0) {
|
||||
throw new Error('browser time zone is unavailable')
|
||||
}
|
||||
return timeZone
|
||||
}
|
||||
@@ -334,6 +334,7 @@ describe('subagent catalogs', () => {
|
||||
{
|
||||
parentSessionId: S1, childSessionId: S2, mode: 'continuable',
|
||||
content: [{ type: 'text', text: 'continue' }],
|
||||
clientTimeZone: new Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
])
|
||||
expect(api.callsOf('session.history')).toEqual([])
|
||||
|
||||
@@ -465,6 +465,7 @@ describe('prompt and cancel errors', () => {
|
||||
{
|
||||
parentSessionId: PARENT, childSessionId: SID, mode: 'continuable',
|
||||
content: [{ type: 'text', text: '继续' }],
|
||||
clientTimeZone: new Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
])
|
||||
expect(api.callsOf('subagent.interrupt')).toEqual([
|
||||
@@ -530,7 +531,12 @@ describe('prompt and cancel errors', () => {
|
||||
expect(result.ok).toBe(true)
|
||||
// Monotone: settlement alone does not step the phase anywhere.
|
||||
expect(session.getSnapshot().composerPhase).toBe('engaging')
|
||||
expect(api.callsOf('session.prompt')).toMatchObject([{ sessionId: SID, mode: 'queue', content: [{ type: 'text', text: '要发的' }] }])
|
||||
expect(api.callsOf('session.prompt')).toMatchObject([{
|
||||
sessionId: SID,
|
||||
mode: 'queue',
|
||||
content: [{ type: 'text', text: '要发的' }],
|
||||
clientTimeZone: new Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
}])
|
||||
// First content lands (running turn): engaging → active.
|
||||
session.handleRunning(true)
|
||||
expect(session.getSnapshot().composerPhase).toBe('active')
|
||||
|
||||
24
packages/client/runtime/tests/time-zone.spec.ts
Normal file
24
packages/client/runtime/tests/time-zone.spec.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { resolvedClientTimeZone } from '../src/client/time-zone.ts'
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('browser time zone', () => {
|
||||
it('returns the runtime-resolved zone', () => {
|
||||
expect(resolvedClientTimeZone()).toBe(
|
||||
new Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
)
|
||||
})
|
||||
|
||||
it.each([undefined, ''])('fails loud when the runtime exposes no zone %#', (timeZone) => {
|
||||
const options = new Intl.DateTimeFormat().resolvedOptions()
|
||||
vi.spyOn(Intl.DateTimeFormat.prototype, 'resolvedOptions').mockReturnValue({
|
||||
...options,
|
||||
timeZone: timeZone as string,
|
||||
})
|
||||
|
||||
expect(() => resolvedClientTimeZone()).toThrow('browser time zone is unavailable')
|
||||
})
|
||||
})
|
||||
@@ -77,7 +77,6 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
'settings.general.item': { kind: 'list'; scope: 'root'; owner: SettingsGeneralItemOwnerProps }
|
||||
}
|
||||
}
|
||||
|
||||
/** Owner share of a General preference row (the section supplies nothing). */
|
||||
export interface SettingsGeneralItemOwnerProps {
|
||||
/** Marker field: item owner props are intentionally empty. */
|
||||
|
||||
@@ -365,7 +365,6 @@ describe('SettingsScopeController', () => {
|
||||
expect(scope.getSnapshot()).toMatchObject({ value: { preference: 'light' }, revision: 5 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('SettingsScopeService.bind', () => {
|
||||
it('subscribes before the initial read and converges to the latest queued invalidation', async () => {
|
||||
const initial = deferred<ReturnType<typeof described>>()
|
||||
|
||||
Reference in New Issue
Block a user