feat(schedule): add absolute-time reminders
This commit is contained in:
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
|
||||
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 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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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: {} } },
|
||||
|
||||
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('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')
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user