feat(schedule): add absolute-time reminders

This commit is contained in:
pku-xht
2026-08-06 15:03:48 +08:00
committed by Tianyi Cui
parent 9e61b7d1b1
commit d61059364e
54 changed files with 3169 additions and 374 deletions

View File

@@ -10,6 +10,7 @@ import type {
// plugin-to-plugin value imports are a bundle purity error.
import { transportError } from '@deepseek-ai/dsh-host-apiproxy/api'
import { mergeOrderedBaseline } from '../ordered-baseline.ts'
import { resolvedClientTimeZone } from '../time-zone.ts'
import type { SessionListEntry, TitledSessionSummary } from './lineage.ts'
import { flattenLineage } from './lineage.ts'
import type { PendingInteractionStatus } from './pending.ts'
@@ -514,7 +515,10 @@ export class SessionManager {
opts: { workspaceId?: WorkspaceId; cwd?: string; sessionId?: SessionId } = {},
): Promise<RpcResult<{ sessionId: SessionId }>> {
try {
const shared = opts.sessionId === undefined ? {} : { sessionId: opts.sessionId }
const shared = {
timeZone: resolvedClientTimeZone(),
...(opts.sessionId === undefined ? {} : { sessionId: opts.sessionId }),
}
const payload = opts.workspaceId !== undefined
? { workspaceId: opts.workspaceId, ...shared }
: { ...(opts.cwd === undefined ? {} : { cwd: opts.cwd }), ...shared }

View File

@@ -25,6 +25,7 @@ import { isVisibleAssistantChunk, PartialAccumulator } from './partial.ts'
import { ProjectionValueStore } from './projection-store.ts'
import type { ProjectionsBaseline } from './projection-store.ts'
import { ToolCallTree } from './tool-call-tree.ts'
import { resolvedClientTimeZone } from '../time-zone.ts'
/** Messages requested per history page. */
export const PAGE_MESSAGES = 50
@@ -232,7 +233,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,

View File

@@ -0,0 +1,14 @@
/** Browser-owned time-zone sampling for Session and 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
}

View File

@@ -12,8 +12,11 @@ import TypertRegistry from '@deepseek-ai/dsh-typert-registry'
import * as RuntimeClient from '../src/client/index.ts'
import type { SessionsService } from '../src/client/sessions/service.ts'
import type { WorkspacesService } from '../src/client/workspaces/service.ts'
import { resolvedClientTimeZone } from '../src/client/time-zone.ts'
import { FakeApiClient, ok } from './fake-api.ts'
const CLIENT_TIME_ZONE = resolvedClientTimeZone()
interface Bench {
ctx: Context
api: FakeApiClient
@@ -102,7 +105,10 @@ describe('runtime client apply', () => {
const sessions = bench.ctx.get('sessions') as SessionsService
const workspaces = bench.ctx.get('workspaces') as WorkspacesService
expect(bench.api.callsOf('session.create')).toEqual([{ workspaceId: 'w-recent' }])
expect(bench.api.callsOf('session.create')).toEqual([{
workspaceId: 'w-recent',
timeZone: CLIENT_TIME_ZONE,
}])
expect(sessions.list.getSnapshot().current).toBe('fk-new')
sessions.clear()

View File

@@ -6,11 +6,13 @@
import { describe, expect, it, vi } from 'vitest'
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
import { SessionManager } from '../src/client/sessions/manager.ts'
import { resolvedClientTimeZone } from '../src/client/time-zone.ts'
import { FakeApiClient, deferred, err, ok } from './fake-api.ts'
import { entries, plainTurn } from './event-script.ts'
const S1 = 'fk-m1' as SessionId
const S2 = 'fk-m2' as SessionId
const CLIENT_TIME_ZONE = resolvedClientTimeZone()
type SummaryOver = Partial<{
updatedAt: number
@@ -708,7 +710,11 @@ describe('remaining branches', () => {
api.onCreate = () => Promise.resolve(ok({ sessionId: S1 }))
const manager = new SessionManager(api)
await manager.create({ cwd: '/tmp/w', sessionId: S1 })
expect(api.callsOf('session.create')).toEqual([{ cwd: '/tmp/w', sessionId: S1 }])
expect(api.callsOf('session.create')).toEqual([{
cwd: '/tmp/w',
sessionId: S1,
timeZone: CLIENT_TIME_ZONE,
}])
expect(manager.getListSnapshot().items[0]).toMatchObject({ sessionId: S1, cwd: '/tmp/w' })
await manager.create({ cwd: '/tmp/w' }) // same id returned: no duplicate row
expect(manager.getListSnapshot().items).toHaveLength(1)

View File

@@ -11,6 +11,7 @@ import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
import { Session } from '../src/client/sessions/session.ts'
import { resolvedClientTimeZone } from '../src/client/time-zone.ts'
import { FakeApiClient, deferred, err, ok } from './fake-api.ts'
import { entries, ev, plainTurn } from './event-script.ts'
@@ -19,6 +20,7 @@ const at = (seq: number, e: Record<string, unknown>): SessionEvent =>
const SID = 'fk-s1' as SessionId
const PARENT = 'fk-parent' as SessionId
const CLIENT_TIME_ZONE = resolvedClientTimeZone()
afterEach(() => {
vi.unstubAllGlobals()
@@ -722,7 +724,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')).toEqual([{
sessionId: SID,
mode: 'queue',
content: [{ type: 'text', text: '要发的' }],
clientTimeZone: CLIENT_TIME_ZONE,
}])
// First content lands (running turn): engaging → active.
session.handleRunning(true)
expect(session.getSnapshot().composerPhase).toBe('active')

View File

@@ -10,9 +10,11 @@ import { Context } from 'cordis'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
import { SessionCreateError, SessionsService, scopeOf } from '../src/client/sessions/service.ts'
import { resolvedClientTimeZone } from '../src/client/time-zone.ts'
import { FakeApiClient, deferred, err, ok } from './fake-api.ts'
const sid = (s: string): SessionId => s as SessionId
const CLIENT_TIME_ZONE = resolvedClientTimeZone()
interface Bench {
ctx: Context
@@ -452,7 +454,11 @@ describe('create', () => {
const b = bench()
b.api.onCreate = () => Promise.resolve(ok({ sessionId: sid('fresh') }))
await expect(b.svc.create({ cwd: '/w', sessionId: sid('fresh') })).resolves.toBe('fresh')
expect(b.api.callsOf('session.create')).toEqual([{ cwd: '/w', sessionId: 'fresh' }])
expect(b.api.callsOf('session.create')).toEqual([{
cwd: '/w',
sessionId: 'fresh',
timeZone: CLIENT_TIME_ZONE,
}])
b.api.onCreate = () => Promise.resolve({
rpcId: 'e' as never,
result: { ok: false as const, error: { code: 'internal' as const, message: '爆了', details: {} } },

View 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('fails loud when the runtime exposes no zone', () => {
const options = new Intl.DateTimeFormat().resolvedOptions()
vi.spyOn(Intl.DateTimeFormat.prototype, 'resolvedOptions').mockReturnValue({
...options,
timeZone: '',
})
expect(() => resolvedClientTimeZone()).toThrow('browser time zone is unavailable')
})
})

View File

@@ -2,12 +2,14 @@ import { Context } from 'cordis'
import { describe, expect, it } from 'vitest'
import type { SessionId, WorkspaceId, WorkspaceView } from '@deepseek-ai/dsh-client-connection/client'
import { SessionsService } from '../src/client/sessions/service.ts'
import { resolvedClientTimeZone } from '../src/client/time-zone.ts'
import { WorkspaceManager } from '../src/client/workspaces/manager.ts'
import { DirectoryBrowseError, WorkspaceCreateError, WorkspacesService } from '../src/client/workspaces/service.ts'
import { FakeApiClient, deferred, err, ok } from './fake-api.ts'
const sid = (id: string): SessionId => id as SessionId
const wid = (id: string): WorkspaceId => id as WorkspaceId
const CLIENT_TIME_ZONE = resolvedClientTimeZone()
function workspace(id: string, sessionIds: SessionId[] = [], createdAt = '2026-01-01T00:00:00.000Z'): WorkspaceView {
return {
@@ -188,7 +190,10 @@ describe('WorkspacesService', () => {
// Miss: beta has only a non-blank session → host create with workspaceId.
api.onCreate = () => Promise.resolve(ok({ sessionId: sid('s-fresh') }))
await expect(workspaces.connectWorkspace(wid('beta'))).resolves.toBe('s-fresh')
expect(api.callsOf('session.create')).toEqual([{ workspaceId: 'beta' }])
expect(api.callsOf('session.create')).toEqual([{
workspaceId: 'beta',
timeZone: CLIENT_TIME_ZONE,
}])
// Same guarantee on the create arm (draft hand-off writes the machine pre-open).
expect(sessions.binding(sid('s-fresh'))).toBeDefined()
@@ -196,7 +201,10 @@ describe('WorkspacesService', () => {
// never reused, a fresh accounted session is created instead.
api.onCreate = () => Promise.resolve(ok({ sessionId: sid('s-fresh-3') }))
await expect(workspaces.connectWorkspace(wid('gamma'))).resolves.toBe('s-fresh-3')
expect(api.callsOf('session.create')).toEqual([{ workspaceId: 'beta' }, { workspaceId: 'gamma' }])
expect(api.callsOf('session.create')).toEqual([
{ workspaceId: 'beta', timeZone: CLIENT_TIME_ZONE },
{ workspaceId: 'gamma', timeZone: CLIENT_TIME_ZONE },
])
// Unknown workspace fails loud instead of silently creating in nowhere.
await expect(workspaces.connectWorkspace(wid('ghost'))).rejects.toThrow(/unknown workspace ghost/)
@@ -411,7 +419,10 @@ describe('startInitialSelection', () => {
await b.sessions.refresh()
// Store notifications and the connect round trip are microtask-batched.
await new Promise(resolve => setTimeout(resolve, 0))
expect(b.api.callsOf('session.create')).toEqual([{ workspaceId: 'recent' }])
expect(b.api.callsOf('session.create')).toEqual([{
workspaceId: 'recent',
timeZone: CLIENT_TIME_ZONE,
}])
expect(b.sessions.list.getSnapshot().current).toBe('s-new')
stop()
})